Skip to content

Commit

Permalink
Doc updates, events layout tweaks and fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelzwiers committed Oct 31, 2024
1 parent 18e1b8e commit 91cff8c
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 59 deletions.
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Features
* [x] Multi-echo data\*
* [x] Multi-coil data\*
* [x] Plug-ins
* [ ] Stimulus/behavioural logfiles
* [x] Stimulus/behavioural logfiles

``* = Only DICOM source data``

Expand Down
6 changes: 3 additions & 3 deletions bidscoin/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -820,9 +820,9 @@ def increment_runindex(self, outfolder: Path, bidsname: str, scans_table: pd.Dat
def eventsparser(self) -> EventsParser:
"""Returns a plugin EventsParser instance to parse the stimulus presentation logfile (if any)"""

plugins = [bcoin.import_plugin(plugin, (f"{self.dataformat}Events",)) for plugin in self.plugins]
if plugins and plugins[0]:
return getattr(plugins[0], f"{self.dataformat}Events")(self.provenance, self.events)
for name in self.plugins:
if plugin := bcoin.import_plugin(name, (f"{self.dataformat}Events",)):
return getattr(plugin, f"{self.dataformat}Events")(self.provenance, self.events)


class DataType:
Expand Down
3 changes: 1 addition & 2 deletions bidscoin/bidscoiner.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ def bidscoiner(sourcefolder: str, bidsfolder: str, participant: list=(), force:
return

# Load the data conversion plugins
plugins = [bcoin.import_plugin(plugin, ('bidscoiner_plugin',)) for plugin,options in bidsmap.plugins.items()]
plugins = [plugin for plugin in plugins if plugin] # Filter the empty items from the list
plugins = [plugin for name in bidsmap.plugins if (plugin := bcoin.import_plugin(name, ('bidscoiner_plugin',)))]
if not plugins:
LOGGER.warning(f"The plugins listed in your bidsmap['Options'] did not have a usable `bidscoiner_plugin` function, nothing to do")
LOGGER.info('-------------- FINISHED! ------------')
Expand Down
77 changes: 41 additions & 36 deletions bidscoin/bidseditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1093,15 +1093,18 @@ def __init__(self, runitem: RunItem, bidsmap: BidsMap, template_bidsmap: BidsMap
events_time.cellChanged.connect(self.events_time2run)
events_time.setToolTip(f"Columns: The number of time units per second + the column names that contain timing information (e.g. [10000, 'Time', 'Duration'])\n"
f"Start: The event that marks the beginning of the experiment, i.e. where the clock should be (re)set to 0 (e.g. 'Code=10' if '10' is used to log the pulses)")
events_rows_label = QLabel('Conditions')
events_rows_label = QLabel('Rows')
self.events_rows = events_rows = self.setup_table(events_data.get('rows',[]), 'events_rows')
events_rows.cellChanged.connect(self.events_rows2run)
events_rows.setToolTip(f"The groups of rows that are included in the output table")
events_rows.horizontalHeader().setVisible(True)
events_rows.setStyleSheet('QTableView::item {border-right: 1px solid #d6d9dc;}')
events_columns_label = QLabel('Columns')
self.events_columns = events_columns = self.setup_table(events_data.get('columns',[]), 'events_columns')
events_columns.cellChanged.connect(self.events_columns2run)
events_columns.setToolTip(f"The mappings of the included output columns. To add a new column, enter its mapping in the empty bottom row")
events_columns.horizontalHeader().setVisible(True)
events_columns.setStyleSheet('QTableView::item {border-right: 1px solid #d6d9dc;}')
log_table_label = QLabel('Log data')
log_table = self.setup_table(events_data.get('log_table',[]), 'log_table', minsize=False)
log_table.setShowGrid(True)
Expand Down Expand Up @@ -1156,10 +1159,10 @@ def __init__(self, runitem: RunItem, bidsmap: BidsMap, template_bidsmap: BidsMap
layout2_.setAlignment(arrow_, QtCore.Qt.AlignmentFlag.AlignHCenter)
layout2_.addWidget(events_columns_label)
layout2_.addWidget(events_columns)
layout2_.addWidget(events_time_label)
layout2_.addWidget(events_time)
layout2_.addWidget(events_rows_label)
layout2_.addWidget(events_rows)
layout2_.addWidget(events_time_label)
layout2_.addWidget(events_time)
layout2_.addStretch()
self.events_editbox = events_editbox = QGroupBox(' ')
events_editbox.setSizePolicy(sizepolicy)
Expand Down Expand Up @@ -1243,8 +1246,8 @@ def fill_table(self, table: MyQTable, data: list):

# Some ugly hacks to adjust individual tables
tablename = table.objectName()
header = tablename in ('log_table', 'events_table', 'events_rows')
extrarow = [[{'value': '', 'editable': True}, {'value': '', 'editable': True}]] if tablename in ('events_columns','meta') else []
header = tablename in ('log_table', 'events_table', 'events_rows', 'events_columns')
extrarow = [[{'value': '', 'editable': True}, {'value': '', 'editable': True}]] if tablename in ('events_rows','events_columns','meta') else []
ncols = len(data[0]) if data else 2 # Always at least two columns (i.e. key, value)

# Populate the blocked/hidden table
Expand Down Expand Up @@ -1412,20 +1415,15 @@ def run2data(self) -> tuple:
[{'value': 'start', 'editable': False}, {'value': runitem.events['time']['start'], 'editable': True}]]

# Set up the data for the events conditions / row groups
header = [{'value': '', 'editable': False}]
row_incl = [{'value': 'include', 'editable': False}]
row_cast = [{'value': 'cast', 'editable': False}]
for n, condition in enumerate(runitem.events.get('rows', [])):
header += [{'value': f"{n + 1}", 'editable': False}]
row_incl += [{'value': f"{dict(condition['include'])}", 'editable': True}]
row_cast += [{'value': f"{dict(condition.get('cast') or {})}", 'editable': True}]
header += [{'value': 'new', 'editable': False}] # = Extra column
events_data['rows'] = [header, row_incl, row_cast]
events_data['rows'] = [[{'value': 'condition', 'editable': False}, {'value': 'cast output', 'editable': False}]]
for condition in runitem.events.get('rows') or []:
events_data['rows'].append([{'value': f"{dict(condition['include'])}", 'editable': True}, {'value': f"{dict(condition.get('cast') or {})}", 'editable': True}])

# Set up the data for the events columns
events_data['columns'] = []
events_data['columns'] = [[{'value': 'input', 'editable': False}, {'value': 'output', 'editable': False}]]
for mapping in runitem.events.get('columns') or []:
events_data['columns'].append([{'value': mapping, 'editable': True}])
for key, value in mapping.items():
events_data['columns'].append([{'value': value, 'editable': True}, {'value': key, 'editable': True}])

# Set up the data for the events table
parser = runitem.eventsparser()
Expand Down Expand Up @@ -1619,19 +1617,23 @@ def events_rows2run(self, rowindex: int, colindex: int):

# row: [[include, {column_in: regex}],
# [cast, {column_out: newvalue}]]
mapping = self.events_rows.item(rowindex, colindex).text().strip()
mapping = self.events_rows.item(rowindex, colindex).text().strip() if self.events_rows.item(rowindex, colindex) else ''
nrows = self.events_rows.rowCount()

LOGGER.verbose(f"User sets events['rows'][{colindex+1}] to {mapping}' for {self.target_run}")
LOGGER.verbose(f"User sets events['rows'][{rowindex}] to {mapping}' for {self.target_run}")
if mapping:
ncols = self.events_rows.columnCount()
if colindex == ncols - 1:
self.target_run.events['rows'].append({'include' if rowindex==0 else 'cast': ast.literal_eval(mapping)})
try:
mapping = ast.literal_eval(mapping) # Convert stringified dict back to dict
except (ValueError, SyntaxError):
mapping = {}
if rowindex == nrows - 1:
self.target_run.events['rows'].append({'include' if colindex==0 else 'cast': mapping})
else:
self.target_run.events['rows'][colindex-1]['include' if rowindex==0 else 'cast'] = ast.literal_eval(mapping)
elif colindex <= len(self.target_run.events['rows']): # Remove the row
del self.target_run.events['rows'][colindex-1]
self.target_run.events['rows'][rowindex]['include' if colindex==0 else 'cast'] = mapping
elif colindex == 0 and rowindex < nrows - 1: # Remove the row
del self.target_run.events['rows'][rowindex]
else:
LOGGER.bcdebug(f"Cannot remove events['rows'][{colindex-1}] for {self.target_run}")
LOGGER.bcdebug(f"Cannot remove events['rows'][{rowindex}] for {self.target_run}")

# Refresh the events tables, i.e. delete empty rows or add a new row if a key is defined on the last row
_,_,_,_,events_data = self.run2data()
Expand All @@ -1644,16 +1646,17 @@ def events_columns2run(self, rowindex: int, colindex: int):
# events_data['columns'] = [[{'source1': target1}],
# [{'source2': target2}],
# [..]]
mapping = self.events_columns.item(rowindex, colindex).text().strip()
LOGGER.verbose(f"User sets the column name to: '{mapping}' for {self.target_run}")
if mapping: # Evaluate and store the data
nrows = self.events_columns.rowCount()
input = self.events_columns.item(rowindex, 0).text().strip() if self.events_columns.item(rowindex, 0) else ''
output = self.events_columns.item(rowindex, 1).text().strip() if self.events_columns.item(rowindex, 1) else ''
nrows = self.events_columns.rowCount()
LOGGER.verbose(f"User sets the column {colindex} to: '{input}: {output}' for {self.target_run}")
if input and output: # Evaluate and store the data
if rowindex == nrows - 1:
self.target_run.events['columns'].append(ast.literal_eval(mapping))
self.target_run.events['columns'].append({output: input})
self.events_columns.insertRow(nrows)
else:
self.target_run.events['columns'][rowindex] = ast.literal_eval(mapping)
else: # Remove the column
self.target_run.events['columns'][rowindex] = {output: input}
elif rowindex < nrows - 1: # Remove the row
del self.target_run.events['columns'][rowindex]
self.events_columns.blockSignals(True) # Not sure if this is needed?
self.events_columns.removeRow(rowindex)
Expand Down Expand Up @@ -1716,6 +1719,7 @@ def change_run(self, suffix_idx):
if val and key in self.target_run.bids and not self.target_run.bids[key]:
self.target_run.bids[key] = val
self.target_run.meta = template_run.meta.copy()
self.target_run.events = copy.deepcopy(template_run.events)

# Reset the edit window with the new target_run
self.reset(refresh=True)
Expand Down Expand Up @@ -1791,10 +1795,11 @@ def reset(self, refresh: bool=False):
self.fill_table(self.attributes_table, attributes_data)
self.fill_table(self.bids_table, bids_data)
self.fill_table(self.meta_table, meta_data)
self.fill_table(self.events_time, events_data['time'])
self.fill_table(self.events_rows, events_data['rows'])
self.fill_table(self.events_columns, events_data['columns'])
self.fill_table(self.events_table, events_data['table'])
if events_data:
self.fill_table(self.events_time, events_data['time'])
self.fill_table(self.events_rows, events_data['rows'])
self.fill_table(self.events_columns, events_data['columns'])
self.fill_table(self.events_table, events_data['table'])

# Refresh the BIDS output name
self.refresh_bidsname()
Expand Down
3 changes: 1 addition & 2 deletions bidscoin/bidsmapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,7 @@ def bidsmapper(sourcefolder: str, bidsfolder: str, bidsmap: str, template: str,
bidsmap_old = copy.deepcopy(bidsmap_new)

# Import the data scanning plugins
plugins = [bcoin.import_plugin(plugin, ('bidsmapper_plugin',)) for plugin in bidsmap_new.plugins]
plugins = [plugin for plugin in plugins if plugin] # Filter the empty items from the list
plugins = [plugin for name in bidsmap_new.plugins if (plugin := bcoin.import_plugin(name, ('bidsmapper_plugin',)))]
if not plugins:
LOGGER.warning(f"The plugins listed in your bidsmap['Options'] did not have a usable `bidsmapper_plugin` function, nothing to do")
LOGGER.info('-------------- FINISHED! ------------')
Expand Down
28 changes: 15 additions & 13 deletions bidscoin/heuristics/bidsmap_dccn.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Options:
anon: y # Set this anonymization flag to 'y' to round off age and discard acquisition date from the meta data
meta: [.json, .tsv, .tsv.gz] # The file extensions of the equally named metadata sourcefiles that are copied over to the BIDS sidecar files
fallback: y # Appends unhandled dcm2niix suffixes to the `acq` label if 'y' (recommended, else the suffix data is discarded)
events2bids:
meta: [.json, .tsv, .tsv.gz]


DICOM:
Expand Down Expand Up @@ -1009,10 +1011,10 @@ Presentation:
subject: <<filepath:/sub-(.*?)/>> # This filesystem property extracts the subject label from the source directory. NB: Any property or attribute can be used as subject-label, e.g. <PatientID>
session: <<filepath:/sub-.*?/ses-(.*?)/>> # This filesystem property extracts the subject label from the source directory. NB: Any property or attribute can be used as session-label, e.g. <StudyID>

beh: # ----------------------- All behavioural runs -------------------
beh: # ----------------------- All behavioural runs -------------------
- attributes: &presentationent_attr
Scenario:
bids: &presentationent_func # See: schema/rules/files/raw/func.yaml
bids: &presentationent_beh # See: schema/rules/files/raw/task.yaml
task: <Scenario>
acq:
run: <<>>
Expand Down Expand Up @@ -1040,34 +1042,34 @@ Presentation:
start:
Code: 10 # Code with which the first (or any) pulse is logged

eeg: # ----------------------- All EEG runs ---------------------------
eeg: # ----------------------- All EEG runs ---------------------------
- attributes: *presentationent_attr
bids: *presentationent_func
bids: *presentationent_beh
meta: *presentation_func_meta
events: *presentation_events

ieeg: # ----------------------- All iEEG runs --------------------------
- attributes: *presentationent_attr
bids: *presentationent_func
bids: *presentationent_beh
meta: *presentation_func_meta
events: *presentation_events

meg: # ----------------------- All MEG runs ---------------------------
meg: # ----------------------- All MEG runs ---------------------------
- attributes: *presentationent_attr
bids: *presentationent_func
bids: *presentationent_beh
meta: *presentation_func_meta
events: *presentation_events

nirs: # ----------------------- All nirs runs --------------------------
nirs: # ----------------------- All NIRS runs --------------------------
- attributes: *presentationent_attr
bids: *presentationent_func
bids: *presentationent_beh
meta: *presentation_func_meta
events: *presentation_events

func: # ----------------------- All functional runs --------------------
- attributes: *presentationent_attr
bids:
<<: *presentationent_func
<<: *presentationent_beh
ce:
dir:
rec:
Expand All @@ -1079,7 +1081,7 @@ Presentation:
<<: *presentationent_attr
Scenario: .*
bids:
<<: *presentationent_func
<<: *presentationent_beh
ce:
dir:
rec:
Expand All @@ -1088,13 +1090,13 @@ Presentation:

extra_data: # ----------------------- All extra data -------------------------
- attributes: *presentationent_attr
bids: *presentationent_func
bids: *presentationent_beh
meta: *presentation_func_meta
events: *presentation_events

exclude: # ----------------------- Data that will be left out -------------
- attributes: *presentationent_attr
bids: *presentationent_func
bids: *presentationent_beh
meta: *presentation_func_meta
events: *presentation_events

Expand Down
1 change: 0 additions & 1 deletion bidscoin/plugins/events2bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from bidscoin import bids
from bidscoin.bids import BidsMap, DataFormat, EventsParser, is_hidden, Plugin
# from convert_eprime.utils import remove_unicode
# from convert_eprime.tests.utils import get_test_data_path

LOGGER = logging.getLogger(__name__)

Expand Down
Loading

0 comments on commit 91cff8c

Please sign in to comment.