class BidsExportRoutine(BaseStandaloneRoutine):
"""
This class provides methods for creating and managing BIDS datasets and their modality agnostic
files plus modality specific files.
"""
categories = ["BIDS"]
targets = ["PsychoPy"]
iconFile = Path(__file__).parent / "BIDS.png"
iconSVG = Path(__file__).parent / "BidsExportRoutine.svg"
tooltip = _translate(
"BIDS Export: Creates a standardized BIDS directory structure and generates "
"required metadata files according to BIDS specifications"
)
plugin = "psychopy-bids"
def __init__(self, exp, name="bidsExport"):
BaseStandaloneRoutine.__init__(self, exp, name=name)
self.exp.requireImport(
importName="BIDSHandler", importFrom="psychopy_bids.bids"
)
self.type = "BIDSexport"
self.params["name"].hint = _translate("Name of the Routine.")
self.params["name"].label = _translate("Routine Name")
hnt = _translate(
"Root name of the dataset (parent folder name) if this task is part of "
"a larger experiment"
)
self.params["dataset_name"] = Param(
"bids",
valType="str",
inputType="single",
categ="Basic",
allowedTypes=[],
canBePath=False,
hint=hnt,
label=_translate("Dataset Name"),
)
# license
hnt = _translate("License of the dataset")
self.params["bids_license"] = Param(
"",
valType="str",
inputType="choice",
categ="Basic",
allowedVals=[
"",
"CC0-1.0",
"CC-BY-4.0",
"CC-BY-SA-4.0",
"CC-BY-ND-4.0",
"CC-BY-NC-4.0",
"CC-BY-NC-SA-4.0",
"CC-BY-NC-ND-4.0",
"ODC-By-1.0",
"ODbL-1.0",
"PDDL-1.0",
],
hint=hnt,
label=_translate("Dataset License"),
)
hnt = _translate("BIDS defined data type")
self.params["data_type"] = Param(
"beh",
valType="str",
inputType="choice",
categ="Basic",
allowedVals=[
"beh",
"eeg",
"func",
"ieeg",
"nirs",
"meg",
"motion",
"mrs",
"pet",
],
hint=hnt,
label=_translate("Data Type"),
)
hnt = _translate(
"Optional label to distinguish between different acquisition parameters "
"or conditions across multiple runs of the same task"
)
self.params["acq"] = Param(
"",
valType="str",
inputType="single",
categ="Basic",
allowedVals=[],
canBePath=False,
hint=hnt,
label=_translate("Acquisition Label"),
)
hnt = _translate(
"Override the session label. Leave empty to use expInfo['session']."
)
self.params["session"] = Param(
"",
valType="str",
inputType="single",
categ="Basic",
allowedTypes=[],
canBePath=False,
hint=hnt,
label=_localized["session"],
)
# Params for dataset description
hnt = _translate(
"Path to a dataset_description.json. If not provided, "
"a default template will be used. Specifying a file will overwrite any "
"existing dataset_description.json"
)
self.params["dataset_description"] = Param(
"",
valType="str",
inputType="file",
allowedTypes=[],
categ="Basic",
updates="constant",
allowedUpdates=["constant"],
hint=hnt,
label=_translate("Dataset Description"),
)
# Params for json_sidecar
hnt = _translate(
"Path to the events/beh sidecar file. Accepts a complete .json sidecar or a 4-column spreadsheet "
"in CSV, TSV, or XLSX format (only for HED tags). "
"Spreadsheet files must adhere to the BIDS 4-column format. "
"If not specified, a default template is used. Existing sidecars will be updated during execution."
)
self.params["json_sidecar"] = Param(
"",
valType="str",
inputType="file",
allowedTypes=[],
categ="Basic",
updates="constant",
allowedUpdates=["constant"],
hint=hnt,
label=_translate("Events/Beh Sidecar"),
)
hnt = _translate(
"Generate preliminary HED metadata from the events file; may not fully comply with BIDS standards."
)
self.params["generate_hed_metadata"] = Param(
False,
valType="bool",
inputType="bool",
categ="Basic",
hint=hnt,
label=_translate("Generate HED Metadata (experimental)"),
)
hnt = _translate(
"Copy all stimulus files referenced in the events file to the BIDS dataset's /stimuli directory"
)
self.params["add_stimuli"] = Param(
True,
valType="bool",
inputType="bool",
categ="Basic",
hint=hnt,
label=_translate("Include Stimuli"),
)
hnt = _translate(
"Include source code, condition files, and a directory structure "
"file in the BIDS dataset's /code directory"
)
self.params["add_code"] = Param(
True,
valType="bool",
inputType="bool",
categ="Basic",
hint=hnt,
label=_translate("Include Source Code, Structure & Condition Files"),
)
hnt = _translate(
"Generate and include requirements.txt with Python and package versions in the BIDS dataset"
)
self.params["add_environment"] = Param(
True,
valType="bool",
inputType="bool",
categ="Basic",
hint=hnt,
label=_translate("Include Dependencies"),
)
hnt = _translate(
"Controls the run label in event filenames. "
"True = auto-increment (run-1, run-2, …); "
"False = omit run entity; "
"string (e.g. '4a') = use as a fixed run label (run-4a)."
)
self.params["runs"] = Param(
True,
valType="code",
inputType="single",
categ="Basic",
hint=hnt,
label=_translate("Run Label"),
)
hnt = _translate(
"Verbosity level for psychopy-bids messages during runtime. "
"DEBUG shows detailed internals, INFO shows normal progress, "
"WARN/ERROR show only important issues."
)
self.params["bids_log_level"] = Param(
"INFO",
valType="str",
inputType="choice",
categ="Basic",
allowedVals=["DEBUG", "INFO", "WARN", "ERROR"],
hint=hnt,
label=_localized["bids_log_level"],
)
# these inherited params are harmless but might as well trim:
for parameter in (
"startType",
"startVal",
"startEstim",
"stopVal",
"stopType",
"durationEstim",
"saveStartStop",
"syncScreenRefresh",
):
if parameter in self.params:
del self.params[parameter]
def writeStartCode(self, buff):
"""Write code at the beginning of the experiment."""
original_indent_level = buff.indentLevel
buff.writeIndentedLines(
"bidsLogLevel = 24\nlogging.addLevel('BIDS', bidsLogLevel)\n"
)
# Create the initial folder structure
# Session: use override if set, otherwise fall back to expInfo['session']
code = "_bids_session = %(session)s or expInfo.get('session') or None\n"
buff.writeIndentedLines(code % self.params)
code = "if _bids_session:\n"
buff.writeIndentedLines(code)
buff.setIndentLevel(1, relative=True)
code = (
"bids_handler = BIDSHandler(dataset=%(dataset_name)s,\n"
" subject=expInfo['participant'], task=expInfo['expName'],\n"
" session=_bids_session, data_type=%(data_type)s, acq=%(acq)s,\n"
" runs=%(runs)s, log_level=%(bids_log_level)s)\n"
)
buff.writeIndentedLines(code % self.params)
buff.setIndentLevel(-1, relative=True)
# Handle case where session is not provided
code = "else:\n"
buff.writeIndentedLines(code)
buff.setIndentLevel(1, relative=True)
code = (
"bids_handler = BIDSHandler(dataset=%(dataset_name)s,\n"
" subject=expInfo['participant'], task=expInfo['expName'],\n"
" data_type=%(data_type)s, acq=%(acq)s, runs=%(runs)s, log_level=%(bids_log_level)s)\n"
)
buff.writeIndentedLines(code % self.params)
buff.setIndentLevel(-1, relative=True)
# Initialize dataset and add license
code = "bids_handler.createDataset()\n"
if self.params["bids_license"] not in ["", None]:
code += "bids_handler.addLicense(%(bids_license)s, force=True)\n"
buff.writeIndentedLines(code % self.params)
# Add source code, condition files, and directory structure if enabled
if self.params["add_code"]:
code = "bids_handler.addTaskCode(force=True)\n"
code += "bids_handler.addConditionFiles(force=True)\n"
buff.writeIndentedLines(code)
# Add environment if enabled
if self.params["add_environment"]:
code = "bids_handler.addEnvironment()\n"
buff.writeIndentedLines(code)
# Add dataset description if provided
if self.params["dataset_description"].val not in ["", None]:
code = "bids_handler.addDatasetDescription(%(dataset_description)s, force=True)\n"
buff.writeIndentedLines(code % self.params)
buff.setIndentLevel(original_indent_level)
def writeExperimentEndCode(self, buff):
"""Write code at the end of the routine."""
original_indent_level = buff.indentLevel
code = "ignore_list = [\n"
buff.writeIndentedLines(code)
buff.setIndentLevel(1, relative=True)
code = (
"'participant',\n"
"'session',\n"
"'date',\n"
"'expName',\n"
"'psychopyVersion',\n"
"'OS',\n"
"'frameRate'\n"
)
buff.writeIndentedLines(code)
buff.setIndentLevel(-1, relative=True)
code = "]\nparticipant_info = {\n"
buff.writeIndentedLines(code)
buff.setIndentLevel(1, relative=True)
code = (
"key: thisExp.extraInfo[key]\n"
"for key in thisExp.extraInfo\n"
"if key not in ignore_list\n"
)
buff.writeIndentedLines(code)
buff.setIndentLevel(-1, relative=True)
code = "}\n# write tsv file and update\ntry:\n"
buff.writeIndentedLines(code)
buff.setIndentLevel(1, relative=True)
code = "if bids_handler.events:\n"
buff.writeIndentedLines(code)
buff.setIndentLevel(1, relative=True)
if self.params["json_sidecar"] == "":
self.params["json_sidecar"] = True
code = "bids_handler.writeEvents(participant_info, add_stimuli=%(add_stimuli)s, execute_sidecar=%(json_sidecar)s, generate_hed_metadata=%(generate_hed_metadata)s)\n"
buff.writeIndentedLines(code % self.params)
buff.setIndentLevel(-2, relative=True)
code = "except Exception as e:\n"
buff.writeIndentedLines(code)
buff.setIndentLevel(1, relative=True)
code = 'print(f"[psychopy-bids(settings)] An error occurred when writing BIDS events: {e}")\n'
buff.writeIndentedLines(code)
buff.setIndentLevel(-1, relative=True)
# Add directory structure if add_code is enabled
if self.params["add_code"]:
code = "bids_handler.addDirectoryStructure(force=True)\n"
buff.writeIndentedLines(code)
buff.setIndentLevel(original_indent_level)