diff --git a/.claude/config.json b/.claude/config.json new file mode 100644 index 000000000..0be8accc5 --- /dev/null +++ b/.claude/config.json @@ -0,0 +1,11 @@ +{ + "tools": { + "gh": { + "version": "2.62.0", + "path": "~/.local/bin/gh", + "description": "GitHub CLI for workflow monitoring", + "required": false, + "setup_command": "See .claude/hooks/SessionStart" + } + } +} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5f8953695..1bd8d66ea 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,7 +2,7 @@ name: Docs on: push: - branches: [ master, develop, 'dev/*' ] + branches: [ master, develop, 'dev/*', 'claude/*' ] pull_request: branches: [ master, develop ] @@ -11,12 +11,12 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5867f769b..fd5e63f5e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: Test on: push: - branches: [ master, develop, 'dev/*' ] + branches: [ master, develop, 'dev/*', 'claude/*' ] pull_request: branches: [ master, develop ] @@ -24,7 +24,7 @@ jobs: python_version: '3.10' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install APT dependencies if: "startsWith(runner.os, 'Linux')" run: | @@ -45,7 +45,7 @@ jobs: # Remove the osx-arm64 environment conda deactivate conda env remove -n eeg-expy-full --yes - + # Create osx-64 platform with audio support conda create -v --platform osx-64 -n eeg-expy-full python=${PYTHON_VERSION} conda activate eeg-expy-full @@ -58,6 +58,17 @@ jobs: run: | conda install --force-reinstall numpy + - name: Debug numpy dependencies (Windows only) + if: matrix.os == 'windows-latest' + run: | + pip install pipdeptree + echo "=== Installed numpy version ===" + python -c "import numpy; print(f'numpy {numpy.__version__} from {numpy.__file__}')" || echo "numpy import failed" + echo "=== Packages depending on numpy ===" + pipdeptree -r -p numpy + echo "=== PsychoPy dependencies ===" + pipdeptree -p psychopy | head -50 + - name: Run eegnb install test run: | if [ "$RUNNER_OS" == "Linux" ]; then @@ -88,7 +99,7 @@ jobs: python_version: [3.9] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install conda uses: conda-incubator/setup-miniconda@v3 with: diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..38048356e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,156 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +EEG-ExPy is a Python library for cognitive neuroscience experiments using consumer-grade EEG devices. It provides a unified framework for running classic EEG experiments (N170, P300, SSVEP, etc.) with devices like Muse, OpenBCI, and others. The goal is to democratize cognitive neuroscience research by making it accessible with affordable hardware. + +## Claude Code Configuration + +The `.claude/` directory contains configuration for Claude Code sessions: + +### `.claude/config.json` +- **GitHub CLI (gh)**: Version 2.62.0, configured for workflow monitoring + - Path: `~/.local/bin/gh` + - Optional tool (not required) + - Setup instructions in `.claude/hooks/SessionStart` + +## Development Commands + +### Environment Setup +```bash +# Install in development mode with all dependencies +pip install --use-pep517 .[full] + +# Install specific dependency groups +pip install -e .[streaming] # For EEG streaming +pip install -e .[stimpres] # For stimulus presentation +pip install -e .[streamstim] # Both streaming and stimulus +pip install -e .[docsbuild] # For documentation building +``` + +### Testing and Quality Control +```bash +# Run tests +make test +# OR: pytest --cov=eegnb --cov-report=term --cov-report=xml --cov-report=html --nbval-lax --current-env + +# Run type checking (excludes visual_cueing due to errors) +make typecheck +# OR: python -m mypy --exclude 'examples/visual_cueing' + +# Run single test file +pytest tests/test_specific.py +``` + +### Documentation +```bash +# Build documentation +make docs +# OR: cd doc && make html + +# Clean documentation build +make clean +# OR: cd doc && make clean +``` + +### CLI Usage +```bash +# Run experiments via CLI +eegnb runexp --experiment visual_n170 --eegdevice muse2016 --recdur 120 +# OR: eegexpy runexp --experiment visual_n170 --eegdevice muse2016 --recdur 120 + +# Interactive mode +eegnb runexp --prompt +``` + +## Architecture + +### Core Components + +1. **BaseExperiment Class** (`eegnb/experiments/Experiment.py:29`) + - Abstract base class for all experiments + - Key parameters: `n_trials`, `iti` (inter-trial interval), `soa` (stimulus onset asynchrony), `jitter` + - Subclasses must implement `load_stimulus()` and `present_stimulus()` methods + - Supports both standard displays and VR via `use_vr` parameter + +2. **EEG Device Abstraction** (`eegnb/devices/eeg.py`) + - Unified interface for multiple EEG devices via BrainFlow and muselsl + - Supported devices: Muse, OpenBCI (Cyton/Ganglion), g.tec Unicorn, BrainBit, synthetic + - Handles device-specific streaming and marker insertion + +3. **CLI Interface** (`eegnb/cli/__main__.py`) + - Command-line interface using Click + - Entry points: `eegnb` and `eegexpy` + - Supports interactive prompts and direct parameter specification + +### Experiment Implementation Pattern + +Each experiment inherits from `BaseExperiment` and implements: +- `load_stimulus()`: Load visual/auditory stimuli +- `present_stimulus(idx)`: Present stimulus for trial idx and push EEG markers + +Example from N170 experiment (`eegnb/experiments/visual_n170/n170.py:21`): +```python +class VisualN170(Experiment.BaseExperiment): + def load_stimulus(self): + # Load face and house images + self.faces = list(map(load_image, glob(os.path.join(FACE_HOUSE, "faces", "*_3.jpg")))) + self.houses = list(map(load_image, glob(os.path.join(FACE_HOUSE, "houses", "*.3.jpg")))) + return [self.houses, self.faces] + + def present_stimulus(self, idx: int): + label = self.trials["parameter"].iloc[idx] + image = choice(self.faces if label == 1 else self.houses) + image.draw() + if self.eeg: + self.eeg.push_sample(marker=self.markernames[label], timestamp=time()) + self.window.flip() +``` + +### Available Experiments + +Located in `eegnb/experiments/`: +- `visual_n170/` - Face/object recognition ERP +- `visual_p300/` - Oddball paradigm ERP +- `visual_ssvep/` - Steady-state visual evoked potentials +- `auditory_oddball/` - Auditory ERP paradigms +- `visual_cueing/` - Spatial attention cueing +- `visual_gonogo/` - Go/No-go task +- `somatosensory_p300/` - Tactile P300 + +### Dependencies and Requirements + +The `requirements.txt` is organized into sections: +- **Analysis**: Core analysis tools (MNE, scikit-learn, pandas) +- **Streaming**: EEG device streaming (brainflow, muselsl, pylsl) +- **Stimpres**: Stimulus presentation (PsychoPy, psychtoolbox) +- **Docsbuild**: Documentation building (Sphinx, etc.) + +### Platform-Specific Notes + +- **macOS**: Uses `pyobjc==7.3` for GUI support +- **Windows**: Includes `psychxr` for VR support, `pywinhook` for input handling +- **Linux**: Requires `pyo>=1.0.3` for audio, additional system dependencies for PsychoPy + +### Data Storage + +- Default data directory: `eegnb.DATA_DIR` +- Filename generation: `eegnb.generate_save_fn()` +- Analysis utilities in `eegnb/analysis/` + +### Configuration + +- PsychoPy preferences set to PTB audio library with high precision latency mode +- Test configuration in `pyproject.toml` with pytest coverage and nbval for notebooks +- Makefile provides convenient commands for common tasks + +### Testing Strategy + +- pytest with coverage reporting +- nbval for notebook validation +- Excludes `examples/**.py` and `baseline_task.py` from tests +- Type checking with mypy (excludes visual_cueing) + +This architecture enables rapid development of new experiments while maintaining consistency across the experimental framework and supporting diverse EEG hardware platforms. diff --git a/requirements.txt b/requirements.txt index 001b69229..07756ead7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,8 @@ scikit-learn>=0.23.2 pandas>=1.1.4 -numpy>=1.26.0; python_version >= "3.9" +# psychxr build pinned to this version of numpy. +numpy>=1.26,<1.27; python_version >= "3.9" numpy<=1.24.4; python_version == "3.8" mne>=0.20.8 seaborn>=0.11.0 @@ -60,7 +61,8 @@ ffpyplayer==4.5.2 # 4.5.3 fails to build as wheel. psychtoolbox scikit-learn>=0.23.2 pandas>=1.1.4 -numpy>=1.26.0; python_version >= "3.9" +# psychxr build pinned to this version of numpy. +numpy>=1.26,<1.27; python_version >= "3.9" numpy==1.24.4; python_version == "3.8" mne>=0.20.8 seaborn>=0.11.0