diff --git a/bids2openminds/converter.py b/bids2openminds/converter.py index b2f63ca..80cf578 100644 --- a/bids2openminds/converter.py +++ b/bids2openminds/converter.py @@ -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." @@ -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: + 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: + 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: + # 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) @@ -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 @@ -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") @@ -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__": diff --git a/bids2openminds/main.py b/bids2openminds/main.py index 92401f4..45dff61 100644 --- a/bids2openminds/main.py +++ b/bids2openminds/main.py @@ -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 @@ -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, @@ -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 ) @@ -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): + + 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 = {} @@ -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 @@ -345,9 +350,8 @@ 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) @@ -355,7 +359,7 @@ def create_subjects(subject_id, layout_df, layout, collection): 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 ) @@ -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 @@ -392,10 +396,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} {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 @@ -403,7 +405,7 @@ def create_subjects(subject_id, layout_df, layout, collection): 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), diff --git a/bids2openminds/report.py b/bids2openminds/report.py index 440a158..f984006 100644 --- a/bids2openminds/report.py +++ b/bids2openminds/report.py @@ -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 @@ -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: diff --git a/docs/source/usage.rst b/docs/source/usage.rst index d0e17f8..0583fd9 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -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 ########## @@ -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. Returns ####### @@ -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) Or one can chose the default parmetrs as following: @@ -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. + + diff --git a/test/bids_examples_ds005.jsonld b/test/bids_examples_ds005.jsonld index a3793d4..7095924 100644 --- a/test/bids_examples_ds005.jsonld +++ b/test/bids_examples_ds005.jsonld @@ -13,8 +13,7 @@ }, "value": 28 }, - "internalIdentifier": "Studied state sub-01", - "lookupLabel": "Studied state sub-01" + "lookupLabel": "sub-01_ses-01" }, { "@id": "https://openminds.ebrains.eu/instances/unitOfMeasurement/year", @@ -71,8 +70,7 @@ }, "value": 21 }, - "internalIdentifier": "Studied state sub-02", - "lookupLabel": "Studied state sub-02" + "lookupLabel": "sub-02_ses-01" }, { "@id": "_:000006", @@ -110,8 +108,7 @@ }, "value": 27 }, - "internalIdentifier": "Studied state sub-03", - "lookupLabel": "Studied state sub-03" + "lookupLabel": "sub-03_ses-01" }, { "@id": "_:000009", @@ -140,8 +137,7 @@ }, "value": 25 }, - "internalIdentifier": "Studied state sub-04", - "lookupLabel": "Studied state sub-04" + "lookupLabel": "sub-04_ses-01" }, { "@id": "_:000011", @@ -170,8 +166,7 @@ }, "value": 20 }, - "internalIdentifier": "Studied state sub-05", - "lookupLabel": "Studied state sub-05" + "lookupLabel": "sub-05_ses-01" }, { "@id": "_:000013", @@ -200,8 +195,7 @@ }, "value": 20 }, - "internalIdentifier": "Studied state sub-06", - "lookupLabel": "Studied state sub-06" + "lookupLabel": "sub-06_ses-01" }, { "@id": "_:000015", @@ -230,8 +224,7 @@ }, "value": 24 }, - "internalIdentifier": "Studied state sub-07", - "lookupLabel": "Studied state sub-07" + "lookupLabel": "sub-07_ses-01" }, { "@id": "_:000017", @@ -260,8 +253,7 @@ }, "value": 25 }, - "internalIdentifier": "Studied state sub-08", - "lookupLabel": "Studied state sub-08" + "lookupLabel": "sub-08_ses-01" }, { "@id": "_:000019", @@ -290,8 +282,7 @@ }, "value": 19 }, - "internalIdentifier": "Studied state sub-09", - "lookupLabel": "Studied state sub-09" + "lookupLabel": "sub-09_ses-01" }, { "@id": "_:000021", @@ -320,8 +311,7 @@ }, "value": 20 }, - "internalIdentifier": "Studied state sub-10", - "lookupLabel": "Studied state sub-10" + "lookupLabel": "sub-10_ses-01" }, { "@id": "_:000023", @@ -350,8 +340,7 @@ }, "value": 20 }, - "internalIdentifier": "Studied state sub-11", - "lookupLabel": "Studied state sub-11" + "lookupLabel": "sub-11_ses-01" }, { "@id": "_:000025", @@ -380,8 +369,7 @@ }, "value": 21 }, - "internalIdentifier": "Studied state sub-12", - "lookupLabel": "Studied state sub-12" + "lookupLabel": "sub-12_ses-01" }, { "@id": "_:000027", @@ -410,8 +398,7 @@ }, "value": 22 }, - "internalIdentifier": "Studied state sub-13", - "lookupLabel": "Studied state sub-13" + "lookupLabel": "sub-13_ses-01" }, { "@id": "_:000029", @@ -440,8 +427,7 @@ }, "value": 19 }, - "internalIdentifier": "Studied state sub-14", - "lookupLabel": "Studied state sub-14" + "lookupLabel": "sub-14_ses-01" }, { "@id": "_:000031", @@ -470,8 +456,7 @@ }, "value": 20 }, - "internalIdentifier": "Studied state sub-15", - "lookupLabel": "Studied state sub-15" + "lookupLabel": "sub-15_ses-01" }, { "@id": "_:000033", @@ -500,8 +485,7 @@ }, "value": 22 }, - "internalIdentifier": "Studied state sub-16", - "lookupLabel": "Studied state sub-16" + "lookupLabel": "sub-16_ses-01" }, { "@id": "_:000035", @@ -561,7 +545,7 @@ { "@id": "_:000039", "@type": "https://openminds.ebrains.eu/core/FileRepository", - "IRI": "file:///home/peyman-user/Desktop/data/bids_test/bids-examples/ds005", + "IRI": "PREFIX", "format": { "@id": "https://openminds.ebrains.eu/instances/contentTypes/application_vnd.bids" }, @@ -5530,7 +5514,6 @@ "repository": { "@id": "_:000039" }, - "shortName": "Mixed-gambles task", "studiedSpecimen": [ { "@id": "_:000002" @@ -5647,8 +5630,7 @@ { "@id": "_:000229" } - ], - "shortName": "Mixed-gambles task" + ] } ] } diff --git a/test/bids_examples_eeg_rest_fmri.jsonld b/test/bids_examples_eeg_rest_fmri.jsonld index 0067dfd..c7166c3 100644 --- a/test/bids_examples_eeg_rest_fmri.jsonld +++ b/test/bids_examples_eeg_rest_fmri.jsonld @@ -6,8 +6,7 @@ { "@id": "_:000000", "@type": "https://openminds.ebrains.eu/core/SubjectState", - "internalIdentifier": "Studied state sub-32", - "lookupLabel": "Studied state sub-32" + "lookupLabel": "sub-32_ses-01" }, { "@id": "_:000001", @@ -40,8 +39,7 @@ { "@id": "_:000003", "@type": "https://openminds.ebrains.eu/core/SubjectState", - "internalIdentifier": "Studied state sub-35", - "lookupLabel": "Studied state sub-35" + "lookupLabel": "sub-35_ses-01" }, { "@id": "_:000004", @@ -60,8 +58,7 @@ { "@id": "_:000005", "@type": "https://openminds.ebrains.eu/core/SubjectState", - "internalIdentifier": "Studied state sub-36", - "lookupLabel": "Studied state sub-36" + "lookupLabel": "sub-36_ses-01" }, { "@id": "_:000006", @@ -118,7 +115,7 @@ { "@id": "_:000010", "@type": "https://openminds.ebrains.eu/core/FileRepository", - "IRI": "file:///home/peyman-user/Desktop/Code/bids-examples/eeg_rest_fmri", + "IRI": "PREFIX", "format": { "@id": "https://openminds.ebrains.eu/instances/contentTypes/application_vnd.bids" }, @@ -1838,7 +1835,6 @@ "repository": { "@id": "_:000010" }, - "shortName": "EEG, fMRI and NODDI at rest'", "studiedSpecimen": [ { "@id": "_:000001" @@ -1937,8 +1933,7 @@ { "@id": "_:000085" } - ], - "shortName": "EEG, fMRI and NODDI at rest'" + ] } ] } diff --git a/test/test_bids_examples.py b/test/test_bids_examples.py index 7f3ed24..c517243 100644 --- a/test/test_bids_examples.py +++ b/test/test_bids_examples.py @@ -17,7 +17,8 @@ @pytest.mark.parametrize("dataset_label, dataset_subject_number, dataset_subject_state_number, dataset_person_number, dataset_files_number, dataset_file_bundles_number, dataset_behavioral_protocol_number", example_dataset) def test_example_datasets(dataset_label, dataset_subject_number, dataset_subject_state_number, dataset_person_number, dataset_files_number, dataset_file_bundles_number, dataset_behavioral_protocol_number): test_dir = os.path.join("bids-examples", dataset_label) - bids2openminds.converter.convert(test_dir, save_output=True) + bids2openminds.converter.convert( + test_dir, save_output=True, short_name=False) c = Collection() c.load(os.path.join(test_dir, "openminds.jsonld")) diff --git a/test/test_example_datasets_click.py b/test/test_example_datasets_click.py index 3c32416..47d6dd5 100644 --- a/test/test_example_datasets_click.py +++ b/test/test_example_datasets_click.py @@ -10,7 +10,7 @@ def test_example_datasets_click(): test_dir = os.path.join("bids-examples", test_data_set) runner = CliRunner() - result = runner.invoke(convert_click, [test_dir]) + result = runner.invoke(convert_click, [test_dir, "--short-name", "False"]) assert result.exit_code == 0 c = Collection() c.load(os.path.join(test_dir, "openminds.jsonld")) @@ -22,7 +22,8 @@ def test_example_datasets_click_seperate_files(): if os.path.isdir(path_openminds): shutil.rmtree(path_openminds) runner = CliRunner() - result = runner.invoke(convert_click, ["--multiple-files", test_dir]) + result = runner.invoke( + convert_click, ["--multiple-files", test_dir, "--short-name", "False"]) assert result.exit_code == 0 numer_of_files = len(os.listdir(path_openminds)) assert numer_of_files == number_of_openminds_files @@ -33,7 +34,7 @@ def test_example_datasets_click_output_location(): openminds_file = os.path.join("bids-examples", "test_openminds.jsonld") runner = CliRunner() result = runner.invoke( - convert_click, ["-o", openminds_file, test_dir]) + convert_click, ["-o", openminds_file, test_dir, "--short-name", "False"]) assert result.exit_code == 0 c = Collection() c.load(openminds_file) diff --git a/test/test_thorough_bids_example.py b/test/test_thorough_bids_example.py index 9360221..47e9841 100644 --- a/test/test_thorough_bids_example.py +++ b/test/test_thorough_bids_example.py @@ -33,7 +33,8 @@ def load_collections(): reference_collection = Collection() reference_collection.load(tempdir+test_standard_name) - bids2openminds.converter.convert(test_dataset, save_output=True) + bids2openminds.converter.convert( + test_dataset, save_output=True, short_name=False) generated_collection = Collection() generated_collection.load(os.path.join( test_dataset, "openminds.jsonld"))