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
2 changes: 1 addition & 1 deletion brukerapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__all__ = ["dataset","exceptions","filters","folders", "jcampdx","schemes","utils"]
__all__ = ["dataset", "exceptions", "folders", "jcampdx", "utils"]
4 changes: 2 additions & 2 deletions brukerapi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,12 @@ def report(args):
if input.is_dir():
# folder in-place
if output is None:
Folder(input).report(format_=args.format, props=args.props, verbose=args.verbose)
Folder(str(input)).report(format_=args.format, props=args.props, verbose=args.verbose)
else:
# folder to folder -- an output folder that does not exist yet is
# created rather than silently matching no branch and exiting 0
output.mkdir(parents=True, exist_ok=True)
Folder(input).report(path_out=output, format_=args.format, props=args.props, verbose=args.verbose)
Folder(str(input)).report(path_out=output, format_=args.format, props=args.props, verbose=args.verbose)
# dataset in-place
elif output is None:
Dataset(input, add_parameters=["subject"]).report(props=args.props, verbose=args.verbose, format_=args.format)
Expand Down
32 changes: 20 additions & 12 deletions brukerapi/folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class Folder:

def __init__(
self,
path: str,
parent: "Folder" = None,
path: str | Path,
parent: "Folder | None" = None,
recursive: bool | None = None, # noqa: FBT001
dataset_index: list | None = None,
dataset_state: dict | None = None,
Expand Down Expand Up @@ -147,8 +147,9 @@ def query(self, query):

self.clean(node=self)

def query_pass(self, query: str, node: "Folder" = None):
children_out = []
def query_pass(self, query: str, node: "Folder | None" = None):
node = self if node is None else node
children_out: list[Folder | Dataset | JCAMPDX] = []
for child in node.children:
if isinstance(child, Folder):
children_out.append(self.query_pass(query, node=child))
Expand All @@ -164,7 +165,7 @@ def query_pass(self, query: str, node: "Folder" = None):
node.children = children_out
return node

def clean(self, node: "Folder" = None) -> "Folder":
def clean(self, node: "Folder | None" = None) -> "Folder":
"""Remove empty folders from the tree

:param node:
Expand Down Expand Up @@ -208,9 +209,9 @@ def get_study_list(self) -> list:
"""List of :obj:`.Study` instances contained in folder and its sub-folders"""
return TypeFilter(Study).list(self)

def make_tree(self, *, recursive: bool = True) -> list:
def make_tree(self, *, recursive: bool = True) -> list["Folder | Dataset | JCAMPDX"]:
"""Build a folder tree with optimized traversal."""
children = []
children: list[Folder | Dataset | JCAMPDX] = []
entries = list(self.path.iterdir())

for path in entries:
Expand Down Expand Up @@ -334,8 +335,8 @@ class Study(Folder):

def __init__(
self,
path: str,
parent: "Folder" = None,
path: str | Path,
parent: "Folder | None" = None,
recursive: bool | None = None, # noqa: FBT001
dataset_index: list | None = None,
dataset_state: dict | None = None,
Expand Down Expand Up @@ -415,8 +416,8 @@ class Experiment(Folder):

def __init__(
self,
path: str,
parent: "Folder" = None,
path: str | Path,
parent: "Folder | None" = None,
recursive: bool | None = None, # noqa: FBT001
dataset_index: list | None = None,
dataset_state: dict | None = None,
Expand Down Expand Up @@ -463,7 +464,14 @@ def _get_proc(self, proc_id):


class Processing(Folder):
def __init__(self, path, parent=None, recursive=None, dataset_index=None, dataset_state: dict | None = None):
def __init__(
self,
path: str | Path,
parent: "Folder | None" = None,
recursive: bool | None = None, # noqa: FBT001
dataset_index: list | None = None,
dataset_state: dict | None = None,
):
"""The constructor for Processing class.

:param path: path to a folder
Expand Down
4 changes: 2 additions & 2 deletions brukerapi/mergers.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ def _merge_data(cls, dataset, fg_abs_index):
:param fg_abs_index: index of dimension of the data array to be merged
:return:
"""
slc_re = [slice(None, None, None) for _ in range(dataset.data.ndim)]
slc_im = [slice(None, None, None) for _ in range(dataset.data.ndim)]
slc_re: list[slice | int] = [slice(None, None, None) for _ in range(dataset.data.ndim)]
slc_im: list[slice | int] = [slice(None, None, None) for _ in range(dataset.data.ndim)]
slc_re[fg_abs_index] = 0
slc_im[fg_abs_index] = 1

Expand Down
4 changes: 4 additions & 0 deletions brukerapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ def simple_measurement(dataset):
axes = (0, 1)
elif dataset.encoded_dim == 3:
axes = (0, 1, 2)
else:
raise ValueError("Simple measurement supports one to three encoded dimensions")

return np.fft.fftshift(np.fft.fft2(dataset.data, axes=axes), axes=axes)

Expand All @@ -74,6 +76,8 @@ def simple_reconstruction(dataset, **kwargs):
axes = (0, 1)
elif dataset.encoded_dim == 3:
axes = (0, 1, 2)
else:
raise ValueError("Simple reconstruction supports one to three encoded dimensions")

data = np.fft.fftshift(np.fft.ifft2(dataset.data, axes=axes), axes=axes)

Expand Down
22 changes: 17 additions & 5 deletions docs/source/compatibility.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,22 @@ Data contract and limitations

* FID data is returned as ordered raw k-space. ``AQ_mod=qf`` remains real;
quadrature modes are assembled as complex data.
* RARE/EPI phase-line ordering and EPI odd-line mirroring are applied.
Ramp-sampling regridding and ``RECO_qopts`` corrections are not full
reconstruction steps and remain the caller's responsibility.
* RARE/EPI phase-line ordering and EPI odd-line mirroring are applied. The
reader uses ``ACQ_scan_size`` and the selected reconstruction's
``RECO_inp_order`` when available; pulse-program scheme inference is the
fallback for a FID without a reconstruction. The default reconstruction is
the conventional first one, and ``reco_path=`` selects a different ``reco``
file. A disagreement between explicit metadata and the fallback inference
emits ``RuntimeWarning``. Ramp-sampling regridding and ``RECO_qopts``
corrections are not full reconstruction steps and remain the caller's
responsibility.
* Rawdata jobs are returned as complex ordered samples in their stored job
layout through ``data``. Prefer ``raw`` for the normalized
``(sample, shot, receiver)`` acquisition stream and ``kspace`` for validated
Cartesian PV360 layout conversion.
Cartesian PV360 layout conversion. ``STORE_discard`` jobs are omitted from
folder discovery because no binary exists. When both records are present,
``ACQ_ScanPipeJobSettings.nStoredScans`` determines the stored shape and is
checked against ``ACQ_jobs``; the per-job receiver selection is also read.
* 2dseq values are scaled as ``stored * slope + offset``. Visu slopes/offsets
take precedence, with RECO values as fallback.
* PV7/PV360 2dseq geometry uses the version-independent Visu geometry fields.
Expand Down Expand Up @@ -86,4 +95,7 @@ Data contract and limitations
``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.
per-package transform. If optional slice-package metadata is absent (notably
in PV5.1), contiguous frames with a common orientation are treated as one
package. Consequently two adjacent inferred packages with the same
orientation cannot be distinguished.
6 changes: 6 additions & 0 deletions docs/source/tutorials/how-to-2dseq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ in-memory datasets, including their own geometry, with:
for package in dataset.slice_packages:
print(package.data.shape, package.affine, package.resolution)

This also works for older PV5.1 datasets that do not have the optional
``VisuCoreSlicePacks*`` parameters. In that case packages are inferred from
contiguous frames with the same orientation. This inference cannot distinguish
two immediately adjacent packages that share an orientation; PV6 and later
datasets use the explicit package descriptors when available.

Use memory-mapped random access when only a sub-array is needed:

.. code-block:: python
Expand Down
18 changes: 18 additions & 0 deletions docs/source/tutorials/how-to-fid.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ Use the explicit views in new code: ``dataset.raw`` is the decoded acquisition
stream with axes ``(sample, shot, receiver)``; ``dataset.kspace`` is the same
ordered k-space exposed through ``data`` for compatibility.

When an experiment has a reconstruction, its ``reco`` declaration is used to
determine EPI continuous-train handling and odd-line reversal
(``RECO_inp_order=REV_ALT_ROWS``). The conventional first reconstruction is
used by default. Select another reconstruction explicitly when its input-order
metadata is the one that corresponds to the data being read:

.. code-block:: python

dataset = Dataset(
'path/to/fid',
reco_path='path/to/pdata/2/reco',
)

For a FID without a reconstruction, the reader falls back to the acquisition
declaration and, where necessary, the pulse-program scheme inference. A
``RuntimeWarning`` is emitted if the selected ``reco`` and scheme inference
disagree; the declared reconstruction value is used.

For custom pulse-program names the scheme is inferred from acquisition
metadata. An explicit override is available when inference is ambiguous:

Expand Down
15 changes: 15 additions & 0 deletions docs/source/tutorials/how-to-rawdata.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,18 @@ metadata-reordered array. BART consumers can request its 16-axis layout:
decoded job layout. It emits ``FutureWarning`` because that layout differs from
FID ``data``. Prefer ``raw`` or ``kspace`` in new applications. EPI and
non-Cartesian jobs are not reconstructed by ``kspace``.

Job discovery honours ``ACQ_ScanPipeJobSettings``. Jobs marked
``STORE_discard`` are deliberately omitted because ParaVision does not write a
corresponding ``rawdata.jobN`` file. For a present job, the reader sizes its
data from the settings record's ``nStoredScans`` and exposes the relevant
per-job metadata:

.. code-block:: python

settings = rawdata.rawdata_job_settings
scans = rawdata.rawdata_stored_scans
receivers = rawdata.rawdata_channels

If the settings and ``ACQ_jobs`` records disagree about the stored scan count,
the settings value is used and a ``RuntimeWarning`` identifies the mismatch.
Loading
Loading