diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..403ab88 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +name: Tests + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10"] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Install package + run: | + pip install -e . + + - name: Run tests + run: | + pytest tests/ -v + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a8c2003 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:conda", + "python-envs.defaultPackageManager": "ms-python.python:conda", + "python-envs.pythonProjects": [] +} \ No newline at end of file diff --git a/LFPAnalysis/lfp_preprocess_utils.py b/LFPAnalysis/lfp_preprocess_utils.py index 74279e7..7de930e 100644 --- a/LFPAnalysis/lfp_preprocess_utils.py +++ b/LFPAnalysis/lfp_preprocess_utils.py @@ -1573,15 +1573,6 @@ def load_elec(elec_path=None, site='MSSM'): elec_data['manual'][elec_data['Notes'].str.lower().str.contains('cyst', na=False)] = 'oob' elec_data['manual'][elec_data['Notes'].str.lower().str.contains('bad', na=False)] = 'oob' - - - - - - - - - return elec_data def make_mne_scalp(load_path=None, overwrite=True, return_data=False): @@ -1605,17 +1596,6 @@ def make_mne_scalp(load_path=None, overwrite=True, return_data=False): whether to overwrite existing data for this person if it exists return_data: bool whether to actually return the data or just save it in the directory - eeg_names : list - list of channel names that pertain to scalp EEG in case the hardcoded options don't work - resp_names : list - list of channel names that pertain to respiration in case the hardcoded options don't work - ekg_names : list - list of channel names that pertain to the EKG in case the hardcoded options don't work - sync_name : str - provide the sync name in case the hardcoded options don't work - sync_type : str - what type of sync signal was used? options: ['photodiode', 'audio', 'ttl'] - Returns ------- mne_data : mne object @@ -2515,7 +2495,7 @@ def compute_and_baseline_tfr(baseline_event, task_events, freqs, n_cycles, load_ iteration +=1 - zpow = mne.time_frequency.EpochsTFR(event_epochs_reref.info, baseline_corrected_power, + zpow = mne.time_frequency.EpochsTFRArray(event_epochs_reref.info, baseline_corrected_power, temp_pow.times, freqs) zpow.metadata = event_epochs_reref.metadata diff --git a/LFPAnalysisBook/module-01-00_BroadIntroduction.md b/LFPAnalysisBook/module-01-00_BroadIntroduction.md index 8063003..8536f2a 100644 --- a/LFPAnalysisBook/module-01-00_BroadIntroduction.md +++ b/LFPAnalysisBook/module-01-00_BroadIntroduction.md @@ -12,14 +12,37 @@ messing around with the code contained herein. [The origin of extracellular fields and currents — EEG, ECoG, LFP and spikes](https://www.nature.com/articles/nrn3241) - [Advances in human intracranial electroencephalography research, guidelines and good practices](https://pubmed.ncbi.nlm.nih.gov/35792291/) [A tutorial review of functional connectivity analysis methods and their interpretational pitfalls](https://www.frontiersin.org/journals/systems-neuroscience/articles/10.3389/fnsys.2015.00175/full) ## Video resources: -[Mike X. Cohen describes time-frequency analyses in detail](https://www.youtube.com/watch?v=7ahrcB5HL0k&list=PLn0OLiymPak2BYu--bR0ADNBJsC4kuRWs) + +## Introductory Videos + +[Origin of EEG](https://youtu.be/Bmt89hHyxuM?si=fwcztI_cjwQ4fbyD) + +[Preprocessing basics](https://youtu.be/JMB9nZNGVyk?si=mg-VddKF-ZqNq6WH) + +[Time-domain (ERP)](https://youtu.be/iFWrVzLYop0?si=jgSZk5UQguJGt9PR) + +[Rhythm-based analyses](https://youtu.be/3hk4z3yrMzk?si=stSIsMaZH9vW3Ipg) + +[Interpreting time-frequency plots](https://youtu.be/s2MfmIx8wv4?si=7th8mEBbga4a0bE5) + +## Basic coding practice: + +[Python Novice Inflammation (Software Carpentry)](https://swcarpentry.github.io/python-novice-inflammation/) + +[Python Novice Gapminder (Software Carpentry)](https://swcarpentry.github.io/python-novice-gapminder/) + +[Case Studies in Python (Mark Kramer)](https://mark-kramer.github.io/Case-Studies-Python/01.html) + +## Statistics resources (not Python): + +[Introduction to Statistics (Statsthinking21)](https://statsthinking21.github.io/statsthinking21-core-site/) + ## Code resources: @@ -29,6 +52,8 @@ The material in this JupyterBook will be most easily consumed if you have alread [Writing good research code](https://goodresearch.dev/) +[More advanced practice for data analysis](https://neuraldatascience.io/intro.html) + +**Note:** You'll need to install the package itself after creating the conda environment using `pip install -e .` from the repository directory. ## Updating -To update the code to reflect changes in the repository: +### If installed via pip: + +```bash +pip install --upgrade --force-reinstall git+https://github.com/seqasim/LFPAnalysis.git ``` -cd path_to_install + +### If installed from source (git clone): + +```bash +cd path_to_install/LFPAnalysis git pull +pip install -e . # Reinstall to pick up changes +``` + +## Testing + +To run the test suite, first make sure you have the package installed and pytest available: + +```bash +pip install pytest +pytest tests/ ``` - +Or run with more verbose output: + +```bash +pytest tests/ -v +``` ## Where to start? diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..880d33c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,23 @@ +[pytest] +# Pytest configuration file + +# Test discovery patterns +python_files = test_*.py +python_classes = Test* +python_functions = test_* + +# Test paths +testpaths = tests + +# Output options +addopts = + -v + --tb=short + --strict-markers + +# Markers for categorizing tests +markers = + slow: marks tests as slow (deselect with '-m "not slow"') + integration: marks tests as integration tests + unit: marks tests as unit tests + diff --git a/requirements.txt b/requirements.txt index 5609e25..38aabd9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,24 @@ - mne - seaborn - neurodsp==2.2.0 - ipykernel - openpyxl - ipython - ipywidgets - ipyevents - fooof==1.0.0 - dcor - llvmlite - pyyaml - sparse - tabulate - statsmodels - levenshtein - pycatch22 - wheel - notebook - h5io - numba - tensorpac - nibabel +mne +seaborn +neurodsp==2.2.0 +ipykernel +openpyxl +ipython +ipywidgets +ipyevents +fooof==1.0.0 +dcor +llvmlite +pyyaml +sparse +tabulate +statsmodels +levenshtein +pycatch22 +wheel +notebook +h5io +numba +tensorpac +nibabel +pytest diff --git a/setup.py b/setup.py index 86347ba..4be0d7f 100644 --- a/setup.py +++ b/setup.py @@ -1,35 +1,46 @@ from setuptools import find_packages, setup -import requests # Get the repository owner and name from the GitHub URL github_url = 'https://github.com/seqasim/LFPAnalysis' -owner, repo = github_url.split('/')[-2:] - -# Get the list of contributors from the GitHub API -response = requests.get(f'https://api.github.com/repos/{owner}/{repo}/contributors') -contributors = response.json() - -# Create a list of author strings in the format "Name " -authors = [f"{c['login']}" for c in contributors] # Get long description -with open("README.md", "r") as fh: - __long_description__ = fh.read() +try: + with open("README.md", "r", encoding='utf-8') as fh: + __long_description__ = fh.read() +except FileNotFoundError: + __long_description__ = 'Package to process LFP data' # Get requirements -with open('requirements.txt') as f: - required = f.read().splitlines() +try: + with open('requirements.txt', 'r', encoding='utf-8') as f: + required = [line.strip() for line in f.read().splitlines() + if line.strip() and not line.strip().startswith('#')] +except FileNotFoundError: + required = [] setup( name='LFPAnalysis', version='1.0.0', description='Package to process LFP data', + long_description=__long_description__, + long_description_content_type='text/markdown', url=github_url, - author=', '.join(authors), + author='Salman Qasim', + author_email='', # Add email if desired packages=find_packages(), - package_data={'': ['data/*']}, + package_data={'LFPAnalysis': ['YBA_ROI_labelled.xlsx']}, include_package_data=True, install_requires=required, + python_requires='>=3.8', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Science/Research', + 'Topic :: Scientific/Engineering', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + ], ) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..b063fb3 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# Tests for LFPAnalysis package + diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..eb809e5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,10 @@ +""" +Pytest configuration and shared fixtures for LFPAnalysis tests. +""" +import pytest +import sys +from pathlib import Path + +# Add the parent directory to the path so we can import LFPAnalysis +sys.path.insert(0, str(Path(__file__).parent.parent)) + diff --git a/tests/test_analysis_utils.py b/tests/test_analysis_utils.py new file mode 100644 index 0000000..6507d7e --- /dev/null +++ b/tests/test_analysis_utils.py @@ -0,0 +1,15 @@ +""" +Basic tests for analysis_utils module. +""" +import pytest +import numpy as np +import pandas as pd +from LFPAnalysis import analysis_utils + + +def test_module_imports(): + """Test that analysis_utils module can be imported and has expected structure.""" + # Just verify the module exists and can be accessed + assert hasattr(analysis_utils, '__name__') + assert analysis_utils.__name__ == 'LFPAnalysis.analysis_utils' + diff --git a/tests/test_imports.py b/tests/test_imports.py new file mode 100644 index 0000000..4082e14 --- /dev/null +++ b/tests/test_imports.py @@ -0,0 +1,47 @@ +""" +Basic import tests to verify all modules can be imported successfully. +""" +import pytest + + +def test_import_analysis_utils(): + """Test that analysis_utils can be imported.""" + import LFPAnalysis.analysis_utils + assert LFPAnalysis.analysis_utils is not None + + +def test_import_lfp_preprocess_utils(): + """Test that lfp_preprocess_utils can be imported.""" + import LFPAnalysis.lfp_preprocess_utils + assert LFPAnalysis.lfp_preprocess_utils is not None + + +def test_import_oscillation_utils(): + """Test that oscillation_utils can be imported.""" + import LFPAnalysis.oscillation_utils + assert LFPAnalysis.oscillation_utils is not None + + +def test_import_statistics_utils(): + """Test that statistics_utils can be imported.""" + import LFPAnalysis.statistics_utils + assert LFPAnalysis.statistics_utils is not None + + +def test_import_sync_utils(): + """Test that sync_utils can be imported.""" + import LFPAnalysis.sync_utils + assert LFPAnalysis.sync_utils is not None + + +def test_import_nlx_utils(): + """Test that nlx_utils can be imported.""" + import LFPAnalysis.nlx_utils + assert LFPAnalysis.nlx_utils is not None + + +def test_import_iowa_utils(): + """Test that iowa_utils can be imported.""" + import LFPAnalysis.iowa_utils + assert LFPAnalysis.iowa_utils is not None + diff --git a/tests/test_iowa_utils.py b/tests/test_iowa_utils.py new file mode 100644 index 0000000..8067040 --- /dev/null +++ b/tests/test_iowa_utils.py @@ -0,0 +1,12 @@ +""" +Basic tests for iowa_utils module. +""" +import pytest +from LFPAnalysis import iowa_utils + + +def test_module_imports(): + """Test that iowa_utils module can be imported.""" + assert hasattr(iowa_utils, '__name__') + assert iowa_utils.__name__ == 'LFPAnalysis.iowa_utils' + diff --git a/tests/test_lfp_preprocess_utils.py b/tests/test_lfp_preprocess_utils.py new file mode 100644 index 0000000..3faa271 --- /dev/null +++ b/tests/test_lfp_preprocess_utils.py @@ -0,0 +1,43 @@ +""" +Basic tests for lfp_preprocess_utils module. +""" +import pytest +import numpy as np +from LFPAnalysis import lfp_preprocess_utils + + +def test_mean_baseline_time_zscore(): + """Test mean_baseline_time function with zscore mode.""" + # Create simple test data + data = np.random.randn(2, 100) # 2 channels, 100 time points + baseline = np.random.randn(2, 50) # 2 channels, 50 baseline time points + + result = lfp_preprocess_utils.mean_baseline_time(data, baseline, mode='zscore') + + # Check that output has correct shape + assert result.shape == data.shape + # Check that it's a numpy array + assert isinstance(result, np.ndarray) + + +def test_mean_baseline_time_mean(): + """Test mean_baseline_time function with mean mode.""" + data = np.random.randn(2, 100) + baseline = np.random.randn(2, 50) + + result = lfp_preprocess_utils.mean_baseline_time(data, baseline, mode='mean') + + assert result.shape == data.shape + assert isinstance(result, np.ndarray) + + +def test_mean_baseline_time_ratio(): + """Test mean_baseline_time function with ratio mode.""" + data = np.abs(np.random.randn(2, 100)) + 0.1 # Ensure positive values + baseline = np.abs(np.random.randn(2, 50)) + 0.1 + + result = lfp_preprocess_utils.mean_baseline_time(data, baseline, mode='ratio') + + assert result.shape == data.shape + assert isinstance(result, np.ndarray) + diff --git a/tests/test_nlx_utils.py b/tests/test_nlx_utils.py new file mode 100644 index 0000000..e3fc187 --- /dev/null +++ b/tests/test_nlx_utils.py @@ -0,0 +1,12 @@ +""" +Basic tests for nlx_utils module. +""" +import pytest +from LFPAnalysis import nlx_utils + + +def test_module_imports(): + """Test that nlx_utils module can be imported.""" + assert hasattr(nlx_utils, '__name__') + assert nlx_utils.__name__ == 'LFPAnalysis.nlx_utils' + diff --git a/tests/test_oscillation_utils.py b/tests/test_oscillation_utils.py new file mode 100644 index 0000000..aa0aa28 --- /dev/null +++ b/tests/test_oscillation_utils.py @@ -0,0 +1,45 @@ +""" +Basic tests for oscillation_utils module. +""" +import pytest +import numpy as np +from LFPAnalysis import oscillation_utils + + +def test_find_nearest_value(): + """Test find_nearest_value function.""" + array = np.array([1.0, 2.5, 3.7, 5.2, 6.8]) + value = 3.0 + + nearest_val, idx = oscillation_utils.find_nearest_value(array, value) + + # Should find 2.5 or 3.7 as nearest + assert nearest_val in array + assert idx >= 0 + assert idx < len(array) + assert nearest_val == array[idx] + + +def test_find_nearest_value_exact_match(): + """Test find_nearest_value with exact match.""" + array = np.array([1.0, 2.5, 3.7, 5.2, 6.8]) + value = 3.7 + + nearest_val, idx = oscillation_utils.find_nearest_value(array, value) + + assert nearest_val == 3.7 + assert idx == 2 + + +def test_find_nearest_value_at_boundary(): + """Test find_nearest_value at array boundaries.""" + array = np.array([1.0, 2.5, 3.7, 5.2, 6.8]) + + # Test value smaller than min + nearest_val, idx = oscillation_utils.find_nearest_value(array, 0.5) + assert idx == 0 + + # Test value larger than max + nearest_val, idx = oscillation_utils.find_nearest_value(array, 10.0) + assert idx == len(array) - 1 + diff --git a/tests/test_statistics_utils.py b/tests/test_statistics_utils.py new file mode 100644 index 0000000..4c84faf --- /dev/null +++ b/tests/test_statistics_utils.py @@ -0,0 +1,12 @@ +""" +Basic tests for statistics_utils module. +""" +import pytest +from LFPAnalysis import statistics_utils + + +def test_module_imports(): + """Test that statistics_utils module can be imported.""" + assert hasattr(statistics_utils, '__name__') + assert statistics_utils.__name__ == 'LFPAnalysis.statistics_utils' + diff --git a/tests/test_sync_utils.py b/tests/test_sync_utils.py new file mode 100644 index 0000000..56fba47 --- /dev/null +++ b/tests/test_sync_utils.py @@ -0,0 +1,44 @@ +""" +Basic tests for sync_utils module. +""" +import pytest +import numpy as np +from LFPAnalysis import sync_utils + + +def test_moving_average(): + """Test moving_average function with default window size.""" + a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + n = 3 + + result = sync_utils.moving_average(a, n) + + # Result should be shorter by n-1 + assert len(result) == len(a) - n + 1 + assert isinstance(result, np.ndarray) + # First value should be average of first n values + assert np.isclose(result[0], np.mean(a[:n])) + + +def test_moving_average_default_window(): + """Test moving_average function with default window size.""" + a = np.random.randn(20) + + result = sync_utils.moving_average(a) + + # Default window is 11, so result should be len(a) - 10 + assert len(result) == len(a) - 10 + assert isinstance(result, np.ndarray) + + +def test_moving_average_single_value(): + """Test moving_average with window size of 1.""" + a = np.array([1, 2, 3, 4, 5]) + n = 1 + + result = sync_utils.moving_average(a, n) + + # With window of 1, should return same values + assert len(result) == len(a) + np.testing.assert_array_almost_equal(result, a) +