Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BIDS DSI-24, ET Data, and 1020 support #369

Merged
merged 12 commits into from
Dec 10, 2024
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Contributions

- BIDS
- Bundling support and refactor of `convert` module. See `demo_convert.py` #362
- Bundling support and refactor of `convert` module. See `demo_convert.py` #362 Add support for 1020 channels and eye tracker data #369
- Library Refactor
- Refactor `helpers` into `io` and `core` #362
- Dependencies
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ This a list of the major modules and their functionality. Each module will conta
- `feedback`: feedback mechanisms for sound and visual stimuli.
- `gui`: end-user interface into registered bci tasks and parameter editing. See BCInterface.py.
- `helpers`: helpful functions needed for interactions between modules and general utility.
- `io`: load, save, and convert data files. Ex. BrainVision, EDF, MNE, CSV, JSON, etc.
- `io`: load, save, and convert data files. Ex. BIDS, BrainVision, EDF, MNE, CSV, JSON, etc.
- `language`: gives probabilities of next symbols during typing.
- `main`: executor of experiments. Main entry point into the application
- `parameters`: location of json parameters. This includes parameters.json (main experiment / app configuration) and device.json (device registry and configuration).
Expand Down
5 changes: 4 additions & 1 deletion bcipy/core/demo/demo_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
# if no path is provided, prompt for one using a GUI
path = args.path
if not path:
path = load_experimental_data()
path = load_experimental_data(
message="Select the folder containing the raw_data.csv, parameters.json and triggers.txt",
strict=True
)

trial_window = (0, 1.0)

Expand Down
2 changes: 1 addition & 1 deletion bcipy/core/demo/demo_session_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def main(data_dir: str):
args = parser.parse_args()
path = args.path
if not path:
path = ask_directory()
path = ask_directory(strict=True)

if args.db or args.csv or args.charts:
session = read_session(Path(path, SESSION_DATA_FILENAME))
Expand Down
31 changes: 31 additions & 0 deletions bcipy/core/raw_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,3 +402,34 @@ def sample_data(rows: int = 1000,
data.append([timestamp] + channel_data + [trg])

return data


def get_1020_channels() -> List[str]:
"""Returns the standard 10-20 channel names.

Note: The 10-20 system is a standard for EEG electrode placement. The following is not a complete list of all
possible channels, but the most common ones used in BCI research. This excludes the reference and ground channels.

Returns
-------
list of channel names
"""
return [
'Fp1', 'Fp2', 'F7', 'F3', 'Fz', 'F4', 'F8', 'T3', 'C3', 'Cz', 'C4',
'T4', 'T5', 'P3', 'Pz', 'P4', 'T6', 'O1', 'O2'
]


def get_1020_channel_map(channels_name: List[str]) -> List[int]:
"""Returns a list of 1s and 0s indicating if the channel name is in the 10-20 system.

Parameters
----------
channels_name : list of channel names

Returns
-------
list of 1s and 0s indicating if the channel name is in the 10-20 system
"""
valid_channels = get_1020_channels()
return [1 if name in valid_channels else 0 for name in channels_name]
23 changes: 22 additions & 1 deletion bcipy/core/tests/test_raw_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@

from bcipy.exceptions import BciPyCoreException
from bcipy.core.raw_data import (RawData, RawDataReader, RawDataWriter,
load, sample_data, settings, write)
load, sample_data, settings, write,
get_1020_channel_map, get_1020_channels)


class TestRawData(unittest.TestCase):
Expand Down Expand Up @@ -377,5 +378,25 @@ def test_data_by_channel_map_applies_transformation(self):
verify(RawData, times=1).by_channel(transform)


class Test1020(unittest.TestCase):
"""Tests for 10-20 channel mapping functions."""

def test_get_1020_channels(self):
"""Tests that the 10-20 channel map is correctly generated."""
channels = get_1020_channels()
self.assertEqual(19, len(channels))
self.assertTrue(isinstance(channels[0], str))

def test_get_1020_channel_map(self):
"""Tests that the 10-20 channel map is correctly generated."""
# all but the last channel are valid 10-20 channels
channels = ['Fp1', 'Fp2', 'F3', 'F4', 'C3', 'C4', 'P3', 'P4', 'O1', 'invalid']
channel_map = get_1020_channel_map(channels)
self.assertEqual(10, len(channel_map))
self.assertEqual(0, channel_map[-1])
for i in range(len(channels) - 1):
self.assertEqual(1, channel_map[i])


if __name__ == '__main__':
unittest.main()
46 changes: 34 additions & 12 deletions bcipy/gui/file_dialog.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# pylint: disable=no-name-in-module,missing-docstring,too-few-public-methods
import sys
from pathlib import Path
from typing import Union

from PyQt6 import QtGui
from PyQt6.QtWidgets import QApplication, QFileDialog, QWidget

from bcipy.preferences import preferences
from bcipy.exceptions import BciPyCoreException

DEFAULT_FILE_TYPES = "All Files (*)"

Expand Down Expand Up @@ -61,18 +63,22 @@ def ask_directory(self, directory: str = "", prompt: str = "Select Directory") -
def ask_filename(
file_types: str = DEFAULT_FILE_TYPES,
directory: str = "",
prompt: str = "Select File") -> str:
"""Prompt for a file.
prompt: str = "Select File",
strict: bool = False) -> Union[str, BciPyCoreException]:
"""Prompt for a file using a GUI.

Parameters
----------
- file_types : optional file type filters; Examples: 'Text files (*.txt)'
or 'Image files (*.jpg *.gif)' or '*.csv;;*.pkl'
- directory : optional directory
- prompt : optional prompt message to display to users
- strict : optional flag to raise an exception if the user cancels the dialog. Default is False.
If False, an empty string is returned.

Returns
-------
path to file or None if the user cancelled the dialog.
path to file or raises an exception if the user cancels the dialog.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we always want an exception to be thrown on a dialog cancel. For instance, in GUI applications, a user might initially want to change a path, but then decide to stick with the original value. This would result in a lot of extra error handling.

What about using an additional parameter to specify the behavior? It could either be a boolean indicating whether or not an exception should be thrown (ex. ask_filename(..., strict=True)), or we could provide a default value like a dict.get (ex. ask_filename(..., default='')).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good point. In most of my use cases, this was the fallback GUI if no path was given. It's helpful to have an error when exiting without a choice in those cases. I'll use the boolean, as you suggest.

"""
app = QApplication(sys.argv)
dialog = FileDialog()
Expand All @@ -84,18 +90,29 @@ def ask_filename(
if filename and path.is_file():
preferences.last_directory = str(path.parent)

# Alternatively, we could use `app.closeAllWindows()`
app.quit()
# Alternatively, we could use `app.closeAllWindows()`
app.quit()

return filename

if strict:
raise BciPyCoreException('No file selected.')

return ''

return filename

def ask_directory(prompt: str = "Select Directory", strict: bool = False) -> Union[str, BciPyCoreException]:
"""Prompt for a directory using a GUI.

def ask_directory(prompt: str = "Select Directory") -> str:
"""Prompt for a directory.
Parameters
----------
prompt : optional prompt message to display to users
strict : optional flag to raise an exception if the user cancels the dialog. Default is False.
If False, an empty string is returned.

Returns
-------
path to directory or None if the user cancelled the dialog.
path to directory or raises an exception if the user cancels the dialog.
"""
app = QApplication(sys.argv)

Expand All @@ -107,7 +124,12 @@ def ask_directory(prompt: str = "Select Directory") -> str:
if name and Path(name).is_dir():
preferences.last_directory = name

# Alternatively, we could use `app.closeAllWindows()`
app.quit()
# Alternatively, we could use `app.closeAllWindows()`
app.quit()

return name

if strict:
raise BciPyCoreException('No directory selected.')

return name
return ''
5 changes: 4 additions & 1 deletion bcipy/helpers/demo/demo_visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@

path = args.path
if not path:
path = load_experimental_data()
path = load_experimental_data(
message="Select the folder containing the raw_data.csv, parameters.json and triggers.txt",
strict=True
)

parameters = load_json_parameters(f'{path}/{DEFAULT_PARAMETERS_FILENAME}',
value_cast=True)
Expand Down
2 changes: 1 addition & 1 deletion bcipy/helpers/offset.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ def extract_data_latency_calculation(
args = parser.parse_args()
data_path = args.data_path
if not data_path:
data_path = ask_directory()
data_path = ask_directory(prompt="Please select a BciPy time test directory..", strict=True)

# grab the stim length from the data directory parameters
stim_length = load_json_parameters(f'{data_path}/{DEFAULT_PARAMETERS_FILENAME}', value_cast=True)['stim_length']
Expand Down
6 changes: 3 additions & 3 deletions bcipy/io/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

The BciPy IO module contains functionality for loading and saving data in various formats. This includes the ability to convert data to BIDS format, load and save raw data, and load and save triggers.

- `convert`: functionality for converting the bcipy raw data output to other formats (currently, EDF)
- `load`: methods for loading most BciPy data. For loading of triggers, see triggers.py
- `save`: methods for saving BciPy data in supported formats. For saving of triggers, see triggers.py
- `convert`: functionality for converting the bcipy raw data output to other formats (currently, BrainVision and EDF), and for converting the raw data to BIDS format.
- `load`: methods for loading most BciPy data formats, including raw data and triggers.
- `save`: methods for saving BciPy data in supported formats.
Loading
Loading