Skip to content
Snippets Groups Projects
Commit d3b9fa21 authored by Toumji Abdallah's avatar Toumji Abdallah
Browse files

Implemented generic validation method for tsv and regex match in the generic validation method

Small refactor and start of tests
parent 2bd30b87
No related branches found
No related tags found
No related merge requests found
import json
import os
import tkinter as tk
from tkinter import ttk
import mne
import BIDSHandler.MainApplication as MainApplication
import BIDSHandler.Tools as Tools
......@@ -33,8 +35,8 @@ class Application(tk.Frame):
self.main_frame = tk.Frame(self.root)
self.main_frame.pack(anchor='center')
# Temporary list, to transform into json later
self.possible_forms = ['dataset_description.json', 'participants.tsv', 'participants.json', 'README', 'scans.tsv']
self.possible_forms = json.load(open(os.path.join(os.path.dirname(__file__), 'res/BaseFile.json')))
self.extension_to_data_type = {'edf': 'eeg'}
self.extension_to_possible_sidecar = {'edf': ['channels.tsv', 'events.tsv', 'electrodes.tsv', 'coordsystem.json', 'eeg.json']}
self.actual_step = 0
......@@ -85,9 +87,12 @@ class Application(tk.Frame):
self.checkbox_vars = []
for form in self.possible_forms:
for form, requirement in self.possible_forms.items():
self.checkbox_vars.append(tk.IntVar())
checkbox = tk.Checkbutton(checkbox_frame, text=form, variable=self.checkbox_vars[-1])
if requirement == 'required':
checkbox.select()
checkbox.config(state='disabled')
checkbox.pack()
......@@ -115,7 +120,7 @@ class Application(tk.Frame):
for i, var in enumerate(self.checkbox_vars):
if var.get() == 1:
self.selected_forms.append(self.possible_forms[i])
self.selected_forms.append(list(self.possible_forms.keys())[i])
self.project_name = self.name_entry.get()
......@@ -560,6 +565,7 @@ class Application(tk.Frame):
The user is guided through the process of selecting the experiments to apply the sidecar to.
'''
if not create:
self.sidecar_type_dropdown.grid_forget()
self.dropdown_label.grid_forget()
......@@ -567,6 +573,7 @@ class Application(tk.Frame):
self.integrate_sidecar_window.lift()
self.integrate_sidecar_label.config(text='File selected: ' + self.sidecar_file + '\nType: ' + self.sidecar_type.get() + '\nExperiments selected: ' + str(self.experiments_files))
# To Do : add buttons to select all the experiments of a subject or a session etc.
self.add_experiment_button = tk.Button(self.integrate_sidecar_window, text='Add experiment', command= lambda : self.select_experiments(create))
self.add_experiment_button.grid(row=2, column=0)
......@@ -614,7 +621,7 @@ class Application(tk.Frame):
concerned_files = []
for sub in entities.keys():
for ses in entities[sub].keys():
concerned_files.append(entities[sub][ses])
concerned_files.extend(entities[sub][ses])
path = self.find_shared_entities(concerned_files)
if path == '':
no_shared = True
......@@ -628,7 +635,7 @@ class Application(tk.Frame):
first_if = True
concerned_files = []
for ses in entities[sub].keys():
concerned_files.append(entities[sub][ses])
concerned_files.extend(entities[sub][ses])
path = self.find_shared_entities(concerned_files)
if path == '':
no_shared = True
......@@ -637,9 +644,10 @@ class Application(tk.Frame):
if no_shared or not first_if:
for ses in entities[sub].keys():
concerned_files = []
concerned_files.append(entities[sub][ses])
concerned_files.extend(entities[sub][ses])
path = self.find_shared_entities(concerned_files)
path_list.append(self.project_directory + '/' + sub + '/' + ses + '/' + path + sidecar_type)
# To Do automate the selection of the data type folder
path_list.append(self.project_directory + '/' + sub + '/' + ses + '/eeg/' + path + sidecar_type)
return path_list
def get_entities(self, experiments_files):
......@@ -665,11 +673,13 @@ class Application(tk.Frame):
elif 'ses-' in info:
ses = info
if ses not in entities[sub]:
entities[sub][ses] = {}
entities[sub][ses] = []
file_entities = {}
entity_list = directory[-1].split('_')
for info in entity_list[:-1]:
entities[sub][ses][info.split('-')[0]] = info.split('-')[1]
file_entities[info.split('-')[0]] = info.split('-')[1]
entities[sub][ses].append(file_entities)
return entities
def find_shared_entities(self, entities):
......@@ -727,3 +737,13 @@ class Application(tk.Frame):
shutil.rmtree(self.project_directory + '/' + 'res')
self.integrate_sidecar_label.config(text='Sidecar placed')
def extract_metadata(self, file_path):
''' This method is used to extract the metadata of a file.
The metadata of a file is extracted.
:param file_path: str
The path of the file.
:return: dict
A dict containing the metadata of the file.
'''
return mne.io.read_raw_edf(file_path).info
......@@ -48,16 +48,16 @@ class Form:
if column not in self.table.model.df.columns:
self.validation_label.config(image=self.invalid_icon)
return False
# if 'valid condition' in self.form[1][column]:
# for i, cell in enumerate(self.table.model.df[column]):
# is_null = pd.isnull(cell)
# if not is_null and not cell == '':
# print(cell)
# if not EntryManager.text_to_condition(self.form[1][column]['valid condition'], cell):
# self.validation_label.config(image=self.invalid_icon)
# # If the condition is not met, create a popup to inform the user.
# mb.showinfo('Invalid condition', 'The condition for the column ' + column + ' is not met.\n' + 'The condition is: ' + self.form[1][column]['valid condition'] + '\n' + 'The value is: ' + cell)
# return False
if column in self.table.model.df.columns and 'valid condition' in self.form[1][column]:
for i, cell in enumerate(self.table.model.df[column]):
is_null = pd.isnull(cell)
if not is_null and not cell == '':
print(cell)
if not EntryManager.text_to_condition(self.form[1][column]['valid condition'], cell):
self.validation_label.config(image=self.invalid_icon)
# If the condition is not met, create a popup to inform the user.
mb.showinfo('Invalid condition', 'The condition for the column ' + column + ' is not met.\n' + 'The condition is: ' + self.form[1][column]['valid condition'] + '\n' + 'The value is: ' + cell)
return False
# Drop the empty column.
if '' in self.table.model.df.columns:
self.table.model.df.drop('', axis=1, inplace=True)
......@@ -88,7 +88,7 @@ class Form:
for entry in self.entries:
if not entry.get_text() == '' and not entry.get_text() == [''] and not entry.get_text() == {''}:
data[entry.name] = entry.get_text()
with open(os.path.join(path, self.form[0] + '.json'), 'w') as file:
with open(os.path.join(path, self.form[0]), 'w') as file:
json.dump(data, file)
def create_form_tsv(self):
......@@ -183,7 +183,7 @@ class FormHandler:
new_form = (self.form_frame_list[index].form[0].replace('.tsv', '.json'), {})
for column in self.form_frame_list[index].table.model.df.columns:
new_form[1][column] = {'description': 'Describe what this field correspond to', 'requirement level': 'recommended', 'multiple': False}
new_form[1][column] = {'description': 'Describe what this field correspond to', 'requirement level': 'recommended', 'multiple': False, 'valid condition': 'len(value) > 0'}
# Then replace the form in the form_frame_list with the new form.
self.form_frame_list[i] = Form(new_form, self.frame, self.help_icon, self.invalid_icon, self.valid_icon)
......
{
"dataset_description.json": "required",
"participants.tsv": "required",
"participants.json": "recommended",
"README": "recommended"
}
\ No newline at end of file
......@@ -10,13 +10,13 @@
"requirement level" : "required",
"multiple" : false,
"description" : "The version of the BIDS standard that was used",
"valid condition": "float(value) > 0"
"valid condition": "len(value) > 0 ; float(value) > 0"
},
"HEDVersion" : {
"requirement level": "recommended",
"multiple": true,
"multiple": false,
"description": "If HED tags are used: The version of the HED schema used to validate HED tags for study. May include a single schema or a base schema and one or more library schema.",
"valid condition": "float(value) > 0"
"valid condition": "bool(re.fullmatch(r'^(?:[a-zA-Z]+:)?(?:[a-zA-Z]+_)?(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-(?:(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+(?:[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$', value))"
},
"DatasetLinks" : {
"requirement level": "optional",
......@@ -93,7 +93,7 @@
"requirement level" : "recommended",
"multiple" : false,
"description" : "Version of the pipeline.",
"valid condition": "float(value) > 0"
"valid condition": "len(value) > 0 ; float(value) > 0"
},
"Description" : {
"requirement level" : "optional",
......@@ -126,12 +126,12 @@
"participant_id" : {
"requirement level" : "required",
"description" : "Unique participant identifier",
"valid condition": "re.match(r\"sub-[a-zA-Z0-9]+\", value) == True"
"valid condition": "bool(re.fullmatch(r'sub-.*', value))"
},
"age" : {
"requirement level": "recommended",
"description": "Age of the participant at time of testing",
"valid condition": "float(value) > 0; float(value) < 120"
"valid condition": "len(value) > 0 ; float(value) > 0 ; float(value) < 120"
},
"handedness" : {
"requirement level": "recommended",
......
......@@ -43,22 +43,17 @@
"notch": {
"requirement level": "optional",
"description": "Frequencies used for the notch filter applied to the channel, in Hz. If notch filters are applied at multiple frequencies, these frequencies MAY be specified as a list, for example, [60, 120, 180]. If no notch filter was applied, use n/a.",
"valid condition": "value == 'n/a' or float(value) > 0",
"multiple": true
"valid condition": "value == 'n/a' or float(value) > 0"
},
"status": {
"requirement level": "optional",
"description": "Frequencies used for the notch filter applied to the channel, in Hz. If notch filters are applied at multiple frequencies, these frequencies MAY be specified as a list, for example, [60, 120, 180]. If no notch filter was applied, use n/a.",
"valid condition": "value in ['good', 'bad', 'n/a']",
"multiple": false
"valid condition": "value in ['good', 'bad', 'n/a']"
},
"status_description": {
"requirement level": "optional",
"description": "Freeform text description of noise or artifact affecting data quality on the channel. It is meant to explain why the channel was declared bad in the status column.",
"valid condition": "len(value) > 0",
"multiple": false
"valid condition": "len(value) > 0"
}
}
}
\ No newline at end of file
import unittest
import BIDSHandler.CreationTool.Application as Application
import tkinter as tk
class MyTestCase(unittest.TestCase):
def setUp(self):
self.app = Application.Application(root=tk.Tk())
self.app.project_directory = 'test'
def test_get_entities(self):
# Test the get_entities method of the Application class.
files = ['test/sub-01/ses-01/sub-01_ses-01_task-rest_run-01_eeg.edf',
'test/sub-01/ses-01/sub-01_ses-01_task-rest_run-02_eeg.edf',
'test/sub-01/ses-02/sub-01_ses-02_task-rest_run-01_eeg.edf',
'test/sub-01/ses-02/sub-01_ses-02_task-work_run-01_eeg.edf',
'test/sub-02/ses-01/sub-02_ses-01_task-rest_run-01_eeg.edf',
'test/sub-02/ses-01/sub-02_ses-01_task-rest_run-02_eeg.edf',
'test/sub-02/ses-02/sub-02_ses-02_task-rest_run-01_eeg.edf',
'test/sub-02/ses-02/sub-02_ses-02_task-work_run-01_eeg.edf']
entities = self.app.get_entities(files)
self.assertEqual({'sub-01': {'ses-01': [{'sub': '01', 'ses': '01', 'task': 'rest', 'run': '01'},
{'sub': '01', 'ses': '01', 'task': 'rest', 'run': '02'}],
'ses-02': [{'sub': '01', 'ses': '02', 'task': 'rest', 'run': '01'},
{'sub': '01', 'ses': '02', 'task': 'work', 'run': '01'}]},
'sub-02': {'ses-01': [{'sub': '02', 'ses': '01', 'task': 'rest', 'run': '01'},
{'sub': '02', 'ses': '01', 'task': 'rest', 'run': '02'}],
'ses-02': [{'sub': '02', 'ses': '02', 'task': 'rest', 'run': '01'},
{'sub': '02', 'ses': '02', 'task': 'work', 'run': '01'}]}}, entities)
def test_find_shared_entities(self):
# Test the get_entities method of the Application class.
files = ['test/sub-01/ses-01/sub-01_ses-01_task-rest_run-01_eeg.edf',
'test/sub-01/ses-01/sub-01_ses-01_task-rest_run-02_eeg.edf',
'test/sub-01/ses-02/sub-01_ses-02_task-rest_run-01_eeg.edf',
'test/sub-01/ses-02/sub-01_ses-02_task-work_run-01_eeg.edf',
'test/sub-02/ses-01/sub-02_ses-01_task-rest_run-01_eeg.edf',
'test/sub-02/ses-01/sub-02_ses-01_task-rest_run-02_eeg.edf',
'test/sub-02/ses-02/sub-02_ses-02_task-rest_run-01_eeg.edf',
'test/sub-02/ses-02/sub-02_ses-02_task-work_run-01_eeg.edf']
entities = self.app.get_entities(files)
files_entities = []
for sub in entities.keys():
for ses in entities[sub].keys():
files_entities.extend(entities[sub][ses])
shared_entities = self.app.find_shared_entities(files_entities)
self.assertEqual('', shared_entities)
sub_files = ['test/sub-01/ses-01/sub-01_ses-01_task-rest_run-01_eeg.edf',
'test/sub-01/ses-01/sub-01_ses-01_task-rest_run-02_eeg.edf',
'test/sub-01/ses-02/sub-01_ses-02_task-rest_run-01_eeg.edf']
entities = self.app.get_entities(sub_files)
files_entities = []
for sub in entities.keys():
for ses in entities[sub].keys():
files_entities.extend(entities[sub][ses])
shared_entities = self.app.find_shared_entities(files_entities)
self.assertEqual('sub-01_task-rest_', shared_entities)
def test_get_optimal_files(self):
# Test the get_optimal_files method of the Application class.
self.app.number_of_subjects = 2
self.app.number_of_sessions = 2
files = ['test/sub-01/ses-01/sub-01_ses-01_task-rest_run-01_eeg.edf',
'test/sub-01/ses-01/sub-01_ses-01_task-rest_run-02_eeg.edf',
'test/sub-01/ses-02/sub-01_ses-02_task-rest_run-01_eeg.edf',
'test/sub-01/ses-02/sub-01_ses-02_task-work_run-01_eeg.edf',
'test/sub-02/ses-01/sub-02_ses-01_task-rest_run-01_eeg.edf',
'test/sub-02/ses-01/sub-02_ses-01_task-rest_run-02_eeg.edf',
'test/sub-02/ses-02/sub-02_ses-02_task-rest_run-01_eeg.edf',
'test/sub-02/ses-02/sub-02_ses-02_task-work_run-01_eeg.edf']
optimal_files = self.app.get_optimal_files(files, 'channels.tsv')
print(optimal_files)
if __name__ == '__main__':
unittest.main()
......@@ -41,6 +41,9 @@ class Application(tk.Frame):
creation_tool_button = tk.Button(self.main_frame, text='Creation Tool', command=self.creation_tool)
creation_tool_button.pack()
validation_button = tk.Button(self.main_frame, text='Validate BIDS', command=self.validate_bids)
validation_button.pack()
def viewing_tool(self):
''' This method is used to show the viewing tool.
'''
......@@ -56,3 +59,9 @@ class Application(tk.Frame):
creation_tool = CreationTool.Application(self.root)
creation_tool.pack()
def validate_bids(self):
''' This method is used to open a web browser at https://bids-standard.github.io/bids-validator/ to validate a BIDS project.
'''
import webbrowser
webbrowser.open('https://bids-standard.github.io/bids-validator/')
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment