Development Setup#
This guide covers setting up a development environment for EEGPrep and contributing to the project.
Prerequisites#
System Requirements#
Python: 3.12 or higher
Git: For version control
uv: Default package and environment manager
Check your Python version:
python --version
Required Tools#
Git: https://git-scm.com/
Python: https://www.python.org/
Optional Tools#
Conda: For environment management (https://conda.io/)
Docker: For containerized development
Make: For running build commands
Installation from Source#
Clone the Repository#
git clone https://github.com/sccn/eegprep.git
cd eegprep
Create the uv Environment#
Install the default development environment:
uv python install 3.12
uv sync --group dev
uv sync creates .venv/ and installs EEGPrep in editable mode from the
locked dependency set. The development environment includes the GUI and
eegprep-console runtime dependencies so uv run eegprep-console --full
works from a fresh checkout. Use uv run for commands so they execute inside
this environment.
Install Documentation Dependencies#
uv sync --extra docs --group dev
This installs:
The eegprep package in editable mode
Development dependencies used by repo tooling
Documentation dependencies
Running Tests#
Porting The Current EEGLAB Tests#
Ports of the upstream MATLAB tests use only the current
sccn/eeglab_tests repository. The
older sccn/eeglab-testcases repository is stale and must not be used.
EEGPrep currently pins eeglab_tests commit
fec88f9ffe7c225eb1de8924db8b6a3e0d53a4d9 and the EEGLAB submodule commit
d0963be9d81acc5b66e84f8d3d7c89e3ce127cc4.
That suite commit is the squash merge of sccn/eeglab_tests pull request
16 on master.
The current deliverable is faithful ports of these existing tests, validated against MATLAB. It does not include newly authored MATLAB scientific cases, implementation-coverage targets, or EEGPrep capability fixes. Reuse unchanged verified results; obtain fresh evidence for changed or previously unverified paths. Passing the translated tests on MATLAB validates their translation, not whether EEGPrep itself implements every tested workflow.
Translate the behavior and assertions of each MATLAB scenario into the closest existing pytest module. Decorate the Python test with its upstream path and test name so coverage remains traceable without a separate conversion matrix:
from tests.eeglab_tests import eeglab_test
@eeglab_test("regression_tests/t_pop_selectevent.m", "testRetainsMatchingEpochs")
def test_pop_selectevent_retains_matching_epochs():
...
One Python test may carry more than one decorator when it genuinely covers multiple equivalent upstream scenarios. Do not combine tests merely to reduce the number of ports. Preserve input shapes, dtypes, empty values, indexing, warnings, errors, and all scientifically relevant output fields. Use a live MATLAB comparison when practical or small expected data generated from the pinned suite when ordinary CI must run without MATLAB.
When a faithful port exposes missing behavior or a defect, keep the failing scenario visible and record it in Beads without changing runtime capabilities as part of the test port. Excluding pure MATLAB runtime behavior requires an explicitly agreed scope decision and a concrete technical rationale. Never replace an applicable assertion with a no-crash smoke test or broaden a numerical tolerance simply to make the port pass.
Before declaring the port complete, clone the current suite at its pinned commit and run the source-driven audit:
git clone https://github.com/sccn/eeglab_tests.git /tmp/eeglab_tests
git -C /tmp/eeglab_tests checkout fec88f9ffe7c225eb1de8924db8b6a3e0d53a4d9
git -C /tmp/eeglab_tests submodule update --init eeglab
uv run python -m tools.eeglab_test_port_audit /tmp/eeglab_tests
The command discovers wrapper and regression methods directly from MATLAB,
collects eeglab_test metadata through pytest, and prints every missing or
stale reference. Pass --json for automation. It deliberately rejects the
old eeglab-testcases repository, a checkout at another commit, and
provenance that does not exist in the pinned suite.
Source fidelity and graphical validation#
Source-reference counts are not execution results or scientific coverage. Backend-dispatched contracts must still be checked against the actual source body and executed against the pinned MATLAB reference. Keep generated-data Python regression tests as separate supplements; they are not substitutes for the source recording, workload, call sequence, or assertions. A source wrapper whose body is entirely commented out or immediately returns is inactive, not a successful behavioral test. Such definitions remain visible in the inventory without attaching misleading provenance to a different Python scenario.
The pinned inventory contains 813 definitions: 709 have Python source references, and 104 remain unmatched. Inspection classifies the unmatched definitions as 60 comment-only leaves, 27 empty functions, one commented tutorial body, five help-only calls, eight no-argument helper returns, two disabled early-return cases, and one TODO-status-only leaf. Meaningful interpolation and memory-mapped-object helper calls remain inside their parent workflow ports. The raw audit still reports the 104 unmatched entries; do not create dummy ports or describe this as 813 passing tests. This source accounting is distinct from assertion fidelity and execution evidence.
Mark every contract that creates plots, windows, dialogs, or graphical
callbacks with gui, even when MATLAB figures would be invisible. This
includes progress bars in numerical helpers such as eeg_context and
browser-opening paths in either backend, even if the other backend is non-GUI.
The marker allows explicitly deferred graphical validation to remain excluded using
-m "not gui". Collecting those contracts or checking their syntax does not
validate their runtime behavior. Modal Python dialogs may require interaction;
do not substitute a mock renderer or invent accept/cancel actions to make an
original interactive workflow automatic.
MATLAB-specific graphics objects, caller workspaces, and object arrays sometimes require small test-only native boundaries. Keep the workflow and assertions in Python and dispatch actual EEGLAB processing functions. Translate ordinary language operations such as file deletion or closing a figure using real Python APIs, not nonexistent EEGPrep counterparts to MATLAB builtins. Do not replace a missing operation with a fabricated failure or a passing assertion that the feature is absent.
Preserve the limitations of the source oracle too: a no-assertion smoke test does not establish numerical correctness, and a legacy wrapper that ignores a returned failure status does not establish that its operation succeeded. Record source failures and unresolved language-specific observables separately instead of weakening comparisons or counting a native-only diagnostic as a completed Python port.
MATLAB tables nested in reference outputs, such as FieldTrip trialinfo,
cross the test bridge as MATLAB-written MAT bytes and are restored to native
tables before the next call. This preserves variable classes, dimensions and
table properties; it does not replace a table with a lossy struct or omit its
metadata. The transport regression compares the real FieldTrip preprocessing
chain with direct native execution using the original epoched sample dataset.
Large outputs retain their complete arrays. When the aggregate output container approaches MAT v7’s 2 GB variable limit, the test bridge writes numerical leaves to per-call sidecars and reassembles their original classes and dimensions before deleting the temporary directory. The slow transport regression sends more than 2 GiB through the bridge and back to MATLAB for exact comparison. Large Python inputs use the same sidecars before SciPy’s MAT5 writer reaches its container or individual-matrix size limit. Buffered Fortran-order chunks preserve complete values, classes and dimensions without another full-array copy. Separate slow checks exercise both a nested aggregate and a single matrix larger than 4 GiB against native MATLAB assertions. This is test transport, not subject selection, downsampling or a production dataset serializer.
A user-approved Python-only exception applies to MATLAB caller-workspace and
copy-count introspection in checkmmo and the nested helpers of checkmmo2.
Actual construction, copying, writes, data-integrity checks and debug-output
assertions remain in scope. MATLAB retains the original count assertions.
Because checkmmo’s source oracle consists entirely of count assertions, its
Python result records execution only, not count or scientific parity. The
audit reports these exceptions separately without removing source definitions
or treating unresolved diagnostic failures as passes.
Explicit MATLAB support paths#
Backend-neutral contracts run with --eeglab-backend=matlab and an explicit
--eeglab-root. The automatically assigned eeglab_contract marker selects
tests using the backend fixture, excluding ordinary direct-Python regressions
and transport-only harness checks. For a non-graphical, non-slow contract run:
uv run --no-sync pytest tests --eeglab-backend=matlab \
--eeglab-root=/absolute/path/to/eeglab_tests/eeglab \
-m "eeglab_contract and not gui and not slow" --junitxml=contracts.xml
This is a selected run, not full acceptance: graphical and slow contracts are
not validated by this selection, and original prerequisite guards may return
without exercising their scientific branches. Report those limits separately
from pass counts.
The guarded statistical and legacy ERSP contracts record
eeglab_source_body_entered in each JUnit test case’s properties; a
False value means the original prerequisite/version guard returned before
the scientific body. The clustering contract separately records
eeglab_optional_kmeans_branch_entered, since its first clustering operation
still runs when the optional branch is unavailable. These properties do not
change the source guards, assertions, or pytest outcomes. A passing case with a
false guard property is not evidence that the guarded behavior was validated.
For the full contract selection, omit the not gui and not slow filters.
MATLAB windows, progress dialogs, video rendering, full-recording ICA and
multi-subject bootstraps can run for substantially longer than the numerical
subset. Run deliberately on a machine where graphical interaction is allowed:
uv run --no-sync pytest tests --eeglab-backend=matlab \
--eeglab-root=/absolute/path/to/eeglab_tests/eeglab \
-m eeglab_contract --junitxml=all-contracts.xml
For long or concurrent runs, supply a distinct --basetemp directory for
each run if generated datasets and model files must be retained. Pytest’s
default temporary-directory retention can remove earlier runs while another
process is still using their evidence. Use a new directory: pytest clears an
existing --basetemp before running.
Known reference findings must stay visible in that report. In the pinned
suite, the three legacy erpimage leaves pass_general, pass_many_args
and pass_times return tc_notpassed on MATLAB R2026a: they compare a
numeric handle vector with modern cell outputs. Their original wrappers
discard the returned status. A green wrapper therefore does not validate
those predicates. The Python contracts retain the comparisons and fail;
the graphics transport only removes handles the source never compares.
The original statcond cases 9, 10, 13 and 14 also fail directly in native
MATLAB R2026a with their stored zero absolute/relative tolerances. Their
maximum p-value residuals are approximately 6.07e-18, 2.78e-17,
7.81e-18 and 1.73e-17; the direct fixture run passed 74 of 78 cases,
matching the hybrid failures. These are retained reference fixture/platform
discrepancies, not established EEGPrep defects. The original tolerances and
failing assertions remain unchanged. The Python backend now runs the same
fixtures. Cases 31-78 match, including the seeded resampling arrays. Every
fixture input is double. Cases 1, 2, and 7-14 are t-tests. On the Python
backend their statistic tolerance is n * eps, where eps is float64
machine epsilon and n is the accumulation length: the case count of the
paired difference, or the sum of both case counts for an unpaired test,
because both samples enter the means and the pooled variance. On these
fixtures that tolerance is about 2.2e-15 to 4.4e-15. The two-tailed
p-value is 2 * sf(|t|, df). Its relative condition number in t is
|t| * 2 * pdf(|t|, df) / |p|. The p-value tolerance is the statistic
tolerance times the maximum of that factor on the stored reference, one
bound for the whole array, about 1e-13 here. A per-element bound is
tighter than the last-bit tcdf difference on some elements whose t
already matches. The statistic matches bit for bit in 9 of these 10 cases,
so the p-value bound covers tcdf implementation differences. Zero and
subnormal reference probabilities stay exact: they are left out of the
maximum, and a non-finite bound fails the comparison. Degrees of freedom
and the empty parametric surrogate stay exact. The stored fixture
tolerances remain zero, and the MATLAB backend still uses them. The worst
residuals inside this bound are a statistic relative error of 8.8e-16
and a p-value relative error of 1.9e-14.
The ANOVA cases (3-6 and 15-30) are parametric F tests. EEGLAB accumulates
those sums of squares in double (anova1_cell.m returns the column effect
of anova2_cell.m; anova1rm_cell.m lines 67-68 and 97-112;
anova2_cell.m lines 87-106; anova2rm_cell.m lines 83-194) and evaluates
the tail in myfpval.m lines 11-19. The stored tolerances remain zero. On
the Python backend each sum of squares over the N observations of one
feature uses the same accumulation rule as the t-test, N * eps = 2 * N * u
with u = 2^{-53}. An F ratio adds the numerator and the denominator.
Repeated-measures brackets multiply each side by κ = Σ|term| / |sum|,
applied per element, because anova1rm_cell.m and anova2rm_cell.m
subtract those terms (the interaction residual has eight). The unpaired
factor is 1 because anova1_cell delegates to the two-pass anova2_cell,
so that tolerance is 4 * N * u. The p-value limit is also per element.
Its derived part is the F tolerance times |F| * pdf(F; df1, df2). Its
measured part is C_beta * u * |p| with C_beta = 2048, an allowance for
betainc evaluation error between MATLAB and SciPy rather than an operation
count. That one constant covers the full tail down to p = 1e-300: a
measured MATLAB sweep reaches 1966.5u and SciPy reaches about 70u. Zero and
subnormal reference probabilities stay exact. Degrees of
freedom and the empty parametric surrogate stay exact. The MATLAB backend
still uses the stored zero tolerance. F resampling arrays are float64 when
arraycomp is on, because statcond.m lines 384 and 421 replace the
single preallocation at line 261 with the double ANOVA output, and float32
when arraycomp is off. concatdata.m stores surrogate draws as single
and ttest_cell keeps that class, so a t-test resampling array and its
confidence interval stay float32 for both arraycomp modes. A confidence
interval follows the surrogate class. Supplied stats and resampling arrays
keep the class they were given when naccu matches the supplied length.
A supplied statistic is accepted when its shape matches the resampling array
without the last axis after trailing length-1 dimensions are removed. A
transpose or a flattened column is rejected. p-values stay float64. Cases
31-78 are returnresamplingarray condition grids and stay float32. On
these fixtures the largest measured-to-limit ratio is about 0.030 for F and
about 0.16 for p.
Some graphical wrappers also discard failure statuses. The two gradplot
center cases and two headmovie cases returned tc_notpassed in retained
native-backed runs, despite their wrapper-level pytest passes. The headmovie
failure is a nonscalar logical condition on the pinned R2026a runtime. These
recorded statuses are source-oracle limitations, not successful scientific
validation; do not change the original predicates to make the report green.
The original std_topoplot workflow requires .icatopo files generated
by the suite’s earlier std_precomp workflow. A fresh source data checkout
does not contain them. Its independent port creates the real scalp cache in
a writable copy of the original STUDY, then reloads the original STUDY before
the unchanged four plotting calls. No synthetic cache or reference-tree write
is used.
The test session initializes the pinned EEGLAB checkout and
its already installed plugins without installing anything. Optional upstream
dependency files can live outside that checkout: pass their directory using
--eeglab-support-path PATH. The directory must exist. This option is
repeatable; each directory is added at the beginning of the session’s MATLAB
path after EEGLAB plugin initialization, so the last directory takes precedence
over earlier support directories and installed plugins. Subdirectories are not
added recursively. The session quits its engine on teardown; it never calls
savepath or changes the package runtime, reference source, or user options.
Two upstream packaging gaps encountered with the pinned suite have been validated using source-exact files in an ignored local directory:
JSONio bundled with
bids-matlab-tools8.0/EEG-BIDSlacks an Apple Silicon MEX. Official JSONio commite6c5b3ea16142e8e428aa254fc042dfad6011c30addsjsonread.mexmaca64(Git blob396b1d429c4887446c1c0a20b4e6f457942abe1b). The bundledjsonread.chas blobfa7902c4a5bbb74ddd02e76ed423a781602b33b6, identical to the preceding official revision82d835d17348b0d060e8af881da308b897a627ff. The Apple Silicon commit changes only that C file’s header comment, not parser logic;jsmn.candjsmn.hare unchanged. This binary is for native Apple Silicon MATLAB only.Fileio260210/private/ft_datatype_sens.mcalls the absentft_deleteopt. That caller’s blobc66bdfebf2528518981ef75dc257d5b15c9da403exactly matches official FieldTrip revisionefdd7db8bf5623e580c8f12f81bb57b325662e29. The matching revision’sutilities/ft_deleteopt.mhas blob2eebd35ca862eac7001bb3d01e49250501e849ff. Use that original helper, not a replacement implementation. Itsremovefieldsdependency is already supplied by the installed FieldTrip utilities.
For that Apple Silicon setup, download the two pinned files explicitly from their official repositories. From the main EEGPrep checkout (not a temporary worktree), run:
mkdir -p .notes/reference/matlab-test-dependencies
curl --fail --location https://raw.githubusercontent.com/gllmflndn/JSONio/e6c5b3ea16142e8e428aa254fc042dfad6011c30/jsonread.mexmaca64 \
--output .notes/reference/matlab-test-dependencies/jsonread.mexmaca64
curl --fail --location https://raw.githubusercontent.com/fieldtrip/fieldtrip/efdd7db8bf5623e580c8f12f81bb57b325662e29/utilities/ft_deleteopt.m \
--output .notes/reference/matlab-test-dependencies/ft_deleteopt.m
git hash-object .notes/reference/matlab-test-dependencies/jsonread.mexmaca64
git hash-object .notes/reference/matlab-test-dependencies/ft_deleteopt.m
Check that the two printed hashes match the blobs above before execution.
The legacy BIDS importer prepends its own JSONio folder during import,
and starting EEGLAB again also reorders plugin paths. For full BIDS/LIMO
workflows on Apple Silicon, install that same verified binary alongside
jsonread.m in each active BIDS plugin’s JSONio directory (both
EEG-BIDS and bids-matlab-tools8.0 if both are present). A symlink to
the verified support file is sufficient. This is an explicit local native
dependency installation, not a MATLAB-source correction; do not overwrite
source files or change the pinned revision. The standalone support path alone
is insufficient once the importer changes path precedence.
The STUDY-statistics and advanced source-reconstruction tutorials require the
full FieldTrip distribution. Fileio also bundles a partial FieldTrip whose
ft_defaults can shadow the full plugin and look for statfun/preproc
modules in the wrong directory. These two contracts temporarily prioritize
the full plugin owning ft_freqstatistics after EEGLAB startup, clear its
cached initialization and invoke its actual ft_defaults. They restore the
previous path afterward; no scientific function or option is substituted.
Full LIMO validation also requires Parallel Computing Toolbox (the reference
calls gcp during cleanup) and Image Processing Toolbox for bwlabeln in
cluster analysis, or the reference’s supported SPM alternative. A license
entitlement alone does not install either toolbox. Record installed versions
alongside the reference revisions; do not patch LIMO to ignore missing tools.
No downloads occur automatically in pytest. Run the non-GUI parser/BIDS metadata smoke and the original Fileio contract with normal test selection:
uv run --no-sync pytest \
tests/test_eeglab_hybrid.py::test_matlab_bids_metadata_loaders \
tests/test_sample_data_pop_functions.py::test_upstream_pop_fileio_original_sample_options \
--eeglab-backend=matlab \
--eeglab-root=/absolute/path/to/eeglab_tests/eeglab \
--eeglab-suite-root=/absolute/path/to/eeglab_tests \
--eeglab-support-path=/absolute/path/to/eegprep/.notes/reference/matlab-test-dependencies
The metadata smoke calls the actual JSONio parser and original BIDS loaders on
ds002718 metadata. The Fileio source contract only requires its three
original calls to complete; it does not establish numerical or channel-location
correctness, and existing import warnings remain visible. These checks do not
validate LIMO preprocessing, bootstraps, or GUI workflows.
Tutorial-wrapper provenance#
The tutorial-wrapper ports additionally pin
sccn/eeglab-tutorial-scripts commit
58bf12dd53e894dd3ee1285946563cd94999db16. The four tutorial references
plot_study_erp, source_reconstruction_advanced,
source_reconstruction_eeg, and time_freq_all_elec are MATLAB Live
Scripts (.mlx) in that commit; they are not missing source files. MATLAB
can execute them, while Octave cannot execute the Live Script format.
The ten active wrapper contracts in tests/test_tutorial_eeglab_tests.py
retain the original scripts’ executable operation sequences, arguments, and
full inputs: the continuous and ICA-epoched sample recordings, five-subject N400 study,
and P300 BIDS tree (with the original subjects 1–2 selection). Workflows that
write datasets, measure caches, spline files, or videos use isolated working
directories and complete copies of the source input trees when needed. The
original FieldTrip, DIPFIT, PICARD, and ICLabel dependencies remain required;
missing Python capabilities are not converted into successful skips or
expected failures. Python owns the workflow sequence, with small test-only
boundaries for native caller workspaces, function handles, and video objects.
All ten active tutorials start the EEGLAB GUI or create figures and are
marked gui. They require explicit graphical validation; ordinary
generated-data regression results do not validate these source contracts.
Future Python video-export runs also require ffmpeg for the equivalent
30-fps MPEG-4 or uncompressed AVI writer.
tutorial2_wrapperTest.test_bids_process_face_experiment is inactive because
its entire wrapper body is commented out. It carries no source provenance and
is not represented by a no-op passing test. The existing face-recognition and
other generated-data workflows remain supplemental Python regressions, without
claims that their synthetic recordings are the original source inputs.
The original event_processing_study script leaves EEG unchanged after
editing and reloading ALLEEG. On the pinned reference, its final
eeglab redraw therefore opens pop_newset’s dataset-change dialog.
The source specifies no answer. A manual run that selects Cancel exercises
that cancellation path but does not prove an unattended workflow or assert
the resulting STUDY contents. Do not silently overwrite a dataset, synchronize
the variables, or invent a dialog response inside the port.
The test-only redraw boundary returns the actual post-dialog workspace or
session state so subsequent source operations use the user’s real choice.
This state transport does not select an answer or add a Cancel-only scientific
expectation. Interactive acceptance remains a separate required check.
The source also includes a manual follow-up at
tutorial_scripts/event_processing_study.m:39-41: open
STUDY > Select/Edit study design(s), press the lower New button under
Edit the independent variables for this design, and check that rt is
available in Add variable. The Python wrapper currently stops at redraw;
its passing result does not include this follow-up. A generated-data
std_makedesign test is not a substitute for the original menu workflow.
Native manual validation on September 29, 2026 observed rt selectable as
a continuous variable after explicitly canceling the stale-EEG overwrite.
The retained dataset has 157 events with 75 numeric rt values and 82 empty
fields; the 80 STUDY trial records contain the same 75 numeric values and
five empty fields. The workspace was unchanged by opening and closing the
design dialogs, and no additional design was saved. EEGPrep’s
pop_studydesign dialog now runs the visible EEGLAB controls: subject
selection, design New/Rename/Delete, variable New/Edit/Delete, List factors,
resave, Help, OK, and Cancel. New opens Add variable, lists a continuous
trial factor such as rt, and on OK writes STUDY.design and rebuilds
trialinfo when that factor is not already available. Canceling still
leaves the workspace unchanged. Paired-statistics checkboxes are commented
out of the current EEGLAB dialog, so EEGPrep does not show them; pairing is
stored by std_makedesign. GitHub Actions replays the dialog command
history in EEGLAB and compares STUDY.design for New, Rename, Delete, a
combined categorical value, a continuous variable, and an empty design.
The September 29 observation remains the
manual MATLAB acceptance record for the original dialog. The Python tutorial
wrapper still stops at redraw, so a passing wrapper does not exercise this
follow-up.
Keep this original manual instruction as a separate manual acceptance case.
Automating it with a new cross-backend GUI driver is not required to preserve
the original test, and its manual result must not be added to automated pass
counts. The Python test’s fixture closes the window during teardown, so perform
the check while paused immediately after _tutorial_redraw returns, before
allowing teardown to proceed.
The movie tutorial requires visible MATLAB figures for native frame capture. On R2026a, the original 2-D section produces inconsistent frame dimensions under the harness’s hidden-figure default, but completes with 91 equal-sized frames using the original visible default. Its port temporarily enables visible figures for this capture workflow and restores the previous setting; it does not resize frames, change plotting options or patch the source. Native tutorial figure creation also finishes pending GUI activation and restores the created figure as MATLAB’s current target before the next Engine call. Otherwise R2026a can reactivate an older docked figure between calls, causing later plots to reuse its colorbar axes. This test-only boundary keeps the source’s figure selection without changing docking or plotting options.
LIMO preprocessing source correction#
The separately approved limo_preproc_stats_hw.m correction captures
pop_limo’s existing third output and resolves the adjacency and Beta/LIMO/
contrast text-list paths from the generated model directory. The pinned
reference writes these under derivatives2/derivatives with a study-prefixed
GLM name; the original script expects the older paths. The correction preserves
all 18 subjects, preprocessing options, text-list inputs, 1,000 second-level
bootstraps, calculations and plots.
prepare_limo_preprocessing_source_overlay in
tests.test_limo_eeglab_tests verifies the original SHA-256 and applies
tests/matlab/limo_preproc_stats_hw.source-corrections.patch only to a scratch
copy. Its integrity regression reverses the approved edits byte-for-byte and
checks that the pinned source remains unchanged. The Python-owned workflow uses
the same path expressions without evaluating the complete native script.
Ordinary integrity checks do not establish full live validation.
The port also publishes the current STUDY before pop_limo and the
original three-argument contrast call. Native limo_settings_script reads
base STUDY even when its caller has a valid local value; the earlier redraw
precedes the script’s new design. These explicit assignments reproduce the
script’s current workspace instead of supplying its stale pre-design copy.
The Python-owned workflow snapshots/restores base/global state, and the scratch
overlay’s inverse removes both assignments to recover the pinned source.
The pinned preprocessing script is interactive even after all input paths are
resolved: limo_add_plots plots its supplied files, then asks for another
central-tendency file until Cancel is selected. The first such dialog follows
the three unweighted ERP files. Its port and scratch source overlay now use
eegprep_test_limo_add_plots: the original native plotting function executes
with every supplied file and unchanged options, while a scoped file-input
fixture declines exactly one optional next-file request. A different prompt,
repeated request or missing expected request fails. The original path and file
chooser are restored even on failure. Live checks assert the actual plotted
values and confidence-interval patches, and reject a required metadata chooser.
This is explicit scripted file-input cancellation, not validation of clicking
the native chooser. No extra file, computed result or plotting implementation
is fabricated. The overlay’s inverse also removes these eight wrapper names
and recovers the original source byte-for-byte.
The subsequent original per-subject plotting call exposes a separate pinned
LIMO failure: its 'variable' parser assigns the numeric subject index to
infile instead of retaining the supplied file list. load(file) then
fails because the index is not a filename. Both the direct native call and
the test wrapper reproduce this with the retained real six-file ERP inputs.
The wrapper does not change this parser, suppress the error or omit the
18-subject plotting loop. Successful optional-file cancellation therefore
does not establish full preprocessing-workflow success.
LIMO integration source correction#
The companion limo_test_integration.m overlay applies five separately
approved path-only corrections: the generated derivative/list directory,
study-prefixed GLM names, Gp-1 group spelling, grouped contrast basenames
from the first subject’s returned contrast files, and the second-level
H0/Betas_desc-H0.mat filename. The Python workflow and native overlay use
the same generated model paths. Original text-list, cell and ragged-array
inputs, all 18 subjects, OLS/WLS models, nine status sections, 101 bootstraps,
statistical options and assertions remain unchanged. Historical cleanup is not
corrected by this overlay.
A separately authorized workspace-setup correction publishes the current local
STUDY into MATLAB’s base workspace immediately before the original
two pop_limo model calls and the three-argument WLS
limo_batch('contrast only', [], contrast) call. Native
function-scope reproduction with all 18 retained models confirmed that the
earlier no-argument eeglab call clears base/global STUDY and that LIMO’s
implicit discovery reads base, not the caller’s valid local variable. The
limo_settings_script called from limo_batch also overwrites an explicitly
passed local STUDY from base before metadata export, requiring the same setup
before both model calls. After the original post-load 6/7/5 group assignment,
the overlay also refreshes only STUDY.group using the original
std_checkset expression unique_bc({STUDY.datasetinfo.group}). The load
checked this summary before the manual assignments, retaining an empty label; native
LIMO requires it to emit grouped text lists. Dataset membership and order are
unchanged. The three-argument discovery path remains exercised; no fourth
argument or runtime repair is substituted. The native overlay uses the existing state-only
eegprep_test_base_workspace helper with onCleanup; the Python-owned
MATLAB workflow uses the same snapshot/restore helper and a pytest finalizer.
Both preserve the prior base/global workspace even on failure. The native
snapshot helper remains locked until restoration because EEGLAB initialization
executes clear functions, which otherwise discards its saved state. The native
overlay requires tests/matlab on the path, as supplied by the hybrid harness.
prepare_limo_integration_source_overlay in tests.test_limo_eeglab_tests
checks the original SHA-256 before applying the exact substrings and occurrence
counts in tests/matlab/limo_test_integration.source-corrections.json to a
scratch copy. JSON preserves literal trailing spaces on affected source lines
that the required whitespace checks would remove from a unified patch. An
independent inverse regression recovers the pinned source byte-for-byte; no
reference checkout is edited. Full live validation remains a separate check.
The retained full integration run completed all 18 OLS and 18 WLS models,
with 36 OLS and 18 WLS contrasts, then failed before the seven second-level
sections. Pinned limo_glm_handling writes subject-prefixed
sub-NNN_desc-R2.mat files, but limo_best_electrodes loads R2.mat
when given the original LIMO-model list. The identical native list-input call
reproduces the failure. Preserve that input and failure: replacing the list
with R2-file paths would bypass the workflow being ported, not validate it.
Isolated LIMO implementation corrections#
Implementation corrections were explicitly approved on September 29, 2026
for an isolated reference copy only. They are separate from the test-source
overlays above. tests/matlab/limo_isolated_reference_fixes.patch records
eight corrections in four files:
The plotting parser assigns
'variable'to its existing selectorvinstead of overwriting the filename list.For explicit scalar subject selection in four-dimensional Time data with one condition, the plotter selects the fourth (subject) axis, preserving channel/frame dimensions. The writer saves arrays as channel, frame, condition, subject; the original reader wrongly selected the condition axis. Other layouts and unspecified-variable interaction are unchanged.
The R2 reader uses the mass-univariate writer’s subject-prefixed filename, retaining
R2.matfor non-BIDS and multivariate models.The reader checks whether optional
chanlocs.urchanmetadata exists before reading it, matching its documented contract. The optional output retains its existing NaN initialization when absent; no channel mapping is invented.limo_random_selectunwraps a singleton inner filename cell beforefileparts. Nativelimo_batchreturns these nested contrast lists; concatenating the cell-valued basename and extension previously turned one filename into two entries. The original list shapes remain unchanged.The original group-orientation warning formats its numeric group count into the message instead of passing that count as a graphical window title. The warning and source input orientation are retained.
Channel selection parses the existing numeric text as a vector, preserving the supplied optimized channel indices. The scalar-only conversion previously returned NaN for the original multi-subject vector.
Single-channel Time-domain TFCE null data retain the channel, frame and bootstrap axes when selecting the existing t-statistic. The original
squeezeremoved the channel axis, so the unchanged bootstrap loop indexed a nonexistent third dimension.reshapepreserves the same values as1 x frames x bootstraps; multi-channel and Time-Frequency branches are unchanged.
Numerical operations, input lists, subjects, bootstrap counts and source-test assertions are unchanged. Pytest never applies this patch.
The initial two-correction checks exposed the optional-field and subject-axis defects above. The subject plot completed subject 1 before failing on subject 2, but even that first plot incorrectly retained all subjects. Completion alone was not evidence of correct subject selection. All input hashes stayed unchanged.
After the optional-field correction, a bounded headless MATLAB check completed the unchanged 18-model-list, one-output R2 call in 13.68 seconds. All returned channels exactly matched the original maximum-F/first-column-major-index rule on the retained R2 arrays. Original reference and input hashes were unchanged, the desktop was disabled and the private engine quit. This validates the corrected reader only, not the full integration workflow. The subject-axis correction subsequently passed live validation for all 18 subjects in 145.29 seconds. All six actual plotted traces per subject had exactly the original channel-50 samples and time coordinates; retained inputs and original sources were unchanged. This validates that plotting segment, not the later preprocessing contrasts, group analyses or final results display.
The remaining preprocessing segment subsequently passed in 876.21 seconds,
executing all 43 original calls through limo_eeg(5, ...). This included
the mean and weighted-mean plots, all 18 subject plots, contrasts, three group
analyses, paired differences and final results display. It used isolated
copies of retained models with path-only metadata relocation; retained inputs,
reused 1,000-bootstrap ANOVA outputs and original sources stayed unchanged.
An earlier segmented attempt failed in 241.65 seconds because its scratch
session lacked the PSOM support path. The successful attempt used native
limo_eeg(2) setup to restore paths normally initialized by the omitted
std_limo stage. This is a successful remaining-segment run, not a fresh
full preprocessing workflow pass or a rerun of the retained bootstrap stage.
With the first four corrections applied, the exact seven-section integration tail failed in 152.20 seconds. All seven section statuses were failures: the first four stopped at nested contrast-file parsing, ANOVA at the warning title, ANCOVA at a text-input check, and repeated-measures ANOVA at optimized channel indexing. Some repeated-measures analyses, 101-bootstrap calculations and contrasts completed before that final failure; those partial outputs do not make the section or the full integration test pass. Retained input hashes and file-tree metadata were unchanged.
A bounded native input probe confirmed that singleton-cell fileparts
outputs produced separate basename and extension entries, and that scalar-only
channel parsing returned NaN. The corrected parser exactly retained the
original scalar and 18- and 36-entry column vectors. Together with the warning
call’s verified argument mismatch, this motivated the three additional
limo_random_select corrections above. The seven-correction rerun completed
in 1,055.19 seconds: regressions, ANOVA, ANCOVA with contrast, and
repeated-measures ANOVA with contrast passed their original section status
checks. One-sample, paired and two-sample t-tests failed later in the native
single-channel TFCE bootstrap loop. The overall seven-section test failed;
original sources, retained input hashes and file-tree metadata were unchanged.
The eighth correction addresses that axis loss in the isolated reference only.
Its bounded live probe passed in 60.22 seconds: it reproduced the original
failure for all three retained t-test outputs, then compared all 303 corrected
bootstrap TFCE vectors, each containing 176 values, exactly against the
unchanged native TFCE algorithm. Original sources and retained inputs stayed
unchanged. The three complete t-test sections then passed in 90.12 seconds,
executing all nine original calls with 101 bootstraps and TFCE enabled;
original sources, retained inputs and file-tree metadata were unchanged.
Together with the four successful sections from the preceding run, all seven
remaining integration sections now have successful corrected-reference
execution evidence. The single-channel t-test correction does not alter the
other four sections’ paths. This is segmented evidence across two recorded
runs, not a single fresh full nine-section integration pass.
Source smoke-test success is not proof of every intended output. A separate,
unchanged native contrast filename mismatch remains: limo_contrast writes
H0/con_1H0.mat, while Level-2 limo_tfce_handling looks for
H0/con_1_desc-H0.mat. The retained ANCOVA50 run produced the former and
observed-data TFCE, but no contrast-null TFCE output. Its source status check
still passed. This native limitation is recorded, not patched or counted as
null-contrast TFCE validation; the eight corrections above do not address it.
Keep the pinned original intact. Create an independent local clone at its
recorded revision and copy the installed plugins/support files, excluding
.git files: the original checkout’s relative submodule pointer is not
valid at another location. Before applying the patch, verify SHA-256 values:
limo_add_plots.m:ed715d6897c3cd8792fc9860ad34465f212d236f1deb733827ac4699bbf2bba7.limo_best_electrodes.m:ab910cd979623d07a45ec9fdb293b3aa6fa7d4b15c982778942cce7f47a9af42.limo_random_select.m:fa35655e25741fe56401b52cde2f9fa520c2491d03b152a8390c16dfdfa07541.limo_tfce_handling.m:f74beb9751e6a47f2665b7640f9edb2476bb4311d8818c36d0b7010f561be1ec.
Apply the patch only in that copy. Verify that these are its only source-file
differences and that MATLAB’s which resolves all four functions there. A
matching Git HEAD alone does not establish an unchanged oracle. Pass the copy
as --eeglab-root and the original suite as --eeglab-suite-root; record
the patch and file hashes with every result. Corrected-reference results must
remain distinct from original-reference failures.
Reusing retained first-level models to execute an unchanged remaining Python
test section provides segmented evidence, not a fresh whole-test pass. Do not
invent successful status entries for earlier sections. Use new output
directories and verify retained inputs are unchanged; operations such as
limo_batch('contrast only', ...) modify model directories and therefore
require isolated input copies too.
The remaining integration sections are not non-graphical: TFCE creates
waitbars, regression/ANCOVA create design figures, and the original ANOVA
orientation can open a warning dialog. skip design check does not disable
these operations. Defer these sections and preprocessing/IRLS graphical work
when desktop use is disallowed; do not silently suppress their source steps
or count an unexecuted section as passing.
Standalone IRLS source correction#
The standalone IRLS contract translates limo_zIRLS_validation_4_Arno.m
and its full limo_test_glmboot helper into Python-owned workflows and
bootstrap-analysis loops. It retains all subjects, cleaning/PICARD/ICLabel/ASR,
ERP/spectrum/ERSP/ITC precomputation, 2,500 null bootstraps per subject, the
1,000-test/1,500-null split, 300-step convergence analysis, six result fields,
results.mat and native .fig exports. Collection and ordinary helper
tests do not establish live validation of this full workflow.
The approved corrections are retained separately in
tests/matlab/limo_zIRLS_validation_4_Arno.source-corrections.patch. It assigns the
otherwise undefined chanlocs variable to
fullfile(STUDY.filepath, 'derivatives', 'limo_gp_level_chanlocs.mat'),
which the reference std_limo generates. It does not invent adjacency data.
It also captures pop_limo’s third output, loads each model from
LIMOfiles.mat{s}, and derives its H0 directory from that returned path.
This uses the generated model paths without duplicating version-dependent
directory names. Scientific options, subjects and bootstrap counts are unchanged.
The third approved correction changes only the validation helper’s discovery
pattern from H0_*.mat to *H0.mat, matching LIMO 4.1.2’s generated
sub-*_desc-Condition_effect_1H0.mat files. Existing Betas, tfce and
R2 exclusions, variable selection and statistical calculations are unchanged.
prepare_irls_source_overlay in tests.eeglab_tests.limo_irls verifies
the original script/helper hashes and applies the patch only to a scratch
copy. Its regression reverses all approved edits to recover both pinned files
byte-for-byte. A corrected native run needs a writable ds002718 copy beside the
returned unittesting_limo directory. The Python contract never executes
the complete native test script or its helper.
Other source behavior remains unchanged. On September 30, 2026, the user stopped exhaustive IRLS execution and accepted an explicitly incomplete execution result for this test-porting deliverable. The full 18-subject, 2,500-bootstrap test remains intact; it was not replaced with a smaller test.
The retained-model tail ran for 48,428.80 seconds before the requested
interruption. Channels 1–23 of the first subject completed their bootstrap
loops; channel 24 was interrupted. This is partial execution evidence, not
23 independently validated channel results or a passed IRLS workflow. No
subject completed the full bootstrap stage, no H0 checkpoint was saved, and
the subsequent error-rate analysis, confidence intervals, final six result
fields, results.mat and figure exports were not validated by this run.
The JUnit result records one InterruptedError failure, not a scientific
assertion failure. The manifest records failed-or-interrupted, no completed
subjects and unchanged retained inputs; six native computation source hashes
were also verified unchanged. The log, manifest and JUnit XML are retained
under .notes/irls-retained-full.XFjjLX in the validation workspace.
All owned runner, MATLAB and worker processes exited, and the scheduled
monitor was removed. Full IRLS execution remains an acknowledged validation
gap, not a requirement to rerun during this closeout. Source-test fidelity,
classified execution evidence and this approved execution exception must not
be summarized as an all-tests-passed result or proof of EEGPrep capability.
Missing Python LIMO primitives remain visible failures, not replacement
algorithms. savefig writes MATLAB .fig files for the IRLS
saveas(..., 'fig') export; that full workflow is still not a completed run.
Test Discovery#
Tests are located in the tests/ directory. Run all tests:
uv run pytest tests
Run specific test file:
uv run pytest tests/test_clean_artifacts.py
Run specific test function:
uv run pytest tests/test_clean_artifacts.py::TestClassName::test_method_name
Run a marker subset:
uv run pytest -m "not slow"
Markers include slow, matlab, octave, gui, visual, and
parity. Legacy unittest tests are categorized during collection in
tests/conftest.py so marker expressions work without rewriting the tests.
Continuous Integration#
Tests run automatically on:
Every push to a branch
Every pull request
Scheduled nightly runs
Check CI status on GitHub Actions.
Nightly differential fuzzing#
.github/workflows/fuzz-nightly.yml compares EEGPrep with EEGLAB on
generated EEG inputs. EEGLAB, reached through the matlab-batch shim, is the
oracle. The job runs at 10:23 UTC and can be started manually with
workflow_dispatch. A known-good sanity case runs first. If that case
fails, the job fails and does not open an issue. Later mismatches are shrunk
with Hypothesis, saved under the fuzz-repros artifact, and filed (at most
three new issues a night) with the labels fuzz and fuzz-unconfirmed.
Per-function absolute and relative tolerances live in
tests/fuzz/tolerances.toml. They are changed only by reviewed human pull
requests.
Run it locally against a MATLAB session that is already serving
tests/matlab/eegprep_batch_engine_serve.m the same way the workflow does:
export EEGPREP_MATLAB_BATCH_DIR=/tmp/eegprep-matlab-batch
export EEGPREP_EEGLAB_ROOT=src/eegprep/eeglab
uv run python -m tests.fuzz run --output fuzz-out --examples 4
Without MATLAB, the fake oracle checks the harness itself:
uv run python -m tests.fuzz run --oracle fake --fake-mode match --examples 2 --output fuzz-out
Replay a saved failure after downloading the artifact:
uv run python -m tests.fuzz reproduce fuzz-out/repros/pop_reref-numeric-1e-3.json
The JSON records the seed, the parameters, and a hash of the input crop from
sample_data/eeglab_data.set. To turn that file into a regression test,
copy it into the tree and assert the two sides agree. Skip the test when
MATLAB is not configured:
import os
import pytest
from tests.fuzz.harness import run_repro
@pytest.mark.matlab
def test_pop_reref_fuzz_repro():
if os.getenv("EEGPREP_SKIP_MATLAB") == "1":
pytest.skip("MATLAB not available")
outcome = run_repro("tests/fuzz/repros/pop_reref-numeric-1e-3.json")
assert not outcome.diverged
Full-Pipeline Parity Harness#
The tests/ suite covers parity function by function. End-to-end parity — does
a complete preprocessing pipeline in EEGPrep reproduce EEGLAB numerically, step
by step, on real data? — lives in a separate repository:
sccn/eegprep_parity_test.
It runs the same seven-step pipeline twice, once in EEGPrep and once in
MATLAB/EEGLAB through the eeglabcompat MATLAB Engine bridge, then reports the
per-step difference for every subject:
Import (BIDS to EEG, select EEG channels)
Average re-reference
clean_rawdata(ASR)Picard ICA (identity init, deterministic)
ICLabel and component removal
Spherical-spline interpolation
Epoch and baseline
For each step it records max_abs_diff and rms_diff in microvolts on the
common channels and minimum length, plus ICA decomposition parity (AMARI
distance, component correlation) and ICLabel rejection counts. Reference
summaries are committed in the harness so a run can be checked against a known
good result with its verify.py.
Why it is not part of tests/#
Data size. The input is a 13-subject, 64-channel BIDS P300 dataset of about 790 MB, tracked with Git LFS. That is far too large to vendor into this repository.
Runtime. A full run over all subjects and steps takes roughly 45 to 60 minutes.
MATLAB requirement. The oracle side needs a licensed MATLAB plus the MATLAB Engine for Python, with the interpreter architecture matching MATLAB’s, and an EEGLAB checkout pointed to by
EEGPREP_EEGLAB_ROOT.
Bit-level agreement also depends on the NumPy and MATLAB stacks sharing a BLAS accumulation order, so results are reproducible per platform pair rather than universally. The harness documents the validated combinations.
Folding a reduced version into tests/ — one subject, a few steps, behind the
existing matlab and slow markers — would be a worthwhile future
improvement. It is not wired up today.
Running it#
git clone https://github.com/sccn/eegprep_parity_test
cd eegprep_parity_test
uv venv --python 3.12 .venv && source .venv/bin/activate
pip install -r requirements.txt
pip install "$MATLABROOT/extern/engines/python"
export EEGPREP_EEGLAB_ROOT=/path/to/eeglab
python run_parity.py # all subjects, all steps
python run_parity.py sub-005 --steps 1 2 3
python verify.py summary_new.md expected_results/summary_arm_R2025a.md
Point the harness at the EEGPrep you want to test by installing it into that
environment, for example pip install -e /path/to/eegprep for a working tree.
See the harness’s own README.md and docs/SETUP.md for the validated
platform combinations and the required EEGLAB patches.
EEG And Session Contracts#
The EEG dictionary, session-selection, and history contracts that feature code must honor are documented for users and extension authors in EEG and Session Contracts.
EEGLAB Core Parity Matrix#
The Phase 1 core parity epic uses a committed machine-readable matrix at
docs/parity/eeglab_core_parity_matrix.json. The matrix classifies the
EEGLAB public and semi-public functions in the scope categories recorded in
its metadata and source issue. It is a work contract for later phase agents,
not package runtime data.
Rows use these statuses:
implemented: EEGPrep already covers the behavior.partial: EEGPrep has an implementation, but important EEGLAB behavior, options, or workflow paths remain.port: the row should be ported or wrapped during the responsible phase.consolidated: EEGPrep covers the behavior through another module or helper, and a duplicate same-name file is not needed.stale_skip: the function is obsolete or stale enough to skip.matlab_runtime_skip: the function is MATLAB-specific runtime, GUI shim, path, deployment, or compatibility behavior that should not exist in standalone EEGPrep package code.external_dependency_skip: the function depends on external MATLAB toolboxes/plugins or web/path integration outside this epic.
Use stale_skip only when every stale-policy field in the matrix is false:
the function is not menu-reachable, not a documented user API, not called by an
in-scope workflow, not required by parity tests, not needed as a helper for the
remaining phases, and not a likely compatibility alias users type. When in
doubt, leave the row as port or partial and add notes for the
responsible phase.
When a later phase implements or intentionally skips a row:
Update
status,eegprep_equivalent,rationale,responsible_phase,user_facing_surface, andtest_notesin the JSON row.For a new
stale_skiprow, include the completestale_policyobject with all fields set tofalseand explain the evidence inrationale.For
partialrows, keep the rowpartialuntil unsupported behavior is implemented or explicitly reclassified with a defensible limitation. Any row left asportorpartialafter a phase closes must cite a concrete follow-up issue infollow_up_issueor, when needed for prose context, inrationaleortest_notes.Run the validator:
uv run --no-sync python -m tools.eeglab_parity_matrix
Run the focused matrix tests:
uv run --no-sync pytest tests/test_eeglab_parity_matrix.py
The validator may read src/eegprep/eeglab because it is development
tooling. Installed package code under src/eegprep must not read, import
from, or shell out to src/eegprep/eeglab. If EEGLAB-like help text,
examples, options, or resources are needed at runtime, convert them into
EEGPrep-owned packaged resources instead of reaching into the vendored
reference tree.
EEGLAB Final Standalone Parity Matrix#
The final standalone parity epic uses a second machine-readable matrix at
docs/parity/eeglab_final_parity_matrix.json. It extends the core matrix into
the remaining product surfaces that are not simple core-function parity rows:
bundled plugin depth, MATLAB object/storage semantics, optional-toolbox
workflows, and documentation/tutorial coverage.
Rows group source files into user workflows, but every discovered final-epic EEGLAB reference path must be covered exactly once. The validator discovers:
plugins/clean_rawdata,plugins/firfilt,plugins/ICLabel, andplugins/dipfitfiles, excluding vendored third-party MatConvNet and Manopt internals, examples, and tests;functions/@eegobj,functions/@memmapdata, andfunctions/@mmo;EEGLAB tutorial scripts under
tutorial_scripts;selected optional-toolbox workflow rows that point back to the core matrix.
Final matrix statuses are implemented, partial, port,
consolidated, stale_skip, matlab_runtime_skip,
optional_dependency, external_plugin, and docs_gap. Non-skip rows
must name a responsible Phase 2-8 issue. Skip rows must use
responsible_phase: "none". optional_dependency rows must name the
backend decision, fallback behavior, user-facing message, and phase contract so
later agents do not silently fake external-toolbox behavior.
Validate the final matrix with:
uv run --no-sync python -m tools.eeglab_final_parity_matrix --json
The docs architecture for the final epic is recorded directly in the matrix
metadata and its source issue. It should be useful to EEG researchers first:
describe EEGPrep’s standalone Python package, Qt GUI, and eegprep-console
behavior accurately, and use EEGLAB comparisons only where they help users
migrate or understand familiar concepts.
Building Documentation#
Build HTML Documentation#
Sync the docs extra, then build with the same command used by the Phase 7 acceptance criteria:
uv sync --group dev --extra docs
uv run --no-sync sphinx-build -b html docs/source docs/_build/html
The docs/Makefile target remains available for local iteration:
uv run make -C docs html
The direct Sphinx command writes to docs/_build/html/. The Makefile target
writes to docs/build/html/.
View Documentation Locally#
Open the built documentation in your browser:
open docs/build/html/index.html # macOS
xdg-open docs/build/html/index.html # Linux
start docs/build/html/index.html # Windows
Or use a local server:
cd docs/build/html
uv run python -m http.server 8000
Then visit http://localhost:8000 in your browser.
Clean Build#
Remove old build files and rebuild:
uv run make -C docs clean
uv run make -C docs html
Build Options#
Build PDF documentation (requires LaTeX):
uv run make -C docs latexpdf
Build EPUB documentation:
uv run make -C docs epub
Debugging Tips#
Logging#
Enable debug logging in your code:
import logging
# Set up logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# Use logging in your code
logger.debug("Debug message")
logger.info("Info message")
logger.warning("Warning message")
logger.error("Error message")
Breakpoints#
Use Python’s built-in debugger:
import pdb
def my_function():
x = 10
pdb.set_trace() # Execution pauses here
y = x + 5
return y
Or use the newer breakpoint() function (Python 3.7+):
def my_function():
x = 10
breakpoint() # Execution pauses here
y = x + 5
return y
Profiling#
Profile code performance:
import cProfile
import pstats
# Profile a function
profiler = cProfile.Profile()
profiler.enable()
# Your code here
my_function()
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats(10) # Print top 10 functions
Memory Profiling#
Install memory profiler:
uv add --dev memory-profiler
Use it in your code:
from memory_profiler import profile
@profile
def my_function():
large_list = [i for i in range(1000000)]
return sum(large_list)
Run with:
uv run python -m memory_profiler script.py
Release Process#
Releases are published by .github/workflows/release.yml when a v* tag is
pushed. See Releasing for the full procedure, including the dry run and the
separate Docker image step.
EEGPrep uses Semantic Versioning: MAJOR for incompatible API changes, MINOR for backward-compatible functionality, and PATCH for backward-compatible bug fixes.
The version lives only in src/eegprep/__init__.py; pyproject.toml reads it
via dynamic = ["version"].
Common Issues#
Import Errors#
Problem: ModuleNotFoundError: No module named 'eegprep'
Solution: Install the package in editable mode:
uv sync --group dev
Test Failures#
Problem: Tests fail with import errors
Solution: Ensure you’re in the virtual environment and dependencies are installed:
uv sync --group dev
uv run pytest tests
Documentation Build Errors#
Problem: Sphinx build fails with missing modules
Solution: Install documentation dependencies:
uv sync --extra docs --group dev
Git Conflicts#
Problem: Merge conflicts when pulling upstream changes
Solution: Resolve conflicts manually:
git fetch upstream
git rebase upstream/develop
# Resolve conflicts in your editor
git add .
git rebase --continue
Virtual Environment Issues#
Problem: Virtual environment not activating
Solution: Recreate the virtual environment:
rm -rf .venv
uv sync --group dev
Dependency Conflicts#
Problem: Dependency version conflicts
Solution: Refresh the locked environment:
uv lock
uv sync --group dev
Getting Help#
Check the Contributing to EEGPrep guide
Review existing GitHub Issues
Ask in GitHub Discussions
Contact the maintainers
Happy developing!