SubgridOptimization is a Python package for spatially constrained channel selection in high-density ECoG decoding. It is based on the methodology described in the manuscript Optimal Size and Placement of Electrocorticography Grids for Classification of Hand Movements. The package searches for compact axis-aligned rectangular electrode subgrids that preserve decoding performance while reducing grid size and implantation footprint.
The repository currently provides two optimization strategies:
SpatialExhaustiveSearch, which evaluates all admissible rectangular subgrids of a gridSpatialStochasticHillClimbing, which approximates the search by iterative stochastic expansion of rectangular subgrids
In addition, the package provides utilities for performance-size and channel-importance analysis:
elimination_plot, which summarizes decoding performance as a function of retained grid size, expressed either as channel count or estimated physical areaimportance_plot, which estimates channel relevance by averaging the decoding scores of all evaluated subgrids that include a given electrode
-
exhaustive and stochastic search strategies rectangular subgrid optimization under explicit spatial constraints
-
visualization for performance-size analysis and channel-importance mapping from evaluated subgrids
-
compatibility with standard
scikit-learnestimators and pipelines
The package is tested with:
- Python
3.9 - Linux
The core runtime dependencies are:
numpyscipyscikit-learnmatplotlibpandastqdm
Clone the repository and install it into a local Python environment.
git clone https://github.com/DirkKeller/Subgrid-Optimization.git
cd Subgrid-Optimization
python -m venv .venv
source .venv/bin/activate
pip install --upgrade pipInstall directly from the project metadata:
pip install -e .or manually:
pip install "numpy>=1.25,<2.0" "pandas>=1.5,<3.0" "scipy>=1.11,<2.0" "scikit-learn>=1.3,<1.5" "matplotlib>=3.7,<4.0" "seaborn>=0.13,<0.14" "tqdm>=4.66,<5.0"Build the image:
docker build -t subgrid-optimization .Run the synthetic example:
docker run --rm -it subgrid-optimizationRun the example and write outputs to your local machine:
mkdir -p output
docker run --rm -it \
-v "$(pwd)/output:/app/output" \
subgrid-optimizationOpen an interactive shell inside the container:
docker run --rm -it subgrid-optimization /bin/bashThe example below demonstrates the basic estimator interface on synthetic input with the expected tensor shape [samples, height, width, time].
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import MinMaxScaler
from sklearn.svm import SVC
from subopt.exhaustive import SpatialExhaustiveSearch
# Example data
X = np.random.randn(100, 8, 8, 20)
y = np.random.randint(0, 3, size=100)
grid = np.arange(1, 65).reshape(X.shape[1:3])
estimator = Pipeline([
("scaler", MinMaxScaler()),
("clf", SVC(kernel="linear"))
])
optimizer = SpatialExhaustiveSearch(
estimator=estimator,
grid=grid,
metric="f1_weighted",
cv=5
)
optimizer.fit(X, y)
print(optimizer.score_)
print(optimizer.mask_)The included example examples/synth_data.py generates a structured synthetic hand-movement decoding dataset without requiring external recordings. It is intended as a realistic test bed for validating the search procedures and visual outputs.
- 8x8 spatial grid
- event-locked epochs from
-0.5 sto1.5 s - realistic low-frequency drift and temporally correlated channel noise
- partially overlapping class-specific motor hotspots for thumb, index, and little finger
- shared activation around a central-sulcus band to mimic overlapping sensorimotor representations
- weighted-F1 scoring with a linear SVM and stratified cross-validation
Run it with:
python examples/synth_data.pyThe script writes outputs to ./output/synthetic_example/, including:
synthetic_results.csv- optimizer-specific elimination plots
- optimizer-specific importance plots
- a summary of the implanted synthetic hotspots
The exhaustive algorithm enumerates every rectangular subgrid that can be embedded within the original grid and evaluates each candidate with cross-validation.
For a grid with width W and height H, the number of rectangular subgrids is:
The stochastic optimizer starts from random seed electrodes and expands a rectangular subgrid one valid direction at a time. Expansion alternates between:
- exploration, where a candidate direction is sampled randomly
- exploitation, where all valid candidate directions are scored and the best one is selected
This produces an efficient approximation when exhaustive search is computationally expensive.
The optimizers expect input data with the following shape conventions:
X.shape = [samples, channel_height, channel_width, time]y.shape = [samples]
When a grid definition is provided, it should preserve the spatial arrangement of channels and uniquely encode channel identity.
Each algorithm possesses an elimination_plot, which visualizes performance against either channel count or estimated physical area. In the accompanying paper, subgrid surface area is computed from electrode spacing and exposed diameter, and performance-area curves are used to identify a knee-point where accuracy begins to decline more sharply.
Morover, the importance_plot function implements the same general idea as the paper’s informative-electrode analysis. Each electrode is assigned the mean performance of all subgrids containing that electrode, which yields a participant-specific spatial importance map.
SubgridOptimization/
├── pyproject.toml
├── Dockerfile
├── README.md
├── LICENSE
├── .gitignore
├── opt/
│ ├── __init__.py
│ ├── base.py
│ ├── exhaustive.py
│ ├── hillclimbing.py
│ └── utils.py
├── examples/
│ └── synth_data.py
└── docs/
└── figures/
├── paper_fig1.png
└── paper_fig2.png
If you use this repository, please cite the associated manuscript and the software repository.
@article{keller2026optimal,
author = {Keller, Dirk and Offenberg, Elena C. and Mehrkanoon, Siamak and Berezutskaya, Julia and Vansteensel, Mariska J. and Branco, Mariana P.},
title = {Optimal Size and Placement of Electrocorticography Grids for Classification of Hand Movements},
year = {2026},
journal = {To be added},
doi = {To be added}
}
