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
54 changes: 47 additions & 7 deletions bids2openminds/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
from openminds import Collection
import os
import click
import re
from . import main
from . import utility
from . import report


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, short_name=None):
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."
Expand All @@ -32,8 +33,42 @@ def convert(input_path, save_output=False, output_path=None, multiple_files=Fal

dataset_description = utility.read_json(dataset_description_path.iat[0, 0])

if short_name is None:
while True:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand that part. This should not be a prompt but a function in my opinion and the short name should be set automatically from available information when not specified/overwritten and should not be required for proceeding.

Automatic setting could use the short citation handle (e.g. Zehl et al. 2024) as done in EBRAINS.

input_name = input(
"To convert this dataset, a short name is required.\n"
"Please enter an informative short name (less than 10 characters).\n"
"If you don't want to assign one now, press Enter, "
"lookup labels and internal identifiers will be the same.\n> "
).strip()

# User pressed Enter → skip assigning
if not input_name:
short_name = None
break

# Check length
if len(input_name) < 10:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not an requirement from our side

short_name = input_name
break
else:
print(
"The short name must be fewer than 10 characters. Please try again.\n")

elif short_name is False:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't be none later needs to be defined (is a required property and needs to be set as a default)

# Explicit opt-out — skip prompt
short_name = None

else:
# Ensure the short name has no spaces and is under 10 characters
short_name = short_name.replace(" ", "")
if len(short_name) > 10:
while len(short_name) > 10:
short_name = input(
"The short name must be fewer than 10 characters. Please try again.").strip()

[subjects_dict, subject_state_dict, subjects_list] = main.create_subjects(
subjects_id, layout_df, bids_layout, collection)
subjects_id, layout_df, bids_layout, collection, short_name)

behavioral_protocols, behavioral_protocols_dict = main.create_behavioral_protocol(
bids_layout, collection)
Expand All @@ -42,10 +77,10 @@ def convert(input_path, save_output=False, output_path=None, multiple_files=Fal
layout_df, input_path, collection)

dataset_version = main.create_dataset_version(
bids_layout, dataset_description, layout_df, subjects_list, file_repository, behavioral_protocols, collection)
bids_layout, dataset_description, layout_df, subjects_list, file_repository, behavioral_protocols, short_name, collection)

dataset = main.create_dataset(
dataset_description, dataset_version, collection)
dataset_description, dataset_version, short_name, collection)

failures = collection.validate(ignore=["required", "value"])
assert len(failures) == 0
Expand All @@ -60,9 +95,13 @@ def convert(input_path, save_output=False, output_path=None, multiple_files=Fal
collection.save(output_path, individual_files=multiple_files,
include_empty_properties=include_empty_properties)

save_text = f"the openMINDS file is in {output_path}"
else:
save_text = save_text = "No files were saved; the function returned the output collection instead."

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

else:
print("Conversion was successful")
Expand All @@ -77,9 +116,10 @@ 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("-n", "--short-name", default=None, help="Short name for the dataset (less than 10 characters). If None, you'll be prompted (default); if False, no short name will be assigned.")
def convert_click(input_path, output_path, multiple_files, include_empty_properties, quiet, short_name):
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, short_name=short_name)


if __name__ == "__main__":
Expand Down
40 changes: 21 additions & 19 deletions bids2openminds/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def create_openminds_age(data_subject):
return None


def create_dataset_version(bids_layout, dataset_description, layout_df, studied_specimens, file_repository, behavioral_protocols, collection):
def create_dataset_version(bids_layout, dataset_description, layout_df, studied_specimens, file_repository, behavioral_protocols, short_name, collection):

# Fetch the dataset type from dataset description file

Expand Down Expand Up @@ -236,7 +236,7 @@ def create_dataset_version(bids_layout, dataset_description, layout_df, studied_
dataset_version = omcore.DatasetVersion(
digital_identifier=digital_identifier,
experimental_approaches=experimental_approaches,
short_name=dataset_description["Name"],
short_name=short_name,
full_name=dataset_description["Name"],
studied_specimens=studied_specimens,
authors=authors,
Expand All @@ -254,13 +254,13 @@ def create_dataset_version(bids_layout, dataset_description, layout_df, studied_
return dataset_version


def create_dataset(dataset_description, dataset_version, collection):
def create_dataset(dataset_description, dataset_version, short_name, collection):

dataset = omcore.Dataset(
digital_identifier=dataset_version.digital_identifier,
authors=dataset_version.authors,
full_name=dataset_version.full_name,
short_name=dataset_version.short_name,
short_name=short_name,
has_versions=dataset_version
)

Expand Down Expand Up @@ -315,7 +315,13 @@ def sex_openminds(data_subject: pd.DataFrame):
return None


def create_subjects(subject_id, layout_df, layout, collection):
def create_subjects(subject_id, layout_df, layout, collection, dataset_short_name):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

short_name or dataset_short_name? what is the difference?


if dataset_short_name:
dataset_short_name_ = dataset_short_name + "_"
else:
# If there is no short name, assign an empty string so lookup labels don’t include it
dataset_short_name_ = ""

sessions = layout.get_sessions()
subjects_dict = {}
Expand All @@ -333,9 +339,8 @@ def create_subjects(subject_id, layout_df, layout, collection):
# dealing with condition that have no seasion
if not sessions:
state = omcore.SubjectState(
internal_identifier=f"Studied state {subject_name}".strip(
),
lookup_label=f"Studied state {subject_name}".strip()
lookup_label=f"{dataset_short_name_}{subject_name}_ses-01".strip(),
internal_identifier=None
)
collection.add(state)
state_cache_dict[""] = state
Expand All @@ -345,17 +350,16 @@ def create_subjects(subject_id, layout_df, layout, collection):
for session in sessions:
if not (table_filter(table_filter(layout_df, session, "session"), subject, "subject").empty):
state = omcore.SubjectState(
internal_identifier=f"Studied state {subject_name} {session}".strip(
),
lookup_label=f"Studied state {subject_name} {session}".strip(
lookup_label=f"{dataset_short_name_}{subject_name}_ses-{session}".strip(),
internal_identifier=f"{subject_name}_ses-{session}".strip(
)
)
collection.add(state)
state_cache_dict[f"{session}"] = state
state_cache.append(state)
subject_state_dict[f"{subject}"] = state_cache_dict
subject_cache = omcore.Subject(
lookup_label=f"{subject_name}",
lookup_label=f"{dataset_short_name_}{subject_name}",
internal_identifier=f"{subject_name}",
studied_states=state_cache
)
Expand All @@ -380,8 +384,8 @@ def create_subjects(subject_id, layout_df, layout, collection):
state = omcore.SubjectState(
age=create_openminds_age(data_subject),
handedness=handedness_openminds(data_subject),
internal_identifier=f"Studied state {subject_name}".strip(),
lookup_label=f"Studied state {subject_name}".strip()
internal_identifier=None,
lookup_label=f"{dataset_short_name_}{subject_name}_ses-01".strip()
)
collection.add(state)
state_cache_dict[""] = state
Expand All @@ -392,18 +396,16 @@ def create_subjects(subject_id, layout_df, layout, collection):
state = omcore.SubjectState(
age=create_openminds_age(data_subject),
handedness=handedness_openminds(data_subject),
internal_identifier=f"Studied state {subject_name} {session}".strip(
),
lookup_label=f"Studied state {subject_name} {session}".strip(
)
internal_identifier=f"{subject_name}_ses-{session}".strip(),
lookup_label=f"{dataset_short_name_}{subject_name}_ses-{session}".strip()
)
collection.add(state)
state_cache_dict[f"{session}"] = state
state_cache.append(state)
subject_state_dict[f"{subject}"] = state_cache_dict
subject_cache = omcore.Subject(
biological_sex=sex_openminds(data_subject),
lookup_label=f"{subject_name}",
lookup_label=f"{dataset_short_name_}{subject_name}",
internal_identifier=f"{subject_name}",
# TODO species should default to homo sapiens
species=spices_openminds(data_subject),
Expand Down
7 changes: 4 additions & 3 deletions bids2openminds/report.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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, save_text):
subject_number = 0
subject_state_numbers = []
file_bundle_number = 0
Expand Down Expand Up @@ -60,9 +60,10 @@ def create_report(dataset, dataset_version, collection, dataset_description, inp
report = f"""
Conversion Report
=================
Conversion was successful, the openMINDS file is in {output_path}
Conversion was successful, {save_text}

Dataset title : {dataset.full_name}
Dataset full name : {dataset.full_name}
Dataset short name: {dataset.short_name}


Experimental approaches detected:
Expand Down
32 changes: 21 additions & 11 deletions 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, short_name=None, multiple_files=False, include_empty_properties=False, quiet=False):

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.
- ``short_name`` (str or bool, default=None): A short name for the dataset (less than 10 characters). If None, you'll be prompted (default); if False, no short name will be assigned.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the character limitation is not correct. we don't have this. also if False it cannot just be not defined. It has to be defined. If False a default needs to be set.


Returns
#######
Expand All @@ -32,7 +33,7 @@ Example Usage
#############
>>> import bids2openminds.converter as converter
>>> input_path = "/path/to/BIDS/dataset"
>>> collection = converter.convert(input_path, save_output=True, output_path="/path/to/output", multiple_files=False, include_empty_properties=False, quiet=False)
>>> collection = converter.convert(input_path, save_output=True, short_name=ShortName, output_path="/path/to/output", multiple_files=False, include_empty_properties=False, quiet=False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the convention (CamelCase) how to write string variables?


Or one can chose the default parmetrs as following:

Expand All @@ -48,14 +49,23 @@ This function is also accessible via a command-line interface using the `click`

Usage: bids2openminds [OPTIONS] INPUT_PATH

Arguments:
input-path Path to the BIDS directory.

Options:
-o, --output-path PATH The output path or filename for OpenMINDS file/files.
--single-file Save the entire collection into a single file (default).
--multiple-files Save each node into a separate file within the specified directory.
-e, --include-empty-properties
Include empty properties in the final file.
-q, --quiet Suppress warnings and reports.
-o, --output-path PATH The output path or filename for OpenMINDS
file/files.
--single-file Save the entire collection into a single
file (default).
--multiple-files Each node is saved into a separate file
within the specified directory. 'output-
path' if specified, must be a directory.
-e, --include-empty-properties Whether to include empty properties in the
final file.
-q, --quiet Not generate the final report and no
warning.
-n, --short-name TEXT Short name for the dataset (less than 10
characters). If None, you'll be prompted
(default); if False, no short name will be
assigned.
--help Show this message and exit.



Loading