Skip to content

Merge develop into the browser epic - #403

Merged
neuromechanist merged 46 commits into
epic/324-pyodide-browserfrom
chore/merge-develop-into-epic
Sep 21, 2026
Merged

neuromechanist merged 46 commits into
epic/324-pyodide-browserfrom
chore/merge-develop-into-epic

Conversation

@neuromechanist

Copy link
Copy Markdown
Member

Why

The epic branch was 45 commits behind develop, and the browser work is being built on top of
it. Too many moving parts to keep stacking on a stale base.

It also meant the epic branch did not have #397's resample fix, #399's Python 3.12 floor, or any
of develop's MemmapData work.

Conflicts, and how each was resolved

File Resolution
pyproject.toml Develop's Pyodide-bounded floors, but this branch's extras split kept. Develop still carries oct2py in the base install; taking its side wholesale would have undone phase/374. Base is 14 deps, oct2py/psutil/pyedflib stay in extras.
.github/workflows/docs.yml This branch's iclabel extra, develop's 3.12 pin.
.github/workflows/release.yml This branch's ICLabel wheel smoke test and heredoc, develop's 3.12 pin.
src/eegprep/functions/adminfunc/storage.py Both sides added to the same class; both kept. This branch's mutation tracking, develop's _BackingFile / _validate_backing_file / empty().
docs/source/changelog.rst Both sides appended to Unreleased; both kept.
docs/source/api/ica_and_components.rst Develop's newer API entries. This branch's list was simply older.
uv.lock Regenerated, not hand-merged.

Four more 3.11 pins came in from this side without conflicting and are bumped too: the sdist
smoke test in release.yml and the Pyodide harness job in test.yml. Only the quoted MATLAB
error message still contains "3.11".

Two bugs the merge exposed

Neither was findable on this branch alone, which is the argument for not letting it drift again.

1. MemmapData.__array_function__ broke every numpy function on a mapped array. It returned
NotImplemented for everything except copyto and put. Defining that method opts the type into
numpy's dispatch protocol, and there NotImplemented raises rather than falling back, so:

TypeError: no implementation found for 'numpy.size' on types that implement
__array_function__: [<class '...storage.MemmapData'>]

np.size(data) in pop_reref.py:297 is one caller of many. Develop's #351 mmo memmap parity
test covers this path and this branch never had that test. It now delegates to the mapped array,
unwrapping nested handles as well so np.concatenate([a, b]) does not dispatch straight back into
itself and recurse.

2. The tomli backport in tools/check_pyodide_base_resolution.py is dead on a 3.12 floor,
and ty flagged its now-unused ignore directive.

Verification

  • 3196 passed, 277 skipped, 0 failed with MATLAB skipped
  • ruff check and ruff format --check clean
  • ty check clean, no diagnostics
  • ./pre-commit.py --all-files OK over 1221 files
  • uv lock --check in sync
  • Delegation verified by hand: np.size, np.mean, np.stack, and np.concatenate on nested
    handles all work, and np.copyto still advances the mutation revision

Note on threadpoolctl

I previously described threadpoolctl>=3.6.0 on this branch as a live browser-install bug. That
was wrong
and is corrected in #402 and on #400: threadpoolctl publishes a py3-none-any wheel,
so micropip installs any version from PyPI. The floor moves to >=3.5.0 here only because that is
what develop carries, not because the old value was broken.

Related: #324, #386, #399.

* Add provenance for current EEGLAB test ports

* Add MATLAB fixture loader for test ports
* Port current pop regression tests

* Port core popfunc conversion tests

* Port ICA rejection helper tests

* Port current pop utility workflows
* Port current EEGLAB admin and GUI tests

* Remove internal implementation notes
* Port current EEGLAB sigprocfunc tests

* Contain topoplot finite matrix warnings

* style: format pop reref tests
* Port current dataset workflow tests

* Port current pop_eegfilt test
* Port EEGLAB numerical utility tests

* fix: harden numerical utility contracts
suraj-ranganath and others added 16 commits September 18, 2026 17:31
* feat: implement standalone LIMO workflows

* fix: validate LIMO design cells
* test: adjudicate remaining EEGLAB methods

* test: port current EEGLAB tutorial workflows

* test: port face BIDS tutorial workflow

* test: add EEGLAB port completeness audit

* test: make live MATLAB validation exhaustive
…py (#397)

* Fix the resampling ratio for non-integer sampling rates

EEGLAB computes the ratio as rat(freq/EEG.srate, 1e-12), pop_resample.m
line 119. This was ported as sympy.nsimplify, which is a different
function: it looks for a simple symbolic expression rather than a
rational one, and returns an irrational when no simple fraction is close
enough. as_numer_denom then hands back a numerator that is not an
integer and int() truncates it silently.

A 2000 Hz recording resampled to 999.9 Hz was therefore resampled by
5/12 instead of 9999/20000, a 16.7 percent error, while the output was
stamped with the rate that was asked for. The event latencies were
rescaled by the same wrong ratio, so the result stayed internally
consistent and gave the reader nothing to notice. Across realistic
downsampling pairs, 24 of 86 came back wrong, all of them with a
fractional source rate; the worst produced 117.04 Hz labeled 128 Hz.

_rational_within expands the continued fraction one term at a time and
stops at the first convergent inside the tolerance, which is what rat
does and which returns the smallest denominator that will do. That
matters beyond tidiness, because the denominator sets the anti-aliasing
filter length: an earlier draft searched by doubling a denominator cap,
which can overshoot the minimal answer and buy filter taps for nothing.

The ratio tests fail 28 subtests against the previous implementation,
and the end-to-end test added here fails against it too, computing
21/84 where the correct ratio is 2560/9999. An earlier draft of that
test used whole-number rates only and passed against the defect, which
made it worthless as a guard.

Also logs a warning when the ratio's denominator is large. 128 Hz from a
512.03 Hz recording is 12800/51203, which builds a filter of about 1.64
million taps and costs seconds per minute of 64-channel data against
milliseconds for a whole-number pair. That cost is EEGLAB's, inherited
with its tolerance, and correct rather than a defect, but a resample
that appears to hang should say why.

* Drop sympy, which no longer has a caller

The nsimplify the previous commit replaced was the only use of sympy
anywhere in src/, so it leaves the base install.

Verified by running the suite with sympy absent from the environment
rather than merely absent from pyproject.toml, which is the only check
that means anything here: eeglabio has no import site in src/ either,
and is not removable, because mne imports it when asked to write an
EEGLAB file and eeg_mne2eeg exports through mne.

The uv.lock edit removes the two lines naming sympy in eegprep's own
dependency lists, and is scoped to eegprep's package block. An earlier
draft used an unscoped replace and took torch's sympy edge with it,
orphaning the package entry: uv lock --check still passed, because the
lock stayed self-consistent, and no CI job installs the torch extra with
--locked, so nothing would have caught it.
… ships (#399)

* Require Python 3.12

The browser target sets this, in the opposite direction to the obvious
one. Pyodide 0.29.5 ships CPython 3.13.2 and its wheels are cp313, so
requires-python is a ceiling rather than a floor: 3.14 would make
eegprep uninstallable in the browser until Pyodide moves. 3.12 keeps one
version of headroom and lets CI cover both 3.12 and the 3.13 the browser
actually runs.

The matrix drops 3.10 and 3.11 and gains 3.13. tomllib has been in the
standard library since 3.11, so the tomli backport and its two
try/except shims are dead code and go with it.

* Raise dependency floors, bounded by what Pyodide ships

Two of these are live bugs rather than tidying.

threadpoolctl was declared >=3.6.0 while Pyodide 0.29.5 ships 3.5.0, so
the floor is above what the browser can provide and a browser install
cannot satisfy it. matplotlib>=3.9.2 would have introduced the same bug,
since Pyodide has 3.8.4; 3.8.0 is the newest floor that does not.

The rest were fiction in the other direction: numpy>=1.20 ships cp37
through cp39 wheels and cannot install on the Python this package now
requires. Floors are now the first release with real 3.12 support,
capped by the version Pyodide ships: numpy 2.1.0, scipy 1.14.1,
matplotlib 3.8.0, h5py 3.12.1, threadpoolctl 3.5.0.

scipy is split by platform. 1.14.1 is exactly what Pyodide has, so the
general floor cannot go higher, but scipy built before 1.16.3 fails to
dlopen on current macOS with a zero-fill section error, so darwin gets
its own floor.

* Pin every CI job to Python 3.12

The 3.12 floor landed in the test matrix but not in the jobs that
install a single interpreter, so docs, release, ruff/ty, and the
Claude workflows all still asked for 3.11 and uv refused:

  error: The requested interpreter resolved to Python 3.11.16, which
  is incompatible with the project's Python requirement: `>=3.12`

Tested: the failing jobs were "Ruff and ty" and "Build Documentation".

* Retire the last 3.10 and 3.11 references

The floor moved but the surrounding claims did not: the README badge,
the AGENTS notes, four docs pages, and the six example extensions all
still advertised 3.10 or 3.11, and ty was still type-checking against
3.10 semantics.

pre-commit.py carried a tomli backport for 3.10 and a matching
"tomli is not installed; skipping TOML syntax checks" branch. On a
3.12 floor tomllib is always stdlib, so both are dead, and the branch
was a silent skip of a check the script claims to run.

Tested: ruff check and format clean, ty clean apart from three
pre-existing unresolved torch imports (the local venv has no torch
extra), tests/test_extension_catalog.py 22 passed, and
./pre-commit.py --all-files reports OK with config syntax checked.

* Let CI test 3.13 where MATLAB cannot follow

The 3.13 job failed on the MATLAB engine install, not on eegprep:

  MATLAB Engine for Python supports Python version 3.9, 3.10, 3.11,
  and 3.12, but your version of Python is 3.13

uv sync had already succeeded, so every dependency floor resolves on
3.13; only MathWorks' engine has no build for it. The workflow already
treats MATLAB as optional and falls through to a plain pytest run when
the engine will not start, but the install step aborted the job before
reaching that check. Tolerate the install failing so the fall-through
runs, which also means this starts working by itself once a MATLAB
release covers 3.13, rather than encoding a compatibility table in CI.

* Fix a parity-harness instruction the new floor broke

development.rst told the reader to build the parity harness venv with
--python 3.11 and then pip install -e this working tree into it. Under
requires-python >=3.12 that install now fails, so the instruction was
not merely stale, it no longer worked.

Also split the two-sentence line in faq.rst onto its own lines, per the
semantic line break convention.

* Stop pre-commit reporting OK with YAML unchecked

Removing the tomli backport fixed the TOML half of a false green: a
missing parser printed one warning, skipped every file of that type,
recorded no error, and let the run print OK. The YAML half was left
behind with the identical shape.

pyyaml is declared in this script's inline dependencies, so uv installs
it before the body runs and the only way to reach the fallback is to
start the script without uv. That is a misuse, not a degraded
environment, so it now exits with the invocation to use instead of
quietly checking nothing.

Also corrects the header, which still said config parsing happens "when
dependencies are available". It is now unconditional.

Tested: ./pre-commit.py --all-files reports OK over 1221 files with
config syntax checked.

* Test that the floors stay installable in the browser

Pyodide bundles its own build of every compiled package, so a floor
above what a Pyodide release ships cannot be satisfied in the browser.
That makes these floors and requires-python a ceiling, which is the
opposite of how a floor reads and is invisible from the code.

The epic branch's tools/check_pyodide_base_resolution.py was supposed
to catch this and does not: it matches package names against the
Pyodide lock and never compares versions, so threadpoolctl>=3.6.0 sits
on that branch today against a distribution shipping 3.5.0, and the
gate calls it ok. Filed as #400.

These tests close the gap on develop, offline, against the versions
Pyodide 0.29.5 bundles. Verified non-vacuous by reintroducing the real
bug: threadpoolctl>=3.6.0 fails two of them by name, and a
requires-python of >=3.14 fails the interpreter check, since Pyodide
runs CPython 3.13.2.

Also records the constraint above the floors it governs, which was
documented for scipy alone, and bumps the ruff target to py312, missed
when the ty target moved.

The catalog fixture's python_requires went back to a value no
interpreter bump can disturb; pinning it to the project floor coupled
an unrelated happy path to every future bump. The field's real
behavior, an unsatisfiable floor and a malformed specifier, now has the
two tests its eegprep_requires sibling already had and it did not.
The epic branch was 45 commits behind develop and building on it meant
building on a stale base. Seven files conflicted.

pyproject.toml: took develop's Pyodide-bounded floors, but kept this
branch's extras split. Develop still carries oct2py in the base install
and taking its side wholesale would have undone phase/374.

docs.yml and release.yml: kept this branch's ICLabel jobs, took
develop's 3.12 pins. Four more 3.11 pins came in from this side without
conflicting, in the sdist smoke test and the Pyodide harness job, and
are bumped too.

storage.py: both sides added to the same class. This branch's mutation
tracking and develop's _BackingFile, _validate_backing_file and empty()
are all kept.

changelog.rst and ica_and_components.rst: both sides appended; kept
both, and develop's newer API entries.

uv.lock: regenerated rather than hand-merged.

Two bugs the merge exposed, neither findable on this branch alone:

MemmapData.__array_function__ returned NotImplemented for everything
except copyto and put. Defining that method opts the type into numpy's
dispatch protocol, where NotImplemented raises rather than falling
back, so every other numpy function failed on a MemmapData.
np.size(data) in pop_reref is one caller of many. Develop's #351 test
covers it and this branch never had that test. It now delegates to the
mapped array, unwrapping nested handles so np.concatenate([a, b]) does
not dispatch straight back into itself.

The tomli backport in check_pyodide_base_resolution.py is dead on a
3.12 floor and ty flagged its ignore directive.

Tested: 3196 passed, 277 skipped, 0 failed with MATLAB skipped. Ruff
check and format clean, ty clean with no diagnostics, pre-commit OK
over 1221 files.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, reopen this pull request to trigger a review.

@claude

claude Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@neuromechanist
neuromechanist merged commit e9f1017 into epic/324-pyodide-browser Sep 21, 2026
12 of 13 checks passed
@neuromechanist
neuromechanist deleted the chore/merge-develop-into-epic branch September 21, 2026 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants