Skip to content
Open
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: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Options:
final file.
-q, --quiet Not generate the final report and no
warning.
--openminds-version [v4|v5] openMINDS schema version to use for the
output. [default: v4]
--help Show this message and exit.
```

Expand Down
14 changes: 10 additions & 4 deletions bids2openminds/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from . import main
from . import utility
from . import report
from . import openminds_version as om

_ENTITY_RENAMES = {"sub": "subject", "ses": "session"}

Expand Down Expand Up @@ -60,11 +61,14 @@ def layout_to_df(layout):
return pd.DataFrame(rows)


def convert(input_path, save_output=False, output_path=None, multiple_files=False, include_empty_properties=False, quiet=False):
def convert(input_path, save_output=False, output_path=None, multiple_files=False, include_empty_properties=False, quiet=False, openminds_version="v4"):
if not (os.path.isdir(input_path)):
raise NotADirectoryError(
f"The input directory is not valid, you have specified {input_path} which is not a directory."
)

# Select the openMINDS schema version for all objects created below.
om.configure(openminds_version)
# TODO use BIDSValidator to check if input directory is a valid BIDS directory
# if not(BIDSValidator().is_bids(input_path)):
# raise NotADirectoryError(f"The input directory is not valid, you have specified {input_path} which is not a BIDS directory.")
Expand Down Expand Up @@ -118,7 +122,7 @@ def convert(input_path, save_output=False, output_path=None, multiple_files=Fal

if not quiet:
print(report.create_report(dataset, dataset_version, collection,
dataset_description, input_path, output_path))
dataset_description, input_path, output_path, openminds_version))

else:
print("Conversion was successful")
Expand All @@ -133,9 +137,11 @@ def convert(input_path, save_output=False, output_path=None, multiple_files=Fal
@click.option("--multiple-files", "multiple_files", flag_value=True, help="Each node is saved into a separate file within the specified directory. 'output-path' if specified, must be a directory.")
@click.option("-e", "--include-empty-properties", is_flag=True, default=False, help="Whether to include empty properties in the final file.")
@click.option("-q", "--quiet", is_flag=True, default=False, help="Not generate the final report and no warning.")
def convert_click(input_path, output_path, multiple_files, include_empty_properties, quiet):
@click.option("--openminds-version", type=click.Choice(["v4", "v5"]), default="v4", show_default=True, help="openMINDS schema version to use for the output.")
def convert_click(input_path, output_path, multiple_files, include_empty_properties, quiet, openminds_version):
convert(input_path, save_output=True, output_path=output_path,
multiple_files=multiple_files, include_empty_properties=include_empty_properties, quiet=quiet)
multiple_files=multiple_files, include_empty_properties=include_empty_properties, quiet=quiet,
openminds_version=openminds_version)


if __name__ == "__main__":
Expand Down
180 changes: 124 additions & 56 deletions bids2openminds/main.py

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions bids2openminds/openminds_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
Single source of truth for the active openMINDS schema version.

The bids2openminds converter can emit either openMINDS v4 or v5. The ``openminds``
package exposes each schema version under a separate import path
(``openminds.v4.core``, ``openminds.v5.core``, ...), so this module holds the
``core`` and ``controlled_terms`` submodules of the *currently selected* version.
The rest of the package references them through ``om.core`` / ``om.controlled_terms``
(``from . import openminds_version as om``) instead of hard-coding a version.

Always use attribute access (``om.core.X``), never ``from .openminds_version import core``,
so the reference reflects the version chosen at runtime by :func:`configure`.

It is named ``openminds_version`` rather than ``schema`` because both BIDS and
openMINDS use the word "schema"; this module is specifically about the openMINDS
package *version*.
"""

import importlib

SUPPORTED_VERSIONS = ("v4", "v5")
DEFAULT_VERSION = "v4"

#: The currently configured version string (one of SUPPORTED_VERSIONS).
version = None
#: The ``openminds.<version>.core`` submodule for the configured version.
core = None
#: The ``openminds.<version>.controlled_terms`` submodule for the configured version.
controlled_terms = None


def configure(requested_version=DEFAULT_VERSION):
"""Select the openMINDS schema version to use for the output.

Rebinds the module-level :data:`core` and :data:`controlled_terms` to the
submodules of the requested version and records the choice in :data:`version`.
Call this before creating any openMINDS objects.

Parameters:
- requested_version (str): one of :data:`SUPPORTED_VERSIONS` ("v4" or "v5").

Raises:
- ValueError: if ``requested_version`` is not supported.
"""
global version, core, controlled_terms
if requested_version not in SUPPORTED_VERSIONS:
raise ValueError(
f"Unsupported openMINDS version {requested_version!r}; "
f"choose one of {', '.join(SUPPORTED_VERSIONS)}."
)
core = importlib.import_module(f"openminds.{requested_version}.core")
controlled_terms = importlib.import_module(
f"openminds.{requested_version}.controlled_terms")
version = requested_version


# Initialise to the default version on import so that simply importing the package
# (and the existing unit tests, which exercise functions directly) works without an
# explicit configure() call.
configure(DEFAULT_VERSION)
44 changes: 30 additions & 14 deletions bids2openminds/report.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import os


def create_report(dataset, dataset_version, collection, dataset_description, input_path, output_path):
def create_report(dataset, dataset_version, collection, dataset_description, input_path, output_path, openminds_version="v4"):
subject_number = 0
subject_state_numbers = []
file_bundle_number = 0
files_number = 0
behavioral_protocols_numbers = 0
content_type_list = ""

for item in collection:
Expand All @@ -23,14 +22,30 @@ def create_report(dataset, dataset_version, collection, dataset_description, inp

file_bundle_number += 1

if item.type_.endswith("BehavioralProtocol"):

behavioral_protocols_numbers += 1

if item.type_.endswith("ContentType"):

content_type_list += f"{item.name}\n"

# In v5, DatasetVersion no longer carries `authors`: they live inside
# `contributions`. Derive them version-agnostically.
authors = getattr(dataset_version, "authors", None)
if authors is None and getattr(dataset_version, "contributions", None):
authors = []
for contribution in dataset_version.contributions:
authors.extend(contribution.contributors or [])

# Behavioral protocols are linked on the DatasetVersion: in v4 via the dedicated
# `behavioral_protocols` property, in v5 merged into the general `protocols`
# property (which may also hold non-behavioral Protocols), see openMINDS_core #377.
behavioral_protocols = getattr(dataset_version, "behavioral_protocols", None)
if not behavioral_protocols:
protocols = getattr(dataset_version, "protocols", None) or []
behavioral_protocols = [
p for p in protocols if p.type_.endswith("BehavioralProtocol")
]
behavioral_protocols = behavioral_protocols or None
behavioral_protocols_numbers = len(behavioral_protocols or [])

experimental_approaches_list = ""
if dataset_version.experimental_approaches is not None:
for approache in dataset_version.experimental_approaches:
Expand All @@ -52,16 +67,16 @@ def create_report(dataset, dataset_version, collection, dataset_description, inp
techniques_list = "No techniques were detected. Please follow the BIDS recommendations for suffixes, as bids2openminds detects techniques based on suffixes."

behavioral_protocols_list = ""
if dataset_version.behavioral_protocols is not None:
for behavioral_protocol in dataset_version.behavioral_protocols:
if behavioral_protocols is not None:
for behavioral_protocol in behavioral_protocols:
behavioral_protocols_list += f"{behavioral_protocol.name}\n"
else:
behavioral_protocols_list = "No behavioral protocols were detected. Please follow the BIDS recommendations for task labels, as bids2openminds detects behavioral protocols based on task labels."

author_list = ""
i = 1
if dataset_version.authors is not None:
for author in dataset_version.authors:
if authors is not None:
for author in authors:
if author.family_name is not None:
author_list += f" {i}. {author.family_name}, {author.given_name}\n"
i += 1
Expand All @@ -78,15 +93,16 @@ def create_report(dataset, dataset_version, collection, dataset_description, inp

report = f"""
Conversion Report
=================
=================
Conversion was successful, the openMINDS file is in {output_path}
openMINDS schema version: {openminds_version} (load it back with Collection.load(..., version="{openminds_version}"))

Dataset title : {dataset.full_name}


The following elements were converted:
------------------------------------------
+ number of authors : {len(dataset_version.authors or [])}
The following elements were converted:
------------------------------------------
+ number of authors : {len(authors or [])}
+ number of converted subjects: {subject_number}
+ number of states per subject: {text_subject_state_numbers}
+ number of files: {files_number}
Expand Down
28 changes: 13 additions & 15 deletions bids2openminds/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@

import pandas as pd

import openminds.v4.controlled_terms as controlled_terms
from openminds.v4.core import Hash, QuantitativeValue, ContentType
from openminds.v4.controlled_terms import UnitOfMeasurement
from . import openminds_version as om


def read_json(file_path: str) -> dict:
Expand Down Expand Up @@ -115,7 +113,7 @@ def file_hash(file_path: str, algorithm: str = "MD5"):
hash_value = hash_object.hexdigest()

# Create a openMINDS Hash object with the algorithm and digest
openminds_hash = Hash(algorithm=algorithm, digest=hash_value)
openminds_hash = om.core.Hash(algorithm=algorithm, digest=hash_value)

return openminds_hash

Expand All @@ -131,8 +129,8 @@ def file_storage_size(file_path: str):
as an openMINDS object and the int is the raw byte count.
"""
file_stats = os.stat(file_path)
file_size = QuantitativeValue(
value=file_stats.st_size, unit=UnitOfMeasurement.by_name("byte"))
file_size = om.core.QuantitativeValue(
value=file_stats.st_size, unit=om.controlled_terms.UnitOfMeasurement.by_name("byte"))
return file_size, file_stats.st_size


Expand Down Expand Up @@ -162,19 +160,19 @@ def detect_nifti_version(file_name, extension, file_size):
return None

if sizeof_hdr == nii1_sizeof_hdr:
return ContentType.by_name("application/vnd.nifti.1")
return om.core.ContentType.by_name("application/vnd.nifti.1")

elif sizeof_hdr == nii2_sizeof_hdr:
return ContentType.by_name("application/vnd.nifti.2")
return om.core.ContentType.by_name("application/vnd.nifti.2")

else: # big endian
sizeof_hdr = int.from_bytes(byte_data, byteorder='big')

if sizeof_hdr == nii1_sizeof_hdr:
return ContentType.by_name("application/vnd.nifti.1")
return om.core.ContentType.by_name("application/vnd.nifti.1")

elif sizeof_hdr == nii2_sizeof_hdr:
return ContentType.by_name("application/vnd.nifti.2")
return om.core.ContentType.by_name("application/vnd.nifti.2")

if extension == ".nii.gz":
try:
Expand All @@ -189,18 +187,18 @@ def detect_nifti_version(file_name, extension, file_size):
return None

if sizeof_hdr == nii1_sizeof_hdr:
return ContentType.by_name("application/vnd.nifti.1")
return om.core.ContentType.by_name("application/vnd.nifti.1")

elif sizeof_hdr == nii2_sizeof_hdr:
return ContentType.by_name("application/vnd.nifti.2")
return om.core.ContentType.by_name("application/vnd.nifti.2")

else: # big endian
sizeof_hdr = int.from_bytes(byte_data, byteorder='big')

if sizeof_hdr == nii1_sizeof_hdr:
return ContentType.by_name("application/vnd.nifti.1")
return om.core.ContentType.by_name("application/vnd.nifti.1")

elif sizeof_hdr == nii2_sizeof_hdr:
return ContentType.by_name("application/vnd.nifti.2")
return om.core.ContentType.by_name("application/vnd.nifti.2")

return ContentType.by_name("application/vnd.nifti.1")
return om.core.ContentType.by_name("application/vnd.nifti.1")
5 changes: 4 additions & 1 deletion docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The ``convert`` function processes a Brain Imaging Data Structure (BIDS) directo

Function Signature
##################
>>> def convert(input_path, save_output=False, output_path=None, multiple_files=False, include_empty_properties=False, quiet=False):
>>> def convert(input_path, save_output=False, output_path=None, multiple_files=False, include_empty_properties=False, quiet=False, openminds_version="v4"):

Parameters
##########
Expand All @@ -23,6 +23,7 @@ Parameters
- ``multiple_files`` (bool, default=False): If True, the openMINDS data will be saved into multiple files within the specified output_path.
- ``include_empty_properties`` (bool, default=False): If True, includes all the openMINDS properties with empty values in the final output. Otherwise includes only properties that have a non `None` value.
- ``quiet`` (bool, default=False): If True, suppresses warnings and the final report output. Only prints success messages.
- ``openminds_version`` (str, default="v4"): The openMINDS schema version to use for the output, either ``"v4"`` or ``"v5"``. The chosen version is not recorded in the output, so pass the same value to ``Collection.load(..., version=...)`` when reloading.

Returns
#######
Expand Down Expand Up @@ -65,3 +66,5 @@ This function is also accessible via a command-line interface using the `click`
-e, --include-empty-properties
Include empty properties in the final file.
-q, --quiet Suppress warnings and reports.
--openminds-version [v4|v5]
openMINDS schema version to use for the output (default: v4).
19 changes: 19 additions & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pytest

import bids2openminds.openminds_version as om


@pytest.fixture(autouse=True)
def reset_openminds_version():
"""Reset the global openMINDS version around every test.

``convert(..., openminds_version=...)`` (and tests calling
``om.configure``) mutate module-level state in
``bids2openminds.openminds_version``. Resetting to the default before and
after each test keeps a test that selects a non-default version from leaking
that choice into later tests, which call the conversion functions directly
and expect the default (v4).
"""
om.configure(om.DEFAULT_VERSION)
yield
om.configure(om.DEFAULT_VERSION)
Loading
Loading