Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 64 additions & 53 deletions brukerapi/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,7 @@ def __init__(self, path, **state):
(name for name in content if re.fullmatch(r"rawdata\.job\d+", name)),
key=lambda name: int(name.rsplit("job", 1)[1]),
)
named_rawdata_jobs = sorted(
name
for name in content
if name not in rawdata_jobs and self.is_supported_path(self.path / name, {"rawdata"})
)
named_rawdata_jobs = sorted(name for name in content if name not in rawdata_jobs and self.is_supported_path(self.path / name, {"rawdata"}))
if "fid" in content:
self.path = self.path / "fid"
elif "2dseq" in content:
Expand All @@ -273,10 +269,7 @@ def __init__(self, path, **state):
if self.type not in DEFAULT_STATES:
raise UnsupportedDatasetType(self.type)
if self.type == "fid" and self.subtype in FID_COMPANION_SUBTYPES and not state.get("_auxiliary"):
raise UnsupportedDatasetType(
f"{self.path.name} is a fid companion (spec §3.5); load it as "
f"Dataset({with_suffix(self.path, '')!s}).fid_companions[{self.subtype!r}]"
)
raise UnsupportedDatasetType(f"{self.path.name} is a fid companion (spec §3.5); load it as Dataset({with_suffix(self.path, '')!s}).fid_companions[{self.subtype!r}]")
if not self._is_supported_subtype() and not (state.get("_auxiliary") and self.type == "fid" and self.subtype in FID_COMPANION_SUBTYPES):
raise UnsupportedDatasetType(self.path.name)

Expand Down Expand Up @@ -596,9 +589,9 @@ def get(self, name, default=None):
A name no configuration declares still raises, so a typo does not
quietly become `default`::

dataset.get("TE") # 4.0, or None where there is no echo time
dataset.get("TE", "n/a") # 4.0, or "n/a"
dataset.get("TEE") # AttributeError
dataset.get("TE") # 4.0, or None where there is no echo time
dataset.get("TE", "n/a") # 4.0, or "n/a"
dataset.get("TEE") # AttributeError

Parameters are a separate layer, read with ``dataset[key]`` or
``dataset.parameters``.
Expand Down Expand Up @@ -727,8 +720,7 @@ def rawdata_stored_scans(self):
settings_stored = int(settings[9])
if settings_stored != job_stored and not getattr(self, "_warned_rawdata_stored_scans", False):
warnings.warn(
f"ACQ_ScanPipeJobSettings nStoredScans={settings_stored} disagrees with "
f"ACQ_jobs nStoredScans={job_stored} for {self.path}; using the settings value",
f"ACQ_ScanPipeJobSettings nStoredScans={settings_stored} disagrees with ACQ_jobs nStoredScans={job_stored} for {self.path}; using the settings value",
RuntimeWarning,
stacklevel=2,
)
Expand Down Expand Up @@ -779,9 +771,7 @@ def _infer_scheme_id(self):
if descriptions and descriptions[0] == "Spectroscopic":
return "SPECTROSCOPY" if int(dim) == 1 else "CSI"

if n_projections is not None and int(n_projections) > 0 and (
(self.path.parent / "traj").exists() or "RADIAL" in family or "UTE" in family
):
if n_projections is not None and int(n_projections) > 0 and ((self.path.parent / "traj").exists() or "RADIAL" in family or "UTE" in family):
return "RADIAL"

acq_size = np.atleast_1d(self._parameter_value("ACQ_size", []))
Expand Down Expand Up @@ -940,9 +930,7 @@ def _read_binary_file(self, path, dtype, shape):
f"expected {expected_bytes} bytes for shape {tuple(shape)} and dtype {dtype}"
)

raise InvalidDataset(
f"Invalid dataset size at {path}: expected {expected_bytes} bytes for shape {tuple(shape)} and dtype {dtype}, got {actual_bytes} bytes"
)
raise InvalidDataset(f"Invalid dataset size at {path}: expected {expected_bytes} bytes for shape {tuple(shape)} and dtype {dtype}, got {actual_bytes} bytes")

return read_array(path, dtype, shape)

Expand Down Expand Up @@ -1343,9 +1331,7 @@ def affine(self):
"""
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),"
)
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)",
Expand Down Expand Up @@ -1468,42 +1454,67 @@ def frame_group_values(self):
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]), []).append(axis)
try:
offset = int(dependencies[index][1])
except (TypeError, ValueError):
offset = 0
grouped.setdefault(str(dependencies[index][0]), []).append((axis, offset, group_name))

values = {}
data_shape = self._data.shape
for name, axes in grouped.items():
for name, windows in grouped.items():
if name not in self:
continue
parameter = np.asarray(self[name].value)
parameter_axes = axes
if len(parameter_axes) == 1 and parameter.ndim and parameter.shape[0] != data_shape[parameter_axes[0]]:
matches = [axis for axis in range(self.encoded_dim, self._data.ndim) if data_shape[axis] == parameter.shape[0]]
if len(matches) == 1:
parameter_axes = matches
axis_sizes = tuple(data_shape[axis] for axis in parameter_axes)
leading_size = int(np.prod(axis_sizes, dtype=int))
if parameter.size == 0 or parameter.size % leading_size:
continue
payload_shape = parameter.shape[len(parameter_axes) :]
if parameter.ndim < len(parameter_axes) or parameter.shape[: len(parameter_axes)] != axis_sizes:
payload_shape = (parameter.size // leading_size,)
aligned = np.reshape(parameter, axis_sizes + payload_shape, order="F")

payload_axes = tuple(range(self._data.ndim, self._data.ndim + len(payload_shape)))
source_positions = tuple(parameter_axes) + payload_axes
aligned = np.transpose(aligned, np.argsort(source_positions))
target_shape = []
source_axis = 0
for axis in range(self._data.ndim + len(payload_shape)):
if axis in source_positions:
target_shape.append(aligned.shape[source_axis])
source_axis += 1
else:
target_shape.append(1)
values[name] = np.reshape(aligned, target_shape)
for axis, offset, group_name in windows:
# Spec 7.4: `valsStart` is where this group's block begins inside
# the dependent array. Two groups may share one array -- a DTI
# PROCNO concatenates the cycle labels and the map labels into a
# single VisuFGElemComment -- and then each needs its own window.
# Only such a shared array is keyed by group, so a parameter owned
# by one group keeps its plain name.
key = name if len(windows) == 1 else f"{name}[{group_name}]"
value = self._frame_group_window(name, axis, offset, shared=len(windows) > 1)
if value is not None:
values[key] = value
return values

def _frame_group_window(self, name, axis, offset, *, shared):
"""The slice of dependent parameter `name` that the group on `axis` owns."""
data_shape = self._data.shape
parameter = np.asarray(self[name].value)
axis_size = data_shape[axis]
if (offset or shared) and parameter.ndim and parameter.shape[0] >= offset + axis_size:
parameter = parameter[offset : offset + axis_size]
return self._align_to_frame_groups(parameter, [axis])

def _align_to_frame_groups(self, parameter, parameter_axes):
"""`parameter` reshaped so its leading axes line up with :attr:`data`."""
data_shape = self._data.shape
if len(parameter_axes) == 1 and parameter.ndim and parameter.shape[0] != data_shape[parameter_axes[0]]:
matches = [axis for axis in range(self.encoded_dim, self._data.ndim) if data_shape[axis] == parameter.shape[0]]
if len(matches) == 1:
parameter_axes = matches
axis_sizes = tuple(data_shape[axis] for axis in parameter_axes)
leading_size = int(np.prod(axis_sizes, dtype=int))
if parameter.size == 0 or parameter.size % leading_size:
return None
payload_shape = parameter.shape[len(parameter_axes) :]
if parameter.ndim < len(parameter_axes) or parameter.shape[: len(parameter_axes)] != axis_sizes:
payload_shape = (parameter.size // leading_size,)
aligned = np.reshape(parameter, axis_sizes + payload_shape, order="F")

payload_axes = tuple(range(self._data.ndim, self._data.ndim + len(payload_shape)))
source_positions = tuple(parameter_axes) + payload_axes
aligned = np.transpose(aligned, np.argsort(source_positions))
target_shape = []
source_axis = 0
for axis in range(self._data.ndim + len(payload_shape)):
if axis in source_positions:
target_shape.append(aligned.shape[source_axis])
source_axis += 1
else:
target_shape.append(1)
return np.reshape(aligned, target_shape)

@staticmethod
def _metadata_field(name, prefix):
"""Snake-case `name` with `prefix` removed, if it ends on a word boundary.
Expand Down
Loading