From 62ea72e8ec156e325460979477d8b8fbac66f033 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sun, 26 Jul 2026 14:26:09 -0400 Subject: [PATCH] parse_value returns string values with their <...> delimiters attached Fixes #176 Spec 2.2: the angle brackets delimit a JCAMP-DX string, they are not part of its value. `parse_value` returned whole `<...>` matches, so every consumer had to strip them -- which the library itself did in 26 places across the property recipes, and which `metadata` never did, leaving two brukerapi APIs reporting different values for the same parameter. `_STRING_RE` now captures the inside, and every parse path unquotes: a value of all-delimited strings skips the numeric casts, since `<1> <2>` is a list of two strings and never of two numbers. `<>` reads as the empty string. Nothing about a Python string says whether it is a JCAMP string or an undelimited enum symbol, so writing has to be told. A parameter keeps whichever form it was already written in, which is what makes read-modify-write -- the frame-group and slice-package splitters -- put `` and `` back as they were, while `VisuCoreFrameType=MAGNITUDE_IMAGE` stays bare. With that in place the workarounds go: the 26 `[1:-1]` slices and the delimited literals in brukerapi/config, and the `.strip("<>")` and `f"<{fg}>"` guards in dataset, schemas, splitters and mergers. Three recipes that relied on `[1:-1]` to break the `#PARAM.accessor` substitution needed parentheses instead. Verified against the 3,468-target corpus: identical load outcome, shape, dtype and error message for every one. The property references regenerate with the delimiters dropped and nothing else changed, and every visu_pars still rewrites byte-for-byte. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NuK1cZi8U54WXAdXMmGpzy --- brukerapi/config/properties_2dseq_core.json | 2 +- brukerapi/config/properties_2dseq_custom.json | 6 +- brukerapi/config/properties_fid_core.json | 66 ++++++------- brukerapi/config/properties_fid_custom.json | 6 +- brukerapi/config/properties_rawdata_core.json | 4 +- .../config/properties_rawdata_custom.json | 2 +- brukerapi/config/properties_traj_core.json | 6 +- brukerapi/config/properties_traj_custom.json | 2 +- brukerapi/dataset.py | 19 ++-- brukerapi/jcampdx.py | 58 ++++++++++-- brukerapi/mergers.py | 6 +- brukerapi/schemas.py | 10 +- brukerapi/splitters.py | 8 +- docs/source/cli.rst | 6 +- docs/source/tutorials/how-to-use-filter.rst | 2 +- examples/split_fg_echo.ipynb | 8 +- test/config/properties_0.2H2.json | 92 +++++++++--------- ...es_20200612_094625_lego_phantom_3_1_2.json | 94 +++++++++---------- ...0128_122257_LEGO_PHANTOM_API_TEST_1_1.json | 84 ++++++++--------- test/config/properties_PV360_StdData.json | 72 +++++++------- test/test_api.py | 31 ++++-- test/test_dataset.py | 18 ++-- test/test_frames.py | 2 +- test/test_jcampdx.py | 87 ++++++++++++----- test/test_latent_conformance.py | 2 +- test/test_paths.py | 2 +- test/test_property_configs.py | 4 +- test/test_split.py | 6 +- 28 files changed, 403 insertions(+), 302 deletions(-) diff --git a/brukerapi/config/properties_2dseq_core.json b/brukerapi/config/properties_2dseq_core.json index 59e121a..bab4661 100644 --- a/brukerapi/config/properties_2dseq_core.json +++ b/brukerapi/config/properties_2dseq_core.json @@ -1,7 +1,7 @@ { "pv_version": [ { - "cmd": "#VisuCreatorVersion[1:-1].split(';')[0]", + "cmd": "(#VisuCreatorVersion).split(';')[0]", "conditions": [], "comment": "spec 7.1: VisuCreator/VisuCreatorVersion are semicolon-separated lists; the first creator is the one that wrote the dataset" } diff --git a/brukerapi/config/properties_2dseq_custom.json b/brukerapi/config/properties_2dseq_custom.json index 21bb166..f8ebd4f 100644 --- a/brukerapi/config/properties_2dseq_custom.json +++ b/brukerapi/config/properties_2dseq_custom.json @@ -1,7 +1,7 @@ { "subj_id": [ { - "cmd": "#VisuSubjectName[1:-1]", + "cmd": "#VisuSubjectName", "conditions": [ ] }, @@ -158,13 +158,13 @@ ], "date": [ { - "cmd": "datetime.datetime.strptime(#VisuStudyDate[1:-1], '%X %d %b %Y')", + "cmd": "datetime.datetime.strptime(#VisuStudyDate, '%X %d %b %Y')", "conditions": [ "'T' not in #VisuStudyDate" ] }, { - "cmd": "datetime.datetime.strptime(#VisuStudyDate[1:-1], '%Y-%m-%dT%X,%f%z')", + "cmd": "datetime.datetime.strptime(#VisuStudyDate, '%Y-%m-%dT%X,%f%z')", "conditions": [ "'T' in #VisuStudyDate" ] diff --git a/brukerapi/config/properties_fid_core.json b/brukerapi/config/properties_fid_core.json index 542d2d2..da1bf54 100644 --- a/brukerapi/config/properties_fid_core.json +++ b/brukerapi/config/properties_fid_core.json @@ -1,7 +1,7 @@ { "pv_version": [ { - "cmd": "#ACQ_sw_version[1:-1].replace('PV-','').replace('PV ','').strip()", + "cmd": "(#ACQ_sw_version).replace('PV-','').replace('PV ','').strip()", "conditions": [], "comment": "spec 7.1/13: version gates compare on this parsed version, so an unlisted point release is not silently unsupported" }, @@ -131,7 +131,7 @@ { "cmd": "#ACQ_size.tuple[0] * @channels", "conditions": [ - ["#PULPROG", ["",""]], + ["#PULPROG", ["SPIRAL.ppg","DtiSpiral.ppg"]], "@pv_version.split('.')[0]=='5'" ] }, @@ -161,23 +161,23 @@ "conditions": [ ["#PULPROG", [ - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "" + "FLASH.ppg", + "FLASHAngio.ppg", + "RAREVTR.ppg", + "MGE.ppg", + "RARE.ppg", + "FLASHAngio.ppg", + "FcFLASH.ppg", + "DtiStandard.ppg", + "FISP.ppg", + "MSME.ppg", + "FAIR_RARE.ppg", + "RAREst.ppg", + "FLOWMAP.ppg", + "MDEFT.ppg", + "igFLASH.ppg", + "B1Map.ppg", + "DESS.ppg" ] ], "#ACQ_dim==2" @@ -186,7 +186,7 @@ { "cmd": "'CART_3D'" , "conditions": [ - ["#PULPROG[1:-1]", + ["#PULPROG", [ "FLASH.ppg", "FLASHAngio.ppg", @@ -203,7 +203,7 @@ { "cmd": "'FIELD_MAP'" , "conditions": [ - ["#PULPROG[1:-1]", + ["#PULPROG", [ "FieldMap.ppg" ] @@ -213,7 +213,7 @@ { "cmd": "'RADIAL'" , "conditions": [ - ["#PULPROG[1:-1]", + ["#PULPROG", [ "UTE.ppg", "UTE3D.ppg" @@ -224,7 +224,7 @@ { "cmd": "'EPI'" , "conditions": [ - ["#PULPROG[1:-1]", + ["#PULPROG", [ "EPI.ppg", "navigatorEPI_OM.ppg", @@ -240,7 +240,7 @@ { "cmd": "'dEPI'" , "conditions": [ - ["#PULPROG[1:-1]", + ["#PULPROG", [ "DtiEpi.ppg", "EPSI.ppg" @@ -251,7 +251,7 @@ { "cmd": "'SPECTROSCOPY'" , "conditions": [ - ["#PULPROG[1:-1]", + ["#PULPROG", [ "PRESS.ppg", "STEAM.ppg", @@ -269,7 +269,7 @@ { "cmd": "'CSI'" , "conditions": [ - ["#PULPROG[1:-1]", + ["#PULPROG", [ "CSI.ppg" ] @@ -279,7 +279,7 @@ { "cmd": "'SPIRAL'" , "conditions": [ - ["#PULPROG[1:-1]", + ["#PULPROG", [ "SPIRAL.ppg", "DtiSpiral.ppg" @@ -290,7 +290,7 @@ { "cmd": "'ZTE'" , "conditions": [ - ["#PULPROG[1:-1]", + ["#PULPROG", [ "ZTE.ppg" ] @@ -342,7 +342,7 @@ "cmd": "#NSegments*#ACQ_size[2]*#NI*#NR", "conditions": [ "#GO_block_size!='Standard_KBlock_Format'", - "#PULPROG[1:-1]=='EPSI.ppg'" + "#PULPROG=='EPSI.ppg'" ] }, { @@ -484,7 +484,7 @@ ], "conditions": [ "#GO_block_size!='Standard_KBlock_Format'", - "#PULPROG[1:-1]=='EPSI.ppg'" + "#PULPROG=='EPSI.ppg'" ] }, { @@ -616,7 +616,7 @@ "cmd": [0,4,3,2,1], "conditions": [ "#GO_block_size!='Standard_KBlock_Format'", - "#PULPROG[1:-1]=='EPSI.ppg'" + "#PULPROG=='EPSI.ppg'" ] }, { @@ -726,7 +726,7 @@ ], "conditions": [ "#GO_block_size!='Standard_KBlock_Format'", - "#PULPROG[1:-1]=='EPSI.ppg'" + "#PULPROG=='EPSI.ppg'" ], "comment": "spec 6.3: matches RECO_inp_size -- read x phase x spectral points x receivers" }, @@ -853,7 +853,7 @@ ], "conditions": [ "#GO_block_size!='Standard_KBlock_Format'", - "#PULPROG[1:-1]=='EPSI.ppg'" + "#PULPROG=='EPSI.ppg'" ] }, { diff --git a/brukerapi/config/properties_fid_custom.json b/brukerapi/config/properties_fid_custom.json index 3d94fdd..cd32ce5 100644 --- a/brukerapi/config/properties_fid_custom.json +++ b/brukerapi/config/properties_fid_custom.json @@ -1,7 +1,7 @@ { "subj_id": [ { - "cmd": "#SUBJECT_id[1:-1]", + "cmd": "#SUBJECT_id", "conditions": [ ] @@ -50,13 +50,13 @@ ], "date": [ { - "cmd": "datetime.datetime.strptime(#ACQ_time,'<%Y-%m-%dT%X,%f%z>')", + "cmd": "datetime.datetime.strptime(#ACQ_time,'%Y-%m-%dT%X,%f%z')", "conditions": [ "'T' in #ACQ_time" ] }, { - "cmd": "datetime.datetime.strptime(#ACQ_time,'<%H:%M:%S %d %b %Y>')", + "cmd": "datetime.datetime.strptime(#ACQ_time,'%H:%M:%S %d %b %Y')", "conditions": [ "'T' not in #ACQ_time" ] diff --git a/brukerapi/config/properties_rawdata_core.json b/brukerapi/config/properties_rawdata_core.json index 6c41fe2..d4807a2 100644 --- a/brukerapi/config/properties_rawdata_core.json +++ b/brukerapi/config/properties_rawdata_core.json @@ -1,7 +1,7 @@ { "pv_version": [ { - "cmd": "#ACQ_sw_version[1:-1].replace('PV-','').replace('PV ','').strip()", + "cmd": "(#ACQ_sw_version).replace('PV-','').replace('PV ','').strip()", "conditions": [], "comment": "spec 7.1/13: version gates compare on this parsed version, so an unlisted point release is not silently unsupported" }, @@ -12,7 +12,7 @@ ], "job_desc": [ { - "cmd": "[v for v in #ACQ_jobs.nested if v[-1] == '<{}>'.format(@subtype)][0]", + "cmd": "[v for v in #ACQ_jobs.nested if v[-1] == @subtype][0]", "conditions": [ "len(#ACQ_jobs.nested[0])==9" ] diff --git a/brukerapi/config/properties_rawdata_custom.json b/brukerapi/config/properties_rawdata_custom.json index 902a02d..36d64f9 100644 --- a/brukerapi/config/properties_rawdata_custom.json +++ b/brukerapi/config/properties_rawdata_custom.json @@ -1,7 +1,7 @@ { "subj_id": [ { - "cmd": "#SUBJECT_id[1:-1]", + "cmd": "#SUBJECT_id", "conditions": [ ] diff --git a/brukerapi/config/properties_traj_core.json b/brukerapi/config/properties_traj_core.json index 481f846..af82872 100644 --- a/brukerapi/config/properties_traj_core.json +++ b/brukerapi/config/properties_traj_core.json @@ -10,7 +10,7 @@ "cmd": "'RADIAL'", "conditions": [ [ - "#PULPROG[1:-1]", + "#PULPROG", [ "UTE.ppg", "UTE3D.ppg", @@ -23,7 +23,7 @@ "cmd": "'SPIRAL'", "conditions": [ [ - "#PULPROG[1:-1]", + "#PULPROG", [ "SPIRAL.ppg", "DtiSpiral.ppg" @@ -35,7 +35,7 @@ "cmd": "'ZTE'", "conditions": [ [ - "#PULPROG[1:-1]", + "#PULPROG", [ "ZTE.ppg" ] diff --git a/brukerapi/config/properties_traj_custom.json b/brukerapi/config/properties_traj_custom.json index 2da9aa1..55724b3 100644 --- a/brukerapi/config/properties_traj_custom.json +++ b/brukerapi/config/properties_traj_custom.json @@ -1,7 +1,7 @@ { "subj_id": [ { - "cmd": "#SUBJECT_id[1:-1]", + "cmd": "#SUBJECT_id", "conditions": [] }, { diff --git a/brukerapi/dataset.py b/brukerapi/dataset.py index e9db08c..bfb2066 100644 --- a/brukerapi/dataset.py +++ b/brukerapi/dataset.py @@ -607,8 +607,7 @@ def _rawdata_job_index(self): if not jobs: return None if len(jobs[0]) == 9: - title = f"<{self.subtype}>" - return next((index for index, job in enumerate(jobs) if job[-1] == title), None) + return next((index for index, job in enumerate(jobs) if job[-1] == self.subtype), None) try: return int(self.subtype.removeprefix("job")) except ValueError: @@ -628,7 +627,7 @@ def rawdata_job_settings(self): def rawdata_job_discarded(self): """Whether §13.1 says this job's data is intentionally not written.""" settings = self.rawdata_job_settings - return bool(settings and str(settings[0]).strip("<>") == "STORE_discard") + return bool(settings and str(settings[0]) == "STORE_discard") @property def rawdata_stored_scans(self): @@ -663,7 +662,7 @@ def rawdata_channels(self): fallback = int(self._parameter_value("PVM_EncNReceivers", 1)) if selected is not None: values = np.atleast_1d(selected) - channels = sum(str(value).strip("<>").casefold() in {"yes", "on", "1"} for value in values) + channels = sum(str(value).casefold() in {"yes", "on", "1"} for value in values) # Some systems expose more physical receiver paths than the job # writes. Only promote the per-channel declaration when it agrees # with the method's logical receiver count. @@ -674,8 +673,8 @@ def rawdata_channels(self): def _infer_scheme_id(self): # Source of truth for format inference: # https://github.com/gdevenyi/brkraw-legacy/blob/main/FILE_FORMAT.md - pulprog = str(self._parameter_value("PULPROG", "")).strip("<>").upper() - method = str(self._parameter_value("Method", "")).strip("<>").upper() + pulprog = str(self._parameter_value("PULPROG", "")).upper() + method = str(self._parameter_value("Method", "")).upper() family = f"{pulprog} {method}" if "SPIRAL" in family or self._parameter_value("PVM_SpiralNbOfInterleaves") is not None: @@ -1172,7 +1171,7 @@ def _frame_geometry(self): 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() + disk_order = str(self._parameter_value("VisuCoreDiskSliceOrder", "")).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]: @@ -1357,14 +1356,14 @@ def frame_group_values(self): for descriptor in descriptors: try: 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) + group_name = str(descriptor[1]) + axis = next(axis for axis, dim_type in enumerate(self.dim_type) if str(dim_type) == group_name) except (IndexError, StopIteration, TypeError, ValueError): continue 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) + grouped.setdefault(str(dependencies[index][0]), []).append(axis) values = {} data_shape = self._data.shape diff --git a/brukerapi/jcampdx.py b/brukerapi/jcampdx.py index 77afd20..1fc89e8 100644 --- a/brukerapi/jcampdx.py +++ b/brukerapi/jcampdx.py @@ -45,8 +45,40 @@ # 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"<(?:\\.|[^<>\\])*>|<[^<>]*>") +# never terminates under it is re-read with the backslash as content. The +# inside is captured, because the angle brackets delimit the value, they are +# not part of it. +_STRING_RE = re.compile(r"<((?:\\.|[^<>\\])*)>|<([^<>]*)>") + + +def unquote_string(val_str): + """Return the value of a ``<...>`` string, without its delimiters. + + Spec 2.2: the angle brackets are the string delimiter, so `` is the + string `abc` and `<>` is the empty string. Anything that is not a delimited + string -- an enum symbol, a number -- is returned unchanged. The text + between the delimiters is kept verbatim, escapes included, so that + :func:`quote_string` restores the value exactly as it was written. + """ + match = _STRING_RE.fullmatch(val_str) + if match is None: + return val_str + return match.group(1) if match.group(1) is not None else match.group(2) + + +def quote_string(value): + """Put string values of ``value`` back inside their ``<...>`` delimiters. + + The inverse of :func:`unquote_string`, applied to every string in a scalar, + list, nested list or array value, and preserving its container. + """ + if isinstance(value, str): + return f"<{value}>" + if isinstance(value, list): + return [quote_string(item) for item in value] + if isinstance(value, np.ndarray) and value.dtype.kind in "US": + return np.array([f"<{item}>" for item in value.flat]).reshape(value.shape) + return value class Parameter: @@ -242,6 +274,13 @@ def value(self): def value(self, value): size = self.size + # Spec 2.2: a string is written inside <...>, an enum symbol is written + # bare, and nothing about the Python value tells the two apart. Which + # one this parameter holds is read from how it is already written, so + # that a value taken from `value` and put back is written as it came. + if _STRING_RE.search(self.val_str): + value = quote_string(value) + if isinstance(value, float): val_str = self.serialize_float(value, self.version) elif isinstance(value, int): @@ -381,11 +420,16 @@ def parse_value(cls, val_str, size_bracket=None): # sharp string if val_str.startswith("<") and val_str.endswith(">"): - val_strs = _STRING_RE.findall(val_str) + val_strs = [value for value in cls._split_outside_angle_brackets(val_str, " ") if value] + + # `<1> <2>` is a list of the strings "1" and "2", never of numbers, + # so an all-string value skips the numeric casts made below. + if val_strs and all(_STRING_RE.fullmatch(value) for value in val_strs): + val_strs = [unquote_string(value) for value in val_strs] - if len(val_strs) == 1: - return val_strs[0] - return np.array(val_strs) + if len(val_strs) == 1: + return val_strs[0] + return np.array(val_strs) # int/float if _SINGLE_NUMBER_RE.fullmatch(val_str): @@ -425,7 +469,7 @@ def parse_value(cls, val_str, size_bracket=None): except ValueError: pass - return np.array(val_strs) + return np.array([unquote_string(value) for value in val_strs]) if not val_strs: # a parameter written with no value at all, e.g. `##$ACQ_operator= ` return "" diff --git a/brukerapi/mergers.py b/brukerapi/mergers.py index 4ff04dd..2f9be51 100644 --- a/brukerapi/mergers.py +++ b/brukerapi/mergers.py @@ -12,14 +12,14 @@ def merge(cls, dataset, fg): """ - if f"<{fg}>" not in dataset.dim_type: + if fg not in dataset.dim_type: raise ValueError(f"Dataset does not contain {fg} frame group") """ CHECK if FG and index are valid """ # absolute index of FG_SLICE among dimensions of the dataset - fg_abs_index = dataset.dim_type.index(f"<{fg}>") + fg_abs_index = dataset.dim_type.index(fg) # index of FG_SLICE among frame group dimensions of the dataset fg_rel_index = fg_abs_index - dataset.encoded_dim @@ -115,7 +115,7 @@ def _merge_VisuFGOrderDesc(cls, dataset, fg): value = parameter.nested for fg_ in value: - if fg_[1] == f"<{fg}>": + if fg_[1] == fg: value.remove(fg_) if value: parameter.value = value diff --git a/brukerapi/schemas.py b/brukerapi/schemas.py index 18f30d0..d851b73 100644 --- a/brukerapi/schemas.py +++ b/brukerapi/schemas.py @@ -181,7 +181,7 @@ def _declared_or_inferred(self, parameter, declared_value, inferred, warning_fla def continuous_train(self): """Whether §5.3 declares a phase-factor continuous acquisition train.""" value = self._dataset._parameter_value("ACQ_scan_size") - declared = None if value is None else str(value).strip("<>").casefold() == "acq_phase_factor_scans" + declared = None if value is None else str(value).casefold() == "acq_phase_factor_scans" return self._declared_or_inferred( "ACQ_scan_size", declared, @@ -193,7 +193,7 @@ def continuous_train(self): def mirror_odd_lines(self): """Whether a selected reconstruction requests §6.2 odd-line reversal.""" value = self._dataset._parameter_value("RECO_inp_order") - declared = None if value is None else str(value).strip("<>").upper() == "REV_ALT_ROWS" + declared = None if value is None else str(value).upper() == "REV_ALT_ROWS" return self._declared_or_inferred( "RECO_inp_order", declared, @@ -930,14 +930,14 @@ def deserialize(self, data, layouts): return self._combine_complex_frames(data) def _frame_group_axis(self, name): - normalized_name = name.strip("<>").upper() + normalized_name = name.upper() for axis, dim_type in enumerate(self._dataset.dim_type): - if str(dim_type).strip("<>").upper() == normalized_name: + if str(dim_type).upper() == normalized_name: return axis return None def _apply_disk_slice_order(self, data): - disk_order = str(self._dataset._parameter_value("VisuCoreDiskSliceOrder", "")).strip("<>").lower() + disk_order = str(self._dataset._parameter_value("VisuCoreDiskSliceOrder", "")).lower() if disk_order != "disk_reverse_slice_order": return data diff --git a/brukerapi/splitters.py b/brukerapi/splitters.py index 85d66f2..2487e49 100644 --- a/brukerapi/splitters.py +++ b/brukerapi/splitters.py @@ -130,14 +130,14 @@ def split(self, dataset, select=None, write=None, path_out=None, **kwargs): if write is None: write = False - if f"<{self.fg}>" not in dataset.dim_type: + if self.fg not in dataset.dim_type: raise ValueError(f"Dataset does not contain {self.fg} frame group") """ CHECK if FG and index are valid """ # absolute index of FG_SLICE among dimensions of the dataset - fg_abs_index = dataset.dim_type.index(f"<{self.fg}>") + fg_abs_index = dataset.dim_type.index(self.fg) # index of FG_SLICE among frame group dimensions of the dataset fg_rel_index = fg_abs_index - dataset.encoded_dim @@ -243,7 +243,7 @@ def _split_VisuFGOrderDesc(self, visu_pars, fg): value = VisuFGOrderDesc.nested for fg_ in value: - if fg_[1] == f"<{fg}>": + if fg_[1] == fg: value.remove(fg_) if value: VisuFGOrderDesc.value = value @@ -301,7 +301,7 @@ def split(self, dataset, write=None, path_out=None): fg_rel_index = group_index else: groups = dataset["VisuFGOrderDesc"].sub_list(1) - fg_rel_index = groups.index("") if "" in groups else 0 + fg_rel_index = groups.index("FG_SLICE") if "FG_SLICE" in groups else 0 # index of the slice axis among the dimensions of the dataset fg_abs_index = fg_rel_index + dataset.encoded_dim diff --git a/docs/source/cli.rst b/docs/source/cli.rst index 4351fba..d5a7184 100644 --- a/docs/source/cli.rst +++ b/docs/source/cli.rst @@ -35,7 +35,7 @@ The following is the content of the resulting `report.json` file located in the "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "encoded_dim": 2, "shape_final": [ @@ -159,7 +159,7 @@ The `filter` sub-command provides an option to make various queries on folders c **List all data sets measured using the EPI pulse sequence**:: - bruker filter -i ${DATA_PATH}/20200612_094625_lego_phantom_3_1_2 -q "#PULPROG==''" + bruker filter -i ${DATA_PATH}/20200612_094625_lego_phantom_3_1_2 -q "#PULPROG=='EPI.ppg'" pipelines @@ -169,4 +169,4 @@ It is possible to assemble pipelines using the Bruker API and xargs. Let us see Using filter and report subcommands to only report datasets measured by the MGE sequence:: - bruker filter -i /home/tomas/data/20200612_094625_lego_phantom_3_1_2/ -q "#PULPROG==''" | xargs -I {} bruker report -i {} -o /home/tomas/data/reports + bruker filter -i /home/tomas/data/20200612_094625_lego_phantom_3_1_2/ -q "#PULPROG=='MGE.ppg'" | xargs -I {} bruker report -i {} -o /home/tomas/data/reports diff --git a/docs/source/tutorials/how-to-use-filter.rst b/docs/source/tutorials/how-to-use-filter.rst index e76b1fc..1b17d20 100644 --- a/docs/source/tutorials/how-to-use-filter.rst +++ b/docs/source/tutorials/how-to-use-filter.rst @@ -61,7 +61,7 @@ using the RARE pulse sequence only. .. code-block:: python - study.filter(parameter='PULPROG', operator='==', value='') + study.filter(parameter='PULPROG', operator='==', value='RARE.ppg') study.filter(type=Dataset) study.print() diff --git a/examples/split_fg_echo.ipynb b/examples/split_fg_echo.ipynb index bf0f487..cb92051 100644 --- a/examples/split_fg_echo.ipynb +++ b/examples/split_fg_echo.ipynb @@ -73,7 +73,7 @@ "text": [ "(128, 128, 2, 9)\n", "['spatial', 'spatial', 'FG_ECHO', 'FG_SLICE']\n", - "[[2, '', '<>', 0, 1], [9, '', '<>', 1, 2]]\n", + "[[2, 'FG_ECHO', '', 0, 1], [9, 'FG_SLICE', '', 1, 2]]\n", "[12 60]\n" ] } @@ -144,15 +144,15 @@ "text": [ "ORIGINAL DATA SET\n", "(128, 128, 2, 9)\n", - "[[2, '', '<>', 0, 1], [9, '', '<>', 1, 2]]\n", + "[[2, 'FG_ECHO', '', 0, 1], [9, 'FG_SLICE', '', 1, 2]]\n", "[12 60]\n", "ECHO 0\n", "(128, 128, 9)\n", - "[9, '', '<>', 1, 2]\n", + "[9, 'FG_SLICE', '', 1, 2]\n", "12.0\n", "ECHO 1\n", "(128, 128, 9)\n", - "[9, '', '<>', 1, 2]\n", + "[9, 'FG_SLICE', '', 1, 2]\n", "60.0\n" ] } diff --git a/test/config/properties_0.2H2.json b/test/config/properties_0.2H2.json index 0b61893..06e78d5 100644 --- a/test/config/properties_0.2H2.json +++ b/test/config/properties_0.2H2.json @@ -117,7 +117,7 @@ "spatial", "spatial", "frame", - "" + "FG_FLOW" ], "dwell_s": 6.666666666666667e-06, "encoded_dim": 2, @@ -201,8 +201,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_MOVIE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -310,7 +310,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -397,8 +397,8 @@ "spatial", "spatial", "frame", - "", - "" + "FG_MOVIE", + "FG_IRMODE" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -500,8 +500,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_MOVIE" ], "dwell_s": 4.1999999999999945e-06, "encoded_dim": 2, @@ -636,7 +636,7 @@ "spatial", "spatial", "frame", - "" + "FG_MOVIE" ], "dwell_s": 2.5e-06, "encoded_dim": 2, @@ -757,8 +757,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_ECHO", + "FG_SLICE" ], "dwell_s": 2.5e-06, "encoded_dim": 2, @@ -1084,7 +1084,7 @@ "spatial", "spatial", "frame", - "" + "FG_ECHO" ], "dwell_s": 2.5e-06, "encoded_dim": 2, @@ -1172,7 +1172,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 3.799999999999998e-06, "encoded_dim": 2, @@ -1280,7 +1280,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 2.5e-06, "encoded_dim": 2, @@ -1585,7 +1585,7 @@ "spatial", "spatial", "spatial", - "" + "FG_ECHO" ], "dwell_s": 2.5e-06, "encoded_dim": 3, @@ -1643,7 +1643,7 @@ "date": "2020-06-16 15:13:18", "dim_type": [ "spectroscopic", - "" + "FG_CYCLE" ], "dwell_s": null, "encoded_dim": 1, @@ -1792,7 +1792,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -1890,8 +1890,8 @@ "spatial", "spatial", "frame", - "", - "" + "FG_ECHO", + "FG_MOVIE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -2040,8 +2040,8 @@ "spatial", "spatial", "frame", - "", - "" + "FG_ISA", + "FG_MOVIE" ], "dwell_s": null, "encoded_dim": 2, @@ -2188,8 +2188,8 @@ "spatial", "spatial", "frame", - "", - "" + "FG_ISA", + "FG_ECHO" ], "dwell_s": null, "encoded_dim": 2, @@ -2320,8 +2320,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_MOVIE" ], "dwell_s": 1.5312499999999972e-06, "encoded_dim": 2, @@ -3449,8 +3449,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_DTI", + "FG_SLICE" ], "dwell_s": null, "encoded_dim": 2, @@ -4207,7 +4207,7 @@ "spatial", "spatial", "frame", - "" + "FG_ECHO" ], "dwell_s": 8.4e-06, "encoded_dim": 2, @@ -4318,7 +4318,7 @@ "spatial", "spatial", "frame", - "" + "FG_ISA" ], "dwell_s": null, "encoded_dim": 2, @@ -4406,7 +4406,7 @@ "spatial", "spatial", "frame", - "" + "FG_ISA" ], "dwell_s": null, "encoded_dim": 2, @@ -4496,8 +4496,8 @@ "spatial", "spatial", "frame", - "", - "" + "FG_ECHO", + "FG_CARDIAC_MOVIE" ], "dwell_s": 3.3333333333333333e-06, "encoded_dim": 2, @@ -4609,8 +4609,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_ECHO", + "FG_SLICE" ], "dwell_s": 6.4e-06, "encoded_dim": 2, @@ -4837,8 +4837,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_ECHO", + "FG_SLICE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -4937,9 +4937,9 @@ "dim_type": [ "spatial", "spatial", - "", - "", - "" + "FG_ECHO", + "FG_SLICE", + "FG_CYCLE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -5058,9 +5058,9 @@ "spatial", "spatial", "frame", - "", - "", - "" + "FG_MOVIE", + "FG_IRMODE", + "FG_CYCLE" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -5189,9 +5189,9 @@ "dim_type": [ "spatial", "spatial", - "", - "", - "" + "FG_ECHO", + "FG_SLICE", + "FG_MOVIE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -5314,7 +5314,7 @@ "spatial", "spatial", "frame", - "" + "FG_CYCLE" ], "dwell_s": 5e-06, "encoded_dim": 2, 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 549b739..dbd0a5c 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 @@ -37,7 +37,7 @@ "spatial", "spatial", "frame", - "" + "FG_MOVIE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -123,8 +123,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_CYCLE" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -318,8 +318,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_MOVIE", + "FG_SLICE" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -423,8 +423,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_FLOW", + "FG_SLICE" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -516,8 +516,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_DIFFUSION" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -709,8 +709,8 @@ "spatial", "spatial", "frame", - "", - "" + "FG_MOVIE", + "FG_IRMODE" ], "dwell_s": 2.5000000000000044e-06, "encoded_dim": 2, @@ -804,9 +804,9 @@ "dim_type": [ "spatial", "spatial", - "", - "", - "" + "FG_SLICE", + "FG_MOVIE", + "FG_CYCLE" ], "dwell_s": 2.5000000000000044e-06, "encoded_dim": 2, @@ -929,7 +929,7 @@ "spatial", "spatial", "frame", - "" + "FG_DIFFUSION" ], "dwell_s": 2.5000000000000044e-06, "encoded_dim": 2, @@ -1068,7 +1068,7 @@ "spatial", "spatial", "frame", - "" + "FG_MOVIE" ], "dwell_s": 2.5000000000000044e-06, "encoded_dim": 2, @@ -1171,7 +1171,7 @@ "spatial", "spatial", "frame", - "" + "FG_ECHO" ], "dwell_s": 3.3333333333333333e-06, "encoded_dim": 2, @@ -1267,7 +1267,7 @@ "spatial", "spatial", "frame", - "" + "FG_ECHO" ], "dwell_s": 3.3333333333333333e-06, "encoded_dim": 2, @@ -1355,7 +1355,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 2.5e-06, "encoded_dim": 2, @@ -1440,7 +1440,7 @@ "spatial", "spatial", "frame", - "" + "FG_DIFFUSION" ], "dwell_s": 2.5e-06, "encoded_dim": 2, @@ -2093,7 +2093,7 @@ "date": "2020-06-12 09:46:25.256000+02:00", "dim_type": [ "spectroscopic", - "" + "FG_CYCLE" ], "dwell_s": null, "encoded_dim": 1, @@ -2181,7 +2181,7 @@ "date": "2020-06-12 09:46:25.256000+02:00", "dim_type": [ "spectroscopic", - "" + "FG_CYCLE" ], "dwell_s": null, "encoded_dim": 1, @@ -2272,7 +2272,7 @@ "date": "2020-06-12 09:46:25.256000+02:00", "dim_type": [ "spectroscopic", - "" + "FG_CYCLE" ], "dwell_s": 6.25e-07, "encoded_dim": 1, @@ -2405,7 +2405,7 @@ "date": "2020-06-12 09:46:25.256000+02:00", "dim_type": [ "spectroscopic", - "" + "FG_ECHO" ], "dwell_s": null, "encoded_dim": 1, @@ -2534,7 +2534,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -2618,7 +2618,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 8.533333333333334e-06, "encoded_dim": 2, @@ -2734,8 +2734,8 @@ "spatial", "spatial", "frame", - "", - "" + "FG_IRMODE", + "FG_MOVIE" ], "dwell_s": 8.4e-06, "encoded_dim": 2, @@ -2889,7 +2889,7 @@ "spatial", "spatial", "frame", - "" + "FG_MOVIE" ], "dwell_s": 6.4e-06, "encoded_dim": 2, @@ -2980,7 +2980,7 @@ "spatial", "spatial", "frame", - "" + "FG_ISA" ], "dwell_s": null, "encoded_dim": 2, @@ -3100,8 +3100,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_ECHO", + "FG_SLICE" ], "dwell_s": 7.000000000000003e-06, "encoded_dim": 2, @@ -3479,8 +3479,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_ISA", + "FG_SLICE" ], "dwell_s": null, "encoded_dim": 2, @@ -3609,7 +3609,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 1.5333333333333315e-06, "encoded_dim": 2, @@ -3697,7 +3697,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 1.5000000000000038e-05, "encoded_dim": 2, @@ -3794,7 +3794,7 @@ "spatial", "spatial", "spatial", - "" + "FG_DIFFUSION" ], "dwell_s": 1.499999999999998e-06, "encoded_dim": 3, @@ -3964,7 +3964,7 @@ "spatial", "spatial", "spatial", - "" + "FG_DTI" ], "dwell_s": null, "encoded_dim": 3, @@ -4088,7 +4088,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -4172,8 +4172,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_CARDIAC_MOVIE", + "FG_SLICE" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -4324,8 +4324,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_ECHO", + "FG_SLICE" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -4469,7 +4469,7 @@ "spatial", "spatial", "frame", - "" + "FG_ECHO" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -4553,7 +4553,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -4658,8 +4658,8 @@ "spatial", "spatial", "frame", - "", - "" + "FG_MOVIE", + "FG_IRMODE" ], "dwell_s": 5e-06, "encoded_dim": 2, 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 f02970b..2520d8a 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 @@ -33,7 +33,7 @@ "spatial", "spatial", "frame", - "" + "FG_DIFFUSION" ], "dwell_s": 2.5000000000000044e-06, "encoded_dim": 2, @@ -128,7 +128,7 @@ "spatial", "spatial", "frame", - "" + "FG_DIFFUSION" ], "dwell_s": 2.5e-06, "encoded_dim": 2, @@ -210,8 +210,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_DIFFUSION" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -517,9 +517,9 @@ "dim_type": [ "spatial", "spatial", - "", - "", - "" + "FG_SLICE", + "FG_IRMODE", + "FG_CYCLE" ], "dwell_s": 2.5000000000000044e-06, "encoded_dim": 2, @@ -649,8 +649,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_IRMODE" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -742,8 +742,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_CYCLE" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -924,7 +924,7 @@ "spatial", "spatial", "frame", - "" + "FG_FLOW" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -1008,8 +1008,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_CYCLE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -1285,7 +1285,7 @@ "spatial", "spatial", "frame", - "" + "FG_CARDIAC_MOVIE" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -1474,8 +1474,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_ECHO", + "FG_SLICE" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -1639,8 +1639,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_ECHO", + "FG_SLICE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -1751,7 +1751,7 @@ "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", - "" + "FG_COMPLEX" ], "dwell_s": null, "encoded_dim": 1, @@ -1792,7 +1792,7 @@ "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", - "" + "FG_CYCLE" ], "dwell_s": 6.25e-07, "encoded_dim": 1, @@ -1837,8 +1837,8 @@ "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", - "", - "" + "FG_CYCLE", + "FG_COMPLEX" ], "dwell_s": 6.25e-07, "encoded_dim": 1, @@ -1921,7 +1921,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -2013,8 +2013,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_MOVIE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -2297,7 +2297,7 @@ "spatial", "spatial", "frame", - "" + "FG_CYCLE" ], "dwell_s": 2.5e-06, "encoded_dim": 2, @@ -2394,7 +2394,7 @@ "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", - "" + "FG_COMPLEX" ], "dwell_s": 6.240000000000001e-05, "encoded_dim": 1, @@ -2543,8 +2543,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_MOVIE" ], "dwell_s": 2.5000000000000044e-06, "encoded_dim": 2, @@ -2725,8 +2725,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_ECHO", + "FG_SLICE" ], "dwell_s": 3.3333333333333333e-06, "encoded_dim": 2, @@ -2859,8 +2859,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_ECHO", + "FG_SLICE" ], "dwell_s": 3.3333333333333333e-06, "encoded_dim": 2, @@ -2988,7 +2988,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 5e-06, "encoded_dim": 2, @@ -3250,7 +3250,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 9.333333333333338e-06, "encoded_dim": 2, @@ -3361,8 +3361,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_ECHO", + "FG_SLICE" ], "dwell_s": 1e-05, "encoded_dim": 2, @@ -3448,7 +3448,7 @@ "date": "2021-01-28 12:22:57.085000+01:00", "dim_type": [ "spectroscopic", - "" + "FG_ECHO" ], "dwell_s": null, "encoded_dim": 1, @@ -3558,7 +3558,7 @@ "spectroscopic", "spatial", "spatial", - "" + "FG_COMPLEX" ], "dwell_s": 6.240000000000001e-05, "encoded_dim": 3, @@ -3637,8 +3637,8 @@ "spatial", "spatial", "frame", - "", - "" + "FG_ECHO", + "FG_CYCLE" ], "dwell_s": 5e-06, "encoded_dim": 2, diff --git a/test/config/properties_PV360_StdData.json b/test/config/properties_PV360_StdData.json index 1c6d3c7..c305588 100644 --- a/test/config/properties_PV360_StdData.json +++ b/test/config/properties_PV360_StdData.json @@ -32,8 +32,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_DIFFUSION" ], "dwell_s": 2.8e-06, "encoded_dim": 2, @@ -463,8 +463,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_DTI" ], "dwell_s": 2.8e-06, "encoded_dim": 2, @@ -774,8 +774,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_DIFFUSION" ], "dwell_s": 2.8e-06, "encoded_dim": 2, @@ -1505,8 +1505,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_SLICE", + "FG_DTI" ], "dwell_s": 2.8e-06, "encoded_dim": 2, @@ -1855,7 +1855,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 6.7e-06, "encoded_dim": 2, @@ -2035,7 +2035,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 5.2e-06, "encoded_dim": 2, @@ -2131,7 +2131,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 7.499999999999999e-06, "encoded_dim": 2, @@ -2239,8 +2239,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_ECHO", + "FG_SLICE" ], "dwell_s": 6.7e-06, "encoded_dim": 2, @@ -2430,8 +2430,8 @@ "dim_type": [ "spatial", "spatial", - "", - "" + "FG_ISA", + "FG_SLICE" ], "dwell_s": 6.7e-06, "encoded_dim": 2, @@ -2571,7 +2571,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_SLICE" ], "dwell_s": 2.4e-06, "encoded_dim": 2, @@ -2669,7 +2669,7 @@ "spatial", "spatial", "frame", - "" + "FG_ECHO" ], "dwell_s": 6.4e-06, "encoded_dim": 2, @@ -2779,7 +2779,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_ISA" ], "dwell_s": 6.4e-06, "encoded_dim": 2, @@ -2879,7 +2879,7 @@ "spatial", "spatial", "frame", - "" + "FG_ECHO" ], "dwell_s": 6.7e-06, "encoded_dim": 2, @@ -2989,7 +2989,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_ISA" ], "dwell_s": 6.7e-06, "encoded_dim": 2, @@ -3089,7 +3089,7 @@ "spatial", "spatial", "frame", - "" + "FG_ECHO" ], "dwell_s": 6.7e-06, "encoded_dim": 2, @@ -3199,7 +3199,7 @@ "dim_type": [ "spatial", "spatial", - "" + "FG_ISA" ], "dwell_s": 6.7e-06, "encoded_dim": 2, @@ -3359,7 +3359,7 @@ 4385.964912280701, 256, 1, - "" + "Navigator" ], "numpy_dtype": "int32", "pv_version": "360.3.6", @@ -3388,7 +3388,7 @@ 178571.42857142855, 700, 1, - "" + "job0" ], "numpy_dtype": "int32", "pv_version": "360.3.6", @@ -3417,7 +3417,7 @@ 178571.42857142855, 1300, 1, - "" + "job0" ], "numpy_dtype": "int32", "pv_version": "360.3.6", @@ -3446,7 +3446,7 @@ 4385.964912280701, 256, 1, - "" + "job0" ], "numpy_dtype": "int32", "pv_version": "360.3.6", @@ -3475,7 +3475,7 @@ 20000, 16960, 1, - "" + "job0" ], "numpy_dtype": "int32", "pv_version": "360.3.6", @@ -3504,7 +3504,7 @@ 74626.86567164179, 2592, 1, - "" + "job0" ], "numpy_dtype": "int32", "pv_version": "360.3.6", @@ -3533,7 +3533,7 @@ 96153.84615384616, 2304, 1, - "" + "job0" ], "numpy_dtype": "int32", "pv_version": "360.3.6", @@ -3562,7 +3562,7 @@ 66666.66666666667, 2304, 1, - "" + "job0" ], "numpy_dtype": "int32", "pv_version": "360.3.6", @@ -3591,7 +3591,7 @@ 74626.86567164179, 10560, 1, - "" + "job0" ], "numpy_dtype": "int32", "pv_version": "360.3.6", @@ -3620,7 +3620,7 @@ 208333.33333333334, 5, 1, - "" + "job0" ], "numpy_dtype": "int32", "pv_version": "360.3.6", @@ -3649,7 +3649,7 @@ 78125, 1536, 1, - "" + "job0" ], "numpy_dtype": "int32", "pv_version": "360.3.6", @@ -3678,7 +3678,7 @@ 74626.86567164179, 2048, 1, - "" + "job0" ], "numpy_dtype": "int32", "pv_version": "360.3.6", @@ -3707,7 +3707,7 @@ 74626.86567164179, 2048, 1, - "" + "job0" ], "numpy_dtype": "int32", "pv_version": "360.3.6", @@ -3736,7 +3736,7 @@ 200000, 51360, 1, - "" + "job0" ], "numpy_dtype": "int32", "pv_version": "360.3.6", diff --git a/test/test_api.py b/test/test_api.py index 8b07cfe..aec2042 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -53,28 +53,41 @@ def test_metadata_groups_follow_the_specification(tmp_path): metadata = dataset.metadata assert metadata["visu_equipment"] == { - "manufacturer": "", - "acq_software_version": "", - "institution": "", - "station": "", + "manufacturer": "Bruker BioSpin MRI GmbH", + "acq_software_version": "PV-360.3.7", + "institution": "Institute", + "station": "System C1", } - assert metadata["visu_instance"]["uid"] == "<2.16.756.5.5.100.1>" + assert metadata["visu_instance"]["uid"] == "2.16.756.5.5.100.1" assert metadata["visu_instance"]["type"] == "STANDARD_INSTANCE" - assert metadata["visu_instance"]["creator_version"] == "<6.0.1>" + assert metadata["visu_instance"]["creator_version"] == "6.0.1" assert metadata["visu_series"]["experiment_number"] == 8 assert metadata["visu_series"]["processing_number"] == 1 - assert metadata["visu_acq"]["protocol"] == "" + assert metadata["visu_acq"]["protocol"] == "T1_FLASH" assert metadata["visu_acq"]["echo_time"] == 3.5 assert "uisition_protocol" not in metadata["visu_acq"] +def test_metadata_reports_the_same_string_as_the_property_that_reads_it(tmp_path): + """`subj_id` and `metadata` read VisuSubjectName; they must agree. + + `subj_id` stripped the `<...>` delimiters in its recipe, `metadata` -- the + newer surface -- did not, so the two APIs reported different values for one + parameter for every string-valued field of every group. + """ + dataset = Dataset(study(tmp_path), load=LOAD_STAGES["properties"]) + + assert dataset.metadata["visu_subject"]["name"] == "synthetic" + assert dataset.subj_id == dataset.metadata["visu_subject"]["name"] + + def test_metadata_can_be_exported(tmp_path): dataset = Dataset(study(tmp_path, extra=EQUIPMENT), load=LOAD_STAGES["properties"]) exported = dataset.to_dict(props=["metadata"]) # arrays inside the grouped dict used to make json.dump fail outright - assert json.loads(json.dumps(exported))["metadata"]["visu_equipment"]["station"] == "" + assert json.loads(json.dumps(exported))["metadata"]["visu_equipment"]["station"] == "System C1" def test_report_honours_the_requested_format(tmp_path): @@ -175,7 +188,7 @@ def test_single_slice_datasets_do_not_claim_a_third_spatial_axis(tmp_path): dataset = Dataset(path) assert dataset.is_single_slice - assert dataset.dim_type == ["spatial", "spatial", "frame", ""] + assert dataset.dim_type == ["spatial", "spatial", "frame", "FG_MOVIE"] assert len(dataset.dim_type) == dataset.data.ndim diff --git a/test/test_dataset.py b/test/test_dataset.py index afbf232..b98c345 100644 --- a/test/test_dataset.py +++ b/test/test_dataset.py @@ -307,7 +307,7 @@ def test_ambiguous_scheme_error_names_pulse_program_and_method(): with pytest.raises( UnknownAcqSchemeException, - match=r"PULPROG='', Method=''; pass scheme_id=", + match=r"PULPROG='customResearchSequence.ppg', Method='CustomMethod'; pass scheme_id=", ): dataset.load_schema() @@ -491,14 +491,14 @@ def test_2dseq_slice_packages_are_separate_in_memory_datasets(): @pytest.mark.parametrize( ("disk_order", "reverse"), [ - ("", True), + ("disk_reverse_slice_order", True), ("disk_normal_slice_order", False), ], ) def test_2dseq_disk_slice_order_is_applied_and_reversible(disk_order, reverse): dataset = SimpleNamespace( _state={"scale": False, "combine_complex": False}, - dim_type=["spatial", "", ""], + dim_type=["spatial", "FG_SLICE", "FG_ECHO"], _parameter_value=lambda name, default=None: disk_order if name == "VisuCoreDiskSliceOrder" else default, ) schema = Schema2dseq.__new__(Schema2dseq) @@ -522,7 +522,7 @@ def test_2dseq_disk_slice_order_uses_third_encoded_axis_for_3d_volume(): dataset = SimpleNamespace( _state={"scale": False, "combine_complex": False}, encoded_dim=3, - dim_type=["spatial", "spatial", "spatial", ""], + dim_type=["spatial", "spatial", "spatial", "FG_ECHO"], path="3d-volume/2dseq", _parameter_value=lambda name, default=None: "disk_reverse_slice_order" if name == "VisuCoreDiskSliceOrder" else default, ) @@ -546,7 +546,7 @@ def test_2dseq_disk_slice_order_without_slice_axis_warns_and_leaves_data_unchang dataset = SimpleNamespace( _state={"scale": False, "combine_complex": False}, encoded_dim=2, - dim_type=["spatial", "spatial", ""], + dim_type=["spatial", "spatial", "FG_ECHO"], path="no-slice-axis/2dseq", _parameter_value=lambda name, default=None: "disk_reverse_slice_order" if name == "VisuCoreDiskSliceOrder" else default, ) @@ -876,7 +876,7 @@ def test_2dseq_transposition_metadata_does_not_change_stored_data(parameter): shape_storage=(2, 3, 2), shape_final=(2, 3, 2), _state={"scale": False, "combine_complex": False}, - dim_type=["spatial", "spatial", ""], + dim_type=["spatial", "spatial", "FG_ECHO"], _parameter_value=lambda name, default=None: 1 if name == parameter else default, ) schema = Schema2dseq.__new__(Schema2dseq) @@ -1084,7 +1084,7 @@ def test_data_load(test_data): ( axis for axis, dim_type in enumerate(dataset.dim_type) - if str(dim_type).strip("<>").upper() == "FG_COMPLEX" + if str(dim_type).upper() == "FG_COMPLEX" ), None, ) @@ -1181,5 +1181,5 @@ def test_add_parameters_loads_the_named_parameter_file(tmp_path): 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"] == "" + assert dataset["SUBJECT_id"].value == "lego_phantom_3" + assert dataset.metadata["subject"]["id"] == "lego_phantom_3" diff --git a/test/test_frames.py b/test/test_frames.py index 2056d1f..62f39e2 100644 --- a/test/test_frames.py +++ b/test/test_frames.py @@ -89,7 +89,7 @@ def test_frame_group_values_follow_the_descriptor_window(tmp_path): values = dataset.frame_group_values - assert dataset.dim_type == ["spatial", "spatial", "", ""] + assert dataset.dim_type == ["spatial", "spatial", "FG_ECHO", "FG_SLICE"] 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) diff --git a/test/test_jcampdx.py b/test/test_jcampdx.py index f7f316a..8a0e0b0 100644 --- a/test/test_jcampdx.py +++ b/test/test_jcampdx.py @@ -48,12 +48,55 @@ def test_jcampdx(test_jcampdx_data): assert value_ref == value_test -def test_parse_value_preserves_delimiters_inside_angle_brackets(): +def test_a_string_is_read_without_its_delimiters(): + """Spec 2.2: `<...>` delimits a string, so the brackets are not its value. + + Returning them meant every consumer had to strip them, which the library + itself did in 26 places, and which the `metadata` surface never did -- so + two brukerapi APIs reported different values for one parameter. + """ + assert GenericParameter.parse_value("") == "abc" + assert np.array_equal(GenericParameter.parse_value(" "), np.array(["a", "b"])) + # A string that happens to look like a number stays a string. + assert np.array_equal(GenericParameter.parse_value("<1> <2>"), np.array(["1", "2"])) + + +def test_an_empty_string_is_read_as_the_empty_string(): + """`<>` is a string with nothing in it, which is what `''` says. + + A parameter written with no value at all reads the same way; the two are + not distinguishable by value. + """ + assert GenericParameter.parse_value("<>") == "" + assert np.array_equal(GenericParameter.parse_value("<> <>"), np.array(["", ""])) + + +def test_a_string_parameter_is_written_back_inside_its_delimiters(): + """Reading a value and putting it back must not change what is on disk. + + Nothing about a Python string says whether it is a JCAMP string or an enum + symbol, so the parameter keeps whichever form it was written in: a struct + of strings stays delimited, an enum stays bare. + """ + descriptor = GenericParameter("##$VisuFGOrderDesc", "( 1 )", "(9, , <>, 0, 2)", "5.01") + descriptor.value = descriptor.value + assert descriptor.val_str == "(9, , <>, 0, 2)" + + units = GenericParameter("##$VisuCoreDataUnits", "( 2, 65 )", " ", "5.01") + units.value = units.value[0] + assert units.val_str == "" + + frame_type = GenericParameter("##$VisuCoreFrameType", "( 1 )", "MAGNITUDE_IMAGE", "5.01") + frame_type.value = "COMPLEX_IMAGE" + assert frame_type.val_str == "COMPLEX_IMAGE" + + +def test_parse_value_does_not_split_a_string_on_a_delimiter_it_contains(): enum = "(operation, <[1H] TX Volume, RX Surface Array>)" struct = "(7, , 9)" - assert GenericParameter.parse_value(enum) == ["operation", "<[1H] TX Volume, RX Surface Array>"] - assert GenericParameter.parse_value(struct) == [7, "", 9] + assert GenericParameter.parse_value(enum) == ["operation", "[1H] TX Volume, RX Surface Array"] + assert GenericParameter.parse_value(struct) == [7, "label, with comma) and parenthesis", 9] def test_parse_value_of_an_empty_parameter(): @@ -66,18 +109,18 @@ def test_parse_value_of_an_empty_parameter(): assert GenericParameter.parse_value(" ") == "" -def test_parallel_lists_preserve_delimiters_inside_angle_brackets(): +def test_parallel_lists_do_not_split_a_string_on_a_delimiter_it_contains(): value = "(first, ) (second, )" parts = GenericParameter.split_parallel_lists(value) assert [GenericParameter.parse_value(part) for part in parts] == [ - ["first", ""], - ["second", ""], + ["first", "Display, One"], + ["second", "Display) Two, value"], ] -def test_jcampdx_get_value_preserves_enum_display_name(tmp_path): +def test_jcampdx_get_value_keeps_the_whole_enum_display_name(tmp_path): path = tmp_path / "configscan" path.write_text( "##TITLE=Parameter List\n" @@ -89,7 +132,7 @@ def test_jcampdx_get_value_preserves_enum_display_name(tmp_path): assert JCAMPDX(path).get_value("CONFIG_SCAN_operation_mode") == [ "operation", - "<[1H] TX Volume, RX Surface Array>", + "[1H] TX Volume, RX Surface Array", ] @@ -104,14 +147,14 @@ def test_run_length_expansion_handles_angle_bracket_strings(): assert np.array_equal( parameter.value, - np.array(["start", "", "", "end"]), + np.array(["start", "Name, display value", "Name, display value", "end"]), ) def test_run_length_expansion_handles_multiple_and_nested_runs(): parameter = GenericParameter("##$VALUES", "", "@2*(1) @2*(@2*())", "4.24") - assert np.array_equal(parameter.value, np.array(["1", "1", "", "", "", ""])) + assert np.array_equal(parameter.value, np.array(["1", "1", "enum", "enum", "enum", "enum"])) def test_jcampdx_data_parameter_parses_multiline_xy_pairs(tmp_path): @@ -206,7 +249,7 @@ def test_a_geometry_object_is_an_ordinary_nested_struct(tmp_path): 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 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]) @@ -247,7 +290,7 @@ def test_parse_value_joins_a_wrap_between_parallel_lists(): "5.0", ).value - assert value == [[1, ""], [2, ""]] + assert value == [[1, "FIRST"], [2, "SECOND"]] def test_wrap_lines_respects_78_columns_and_preserves_tokens(): @@ -329,7 +372,7 @@ def test_jcampdx_keeps_double_hash_inside_bracketed_value(tmp_path): "##END=\n" ) - assert JCAMPDX(path).get_value("PULPROG") == "" + assert JCAMPDX(path).get_value("PULPROG") == "HpMode,On##$EndBis,04,FA#" def test_jcampdx_record_without_assignment_raises_typed_error(): @@ -350,7 +393,7 @@ def test_load_parameter_allows_hash_and_dollar_in_value(tmp_path): key, parameter = JCAMPDX.load_parameter(path, "VALUE") assert key == "VALUE" - assert parameter.value == "" + assert parameter.value == "cost $5 #tag" def test_jcampdx_round_trip_preserves_comments_and_end_marker(tmp_path): @@ -422,8 +465,8 @@ def test_a_wrap_inside_a_string_does_not_invent_a_space(tmp_path): parameters = JCAMPDX(path) - assert [element[1] for element in parameters["ACQ_coil_elements"].value] == ["<1H>", "<1H>"] - assert parameters["ExcPulse"].value[1] == "" + assert [element[1] for element in parameters["ACQ_coil_elements"].value] == ["1H", "1H"] + assert parameters["ExcPulse"].value[1] == "gauss.exc" def test_a_wrap_at_a_space_keeps_exactly_one_space(tmp_path): @@ -556,11 +599,13 @@ def test_escaped_delimiters_inside_a_string_are_not_delimiters(tmp_path): parameters = JCAMPDX(path) + # The escapes stay in the value: they are content, and keeping them verbatim + # is what lets the descriptor be written back exactly as it was read. assert parameters["RecoStageEdges"].value == [ - ["", 0, "S>"], - ["", 0, "CAST0>"], + ["input", 0, "Q-\\>S"], + ["compute", 0, "Q0-\\>CAST0"], ] - assert parameters["RecoStageNodes"].value[2] == ";}>" + assert parameters["RecoStageNodes"].value[2] == "RecoFileSource Q{numChan=1;byteOrder=\\;}" def test_a_nested_struct_keeps_its_inner_tuple(tmp_path): @@ -576,7 +621,7 @@ def test_a_nested_struct_keeps_its_inner_tuple(tmp_path): ) assert JCAMPDX(path)["AdjKnownList"].value == [ - ["EMPTY", "", "<>", "on_demand", "HANDLE_ACQUISITION"], + ["EMPTY", "NO_ADJUSTMENT", "", "on_demand", "HANDLE_ACQUISITION"], "No", "No", ] @@ -599,7 +644,7 @@ def test_a_trailing_backslash_in_a_string_is_content_not_an_escape(tmp_path): "##END=\n" ) - assert JCAMPDX(path)["VisuStudyDescription"].value == "<\\>" + assert JCAMPDX(path)["VisuStudyDescription"].value == "\\" def test_the_last_parameter_survives_a_file_without_an_end_marker(tmp_path): diff --git a/test/test_latent_conformance.py b/test/test_latent_conformance.py index 47c2831..16c326a 100644 --- a/test/test_latent_conformance.py +++ b/test/test_latent_conformance.py @@ -228,7 +228,7 @@ def test_a_dollar_comment_inside_a_string_is_data(tmp_path): "##END=\n" ) - assert JCAMPDX(path)["PVM_Comment"].value == "" + assert JCAMPDX(path)["PVM_Comment"].value == "a$$b" def test_a_scalar_struct_is_not_eaten_as_a_size_bracket(tmp_path): diff --git a/test/test_paths.py b/test/test_paths.py index dbabc52..febe765 100644 --- a/test/test_paths.py +++ b/test/test_paths.py @@ -86,7 +86,7 @@ def test_parameters_resolve_through_relative_paths(study_dir, study_zip): """``../../acqp`` resolves inside an archive, where ``..`` is not collapsed.""" dataset = Dataset(study_zip / "1" / "pdata" / "1" / "2dseq", scale=False, parameter_files=["acqp"]) - assert dataset["ACQ_scan_name"].value == "" + assert dataset["ACQ_scan_name"].value == "demo" def test_folder_traverses_an_archive(study_zip): diff --git a/test/test_property_configs.py b/test/test_property_configs.py index 727cd3e..84293d3 100644 --- a/test/test_property_configs.py +++ b/test/test_property_configs.py @@ -64,7 +64,7 @@ def test_rawdata_pv360_branches_match_on_the_parsed_major_version(): assert "@pv_version.split('.')[0]=='360'" in branch["conditions"] assert all("ACQ_sw_version" not in " ".join(branch["conditions"]) for branch in config["job_desc"]) - assert config["pv_version"][0]["cmd"].startswith("#ACQ_sw_version[1:-1]") + assert config["pv_version"][0]["cmd"].startswith("(#ACQ_sw_version)") def test_rawdata_job_layout_is_selected_by_record_arity(): @@ -72,7 +72,7 @@ def test_rawdata_job_layout_is_selected_by_record_arity(): assert config["job_desc"] == [ { - "cmd": "[v for v in #ACQ_jobs.nested if v[-1] == '<{}>'.format(@subtype)][0]", + "cmd": "[v for v in #ACQ_jobs.nested if v[-1] == @subtype][0]", "conditions": ["len(#ACQ_jobs.nested[0])==9"], }, { diff --git a/test/test_split.py b/test/test_split.py index c92b055..eb63b4d 100644 --- a/test/test_split.py +++ b/test/test_split.py @@ -29,19 +29,19 @@ def test_split(test_split_data, tmp_path): tmp_path /= "FG/" dataset = Dataset(test_split_data[0]) - if "<{}>".format("FG_ECHO") not in dataset.dim_type: + if "FG_ECHO" not in dataset.dim_type: return datasets = FrameGroupSplitter("FG_ECHO").split(dataset, write=True, path_out=tmp_path) - assert len(datasets) == dataset.shape[dataset.dim_type.index("<{}>".format("FG_ECHO"))] + assert len(datasets) == dataset.shape[dataset.dim_type.index("FG_ECHO")] def test_splitSlicePkg(test_split_data, tmp_path): tmp_path /= "Slice/" dataset = Dataset(test_split_data[0]) - if "<{}>".format("FG_SLICE") not in dataset.dim_type: + if "FG_SLICE" not in dataset.dim_type: return if "VisuCoreSlicePacksSlices" not in dataset: return