From 09290d18c347b214bd5636f3e3d7754990653c85 Mon Sep 17 00:00:00 2001 From: Seyed Yahya Shirazi Date: Tue, 22 Sep 2026 14:09:18 -0700 Subject: [PATCH 01/26] Add epic #324 plan of record --- .context/issue-324/plan.md | 229 +++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 .context/issue-324/plan.md diff --git a/.context/issue-324/plan.md b/.context/issue-324/plan.md new file mode 100644 index 00000000..d821478b --- /dev/null +++ b/.context/issue-324/plan.md @@ -0,0 +1,229 @@ +# Plan: Make MLX a first-class, end-to-end pamica backend (epic) + +Integration branch: `dev` (repo policy; `main` is release-only). +Epic branch: `feature/issue-324-epic-mlx-first-class`, worktree `../epic-mlx-first-class`. +Phase PRs squash-merge into the epic branch; the epic merges into `dev` with a regular merge commit. + +## Context + +The MLX backend reached full fitting and non-fitting parity with `AMICATorchNG` in epic #278, +but it is still not usable end to end. +Every MLX user is a raw-`AMICAMLXNG` user: the `AMICA` and `AMICAICA` wrappers construct torch unconditionally (#313), +MLX has no explicit `pcakeep`/`pcadb` (#323), +and no params-file path (#304). +We are the first users (per-session AMICA on 64-channel BioSemi EEG on Apple Silicon, `pcakeep = nbchan - 1` after average reference), +so the bar is a workflow we trust, not a feature checklist. + +Evidence gathered while planning (2026-09-22, all on the bundled real EEG sample, 32 channels x 30504 frames): + +1. `AMICAICA.apply` after a `pcakeep=20` fit, with nothing excluded, returns data with relative error 0.080 against the input: + the discarded 12-dimensional PCA residual is silently dropped (#322). + Exporting the full PCA basis instead gives 1.2e-15, excluding a component leaves the residual untouched (1.2e-15), + `get_sources` is unchanged (2.3e-15), and `apply(n_pca_components=n_components_)` reproduces today's output exactly (0.0). + MNE's own `ICA` keeps the full PCA basis and restores the residual by default (`ICA._pick_sources`, MNE 1.12.1). +2. `AMICATorchNG` validates neither `pcakeep` nor `pcadb`: + `pcakeep=-3` silently fits 29 sources (negative slice), `pcakeep=2.7` silently truncates to 2, + `pcakeep=0` and `pcadb<=0` produce a degenerate `nan_ll` fit. +3. `AMICATorchNG`'s upfront `mir_step` gate rejects `pcakeep=32` on 32-channel data (no reduction happens); + that is the value in the bundled `input.param` and `sample_params.json`. +4. Fortran reference review (`amica15.f90`): + `numeigs = min(pcakeep, count(eigs > mineig))` (413, 466); + the written `S` is `nx x nx`, and in the default `do_approx_sphere` path its rows beyond `numeigs` are zero (501-508), + so the reference output carries no representation of the discarded subspace; + `pcadb` is parsed (3459-3461) but never used. + pamica's EEGLAB export already pads a reduced sphere with zero rows (`numpy_impl/load.py`, `write_amicaout`). +5. With the JSON-schema spellings mapped to canonical names (`min_grad_norm->min_nd`, `max_decs->maxdecs`, `share_int->share_iter`), + `sample_params.json` and `input.param` agree on all 47 shared keys except the data path `files`. + Without the mapping, `AMICA.from_params_file(sample_params.json)` does not apply `max_decs`/`min_grad_norm`/`share_int`. + +## What already exists to reuse (do not rebuild) + +- `pamica/rank.py::numerical_rank`: the shared rank decision, called by all three array backends. +- `AMICATorchNG._pca_reduction_requested` / `_pca_reduced` and the MLX `_pca_reduced` port. +- `AMICAICA.to_mne_ica`: retained-basis extraction (eigh for a square sphere, SVD for a reduced one). +- `pamica/fortran_params.py::read_fortran_param_file` and `FORTRAN_TO_PAMICA_KEY`. +- `AMICA.from_params_file` content sniffing and `AMICA.fit`'s per-call file-default merge. +- `AMICAMLXNG` full public surface (transform, accessors, mir/pmi, LLt scoring, variance_order, EEGLAB export, state_dict/save/load). +- Test patterns: `pamica/tests/test_rank_policy.py` (cross-backend anti-drift), `pamica/tests/test_mlx_*_cross_backend.py`, `pamica/tests/mne_tests/`. + +## Phases + +| # | Slug | Issue | Implementer | Wave | Target | +|---|---|---|---|---|---| +| 1 | pca-policy | #323 | Opus | 1 | epic branch | +| 2 | pca-residual | #322 | Opus | 1 | `dev` directly (see judgment call 1) | +| 3 | params-surface | #304 | Sonnet | 1 | epic branch | +| 4 | backend-selection | #313 | Opus | 2 | epic branch | +| 5 | backend-guards | #306 | Sonnet | 2 | epic branch | +| 6 | e2e-validation | #315 | Opus | 3 | epic branch | + +Every phase: `uv run ruff check --fix . && uv run ruff format .`, `uv run ty check`, full `uv run pytest` (MLX suites run on this host), +existing Fortran-parity tests green (default path bit-identical), atomic commits, PR reviewed with `/review-pr` (pr-review-toolkit, Sonnet), all findings addressed. +Implementers never run `pre-commit install` in a worktree (shared hooks hazard). + +### Phase 1: pca-policy (#323) + +Decided policies: + +- `pamica/rank.py` gains `validate_pca_reduction(pcakeep, pcadb) -> None`: + `pcakeep` is `None` or an integral (`numbers.Integral`, `bool` rejected) `>= 1`; + `pcadb` is `None` or a real (`numbers.Real`, `bool` rejected), finite, `> 0`; + otherwise `ValueError` naming the parameter and value. + Both set is allowed: `pcakeep` takes precedence and `pcadb` is ignored, matching the reference (which ignores `pcadb` entirely); + both bundled param files set both. Documented, logged at INFO once at construction. +- `pamica/rank.py` gains `pca_reduction_requested(pcakeep, pcadb, n_channels) -> bool`: + `pcakeep` set: `pcakeep < n_channels`; else `pcadb` set: `True` (cannot be known before the eigenvalues); else `False`. +- `numerical_rank` calls `validate_pca_reduction` first (single fit-time choke point, catches post-construction attribute changes). +- All three constructors validate (fail fast). Torch `_pca_reduction_requested` delegates to the shared predicate with the fitted data's channel count. +- MLX constructor gains `pcakeep: Optional[int] = None, pcadb: Optional[float] = None` (same position and docstring semantics as torch), + passes them to `numerical_rank`, gains `_pca_reduction_requested` and the same upfront `mir_step` gate and message as torch, + and persists both in `state_dict()["config"]` (additive, like `keep_best`: a payload without them loads with `None`, no format bump). +- Rewrite every docstring, comment and error message that says MLX has no explicit `pcakeep`/`pcadb` + (`mlx_impl/core.py` around 2405-2440, 3240-3310) and the differences-page section "`mir_step`'s upfront PCA-reduction gate: MLX has none". + +Deliverables: `pamica/rank.py`, `torch_impl/core.py`, `numpy_impl/core.py`, `mlx_impl/core.py`, +`docs/api/mlx-backend.md`, `docs/guides/amica-differences.md` (backend-table row "Explicit `pcakeep`/`pcadb`"; `pcadb` recorded as a pamica extension), `docs/changelog.md`, +tests below. + +Decision gate (tests, pre-registered): + +1. `test_rank_policy.py`: validator accepts/rejects the listed cases (including `True`, `2.7`, `0`, `-3`, `np.int64(5)` accepted, `nan`/`inf`/`0`/`-5` pcadb); predicate truth table; `numerical_rank` raises on invalid input. +2. New `pamica/tests/test_pca_reduction_cross_backend.py` on the real sample: + (a) `pcakeep=20`: torch, NumPy and MLX all fit 20 sources with a `(20, 32)` sphere; torch vs NumPy sphere `allclose(rtol=1e-10)`; torch vs MLX within float32 tolerance; `sldet` agrees; + (b) same for a `pcadb` value that reduces the sample (implementer picks and records it); + (c) `pcakeep` + `pcadb` equals `pcakeep` alone on every backend; + (d) invalid values raise `ValueError` at construction on every backend; + (e) `mir_step > 0`: torch and MLX both reject `pcakeep < n` up front and both accept `pcakeep >= n`; + (f) torch vs MLX after a short fit with `pcakeep=20`: Hungarian-matched component correlation at or above the threshold the existing MLX cross-backend suites use (cite it); + (g) MLX `save`/`load` round trip keeps `pcakeep`/`pcadb` and the reduced sphere; a payload with those config keys removed still loads. +3. Regression: `AMICA.from_params_file(input.param)` then `fit(mir_step=1)` no longer raises. +4. MLX via `pytest.importorskip`; torch and NumPy unconditional. + +### Phase 2: pca-residual (#322) + +Decided policies: + +- The residual is restored by default, through MNE's native mechanism: + `to_mne_ica` exports the full orthonormal PCA basis (`n_channels x n_channels`) and the full `pca_explained_variance_`; + `n_components_` stays the retained rank; `n_pca_components` stays `None`, so MNE's `apply` keeps every PCA component (MNE's own `ICA` default). + Reference (rank-reduced) behavior: `apply(..., n_pca_components=ica.n_components_)`. No new keyword. +- The basis is computed once in `AMICAICA.fit` and stored as documented fitted attributes (suggested names mirror MNE: `pca_components_`, `pca_explained_variance_`); + nothing is recomputed from data at `apply` time and no time series is stored. + Retained rows: exactly today's `to_mne_ica` computation from the fitted sphere. + Residual rows: null space of the fitted sphere (full SVD), rotated to the eigenvectors of the pre-whitened fit data's population covariance restricted to that null space, + descending variance, variances clipped at 0. + Covariance accumulated in column blocks (two pass: mean, then centered blocks); no full-size data copies (306-channel MEG must stay cheap). +- Full-rank fits: export unchanged. The first commit is a pin test captured against the current code. +- `to_mne_ica` logs at INFO when a residual is exported: its dimension and the opt-out. +- Documentation must state plainly that this goes beyond the reference (user requirement): + ADR `.context/decisions/NNNN-restore-pca-residual-in-mne-export.md`; + a `docs/guides/amica-differences.md` section covering what the reference does (the model spans `numeigs` dimensions; `S` rows beyond `numeigs` are zero in the default path, amica15.f90:501-508; back-projection from the reference output, for example EEGLAB's through the `nchan x nw` `icawinv`, reconstructs only the retained subspace, dropping the residual without notice), + what pamica does, why (MNE's own default; the residual was never part of the decomposition, so it is not ICA's to remove), + how to restore the reference behavior, and that sources, maps and likelihood are unchanged (only reconstruction changes); + explicit statements in the `AMICAICA` class Notes and the `fit`, `to_mne_ica` and `apply` docstrings; + `docs/api/mne-compat.md`; a changelog entry flagged as a behavior change. + +Decision gate (tests on real data, pre-registered): + +1. Pin: full-rank export attributes and `apply` output identical to the pre-change code. +2. `pcakeep=20`: `apply()` with no exclusions reproduces the input (relative error `<= 1e-10`); + `apply(n_pca_components=n_components_)` equals the retained-subspace reconstruction; + `exclude=[k]` changes the data by exactly minus component `k`'s back-projection, with a residual-subspace component `<= 1e-10` relative; + `get_sources` equals `amica_.transform` at the existing tolerance; + residual variances equal the discarded covariance eigenvalues (`rtol=1e-8`); `pca_components_` orthonormal. +3. `n_models=2` with `pcakeep`: every exported model restores the residual (the `c` offset does not leak into it). +4. `Epochs` input; `ica.save` then `mne.preprocessing.read_ica` gives the same `apply` output; float `n_pca_components` works. +5. Mixed channel types (non-uniform `pre_whitener_`): residual restored in original units. +6. Automatic rank reduction (average-referenced EEG, rank 31, no `pcakeep`): `apply()` with no exclusions reproduces the input. + +### Phase 3: params-surface (#304) + +Decided policies: + +- `pamica/fortran_params.py` gains `read_params_file(path) -> dict`, the single params-file entry point: + JSON when the first non-whitespace character is `{` or `[` (top level must be an object), Fortran text otherwise. + Both formats return canonical pamica keys: JSON keys pass through one alias table + (`min_grad_norm->min_nd`, `max_decs->maxdecs`, `numrej->maxrej`, `num_mix_comps->num_mix`, `share_int->share_iter`); + a JSON file carrying both an alias and its canonical key raises `ValueError`. +- `writestep`, `do_history`, `histstep` move from unsupported to translated (identity): + the reader translates what any backend supports, and each consumer warns about what it cannot apply (the wrapper already does; #312 tracks torch/MLX). +- `AMICA.from_params_file` uses `read_params_file`. + Behavior change: `sample_params.json`'s `max_decs`/`min_grad_norm`/`share_int` are now applied (changelog). +- `AMICA_NumPy(params_file=...)` and `load_default_params` use `read_params_file`, then map canonical keys to NumPy's spellings + (`min_nd->min_grad_norm`, `maxdecs->max_decs`, `share_iter->share_int`); NumPy's keyword spellings are unchanged. + Data-location keys come from the same parsed dict (no second `json.load`). + Keys NumPy does not consume produce one `logger.warning` naming them. +- NumPy `pdftype != 0` raises `NotImplementedError` at construction (the legacy backend implements only the generalized Gaussian); + `numpy_impl/params.json` default `pdftype` changes 1 -> 0; the module docstring states the reason (per `.rules/backend_parity.md`). +- MLX's params-file path is `AMICA.from_params_file(..., backend="mlx")`, delivered in Phase 4; this phase's differences-table row says so. + +Decision gate (pre-registered): + +1. Anti-drift: `read_params_file(sample_params.json)` and `read_params_file(input.param)` agree on every shared key except `files`. +2. Alias/canonical conflict, non-object JSON and unparseable text each raise `ValueError`. +3. NumPy from `input.param` equals NumPy from `sample_params.json` on every shared hyperparameter, and `fit()` with no data runs from `input.param` (test chdirs to `sample_data`, like the binary). +4. NumPy `pdftype` 1-4 raise `NotImplementedError`; 0 fits. +5. Wrapper from `sample_params.json` applies `maxdecs=3`, `min_nd`, `share_iter` to the backend, and the "not applied" warning no longer lists them. +6. NumPy's unconsumed-key warning names the keys; `writestep`/`do_history`/`histstep` from Fortran text reach NumPy, and the wrapper warns they are not applied. + +### Phase 4: backend-selection (#313), detailed plan before it starts + +Decided policies: + +- `AMICA(..., backend="torch")`, accepting `"torch"` and `"mlx"`; anything else raises `ValueError`; + `"mlx"` without MLX installed raises `ImportError` with the install hint at construction. + Default stays `"torch"` (float64 Fortran parity); no `"auto"`. +- `device` and `dtype` are torch-only; passing either with `backend="mlx"` raises `ValueError` (MLX is float32-only). +- The constructor-kwarg and file-param partitions are computed per backend from the backend class signature (one helper), + so `AMICA.from_params_file(path, backend="mlx")` works and unapplied keys are warned per backend. +- The #50 degenerate-fit contract, `converged_`/`is_fitted_`/`stop_reason_`, restart records and `mir_history_` behave identically for both backends. +- One wrapper save format for both backends; the payload records its backend and `load` dispatches; + a payload without the field is a torch payload (the only kind written before this change); + loading an MLX payload without MLX installed raises `ImportError` naming the backend. +- `AMICAICA(..., backend="torch")` forwards to `AMICA`; `device` torch-only; + `to_mne_ica` and the Phase 2 residual computation read mean, sphere and `c` through one backend-agnostic float64 accessor (no `.cpu()` in `mne_compat`). +- End-to-end tests, both backends (MLX `importorskip`): wrapper fit, transform, save, load, transform; + `from_params_file(input.param, backend="mlx")`; `pcakeep` through the wrapper (closes #323's third bullet); + `AMICAICA(backend="mlx")` on real EEG: `get_sources` equals `transform` (float32 tolerance), residual restored by `apply`, `ica.save`/`read_ica`; + wrapper EEGLAB export from MLX reloads through `loadmodout`; + torch vs MLX through the wrapper: same `n_components_`, Hungarian-matched component correlation at the existing threshold. +- Docs: `docs/guides/backends.md`, `docs/api/mne-compat.md`, differences page (drop the torch-only-wrapper caveat), README quick start, the `amica.py` line in `AGENTS.md`. + +### Phase 5: backend-guards (#306), detailed plan before it starts + +The three #306 items (degenerate-fit guards on raw accessors, `transform` input validation, named `from_state_dict` config errors), +identical on torch and MLX, one shared test per item in `pamica/tests/`. + +### Phase 6: e2e-validation (#315), detailed plan before it starts + +- `validate_implementations.py --backend {torch,numpy,mlx}` (default `torch`, unchanged output), producing each backend's row against the Fortran binary. +- An end-to-end workflow test mirroring our own use: + average-referenced real EEG, `pcakeep = nbchan - 1`, fit through `AMICA(backend="mlx")` and `AMICAICA(backend="mlx")`, + EEGLAB export and reload, MNE `apply` with residual restoration, params-file driven run. +- Docs: an Apple Silicon (MLX) path in `docs/getting-started.md`; the validation guide's parity table gains the NumPy and MLX harness rows. + +## Prerequisites (user actions) + +None. MLX, MNE and the Fortran binary are available on this host. + +## Agent budget + +Six implementers (one per phase; at most three concurrent), plus one pr-review-toolkit run per phase PR and one for the epic PR (Sonnet specialists). + +## Open judgment calls (veto any) + +1. Phase 2 (#322) ships straight to `dev`, not through the epic branch: it is the external tester's issue and independent of MLX. The epic branch merges `dev` before Phase 4 (precedent: epic #278 fast-forwarded from `dev`). +2. The residual basis is computed by the MNE wrapper at fit time, not stored in the three backends' persisted state: the fitted sphere already defines the retained subspace, so this avoids three persistence-format changes ("PCA is cheap"). +3. `pcakeep` and `pcadb` together are allowed, `pcakeep` wins (the reference ignores `pcadb`; both bundled param files set both). +4. New validation turns previously silent bad values (`pcakeep <= 0` or non-integer, `pcadb <= 0`) into `ValueError`. +5. The `mir_step` gate no longer rejects `pcakeep >= n_channels`. +6. JSON key canonicalization changes what `AMICA.from_params_file(sample_params.json)` applies. +7. NumPy `pdftype != 0` raises; the NumPy `params.json` default changes from 1 to 0. +8. `writestep`/`do_history`/`histstep` are translated from Fortran text (NumPy applies them; the wrapper warns). +9. The wrapper's backend default stays `"torch"`; no `"auto"`. +10. Scope: #306 and #315 join the epic for end-to-end trust; #307, #312, #314, #317, #319, #320 stay out. + +## Verification + +Each phase's gate tests above, run by the implementer and re-run by the lead before merge (the lead also spot-checks cited lines and keystone numbers). +Epic PR: full suite on the epic branch, `validate_implementations.py` for every backend, pr-review-toolkit review, then user approval of the final merge. From 98e510ef8bba8553712eb2c3c00fe32293bd48e6 Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Tue, 22 Sep 2026 16:02:54 -0700 Subject: [PATCH 02/26] Phase 1: shared pcakeep/pcadb policy and MLX support (#326) * Add shared pcakeep/pcadb validation policy validate_pca_reduction rejects non-integer or < 1 pcakeep and non-finite or <= 0 pcadb; pca_reduction_requested answers the upfront mir_step question; numerical_rank re-validates at fit time. Rank policy unit tests pass (44). * Validate pcakeep/pcadb in torch and NumPy Both constructors call the shared validate_pca_reduction. The torch mir_step gate delegates to pca_reduction_requested with the fitted data's channel count, so pcakeep >= n_channels (the bundled input.param's pcakeep 32) is no longer rejected. New regression test fails before the fix and passes after; wrapper suite green. * Persist torch pcakeep/pcadb as plain scalars The shared validator accepts numpy integers, which the weights_only torch.load in AMICA.load refuses to unpickle; state_dict now casts to int/float. None stays None, so default payloads are unchanged. * Add pcakeep/pcadb to the MLX backend AMICAMLXNG takes pcakeep/pcadb with AMICATorchNG's position, semantics and shared validation, passes them to numerical_rank, gains the same upfront mir_step gate and message, and persists both in state_dict config (additive; older payloads load with None). Docstrings and the mir() error no longer claim MLX lacks explicit reduction. MLX mir/persistence/backend suites pass (68). * Add cross-backend pcakeep/pcadb tests Real sample: torch/NumPy/MLX keep the same rank and sphere for pcakeep=20 and pcadb=18.5 (rank 10); pcakeep wins over pcadb; invalid values raise at construction; torch and MLX give the same mir_step gate verdict; torch vs MLX reduced fits match (min |corr| > 0.999); MLX and torch persistence round-trip the request. 61 passed; 53 of them fail on the pre-#323 sources. * Reword constructor comments flagged by typos Comment-only; full-repo typos check passes. * Document shared pcakeep/pcadb policy Differences guide: backend-table row, at-a-glance row 11 (pcadb is a pamica extension; pcakeep wins), a pcakeep/pcadb section, and the mir_step gate section rewritten for both backends. MLX API page and changelog entries. mkdocs build --strict passes. * Ignore pcakeep/pcadb without sphering No backend reduces when do_sphere=False (the reference sets numeigs = nx there, amica15.f90:527), yet the mir_step gate still fired. The shared predicate takes do_sphere and validates first; a new log_ignored_pca_request (called by all three constructors) warns once that the request is ignored, and carries the precedence INFO note, so validate_pca_reduction is now validation only. Rank policy and cross-backend tests pass (126). * Refit and restart from the input geometry _preprocess shrinks n_channels to the kept rank, and torch/MLX then validated the next fit's X against it: a refit, or the second of n_restarts under any rank reduction (explicit or automatic), raised 'X has 32 channels, model expects 20'. Both backends now keep the constructor's count in _n_input_channels, validate X against it, and reset n_channels/n_comps from it at the start of every _fit_once. _load_params derives it from the restored sphere's width, so a reloaded reduced model refits. 16 new cross-backend tests; the 12 torch/MLX cases fail before the fix, NumPy is the passing control. * Test reassignment, numpy scalars, torch payload A pcakeep/pcadb reassigned after construction raises the validator's ValueError at fit time on torch, NumPy and MLX (and at the mir_step gate on torch/MLX); the validator rejects numpy.bool_ and accepts numpy floats for pcadb; a torch state dict without the two config keys loads with None and keeps its reduced sphere. Rank policy and cross-backend suites: 160 passed. * Cite torch symbols, not lines, in MLX core Every torch_impl/core.py line citation in mlx_impl/core.py (about 50, many already stale and six shifted by this PR) now names the symbol (AMICATorchNG. or pamica.torch_impl.core.), which does not rot. Fortran line citations are unchanged. Comment-only. * Update docs for review findings Differences guide: backend-table intro describes the raw MLX backend accurately; PCA, MIR and PMI defined on first use; row 11 wording; the pcakeep/pcadb and mir_step sections cover do_sphere=False and fit-time checks. MLX API page and changelog use semantic line breaks and add the do_sphere, refit/restart and invalid-payload-load entries. mkdocs build --strict and typos pass. --- docs/api/mlx-backend.md | 28 +- docs/changelog.md | 37 + docs/guides/amica-differences.md | 71 +- pamica/mlx_impl/core.py | 357 ++++++---- pamica/numpy_impl/core.py | 24 +- pamica/rank.py | 169 ++++- pamica/tests/mlx_tests/test_mlx_mir.py | 9 +- .../tests/test_pca_reduction_cross_backend.py | 650 ++++++++++++++++++ pamica/tests/test_rank_policy.py | 164 ++++- .../torch_tests/test_amica_ng_wrapper.py | 21 + pamica/torch_impl/core.py | 104 ++- 11 files changed, 1444 insertions(+), 190 deletions(-) create mode 100644 pamica/tests/test_pca_reduction_cross_backend.py diff --git a/docs/api/mlx-backend.md b/docs/api/mlx-backend.md index 270b9c33..5a47dca4 100644 --- a/docs/api/mlx-backend.md +++ b/docs/api/mlx-backend.md @@ -16,12 +16,32 @@ Source extraction (`transform` and the `get_mixing_matrix`/`get_unmixing_matrix` best-iterate safeguard (`keep_best`) is implemented (epic #278 Phase 2, issue #288). Outlier rejection (`do_reject`), the LLt-stash-backed scoring accessors (`model_loglik`/`model_probability`), the EEGLAB `write_amica_output` export, -and the MIR/PMI diagnostics (`mir`/`pmi`, `fit(mir_step=...)` waypoints) are +and the Mutual Information Reduction (MIR) and Pairwise Mutual Information (PMI) diagnostics (`mir`/`pmi`, `fit(mir_step=...)` waypoints) are implemented (epic #278 Phase 3, issue #289). The `variance_order` accessor (the EEGLAB back-projected-variance component order) landed in the epic's -post-Phase-3 polish round, ahead of merge to `dev` -- epic #278 is complete; -there are no remaining gaps against the PyTorch backend other than -float32-only precision (Apple GPUs have no float64). +post-Phase-3 polish round, ahead of merge to `dev`, completing epic #278. + +Explicit principal component analysis (PCA) reduction followed in epic #324 Phase 1 (issue #323). +`pcakeep` and `pcadb` carry the PyTorch backend's names, defaults, validation and precedence, +all from the shared `pamica.rank` policy. +Both default to `None`, which leaves only automatic `mineig`/`mineig_rel` rank detection. +`pcakeep` must be an integer of at least 1 and `pcadb` a finite number greater than 0, +or the constructor raises `ValueError`. +When both are set, `pcakeep` takes precedence and `pcadb` is ignored, +as in the reference, which parses `pcadb` but never uses it; +with `do_sphere=False` both are ignored, with one warning. +A reduced fit has a non-square `(n_channels, n_channels_in)` sphere, +and both parameters persist through `state_dict`/`save`. +`fit(mir_step > 0)` rejects an explicit reduction request up front, exactly as the PyTorch backend does +(see [the differences guide](../guides/amica-differences.md#explicit-dimensionality-reduction-pcakeep-and-pcadb-issue-323)). +With it there are no remaining gaps against the PyTorch backend other than float32-only precision +(Apple GPUs have no float64). + +```python +model = AMICAMLXNG(n_channels=X.shape[0], pcakeep=X.shape[0] - 1) # e.g. average reference +model.fit(X) +model.get_sensor_mixing_matrix() # (n_channels_in, pcakeep) scalp maps +``` MLX is an optional dependency (Apple Silicon only), so it is imported separately and is not part of the default `import pamica` surface: diff --git a/docs/changelog.md b/docs/changelog.md index f96bcb35..4227a6da 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,43 @@ Release notes are also published on the ## Unreleased +- **MLX backend: explicit `pcakeep`/`pcadb`** (issue #323, epic #324 Phase 1). + `AMICAMLXNG` gained `pcakeep` and `pcadb` with `AMICATorchNG`'s names, defaults (`None`), position, validation and precedence. + They go through the shared `pamica.rank` policy, so all three array backends keep the same rank and build the same sphere + (cross-backend test on the bundled sample: torch vs NumPy sphere within 1e-10 relative, MLX within float32 rounding). + `fit(mir_step > 0)` gained the same upfront reduction gate and message as the PyTorch backend. + Both parameters persist additively in `state_dict()["config"]`: + a payload written before this change loads with `None`, and there is no `format_version` bump. +- **Behavior change: invalid `pcakeep`/`pcadb` now raise `ValueError` at construction on every backend.** + `pcakeep` must be an integer of at least 1 (a `bool` or a float is rejected) and `pcadb` a finite number greater than 0; + a value assigned to the attribute after construction fails at fit time. + The PyTorch and NumPy backends used to accept these silently: + `pcakeep=-3` sliced from the end and fitted 29 of 32 sources on the bundled sample, `pcakeep=2.7` truncated to 2, + and `pcakeep=0` or `pcadb <= 0` ran to a degenerate `nan_ll` fit. + Because construction validates, a saved PyTorch model whose config carries such a value + (only possible before this change) now fails to load with the same `ValueError`. + Setting both stays valid: `pcakeep` takes precedence and `pcadb` is ignored (one INFO log line), + as in the reference, which parses `pcadb` but never uses it. + `state_dict` now stores both as plain `int`/`float`, + so a numpy-scalar request survives `AMICA.save`/`load` (whose `weights_only` load refuses numpy scalars). +- **Behavior change: `pcakeep`/`pcadb` with `do_sphere=False` are ignored with one warning.** + No backend reduces without sphering, as in the reference, which keeps every dimension there (amica15.f90:527). + The request used to be dropped silently, and the PyTorch `mir_step` gate still refused it. + Every backend now logs one WARNING at construction, and the gate no longer counts it as a reduction request. +- **Behavior change: `mir_step` no longer rejects `pcakeep >= n_channels`.** + The PyTorch backend's upfront gate refused any explicit `pcakeep`, + including the bundled `input.param`'s `pcakeep 32` on the 32-channel sample, which reduces nothing; + `AMICA.from_params_file("input.param").fit(X, mir_step=1)` raised. + The gate now rejects only a real request (`pcakeep` below the channel count, or any `pcadb`, while sphering), + identically on the PyTorch and MLX backends. +- **Fix: refits and best-of-N restarts after a rank reduction** (PyTorch and MLX backends). + A rank reduction, explicit or automatic (`mineig`/`mineig_rel`, for example on Maxwell-filtered MEG), + shrinks the model's `n_channels` to the kept rank, and the next fit validated its data against that shrunk count. + A second `fit()` on the same instance, and the second restart of any `n_restarts > 1` fit, + raised `ValueError: X has 32 channels, model expects 20`, which ended the whole multi-restart fit. + Every fit now starts from the constructor's input channel count, + including a fit on a model reloaded from `state_dict`/`save`. + The NumPy backend was not affected. - **Epic #278 polish round: audit-driven fixes ahead of merge to `dev`.** `AMICAMLXNG` gained `variance_order` (issue #92), the EEGLAB back-projected-variance component order, closing the one accessor gap the diff --git a/docs/guides/amica-differences.md b/docs/guides/amica-differences.md index 672d23a3..115332da 100644 --- a/docs/guides/amica-differences.md +++ b/docs/guides/amica-differences.md @@ -22,10 +22,11 @@ that is not listed, that is a bug worth | 8 | Columns merged away by `share_comps` | updated to NaN, then hidden by the `comp_used` mask | frozen at their last finite value (never divided) | a fit must not end holding NaN parameters, mask or no mask; the columns are dead either way | — (see issues #60, #240) | | 9 | Block-size search | on (`do_opt_block=1`), sweeps 128–1024, **aborts** if a candidate cannot allocate | off; sweeps 4096–32768; a candidate that cannot allocate is skipped and the fit continues | the choice is timing-based and therefore machine-dependent, which a parity run cannot have; Fortran's range sits far below where any pamica backend peaks; and running out of memory is a reason to use a smaller block, not to stop | `do_opt_block=True` (but pin `block_size` for a bit-for-bit comparison) | | 10 | Restarts across seeds | none (its `maxrestarts` only *recovers* from an early NaN) | available as `n_restarts`, **off by default** (`n_restarts=1`) | the weakest under-determined components are init-basin sensitive, so best-of-N buys robustness; but a default that ran N fits would change every result and cost N times as long | `n_restarts=1` (the default) | +| 11 | `pcadb` | parsed (amica15.f90:3459-3461), never used | unset by default; when set alone, keeps the dimensions within `pcadb` dB of the largest eigenvalue; ignored when `pcakeep` is also set | a dB cut is a scale-free way to drop low-variance directions; letting `pcakeep` win preserves what a reference `input.param` that sets both (both bundled files do) means to the binary | leave `pcadb` unset (the default), or set `pcakeep` | Rows 1, 2 and 7 arrived with [ADR 0004](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0004-rank-deficient-input-handling.md); row 3 with ADR 0003; row 5 with issue #50; row 8 with issues #60 and #240; -row 9 with issue #232; row 10 with issue #198. +row 9 with issue #232; row 10 with issue #198; row 11 with issue #323. Two `share_comps` details are pamica's own because the reference cannot decide them: the A-freeze window after a merge is anchored on `share_start` (the literal @@ -122,7 +123,10 @@ fitting; there is no `info` from which to infer types. ## Backend differences -Separate from reference divergences: the optional MLX backend is a subset. +Separate from reference divergences, this table compares the backends. +The raw MLX backend (`AMICAMLXNG`) has the PyTorch backend's full feature surface except precision: +it is float32-only, because Apple GPUs have no float64. +The `AMICA` and `AMICAICA` wrappers do not construct it yet (epic #324 Phase 4, issue #313). | Feature | PyTorch | NumPy | MLX | Native Fortran | |---|---|---|---|---| @@ -132,11 +136,12 @@ Separate from reference divergences: the optional MLX backend is a subset. | Outlier rejection | yes | yes | yes | yes | | Precision | f64/f32 | f64 | f32 only | f64 | | Rank detection | yes | yes | yes | yes (absolute floor) | +| Explicit `pcakeep`/`pcadb` | yes | yes | yes | `pcakeep` only; `pcadb` parsed but unused | | `min_dll` stop | yes | yes | yes | yes | | `min_nd` stop / gradient norm | yes | yes (as `min_grad_norm`) | yes | yes | | `keep_best` best-iterate restore | yes | no | yes | n/a | | `n_restarts` best-of-N restarts | yes | yes | yes | n/a | -| MIR diagnostic | yes | no | yes | n/a | +| Mutual Information Reduction (MIR) diagnostic | yes | no | yes | n/a | | Persistence | `state_dict` + EEGLAB `amicaout` export | EEGLAB `amicaout` | `state_dict`/`.npz` `save`-`load` + EEGLAB `amicaout` export | EEGLAB `amicaout` | | Fortran `input.param` reader | yes (`AMICA.from_params_file`, #132) | no (`params_file` is JSON-only) | no | native | @@ -158,7 +163,7 @@ coupling, no pickle. The best-iterate safeguard (`keep_best`, row 3 above) followed in epic #278 Phase 2 (issue #288), a direct port of PyTorch's #51 restore decision onto MLX's float32 trajectory. Outlier rejection (`do_reject`), the LLt-stash-backed `model_loglik`/`model_probability` -accessors, the EEGLAB `write_amica_output` export, and the MIR/PMI +accessors, the EEGLAB `write_amica_output` export, and the MIR and Pairwise Mutual Information (PMI) diagnostics followed in epic #278 Phase 3 (issue #289) -- see their own sections below. Any genuinely unsupported parameter still fails loudly: it is simply absent from the constructor, rather than silently downgrading. @@ -447,21 +452,49 @@ the good set and which does not -- a WORK divergence, matching the `do_choose_pdfs`-adjacent `drho_n`/lgamma-table WORK divergences already documented above, not a correctness one. -## `mir_step`'s upfront PCA-reduction gate: MLX has none, and that is parity, not a gap - -`AMICATorchNG.fit(mir_step=...)` rejects an EXPLICIT `pcakeep`/`pcadb` -request up front (before any preprocessing runs), but -- deliberately, as of -issue #300 -- never an AUTOMATIC `mineig`/`mineig_rel`-detected reduction -upfront: that case is only ever caught downstream, inside the first -per-waypoint `mir()` call, by the geometry-based `_pca_reduced()` guard -issue #300 introduced (`sphere.shape[0] != sphere.shape[1]`). `AMICAMLXNG` -has no explicit `pcakeep`/`pcadb` constructor parameter at all (only -automatic rank detection, same as every other MLX rank-reduction path), so -there is nothing for an upfront gate to check regardless of when it would -run -- MLX's `mir_step` validation is the bare `mir_step >= 0` check, and -automatic reduction degrades to the same warned, all-`NaN` `mir_history_` -waypoints on both backends. This is intentional torch-parity for the -auto-detected case, not a knowability limitation of MLX's fit-loop timing. +## Explicit dimensionality reduction: `pcakeep` and `pcadb` (issue #323) + +All three array backends take `pcakeep` and `pcadb`, the explicit principal component analysis (PCA) reduction, +with the same names, defaults (`None`), validation and precedence, decided once in `pamica/rank.py`. +`pcakeep` keeps that many principal dimensions and is the reference's own keyword: +`numeigs = min(pcakeep, count(eigs > mineig))` (amica15.f90:413, 466), +so a value at or above the channel count keeps every dimension the data have. +`pcadb` is a pamica extension (row 11 above): +the reference parses it (amica15.f90:3459-3461) but never reads it again, so the binary never reduces by it, +while pamica keeps the dimensions whose covariance eigenvalue lies within `pcadb` dB of the largest. + +When both are set, `pcakeep` takes precedence and `pcadb` is ignored, with one INFO log line saying so. +That is what the reference does, and it matters in practice: +both bundled parameter files set `pcakeep 32` and `pcadb 30`, +and on the 32-channel sample `pcadb=30` alone would keep 26 dimensions where the binary keeps all 32. +Both are ignored, with one WARNING, when `do_sphere=False`: +reduction happens only while sphering, as in the reference, whose no-sphere branch sets `numeigs = nx` (amica15.f90:527). + +A value that cannot mean anything raises `ValueError` at construction on every backend: +`pcakeep` must be an integer of at least 1 (not a `bool`, not a float), and `pcadb` a finite number greater than 0. +The check runs again at fit time, so a value assigned to the attribute after construction fails there. +Before #323 the PyTorch and NumPy backends accepted these silently: +`pcakeep=-3` sliced from the end and fitted 29 of 32 dimensions, `pcakeep=2.7` fitted 2, +and `pcakeep=0` or `pcadb <= 0` ran to a degenerate `nan_ll` fit. +The reference has no such check; +with `pcakeep <= 0` it sets `numeigs = min(pcakeep, ...) <= 0` and carries on, the situation row 2 describes. + +## `mir_step`'s upfront PCA-reduction gate + +MIR is undefined on a rank-reduced sphere, +so `fit(mir_step > 0)` checks for PCA reduction in two places, identically on `AMICATorchNG` and `AMICAMLXNG`. +An explicit reduction request is rejected up front with a `ValueError`, before any preprocessing runs. +That means `pcakeep` below the channel count, or any `pcadb`, +since whether it cuts anything depends on eigenvalues that do not exist yet. +`pcakeep` at or above the channel count is not a request and passes, +nor is any request with `do_sphere=False`, which never reduces. +Before #323 the PyTorch gate rejected every explicit `pcakeep`, including the bundled `input.param`'s `pcakeep 32` on 32 channels, +and MLX had no explicit reduction to gate. + +Automatic reduction (`mineig`/`mineig_rel` rank detection) cannot be known before the sphere exists, +so it is caught per waypoint instead, by `mir()`'s geometry-based `_pca_reduced()` guard (issue #300): +the first scheduled waypoint records a warned `NaN` entry in `mir_history_`, +later waypoints are skipped, and the fit itself completes. ## The block-size search picks a machine-dependent value (issue #232) diff --git a/pamica/mlx_impl/core.py b/pamica/mlx_impl/core.py index f2c2ff13..19d0ca59 100644 --- a/pamica/mlx_impl/core.py +++ b/pamica/mlx_impl/core.py @@ -34,7 +34,9 @@ waypoints) are implemented (epic #278 Phase 3, issue #289). The EEGLAB back-projected-variance component order (``variance_order``) landed in the epic's post-Phase-3 polish round, ahead of merge to ``dev``, closing the one -accessor gap Phase 3 left open: every AMICATorchNG-supported feature this +accessor gap Phase 3 left open. Explicit PCA reduction (``pcakeep``/``pcadb``, +with AMICATorchNG's validation, precedence and upfront ``mir_step`` gate) joined +in epic #324 Phase 1 (issue #323), so every AMICATorchNG-supported feature this backend can support (float32 GPU limits aside) is now ported. Newton (issue #264) runs entirely in float32 on the GPU stream: the curvature @@ -59,8 +61,8 @@ (``self.dorho = pdftype == 0``, Fortran ``dorho=.false.``), which also gates the ``drho_n`` accumulation and the per-iteration lgamma-table refresh here. AMICATorchNG already gates its digamma pull behind the same ``self.dorho`` -flag (core.py:1664-1678), so that is not a divergence; its genuine dead work -for a non-GG fit is the ``drho_n`` accumulation, which it computes +flag (``AMICATorchNG._update_parameters``), so that is not a divergence; its genuine +dead work for a non-GG fit is the ``drho_n`` accumulation, which it computes unconditionally in ``_get_block_updates`` (no ``dorho`` gate there), and the inline ``torch.lgamma(1+1/rho)`` term ``_log_pdf_only`` recomputes on every call to build the (dead, for non-GG) GG-fallthrough branch. This backend @@ -104,13 +106,20 @@ from ..metrics import mir as mir_metric from ..metrics import pairwise_mi from ..numpy_impl.utils import identify_shared_components -from ..rank import MINEIG, MINEIG_REL, numerical_rank +from ..rank import ( + MINEIG, + MINEIG_REL, + log_ignored_pca_request, + numerical_rank, + pca_reduction_requested, + validate_pca_reduction, +) logger = logging.getLogger(__name__) # Human-readable names for the ``pdftype``/``pdtype`` source-density family -# codes (issue #265, mirroring AMICATorchNG's PDFTYPE_NAMES, torch core.py: -# 65-71 -- duplicated rather than imported so this module keeps no torch +# codes (issue #265, mirroring ``pamica.torch_impl.core.PDFTYPE_NAMES`` -- +# duplicated rather than imported so this module keeps no torch # dependency). Exposed alongside the numeric codes so a fitted model's # per-source density family is inspectable (issue #142). PDFTYPE_NAMES = { @@ -127,7 +136,7 @@ # constants (amica15.f90:1333/1359/1371) so the log-density matches the reference # binary bit-for-bit: 2.506628274 = sqrt(2*pi) (Gaussian, pdtype 2); 4.132731354 / # 1.858073988 = the sub-/super-Gaussian cosh normalizers (pdtype 4 / 1). Ported -# verbatim from AMICATorchNG (core.py:76-82, policy 1). +# verbatim from the same-named ``pamica.torch_impl.core`` constants (policy 1). _LOG_SQRT_2PI = math.log(2.506628274) _LOG_NORM_COSH_SUB = math.log(4.132731354) _LOG_NORM_COSH_SUP = math.log(1.858073988) @@ -179,10 +188,10 @@ # this guard already makes rare in practice. _INV_COND_THRESHOLD = 1e12 -# Best-iterate safeguard (issue #51, ported from AMICATorchNG core.py:90-113 -- -# epic #278 Phase 2, issue #288). The lrate schedule is deliberately -# non-monotone: it anneals only *after* an LL decrease, so a late Newton -# fallback can overshoot and a run can end below a peak it already reached. +# Best-iterate safeguard (issue #51, ported from +# ``pamica.torch_impl.core._KEEP_BEST_TOL`` -- epic #278 Phase 2, issue #288). The lrate +# schedule is deliberately non-monotone: it anneals only *after* an LL decrease, so a +# late Newton fallback can overshoot and a run can end below a peak it already reached. # fit() therefore tracks the highest-LL iterate and restores it when the final # LL falls more than this tolerance below that peak. Units: mean # log-likelihood per sample-channel, the same normalized scale as @@ -200,7 +209,7 @@ def _logcosh(x: mx.array) -> mx.array: """Numerically stable ``log cosh(x) = |x| - log2 + log1p(exp(-2|x|))`` - (AMICATorchNG ``_logcosh``, core.py:113-116). Naive ``mx.log(mx.cosh(x))`` + (``pamica.torch_impl.core._logcosh``). Naive ``mx.log(mx.cosh(x))`` overflows to inf in float32 by ``|x| == 90`` (measured crossover: finite at 89.0, inf at 89.5; ``cosh`` itself overflows first) -- reachable, since ``beta`` clips at ``invsigmax=1000`` -- while this form stays within float32 @@ -211,8 +220,7 @@ def _logcosh(x: mx.array) -> mx.array: def _score(y: mx.array, rho: mx.array, pdtype: Optional[mx.array] = None) -> mx.array: - """Source-density score ``fp = -d(log pdf)/dy`` (AMICATorchNG ``_score``, - core.py:184-222). + """Source-density score ``fp = -d(log pdf)/dy`` (``pamica.torch_impl.core._score``). ``pdtype is None`` (the ``pdftype=0`` fast path): GG score only -- exactly the pre-#265 ``_score_gg`` body, so it adds ZERO extra graph nodes relative @@ -254,7 +262,7 @@ def _log_pdf( pdtype: Optional[mx.array] = None, ) -> tuple[mx.array, Optional[mx.array]]: """GG log-density and ``|y|^rho``, extended with the fixed non-GG families - (AMICATorchNG ``_log_pdf_only``, core.py:225-267). + (``pamica.torch_impl.core._log_pdf_only``). ``pdtype is None`` (the ``pdftype=0`` fast path): byte-for-byte the pre-#265 ``_log_pdf_gg`` body -- ``lgamma_table = lgamma(1+1/rho)`` @@ -516,6 +524,36 @@ class AMICAMLXNG: the merge; issue #269) and under ``do_reject`` (the good-sample set, and so the LL normalization, changes across iterations, so per-iteration LLs are not comparable), matching AMICATorchNG. + + The explicit PCA-reduction parameters (issue #323) likewise carry + AMICATorchNG's names, defaults, validation and semantics. The validation + and the precedence rule are the shared :mod:`pamica.rank` policy, so the + three array backends cannot disagree on them: + + ``pcakeep`` (None) + Keep this many principal dimensions (Fortran ``pcakeep``). Must be an + integer >= 1, or the constructor raises ``ValueError``. Capped by the + detected numerical rank, matching Fortran's ``numeigs = min(pcakeep, + count(eigs > mineig))`` (amica15.f90:413), so ``pcakeep >= + n_channels`` keeps every dimension the data have. + ``pcadb`` (None) + Keep the dimensions whose covariance eigenvalue lies within ``pcadb`` + dB of the largest. Must be a finite number > 0, or the constructor + raises ``ValueError``. A pamica extension: the reference parses + ``pcadb`` (amica15.f90:3459-3461) but never uses it. When both are + set, ``pcakeep`` takes precedence and ``pcadb`` is ignored (one INFO + log line), as in the reference. + + Both are ignored, with one WARNING, when ``do_sphere=False``: reduction + happens only while sphering, as in the reference (``numeigs = nx`` in its + no-sphere branch, amica15.f90:527). + + With both ``None`` (the default) only automatic ``mineig``/``mineig_rel`` + rank detection sizes the model, exactly as before #323. A reduced fit has + a non-square ``(n_channels, n_channels_in)`` sphere; map its components + back to the input channels with :meth:`get_sensor_mixing_matrix`. + ``fit(mir_step > 0)`` rejects an explicit reduction request up front, as + AMICATorchNG does (see :meth:`_fit_once`). """ def __init__( @@ -566,6 +604,8 @@ def __init__( do_mean: bool = True, do_sphere: bool = True, do_approx_sphere: bool = True, + pcakeep: Optional[int] = None, + pcadb: Optional[float] = None, mineig: float = MINEIG, mineig_rel: Optional[float] = MINEIG_REL, seed: Optional[int] = None, @@ -574,6 +614,10 @@ def __init__( keep_best: bool = True, ): self.n_channels = n_channels + # The input channel count, kept apart from n_channels, which + # _preprocess shrinks to the kept rank on any rank reduction; same + # role as AMICATorchNG._n_input_channels (see _fit_once). + self._n_input_channels = n_channels self.n_models = n_models # multi-model (#81) + component sharing (#263) self.n_mix = n_mix self.n_comps = n_channels * n_models @@ -625,7 +669,7 @@ def __init__( # Outlier rejection (issue #123's AMICATorchNG mechanism, epic #278 # Phase 3/#289), same names/defaults/validation as AMICATorchNG - # (torch_impl/core.py:593-597, 679-685; Fortran do_reject/rejsig/ + # (``AMICATorchNG.__init__``; Fortran do_reject/rejsig/ # rejstart/rejint/maxrej, amica15_header.f90). numrej/good_idx are set # up per fit in _fit_once (good_idx = None until then, matching the # do_reject=False no-op path). @@ -652,7 +696,7 @@ def __init__( self.rholratefact = rholratefact # Source-density family selection (issue #265, porting AMICATorchNG's - # issue #26 -- see torch_impl/core.py:640-676 for the identical block). + # issue #26 -- see ``AMICATorchNG.__init__`` for the identical block). # Values match Fortran's per-source pdtype codes: 0 generalized Gaussian # (the default, GG-mixture with adaptive rho), 2 Gaussian mixture, 3 # logistic (sech^2) mixture, 4 sub-Gaussian cosh+ (single component). @@ -722,6 +766,14 @@ def __init__( self.do_mean = do_mean self.do_sphere = do_sphere self.do_approx_sphere = do_approx_sphere + # Explicit PCA reduction (issue #323), validated by the policy shared + # with the PyTorch and NumPy backends (pamica/rank.py) so a bad value + # fails here, before any data is touched, exactly as it does there; + # then one log line for any part of it a fit will ignore. + validate_pca_reduction(pcakeep, pcadb) + log_ignored_pca_request(pcakeep, pcadb, do_sphere) + self.pcakeep = pcakeep + self.pcadb = pcadb # Numerical-rank floors (issue #223); see pamica/rank.py and ADR 0004. self.mineig = mineig self.mineig_rel = mineig_rel @@ -828,7 +880,7 @@ def __init__( # issue #155), STASHED as the training E-step computes it rather than # recomputed by a separate forward pass at write time (issue #157; # epic #278 Phase 3, issue #289 -- port of AMICATorchNG's identical - # mechanism, torch_impl/core.py:880-902). ``_llt_logv``/``_llt_ll`` + # mechanism in ``AMICATorchNG.__init__``). ``_llt_logv``/``_llt_ll`` # are the live per-fit buffers (Fortran's ``modloglik``/``loglik``), # zero-filled so a ``do_reject`` sample keeps Fortran's zero # sentinel. ``fit`` converts them into the compact numpy @@ -884,17 +936,22 @@ def _preprocess(self, X: np.ndarray) -> mx.array: if self.do_sphere: # Population covariance (/N), matching Fortran's DSYRK scatter, not # numpy's default sample covariance (/(N-1)) -- the same choice, and - # the reasoning for it, at torch_impl/core.py:823-827. + # the reasoning for it, in ``AMICATorchNG._preprocess``. cov = np.cov(Xc, bias=True) evals, evecs = np.linalg.eigh(cov) order = np.argsort(evals)[::-1] evals = evals[order] evecs = evecs[:, order] - # Numerical rank, decided by the policy shared with the PyTorch and - # NumPy backends (pamica/rank.py, issue #223) so the three cannot - # disagree. Fortran: numeigs = min(pcakeep, count(eigs > mineig)). + # Numerical rank plus explicit PCA reduction, decided by the policy + # shared with the PyTorch and NumPy backends (pamica/rank.py, + # issues #223/#323) so the three cannot disagree. Fortran: + # numeigs = min(pcakeep, count(eigs > mineig)). n_comp = numerical_rank( - evals, mineig=self.mineig, mineig_rel=self.mineig_rel + evals, + mineig=self.mineig, + mineig_rel=self.mineig_rel, + pcakeep=self.pcakeep, + pcadb=self.pcadb, ) evals = evals[:n_comp] V = evecs[:, :n_comp] @@ -939,7 +996,7 @@ def _preprocess(self, X: np.ndarray) -> mx.array: # ------------------------------------------------------------------ def _initialize_parameters(self): """Initialize parameters with the *same* ``np.random.RandomState`` draw - order as AMICATorchNG/AMICA_NumPy (core.py:918-973), so a shared seed + order as ``AMICATorchNG._initialize_parameters``/AMICA_NumPy, so a shared seed gives a bit-identical (float32-cast) starting point.""" rng = np.random.RandomState(self.seed) n, m, ncomp, nmix = self.n_channels, self.n_models, self.n_comps, self.n_mix @@ -975,7 +1032,7 @@ def _initialize_parameters(self): self.c = mx.array(np.zeros((n, m), dtype=np.float32)) # Per-source density-family codes, Fortran `pdtype = pdftype` - # (amica15.f90:611; AMICATorchNG core.py:960-963). In adaptive mode + # (amica15.f90:611; ``AMICATorchNG._initialize_parameters``). In adaptive mode # (pdftype==1) every source starts as the super-Gaussian code (1), # since self.pdftype IS 1 there -- no special-case fill needed. self.pdtype = mx.array(np.full((n, m), self.pdftype, dtype=np.int32)) @@ -984,7 +1041,7 @@ def _initialize_parameters(self): # Reset the mutable optimization state to the pristine constructor values # (lrate_cap, newtrate and rholrate are ratcheted down during fit, and # n_newton_fallbacks counts one fit), so a re-fit starts fresh -- - # AMICATorchNG does the same at core.py:966-971/:1936. + # AMICATorchNG does the same in ``_initialize_parameters``/``_fit_once``. self.lrate = self.lrate0 self.lrate_cap = self.lrate0 self.newtrate = self.newtrate0 @@ -1139,7 +1196,7 @@ def _update_unmixing_matrices(self): def _pdtype_h(self, h: int) -> Optional[mx.array]: """Per-source density-family codes for model ``h``, shaped for broadcasting against ``(batch, n_channels, n_mix)`` arrays (AMICATorchNG - ``_pdtype_h``, core.py:984-993), or ``None`` on the default + ``_pdtype_h``), or ``None`` on the default ``pdftype=0`` (GG-only) fast path so the E-step stays bit-identical to the pre-#265 implementation (policy 2). """ @@ -1152,8 +1209,8 @@ def _pdtype_h(self, h: int) -> Optional[mx.array]: # E-step # ------------------------------------------------------------------ def _forward(self, Xb: mx.array): - """E-step forward pass for one block, per model (AMICATorchNG._forward, - core.py:998-1071). ``Xb`` is ``(n_channels, batch)``. Returns ``logV`` + """E-step forward pass for one block, per model (``AMICATorchNG._forward``). + ``Xb`` is ``(n_channels, batch)``. Returns ``logV`` ``(batch, n_models)`` and per-model lists ``(b, z, y, az_rho)``. For n_models=1 (c=0, gm=1, comp_list=identity) this is numerically identical to the single-model path. Each model's ``az_rho`` entry is ``None`` when @@ -1197,8 +1254,8 @@ def _forward(self, Xb: mx.array): return logV, b_list, z_list, y_list, azrho_list def _get_block_updates(self, Xb: mx.array) -> dict: - """Exact-EM sufficient statistics for one block (AMICATorchNG. - _get_block_updates, core.py:1141-1283). Mixture stats are scattered into + """Exact-EM sufficient statistics for one block + (``AMICATorchNG._get_block_updates``). Mixture stats are scattered into their ``comp_list`` columns; ``dWtmp``/``dgm``/``dc_numer`` are per-model. For n_models=1 (v==1, identity comp_list) this reproduces the single-model accumulators exactly. @@ -1229,7 +1286,7 @@ def _get_block_updates(self, Xb: mx.array) -> dict: # Per-sample total log-likelihood (Fortran ``P``/``loglik``, # amica15.f90:1402), kept as a vector rather than folded straight into # the scalar sum so the LLt stash can reuse it (issue #157, epic #278 - # Phase 3/#289, porting AMICATorchNG core.py:1222-1227); ``block_ll`` + # Phase 3/#289, porting ``AMICATorchNG._get_block_updates``); ``block_ll`` # is the same summation as before, bit for bit. block_ll_samples = mx.logsumexp(logV, axis=1) block_ll = block_ll_samples.sum() @@ -1272,8 +1329,8 @@ def zeros(): dmu_n = dmu_n.at[:, idx].add(ufp.sum(0).T) # Phase A guard: float32 can round y to exactly 0 (fp(0)=0 => ufp=0), # so ufp/y is 0/0=NaN; where y==0, 0/1 contributes 0 (issue #75). - # torch's safe_y substitution (core.py:1231) is mirrored exactly for - # every family here, even though the true fp/y limit at y->0 is a + # torch's safe_y substitution (``_get_block_updates``) is mirrored exactly + # for every family here, even though the true fp/y limit at y->0 is a # finite nonzero constant for codes 2/3/1 (fp'(0): 1 Gaussian # (fp=y), 0.5 logistic (fp=tanh(y/2)), 2 super-Gaussian # (fp=y+tanh(y))) and 0 for code 4 (fp=y-tanh(y), whose Taylor @@ -1343,10 +1400,9 @@ def _accumulate_blocks(self, X: mx.array, stash_llt: bool = False) -> dict: Scatter each block's per-sample ``logV``/``ll_samples`` into the ``_llt_logv``/``_llt_ll`` buffers as it goes, so the LLt output never needs a second pass over the data (issue #157, epic #278 - Phase 3/#289 -- port of AMICATorchNG._accumulate_blocks, - torch_impl/core.py:1339-1388). The scatter is itself just another - lazy MLX op (``self._llt_logv[rows] = logv``), so it joins the - same per-iteration graph ``fit`` already evaluates once -- no + Phase 3/#289 -- port of ``AMICATorchNG._accumulate_blocks``). The scatter is + itself just another lazy MLX op (``self._llt_logv[rows] = logv``), so it + joins the same per-iteration graph ``fit`` already evaluates once -- no extra sync is added here. Only the training loop sets this; the ``_tune_block_size`` probes leave the buffers untouched, so the tuner still leaves no state behind. The per-sample values are @@ -1459,7 +1515,7 @@ def probe(size: int) -> float: # ------------------------------------------------------------------ def _finalize_newton_stats(self, acc: dict): """Reduce the Newton block accumulators into ``(sigma2, lambda_, kappa)`` - (AMICATorchNG._finalize_newton_stats, core.py:1307-1331; Fortran + (``AMICATorchNG._finalize_newton_stats``; Fortran amica15.f90:1666-1680). The Fortran ``baralpha``/``dkappa_denom``/``dlambda_denom`` @@ -1498,7 +1554,7 @@ def _finalize_newton_stats(self, acc: dict): def _newton_direction(self, dA_h, sigma2_h, lambda_h, kappa_h): """Per-model Newton direction ``H`` from the natural gradient ``dA_h`` - (AMICATorchNG._newton_direction, core.py:1333-1361). + (``AMICATorchNG._newton_direction``). Vectorized port of the per-source-pair 2x2 solve (Fortran amica15.f90:1718-1741): @@ -1542,8 +1598,7 @@ def _newton_direction(self, dA_h, sigma2_h, lambda_h, kappa_h): def _update_parameters(self, acc: dict, n_samples: int): """Exact-EM mixture updates + natural-gradient A-update, optionally - Newton-preconditioned (AMICATorchNG._update_parameters, - core.py:1363-1616).""" + Newton-preconditioned (``AMICATorchNG._update_parameters``).""" # Fortran builds dAk from the PREVIOUS iteration's model weights: gm is # not reassigned until update_params (amica15.f90:1788+), after the # dAk/zeta accumulation in accum_updates_and_likelihood (:1749-1761). @@ -1560,10 +1615,10 @@ def _update_parameters(self, acc: dict, n_samples: int): tiny = float(np.finfo(np.float32).tiny) # Per-model data-space bias c[i,h] = sum_t v_h*x / sum_t v_h (Fortran - # update_c, core.py:1401-1423). Skipped for n_models=1 (v==1 => c is the - # zero data mean; the update would add a float-sum residual and break the - # #24 bit-exact single-model path). A dead model (dgm[h]==0) keeps its - # prior c rather than writing 0/0, and is surfaced (matching AMICATorchNG). + # update_c, as in ``AMICATorchNG._update_parameters``). Skipped for n_models=1 + # (v==1 => c is the zero data mean; the update would add a float-sum residual + # and break the #24 bit-exact single-model path). A dead model (dgm[h]==0) keeps + # its prior c rather than writing 0/0, and is surfaced (matching AMICATorchNG). if self.n_models > 1: dgm = acc["dgm"] live = dgm > 0.0 @@ -1620,9 +1675,9 @@ def _update_parameters(self, acc: dict, n_samples: int): # GG shape update with the 1/psi(1+1/rho) digamma factor (Fortran # :2013-2014); digamma is computed host-side (MLX has none). A NaN here # (e.g. from an upstream mu/beta blow-up) is reset to rho0 and surfaced, - # matching AMICATorchNG (core.py:1671-1693), so it does not silently + # matching ``AMICATorchNG._update_parameters``, so it does not silently # poison the lgamma table and every subsequent E-step. - # Deliberate divergence from AMICATorchNG (core.py:1671-1675), which also + # Deliberate divergence from ``AMICATorchNG._update_parameters``, which also # skips the update when rho is pinned to a boundary (all 1.0 or all 2.0): # that early-exit needs a host sync on a (n_mix, n_comps) reduction over # rho every iteration. This backend does make a few scalar host syncs per @@ -1657,10 +1712,10 @@ def _update_parameters(self, acc: dict, n_samples: int): ) # Natural-gradient A-update. A is stored as Fortran's A^T, so the update - # is a LEFT-multiply by the transposed direction (core.py:1506-1514, - # #24 root cause). Each model's direction is scattered into its mixing - # columns as a gm-weighted average (Fortran dAk/zeta, core.py:1546-1561) - # using the PREVIOUS iteration's gm (gm_prev, see the snapshot above): + # is a LEFT-multiply by the transposed direction (as in + # ``AMICATorchNG._update_parameters``, #24 root cause). Each model's direction + # is scattered into its mixing columns as a gm-weighted average (Fortran + # dAk/zeta) using the PREVIOUS iteration's gm (gm_prev, see the snapshot above): # for the default disjoint comp_list every column has one contributor, so # gm cancels and n_models=1 is byte-for-byte the old `A - lrate*(dA.T@A)`; # a SHARED column (#263) takes Fortran's responsibility-weighted average, @@ -1684,7 +1739,7 @@ def _update_parameters(self, acc: dict, n_samples: int): # always False with sharing off, so the default path is unchanged. # Newton only swaps out the per-model DIRECTION; the dAk/zeta scatter, # the gradient norm and the freeze structure below are untouched by it - # (AMICATorchNG core.py:1531-1545). A model whose curvature fails the + # (``AMICATorchNG._update_parameters``). A model whose curvature fails the # positive-definiteness guard falls back to its natural gradient for this # iteration, and -- as in Fortran -- ANY model falling back also sends # the lrate ramp to lrate_cap instead of newtrate. @@ -1771,8 +1826,8 @@ def _update_parameters(self, acc: dict, n_samples: int): scale = mx.sqrt((self.A**2).sum(axis=0)) # (n_comps,) # A zero-norm (collapsed) column is left untouched, not rescaled: # safe_scale is 1 there, so A/beta are unchanged and mu*safe_scale - # keeps its prior value (matching AMICATorchNG's nonzero mask, - # core.py:1608-1614 -- using raw `scale` would zero mu instead). + # keeps its prior value (matching AMICATorchNG's nonzero mask in + # _update_parameters -- using raw `scale` would zero mu instead). safe_scale = mx.where(scale > 0, scale, mx.ones_like(scale)) self.A = self.A / safe_scale self.mu = self.mu * safe_scale @@ -1795,7 +1850,7 @@ def _update_parameters(self, acc: dict, n_samples: int): # ------------------------------------------------------------------ def _choose_pdfs(self, X: mx.array) -> None: """Extended-Infomax adaptive PDF switch (Fortran ``do_choose_pdfs``, - AMICATorchNG ``_choose_pdfs``, core.py:1790-1823). + ``AMICATorchNG._choose_pdfs``). Re-estimates each source's kurtosis from the current model activations and sets its density family to the super-Gaussian (code 1) or @@ -1868,7 +1923,7 @@ def _choose_pdfs(self, X: mx.array) -> None: def _pdtype_from_kurtosis(self, kurt: np.ndarray, nsub: np.ndarray) -> np.ndarray: """Map per-source excess kurtosis to a density-family code (pure numpy; - AMICATorchNG ``_pdtype_from_kurtosis``, core.py:1825-1852). + ``AMICATorchNG._pdtype_from_kurtosis``). Super-Gaussian (positive kurtosis) -> code 1; sub-Gaussian -> code 4. Only sources with a meaningful signal switch: a dead model @@ -1897,7 +1952,7 @@ def _pdtype_from_kurtosis(self, kurt: np.ndarray, nsub: np.ndarray) -> np.ndarra # ------------------------------------------------------------------ # Best-iterate safeguard (issue #51, epic #278 Phase 2/#288; port of - # AMICATorchNG._snapshot_params/_restore_params, core.py:2025-2062) + # AMICATorchNG._snapshot_params/_restore_params) # ------------------------------------------------------------------ def _snapshot_params(self) -> dict: """Snapshot the fitted state for the best-iterate safeguard (issue #51). @@ -1915,7 +1970,7 @@ def _snapshot_params(self) -> dict: fix this ports). Also captures the LLt stash (issue #157, epic #278 Phase 3/#289; port - of AMICATorchNG._snapshot_params, torch_impl/core.py:2025-2057). + of ``AMICATorchNG._snapshot_params``). ``fit`` snapshots immediately after the E-step that produced both the candidate ``best_ll`` and this iteration's ``_llt_logv``/``_llt_ll``, so a restore rolls the on-disk LLt back to the E-step of the restored @@ -2197,7 +2252,7 @@ def shared_components(self) -> list: Note that a merge synchronizes only the mixture parameters routed through ``comp_list`` (``mu``/``alpha``/``beta``/``rho``); the per-source density *family* code ``pdtype`` is a separate array and is - not synchronized (issue #265, matching AMICATorchNG core.py:2669-2673), + not synchronized (issue #265, matching ``AMICATorchNG.shared_components``), so under the adaptive switcher (``pdftype=1``) a shared pair can still report different :meth:`get_pdftype` codes. @@ -2416,32 +2471,25 @@ def _fit_once( ``ll_history``: entry ``i`` is computed after iteration ``i``'s parameter update, while ``ll_history[i]`` is the likelihood of the parameters before it, so the two are one update apart (issue #161). - There is no upfront PCA-reduction ``ValueError`` gate here, but this - is NOT a knowability limitation -- the fitted sphere's shape is - known immediately after :meth:`_preprocess` runs, a few lines below - this docstring, well before the iteration loop (let alone the first - waypoint) starts. It is PARITY with ``AMICATorchNG``'s own deliberate - scope: that backend's upfront gate (``_pca_reduction_requested()``) - only ever fires for an EXPLICIT ``pcakeep``/``pcadb`` request -- - checked before ``_preprocess`` runs at all, purely so a bad explicit - config fails fast without paying for any preprocessing work -- and - never for AUTOMATIC ``mineig``/``mineig_rel``-detected reduction, - which torch itself only catches downstream, per-waypoint, inside its - own :meth:`mir` call (issue #283's fix targeted exactly that - automatic-detection gap, not the upfront-vs-downstream split, which - issue #300 chose to keep). This backend has no explicit - ``pcakeep``/``pcadb`` parameter at all, so there is nothing for an - upfront gate to check regardless of preprocessing order -- the - automatic-reduction case is caught the same way on both backends: - downstream, per-waypoint, inside :meth:`mir`'s own :meth:`_pca_reduced` - guard, whose ``ValueError`` is caught and logged here rather than - propagated. + Incompatible with PCA reduction, same as :meth:`mir` itself, and + gated exactly as ``AMICATorchNG._fit_once`` gates it (issue #323): + an explicit reduction request, ``pcakeep < n_channels`` or any + ``pcadb`` while sphering (``pcakeep >= n_channels`` keeps every + dimension, and ``do_sphere=False`` never reduces, so neither is one), + raises ``ValueError`` up front, before :meth:`_preprocess` + runs, so a bad explicit config fails without paying for any + preprocessing. AUTOMATIC ``mineig``/``mineig_rel`` reduction is not a + request, and is caught the same way on both backends: downstream, + per-waypoint, inside :meth:`mir`'s own :meth:`_pca_reduced` guard, + whose ``ValueError`` is caught and logged here rather than propagated + (issue #283; issue #300 chose to keep that upfront-vs-downstream + split). """ if X.ndim != 2: raise ValueError(f"X must be 2D (n_channels, n_samples), got {X.shape}") - if X.shape[0] != self.n_channels: + if X.shape[0] != self._n_input_channels: raise ValueError( - f"X has {X.shape[0]} channels, model expects {self.n_channels}" + f"X has {X.shape[0]} channels, model expects {self._n_input_channels}" ) if mir_step < 0: raise ValueError(f"mir_step must be >= 0, got {mir_step}") @@ -2454,6 +2502,23 @@ def _fit_once( # "did an E-step ever actually run", only "did stop_reason end # up degenerate". Reject up front instead. raise ValueError(f"max_iter must be >= 1, got {max_iter}") + # Same gate and message as AMICATorchNG._fit_once (issue #323). It + # sees only the explicit request; automatic reduction is caught per + # waypoint by mir()'s fitted-geometry guard below. + if mir_step > 0 and self._pca_reduction_requested(X.shape[0]): + raise ValueError( + "mir_step > 0 is incompatible with PCA reduction " + "(pcakeep/pcadb): the sphere is rank-deficient, so MIR's " + "log-Jacobian term is undefined. Rejected up front rather " + "than failing mid-fit at the first waypoint." + ) + + # Size every fit from the input geometry, as AMICATorchNG._fit_once + # does. _preprocess shrinks n_channels/n_comps to the kept rank, so + # without this a refit, or the second of n_restarts, would start from + # the previous fit's rank. A no-op for full-rank data. + self.n_channels = self._n_input_channels + self.n_comps = self.n_channels * self.n_models X_t = self._preprocess(X) n_total = X_t.shape[1] @@ -2513,7 +2578,7 @@ def _fit_once( # keep_best defaults on, so a user enabling sharing/rejection # would otherwise silently lose the safeguard; surface it once. # do_reject checked first, matching AMICATorchNG's precedence - # exactly (torch_impl/core.py:2425) -- both can be true at once + # exactly (``AMICATorchNG._fit_once``) -- both can be true at once # (see test_mlx_reject.py's genuine-merge-plus-reject test), and # the two backends must report the same reason for the same # configuration. @@ -2669,8 +2734,8 @@ def _fit_once( # Name the offenders. Everything here is already materialized, so # the per-tensor reads add no mid-graph sync -- this is the MLX # stand-in for AMICATorchNG's inline mu/beta/alpha canary - # (torch_impl/core.py:1461-1474), which MLX cannot afford inside - # _update_parameters because it would sync the lazy graph. + # (in ``AMICATorchNG._update_parameters``), which MLX cannot afford + # inside _update_parameters because it would sync the lazy graph. bad = [ name for name, value in checked.items() @@ -2686,7 +2751,7 @@ def _fit_once( break # Extended-Infomax adaptive PDF switch (Fortran do_choose_pdfs, - # AMICATorchNG core.py:2030-2042). Runs on the + # ``AMICATorchNG._fit_once``). Runs on the # kurt_start/num_kurt/kurt_int schedule using the just-updated W; # the new per-source families take effect from the next E-step. # itf is the Fortran-style 1-indexed iteration. num_kurt=0 disables @@ -2903,8 +2968,8 @@ def _fit_once( # Switching Newton on changes the step direction, so the decrease # counter accumulated during the natural-gradient phase no longer # describes the schedule now running: Fortran clears it on the - # switch-on iteration (amica15.f90:1099-1102, AMICATorchNG - # core.py:2218-2219). + # switch-on iteration (amica15.f90:1099-1102, + # ``AMICATorchNG._fit_once``). if self.do_newton and it == self.newt_start: numdecs = 0 @@ -2983,7 +3048,7 @@ def _fit_once( def transform(self, X: np.ndarray, model_idx: int = 0) -> np.ndarray: """Apply the learned unmixing matrix to (new) data (issue #287, port of - ``AMICATorchNG.transform``, torch_impl/core.py:2928-2946). + ``AMICATorchNG.transform``). Sources are ``S = W[model_idx]^T @ (sphere @ (X - mean) - c[:, model_idx])`` (issue #24 transpose convention, issue #27 per-model @@ -3014,7 +3079,7 @@ def transform(self, X: np.ndarray, model_idx: int = 0) -> np.ndarray: # ------------------------------------------------------------------ def _check_model_idx(self, model_idx: int) -> None: """Validate a model index against the fitted ``n_models`` (AMICATorchNG - ``_check_model_idx``, core.py:2911-2926). Raises a clear ``ValueError`` + ``_check_model_idx``). Raises a clear ``ValueError`` (rejecting negatives, which MLX's negative indexing would otherwise turn into a silent wrong-model result) instead of an opaque array error.""" if not isinstance(model_idx, (int, np.integer)): @@ -3029,7 +3094,7 @@ def _check_model_idx(self, model_idx: int) -> None: def get_pdftype(self, model_idx: int = 0) -> np.ndarray: """Per-source density-family code for model ``model_idx`` (AMICATorchNG - ``get_pdftype``, core.py:2609-2627). + ``get_pdftype``). One integer per source component (0-4; 0 generalized Gaussian, 1 super-Gaussian cosh, 2 Gaussian, 3 logistic, 4 sub-Gaussian cosh). All @@ -3062,8 +3127,7 @@ def get_pdftype(self, model_idx: int = 0) -> np.ndarray: def get_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: """True mixing matrix ``A_fort`` = (stored A)^T (issue #24 convention; - issue #287 port of ``AMICATorchNG.get_mixing_matrix``, torch_impl/ - core.py:2948-2956).""" + issue #287 port of ``AMICATorchNG.get_mixing_matrix``).""" if self.A is None or self.comp_list is None: raise RuntimeError( "AMICAMLXNG.get_mixing_matrix() requires a fitted model; call " @@ -3075,24 +3139,26 @@ def get_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: @property def n_channels_in(self) -> int: """Input channel count, i.e. the width of the sphere (issue #287 port - of ``AMICATorchNG.n_channels_in``, torch_impl/core.py:2958-2967). + of ``AMICATorchNG.n_channels_in``). Differs from ``n_channels`` only when rank reduction shrank the model to the detected numerical rank (issue #223); equal to it for full-rank data and before :meth:`fit`/:meth:`from_state_dict`. - Derived from the sphere (rather than stored) so it cannot drift from - the sphere it describes -- including on a reloaded rank-reduced - model, whose ``sphere`` width is exactly this value (see - :meth:`_load_params`'s shape guard). + Read off the sphere whenever one exists, so it cannot drift from the + sphere it describes, including on a reloaded rank-reduced model, + whose ``sphere`` width is exactly this value (see + :meth:`_load_params`'s shape guard). Before the first fit it is the + constructor's channel count, the width :meth:`fit` accepts. """ - return self.n_channels if self.sphere is None else int(self.sphere.shape[1]) + if self.sphere is None: + return self._n_input_channels + return int(self.sphere.shape[1]) def get_sensor_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: """Mixing matrix mapped back to input-channel space (issue #287 port of - ``AMICATorchNG.get_sensor_mixing_matrix``, torch_impl/core.py: - 2969-2991): ``pinv(sphere) @ A``, via :meth:`_pinv_sphere` -- the only - correct back-map when rank reduction has left the sphere non-square - (issue #223). + ``AMICATorchNG.get_sensor_mixing_matrix``): ``pinv(sphere) @ A``, via + :meth:`_pinv_sphere` -- the only correct back-map when rank reduction has left + the sphere non-square (issue #223). """ if self.sphere is None: raise RuntimeError( @@ -3110,10 +3176,9 @@ def get_sensor_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: def get_unmixing_matrix(self, model_idx: int = 0) -> np.ndarray: """True unmixing matrix ``W_fort`` = (stored W)^T (issue #24 - convention; issue #287 port of ``AMICATorchNG.get_unmixing_matrix``, - torch_impl/core.py:2993-3001). MLX's ``W`` is model-major (``(n_models, - n, n)``), so the per-model slice is ``W[model_idx]`` rather than - torch's ``W[:, :, model_idx]``.""" + convention; issue #287 port of ``AMICATorchNG.get_unmixing_matrix``). MLX's + ``W`` is model-major (``(n_models, n, n)``), so the per-model slice is + ``W[model_idx]`` rather than torch's ``W[:, :, model_idx]``.""" if self.W is None: raise RuntimeError( "AMICAMLXNG.get_unmixing_matrix() requires a fitted model; " @@ -3124,8 +3189,7 @@ def get_unmixing_matrix(self, model_idx: int = 0) -> np.ndarray: def get_rho(self, model_idx: int = 0) -> np.ndarray: """Generalized-Gaussian shape parameter ``rho`` for model - ``model_idx`` (issue #287 port of ``AMICATorchNG.get_rho``, torch_impl/ - core.py:3231-3259; issue #142). + ``model_idx`` (issue #287 port of ``AMICATorchNG.get_rho``; issue #142). One value per (mixture component, source): ``rho == 2`` is Gaussian- shaped, ``rho == 1`` Laplacian, ``rho < 1`` heavier-tailed. Only the @@ -3156,14 +3220,13 @@ def get_rho(self, model_idx: int = 0) -> np.ndarray: # ------------------------------------------------------------------ # EEGLAB drop-in output (issue #92; epic #278 polish port of - # AMICATorchNG.variance_order, torch_impl/core.py:3223-3283) + # AMICATorchNG.variance_order) # ------------------------------------------------------------------ def variance_order( self, model_idx: int = 0, return_svar: bool = False ) -> np.ndarray | tuple: """EEGLAB back-projected-variance component order (IC1 = highest - variance) (port of ``AMICATorchNG.variance_order``, torch_impl/ - core.py:3223-3283). + variance) (port of ``AMICATorchNG.variance_order``). Returns the source indices sorted by descending back-projected variance, the ordering EEGLAB's ``loadmodout15.m`` applies on load @@ -3232,21 +3295,36 @@ def variance_order( # ------------------------------------------------------------------ # MIR/PMI diagnostics (issue #137; epic #278 Phase 3/#289 port of - # AMICATorchNG.mir/pmi, torch_impl/core.py:2929-3036) + # AMICATorchNG.mir/pmi) # ------------------------------------------------------------------ + def _pca_reduction_requested(self, n_channels: int) -> bool: + """Whether the explicit ``pcakeep``/``pcadb`` asks to fit fewer than + ``n_channels`` dimensions (port of + ``AMICATorchNG._pca_reduction_requested``; + both delegate to :func:`pamica.rank.pca_reduction_requested`, issue + #323). + + ``n_channels`` is the channel count of the data being fitted, not + ``self.n_channels``, which :meth:`_preprocess` shrinks to the kept + rank. Config-only, not geometry: used solely by :meth:`_fit_once`'s + upfront ``mir_step`` gate, which runs before this fit's sphere exists, + so AUTOMATIC ``mineig``/``mineig_rel`` reduction is not knowable here. + Use :meth:`_pca_reduced` wherever a fitted sphere already exists. + """ + return pca_reduction_requested( + self.pcakeep, self.pcadb, n_channels, self.do_sphere + ) + def _pca_reduced(self) -> bool: """Whether the fitted sphere is rank-reduced (non-square) -- the #300 - fitted-geometry guard (port of ``AMICATorchNG._pca_reduced``, - torch_impl/core.py:2942-2956). - - Derived from the fitted geometry -- ``sphere.shape[0] != - sphere.shape[1]`` -- so it also catches rank reduction from - AUTOMATIC numerical-rank detection (``mineig``/``mineig_rel``), not - just an explicit reduction request. Unlike ``AMICATorchNG``, this - backend has no explicit ``pcakeep``/``pcadb`` constructor parameter - (only automatic ``mineig``/``mineig_rel`` reduction), so there is no - separate config-only ``_pca_reduction_requested()`` check to make - redundant here: this geometry check is the only PCA guard MIR needs. + fitted-geometry guard (port of ``AMICATorchNG._pca_reduced``). + + Derived from the fitted geometry (``sphere.shape[0] != + sphere.shape[1]``), so it catches rank reduction from an explicit + ``pcakeep``/``pcadb`` and from AUTOMATIC numerical-rank detection + (``mineig``/``mineig_rel``) alike. The config-only + :meth:`_pca_reduction_requested` complements it for the upfront + ``mir_step`` gate, which runs before this fit's sphere exists. ``False`` before :meth:`fit` (``sphere`` is ``None``) and for a full-rank fit. """ @@ -3286,8 +3364,8 @@ def mir( If the model is unfitted. ValueError If the fitted sphere is rank-reduced (non-square): whether from - automatic ``mineig``/``mineig_rel`` numerical-rank detection (the - only source of reduction on this backend), the sphere is + explicit ``pcakeep``/``pcadb`` or from automatic ``mineig``/ + ``mineig_rel`` numerical-rank detection, the sphere is rank-deficient, so MIR's log-Jacobian term is undefined (issue #283/#300). """ @@ -3300,10 +3378,10 @@ def mir( raise ValueError( "mir() is incompatible with PCA reduction: the fitted " f"sphere is rank-deficient ({self.n_channels} of " - f"{self.n_channels_in} channels kept) from automatic " - "mineig/mineig_rel numerical-rank detection, so MIR's " - "log-Jacobian term is undefined for the resulting " - "non-square/non-invertible unmixing." + f"{self.n_channels_in} channels kept), whether from explicit " + "pcakeep/pcadb or automatic mineig/mineig_rel numerical-rank " + "detection, so MIR's log-Jacobian term is undefined for the " + "resulting non-square/non-invertible unmixing." ) unmixing = np.array(self.W[model_idx].T @ self.sphere) return mir_metric(unmixing, X, nbins) @@ -3339,8 +3417,7 @@ def pmi( # ------------------------------------------------------------------ # Multi-model posterior (issue #141; epic #278 Phase 3/#289 port of - # AMICATorchNG.model_loglik/model_probability, torch_impl/core.py: - # 3041-3132) + # AMICATorchNG.model_loglik/model_probability) # ------------------------------------------------------------------ def model_loglik(self, X: np.ndarray) -> np.ndarray: """Per-model, per-sample log-likelihood ``Lht`` on (new) data. @@ -3439,7 +3516,7 @@ def model_probability(self, X: np.ndarray) -> np.ndarray: # ------------------------------------------------------------------ # EEGLAB export (issue #92; epic #278 Phase 3/#289 port of - # AMICATorchNG.write_amica_output, torch_impl/core.py:3359-3471) + # AMICATorchNG.write_amica_output) # ------------------------------------------------------------------ def write_amica_output(self, outdir) -> None: """Write this fitted model as the Fortran/EEGLAB AMICA output @@ -3572,7 +3649,7 @@ def write_amica_output(self, outdir) -> None: # Persistence (issue #287) # ------------------------------------------------------------------ # Full fitted-parameter snapshot -- the same 12-name set as AMICATorchNG's - # _PARAM_TENSORS (torch_impl/core.py:3484-3489): A/W/c/comp_list/mean/ + # _PARAM_TENSORS: A/W/c/comp_list/mean/ # sphere are what transform()/get_*matrix() read back; mu/alpha/beta/rho/ # gm are the mixture-PDF EM state; pdtype is the per-source density-family # code (issue #265) -- a non-default pdftype model, or the adaptive @@ -3722,6 +3799,14 @@ def state_dict(self) -> dict: "do_mean": self.do_mean, "do_sphere": self.do_sphere, "do_approx_sphere": self.do_approx_sphere, + # Explicit PCA reduction (issue #323). Additive, like keep_best + # below: a payload written before #323 lacks both keys, and + # cls(**config) then falls back to the constructor defaults (None), + # which is what that fit ran with, so no format_version bump. Cast + # to plain int/float because save() JSON-encodes config and the + # validator accepts numpy scalars (np.int64), which json cannot. + "pcakeep": None if self.pcakeep is None else int(self.pcakeep), + "pcadb": None if self.pcadb is None else float(self.pcadb), "mineig": self.mineig, "mineig_rel": self.mineig_rel, "seed": self.seed, @@ -3837,6 +3922,10 @@ def _load_params(self, state: dict) -> None: f"(n_channels, n_channels_in) with n_channels={n}" ) n_channels_in = sphere_shape[1] + # config's n_channels is the fitted (possibly reduced) rank, so the + # constructor set the input count to it; the restored sphere's width + # is the true input count, which a refit validates X against. + self._n_input_channels = int(n_channels_in) expected_shapes = { "A": (n, ncomp), "W": (m, n, n), diff --git a/pamica/numpy_impl/core.py b/pamica/numpy_impl/core.py index aec9eead..aba267e3 100644 --- a/pamica/numpy_impl/core.py +++ b/pamica/numpy_impl/core.py @@ -81,7 +81,13 @@ from tqdm import tqdm from .. import blocktune from .. import restarts -from ..rank import MINEIG, MINEIG_REL, numerical_rank +from ..rank import ( + MINEIG, + MINEIG_REL, + log_ignored_pca_request, + numerical_rank, + validate_pca_reduction, +) from .utils import ( gammaln, identify_shared_components, @@ -170,6 +176,16 @@ def __init__( pamica extension: Fortran has no search over seeds, and it is unrelated to ``maxrestarts``/``restartiter``, its recovery path after an early non-finite likelihood. See :mod:`pamica.restarts`. + + ``pcakeep`` (None) and ``pcadb`` (None) carry AMICATorchNG's + names, defaults, validation and semantics (issue #323): explicit + PCA reduction capped by the detected numerical rank. ``pcakeep`` + must be an integer >= 1 and ``pcadb`` a finite number > 0, or the + constructor raises ``ValueError``; when both are set ``pcakeep`` + takes precedence and ``pcadb`` is ignored, as in the reference + (which parses ``pcadb`` but never uses it). Both are ignored, with + one WARNING, when ``do_sphere`` is False, as in the reference. See + :mod:`pamica.rank`. """ # Store progress bar settings self.use_tqdm = use_tqdm @@ -273,6 +289,12 @@ def __init__( self.do_approx_sphere = params.get("do_approx_sphere", True) self.pcakeep = params.get("pcakeep") self.pcadb = params.get("pcadb") + # Explicit PCA reduction, validated by the policy shared with the + # PyTorch and MLX backends (pamica/rank.py, issue #323): a bad value + # fails here rather than as a silently wrongly sized or degenerate fit; + # then one log line for any part of it a fit will ignore. + validate_pca_reduction(self.pcakeep, self.pcadb) + log_ignored_pca_request(self.pcakeep, self.pcadb, self.do_sphere) # Numerical-rank floors (issue #223); see pamica/rank.py and ADR 0004. self.mineig = params.get("mineig", MINEIG) self.mineig_rel = params.get("mineig_rel", MINEIG_REL) diff --git a/pamica/rank.py b/pamica/rank.py index bfe488a9..1c9fdec2 100644 --- a/pamica/rank.py +++ b/pamica/rank.py @@ -15,13 +15,27 @@ Only the *decision* is shared. Each backend builds its own sphering matrix in its own array library, because the PyTorch path is bit-exact against Fortran and must not be routed through a different eigensolver. + +The explicit PCA-reduction request (``pcakeep``/``pcadb``) is part of the same +decision, so its validation and its "is a reduction requested?" predicate live +here too (issue #323): every backend constructor calls +:func:`validate_pca_reduction` and :func:`log_ignored_pca_request`, +:func:`numerical_rank` re-checks at fit time, and the upfront ``mir_step`` gates +ask :func:`pca_reduction_requested`. ``pcadb`` is a pamica extension: the +reference parses it (amica15.f90:3459-3461) but never reads it again, so only +``pcakeep`` enters its ``numeigs``. """ +import logging +import math +import numbers from typing import Optional import numpy as np import numpy.typing as npt +logger = logging.getLogger(__name__) + # Fortran's absolute covariance-eigenvalue floor (amica15_header.f90:66). MINEIG = 1e-15 @@ -31,6 +45,146 @@ MINEIG_REL = 1e-12 +def validate_pca_reduction(pcakeep: Optional[int], pcadb: Optional[float]) -> None: + """Reject an explicit PCA-reduction request that cannot mean anything. + + Called by every backend constructor, so a bad value fails before any data + is touched, and again at fit time by :func:`numerical_rank` and + :func:`pca_reduction_requested`, which catch an attribute reassigned after + construction. Validation only: it never logs, so the fit-time re-checks + stay silent (the constructors log through :func:`log_ignored_pca_request`). + + Parameters + ---------- + pcakeep : int, optional + Number of principal dimensions to keep. Must be ``None`` or an integer + (``numbers.Integral``, so numpy integers qualify) of at least 1. + ``bool`` and ``numpy.bool_`` are rejected (``bool`` subclasses + ``int``), and a float is rejected rather than truncated. Unvalidated, + ``pcakeep=-3`` sliced from the end and fitted 29 of 32 dimensions, + ``pcakeep=2.7`` silently kept 2, and ``pcakeep=0`` built an empty model + that ended in ``nan_ll``. + pcadb : float, optional + Keep the dimensions whose eigenvalue lies within ``pcadb`` dB of the + largest. Must be ``None`` or a finite real number (``numbers.Real``, so + numpy floats qualify; ``bool`` and ``numpy.bool_`` rejected) greater + than 0; ``pcadb <= 0`` kept nothing and ended in ``nan_ll``. + + Raises + ------ + ValueError + Naming the parameter and the offending value. + + Notes + ----- + Setting both is allowed: ``pcakeep`` takes precedence and ``pcadb`` is + ignored, the order :func:`numerical_rank` applies them in. This matches + the reference, which parses ``pcadb`` (amica15.f90:3459-3461) but never + uses it, so a Fortran ``input.param`` that sets both (as both bundled + parameter files do) means its ``pcakeep``. + """ + if pcakeep is not None and ( + isinstance(pcakeep, bool) + or not isinstance(pcakeep, numbers.Integral) + or pcakeep < 1 + ): + raise ValueError(f"pcakeep must be None or an integer >= 1, got {pcakeep!r}") + if pcadb is not None and ( + isinstance(pcadb, bool) + or not isinstance(pcadb, numbers.Real) + or not math.isfinite(pcadb) + or pcadb <= 0 + ): + raise ValueError( + f"pcadb must be None or a finite real number > 0, got {pcadb!r}" + ) + + +def log_ignored_pca_request( + pcakeep: Optional[int], pcadb: Optional[float], do_sphere: bool +) -> None: + """Log the part of a valid ``pcakeep``/``pcadb`` request that a fit will + ignore. Called once by every backend constructor, after + :func:`validate_pca_reduction`, so each note appears once per model. + + * ``do_sphere=False``: one WARNING that both are ignored. Reduction only + happens while sphering, as in the reference, whose no-sphere branch keeps + every dimension (``numeigs = nx``, amica15.f90:527). + * Otherwise, both set: one INFO line that ``pcadb`` is ignored because + ``pcakeep`` takes precedence (see :func:`validate_pca_reduction`). + + Parameters + ---------- + pcakeep, pcadb : int, float, optional + The validated request. + do_sphere : bool + The backend's ``do_sphere`` setting. + """ + requested = [ + f"{name}={value!r}" + for name, value in (("pcakeep", pcakeep), ("pcadb", pcadb)) + if value is not None + ] + if not requested: + return + if not do_sphere: + logger.warning( + "%s ignored because do_sphere=False: PCA reduction happens only " + "while sphering, as in the reference, which keeps every dimension " + "when it does not sphere (numeigs = nx, amica15.f90:527).", + " and ".join(requested), + ) + elif pcakeep is not None and pcadb is not None: + logger.info( + "pcakeep=%d and pcadb=%g are both set; pcadb is ignored because " + "pcakeep takes precedence (as in the reference, which parses pcadb " + "but never uses it).", + pcakeep, + pcadb, + ) + + +def pca_reduction_requested( + pcakeep: Optional[int], + pcadb: Optional[float], + n_channels: int, + do_sphere: bool, +) -> bool: + """Whether an explicit ``pcakeep``/``pcadb`` asks to fit fewer than + ``n_channels`` dimensions. + + Config-only: it runs before any eigenvalue exists, for the upfront + ``mir_step`` gates, so it answers what the *request* implies, not what + the fit will find. ``pcakeep >= n_channels`` is not a request (the + reference's ``min(pcakeep, ...)`` keeps every dimension the data have, + e.g. the bundled ``input.param``'s ``pcakeep 32`` on 32 channels); any + ``pcadb`` is one, because whether it cuts anything depends on the + eigenvalues; and ``pcakeep`` wins when both are set, as in + :func:`numerical_rank`. Without sphering nothing is a request, because no + backend reduces then (see :func:`log_ignored_pca_request`). Rank reduction + from automatic detection (``mineig``/``mineig_rel``) is not a request and + cannot be known here; the backends catch it once the fitted sphere exists. + + Validates first, so a ``pcakeep``/``pcadb`` reassigned after construction + raises the validator's ``ValueError`` here rather than a ``TypeError``. + + Parameters + ---------- + pcakeep, pcadb : int, float, optional + As :func:`validate_pca_reduction`. + n_channels : int + Channel count of the data being fitted. + do_sphere : bool + The backend's ``do_sphere`` setting. + """ + validate_pca_reduction(pcakeep, pcadb) + if not do_sphere: + return False + if pcakeep is not None: + return pcakeep < n_channels + return pcadb is not None + + def numerical_rank( evals: npt.ArrayLike, *, @@ -56,7 +210,9 @@ def numerical_rank( reproduce Fortran's absolute-only behavior exactly. pcakeep, pcadb : int or float, optional Explicit PCA reduction, capped by the detected rank - (Fortran ``min(pcakeep, ...)``). + (Fortran ``min(pcakeep, ...)``). Validated first by + :func:`validate_pca_reduction`; ``pcakeep`` takes precedence and + ``pcadb`` is ignored when both are set. Returns ------- @@ -66,10 +222,15 @@ def numerical_rank( Raises ------ ValueError - If no eigenvalue clears the threshold, so there is nothing to - decompose. Fortran would compute ``numeigs = 0`` and carry on into - undefined behavior. + If ``pcakeep``/``pcadb`` is invalid (see + :func:`validate_pca_reduction`), or if no eigenvalue clears the + threshold, so there is nothing to decompose. Fortran would compute + ``numeigs = 0`` and carry on into undefined behavior. """ + # The fit-time choke point every backend passes through, so a pcakeep or + # pcadb reassigned after construction cannot slip past the constructors' + # checks. Validation only: the constructor already logged any notes. + validate_pca_reduction(pcakeep, pcadb) ev = np.asarray(evals, dtype=np.float64) if ev.ndim != 1 or ev.size == 0: raise ValueError( diff --git a/pamica/tests/mlx_tests/test_mlx_mir.py b/pamica/tests/mlx_tests/test_mlx_mir.py index bba2888d..64b07c55 100644 --- a/pamica/tests/mlx_tests/test_mlx_mir.py +++ b/pamica/tests/mlx_tests/test_mlx_mir.py @@ -275,8 +275,8 @@ def flaky_mir(self, X, **kwargs): # --- the #300 fitted-geometry PCA guard ------------------------------------- def test_mir_raises_under_auto_detected_rank_reduction(real_data): """Rank reduction from AUTOMATIC mineig/mineig_rel numerical-rank - detection (no explicit pcakeep/pcadb -- this backend has none) must - trip the documented ValueError, not an opaque LinAlgError. Real EEG + detection (no explicit pcakeep/pcadb requested) must trip the + documented ValueError, not an opaque LinAlgError. Real EEG projected onto a rank-16 subspace via SVD, matching test_amica_ng_wrapper.py::test_mir_raises_under_auto_detected_rank_reduction.""" x = real_data - real_data.mean(axis=1, keepdims=True) @@ -305,8 +305,9 @@ def test_mir_step_on_rank_reduced_data_completes_with_one_warned_nan_waypoint( ): """No upfront mir_step gate exists for AUTOMATIC rank reduction on EITHER backend (see amica-differences.md's "mir_step's upfront - PCA-reduction gate" section -- this is intentional torch-parity, not a - gap): fit(mir_step=N) on rank-reduced real data must still complete. + PCA-reduction gate" section; the gate sees only an explicit + pcakeep/pcadb request, identically on both): fit(mir_step=N) on + rank-reduced real data must still complete. The PCA-reduction ValueError is a geometry fact of this fit -- it will fire identically on EVERY scheduled waypoint for as long as the fit runs -- so PR #318's flood fix means only the FIRST scheduled waypoint diff --git a/pamica/tests/test_pca_reduction_cross_backend.py b/pamica/tests/test_pca_reduction_cross_backend.py new file mode 100644 index 00000000..1ce05bbb --- /dev/null +++ b/pamica/tests/test_pca_reduction_cross_backend.py @@ -0,0 +1,650 @@ +"""Explicit PCA reduction (``pcakeep``/``pcadb``) across backends (issue #323, +epic #324 Phase 1). + +The validation, the ``pcakeep``-over-``pcadb`` precedence and the "is a +reduction requested?" predicate are one shared decision (``pamica/rank.py``, +unit-tested in ``test_rank_policy.py``). This module is the anti-drift guard +required by ``.rules/backend_parity.md``. It fails if any backend applies that +decision differently, whether as a different kept rank, a different sphere, a +different upfront ``mir_step`` verdict, or a request that one backend accepts +and another rejects. + +PyTorch and NumPy always run, so a divergence between them cannot land. MLX is +an optional Apple-Silicon backend: its checks skip individually (via +``pytest.importorskip`` plus an Apple-GPU guard) instead of skipping the +module, so the unconditional comparisons still run everywhere. + +Real bundled sample EEG only (32 channels x 30504 frames), no synthetic data or +mocks (``.rules/testing.md``). Every fit is a few iterations: these are +preprocessing and gate tests, not convergence tests. +""" + +import logging +import math +from pathlib import Path +from typing import Callable, Dict, Tuple + +import numpy as np +import pytest +import torch +from scipy.optimize import linear_sum_assignment + +from pamica import AMICA, AMICA_NumPy +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +NMIX = 3 +SEED = 42 + +PCAKEEP = 20 +# Chosen from the sample's covariance spectrum (population covariance, dB below +# the largest eigenvalue): the 10th eigenvalue sits at -17.21 dB and the 11th at +# -19.92 dB, one of the widest gaps in the spectrum, so pcadb=18.5 keeps +# exactly 10 dimensions with more than 1.2 dB of margin on either side. +PCADB = 18.5 +PCADB_RANK = 10 +# The bundled input.param/sample_params.json value. On its own it keeps 26 of +# the sample's 32 dimensions (test_rank_policy.py pins that), so it visibly +# changes the model unless pcakeep's precedence discards it. +BUNDLED_PCADB = 30.0 + +CONFIGS: Dict[str, dict] = { + "pcakeep": {"pcakeep": PCAKEEP}, + "pcadb": {"pcadb": PCADB}, + "both": {"pcakeep": PCAKEEP, "pcadb": BUNDLED_PCADB}, +} +EXPECTED_RANK = {"pcakeep": PCAKEEP, "pcadb": PCADB_RANK, "both": PCAKEEP} + +# Torch and NumPy build the sphere from the same float64 covariance with +# different LAPACK eigensolvers (torch.linalg.eigh vs scipy.linalg.eigh), so +# agreement is to roundoff, not bit-exact. Measured on the sample: max abs +# difference 2.2e-14 (entries up to 0.12), max entrywise relative 2.0e-11. +SPHERE_RTOL = 1e-10 +SPHERE_ATOL = 1e-13 +# MLX stores the float32 cast of its float64 host sphere (_sphere_np), so the +# GPU copy agrees to float32 rounding (relative 2**-24 ~ 6e-8; measured max +# abs 3.6e-9) and the host copy to float64 roundoff (measured 3.5e-15). +MLX_F32_RTOL = 1e-6 +MLX_F32_ATOL = 1e-12 + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +@pytest.fixture(scope="module") +def real_data() -> np.ndarray: + return load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD).astype( + np.float64 + ) + + +def _mlx_core(): + """The MLX backend module, or skip this test (never the whole module).""" + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + mx = mlx_core.mx + if mx.default_device().type != mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + return mlx_core + + +def _torch(**kwargs) -> AMICATorchNG: + return AMICATorchNG( + n_channels=NW, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + **kwargs, + ) + + +def _numpy(outdir: Path, max_iter: int = 2, **kwargs): + return AMICA_NumPy( + num_models=1, + num_mix=NMIX, + max_iter=max_iter, + seed=SEED, + use_tqdm=False, + outdir=str(outdir), + **kwargs, + ) + + +def _mlx(**kwargs): + return _mlx_core().AMICAMLXNG(n_channels=NW, n_mix=NMIX, seed=SEED, **kwargs) + + +# A fitted backend reduced to what these tests compare: the kept source count, +# the input width, the float64 sphere and sldet (plus the model itself). +Fit = Dict[str, object] + + +def _fit_torch(X: np.ndarray, _tmp: Path, **kwargs) -> Fit: + m = _torch(**kwargs) + m.fit(X, max_iter=2, verbose=False) + assert m.stop_reason not in AMICATorchNG._DEGENERATE_STOP_REASONS + assert m.sphere is not None + return { + "n": m.n_channels, + "n_in": m.n_channels_in, + "sphere": m.sphere.cpu().numpy(), + "sldet": m.sldet, + "model": m, + } + + +def _fit_numpy(X: np.ndarray, tmp: Path, **kwargs) -> Fit: + m = _numpy(tmp, **kwargs) + m.fit(X) + assert m.converged is True + return { + "n": m.data_dim, + "n_in": m.data_dim_in, + "sphere": np.asarray(m.sphere), + "sldet": m.sldet, + "model": m, + } + + +def _fit_mlx(X: np.ndarray, _tmp: Path, **kwargs) -> Fit: + m = _mlx(**kwargs) + m.fit(X, max_iter=2, verbose=False) + assert m.stop_reason not in m._DEGENERATE_STOP_REASONS + return { + "n": m.n_channels, + "n_in": m.n_channels_in, + "sphere": np.array(m.sphere).astype(np.float64), + "sphere_host": m._sphere_np, + "sldet": m.sldet, + "model": m, + } + + +FITTERS: Dict[str, Callable[..., Fit]] = { + "torch": _fit_torch, + "numpy": _fit_numpy, + "mlx": _fit_mlx, +} + + +@pytest.fixture(scope="module") +def fits(real_data, tmp_path_factory) -> Callable[[str, str], Fit]: + """Fit each (backend, config) pair once per module and share it.""" + cache: Dict[Tuple[str, str], Fit] = {} + + def get(backend: str, config: str) -> Fit: + if backend == "mlx": + _mlx_core() # skip before fitting, not after + key = (backend, config) + if key not in cache: + tmp = tmp_path_factory.mktemp(f"{backend}_{config}") + cache[key] = FITTERS[backend](real_data, tmp, **CONFIGS[config]) + return cache[key] + + return get + + +# --- (a)/(b): every backend keeps the same rank and builds the same sphere --- +@pytest.mark.parametrize("config", ["pcakeep", "pcadb"]) +def test_torch_and_numpy_agree_on_the_reduced_sphere(fits, config): + """Unconditional anti-drift pair: runs wherever the test suite runs.""" + rank = EXPECTED_RANK[config] + t = fits("torch", config) + n = fits("numpy", config) + for fit in (t, n): + assert fit["n"] == rank + assert fit["n_in"] == NW + assert fit["sphere"].shape == (rank, NW) + np.testing.assert_allclose( + n["sphere"], t["sphere"], rtol=SPHERE_RTOL, atol=SPHERE_ATOL + ) + assert n["sldet"] == pytest.approx(t["sldet"], rel=1e-12) + + +@pytest.mark.parametrize("config", ["pcakeep", "pcadb"]) +def test_mlx_agrees_with_torch_on_the_reduced_sphere(fits, config): + rank = EXPECTED_RANK[config] + t = fits("torch", config) + m = fits("mlx", config) + assert m["n"] == rank + assert m["n_in"] == NW + assert m["sphere"].shape == (rank, NW) + # The GPU copy is float32; the host copy is the float64 computation. + np.testing.assert_allclose( + m["sphere"], t["sphere"], rtol=MLX_F32_RTOL, atol=MLX_F32_ATOL + ) + np.testing.assert_allclose( + m["sphere_host"], t["sphere"], rtol=SPHERE_RTOL, atol=SPHERE_ATOL + ) + assert m["sldet"] == pytest.approx(t["sldet"], rel=1e-12) + + +# --- (c): pcakeep wins over pcadb, identically on every backend -------------- +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_pcakeep_and_pcadb_together_equal_pcakeep_alone(fits, backend): + """BUNDLED_PCADB alone would keep 26 dimensions; with pcakeep set it is + ignored, so the sphere is bit-for-bit the pcakeep-only one.""" + both = fits(backend, "both") + alone = fits(backend, "pcakeep") + assert both["n"] == alone["n"] == PCAKEEP + np.testing.assert_array_equal(both["sphere"], alone["sphere"]) + assert both["sldet"] == alone["sldet"] + + +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_precedence_is_logged_once_per_model(real_data, tmp_path, caplog, backend): + """One INFO line at construction; the fit-time re-check in + numerical_rank stays silent, so a fit does not repeat it.""" + X = real_data[:, :4096] + with caplog.at_level(logging.INFO, logger="pamica.rank"): + if backend == "torch": + _torch(pcakeep=PCAKEEP, pcadb=BUNDLED_PCADB).fit( + X, max_iter=1, verbose=False + ) + elif backend == "numpy": + _numpy(tmp_path, max_iter=1, pcakeep=PCAKEEP, pcadb=BUNDLED_PCADB).fit(X) + else: + _mlx(pcakeep=PCAKEEP, pcadb=BUNDLED_PCADB).fit(X, max_iter=1, verbose=False) + notes = [r for r in caplog.records if "takes precedence" in r.getMessage()] + assert len(notes) == 1, [r.getMessage() for r in notes] + + +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_do_sphere_false_ignores_the_request_with_one_warning( + real_data, tmp_path, caplog, backend +): + """No backend reduces without sphering (the reference's no-sphere branch + keeps every dimension, numeigs = nx at amica15.f90:527), so pcakeep=20 is + ignored: the fit is full width, and the constructor says so exactly once.""" + X = real_data[:, :4096] + kwargs = {"pcakeep": PCAKEEP, "do_sphere": False} + with caplog.at_level(logging.INFO, logger="pamica.rank"): + if backend == "torch": + m = _torch(**kwargs) + m.fit(X, max_iter=1, verbose=False) + assert m.sphere is not None + n, sphere = m.n_channels, m.sphere.cpu().numpy() + elif backend == "numpy": + m = _numpy(tmp_path, max_iter=1, **kwargs) + m.fit(X) + n, sphere = m.data_dim, np.asarray(m.sphere) + else: + m = _mlx(**kwargs) + m.fit(X, max_iter=1, verbose=False) + n, sphere = m.n_channels, np.array(m.sphere) + warnings = [ + r + for r in caplog.records + if r.levelno == logging.WARNING and "do_sphere=False" in r.getMessage() + ] + assert len(warnings) == 1, [r.getMessage() for r in caplog.records] + assert n == NW + np.testing.assert_array_equal(sphere, np.eye(NW)) + + +# --- (d): an invalid request fails at construction on every backend ---------- +INVALID = [ + ({"pcakeep": -3}, "pcakeep"), + ({"pcakeep": 0}, "pcakeep"), + ({"pcakeep": 2.7}, "pcakeep"), + ({"pcakeep": True}, "pcakeep"), + ({"pcakeep": "20"}, "pcakeep"), + ({"pcadb": 0.0}, "pcadb"), + ({"pcadb": -5.0}, "pcadb"), + ({"pcadb": math.nan}, "pcadb"), + ({"pcadb": math.inf}, "pcadb"), + ({"pcadb": True}, "pcadb"), + ({"pcakeep": PCAKEEP, "pcadb": -5.0}, "pcadb"), +] + + +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +@pytest.mark.parametrize(("kwargs", "name"), INVALID) +def test_invalid_request_raises_at_construction(tmp_path, backend, kwargs, name): + """Before #323, torch and NumPy accepted every one of these silently: + pcakeep=-3 fitted 29 sources, pcakeep=2.7 fitted 2, and pcakeep=0 or + pcadb<=0 ran to a degenerate nan_ll fit.""" + with pytest.raises(ValueError, match=name): + if backend == "torch": + _torch(**kwargs) + elif backend == "numpy": + _numpy(tmp_path, **kwargs) + else: + _mlx(**kwargs) + + +# --- (e): the upfront mir_step gate gives the same verdict on torch and MLX -- +REDUCING = [ + {"pcakeep": PCAKEEP}, + {"pcakeep": NW - 1}, + {"pcadb": PCADB}, + {"pcakeep": PCAKEEP, "pcadb": BUNDLED_PCADB}, +] +NOT_REDUCING = [ + {"pcakeep": NW}, + {"pcakeep": 2 * NW}, + {"pcakeep": NW, "pcadb": BUNDLED_PCADB}, # the bundled input.param + # Without sphering nothing is reduced, so neither is a request. + {"pcakeep": PCAKEEP, "do_sphere": False}, + {"pcadb": PCADB, "do_sphere": False}, +] + + +def _backend(name: str, **kwargs): + return _torch(**kwargs) if name == "torch" else _mlx(**kwargs) + + +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +@pytest.mark.parametrize("kwargs", REDUCING) +def test_mir_step_rejects_a_reduction_request_up_front(real_data, backend, kwargs): + m = _backend(backend, **kwargs) + with pytest.raises(ValueError, match="Rejected up front"): + m.fit(real_data[:, :4096], max_iter=2, verbose=False, mir_step=1) + # Up front means before preprocessing: no sphere was built. + assert m.sphere is None + + +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +@pytest.mark.parametrize("kwargs", NOT_REDUCING) +def test_mir_step_accepts_a_request_that_reduces_nothing(real_data, backend, kwargs): + """pcakeep >= n_channels keeps every dimension (Fortran's min()), and + do_sphere=False reduces nothing at all, so neither is a reduction request; + the pre-#323 torch gate refused both anyway.""" + m = _backend(backend, **kwargs) + m.fit(real_data[:, :4096], max_iter=2, verbose=False, mir_step=1) + assert m.n_channels == m.n_channels_in == NW + assert [row[0] for row in m.mir_history_] == [0, 1] + assert all(math.isfinite(row[1]) for row in m.mir_history_) + + +# --- (f): the reduced fits find the same components -------------------------- +def _matched_abs_corr(a: np.ndarray, b: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """Hungarian-matched |corr| between the rows (sources) of ``a`` and ``b``: + best-|corr| matching is invariant to the per-component sign, scale and + order. Same construction as ``torch_tests/test_amica_ng_wrapper.py``'s + loadmodout round-trip test (none of the MLX cross-backend suites has a + matching helper to reuse).""" + a = a - a.mean(axis=1, keepdims=True) + b = b - b.mean(axis=1, keepdims=True) + a = a / np.linalg.norm(a, axis=1, keepdims=True) + b = b / np.linalg.norm(b, axis=1, keepdims=True) + corr = np.abs(a @ b.T) + rows, cols = linear_sum_assignment(1.0 - corr) + return corr[rows, cols], cols + + +def test_torch_and_mlx_reduced_fits_find_the_same_components(real_data): + """Same seed, pcakeep=20, 10 iterations: float64 torch vs float32 MLX. + + Threshold: the MLX cross-backend suites (``test_mlx_*_cross_backend.py``) + compare fitted states at float32 tolerance and have no Hungarian-matched + correlation bar, so this uses the strictest one in the suite, min matched + |corr| > 0.999 (``torch_tests/test_amica_ng_wrapper.py``), well above the + 0.95 cross-implementation gate (``torch_tests/test_ng_backend.py:: + test_end_to_end_correlation_vs_fortran``). The log-likelihood bar is + ``mlx_tests/test_mlx_backend.py::test_converged_ll_matches_torch_float32``'s + 1e-2. Measured: min matched |corr| 0.99999994, identity matching, LL + difference 1.9e-7. + """ + mlx_core = _mlx_core() + t = _torch(pcakeep=PCAKEEP) + t.fit(real_data, max_iter=10, verbose=False) + m = mlx_core.AMICAMLXNG(n_channels=NW, n_mix=NMIX, seed=SEED, pcakeep=PCAKEEP) + m.fit(real_data, max_iter=10, verbose=False) + assert t.n_channels == m.n_channels == PCAKEEP + + matched, cols = _matched_abs_corr(t.transform(real_data), m.transform(real_data)) + assert matched.min() > 0.999, f"min matched |corr| {matched.min():.6f}" + # Same seed, same sphere: the components come out in the same order. + np.testing.assert_array_equal(cols, np.arange(PCAKEEP)) + assert t.final_ll_ is not None and m.final_ll_ is not None + assert abs(t.final_ll_ - m.final_ll_) < 1e-2 + + +# --- (g): persistence keeps the request and the reduced sphere -------------- +def test_mlx_save_load_round_trips_the_reduction(real_data, tmp_path): + """numpy scalars are accepted by the validator, so they are the case that + exercises state_dict's cast to JSON-serializable int/float.""" + mlx_core = _mlx_core() + m = mlx_core.AMICAMLXNG( + n_channels=NW, + n_mix=NMIX, + seed=SEED, + pcakeep=np.int64(PCAKEEP), + pcadb=np.float64(BUNDLED_PCADB), + ) + m.fit(real_data, max_iter=2, verbose=False) + path = tmp_path / "model.npz" + m.save(str(path)) + loaded = mlx_core.AMICAMLXNG.load(str(path)) + + assert loaded.pcakeep == PCAKEEP and type(loaded.pcakeep) is int + assert loaded.pcadb == BUNDLED_PCADB and type(loaded.pcadb) is float + assert loaded.n_channels == PCAKEEP and loaded.n_channels_in == NW + np.testing.assert_array_equal(np.array(loaded.sphere), np.array(m.sphere)) + np.testing.assert_array_equal(loaded.transform(real_data), m.transform(real_data)) + + +def test_mlx_payload_without_the_keys_loads_with_defaults(real_data): + """A payload written before #323 has no pcakeep/pcadb config keys; it must + load with the constructor defaults (None), and its sphere still comes from + params, so a reduced model stays reduced.""" + mlx_core = _mlx_core() + m = mlx_core.AMICAMLXNG(n_channels=NW, n_mix=NMIX, seed=SEED, pcakeep=PCAKEEP) + m.fit(real_data, max_iter=2, verbose=False) + state = m.state_dict() + del state["config"]["pcakeep"] + del state["config"]["pcadb"] + loaded = mlx_core.AMICAMLXNG.from_state_dict(state) + + assert loaded.pcakeep is None and loaded.pcadb is None + assert loaded.n_channels == PCAKEEP and loaded.n_channels_in == NW + np.testing.assert_array_equal(np.array(loaded.sphere), np.array(m.sphere)) + + +def test_torch_payload_without_the_keys_loads_with_defaults(real_data): + """The torch twin of the MLX test above: the keys were always in torch's + config, but a hand-built or trimmed payload without them must still load + with the constructor defaults and keep its reduced sphere.""" + m = _torch(pcakeep=PCAKEEP) + m.fit(real_data, max_iter=2, verbose=False) + state = m.state_dict() + del state["config"]["pcakeep"] + del state["config"]["pcadb"] + loaded = AMICATorchNG.from_state_dict(state, device="cpu") + + assert loaded.pcakeep is None and loaded.pcadb is None + assert loaded.n_channels == PCAKEEP and loaded.n_channels_in == NW + assert loaded.sphere is not None and m.sphere is not None + torch.testing.assert_close(loaded.sphere, m.sphere, rtol=0.0, atol=0.0) + + +def test_torch_save_load_round_trips_numpy_scalar_request(real_data, tmp_path): + """AMICA.load reads with torch.load(weights_only=True), which refuses numpy + scalars; state_dict persists the validated request as plain int/float.""" + model = AMICA(device="cpu", verbose=False) + model.fit( + real_data, + max_iter=2, + seed=SEED, + pcakeep=np.int64(PCAKEEP), + pcadb=np.float64(BUNDLED_PCADB), + ) + path = tmp_path / "model.pt" + model.save(str(path)) + loaded = AMICA.load(str(path), device="cpu") + + assert loaded.model_ is not None and model.model_ is not None + assert loaded.model_.pcakeep == PCAKEEP and type(loaded.model_.pcakeep) is int + assert loaded.model_.pcadb == BUNDLED_PCADB + assert type(loaded.model_.pcadb) is float + assert loaded.model_.sphere is not None and model.model_.sphere is not None + assert tuple(loaded.model_.sphere.shape) == (PCAKEEP, NW) + torch.testing.assert_close( + loaded.model_.sphere, model.model_.sphere, rtol=0.0, atol=0.0 + ) + + +# --- refits and restarts start from the input geometry ----------------------- +# _preprocess shrinks n_channels to the kept rank. torch and MLX used to +# validate the next fit's X against that shrunk count, so a refit, or the +# second of n_restarts (which _fit_restarts deliberately does not catch), died +# with "X has 32 channels, model expects 20". NumPy re-derives its sizes from +# the data on every fit and is the always-on control. +AUTO_RANK = 20 +BACKENDS = ["torch", "numpy", "mlx"] + + +@pytest.fixture(scope="module") +def rank_deficient(real_data) -> np.ndarray: + """The real sample projected onto its leading 20 principal directions (the + construction of test_rank_policy.py's fixture), so automatic mineig_rel + detection reduces it to rank 20 with no explicit request.""" + x = real_data - real_data.mean(axis=1, keepdims=True) + U = np.linalg.svd(x, full_matrices=False)[0][:, :AUTO_RANK] + return U @ (U.T @ x) + + +def _reduction(real_data, rank_deficient, reduction: str): + if reduction == "pcakeep": + return real_data, {"pcakeep": PCAKEEP} + return rank_deficient, {} + + +def _new(backend: str, tmp: Path, **kwargs): + if backend == "torch": + return _torch(**kwargs) + if backend == "numpy": + return _numpy(tmp, **kwargs) + return _mlx(**kwargs) + + +def _fit(backend: str, m, X: np.ndarray) -> None: + if backend == "numpy": + m.fit(X) # max_iter is a constructor argument there (2, see _numpy) + else: + m.fit(X, max_iter=2, verbose=False) + + +def _geometry(backend: str, m) -> Tuple[int, int]: + if backend == "numpy": + return m.data_dim, m.data_dim_in + return m.n_channels, m.n_channels_in + + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("reduction", ["pcakeep", "automatic"]) +def test_refit_after_a_rank_reduction( + real_data, rank_deficient, tmp_path, backend, reduction +): + """A second fit on the same instance starts from the constructor's + geometry, so it reduces to the same rank; on torch and MLX, which + re-initialize every parameter per fit, it also reproduces the first fit. + NumPy's refit warm-starts from the fitted A (_initialize_parameters keeps + a non-None A), so only its geometry is compared.""" + X, kwargs = _reduction(real_data, rank_deficient, reduction) + if backend == "mlx": + _mlx_core() + m = _new(backend, tmp_path, **kwargs) + _fit(backend, m, X) + assert _geometry(backend, m) == (PCAKEEP, NW) + first_ll = None if backend == "numpy" else list(m.ll_history) + + _fit(backend, m, X) + assert _geometry(backend, m) == (PCAKEEP, NW) + if first_ll is not None: + assert list(m.ll_history) == first_ll + + +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize("reduction", ["pcakeep", "automatic"]) +def test_restarts_survive_a_rank_reduction( + real_data, rank_deficient, tmp_path, backend, reduction +): + X, kwargs = _reduction(real_data, rank_deficient, reduction) + if backend == "mlx": + _mlx_core() + seeds = [SEED, SEED + 1] + m = _new(backend, tmp_path, n_restarts=2, restart_seeds=seeds, **kwargs) + _fit(backend, m, X) + assert _geometry(backend, m) == (PCAKEEP, NW) + assert m.restart_seeds_ == seeds + assert len(m.restart_lls_) == 2 + assert all(math.isfinite(ll) for ll in m.restart_lls_), m.restart_stop_reasons_ + + +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_channel_check_names_the_input_count(real_data, backend): + """Before and after a reduced fit the model accepts, and names, the input + width; the shrunk rank is never mistaken for it (a 20-channel X used to be + accepted by a model fitted to 32 channels and reduced to 20).""" + m = _backend(backend, pcakeep=PCAKEEP) + assert m.n_channels_in == NW + m.fit(real_data, max_iter=1, verbose=False) + assert (m.n_channels, m.n_channels_in) == (PCAKEEP, NW) + with pytest.raises( + ValueError, match=f"X has {PCAKEEP} channels, model expects {NW}" + ): + m.fit(real_data[:PCAKEEP], max_iter=1, verbose=False) + + +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_reloaded_reduced_model_transforms_and_refits(real_data, tmp_path, backend): + """The input count is derived from the restored sphere's width, since + config's n_channels is the fitted rank. NumPy is not covered: it persists + an EEGLAB amicaout directory, not a model state it can load and refit.""" + X = real_data + if backend == "torch": + m = _torch(pcakeep=PCAKEEP) + m.fit(X, max_iter=2, verbose=False) + path = tmp_path / "state.pt" + torch.save(m.state_dict(), path) + loaded = AMICATorchNG.from_state_dict( + torch.load(path, weights_only=True), device="cpu" + ) + else: + mlx_core = _mlx_core() + m = _mlx(pcakeep=PCAKEEP) + m.fit(X, max_iter=2, verbose=False) + path = tmp_path / "model.npz" + m.save(str(path)) + loaded = mlx_core.AMICAMLXNG.load(str(path)) + + assert (loaded.n_channels, loaded.n_channels_in) == (PCAKEEP, NW) + np.testing.assert_array_equal(loaded.transform(X), m.transform(X)) + loaded.fit(X, max_iter=2, verbose=False) + assert (loaded.n_channels, loaded.n_channels_in) == (PCAKEEP, NW) + assert loaded.final_ll_ is not None and math.isfinite(loaded.final_ll_) + + +# --- a request reassigned after construction is caught at fit time ---------- +@pytest.mark.parametrize("backend", BACKENDS) +@pytest.mark.parametrize(("name", "value"), [("pcakeep", -3), ("pcadb", -5.0)]) +def test_reassigned_request_is_caught_at_fit_time( + real_data, tmp_path, backend, name, value +): + """The constructors validate, but the attributes are public; the fit-time + re-check in numerical_rank catches a bad value set afterwards.""" + m = _new(backend, tmp_path) + setattr(m, name, value) + with pytest.raises(ValueError, match=f"{name} must be None"): + _fit(backend, m, real_data[:, :4096]) + + +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +@pytest.mark.parametrize(("name", "value"), [("pcakeep", "20"), ("pcadb", -5.0)]) +def test_reassigned_request_is_caught_by_the_mir_step_gate( + real_data, backend, name, value +): + """With mir_step > 0 the upfront gate reads the request before + numerical_rank does, so it validates too: a reassigned string raises the + validator's ValueError, not a TypeError from comparing it.""" + m = _backend(backend) + setattr(m, name, value) + with pytest.raises(ValueError, match=f"{name} must be None"): + m.fit(real_data[:, :4096], max_iter=2, verbose=False, mir_step=1) diff --git a/pamica/tests/test_rank_policy.py b/pamica/tests/test_rank_policy.py index 83cdc539..4a68b6a2 100644 --- a/pamica/tests/test_rank_policy.py +++ b/pamica/tests/test_rank_policy.py @@ -4,14 +4,28 @@ ``pamica.rank`` and is tested once here. The cross-backend test at the bottom is the anti-drift guard required by ``.rules/backend_parity.md``: it fails if any backend starts answering "how many dimensions are real?" differently. + +The explicit ``pcakeep``/``pcadb`` request is part of the same decision (issue +#323): its validator and its "is a reduction requested?" predicate are unit-tested +here, and ``test_pca_reduction_cross_backend.py`` checks that every backend +applies them identically. """ +import logging +import math from pathlib import Path import numpy as np import pytest -from pamica.rank import MINEIG, MINEIG_REL, numerical_rank +from pamica.rank import ( + MINEIG, + MINEIG_REL, + log_ignored_pca_request, + numerical_rank, + pca_reduction_requested, + validate_pca_reduction, +) from pamica.torch_impl.utils import load_eeglab_data SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" @@ -103,6 +117,154 @@ def test_defaults_match_the_documented_constants() -> None: assert MINEIG_REL == 1e-12 +# --- explicit pcakeep/pcadb policy (issue #323) ------------------------------- +@pytest.mark.parametrize( + ("pcakeep", "pcadb"), + [ + (None, None), + (1, None), + (20, None), + (np.int64(5), None), # numpy integers are numbers.Integral + (None, 3.0), + (None, 30), # an int is a real number of dB + (None, np.float32(3.0)), # numpy floats are numbers.Real + (None, np.float64(30.0)), + (32, 30.0), # both bundled param files set both + ], +) +def test_validator_accepts(pcakeep, pcadb) -> None: + validate_pca_reduction(pcakeep, pcadb) + + +@pytest.mark.parametrize( + "pcakeep", [True, False, np.bool_(True), np.bool_(False), 0, -3, 2.7, "20"] +) +def test_validator_rejects_pcakeep(pcakeep) -> None: + """``True``/``False`` are ints to Python but never a dimension count; + ``-3`` used to slice from the end (29 of 32 kept) and ``2.7`` to truncate.""" + with pytest.raises(ValueError, match="pcakeep") as info: + validate_pca_reduction(pcakeep, None) + assert repr(pcakeep) in str(info.value) + + +@pytest.mark.parametrize( + "pcadb", [0, -5, math.nan, math.inf, True, False, np.bool_(True), np.bool_(False)] +) +def test_validator_rejects_pcadb(pcadb) -> None: + with pytest.raises(ValueError, match="pcadb") as info: + validate_pca_reduction(None, pcadb) + assert repr(pcadb) in str(info.value) + + +def test_validator_rejects_either_parameter_when_both_are_set() -> None: + """A valid partner does not rescue an invalid value: pcakeep's precedence + is about which one sizes the model, not about skipping validation.""" + with pytest.raises(ValueError, match="pcadb"): + validate_pca_reduction(20, -5.0) + with pytest.raises(ValueError, match="pcakeep"): + validate_pca_reduction(0, 30.0) + + +def test_validator_never_logs(caplog) -> None: + """Validation only, so the fit-time re-checks cannot repeat the + constructor's notes.""" + with caplog.at_level(logging.DEBUG, logger="pamica.rank"): + validate_pca_reduction(32, 30.0) + validate_pca_reduction(20, None) + assert not caplog.records + + +def test_ignored_request_logs_the_precedence_once(caplog) -> None: + with caplog.at_level(logging.INFO, logger="pamica.rank"): + log_ignored_pca_request(32, 30.0, True) + log_ignored_pca_request(20, None, True) + log_ignored_pca_request(None, 30.0, True) + log_ignored_pca_request(None, None, True) + assert len(caplog.records) == 1 + assert caplog.records[0].levelno == logging.INFO + assert "pcadb is ignored" in caplog.records[0].getMessage() + + +@pytest.mark.parametrize( + ("pcakeep", "pcadb"), [(20, None), (None, 30.0), (20, 30.0), (np.int64(20), None)] +) +def test_ignored_request_warns_once_without_sphering(caplog, pcakeep, pcadb) -> None: + """Without sphering nothing is reduced (the reference's no-sphere branch + sets numeigs = nx, amica15.f90:527): one WARNING, and no precedence note, + since pcadb's precedence is moot when neither is used.""" + with caplog.at_level(logging.INFO, logger="pamica.rank"): + log_ignored_pca_request(pcakeep, pcadb, False) + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelno == logging.WARNING + assert "do_sphere=False" in record.getMessage() + + +def test_nothing_to_ignore_without_a_request(caplog) -> None: + with caplog.at_level(logging.DEBUG, logger="pamica.rank"): + log_ignored_pca_request(None, None, False) + assert not caplog.records + + +@pytest.mark.parametrize( + ("pcakeep", "pcadb", "n_channels", "do_sphere", "expected"), + [ + (None, None, 32, True, False), + (20, None, 32, True, True), + (31, None, 32, True, True), + (32, None, 32, True, False), # the bundled input.param: nothing is reduced + (64, None, 32, True, False), + (None, 30.0, 32, True, True), # unknowable before the eigenvalues exist + (32, 30.0, 32, True, False), # pcakeep wins, as in numerical_rank + (20, 30.0, 32, True, True), + # Without sphering no backend reduces (amica15.f90:527: numeigs = nx). + (20, None, 32, False, False), + (None, 30.0, 32, False, False), + (20, 30.0, 32, False, False), + (None, None, 32, False, False), + ], +) +def test_reduction_predicate_truth_table( + pcakeep, pcadb, n_channels, do_sphere, expected +): + assert pca_reduction_requested(pcakeep, pcadb, n_channels, do_sphere) is expected + + +@pytest.mark.parametrize( + ("pcakeep", "pcadb", "name"), + [(-3, None, "pcakeep"), ("20", None, "pcakeep"), (None, -5.0, "pcadb")], +) +def test_reduction_predicate_validates_first(pcakeep, pcadb, name) -> None: + """The gate runs at fit time, so a reassigned attribute must raise the + validator's ValueError there, not a TypeError from comparing a string.""" + with pytest.raises(ValueError, match=name): + pca_reduction_requested(pcakeep, pcadb, 32, True) + + +@pytest.mark.parametrize( + ("pcakeep", "pcadb"), + [(-3, None), (2.7, None), (0, None), (True, None), (None, 0.0), (None, -5.0)], +) +def test_numerical_rank_rejects_invalid_reduction( + real_data: np.ndarray, pcakeep, pcadb +) -> None: + """The fit-time choke point: an attribute reassigned after construction + reaches numerical_rank unvalidated by any constructor.""" + with pytest.raises(ValueError, match="pcakeep" if pcadb is None else "pcadb"): + numerical_rank(_cov_eigenvalues(real_data), pcakeep=pcakeep, pcadb=pcadb) + + +def test_numerical_rank_pcakeep_wins_over_pcadb(real_data: np.ndarray, caplog): + """pcadb=30 alone cuts the real sample to 26 dimensions; with pcakeep set + it is ignored, and the fit-time re-check does not repeat the INFO note.""" + ev = _cov_eigenvalues(real_data) + assert numerical_rank(ev, pcadb=30.0) == 26 + with caplog.at_level(logging.INFO, logger="pamica.rank"): + assert numerical_rank(ev, pcakeep=20, pcadb=30.0) == 20 + assert numerical_rank(ev, pcakeep=NW, pcadb=30.0) == NW + assert not caplog.records + + def test_all_backends_agree_on_the_rank(rank_deficient: np.ndarray) -> None: """Anti-drift guard: every backend must size its model identically. diff --git a/pamica/tests/torch_tests/test_amica_ng_wrapper.py b/pamica/tests/torch_tests/test_amica_ng_wrapper.py index b124f2a7..6b0bd30a 100644 --- a/pamica/tests/torch_tests/test_amica_ng_wrapper.py +++ b/pamica/tests/torch_tests/test_amica_ng_wrapper.py @@ -957,6 +957,27 @@ def test_mir_step_raises_under_pca_reduction_up_front(real_data): ) +def test_bundled_input_param_passes_the_mir_step_gate(real_data): + """Issue #323 regression: the bundled ``input.param`` sets ``pcakeep 32`` + (and ``pcadb 30``, ignored because ``pcakeep`` takes precedence) for the + 32-channel sample, which reduces nothing. The upfront ``mir_step`` gate + used to treat any explicit ``pcakeep`` as a reduction request and refuse + the reference's own configuration. A gate test, not a convergence test. + """ + param_file = SAMPLE_DIR / "input.param" + if not param_file.exists(): + pytest.skip("bundled input.param missing") + model = AMICA.from_params_file(str(param_file), device="cpu", verbose=False) + model.fit(real_data, max_iter=2, mir_step=1) + + ng = model.model_ + assert ng is not None + assert ng.pcakeep == NW and ng.pcadb == 30.0 + assert ng.n_channels == ng.n_channels_in == NW + assert [row[0] for row in model.mir_history_] == [0, 1] + assert all(math.isfinite(row[1]) for row in model.mir_history_) + + def test_mir_raises_under_auto_detected_rank_reduction(real_data): """Issue #283 regression: rank reduction from AUTOMATIC ``mineig_rel`` numerical-rank detection (no explicit ``pcakeep``/``pcadb``) must trip the diff --git a/pamica/torch_impl/core.py b/pamica/torch_impl/core.py index dd66b108..db542d47 100644 --- a/pamica/torch_impl/core.py +++ b/pamica/torch_impl/core.py @@ -57,7 +57,14 @@ from .. import restarts from ..metrics import mir as mir_metric from ..metrics import pairwise_mi -from ..rank import MINEIG, MINEIG_REL, numerical_rank +from ..rank import ( + MINEIG, + MINEIG_REL, + log_ignored_pca_request, + numerical_rank, + pca_reduction_requested, + validate_pca_reduction, +) from .utils import setup_device logger = logging.getLogger(__name__) @@ -511,10 +518,19 @@ class AMICATorchNG: do_mean, do_sphere, do_approx_sphere : bool Preprocessing options, matching ``pamica.AMICA._preprocess_data``. pcakeep, pcadb : int, float, optional - PCA dimensionality-reduction options (rarely used; see - ``pamica.AMICA._preprocess_data``). Both are capped by the detected - numerical rank (``mineig``), matching Fortran's - ``numeigs = min(pcakeep, count(eigs > mineig))``. + Explicit PCA dimensionality reduction. ``pcakeep`` keeps that many + principal dimensions (Fortran ``pcakeep``); ``pcadb`` keeps those whose + covariance eigenvalue lies within ``pcadb`` dB of the largest (a pamica + extension: the reference parses ``pcadb`` but never uses it). Both are + capped by the detected numerical rank (``mineig``/``mineig_rel``), + matching Fortran's ``numeigs = min(pcakeep, count(eigs > mineig))``. + ``pcakeep`` must be an integer >= 1 and ``pcadb`` a finite number > 0; + anything else raises ``ValueError`` at construction. When both are set, + ``pcakeep`` takes precedence and ``pcadb`` is ignored (one INFO log + line), as in the reference. Both are ignored, with one WARNING, when + ``do_sphere=False``: reduction happens only while sphering, as in the + reference. ``None`` (the default) for both leaves only automatic rank + detection. See :mod:`pamica.rank`. mineig : float, default=1e-15 Absolute floor on data-covariance eigenvalues used to detect the numerical rank (Fortran ``mineig``, amica15.f90:413 and @@ -627,6 +643,11 @@ def __init__( dtype: torch.dtype = torch.float64, ): self.n_channels = n_channels + # The input channel count, kept apart from n_channels, which + # _preprocess shrinks to the kept rank on any rank reduction. fit() + # validates X against this and resets n_channels/n_comps from it, so a + # refit or a later restart starts from the constructor's geometry. + self._n_input_channels = n_channels self.n_models = n_models self.n_mix = n_mix self.n_comps = n_channels * n_models @@ -771,6 +792,12 @@ def __init__( self.do_mean = do_mean self.do_sphere = do_sphere self.do_approx_sphere = do_approx_sphere + # Explicit PCA reduction, validated by the policy shared with the NumPy + # and MLX backends (pamica/rank.py, issue #323) so a bad value fails + # here rather than as a silently wrongly sized or nan_ll fit; then one + # log line for any part of it a fit will ignore. + validate_pca_reduction(pcakeep, pcadb) + log_ignored_pca_request(pcakeep, pcadb, do_sphere) self.pcakeep = pcakeep self.pcadb = pcadb @@ -2312,12 +2339,15 @@ def _fit_once( while ``ll_history[i]`` is the likelihood of the parameters before it, so the two are one update apart (issue #161). Incompatible with PCA reduction, same as :meth:`mir` itself. This - upfront gate only sees explicit ``pcakeep``/``pcadb`` (the sphere - for THIS fit does not exist yet, so automatic ``mineig``/ - ``mineig_rel`` rank reduction cannot be checked here); that case is - instead caught once the sphere exists, inside the per-waypoint - :meth:`mir` call below, whose ``ValueError`` is already caught and - logged rather than propagated (issue #283). + upfront gate only sees an explicit reduction request: ``pcakeep < + n_channels`` or any ``pcadb`` while sphering (``pcakeep >= + n_channels`` keeps every dimension, and ``do_sphere=False`` never + reduces, so neither is one; issue #323). The sphere for THIS + fit does not exist yet, so automatic ``mineig``/``mineig_rel`` rank + reduction cannot be checked here; that case is instead caught once + the sphere exists, inside the per-waypoint :meth:`mir` call below, + whose ``ValueError`` is already caught and logged rather than + propagated (issue #283). Returns ------- @@ -2381,9 +2411,9 @@ def _fit_once( raise ValueError( f"X must be a 2D array (n_channels, n_samples), got shape {X.shape}" ) - if X.shape[0] != self.n_channels: + if X.shape[0] != self._n_input_channels: raise ValueError( - f"X has {X.shape[0]} channels, model expects {self.n_channels}" + f"X has {X.shape[0]} channels, model expects {self._n_input_channels}" ) if mir_step < 0: raise ValueError(f"mir_step must be >= 0, got {mir_step}") @@ -2396,7 +2426,7 @@ def _fit_once( # "did an E-step ever actually run", only "did stop_reason end # up degenerate". Reject up front instead. raise ValueError(f"max_iter must be >= 1, got {max_iter}") - if mir_step > 0 and self._pca_reduction_requested(): + if mir_step > 0 and self._pca_reduction_requested(X.shape[0]): raise ValueError( "mir_step > 0 is incompatible with PCA reduction " "(pcakeep/pcadb): the sphere is rank-deficient, so MIR's " @@ -2404,6 +2434,13 @@ def _fit_once( "than failing mid-fit at the first waypoint." ) + # Size every fit from the input geometry. _preprocess shrinks + # n_channels/n_comps to the kept rank, so without this a refit, or the + # second of n_restarts, would start from the previous fit's rank. + # A no-op for full-rank data, whose sizes never change. + self.n_channels = self._n_input_channels + self.n_comps = self.n_channels * self.n_models + X_t = self._preprocess(X) n_total = X_t.shape[1] @@ -2961,10 +2998,13 @@ def n_channels_in(self) -> int: Differs from ``n_channels`` only when rank reduction shrank the model to the detected numerical rank (issue #223); equal to it for full-rank data - and before :meth:`fit`. Derived rather than stored, so it cannot drift - from the sphere it describes. + and before :meth:`fit`. Read off the sphere whenever one exists, so it + cannot drift from the sphere it describes; before the first fit it is + the constructor's channel count, the width :meth:`fit` accepts. """ - return self.n_channels if self.sphere is None else int(self.sphere.shape[1]) + if self.sphere is None: + return self._n_input_channels + return int(self.sphere.shape[1]) def get_sensor_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: """Mixing matrix mapped back to input-channel space. @@ -3000,9 +3040,17 @@ def get_unmixing_matrix(self, model_idx: int = 0) -> np.ndarray: self._check_model_idx(model_idx) return self.W[:, :, model_idx].T.cpu().numpy() - def _pca_reduction_requested(self) -> bool: - """Whether an explicit PCA-reduction parameter (``pcakeep``/``pcadb``) - was passed to the constructor. + def _pca_reduction_requested(self, n_channels: int) -> bool: + """Whether the explicit ``pcakeep``/``pcadb`` asks to fit fewer than + ``n_channels`` dimensions (the shared predicate, + :func:`pamica.rank.pca_reduction_requested`, issue #323). + + ``n_channels`` is the channel count of the data being fitted, not + ``self.n_channels``, which :meth:`_preprocess` shrinks to the kept rank. + ``pcakeep >= n_channels`` is not a request (it keeps every dimension); + any ``pcadb`` is, since whether it cuts anything depends on the + eigenvalues; ``pcakeep`` wins when both are set; and with + ``do_sphere=False`` nothing is, since no reduction happens then. Config-only, not geometry: used solely by :meth:`_fit_once`'s upfront ``mir_step`` gate, which runs BEFORE :meth:`_preprocess` builds this @@ -3011,7 +3059,9 @@ def _pca_reduction_requested(self) -> bool: knowable. Use :meth:`_pca_reduced` instead wherever a fitted sphere already exists (issue #283). """ - return self.pcakeep is not None or self.pcadb is not None + return pca_reduction_requested( + self.pcakeep, self.pcadb, n_channels, self.do_sphere + ) def _pca_reduced(self) -> bool: """Whether the fitted sphere is rank-reduced (non-square). @@ -3600,8 +3650,11 @@ def state_dict(self) -> dict: "do_mean": self.do_mean, "do_sphere": self.do_sphere, "do_approx_sphere": self.do_approx_sphere, - "pcakeep": self.pcakeep, - "pcadb": self.pcadb, + # Plain int/float: the validator accepts numpy scalars (np.int64), + # which torch.load(weights_only=True) refuses to unpickle (issue + # #323). Same cast as AMICAMLXNG.state_dict, whose JSON needs it. + "pcakeep": None if self.pcakeep is None else int(self.pcakeep), + "pcadb": None if self.pcadb is None else float(self.pcadb), "mineig": self.mineig, "mineig_rel": self.mineig_rel, "seed": self.seed, @@ -3726,6 +3779,11 @@ def _load_params(self, state: dict) -> None: setattr(self, name, tensor.to(self.device, self.dtype)) # sphere was just replaced, so any cached back-map describes the old one. self._sphere_pinv = None + # config's n_channels is the fitted (possibly reduced) rank, so the + # constructor above set the input count to it; the restored sphere's + # width is the true input count, which a refit validates X against. + assert self.sphere is not None # just set by the loop above + self._n_input_channels = int(self.sphere.shape[1]) extra = state["extra"] self.sldet = extra["sldet"] From b6ecf22a1a4aca2273dcf6125e5c7c973374810e Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Tue, 22 Sep 2026 16:22:10 -0700 Subject: [PATCH 03/26] Phase 3: shared params-file reader for every backend (#327) * Add shared params-file reader with JSON aliases read_params_file (issue #304) is the single params-file entry point every backend should use: content-sniffs JSON vs Fortran input.param text and returns canonical pamica keys either way, aliasing a JSON file's schema spellings (min_grad_norm/max_decs/numrej/num_mix_comps/ share_int) via one table. A file with both an alias and its canonical key raises ValueError. writestep/do_history/histstep move from FORTRAN_UNSUPPORTED_KEYS to translated (identity) keys: the reader translates what any backend supports, not just the wrapper; the legacy NumPy backend already implements periodic checkpointing under these names. * Route AMICA.from_params_file through read_params_file Replaces the wrapper's own inline JSON/Fortran content sniffing with the shared reader (issue #304). Behavior change: sample_params.json's own max_decs/min_grad_norm/share_int settings are now applied (as maxdecs/min_nd/share_iter) instead of silently landing in the "not applied" warning. * Route NumPy backend through shared params-file reader AMICA_NumPy(params_file=...) and load_default_params now read through read_params_file (issue #304) instead of a bare json.load, so the legacy backend accepts the literal Fortran input.param text too, not just JSON. Canonical keys map to this backend's own spellings (min_nd -> min_grad_norm, maxdecs -> max_decs, share_iter -> share_int); files/data_dim/field_dim come off the same parsed dict instead of a second file read. A params-file setting this backend does not consume is named in one logger.warning. from_json_file is renamed to from_params_file (matches the PyTorch wrapper's name, now accepts both formats); no alias left behind. pdftype != 0 now raises NotImplementedError at construction: this backend implements only the generalized-Gaussian source density and previously silently ignored the setting. numpy_impl/params.json's bundled default pdftype changes 1 -> 0 to match; the value was never read by the fit path, so no previously-passing fit's numerics change. * Dedup validate_implementations.py's Fortran alias table _FORTRAN_ALIASES is now derived from fortran_params. PAMICA_KEY_TO_FORTRAN_KEY (the shared reader's single source of truth for pamica-key -> Fortran-keyword) plus the one JSON-only share_int addition, instead of a hand-maintained duplicate. load_sample_data reads sample_params.json through read_params_file, so min_nd/maxdecs/ share_iter land in run_pytorch_amica's ng_kwargs via the plain _NG_PARAMS filter; the max_decs special case is no longer needed. * Document the shared params-file reader (issue #304) docs/guides/validation.md: read_params_file, the JSON alias table, the writestep/do_history/histstep move, and the NumPy backend's own params-file/pdftype behavior. docs/guides/amica-differences.md: the backend-differences table's Fortran input.param reader row (NumPy now yes, MLX awaits epic #324 Phase 4) and the NumPy pdftype NotImplementedError. docs/api/numpy-backend.md: the renamed from_params_file classmethod and the pdftype guard. docs/changelog.md: Unreleased entry covering the shared reader, the wrapper's now-applied JSON aliases, NumPy's input.param support, the from_json_file rename, and the NumPy pdftype guard. * Route NumPy CLI through the shared params-file reader cli.py's load_params() did a bare json.load, so the CLI died with JSONDecodeError on a Fortran input.param even though AMICA(params_ file=...) already accepted it. It now calls the same _read_numpy_keyed_params helper the constructor uses (no duplicated key-mapping table), so both formats work; files stays unresolved relative to the working directory, matching Fortran-binary semantics. New test_numpy_cli.py exercises main() end to end on real bundled data for both formats; verified to fail with JSONDecodeError on the pre-fix load_params(). * Compose _FORTRAN_ALIASES from the shared alias tables Replaces the hand-added share_int entry with a composition of JSON_ALIAS_TO_CANONICAL (JSON alias -> canonical) and PAMICA_KEY_TO_FORTRAN_KEY (canonical -> Fortran keyword), so write_fortran_param_file accepts either raw JSON-schema keys or canonical keys with no hand-maintained entry left in this module. Adds coverage: canonical maxdecs/min_nd are written as Fortran max_decs/min_grad_norm, and an end-to-end test feeds load_sample_ data()'s canonical-keyed params straight into write_fortran_param_ file, asserting the written file carries Fortran's own spellings and none of the canonical ones. * Add precedence, warning and table-size regression tests NumPy: explicit kwargs win over params-file defaults (mirrors the PyTorch wrapper's precedence test). Wrapper: fitting from input.param names writestep/do_history/histstep in the "not applied" warning, since AMICATorchNG has no matching constructor keyword (issue #312). Pins FORTRAN_TO_PAMICA_KEY/FORTRAN_UNSUPPORTED_KEYS table sizes so a future move between them forces the docstring/doc counts to be revisited. * Fix stale docs from PR #327 review fortran_params.py module docstring and its mirror in validation.md still claimed share_int was "out of scope" and surfaced only via the "not applied" warning -- stale since read_params_file/ JSON_ALIAS_TO_CANONICAL landed; both now correctly say share_iter needs no Fortran-side rename and the JSON alias table handles share_int on the way in. Reworded four "silently landed in the not applied warning" claims (amica.py, changelog.md, validation.md, a test docstring): the warning already named max_decs/min_grad_norm/share_int before this epic, it just did not apply them -- "silently" overstated that. numpy_impl/core.py's __init__ docstring now says params_file accepts both formats. AGENTS.md's architecture-map line for fortran_params.py now describes it as the shared reader for the wrapper, AMICA_NumPy and the NumPy CLI. amica-differences.md spells out "generalized Gaussian (GG)" at first use. Applies semantic line breaks to the Markdown prose this epic added (changelog.md, validation.md, amica-differences.md, numpy-backend.md), without reflowing pre-existing text. * Fix unparsable spelling flagged by typos --- AGENTS.md | 2 +- docs/api/numpy-backend.md | 16 ++ docs/changelog.md | 41 ++++ docs/guides/amica-differences.md | 31 ++- docs/guides/validation.md | 90 ++++++-- pamica/amica.py | 26 ++- pamica/fortran_params.py | 170 +++++++++++--- pamica/numpy_impl/cli.py | 54 +++-- pamica/numpy_impl/core.py | 210 +++++++++++++++--- pamica/numpy_impl/params.json | 2 +- pamica/tests/test_fortran_param_forwarding.py | 26 +++ pamica/tests/test_fortran_params.py | 181 +++++++++++++-- pamica/tests/test_numpy_cli.py | 102 +++++++++ pamica/tests/test_numpy_params_file.py | 190 ++++++++++++++++ pamica/tests/test_sample_data.py | 8 +- validate_implementations.py | 46 +++- 16 files changed, 1038 insertions(+), 157 deletions(-) create mode 100644 pamica/tests/test_numpy_cli.py create mode 100644 pamica/tests/test_numpy_params_file.py diff --git a/AGENTS.md b/AGENTS.md index 9543fd83..164144f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ pamica/ │ └── ... ├── blocktune.py # Shared block-size auto-tuner policy (#232, all backends) ├── restarts.py # Shared best-of-N restart policy (#198, all backends) -├── fortran_params.py # Fortran input.param reader for AMICA.from_params_file (#132) +├── fortran_params.py # Shared params-file reader (JSON + Fortran input.param, #132/#304) for AMICA.from_params_file, AMICA_NumPy and the NumPy CLI ├── amica17.f90, funmod2.f90 # Fortran reference source (read-only, for parity) ├── sample_data/ # Sample EEG data + Fortran binary (amica15mac) └── tests/ # Tests, incl. tests/torch_tests/ (vs-Fortran parity) diff --git a/docs/api/numpy-backend.md b/docs/api/numpy-backend.md index c6284be4..add7c8e1 100644 --- a/docs/api/numpy-backend.md +++ b/docs/api/numpy-backend.md @@ -5,4 +5,20 @@ command-line interface. It carries the same parity fixes as the PyTorch backend, plus baralpha and outlier rejection (`do_reject`), which mirrors the PyTorch backend's `good_idx` sample-dropping mechanism (issue #123). +`AMICA_NumPy(params_file=...)` and the `AMICA_NumPy.from_params_file` classmethod +(renamed from `from_json_file`, issue #304) accept both pamica's JSON schema and +the literal Fortran `input.param` text format, content-sniffed the same way as the +PyTorch wrapper's `AMICA.from_params_file` -- see +[Parameter files](../guides/validation.md#parameter-files). +The NumPy CLI (`python -m pamica.numpy_impl.cli`) accepts both formats too, through +the same shared reader. +Settings the file carries that this backend does not consume are named in one +`logger.warning` rather than silently dropped. + +This backend implements only the generalized-Gaussian source density +(`pdftype=0`); `AMICA_NumPy(pdftype=...)` with any other value raises +`NotImplementedError` at construction instead of silently doing nothing -- see +the backend-differences table in +[amica-differences.md](../guides/amica-differences.md). + ::: pamica.AMICA_NumPy diff --git a/docs/changelog.md b/docs/changelog.md index 4227a6da..bd67d46b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -42,6 +42,47 @@ Release notes are also published on the Every fit now starts from the constructor's input channel count, including a fit on a model reloaded from `state_dict`/`save`. The NumPy backend was not affected. +- **Phase 3 of epic #324: shared params-file reader for every backend (issue #304).** + `pamica/fortran_params.py` gains `read_params_file`, the single params-file entry point + every backend now uses. + It content-sniffs JSON vs. the literal Fortran `input.param` text and returns pamica's + canonical keys either way. + A JSON file's own alias spellings (`min_grad_norm`, `max_decs`, `numrej`, `num_mix_comps`, + `share_int`) are translated to the canonical/constructor names via one table, + `JSON_ALIAS_TO_CANONICAL`; + a file carrying both an alias and its canonical key raises `ValueError` naming both. + `writestep`/`do_history`/`histstep` moved from `FORTRAN_UNSUPPORTED_KEYS` to translated + (identity) keys -- the legacy NumPy backend already implements periodic on-disk + checkpointing under these names, and the reader now translates what any backend supports + rather than only what its one production caller (the PyTorch wrapper) did; + torch/MLX have no matching mechanism yet (issue #312). + - **Behavior change:** `AMICA.from_params_file` now applies `sample_params.json`'s own + `max_decs`/`min_grad_norm`/`share_int` settings (as `maxdecs`/`min_nd`/`share_iter`) + instead of only naming them in the "not applied" warning. + Their raw JSON spelling previously matched neither a named `fit()` parameter nor an + `AMICATorchNG` keyword, so they were only named in the "not applied" warning rather + than actually applied. + - **Behavior change (legacy NumPy backend):** `AMICA_NumPy(params_file=...)` now accepts + the literal Fortran `input.param` text format, not just JSON -- previously a non-JSON + file raised a raw `json.JSONDecodeError`. + A params-file setting this backend does not consume is now named in one + `logger.warning` instead of silently vanishing. + The NumPy CLI (`python -m pamica.numpy_impl.cli`) accepts both formats too, through + the same reader, instead of its own separate `json.load`. + - **Breaking change (legacy NumPy backend):** `AMICA_NumPy.from_json_file` is renamed to + `from_params_file` (matching the PyTorch wrapper's classmethod name, and now accepting + both formats), with no alias left behind. + - **Breaking change (legacy NumPy backend):** `AMICA_NumPy(pdftype=...)` with anything + other than `0` now raises `NotImplementedError` at construction -- this backend + implements only the generalized-Gaussian source density, and previously silently + ignored the setting (`pdftype` is read but never consulted by the fit path). + `numpy_impl/params.json`'s bundled default `pdftype` changed `1` -> `0` to match; + since the value was never read, no previously-passing fit's numerics change. + - `validate_implementations.py`'s own JSON-schema-to-Fortran-keyword alias table + (`_FORTRAN_ALIASES`) is now composed from the two shared tables (`JSON_ALIAS_TO_CANONICAL` + then `PAMICA_KEY_TO_FORTRAN_KEY`) instead of one hand-maintained entry, and its + `max_decs` special case is gone -- `load_sample_data` reads through `read_params_file`, + so canonical keys reach `run_pytorch_amica`'s `ng_kwargs` automatically. - **Epic #278 polish round: audit-driven fixes ahead of merge to `dev`.** `AMICAMLXNG` gained `variance_order` (issue #92), the EEGLAB back-projected-variance component order, closing the one accessor gap the diff --git a/docs/guides/amica-differences.md b/docs/guides/amica-differences.md index 115332da..f1a7bfa9 100644 --- a/docs/guides/amica-differences.md +++ b/docs/guides/amica-differences.md @@ -143,13 +143,30 @@ The `AMICA` and `AMICAICA` wrappers do not construct it yet (epic #324 Phase 4, | `n_restarts` best-of-N restarts | yes | yes | yes | n/a | | Mutual Information Reduction (MIR) diagnostic | yes | no | yes | n/a | | Persistence | `state_dict` + EEGLAB `amicaout` export | EEGLAB `amicaout` | `state_dict`/`.npz` `save`-`load` + EEGLAB `amicaout` export | EEGLAB `amicaout` | -| Fortran `input.param` reader | yes (`AMICA.from_params_file`, #132) | no (`params_file` is JSON-only) | no | native | - -The NumPy row's "GG only" corrects an earlier version of this table, which -listed "all five": `AMICA_NumPy._compute_log_pdf` (its fit-path density -function) has no `pdtype` parameter at all, so the legacy backend never -implemented the non-GG families the PyTorch and MLX backends carry (issue -#265). +| Fortran `input.param` reader | yes (`AMICA.from_params_file`, #132) | yes (`AMICA_NumPy(params_file=...)` / `from_params_file`, #304) | via `AMICA(backend="mlx")` (epic #324 Phase 4) | native | + +The NumPy row's "GG only" (generalized Gaussian, GG) corrects an earlier +version of this table, which listed "all five": `AMICA_NumPy._compute_log_pdf` +(its fit-path density function) has no `pdtype` parameter at all, so the +legacy backend never implemented the non-GG families the PyTorch and MLX +backends carry (issue #265). +Issue #304 made this an enforced contract rather than a silent gap: +`AMICA_NumPy(pdftype=...)` with anything other than `0` now raises +`NotImplementedError` at construction (the `.rules/backend_parity.md` +narrow exception). +The bundled `numpy_impl/params.json`'s default `pdftype` changed `1` -> `0` +to match; it was never read by the fit path, so this was a dormant +divergence, not a behavior change to any actual fit. + +The NumPy row's params-file reader (`params_file=...` / `from_params_file`, +renamed from `from_json_file`) now accepts the same two formats as the +PyTorch wrapper -- pamica's JSON schema and the literal Fortran +`input.param` text, content-sniffed -- through the same shared +`pamica.fortran_params.read_params_file` (issue #304). +Previously `params_file` was JSON-only and a Fortran text file raised a raw +`json.JSONDecodeError`; the NumPy CLI accepts both formats the same way now +too, through the same underlying helper, instead of its own separate +`json.load`. `transform` and the `get_mixing_matrix`/`get_unmixing_matrix`/ `get_sensor_mixing_matrix`/`get_rho` accessors, plus `state_dict`/ diff --git a/docs/guides/validation.md b/docs/guides/validation.md index 79a8f0e5..f1a96a8b 100644 --- a/docs/guides/validation.md +++ b/docs/guides/validation.md @@ -566,34 +566,60 @@ model = AMICA.from_params_file("sample_data/input.param") # Fortran text forma model = AMICA.from_params_file("sample_data/sample_params.json") # JSON, as before ``` -The translation lives in `pamica/fortran_params.py` (`read_fortran_param_file`), which parses -Fortran's whitespace-separated `key value` lines (`#` full-line comments, plus a deliberately -permissive inline `" #..."` trailing comment; ints/floats/strings, including Fortran's `d`/`D` -double-precision exponent marker; `0`/`1` boolean flags using Fortran's own `k == 1` semantics) -into a dict targeting pamica's actual Python call surface: `AMICA.fit`'s named parameters -(`max_iter`, `lrate`, `do_mean`, `do_sphere`, `do_newton`) and `AMICATorchNG` constructor -keywords. It was built by reading every `case('...')` arm of `amica15.f90`'s parameter parser -(~amica15.f90:3100-3700) against `AMICATorchNG`'s constructor and `validate_implementations.py`'s -`_NG_PARAMS`/`_HANDLED_KEYS`. `AMICA.from_params_file` (which sniffs `.param` vs `.json` content -rather than trusting the file extension) stashes the translated dict on the returned instance, and +Every backend reads through one function now (issue #304): `pamica.fortran_params.read_params_file` +is the single params-file entry point, used by `AMICA.from_params_file` above and by +`AMICA_NumPy(params_file=...)` / `AMICA_NumPy.from_params_file` (the legacy backend's `params_file` +used to accept only JSON, raising a raw `json.JSONDecodeError` on a Fortran text file). +It content-sniffs the same way (JSON if the file starts with `{`/`[`, Fortran text otherwise -- +a JSON top level that is not an object raises `ValueError`) and returns pamica's canonical keys +either way. +A JSON file's own keys pass through one alias table, `JSON_ALIAS_TO_CANONICAL`: +pamica's JSON schema (`sample_params.json`, `numpy_impl/params.json`) spells five settings +differently from the canonical/constructor name -- + +| JSON schema key | canonical pamica key | Note | +| ------------------ | --------------------- | ---------------------------------------------- | +| `min_grad_norm` | `min_nd` | same rename Fortran's own keyword needs | +| `max_decs` | `maxdecs` | same rename Fortran's own keyword needs | +| `numrej` | `maxrej` | same rename Fortran's own keyword needs | +| `num_mix_comps` | `num_mix` | same rename Fortran's own keyword needs | +| `share_int` | `share_iter` | JSON-schema-only; Fortran's own spelling already matches | + +-- and a file carrying both a setting's alias and its canonical key (e.g. both `max_decs` and +`maxdecs`) raises `ValueError` naming both rather than picking one silently. +Applying this table is itself a behavior change for the PyTorch wrapper: +`sample_params.json`'s own `max_decs`/`min_grad_norm`/`share_int` settings previously matched +neither a named `fit()` parameter nor an `AMICATorchNG` keyword under their raw JSON spelling, +so they were only named in the "not applied" warning rather than applied; +fitting from that file now applies them. + +The Fortran-text branch (`read_fortran_param_file`) parses Fortran's whitespace-separated +`key value` lines (`#` full-line comments, plus a deliberately permissive inline `" #..."` +trailing comment; ints/floats/strings, including Fortran's `d`/`D` double-precision exponent +marker; `0`/`1` boolean flags using Fortran's own `k == 1` semantics) into a dict targeting +pamica's actual Python call surface: `AMICA.fit`'s named parameters (`max_iter`, `lrate`, +`do_mean`, `do_sphere`, `do_newton`) and `AMICATorchNG` constructor keywords. It was built by +reading every `case('...')` arm of `amica15.f90`'s parameter parser (~amica15.f90:3100-3700) +against `AMICATorchNG`'s constructor and `validate_implementations.py`'s `_NG_PARAMS`/ +`_HANDLED_KEYS`. `AMICA.from_params_file` stashes the translated dict on the returned instance, and `fit()` applies it as **per-call defaults**: an argument passed explicitly to `fit()` always wins over the file's value, whether that argument is one of the five named parameters above or an `AMICATorchNG` keyword passed through `**kwargs` (e.g. `block_size`, `rho0`, `newt_start`). -89 Fortran keywords are recognized; 57 (56 distinct pamica-side names) are translated and 32 are +89 Fortran keywords are recognized; 60 (59 distinct pamica-side names) are translated and 29 are deliberately unsupported (checkpoint warm-start, per-family EM freeze toggles, FIR/DFT -pre-filtering, console/output-file reporting, ...) -- +pre-filtering, console reporting, ...) -- a keyword this reader drops always fires a `logger.warning` naming it, whether that is because it is a real Fortran keyword pamica has no equivalent for, or because it is not a Fortran keyword this reader recognizes at all (the bundled `sample_data/input.param` template itself carries three such stale entries -- `field_blocksize`, `doPCA`, `load_W` -- that predate this parser and are not `case('...')` arms in `amica15.f90` either, so the reference binary already ignores them too). A malformed line (a keyword with no value, or a non-empty file where not one keyword is recognized -by the reference parser at all -- e.g. a JSON file mistakenly handed to this reader) raises -`ValueError` rather than being dropped or defaulted silently. Data-location metadata the file -carries (`files`, `outdir`, `data_dim`, `field_dim`, ...) matches no `fit()`/`AMICATorchNG` -parameter by design; `fit()` names these in a single warning as "not applied" rather than -forwarding or silently dropping them. +by the reference parser at all -- e.g. a JSON file mistakenly handed to `read_fortran_param_file` +directly) raises `ValueError` rather than being dropped or defaulted silently. Data-location +metadata the file carries (`files`, `outdir`, `data_dim`, `field_dim`, ...) matches no +`fit()`/`AMICATorchNG` parameter by design; `fit()` names these in a single warning as +"not applied" rather than forwarding or silently dropping them. Only three keywords are renamed, because Fortran spells them differently from the pamica-side (constructor) name: @@ -607,9 +633,10 @@ Only three keywords are renamed, because Fortran spells them differently from th `num_mix_comps`/`num_mix` both collapse to the pamica key `num_mix`, read directly by `from_params_file` to size the instance (`AMICA(n_mix=...)`) before the rest of the dict ever reaches `fit()`. `share_iter` is **not** renamed -- it already matches `AMICATorchNG.share_iter` -exactly, unlike `sample_params.json`'s own schema, which spells the same setting `share_int` (a -pre-existing mismatch in that JSON file, out of scope here, that `fit()`'s per-call-default merge -now surfaces as a "not applied" warning when fitting from it rather than silently ignoring it). +exactly, so this table has no entry for it. `sample_params.json`'s own JSON schema instead spells +the same setting `share_int`; the JSON alias table above (`JSON_ALIAS_TO_CANONICAL`) is what maps +that JSON-schema spelling -- and the JSON schema's `max_decs`/`min_grad_norm` spellings for the +two renamed settings above -- to the canonical names on the way in, not this Fortran-keyword table. `do_opt_block`, `blk_min`, `blk_max` and `blk_step` moved from the unsupported table to identity mappings with issue #232: pamica now implements the block-size search under Fortran's own four @@ -617,5 +644,28 @@ names and Fortran's arithmetic stepping, so a file carrying them is applied rath about and dropped. pamica's *defaults* for the three bounds differ (Fortran's 128-1024 is far below where any pamica backend peaks), but a file that sets them is honored as written. +`writestep`, `do_history` and `histstep` moved the same way with issue #304: the reader +translates what *any* pamica backend can honor, not just the one a given call targets, and +each consumer warns about what *it* cannot apply. +The legacy NumPy backend is the one backend that implements periodic on-disk checkpointing +under these exact names today (`numpy_impl/core.py`'s fit loop and `_write_history`); +torch/MLX have no matching mechanism yet (issue #312), so `AMICA.fit` names these three in +its own "not applied" warning instead of silently dropping them. + Every other translated keyword keeps its Fortran spelling; see `FORTRAN_TO_PAMICA_KEY` and `FORTRAN_UNSUPPORTED_KEYS` in `pamica/fortran_params.py` for the full tables. + +The legacy NumPy backend reads through the same `read_params_file` (`AMICA_NumPy(params_file=...)` +or the `from_params_file` classmethod -- renamed from `from_json_file`, since it now accepts +both formats), mapping the canonical keys above to its own attribute spellings (`min_nd` -> +`min_grad_norm`, `maxdecs` -> `max_decs`, `share_iter` -> `share_int`; `maxrej`/`num_mix` need +no translation, already matching). +A params-file setting this backend does not consume (e.g. `kurt_start`/`num_kurt`/`kurt_int`, +the adaptive-pdf schedule it has no family switch for) is named in a single `logger.warning` +rather than silently dropped. +`AMICA_NumPy(pdftype=...)` with anything other than `0` -- the only source-density family +this backend implements -- raises `NotImplementedError` at construction (see the +backend-differences table above). +The NumPy CLI (`python -m pamica.numpy_impl.cli`) reads through the same helper +(`_read_numpy_keyed_params`), so it accepts both formats too, instead of its own separate +`json.load` (issue #304). diff --git a/pamica/amica.py b/pamica/amica.py index af25971d..00eba320 100644 --- a/pamica/amica.py +++ b/pamica/amica.py @@ -824,9 +824,18 @@ def from_params_file(cls, params_file: str, **kwargs) -> "AMICA": otherwise) rather than trusting the extension -- a compact JSON file saved with a ``.param`` extension must still parse as JSON, not be silently misread as garbled Fortran text (issue #132 review item 3). - See :func:`pamica.fortran_params.read_fortran_param_file` for the - Fortran-side key-mapping table and the deliberately-unmapped keys it - warns about rather than silently drops. + Both formats are read through :func:`pamica.fortran_params. + read_params_file` (issue #304), which also applies pamica's JSON + schema's own alias spellings (``min_grad_norm``/``max_decs``/ + ``share_int``/...) to the canonical/constructor names -- so a + ``sample_params.json`` fit now applies its ``max_decs``/ + ``min_grad_norm``/``share_int`` settings, which previously matched + neither a named ``fit()`` parameter nor an ``AMICATorchNG`` keyword + under their raw JSON spelling and were only named in the "not + applied" warning rather than applied. See that function and + :func:`pamica.fortran_params.read_fortran_param_file` for the + Fortran-side key-mapping table and the deliberately-unmapped keys + they warn about rather than silently drop. The full translated dict (beyond the ``n_models``/``n_mix`` used to size the instance here) is stashed on the returned instance and @@ -845,16 +854,9 @@ def from_params_file(cls, params_file: str, **kwargs) -> "AMICA": amica : AMICA Configured AMICA instance """ - import json + from .fortran_params import read_params_file - path = Path(params_file) - text = path.read_text() - if text.lstrip().startswith(("{", "[")): - params = json.loads(text) - else: - from .fortran_params import read_fortran_param_file - - params = read_fortran_param_file(path) + params = read_params_file(Path(params_file)) # Extract relevant parameters n_models = params.get("num_models", 1) diff --git a/pamica/fortran_params.py b/pamica/fortran_params.py index dcc55bca..872c2332 100644 --- a/pamica/fortran_params.py +++ b/pamica/fortran_params.py @@ -31,7 +31,7 @@ *constructor's* spelling, not the JSON schema's. ``FORTRAN_TO_PAMICA_KEY`` lists every Fortran keyword this module can -translate (57 keys covering 56 distinct pamica-side names -- Fortran accepts +translate (60 keys covering 59 distinct pamica-side names -- Fortran accepts both ``num_mix_comps`` and ``num_mix`` for the same setting). Of those, three are renamed because Fortran spells them differently from the pamica-side name: @@ -50,29 +50,54 @@ the instance at construction time (unchanged pre-existing behavior), so those two keys are consumed *before* the rest of the dict ever reaches ``fit()``'s per-call-default merge. ``share_iter`` is **not** renamed here -- -it already matches ``AMICATorchNG.share_iter`` exactly. This is a deliberate -divergence from ``sample_params.json``, whose own schema spells the same -setting ``share_int`` (and, along with its ``max_decs``/``min_grad_norm`` -keys, does not match the constructor either -- a pre-existing gap in that -JSON file, out of scope here, that ``AMICA.fit``'s per-call-default merge -now surfaces as a "not applied" warning when fitting from it). +it already matches ``AMICATorchNG.share_iter`` exactly, so this table has no +entry for it. ``sample_params.json``'s own JSON schema instead spells the +same setting ``share_int``; ``read_params_file``'s ``JSON_ALIAS_TO_CANONICAL`` +table (below) is what maps that JSON-schema spelling -- and the JSON +schema's ``max_decs``/``min_grad_norm`` spellings for the two renamed +settings above -- to the canonical names on the way in, not this +Fortran-keyword table. Every other translated key keeps its Fortran spelling. -``FORTRAN_UNSUPPORTED_KEYS`` lists the remaining 32 keywords the Fortran +``writestep``/``do_history``/``histstep`` are identity mappings too, even +though they configure a mechanism (periodic on-disk checkpointing) that only +the legacy NumPy backend implements today (``numpy_impl/core.py``'s fit loop +and ``_write_history``) -- the reader's job is to translate every keyword +*some* pamica backend can honor, not just the ones this call's target +backend happens to support; each consumer is responsible for warning about +what *it* cannot apply. Porting these three to the torch/MLX backends is +tracked as issue #312; until then, ``AMICA.fit`` (which has no matching +mechanism) names them in its own "not applied" warning rather than silently +dropping them, the same way it already handles any other translated key with +no ``AMICATorchNG`` equivalent. + +``FORTRAN_UNSUPPORTED_KEYS`` lists the remaining 29 keywords the Fortran parser accepts (checkpoint warm-start, per-family EM freeze toggles, FIR/DFT -pre-filtering, console/file reporting cadence, ...) that this module does not -translate -- most with no pamica equivalent anywhere, but a few -(``writestep``/``do_history``/``histstep``) exist on the legacy NumPy backend -only (see the table entry below); this module's output targets the torch -wrapper, which has no matching mechanism (issue #312). These are -*deliberately* unmapped, not missed, and ``read_fortran_param_file`` warns -about them by name rather than dropping them in silence. A keyword absent -from *both* dicts is not a Fortran keyword this module knows about at all -(a typo, or a different ``amica15.f90`` revision than the one bundled here) -and is also warned about, separately. +pre-filtering, console reporting, ...) that this module does not translate -- +none has a pamica equivalent on any backend. These are *deliberately* +unmapped, not missed, and ``read_fortran_param_file`` warns about them by +name rather than dropping them in silence. A keyword absent from *both* +dicts is not a Fortran keyword this module knows about at all (a typo, or a +different ``amica15.f90`` revision than the one bundled here) and is also +warned about, separately. + +``read_params_file`` (issue #304) is the single params-file entry point every +backend should use: it JSON-decodes a file whose first non-whitespace +character is ``{``/``[`` (the JSON top level must be an object) and otherwise +parses it as Fortran text via :func:`read_fortran_param_file`, so either +format lands on the same canonical pamica keys. A JSON file's own keys pass +through ``JSON_ALIAS_TO_CANONICAL`` -- pamica's JSON schema +(``sample_params.json``, ``numpy_impl/params.json``) spells five settings +differently from the canonical/constructor name (the same three Fortran +renames above, plus ``share_int`` for ``share_iter``, which is JSON-schema- +only -- Fortran's own spelling already matches the canonical name). A JSON +file that carries both a setting's alias and its canonical key (e.g. +``max_decs`` and ``maxdecs``) is ambiguous and raises ``ValueError`` naming +both, rather than picking one silently. """ +import json import logging import re from pathlib import Path @@ -253,6 +278,13 @@ "invsigmin": "invsigmin", # Reproducibility. "seed": "seed", + # Periodic on-disk checkpointing: identity mappings (Fortran's own + # spelling matches pamica's, since only the legacy NumPy backend + # implements this mechanism today -- see the module docstring and + # issue #312 for the torch/MLX gap). + "writestep": "writestep", + "do_history": "do_history", + "histstep": "histstep", } # --------------------------------------------------------------------------- @@ -303,21 +335,38 @@ "print_debug": "Fortran console verbosity, no pamica equivalent", "write_nd": "Fortran output-file toggle (weight/gradient dumps)", "write_LLt": "Fortran output-file toggle (per-sample log-likelihood dumps)", - # Periodic on-disk checkpointing DOES have a pamica equivalent, but only - # on the legacy NumPy backend (numpy_impl/core.py's fit loop, ~2195-2198, - # and _write_history, ~2460-2489), which implements writestep/do_history/ - # histstep with the same names and cadence semantics as Fortran. These - # stay unsupported HERE only because this module's output targets the - # torch wrapper (AMICA.fit/AMICATorchNG), which has no matching - # mechanism; porting it to torch/MLX is tracked as issue #312. - "writestep": "on-disk checkpoint write cadence; NumPy backend only (issue #312)", - "do_history": "periodic weight-history dump; NumPy backend only (issue #312)", - "histstep": "weight-history dump cadence; NumPy backend only (issue #312)", # Misc. "decwindow": "decrease-count window size; pamica exposes only max_decs/maxdecs", "fix_init": "fixed initialization scheme, not ported", } +# --------------------------------------------------------------------------- +# JSON-schema key -> canonical pamica key (issue #304). pamica's own JSON +# schema (sample_params.json, numpy_impl/params.json) spells five settings +# differently from the canonical/constructor name used above: the same three +# Fortran renames (min_grad_norm/max_decs/numrej -> min_nd/maxdecs/maxrej), +# plus num_mix_comps -> num_mix (the JSON schema itself already spells this +# one "num_mix", so the alias only matters for a file that instead uses +# Fortran's "num_mix_comps" spelling), plus share_int -> share_iter, which is +# JSON-schema-only: Fortran's own spelling ("share_iter") already matches the +# canonical name, so it needs no entry in FORTRAN_TO_PAMICA_KEY's rename table. +# Derived from FORTRAN_TO_PAMICA_KEY's own renames (the non-identity entries) +# so the two tables cannot drift apart, plus the one JSON-only addition. +JSON_ALIAS_TO_CANONICAL: dict = { + key: value for key, value in FORTRAN_TO_PAMICA_KEY.items() if key != value +} +JSON_ALIAS_TO_CANONICAL["share_int"] = "share_iter" + +# Canonical pamica key -> the Fortran keyword it came from, for the settings +# FORTRAN_TO_PAMICA_KEY actually renames (the inverse of the non-identity +# entries above). Used by callers that need to go the other way -- write a +# Fortran-format param file from pamica-keyed settings -- so that knowledge +# lives in one place too (see validate_implementations.py's +# write_fortran_param_file). +PAMICA_KEY_TO_FORTRAN_KEY: dict = { + value: key for key, value in FORTRAN_TO_PAMICA_KEY.items() if key != value +} + def _fortran_accepted_keys(source: Optional[Path] = None) -> Optional[set]: """Keywords the reference binary's parameter parser accepts. @@ -515,3 +564,68 @@ def read_fortran_param_file(path: Union[str, Path]) -> dict: sorted(unsupported), ) return result + + +def _apply_json_aliases(data: dict, path: Union[str, Path]) -> dict: + """Translate a parsed JSON parameter file's keys to canonical pamica keys. + + See ``JSON_ALIAS_TO_CANONICAL``. A key that carries both an alias and its + own canonical spelling (e.g. both ``max_decs`` and ``maxdecs``) is + ambiguous -- which one wins would be an arbitrary silent choice -- so this + raises rather than picking one. Non-aliased keys (including NumPy-only + JSON keys like ``kurt_start``) pass through unchanged. + """ + conflicts = sorted( + (alias, canonical) + for alias, canonical in JSON_ALIAS_TO_CANONICAL.items() + if alias in data and canonical in data + ) + if conflicts: + names = ", ".join(f"{alias!r}/{canonical!r}" for alias, canonical in conflicts) + raise ValueError( + f"{path}: JSON parameter file has both an alias and its canonical " + f"key for the same setting ({names}); keep only one." + ) + return {JSON_ALIAS_TO_CANONICAL.get(k, k): v for k, v in data.items()} + + +def read_params_file(path: Union[str, Path]) -> dict: + """Parse a pamica parameter file into canonical pamica keys (issue #304). + + The single params-file entry point every backend should use: content-sniffs + the format (JSON if the file's first non-whitespace character is ``{`` or + ``[``, otherwise the literal Fortran ``input.param`` text format via + :func:`read_fortran_param_file`), rather than trusting the file extension, + and returns the same canonical pamica keys either way. + + A JSON top level must be an object (a JSON array has no keys to + translate); a JSON file's own keys pass through ``JSON_ALIAS_TO_CANONICAL`` + (see the module docstring), and a file that sets both an alias and its + canonical key for the same setting raises ``ValueError``. Malformed JSON + raises ``json.JSONDecodeError`` (a ``ValueError`` subclass). The Fortran + branch's own error/warning behavior (malformed lines, unrecognized/ + unsupported keywords, an effectively-empty file) is documented on + :func:`read_fortran_param_file`. + + Parameters + ---------- + path : str or Path + Path to a JSON or Fortran-format parameter file. + + Returns + ------- + dict + Parsed settings, keyed by pamica's canonical names (see + ``FORTRAN_TO_PAMICA_KEY``). + """ + path = Path(path) + text = path.read_text() + if text.lstrip()[:1] in ("{", "["): + data = json.loads(text) + if not isinstance(data, dict): + raise ValueError( + f"{path}: JSON parameter file's top level must be an object, " + f"got {type(data).__name__}" + ) + return _apply_json_aliases(data, path) + return read_fortran_param_file(path) diff --git a/pamica/numpy_impl/cli.py b/pamica/numpy_impl/cli.py index a4a7bd5c..9bbb9c09 100644 --- a/pamica/numpy_impl/cli.py +++ b/pamica/numpy_impl/cli.py @@ -4,7 +4,7 @@ This module provides a command-line interface for running the AMICA algorithm. It handles: -1. Parameter loading from JSON configuration files +1. Parameter loading from a JSON or Fortran-format (``input.param``) file 2. Data loading from binary files 3. Model initialization and training 4. Result saving @@ -12,12 +12,15 @@ Example usage: python -m pamica.numpy_impl.cli params.json --outdir results --seed 42 # Use -m flag to run as module -The parameter file should be in JSON format and must include: +The parameter file may be pamica's own JSON schema or the literal Fortran +``input.param`` text format, auto-detected by content the same way +``AMICA(params_file=...)`` itself does (:func:`pamica.fortran_params. +read_params_file`, issue #304). Either way it must include: - files: List of binary data files to process - data_dim: Number of channels/dimensions - field_dim: Number of samples per channel for each file -Optional parameters can be included in the JSON file: +Optional parameters can be included in the file: - num_models: Number of models (default: 1) - num_mix: Number of mixture components (default: 3) - max_iter: Maximum iterations (default: 2000) @@ -25,13 +28,12 @@ """ import argparse -import json import logging import sys from pathlib import Path from typing import Dict, Any, Optional -from .core import AMICA +from .core import AMICA, _read_numpy_keyed_params from .data import load_multiple_files @@ -43,7 +45,7 @@ def parse_args() -> argparse.Namespace: ------- args : argparse.Namespace Parsed command line arguments with the following attributes: - - paramfile: Path to JSON parameter file + - paramfile: Path to a JSON or Fortran-format (input.param) parameter file - outdir: Output directory for results (default: 'output') - seed: Random seed for reproducibility (optional) - verbose: Flag for detailed per-line progress output (disables tqdm progress bar) @@ -51,7 +53,10 @@ def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="AMICA: Adaptive Mixture ICA") # Required arguments - parser.add_argument("paramfile", help="Parameter file in JSON format") + parser.add_argument( + "paramfile", + help="Parameter file (pamica JSON schema or Fortran input.param text)", + ) # Optional arguments parser.add_argument( @@ -71,10 +76,19 @@ def load_params( paramfile: str, default_paramfile: Optional[str] = None ) -> Dict[str, Any]: """ - Load and validate AMICA parameters from JSON configuration file. + Load and validate AMICA parameters from a params file. The function loads default parameters from default_paramfile (if provided), - then updates them with user-provided parameters from paramfile. + then updates them with user-provided parameters from paramfile. Both are + read through :func:`pamica.numpy_impl.core._read_numpy_keyed_params` + (the same helper ``AMICA(params_file=...)`` itself uses, issue #304), so + either a pamica JSON-schema file or the literal Fortran ``input.param`` + text format works here -- not just JSON, as before -- and the returned + keys are already mapped to this backend's own spellings (no separate + key-mapping table duplicated in this module). ``files`` is returned + exactly as the params file spells it, still relative to the current + working directory: this function performs no path resolution, matching + the reference Fortran binary's own semantics. Required parameters in paramfile are: - files: List of data files to process @@ -84,26 +98,22 @@ def load_params( Parameters ---------- paramfile : str - Path to JSON parameter file with user settings + Path to a JSON or Fortran-format parameter file with user settings. default_paramfile : str, optional - Path to JSON file with default parameters + Path to a JSON or Fortran-format file with default parameters. Returns ------- params : dict - Dictionary of parameters + Dictionary of parameters. """ - # Load default parameters if provided - if default_paramfile: - with open(default_paramfile) as f: - params = json.load(f) - else: - params = {} + # Load default parameters if provided. + params = ( + dict(_read_numpy_keyed_params(default_paramfile)) if default_paramfile else {} + ) - # Update with user parameters - with open(paramfile) as f: - user_params = json.load(f) - params.update(user_params) + # Update with user parameters. + params.update(_read_numpy_keyed_params(paramfile)) # Required parameters required = {"files", "data_dim", "field_dim"} diff --git a/pamica/numpy_impl/core.py b/pamica/numpy_impl/core.py index aba267e3..1ae1ff38 100644 --- a/pamica/numpy_impl/core.py +++ b/pamica/numpy_impl/core.py @@ -6,6 +6,18 @@ algorithm, which performs blind source separation using a mixture of adaptive independent component analyzers. +Source-density family scope (issue #304, the ``.rules/backend_parity.md`` +narrow exception): this backend implements only the generalized-Gaussian +source density (``pdftype=0``) -- ``_compute_log_pdf``, the fit-path density +function, has no ``pdftype`` parameter and always runs the GG update. +``AMICA(pdftype=...)`` with anything other than ``0`` raises +``NotImplementedError`` at construction rather than silently ignoring the +setting. The extended-Infomax adaptive switcher (``pdftype=1``), Gaussian +(``pdftype=2``), logistic (``pdftype=3``) and sub-Gaussian cosh+ +(``pdftype=4``) families are implemented on the PyTorch (``AMICATorchNG``, +via the :class:`~pamica.amica.AMICA` wrapper) and MLX (``AMICAMLXNG``) +backends instead. + Key Features ----------- * Multiple Source Models: Can learn different mixing models for different parts of the data @@ -74,13 +86,13 @@ from scipy import linalg from scipy.special import digamma import logging -import json import time from pathlib import Path from typing import Dict, List, Optional, Tuple, Union from tqdm import tqdm from .. import blocktune from .. import restarts +from ..fortran_params import read_params_file from ..rank import ( MINEIG, MINEIG_REL, @@ -94,30 +106,134 @@ get_unmixing_matrices, ) +logger = logging.getLogger(__name__) + +# Data-location keys: not AMICA hyperparameters, so they are excluded from +# load_default_params()'s return value. AMICA.__init__ reads files/data_dim/ +# field_dim off the same parsed dict separately (see _config_files/ +# _config_data_dim/_config_field_dim below); num_samples has no NumPy-backend +# consumer at all (fit() derives sample count from the loaded data itself). +_DATA_LOCATION_KEYS = frozenset({"files", "num_samples", "data_dim", "field_dim"}) + +# Canonical pamica key (read_params_file's output spelling, issue #304) -> +# this backend's own constructor/attribute spelling, for the three settings +# where they differ. maxrej/num_mix need no entry here: this backend already +# spells them the same as the canonical name (see fortran_params.py's +# FORTRAN_TO_PAMICA_KEY rename table and its JSON_ALIAS_TO_CANONICAL). +_CANONICAL_TO_NUMPY_KEY = { + "min_nd": "min_grad_norm", + "maxdecs": "max_decs", + "share_iter": "share_int", +} + +# Every params-file key this constructor actually reads (via params.get(...) +# below, or files/data_dim/field_dim via the raw parsed dict), keyed by this +# backend's own spelling. Built explicitly from those call sites rather than +# guessed, so a key can only be "consumed" here if the constructor truly uses +# it (see the unconsumed-key warning in __init__). num_samples is +# deliberately absent: see _DATA_LOCATION_KEYS. +_CONSUMED_KEYS = frozenset( + { + "num_models", + "num_mix", + "max_iter", + "do_newton", + "newt_start", + "newt_ramp", + "newtrate", + "do_reject", + "rejsig", + "rejstart", + "rejint", + "maxrej", + "num_comps", + "lrate", + "minlrate", + "lratefact", + "rho0", + "minrho", + "maxrho", + "rholrate", + "rholratefact", + "invsigmax", + "invsigmin", + "do_history", + "histstep", + "do_opt_block", + "block_size", + "blk_min", + "blk_max", + "blk_step", + "share_comps", + "comp_thresh", + "share_start", + "share_int", + "doscaling", + "scalestep", + "do_sphere", + "do_mean", + "do_approx_sphere", + "pcakeep", + "pcadb", + "mineig", + "mineig_rel", + "writestep", + "max_decs", + "maxincs", + "restartiter", + "maxrestarts", + "min_dll", + "min_grad_norm", + "use_min_dll", + "use_grad_norm", + "pdftype", + "outdir", + "seed", + "n_restarts", + "restart_seeds", + # Data-location metadata, read from the same parsed dict via + # _config_files/_config_data_dim/_config_field_dim (fit()'s no-data + # path), not via params.get(). + "files", + "data_dim", + "field_dim", + } +) + + +def _read_numpy_keyed_params(params_file: Optional[Union[str, Path]]) -> Dict: + """Parse a params file and map its canonical keys to this backend's own + spellings (``_CANONICAL_TO_NUMPY_KEY``), without stripping anything. + + Reads through :func:`pamica.fortran_params.read_params_file` (issue + #304), so both pamica's JSON schema and the literal Fortran + ``input.param`` text format are accepted, for both the bundled default + params.json (``params_file=None``) and a user-supplied file. + """ + if params_file is None: + params_file = Path(__file__).parent / "params.json" + canonical = read_params_file(params_file) + return {_CANONICAL_TO_NUMPY_KEY.get(k, k): v for k, v in canonical.items()} + def load_default_params(params_file: Optional[Union[str, Path]] = None) -> Dict: """ - Load default parameters from JSON file. + Load default parameters from a params file. Parameters ---------- params_file : str, optional - Path to JSON parameter file. If None, uses default params.json + Path to a JSON or Fortran-format (``input.param``) parameter file + (issue #304). If None, uses the bundled default params.json. Returns ------- params : dict - Dictionary of default parameters + Dictionary of default parameters, keyed by this backend's own + spellings, excluding data-location keys (see ``_DATA_LOCATION_KEYS``). """ - if params_file is None: - params_file = Path(__file__).parent / "params.json" - - with open(params_file) as f: - params = json.load(f) - - # Remove data-specific parameters - data_params = {"files", "num_samples", "data_dim", "field_dim"} - return {k: v for k, v in params.items() if k not in data_params} + params = _read_numpy_keyed_params(params_file) + return {k: v for k, v in params.items() if k not in _DATA_LOCATION_KEYS} class AMICA: @@ -146,7 +262,8 @@ def __init__( Parameters ---------- params_file : str, optional - Path to JSON parameter file with default values + Path to a JSON or Fortran-format (``input.param``) parameter + file with default values (issue #304). use_tqdm : bool, default=True Whether to use tqdm progress bar (False will use per-line printing) verbose : bool, default=False @@ -190,8 +307,27 @@ def __init__( # Store progress bar settings self.use_tqdm = use_tqdm self.verbose = verbose - # Load default parameters - params = load_default_params(params_file) + # Load default parameters (issue #304: both the bundled default and a + # user-supplied params_file are read through the same translator, so + # a literal Fortran input.param works here too, not just JSON). Kept + # unstripped (raw_params) so the data-location keys below can be read + # off the same parse instead of a second file read. + raw_params = _read_numpy_keyed_params(params_file) + params = {k: v for k, v in raw_params.items() if k not in _DATA_LOCATION_KEYS} + + # A params-file setting this backend does not consume is silently + # dropped nowhere else in this constructor, so name it here rather + # than letting it vanish (issue #304). Checked before the **kwargs + # override below: kwargs are explicit Python-level arguments, not + # file settings, so they are not subject to this warning. + unconsumed = sorted(set(raw_params) - _CONSUMED_KEYS) + if unconsumed: + logger.warning( + "AMICA (NumPy): %d parameter-file setting(s) have no " + "NumPy-backend equivalent and are not applied: %s", + len(unconsumed), + unconsumed, + ) # Override with any provided parameters params.update(kwargs) @@ -325,22 +461,34 @@ def __init__( self.min_grad_norm = params.get("min_grad_norm", 1e-7) self.use_min_dll = params.get("use_min_dll", True) self.use_grad_norm = params.get("use_grad_norm", True) - # Inert: never read after assignment (this backend always runs the GG - # update; _compute_log_pdf takes no pdftype). Aligned to 0 to match - # the torch/MLX constructor default, for surface consistency only. + # This backend implements only the generalized-Gaussian source + # density (pdftype 0): _compute_log_pdf takes no pdftype and always + # runs the GG update, so a non-zero pdftype would silently configure + # nothing (issue #304; the narrow-exception rule in + # .rules/backend_parity.md). pdftype 1 (extended-Infomax adaptive + # switcher), 2 (Gaussian), 3 (logistic) and 4 (sub-Gaussian cosh+) are + # available on AMICA/AMICATorchNG and AMICAMLXNG instead of silently + # doing nothing here. self.pdftype = params.get("pdftype", 0) + if self.pdftype != 0: + raise NotImplementedError( + "AMICA_NumPy implements only the generalized-Gaussian source " + f"density (pdftype=0); got pdftype={self.pdftype}. pdftype " + "1 (extended-Infomax adaptive switcher), 2 (Gaussian), 3 " + "(logistic) and 4 (sub-Gaussian cosh+) are available on " + "AMICA (AMICATorchNG) and AMICAMLXNG." + ) self.outdir = Path(params.get("outdir", "output")) # Data-source config (used by fit() when called without explicit - # data). load_default_params() strips 'files'/'data_dim'/'field_dim' - # from `params` (they are data-specific, not hyperparameters), so - # read them directly from the raw params_file JSON instead. + # data), read off the same parsed dict as the hyperparameters above + # (issue #304: no second file read) -- files/data_dim/field_dim are + # data-specific, not hyperparameters, so load_default_params()/the + # _DATA_LOCATION_KEYS filter above strips them from `params`. self._config_files = None self._config_data_dim = None self._config_field_dim = None if params_file is not None: - with open(params_file) as f: - raw_params = json.load(f) self._config_files = raw_params.get("files") self._config_data_dim = raw_params.get("data_dim") self._config_field_dim = raw_params.get("field_dim") @@ -489,11 +637,15 @@ def _setup_logging(self): self.logger.propagate = False @classmethod - def from_json_file(cls, params_file: str, **kwargs) -> "AMICA": + def from_params_file(cls, params_file: str, **kwargs) -> "AMICA": """ - Construct an AMICA model from a JSON parameter file. + Construct an AMICA model from a parameter file. - Equivalent to ``AMICA(params_file=params_file, **kwargs)``. If the + Equivalent to ``AMICA(params_file=params_file, **kwargs)``. Accepts + both pamica's own JSON schema and the literal Fortran ``input.param`` + text format, auto-detected from the file's content (issue #304; see + :func:`pamica.fortran_params.read_params_file`) -- matches the + PyTorch wrapper's :meth:`~pamica.amica.AMICA.from_params_file`. If the parameter file defines ``files``/``data_dim``/``field_dim``, a subsequent call to :meth:`fit` with no arguments will load the data described there (see :meth:`fit`). @@ -501,7 +653,7 @@ def from_json_file(cls, params_file: str, **kwargs) -> "AMICA": Parameters ---------- params_file : str - Path to JSON parameter file. + Path to a JSON or Fortran-format parameter file. **kwargs Additional overrides passed through to the constructor. @@ -592,7 +744,7 @@ def fit(self, data: Optional[np.ndarray] = None) -> "AMICA": data : ndarray of shape (n_channels, n_samples), optional The input data to fit the model to. If omitted, the data is loaded from the ``files``/``data_dim``/``field_dim`` parameters - supplied via ``params_file`` (see :meth:`from_json_file`). + supplied via ``params_file`` (see :meth:`from_params_file`). Returns ------- diff --git a/pamica/numpy_impl/params.json b/pamica/numpy_impl/params.json index 5863c4e4..c293f70a 100644 --- a/pamica/numpy_impl/params.json +++ b/pamica/numpy_impl/params.json @@ -66,5 +66,5 @@ "use_min_dll": true, "use_grad_norm": true, - "pdftype": 1 + "pdftype": 0 } diff --git a/pamica/tests/test_fortran_param_forwarding.py b/pamica/tests/test_fortran_param_forwarding.py index c5244aba..c509366b 100644 --- a/pamica/tests/test_fortran_param_forwarding.py +++ b/pamica/tests/test_fortran_param_forwarding.py @@ -71,6 +71,32 @@ def test_aliased_names_are_translated(tmp_path): assert got["numrej"] == "7" +def test_canonical_names_are_also_translated(tmp_path): + """Issue #304: ``_FORTRAN_ALIASES`` now also accepts pamica's canonical + key spellings (``maxdecs``/``min_nd``), not just the JSON-schema aliases + the test above covers -- ``load_sample_data`` hands ``params`` through + already canonical-keyed (see the end-to-end test below).""" + got = _written(tmp_path, {"maxdecs": 4, "min_nd": 0.0001}) + assert got["max_decs"] == "4" + assert got["min_grad_norm"] == "0.0001" + + +def test_load_sample_data_writes_fortran_spellings_end_to_end(tmp_path): + """The production call sequence: ``load_sample_data()`` (canonical-keyed + via ``read_params_file``, issue #304) feeding straight into + ``write_fortran_param_file``, exactly as ``run_fortran_amica`` does. The + written file must carry Fortran's own spellings, never the canonical + ones ``write_fortran_param_file`` translated them from.""" + from validate_implementations import load_sample_data + + _, params = load_sample_data() + got = _written(tmp_path, params) + for fortran_key in ("max_decs", "min_grad_norm", "share_iter", "numrej"): + assert fortran_key in got + for canonical_key in ("maxdecs", "min_nd", "share_int", "maxrej"): + assert canonical_key not in got + + def test_keys_absent_from_the_template_are_appended(tmp_path): """The binary accepts more keywords than the shipped template lists.""" assert "do_approx_sphere" not in TEMPLATE.read_text() diff --git a/pamica/tests/test_fortran_params.py b/pamica/tests/test_fortran_params.py index 2a9781e1..11066236 100644 --- a/pamica/tests/test_fortran_params.py +++ b/pamica/tests/test_fortran_params.py @@ -19,8 +19,10 @@ from pamica.fortran_params import ( FORTRAN_TO_PAMICA_KEY, FORTRAN_UNSUPPORTED_KEYS, + JSON_ALIAS_TO_CANONICAL, _fortran_accepted_keys, read_fortran_param_file, + read_params_file, ) from pamica.torch_impl.utils import load_eeglab_data @@ -133,6 +135,20 @@ def test_block_size_search_keys_translate(parsed): assert key not in FORTRAN_UNSUPPORTED_KEYS +def test_checkpoint_keys_translate(parsed): + """writestep/do_history/histstep moved out of the unsupported table + (issue #304): the reader translates what ANY backend supports, and the + legacy NumPy backend implements periodic on-disk checkpointing under + these exact names/semantics. Torch/MLX have no matching mechanism yet + (issue #312), so their consumer (AMICA.fit) is the one that warns.""" + assert parsed["do_history"] is False + assert parsed["writestep"] == 20 + assert parsed["histstep"] == 10 + for key in ("writestep", "do_history", "histstep"): + assert FORTRAN_TO_PAMICA_KEY[key] == key + assert key not in FORTRAN_UNSUPPORTED_KEYS + + def test_block_size_search_keys_reach_the_backend(tmp_path): """The reader targets the actual Python call surface, so the four keys must land on AMICATorchNG constructor keywords rather than being dropped by @@ -159,6 +175,17 @@ def test_mapping_tables_partition_every_known_fortran_key(): assert accepted == mapped | unsupported +def test_table_sizes_match_the_documented_counts(): + """Pins the module docstring's/docs' "60 keys covering 59 distinct + pamica-side names" and "29 keywords" figures, so a future move between + FORTRAN_TO_PAMICA_KEY and FORTRAN_UNSUPPORTED_KEYS (like writestep/ + do_history/histstep's issue #304 move) forces those counts to be + revisited rather than silently going stale.""" + assert len(FORTRAN_TO_PAMICA_KEY) == 60 + assert len(set(FORTRAN_TO_PAMICA_KEY.values())) == 59 + assert len(FORTRAN_UNSUPPORTED_KEYS) == 29 + + def test_missing_source_disables_unrecognized_filtering(tmp_path, monkeypatch): """Without the reference source, every keyword in the file is treated as Fortran-accepted (mirrors validate_implementations.fortran_accepted_keys); @@ -389,6 +416,41 @@ def test_unhandled_file_settings_warn_once(self, real_data, caplog): assert "data_dim" in warnings assert "files" in warnings + def test_checkpoint_settings_warn_not_applied(self, real_data, caplog): + """writestep/do_history/histstep are translated (issue #304) because + the legacy NumPy backend supports them, but AMICATorchNG has no + matching constructor keyword (issue #312), so fitting from + input.param -- which sets all three -- must still name them in the + "not applied" warning rather than silently dropping them.""" + model = AMICA.from_params_file(str(PARAM_FILE), verbose=False) + with caplog.at_level(logging.WARNING, logger="pamica.amica"): + model.fit(real_data[:, :4096], max_iter=1, seed=0) + warnings = "\n".join(r.message for r in caplog.records) + assert "writestep" in warnings + assert "do_history" in warnings + assert "histstep" in warnings + + def test_json_alias_settings_now_reach_the_backend(self, real_data, caplog): + """Issue #304 behavior change: sample_params.json's own alias + spellings (max_decs/min_grad_norm/share_int) are translated to the + canonical/constructor names (maxdecs/min_nd/share_iter) by + read_params_file, so they now reach AMICATorchNG -- previously they + matched neither a named fit() parameter nor an AMICATorchNG keyword + under their raw JSON spelling, so they were only named in the "not + applied" warning rather than actually applied.""" + model = AMICA.from_params_file(str(JSON_FILE), verbose=False) + with caplog.at_level(logging.WARNING, logger="pamica.amica"): + model.fit(real_data[:, :4096], max_iter=3, seed=0) + backend = model.model_ + assert backend is not None + assert backend.maxdecs == 3 # sample_params.json's max_decs + assert backend.min_nd == 1e-7 # sample_params.json's min_grad_norm + assert backend.share_iter == 100 # sample_params.json's share_int + warnings = "\n".join(r.message for r in caplog.records) + assert "max_decs" not in warnings + assert "min_grad_norm" not in warnings + assert "share_int" not in warnings + def test_fit_without_from_params_file_is_unaffected(self, real_data): """A plain AMICA(...) instance has no _file_params, so fit() must behave exactly as before (hard-coded defaults, no warning).""" @@ -398,26 +460,99 @@ def test_fit_without_from_params_file_is_unaffected(self, real_data): assert model.model_.do_newton is False # the ordinary hard default -def test_param_and_json_agree_on_overlapping_settings(): - """Round trip: sample_data/input.param and sample_data/sample_params.json - describe the same reference run, so every setting present in both must - agree once the Fortran reader's renames are applied. `files` is excluded: - the two files spell the same data file's path relative to different - working directories (input.param relative to sample_data/, sample_params.json - relative to the repo root), which is a path-context difference, not a - parameter-translation one.""" - from_param = read_fortran_param_file(PARAM_FILE) - from_json = json.loads(JSON_FILE.read_text()) - - common = sorted((set(from_param) & set(from_json)) - {"files"}) - # 39 as of this writing (min_grad_norm/max_decs/share_int no longer - # overlap by construction: this reader targets the constructor's - # min_nd/maxdecs/share_iter spelling, which sample_params.json's own - # schema does not use -- see FORTRAN_TO_PAMICA_KEY's module docstring). - assert len(common) >= 35 # sanity: most settings really do overlap - mismatched = { - key: (from_param[key], from_json[key]) - for key in common - if from_param[key] != from_json[key] - } - assert mismatched == {} +class TestReadParamsFile: + """``read_params_file`` (issue #304): the single params-file entry point + every backend should use -- content-sniffed JSON/Fortran-text dispatch, + both landing on the same canonical pamica keys.""" + + def test_param_and_json_agree_on_overlapping_settings(self): + """Anti-drift: sample_data/input.param and sample_data/sample_params.json + describe the same reference run, so every canonical key present in + both must agree once each format's own translation (Fortran renames / + JSON aliases) is applied. `files` is excluded: the two files spell + the same data file's path relative to different working directories + (input.param relative to sample_data/, sample_params.json relative to + the repo root), which is a path-context difference, not a + parameter-translation one.""" + from_param = read_params_file(PARAM_FILE) + from_json = read_params_file(JSON_FILE) + + common = sorted((set(from_param) & set(from_json)) - {"files"}) + # 49 as of this writing (min_grad_norm/max_decs/share_int now DO + # overlap, unlike the pre-#304 raw-json comparison: both formats are + # translated to the same canonical min_nd/maxdecs/share_iter keys). + assert len(common) >= 47 + mismatched = { + key: (from_param[key], from_json[key]) + for key in common + if from_param[key] != from_json[key] + } + assert mismatched == {} + # "files" is the only shared key excluded above, and it really is a + # (path-context) difference, not an accidental agreement masked by + # the exclusion. + assert "files" in from_param and "files" in from_json + assert from_param["files"] != from_json["files"] + + def test_json_top_level_must_be_an_object(self, tmp_path): + dest = tmp_path / "params.json" + dest.write_text("[1, 2, 3]") + with pytest.raises(ValueError, match="top level must be an object"): + read_params_file(dest) + + def test_json_alias_and_canonical_key_conflict_raises(self, tmp_path): + dest = tmp_path / "params.json" + dest.write_text(json.dumps({"max_decs": 3, "maxdecs": 5})) + with pytest.raises(ValueError, match="max_decs.*maxdecs|maxdecs.*max_decs"): + read_params_file(dest) + + def test_json_alias_is_translated_to_canonical(self, tmp_path): + dest = tmp_path / "params.json" + dest.write_text( + json.dumps( + { + "min_grad_norm": 1e-6, + "max_decs": 4, + "numrej": 2, + "num_mix_comps": 5, + "share_int": 50, + } + ) + ) + got = read_params_file(dest) + assert got == { + "min_nd": 1e-6, + "maxdecs": 4, + "maxrej": 2, + "num_mix": 5, + "share_iter": 50, + } + + def test_json_non_aliased_keys_pass_through_unchanged(self, tmp_path): + dest = tmp_path / "params.json" + dest.write_text(json.dumps({"kurt_start": 3, "pdftype": 1})) + assert read_params_file(dest) == {"kurt_start": 3, "pdftype": 1} + + def test_unparsable_non_json_text_raises(self, tmp_path): + """Content that starts with neither ``{``/``[`` nor a valid Fortran + ``key value`` line falls through to the Fortran reader, which raises + rather than silently returning all defaults.""" + dest = tmp_path / "params.txt" + dest.write_text("this is not a parameter file at all, just prose.\n") + with pytest.raises(ValueError): + read_params_file(dest) + + def test_fortran_text_still_dispatches_to_the_fortran_reader(self): + assert read_params_file(PARAM_FILE) == read_fortran_param_file(PARAM_FILE) + + def test_alias_table_derived_from_fortran_renames_plus_share_int(self): + """JSON_ALIAS_TO_CANONICAL has exactly the module docstring's five + entries: the three Fortran renames, num_mix_comps, and the JSON-only + share_int addition.""" + assert JSON_ALIAS_TO_CANONICAL == { + "min_grad_norm": "min_nd", + "max_decs": "maxdecs", + "numrej": "maxrej", + "num_mix_comps": "num_mix", + "share_int": "share_iter", + } diff --git a/pamica/tests/test_numpy_cli.py b/pamica/tests/test_numpy_cli.py new file mode 100644 index 00000000..1fdffd42 --- /dev/null +++ b/pamica/tests/test_numpy_cli.py @@ -0,0 +1,102 @@ +"""NumPy CLI accepts both params-file formats (issue #304 PR #327 review). + +Before this fix, ``cli.py``'s own ``load_params()`` did a bare ``json.load``, +so ``python -m pamica.numpy_impl.cli input.param`` died with a raw +``json.JSONDecodeError`` before ever reaching ``AMICA(params_file=...)`` -- +even though the constructor itself already accepted the literal Fortran text +format. This exercises the CLI's actual ``main()`` entry point end to end on +real bundled data (a tiny iteration budget), for both formats, so the +regression cannot come back unnoticed. + +Real bundled ``sample_data/input.param``/``sample_params.json`` and +``eeglab_data.fdt`` only (NO MOCKS); each params file is a tmp copy with +``max_iter`` lowered and ``files`` repointed at the real ``.fdt``'s absolute +path, so neither the CLI's nor the fit's working directory matters. +""" + +import sys +from pathlib import Path + +import pytest + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +PARAM_FILE = SAMPLE_DIR / "input.param" +JSON_FILE = SAMPLE_DIR / "sample_params.json" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" + +pytestmark = pytest.mark.skipif( + not PARAM_FILE.exists() or not JSON_FILE.exists() or not DATA_FILE.exists(), + reason="bundled sample_data/input.param, sample_params.json or eeglab_data.fdt missing", +) + + +def _fortran_text_with_tiny_iters(tmp_path: Path) -> Path: + """A tmp copy of the real input.param: max_iter lowered to 2, files + repointed at the real sample .fdt's absolute path.""" + dest = tmp_path / "input.param" + lines = [] + for line in PARAM_FILE.read_text().splitlines(): + key = line.split()[0] if line.split() else None + if key == "max_iter": + lines.append("max_iter 2") + elif key == "files": + lines.append(f"files {DATA_FILE.resolve()}") + else: + lines.append(line) + dest.write_text("\n".join(lines) + "\n") + return dest + + +def _json_with_tiny_iters(tmp_path: Path) -> Path: + """A tmp copy of the real sample_params.json: max_iter lowered to 2, + files repointed at the real sample .fdt's absolute path.""" + import json + + payload = json.loads(JSON_FILE.read_text()) + payload["max_iter"] = 2 + payload["files"] = [str(DATA_FILE.resolve())] + dest = tmp_path / "sample_params.json" + dest.write_text(json.dumps(payload)) + return dest + + +def _run_cli(paramfile: Path, outdir: Path, monkeypatch: pytest.MonkeyPatch) -> None: + from pamica.numpy_impl import cli + + monkeypatch.setattr( + sys, "argv", ["cli.py", str(paramfile), "--outdir", str(outdir), "--seed", "0"] + ) + cli.main() + + +def test_cli_accepts_fortran_text_params_file(tmp_path, monkeypatch): + """The regression this whole file guards: a literal Fortran input.param + must drive the CLI end to end, not just AMICA(params_file=...) directly.""" + dest = _fortran_text_with_tiny_iters(tmp_path) + outdir = tmp_path / "amicaout" + _run_cli(dest, outdir, monkeypatch) + assert (outdir / "W").exists() + + +def test_cli_still_accepts_json_params_file(tmp_path, monkeypatch): + """Unaffected by the fix: the pre-existing JSON path keeps working.""" + dest = _json_with_tiny_iters(tmp_path) + outdir = tmp_path / "amicaout_json" + _run_cli(dest, outdir, monkeypatch) + assert (outdir / "W").exists() + + +def test_load_params_maps_canonical_keys_for_fortran_text(tmp_path): + """load_params() itself (not just main()) returns this backend's own + attribute spellings for the three renamed settings, matching + AMICA(params_file=...)'s own _read_numpy_keyed_params.""" + from pamica.numpy_impl.cli import load_params + + dest = _fortran_text_with_tiny_iters(tmp_path) + params = load_params(str(dest)) + assert params["max_decs"] == 3 # input.param's own max_decs + assert params["min_grad_norm"] == 1e-7 # input.param's own min_grad_norm + assert params["share_int"] == 100 # input.param's own share_iter + assert "maxdecs" not in params + assert "min_nd" not in params + assert "share_iter" not in params diff --git a/pamica/tests/test_numpy_params_file.py b/pamica/tests/test_numpy_params_file.py new file mode 100644 index 00000000..d9f9e15c --- /dev/null +++ b/pamica/tests/test_numpy_params_file.py @@ -0,0 +1,190 @@ +"""NumPy backend params-file surface (issue #304). + +``AMICA_NumPy`` used to accept only JSON parameter files -- ``params_file=...`` +routed straight to ``json.load``, so handing it the literal Fortran +``input.param`` text produced a raw ``json.JSONDecodeError`` rather than +either support or a named error. It now reads through the same +``pamica.fortran_params.read_params_file`` every backend uses (both the +bundled default ``params.json`` and a user-supplied file), maps the result to +this backend's own attribute spellings, and warns once by name about any +setting it does not consume. A ``pdftype`` other than 0 -- the only +source-density family this backend implements -- raises +``NotImplementedError`` at construction instead of silently doing nothing +(the legacy behavior the bundled ``params.json`` shipped with: its default +``pdftype`` was 1, even though the fit path never read it). +""" + +import logging +from pathlib import Path + +import numpy as np +import pytest + +from pamica import AMICA_NumPy as AMICA +from pamica.fortran_params import read_params_file +from pamica.numpy_impl.core import _CANONICAL_TO_NUMPY_KEY +from pamica.numpy_impl.data import load_data_file + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +PARAM_FILE = SAMPLE_DIR / "input.param" +JSON_FILE = SAMPLE_DIR / "sample_params.json" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" + +pytestmark = pytest.mark.skipif( + not PARAM_FILE.exists() or not JSON_FILE.exists() or not DATA_FILE.exists(), + reason="bundled sample_data/input.param, sample_params.json or eeglab_data.fdt missing", +) + + +def _shared_numpy_hyperparam_attrs() -> list: + """Canonical keys ``read_params_file`` returns for *both* bundled files, + minus data-location metadata (not a hyperparameter, and not stored under + a plain same-named attribute), mapped to this backend's own attribute + spellings. Derived from the parsed files themselves rather than + hand-listed, so it cannot drift from the anti-drift guarantee + ``test_fortran_params.py`` already proves at the params-file layer.""" + from_json = read_params_file(JSON_FILE) + from_param = read_params_file(PARAM_FILE) + # Data-location metadata, plus outdir: both bundled files happen to set + # the same "./amicaout/" value, but the constructions below pass an + # explicit outdir= override (test hygiene -- never write into the repo), + # which would make this comparison spuriously fail on that key alone. + excluded = {"files", "data_dim", "field_dim", "num_samples", "outdir"} + shared = (set(from_json) & set(from_param)) - excluded + return sorted(_CANONICAL_TO_NUMPY_KEY.get(k, k) for k in shared) + + +def _real_data(n_samples: int) -> np.ndarray: + data = load_data_file(str(DATA_FILE), 32, 30504, dtype=np.float32) + return data[:, :n_samples].astype(np.float64) + + +class TestNumpyFromBothParamsFormats: + """Gate 3: NumPy from ``input.param`` equals NumPy from + ``sample_params.json`` on every shared hyperparameter attribute.""" + + def test_shared_hyperparameters_agree(self, tmp_path): + from_json = AMICA( + params_file=str(JSON_FILE), + outdir=str(tmp_path / "json_out"), + use_tqdm=False, + ) + from_param = AMICA( + params_file=str(PARAM_FILE), + outdir=str(tmp_path / "param_out"), + use_tqdm=False, + ) + shared_attrs = _shared_numpy_hyperparam_attrs() + assert len(shared_attrs) >= 40 # sanity: most settings really do overlap + mismatched = { + attr: (getattr(from_param, attr), getattr(from_json, attr)) + for attr in shared_attrs + if getattr(from_param, attr) != getattr(from_json, attr) + } + assert mismatched == {} + + def test_fortran_text_fit_runs_with_no_explicit_data(self, tmp_path, monkeypatch): + """input.param's own ``files``/``data_dim``/``field_dim`` resolve + ``./eeglab_data.fdt`` relative to the working directory, exactly as + the Fortran binary itself resolves it -- so this chdirs into + sample_data/ rather than passing an absolute path.""" + monkeypatch.chdir(SAMPLE_DIR) + model = AMICA( + params_file="input.param", + max_iter=3, + seed=0, + use_tqdm=False, + outdir=str(tmp_path / "amicaout"), + ) + model.fit() + assert model.converged is True + assert len(model.ll) == 3 + assert np.isfinite(model.ll[-1]) + + +class TestNumpyKwargsOverrideFile: + """Explicit kwargs win over params-file defaults (mirrors the PyTorch + wrapper's ``TestFitAppliesFileDefaults.test_explicit_kwargs_win_over_ + file_defaults`` in ``test_fortran_params.py``): ``params.update(kwargs)`` + in ``__init__`` applies the file's dict first and then overwrites it with + whatever the caller passed explicitly.""" + + def test_explicit_kwargs_win_over_file_defaults(self, tmp_path): + model = AMICA( + params_file=str(PARAM_FILE), + min_grad_norm=5e-5, + max_decs=7, + share_int=42, + use_tqdm=False, + outdir=str(tmp_path / "out"), + ) + assert model.min_grad_norm == 5e-5 # kwarg wins over input.param's 1e-7 + assert model.max_decs == 7 # kwarg wins over input.param's 3 + assert model.share_int == 42 # kwarg wins over input.param's 100 + + +class TestNumpyPdftypeGuard: + """Gate 4: pdftype 1-4 raise NotImplementedError; pdftype 0 constructs.""" + + @pytest.mark.parametrize("pdftype", [1, 2, 3, 4]) + def test_non_gg_pdftype_raises(self, pdftype, tmp_path): + with pytest.raises(NotImplementedError, match="generalized-Gaussian"): + AMICA(pdftype=pdftype, use_tqdm=False, outdir=str(tmp_path / "out")) + + def test_gg_pdftype_constructs(self, tmp_path): + model = AMICA(pdftype=0, use_tqdm=False, outdir=str(tmp_path / "out")) + assert model.pdftype == 0 + + def test_default_pdftype_is_gg(self, tmp_path): + """The bundled params.json default changed 1 -> 0 (issue #304): the + constructor's own fallback was already 0, but the file default + silently overrode it on every params_file=None construction.""" + model = AMICA(use_tqdm=False, outdir=str(tmp_path / "out")) + assert model.pdftype == 0 + + +class TestNumpyUnconsumedKeyWarning: + """Gate 6: NumPy's unconsumed-key warning names the keys; writestep/ + do_history/histstep from Fortran text reach NumPy's attributes.""" + + def test_input_param_unconsumed_keys_are_named(self, tmp_path, caplog): + """input.param carries kurt_start/num_kurt/kurt_int (adaptive pdf, + not on this backend) and num_samples (no NumPy-backend consumer).""" + with caplog.at_level(logging.WARNING, logger="pamica.numpy_impl.core"): + AMICA( + params_file=str(PARAM_FILE), + use_tqdm=False, + outdir=str(tmp_path / "out"), + ) + warnings = "\n".join(r.message for r in caplog.records) + assert "kurt_start" in warnings + assert "num_kurt" in warnings + assert "kurt_int" in warnings + assert "num_samples" in warnings + + def test_sample_params_json_has_no_unconsumed_keys(self, tmp_path, caplog): + """sample_params.json carries nothing this backend cannot honor.""" + with caplog.at_level(logging.WARNING, logger="pamica.numpy_impl.core"): + AMICA( + params_file=str(JSON_FILE), + use_tqdm=False, + outdir=str(tmp_path / "out"), + ) + assert caplog.records == [] + + def test_bundled_default_has_no_unconsumed_keys(self, tmp_path, caplog): + with caplog.at_level(logging.WARNING, logger="pamica.numpy_impl.core"): + AMICA(use_tqdm=False, outdir=str(tmp_path / "out")) + assert caplog.records == [] + + def test_checkpoint_keys_reach_numpy_attributes(self, tmp_path): + """writestep/do_history/histstep moved from unsupported to + translated (issue #304): the legacy NumPy backend is the one backend + that actually implements periodic on-disk checkpointing under these + names, so a Fortran input.param's settings must reach it.""" + model = AMICA( + params_file=str(PARAM_FILE), use_tqdm=False, outdir=str(tmp_path / "out") + ) + assert model.writestep == 20 # input.param's own writestep + assert model.do_history is False # input.param's own do_history (0) + assert model.histstep == 10 # input.param's own histstep diff --git a/pamica/tests/test_sample_data.py b/pamica/tests/test_sample_data.py index 36a3f9cf..1de4fac3 100644 --- a/pamica/tests/test_sample_data.py +++ b/pamica/tests/test_sample_data.py @@ -167,7 +167,9 @@ def test_sample_data_scikit(tmp_path): # Initialize and fit AMICA model using scikit-learn style API. # Override outdir (the params file defaults to the relative './amicaout/') # so this test does not write stray output into the repo root. - model = AMICA.from_json_file(sample_params_file, outdir=str(tmp_path / "amicaout")) + model = AMICA.from_params_file( + sample_params_file, outdir=str(tmp_path / "amicaout") + ) model.fit() # Compare weights @@ -282,7 +284,9 @@ def test_sample_data_light(tmp_path): # Initialize AMICA model with reduced iterations. Override outdir (the # params file defaults to the relative './amicaout/') so this test does # not write stray output into the repo root. - model = AMICA.from_json_file(sample_params_file, outdir=str(tmp_path / "amicaout")) + model = AMICA.from_params_file( + sample_params_file, outdir=str(tmp_path / "amicaout") + ) model.max_iter = 50 # Override max_iter for quick testing # Fit the model diff --git a/validate_implementations.py b/validate_implementations.py index 1127e8ee..5b604267 100644 --- a/validate_implementations.py +++ b/validate_implementations.py @@ -7,7 +7,6 @@ """ import numpy as np -import json import os import re import subprocess @@ -19,6 +18,11 @@ from typing import Dict, Tuple, Optional from pamica import AMICA +from pamica.fortran_params import ( + JSON_ALIAS_TO_CANONICAL, + PAMICA_KEY_TO_FORTRAN_KEY, + read_params_file, +) from pamica.torch_impl import AMICATorchNG from pamica.torch_impl.utils import load_eeglab_data @@ -30,7 +34,11 @@ # rather than forwarded as AMICATorchNG constructor kwargs. Any key that is # neither here nor an AMICATorchNG kwarg is a setting the NG backend cannot # honor; run_pytorch_amica warns about those so a parity comparison against the -# Fortran run can't silently diverge. +# Fortran run can't silently diverge. `max_decs` is not listed: load_sample_data +# now reads params.json through read_params_file (issue #304), which already +# renames it (and min_grad_norm/share_int) to the canonical maxdecs/min_nd/ +# share_iter spelling -- the same spelling _NG_PARAMS filters on -- so those +# three land in ng_kwargs automatically instead of needing a special case here. _HANDLED_KEYS = { "files", "outdir", @@ -40,7 +48,6 @@ "num_mix", "num_comps", "max_iter", - "max_decs", "lrate", "do_mean", "do_sphere", @@ -68,8 +75,12 @@ def load_sample_data() -> Tuple[np.ndarray, Dict]: if not data_file.exists(): raise FileNotFoundError(f"Sample data not found at {data_file}") - with open(params_file, "r") as f: - params = json.load(f) + # Issue #304: read through the shared canonical reader rather than a raw + # json.load, so params.json's own alias spellings (min_grad_norm/max_decs/ + # share_int) are translated to the canonical min_nd/maxdecs/share_iter + # names -- the same spelling _NG_PARAMS filters run_pytorch_amica's + # ng_kwargs on -- instead of silently landing in the "ignored" warning. + params = read_params_file(params_file) data = load_eeglab_data( str(data_file), @@ -81,12 +92,21 @@ def load_sample_data() -> Tuple[np.ndarray, Dict]: return data, params -# params.json spellings that differ from the Fortran keyword of the same setting. +# params.json/pamica-canonical spellings that differ from the Fortran keyword +# of the same setting, keyed both ways so write_fortran_param_file accepts +# either spelling with no hand-maintained entry (issue #304): the canonical +# ("pamica-key") entries come straight from PAMICA_KEY_TO_FORTRAN_KEY (the +# single source of truth for pamica-key -> Fortran-keyword, itself the +# inverse of FORTRAN_TO_PAMICA_KEY's renames); the JSON-schema-keyed entries +# (e.g. "share_int") are composed by chaining each JSON_ALIAS_TO_CANONICAL +# alias through the same table (JSON alias -> canonical -> Fortran keyword; +# a canonical that PAMICA_KEY_TO_FORTRAN_KEY has no rename for, like +# share_iter, falls back to its own spelling, which already matches Fortran). _FORTRAN_ALIASES = { - "num_mix": "num_mix_comps", - "share_int": "share_iter", - "maxrej": "numrej", + json_alias: PAMICA_KEY_TO_FORTRAN_KEY.get(canonical, canonical) + for json_alias, canonical in JSON_ALIAS_TO_CANONICAL.items() } +_FORTRAN_ALIASES.update(PAMICA_KEY_TO_FORTRAN_KEY) # params.json keys that configure the Python side only, so having no Fortran # keyword is expected rather than a dropped setting. @@ -396,10 +416,12 @@ def run_pytorch_amica( # backend seeds init, builds the symmetric-ZCA sphere, and starts from an # identity-plus-small-perturbation mixing matrix internally, so no manual # parameter poking is needed (unlike the removed basic backend). AMICA.fit() - # handles device selection (and the MPS/float64 -> CPU fallback). + # handles device selection (and the MPS/float64 -> CPU fallback). `params` + # is already canonical-keyed (read_params_file, issue #304), so maxdecs/ + # min_nd/share_iter land here via the plain _NG_PARAMS filter -- no + # special case needed for the json schema's max_decs/min_grad_norm/ + # share_int spellings. ng_kwargs = {k: v for k, v in params.items() if k in _NG_PARAMS} - if "max_decs" in params: # json name -> AMICATorchNG's `maxdecs` - ng_kwargs["maxdecs"] = params["max_decs"] # lrate/do_mean/do_sphere/do_newton/seed/device are passed explicitly to # AMICA()/fit(); drop them from **kwargs to avoid duplicate keyword args. for k in ("lrate", "do_mean", "do_sphere", "do_newton", "seed", "device"): From 2871656ebe421b871943af3208980e85539dab9a Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Tue, 22 Sep 2026 17:48:19 -0700 Subject: [PATCH 04/26] Phase 5: guard raw backend accessors (#329) * Guard AMICATorchNG accessors on degenerate fits (#306) transform/get_*matrix/get_rho/variance_order/model_loglik/ model_probability/mir/pmi now refuse a degenerate fit or a non-finite parameter, and validate X shape like fit() does. from_state_dict wraps a malformed config's TypeError as ValueError. * Guard AMICAMLXNG accessors on degenerate fits (#306) Same guard as AMICATorchNG (backend_parity.md): transform/ get_*matrix/get_rho/variance_order/model_loglik/model_probability/ mir/pmi refuse a degenerate fit or a non-finite parameter, and validate X shape like fit() does. from_state_dict (and load(), which delegates to it) wraps a malformed config's TypeError as ValueError. * Guard AMICA_NumPy accessors on degenerate fits (#306) Same guard, ported to the legacy backend's own converged/ stop_reason bookkeeping: transform/get_weights/ get_sensor_mixing_matrix refuse a degenerate fit or a non-finite parameter, and transform validates data shape like fit() does. * Add cross-backend backend-guard tests (#306) Parametrized over torch, MLX (importorskip) and NumPy: every guarded accessor raises on a degenerate stop_reason and on a non-finite parameter, and still works on a healthy fit; every data-taking accessor rejects a 1D or wrong-channel-count array, including after a pcakeep reduction; from_state_dict raises ValueError (chaining TypeError) on a malformed config. * Document raw-backend degenerate-fit guard (#306) amica-differences.md row 5: the raw torch/MLX/NumPy backends now refuse degenerate models themselves, not just the AMICA wrapper. changelog.md: note the behavior change under Unreleased. * Add shared model_probability_from_loglik helper (#306) Factors the NaN-vs--inf diagnosis and column-wise softmax normalization out of AMICATorchNG/AMICAMLXNG.model_probability so neither backend duplicates it; lives under pamica/metrics/, matching the existing mir/pairwise_mi split (backend computes the array, a shared pure function does the rest). * Guard get_pdftype/shared_components, dedupe sweep (torch) Reviewer follow-up on #306 (PR #329): - get_pdftype/shared_components now go through _check_usable too. - New _nonfinite_params() batches the isfinite sweep into one stacked reduction (one device sync instead of one per parameter, skipping None entries), used by _check_usable, state_dict and write_amica_output instead of each repeating the sweep. - model_probability no longer runs the guard twice (its own _check_usable plus model_loglik's): the core Lht computation moved to _model_loglik_unchecked, called by both public methods after their own single guard + shape check. - model_probability's NaN-vs--inf diagnosis now delegates to the shared pamica.metrics.model_probability_from_loglik. - mir()'s Raises docstring now covers the degenerate-fit RuntimeError and the bad-shape ValueError. * Guard get_pdftype/shared_components, dedupe sweep (MLX) Same follow-up as the torch commit, ported to MLX: - get_pdftype/shared_components now go through _check_usable too. - New _nonfinite_params() batches the isfinite sweep into one stacked reduction -- one MLX graph evaluation instead of one per parameter (measured ~2.2ms -> ~0.4ms for the sweep, transform() on 512 samples ~2.5ms -> ~0.6ms), skipping None entries, used by _check_usable, state_dict and write_amica_output. - model_probability no longer runs the guard twice: the core Lht computation moved to _model_loglik_unchecked, called by both public methods after their own single guard + shape check. - model_probability delegates its NaN-vs--inf diagnosis to the shared pamica.metrics.model_probability_from_loglik. - mir()'s Raises docstring now covers the degenerate-fit RuntimeError and the bad-shape ValueError. * Name what get_weights returns in its guard message (#306) Reviewer follow-up: the action string said 'get the unmixing matrix', not what the public method itself is called. * Extend backend-guard tests: pdftype, shared_components, pca (#306) get_pdftype/shared_components added to the torch/MLX accessor table, so the existing degenerate/non-finite/healthy parametrized tests now cover them. The pcakeep channel-count test now loops over every data-taking accessor (transform/model_loglik/ model_probability/pmi), not just transform; mir is deliberately excluded there since it always rejects a pcakeep-reduced model on other grounds (rank-deficient sphere). * Test model_probability_from_loglik directly (#306) Real Lht from a real 2-model fit's model_loglik() on the bundled sample: a healthy Lht's columns sum to 1 and match the backend's own model_probability() (behavior-preserving); a column poisoned entirely to -inf and a single entry poisoned to NaN raise distinct messages. * List get_pdftype/shared_components in the changelog entry (#306) The Unreleased entry for the backend-guard behavior change now names the two accessors added in the PR #329 review round. --- docs/changelog.md | 23 ++ docs/guides/amica-differences.md | 4 +- pamica/metrics/__init__.py | 8 +- pamica/metrics/posterior.py | 60 +++++ pamica/mlx_impl/core.py | 189 +++++++++++--- pamica/numpy_impl/core.py | 55 +++++ pamica/tests/test_backend_guards.py | 327 +++++++++++++++++++++++++ pamica/tests/test_metrics_posterior.py | 105 ++++++++ pamica/torch_impl/core.py | 194 ++++++++++++--- 9 files changed, 882 insertions(+), 83 deletions(-) create mode 100644 pamica/metrics/posterior.py create mode 100644 pamica/tests/test_backend_guards.py create mode 100644 pamica/tests/test_metrics_posterior.py diff --git a/docs/changelog.md b/docs/changelog.md index 8e17336a..4f51f807 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,29 @@ Release notes are also published on the ## Unreleased +- **Raw backend accessors now guard against degenerate fits and bad input shape** (issue #306, epic #324 Phase 5). + **Behavior change:** + `AMICATorchNG`, `AMICAMLXNG` and the legacy NumPy `AMICA` backend's fitted-output accessors + (`transform`, `get_mixing_matrix`, `get_unmixing_matrix`, `get_sensor_mixing_matrix`, `get_rho`, + `get_pdftype`, `shared_components`, `variance_order`, `model_loglik`, `model_probability`, + `mir` and `pmi` on torch/MLX; + `transform`, `get_weights` and `get_sensor_mixing_matrix` on NumPy) + now raise `RuntimeError` when called on a fit the backend itself classified as degenerate, + or when a fitted parameter holds a non-finite value, + instead of silently returning NaN-tainted output. + The accessors that take data (`transform`, `model_loglik`, `model_probability`, `mir` and `pmi`) + now also validate that the input is a 2D array with the model's fitted input channel count, + raising the same named `ValueError` that `fit()` already raises for the identical mistake, + instead of a raw matmul/broadcast error. + `model_probability` also now tells apart a NaN log-likelihood (numerical corruption) + from every model underflowing to `-inf` (an extreme outlier), where it previously reported both the same way. + `AMICATorchNG.from_state_dict` and `AMICAMLXNG.from_state_dict` + now raise `ValueError` naming the payload as the culprit when the saved config does not match the constructor, + such as a missing or unexpected key, + chaining the original `TypeError` instead of letting it propagate bare. + This closes the gap the `AMICA` wrapper's own degenerate-fit guard (issue #50) never covered: + a caller using a raw backend directly now gets the same protection. + See `docs/guides/amica-differences.md` row 5. - **`AMICAICA.apply` restores the PCA residual of rank-reduced fits** (issue #322, epic #324). **Behavior change for `pcakeep`/`pcadb` and rank-deficient fits:** `AMICAICA.fit` now computes the full orthonormal PCA basis once diff --git a/docs/guides/amica-differences.md b/docs/guides/amica-differences.md index 2c39dbb6..ed11bed0 100644 --- a/docs/guides/amica-differences.md +++ b/docs/guides/amica-differences.md @@ -16,7 +16,7 @@ that is not listed, that is a bug worth | 2 | Zero numerical rank | `numeigs = 0`, continues | `ValueError` naming cause and fix | fitting a zero-dimensional model is not a recoverable state | — (no reason to want it) | | 3 | Returned iterate | last EM iterate | highest-likelihood iterate (`keep_best`) | the lrate schedule is non-monotone; late Newton overshoots cut LL variance 12.7x → 2.0x | `keep_best=False` | | 4 | Newton | on (`do_newton=1`) | off | isolates the algorithm from initialization for parity work | `do_newton=True` | -| 5 | Degenerate fits | returns NaN sources, and writes them out on its `writestep` cadence | the `AMICA` wrapper refuses `transform`/`get_*`/`save` (the raw `AMICATorchNG` backend has no such guard of its own — tracked as issue #306); NumPy reports `converged=False` with a `stop_reason`, refuses the final write, and skips each periodic checkpoint with a logged reason (leaving the last valid one on disk) | NaN sources silently poison downstream analysis, and `loadmodout` reads a NaN checkpoint back without complaint | — (see issues #50 and #240) | +| 5 | Degenerate fits | returns NaN sources, and writes them out on its `writestep` cadence | the `AMICA` wrapper refuses `transform`/`get_*`/`save`; the raw `AMICATorchNG`, `AMICAMLXNG` and NumPy `AMICA` backends now refuse their own output accessors on a degenerate fit too (issue #306), not just the wrapper; NumPy additionally reports `converged=False` with a `stop_reason`, refuses the final write, and skips each periodic checkpoint with a logged reason (leaving the last valid one on disk) | NaN sources silently poison downstream analysis, and `loadmodout` reads a NaN checkpoint back without complaint | — (see issues #50, #240 and #306) | | 6 | Precision | float64 | float64 (float32 on Apple GPUs) | Apple GPUs have no float64; float32 agrees to ~7 significant digits, not bit-parity | `dtype=torch.float64` | | 7 | Sensor-space maps | `Spinv` applied internally | `get_sensor_mixing_matrix()` | `get_mixing_matrix()` returns sphered-space `A`; switching its meaning by data conditioning would be worse | — | | 8 | Columns merged away by `share_comps` | updated to NaN, then hidden by the `comp_used` mask | frozen at their last finite value (never divided) | a fit must not end holding NaN parameters, mask or no mask; the columns are dead either way | — (see issues #60, #240) | @@ -26,7 +26,7 @@ that is not listed, that is a bug worth | 12 | `pcadb` | parsed (amica15.f90:3459-3461), never used | unset by default; when set alone, keeps the dimensions within `pcadb` dB of the largest eigenvalue; ignored when `pcakeep` is also set | a dB cut is a scale-free way to drop low-variance directions; letting `pcakeep` win preserves what a reference `input.param` that sets both (both bundled files do) means to the binary | leave `pcadb` unset (the default), or set `pcakeep` | Rows 1, 2 and 7 arrived with [ADR 0004](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0004-rank-deficient-input-handling.md); -row 3 with ADR 0003; row 5 with issue #50; row 8 with issues #60 and #240; +row 3 with ADR 0003; row 5 with issue #50, extended to the raw backends by issue #306; row 8 with issues #60 and #240; row 9 with issue #232; row 10 with issue #198; row 11 with issue #322 (ADR 0005); row 12 with issue #323. diff --git a/pamica/metrics/__init__.py b/pamica/metrics/__init__.py index 2cbc8480..a4093d95 100644 --- a/pamica/metrics/__init__.py +++ b/pamica/metrics/__init__.py @@ -1,4 +1,10 @@ from .mir import mir from .pmi import block_diagonal_order, pairwise_mi +from .posterior import model_probability_from_loglik -__all__ = ["mir", "pairwise_mi", "block_diagonal_order"] +__all__ = [ + "mir", + "pairwise_mi", + "block_diagonal_order", + "model_probability_from_loglik", +] diff --git a/pamica/metrics/posterior.py b/pamica/metrics/posterior.py new file mode 100644 index 00000000..744cbda4 --- /dev/null +++ b/pamica/metrics/posterior.py @@ -0,0 +1,60 @@ +"""Shared multi-model posterior helper (issue #141; PR #329 review of #306). + +Factored out of ``AMICATorchNG.model_probability``/``AMICAMLXNG. +model_probability`` so the NaN-vs-``-inf`` diagnosis and the column-wise +softmax normalization are not duplicated: both backends compute ``Lht`` (the +per-model, per-sample log-likelihood) themselves, in whatever array library +they use, then hand the resulting numpy array to this one pure function -- +the same pattern already established by ``pamica.metrics.mir``/ +``pairwise_mi``, which the backends also call after doing their own +backend-specific setup. +""" + +import numpy as np + + +def model_probability_from_loglik(Lht: np.ndarray, *, caller: str) -> np.ndarray: + """Column-wise softmax over models of ``Lht`` (model dominance), i.e. + ``P(model h | x_t)``; each column sums to 1. All ones for a single model. + + Parameters + ---------- + Lht : np.ndarray of shape (n_models, n_samples) + Per-model, per-sample log-likelihood, e.g. from + ``AMICATorchNG.model_loglik``/``AMICAMLXNG.model_loglik``. + caller : str + Prefix for the error message, so the raised exception still names + which backend/method it came from (e.g. + ``"AMICATorchNG.model_probability()"``). + + Returns + ------- + prob : np.ndarray of shape (n_models, n_samples) + + Raises + ------ + ValueError + If a log-likelihood is NaN (numerical corruption) at some sample, or + if every model underflows to ``-inf`` at some sample (the posterior + is undefined there). The two are diagnosed and reported separately: + NaN and ``-inf`` are different failure modes, and reporting both + under one "-inf" message misdiagnoses the NaN case (PR #311 review + scope extension, issue #306). + """ + col_max = Lht.max(axis=0, keepdims=True) + if not np.isfinite(col_max).all(): + nan_mask = np.isnan(col_max) + if nan_mask.any(): + raise ValueError( + f"{caller}: {int(nan_mask.sum())} sample(s) have a NaN " + "log-likelihood (numerical corruption), so the posterior is " + "undefined there." + ) + n_bad = int((~np.isfinite(col_max)).sum()) + raise ValueError( + f"{caller}: every model has -inf log-likelihood at {n_bad} " + "sample(s), so the posterior is undefined there (an extreme " + "outlier under a tight source density)." + ) + ex = np.exp(Lht - col_max) + return ex / ex.sum(axis=0, keepdims=True) diff --git a/pamica/mlx_impl/core.py b/pamica/mlx_impl/core.py index 19d0ca59..3c90d902 100644 --- a/pamica/mlx_impl/core.py +++ b/pamica/mlx_impl/core.py @@ -104,7 +104,7 @@ from .. import blocktune from .. import restarts from ..metrics import mir as mir_metric -from ..metrics import pairwise_mi +from ..metrics import model_probability_from_loglik, pairwise_mi from ..numpy_impl.utils import identify_shared_components from ..rank import ( MINEIG, @@ -2265,6 +2265,7 @@ def shared_components(self) -> list: "AMICAMLXNG.shared_components() requires a fitted model; call " "fit() first." ) + self._check_usable("get the shared components") cl = np.array(self.comp_list) # (n_channels, n_models) groups = [] for col in np.unique(cl): @@ -3069,6 +3070,8 @@ def transform(self, X: np.ndarray, model_idx: int = 0) -> np.ndarray: "AMICAMLXNG.transform() requires a fitted model; call fit() first." ) self._check_model_idx(model_idx) + self._check_usable("transform") + self._check_input_shape(X) X_arr = mx.array(np.ascontiguousarray(X).astype(np.float32)) X_t = self.sphere @ (X_arr - self.mean) S = self.W[model_idx].T @ (X_t - self.c[:, model_idx : model_idx + 1]) @@ -3092,6 +3095,72 @@ def _check_model_idx(self, model_idx: int) -> None: f"fit (valid: 0..{self.n_models - 1})." ) + def _nonfinite_params(self) -> list: + """Names of :attr:`_PARAM_ARRAYS` currently holding a non-finite + value (matching the legacy NumPy backend's own + ``_nonfinite_params``; issue #306 cross-backend review; port of + ``AMICATorchNG._nonfinite_params``). Parameters not yet allocated + (``None``, e.g. a partially initialized instance) are skipped rather + than treated as bad. + + Single-sync fast path: every parameter's ``isfinite().all()`` is + stacked into one small array and reduced with exactly one + ``.item()`` MLX graph evaluation, instead of one evaluation per + parameter -- MLX is lazy, so each ``bool(mx.array)``/``.item()`` + forces the whole pending graph to materialize on the accelerator, + making the naive per-parameter loop essentially the entire cost of a + small ``transform()`` call (measured ~2.2 ms across a 12-array + sweep, PR #329 review). The per-parameter breakdown -- one further, + small evaluation -- is only computed once that reduction is already + ``False``. + """ + names = [name for name in self._PARAM_ARRAYS if getattr(self, name) is not None] + if not names: + return [] + flags = mx.stack([mx.all(mx.isfinite(getattr(self, name))) for name in names]) + if bool(mx.all(flags).item()): + return [] + bad = (~flags).tolist() + return [name for name, is_bad in zip(names, bad) if is_bad] + + def _check_usable(self, action: str) -> None: + """Refuse to serve output from a degenerate fit (issue #306; port of + ``AMICATorchNG._check_usable``). + + Callers first check their own unfitted marker(s) and raise the + existing ``requires a fitted model`` ``RuntimeError`` (unchanged); + this assumes a fit has actually run and adds the two layers + :meth:`state_dict`/:meth:`write_amica_output` already use beyond + that: the ``stop_reason`` gate, then a defense-in-depth isfinite + sweep via :meth:`_nonfinite_params`. Mirrors the + :class:`~pamica.AMICA` wrapper's ``_check_usable`` (issue #50) for + callers using this backend directly. + """ + if self.stop_reason in self._DEGENERATE_STOP_REASONS: + raise RuntimeError( + f"Refusing to {action}: fit ended degenerate (stop_reason=" + f"{self.stop_reason!r}), so the model holds non-finite " + f"parameters and would produce NaN output. Lower lrate, " + f"disable Newton, or check data conditioning, then refit." + ) + nonfinite = self._nonfinite_params() + if nonfinite: + raise RuntimeError( + f"Refusing to {action}: parameters {nonfinite} hold " + f"non-finite values (stop_reason={self.stop_reason!r})." + ) + + def _check_input_shape(self, X: np.ndarray) -> None: + """Validate a data array against the fitted input channel count, + mirroring :meth:`fit`'s own ``X`` validation (issue #306; port of + ``AMICATorchNG._check_input_shape``).""" + if X.ndim != 2: + raise ValueError(f"X must be 2D (n_channels, n_samples), got {X.shape}") + if X.shape[0] != self.n_channels_in: + raise ValueError( + f"X has {X.shape[0]} channels, model expects {self.n_channels_in}" + ) + def get_pdftype(self, model_idx: int = 0) -> np.ndarray: """Per-source density-family code for model ``model_idx`` (AMICATorchNG ``get_pdftype``). @@ -3112,6 +3181,7 @@ def get_pdftype(self, model_idx: int = 0) -> np.ndarray: "AMICAMLXNG.get_pdftype() requires a fitted model; call fit() first." ) self._check_model_idx(model_idx) + self._check_usable("get the density family") codes = np.array(self.pdtype[:, model_idx], dtype=np.int64) # Silent-failure guard: an out-of-range stored code would otherwise # fall through the _score/_log_pdf mx.where chain to a stale GG @@ -3134,6 +3204,7 @@ def get_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: "fit() first." ) self._check_model_idx(model_idx) + self._check_usable("get the mixing matrix") return np.array(self.A[:, self.comp_list[:, model_idx]].T) @property @@ -3171,6 +3242,7 @@ def get_sensor_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: "model; call fit() first." ) self._check_model_idx(model_idx) + self._check_usable("get the sensor mixing matrix") A = np.array(self.A[:, self.comp_list[:, model_idx]].T, dtype=np.float64) return self._pinv_sphere() @ A @@ -3185,6 +3257,7 @@ def get_unmixing_matrix(self, model_idx: int = 0) -> np.ndarray: "call fit() first." ) self._check_model_idx(model_idx) + self._check_usable("get the unmixing matrix") return np.array(self.W[model_idx].T) def get_rho(self, model_idx: int = 0) -> np.ndarray: @@ -3206,15 +3279,12 @@ def get_rho(self, model_idx: int = 0) -> np.ndarray: "AMICAMLXNG.get_rho() requires a fitted model; call fit() first." ) self._check_model_idx(model_idx) - # Defense-in-depth, matching state_dict()'s isfinite sweep: a - # degenerate multi-model fit can leave one model's rho non-finite - # without the aggregate LL tripping nan_ll. Refuse rather than return - # a silent NaN. - if not bool(mx.all(mx.isfinite(self.rho)).item()): - raise RuntimeError( - "AMICAMLXNG.get_rho(): rho holds non-finite values (a " - "degenerate fit); inspect stop_reason and refit." - ) + # Folded into the shared guard (issue #306): a degenerate multi-model + # fit can leave one model's rho non-finite without the aggregate LL + # tripping a _DEGENERATE_STOP_REASONS marker, which _check_usable's + # defense-in-depth isfinite sweep over _PARAM_ARRAYS (rho included) + # still catches. Refuse rather than return a silent NaN. + self._check_usable("get rho") idx = self.comp_list[:, model_idx] return np.array(self.rho[:, idx]) @@ -3272,6 +3342,7 @@ def variance_order( "AMICAMLXNG.variance_order() requires a fitted model; call fit() first." ) self._check_model_idx(model_idx) + self._check_usable("compute the variance order") cl = self.comp_list[:, model_idx] alpha = np.array(self.alpha[:, cl], dtype=np.float64) mu = np.array(self.mu[:, cl], dtype=np.float64) @@ -3361,9 +3432,11 @@ def mir( Raises ------ RuntimeError - If the model is unfitted. + If the model is unfitted, or the fit ended degenerate (issue + #306). ValueError - If the fitted sphere is rank-reduced (non-square): whether from + If ``X`` is not a 2D array of the fitted input channel count, or + if the fitted sphere is rank-reduced (non-square): whether from explicit ``pcakeep``/``pcadb`` or from automatic ``mineig``/ ``mineig_rel`` numerical-rank detection, the sphere is rank-deficient, so MIR's log-Jacobian term is undefined @@ -3374,6 +3447,8 @@ def mir( "AMICAMLXNG.mir() requires a fitted model; call fit() first." ) self._check_model_idx(model_idx) + self._check_usable("compute MIR") + self._check_input_shape(X) if self._pca_reduced(): raise ValueError( "mir() is incompatible with PCA reduction: the fitted " @@ -3411,7 +3486,11 @@ def pmi( Raises ------ RuntimeError - If the model is unfitted (via :meth:`transform`). + If the model is unfitted, or the fit ended degenerate (issue + #306), both via :meth:`transform`. + ValueError + If ``X`` is not a 2D array of the fitted input channel count + (via :meth:`transform`). """ return pairwise_mi(self.transform(X, model_idx=model_idx), nbins) @@ -3450,14 +3529,30 @@ def model_loglik(self, X: np.ndarray) -> np.ndarray: Raises ------ RuntimeError - If the model is unfitted. + If the model is unfitted, or the fit ended degenerate (issue + #306). ValueError - If ``X`` contains non-finite (NaN/Inf) values. + If ``X`` is not a 2D array of the fitted input channel count, or + contains non-finite (NaN/Inf) values. """ if self.sphere is None or self.mean is None or self.W is None: raise RuntimeError( "AMICAMLXNG.model_loglik() requires a fitted model; call fit() first." ) + self._check_usable("compute the model log-likelihood") + self._check_input_shape(X) + return self._model_loglik_unchecked(X) + + def _model_loglik_unchecked(self, X: np.ndarray) -> np.ndarray: + """Core ``Lht`` computation for :meth:`model_loglik`, with no + degenerate-fit guard or shape validation of its own (issue #306 PR + #329 review; port of ``AMICATorchNG._model_loglik_unchecked``): + :meth:`model_loglik` and :meth:`model_probability` each do their own + single guard + shape check, with their own action wording, then + both call this -- so the guard no longer runs twice on a + :meth:`model_probability` call, which used to run its own + ``_check_usable`` and then :meth:`model_loglik`'s (measured ~2x the + cost of a single guard evaluation on this backend).""" X = np.ascontiguousarray(X) if not np.isfinite(X).all(): bad = np.flatnonzero(~np.isfinite(X).all(axis=1)) @@ -3495,24 +3590,32 @@ def model_probability(self, X: np.ndarray) -> np.ndarray: Raises ------ RuntimeError - If the model is unfitted. + If the model is unfitted, or the fit ended degenerate (issue + #306). ValueError - If ``X`` is non-finite, or if every model underflows to ``-inf`` + If ``X`` is not a 2D array of the fitted input channel count, if + ``X`` is non-finite, if every model underflows to ``-inf`` log-likelihood at some sample (the posterior is undefined - there). + there), or if a log-likelihood is NaN (numerical corruption, + distinct from the ``-inf`` underflow case above). """ - Lht = self.model_loglik(X) - col_max = Lht.max(axis=0, keepdims=True) - if not np.isfinite(col_max).all(): - n_bad = int((~np.isfinite(col_max)).sum()) - raise ValueError( - f"AMICAMLXNG.model_probability(): every model has -inf " - f"log-likelihood at {n_bad} sample(s), so the posterior is " - "undefined there (an extreme outlier under a tight source " - "density)." + if self.sphere is None or self.mean is None or self.W is None: + raise RuntimeError( + "AMICAMLXNG.model_probability() requires a fitted model; " + "call fit() first." ) - ex = np.exp(Lht - col_max) - return ex / ex.sum(axis=0, keepdims=True) + self._check_usable("compute the model probability") + self._check_input_shape(X) + Lht = self._model_loglik_unchecked(X) + # NaN and -inf are different failure modes and must not share a + # message: -inf is every model underflowing at a real sample (an + # extreme outlier), while NaN is numerical corruption. isfinite alone + # conflates them (PR #311 review scope extension, issue #306). The + # diagnosis + normalization is shared with AMICATorchNG (PR #329 + # review) rather than duplicated per backend. + return model_probability_from_loglik( + Lht, caller="AMICAMLXNG.model_probability()" + ) # ------------------------------------------------------------------ # EEGLAB export (issue #92; epic #278 Phase 3/#289 port of @@ -3578,11 +3681,7 @@ def write_amica_output(self, outdir) -> None: # neutralizes a stale LLt stash: a failed final iteration's # _llt_lht/_llt_lt (from before the nan_params/nan_ll break) can no # longer reach disk once this guard refuses the write outright. - nonfinite = [ - name - for name in self._PARAM_ARRAYS - if not bool(mx.all(mx.isfinite(getattr(self, name))).item()) - ] + nonfinite = self._nonfinite_params() if nonfinite: raise RuntimeError( f"Refusing to write output for a model with non-finite " @@ -3722,11 +3821,7 @@ def state_dict(self) -> dict: # bookkeeping ever misses it (the codebase has known NaN-suppression # risks). isfinite on the integer comp_list/pdtype is trivially # all-True. - nonfinite = [ - name - for name in self._PARAM_ARRAYS - if not bool(mx.all(mx.isfinite(getattr(self, name))).item()) - ] + nonfinite = self._nonfinite_params() if nonfinite: raise RuntimeError( f"Refusing to serialize a model with non-finite parameters " @@ -3884,7 +3979,21 @@ def from_state_dict(cls, state: dict) -> "AMICAMLXNG": f"(format_version={version}); the payload may be truncated." ) config = dict(state["config"]) - obj = cls(**config) + # A missing/unexpected key in a malformed or foreign-version payload + # surfaces as a bare TypeError from the constructor call; every other + # validation step in this method already names the payload as the + # culprit with a ValueError, so wrap this one the same way instead of + # letting a mismatched-keyword TypeError propagate unexplained + # (issue #306; :meth:`load`'s .npz path delegates to this method, so + # it is covered too). + try: + obj = cls(**config) + except TypeError as exc: + raise ValueError( + f"malformed AMICAMLXNG state: config does not match the " + f"AMICAMLXNG constructor ({exc}); the payload may be " + "truncated or from an incompatible version." + ) from exc obj._load_params(state) return obj diff --git a/pamica/numpy_impl/core.py b/pamica/numpy_impl/core.py index 1ae1ff38..1290b914 100644 --- a/pamica/numpy_impl/core.py +++ b/pamica/numpy_impl/core.py @@ -704,6 +704,50 @@ def _pinv_sphere(self) -> np.ndarray: self._sphere_pinv = np.linalg.pinv(self.sphere) return self._sphere_pinv + def _check_usable(self, action: str) -> None: + """Refuse to serve output from a degenerate fit (issue #306; port of + ``AMICATorchNG._check_usable`` for the legacy NumPy backend). + + Callers first check their own unfitted marker(s) and raise the + existing ``"Model has not been fitted yet"`` ``RuntimeError`` + (unchanged); this assumes a fit has actually run. NumPy's single + ``converged`` flag already folds together every degenerate cause + (non-finite LL, non-finite exit parameters, or a restart error, see + :meth:`_fit_once`) that the PyTorch/MLX backends track via + ``stop_reason``/``_DEGENERATE_STOP_REASONS``, so the equivalent gate + here is simply ``not self.converged``; the defense-in-depth isfinite + sweep reuses :meth:`_nonfinite_params`, the same predicate ``fit()``'s + own outcome bookkeeping and every checkpoint write already trust. + """ + if not self.converged: + raise RuntimeError( + f"Refusing to {action}: fit ended degenerate (stop_reason=" + f"{self.stop_reason!r}), so the model holds non-finite " + f"parameters and would produce NaN output. Lower lrate, " + f"disable Newton, or check data conditioning, then refit." + ) + nonfinite = self._nonfinite_params() + if nonfinite: + raise RuntimeError( + f"Refusing to {action}: parameters {nonfinite} hold " + f"non-finite values (stop_reason={self.stop_reason!r})." + ) + + def _check_input_shape(self, data: np.ndarray) -> None: + """Validate a data array against the fitted input channel count, + mirroring :meth:`fit`'s own ``data`` validation (issue #306): a raw + matmul/broadcast error deep inside a method is less useful than this + named ``ValueError`` at the entry point.""" + if data.ndim != 2: + raise ValueError( + f"data must be a 2D array (n_channels, n_samples), got shape " + f"{data.shape}" + ) + if data.shape[0] != self.data_dim_in: + raise ValueError( + f"data has {data.shape[0]} channels, model expects {self.data_dim_in}" + ) + def get_sensor_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: """Mixing matrix mapped back to input-channel space. @@ -714,6 +758,7 @@ def get_sensor_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: """ if self.sphere is None or self.A is None or self.comp_list is None: raise RuntimeError("Model has not been fitted yet; call fit() first.") + self._check_usable("get the sensor mixing matrix") A = self.A[:, self.comp_list[:, model_idx]] return self._pinv_sphere() @ A @@ -728,6 +773,7 @@ def get_weights(self) -> np.ndarray: """ if self.W is None: raise RuntimeError("Model has not been fitted yet; call fit() first.") + self._check_usable("get the weights (unmixing matrix)") # Internal W = inv(A) is stored transposed relative to the true unmixing # (the E-step forms activations as (X-c)^T @ W), so return W^T (issue #24). # This is the raw unmixing matrix; it does not account for the per-model @@ -2693,9 +2739,18 @@ def transform(self, data: np.ndarray) -> np.ndarray: ------- S : ndarray of shape (n_components, n_samples, n_models) The unmixed sources for each model + + Raises + ------ + RuntimeError + If the model is unfitted, or the fit ended degenerate (issue #306). + ValueError + If ``data`` is not a 2D array of the fitted input channel count. """ if self.W is None or self.comp_list is None or self.c is None: raise RuntimeError("Model has not been fitted yet; call fit() first.") + self._check_usable("transform") + self._check_input_shape(data) if self.mean is not None: data = data - self.mean diff --git a/pamica/tests/test_backend_guards.py b/pamica/tests/test_backend_guards.py new file mode 100644 index 00000000..bad64115 --- /dev/null +++ b/pamica/tests/test_backend_guards.py @@ -0,0 +1,327 @@ +"""Cross-backend guard tests for degenerate-fit output and bad-shape input +(issue #306, epic #324 Phase 5). + +Three gaps existed identically in ``AMICATorchNG``, ``AMICAMLXNG`` and the +legacy NumPy ``AMICA`` backend, so per ``.rules/backend_parity.md`` they are +fixed and tested together in one module rather than three: + +1. Fitted-output accessors (``transform``, ``get_mixing_matrix``, + ``get_unmixing_matrix``, ``get_sensor_mixing_matrix``, ``get_rho``, + ``variance_order``, ``model_loglik``, ``model_probability``, ``mir``, + ``pmi`` on torch/MLX; ``transform``, ``get_weights``, + ``get_sensor_mixing_matrix`` on NumPy) used to return numeric-looking + output from a fit the backend itself classified as degenerate, with no + warning. They now raise ``RuntimeError``, mirroring the two-layer pattern + ``state_dict()``/``write_amica_output()`` already used: a ``stop_reason`` + gate, then a defense-in-depth isfinite sweep over the fitted parameters. +2. The data-taking accessors among those (``transform``, ``model_loglik``, + ``model_probability``, ``mir``, ``pmi`` on torch/MLX; ``transform`` on + NumPy) did not validate ``X``/``data`` the way ``fit()`` does, so a + wrong-shaped array failed with a raw matmul/broadcast error instead of the + same named ``ValueError`` ``fit()`` gives for the identical mistake. +3. ``from_state_dict``'s ``cls(**config)`` surfaced a malformed (missing or + unexpected key) config as a bare ``TypeError`` instead of a named + ``ValueError`` naming the payload, like every neighboring validation step + in that method. + +Real bundled sample EEG only (32 channels x 30504 frames), sliced to a few +thousand samples and 2 iterations to keep fits fast -- these are guard/ +validation tests, not convergence tests (``.rules/testing.md``). Degenerate +state is produced the established way (``pamica/tests/torch_tests/ +test_ng_backend.py`` ~1078-1113, ``test_ng_exception_consistency.py`` +~127-130): fit for real, then set ``stop_reason``/``converged`` directly or +poison one parameter entry with NaN, rather than trying to drive a real fit +into divergence. +""" + +from pathlib import Path +from typing import Callable, List, Tuple + +import numpy as np +import pytest +import torch + +from pamica.numpy_impl.core import AMICA as AMICA_NumPy +from pamica.torch_impl import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +NMIX = 3 +SEED = 42 +# Sliced for speed (module docstring): guard/validation tests, not +# convergence tests. +N_SAMPLES = 2048 +PCAKEEP = 20 + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +def _mlx_core(): + """The MLX backend module, or skip this test (never the whole module).""" + return pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + + +@pytest.fixture(scope="module") +def real_data() -> np.ndarray: + return load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD).astype( + np.float64 + )[:, :N_SAMPLES] + + +def _fit_torch(data: np.ndarray, **kwargs) -> AMICATorchNG: + m = AMICATorchNG( + n_channels=NW, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + block_size=256, + **kwargs, + ) + m.fit(data, max_iter=2, verbose=False) + return m + + +def _fit_mlx(data: np.ndarray, **kwargs): + AMICAMLXNG = _mlx_core().AMICAMLXNG + m = AMICAMLXNG(n_channels=NW, n_mix=NMIX, seed=SEED, block_size=256, **kwargs) + m.fit(data.astype(np.float32), max_iter=2, verbose=False) + return m + + +def _fit_numpy(data: np.ndarray, tmp_path: Path, **kwargs) -> AMICA_NumPy: + m = AMICA_NumPy( + num_models=1, + num_mix=NMIX, + max_iter=2, + seed=SEED, + block_size=256, + do_newton=False, + use_tqdm=False, + outdir=str(tmp_path / "numpy_out"), + **kwargs, + ) + m.fit(data) + return m + + +# (name, fn(model, X) -> result, is_data_accessor). ``is_data_accessor`` marks +# the methods that take an ``X``/``data`` array and so are covered by item 2's +# shape validation; the rest only take ``model_idx``. +Accessor = Tuple[str, Callable, bool] + +_TORCH_MLX_ACCESSORS: List[Accessor] = [ + ("transform", lambda m, X: m.transform(X), True), + ("get_mixing_matrix", lambda m, X: m.get_mixing_matrix(), False), + ("get_unmixing_matrix", lambda m, X: m.get_unmixing_matrix(), False), + ("get_sensor_mixing_matrix", lambda m, X: m.get_sensor_mixing_matrix(), False), + ("get_rho", lambda m, X: m.get_rho(), False), + ("get_pdftype", lambda m, X: m.get_pdftype(), False), + ("shared_components", lambda m, X: m.shared_components(), False), + ("variance_order", lambda m, X: m.variance_order(), False), + ("model_loglik", lambda m, X: m.model_loglik(X), True), + ("model_probability", lambda m, X: m.model_probability(X), True), + ("mir", lambda m, X: m.mir(X), True), + ("pmi", lambda m, X: m.pmi(X), True), +] + +_NUMPY_ACCESSORS: List[Accessor] = [ + ("transform", lambda m, X: m.transform(X), True), + ("get_weights", lambda m, X: m.get_weights(), False), + ("get_sensor_mixing_matrix", lambda m, X: m.get_sensor_mixing_matrix(), False), +] + +_BACKENDS = ["torch", "mlx", "numpy"] + + +def _build(backend: str, real_data: np.ndarray, tmp_path: Path, **kwargs): + """Fit a fresh small model on ``real_data`` and return + ``(model, accessors)`` for the requested backend.""" + if backend == "torch": + return _fit_torch(real_data, **kwargs), _TORCH_MLX_ACCESSORS + if backend == "mlx": + return _fit_mlx(real_data, **kwargs), _TORCH_MLX_ACCESSORS + if backend == "numpy": + return _fit_numpy(real_data, tmp_path, **kwargs), _NUMPY_ACCESSORS + raise ValueError(backend) # pragma: no cover - parametrize typo guard + + +def _poison_a00(backend: str, m) -> None: + """NaN-poison a single entry of the fitted mixing matrix ``A`` in place, + the sanctioned direct-poisoning pattern (``.rules/testing.md``).""" + if backend == "mlx": + mx = _mlx_core().mx + m.A[0, 0] = mx.array(float("nan")) + else: + m.A[0, 0] = float("nan") + + +# --------------------------------------------------------------------- +# Item 1: degenerate-fit guard on every fitted-output accessor +# --------------------------------------------------------------------- + + +@pytest.mark.parametrize("backend", _BACKENDS) +def test_guarded_accessors_work_on_a_healthy_fit(backend, real_data, tmp_path): + m, accessors = _build(backend, real_data, tmp_path) + for _name, fn, _is_data in accessors: + assert fn(m, real_data) is not None + + +@pytest.mark.parametrize("backend", _BACKENDS) +def test_guarded_accessors_refuse_degenerate_stop_reason(backend, real_data, tmp_path): + m, accessors = _build(backend, real_data, tmp_path) + + # A real divergence sets this marker (and leaves NaNs in the params); + # assert the guard fires on the marker deterministically, without + # needing to induce an actual blow-up (established pattern, see module + # docstring). + if backend in ("torch", "mlx"): + m.stop_reason = "nan_ll" + else: + # NumPy's single ``converged`` flag is its degenerate marker; a real + # divergence sets both it and a descriptive stop_reason together. + m.converged = False + m.stop_reason = "Non-finite likelihood (NaN/-inf) encountered" + + for name, fn, _is_data in accessors: + with pytest.raises(RuntimeError, match="degenerate"): + fn(m, real_data) + + +@pytest.mark.parametrize("backend", _BACKENDS) +def test_guarded_accessors_refuse_nonfinite_param(backend, real_data, tmp_path): + """Defense-in-depth: even with a non-degenerate stop_reason/converged + state, a non-finite parameter blocks every guarded accessor.""" + m, accessors = _build(backend, real_data, tmp_path) + + if backend in ("torch", "mlx"): + m.stop_reason = "max_iter" # not a degenerate marker + else: + m.converged = True + m.stop_reason = "max_iter reached" + assert m.A is not None + _poison_a00(backend, m) + + for name, fn, _is_data in accessors: + with pytest.raises(RuntimeError, match="non-finite"): + fn(m, real_data) + + +# --------------------------------------------------------------------- +# Item 2: input-shape validation on the data-taking accessors +# --------------------------------------------------------------------- + + +@pytest.mark.parametrize("backend", _BACKENDS) +def test_data_accessors_reject_1d_and_wrong_channel_count(backend, real_data, tmp_path): + m, accessors = _build(backend, real_data, tmp_path) + data_accessors = [(name, fn) for name, fn, is_data in accessors if is_data] + assert data_accessors, "expected at least one data-taking accessor" + + bad_1d = real_data[0] + bad_channels = real_data[:20] # fitted on 32 channels + + for name, fn in data_accessors: + with pytest.raises(ValueError): + fn(m, bad_1d) + with pytest.raises(ValueError): + fn(m, bad_channels) + + +# mir() is deliberately excluded here: it raises ValueError for ANY +# pcakeep-reduced model (its rank-deficient-sphere guard, unrelated to +# channel-count validation), so a "good" 32-channel input would never +# return a value to assert on. transform/model_loglik/model_probability/pmi +# all still validate the input shape ahead of any PCA-specific check. +_PCA_CHANNEL_CHECK_NAMES = ("transform", "model_loglik", "model_probability", "pmi") + + +@pytest.mark.parametrize("backend", _BACKENDS) +def test_channel_count_follows_n_channels_in_after_pca(backend, real_data, tmp_path): + """After a ``pcakeep=20`` fit on 32 channels, the model's fitted rank is + 20 but its input channel count stays 32: every data-taking accessor + (transform/model_loglik/model_probability/pmi) must accept a 32-channel + array and reject a 20-channel one (the ``n_channels_in`` rule).""" + m, accessors = _build(backend, real_data, tmp_path, pcakeep=PCAKEEP) + data_accessors = [ + (name, fn) + for name, fn, is_data in accessors + if is_data and name in _PCA_CHANNEL_CHECK_NAMES + ] + assert data_accessors + + if backend == "torch": + assert m.n_channels_in == NW + assert m.n_channels == PCAKEEP + good, bad = real_data, real_data[:PCAKEEP] + elif backend == "mlx": + assert m.n_channels_in == NW + assert m.n_channels == PCAKEEP + good = real_data.astype(np.float32) + bad = real_data[:PCAKEEP].astype(np.float32) + else: + assert m.data_dim_in == NW + assert m.data_dim == PCAKEEP + good, bad = real_data, real_data[:PCAKEEP] + + for name, fn in data_accessors: + assert fn(m, good) is not None + with pytest.raises(ValueError, match="channels"): + fn(m, bad) + + +# --------------------------------------------------------------------- +# Item 3: from_state_dict surfaces a malformed config as a named ValueError +# --------------------------------------------------------------------- + + +def test_torch_from_state_dict_unexpected_key_raises_value_error(real_data): + m = _fit_torch(real_data) + state = m.state_dict() + bad_state = {**state, "config": {**state["config"], "bogus_key_xyz": 123}} + + with pytest.raises(ValueError, match="malformed AMICATorchNG state") as excinfo: + AMICATorchNG.from_state_dict(bad_state) + assert isinstance(excinfo.value.__cause__, TypeError) + + +def test_torch_from_state_dict_missing_key_raises_value_error(real_data): + m = _fit_torch(real_data) + state = m.state_dict() + config = dict(state["config"]) + del config["n_channels"] + bad_state = {**state, "config": config} + + with pytest.raises(ValueError, match="malformed AMICATorchNG state") as excinfo: + AMICATorchNG.from_state_dict(bad_state) + assert isinstance(excinfo.value.__cause__, TypeError) + + +def test_mlx_from_state_dict_unexpected_key_raises_value_error(real_data): + AMICAMLXNG = _mlx_core().AMICAMLXNG + m = _fit_mlx(real_data) + state = m.state_dict() + bad_state = {**state, "config": {**state["config"], "bogus_key_xyz": 123}} + + with pytest.raises(ValueError, match="malformed AMICAMLXNG state") as excinfo: + AMICAMLXNG.from_state_dict(bad_state) + assert isinstance(excinfo.value.__cause__, TypeError) + + +def test_mlx_from_state_dict_missing_key_raises_value_error(real_data): + AMICAMLXNG = _mlx_core().AMICAMLXNG + m = _fit_mlx(real_data) + state = m.state_dict() + config = dict(state["config"]) + del config["n_channels"] + bad_state = {**state, "config": config} + + with pytest.raises(ValueError, match="malformed AMICAMLXNG state") as excinfo: + AMICAMLXNG.from_state_dict(bad_state) + assert isinstance(excinfo.value.__cause__, TypeError) diff --git a/pamica/tests/test_metrics_posterior.py b/pamica/tests/test_metrics_posterior.py new file mode 100644 index 00000000..51362115 --- /dev/null +++ b/pamica/tests/test_metrics_posterior.py @@ -0,0 +1,105 @@ +"""Unit tests for ``pamica.metrics.posterior.model_probability_from_loglik`` +(issue #306 PR #329 review). + +The NaN-vs-``-inf`` diagnosis and the column-wise softmax normalization +behind ``AMICATorchNG.model_probability``/``AMICAMLXNG.model_probability`` +were factored into this one pure function so the two backends do not +duplicate the logic; see ``pamica/metrics/posterior.py`` for why it lives +under ``pamica/metrics/`` (the same "backend computes ``Lht``/data, a shared +pure function does the rest" split already used by ``pamica.metrics.mir``/ +``pairwise_mi``). + +Per ``.rules/testing.md``, every test exercises a real ``Lht`` -- the actual +per-model, per-sample log-likelihood from a real small fit's +``model_loglik()`` on the bundled sample EEG data -- rather than a +fabricated array. The error-path tests poison a COPY of that real ``Lht`` +(one column set entirely to ``-inf``, a separate single entry set to +``NaN``), the same direct-poisoning pattern used throughout +``test_backend_guards.py``. +""" + +from pathlib import Path +from typing import Tuple + +import numpy as np +import pytest +import torch + +from pamica.metrics import model_probability_from_loglik +from pamica.torch_impl import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +NMIX = 3 +SEED = 42 +N_SAMPLES = 2048 + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +@pytest.fixture(scope="module") +def fitted_model() -> Tuple[AMICATorchNG, np.ndarray]: + """A real 2-model fit and the data it was fit on, so + ``model_loglik(data)`` returns a real ``(2, N_SAMPLES)`` ``Lht`` (two + models, so a single poisoned row does not, on its own, make an entire + column non-finite -- the -inf test needs both rows bad at one column, + the NaN test needs only one).""" + data = load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD).astype( + np.float64 + )[:, :N_SAMPLES] + m = AMICATorchNG( + n_channels=NW, + n_models=2, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + block_size=256, + ) + m.fit(data, max_iter=2, verbose=False) + return m, data + + +def test_healthy_lht_columns_sum_to_one_and_match_model_probability(fitted_model): + m, data = fitted_model + Lht = m.model_loglik(data) + + prob = model_probability_from_loglik(Lht, caller="test") + + np.testing.assert_allclose(prob.sum(axis=0), 1.0) + # Behavior-preserving: AMICATorchNG.model_probability now delegates to + # this exact function, so the two must agree exactly (both are computed + # from the identical Lht, no separate rounding path). + np.testing.assert_array_equal(prob, m.model_probability(data)) + + +def test_all_models_neg_inf_at_a_sample_raises_distinct_message(fitted_model): + m, data = fitted_model + Lht = m.model_loglik(data).copy() + Lht[:, 100] = -np.inf # every model (both rows) underflows at sample 100 + + with pytest.raises(ValueError, match="-inf") as excinfo: + model_probability_from_loglik(Lht, caller="test") + assert "NaN" not in str(excinfo.value) + + +def test_nan_loglik_raises_a_different_message_than_neg_inf(fitted_model): + m, data = fitted_model + Lht = m.model_loglik(data).copy() + Lht[0, 100] = np.nan # only one model's entry -- still poisons col_max + + with pytest.raises(ValueError, match="NaN") as excinfo: + model_probability_from_loglik(Lht, caller="test") + assert "-inf" not in str(excinfo.value) + + +def test_caller_prefix_appears_in_both_error_messages(fitted_model): + m, data = fitted_model + Lht = m.model_loglik(data).copy() + Lht[:, 0] = -np.inf + + with pytest.raises(ValueError, match=r"^AMICATorchNG\.model_probability\(\)"): + model_probability_from_loglik(Lht, caller="AMICATorchNG.model_probability()") diff --git a/pamica/torch_impl/core.py b/pamica/torch_impl/core.py index db542d47..c38f36ba 100644 --- a/pamica/torch_impl/core.py +++ b/pamica/torch_impl/core.py @@ -56,7 +56,7 @@ from .. import blocktune from .. import restarts from ..metrics import mir as mir_metric -from ..metrics import pairwise_mi +from ..metrics import model_probability_from_loglik, pairwise_mi from ..rank import ( MINEIG, MINEIG_REL, @@ -2962,6 +2962,75 @@ def _check_model_idx(self, model_idx: int) -> None: f"fit (valid: 0..{self.n_models - 1})." ) + def _nonfinite_params(self) -> list: + """Names of :attr:`_PARAM_TENSORS` currently holding a non-finite + value (matching the legacy NumPy backend's own + ``_nonfinite_params``; issue #306 cross-backend review). Parameters + not yet allocated (``None``, e.g. a partially initialized instance) + are skipped rather than treated as bad. + + Single-sync fast path: every parameter's ``isfinite().all()`` is + stacked into one small tensor and reduced with exactly one + ``.item()``/``bool()`` device sync, instead of one sync per + parameter (measured ~2.2 ms of avoidable host-device round trips + across a 12-tensor sweep before this, PR #329 review -- CUDA/MPS + pay for each ``bool(tensor)``, not just MLX). The per-parameter + breakdown -- one further, small sync -- is only computed once that + reduction is already ``False``. + """ + names = [ + name for name in self._PARAM_TENSORS if getattr(self, name) is not None + ] + if not names: + return [] + flags = torch.stack( + [torch.isfinite(getattr(self, name)).all() for name in names] + ) + if bool(flags.all()): + return [] + bad = flags.logical_not().tolist() + return [name for name, is_bad in zip(names, bad) if is_bad] + + def _check_usable(self, action: str) -> None: + """Refuse to serve output from a degenerate fit (issue #306). + + Callers first check their own unfitted marker(s) and raise the + existing ``requires a fitted model`` ``RuntimeError`` (unchanged); + this assumes a fit has actually run and adds the two layers + :meth:`state_dict`/:meth:`write_amica_output` already use beyond + that: the ``stop_reason`` gate, then a defense-in-depth isfinite + sweep via :meth:`_nonfinite_params`. Mirrors the + :class:`~pamica.AMICA` wrapper's ``_check_usable`` (issue #50) for + callers using this backend directly. + """ + if self.stop_reason in self._DEGENERATE_STOP_REASONS: + raise RuntimeError( + f"Refusing to {action}: fit ended degenerate (stop_reason=" + f"{self.stop_reason!r}), so the model holds non-finite " + f"parameters and would produce NaN output. Lower lrate, " + f"disable Newton, or check data conditioning, then refit." + ) + nonfinite = self._nonfinite_params() + if nonfinite: + raise RuntimeError( + f"Refusing to {action}: parameters {nonfinite} hold " + f"non-finite values (stop_reason={self.stop_reason!r})." + ) + + def _check_input_shape(self, X: np.ndarray) -> None: + """Validate a data array against the fitted input channel count, + mirroring :meth:`fit`'s own ``X`` validation (issue #306): a raw + matmul/broadcast error deep inside a method is less useful than this + named ``ValueError`` at the entry point.""" + if X.ndim != 2: + raise ValueError( + f"X must be a 2D array (n_channels, n_samples), got shape {X.shape}" + ) + if X.shape[0] != self.n_channels_in: + raise ValueError( + f"X has {X.shape[0]} channels, model expects {self.n_channels_in}" + ) + def transform(self, X: np.ndarray, model_idx: int = 0) -> np.ndarray: """Apply the learned unmixing matrix to (new) data. @@ -2976,6 +3045,8 @@ def transform(self, X: np.ndarray, model_idx: int = 0) -> np.ndarray: "AMICATorchNG.transform() requires a fitted model; call fit() first." ) self._check_model_idx(model_idx) + self._check_usable("transform") + self._check_input_shape(X) X_t = torch.from_numpy(np.ascontiguousarray(X)).to(self.device, self.dtype) X_t = self.sphere @ (X_t - self.mean) # c is the per-model data-space center: unmix as W(x - c) (issue #27). @@ -2990,6 +3061,7 @@ def get_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: "fit() first." ) self._check_model_idx(model_idx) + self._check_usable("get the mixing matrix") return self.A[:, self.comp_list[:, model_idx]].T.cpu().numpy() @property @@ -3027,6 +3099,7 @@ def get_sensor_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: "model; call fit() first." ) self._check_model_idx(model_idx) + self._check_usable("get the sensor mixing matrix") A = self.A[:, self.comp_list[:, model_idx]].T return (self._pinv_sphere() @ A).cpu().numpy() @@ -3038,6 +3111,7 @@ def get_unmixing_matrix(self, model_idx: int = 0) -> np.ndarray: "fit() first." ) self._check_model_idx(model_idx) + self._check_usable("get the unmixing matrix") return self.W[:, :, model_idx].T.cpu().numpy() def _pca_reduction_requested(self, n_channels: int) -> bool: @@ -3106,9 +3180,11 @@ def mir( Raises ------ RuntimeError - If the model is unfitted. + If the model is unfitted, or the fit ended degenerate (issue + #306). ValueError - If the fitted sphere is rank-reduced (non-square): whether from + If ``X`` is not a 2D array of the fitted input channel count, or + if the fitted sphere is rank-reduced (non-square): whether from explicit ``pcakeep``/``pcadb`` or from automatic ``mineig``/ ``mineig_rel`` numerical-rank detection, the sphere is rank-deficient, so MIR's log-Jacobian term is undefined @@ -3119,6 +3195,8 @@ def mir( "AMICATorchNG.mir() requires a fitted model; call fit() first." ) self._check_model_idx(model_idx) + self._check_usable("compute MIR") + self._check_input_shape(X) if self._pca_reduced(): raise ValueError( "mir() is incompatible with PCA reduction: the fitted " @@ -3155,7 +3233,11 @@ def pmi( Raises ------ RuntimeError - If the model is unfitted (via ``transform``). + If the model is unfitted, or the fit ended degenerate (issue + #306), both via :meth:`transform`. + ValueError + If ``X`` is not a 2D array of the fitted input channel count + (via :meth:`transform`). """ return pairwise_mi(self.transform(X, model_idx=model_idx), nbins) @@ -3192,14 +3274,32 @@ def model_loglik(self, X: np.ndarray) -> np.ndarray: Raises ------ RuntimeError - If the model is unfitted. + If the model is unfitted, or the fit ended degenerate (issue + #306). ValueError - If ``X`` contains non-finite (NaN/Inf) values. + If ``X`` is not a 2D array of the fitted input channel count, or + contains non-finite (NaN/Inf) values. """ if self.sphere is None or self.mean is None or self.W is None: raise RuntimeError( "AMICATorchNG.model_loglik() requires a fitted model; call fit() first." ) + self._check_usable("compute the model log-likelihood") + self._check_input_shape(X) + return self._model_loglik_unchecked(X) + + def _model_loglik_unchecked(self, X: np.ndarray) -> np.ndarray: + """Core ``Lht`` computation for :meth:`model_loglik`, with no + degenerate-fit guard or shape validation of its own (issue #306 PR + #329 review): :meth:`model_loglik` and :meth:`model_probability` each + do their own single guard + shape check, with their own action + wording, then both call this -- so the guard no longer runs twice + (~2x cost) on a :meth:`model_probability` call, which used to run + its own ``_check_usable`` and then :meth:`model_loglik`'s.""" + # Narrows Optional[Tensor] for the type checker: both callers already + # checked these are set before calling this private helper. + assert self.sphere is not None + assert self.mean is not None X = np.ascontiguousarray(X) if not np.isfinite(X).all(): bad = np.flatnonzero(~np.isfinite(X).all(axis=1)) @@ -3237,23 +3337,32 @@ def model_probability(self, X: np.ndarray) -> np.ndarray: Raises ------ RuntimeError - If the model is unfitted. + If the model is unfitted, or the fit ended degenerate (issue + #306). ValueError - If ``X`` is non-finite, or if every model underflows to ``-inf`` - log-likelihood at some sample (the posterior is undefined there). + If ``X`` is not a 2D array of the fitted input channel count, if + ``X`` is non-finite, if every model underflows to ``-inf`` + log-likelihood at some sample (the posterior is undefined + there), or if a log-likelihood is NaN (numerical corruption, + distinct from the ``-inf`` underflow case above). """ - Lht = self.model_loglik(X) - col_max = Lht.max(axis=0, keepdims=True) - if not np.isfinite(col_max).all(): - n_bad = int((~np.isfinite(col_max)).sum()) - raise ValueError( - f"AMICATorchNG.model_probability(): every model has -inf " - f"log-likelihood at {n_bad} sample(s), so the posterior is " - "undefined there (an extreme outlier under a tight source " - "density)." + if self.sphere is None or self.mean is None or self.W is None: + raise RuntimeError( + "AMICATorchNG.model_probability() requires a fitted model; " + "call fit() first." ) - ex = np.exp(Lht - col_max) - return ex / ex.sum(axis=0, keepdims=True) + self._check_usable("compute the model probability") + self._check_input_shape(X) + Lht = self._model_loglik_unchecked(X) + # NaN and -inf are different failure modes and must not share a + # message: -inf is every model underflowing at a real sample (an + # extreme outlier), while NaN is numerical corruption. isfinite alone + # conflates them (PR #311 review scope extension, issue #306). The + # diagnosis + normalization is shared with AMICAMLXNG (PR #329 + # review) rather than duplicated per backend. + return model_probability_from_loglik( + Lht, caller="AMICATorchNG.model_probability()" + ) # ------------------------------------------------------------------ # Fitted-parameter metadata (issue #142) @@ -3276,6 +3385,7 @@ def get_pdftype(self, model_idx: int = 0) -> np.ndarray: "AMICATorchNG.get_pdftype() requires a fitted model; call fit() first." ) self._check_model_idx(model_idx) + self._check_usable("get the density family") return self.pdtype[:, model_idx].detach().cpu().numpy() def get_rho(self, model_idx: int = 0) -> np.ndarray: @@ -3296,15 +3406,12 @@ def get_rho(self, model_idx: int = 0) -> np.ndarray: "AMICATorchNG.get_rho() requires a fitted model; call fit() first." ) self._check_model_idx(model_idx) - # Defense-in-depth, matching state_dict()'s isfinite sweep: a degenerate - # multi-model fit can leave one model's rho non-finite without the - # aggregate LL tripping nan_ll, and _check_usable only inspects - # stop_reason. Refuse rather than return a silent NaN. - if not torch.isfinite(self.rho).all(): - raise RuntimeError( - "AMICATorchNG.get_rho(): rho holds non-finite values (a " - "degenerate fit); inspect stop_reason and refit." - ) + # Folded into the shared guard (issue #306): a degenerate multi-model + # fit can leave one model's rho non-finite without the aggregate LL + # tripping a _DEGENERATE_STOP_REASONS marker, which _check_usable's + # defense-in-depth isfinite sweep over _PARAM_TENSORS (rho included) + # still catches. Refuse rather than return a silent NaN. + self._check_usable("get rho") idx = self.comp_list[:, model_idx] return self.rho[:, idx].detach().cpu().numpy() @@ -3333,6 +3440,7 @@ def shared_components(self) -> list: "AMICATorchNG.shared_components() requires a fitted model; call " "fit() first." ) + self._check_usable("get the shared components") cl = self.comp_list.detach().cpu().numpy() # (n_sources, n_models) groups = [] for col in np.unique(cl): @@ -3387,6 +3495,7 @@ def variance_order( "fit() first." ) self._check_model_idx(model_idx) + self._check_usable("compute the variance order") cl = self.comp_list[:, model_idx].cpu().numpy() alpha = self.alpha[:, cl].cpu().numpy() mu = self.mu[:, cl].cpu().numpy() @@ -3457,11 +3566,7 @@ def write_amica_output(self, outdir) -> None: # neutralizes a stale LLt stash: a failed final iteration's # _llt_lht/_llt_lt (from before the nan_params/nan_ll break) can no # longer reach disk once this guard refuses the write outright. - nonfinite = [ - name - for name in self._PARAM_TENSORS - if not torch.isfinite(getattr(self, name)).all() - ] + nonfinite = self._nonfinite_params() if nonfinite: raise RuntimeError( f"Refusing to write output for a model with non-finite " @@ -3574,11 +3679,7 @@ def state_dict(self) -> dict: # Defense-in-depth: catch a non-finite parameter even if stop_reason # bookkeeping ever misses it (the codebase has known NaN-suppression # risks). isfinite on the integer comp_list is trivially all-True. - nonfinite = [ - name - for name in self._PARAM_TENSORS - if not torch.isfinite(getattr(self, name)).all() - ] + nonfinite = self._nonfinite_params() if nonfinite: raise RuntimeError( f"Refusing to serialize a model with non-finite parameters " @@ -3744,7 +3845,20 @@ def from_state_dict( ) config = dict(state["config"]) config["dtype"] = getattr(torch, config["dtype"]) - obj = cls(device=device, **config) + # A missing/unexpected key in a malformed or foreign-version payload + # surfaces as a bare TypeError from the constructor call; every other + # validation step in this method already names the payload as the + # culprit with a ValueError, so wrap this one the same way instead of + # letting a mismatched-keyword TypeError propagate unexplained + # (issue #306). + try: + obj = cls(device=device, **config) + except TypeError as exc: + raise ValueError( + f"malformed AMICATorchNG state: config does not match the " + f"AMICATorchNG constructor ({exc}); the payload may be " + "truncated or from an incompatible version." + ) from exc obj._load_params(state) return obj From 4324333f77cc4d1f54d6703418520382922d82e7 Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Tue, 22 Sep 2026 18:03:52 -0700 Subject: [PATCH 05/26] Phase 4: backend selection in AMICA and AMICAICA (#330) * test: pin loading a version-1 AMICA save * feat: add float64 preprocessing accessors * feat: select the AMICA backend (torch or mlx) * feat: save the AMICA backend (format v2) * feat: select the AMICAICA backend * test: cover MLX export and torch-MLX agreement * docs: document backend selection in the wrappers * fix: name a malformed MLX save payload * docs: note the MLX wrapper in write_amica_output * fix: word the unexpected-keyword error for both * feat: show the backend in AMICA and AMICAICA repr * test: run MLX load errors without MLX * test: degenerate AMICAICA fit builds no basis * test: pin the weights-only save refusal * test: drive MLX from the JSON params file * test: cover fit_transform on both backends * test: fold the v1 backend check into the pin * docs: add Raises to the AMICA accessors * docs: add backend Raises to AMICAICA * test: AMICAICA propagates fit keyword errors * docs: name both backends in the API index * docs: point getting-started at backend=mlx * docs: keep MLX precision figures in one place * docs: mark the example's external input files * feat: guard the preprocessing accessors (#306) --- AGENTS.md | 11 +- README.md | 5 +- docs/api/amica.md | 27 +- docs/api/index.md | 10 +- docs/api/mlx-backend.md | 19 +- docs/api/mne-compat.md | 29 + docs/changelog.md | 33 +- docs/getting-started.md | 4 + docs/guides/amica-differences.md | 8 +- docs/guides/backends.md | 99 ++- docs/guides/eeglab.md | 6 +- pamica/amica.py | 671 +++++++++++++--- pamica/mlx_impl/core.py | 66 +- pamica/mne_compat/core.py | 113 ++- pamica/tests/mne_tests/test_mne_backends.py | 278 +++++++ pamica/tests/test_backend_accessors.py | 205 +++++ pamica/tests/test_backend_guards.py | 4 + pamica/tests/test_wrapper_backends.py | 720 ++++++++++++++++++ .../torch_tests/test_amica_ng_wrapper.py | 4 +- pamica/tests/torch_tests/test_ng_sharing.py | 8 +- pamica/torch_impl/core.py | 51 ++ 21 files changed, 2178 insertions(+), 193 deletions(-) create mode 100644 pamica/tests/mne_tests/test_mne_backends.py create mode 100644 pamica/tests/test_backend_accessors.py create mode 100644 pamica/tests/test_wrapper_backends.py diff --git a/AGENTS.md b/AGENTS.md index 164144f6..94feac77 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,12 +3,12 @@ ## Project Context **Purpose:** Python implementation of AMICA (Adaptive Mixture Independent Component Analysis) that reproduces the results of the reference Fortran binary. Targets EEG/EMG source separation with GPU/MPS/CPU support. **Tech Stack:** Python 3.12+, PyTorch (primary backend, MPS/CUDA/CPU), NumPy/SciPy (legacy backend), matplotlib. Reference implementation is Fortran (`amica17.f90`, `funmod2.f90`). -**Architecture:** The scikit-learn-style `AMICA` interface wraps `AMICATorchNG` (`torch_impl/core.py`), the natural-gradient EM port that reaches Fortran parity (Newton, exact-EM mixture updates, symmetric-ZCA sphere, Jacobian LL). This is the single PyTorch backend: the earlier Adam/autograd backends (`AMICATorch`, `AMICATorchV2`) and their mixture/optimizer/PDF helper modules were removed in issue #32 as superseded. The legacy NumPy implementation (`numpy_impl/core.py`, retained as `AMICA_NumPy`) carries the same parity fixes plus baralpha and outlier rejection (`do_reject`, ported from the PyTorch backend's `good_idx` mechanism in issue #123). Correctness is defined by parity with the Fortran binary, validated by `validate_implementations.py`. +**Architecture:** The scikit-learn-style `AMICA` interface wraps `AMICATorchNG` (`torch_impl/core.py`) by default, or `AMICAMLXNG` (`mlx_impl/core.py`) with `backend="mlx"` (#313); `AMICATorchNG` is the natural-gradient EM port that reaches Fortran parity (Newton, exact-EM mixture updates, symmetric-ZCA sphere, Jacobian LL). This is the single PyTorch backend: the earlier Adam/autograd backends (`AMICATorch`, `AMICATorchV2`) and their mixture/optimizer/PDF helper modules were removed in issue #32 as superseded. The legacy NumPy implementation (`numpy_impl/core.py`, retained as `AMICA_NumPy`) carries the same parity fixes plus baralpha and outlier rejection (`do_reject`, ported from the PyTorch backend's `good_idx` mechanism in issue #123). Correctness is defined by parity with the Fortran binary, validated by `validate_implementations.py`. ## Architecture Map ``` pamica/ -├── amica.py # Main scikit-learn-style AMICA interface (wraps AMICATorchNG) +├── amica.py # Main scikit-learn-style AMICA interface (wraps AMICATorchNG, or AMICAMLXNG with backend="mlx") ├── __init__.py # Exposes AMICA (PyTorch), AMICA_NumPy (legacy), numpy_impl, torch_impl ├── torch_impl/ # PyTorch backend │ ├── core.py # Natural-gradient EM port (AMICATorchNG); Fortran-parity, primary backend @@ -95,7 +95,8 @@ remaining MLX gap vs the PyTorch backend is none, other than float32-only precis have no float64). ## Key Files -- **Main interface:** `pamica/amica.py` (thin wrapper over `AMICATorchNG`) +- **Main interface:** `pamica/amica.py` (thin wrapper over `AMICATorchNG`, or `AMICAMLXNG` with + `backend="mlx"`, #313; `AMICAICA` in `pamica/mne_compat/core.py` takes the same `backend`) - **PyTorch backend:** `pamica/torch_impl/core.py` (`AMICATorchNG`, natural-gradient EM, Fortran-parity; ADR `.context/decisions/0001-torch-backend-natural-gradient-em.md`). This is the only PyTorch backend; the basic `AMICATorch`/`AMICATorchV2` paths were removed in #32. @@ -107,6 +108,10 @@ have no float64). - PyTorch backend with GPU/MPS/CPU support; the `AMICATorchNG` natural-gradient EM backend now matches the Fortran reference (LL ~ -3.40, component correlation ~0.997) with Newton enabled and positive-definite (issue #24). +- `AMICA(backend="mlx")` and `AMICAICA(backend="mlx")` (epic #324 Phase 4, #313) run the MLX backend end to + end: fit, `from_params_file`, `pcakeep`, the #50 degenerate-fit contract, `.pt` save/load (wrapper + `format_version` 2 records the backend; version 1 still loads as torch), EEGLAB export and MNE `apply`. + `device`/`dtype` are torch-only; the default stays `"torch"`. - `validate_implementations.py` runs the PyTorch (NG) backend against the Fortran binary and matches components via the Hungarian algorithm; it does not exercise the NumPy or MLX backends. NumPy-vs- Fortran parity lives in pytest (`test_sample_data_numpy_vs_fortran`), and MLX validation lives in diff --git a/README.md b/README.md index 42b06901..945425c1 100644 --- a/README.md +++ b/README.md @@ -70,11 +70,12 @@ The wrapper auto-selects a device and computes in float64 for Fortran parity. overhead-bound (float32 is about as fast as float64). - On Apple Silicon the MLX backend is the fastest option and carries the full feature surface (all pdf families, Newton, rejection, EEGLAB export, and - Mutual Information Reduction (MIR) diagnostics); import it explicitly. + Mutual Information Reduction (MIR) diagnostics); select it with + `backend="mlx"` on `AMICA` or the MNE wrapper `AMICAICA` (float32 only). ```python AMICA(device="cuda").fit(X) # NVIDIA GPU, float64 -from pamica.mlx_impl import AMICAMLXNG # Apple GPU (install the mlx extra) +AMICA(backend="mlx").fit(X) # Apple GPU, float32 (install the mlx extra) ``` ### EEGLAB interoperability diff --git a/docs/api/amica.md b/docs/api/amica.md index 5c2e0b73..4a639f6e 100644 --- a/docs/api/amica.md +++ b/docs/api/amica.md @@ -1,6 +1,29 @@ # AMICA -The main scikit-learn-style interface. Wraps the natural-gradient EM backend -([`AMICATorchNG`](torch-backend.md)). +The main scikit-learn-style interface. +It wraps a natural-gradient expectation-maximization (EM) backend: +[`AMICATorchNG`](torch-backend.md) by default (float64, Fortran parity), +or the Apple-GPU [`AMICAMLXNG`](mlx-backend.md) with `backend="mlx"` (float32, issue #313). +Every method works the same on both; +see [Selecting a backend](../guides/backends.md#selecting-a-backend) for the rules and the precision of each. + +```python +from pamica import AMICA + +model = AMICA(backend="mlx").fit(X, pcakeep=X.shape[0] - 1, seed=42) +model.save("model.pt") # records the backend +model = AMICA.load("model.pt") # restored on the MLX backend +``` + +`device` and a `dtype` fit keyword apply to the PyTorch backend only and raise `ValueError` with `backend="mlx"`; +`backend="mlx"` without MLX installed raises `ImportError`. + +`get_sphere()`, `get_mean()` and `get_model_center(model_idx)` return the fitted preprocessing as float64 arrays on either backend, +so the transform can be composed by hand: +`transform(X)` is `W @ (sphere @ (X - mean[:, None]) - c[:, None])` with `W = get_unmixing_matrix()`. +`get_sensor_mixing_matrix()` gives the scalp maps in input-channel space, which is the only valid back-map after rank reduction. + +`save` writes `format_version` 2, which records the backend; +`load` restores the model on that backend and still reads version 1 files, which predate backend selection and always hold a PyTorch model. ::: pamica.AMICA diff --git a/docs/api/index.md b/docs/api/index.md index 3be3169a..220d2c00 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -6,10 +6,11 @@ The public import surface is stable: from pamica import AMICA, AMICA_NumPy, AMICATorchNG ``` -- **[`AMICA`](amica.md)** — the main scikit-learn-style interface. Wraps the - PyTorch natural-gradient EM backend. Start here. +- **[`AMICA`](amica.md)** — the main scikit-learn-style interface. + Wraps a natural-gradient EM backend: PyTorch by default, or MLX with `backend="mlx"`. + Start here. - **[`AMICATorchNG`](torch-backend.md)** — the PyTorch natural-gradient EM - backend (Fortran parity). The `AMICA` interface delegates to this class. + backend (Fortran parity). The `AMICA` interface delegates to this class by default. - **[`pamica.metrics`](metrics.md)** — separation-quality metrics (`mir`, `pairwise_mi`, `block_diagonal_order`) as free functions over plain arrays. Also reachable as `AMICA.mir`/`AMICA.pmi` on a fitted model. @@ -27,6 +28,7 @@ from pamica.mlx_impl import AMICAMLXNG # requires the `mlx` extra - **[`AMICAMLXNG`](mlx-backend.md)** — the optional Apple-GPU (MLX) backend; the fastest option on Apple Silicon (float32). + `AMICA(backend="mlx")` builds it without this import. The optional MNE-Python wrapper is likewise imported explicitly: @@ -36,4 +38,4 @@ from pamica.mne_compat import AMICAICA # requires the `mne` extra - **[`AMICAICA`](mne-compat.md)** — fit AMICA from an MNE `Raw`/`Epochs` and interoperate with `mne.preprocessing.ICA` (`get_sources`, `apply`, - `plot_components`, `to_mne_ica`). + `plot_components`, `to_mne_ica`), on either backend (`backend="mlx"`). diff --git a/docs/api/mlx-backend.md b/docs/api/mlx-backend.md index 5a47dca4..92af83a0 100644 --- a/docs/api/mlx-backend.md +++ b/docs/api/mlx-backend.md @@ -8,6 +8,17 @@ multi-model natural-gradient AMICA across all five source-density families fastest option on Apple hardware; see [Backends & Devices](../guides/backends.md) for the performance comparison. +Most users reach this backend through the wrappers rather than this class: +`AMICA(backend="mlx")` and the MNE wrapper `AMICAICA(backend="mlx")` (epic #324 Phase 4, issue #313) +add the params-file reader, the degenerate-fit contract, `.pt` `save`/`load` and the MNE export on top of it. +See [Selecting a backend](../guides/backends.md#selecting-a-backend). + +```python +from pamica import AMICA + +model = AMICA(backend="mlx").fit(X, seed=42) # builds AMICAMLXNG +``` + Source extraction (`transform` and the `get_mixing_matrix`/`get_unmixing_matrix`/ `get_sensor_mixing_matrix`/`get_rho` accessors) and persistence (`state_dict`/`from_state_dict` and `.npz` `save`/`load`) are implemented (epic @@ -36,6 +47,9 @@ and both parameters persist through `state_dict`/`save`. (see [the differences guide](../guides/amica-differences.md#explicit-dimensionality-reduction-pcakeep-and-pcadb-issue-323)). With it there are no remaining gaps against the PyTorch backend other than float32-only precision (Apple GPUs have no float64). +`get_sphere()`, `get_mean()` and `get_model_center()` (issue #313) return the fitted preprocessing as float64 arrays, +with the PyTorch backend's names and shapes; +the mean and centers are this backend's float32 values, and the sphere is its float64 host copy. ```python model = AMICAMLXNG(n_channels=X.shape[0], pcakeep=X.shape[0] - 1) # e.g. average reference @@ -43,8 +57,9 @@ model.fit(X) model.get_sensor_mixing_matrix() # (n_channels_in, pcakeep) scalp maps ``` -MLX is an optional dependency (Apple Silicon only), so it is imported separately -and is not part of the default `import pamica` surface: +MLX is an optional dependency (Apple Silicon only), so the class is imported separately +and is not part of the default `import pamica` surface +(`AMICA(backend="mlx")` imports it on first use): ```python from pamica.mlx_impl import AMICAMLXNG # requires the `mlx` extra diff --git a/docs/api/mne-compat.md b/docs/api/mne-compat.md index af7a3b1f..5977a502 100644 --- a/docs/api/mne-compat.md +++ b/docs/api/mne-compat.md @@ -38,6 +38,35 @@ non-finite input, supports principal component analysis (PCA) reduction and a degenerate (diverged) fit is refused by the consumer methods rather than emitting NaNs. +## Choosing the backend + +`AMICAICA` fits through [`AMICA`](amica.md), so it takes the same `backend` parameter: +the PyTorch backend by default, or the Apple-GPU MLX backend with `backend="mlx"` (issue #313). +Everything on this page works on both. + +```python +# raw: 32 average-referenced EEG channels, so rank 31 +ica = AMICAICA(backend="mlx", random_state=42).fit(raw, picks="eeg", pcakeep=31) +clean = ica.apply(raw.copy(), exclude=[0]) +``` + +As with `AMICA`, `device` (and a `dtype` fit keyword) apply to the PyTorch backend only and raise `ValueError` with `backend="mlx"`, +and `backend="mlx"` without MLX installed raises `ImportError`. + +The export reads the fitted mean, sphere and per-model centers through the backend-agnostic float64 accessors +`AMICA.get_mean()`, `get_sphere()` and `get_model_center()`, so both backends take the same code path. +Precision follows the backend: + +- A PyTorch fit (float64 by default) exports at float64 parity. +- An MLX fit computes in float32, so its unmixing matrix, mean and centers carry float32 rounding + and the export is float32-consistent rather than float64-parity. + `get_sources` agrees with `ica.amica_.transform` (which runs in float32) within float32 tolerance, + and excluding a component changes the data by that component's back-projection to the same tolerance. +- Reconstruction does not depend on the backend's precision. + MNE's mixing is the float64 pseudo-inverse of the exported unmixing and the PCA basis is orthonormal, + so `apply` with nothing excluded returns the input to float64 round-off on either backend, residual included. + [Precision on the MLX backend](../guides/backends.md#precision-on-the-mlx-backend) gives the measured figures. + ## Interoperating with `mne.preprocessing.ICA` `to_mne_ica()` returns a fully-populated diff --git a/docs/changelog.md b/docs/changelog.md index 4f51f807..8a48e5d6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,12 +5,43 @@ Release notes are also published on the ## Unreleased +- **Phase 4 of epic #324: backend selection in `AMICA` and `AMICAICA` (issue #313).** + `AMICA` and `AMICAICA` gain a `backend` parameter: + `"torch"` (the default, `AMICATorchNG`, float64 Fortran parity) or `"mlx"` (`AMICAMLXNG`, Apple GPU, float32 only). + Every wrapper feature now runs on MLX: + `fit` with any backend keyword (including `pcakeep`, which closes #323's remaining item), `from_params_file(..., backend="mlx")` (#304's MLX path), + the #50 degenerate-fit contract, `save`/`load`, the EEGLAB export and the MNE path. + An unknown backend raises `ValueError`, `backend="mlx"` without MLX installed raises `ImportError` at construction, + and `device` or a `dtype` fit keyword with `backend="mlx"` raises `ValueError`, since MLX runs only on its default device in float32. + `import pamica` still never imports MLX. + - The keywords `fit` takes from `**kwargs` and from a params file are derived from the selected backend class's own signature, + and the "not applied" warning names that class. + A keyword the selected backend does not take now raises `TypeError` from `AMICA.fit` itself, naming the backend, + instead of from the backend constructor. + - The degenerate-fit contract uses each backend class's own `_DEGENERATE_STOP_REASONS`, + so an MLX fit that stops on `nan_params` is refused like a PyTorch `nan_ll` fit. + - **Save format version 2:** `AMICA.save` records the backend in `wrapper["backend"]`, + and `AMICA.load` restores the model on that backend. + An MLX model's numpy arrays are stored as CPU tensors of the same dtype, so the file still loads with `torch.load(weights_only=True)`. + Version 1 files, written before this change, still load (always as PyTorch models). + Loading an MLX file needs MLX installed and `device=None`. + - **Fix:** a numpy scalar in the backend's config or fit record (for example `fit(X, seed=np.int64(42))`) is now saved as the equivalent Python number. + `AMICA.save` used to write such a file, which `AMICA.load` then refused (the `weights_only` unpickler rejects numpy scalars). + Any other value that loading could not read back now raises `TypeError` at save time. + - **New accessors:** `get_sphere()`, `get_mean()` and `get_model_center(model_idx)` on `AMICATorchNG`, `AMICAMLXNG` and `AMICA`, + with the same names and shapes on both backends and float64 arrays from both. + The torch and MLX ones carry the degenerate-fit guard of the issue #306 entry below. + `AMICA` also gains `get_sensor_mixing_matrix()`, which both backends already had. + - `AMICAICA` reads the fitted mean, sphere and centers through those accessors, with no backend-specific array calls. + An MLX export is float32-consistent (sources agree with the MLX `transform` within float32 tolerance), + while `apply` with nothing excluded still returns the input to float64 round-off. + A degenerate `AMICAICA` fit now leaves `pca_components_`/`pca_explained_variance_` as `None`; it was never exportable. - **Raw backend accessors now guard against degenerate fits and bad input shape** (issue #306, epic #324 Phase 5). **Behavior change:** `AMICATorchNG`, `AMICAMLXNG` and the legacy NumPy `AMICA` backend's fitted-output accessors (`transform`, `get_mixing_matrix`, `get_unmixing_matrix`, `get_sensor_mixing_matrix`, `get_rho`, `get_pdftype`, `shared_components`, `variance_order`, `model_loglik`, `model_probability`, - `mir` and `pmi` on torch/MLX; + `mir` and `pmi` on torch/MLX, plus Phase 4's `get_sphere`, `get_mean` and `get_model_center`; `transform`, `get_weights` and `get_sensor_mixing_matrix` on NumPy) now raise `RuntimeError` when called on a fit the backend itself classified as degenerate, or when a fitted parameter holds a non-finite value, diff --git a/docs/getting-started.md b/docs/getting-started.md index 340bc425..109c24cd 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -25,6 +25,10 @@ pamica` never requires it. uv sync --extra mlx # or, in an existing environment: uv pip install mlx ``` +Then select it with `AMICA(backend="mlx")` (or `AMICAICA(backend="mlx")` for MNE users); +it computes in float32. +See [Selecting a backend](guides/backends.md#selecting-a-backend). + ## Quickstart The main entry point is the scikit-learn-style [`AMICA`](api/amica.md) class, diff --git a/docs/guides/amica-differences.md b/docs/guides/amica-differences.md index ed11bed0..0277cd22 100644 --- a/docs/guides/amica-differences.md +++ b/docs/guides/amica-differences.md @@ -16,7 +16,7 @@ that is not listed, that is a bug worth | 2 | Zero numerical rank | `numeigs = 0`, continues | `ValueError` naming cause and fix | fitting a zero-dimensional model is not a recoverable state | — (no reason to want it) | | 3 | Returned iterate | last EM iterate | highest-likelihood iterate (`keep_best`) | the lrate schedule is non-monotone; late Newton overshoots cut LL variance 12.7x → 2.0x | `keep_best=False` | | 4 | Newton | on (`do_newton=1`) | off | isolates the algorithm from initialization for parity work | `do_newton=True` | -| 5 | Degenerate fits | returns NaN sources, and writes them out on its `writestep` cadence | the `AMICA` wrapper refuses `transform`/`get_*`/`save`; the raw `AMICATorchNG`, `AMICAMLXNG` and NumPy `AMICA` backends now refuse their own output accessors on a degenerate fit too (issue #306), not just the wrapper; NumPy additionally reports `converged=False` with a `stop_reason`, refuses the final write, and skips each periodic checkpoint with a logged reason (leaving the last valid one on disk) | NaN sources silently poison downstream analysis, and `loadmodout` reads a NaN checkpoint back without complaint | — (see issues #50, #240 and #306) | +| 5 | Degenerate fits | returns NaN sources, and writes them out on its `writestep` cadence | the `AMICA` wrapper, on either backend, refuses `transform`/`get_*`/`save`; the raw `AMICATorchNG`, `AMICAMLXNG` and NumPy `AMICA` backends now refuse their own output accessors on a degenerate fit too (issue #306), not just the wrapper; NumPy additionally reports `converged=False` with a `stop_reason`, refuses the final write, and skips each periodic checkpoint with a logged reason (leaving the last valid one on disk) | NaN sources silently poison downstream analysis, and `loadmodout` reads a NaN checkpoint back without complaint | — (see issues #50, #240 and #306) | | 6 | Precision | float64 | float64 (float32 on Apple GPUs) | Apple GPUs have no float64; float32 agrees to ~7 significant digits, not bit-parity | `dtype=torch.float64` | | 7 | Sensor-space maps | `Spinv` applied internally | `get_sensor_mixing_matrix()` | `get_mixing_matrix()` returns sphered-space `A`; switching its meaning by data conditioning would be worse | — | | 8 | Columns merged away by `share_comps` | updated to NaN, then hidden by the `comp_used` mask | frozen at their last finite value (never divided) | a fit must not end holding NaN parameters, mask or no mask; the columns are dead either way | — (see issues #60, #240) | @@ -185,7 +185,9 @@ The decision and the alternatives considered are recorded in ADR 0005. Separate from reference divergences, this table compares the backends. The raw MLX backend (`AMICAMLXNG`) has the PyTorch backend's full feature surface except precision: it is float32-only, because Apple GPUs have no float64. -The `AMICA` and `AMICAICA` wrappers do not construct it yet (epic #324 Phase 4, issue #313). +The `AMICA` and `AMICAICA` wrappers construct it with `backend="mlx"` (epic #324 Phase 4, issue #313), +so the params-file reader, the degenerate-fit contract, `.pt` `save`/`load`, the EEGLAB export and the MNE path all run on it +(see [Selecting a backend](backends.md#selecting-a-backend)). | Feature | PyTorch | NumPy | MLX | Native Fortran | |---|---|---|---|---| @@ -202,7 +204,7 @@ The `AMICA` and `AMICAICA` wrappers do not construct it yet (epic #324 Phase 4, | `n_restarts` best-of-N restarts | yes | yes | yes | n/a | | Mutual Information Reduction (MIR) diagnostic | yes | no | yes | n/a | | Persistence | `state_dict` + EEGLAB `amicaout` export | EEGLAB `amicaout` | `state_dict`/`.npz` `save`-`load` + EEGLAB `amicaout` export | EEGLAB `amicaout` | -| Fortran `input.param` reader | yes (`AMICA.from_params_file`, #132) | yes (`AMICA_NumPy(params_file=...)` / `from_params_file`, #304) | via `AMICA(backend="mlx")` (epic #324 Phase 4) | native | +| Fortran `input.param` reader | yes (`AMICA.from_params_file`, #132) | yes (`AMICA_NumPy(params_file=...)` / `from_params_file`, #304) | yes (`AMICA.from_params_file(..., backend="mlx")`) | native | The NumPy row's "GG only" (generalized Gaussian, GG) corrects an earlier version of this table, which listed "all five": `AMICA_NumPy._compute_log_pdf` diff --git a/docs/guides/backends.md b/docs/guides/backends.md index e89c3265..69f39e72 100644 --- a/docs/guides/backends.md +++ b/docs/guides/backends.md @@ -11,18 +11,99 @@ interface, plus an optional Apple-GPU backend and a legacy NumPy reference. | MLX (Apple GPU) | [`AMICAMLXNG`](../api/mlx-backend.md) (`pamica.mlx_impl`) | Optional Apple-Silicon GPU backend; float32 only. | | NumPy reference | [`AMICA_NumPy`](../api/numpy-backend.md) | Legacy oracle + CLI; carries the same parity fixes. | -The `AMICA` wrapper uses `AMICATorchNG`. The MLX backend is imported separately -(`from pamica.mlx_impl import AMICAMLXNG`) so that `import pamica` never -requires MLX. As of epic #278 the MLX backend carries the full -`AMICATorchNG`-equivalent surface (all pdf families, Newton, rejection, -persistence, EEGLAB export, MIR/PMI); the wrapper conveniences -- among them -the `from_params_file` reader -- remain PyTorch-only (issue #313 tracks -backend selection there). See the -[backend table](amica-differences.md#backend-differences). +The `AMICA` wrapper and the MNE wrapper `AMICAICA` build `AMICATorchNG` by default and `AMICAMLXNG` with `backend="mlx"` +(see [Selecting a backend](#selecting-a-backend)). +As of epic #278 the MLX backend carries the full `AMICATorchNG`-equivalent surface +(all pdf families, Newton, rejection, persistence, EEGLAB export, MIR/PMI), +and since epic #324 every wrapper convenience runs on it too. +See the [backend table](amica-differences.md#backend-differences). + +## Selecting a backend + +Pass `backend="mlx"` to `AMICA` or `AMICAICA` to fit on the Apple GPU (issue #313): + +```python +from pamica import AMICA + +model = AMICA() # AMICATorchNG: float64, Fortran parity (the default) +model = AMICA(backend="mlx") # AMICAMLXNG: Apple GPU, float32 +``` + +Everything the wrappers offer works on both backends: +`fit` with any backend keyword (`pcakeep`, `seed`, `n_restarts`, ...), `from_params_file`, +`transform` and the matrix accessors, the degenerate-fit contract (`converged_`, `is_fitted_`, `stop_reason_`), +`save`/`load`, the EEGLAB export and the MNE path. + +- `backend="mlx"` needs MLX (`uv pip install mlx` or the `mlx` extra; Apple Silicon only). + Without it the constructor raises `ImportError`, and `import pamica` never imports MLX either way. +- `device` and `dtype` are PyTorch-only. + MLX always runs on its default device (the Apple GPU) in float32, + so `AMICA(backend="mlx", device=...)` and `fit(..., dtype=...)` on an MLX model raise `ValueError`. +- The default stays `"torch"`, the float64 Fortran-parity path; there is no automatic selection. +- `AMICA.from_params_file(path, backend="mlx")` applies the file's settings through `AMICAMLXNG`'s own constructor, + and names any setting that backend cannot take in its "not applied" warning. +- `save` records the backend (`format_version` 2), so `AMICA.load` restores the model on the backend that fit it. + Files written before backend selection (version 1) still load, as PyTorch models. + Loading an MLX model needs MLX installed and takes no `device`. + +The raw classes stay available for direct use (`from pamica.mlx_impl import AMICAMLXNG`), +but the wrappers are the supported route: they add the degenerate-fit contract, the params-file reader and the MNE export. + +### Precision on the MLX backend + +MLX computes in float32 (about 7 significant digits), so an MLX fit is float32-consistent rather than float64-parity. +On the bundled 32-channel sample, the same data, seed and 10 iterations through both wrappers give the same components in the same order +(Hungarian-matched source correlation above 0.9999999, with and without `pcakeep`) +and log-likelihoods within 5e-6 of each other. + +`get_sphere()`, `get_mean()` and `get_model_center()` return float64 arrays on both backends. +An MLX fit's mean and per-model centers are float32 values; +its sphere is the float64 matrix it was built from, which its float32 GPU computation agrees with to float32 rounding. +After `AMICA.load`, an MLX model's sphere is the saved float32 sphere, upcast. + +In the MNE path, `AMICAICA(backend="mlx")` exports through the same float64 accessors. +`get_sources` agrees with the MLX `transform` to float32 tolerance (measured 2-3e-7 relative, reduced to full rank), +and excluding a component removes its back-projection to the same tolerance. +Reconstruction does not inherit the float32 rounding: +`apply` with nothing excluded returns the input to float64 round-off (measured 1.4e-15 with `pcakeep=20`), the principal component analysis (PCA) residual included, +because MNE's mixing is the float64 pseudo-inverse of the exported unmixing and the PCA basis is orthonormal. + +### End to end on the Apple GPU + +A per-session workflow for average-referenced electroencephalography (EEG), whose rank is `n_channels - 1`: + +```python +import mne +from pamica import AMICA +from pamica.mne_compat import AMICAICA + +# X is your own (n_channels, n_samples) array +model = AMICA(backend="mlx").fit(X, pcakeep=X.shape[0] - 1, seed=42) +sources = model.transform(X) # float32, (n_channels - 1, n_samples) +maps = model.get_sensor_mixing_matrix() # scalp maps, (n_channels, n_channels - 1) +model.write_amica_output("amicaout") # EEGLAB: loadmodout15('amicaout') + +model.save("model.pt") # records backend="mlx" +model = AMICA.load("model.pt") # back on the MLX backend + +# The same settings from your own Fortran input.param or JSON params file: +model = AMICA.from_params_file("/path/to/input.param", backend="mlx").fit(X) + +# MNE: fit from a Raw, then remove a component (the PCA residual is kept). +# Replace the path with your own raw EEGLAB recording (.set). +raw = mne.io.read_raw_eeglab("/path/to/your_recording.set", preload=True) +n_eeg = len(mne.pick_types(raw.info, eeg=True)) # good EEG channels +ica = AMICAICA(backend="mlx", random_state=42) +ica.fit(raw, picks="eeg", pcakeep=n_eeg - 1) +clean = ica.apply(raw.copy(), exclude=[0]) +``` + +`write_amica_output` runs before `save` here because a model restored by `load` carries no per-sample log-likelihood, +so its export omits the `LLt` file (with a warning). ## Device selection -`AMICA(device=...)` accepts `"cuda"`, `"cpu"`, `"mps"`, or `None` (auto): +With the PyTorch backend, `AMICA(device=...)` accepts `"cuda"`, `"cpu"`, `"mps"`, or `None` (auto): - **`None` (auto)** — selects CUDA if available, else CPU. An auto-selected MPS device is redirected to CPU because the parity default is float64, which MPS diff --git a/docs/guides/eeglab.md b/docs/guides/eeglab.md index 54307bc4..0ddb0414 100644 --- a/docs/guides/eeglab.md +++ b/docs/guides/eeglab.md @@ -42,10 +42,10 @@ MLX computes in float32, so its files are not bit-identical to a float64 export the way a torch fit's are to Fortran's): ```python -from pamica.mlx_impl import AMICAMLXNG # requires the mlx extra +from pamica import AMICA -model = AMICAMLXNG(n_channels=X.shape[0], n_mix=3) -model.fit(X) # X is (n_channels, n_samples) +model = AMICA(n_mix=3, backend="mlx") # requires the mlx extra (issue #313) +model.fit(X) # X is (n_channels, n_samples) model.write_amica_output("amicaout") ``` diff --git a/pamica/amica.py b/pamica/amica.py index 00eba320..ccdbebce 100644 --- a/pamica/amica.py +++ b/pamica/amica.py @@ -1,20 +1,29 @@ """ -Main AMICA interface using the PyTorch natural-gradient EM backend. +Main AMICA interface over pamica's natural-gradient EM backends. This module provides the primary :class:`AMICA` class, a scikit-learn-style -wrapper over :class:`AMICATorchNG` (the natural-gradient EM port that reaches -Fortran parity; see ``.context/decisions/0001-torch-backend-natural-gradient-em.md``). +wrapper over :class:`AMICATorchNG` (the default: the PyTorch natural-gradient +EM port that reaches Fortran parity; see +``.context/decisions/0001-torch-backend-natural-gradient-em.md``) or, with +``backend="mlx"``, :class:`pamica.mlx_impl.AMICAMLXNG` (the optional +Apple-GPU port, float32 only; issue #313). """ -import numpy as np -import torch -from pathlib import Path -from typing import Optional, Union +import functools +import importlib.util import inspect import logging +from pathlib import Path +from typing import TYPE_CHECKING, Any, Optional, Union + +import numpy as np +import torch from .torch_impl import AMICATorchNG, setup_device +if TYPE_CHECKING: + from .mlx_impl import AMICAMLXNG + logger = logging.getLogger(__name__) # AMICATorchNG's default parameter dtype, derived from its signature so the @@ -25,18 +34,18 @@ # fit()'s own named parameters that a parameter-file dict can default. _FIT_NAMED_PARAMS = {"max_iter", "lrate", "do_mean", "do_sphere", "do_newton"} -# AMICATorchNG constructor keywords eligible to be defaulted from a -# from_params_file dict via **kwargs (issue #132 review item 2). -# n_channels/n_models/n_mix/device are excluded because fit() always passes -# those positionally in the AMICATorchNG(...) call below; lrate/do_mean/ -# do_sphere/do_newton are excluded because, despite also being AMICATorchNG -# constructor parameters, fit() resolves and passes those as its own named -# arguments (_FIT_NAMED_PARAMS) -- merging them into **kwargs too would raise -# "got multiple values for keyword argument". -_NG_CTOR_PARAMS = ( - set(inspect.signature(AMICATorchNG).parameters) - - {"n_channels", "n_models", "n_mix", "device"} - - _FIT_NAMED_PARAMS +# The backends the wrapper can build (issue #313). "torch" is AMICATorchNG, +# the float64 Fortran-parity default; "mlx" is AMICAMLXNG, the optional +# Apple-GPU backend, which computes in float32 only. +_BACKENDS = ("torch", "mlx") + +# Constructor keywords only AMICATorchNG has: MLX runs on one device (MLX's +# default, the Apple GPU) at one precision (float32), so it takes neither. +_TORCH_ONLY_PARAMS = ("device", "dtype") + +_MLX_INSTALL_HINT = ( + "install it with `uv pip install mlx` or the `mlx` extra " + "(`pip install pamica[mlx]`); MLX runs on Apple Silicon only" ) # Sentinel distinguishing "caller did not pass this fit() argument" from @@ -46,14 +55,171 @@ _UNSET = object() +def _check_backend(backend: str, device) -> None: + """Validate a wrapper's backend choice (issue #313). + + Shared by :class:`AMICA` and :class:`pamica.mne_compat.AMICAICA`, so the + two wrappers accept and refuse exactly the same combinations. MLX's + availability is checked with :func:`importlib.util.find_spec`, which + locates the package without importing it, so neither ``import pamica`` nor + a torch-backed wrapper ever imports MLX. + + Raises + ------ + ValueError + If ``backend`` is not one of ``"torch"``/``"mlx"``, or ``device`` is + set with ``backend="mlx"``. + ImportError + If ``backend="mlx"`` and MLX is not installed. + """ + if backend not in _BACKENDS: + raise ValueError( + f"backend must be one of {', '.join(map(repr, _BACKENDS))}; " + f"got {backend!r}." + ) + if backend == "mlx": + if device is not None: + raise ValueError( + f"device={device!r} applies only to backend='torch'. The MLX " + "backend always runs on MLX's default device (the Apple GPU), " + "so leave device=None with backend='mlx'." + ) + if importlib.util.find_spec("mlx") is None: + raise ImportError( + f"backend='mlx' requires MLX, which is not installed; " + f"{_MLX_INSTALL_HINT}." + ) + + +def _mlx_backend_class() -> "type[AMICAMLXNG]": + """:class:`AMICAMLXNG`, imported on first use so ``import pamica`` never + imports MLX.""" + try: + from .mlx_impl import AMICAMLXNG + except ImportError as exc: + raise ImportError( + f"backend='mlx' requires MLX, which failed to import ({exc}); " + f"{_MLX_INSTALL_HINT}." + ) from exc + return AMICAMLXNG + + +def _backend_class(backend: str) -> "type[AMICATorchNG] | type[AMICAMLXNG]": + """The backend class a validated ``backend`` name selects.""" + return AMICATorchNG if backend == "torch" else _mlx_backend_class() + + +@functools.cache +def _ctor_params(backend_cls: type) -> frozenset: + """Constructor keywords ``fit()`` accepts from ``**kwargs`` and from a + parameter file, for one backend class (issue #132 review item 2, issue + #313). + + Derived from the class signature, so each backend gets exactly its own + keywords: a file setting only AMICATorchNG accepts is applied to a torch + fit and named in the "not applied" warning of an MLX fit. Excluded are + n_channels/n_models/n_mix/device, which fit() passes itself when it builds + the backend, and lrate/do_mean/do_sphere/do_newton, which are backend + constructor parameters too but which fit() resolves as its own named + arguments (_FIT_NAMED_PARAMS); taking them from **kwargs as well would + raise "got multiple values for keyword argument". + """ + return frozenset( + set(inspect.signature(backend_cls).parameters) + - {"n_channels", "n_models", "n_mix", "device"} + - _FIT_NAMED_PARAMS + ) + + +# AMICA.save's payload version (issue #313). Version 2 records the backend +# in wrapper["backend"]; version 1 (written before backend selection) is still +# read, and holds a torch state dict by construction. +_SAVE_FORMAT_VERSION = 2 +_LOADABLE_FORMAT_VERSIONS = (1, 2) + + +def _weights_only_safe(value, path: str): + """``value`` as something ``torch.load(weights_only=True)`` reads back. + + Tensors and plain Python primitives pass through, dicts/lists/tuples are + checked element by element, and numpy scalars (which the ``weights_only`` + unpickler refuses) become the equivalent Python number. Anything else + raises ``TypeError`` naming where it sits in the state, so :meth:`AMICA.save` + fails loudly instead of writing a file :meth:`AMICA.load` cannot open. + """ + if isinstance(value, np.generic): + return value.item() + if value is None or isinstance(value, (bool, int, float, str, torch.Tensor)): + return value + if isinstance(value, dict): + return { + key: _weights_only_safe(item, f"{path}[{key!r}]") + for key, item in value.items() + } + if isinstance(value, (list, tuple)): + return type(value)( + _weights_only_safe(item, f"{path}[{index}]") + for index, item in enumerate(value) + ) + raise TypeError( + f"AMICA.save cannot write {path} = {value!r} ({type(value).__name__}): " + "AMICA.load reads with torch.load(weights_only=True), which accepts " + "only tensors and plain Python primitives." + ) + + +def _state_to_payload(state: dict) -> dict: + """A backend ``state_dict`` in the form :meth:`AMICA.save` writes. + + AMICAMLXNG's params are numpy arrays, which the ``weights_only`` + unpickler refuses, so each becomes a CPU tensor of the same dtype (int64, + int32, bool and float32 all map exactly); :func:`_mlx_state_from_payload` + turns them back. AMICATorchNG's params are tensors already. Everything + else goes through :func:`_weights_only_safe`. + """ + payload = {} + for key, value in state.items(): + if key == "params": + payload[key] = { + name: torch.from_numpy(np.array(array)) + if isinstance(array, np.ndarray) + else _weights_only_safe(array, f"state['params'][{name!r}]") + for name, array in value.items() + } + else: + payload[key] = _weights_only_safe(value, f"state[{key!r}]") + return payload + + +def _mlx_state_from_payload(state: dict, filepath: str) -> dict: + """Invert :func:`_state_to_payload` for an AMICAMLXNG state: the params + back to numpy arrays of their original dtype.""" + if not isinstance(state.get("params"), dict): + raise ValueError( + f"malformed AMICA save file {filepath!r}: the MLX state has no " + "'params' section; the file may be truncated or corrupted." + ) + params = {} + for name, tensor in state["params"].items(): + if not isinstance(tensor, torch.Tensor): + raise ValueError( + f"malformed AMICA save file {filepath!r}: MLX param {name!r} is " + f"a {type(tensor).__name__}, not a tensor." + ) + params[name] = tensor.numpy() + return {**state, "params": params} + + class AMICA: """ - Adaptive Mixture ICA using the PyTorch natural-gradient EM backend. + Adaptive Mixture ICA over pamica's natural-gradient EM backends. This is the main interface for pamica, providing a scikit-learn style API over :class:`AMICATorchNG`, the natural-gradient EM implementation that matches the Fortran reference (Newton, exact-EM mixture updates, - symmetric-ZCA sphere, Jacobian LL). + symmetric-ZCA sphere, Jacobian LL), or, with ``backend="mlx"``, over its + Apple-GPU port :class:`pamica.mlx_impl.AMICAMLXNG` (issue #313). Every + method below behaves the same on both backends; only precision differs. Parameters ---------- @@ -66,14 +232,32 @@ class AMICA: (auto), an auto-selected MPS device is redirected to CPU because the backend computes in float64 for Fortran parity and MPS cannot represent it; pass ``dtype=torch.float32`` (with ``device="mps"``) to - run on MPS instead. + run on MPS instead. PyTorch backend only: with ``backend="mlx"`` it + must stay ``None``. verbose : bool, default=True Whether to show progress during fitting + backend : {"torch", "mlx"}, default="torch" + Which backend :meth:`fit` builds. ``"torch"`` is + :class:`AMICATorchNG`, float64 by default and the Fortran-parity + path. ``"mlx"`` is :class:`pamica.mlx_impl.AMICAMLXNG`, the fastest + option on Apple Silicon, which computes in float32 only (about 7 + significant digits, not float64 parity), so it takes neither + ``device`` nor ``dtype``. MLX is an optional dependency: ``"mlx"`` + without it raises ``ImportError`` here, and ``import pamica`` never + imports it. + + Raises + ------ + ValueError + If ``backend`` is not ``"torch"`` or ``"mlx"``, or ``device`` is set + with ``backend="mlx"``. + ImportError + If ``backend="mlx"`` and MLX is not installed. Attributes ---------- - model_ : AMICATorchNG - The underlying PyTorch model + model_ : AMICATorchNG or AMICAMLXNG + The underlying backend model is_fitted_ : bool Whether a *usable* model is available. ``fit`` sets this True only when the fit converged normally; a degenerate fit (see ``converged_``) leaves @@ -91,7 +275,9 @@ class AMICA: ``grad_norm_floor`` fire together as two halves of the same likelihood-decrease branch; ``min_dll``/``grad_norm`` are separate, unconditional per-iteration checks); only ``nan_ll``/``singular_ll`` - are degenerate (see ``converged_``). None of these checks short- + are degenerate (see ``converged_``), plus each backend's own further + markers (MLX's ``"nan_params"``, and under best-of-N restarts the + marker of a restart that raised). None of these checks short- circuits on an earlier one in the same iteration, so under the shipped ``use_grad_norm=True`` default ``"grad_norm"`` always takes precedence over ``"grad_norm_floor"`` when both would apply -- @@ -142,6 +328,9 @@ class AMICA: >>> >>> # Get mixing matrix >>> A = amica.get_mixing_matrix() + >>> + >>> # The same on the Apple GPU (requires the mlx extra) + >>> S_mlx = AMICA(backend="mlx").fit(X, max_iter=100).transform(X) """ def __init__( @@ -150,13 +339,16 @@ def __init__( n_mix: int = 3, device: Optional[Union[str, torch.device]] = None, verbose: bool = True, + backend: str = "torch", ): + _check_backend(backend, device) self.n_models = n_models self.n_mix = n_mix self.device = device self.verbose = verbose + self.backend = backend - self.model_ = None + self.model_: Optional[Union[AMICATorchNG, "AMICAMLXNG"]] = None self.is_fitted_ = False self.ll_history_ = [] self.final_ll_ = None @@ -172,15 +364,16 @@ def __init__( self.restart_stop_reasons_ = [] # Set by from_params_file (issue #132 review item 2): the full # translated parameter-file dict, applied by fit() as per-call - # defaults (an explicitly passed fit()/AMICATorchNG kwarg always - # wins). None for an instance built directly via AMICA(...). + # defaults (an explicitly passed fit()/backend kwarg always wins). + # None for an instance built directly via AMICA(...). self._file_params: Optional[dict] = None def _select_device(self, ng_dtype) -> Union[str, torch.device]: - """Resolve the compute device, applying the MPS/float64 fallback. + """Resolve the torch compute device, applying the MPS/float64 fallback. - ``AMICATorchNG`` defaults to float64 for Fortran parity, which MPS - cannot represent. When the device was auto-selected (the user did not + PyTorch backend only; MLX has no device choice. ``AMICATorchNG`` + defaults to float64 for Fortran parity, which MPS cannot represent. + When the device was auto-selected (the user did not pin one) and resolved to MPS for a float64 run, fall back to CPU so the default config runs instead of crashing. CUDA supports float64, so only MPS needs this. An explicit ``device="mps"`` is left untouched and @@ -236,14 +429,19 @@ def fit( disables the waypoints; see :meth:`AMICATorchNG.fit` for details and the interaction with ``keep_best``. **kwargs - Additional parameters passed to the :class:`AMICATorchNG` - constructor (e.g. ``block_size``, ``rho0``, ``seed``, ``dtype``, - ``use_min_dll``, ``min_dll``, ``maxincs``, ``use_grad_norm``, + Additional parameters passed to the backend constructor, + :class:`AMICATorchNG` or :class:`pamica.mlx_impl.AMICAMLXNG` + (the two take the same keywords, except that ``dtype`` is + PyTorch-only and raises ``ValueError`` with ``backend="mlx"``, + which is float32-only; e.g. ``block_size``, ``rho0``, ``seed``, + ``pcakeep``, ``use_min_dll``, ``min_dll``, ``maxincs``, ``use_grad_norm``, ``min_nd`` -- the issue #207 convergence stops, Fortran-faithful defaults ``True``/``1e-9``/``5``/``True``/``1e-7`` -- or ``do_opt_block``/``blk_min``/``blk_max``/``blk_step``, the issue #232 block-size search, off by default) -- the backend's tunables - are constructor arguments, not fit() kwargs. + are constructor arguments, not fit() kwargs. A keyword the + selected backend's constructor does not take raises + ``TypeError``. ``n_restarts`` (default 1) runs the fit from that many seeds and keeps the highest-likelihood one (issue #198), recording every @@ -256,8 +454,8 @@ def fit( Rank-deficient input (Maxwell-filtered MEG, average-referenced or interpolated EEG) is handled by ``mineig``/``mineig_rel`` (issue #223): the model is sized to the detected numerical rank and - :meth:`AMICATorchNG.get_sensor_mixing_matrix` maps components back - to input channels. ``mineig`` is an absolute eigenvalue floor and so + :meth:`get_sensor_mixing_matrix` maps components back to input + channels. ``mineig`` is an absolute eigenvalue floor and so unit-dependent; pass ``mineig_rel`` for data far from unit scale. When the instance was built via :meth:`from_params_file` (issue @@ -265,15 +463,25 @@ def fit( left unset here falls back to that file's translated value instead of the hard-coded default; an explicitly passed argument always wins over the file. Settings the file carries that match neither a - named ``fit()`` parameter nor an :class:`AMICATorchNG` constructor - keyword (data-location metadata like ``files``/``outdir``/ - ``data_dim``, or a setting with no pamica equivalent) are not - applied and are named in a single ``logger.warning``. + named ``fit()`` parameter nor a constructor keyword of the + selected backend (data-location metadata like ``files``/ + ``outdir``/``data_dim``, or a setting with no equivalent in that + backend) are not applied and are named in a single + ``logger.warning``. Returns ------- self : AMICA Fitted model + + Raises + ------ + ValueError + If ``X`` is not 2D, or ``dtype``/``device`` is passed with + ``backend="mlx"``. + TypeError + If a keyword is neither a ``fit()`` parameter nor a constructor + keyword of the selected backend. """ # Validate input if X.ndim != 2: @@ -281,13 +489,35 @@ def fit( n_channels, n_samples = X.shape + # Re-checked here, not only in __init__, so a backend/device attribute + # changed after construction cannot slip past the validation. + _check_backend(self.backend, self.device) + backend_cls = _backend_class(self.backend) + ctor_params = _ctor_params(backend_cls) + if self.backend == "mlx": + torch_only = sorted(set(kwargs) & set(_TORCH_ONLY_PARAMS)) + if torch_only: + raise ValueError( + f"{torch_only} apply only to backend='torch'. The MLX " + "backend computes in float32 only (Apple GPUs have no " + "float64) on MLX's default device; use backend='torch' " + "for float64 Fortran-parity runs." + ) + unknown = sorted(set(kwargs) - ctor_params) + if unknown: + raise TypeError( + f"AMICA.fit got unexpected keyword argument(s) {unknown}: " + "neither a fit() parameter nor a constructor keyword of " + f"{backend_cls.__name__}." + ) + # Apply from_params_file's translated dict as per-call defaults # (issue #132 review item 2): an explicitly passed argument here # always wins, whether named (max_iter/lrate/do_mean/do_sphere/ - # do_newton, via the _UNSET sentinel) or in **kwargs (AMICATorchNG - # constructor keywords, via plain dict membership). Settings the file - # carries that apply to neither surface are named in one warning - # rather than silently discarded. + # do_newton, via the _UNSET sentinel) or in **kwargs (the selected + # backend's constructor keywords, via plain dict membership). Settings + # the file carries that apply to neither surface are named in one + # warning rather than silently discarded. file_params = self._file_params or {} def _file_default(explicit, name, hard_default): @@ -303,18 +533,19 @@ def _file_default(explicit, name, hard_default): if file_params: for key, value in file_params.items(): - if key in _NG_CTOR_PARAMS and key not in kwargs: + if key in ctor_params and key not in kwargs: kwargs[key] = value - handled = _FIT_NAMED_PARAMS | _NG_CTOR_PARAMS | {"num_models", "num_mix"} + handled = _FIT_NAMED_PARAMS | ctor_params | {"num_models", "num_mix"} unhandled = sorted(set(file_params) - handled) if unhandled: logger.warning( "AMICA.fit: %d parameter-file setting(s) match neither a " - "fit()/AMICATorchNG parameter and were NOT applied " + "fit()/%s parameter and were NOT applied " "(informational only -- data-location metadata like " "files/outdir/data_dim/num_comps is expected here; " - "anything else means pamica has no equivalent): %s", + "anything else means this backend has no equivalent): %s", len(unhandled), + backend_cls.__name__, unhandled, ) @@ -322,8 +553,13 @@ def _file_default(explicit, name, hard_default): print(f"Fitting AMICA with {n_channels} channels, {n_samples} samples") print(f"Models: {self.n_models}, Mixture components: {self.n_mix}") - # Setup device (with the MPS/float64 parity fallback, see _select_device). - device = self._select_device(kwargs.get("dtype", _NG_DEFAULT_DTYPE)) + # Torch device (with the MPS/float64 parity fallback, see + # _select_device); MLX always runs on MLX's default device. + placement: dict[str, Any] = {} + if self.backend == "torch": + placement["device"] = self._select_device( + kwargs.get("dtype", _NG_DEFAULT_DTYPE) + ) # Build and train the backend on a LOCAL reference first, and only # publish it to self (and derive the fitted-state attributes) once @@ -333,7 +569,7 @@ def _file_default(explicit, name, hard_default): # raise a clean "not fitted"), and a refit keeps the previous, known-good # model rather than a half-trained one falsely marked usable (issue #50 # silent-failure review). - backend = AMICATorchNG( + backend = backend_cls( n_channels=n_channels, n_models=self.n_models, n_mix=self.n_mix, @@ -341,25 +577,13 @@ def _file_default(explicit, name, hard_default): do_mean=do_mean, do_sphere=do_sphere, do_newton=do_newton, - device=device, + **placement, **kwargs, ) backend.fit(X, max_iter=max_iter, verbose=self.verbose, mir_step=mir_step) self.model_ = backend - self.ll_history_ = backend.ll_history - self.final_ll_ = backend.final_ll_ - self.stop_reason_ = backend.stop_reason - self.mir_history_ = backend.mir_history_ - self.restart_seeds_ = backend.restart_seeds_ - self.restart_lls_ = backend.restart_lls_ - self.restart_stop_reasons_ = backend.restart_stop_reasons_ - self.converged_ = self.stop_reason_ not in AMICATorchNG._DEGENERATE_STOP_REASONS - # A degenerate fit (nan_ll/singular_ll) holds non-finite parameters and - # would return NaN sources, so it is not a usable model: is_fitted_ stays - # False and the output methods refuse it (issue #50). stop_reason_/ - # converged_ stay set for inspection. - self.is_fitted_ = self.converged_ + self._mirror_backend() if not self.converged_: logger.warning( "AMICA.fit ended degenerate (stop_reason=%r) at iteration %d: the " @@ -372,16 +596,44 @@ def _file_default(explicit, name, hard_default): return self + def _mirror_backend(self) -> None: + """Copy the fitted backend's record onto the wrapper. + + Shared by :meth:`fit` and :meth:`load`, so both backends and both + routes expose the same surface. A degenerate stop (one of the backend + class's own ``_DEGENERATE_STOP_REASONS``) holds non-finite parameters + and would return NaN sources, so it is not a usable model: + ``is_fitted_`` stays False and the output methods refuse it (issue + #50), while ``stop_reason_``/``converged_`` stay set for inspection. + """ + backend = self.model_ + assert backend is not None + self.ll_history_ = backend.ll_history + self.final_ll_ = backend.final_ll_ + self.stop_reason_ = backend.stop_reason + # mir_history_ is not persisted in state_dict() (a diagnostic + # trajectory, not a fitted parameter), so a loaded model's is always + # empty; exposed anyway for attribute-surface consistency. + self.mir_history_ = backend.mir_history_ + # Restart records are persisted by state_dict (issue #198), so a loaded + # best-of-N model can still say how its parameters were chosen. + self.restart_seeds_ = backend.restart_seeds_ + self.restart_lls_ = backend.restart_lls_ + self.restart_stop_reasons_ = backend.restart_stop_reasons_ + self.converged_ = self.stop_reason_ not in backend._DEGENERATE_STOP_REASONS + self.is_fitted_ = self.converged_ + def _check_usable(self, action: str) -> None: """Raise if the model cannot produce valid output: either never fitted, - or the fit ended degenerate (``nan_ll``/``singular_ll``), leaving - non-finite parameters that would yield NaN sources. This mirrors - :meth:`AMICATorchNG.state_dict`'s refusal to serialize a degenerate model - (issue #50): a diverged fit fails loudly here instead of silently + or the fit ended degenerate (a stop in the backend class's own + ``_DEGENERATE_STOP_REASONS``, such as ``nan_ll``/``singular_ll``), + leaving non-finite parameters that would yield NaN sources. This + mirrors the backends' ``state_dict`` refusal to serialize a degenerate + model (issue #50): a diverged fit fails loudly here instead of silently returning garbage.""" if self.model_ is None: raise ValueError(f"Model must be fitted before {action}.") - if self.stop_reason_ in AMICATorchNG._DEGENERATE_STOP_REASONS: + if self.stop_reason_ in self.model_._DEGENERATE_STOP_REASONS: raise RuntimeError( f"Refusing to {action}: fit ended degenerate " f"(stop_reason={self.stop_reason_!r}), so the model holds " @@ -389,6 +641,18 @@ def _check_usable(self, action: str) -> None: f"lrate, disable Newton, or check data conditioning, then refit." ) + def __repr__(self) -> str: + config = ( + f"backend={self.backend!r}, n_models={self.n_models}, n_mix={self.n_mix}" + ) + if self.model_ is None: + return f"" + if not self.converged_: + return ( + f"" + ) + return f"" + def transform(self, X: np.ndarray, model_idx: int = 0) -> np.ndarray: """ Transform data to source space. @@ -403,7 +667,8 @@ def transform(self, X: np.ndarray, model_idx: int = 0) -> np.ndarray: Returns ------- S : np.ndarray - Sources of shape (n_sources, n_samples) + Sources of shape (n_sources, n_samples): float64 from a default + PyTorch fit, float32 from an MLX fit (its only precision). """ self._check_usable("transform") assert self.model_ is not None @@ -467,6 +732,116 @@ def get_unmixing_matrix(self, model_idx: int = 0) -> np.ndarray: return self.model_.get_unmixing_matrix(model_idx=model_idx) + def get_sensor_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: + """ + Get the mixing matrix in input-channel space, ``pinv(sphere) @ A``. + + These are the scalp maps. Unlike :meth:`get_mixing_matrix` (sphered + space), they stay valid after rank reduction, where the sphere is + non-square (issue #223). + + Parameters + ---------- + model_idx : int, default=0 + Which model's maps to return + + Returns + ------- + A_sensor : np.ndarray + Mixing matrix of shape (n_channels_in, n_sources) + + Raises + ------ + ValueError + If the model is unfitted, or ``model_idx`` is out of range. + TypeError + If ``model_idx`` is not an integer. + RuntimeError + If the fit ended degenerate (issue #50). + """ + self._check_usable("get the sensor mixing matrix") + assert self.model_ is not None + + return self.model_.get_sensor_mixing_matrix(model_idx=model_idx) + + def get_sphere(self) -> np.ndarray: + """ + Get the fitted sphering matrix (issue #313). + + With :meth:`get_mean`, :meth:`get_model_center` and + :meth:`get_unmixing_matrix` it composes :meth:`transform`: + ``S = W @ (sphere @ (X - mean) - c)``. + + Returns + ------- + sphere : np.ndarray of float64 + Shape (n_sources, n_channels_in), square unless the fit was + rank-reduced. An MLX fit returns the float64 sphere its float32 + GPU copy was cast from. + + Raises + ------ + ValueError + If the model is unfitted. + RuntimeError + If the fit ended degenerate (issue #50). + """ + self._check_usable("get the sphere") + assert self.model_ is not None + + return self.model_.get_sphere() + + def get_mean(self) -> np.ndarray: + """ + Get the per-channel mean removed before sphering (issue #313). + + Returns + ------- + mean : np.ndarray of float64 + Shape (n_channels_in,); zeros for a ``do_mean=False`` fit, and + float32 values for an MLX fit. + + Raises + ------ + ValueError + If the model is unfitted. + RuntimeError + If the fit ended degenerate (issue #50). + """ + self._check_usable("get the mean") + assert self.model_ is not None + + return self.model_.get_mean() + + def get_model_center(self, model_idx: int = 0) -> np.ndarray: + """ + Get model ``model_idx``'s center ``c`` in the sphered space (issue #313). + + Parameters + ---------- + model_idx : int, default=0 + Which model's center to return + + Returns + ------- + c : np.ndarray of float64 + Shape (n_sources,); zeros for a single-model fit, and float32 + values for an MLX fit. + + Raises + ------ + ValueError + If the model is unfitted, or ``model_idx`` is out of range. + TypeError + If ``model_idx`` is not an integer. + RuntimeError + If the fit ended degenerate (issue #50). + """ + self._check_usable("get the model center") + assert self.model_ is not None + + return self.model_.get_model_center(model_idx=model_idx) + def mir( self, X: np.ndarray, model_idx: int = 0, nbins: Optional[int] = None ) -> tuple: @@ -499,7 +874,7 @@ def mir( (explicit ``pcakeep``/``pcadb`` or automatic ``mineig``/ ``mineig_rel`` detection), which leaves it rank-deficient so MIR's log-Jacobian term is undefined; or if ``X`` is non-finite or - has a constant channel. See :meth:`AMICATorchNG.mir` and + has a constant channel. See the backend's ``mir`` (:meth:`AMICATorchNG.mir`) and :func:`pamica.metrics.mir`. RuntimeError If the fit ended degenerate (issue #50), since the parameters are @@ -553,7 +928,8 @@ def pmi( def model_loglik(self, X: np.ndarray) -> np.ndarray: """Per-model, per-sample log-likelihood ``Lht`` on ``X`` (issue #141). - Delegates to :meth:`AMICATorchNG.model_loglik`. For a multi-model fit + Delegates to the backend's ``model_loglik`` (:meth:`AMICATorchNG.model_loglik`, + the same on either backend). For a multi-model fit this is the joint log-likelihood of each model at each sample, from which the per-sample model posterior (dominance) is ``softmax(Lht, axis=0)``; see :meth:`model_probability`. @@ -582,7 +958,8 @@ def model_loglik(self, X: np.ndarray) -> np.ndarray: def model_probability(self, X: np.ndarray) -> np.ndarray: """Per-sample posterior probability of each model (issue #141). - Delegates to :meth:`AMICATorchNG.model_probability`: the column-wise + Delegates to the backend's ``model_probability`` (see + :meth:`AMICATorchNG.model_probability`): the column-wise ``softmax`` over models of :meth:`model_loglik`, i.e. ``P(model h | x_t)``. Each column sums to 1; all ones for a single model. @@ -611,7 +988,8 @@ def model_probability(self, X: np.ndarray) -> np.ndarray: def get_pdftype(self, model_idx: int = 0) -> np.ndarray: """Per-source density-family code for model ``model_idx`` (issue #142). - Delegates to :meth:`AMICATorchNG.get_pdftype`. One integer per source + Delegates to the backend's ``get_pdftype`` (see + :meth:`AMICATorchNG.get_pdftype`). One integer per source component (0-4; see :data:`pamica.torch_impl.PDFTYPE_NAMES`). Returns @@ -626,7 +1004,7 @@ def get_pdftype(self, model_idx: int = 0) -> np.ndarray: def get_rho(self, model_idx: int = 0) -> np.ndarray: """Generalized-Gaussian shape ``rho`` for model ``model_idx`` (issue #142). - Delegates to :meth:`AMICATorchNG.get_rho`. + Delegates to the backend's ``get_rho`` (see :meth:`AMICATorchNG.get_rho`). Returns ------- @@ -640,7 +1018,8 @@ def get_rho(self, model_idx: int = 0) -> np.ndarray: def shared_components(self) -> list: """Components shared across models by ``share_comps`` (issue #142). - Delegates to :meth:`AMICATorchNG.shared_components`: one group of + Delegates to the backend's ``shared_components`` (see + :meth:`AMICATorchNG.shared_components`): one group of ``(model_idx, source_idx)`` pairs per shared column; empty when nothing is shared. """ @@ -712,29 +1091,46 @@ def save(self, filepath: str) -> None: """ Save the fitted model to ``filepath`` via ``torch.save``. - Persists the underlying :class:`AMICATorchNG` state (config + fitted - tensors) plus the wrapper's own configuration, so :meth:`load` can - fully reconstruct a transform-ready model. Everything written is a - tensor or plain Python primitive (see - :meth:`AMICATorchNG.state_dict`), so it reloads with - ``weights_only=True``. + Persists the backend's ``state_dict`` (config, fitted arrays and fit + record) plus the wrapper's own configuration, including which backend + built the model, so :meth:`load` can fully reconstruct a + transform-ready model of either backend. Everything written is a + tensor or a plain Python primitive, so the file reloads with + ``torch.load(weights_only=True)``: an MLX model's numpy arrays are + stored as CPU tensors of the same dtype, and numpy scalars in the + config or fit record (a ``seed=np.int64(...)``, say) as the equivalent + Python numbers. + + The file is ``format_version`` 2 (issue #313), which records the + backend; :meth:`load` still reads version 1 files, written before + backend selection existed. Parameters ---------- filepath : str Destination path (a ``.pt`` file by convention). + + Raises + ------ + TypeError + If the backend's state holds a value that ``weights_only`` loading + could not read back (anything but a tensor, an array or a plain + Python primitive), rather than writing a file :meth:`load` cannot + open. """ self._check_usable("save") assert self.model_ is not None + backend = "torch" if isinstance(self.model_, AMICATorchNG) else "mlx" payload = { - "format_version": 1, + "format_version": _SAVE_FORMAT_VERSION, "wrapper": { "n_models": self.n_models, "n_mix": self.n_mix, "verbose": self.verbose, + "backend": backend, }, - "backend": self.model_.state_dict(), + "backend": _state_to_payload(self.model_.state_dict()), } torch.save(payload, filepath) @@ -745,25 +1141,40 @@ def load( """ Load a fitted model saved by :meth:`save`. + The file records which backend built the model (``format_version`` + 2), and the model comes back on that backend. A ``format_version`` 1 + file, written before backend selection existed (issue #313), holds a + PyTorch model by construction and still loads. + Parameters ---------- filepath : str Path to a file written by :meth:`save`. device : str or torch.device, optional - Device to place the restored model on. With ``None`` (auto), the - same MPS/float64 fallback as :meth:`fit` applies so a float64 - parity model never lands on MPS. + Device to place a restored PyTorch model on. With ``None`` (auto), + the same MPS/float64 fallback as :meth:`fit` applies so a float64 + parity model never lands on MPS. An MLX model always loads onto + MLX's default device, so it must stay ``None`` for one. Returns ------- amica : AMICA A fitted model ready for :meth:`transform` / :meth:`get_mixing_matrix`. + + Raises + ------ + ValueError + If the file's ``format_version`` is not 1 or 2, a section is + missing, or ``device`` is set for an MLX model. + ImportError + If the file holds an MLX model and MLX is not installed. """ payload = torch.load(filepath, weights_only=True) version = payload.get("format_version") - if version != 1: + if version not in _LOADABLE_FORMAT_VERSIONS: raise ValueError( - f"unsupported AMICA save format_version: {version!r} (expected 1)" + f"unsupported AMICA save format_version: {version!r} (expected " + f"one of {list(_LOADABLE_FORMAT_VERSIONS)})" ) for key in ("wrapper", "backend"): if key not in payload: @@ -774,40 +1185,51 @@ def load( ) wrapper = payload["wrapper"] + # Version 1 predates backend selection: the wrapper could only build + # AMICATorchNG then, so every version 1 file is a torch payload. + backend = "torch" if version == 1 else wrapper.get("backend") + if backend not in _BACKENDS: + raise ValueError( + f"malformed AMICA save file {filepath!r}: wrapper['backend'] is " + f"{backend!r}, expected one of {list(_BACKENDS)} " + f"(format_version={version})." + ) + if backend == "mlx": + if device is not None: + raise ValueError( + f"{filepath!r} holds an MLX-backend model, which always " + f"loads onto MLX's default device; device={device!r} " + "applies only to PyTorch-backend models, so pass " + "device=None." + ) + if importlib.util.find_spec("mlx") is None: + raise ImportError( + f"{filepath!r} holds an MLX-backend model (backend='mlx'), " + f"but MLX is not installed; {_MLX_INSTALL_HINT}." + ) + model = cls( n_models=wrapper["n_models"], n_mix=wrapper["n_mix"], device=device, verbose=wrapper["verbose"], + backend=backend, ) - - # Resolve the device using the persisted backend dtype so the same - # MPS/float64 fallback as fit() applies to an auto-selected device. - ng_dtype = getattr(torch, payload["backend"]["config"]["dtype"]) - resolved_device = model._select_device(ng_dtype) - model.model_ = AMICATorchNG.from_state_dict( - payload["backend"], device=resolved_device - ) - model.ll_history_ = model.model_.ll_history - model.final_ll_ = model.model_.final_ll_ - # mir_history_ is not persisted in state_dict() (a diagnostic - # trajectory, not a fitted parameter), so a loaded model's is always - # empty; expose it anyway for attribute-surface consistency with - # ll_history_. - model.mir_history_ = model.model_.mir_history_ - # Restart records ARE persisted by state_dict (issue #198), so a loaded - # best-of-N model can still say how its parameters were chosen; a model - # saved before #198 simply has none. - model.restart_seeds_ = model.model_.restart_seeds_ - model.restart_lls_ = model.model_.restart_lls_ - model.restart_stop_reasons_ = model.model_.restart_stop_reasons_ - # state_dict() refuses to serialize a degenerate model, so a loaded model - # is always usable; carry its stop_reason through for inspection anyway. - model.stop_reason_ = model.model_.stop_reason - model.converged_ = ( - model.stop_reason_ not in AMICATorchNG._DEGENERATE_STOP_REASONS - ) - model.is_fitted_ = model.converged_ + state = payload["backend"] + if backend == "torch": + # Resolve the device using the persisted backend dtype so the same + # MPS/float64 fallback as fit() applies to an auto-selected device. + ng_dtype = getattr(torch, state["config"]["dtype"]) + resolved_device = model._select_device(ng_dtype) + model.model_ = AMICATorchNG.from_state_dict(state, device=resolved_device) + else: + model.model_ = _mlx_backend_class().from_state_dict( + _mlx_state_from_payload(state, filepath) + ) + # state_dict() refuses to serialize a degenerate model, so a loaded + # model is always usable; its stop_reason is carried through for + # inspection anyway. + model._mirror_backend() return model @classmethod @@ -840,14 +1262,21 @@ def from_params_file(cls, params_file: str, **kwargs) -> "AMICA": The full translated dict (beyond the ``n_models``/``n_mix`` used to size the instance here) is stashed on the returned instance and applied by :meth:`fit` as per-call defaults -- see ``fit``'s - docstring for the precedence rule. + docstring for the precedence rule. Which settings apply is decided + by the selected backend's own constructor signature, so + ``from_params_file(path, backend="mlx")`` drives + :class:`pamica.mlx_impl.AMICAMLXNG` from the same file (issue #313), + and :meth:`fit` names any setting that backend cannot take in its + "not applied" warning. Parameters ---------- params_file : str Path to a JSON or Fortran-format parameter file. **kwargs - Additional parameters to override. + Constructor arguments (``n_models``, ``n_mix``, ``device``, + ``verbose``, ``backend``); ``n_models``/``n_mix`` given here + override the file's. Returns ------- diff --git a/pamica/mlx_impl/core.py b/pamica/mlx_impl/core.py index 3c90d902..cf219056 100644 --- a/pamica/mlx_impl/core.py +++ b/pamica/mlx_impl/core.py @@ -3260,6 +3260,63 @@ def get_unmixing_matrix(self, model_idx: int = 0) -> np.ndarray: self._check_usable("get the unmixing matrix") return np.array(self.W[model_idx].T) + # ------------------------------------------------------------------ + # Preprocessing accessors (issue #313). Same names, shapes and float64 + # return type as AMICATorchNG's, so a consumer that composes the transform + # itself (the MNE export, pamica.mne_compat) reads every backend alike. + # ------------------------------------------------------------------ + def get_sphere(self) -> np.ndarray: + """Fitted sphering matrix, shape ``(n_channels, n_channels_in)`` + (port of ``AMICATorchNG.get_sphere``). + + Square for a full-rank fit and ``(n_kept, n_channels_in)`` after rank + reduction (issue #223). Read from ``_sphere_np``, the float64 host + copy: after :meth:`fit` it is the float64 sphere :meth:`_preprocess` + computed (the GPU computes with its float32 cast, which agrees to + float32 rounding), and after :meth:`load` it is the persisted float32 + sphere upcast (see :meth:`_load_params`). Returned as an independent + float64 copy. + """ + if self.sphere is None or self._sphere_np is None: + raise RuntimeError( + "AMICAMLXNG.get_sphere() requires a fitted model; call fit() first." + ) + self._check_usable("get the sphere") + return np.array(self._sphere_np, dtype=np.float64) + + def get_mean(self) -> np.ndarray: + """Per-channel mean removed before sphering, shape ``(n_channels_in,)`` + (port of ``AMICATorchNG.get_mean``). + + All zeros for a ``do_mean=False`` fit. The stored mean is float32 + (this backend's only precision); it is returned as an independent + float64 copy of those float32 values. + """ + if self.mean is None: + raise RuntimeError( + "AMICAMLXNG.get_mean() requires a fitted model; call fit() first." + ) + self._check_usable("get the mean") + return np.array(self.mean, dtype=np.float64).ravel() + + def get_model_center(self, model_idx: int = 0) -> np.ndarray: + """Model ``model_idx``'s center ``c`` in sphered space, shape + ``(n_channels,)`` (port of ``AMICATorchNG.get_model_center``). + + The per-model offset :meth:`transform` subtracts after sphering (issue + #27). Identically zero for a single-model fit, since the ``c`` update + is gated to ``n_models > 1``. Returned as an independent float64 copy + of the stored float32 values. + """ + if self.c is None: + raise RuntimeError( + "AMICAMLXNG.get_model_center() requires a fitted model; call " + "fit() first." + ) + self._check_model_idx(model_idx) + self._check_usable("get the model center") + return np.array(self.c[:, int(model_idx)], dtype=np.float64) + def get_rho(self, model_idx: int = 0) -> np.ndarray: """Generalized-Gaussian shape parameter ``rho`` for model ``model_idx`` (issue #287 port of ``AMICATorchNG.get_rho``; issue #142). @@ -3651,10 +3708,11 @@ def write_amica_output(self, outdir) -> None: Raises if the model is unfitted or degenerate (a fit that ended on a non-finite log-likelihood): a NaN model must not be written silently. - This backend has no scikit-learn-style wrapper in front of it (unlike - :class:`~pamica.AMICA`, which already refuses this via its own - usability gate for the PyTorch backend), so the guard has to live - here -- mirrors :meth:`state_dict`'s two-layer guard (stop_reason + The scikit-learn-style :class:`~pamica.AMICA` wrapper + (``backend="mlx"``, issue #313) already refuses this via its own + usability gate, but a caller using :class:`AMICAMLXNG` directly has + no such gate in front of this method, so the guard lives here too -- + mirrors :meth:`state_dict`'s two-layer guard (stop_reason refusal, then a defense-in-depth isfinite sweep over the parameter arrays) so the same protection applies to a direct ``write_amica_output`` call (PR #311 review). diff --git a/pamica/mne_compat/core.py b/pamica/mne_compat/core.py index 1a817e9b..345e8a7e 100644 --- a/pamica/mne_compat/core.py +++ b/pamica/mne_compat/core.py @@ -1,4 +1,4 @@ -"""MNE-Python-facing wrapper over pamica's AMICA backend (issue #139). +"""MNE-Python-facing wrapper over pamica's AMICA backends (issue #139). :class:`AMICAICA` fits AMICA directly from an :class:`mne.io.Raw` / :class:`mne.Epochs` and exposes the standard MNE ICA consumer surface @@ -22,6 +22,10 @@ :meth:`AMICAICA.fit` omits annotated samples by default (recorded in ``good_sample_mask_``), and the scoring methods evaluate the passed instance's good samples, keeping per-sample outputs timeline-aligned via ``NaN`` columns. +The fit runs on either :class:`~pamica.AMICA` backend (``backend="torch"``, the +default, or ``backend="mlx"``, issue #313); the export reads the fitted +preprocessing through the backend-agnostic float64 accessors +(``get_sphere``/``get_mean``/``get_model_center``), never a backend array. """ import logging @@ -36,7 +40,7 @@ import mne # ty: ignore[unresolved-import] from mne.preprocessing import ICA as _MNEICA # ty: ignore[unresolved-import] -from ..amica import AMICA +from ..amica import AMICA, _check_backend from ..torch_impl import PDFTYPE_NAMES __all__ = ["AMICAICA", "PDFTYPE_NAMES"] @@ -217,6 +221,10 @@ class AMICAICA: cannot hold -- source-density family, GG shape, component sharing -- is inspectable via :meth:`get_pdftype` / :meth:`get_rho` / :meth:`shared_components`. + The fit runs on the PyTorch backend by default and on the Apple-GPU MLX + backend with ``backend="mlx"`` (issue #313); see the precision note in the + Notes below. + Parameters ---------- n_models : int, default=1 @@ -229,8 +237,22 @@ class AMICAICA: device : str or torch.device, optional Torch device for the fit (``None`` = auto; the float64 parity backend falls back to CPU when auto-selection lands on MPS). See :class:`AMICA`. + PyTorch backend only: with ``backend="mlx"`` it must stay ``None``. verbose : bool, default=True Whether the underlying :class:`AMICA` prints fit progress. + backend : {"torch", "mlx"}, default="torch" + Which :class:`AMICA` backend fits the data, forwarded to + :class:`AMICA`. ``"mlx"`` is the Apple-GPU backend, float32 only; it + requires MLX (``ImportError`` here otherwise) and takes neither + ``device`` nor a ``dtype`` fit keyword. + + Raises + ------ + ValueError + If ``backend`` is not ``"torch"`` or ``"mlx"``, or ``device`` is set + with ``backend="mlx"``. + ImportError + If ``backend="mlx"`` and MLX is not installed. Attributes ---------- @@ -247,7 +269,7 @@ class AMICAICA: pre_whitener_ : np.ndarray of shape (n_channels, 1) Per-channel-type scaling applied before fitting, following MNE's own ICA convention (one ``std`` per channel type, applied as ``X / pre_whitener_``). - pca_components_ : np.ndarray of shape (n_channels, n_channels) + pca_components_ : np.ndarray of shape (n_channels, n_channels) or None The full orthonormal PCA basis of the fit, in pre-whitened channel space, computed once at fit time and exported as the MNE ICA's ``pca_components_``. The first ``n_components_`` rows span the @@ -255,11 +277,13 @@ class AMICAICA: the PCA residual that reduction discarded, which MNE's ``apply`` restores by default (issue #322); a full-rank fit has no such rows. Shared by every model of a multi-model fit (one sphere per fit). - pca_explained_variance_ : np.ndarray of shape (n_channels,) + ``None`` after a degenerate fit, which exports nothing. + pca_explained_variance_ : np.ndarray of shape (n_channels,) or None Variance of the pre-whitened fit data along each row of ``pca_components_``, in descending order: the retained rows carry the largest covariance eigenvalues and the residual rows the ones the - reduction discarded (clipped at 0 against round-off). + reduction discarded (clipped at 0 against round-off). ``None`` after + a degenerate fit. reject_by_annotation_ : bool Whether the last ``Raw`` fit dropped ``bad_*``-annotated samples (issue #251). Always ``False`` for an ``Epochs`` fit. @@ -314,6 +338,21 @@ class AMICAICA: reference's rank-reduced reconstruction pass MNE's own ``n_pca_components=ica.n_components_`` to :meth:`apply`. See ``docs/guides/amica-differences.md`` and ADR 0005. + + Precision: the export always reads the fit through the same float64 + accessors (``AMICA.get_sphere``/``get_mean``/``get_model_center``), so + both backends go through one code path. A PyTorch fit (float64 by + default) exports at float64 parity. An MLX fit computes in float32, so its + unmixing matrix, mean and centers carry float32 rounding (its sphere is + the float64 one it was built from), and the export is float32-consistent + rather than float64-parity: :meth:`get_sources` agrees with + ``amica_.transform`` (which runs in float32) to float32 tolerance, and + excluding a component changes the data by that component's + back-projection to the same tolerance. Reconstruction does not depend on + that precision: MNE's mixing is the float64 pseudo-inverse of the exported + unmixing and the PCA basis is orthonormal to float64 round-off, so + :meth:`apply` with nothing excluded returns the input to float64 + round-off on either backend, PCA residual included. """ def __init__( @@ -323,12 +362,16 @@ def __init__( random_state: Optional[int] = None, device: Optional[Union[str, torch.device]] = None, verbose: bool = True, + backend: str = "torch", ): + # The same checks, errors and messages as AMICA's own constructor. + _check_backend(backend, device) self.n_models = n_models self.n_mix = n_mix self.random_state = random_state self.device = device self.verbose = verbose + self.backend = backend self.amica_: Optional[AMICA] = None self.info_ = None @@ -396,12 +439,17 @@ def fit( Raises ------ TypeError - If ``inst`` is not an MNE ``Raw``/``Epochs``. + If ``inst`` is not an MNE ``Raw``/``Epochs``, or (from + :meth:`AMICA.fit`) a keyword in ``fit_kwargs`` is neither an + :meth:`AMICA.fit` parameter nor a constructor keyword of the + selected backend. ValueError If ``start``/``stop`` are given for ``Epochs``, ``stop`` exceeds the recording length, the selected data is non-finite, or no samples remain to fit (an empty ``start``/``stop`` range, or ``bad`` - annotations covering the entire selected range). + annotations covering the entire selected range); or (from + :meth:`AMICA.fit`) ``fit_kwargs`` carries ``dtype`` or + ``device`` with ``backend="mlx"``. """ if not isinstance(inst, (mne.io.BaseRaw, mne.BaseEpochs)): raise TypeError( @@ -478,22 +526,25 @@ def fit( n_mix=self.n_mix, device=self.device, verbose=self.verbose, + backend=self.backend, ) amica.fit(X, **fit_kwargs) + backend = amica.model_ + if backend is None: + raise RuntimeError("AMICAICA.fit: AMICA.fit returned without a model.") # The full PCA basis (retained rows, then the residual a rank-reduced # fit discarded), computed once here from the fitted sphere and the fit # data so nothing is recomputed from data at apply time (issue #322). # The sphere does not depend on the model, so one basis serves every - # per-model export. - backend = amica.model_ - if backend is None or backend.sphere is None: - raise RuntimeError( - "AMICAICA.fit: the backend returned without a fitted sphere, so " - "the PCA basis cannot be built." - ) - sphere = np.asarray(backend.sphere.cpu().numpy(), dtype=np.float64) - pca_components, pca_explained_variance = _pca_basis(sphere, X) + # per-model export. Read through the backend-agnostic float64 accessor + # (issue #313), which refuses a degenerate fit: such a fit is kept for + # inspection but never exported (_check_fitted refuses it), so it gets + # no basis. + pca_components: Optional[np.ndarray] = None + pca_explained_variance: Optional[np.ndarray] = None + if amica.converged_: + pca_components, pca_explained_variance = _pca_basis(amica.get_sphere(), X) # Publish to self only after every fallible step above succeeds, so a # failed (re)fit leaves the previously fitted state intact rather than a @@ -574,17 +625,12 @@ def to_mne_ica(self, model_idx: int = 0) -> _MNEICA: raise RuntimeError( "AMICAICA: internal state is inconsistent; refit before to_mne_ica()." ) - backend = amica.model_ - if backend.mean is None or backend.sphere is None or backend.c is None: - raise RuntimeError( - "AMICAICA: the fitted backend is missing mean/sphere/c; refit " - "before to_mne_ica()." - ) - - mean = backend.mean.cpu().numpy().ravel() - sphere = np.asarray(backend.sphere.cpu().numpy(), dtype=np.float64) + # The backend-agnostic float64 accessors (issue #313), identical for + # the PyTorch and MLX backends; see the class Notes on precision. + mean = amica.get_mean() + sphere = amica.get_sphere() w_fort = amica.get_unmixing_matrix(model_idx=model_idx) - c = backend.c.cpu().numpy()[:, model_idx] # per-model center (sphered space) + c = amica.get_model_center(model_idx=model_idx) # sphered space n_ch = sphere.shape[0] # The full PCA basis was computed once at fit time (see _pca_basis): the @@ -623,7 +669,7 @@ def to_mne_ica(self, model_idx: int = 0) -> _MNEICA: ica.pca_explained_variance_ = cov_evals ica.unmixing_matrix_ = unmixing ica.pre_whitener_ = self.pre_whitener_ - ica.n_iter_ = max(int(getattr(backend, "iteration", 0)), 1) + ica.n_iter_ = max(int(getattr(amica.model_, "iteration", 0)), 1) # MNE's own fit sets these; read_ica_eeglab (the precedent for building # an ICA from an external decomposition) sets reject_=None. Without them # ICA.save()/plot_properties raise AttributeError. @@ -979,16 +1025,17 @@ def _check_fitted(self, action: str = "this call") -> None: ) def __repr__(self) -> str: + config = ( + f"backend={self.backend!r}, n_models={self.n_models}, n_mix={self.n_mix}" + ) if self.amica_ is None: - return ( - f"" - ) + return f"" if not self.converged_: return ( f"" + f"{config}, {self._fit_kind})>" ) return ( - f"" + f"" ) diff --git a/pamica/tests/mne_tests/test_mne_backends.py b/pamica/tests/mne_tests/test_mne_backends.py new file mode 100644 index 00000000..7c690f4a --- /dev/null +++ b/pamica/tests/mne_tests/test_mne_backends.py @@ -0,0 +1,278 @@ +"""``AMICAICA`` on the MLX backend (issue #313, epic #324 Phase 4). + +``AMICAICA(backend="mlx")`` fits through ``AMICA(backend="mlx")`` and exports +through the same backend-agnostic float64 accessors as the PyTorch default +(``get_sphere``/``get_mean``/``get_model_center``). MLX computes in float32, so +the export is float32-consistent rather than float64-parity: sources and +component back-projections agree with the backend's own float32 ``transform`` +to float32 tolerance, while reconstruction (``apply`` with nothing excluded, +PCA residual included) is float64 round-off, because MNE's mixing is the +float64 pseudo-inverse of the exported unmixing and the PCA basis is +orthonormal. These tests pin both. + +Real sample EEG only: the bundled EEGLAB ``eeglab_data.set`` (32 channels, +30504 samples) through ``mne.io.read_raw_eeglab``, as in the other +``mne_tests`` modules. MLX tests skip individually without MLX or an Apple GPU; +the construction checks run everywhere. +""" + +import sys +from pathlib import Path + +import numpy as np +import pytest +import torch + +mne = pytest.importorskip("mne") + +from pamica.mne_compat import AMICAICA # noqa: E402 (after importorskip) + +mne.set_log_level("ERROR") + +SAMPLE_DIR = Path(__file__).resolve().parents[2] / "sample_data" +SET_FILE = SAMPLE_DIR / "eeglab_data.set" +SEED = 42 +MAX_ITER = 10 # the export is a function of the fitted state, not convergence +N_KEEP = 20 +NW = 32 + +# Reconstruction and anything ICA never touches: float64 round-off (measured +# 1.4e-15 for apply(), 1.8e-15 for the residual leak of an exclusion). +REL_TOL = 1e-10 +# What passes through MLX's float32 fit: sources against the float32 +# transform (measured 2.0e-7 reduced, 3.0e-7 full rank) and an exclusion +# against its float32 oracle (measured 1.9e-7). +FLOAT32_REL_TOL = 1e-5 + +pytestmark = pytest.mark.skipif( + not SET_FILE.exists(), reason="sample eeglab_data.set missing" +) + + +def _require_mlx(): + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + mx = mlx_core.mx + if mx.default_device().type != mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + return mlx_core.AMICAMLXNG + + +def _rel(a, b): + """Frobenius-norm relative difference of ``a`` from ``b``.""" + return np.linalg.norm(a - b) / np.linalg.norm(b) + + +@pytest.fixture(scope="module") +def raw(): + """Real continuous EEG as an MNE Raw (32 EEG channels, 128 Hz).""" + return mne.io.read_raw_eeglab(str(SET_FILE), preload=True) + + +@pytest.fixture(scope="module") +def mlx_keep(raw): + """An MLX fit reduced to 20 of 32 dimensions by an explicit pcakeep.""" + _require_mlx() + return AMICAICA(random_state=SEED, verbose=False, backend="mlx").fit( + raw, max_iter=MAX_ITER, pcakeep=N_KEEP + ) + + +@pytest.fixture(scope="module") +def torch_keep(raw): + """The PyTorch twin of ``mlx_keep`` (the default backend).""" + return AMICAICA(random_state=SEED, device="cpu", verbose=False).fit( + raw, max_iter=2, pcakeep=N_KEEP + ) + + +@pytest.fixture(scope="module") +def mlx_full(raw): + _require_mlx() + return AMICAICA(random_state=SEED, verbose=False, backend="mlx").fit( + raw, max_iter=MAX_ITER + ) + + +def _data(inst, fitted): + """The fitted channels of ``inst`` in original units.""" + return inst.get_data(picks=fitted.ch_names_) + + +# --- construction --------------------------------------------------------------- +def test_backend_is_validated_like_amica(): + with pytest.raises(ValueError, match="backend must be one of 'torch', 'mlx'"): + AMICAICA(backend="jax") + with pytest.raises(ValueError, match="applies only to backend='torch'"): + AMICAICA(backend="mlx", device="cpu") + assert AMICAICA().backend == "torch" + + +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_unfitted_repr_names_the_backend(backend): + if backend == "mlx": + _require_mlx() + assert repr(AMICAICA(n_models=2, backend=backend)) == ( + f"" + ) + + +def test_fitted_repr_names_the_backend(torch_keep, mlx_keep): + for fitted, backend in ((torch_keep, "torch"), (mlx_keep, "mlx")): + assert repr(fitted) == ( + f"" + ) + + +def test_mlx_without_mlx_raises_import_error(monkeypatch): + """``sys.modules[name] = None`` is Python's marker for an unimportable + module, so the real availability check sees what a machine without MLX + shows it.""" + monkeypatch.setitem(sys.modules, "mlx", None) + with pytest.raises(ImportError, match="uv pip install mlx"): + AMICAICA(backend="mlx") + + +def test_unknown_fit_keyword_propagates_from_amica(raw): + ica = AMICAICA(random_state=SEED, device="cpu", verbose=False) + with pytest.raises(TypeError, match=r"\['blocksize'\].*AMICATorchNG"): + ica.fit(raw, stop=2048, max_iter=1, blocksize=512) + assert ica.amica_ is None + + +def test_mlx_rejects_a_dtype(raw): + _require_mlx() + ica = AMICAICA(random_state=SEED, verbose=False, backend="mlx") + with pytest.raises(ValueError, match="apply only to backend='torch'"): + ica.fit(raw, stop=2048, max_iter=1, dtype=torch.float32) + assert ica.amica_ is None + + +# --- the MLX fit and its export --------------------------------------------------- +def test_mlx_fit_builds_the_mlx_backend(mlx_keep): + AMICAMLXNG = _require_mlx() + assert mlx_keep.backend == "mlx" + assert mlx_keep.amica_ is not None and mlx_keep.amica_.backend == "mlx" + assert type(mlx_keep.amica_.model_) is AMICAMLXNG + assert mlx_keep.converged_ is True + assert mlx_keep.n_components_ == N_KEEP + assert mlx_keep.pca_components_ is not None + assert mlx_keep.pca_components_.shape == (NW, NW) + ica = mlx_keep.to_mne_ica() + assert ica.n_components_ == N_KEEP + assert ica.get_components().shape == (NW, N_KEEP) + + +@pytest.mark.parametrize("fixture", ["mlx_keep", "mlx_full"]) +def test_get_sources_matches_the_mlx_transform(raw, fixture, request): + fitted = request.getfixturevalue(fixture) + s_mne = fitted.get_sources(raw).get_data() + s_mlx = fitted.amica_.transform(_data(raw, fitted) / fitted.pre_whitener_) + assert s_mne.shape == s_mlx.shape == (fitted.n_components_, raw.n_times) + assert _rel(s_mne, s_mlx) <= FLOAT32_REL_TOL + + +@pytest.mark.parametrize("fixture", ["mlx_keep", "mlx_full"]) +def test_apply_reproduces_the_input(raw, fixture, request): + """Nothing excluded: the input comes back to float64 round-off, the + discarded residual included, although MLX's mean and unmixing are float32.""" + fitted = request.getfixturevalue(fixture) + x = _data(raw, fitted) + out = _data(fitted.apply(raw.copy()), fitted) + assert _rel(out, x) <= REL_TOL + + +def test_exclude_removes_only_that_component(raw, mlx_keep): + """``exclude=[0]`` subtracts component 0's back-projection (oracle: the + backend's sphere and unmixing with its float32 transform) and leaves the + residual subspace untouched.""" + amica = mlx_keep.amica_ + ica = mlx_keep.to_mne_ica() + x = _data(raw, mlx_keep) + pw = mlx_keep.pre_whitener_ + change = _data(ica.apply(raw.copy(), exclude=[0]), mlx_keep) - x + w = amica.get_unmixing_matrix().astype(np.float64) + a_0 = (np.linalg.pinv(amica.get_sphere()) @ np.linalg.inv(w))[:, 0] + s_0 = amica.transform(x / pw)[0].astype(np.float64) + assert _rel(change, -pw * np.outer(a_0, s_0)) <= FLOAT32_REL_TOL + d = change / pw + residual_rows = ica.pca_components_[ica.n_components_ :] + assert np.linalg.norm(residual_rows @ d) <= REL_TOL * np.linalg.norm(d) + + +def test_saved_ica_applies_identically(raw, mlx_keep, tmp_path): + ica = mlx_keep.to_mne_ica() + fname = tmp_path / "amica-mlx-ica.fif" + ica.save(fname) + reloaded = mne.preprocessing.read_ica(fname) + assert reloaded.n_components_ == N_KEEP + for exclude in ([], [0]): + np.testing.assert_array_equal( + reloaded.apply(raw.copy(), exclude=exclude).get_data(), + ica.apply(raw.copy(), exclude=exclude).get_data(), + ) + + +def test_mlx_and_torch_export_the_same_basis(mlx_keep, torch_keep): + """Both backends build the PCA basis from their float64 sphere, so the + basis, the variances and the component count agree across backends.""" + assert torch_keep.n_components_ == mlx_keep.n_components_ == N_KEEP + ev_t = torch_keep.pca_explained_variance_ + p_m, p_t = mlx_keep.pca_components_, torch_keep.pca_components_ + assert ev_t is not None and p_m is not None and p_t is not None + np.testing.assert_allclose(mlx_keep.pca_explained_variance_, ev_t, rtol=1e-8) + # Rows are unique up to sign; compare the projectors onto each subspace. + for rows in (slice(0, N_KEEP), slice(N_KEEP, NW)): + np.testing.assert_allclose( + p_m[rows].T @ p_m[rows], p_t[rows].T @ p_t[rows], atol=1e-10 + ) + + +# --- a genuinely degenerate fit --------------------------------------------------- +# AMICAICA.fit rejects non-finite input, so the divergence comes through a fit +# keyword instead: invsigmin forces every source density's inverse scale to at +# least 1e6 on unit-variance sphered data, the densities collapse (the next +# log-likelihood is about -4e8) and the real fit path diverges. Measured over +# seeds 0/1/2/42, 4096 samples and the full recording, with and without +# pcakeep=20, max_iter=10: torch stops on nan_ll at iteration 3 and MLX on +# nan_params at iteration 2, 16 of 16 runs each. +INVSIGMIN, INVSIGMAX = 1e6, 1e7 + + +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_degenerate_fit_builds_no_basis_and_refuses_export(raw, backend): + kwargs = {"device": "cpu"} if backend == "torch" else {} + if backend == "mlx": + _require_mlx() + ica = AMICAICA(random_state=SEED, verbose=False, backend=backend, **kwargs) + ica.fit( + raw, + stop=4096, + max_iter=10, + pcakeep=N_KEEP, + invsigmin=INVSIGMIN, + invsigmax=INVSIGMAX, + ) + + assert ica.amica_ is not None and ica.amica_.model_ is not None + assert ica.stop_reason_ in ica.amica_.model_._DEGENERATE_STOP_REASONS + assert ica.converged_ is False + # Kept for inspection, but never exported, so no basis was built. + assert ica.pca_components_ is None + assert ica.pca_explained_variance_ is None + assert repr(ica) == ( + f"" + ) + with pytest.raises(RuntimeError, match="degenerate"): + ica.to_mne_ica() + with pytest.raises(RuntimeError, match="degenerate"): + ica.get_sources(raw) + # The converged-only gate is what skips the basis; the accessors it would + # have read refuse the same fit with the same meaning, on the wrapper and + # on the raw backend (issue #306). + for owner in (ica.amica_, ica.amica_.model_): + with pytest.raises(RuntimeError, match="degenerate"): + owner.get_sphere() diff --git a/pamica/tests/test_backend_accessors.py b/pamica/tests/test_backend_accessors.py new file mode 100644 index 00000000..6e883c06 --- /dev/null +++ b/pamica/tests/test_backend_accessors.py @@ -0,0 +1,205 @@ +"""``get_sphere``/``get_mean``/``get_model_center`` on every backend (issue #313). + +The three accessors let a consumer that composes the transform itself (the MNE +export in ``pamica.mne_compat``) read the preprocessing of any backend the same +way: same names, same shapes, float64 whatever the backend computes in. This is +the anti-drift guard required by ``.rules/backend_parity.md``: the PyTorch +checks always run, and the MLX ones (``pytest.importorskip`` plus an Apple-GPU +guard) compare MLX against PyTorch on the same data, seed and configuration. + +Real bundled sample EEG only (32 channels x 30504 frames), no synthetic data or +mocks (``.rules/testing.md``). +""" + +from pathlib import Path +from typing import Callable, Dict, Tuple + +import numpy as np +import pytest +import torch + +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +SEED = 42 +PCAKEEP = 20 +N_FRAMES = 8192 + +# The configurations compared: single-model full rank, single-model reduced +# (a non-square sphere), and two models (a nonzero per-model center c). The +# two-model fit runs one iteration, because multi-model trajectories separate +# quickly between float64 torch and float32 MLX (measured: the c columns +# agree to 4.7e-6 of their largest entry after one iteration, and only to +# 5.0e-3 after three), and this compares preprocessing plus one M-step, not +# convergence. +CONFIGS: Dict[str, Tuple[dict, int]] = { + "full": ({}, 3), + "reduced": ({"pcakeep": PCAKEEP}, 3), + "two_models": ({"n_models": 2}, 1), +} +N_KEPT = {"full": NW, "reduced": PCAKEEP, "two_models": NW} + +# MLX's get_sphere is its float64 host sphere, built by the same float64 +# eigendecomposition as torch's with a different LAPACK path (measured +# 1.4e-14 relative, 3.0e-14 reduced): the tolerances of +# test_pca_reduction_cross_backend.py's host-sphere comparison. +SPHERE_RTOL = 1e-10 +SPHERE_ATOL = 1e-13 +# MLX stores mean and c in float32, so they agree to float32 rounding for the +# mean (measured 4.0e-8 relative to its largest entry) and to one float32 +# M-step for c (measured 4.7e-6). +MEAN_TOL = 1e-6 +CENTER_TOL = 5e-5 + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +@pytest.fixture(scope="module") +def X() -> np.ndarray: + return load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD).astype( + np.float64 + )[:, :N_FRAMES] + + +def _mlx_core(): + """The MLX backend module, or skip this test (never the whole module).""" + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + mx = mlx_core.mx + if mx.default_device().type != mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + return mlx_core + + +def _torch(**kwargs) -> AMICATorchNG: + return AMICATorchNG( + n_channels=NW, seed=SEED, device="cpu", dtype=torch.float64, **kwargs + ) + + +def _mlx(**kwargs): + return _mlx_core().AMICAMLXNG(n_channels=NW, seed=SEED, **kwargs) + + +BUILDERS: Dict[str, Callable] = {"torch": _torch, "mlx": _mlx} + + +@pytest.fixture(scope="module") +def fits(X) -> Callable[[str, str], object]: + """Fit each (backend, config) pair once per module and share it.""" + cache: Dict[Tuple[str, str], object] = {} + + def get(backend: str, config: str): + if backend == "mlx": + _mlx_core() # skip before fitting, not after + key = (backend, config) + if key not in cache: + kwargs, max_iter = CONFIGS[config] + model = BUILDERS[backend](**kwargs) + model.fit(X, max_iter=max_iter, verbose=False) + assert model.stop_reason not in model._DEGENERATE_STOP_REASONS + cache[key] = model + return cache[key] + + return get + + +def _as_float64(values) -> np.ndarray: + """A backend array (torch tensor or MLX array) as a float64 numpy array.""" + if isinstance(values, torch.Tensor): + values = values.detach().cpu().numpy() + return np.array(values, dtype=np.float64) + + +# --- each backend: contract -------------------------------------------------- +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_unfitted_accessors_raise(backend): + model = BUILDERS[backend]() + for accessor in (model.get_sphere, model.get_mean, model.get_model_center): + with pytest.raises(RuntimeError, match="requires a fitted model"): + accessor() + + +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +@pytest.mark.parametrize("config", list(CONFIGS)) +def test_accessors_return_the_fitted_state_as_float64(fits, backend, config): + """Shapes, dtype and values: each accessor returns exactly the stored + array (MLX's sphere is its float64 host copy, the others float32 casts).""" + model = fits(backend, config) + n_kept = N_KEPT[config] + sphere = model.get_sphere() + mean = model.get_mean() + assert sphere.shape == (n_kept, NW) and sphere.dtype == np.float64 + assert mean.shape == (NW,) and mean.dtype == np.float64 + stored_sphere = model._sphere_np if backend == "mlx" else model.sphere + np.testing.assert_array_equal(sphere, _as_float64(stored_sphere)) + np.testing.assert_array_equal(mean, _as_float64(model.mean).ravel()) + c = _as_float64(model.c) + for h in range(model.n_models): + center = model.get_model_center(h) + assert center.shape == (n_kept,) and center.dtype == np.float64 + np.testing.assert_array_equal(center, c[:, h]) + # numpy integer indices are valid model indices, as for every accessor. + np.testing.assert_array_equal(model.get_model_center(np.int64(0)), c[:, 0]) + + +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_accessors_return_independent_copies(fits, backend): + """Writing into a returned array must not reach the fitted model.""" + model = fits(backend, "two_models") + for accessor in (model.get_sphere, model.get_mean, model.get_model_center): + before = accessor().copy() + accessor()[...] = np.nan + np.testing.assert_array_equal(accessor(), before) + + +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_model_center_validates_the_index(fits, backend): + model = fits(backend, "two_models") + with pytest.raises(ValueError, match="out of range"): + model.get_model_center(2) + with pytest.raises(ValueError, match="out of range"): + model.get_model_center(-1) + with pytest.raises(TypeError, match="model_idx must be an int"): + model.get_model_center(0.0) + + +def test_single_model_center_is_zero(fits): + """The c update is gated to n_models > 1, so this pins the documented zero.""" + np.testing.assert_array_equal( + fits("torch", "full").get_model_center(), np.zeros(NW) + ) + + +def test_float32_torch_fit_still_returns_float64(X): + model = AMICATorchNG(n_channels=NW, seed=SEED, device="cpu", dtype=torch.float32) + model.fit(X, max_iter=2, verbose=False) + for value in (model.get_sphere(), model.get_mean(), model.get_model_center()): + assert value.dtype == np.float64 + + +# --- anti-drift: MLX agrees with PyTorch --------------------------------------- +@pytest.mark.parametrize("config", list(CONFIGS)) +def test_mlx_accessors_agree_with_torch(fits, config): + t = fits("torch", config) + m = fits("mlx", config) + np.testing.assert_allclose( + m.get_sphere(), t.get_sphere(), rtol=SPHERE_RTOL, atol=SPHERE_ATOL + ) + t_mean = t.get_mean() + assert np.abs(m.get_mean() - t_mean).max() <= MEAN_TOL * np.abs(t_mean).max() + for h in range(t.n_models): + t_c = t.get_model_center(h) + m_c = m.get_model_center(h) + assert m_c.shape == t_c.shape + if t.n_models == 1: + np.testing.assert_array_equal(m_c, t_c) # both identically zero + continue + scale = np.abs(t_c).max() + assert scale > 0, f"model {h}: c should be nonzero for two models" + assert np.abs(m_c - t_c).max() <= CENTER_TOL * scale diff --git a/pamica/tests/test_backend_guards.py b/pamica/tests/test_backend_guards.py index bad64115..040657f3 100644 --- a/pamica/tests/test_backend_guards.py +++ b/pamica/tests/test_backend_guards.py @@ -124,6 +124,10 @@ def _fit_numpy(data: np.ndarray, tmp_path: Path, **kwargs) -> AMICA_NumPy: ("get_pdftype", lambda m, X: m.get_pdftype(), False), ("shared_components", lambda m, X: m.shared_components(), False), ("variance_order", lambda m, X: m.variance_order(), False), + # The preprocessing accessors (issue #313). + ("get_sphere", lambda m, X: m.get_sphere(), False), + ("get_mean", lambda m, X: m.get_mean(), False), + ("get_model_center", lambda m, X: m.get_model_center(model_idx=0), False), ("model_loglik", lambda m, X: m.model_loglik(X), True), ("model_probability", lambda m, X: m.model_probability(X), True), ("mir", lambda m, X: m.mir(X), True), diff --git a/pamica/tests/test_wrapper_backends.py b/pamica/tests/test_wrapper_backends.py new file mode 100644 index 00000000..e90f688f --- /dev/null +++ b/pamica/tests/test_wrapper_backends.py @@ -0,0 +1,720 @@ +"""The ``AMICA`` wrapper end to end on every backend it can build (issue #313). + +``AMICA(backend="torch")`` (the default) builds ``AMICATorchNG`` and +``AMICA(backend="mlx")`` builds ``AMICAMLXNG``; every wrapper method must work +on both. PyTorch always runs, so a torch regression cannot land; the MLX tests +skip individually (``pytest.importorskip`` plus an Apple-GPU guard) instead of +skipping the module, and compare MLX against PyTorch where the two should agree +(``.rules/backend_parity.md``). + +Real bundled sample EEG only (32 channels x 30504 frames), no synthetic data or +mocks (``.rules/testing.md``). Every fit is a few iterations: these are wiring, +persistence and agreement tests, not convergence tests. +""" + +import inspect +import logging +import re +import subprocess +import sys +from pathlib import Path +from typing import Any + +import numpy as np +import pytest +import torch + +from pamica import AMICA +from pamica.fortran_params import read_params_file +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +PARAM_FILE = SAMPLE_DIR / "input.param" +JSON_FILE = SAMPLE_DIR / "sample_params.json" +NW = 32 +FIELD = 30504 +SEED = 42 +PCAKEEP = 20 +N_FRAMES = 8192 # enough for a stable few-iteration fit, cheap on every backend +MAX_ITER = 3 +BACKENDS = ["torch", "mlx"] + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +@pytest.fixture(scope="module") +def real_data() -> np.ndarray: + return load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD).astype( + np.float64 + ) + + +@pytest.fixture(scope="module") +def X(real_data) -> np.ndarray: + return real_data[:, :N_FRAMES] + + +def _require_mlx(): + """The MLX backend class, or skip this test (never the whole module).""" + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + mx = mlx_core.mx + if mx.default_device().type != mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + return mlx_core.AMICAMLXNG + + +def _backend_class(backend: str) -> Any: + return AMICATorchNG if backend == "torch" else _require_mlx() + + +def _wrapper(backend: str, **kwargs) -> AMICA: + """An unfitted wrapper; torch pinned to the CPU so its fits are + deterministic (MLX has no device choice).""" + if backend == "torch": + kwargs.setdefault("device", "cpu") + else: + _require_mlx() + return AMICA(verbose=False, backend=backend, **kwargs) + + +@pytest.fixture(scope="module") +def fitted(X): + """One few-iteration wrapper fit per backend, shared across the module.""" + cache: dict = {} + + def get(backend: str) -> AMICA: + if backend not in cache: + model = _wrapper(backend) + model.fit(X, max_iter=MAX_ITER, seed=SEED) + cache[backend] = model + return cache[backend] + + return get + + +# --- pin: a format_version 1 save (written before issue #313) still loads ----- +def _v1_payload(model: AMICA) -> dict: + """The payload ``AMICA.save`` wrote before issue #313, key for key. + + Version 1 carried no backend name: the wrapper could only build + ``AMICATorchNG``, so every version 1 file holds a torch state dict. + """ + assert model.model_ is not None + return { + "format_version": 1, + "wrapper": { + "n_models": model.n_models, + "n_mix": model.n_mix, + "verbose": model.verbose, + }, + "backend": model.model_.state_dict(), + } + + +def test_v1_payload_still_loads_and_transforms_identically(X, tmp_path): + model = AMICA(device="cpu", verbose=False) + model.fit(X, max_iter=MAX_ITER, block_size=1024, seed=SEED) + path = tmp_path / "v1.pt" + torch.save(_v1_payload(model), path) + + loaded = AMICA.load(str(path), device="cpu") + + assert type(loaded.model_) is AMICATorchNG + assert loaded.backend == "torch" + assert loaded.is_fitted_ and loaded.converged_ + assert loaded.stop_reason_ == model.stop_reason_ + assert loaded.final_ll_ == model.final_ll_ + assert loaded.ll_history_ == model.ll_history_ + np.testing.assert_array_equal(loaded.transform(X), model.transform(X)) + + +# --- backend selection and its errors -------------------------------------------- +def test_unknown_backend_name_lists_the_valid_ones(): + with pytest.raises(ValueError, match="backend must be one of 'torch', 'mlx'"): + AMICA(backend="jax") + + +def test_device_with_the_mlx_backend_raises_at_construction(): + """Checked before MLX's availability, so it raises the same everywhere.""" + with pytest.raises( + ValueError, match="device='cpu' applies only to backend='torch'" + ): + AMICA(backend="mlx", device="cpu") + + +def test_mlx_backend_without_mlx_raises_import_error(monkeypatch): + """``sys.modules[name] = None`` is Python's own marker for a module that + cannot be imported: ``import mlx`` then raises ImportError and + ``importlib.util.find_spec("mlx")`` returns None, exactly as on a machine + without MLX (every CI runner but the macOS one). The wrapper's real + availability check runs unmodified.""" + monkeypatch.setitem(sys.modules, "mlx", None) + with pytest.raises(ImportError, match=r"uv pip install mlx.*`mlx` extra"): + AMICA(backend="mlx") + # The PyTorch default never looks for MLX. + assert AMICA().backend == "torch" + + +def test_import_pamica_never_imports_mlx(): + """Neither ``import pamica`` nor building the wrapper (either backend) + imports MLX; only fitting an MLX model does. A fresh interpreter, since + this process's other tests may already have imported it.""" + code = ( + "import importlib.util, sys\n" + "from pamica import AMICA\n" + "AMICA()\n" + "if importlib.util.find_spec('mlx') is not None:\n" + " AMICA(backend='mlx')\n" + "leaked = sorted(m for m in sys.modules if m == 'mlx' or m.startswith('mlx.'))\n" + "assert not leaked, leaked\n" + ) + subprocess.run([sys.executable, "-c", code], check=True) + + +def test_dtype_with_the_mlx_backend_raises_before_fitting(X): + model = _wrapper("mlx") + with pytest.raises(ValueError, match=r"\['dtype'\] apply only to backend='torch'"): + model.fit(X, max_iter=1, dtype=torch.float32) + assert model.model_ is None # nothing was built + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_unknown_fit_keyword_names_the_backend(X, backend): + model = _wrapper(backend) + name = _backend_class(backend).__name__ + message = rf"\['blocksize'\]: neither a fit\(\) parameter nor a constructor keyword of {name}\." + with pytest.raises(TypeError, match=message): + model.fit(X, max_iter=1, blocksize=512) + + +def test_backend_attribute_is_rechecked_at_fit(X): + """A backend changed after construction goes through the same checks.""" + model = AMICA(verbose=False) + model.backend = "jax" + with pytest.raises(ValueError, match="backend must be one of"): + model.fit(X, max_iter=1) + + +# --- the fitted surface, both backends ------------------------------------------- +@pytest.mark.parametrize("backend", BACKENDS) +def test_fit_publishes_the_backend_record(fitted, backend): + model = fitted(backend) + backend_model = model.model_ + assert type(backend_model) is _backend_class(backend) + assert model.is_fitted_ is True and model.converged_ is True + assert model.stop_reason_ == backend_model.stop_reason == "max_iter" + assert model.ll_history_ == backend_model.ll_history + assert len(model.ll_history_) == MAX_ITER + assert model.final_ll_ == backend_model.final_ll_ + assert model.restart_seeds_ == [SEED] + assert model.restart_lls_ == [model.final_ll_] + assert model.restart_stop_reasons_ == ["max_iter"] + assert model.mir_history_ == [] + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_every_delegating_method_works(fitted, X, backend, tmp_path): + """Each wrapper method returns the backend's own answer, shaped as + documented, on both backends.""" + model = fitted(backend) + b = model.model_ + assert b is not None + np.testing.assert_array_equal(model.transform(X), b.transform(X)) + assert model.transform(X).shape == (NW, N_FRAMES) + for name in ( + "get_mixing_matrix", + "get_unmixing_matrix", + "get_sensor_mixing_matrix", + "get_pdftype", + "get_rho", + "get_model_center", + "variance_order", + ): + np.testing.assert_array_equal( + getattr(model, name)(), getattr(b, name)(), err_msg=name + ) + np.testing.assert_array_equal(model.get_sphere(), b.get_sphere()) + np.testing.assert_array_equal(model.get_mean(), b.get_mean()) + assert model.get_sensor_mixing_matrix().shape == (NW, NW) + assert model.shared_components() == [] + np.testing.assert_array_equal(model.model_loglik(X), b.model_loglik(X)) + np.testing.assert_array_equal(model.model_probability(X), np.ones((1, N_FRAMES))) + mir_nats, _ = model.mir(X) + assert mir_nats == b.mir(X)[0] and np.isfinite(mir_nats) + np.testing.assert_array_equal(model.pmi(X), b.pmi(X)) + model.write_amica_output(str(tmp_path / "amicaout")) + assert (tmp_path / "amicaout" / "W").exists() + + # fit_transform on a fresh wrapper: the fit it runs is this fixture's, and + # it returns that model's own transform of the same data. + fresh = _wrapper(backend) + sources = fresh.fit_transform(X, max_iter=MAX_ITER, seed=SEED) + assert type(fresh.model_) is type(b) and fresh.is_fitted_ + np.testing.assert_array_equal(sources, fresh.transform(X)) + reference = model.transform(X) + if backend == "torch": + np.testing.assert_array_equal(sources, reference) # CPU: bit for bit + else: + assert np.abs(sources - reference).max() <= _float32_tol(reference) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_unfitted_accessors_raise_not_fitted(backend): + model = _wrapper(backend) + for accessor in ( + model.get_sphere, + model.get_mean, + model.get_model_center, + model.get_sensor_mixing_matrix, + ): + with pytest.raises(ValueError, match="fitted"): + accessor() + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_degenerate_fit_is_refused_on_both_backends(real_data, backend): + """The #50 contract: a NaN in real EEG drives a genuine degenerate stop + (torch ``nan_ll``, MLX ``nan_params``), which the wrapper classifies with + the backend class's own ``_DEGENERATE_STOP_REASONS`` and refuses to use.""" + backend_cls = _backend_class(backend) + bad = real_data[:, :4096].copy() + bad[0, 0] = np.nan + model = _wrapper(backend) + model.fit(bad, max_iter=3, seed=0) + assert model.stop_reason_ in backend_cls._DEGENERATE_STOP_REASONS + assert model.converged_ is False and model.is_fitted_ is False + assert repr(model) == ( + f"" + ) + for action in (model.transform, model.model_loglik): + with pytest.raises(RuntimeError, match="degenerate"): + action(real_data[:, :512]) + for accessor in (model.get_sphere, model.get_mean, model.get_model_center): + with pytest.raises(RuntimeError, match="degenerate"): + accessor() + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_repr_names_the_backend_and_state(fitted, backend): + assert repr(_wrapper(backend, n_models=2, n_mix=4)) == ( + f"" + ) + assert repr(fitted(backend)) == ( + f"" + ) + + +# --- from_params_file on MLX (issue #304's MLX path) --------------------------------- +def _not_applied_keys(caplog) -> tuple[str, list]: + """The backend named in, and the keys listed by, fit()'s one "not + applied" warning.""" + messages = [ + r.getMessage() for r in caplog.records if "NOT applied" in r.getMessage() + ] + assert len(messages) == 1, messages + match = re.search(r"fit\(\)/(\w+) parameter.*: \[(.*)\]$", messages[0]) + assert match is not None, messages[0] + keys = [k.strip().strip("'") for k in match.group(2).split(",")] + return match.group(1), keys + + +def test_params_file_drives_the_mlx_backend(X, caplog): + AMICAMLXNG = _require_mlx() + model = AMICA.from_params_file(str(PARAM_FILE), backend="mlx", verbose=False) + assert model.backend == "mlx" + with caplog.at_level(logging.WARNING, logger="pamica.amica"): + model.fit(X, max_iter=3, seed=SEED) + + b = model.model_ + assert b is not None and type(b) is AMICAMLXNG + # The file's values, chosen where they differ from AMICAMLXNG's defaults. + assert b.pcakeep == 32 and b.pcadb == 30.0 + assert b.block_size == 512 + assert b.do_newton is True and b.newt_start == 50 and b.newtrate0 == 1.0 + assert b.lrate0 == 0.05 and b.minlrate == 1e-8 + assert b.maxdecs == 3 and b.rholratefact == 0.5 and b.invsigmax == 100.0 + assert b.mineig == 1e-12 + assert b.n_channels == NW # pcakeep 32 on 32 channels reduces nothing + assert model.is_fitted_ + + backend_name, keys = _not_applied_keys(caplog) + assert backend_name == "AMICAMLXNG" + mlx_params = set(inspect.signature(AMICAMLXNG).parameters) + assert keys and not set(keys) & mlx_params, keys + # Every file key MLX lacks is named: the partition is AMICAMLXNG's own. + file_keys = set(read_params_file(PARAM_FILE)) + fit_named = {"max_iter", "lrate", "do_mean", "do_sphere", "do_newton"} + expected = file_keys - mlx_params - fit_named - {"num_models", "num_mix"} + assert set(keys) == expected + + +def test_json_params_file_drives_the_mlx_backend(X, caplog): + """pamica's JSON schema through the MLX backend, including the JSON-only + spellings the shared reader translates (max_decs -> maxdecs, + min_grad_norm -> min_nd, share_int -> share_iter, issue #304).""" + AMICAMLXNG = _require_mlx() + model = AMICA.from_params_file(str(JSON_FILE), backend="mlx", verbose=False) + assert model.backend == "mlx" + with caplog.at_level(logging.WARNING, logger="pamica.amica"): + model.fit(X, max_iter=3, seed=SEED) + + b = model.model_ + assert b is not None and type(b) is AMICAMLXNG + assert b.pcakeep == 32 and b.pcadb == 30.0 and b.block_size == 512 + assert b.do_newton is True and b.newt_start == 50 and b.newtrate0 == 1.0 + assert b.maxdecs == 3 and b.min_nd == 1e-7 and b.share_iter == 100 + assert b.maxrej == 3 and b.rholratefact == 0.5 and b.invsigmax == 100.0 + assert model.is_fitted_ + + backend_name, keys = _not_applied_keys(caplog) + assert backend_name == "AMICAMLXNG" + mlx_params = set(inspect.signature(AMICAMLXNG).parameters) + assert keys and not set(keys) & mlx_params, keys + assert {"files", "outdir", "data_dim", "num_comps"} <= set(keys) + for translated in ("max_decs", "min_grad_norm", "share_int"): + assert translated not in keys + + +def test_params_file_warning_names_the_torch_backend(X, caplog): + """The same file on the default backend: partitioned by AMICATorchNG.""" + model = AMICA.from_params_file(str(PARAM_FILE), device="cpu", verbose=False) + with caplog.at_level(logging.WARNING, logger="pamica.amica"): + model.fit(X, max_iter=1, seed=SEED) + backend_name, keys = _not_applied_keys(caplog) + assert backend_name == "AMICATorchNG" + assert not set(keys) & set(inspect.signature(AMICATorchNG).parameters) + + +# --- pcakeep through the wrapper on MLX (#323's remaining bullet) --------------------- +def test_pcakeep_through_the_mlx_wrapper(X): + _require_mlx() + model = _wrapper("mlx") + model.fit(X, max_iter=MAX_ITER, seed=SEED, pcakeep=PCAKEEP) + assert model.model_ is not None and model.model_.n_channels == PCAKEEP + assert model.transform(X).shape == (PCAKEEP, N_FRAMES) + assert model.get_sensor_mixing_matrix().shape == (NW, PCAKEEP) + assert model.get_sphere().shape == (PCAKEEP, NW) + assert model.get_model_center().shape == (PCAKEEP,) + + +def test_mir_step_gate_through_the_mlx_wrapper(X): + """The upfront gate rejects a real reduction request and accepts the + bundled files' pcakeep=32 on 32 channels, which reduces nothing.""" + _require_mlx() + model = _wrapper("mlx") + with pytest.raises(ValueError, match="Rejected up front"): + model.fit(X, max_iter=2, seed=SEED, pcakeep=PCAKEEP, mir_step=1) + assert model.model_ is None # a failed fit publishes nothing + model.fit(X, max_iter=2, seed=SEED, pcakeep=NW, mir_step=1) + assert [row[0] for row in model.mir_history_] == [0, 1] + assert all(np.isfinite(row[1]) for row in model.mir_history_) + + +# --- save/load: format_version 2 records the backend ----------------------------- +def _float32_tol(reference: np.ndarray) -> float: + """Normwise float32 tolerance: a few units of float32 rounding (2**-24 + relative) on the largest entry.""" + return 8 * 2.0**-24 * float(np.abs(reference).max()) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_save_load_round_trip(fitted, X, backend, tmp_path): + """fit -> transform -> save -> load -> transform, bit for bit on both + backends: the loaded parameters are the saved ones exactly, and each + backend's transform is deterministic on the same parameters.""" + model = fitted(backend) + path = tmp_path / f"{backend}.pt" + model.save(str(path)) + + payload = torch.load(path, weights_only=True) + assert payload["format_version"] == 2 + assert payload["wrapper"]["backend"] == backend + assert all( + isinstance(t, torch.Tensor) for t in payload["backend"]["params"].values() + ) + + loaded = AMICA.load(str(path)) + assert loaded.backend == backend + assert type(loaded.model_) is _backend_class(backend) + assert loaded.is_fitted_ is True and loaded.converged_ is True + assert loaded.stop_reason_ == model.stop_reason_ + assert loaded.final_ll_ == model.final_ll_ + assert loaded.ll_history_ == model.ll_history_ + assert loaded.restart_seeds_ == model.restart_seeds_ + assert loaded.restart_lls_ == model.restart_lls_ + assert loaded.restart_stop_reasons_ == model.restart_stop_reasons_ + np.testing.assert_array_equal(loaded.transform(X), model.transform(X)) + for name in ( + "get_mixing_matrix", + "get_unmixing_matrix", + "get_mean", + "get_model_center", + ): + np.testing.assert_array_equal( + getattr(loaded, name)(), getattr(model, name)(), err_msg=name + ) + if backend == "torch": + np.testing.assert_array_equal(loaded.get_sphere(), model.get_sphere()) + else: + # Only MLX's float32 sphere is persisted: a reloaded model's float64 + # host copy is that sphere upcast, not the fit's float64 original. + mlx_sphere = np.array(model.model_.sphere, dtype=np.float64) + np.testing.assert_array_equal(loaded.get_sphere(), mlx_sphere) + original = model.get_sphere() + assert np.abs(loaded.get_sphere() - original).max() <= _float32_tol(original) + + +def test_mlx_params_round_trip_with_their_dtypes(fitted, tmp_path): + """Every MLX param comes back with its original dtype and values + (float32, and the integer comp_list/pdtype).""" + model = fitted("mlx") + path = tmp_path / "mlx.pt" + model.save(str(path)) + loaded = AMICA.load(str(path)) + assert model.model_ is not None and loaded.model_ is not None + before = model.model_.state_dict() + after = loaded.model_.state_dict() + assert {a.dtype for a in before["params"].values()} >= { + np.dtype(np.float32), + np.dtype(np.int64), + np.dtype(np.int32), + } + for name, array in before["params"].items(): + assert after["params"][name].dtype == array.dtype, name + np.testing.assert_array_equal(after["params"][name], array, err_msg=name) + assert after["config"] == before["config"] + assert after["extra"] == before["extra"] + + +def test_mlx_bool_array_survives_the_payload_conversion(fitted, tmp_path): + """No MLX param is boolean today; this pins the conversion for one anyway, + using MLX's own real boolean mask (comp_used), through a real + torch.save/torch.load(weights_only=True) round trip.""" + from pamica.amica import _mlx_state_from_payload, _state_to_payload + + b = fitted("mlx").model_ + state = b.state_dict() + comp_used = np.array(b.comp_used) + assert comp_used.dtype == np.bool_ + state["params"]["comp_used"] = comp_used + path = tmp_path / "state.pt" + torch.save(_state_to_payload(state), path) + restored = _mlx_state_from_payload(torch.load(path, weights_only=True), str(path)) + for name, array in state["params"].items(): + assert restored["params"][name].dtype == array.dtype, name + np.testing.assert_array_equal(restored["params"][name], array, err_msg=name) + + +@pytest.mark.parametrize( + ("section", "key", "value"), + [ + ("config", "restart_seeds", {SEED, SEED + 1}), # a set + ("extra", "good_idx", np.arange(4)), # a bare array outside params + ], +) +def test_unsavable_state_value_names_its_key_path(fitted, section, key, value): + """A value torch.load(weights_only=True) could not read back fails at + save time, naming where it sits, instead of writing an unloadable file. + The state is a real fitted torch model's; one entry is replaced.""" + from pamica.amica import _state_to_payload + + b = fitted("torch").model_ + assert b is not None + state = b.state_dict() + state[section][key] = value + path = re.escape(f"state[{section!r}][{key!r}]") + with pytest.raises(TypeError, match=rf"cannot write {path} = .*weights_only"): + _state_to_payload(state) + + +def test_weights_only_safe_converts_scalars_and_names_nested_paths(): + """Called directly: numpy scalars become Python numbers of the matching + type, and a rejected value deep in a list is named down to its index.""" + from pamica.amica import _weights_only_safe + + converted = _weights_only_safe( + {"seed": np.int64(3), "tol": np.float32(0.5), "flag": np.bool_(True)}, + "state['config']", + ) + assert converted == {"seed": 3, "tol": 0.5, "flag": True} + assert [type(v) for v in converted.values()] == [int, float, bool] + path = re.escape("state['extra']['restart_seeds_'][1] = {3} (set)") + with pytest.raises(TypeError, match=path): + _weights_only_safe({"restart_seeds_": [0, {3}]}, "state['extra']") + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_numpy_scalar_settings_save_and_load(X, backend, tmp_path): + """A numpy-integer seed lands in the backend's config and fit record, + which the weights_only unpickler refuses; save stores it as a Python int + so the file loads.""" + model = _wrapper(backend) + model.fit(X[:, :4096], max_iter=2, seed=np.int64(SEED)) + path = tmp_path / "np_seed.pt" + model.save(str(path)) + loaded = AMICA.load(str(path)) + assert loaded.model_ is not None + assert loaded.model_.seed == SEED and type(loaded.model_.seed) is int + assert loaded.restart_seeds_ == [SEED] + np.testing.assert_array_equal(loaded.transform(X), model.transform(X)) + + +def test_v2_payload_without_a_backend_is_malformed(fitted, tmp_path): + path = tmp_path / "model.pt" + fitted("torch").save(str(path)) + payload = torch.load(path, weights_only=True) + del payload["wrapper"]["backend"] + torch.save(payload, path) + with pytest.raises(ValueError, match=r"malformed.*wrapper\['backend'\] is None"): + AMICA.load(str(path)) + + +def test_unknown_format_version_names_the_readable_ones(fitted, tmp_path): + path = tmp_path / "model.pt" + fitted("torch").save(str(path)) + payload = torch.load(path, weights_only=True) + payload["format_version"] = 3 + torch.save(payload, path) + with pytest.raises( + ValueError, match=r"format_version: 3 \(expected one of \[1, 2\]\)" + ): + AMICA.load(str(path)) + + +def _labeled_mlx_payload(fitted, tmp_path) -> Path: + """A real torch-fitted save relabeled ``wrapper["backend"] = "mlx"``. + + Both checks below fire on the recorded backend name before any parameter + is read, so the relabeled file exercises them on every machine, MLX or not. + """ + path = tmp_path / "labeled_mlx.pt" + fitted("torch").save(str(path)) + payload = torch.load(path, weights_only=True) + payload["wrapper"]["backend"] = "mlx" + torch.save(payload, path) + return path + + +def test_mlx_payload_with_a_device_raises(fitted, tmp_path): + path = _labeled_mlx_payload(fitted, tmp_path) + with pytest.raises(ValueError, match="holds an MLX-backend model.*device=None"): + AMICA.load(str(path), device="cpu") + + +def test_mlx_payload_without_mlx_names_the_backend(fitted, tmp_path, monkeypatch): + """Written on a machine with MLX, opened on one without (the + ``sys.modules`` marker, as in the construction test above).""" + path = _labeled_mlx_payload(fitted, tmp_path) + monkeypatch.setitem(sys.modules, "mlx", None) + with pytest.raises(ImportError, match="holds an MLX-backend model.*not installed"): + AMICA.load(str(path)) + + +# --- EEGLAB export from an MLX fit -------------------------------------------------- +@pytest.mark.parametrize("pcakeep", [None, PCAKEEP]) +def test_mlx_eeglab_export_reloads_through_loadmodout(X, tmp_path, pcakeep): + """The wrapper's write_amica_output on an MLX fit is what EEGLAB reads: + shapes as loadmodout15 expects them (a reduced sphere padded to the + Fortran nx x nx record), the written sphere and mean exactly the model's, + and the reloaded sources and maps the live ones up to loadmodout's + variance order and per-component scale.""" + from pamica.numpy_impl.load import loadmodout + + model = _wrapper("mlx") + model.fit(X, max_iter=MAX_ITER, seed=SEED, pcakeep=pcakeep) + n = NW if pcakeep is None else pcakeep + outdir = tmp_path / "amicaout" + model.write_amica_output(str(outdir)) + out = loadmodout(outdir) + + assert (out.num_pcs, out.data_dim, out.num_models) == (n, NW, 1) + assert out.W.shape == (n, n, 1) + assert out.S.shape == (NW, NW) + assert out.A.shape == (NW, n, 1) + assert model.model_ is not None + np.testing.assert_array_equal( + out.S[:n], np.array(model.model_.sphere, dtype=np.float64) + ) + np.testing.assert_array_equal(out.S[n:], 0.0) + np.testing.assert_array_equal(np.ravel(out.data_mean), model.get_mean()) + np.testing.assert_array_equal(out.LL, model.ll_history_) + + # loadmodout reorders by back-projected variance (origord) and rescales + # each component; within that, sources and sensor maps must be the live + # ones to float32 tolerance (measured 2.8e-7 / 9.4e-8 full rank). + order = np.asarray(out.origord).ravel() + assert sorted(order.tolist()) == list(range(n)) + for loaded, live in ( + (out.sources(X), model.transform(X).astype(np.float64)[order]), + (out.A[:, :, 0].T, model.get_sensor_mixing_matrix()[:, order].T), + ): + scale = (loaded * live).sum(axis=1) / (live**2).sum(axis=1) + residual = loaded - scale[:, None] * live + assert np.linalg.norm(residual) <= 1e-5 * np.linalg.norm(loaded) + + +# --- torch and MLX through the wrapper agree ------------------------------------------ +@pytest.mark.parametrize("pcakeep", [None, PCAKEEP]) +def test_torch_and_mlx_wrappers_find_the_same_components(real_data, pcakeep): + """Same data, seed and configuration through AMICA(backend=...): the same + number of components, the same sources (Hungarian-matched |corr| at the + bar of test_pca_reduction_cross_backend.py, which this reuses), and the + same log-likelihood to MLX's float32 bar. Measured on the full sample, 10 + iterations: min matched |corr| 0.99999996 full rank / 0.99999993 at + pcakeep=20, identity matching, LL difference 1.3e-6 / 4.8e-6.""" + from pamica.tests.test_pca_reduction_cross_backend import _matched_abs_corr + + kwargs = {} if pcakeep is None else {"pcakeep": pcakeep} + t = _wrapper("torch") + t.fit(real_data, max_iter=10, seed=SEED, **kwargs) + m = _wrapper("mlx") + m.fit(real_data, max_iter=10, seed=SEED, **kwargs) + + n = NW if pcakeep is None else pcakeep + assert t.model_ is not None and m.model_ is not None + assert t.model_.n_channels == m.model_.n_channels == n + s_t, s_m = t.transform(real_data), m.transform(real_data) + assert s_t.shape == s_m.shape == (n, FIELD) + matched, cols = _matched_abs_corr(s_t, s_m) + assert matched.min() >= 0.999, f"min matched |corr| {matched.min():.6f}" + np.testing.assert_array_equal(cols, np.arange(n)) # same seed, same order + assert t.final_ll_ is not None and m.final_ll_ is not None + assert abs(t.final_ll_ - m.final_ll_) < 1e-2 + + +def test_wrapper_accessors_agree_across_backends(fitted): + """The wrapper's get_sphere/get_mean/get_model_center return the same + shapes on both backends and agree within float32 tolerance (the sphere + closer still: both are float64 computations of the same eigenproblem).""" + t, m = fitted("torch"), fitted("mlx") + np.testing.assert_allclose(m.get_sphere(), t.get_sphere(), rtol=1e-10, atol=1e-13) + t_mean = t.get_mean() + assert m.get_mean().shape == t_mean.shape == (NW,) + assert np.abs(m.get_mean() - t_mean).max() <= _float32_tol(t_mean) + np.testing.assert_array_equal(m.get_model_center(), t.get_model_center()) + assert all( + a.dtype == np.float64 + for model in (t, m) + for a in (model.get_sphere(), model.get_mean(), model.get_model_center()) + ) + + +@pytest.mark.parametrize("damage", ["missing_params", "non_tensor_param"]) +def test_malformed_mlx_payload_raises_a_named_error(fitted, tmp_path, damage): + path = tmp_path / "mlx.pt" + fitted("mlx").save(str(path)) + payload = torch.load(path, weights_only=True) + if damage == "missing_params": + del payload["backend"]["params"] + expected = "no 'params' section" + else: + payload["backend"]["params"]["A"] = [1.0, 2.0] + expected = "MLX param 'A' is a list, not a tensor" + torch.save(payload, path) + with pytest.raises(ValueError, match=f"malformed AMICA save file.*{expected}"): + AMICA.load(str(path)) diff --git a/pamica/tests/torch_tests/test_amica_ng_wrapper.py b/pamica/tests/torch_tests/test_amica_ng_wrapper.py index 6b0bd30a..afbf69ef 100644 --- a/pamica/tests/torch_tests/test_amica_ng_wrapper.py +++ b/pamica/tests/torch_tests/test_amica_ng_wrapper.py @@ -112,7 +112,7 @@ def test_ng_default_device_avoids_mps_float64(real_data, caplog): model.fit(real_data[:, :2048], max_iter=2, block_size=1024, seed=42) # float64 parity runs must never land on MPS. - assert model.model_ is not None + assert isinstance(model.model_, AMICATorchNG) assert model.model_.device.type in ("cpu", "cuda") if torch.backends.mps.is_available(): assert model.model_.device.type == "cpu" @@ -140,7 +140,7 @@ def test_ng_mps_float32_escape_hatch(real_data): model.fit( real_data[:, :2048], max_iter=2, block_size=1024, seed=42, dtype=torch.float32 ) - assert model.model_ is not None + assert isinstance(model.model_, AMICATorchNG) assert model.model_.device.type == "mps" assert model.model_.dtype == torch.float32 diff --git a/pamica/tests/torch_tests/test_ng_sharing.py b/pamica/tests/torch_tests/test_ng_sharing.py index 02d27728..83b82dca 100644 --- a/pamica/tests/torch_tests/test_ng_sharing.py +++ b/pamica/tests/torch_tests/test_ng_sharing.py @@ -265,7 +265,7 @@ def test_non_finite_sphere_still_fails_loudly(real_data): comp_thresh=0.9, ) ng = model.model_ - assert ng is not None and ng.sphere is not None + assert isinstance(ng, AMICATorchNG) and ng.sphere is not None assert not bool(torch.isfinite(ng.sphere).all()) with pytest.raises(RuntimeError, match="non-finite"): ng._identify_shared_comps() @@ -291,7 +291,7 @@ def test_get_sensor_mixing_matrix_non_finite_sphere_fails_loudly(real_data): comp_thresh=0.9, ) ng = model.model_ - assert ng is not None and ng.sphere is not None + assert isinstance(ng, AMICATorchNG) and ng.sphere is not None assert not bool(torch.isfinite(ng.sphere).all()) with pytest.raises(RuntimeError, match="non-finite"): ng.get_sensor_mixing_matrix() @@ -433,7 +433,7 @@ def test_rank_reduced_share_fit_completes(real_data): comp_thresh=0.9, ) ng = model.model_ - assert ng is not None and ng.sphere is not None + assert isinstance(ng, AMICATorchNG) and ng.sphere is not None assert ng.n_channels == 16 and ng.n_channels_in == NW assert tuple(ng.sphere.shape) == (16, NW) # non-square: no inverse exists assert int(ng.comp_used.sum()) < ng.n_comps # the sharing path really ran @@ -466,7 +466,7 @@ def test_low_rank_projected_data_share_fit_completes(real_data): comp_thresh=0.9, ) ng = model.model_ - assert ng is not None and ng.sphere is not None + assert isinstance(ng, AMICATorchNG) and ng.sphere is not None assert ng.n_channels == rank and ng.n_channels_in == NW assert tuple(ng.sphere.shape) == (rank, NW) assert int(ng.comp_used.sum()) < ng.n_comps diff --git a/pamica/torch_impl/core.py b/pamica/torch_impl/core.py index c38f36ba..639a6bbb 100644 --- a/pamica/torch_impl/core.py +++ b/pamica/torch_impl/core.py @@ -3114,6 +3114,57 @@ def get_unmixing_matrix(self, model_idx: int = 0) -> np.ndarray: self._check_usable("get the unmixing matrix") return self.W[:, :, model_idx].T.cpu().numpy() + # ------------------------------------------------------------------ + # Preprocessing accessors (issue #313). Same names, shapes and float64 + # return type as AMICAMLXNG's, so a consumer that composes the transform + # itself (the MNE export, pamica.mne_compat) reads every backend alike. + # ------------------------------------------------------------------ + def get_sphere(self) -> np.ndarray: + """Fitted sphering matrix, shape ``(n_channels, n_channels_in)``. + + Square for a full-rank fit and ``(n_kept, n_channels_in)`` after rank + reduction (issue #223). Together with :meth:`get_mean`, + :meth:`get_model_center` and :meth:`get_unmixing_matrix` it composes + :meth:`transform`: ``S = W_fort @ (sphere @ (X - mean) - c)``. + Returned as an independent float64 copy whatever the fit ``dtype``. + """ + if self.sphere is None: + raise RuntimeError( + "AMICATorchNG.get_sphere() requires a fitted model; call fit() first." + ) + self._check_usable("get the sphere") + return self.sphere.detach().cpu().numpy().astype(np.float64) + + def get_mean(self) -> np.ndarray: + """Per-channel mean removed before sphering, shape ``(n_channels_in,)``. + + All zeros for a ``do_mean=False`` fit. Returned as an independent + float64 copy whatever the fit ``dtype``. + """ + if self.mean is None: + raise RuntimeError( + "AMICATorchNG.get_mean() requires a fitted model; call fit() first." + ) + self._check_usable("get the mean") + return self.mean.detach().cpu().numpy().astype(np.float64).ravel() + + def get_model_center(self, model_idx: int = 0) -> np.ndarray: + """Model ``model_idx``'s center ``c`` in sphered space, shape ``(n_channels,)``. + + The per-model offset :meth:`transform` subtracts after sphering (issue + #27). Identically zero for a single-model fit, since the ``c`` update + is gated to ``n_models > 1``. Returned as an independent float64 copy + whatever the fit ``dtype``. + """ + if self.c is None: + raise RuntimeError( + "AMICATorchNG.get_model_center() requires a fitted model; call " + "fit() first." + ) + self._check_model_idx(model_idx) + self._check_usable("get the model center") + return self.c[:, model_idx].detach().cpu().numpy().astype(np.float64) + def _pca_reduction_requested(self, n_channels: int) -> bool: """Whether the explicit ``pcakeep``/``pcadb`` asks to fit fewer than ``n_channels`` dimensions (the shared predicate, From fb13d76de145419da9c89db94438d60aafbff444 Mon Sep 17 00:00:00 2001 From: Seyed Yahya Shirazi Date: Tue, 22 Sep 2026 19:13:36 -0700 Subject: [PATCH 06/26] Add phases 7-9 to epic #324 plan --- .context/issue-324/plan.md | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/.context/issue-324/plan.md b/.context/issue-324/plan.md index d821478b..453f9124 100644 --- a/.context/issue-324/plan.md +++ b/.context/issue-324/plan.md @@ -227,3 +227,48 @@ Six implementers (one per phase; at most three concurrent), plus one pr-review-t Each phase's gate tests above, run by the implementer and re-run by the lead before merge (the lead also spot-checks cited lines and keystone numbers). Epic PR: full suite on the epic branch, `validate_implementations.py` for every backend, pr-review-toolkit review, then user approval of the final merge. + +## Phases 7-9: component orientation (added 2026-09-22) + +A read-only investigation during Phase 6 found that pamica stores each model's mixing block transposed relative to the reference +(issue #24 convention: `get_mixing_matrix` returns `A[:, comp_list[:, h]].T`), +so component `i` of model `h` is row `i` of the stored block, +while `doscaling`, the share metric and the share merge act on stored columns. +Lead verification: a compensated rescale of stored row 5 changes the log-likelihood by exactly 0.0; of stored column 5, by -3.9e-2. +The user chose to fold the fixes into this epic (decision 2026-09-22). +Evidence and prototypes: #333, #334, #335 and the investigation scratch scripts +(`rowng.py` torch prototype, `fortran_oracle.py`, `merged_oracle.py`, `newt_offbyone.py`). + +| # | Slug | Issue | Wave | Depends on | +|---|---|---|---|---| +| 7 | doscaling-rows | #333 | 4 | epic head | +| 9 | newton-start | #335 | 4 | epic head (parallel with 7) | +| 8 | component-rows | #334 | 5 | Phase 7 merged | + +### Phase 7: doscaling-rows (#333) + +Decided: normalize each model block's component rows (with the matching `mu`/`beta` rescale), identically in torch, NumPy and MLX, as one shared decision; +no layout change in this phase. +Gates: a gated native-binary oracle (seeded, `doscaling` on, `do_newton` off, a few iterations) matching `A`/`mu`/`sbeta` to float64 round-off; +an always-on exact-invariance test (one doscaling pass leaves the log-likelihood unchanged); +`doscaling=False` trajectories byte-identical; +ADR amending ADR 0001; changelog (default trajectories change, scale-blind results do not). + +### Phase 9: newton-start (#335) + +Decided: first confirm, site by site against amica15.f90, every 1-based schedule gate (Newton switch, lrate ramp, rholrate ratchet, and the other `*_start` schedules); +fix every confirmed offset identically in all three backends; record any deliberate difference. +Gates: a gated native oracle on the first Newton iterations; an always-on test of the schedule arithmetic. + +### Phase 8: component-rows (#334) + +Decided: store `A` as `(n_comps, n)` with components as rows and `comp_list` indexing rows; +each per-model block keeps today's `n x n` matrix, so every configuration without sharing stays byte-identical (including Phase 7's doscaling); +the share metric uses `pinv(sphere) @ A.T` and the merge ties rows; all three backends. +Persistence: bump the torch and MLX state format versions and convert unmerged multi-model payloads losslessly; refuse merged ones with a message to refit. +The EEGLAB `A` export becomes the reference layout for `n_models > 1`. +Gates: grouped sources have identical component maps; a planted duplicate is merged with log-likelihood, transform and maps unchanged; +the metric vector equals `get_sensor_mixing_matrix(h)[:, i]`; a gated native oracle from a merged `load_comp_list` state; +byte-identity of every non-sharing configuration against the pre-change code. + +After Phase 8: re-run `validate_implementations.py --backend all` and update the parity rows in docs/guides/validation.md. From e38aa11fc62845ae8350fa8305fb7027e6ada40a Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Tue, 22 Sep 2026 19:55:14 -0700 Subject: [PATCH 07/26] Phase 6: end-to-end validation across backends (#332) * feat: validate every backend in the harness validate_implementations.py gains --backend {torch,numpy,mlx}, a comma-separated list, or all. Each backend runs the bundled sample with the harness's canonical params (NumPy through its own key map, MLX through AMICAMLXNG) against one shared reference run; an explicit --backend adds a parity summary table with runtime. The default torch-only run prints the same report as before (diffed). Tested: dispatch and argument-parsing tests (no binary), and the AMICA_RUN_FORTRAN-gated per-backend parity test against the v0.3.3 native binary (3 passed). * refactor: run the harness MLX row via AMICA With Phase 4 merged, the torch and MLX runs share one AMICA(backend=...) path, so the MLX row exercises the wrapper a user calls. Numbers unchanged (same summary table as the direct AMICAMLXNG run). Tested: harness dispatch tests (29 passed); all-backend run against the v0.3.3 native binary. * docs: use American English spellings 35 British spellings in comments, docstrings and Markdown (behaviour, colour, neighbour, cancelled, licence, optimiser, ...). Proper nouns (Centre de Recherche...) and 'analogue' left as is; no identifiers touched. Tested: ruff, typos. * docs: cite torch core by symbol, not line Seven torch_impl/core.py: citations outside the MLX module pointed at drifted lines; they now name the method (or are dropped where the sentence already names it). Also replaces a stale pamica.py:802-813 citation (a file renamed in #34) in AMICATorchNG._newton_direction. Fortran line citations unchanged. Tested: ruff. * docs: MLX getting-started path and harness rows getting-started gains a complete Apple Silicon (MLX) section (install, fit/transform/save/load, params file, AMICAICA, precision). The validation guide documents running the harness per backend and records the per-backend parity rows with their expected bars. The differences page records do_sphere=False (#328) and NumPy refit warm-start (#312), both verified against the code. AGENTS.md, changelog and the native-backend and testing pages follow. Tested: mkdocs build --strict (scratch env); anchors checked in the built site. * test: keep suite output out of the repository An audit of the non-slow suite found 63 tests (13 files) writing ./output via AMICA_NumPy's default outdir; the slow CLI, end-to-end and pdf-family tests wrote test_output, _ng_e2e_tmp and scratch_amica_parity into the repo. A session-scoped autouse fixture now runs each session (per xdist worker) from a temporary directory, the harness resolves its bundled inputs from its own location, and the slow tests write under tmp_path. Stale .gitignore entries removed. Tested: full non-slow suite (1335 passed, nothing written to the repo); coverage reports still land in the repo root with and without xdist. * test: end-to-end workflow on torch and MLX New mne_tests/test_end_to_end_workflow.py mirrors our own pipeline on the bundled EEG: average reference, AMICAICA with pcakeep = n_channels - 1 on each backend, get_sources vs transform, a single-component exclusion (exact back-projection, residual kept, rank drops by one), EEGLAB export reloaded by loadmodout with the padded sphere, AMICA save/load, an input.param-driven fit, and torch vs MLX Hungarian-matched sources >= 0.999. Not marked slow (about 5 s); MLX tests skip without MLX or an Apple GPU. Tested: 12 passed locally (torch and MLX); measured tolerances recorded in the module. * fix: transpose NumPy sensor mixing matrix AMICA_NumPy.get_sensor_mixing_matrix returned pinv(sphere) @ stored A without the transpose the torch and MLX backends apply (the stored W is the unmixing transposed, issue #24), so its columns were rows of the true mixing matrix: about 10% off the torch maps after five iterations and no inverse of get_weights() @ sphere. New cross-backend test pins torch/NumPy agreement (4.6e-12 full rank, 9.5e-11 at pcakeep=20), MLX agreement at float32 tolerance, and the identity on every backend. Tested: new test fails without the fix (4 NumPy cases) and passes with it (10 passed); backend-guard and share-comps suites green. * chore: remove stale 2025 debug output pytorch_debug_test/out.txt and pytorch_output_test/out.txt were run logs committed in August 2025 by the long-removed basic PyTorch backend; nothing reads them. The only reference, a typos exclusion for the first directory, goes with them. Tested: typos. * docs: pin the harness rows to the v0.3.3 binary States why the rows name an explicit release binary instead of the resolver default (its cached 'latest' is never refreshed) and how to fetch it. Tested: mkdocs build --strict. * Rebuild paper.pdf * fix: NumPy writes files only with an outdir AMICA_NumPy defaulted outdir to ./output, so every fit wrote out.txt at construction, writestep checkpoints, history and its final results into the caller's working directory. The default is now outdir=None, which writes nothing (as torch and MLX never do unless asked); an explicit outdir (keyword, params file, or the CLI's --outdir, whose own default stays output) writes exactly as before. Behavior change recorded in the changelog and the NumPy API page; the conftest chdir stays as defense in depth. Tested: new test_numpy_outdir.py (default leaves the working directory empty with every write path on; explicit and params-file outdir write as before; fails on the old code) and a CLI test that a run without --outdir still writes ./output; NumPy suites (107 passed). * fix: NumPy viz plots the true maps and sources plot_components drew stored-A columns (rows of the true mixing matrix) as mixing vectors, and it and plot_pdf_fits built activations as stored W times raw data (no transpose, sphere or mean); plot_model_comparison skipped the sphere. One helper, _component_maps_and_sources, now computes exactly what get_sensor_mixing_matrix and transform return, and all three plots use it. load_results reads a rank-reduced fit's zero-padded sphere instead of raising. Tested: new test_numpy_viz.py compares the helper and the plotted lines/histograms with the live accessors, full rank and pcakeep=20 (10 passed; 8 fail on the old code). * test: pin the default harness run and summary Pins the literal default run (no --backend: one validation_report.txt, no parity_summary.md, exit 0), renders format_parity_summary with populated rows from two real runs (torch as the stand-in reference, NumPy through compare_results) and checks every numeric column, and runs the no-MLX exit over both 'torch,mlx' and 'all'. Tested: 32 passed, 3 skipped (the AMICA_RUN_FORTRAN-gated parity test). * docs: fix two more British spellings unlabelled and relabelled, missed by the first sweep's word-boundary match. Test names containing 'honour' (identifiers) are left as is. Tested: typos, ruff. * docs: trim the getting-started MLX route The MLX section duplicated the backends guide's end-to-end and precision sections; it is now install, AMICA(backend="mlx") fit/transform, an AMICAICA one-liner and the float32 note, linking to the guide for the full workflow. The install heading no longer uses a bare GPU abbreviation. Tested: mkdocs build --strict; linked anchors checked. * docs: harness status and test working directory progress_summary.md's harness line now matches AGENTS.md (every backend, #315), and the testing guide explains the conftest session chdir and how a test resolves repository paths. Tested: mkdocs build --strict. * docs: one description of reference resolution default_reference_binary's docstring is now the single full description of the reference-binary resolution order; the --native-engine help summarizes it and points there. The native-backend page runs the harness with uv run. Tested: --help renders; harness tests (32 passed). * test: two-iteration fits for the map gate The NumPy-vs-torch sensor-map gate (1e-9) failed on the macOS arm64 runner at 1.3e-9: the backends start within 1e-13 and EM amplifies the gap about sixfold per iteration, so five iterations was platform-sensitive. Two iterations keep the same gate with a 1e-12 gap while the pre-fix transposed maps still differ by 2.6-3%. Tested locally: module passes. --------- Co-authored-by: github-actions[bot] --- .context/issue-136/matlab_viz_verification.md | 16 +- .context/issue-145/full_data_reproduction.md | 4 +- .context/plan.md | 2 +- .context/progress_summary.md | 11 +- .github/workflows/ci.yml | 6 +- .gitignore | 4 - AGENTS.md | 12 +- _typos.toml | 1 - docs/api/metrics.md | 2 +- docs/api/native-backend.md | 9 +- docs/api/numpy-backend.md | 4 + docs/changelog.md | 27 + docs/development/testing.md | 14 +- docs/getting-started.md | 55 +- docs/guides/amica-differences.md | 63 ++- docs/guides/validation.md | 80 ++- native/mpi_single.c | 2 +- pamica/mlx_impl/core.py | 4 +- pamica/numpy_impl/core.py | 79 ++- pamica/numpy_impl/data.py | 10 +- pamica/numpy_impl/viz.py | 100 +++- pamica/tests/conftest.py | 27 + pamica/tests/mlx_tests/test_mlx_export.py | 3 +- pamica/tests/mlx_tests/test_mlx_mir.py | 8 +- pamica/tests/mlx_tests/test_mlx_reject.py | 2 +- pamica/tests/mlx_tests/test_mlx_scoring.py | 2 +- .../mne_tests/test_end_to_end_workflow.py | 254 +++++++++ pamica/tests/mne_tests/test_mne_meg.py | 2 +- pamica/tests/mne_tests/test_mne_wrapper.py | 2 +- pamica/tests/test_fortran_param_forwarding.py | 263 ++++++++- pamica/tests/test_llt_stash.py | 4 +- pamica/tests/test_mlx_convergence_stops.py | 2 +- pamica/tests/test_numpy_cli.py | 16 + pamica/tests/test_numpy_outdir.py | 93 ++++ pamica/tests/test_numpy_share_comps.py | 4 +- pamica/tests/test_numpy_viz.py | 147 +++++ pamica/tests/test_sample_data.py | 91 ++-- .../tests/test_sensor_mixing_cross_backend.py | 142 +++++ pamica/tests/test_viz.py | 2 +- .../torch_tests/test_amica_ng_wrapper.py | 4 +- pamica/tests/torch_tests/test_ng_backend.py | 12 +- .../tests/torch_tests/test_ng_convergence.py | 2 +- .../tests/torch_tests/test_ng_pdf_families.py | 6 +- pamica/torch_impl/core.py | 8 +- pamica/viz.py | 8 +- paper.md | 2 +- paper.pdf | Bin 375742 -> 375736 bytes pytorch_debug_test/out.txt | 60 --- pytorch_output_test/out.txt | 22 - validate_implementations.py | 509 ++++++++++++++---- 50 files changed, 1808 insertions(+), 394 deletions(-) create mode 100644 pamica/tests/conftest.py create mode 100644 pamica/tests/mne_tests/test_end_to_end_workflow.py create mode 100644 pamica/tests/test_numpy_outdir.py create mode 100644 pamica/tests/test_numpy_viz.py create mode 100644 pamica/tests/test_sensor_mixing_cross_backend.py delete mode 100644 pytorch_debug_test/out.txt delete mode 100644 pytorch_output_test/out.txt diff --git a/.context/issue-136/matlab_viz_verification.md b/.context/issue-136/matlab_viz_verification.md index 3a392695..bedd84c4 100644 --- a/.context/issue-136/matlab_viz_verification.md +++ b/.context/issue-136/matlab_viz_verification.md @@ -9,7 +9,7 @@ > `transform()` sources at high fidelity under EITHER orientation, even for a > single-model fit where `c` is identically zero (`W @ sphered` -> mean |corr| > 0.877; `W.T @ sphered` -> 0.932; **0/32** components above 0.999, where a -> merely normalised + variance-reordered `W` would give ~1.000 for all 32, since +> merely normalized + variance-reordered `W` would give ~1.000 for all 32, since > correlation already quotients out scale, sign and permutation). It is also the > only plot with **no MATLAB oracle** (`pop_topohistplot` is broken upstream, > trap 5), so nothing external could catch a wrong activation space. Shipping a @@ -27,7 +27,7 @@ adapted to plots. Plots have no byte-level contract, so the gate has two levels: 1. **Data gate**: every quantity we plot must match MATLAB's where an oracle exists. 2. **Visual gate**: our figure and MATLAB's, rendered on the same real data, must be - a meaningful representation of the same thing (a human judgement call, made by + a meaningful representation of the same thing (a human judgment call, made by the user; accepted 2026-07-16). MATLAB cannot run in CI, so this is recorded rather than automated. @@ -42,9 +42,9 @@ clean-room reimplementation. **Posture: run-and-observe only. No `.m` implementation source was read at any point.** Everything below came from MATLAB `help` text (public API documentation), rendered -figures, and black-box input/output behaviour. That is what makes the MATLAB gate not +figures, and black-box input/output behavior. That is what makes the MATLAB gate not merely a check but the *mechanism* that keeps the reimplementation clean: it lets us -match observable behaviour without deriving from protected expression. +match observable behavior without deriving from protected expression. `pre_ICA_cleaning/getMIR.m` is **Apache-2.0** and was legitimately read (Phase 1 ported it with attribution; see `THIRD_PARTY_NOTICES.md`). @@ -83,11 +83,11 @@ Side-by-side renders on the same data are committed here: - `cmp_pmi.png` — MATLAB `pop_modPMI` vs `pamica.viz.plot_pmi_heatmap`, rendered on **identical signals** (MATLAB's own `EEG.icaact`) so the comparison isolates the estimator and the ordering. Both show the same dependent-subspace cluster near the - centre with the same radiating cross pattern, at the same MI scale. + center with the same radiating cross pattern, at the same MI scale. Accepted deliberate differences: viridis vs jet, we add a colorbar (MATLAB has none), 0-based component labels (pythonic; MATLAB is 1-based), and a different ordering -algorithm (ours greedy nearest-neighbour chain vs MATLAB's iterative cost minimisation, +algorithm (ours greedy nearest-neighbor chain vs MATLAB's iterative cost minimization, `cost = 20.0533 -> 19.5856 -> 18.9268`). ## Deliberate divergences (documented so nobody "fixes" them toward MATLAB) @@ -98,7 +98,7 @@ algorithm (ours greedy nearest-neighbour chain vs MATLAB's iterative cost minimi should not be unsmoothed), and matching exactly would require reading GPL source. 2. **MI diagonal.** MATLAB zeroes it. Our `pairwise_mi` diagonal is self-entropy (~2.83 vs off-diagonal ~0.06), so `plot_pmi_heatmap` masks it (`mask_diagonal=True`). - Plotting it unmasked destroys the colour scale and hides all structure. + Plotting it unmasked destroys the color scale and hides all structure. 3. **Negative MI.** MATLAB's bias correction yields small negatives (-0.006); ours is strictly positive. Expected estimator difference, not a bug. @@ -202,7 +202,7 @@ algorithm (ours greedy nearest-neighbour chain vs MATLAB's iterative cost minimi 6. **Do not read the figures to settle numeric questions.** The maintainer twice misread which model was active at t=0 from a low-res render and briefly believed the models were swapped. Three numeric checks (correlation +0.9886, direct values at t=0, - and pixel-colour sampling showing orange's plateau starting at x=74 vs blue's at + and pixel-color sampling showing orange's plateau starting at x=74 vs blue's at x=120) all confirmed there was no swap. Trust the data gate over the visual one. ## Reproducing diff --git a/.context/issue-145/full_data_reproduction.md b/.context/issue-145/full_data_reproduction.md index f7abff4b..e36103db 100644 --- a/.context/issue-145/full_data_reproduction.md +++ b/.context/issue-145/full_data_reproduction.md @@ -43,12 +43,12 @@ Per torch seed vs Fortran: converge and not a worse fit. This is the fingerprint of the Fable agent's root cause: the Newton step is faithful (~1e-9 vs Fortran); the drift is mu/beta/rho exact-EM ill-conditioning on weak components, amplified by Newton's sharpening, - which occasionally tips a fit into a neighbouring basin Fortran's plain-EM path + which occasionally tips a fit into a neighboring basin Fortran's plain-EM path does not enter. ## Next step Implement the faithful mu/beta/rho ill-conditioning guard (keep the Newton step, -stabilise the exact-EM weak-component updates so the Newton-preconditioned path +stabilize the exact-EM weak-component updates so the Newton-preconditioned path stays in Fortran's basin), then re-run this exact ensemble on `/mnt/local/pamica-145` and re-measure - expectation: seed 42 stops collapsing (all seeds -> ~0.99 vs Fortran, matching within-Fortran self-consistency). diff --git a/.context/plan.md b/.context/plan.md index 1371814b..8fbb46d4 100644 --- a/.context/plan.md +++ b/.context/plan.md @@ -110,7 +110,7 @@ will be hosted at `eeglab.org/pyAMICA`. ### Phase R4: JOSS paper — DONE - [x] `paper.md` + `paper.bib` (PR #102, #101): summary, statement of need, validation, - state of the field, acknowledgements. Drafted via `manuscript-writing`, polished + state of the field, acknowledgments. Drafted via `manuscript-writing`, polished via `humanizer`, passed an independent `paper-review`. Authors: Shirazi (corresponding), Delorme, Makeig. See [[amica-release-readiness]]. diff --git a/.context/progress_summary.md b/.context/progress_summary.md index c43792c3..8ee229dc 100644 --- a/.context/progress_summary.md +++ b/.context/progress_summary.md @@ -71,11 +71,12 @@ what remains as of the v0.1.0 preparation. - UV is the canonical environment (`pyproject.toml` + `uv.lock`); the legacy conda env is retired. - CI is live and green on `main`: ruff lint/format, pytest (excluding slow/Fortran-binary parity), and a build + clean-env import matrix on Python 3.12 and 3.13. Typos check green. -- Validation harness (`validate_implementations.py`) runs the PyTorch (NG) backend against the - Fortran binary (`amica15mac`) and matches components via the Hungarian algorithm, on real sample - EEG; it does not exercise the NumPy or MLX backends. NumPy-vs-Fortran parity lives in pytest - (`test_sample_data_numpy_vs_fortran`), MLX validation in `mlx_tests/` plus the cross-backend - suites (extending the harness itself is tracked as issue #315). +- Validation harness (`validate_implementations.py --backend {torch,numpy,mlx}`, a comma-separated + list, or `all`; default `torch`, whose report is unchanged) runs each backend against one Fortran + reference run with the same settings and matches components via the Hungarian algorithm, on real + sample EEG (#315). All three meet the Fortran bar on the bundled sample (LL within 3.2e-5, + correlation 0.9992, Amari 0.004; rows and bars in `docs/guides/validation.md`), pinned by the + `AMICA_RUN_FORTRAN`-gated test in `test_fortran_param_forwarding.py`. ## Remaining before / around v0.1.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc0f264a..d4cdd3b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ name: CI # the push re-triggers workflows -- see their own headers). Per-SHA grouping # means that bump push can never land in the same concurrency group as the # merge commit's run, so it cannot cancel it -- the failure mode this section -# used to describe (a rapid follow-up push cancelling the real commit's run +# used to describe (a rapid follow-up push canceling the real commit's run # and leaving the trivial bump commit as the one that gets tested) cannot # happen at all now. `lint`/`typecheck` below still skip when the push's head # commit is a bot bump (same author-email + message-prefix guard @@ -62,9 +62,9 @@ on: concurrency: # push: per-SHA group (each push to main/dev gets its own run, never - # cancelled by a later push -- see the header comment above). + # canceled by a later push -- see the header comment above). # pull_request: per-ref group, unchanged (a PR's older, superseded runs are - # still cancelled by a newer push to the same branch). + # still canceled by a newer push to the same branch). group: ci-${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }} cancel-in-progress: true diff --git a/.gitignore b/.gitignore index 8e6e1200..e68b4d12 100644 --- a/.gitignore +++ b/.gitignore @@ -24,10 +24,6 @@ enhanced_test_output/ coverage.xml htmlcov/ -# Temp output from NG e2e test (Fortran run artifacts) -pamica/tests/torch_tests/_ng_e2e_tmp/ -pamica/tests/torch_tests/_ng_e2e_json_tmp/ - # Benchmark run artifacts benchmarks/results/ # Real EEG benchmark data (fetched from OpenNeuro, not committed) + result JSONs diff --git a/AGENTS.md b/AGENTS.md index 94feac77..426c87d4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ pamica/ ├── sample_data/ # Sample EEG data + Fortran binary (amica15mac) └── tests/ # Tests, incl. tests/torch_tests/ (vs-Fortran parity) -validate_implementations.py # Runs both implementations, Hungarian component matching, reports +validate_implementations.py # Runs each backend vs Fortran (--backend), Hungarian matching, reports ``` Module names are topic-based (`core`/`pdf`/`data`/... under `numpy_impl/`, `core`/`utils` under `torch_impl/`); the old `pamica.py`/`amica_*.py`/`amica_torch_ng.py` @@ -112,10 +112,12 @@ have no float64). end: fit, `from_params_file`, `pcakeep`, the #50 degenerate-fit contract, `.pt` save/load (wrapper `format_version` 2 records the backend; version 1 still loads as torch), EEGLAB export and MNE `apply`. `device`/`dtype` are torch-only; the default stays `"torch"`. -- `validate_implementations.py` runs the PyTorch (NG) backend against the Fortran binary and matches - components via the Hungarian algorithm; it does not exercise the NumPy or MLX backends. NumPy-vs- - Fortran parity lives in pytest (`test_sample_data_numpy_vs_fortran`), and MLX validation lives in - `mlx_tests/` plus the cross-backend suites (extending the harness itself is tracked as issue #315). +- `validate_implementations.py --backend {torch,numpy,mlx}` (a comma-separated list, or `all`; default + `torch`, whose report is unchanged) runs each backend against one Fortran reference run with the same + settings and matches components via the Hungarian algorithm (#315). All three meet the Fortran bar on + the bundled sample (LL within 3.2e-5, correlation 0.9992, Amari 0.004; rows and bars in + `docs/guides/validation.md`), pinned by the `AMICA_RUN_FORTRAN`-gated test in + `test_fortran_param_forwarding.py`. - Newton and exact-EM updates are implemented in `AMICATorchNG` and the legacy NumPy `numpy_impl/core.py` (both Fortran-faithful). Adaptive PDF (#26) is DONE (all five `pdftype` families + ext-Infomax switcher); full multi-model matching (#27) is validated by distributional equivalence. diff --git a/_typos.toml b/_typos.toml index 0cde0e4f..dc108704 100644 --- a/_typos.toml +++ b/_typos.toml @@ -6,7 +6,6 @@ extend-exclude = [ "*.f90", # read-only Fortran reference (amica17*.f90, funmod2.f90) "pamica/sample_data/", # sample EEG + Fortran outputs + saved .npy results - "pytorch_debug_test/", # generated debug output "uv.lock", "paper.bib", # citation metadata: accented author names + LaTeX escapes ] diff --git a/docs/api/metrics.md b/docs/api/metrics.md index f2e8753a..62729dea 100644 --- a/docs/api/metrics.md +++ b/docs/api/metrics.md @@ -30,7 +30,7 @@ from pamica.metrics import mir, pairwise_mi, block_diagonal_order `mir` is a direct port of `getMIR.m` from [bigdelys/pre_ICA_cleaning](https://github.com/bigdelys/pre_ICA_cleaning) -(Apache-2.0); the licence is vendored in `THIRD_PARTY_NOTICES.md`. It agrees +(Apache-2.0); the license is vendored in `THIRD_PARTY_NOTICES.md`. It agrees with the original to 1.7e-15 relative on the bundled sample data. `pairwise_mi` and `block_diagonal_order` are a clean-room reimplementation. The diff --git a/docs/api/native-backend.md b/docs/api/native-backend.md index fdd3cc85..9f289a29 100644 --- a/docs/api/native-backend.md +++ b/docs/api/native-backend.md @@ -75,14 +75,15 @@ pointing at the build script. `validate_implementations.py` can source the reference through this engine instead of the bundled macOS-only `amica15mac` fixture, so the real Fortran -reference can be compared against the PyTorch backend on any platform: +reference can be compared against pamica's backends on any platform +(`--backend torch`, the default, or `numpy`, `mlx`, a comma-separated list, or `all`): ```bash # Resolve/download the native binary (or honor PAMICA_NATIVE_BINARY): -python validate_implementations.py --native-engine +uv run python validate_implementations.py --native-engine -# Or point at a specific binary: -python validate_implementations.py --fortran-binary /path/to/amica15 +# Or point at a specific binary, and compare every backend against it: +uv run python validate_implementations.py --fortran-binary /path/to/amica15 --backend all ``` ::: pamica.native.engine.AMICANative diff --git a/docs/api/numpy-backend.md b/docs/api/numpy-backend.md index add7c8e1..fe2e4695 100644 --- a/docs/api/numpy-backend.md +++ b/docs/api/numpy-backend.md @@ -15,6 +15,10 @@ the same shared reader. Settings the file carries that this backend does not consume are named in one `logger.warning` rather than silently dropped. +It writes files only when given an `outdir` (keyword, params file, or the CLI's `--outdir`, default `output`): +there it writes its `out.txt` log, its `writestep` checkpoints and its final results in the Fortran `amicaout` layout. +Without one (the default) a fit writes nothing, like the PyTorch and MLX backends. + This backend implements only the generalized-Gaussian source density (`pdftype=0`); `AMICA_NumPy(pdftype=...)` with any other value raises `NotImplementedError` at construction instead of silently doing nothing -- see diff --git a/docs/changelog.md b/docs/changelog.md index 8a48e5d6..5a1d90e3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,33 @@ Release notes are also published on the ## Unreleased +- **Phase 6 of epic #324: the validation harness covers every backend (issue #315).** + `validate_implementations.py --backend {torch,numpy,mlx}` (or a comma-separated list, or `all`) + compares each backend against one Fortran reference run with the same settings; + the default remains `torch` and prints the same report as before. + NumPy receives the settings through its own key-translation table, PyTorch and MLX through `AMICA(backend=...)`; + an explicit `--backend` also prints and saves a one-row-per-backend summary with runtimes (`parity_summary.md`), + and `--backend mlx` without MLX exits with status 2 and the install hint. + On the bundled sample all three backends meet the reference bar (log-likelihood within 3.2e-5, matched correlation 0.9992, Amari distance 0.004); + the rows and each backend's expected bar are in the validation guide, pinned by an `AMICA_RUN_FORTRAN`-gated test. + - The getting-started page gains a short Apple Silicon (MLX) route that links to the backends guide's full workflow, + and the differences page records two existing divergences: + `do_sphere=False` fits unscaled data where the reference divides each channel by its standard deviation (issue #328), + and a second `fit` on the same `AMICA_NumPy` instance continues from the first (related to issue #312). + - **Behavior change (legacy NumPy backend):** `AMICA_NumPy` writes files only when given an `outdir`. + Its default was `./output`, so every fit wrote `out.txt` at construction, `writestep` checkpoints and its final results into the caller's working directory. + The default is now `outdir=None`, which writes nothing, as the PyTorch and MLX backends never do unless asked. + An explicit `outdir` (keyword, params file, or the command-line interface's `--outdir`, which still defaults to `output`) writes exactly what it did before. + - **Fix:** `AMICA_NumPy.get_sensor_mixing_matrix` returned `pinv(sphere)` times the stored mixing matrix without the transpose the PyTorch and MLX backends apply, + so its columns were the rows of the true mixing matrix rather than the components' sensor maps + (about 10% away from the PyTorch backend's maps after five iterations on the bundled sample, and not an inverse of `get_weights() @ sphere`). + It now matches the PyTorch backend to round-off (4.6e-12 relative), pinned by a cross-backend test. + The NumPy backend's fit, `transform`, `get_weights` and EEGLAB export were not affected. + - **Fix:** the legacy plotting helpers in `pamica.numpy_impl.viz` had the same orientation slip. + `plot_components` drew rows of the mixing matrix as mixing vectors, and it and `plot_pdf_fits` formed activations from the raw data with no mean removal, no sphere and no transpose; + `plot_model_comparison` skipped the sphere. + They now plot the model's own sensor maps and sources (what `get_sensor_mixing_matrix` and `transform` return), checked against those accessors on a real fit. + `load_results` also reads a rank-reduced fit's zero-padded sphere, which it used to reject. - **Phase 4 of epic #324: backend selection in `AMICA` and `AMICAICA` (issue #313).** `AMICA` and `AMICAICA` gain a `backend` parameter: `"torch"` (the default, `AMICATorchNG`, float64 Fortran parity) or `"mlx"` (`AMICAMLXNG`, Apple GPU, float32 only). diff --git a/docs/development/testing.md b/docs/development/testing.md index c557a85d..29b3024f 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -14,6 +14,17 @@ uv run pytest --cov # with coverage uv run pytest pamica/tests/torch_tests/ # PyTorch-vs-Fortran parity tests ``` +## Working directory + +The suite runs from a temporary working directory, not the repository: +a session-scoped autouse fixture in `pamica/tests/conftest.py` switches to one per session (per worker under `pytest-xdist`), +so anything a test writes to a relative path lands there instead of in the repository. +A test that needs a repository path resolves it from `__file__` +(for example `Path(__file__).resolve().parent.parent / "sample_data"`), +or, when a relative path inside a file must resolve against the repository root +(such as `sample_params.json`'s `files`), changes into the root explicitly with `monkeypatch.chdir`. +Coverage reports still land in the repository root. + ## Layout - `pamica/tests/` — end-to-end and interface tests. @@ -21,4 +32,5 @@ uv run pytest pamica/tests/torch_tests/ # PyTorch-vs-Fortran parity tests component sharing, float32 stability, and edge cases. - `pamica/tests/mlx_tests/` — MLX backend tests (Apple Silicon). - `validate_implementations.py` — cross-implementation validation harness - (Hungarian component matching against Fortran). + (Hungarian component matching against Fortran; `--backend` selects torch, + numpy, mlx, a list, or `all`). diff --git a/docs/getting-started.md b/docs/getting-started.md index 109c24cd..ebe815d8 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -16,10 +16,10 @@ uv sync # install the project and its dependencies into .venv A packaged release on the Python Package Index (PyPI) is planned; until then, install from source as above. -### Optional Apple-Silicon GPU backend +### Optional Apple Silicon backend (MLX) -The MLX backend is Apple-only and is therefore an optional extra; `import -pamica` never requires it. +The MLX backend runs on the Apple Silicon graphics processing unit (GPU). +It is Apple-only and is therefore an optional extra; `import pamica` never requires it. ```bash uv sync --extra mlx # or, in an existing environment: uv pip install mlx @@ -27,19 +27,20 @@ uv sync --extra mlx # or, in an existing environment: uv pip install mlx Then select it with `AMICA(backend="mlx")` (or `AMICAICA(backend="mlx")` for MNE users); it computes in float32. -See [Selecting a backend](guides/backends.md#selecting-a-backend). +[Apple Silicon (MLX)](#apple-silicon-mlx) below walks through the whole workflow. ## Quickstart The main entry point is the scikit-learn-style [`AMICA`](api/amica.md) class, -which wraps the natural-gradient EM backend. +which wraps the natural-gradient expectation-maximization (EM) backend. ```python import numpy as np from pamica import AMICA -# X is (n_channels, n_samples); use real EEG/EMG rather than random data -# for a meaningful decomposition. +# X is (n_channels, n_samples); use real electroencephalography (EEG) or +# electromyography (EMG) recordings rather than random data for a meaningful +# decomposition. X = np.random.randn(32, 10000) amica = AMICA(n_models=1, n_mix=3) @@ -61,11 +62,41 @@ print("final log-likelihood:", amica.final_ll_) ## Choosing a device and precision -`AMICA` auto-selects a device. Because the backend computes in float64 for -Fortran parity and Apple MPS cannot represent float64, an auto-selected MPS -device is redirected to CPU; pass `device="mps"` with `dtype=torch.float32` to -run on MPS explicitly. See [Backends & Devices](guides/backends.md) for the full -matrix and performance guidance. +`AMICA` auto-selects a device. +Because the backend computes in float64 for Fortran parity and Apple's Metal Performance Shaders (MPS) device cannot represent float64, +an auto-selected MPS device is redirected to the central processing unit (CPU); +pass `device="mps"` with `dtype=torch.float32` to run on MPS explicitly. +On Apple Silicon the MLX backend below is faster than MPS. +See [Backends & Devices](guides/backends.md) for the full matrix and performance guidance. + +## Apple Silicon (MLX) + +On a Mac with Apple Silicon, the MLX backend (MLX is Apple's array framework) runs AMICA on the GPU +and is the fastest pamica backend there. +It carries the full feature set of the default PyTorch backend, +and both wrappers select it with `backend="mlx"`: + +```bash +uv sync --extra mlx --extra mne # MLX, plus MNE-Python if you use AMICAICA +``` + +```python +from pamica import AMICA +from pamica.mne_compat import AMICAICA + +model = AMICA(backend="mlx").fit(X, seed=42) # X is (n_channels, n_samples) +sources = model.transform(X) + +ica = AMICAICA(backend="mlx", random_state=42).fit(raw) # raw is an MNE Raw +``` + +MLX computes in float32 (about seven significant digits), because Apple GPUs have no float64: +an MLX fit agrees with the float64 PyTorch backend to float32 tolerance, not bit for bit, +so use the default `backend="torch"` for a run you intend to compare against the Fortran reference. +The backends guide has the complete workflow, +from `pcakeep` on average-referenced EEG through save/load, a params-file run, the EEGLAB export and MNE's `apply` +([End to end on the Apple GPU](guides/backends.md#end-to-end-on-the-apple-gpu)), +and the measured precision ([Precision on the MLX backend](guides/backends.md#precision-on-the-mlx-backend)). ## Next steps diff --git a/docs/guides/amica-differences.md b/docs/guides/amica-differences.md index 0277cd22..b4ed4017 100644 --- a/docs/guides/amica-differences.md +++ b/docs/guides/amica-differences.md @@ -24,11 +24,12 @@ that is not listed, that is a bug worth | 10 | Restarts across seeds | none (its `maxrestarts` only *recovers* from an early NaN) | available as `n_restarts`, **off by default** (`n_restarts=1`) | the weakest under-determined components are init-basin sensitive, so best-of-N buys robustness; but a default that ran N fits would change every result and cost N times as long | `n_restarts=1` (the default) | | 11 | Reconstruction after rank reduction (`AMICAICA.apply`) | output has no representation of the discarded principal component analysis (PCA) subspace (sphere rows past `numeigs` are zero), so any back-projection drops it | the MNE export carries the full PCA basis, so `apply` restores the residual | MNE's own `ICA` does; the residual was never part of the independent component analysis (ICA) decomposition, so it is not ICA's to remove | `apply(..., n_pca_components=ica.n_components_)` | | 12 | `pcadb` | parsed (amica15.f90:3459-3461), never used | unset by default; when set alone, keeps the dimensions within `pcadb` dB of the largest eigenvalue; ignored when `pcakeep` is also set | a dB cut is a scale-free way to drop low-variance directions; letting `pcakeep` win preserves what a reference `input.param` that sets both (both bundled files do) means to the binary | leave `pcadb` unset (the default), or set `pcakeep` | +| 13 | Preprocessing with `do_sphere=False` | divides each channel by its standard deviation and adds a log-determinant term to the likelihood (amica15.f90:516-526) | identity sphere with a zero log-determinant, on all three array backends: the data are fitted unscaled | not a deliberate choice: an existing divergence, found during epic #324 and not yet ported | none yet (issue #328) | Rows 1, 2 and 7 arrived with [ADR 0004](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0004-rank-deficient-input-handling.md); row 3 with ADR 0003; row 5 with issue #50, extended to the raw backends by issue #306; row 8 with issues #60 and #240; row 9 with issue #232; row 10 with issue #198; row 11 with issue #322 (ADR 0005); -row 12 with issue #323. +row 12 with issue #323; row 13 is recorded, not yet resolved, by issue #328. Two `share_comps` details are pamica's own because the reference cannot decide them: the A-freeze window after a merge is anchored on `share_start` (the literal @@ -205,6 +206,7 @@ so the params-file reader, the degenerate-fit contract, `.pt` `save`/`load`, the | Mutual Information Reduction (MIR) diagnostic | yes | no | yes | n/a | | Persistence | `state_dict` + EEGLAB `amicaout` export | EEGLAB `amicaout` | `state_dict`/`.npz` `save`-`load` + EEGLAB `amicaout` export | EEGLAB `amicaout` | | Fortran `input.param` reader | yes (`AMICA.from_params_file`, #132) | yes (`AMICA_NumPy(params_file=...)` / `from_params_file`, #304) | yes (`AMICA.from_params_file(..., backend="mlx")`) | native | +| A second `fit` on the same instance | starts from a fresh initialization | continues from the previous fit ([see below](#refitting-a-numpy-instance-continues-from-the-previous-fit-issue-312)) | starts from a fresh initialization | every run starts fresh, or from `load_*` files | The NumPy row's "GG only" (generalized Gaussian, GG) corrects an earlier version of this table, which listed "all five": `AMICA_NumPy._compute_log_pdf` @@ -557,6 +559,38 @@ and `pcakeep=0` or `pcadb <= 0` ran to a degenerate `nan_ll` fit. The reference has no such check; with `pcakeep <= 0` it sets `numeigs = min(pcakeep, ...) <= 0` and carries on, the situation row 2 describes. +## `do_sphere=False` does not scale the channels (issue #328) + +This is row 13 of the table above: a known divergence, not a design decision, recorded here until it is resolved. + +**What the reference does.** +With `do_sphere 0` the reference does not skip preprocessing. +Its no-sphere branch (amica15.f90:516-526) takes the data covariance and sets +`S = diag(1/sqrt(var_i))` for every channel with positive variance, +so each channel is divided by its standard deviation before fitting, +and it accumulates `sldet = sum_i 0.5*log(S(i,i))` into the likelihood. +`numeigs = nx`, so no dimension is dropped (pamica matches that part; see the previous section). + +**What pamica does.** +All three array backends use an identity sphere and `sldet = 0` when `do_sphere=False`, +so they fit the data unscaled. + +**Consequences.** +A `do_sphere=False` fit is not trajectory-comparable to the binary's: +the natural-gradient EM is not scale-invariant through its `A = I + noise` initialization and its step sizes, so the two fit differently scaled data. +The reported log-likelihood also differs by the log-determinant term. +That term looks inconsistent in the reference itself: +`log|det S|` for `S = diag(1/sqrt(var_i))` is `-0.5 * sum_i log(var_i)`, +but the reference adds `0.5*log(S(i,i)) = -0.25 * log(var_i)` per channel, half of that. +Being a constant added to every model's per-sample log-likelihood, +it cancels in the model posteriors and does not change the fitted parameters, only the reported likelihood. +The default `do_sphere=True` path is unaffected. + +**How to restore the reference behavior.** +There is no option for it yet. +Issue #328 decides whether to port the per-channel scaling to all three backends +and whether to reproduce the reference's `0.5*log(S(i,i))` term or use the full log-determinant with an escape hatch. + ## `mir_step`'s upfront PCA-reduction gate MIR is undefined on a rank-reduced sphere, @@ -701,6 +735,33 @@ Practical notes: `fit` returns is the winner's state, which is a tested claim, not just a documented intention. +## Refitting a NumPy instance continues from the previous fit (issue #312) + +This is a difference between the backends, recorded in the backend table above, +not a divergence from the reference. + +**What happens.** +`AMICA`, `AMICATorchNG` and `AMICAMLXNG` draw a fresh initialization on every `fit`, +so fitting the same instance twice with the same seed reproduces the first fit exactly. +`AMICA_NumPy` does not. +Its `_initialize_parameters` initializes a parameter only while it is `None`, +so a second `fit` on the same instance starts from the first fit's `A`, `mu`, `alpha`, `beta`, `rho`, `gm` and `c` +and from its annealed learning-rate ceilings, +and it appends to the first fit's `ll` and `nd` histories instead of starting new ones. +On the bundled sample, the second fit's starting `A` is exactly the first fit's final `A`, +and three plus three iterations leave six entries in `ll`. +Passing `restart_seeds` (or `n_restarts > 1`) resets this state before each fit, so those paths start fresh. + +**Why.** +The initialize-only-if-unset pattern lets the legacy backend honor starting values assigned before `fit`. +It is not a deliberate cross-backend design: +a warm start that works on purpose, on every backend, is the subject of issue #312, +which also covers the reference's own `load_*` warm-start keywords. + +**How to get a fresh fit.** +Construct a new `AMICA_NumPy` instance for each fit, as the NumPy command-line interface and the validation harness do, +or pass `restart_seeds=[seed]`. + ## Unmapped Fortran keywords This page's own contract ("anything not on this page is intended to match the diff --git a/docs/guides/validation.md b/docs/guides/validation.md index f1a96a8b..13c39e24 100644 --- a/docs/guides/validation.md +++ b/docs/guides/validation.md @@ -22,6 +22,7 @@ Throughout, IC abbreviates independent component and LL log-likelihood. | Per-block sufficient statistics and one M-step | vs Fortran | bit-exact ($\sim\!10^{-15}$) | | Single-model solution (`do_newton=0`, $k\approx153$) | log-likelihood, component correlation vs Fortran | LL within ~0.0005 of $-3.6993$; correlation 0.998 | | Single-model solution (`do_newton=0`, bundled, $k\approx30$) | Amari distance vs Fortran | 0.006 | +| Every backend against the reference (harness defaults, bundled) | `validate_implementations.py --backend all`: PyTorch, NumPy and MLX each vs Fortran | LL within 3.2e-5; correlation 0.9992; Amari distance 0.004, for all three | | Multi-model solution | distributional similarity over 20-run ensembles | indistinguishable from Fortran's own run-to-run spread ($p = 0.96$) | | Device and precision invariance | same independent components across CPU/CUDA/MPS/MLX, float32/float64, Linux/macOS | identical (1.000) across all eight torch/MLX combinations | | Cross-backend log-likelihood | converged LL across every backend | agree to ~3 significant digits (max pairwise ~0.003) | @@ -30,13 +31,76 @@ Throughout, IC abbreviates independent component and LL log-likelihood. ## The validation harness -`validate_implementations.py` runs the implementations on real sample EEG, +`validate_implementations.py` runs pamica's backends and the Fortran reference binary on the bundled sample EEG, matches components across implementations with the Hungarian algorithm, and reports log-likelihood and per-component correlation. It always uses real sample data and the Fortran binary, never synthetic data. Conformity with Fortran is measured with two metrics used throughout this page: Hungarian-matched component correlation, and the Amari distance (`amari_distance` in `validate_implementations.py`), a standard unmixing-matrix comparison metric (Amari, Cichocki & Yang, 1996) that is permutation- and scale-invariant by construction and so needs no assignment step. +### Running it per backend + +`--backend` selects which pamica backends are compared against the reference (issue #315): + +```bash +uv run python validate_implementations.py # PyTorch only (the default) +uv run python validate_implementations.py --backend numpy # the legacy NumPy backend +uv run python validate_implementations.py --backend mlx # Apple Silicon; needs `uv sync --extra mlx` +uv run python validate_implementations.py --backend all # torch, numpy and mlx +``` + +A comma-separated list such as `--backend torch,mlx` also works. +Every backend gets the same settings: `sample_params.json` read through the shared canonical reader (`block_size=512`, Newton on from iteration 50), +plus `--max-iter` (default 100) and `--seed` (default 42). +The reference runs once, with the same settings, the seed pinned and one thread, and each backend is compared against that one run. +NumPy receives the settings under its own key names, through the backend's own translation table; +PyTorch and MLX both run through `AMICA(backend=...)`, whose constructors share the canonical names. +A setting a backend cannot apply is named in a warning rather than dropped silently. + +Each backend gets its own report: `validation_report.txt` for PyTorch (the name the default run has always used), +and `validation_report_numpy.txt` or `validation_report_mlx.txt` for the others. +Passing `--backend` explicitly also prints a one-row-per-backend summary with runtimes and saves it as `parity_summary.md`; +the default run prints exactly the report it always has. +`--backend mlx` on a host without MLX stops before running anything, with exit status 2 and the install hint. + +The reference binary is resolved as before: +by default the native engine (`PAMICA_NATIVE_BINARY`, or the release binary for the host, cached after the first download), +falling back, with a warning, to the bundled macOS x86_64 `amica15mac`, which cannot be seeded; +`--fortran-binary PATH` runs a specific binary. + +### Parity rows per backend + +Measured on 2026-09-22 on an Apple M4 Pro (14 cores, 64 GB, macOS 27; MLX 0.32.0, PyTorch 2.12.1, NumPy 2.5.0) +against the v0.3.3 release native engine (`amica15-macos-arm64`, SHA-256 `c8b2ac7f...`), with the harness defaults: + +```bash +uv run python -c "from pamica.native import resolver; print(resolver.resolve(version='v0.3.3'))" +uv run python validate_implementations.py --backend all \ + --fortran-binary ~/.cache/pamica/bin/v0.3.3/amica15-macos-arm64 +``` + +The reference is pinned to that explicit release binary (the first line downloads it and verifies its checksum) rather than to the default resolution, +because the resolver caches its `latest` download without refreshing it, +so on a machine that fetched an earlier release the default can run an older binary than the one these rows were measured with. + +| Backend | Precision | Final LL | LL difference from Fortran | Mean matched correlation | Min matched correlation | Amari distance | Runtime (s) | Expected bar | +|---|---|---:|---:|---:|---:|---:|---:|---| +| Fortran (reference) | float64 | -3.411274 | | | | | 10.0 | the reference | +| PyTorch (`AMICA`) | float64 | -3.411245 | 0.000029 | 0.9992 | 0.9935 | 0.0037 | 17.7 | correlation > 0.95, Amari < 0.05, LL difference < 0.005 | +| NumPy (`AMICA_NumPy`) | float64 | -3.411246 | 0.000029 | 0.9992 | 0.9934 | 0.0037 | 32.8 | the PyTorch bar, and final LL within 1e-5 of PyTorch's | +| MLX (`AMICA(backend="mlx")`) | float32 | -3.411242 | 0.000032 | 0.9992 | 0.9935 | 0.0037 | 3.4 | the PyTorch bar, and final LL within 1e-4 of PyTorch's | + +Every backend meets its bar. +All four runs stop at the 100-iteration budget, so the rows compare matched trajectories rather than converged optima; +the converged single-model evidence is in the next section. +The two float64 backends agree with each other to about 1e-6 in log-likelihood. +MLX computes in float32 (about seven significant digits per operation), so its bar against PyTorch is looser: +it lands within about five significant digits of the float64 likelihood, which is float32 consistency, not float64 parity. +Runtime is one run's wall-clock time for the fit alone and varies by about a third between runs on the same host; +the reference's includes process start-up and is single-threaded. +The same bars are pinned by `test_backend_meets_its_parity_bar_against_fortran` in `pamica/tests/test_fortran_param_forwarding.py`, +which runs when `AMICA_RUN_FORTRAN=1` is set (as the weekly macOS job does). + ## Single-model parity With Newton disabled (`do_newton=0`), the natural-gradient backend reaches Fortran's solution, @@ -434,8 +498,9 @@ guarded to a no-op so the parity results above stay byte-for-byte unchanged. | Component sharing (`share_comps`, #60) | off by default | Fortran `identify_shared_comps` ported; no bit-exact oracle (`Spinv2` is unrunnable), behavior-validated; byte-identical when unshared | | Outlier rejection (`do_reject`, #123) | off by default | `good_idx` mechanism on all three backends (NumPy, PyTorch, MLX -- the last landed epic #278 Phase 3, #289); MLX/NumPy ports validated vs the PyTorch backend | | Degenerate-fit contract (#50) | always | the `AMICA` wrapper refuses a NaN or singular fit (`converged_` / `stop_reason_`); `transform`/`get_*`/`save` raise through the wrapper instead of returning NaN sources. This is a wrapper-level contract, not a raw-backend one -- calling a raw `AMICATorchNG`/`AMICAMLXNG`/`AMICA_NumPy` instance's `transform`/`get_*`/`save` directly, bypassing the wrapper, is not gated (tracked as issue #306). `write_amica_output` is the one exception: both the PyTorch and MLX backends gained the same degenerate/non-finite refusal directly on the raw class in this epic, since it has no wrapper equivalent to gate it. | +| End-to-end workflow (#315) | always, PyTorch and MLX | average-referenced sample EEG with `pcakeep = n_channels - 1` through `AMICAICA`: 31 components, `get_sources` equal to `transform`, an exclusion that removes exactly one back-projection and keeps the residual, the EEGLAB export reloaded by `loadmodout`, `save`/`load`, an `input.param`-driven fit, and the two backends' sources Hungarian-matched with a minimum correlation of at least 0.999 (measured 0.999999999) | -Tests live under `pamica/tests/`: `torch_tests/test_ng_backend.py`, `torch_tests/test_ng_sharing.py`, `torch_tests/test_amica_ng_wrapper.py`, and `test_numpy_reject.py`. +Tests live under `pamica/tests/`: `torch_tests/test_ng_backend.py`, `torch_tests/test_ng_sharing.py`, `torch_tests/test_amica_ng_wrapper.py`, `test_numpy_reject.py`, and `mne_tests/test_end_to_end_workflow.py`. ## Which convergence criterion actually stops a fit @@ -532,13 +597,14 @@ tier: below $k\approx60$ the decomposition is under-determined and backends dive reasons, so a faster variant would reproduce a noisier number and invite the misreading this section exists to prevent. -Two general checks remain useful and are much quicker, but note that neither reproduces a specific -table row: `validate_implementations.py` defaults to a single seed at 100 iterations with `do_newton` -read from `sample_params.json`, and `pytest` runs the parity and behaviour suite. +Two general checks remain useful and are much quicker, but note that neither reproduces a row of +the paper's table: `validate_implementations.py` defaults to a single seed at 100 iterations with `do_newton` +read from `sample_params.json` (it reproduces the [per-backend harness rows](#parity-rows-per-backend) above), +and `pytest` runs the parity and behavior suite. ```bash -uv run python validate_implementations.py # single- and multi-model parity report -uv run pytest # the full parity/behavior test suite +uv run python validate_implementations.py --backend all # single-model parity report, every backend +uv run pytest # the full parity/behavior test suite ``` The multi-model ensemble and Amari detail regenerate from saved fits (no re-fitting) with diff --git a/native/mpi_single.c b/native/mpi_single.c index 396c7b02..f0319669 100644 --- a/native/mpi_single.c +++ b/native/mpi_single.c @@ -46,7 +46,7 @@ void mpi_comm_split_(const int *comm, const int *color, const int *key, } /* Single rank always owns the data; no name buffer to fill beyond a label. The - * hidden Fortran length of `name` is passed last; honour it to avoid overflow. */ + * hidden Fortran length of `name` is passed last; honor it to avoid overflow. */ void mpi_get_processor_name_(char *name, int *resultlen, int *ierr, long name_len) { static const char label[] = "localhost"; diff --git a/pamica/mlx_impl/core.py b/pamica/mlx_impl/core.py index cf219056..73747838 100644 --- a/pamica/mlx_impl/core.py +++ b/pamica/mlx_impl/core.py @@ -1607,7 +1607,7 @@ def _update_parameters(self, acc: dict, n_samples: int): # immutable and gm is # only ever rebound, so a plain rebinding is a safe snapshot (torch # clones because its tensors could be written in place). Exactly gm for - # n_models=1 (both are 1.0) and cancelling for a disjoint comp_list, so + # n_models=1 (both are 1.0) and canceling for a disjoint comp_list, so # the single-model and unshared multi-model paths are unchanged. assert self.gm is not None gm_prev = self.gm @@ -2465,7 +2465,7 @@ def _fit_once( MIR from the current ``W``/``sphere`` every ``mir_step`` iterations and appends it to ``mir_history_`` as ``(iteration, mir_nats, variance)``. ``0`` (default) disables the waypoints and leaves fit - behaviour byte-for-byte unchanged. ``mir_history_`` is a true + behavior byte-for-byte unchanged. ``mir_history_`` is a true trajectory like ``ll_history``: a ``keep_best`` restore does not rewrite it, so the fit-end MIR is ``self.mir(X)`` on the returned parameters, not ``mir_history_[-1]``. Not index-aligned with diff --git a/pamica/numpy_impl/core.py b/pamica/numpy_impl/core.py index 1290b914..6a18ed40 100644 --- a/pamica/numpy_impl/core.py +++ b/pamica/numpy_impl/core.py @@ -303,6 +303,13 @@ def __init__( (which parses ``pcadb`` but never uses it). Both are ignored, with one WARNING, when ``do_sphere`` is False, as in the reference. See :mod:`pamica.rank`. + + ``outdir`` (None) is where the fit writes its ``out.txt`` log, + its ``writestep`` checkpoints, its ``do_history`` snapshots and its + final results, in the Fortran ``amicaout`` layout. The default + writes no files at all, like the PyTorch and MLX backends; set it + (here, in a params file, or with the CLI's ``--outdir``) to get the + reference's on-disk output. """ # Store progress bar settings self.use_tqdm = use_tqdm @@ -478,7 +485,13 @@ def __init__( "(logistic) and 4 (sub-Gaussian cosh+) are available on " "AMICA (AMICATorchNG) and AMICAMLXNG." ) - self.outdir = Path(params.get("outdir", "output")) + # Where results are written. None (the default) writes nothing: no + # out.txt log, no writestep checkpoints, no history and no final + # results, the same as the PyTorch and MLX backends, which never write + # unless asked. An explicit outdir (keyword, params file or the CLI's + # --outdir) writes all of them there, as the reference binary does. + outdir = params.get("outdir") + self.outdir: Optional[Path] = None if outdir is None else Path(outdir) # Data-source config (used by fit() when called without explicit # data), read off the same parsed dict as the hyperparameters above @@ -611,11 +624,6 @@ def _setup_logging(self): self.logger = logging.getLogger("AMICA") self.logger.setLevel(logging.INFO) - # Ensure output directory exists - self.outdir = Path(self.outdir) - if not self.outdir.exists(): - self.outdir.mkdir(parents=True) - # Remove any existing handlers for handler in self.logger.handlers[:]: self.logger.removeHandler(handler) @@ -626,16 +634,25 @@ def _setup_logging(self): console_handler.setFormatter(console_formatter) self.logger.addHandler(console_handler) - # Add file handler for out.txt - self.file_path = self.outdir / "out.txt" - file_handler = logging.FileHandler(self.file_path, mode="w") - file_formatter = logging.Formatter("%(message)s") - file_handler.setFormatter(file_formatter) - self.logger.addHandler(file_handler) + # Add file handler for out.txt, only when an output directory was given. + self.file_path: Optional[Path] = None + if self.outdir is not None: + self.outdir.mkdir(parents=True, exist_ok=True) + self.file_path = self.outdir / "out.txt" + file_handler = logging.FileHandler(self.file_path, mode="w") + file_formatter = logging.Formatter("%(message)s") + file_handler.setFormatter(file_formatter) + self.logger.addHandler(file_handler) # Prevent propagation to avoid duplicate logging self.logger.propagate = False + def _append_to_log_file(self, line: str) -> None: + """Append ``line`` to ``outdir/out.txt``; a no-op without an outdir.""" + if self.file_path is not None: + with open(self.file_path, "a") as f: + f.write(line + "\n") + @classmethod def from_params_file(cls, params_file: str, **kwargs) -> "AMICA": """ @@ -755,11 +772,17 @@ def get_sensor_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: ``Spinv`` mapping (amica15.f90:568-578), and the only way to recover sensor maps when rank reduction has made the sphere non-square (issue #223). Mirrors ``AMICATorchNG.get_sensor_mixing_matrix``. + + ``A`` here is the true mixing matrix, the stored ``A`` transposed: the + stored ``W = inv(stored A)`` is itself the transpose of the unmixing + (:meth:`get_weights`, issue #24 convention). So the result inverts the + spatial filter the sources come from, ``get_weights() @ sphere @ result + == I``, and each column is one component's sensor map. """ if self.sphere is None or self.A is None or self.comp_list is None: raise RuntimeError("Model has not been fitted yet; call fit() first.") self._check_usable("get the sensor mixing matrix") - A = self.A[:, self.comp_list[:, model_idx]] + A = self.A[:, self.comp_list[:, model_idx]].T return self._pinv_sphere() @ A def get_weights(self) -> np.ndarray: @@ -895,8 +918,9 @@ def fit(self, data: Optional[np.ndarray] = None) -> "AMICA": # every checkpoint, so a run that diverged to a non-finite LL (issue #39) # or ended holding non-finite parameters (issue #240) cannot overwrite # the last good on-disk result with NaNs. Under best-of-N restarts this - # writes the WINNER, whose state is live by the time it runs. - if self.converged: + # writes the WINNER, whose state is live by the time it runs. Nothing + # is written without an outdir. + if self.converged and self.outdir is not None: self._write_results() return self @@ -2048,7 +2072,7 @@ def _update_parameters(self, updates: Dict): # only happens to broadcast when num_models == 1: every multi-model # Newton fit raised "operands could not be broadcast together with # shapes (data_dim, num_models) (num_models, 1)". Same as the torch - # backend's ``dgm.unsqueeze(0)`` (torch_impl/core.py:1323). + # backend's ``dgm.unsqueeze(0)`` (``AMICATorchNG._finalize_newton_stats``). dgm = updates["dgm"][None, :] self.sigma2 = updates["dsigma2"] / dgm self.lambda_ = updates["dlambda"] / dgm @@ -2330,8 +2354,7 @@ def _optimize(self): ) # Always write detailed logs to the file - with open(self.file_path, "a") as f: - f.write(detailed_log + "\n") + self._append_to_log_file(detailed_log) # Also log to console if verbose or not using tqdm if self.verbose or not self.use_tqdm: @@ -2423,10 +2446,18 @@ def _optimize(self): # than one interval. Both are skipped (loudly) from a non-finite # state so a checkpoint cannot persist NaN parameters that # loadmodout reads back without complaint (issue #240). - if self.writestep > 0 and itf % self.writestep == 0: + if ( + self.outdir is not None + and self.writestep > 0 + and itf % self.writestep == 0 + ): self._write_checkpoint("results") - if self.do_history and itf % self.histstep == 0: + if ( + self.outdir is not None + and self.do_history + and itf % self.histstep == 0 + ): self._write_checkpoint("history") finally: # Close the progress bar if using tqdm @@ -2440,16 +2471,14 @@ def _optimize(self): ) self.logger.info(final_metrics) # Also log to file if using tqdm (since it wouldn't be logged during iterations) - with open(self.file_path, "a") as f: - f.write(final_metrics + "\n") + self._append_to_log_file(final_metrics) # Record and log the reason the loop stopped (None if it ran to # max_iter). fit() uses self.converged for the terminal outcome. self.stop_reason = convergence_reason if convergence_reason: self.logger.info(convergence_reason) - with open(self.file_path, "a") as f: - f.write(convergence_reason + "\n") + self._append_to_log_file(convergence_reason) # Log final message (only once) final_message = f"Optimization finished after {final_iter + 1} iterations" @@ -2668,6 +2697,7 @@ def _write_results(self): # (loadmodout treats 'nd' as optional). from .load import write_amicaout + assert self.outdir is not None, "_write_results needs an outdir" Lht, Lt = self._llt_arrays() write_amicaout( @@ -2706,6 +2736,7 @@ def _write_history(self): and self.sphere is not None and self.comp_list is not None ) + assert self.outdir is not None, "_write_history needs an outdir" hist_dir = self.outdir / "history" / f"{self.iter:06d}" if not hist_dir.exists(): hist_dir.mkdir(parents=True) diff --git a/pamica/numpy_impl/data.py b/pamica/numpy_impl/data.py index e2b54dc4..324f68e2 100644 --- a/pamica/numpy_impl/data.py +++ b/pamica/numpy_impl/data.py @@ -282,9 +282,17 @@ def _read(name, dtype: type = np.float64): if mean is not None: results["mean"] = mean + # The sphere is (nw, nx): square unless rank reduction kept nw < nx + # dimensions, in which case write_amicaout pads it with zero rows to the + # reference's (nx, nx) record and writes it column-major (a square one is + # written C-order). Read it back the way it was written. S = _read("S") if S is not None: - results["sphere"] = S.reshape(nw, nw) + nx = int(round(np.sqrt(len(S)))) + if nx == nw: + results["sphere"] = S.reshape(nw, nw) + else: + results["sphere"] = S.reshape(nx, nx, order="F")[:nw] ll = _read("LL") if ll is not None: diff --git a/pamica/numpy_impl/viz.py b/pamica/numpy_impl/viz.py index d05d5444..ab219885 100644 --- a/pamica/numpy_impl/viz.py +++ b/pamica/numpy_impl/viz.py @@ -40,6 +40,41 @@ from .data import load_results +def _component_maps_and_sources( + results: dict, data: Optional[np.ndarray] = None, model_idx: int = 0 +) -> Tuple[np.ndarray, Optional[np.ndarray]]: + """Sensor maps and sources of model ``model_idx`` from :func:`load_results`. + + The same quantities :meth:`AMICA_NumPy.get_sensor_mixing_matrix` and + :meth:`AMICA_NumPy.transform` return for the fitted model the directory was + written from. ``load_results`` hands back the stored arrays, whose ``W`` is + the unmixing transposed and whose ``A`` is the mixing transposed (issue #24 + convention), so the maps are ``pinv(sphere) @ A[:, comps].T`` and the + sources are ``W.T @ (sphere @ (data - mean) - c)``. + + Returns + ------- + maps : ndarray, shape (n_channels, n_components) + One sensor map per column. + sources : ndarray, shape (n_components, n_samples), or None + The sources of ``data``; ``None`` when no data is given. + """ + missing = sorted({"sphere", "mean", "c"} - results.keys()) + if missing: + raise KeyError( + f"AMICA results are missing {missing}; the maps and sources need " + "the sphere, mean and model centers written with the fit." + ) + comps = results["comp_list"][:, model_idx] + sphere = results["sphere"] + maps = np.linalg.pinv(sphere) @ results["A"][:, comps].T + if data is None: + return maps, None + sphered = sphere @ (data - results["mean"][:, None]) + centered = sphered - results["c"][:, model_idx][:, None] + return maps, results["W"][:, :, model_idx].T @ centered + + def plot_convergence( results_dir: Union[str, Path], figsize: Tuple[int, int] = (10, 5), @@ -97,7 +132,10 @@ def plot_components( results_dir : str or Path Directory containing results data : ndarray, optional - Data array to compute activations + Data, ``(n_channels, n_samples)`` in the units the model was fitted on, + whose sources (as :meth:`AMICA_NumPy.transform` computes them) are + plotted as activation distributions. The mixing vectors are the sensor + maps :meth:`AMICA_NumPy.get_sensor_mixing_matrix` returns. max_comps : int Maximum number of components to plot figsize : tuple @@ -107,30 +145,30 @@ def plot_components( raw Fortran binary format, never compressed .npz (issue #30). """ results = load_results(results_dir, compressed) + maps, sources = _component_maps_and_sources(results, data) # Get number of components to plot - n_comps = min(results["A"].shape[1], max_comps) + n_comps = min(maps.shape[1], max_comps) if figsize is None: figsize = (12, 2 * n_comps) - fig, axes = plt.subplots(n_comps, 2, figsize=figsize) + fig, axes = plt.subplots(n_comps, 2, figsize=figsize, squeeze=False) - # Plot mixing vectors and activations + # Plot sensor maps (mixing vectors) and activations for i in range(n_comps): # Plot mixing vector ax = axes[i, 0] - ax.plot(results["A"][:, i], "b-") + ax.plot(maps[:, i], "b-") ax.set_title(f"Component {i + 1}: Mixing Vector") ax.set_xlabel("Channel") ax.set_ylabel("Weight") ax.grid(True) # Plot activation if data provided - if data is not None: + if sources is not None: ax = axes[i, 1] - activation = np.dot(results["W"][i, :, 0], data) - ax.hist(activation, bins=50, density=True) + ax.hist(sources[i], bins=50, density=True) ax.set_title(f"Component {i + 1}: Activation Distribution") ax.set_xlabel("Activation") ax.set_ylabel("Density") @@ -154,7 +192,9 @@ def plot_model_comparison( results_dir : str or Path Directory containing results data : ndarray - Data array to reconstruct + Data to reconstruct, ``(n_channels, n_samples)`` in the units the model + was fitted on. Each model's reconstruction is its sensor maps times + its sources, plus the model center and the data mean. n_examples : int Number of example time points figsize : tuple @@ -171,12 +211,24 @@ def plot_model_comparison( if figsize is None: figsize = (12, 3 * n_examples) - fig, axes = plt.subplots(n_examples, n_models + 1, figsize=figsize) + fig, axes = plt.subplots(n_examples, n_models + 1, figsize=figsize, squeeze=False) # Randomly select time points rng = np.random.RandomState(42) times = rng.choice(data.shape[1], n_examples, replace=False) + # Each model's reconstruction in sensor space: its maps times its sources, + # plus what transform() subtracted (the model center, mapped back through + # the sphere, and the data mean). Exact for a full-rank fit; the retained + # subspace for a rank-reduced one. + pinv_sphere = np.linalg.pinv(results["sphere"]) + reconstructions = [] + for h in range(n_models): + maps, sources = _component_maps_and_sources(results, data[:, times], h) + assert sources is not None + offset = pinv_sphere @ results["c"][:, h] + results["mean"] + reconstructions.append(maps @ sources + offset[:, None]) + for i, t in enumerate(times): # Plot original data ax = axes[i, 0] @@ -190,11 +242,7 @@ def plot_model_comparison( for h in range(n_models): ax = axes[i, h + 1] - # Get reconstruction - S = np.dot(results["W"][:, :, h], data[:, t : t + 1]) - X_hat = np.dot(results["A"][:, results["comp_list"][:, h]], S) - - ax.plot(X_hat, "r-") + ax.plot(reconstructions[h][:, i], "r-") ax.plot(data[:, t], "k--", alpha=0.5) ax.set_title(f"Model {h + 1}" if i == 0 else "") ax.set_xlabel("Channel") @@ -253,7 +301,9 @@ def plot_pdf_fits( results_dir : str or Path Directory containing results data : ndarray - Data array + Data, ``(n_channels, n_samples)`` in the units the model was fitted + on, whose model-0 sources (as :meth:`AMICA_NumPy.transform` computes + them) are histogrammed against each fitted source density. max_comps : int Maximum number of components to plot n_points : int @@ -267,9 +317,12 @@ def plot_pdf_fits( from .pdf import compute_pdf results = load_results(results_dir, compressed) + _, S = _component_maps_and_sources(results, data) + assert S is not None + comps = results["comp_list"][:, 0] # Get number of components to plot - n_comps = min(results["A"].shape[1], max_comps) + n_comps = min(S.shape[0], max_comps) n_mix = results["alpha"].shape[0] if figsize is None: @@ -279,9 +332,6 @@ def plot_pdf_fits( if n_comps == 1: axes = [axes] - # Get activations - S = np.dot(results["W"][:, :, 0], data) - for i in range(n_comps): ax = axes[i] @@ -293,11 +343,11 @@ def plot_pdf_fits( pdf_total = np.zeros_like(x) for j in range(n_mix): - # Get mixture parameters - alpha = results["alpha"][j, i] - mu = results["mu"][j, i] - beta = results["beta"][j, i] - rho = results["rho"][j, i] + # Get mixture parameters (source i of model 0 is column comps[i]) + alpha = results["alpha"][j, comps[i]] + mu = results["mu"][j, comps[i]] + beta = results["beta"][j, comps[i]] + rho = results["rho"][j, comps[i]] # Compute PDF y = beta * (x - mu) diff --git a/pamica/tests/conftest.py b/pamica/tests/conftest.py new file mode 100644 index 00000000..54d63baa --- /dev/null +++ b/pamica/tests/conftest.py @@ -0,0 +1,27 @@ +"""Suite-wide fixtures. + +Every test runs with its working directory set to a per-session temporary +directory, so nothing the suite writes to a relative path lands in the +repository. This is defense in depth: the legacy NumPy backend used to default +``outdir`` to ``./output`` and write ``out.txt`` and its model files there +from every fit (63 tests did), and a relative ``outdir`` in a params file +(``sample_params.json`` says ``./amicaout/``) or the NumPy CLI's ``--outdir`` +default of ``output`` still resolve against the working directory. Tests that +need a repository-relative path (a params file whose ``files`` entry is +relative to the repository root, say) resolve it from ``__file__`` or +``monkeypatch.chdir`` explicitly. +""" + +import pytest + + +@pytest.fixture(scope="session", autouse=True) +def _cwd_outside_the_repository(tmp_path_factory): + """Run the session from a temporary directory, restored afterwards. + + Session-scoped, so it is in place before any module- or class-scoped + fixture runs; under ``pytest-xdist`` each worker gets its own directory. + """ + with pytest.MonkeyPatch.context() as mp: + mp.chdir(tmp_path_factory.mktemp("cwd")) + yield diff --git a/pamica/tests/mlx_tests/test_mlx_export.py b/pamica/tests/mlx_tests/test_mlx_export.py index 143f0795..b8620030 100644 --- a/pamica/tests/mlx_tests/test_mlx_export.py +++ b/pamica/tests/mlx_tests/test_mlx_export.py @@ -1,6 +1,5 @@ """EEGLAB export (``write_amica_output``) on the MLX backend -- issue #92, -epic #278 Phase 3/#289 (port of ``AMICATorchNG.write_amica_output``, -torch_impl/core.py:3285-3366). +epic #278 Phase 3/#289 (port of ``AMICATorchNG.write_amica_output``). Round-trips through the shared :func:`pamica.numpy_impl.load.loadmodout` reader (the same reader real EEGLAB output is validated against, issue diff --git a/pamica/tests/mlx_tests/test_mlx_mir.py b/pamica/tests/mlx_tests/test_mlx_mir.py index 64b07c55..84764b72 100644 --- a/pamica/tests/mlx_tests/test_mlx_mir.py +++ b/pamica/tests/mlx_tests/test_mlx_mir.py @@ -1,7 +1,7 @@ """MIR/PMI diagnostics on the MLX backend -- issue #137, epic #278 Phase -3/#289 (port of ``AMICATorchNG.mir``/``pmi``, torch_impl/core.py:2929-3036, -and the ``fit(mir_step=...)`` waypoint machinery, torch_impl/core.py's -issue #161 tests in ``test_ng_convergence.py``/``test_amica_ng_wrapper.py``). +3/#289 (port of ``AMICATorchNG.mir``/``pmi``, and the ``fit(mir_step=...)`` +waypoint machinery, torch_impl/core.py's issue #161 tests in +``test_ng_convergence.py``/``test_amica_ng_wrapper.py``). MLX-only mechanics: the composition pin (against ``pamica.metrics.mir`` directly, order-independent of any other backend), the ``mir_step`` @@ -210,7 +210,7 @@ def test_max_iter_zero_raises(real_data): def test_mir_step_zero_matches_omitted_argument(real_data): - """mir_step=0 (explicit) must leave fit() behaviour byte-for-byte + """mir_step=0 (explicit) must leave fit() behavior byte-for-byte identical to not passing mir_step at all.""" default_m = _fit(real_data, max_iter=3, keep_best=False) explicit_m = AMICAMLXNG( diff --git a/pamica/tests/mlx_tests/test_mlx_reject.py b/pamica/tests/mlx_tests/test_mlx_reject.py index f82deac9..e27fc24d 100644 --- a/pamica/tests/mlx_tests/test_mlx_reject.py +++ b/pamica/tests/mlx_tests/test_mlx_reject.py @@ -203,7 +203,7 @@ def test_keep_best_inactive_reason_prefers_do_reject_when_both_are_on( ): """PR #311 review: when do_reject AND share_comps are BOTH on, the reported reason must be "do_reject", matching AMICATorchNG's exact - precedence (torch_impl/core.py:2425, ``"do_reject" if self.do_reject + precedence (``AMICATorchNG._fit_once``: ``"do_reject" if self.do_reject else "share_comps"``) -- the two backends must report the same reason for the same configuration, not whichever flag MLX happened to check first.""" diff --git a/pamica/tests/mlx_tests/test_mlx_scoring.py b/pamica/tests/mlx_tests/test_mlx_scoring.py index 59f39ab9..7b85a3e6 100644 --- a/pamica/tests/mlx_tests/test_mlx_scoring.py +++ b/pamica/tests/mlx_tests/test_mlx_scoring.py @@ -1,6 +1,6 @@ """``model_loglik``/``model_probability`` on the MLX backend -- issue #141, epic #278 Phase 3/#289 (port of ``AMICATorchNG.model_loglik``/ -``model_probability``, torch_impl/core.py:3041-3132). +``model_probability``). Real sample EEG only (no synthetic/mock). Unlike the torch backend (float64 throughout), MLX's ``model_loglik`` recomputes the sphere-multiply in diff --git a/pamica/tests/mne_tests/test_end_to_end_workflow.py b/pamica/tests/mne_tests/test_end_to_end_workflow.py new file mode 100644 index 00000000..f91e258c --- /dev/null +++ b/pamica/tests/mne_tests/test_end_to_end_workflow.py @@ -0,0 +1,254 @@ +"""The workflow we run ourselves, end to end, on each backend (issue #315). + +Epic #324's first users fit AMICA per session on average-referenced EEG, +keeping ``pcakeep = n_channels - 1`` components (average referencing removes +one dimension), then clean the recording in MNE and hand the same fit to +EEGLAB. This module runs that pipeline on the bundled real EEG through the +public API only, once per backend (the PyTorch default and +``backend="mlx"``), and checks each hand-off: + +1. ``AMICAICA`` fit: 31 components, ``get_sources`` equal to the wrapper's + ``transform``; +2. ``apply`` with one component excluded: only that component's + back-projection is removed, and the discarded residual is kept; +3. the EEGLAB export reloads through ``loadmodout`` with the reduced rank + handled as the reference writes it; +4. ``AMICA.save``/``AMICA.load`` gives identical sources; +5. a fit driven by the bundled Fortran ``input.param``; +6. the two backends find the same components. + +It is the trust gate for the whole pipeline, so it stays fast (a few seconds +per backend) and is not marked slow: CI's macOS job runs it on both backends. +MLX tests skip individually without MLX or an Apple GPU, like the other MLX +suites; the PyTorch ones run everywhere the MNE extra is installed. +""" + +from pathlib import Path + +import numpy as np +import pytest + +mne = pytest.importorskip("mne") + +from pamica import AMICA # noqa: E402 (after importorskip) +from pamica.mne_compat import AMICAICA # noqa: E402 +from pamica.numpy_impl.load import loadmodout # noqa: E402 +from pamica.tests.test_pca_reduction_cross_backend import ( # noqa: E402 + _matched_abs_corr, +) + +mne.set_log_level("ERROR") + +SAMPLE_DIR = Path(__file__).resolve().parents[2] / "sample_data" +SET_FILE = SAMPLE_DIR / "eeglab_data.set" +PARAM_FILE = SAMPLE_DIR / "input.param" +SEED = 42 +MAX_ITER = 10 # the hand-offs are functions of the fitted state, not convergence +NW = 32 +N_KEEP = NW - 1 # pcakeep = n_channels - 1 after the average reference +BACKENDS = ["torch", "mlx"] + +# Anything computed in float64 from the fitted state: apply(), the residual +# leak of an exclusion (measured 4e-16 to 2e-15 on both backends). +REL_TOL = 1e-10 +# What passes through a backend's own transform, the sources and a component's +# back-projection: float64 for PyTorch (measured 3-6e-15), float32 for MLX +# (measured 1.9-2.2e-7). +TRANSFORM_TOL = {"torch": 1e-10, "mlx": 1e-5} + +pytestmark = pytest.mark.skipif( + not SET_FILE.exists(), reason="sample eeglab_data.set missing" +) + + +def _require_backend(backend: str) -> None: + """Skip this test (never the module) when ``backend`` cannot run here.""" + if backend == "mlx": + mlx_core = pytest.importorskip( + "mlx.core", reason="MLX not installed (Apple Silicon only)" + ) + if mlx_core.default_device().type != mlx_core.DeviceType.gpu: + pytest.skip("no Apple GPU") + + +def _rel(a, b) -> float: + """Frobenius-norm relative difference of ``a`` from ``b``.""" + return float(np.linalg.norm(a - b) / np.linalg.norm(b)) + + +@pytest.fixture(scope="module") +def raw() -> "mne.io.BaseRaw": + """The bundled EEG, average-referenced as in our own preprocessing.""" + raw = mne.io.read_raw_eeglab(str(SET_FILE), preload=True) + raw.set_eeg_reference("average") + return raw + + +@pytest.fixture(scope="module") +def fitted(raw): + """One ``AMICAICA`` fit per backend, shared by the checks below.""" + cache: dict = {} + + def get(backend: str) -> AMICAICA: + _require_backend(backend) + if backend not in cache: + n_eeg = len(mne.pick_types(raw.info, eeg=True)) + ica = AMICAICA(backend=backend, random_state=SEED, verbose=False) + ica.fit(raw, picks="eeg", pcakeep=n_eeg - 1, max_iter=MAX_ITER) + cache[backend] = ica + return cache[backend] + + return get + + +def _fit_data(raw, ica: AMICAICA) -> np.ndarray: + """The data the backend was fitted on: the picked channels, pre-whitened.""" + return raw.get_data(picks=ica.ch_names_) / ica.pre_whitener_ + + +def test_average_reference_leaves_rank_n_minus_one(raw): + """The premise of ``pcakeep = n_channels - 1``: after the average + reference the recording has one dimension fewer than it has channels.""" + x = raw.get_data(picks="eeg") + s = np.linalg.svd(x, compute_uv=False) + assert x.shape[0] == NW + assert int((s > 1e-10 * s[0]).sum()) == N_KEEP + + +# --- 1. fit --------------------------------------------------------------------- +@pytest.mark.parametrize("backend", BACKENDS) +def test_fit_keeps_n_minus_one_components(raw, fitted, backend): + ica = fitted(backend) + assert ica.converged_ is True + assert ica.amica_ is not None and ica.amica_.backend == backend + assert ica.n_components_ == N_KEEP + assert ica.amica_.get_sphere().shape == (N_KEEP, NW) + s_mne = ica.get_sources(raw).get_data() + s_amica = ica.amica_.transform(_fit_data(raw, ica)) + assert s_mne.shape == s_amica.shape == (N_KEEP, raw.n_times) + assert _rel(s_mne, s_amica) <= TRANSFORM_TOL[backend] + + +# --- 2. clean one component ------------------------------------------------------- +@pytest.mark.parametrize("backend", BACKENDS) +def test_excluding_a_component_removes_only_its_back_projection(raw, fitted, backend): + """``apply(exclude=[k])`` keeps every channel and changes the data by + exactly minus component ``k``'s back-projection (the scalp map times the + source, computed from the wrapper's own sphere, unmixing and transform), + with nothing leaking into the discarded residual subspace.""" + ica = fitted(backend) + amica = ica.amica_ + assert amica is not None + k = 0 + x = raw.get_data(picks=ica.ch_names_) + out = ica.apply(raw.copy(), exclude=[k]).get_data(picks=ica.ch_names_) + assert out.shape == x.shape == (NW, raw.n_times) + + pw = ica.pre_whitener_ + a_k = ( + np.linalg.pinv(amica.get_sphere()) @ np.linalg.inv(amica.get_unmixing_matrix()) + )[:, k] + s_k = amica.transform(x / pw)[k].astype(np.float64) + change = out - x + assert _rel(change, -pw * np.outer(a_k, s_k)) <= TRANSFORM_TOL[backend] + + # Exactly one dimension is removed: the residual the fit discarded is + # restored, not dropped with the component (issue #322). Ranks are of the + # time-centered data, since the removed back-projection is of centered + # sources (it adds a constant offset per channel). + residual_rows = ica.pca_components_[N_KEEP:] + d = change / pw + assert np.linalg.norm(residual_rows @ d) <= REL_TOL * np.linalg.norm(d) + + def rank(data): + centered = data - data.mean(axis=1, keepdims=True) + s = np.linalg.svd(centered, compute_uv=False) + return int((s > 1e-8 * s[0]).sum()) + + assert rank(x) == N_KEEP + assert rank(out) == N_KEEP - 1 + + # Nothing excluded gives the input back, residual included. + same = ica.apply(raw.copy()).get_data(picks=ica.ch_names_) + assert _rel(same, x) <= REL_TOL + + +# --- 3. EEGLAB export --------------------------------------------------------------- +@pytest.mark.parametrize("backend", BACKENDS) +def test_eeglab_export_reloads_with_the_reduced_rank(raw, fitted, backend, tmp_path): + """``write_amica_output`` through the wrapper reloads with ``loadmodout`` + (the NumPy port of EEGLAB's ``loadmodout15``): a 31-dimensional model in a + 32-channel space, the sphere padded with zero rows to the reference's + ``nx x nx`` record, and ``W``, ``S`` and ``A`` consistent with each other + and with the live model.""" + ica = fitted(backend) + amica = ica.amica_ + assert amica is not None + outdir = tmp_path / "amicaout" + amica.write_amica_output(str(outdir)) + out = loadmodout(outdir) + + assert (out.num_pcs, out.data_dim, out.num_models) == (N_KEEP, NW, 1) + assert out.W.shape == (N_KEEP, N_KEEP, 1) + assert out.S.shape == (NW, NW) + assert out.A.shape == (NW, N_KEEP, 1) + np.testing.assert_array_equal(out.S[N_KEEP:], 0.0) + unmix = out.W[:, :, 0] @ out.S[:N_KEEP] + np.testing.assert_allclose(unmix @ out.A[:, :, 0], np.eye(N_KEEP), atol=1e-8) + + # Up to loadmodout's variance order and per-component rescaling, the + # reloaded sources are the live ones. + x = _fit_data(raw, ica) + order = np.asarray(out.origord).ravel() + assert sorted(order.tolist()) == list(range(N_KEEP)) + loaded = out.sources(x) + live = amica.transform(x).astype(np.float64)[order] + scale = (loaded * live).sum(axis=1) / (live**2).sum(axis=1) + residual = loaded - scale[:, None] * live + assert np.linalg.norm(residual) <= TRANSFORM_TOL[backend] * np.linalg.norm(loaded) + + +# --- 4. save and load ----------------------------------------------------------------- +@pytest.mark.parametrize("backend", BACKENDS) +def test_save_load_gives_identical_sources(raw, fitted, backend, tmp_path): + ica = fitted(backend) + amica = ica.amica_ + assert amica is not None + path = tmp_path / "model.pt" + amica.save(str(path)) + loaded = AMICA.load(str(path)) + assert loaded.backend == backend + x = _fit_data(raw, ica) + np.testing.assert_array_equal(loaded.transform(x), amica.transform(x)) + + +# --- 5. a params-file driven run -------------------------------------------------------- +@pytest.mark.parametrize("backend", BACKENDS) +def test_input_param_drives_the_fit(raw, backend): + """The bundled Fortran ``input.param`` configures the fit on either + backend: its settings reach the backend (``block_size 512``, Newton on, + three mixtures), and its ``pcakeep 32`` is capped by the data's rank 31.""" + _require_backend(backend) + model = AMICA.from_params_file(str(PARAM_FILE), backend=backend, verbose=False) + model.fit(raw.get_data(picks="eeg"), max_iter=3, seed=SEED) + assert model.converged_ is True + backend_model = model.model_ + assert backend_model is not None + assert backend_model.block_size == 512 + assert backend_model.do_newton is True + assert backend_model.n_mix == 3 + assert backend_model.n_channels == N_KEEP + assert len(model.ll_history_) == 3 + + +# --- 6. the two backends agree ------------------------------------------------------------ +def test_torch_and_mlx_find_the_same_components(raw, fitted): + """Same data, seed and settings on both backends: the same number of + components and the same sources (Hungarian-matched |corr|, the bar of the + other torch-vs-MLX suites; measured min 0.999999999).""" + t, m = fitted("torch"), fitted("mlx") + assert t.n_components_ == m.n_components_ == N_KEEP + matched, _ = _matched_abs_corr( + t.get_sources(raw).get_data(), m.get_sources(raw).get_data() + ) + assert matched.min() >= 0.999, f"min matched |corr| {matched.min():.6f}" diff --git a/pamica/tests/mne_tests/test_mne_meg.py b/pamica/tests/mne_tests/test_mne_meg.py index f4720ac4..94492751 100644 --- a/pamica/tests/mne_tests/test_mne_meg.py +++ b/pamica/tests/mne_tests/test_mne_meg.py @@ -39,7 +39,7 @@ def raw(): @pytest.fixture(scope="module") def mixed_type_raw(raw): - """Real EEG relabelled as two channel types on MEG-like unit scales. + """Real EEG relabeled as two channel types on MEG-like unit scales. Half the channels become magnetometers (~1e-13 T), half gradiometers (~1e-11 T/m). The signals are the recording's own; only the declared type and diff --git a/pamica/tests/mne_tests/test_mne_wrapper.py b/pamica/tests/mne_tests/test_mne_wrapper.py index 54f6b2fe..03c413c6 100644 --- a/pamica/tests/mne_tests/test_mne_wrapper.py +++ b/pamica/tests/mne_tests/test_mne_wrapper.py @@ -160,7 +160,7 @@ def test_plot_components_returns_figure(fitted): def test_pca_components_orthonormal_and_variance_ordered(raw, fitted): - """The eigenbasis ordering (invisible to the V-cancelling round trip) is real. + """The eigenbasis ordering (invisible to the V-canceling round trip) is real. get_sources/get_components reduce to W@sphere and inv(sphere)@inv(W) for ANY orthonormal V, so they cannot see a wrong eigenvector order. Pin it here: diff --git a/pamica/tests/test_fortran_param_forwarding.py b/pamica/tests/test_fortran_param_forwarding.py index c509366b..2143cf56 100644 --- a/pamica/tests/test_fortran_param_forwarding.py +++ b/pamica/tests/test_fortran_param_forwarding.py @@ -1,13 +1,18 @@ -"""Parity-harness parameter forwarding (issue #228). +"""Parity harness (``validate_implementations.py``): parameter forwarding +(issue #228) and per-backend dispatch (issue #315). Both arms of a parity run must be configured identically. The writer previously rewrote six hardcoded keys and left everything else at the template's value while the Python side honored it, which silently makes the comparison uncontrolled. +The same holds across backends: each one the harness runs must receive the same +settings, which the dispatch tests below pin. """ +import os import sys from pathlib import Path +import numpy as np import pytest ROOT = Path(__file__).resolve().parents[2] @@ -182,3 +187,259 @@ def test_env_override_selects_the_native_engine(tmp_path, monkeypatch): from validate_implementations import default_reference_binary assert default_reference_binary(download=False) == fake.resolve() + + +# --- per-backend dispatch (issue #315) --------------------------------------- +# +# The harness runs torch, NumPy and MLX against the same reference. These are +# the cheap checks (a couple of iterations on a slice of the real sample, no +# Fortran binary): argument parsing, that every runner maps the same canonical +# params onto its backend, and that the report/summary files land where the +# docs say. The Fortran-backed comparison is the AMICA_RUN_FORTRAN-gated test +# at the end of this module. + +_DISPATCH_SAMPLES = 4096 + + +def _mlx_or_skip(): + """Skip unless MLX imports and sees an Apple GPU, like the MLX suites.""" + mx = pytest.importorskip("mlx.core") + if mx.default_device().type != mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + + +@pytest.fixture(scope="module") +def dispatch_input(): + """The harness's own data and params, capped at two iterations.""" + from validate_implementations import load_sample_data + + data, params = load_sample_data() + params["max_iter"] = 2 + return data[:, :_DISPATCH_SAMPLES], params + + +@pytest.fixture(scope="module") +def dispatch_out(tmp_path_factory): + """The ``output_dir`` every dispatch run below is handed.""" + return tmp_path_factory.mktemp("dispatch") + + +@pytest.fixture(scope="module") +def dispatch_results(dispatch_input, dispatch_out): + """One two-iteration run per available backend, shared by the checks below.""" + from validate_implementations import BACKENDS, mlx_unavailable_reason, run_backend + + data, params = dispatch_input + return { + name: run_backend(name, data, dict(params), dispatch_out, 42) + for name in BACKENDS + if name != "mlx" or mlx_unavailable_reason() is None + } + + +def test_backend_flag_parses_names_lists_and_all(): + from validate_implementations import BACKENDS, build_parser, parse_backends + + assert parse_backends("torch") == ["torch"] + assert parse_backends("all") == list(BACKENDS) + # Order is the caller's, duplicates dropped, whitespace tolerated. + assert parse_backends("mlx, torch,mlx") == ["mlx", "torch"] + parser = build_parser() + assert parser.parse_args([]).backend is None # the historical torch-only run + assert parser.parse_args(["--backend", "numpy,torch"]).backend == [ + "numpy", + "torch", + ] + + +@pytest.mark.parametrize("value", ["cuda", "torch,", "", "ALL"]) +def test_backend_flag_rejects_unknown_names(value, capsys): + from validate_implementations import build_parser + + with pytest.raises(SystemExit) as exc: + build_parser().parse_args(["--backend", value]) + assert exc.value.code == 2 + assert "choose from torch, numpy, mlx" in capsys.readouterr().err + + +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_every_backend_returns_the_comparison_contract(backend, dispatch_results): + """Each runner returns what compare_results reads, in the Fortran layout: + ``W`` the true unmixing and ``A`` the true mixing, so ``A @ W == I`` for a + full-rank single-model fit (a transposed ``A`` would fail this).""" + if backend == "mlx": + _mlx_or_skip() + res = dispatch_results[backend] + assert res["final_iter"] == 2 == len(res["ll_history"]) + assert np.isfinite(res["final_ll"]) + assert res["W"].shape == res["A"].shape == (32, 32) + assert res["W"].dtype == res["A"].dtype == np.float64 + atol = 1e-5 if backend == "mlx" else 1e-10 + np.testing.assert_allclose(res["A"] @ res["W"], np.eye(32), atol=atol) + assert res["runtime_s"] > 0 + + +def test_torch_and_numpy_runners_apply_the_same_params(dispatch_results): + """Anti-drift: the two float64 backends are the same trajectory to ~1e-13, + so any setting one runner maps and the other drops shows up here.""" + torch_res, numpy_res = dispatch_results["torch"], dispatch_results["numpy"] + np.testing.assert_allclose( + numpy_res["ll_history"], torch_res["ll_history"], rtol=1e-10 + ) + np.testing.assert_allclose(numpy_res["W"], torch_res["W"], atol=1e-10) + + +def test_mlx_runner_applies_the_same_params_as_torch(dispatch_results): + """The same anti-drift check for the float32 MLX runner, at float32 + tolerance (measured 4e-7 on the likelihood, 8e-7 on ``W``).""" + _mlx_or_skip() + torch_res, mlx_res = dispatch_results["torch"], dispatch_results["mlx"] + np.testing.assert_allclose( + mlx_res["ll_history"], torch_res["ll_history"], atol=1e-5 + ) + np.testing.assert_allclose(mlx_res["W"], torch_res["W"], atol=1e-5) + + +def test_numpy_runner_writes_under_the_output_dir(dispatch_results, dispatch_out): + """The runner hands AMICA_NumPy the run's own output directory, so its + ``out.txt`` log and final model files sit next to the reports (the backend + writes nothing without an ``outdir``).""" + assert "numpy" in dispatch_results + assert (dispatch_out / "numpy_run" / "out.txt").exists() + assert (dispatch_out / "numpy_run" / "W").exists() + + +def test_main_writes_one_report_per_backend_and_a_summary(tmp_path): + """``main`` end to end without the reference: the PyTorch report keeps its + historical name, other backends get their own, and an explicit + ``--backend`` adds the one-row-per-backend summary.""" + from validate_implementations import main + + out = tmp_path / "out" + argv = ["--backend", "torch,numpy", "--skip-fortran", "--max-iter", "1"] + assert main([*argv, "--output-dir", str(out)]) == 0 + assert (out / "validation_report.txt").exists() + assert (out / "validation_report_numpy.txt").exists() + summary = (out / "parity_summary.md").read_text() + assert "| PyTorch | float64 | 1 |" in summary + assert "| NumPy | float64 | 1 |" in summary + + +def test_default_run_is_the_historical_torch_only_run(tmp_path): + """No ``--backend``: exactly the run the harness has always made, one + PyTorch report under its historical name and no summary table.""" + from validate_implementations import main + + out = tmp_path / "out" + argv = ["--skip-fortran", "--max-iter", "1", "--output-dir", str(out)] + assert main(argv) == 0 + assert sorted(p.name for p in out.iterdir()) == ["validation_report.txt"] + + +def test_parity_summary_renders_every_numeric_column(dispatch_results): + """``format_parity_summary`` with populated rows, from two real backend + runs and no binary: the float64 PyTorch run stands in for the reference + and the NumPy run is compared with it through ``compare_results``, so + every column holds a number (they are the same trajectory, so the values + are known too).""" + from validate_implementations import compare_results, format_parity_summary + + reference, numpy_res = dispatch_results["torch"], dispatch_results["numpy"] + comparison = compare_results(reference, numpy_res, "NumPy") + table = format_parity_summary(reference, [("numpy", numpy_res, comparison)]) + header, rule, ref_row, numpy_row = table.splitlines() + assert header.count("|") == rule.count("|") == ref_row.count("|") == 10 + + ref_cells = [c.strip() for c in ref_row.strip("|").split("|")] + assert ref_cells[:3] == ["Fortran (reference)", "float64", "2"] + assert float(ref_cells[3]) == pytest.approx(reference["final_ll"], abs=1e-6) + assert ref_cells[4:8] == ["", "", "", ""] + assert float(ref_cells[8]) >= 0 + + cells = [c.strip() for c in numpy_row.strip("|").split("|")] + assert cells[:3] == ["NumPy", "float64", "2"] + ll, ll_diff, mean_corr, min_corr, amari, runtime = map(float, cells[3:]) + assert ll == pytest.approx(numpy_res["final_ll"], abs=1e-6) + assert ll_diff == 0.0 # 1e-13 in fact, rendered to six decimals + assert mean_corr == min_corr == 1.0 + assert amari == 0.0 + assert runtime == pytest.approx(numpy_res["runtime_s"], abs=0.05) + + +@pytest.mark.parametrize("backend", ["torch,mlx", "all"]) +def test_mlx_backend_without_mlx_exits_with_the_install_hint( + backend, tmp_path, monkeypatch, capsys +): + """Any request that includes MLX (``torch,mlx`` or ``all``) on a host + without MLX stops before any work, naming the extra to install. Where MLX + is installed, the ImportError its absence raises is reproduced at the real + import site (``None`` in ``sys.modules``); the harness code path itself is + unmodified.""" + from validate_implementations import main + + try: + import pamica.mlx_impl # noqa: F401 + except ImportError: + pass + else: + monkeypatch.setitem(sys.modules, "pamica.mlx_impl", None) + out = tmp_path / "never" + assert main(["--backend", backend, "--output-dir", str(out)]) == 2 + err = capsys.readouterr().err + assert "--backend mlx" in err and "uv sync --extra mlx" in err + assert not out.exists(), "nothing may run before the MLX check" + + +# --- Fortran-backed per-backend parity (issue #315) -------------------------- +# +# Opt-in, like test_ng_pdf_families.py's converged-LL parity test: it runs the +# reference binary (resolved the way the harness resolves it) and every +# available backend at the harness's default settings, then holds each to the +# bars docs/guides/validation.md records for the harness rows. + + +@pytest.fixture(scope="module") +def fortran_parity_rows(tmp_path_factory): + from validate_implementations import ( + BACKENDS, + compare_results, + load_sample_data, + mlx_unavailable_reason, + run_backend, + run_fortran_amica, + ) + + data, params = load_sample_data() + params["max_iter"] = 100 + out = tmp_path_factory.mktemp("fortran_parity") + fortran = run_fortran_amica(data, params, out, seed=42) + assert fortran is not None and "W" in fortran, "reference run failed" + rows = {} + for name in BACKENDS: + if name == "mlx" and mlx_unavailable_reason() is not None: + continue + res = run_backend(name, data, dict(params), out, 42) + rows[name] = (res, compare_results(fortran, res)) + return rows + + +@pytest.mark.skipif( + os.environ.get("AMICA_RUN_FORTRAN") != "1", + reason="opt-in Fortran-binary integration test (set AMICA_RUN_FORTRAN=1)", +) +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_backend_meets_its_parity_bar_against_fortran(backend, fortran_parity_rows): + if backend == "mlx": + _mlx_or_skip() + res, cmp = fortran_parity_rows[backend] + assert cmp["mean_correlation"] > 0.95 + assert cmp["amari_distance"] < 0.05 + assert cmp["ll_difference"] < 0.005 + if backend != "torch" and "torch" in fortran_parity_rows: + # Every backend also lands on the float64 PyTorch likelihood: to the + # ~1e-5 a full float64 NumPy fit is documented to track it at + # (test_full_fit_parity_numpy_vs_ng), and to about five significant + # digits for float32 MLX. + torch_ll = fortran_parity_rows["torch"][0]["final_ll"] + tol = 1e-4 if backend == "mlx" else 1e-5 + assert abs(res["final_ll"] - torch_ll) < tol diff --git a/pamica/tests/test_llt_stash.py b/pamica/tests/test_llt_stash.py index 35fe0e1c..c0a5e686 100644 --- a/pamica/tests/test_llt_stash.py +++ b/pamica/tests/test_llt_stash.py @@ -104,7 +104,7 @@ def _llt_invariant(Lt: np.ndarray, n_good: int, nw: int) -> float: record at once, so they can differ by a summation ULP. The quantity being discriminated -- one M-step of movement -- is ~1e-4 here, eight orders of magnitude above that, so the tests below pair the tolerance with an - explicit check that a neighbouring iterate is far outside it. + explicit check that a neighboring iterate is far outside it. """ return float(Lt.sum()) / (n_good * nw) @@ -124,7 +124,7 @@ def test_fortran_reference_llt_is_the_last_estep(): of its own ``LL`` trajectory -- i.e. it is the E-step that produced that LL, taken before the final ``update_params``. On the machine this was written the residual is exactly 0.0; the assertion allows a summation ULP so it - cannot fail on a different BLAS, and the neighbouring-iterate check below + cannot fail on a different BLAS, and the neighboring-iterate check below shows the tolerance is nowhere near wide enough to match the wrong entry. This is the evidence for the write convention issue #157 adopts, measured diff --git a/pamica/tests/test_mlx_convergence_stops.py b/pamica/tests/test_mlx_convergence_stops.py index 497bd96c..03271942 100644 --- a/pamica/tests/test_mlx_convergence_stops.py +++ b/pamica/tests/test_mlx_convergence_stops.py @@ -2,7 +2,7 @@ Before this, ``AMICAMLXNG`` implemented neither Fortran stop: an MLX fit always spent the whole iteration budget while every other backend stopped on likelihood -stagnation. These tests pin the ported behaviour to the PyTorch backend, which is +stagnation. These tests pin the ported behavior to the PyTorch backend, which is the porting reference: 1. ``use_min_dll``/``min_dll``/``maxincs`` (amica15.f90:1078-1090) diff --git a/pamica/tests/test_numpy_cli.py b/pamica/tests/test_numpy_cli.py index 1fdffd42..f48134e5 100644 --- a/pamica/tests/test_numpy_cli.py +++ b/pamica/tests/test_numpy_cli.py @@ -100,3 +100,19 @@ def test_load_params_maps_canonical_keys_for_fortran_text(tmp_path): assert "maxdecs" not in params assert "min_nd" not in params assert "share_iter" not in params + + +def test_cli_without_outdir_still_writes_to_output(tmp_path, monkeypatch): + """The library default no longer writes files (``outdir=None``), but the + CLI's own ``--outdir`` default is ``output``, so a CLI run without the flag + still writes its results to ``./output`` as before.""" + from pamica.numpy_impl import cli + + dest = _json_with_tiny_iters(tmp_path) + run_dir = tmp_path / "run" + run_dir.mkdir() + monkeypatch.chdir(run_dir) + monkeypatch.setattr(sys, "argv", ["cli.py", str(dest), "--seed", "0"]) + cli.main() + for name in ("out.txt", "W", "S", "LL"): + assert (run_dir / "output" / name).exists(), name diff --git a/pamica/tests/test_numpy_outdir.py b/pamica/tests/test_numpy_outdir.py new file mode 100644 index 00000000..3b504343 --- /dev/null +++ b/pamica/tests/test_numpy_outdir.py @@ -0,0 +1,93 @@ +"""``AMICA_NumPy`` writes files only when given an ``outdir`` (epic #324 Phase 6). + +The legacy backend used to default ``outdir`` to ``./output`` and write its +``out.txt`` log at construction, its ``writestep`` checkpoints during the fit +and its results at the end, into whatever the caller's working directory was. +The PyTorch and MLX backends never write unless asked, so the NumPy default is +now ``outdir=None``: no files at all. An explicit ``outdir`` (keyword or params +file; the CLI passes its own ``--outdir``, default ``output``) writes exactly +what it did before. + +Real bundled sample EEG only; each fit is a few iterations with every write +path switched on (a checkpoint and a history snapshot on every iteration). +""" + +import json +from pathlib import Path +from typing import Any, Dict + +import numpy as np +import pytest + +from pamica import AMICA_NumPy +from pamica.numpy_impl.load import loadmodout +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +JSON_FILE = SAMPLE_DIR / "sample_params.json" +NW = 32 +N_FRAMES = 4096 +MAX_ITER = 3 + +# Every file-writing path on: out.txt, a results checkpoint and a history +# snapshot each iteration, then the final results write. +ALL_WRITES: Dict[str, Any] = dict( + seed=0, + max_iter=MAX_ITER, + block_size=N_FRAMES, + writestep=1, + do_history=True, + histstep=1, +) +RESULT_FILES = ("out.txt", "W", "S", "mean", "gm", "LL", "LLt", "A") + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +@pytest.fixture(scope="module") +def X() -> np.ndarray: + data = load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=30504) + return data.astype(np.float64)[:, :N_FRAMES] + + +def test_default_writes_nothing(X, tmp_path, monkeypatch): + """No ``outdir``: the fit completes and leaves the working directory + empty, with every write path switched on.""" + monkeypatch.chdir(tmp_path) + model = AMICA_NumPy(use_tqdm=False, **ALL_WRITES) + assert model.outdir is None and model.file_path is None + model.fit(X) + assert model.converged + assert len(model.ll) == MAX_ITER + assert list(tmp_path.iterdir()) == [] + + +def test_explicit_outdir_writes_as_before(X, tmp_path, monkeypatch): + """An explicit ``outdir`` gets the log, the checkpoints, the history and + the final results, readable by ``loadmodout``; nothing lands elsewhere.""" + monkeypatch.chdir(tmp_path) + outdir = tmp_path / "amicaout" + model = AMICA_NumPy(use_tqdm=False, outdir=str(outdir), **ALL_WRITES) + assert (outdir / "out.txt").exists() # written at construction, as before + model.fit(X) + assert sorted(p.name for p in tmp_path.iterdir()) == ["amicaout"] + for name in RESULT_FILES: + assert (outdir / name).exists(), name + assert len(list((outdir / "history").iterdir())) == MAX_ITER + assert " iter " in (outdir / "out.txt").read_text() # the per-iteration log + np.testing.assert_array_equal(loadmodout(outdir).LL, model.ll) + + +def test_params_file_outdir_is_explicit(X, tmp_path, monkeypatch): + """A params file that names an ``outdir`` (the bundled + ``sample_params.json`` says ``./amicaout/``) writes there, relative to + the working directory, as it always has.""" + monkeypatch.chdir(tmp_path) + assert json.loads(JSON_FILE.read_text())["outdir"] == "./amicaout/" + model = AMICA_NumPy(params_file=str(JSON_FILE), use_tqdm=False, **ALL_WRITES) + assert model.outdir == Path("./amicaout/") + model.fit(X) + assert sorted(p.name for p in tmp_path.iterdir()) == ["amicaout"] + for name in RESULT_FILES: + assert (tmp_path / "amicaout" / name).exists(), name diff --git a/pamica/tests/test_numpy_share_comps.py b/pamica/tests/test_numpy_share_comps.py index c76a7b46..fd8d4a50 100644 --- a/pamica/tests/test_numpy_share_comps.py +++ b/pamica/tests/test_numpy_share_comps.py @@ -551,8 +551,8 @@ def test_numpy_merge_decision_matches_torch_backend(): """Acceptance test: from one matched fitted state, the two backends reach the identical merge decision now that both compare sensor-space (de-sphered) mixing columns -- ``pinv(sphere) @ A`` -- mirroring - ``AMICATorchNG._identify_shared_comps`` exactly (torch_impl/core.py:1696- - 1741) instead of numpy's former sphered-space comparison. + ``AMICATorchNG._identify_shared_comps`` exactly, instead of numpy's former + sphered-space comparison. """ model = _shared_fit(max_iter=3, share_comps=False) # Perturb one cross-model column into near- (not exact-) collinearity, so diff --git a/pamica/tests/test_numpy_viz.py b/pamica/tests/test_numpy_viz.py new file mode 100644 index 00000000..9bd8e63c --- /dev/null +++ b/pamica/tests/test_numpy_viz.py @@ -0,0 +1,147 @@ +"""The legacy NumPy plotting helpers plot the model's own maps and sources. + +``pamica.numpy_impl.viz`` reads a results directory through ``load_results``, +which returns the stored arrays: the unmixing ``W`` and the mixing ``A`` +transposed (issue #24 convention). The helpers used to plot those as if they +were the true matrices, and to form activations from the raw data with no +mean removal, no sphere and no transpose, so the "mixing vectors" were rows of +the mixing matrix and the activations were not the sources. They now plot what +``AMICA_NumPy.get_sensor_mixing_matrix`` and ``AMICA_NumPy.transform`` return +for the fitted model, which these tests check against the live accessors, +both through the shared data-preparation helper and read back from the +plotted figures. + +Real bundled sample EEG only: a short NumPy fit written to ``tmp_path`` and +read back, full rank and with ``pcakeep`` (a non-square, zero-padded sphere). +""" + +from pathlib import Path + +import matplotlib + +matplotlib.use("Agg") + +import matplotlib.pyplot as plt # noqa: E402 +from matplotlib.patches import Rectangle # noqa: E402 +import numpy as np # noqa: E402 +import pytest # noqa: E402 + +from pamica import AMICA_NumPy # noqa: E402 +from pamica.numpy_impl import viz # noqa: E402 +from pamica.numpy_impl.data import load_results # noqa: E402 +from pamica.torch_impl.utils import load_eeglab_data # noqa: E402 + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +N_FRAMES = 4096 +PCAKEEP = 20 +CONFIGS = {"full": None, "reduced": PCAKEEP} +# Written and read back in float64, so everything agrees to round-off. +TOL = 1e-10 + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +@pytest.fixture(scope="module") +def X() -> np.ndarray: + data = load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=30504) + return data.astype(np.float64)[:, :N_FRAMES] + + +@pytest.fixture(scope="module", params=list(CONFIGS)) +def fitted(request, X, tmp_path_factory): + """A short NumPy fit and the directory it wrote.""" + outdir = tmp_path_factory.mktemp(f"viz_{request.param}") + model = AMICA_NumPy( + use_tqdm=False, + seed=0, + max_iter=5, + block_size=N_FRAMES, + pcakeep=CONFIGS[request.param], + outdir=str(outdir), + ) + model.fit(X) + return model, outdir + + +@pytest.fixture(autouse=True) +def _close_figures(): + yield + plt.close("all") + + +def _live(model: AMICA_NumPy, X: np.ndarray): + """The fitted model's sensor maps and model-0 sources, from its accessors.""" + assert model.comp_list is not None + maps = model.get_sensor_mixing_matrix() + sources = model.transform(X)[model.comp_list[:, 0], :, 0] + return maps, sources + + +def _rel(a, b) -> float: + return float(np.linalg.norm(a - b) / np.linalg.norm(b)) + + +def _bar_heights(ax) -> list: + """The bar heights of the histogram drawn on ``ax``.""" + return [p.get_height() for p in ax.patches if isinstance(p, Rectangle)] + + +def test_maps_and_sources_match_the_accessors(fitted, X): + model, outdir = fitted + maps, sources = viz._component_maps_and_sources(load_results(outdir), X) + live_maps, live_sources = _live(model, X) + n = model.data_dim + assert maps.shape == (NW, n) and sources is not None + assert sources.shape == (n, N_FRAMES) + assert _rel(maps, live_maps) <= TOL + assert _rel(sources, live_sources) <= TOL + + +def test_load_results_reads_a_reduced_sphere(fitted): + """A rank-reduced fit writes its sphere zero-padded to (nx, nx); the + reader returns the fitted (nw, nx) sphere.""" + model, outdir = fitted + np.testing.assert_array_equal(load_results(outdir)["sphere"], model.sphere) + + +def test_plot_components_draws_the_maps_and_the_sources(fitted, X): + model, outdir = fitted + live_maps, live_sources = _live(model, X) + viz.plot_components(outdir, data=X, max_comps=3) + axes = np.array(plt.gcf().axes).reshape(3, 2) + for i in range(3): + drawn_map = axes[i, 0].lines[0].get_ydata() + assert _rel(drawn_map, live_maps[:, i]) <= TOL + heights = _bar_heights(axes[i, 1]) + expected, _ = np.histogram(live_sources[i], bins=50, density=True) + np.testing.assert_allclose(heights, expected, rtol=1e-8) + + +def test_plot_pdf_fits_histograms_the_sources(fitted, X): + model, outdir = fitted + _, live_sources = _live(model, X) + viz.plot_pdf_fits(outdir, X, max_comps=2) + for i, ax in enumerate(plt.gcf().axes): + heights = _bar_heights(ax) + expected, _ = np.histogram(live_sources[i], bins=50, density=True) + np.testing.assert_allclose(heights, expected, rtol=1e-8) + + +def test_plot_model_comparison_draws_the_reconstruction(fitted, X): + """The red line is the model's reconstruction from its own maps and + sources, plus the mean it removed; for a full-rank fit that is the data.""" + model, outdir = fitted + live_maps, live_sources = _live(model, X) + assert model.mean is not None + reconstruction = live_maps @ live_sources + model.mean + viz.plot_model_comparison(outdir, X, n_examples=2) + axes = np.array(plt.gcf().axes).reshape(2, 2) + rng = np.random.RandomState(42) # the helper's own choice of time points + times = rng.choice(X.shape[1], 2, replace=False) + for i, t in enumerate(times): + drawn = axes[i, 1].lines[0].get_ydata() + assert _rel(drawn, reconstruction[:, t]) <= TOL + if model.data_dim == NW: + assert _rel(drawn, X[:, t]) <= TOL diff --git a/pamica/tests/test_sample_data.py b/pamica/tests/test_sample_data.py index 1de4fac3..8930b794 100644 --- a/pamica/tests/test_sample_data.py +++ b/pamica/tests/test_sample_data.py @@ -159,8 +159,11 @@ def test_amicaoutput_sources_contract_and_c_subtraction(): strict=True, raises=AssertionError, ) -def test_sample_data_scikit(tmp_path): +def test_sample_data_scikit(tmp_path, monkeypatch): """Test pamica using scikit-learn style API.""" + # fit() with no data loads sample_params.json's `files`, which is relative + # to the repository root (the suite otherwise runs outside it, conftest.py). + monkeypatch.chdir(Path(__file__).resolve().parents[2]) # Load original results for comparison orig_results = loadmodout(amicaout_dir) @@ -208,7 +211,7 @@ def test_sample_data_scikit(tmp_path): "bar and passes.", strict=True, ) -def test_sample_data_cli(): +def test_sample_data_cli(tmp_path): """Full CLI-vs-Fortran integration test (issue #30 format + #39/#41 stability). Runs the real cli entrypoint for the full 2000-iter sample config and @@ -222,51 +225,45 @@ def test_sample_data_cli(): import sys # cli.py uses relative imports, so it must be run as a module - # (see its module docstring), not as a direct script path. - test_outdir = Path("test_output") - - try: - subprocess.run( - [ - sys.executable, - "-m", - "pamica.numpy_impl.cli", - sample_params_file, - "--outdir", - str(test_outdir), - "--seed", - "0", - ], - check=True, - cwd=Path(__file__).parent.parent.parent, - ) - - # Load and compare results - orig_results = loadmodout(amicaout_dir) - test_results = loadmodout(test_outdir) - - correlations = np.zeros((32, 32)) - for i in range(32): - for j in range(32): - correlations[i, j] = abs( - np.corrcoef(test_results.W[i, :, 0], orig_results.W[j, :, 0])[0, 1] - ) - - # Verify results - max_correlations = np.max(correlations, axis=1) - assert np.all(max_correlations > 0.8), ( - "Some components don't match original results" - ) - - best_matches = np.argmax(correlations, axis=1) - assert len(np.unique(best_matches)) == 32, "Some components are duplicated" - - finally: - # Cleanup - if test_outdir.exists(): - import shutil - - shutil.rmtree(test_outdir) + # (see its module docstring), not as a direct script path. It runs from the + # repository root because sample_params.json's `files` entry is relative to + # it, and writes under tmp_path rather than into the repository. + test_outdir = tmp_path / "test_output" + + subprocess.run( + [ + sys.executable, + "-m", + "pamica.numpy_impl.cli", + sample_params_file, + "--outdir", + str(test_outdir), + "--seed", + "0", + ], + check=True, + cwd=Path(__file__).parent.parent.parent, + ) + + # Load and compare results + orig_results = loadmodout(amicaout_dir) + test_results = loadmodout(test_outdir) + + correlations = np.zeros((32, 32)) + for i in range(32): + for j in range(32): + correlations[i, j] = abs( + np.corrcoef(test_results.W[i, :, 0], orig_results.W[j, :, 0])[0, 1] + ) + + # Verify results + max_correlations = np.max(correlations, axis=1) + assert np.all(max_correlations > 0.8), ( + "Some components don't match original results" + ) + + best_matches = np.argmax(correlations, axis=1) + assert len(np.unique(best_matches)) == 32, "Some components are duplicated" @pytest.mark.xfail( diff --git a/pamica/tests/test_sensor_mixing_cross_backend.py b/pamica/tests/test_sensor_mixing_cross_backend.py new file mode 100644 index 00000000..42acf040 --- /dev/null +++ b/pamica/tests/test_sensor_mixing_cross_backend.py @@ -0,0 +1,142 @@ +"""``get_sensor_mixing_matrix`` agrees across backends (epic #324 Phase 6). + +The sensor-space mixing matrix is what scalp maps, dipole fits and the EEGLAB +``icawinv`` are built from, so it has to invert the spatial filter the sources +come from on every backend: ``unmixing @ sphere @ M == I`` (the sphere may be +non-square after rank reduction, hence ``pinv``). The legacy NumPy backend +returned ``pinv(sphere) @ stored_A`` without the transpose the other two apply +(its stored ``W`` is the unmixing transposed, issue #24's convention), so its +maps were the rows of the true mixing matrix instead of its columns: in this +module's fits, a 3.0% (full rank) and 2.6% (``pcakeep=20``) relative +difference from the PyTorch backend's maps, against 6.1e-13 and 9.6e-13 with +the transpose, which also brings ``get_weights() @ sphere @ M`` to the +identity within 2.4e-15. + +PyTorch and NumPy always run, so a divergence between them cannot land; MLX +uses ``pytest.importorskip`` plus an Apple-GPU guard (``.rules/backend_parity.md``). +Real bundled sample EEG only, two iterations per fit: this checks the +accessor on a fitted state, not convergence. Two, not more, because the +float64 backends start within round-off of each other (spheres 1e-13 apart) +and the EM iterations amplify that gap about sixfold each; at five +iterations the reduced fit's gap was 9.5e-11 on an Apple M-series laptop but +1.3e-9 on the GitHub macOS arm64 runner, whose BLAS rounds differently. The +orientation bug this module guards is a percent-level difference at any +iteration count, so fewer iterations keep a tight round-off gate portable. +""" + +from pathlib import Path + +import numpy as np +import pytest + +from pamica import AMICA_NumPy +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +SEED = 42 +N_FRAMES = 8192 +MAX_ITER = 2 +BLOCK_SIZE = 4096 +PCAKEEP = 20 +CONFIGS = {"full": None, "reduced": PCAKEEP} + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +@pytest.fixture(scope="module") +def X() -> np.ndarray: + data = load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD) + return data.astype(np.float64)[:, :N_FRAMES] + + +def _mlx_class(): + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + if mlx_core.mx.default_device().type != mlx_core.mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + return mlx_core.AMICAMLXNG + + +def _fit(backend: str, X: np.ndarray, pcakeep, outdir: Path): + """A short fit of ``backend`` on ``X``; the same seed and settings on all.""" + if backend == "numpy": + model = AMICA_NumPy( + use_tqdm=False, + seed=SEED, + max_iter=MAX_ITER, + block_size=BLOCK_SIZE, + pcakeep=pcakeep, + outdir=str(outdir), + ) + return model.fit(X) + cls = AMICATorchNG if backend == "torch" else _mlx_class() + kwargs = {"device": "cpu"} if backend == "torch" else {} + model = cls( + n_channels=NW, seed=SEED, block_size=BLOCK_SIZE, pcakeep=pcakeep, **kwargs + ) + data = X if backend == "torch" else X.astype(np.float32) + return model.fit(data, max_iter=MAX_ITER, verbose=False) + + +def _filter(backend: str, model) -> np.ndarray: + """The full spatial filter ``unmixing @ sphere`` the sources come from.""" + if backend == "numpy": + return model.get_weights() @ model.sphere + return model.get_unmixing_matrix().astype(np.float64) @ model.get_sphere() + + +@pytest.fixture(scope="module") +def fits(X, tmp_path_factory): + cache: dict = {} + + def get(backend: str, config: str): + if (backend, config) not in cache: + outdir = tmp_path_factory.mktemp(f"{backend}_{config}") + cache[backend, config] = _fit(backend, X, CONFIGS[config], outdir) + return cache[backend, config] + + return get + + +@pytest.mark.parametrize("config", list(CONFIGS)) +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_sensor_mixing_inverts_the_spatial_filter(fits, backend, config): + """``unmixing @ sphere @ M == I``: each column of ``M`` is the sensor map + of the source with the same index.""" + if backend == "mlx": + _mlx_class() + model = fits(backend, config) + n = NW if CONFIGS[config] is None else PCAKEEP + M = model.get_sensor_mixing_matrix() + assert M.shape == (NW, n) + atol = 1e-5 if backend == "mlx" else 1e-10 + np.testing.assert_allclose(_filter(backend, model) @ M, np.eye(n), atol=atol) + + +@pytest.mark.parametrize("config", list(CONFIGS)) +def test_numpy_sensor_mixing_matches_torch(fits, config): + """The float64 backends follow the same trajectory, so their sensor maps + agree to round-off (measured 6.1e-13 full rank, 9.6e-13 reduced); before + the fix they differed by about 3%.""" + m_torch = fits("torch", config).get_sensor_mixing_matrix() + m_numpy = fits("numpy", config).get_sensor_mixing_matrix() + err = np.linalg.norm(m_numpy - m_torch) / np.linalg.norm(m_torch) + assert err < 1e-9, f"NumPy vs PyTorch sensor mixing differs by {err:.3e}" + + +@pytest.mark.parametrize("config", list(CONFIGS)) +def test_mlx_sensor_mixing_matches_torch(fits, config): + """MLX computes in float32, so after two iterations its sensor maps agree + with the float64 PyTorch ones to float32-trajectory tolerance (measured + 5.4e-6 full rank, 2.0e-5 reduced), under the bar + ``test_mlx_transform_cross_backend.py`` uses for the same accessor.""" + _mlx_class() + m_torch = fits("torch", config).get_sensor_mixing_matrix() + m_mlx = fits("mlx", config).get_sensor_mixing_matrix() + err = np.linalg.norm(m_mlx - m_torch) / np.linalg.norm(m_torch) + assert err < 1e-3, f"MLX vs PyTorch sensor mixing differs by {err:.3e}" diff --git a/pamica/tests/test_viz.py b/pamica/tests/test_viz.py index c2d9f76a..a4a9931b 100644 --- a/pamica/tests/test_viz.py +++ b/pamica/tests/test_viz.py @@ -329,7 +329,7 @@ def test_plot_model_probability_smoothing_edge_corrected_not_dragged_to_zero( """A naive convolve(..., mode="same") zero-pads beyond the data; since Lht sits around -100 (nowhere near 0), that would drag the boundary samples violently toward zero. The edge-corrected smoothing must keep the first - and last smoothed samples close to their neighbours instead (issue #136 + and last smoothed samples close to their neighbors instead (issue #136 MATLAB-oracle finding).""" srate = eeglab_metadata["srate"] fig = plot_model_probability(two_model_output, srate=srate, smooth_sec=1.0) diff --git a/pamica/tests/torch_tests/test_amica_ng_wrapper.py b/pamica/tests/torch_tests/test_amica_ng_wrapper.py index afbf69ef..2f78fd1e 100644 --- a/pamica/tests/torch_tests/test_amica_ng_wrapper.py +++ b/pamica/tests/torch_tests/test_amica_ng_wrapper.py @@ -851,7 +851,7 @@ def test_failing_mir_waypoint_does_not_kill_the_fit(real_data, monkeypatch, capl mid-fit is a transient the natural gradient can pass through (the training path only warns about it). Before this guard, that ValueError propagated straight out of `fit()` and threw away the whole fit -- turning on a - waypoint could lose hours of training over a condition the optimiser was + waypoint could lose hours of training over a condition the optimizer was about to recover from. Forcing the raise via monkeypatch is deliberate and is not mocked data: the @@ -909,7 +909,7 @@ def flaky_mir(self, X, **kwargs): def test_mir_step_zero_matches_omitted_argument(real_data): - """mir_step=0 (explicit) must leave fit() behaviour byte-for-byte identical + """mir_step=0 (explicit) must leave fit() behavior byte-for-byte identical to not passing mir_step at all.""" X = real_data[:, :4096] default_model = AMICA(n_models=1, n_mix=3, device="cpu", verbose=False) diff --git a/pamica/tests/torch_tests/test_ng_backend.py b/pamica/tests/torch_tests/test_ng_backend.py index 2894c6c5..e1a043ca 100644 --- a/pamica/tests/torch_tests/test_ng_backend.py +++ b/pamica/tests/torch_tests/test_ng_backend.py @@ -1153,7 +1153,7 @@ def test_state_dict_snapshots_not_aliases(): @pytest.mark.slow @pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") -def test_end_to_end_correlation_vs_fortran(): +def test_end_to_end_correlation_vs_fortran(tmp_path): """Epic definition-of-done: Hungarian-matched component correlation vs the Fortran binary > 0.95 on the sample data. @@ -1177,8 +1177,7 @@ def test_end_to_end_correlation_vs_fortran(): data, params = load_sample_data() params = dict(params) params["max_iter"] = 100 - out = root / "pamica" / "tests" / "torch_tests" / "_ng_e2e_tmp" - out.mkdir(parents=True, exist_ok=True) + out = tmp_path fortran = run_fortran_amica(data, params, out, SEED) assert fortran is not None, "Fortran binary run failed" @@ -1208,7 +1207,7 @@ def test_end_to_end_correlation_vs_fortran(): @pytest.mark.slow @pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") -def test_end_to_end_correlation_vs_fortran_from_sample_params_json(): +def test_end_to_end_correlation_vs_fortran_from_sample_params_json(tmp_path): """Same correctness bar as ``test_end_to_end_correlation_vs_fortran``, but built the way a user actually reproduces the paper's Table 1 numbers: via ``run_pytorch_amica``, which maps ``sample_data/sample_params.json`` onto @@ -1260,8 +1259,7 @@ def test_end_to_end_correlation_vs_fortran_from_sample_params_json(): "whitening instead of symmetric ZCA sphering)" ) - out = root / "pamica" / "tests" / "torch_tests" / "_ng_e2e_json_tmp" - out.mkdir(parents=True, exist_ok=True) + out = tmp_path fortran = run_fortran_amica(data, params, out, SEED) assert fortran is not None, "Fortran binary run failed" @@ -1495,7 +1493,7 @@ def _accumulate_blocks(self, X, stash_llt=False): @pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") def test_keep_best_does_not_rescue_a_diverged_fit_that_peaked_earlier(): """The end-of-fit restore guard's ``stop_reason not in - _DEGENERATE_STOP_REASONS`` exclusion (core.py:2725) had zero coverage: + _DEGENERATE_STOP_REASONS`` exclusion (``AMICATorchNG._fit_once``) had zero coverage: every existing keep_best test either never diverges or diverges on iteration 0 (no ``best_snapshot`` yet to wrongly rescue). This forces a fit to run ``_DEGENERATE_NAN_AFTER`` real iterations -- so a genuine, diff --git a/pamica/tests/torch_tests/test_ng_convergence.py b/pamica/tests/torch_tests/test_ng_convergence.py index 956f1481..1b130d35 100644 --- a/pamica/tests/torch_tests/test_ng_convergence.py +++ b/pamica/tests/torch_tests/test_ng_convergence.py @@ -270,7 +270,7 @@ def test_lrate_floor_still_reachable_without_grad_norm(real_data): assert ng.lrate <= ng.minlrate # The exact crossing iteration (23 on macOS-arm64) is a snapshot, not an # invariant: the same claim elsewhere in this file varied 326 -> 1076 across - # BLAS implementations. Assert the behaviour, which is that the stop fired + # BLAS implementations. Assert the behavior, which is that the stop fired # before the budget was exhausted, not the iteration it happened on. assert len(ng.ll_history) < 30 diff --git a/pamica/tests/torch_tests/test_ng_pdf_families.py b/pamica/tests/torch_tests/test_ng_pdf_families.py index cc7013ed..830a1204 100644 --- a/pamica/tests/torch_tests/test_ng_pdf_families.py +++ b/pamica/tests/torch_tests/test_ng_pdf_families.py @@ -360,7 +360,7 @@ def test_multimodel_fixed_family(): reason="opt-in Fortran-binary integration test (set AMICA_RUN_FORTRAN=1)", ) @pytest.mark.parametrize("pdftype,n_mix", [(0, 3), (2, 3), (3, 3), (4, 1), (1, 1)]) -def test_family_converged_ll_matches_fortran(pdftype: int, n_mix: int): +def test_family_converged_ll_matches_fortran(tmp_path, pdftype: int, n_mix: int): """Converged LL parity vs amica15mac with the optimizer matched (Newton on). Slow (runs the binary + a full NG fit per family); gated behind @@ -374,9 +374,7 @@ def test_family_converged_ll_matches_fortran(pdftype: int, n_mix: int): data, params = load_sample_data() fp = dict(params) fp.update(pdftype=pdftype, num_mix=n_mix, max_iter=150, do_newton=True) - run_dir = SAMPLE_DIR.parent.parent / "scratch_amica_parity" / f"pt{pdftype}" - run_dir.mkdir(parents=True, exist_ok=True) - fres = run_fortran_amica(data, fp, run_dir, seed=0) + fres = run_fortran_amica(data, fp, tmp_path, seed=0) assert fres is not None and "final_ll" in fres kw: dict[str, Any] = dict(num_kurt=0) if pdftype == 1 else {} diff --git a/pamica/torch_impl/core.py b/pamica/torch_impl/core.py index 639a6bbb..a22ce4ba 100644 --- a/pamica/torch_impl/core.py +++ b/pamica/torch_impl/core.py @@ -1548,8 +1548,8 @@ def _finalize_newton_stats(self, acc: Dict[str, torch.Tensor]): def _newton_direction(self, dA_h, sigma2_h, lambda_h, kappa_h): """Per-model Newton direction ``H`` from the natural gradient ``dA_h``. - Vectorized port of the per-source-pair 2x2 solve (amica17.f90:1817-1832, - pamica.py:802-813): + Vectorized port of the per-source-pair 2x2 solve (amica17.f90:1817-1832; + the NumPy backend's loop in ``AMICA_NumPy._update_parameters``): H[i,i] = dA_h[i,i] / lambda[i] sk1 = sigma2[i]*kappa[k]; sk2 = sigma2[k]*kappa[i] (i != k) @@ -2330,7 +2330,7 @@ def _fit_once( If > 0, compute MIR (issue #137) from the current ``W``/``sphere`` every ``mir_step`` iterations and append it to ``mir_history_`` as ``(iteration, mir_nats, variance)``. ``0`` (default) disables the - waypoints and leaves fit behaviour byte-for-byte unchanged. + waypoints and leaves fit behavior byte-for-byte unchanged. ``mir_history_`` is a true trajectory like ``ll_history``: a ``keep_best`` (issue #51) restore does not rewrite it, so the fit-end MIR is ``self.mir(X)`` on the returned parameters, not @@ -2378,7 +2378,7 @@ def _fit_once( * Without a keep-best restore, ``final_ll_`` is ``ll_history[-1]``, the LL of the parameters as they stood *before* the last M-step -- so the exported ``LLt`` is one M-step older than the exported ``W``/``A``. - That is the reference's own behaviour (Fortran fills ``modloglik`` + That is the reference's own behavior (Fortran fills ``modloglik`` during iteration i's E-step, ``update_params`` then moves the parameters, and ``write_output`` writes both -- amica15.f90:996, 1122, 1124-1127), adopted deliberately so pamica's on-disk ``LLt`` is diff --git a/pamica/viz.py b/pamica/viz.py index fcfa5895..1320ec96 100644 --- a/pamica/viz.py +++ b/pamica/viz.py @@ -10,7 +10,7 @@ replacement for it. Both plots mirror ``postAmicaUtility``'s ``modprobplot``/``pop_modPMI`` MATLAB -behaviour, observed by running the real GPL-licensed functions and reading +behavior, observed by running the real GPL-licensed functions and reading their rendered output and ``help`` text, never their source, per the project's clean-room posture for GPL code. Every quantity they draw is pinned to a MATLAB oracle; see ``.context/issue-136/matlab_viz_verification.md``. @@ -68,7 +68,7 @@ def plot_pmi_heatmap( mask_diagonal : bool, default True `pairwise_mi`'s diagonal is each component's self-entropy (~2.83 on real data), not a mutual information -- an order of magnitude above - the ~0.06 off-diagonal values. Left unmasked it blows out the colour + the ~0.06 off-diagonal values. Left unmasked it blows out the color scale and hides all off-diagonal structure, so this defaults to True. Deliberate divergence: MATLAB instead zeroes its diagonal. Masking is the closer analogue here because our diagonal is not a small value, it @@ -160,7 +160,7 @@ def plot_model_probability( Top panel: `softmax(Lht)` over models, one line per model ("Probability of Model Being Active"). Bottom panel: the per-sample log-likelihood of the single most probable model at each timepoint (``Lht.max(axis=0)``), not - the total ``Lt``, matching the observed MATLAB behaviour. + the total ``Lt``, matching the observed MATLAB behavior. Provide exactly one source of ``Lht``: a written ``out`` (an :class:`AmicaOutput`), or a live ``lht`` array (for example from @@ -278,7 +278,7 @@ def plot_model_probability( # SMOOTHED LOG-LIKELIHOOD correlates at 0.9939 (1 s window) / 0.9836 # (5 s), while the PROBABILITIES it becomes after the softmax below # correlate at 0.9886 / 0.9594. Both are recorded in - # `.context/issue-136/matlab_viz_verification.md`; an unlabelled 0.994 + # `.context/issue-136/matlab_viz_verification.md`; an unlabeled 0.994 # here previously read as a stale copy of the 0.9886 figure (#136). # # Known, accepted divergence: MATLAB additionally pins its first/last diff --git a/paper.md b/paper.md index 8991f16c..5c161804 100644 --- a/paper.md +++ b/paper.md @@ -159,7 +159,7 @@ Every AI-assisted change was reviewed by a human before merge and run through th which scores output against the reference binary on real recordings rather than generated fixtures. The reported numbers came from running the software and were checked against their run records, as was every bibliographic entry. -# Acknowledgements +# Acknowledgments We thank Jason Palmer and his advisor Ken Kreutz-Delgado, co-developers of AMICA, for the reference implementation, and the EEGLAB community for the tools and sample data used to validate this work. diff --git a/paper.pdf b/paper.pdf index d44f670df70920d0dec362aafd2d3dcb9954f151..610fed8225008564690e8d64639601fc760df247 100644 GIT binary patch delta 6118 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 delta 6122 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 diff --git a/pytorch_debug_test/out.txt b/pytorch_debug_test/out.txt deleted file mode 100644 index e66d1156..00000000 --- a/pytorch_debug_test/out.txt +++ /dev/null @@ -1,60 +0,0 @@ -====================================================================== -PyTorch AMICA - 2025-08-20 21:52:30 -====================================================================== -Data dimensions: 32 channels, 30504 samples -Number of models: 1 -Number of sources: 32 -Mixture components: 3 -Max iterations: 200 -Learning rate: 0.050000 -Newton optimization: False -Device: mps ----------------------------------------------------------------------- - Starting optimization... - iter 0 lrate = 0.0500000000 LL = -46.1106338501 nd = 1.8388038654, ( 0.14 s, 0.0 h) - iter 5 lrate = 0.0500000000 LL = -46.6084785461 nd = 1.3568564169, D = -0.13992 -0.13992 ( 0.47 s, 0.0 h) - iter 10 lrate = 0.0500000000 LL = -45.8908271790 nd = 0.9524533988, D = 0.10653 0.10653 ( 0.79 s, 0.0 h) - iter 15 lrate = 0.0500000000 LL = -45.4929542542 nd = 0.8804289343, D = 0.13743 0.13743 ( 1.10 s, 0.0 h) - iter 20 lrate = 0.0500000000 LL = -45.2655258179 nd = 0.7485299054, D = 1.80435e-03 1.80435e-03 ( 1.43 s, 0.0 h) - iter 25 lrate = 0.0500000000 LL = -45.1307907104 nd = 0.5092590386, D = 0.03450 0.03450 ( 1.76 s, 0.0 h) - iter 30 lrate = 0.0500000000 LL = -45.0472755432 nd = 0.4059172022, D = 0.01441 0.01441 ( 2.09 s, 0.0 h) - iter 35 lrate = 0.0500000000 LL = -44.9745368958 nd = 0.3057209618, D = 0.01442 0.01442 ( 2.41 s, 0.0 h) - iter 40 lrate = 0.0500000000 LL = -44.9138679504 nd = 0.2424595217, D = 0.01094 0.01094 ( 2.74 s, 0.0 h) - iter 45 lrate = 0.0500000000 LL = -44.8604202271 nd = 0.1908976145, D = 0.01020 0.01020 ( 3.07 s, 0.0 h) - iter 50 lrate = 0.0500000000 LL = -44.8110847473 nd = 0.1576567925, D = 0.01009 0.01009 ( 3.40 s, 0.0 h) - iter 55 lrate = 0.0500000000 LL = -44.7636909485 nd = 0.1259884652, D = 9.34601e-03 9.34601e-03 ( 3.72 s, 0.0 h) - iter 60 lrate = 0.0500000000 LL = -44.7193870544 nd = 0.1054436684, D = 8.37708e-03 8.37708e-03 ( 4.05 s, 0.0 h) - iter 65 lrate = 0.0500000000 LL = -44.6821136475 nd = 0.0902402660, D = 6.73676e-03 6.73676e-03 ( 4.38 s, 0.0 h) - iter 70 lrate = 0.0500000000 LL = -44.6525382996 nd = 0.0685699748, D = 5.27954e-03 5.27954e-03 ( 4.70 s, 0.0 h) - iter 75 lrate = 0.0500000000 LL = -44.6293182373 nd = 0.0586191846, D = 4.30298e-03 4.30298e-03 ( 5.03 s, 0.0 h) - iter 80 lrate = 0.0500000000 LL = -44.6095657349 nd = 0.0511772142, D = 3.73077e-03 3.73077e-03 ( 5.36 s, 0.0 h) - iter 85 lrate = 0.0500000000 LL = -44.5924568176 nd = 0.0433048637, D = 3.25012e-03 3.25012e-03 ( 5.69 s, 0.0 h) - iter 90 lrate = 0.0500000000 LL = -44.5777168274 nd = 0.0380624992, D = 2.71988e-03 2.71988e-03 ( 6.02 s, 0.0 h) - iter 95 lrate = 0.0500000000 LL = -44.5657577515 nd = 0.0324847501, D = 2.20871e-03 2.20871e-03 ( 6.35 s, 0.0 h) - iter 100 lrate = 0.0500000000 LL = -44.5557022095 nd = 0.0300861700, D = 1.91498e-03 1.91498e-03 ( 6.67 s, 0.0 h) - iter 105 lrate = 0.0500000000 LL = -44.5467262268 nd = 0.0281934183, D = 1.73569e-03 1.73569e-03 ( 7.00 s, 0.0 h) - iter 110 lrate = 0.0500000000 LL = -44.5382995605 nd = 0.0275648207, D = 1.67847e-03 1.67847e-03 ( 7.32 s, 0.0 h) - iter 115 lrate = 0.0500000000 LL = -44.5299949646 nd = 0.0276074207, D = 1.64795e-03 1.64795e-03 ( 7.65 s, 0.0 h) - iter 120 lrate = 0.0500000000 LL = -44.5218276978 nd = 0.0266783812, D = 1.61362e-03 1.61362e-03 ( 7.98 s, 0.0 h) - iter 125 lrate = 0.0500000000 LL = -44.5140037537 nd = 0.0257645809, D = 1.51825e-03 1.51825e-03 ( 8.30 s, 0.0 h) - iter 130 lrate = 0.0500000000 LL = -44.5067443848 nd = 0.0245546721, D = 1.39618e-03 1.39618e-03 ( 8.63 s, 0.0 h) - iter 135 lrate = 0.0500000000 LL = -44.5001487732 nd = 0.0229063824, D = 1.28174e-03 1.28174e-03 ( 8.96 s, 0.0 h) - iter 140 lrate = 0.0500000000 LL = -44.4940223694 nd = 0.0221823277, D = 1.18637e-03 1.18637e-03 ( 9.28 s, 0.0 h) - iter 145 lrate = 0.0500000000 LL = -44.4883460999 nd = 0.0214190105, D = 1.10245e-03 1.10245e-03 ( 9.61 s, 0.0 h) - iter 150 lrate = 0.0500000000 LL = -44.4830741882 nd = 0.0201898614, D = 1.02234e-03 1.02234e-03 ( 9.94 s, 0.0 h) - iter 155 lrate = 0.0500000000 LL = -44.4782028198 nd = 0.0196264000, D = 9.42230e-04 9.42230e-04 ( 10.26 s, 0.0 h) - iter 160 lrate = 0.0500000000 LL = -44.4738464355 nd = 0.0175391742, D = 8.23975e-04 8.23975e-04 ( 10.59 s, 0.0 h) - iter 165 lrate = 0.0500000000 LL = -44.4702224731 nd = 0.0162467560, D = 6.75201e-04 6.75201e-04 ( 10.92 s, 0.0 h) - iter 170 lrate = 0.0500000000 LL = -44.4671745300 nd = 0.0156724146, D = 5.76019e-04 5.76019e-04 ( 11.25 s, 0.0 h) - iter 175 lrate = 0.0500000000 LL = -44.4645309448 nd = 0.0140820463, D = 5.07355e-04 5.07355e-04 ( 11.57 s, 0.0 h) - iter 180 lrate = 0.0500000000 LL = -44.4622268677 nd = 0.0130668394, D = 4.38690e-04 4.38690e-04 ( 11.90 s, 0.0 h) - iter 185 lrate = 0.0500000000 LL = -44.4602165222 nd = 0.0126989229, D = 3.92914e-04 3.92914e-04 ( 12.23 s, 0.0 h) - iter 190 lrate = 0.0500000000 LL = -44.4583663940 nd = 0.0119746673, D = 3.54767e-04 3.54767e-04 ( 12.55 s, 0.0 h) - iter 195 lrate = 0.0500000000 LL = -44.4566001892 nd = 0.0120517037, D = 3.43323e-04 3.43323e-04 ( 12.88 s, 0.0 h) ----------------------------------------------------------------------- -Convergence: Completed -Final iteration: 199 -Final LL: -44.4552345276 -Final gradient norm: 1.1440051302e-02 -Total time: 13.12 seconds -====================================================================== diff --git a/pytorch_output_test/out.txt b/pytorch_output_test/out.txt deleted file mode 100644 index aa56d6d6..00000000 --- a/pytorch_output_test/out.txt +++ /dev/null @@ -1,22 +0,0 @@ -====================================================================== -PyTorch AMICA - 2025-08-20 21:43:51 -====================================================================== -Data dimensions: 32 channels, 30504 samples -Number of models: 1 -Number of sources: 32 -Mixture components: 3 -Max iterations: 50 -Learning rate: 0.050000 -Newton optimization: True -Newton start: 50 -Newton ramp: 10 -Device: mps ----------------------------------------------------------------------- - Starting preprocessing... -Removing mean from data -Sphering data - minimum eigenvalues = 259.19741821289062 - maximum eigenvalues = 1932.54589843750000 - num eigs kept = 32 -Optimal block size: 512 - Starting optimization... diff --git a/validate_implementations.py b/validate_implementations.py index 5b604267..0d3c5f76 100644 --- a/validate_implementations.py +++ b/validate_implementations.py @@ -1,44 +1,69 @@ #!/usr/bin/env python """ -Validation script to compare Fortran binary and PyTorch AMICA implementations. - -This script ensures both implementations produce similar results when starting -from the same initialization and random seed. +Validation script to compare the Fortran reference binary with pamica's backends. + +Every backend (``--backend torch``, ``numpy`` or ``mlx``; a comma-separated +list; or ``all``) runs the bundled sample EEG with the same parameters as the +reference run (``sample_params.json``, read through the shared canonical +reader) and is compared against it with the same metrics: final +log-likelihood, Hungarian-matched component correlation, mixing-matrix error +and Amari distance. The default, ``torch``, prints exactly the report this +script has always printed. """ import numpy as np import os import re import subprocess +import sys +import time import torch import inspect from pathlib import Path import shutil import argparse -from typing import Dict, Tuple, Optional +from contextlib import redirect_stdout +from typing import Callable, Dict, List, Tuple, Optional -from pamica import AMICA +from pamica import AMICA, AMICA_NumPy from pamica.fortran_params import ( JSON_ALIAS_TO_CANONICAL, PAMICA_KEY_TO_FORTRAN_KEY, read_params_file, ) +from pamica.numpy_impl.core import _CANONICAL_TO_NUMPY_KEY, _CONSUMED_KEYS from pamica.torch_impl import AMICATorchNG from pamica.torch_impl.utils import load_eeglab_data +# Backends the harness can compare against the reference, in report order, with +# the label each one's report uses and the precision it computes in. +BACKENDS = ("torch", "numpy", "mlx") +_BACKEND_LABELS = {"torch": "PyTorch", "numpy": "NumPy", "mlx": "MLX"} +_BACKEND_PRECISION = {"torch": "float64", "numpy": "float64", "mlx": "float32"} + # Constructor kwargs accepted by AMICATorchNG, used to filter the sample # params.json down to what the natural-gradient backend understands. _NG_PARAMS = set(inspect.signature(AMICATorchNG).parameters) - {"n_channels"} +# The repository this script lives in: every bundled input below is resolved +# against it, so the harness runs from any working directory. +_REPO = Path(__file__).resolve().parent +_SAMPLE_DIR = _REPO / "pamica" / "sample_data" + +# Data-location keys: they tell the reference binary where the data live, but +# every Python run here is handed the loaded array directly. +_DATA_LOCATION_KEYS = {"files", "outdir", "data_dim", "field_dim"} + # params.json keys consumed explicitly (as AMICA()/fit() args or run metadata) -# rather than forwarded as AMICATorchNG constructor kwargs. Any key that is -# neither here nor an AMICATorchNG kwarg is a setting the NG backend cannot -# honor; run_pytorch_amica warns about those so a parity comparison against the -# Fortran run can't silently diverge. `max_decs` is not listed: load_sample_data -# now reads params.json through read_params_file (issue #304), which already -# renames it (and min_grad_norm/share_int) to the canonical maxdecs/min_nd/ -# share_iter spelling -- the same spelling _NG_PARAMS filters on -- so those -# three land in ng_kwargs automatically instead of needing a special case here. +# rather than forwarded as backend constructor kwargs. Any key that is neither +# here nor a key the backend run applies is a setting that backend cannot +# honor; each run_*_amica warns about those (_warn_ignored) so a parity +# comparison against the Fortran run can't silently diverge. `max_decs` is not +# listed: load_sample_data reads params.json through read_params_file (issue +# #304), which already renames it (and min_grad_norm/share_int) to the +# canonical maxdecs/min_nd/share_iter spelling -- the spelling the backend +# constructors use -- so those three are applied automatically instead of +# needing a special case here. _HANDLED_KEYS = { "files", "outdir", @@ -68,9 +93,8 @@ def set_all_seeds(seed: int): def load_sample_data() -> Tuple[np.ndarray, Dict]: """Load the sample EEG data and parameters.""" - sample_dir = Path("pamica/sample_data") - data_file = sample_dir / "eeglab_data.fdt" - params_file = sample_dir / "sample_params.json" + data_file = _SAMPLE_DIR / "eeglab_data.fdt" + params_file = _SAMPLE_DIR / "sample_params.json" if not data_file.exists(): raise FileNotFoundError(f"Sample data not found at {data_file}") @@ -120,7 +144,7 @@ def load_sample_data() -> Tuple[np.ndarray, Dict]: def fortran_accepted_keys( - source: Path = Path("pamica/amica15.f90"), + source: Path = _REPO / "pamica" / "amica15.f90", ) -> Optional[set]: """Keywords the reference binary's parameter parser accepts. @@ -200,18 +224,25 @@ def write_fortran_param_file( dest.write_text("".join(out)) -LEGACY_BINARY = Path("pamica/sample_data/amica15mac") +LEGACY_BINARY = _SAMPLE_DIR / "amica15mac" def default_reference_binary(*, download: bool = True) -> Path: - """The reference binary to compare against, preferring the native engine. + """The reference binary a run uses when none is named, preferring the + native engine. + + This is the one description of the resolution order (the + ``--native-engine`` help refers to it; ``--fortran-binary`` bypasses it): + first the native engine, through + :func:`pamica.native.resolver.resolve` -- the ``PAMICA_NATIVE_BINARY`` + override, then the cached release binary for the host, then a fresh + download of it (verified against its SHA-256); failing all of those, the + bundled macOS x86_64 ``sample_data/amica15mac`` fixture, with a warning. The native engine is built from a source carrying the ``seed`` option - (sccn/amica PR #54), so its runs are reproducible; the bundled - ``amica15mac`` fixture predates it and re-randomizes its initialization on - every run, which makes any comparison against it a comparison with a random - draw (issue #228). Falls back to the fixture, loudly, when the native engine - cannot be resolved. + (sccn/amica PR #54), so its runs are reproducible; the fixture predates it + and re-randomizes its initialization on every run, which makes any + comparison against it a comparison with a random draw (issue #228). """ try: from pamica.native import resolver @@ -256,12 +287,12 @@ def run_fortran_amica( fortran_dir.mkdir(exist_ok=True) # Copy the sample data file to working directory - sample_data_file = Path("pamica/sample_data/eeglab_data.fdt") + sample_data_file = _SAMPLE_DIR / "eeglab_data.fdt" working_data_file = fortran_dir / "eeglab_data.fdt" shutil.copy(sample_data_file, working_data_file) # Copy and modify the parameter file - sample_param_file = Path("pamica/sample_data/input.param") + sample_param_file = _SAMPLE_DIR / "input.param" working_param_file = fortran_dir / "input.param" with open(sample_param_file, "r") as f: @@ -293,12 +324,14 @@ def run_fortran_amica( # Change to working directory to run os.chdir(fortran_dir) + start = time.perf_counter() result = subprocess.run( [str(binary_path), "input.param"], capture_output=True, text=True, timeout=300, # 5 minute timeout ) + runtime_s = time.perf_counter() - start os.chdir(original_dir) @@ -327,8 +360,9 @@ def run_fortran_amica( print(f"Error running Fortran AMICA: {e}") return None - # Parse results - results = {} + # Parse results. runtime_s is the binary's wall-clock (single-threaded, per + # the max_threads pin above), process start-up included. + results = {"runtime_s": runtime_s} # Read convergence info from output out_file = fortran_output / "out.txt" @@ -392,57 +426,83 @@ def run_fortran_amica( return results -def run_pytorch_amica( - data: np.ndarray, params: Dict, output_dir: Path, seed: int -) -> Dict: - """Run the PyTorch natural-gradient EM backend and collect results.""" - print("Running PyTorch AMICA (natural-gradient EM backend)...") - - # Set seed for reproducibility (AMICATorchNG also seeds its own init). - set_all_seeds(seed) +def _check_full_rank_comps(params: Dict, label: str) -> None: + """Refuse a reference run with ``num_comps != data_dim``. - # AMICATorchNG cannot do PCA source reduction (n_sources == n_channels), so - # a Fortran run with num_comps < data_dim would not be an apples-to-apples - # comparison. Fail loudly rather than silently running full-rank. + ``num_comps`` is a reference-binary keyword with no pamica counterpart: + every pamica backend sizes its model from the data rank and + ``pcakeep``/``pcadb`` instead. A Fortran run configured with a different + ``num_comps`` would therefore fit a differently sized model than the + Python run, so fail loudly rather than report an uncontrolled comparison. + """ n_comps = params.get("num_comps", params["data_dim"]) if n_comps != params["data_dim"]: raise ValueError( - f"num_comps={n_comps} != data_dim={params['data_dim']}: " - "AMICATorchNG does not support PCA source reduction, so the " - "PyTorch<->Fortran comparison would not be apples-to-apples." + f"num_comps={n_comps} != data_dim={params['data_dim']}: pamica " + "has no num_comps (its backends size the model from the data rank " + f"and pcakeep/pcadb), so the {label}<->Fortran comparison would " + "not be apples-to-apples." + ) + + +def _warn_ignored(params: Dict, applied: set, backend_class: str, short: str) -> None: + """Name every params.json setting a backend run cannot honor. + + A parity harness must not silently ignore requested settings: the + Fortran run may use them, so the two runs would then be configured + differently. + """ + ignored = sorted(set(params) - applied - _HANDLED_KEYS) + if ignored: + print( + f"WARNING: params.json settings with no {backend_class} equivalent are " + f"ignored ({short} uses its own behavior): {ignored}. The Fortran run " + "may honor them, so a parity comparison can differ." ) - # Map the sample params.json onto AMICATorchNG constructor kwargs. The + +def _run_wrapper_amica(backend: str, data: np.ndarray, params: Dict, seed: int) -> Dict: + """Fit through the ``AMICA`` wrapper with ``backend`` (``"torch"`` or + ``"mlx"``) and collect results. + + The two backends take the same constructor keywords, so the canonical + params map onto both the same way; the wrapper's own ``fit`` resolves the + PyTorch device (with its MPS/float64 -> CPU fallback). + """ + if backend == "torch": + backend_cls, short = AMICATorchNG, "NG" + backend_params = _NG_PARAMS + else: + from pamica.mlx_impl import AMICAMLXNG + + backend_cls, short = AMICAMLXNG, "MLX" + backend_params = set(inspect.signature(AMICAMLXNG).parameters) - {"n_channels"} + + _check_full_rank_comps(params, _BACKEND_LABELS[backend]) + + # Map the sample params.json onto the backend's constructor kwargs. The # backend seeds init, builds the symmetric-ZCA sphere, and starts from an # identity-plus-small-perturbation mixing matrix internally, so no manual - # parameter poking is needed (unlike the removed basic backend). AMICA.fit() - # handles device selection (and the MPS/float64 -> CPU fallback). `params` + # parameter poking is needed (unlike the removed basic backend). `params` # is already canonical-keyed (read_params_file, issue #304), so maxdecs/ - # min_nd/share_iter land here via the plain _NG_PARAMS filter -- no - # special case needed for the json schema's max_decs/min_grad_norm/ + # min_nd/share_iter land here via the plain constructor-signature filter + # -- no special case needed for the json schema's max_decs/min_grad_norm/ # share_int spellings. - ng_kwargs = {k: v for k, v in params.items() if k in _NG_PARAMS} + backend_kwargs = {k: v for k, v in params.items() if k in backend_params} # lrate/do_mean/do_sphere/do_newton/seed/device are passed explicitly to # AMICA()/fit(); drop them from **kwargs to avoid duplicate keyword args. for k in ("lrate", "do_mean", "do_sphere", "do_newton", "seed", "device"): - ng_kwargs.pop(k, None) + backend_kwargs.pop(k, None) - # A parity harness must not silently ignore requested settings: warn about - # any params.json key the NG backend cannot honor (the Fortran run may use - # them, so the two runs would then be configured differently). - ignored = sorted(set(params) - set(ng_kwargs) - _HANDLED_KEYS) - if ignored: - print( - "WARNING: params.json settings with no AMICATorchNG equivalent are " - f"ignored (NG uses its own behavior): {ignored}. The Fortran run " - "may honor them, so a parity comparison can differ." - ) + _warn_ignored(params, set(backend_kwargs), backend_cls.__name__, short) model = AMICA( n_models=params.get("num_models", 1), n_mix=params.get("num_mix", 3), verbose=True, + backend=backend, ) + start = time.perf_counter() model.fit( data, max_iter=params.get("max_iter", 100), @@ -451,8 +511,9 @@ def run_pytorch_amica( do_sphere=params.get("do_sphere", True), do_newton=params.get("do_newton", False), seed=seed, - **ng_kwargs, + **backend_kwargs, ) + runtime_s = time.perf_counter() - start return { # final_ll_ is the LL of the fitted parameters (issue #51 best-iterate @@ -460,12 +521,119 @@ def run_pytorch_amica( # sit below the returned iterate after a late overshoot. "final_ll": model.final_ll_, "final_iter": len(model.ll_history_), - "W": model.get_unmixing_matrix(0), - "A": model.get_mixing_matrix(0), - "ll_history": model.ll_history_, + # float64 (a no-op for torch), so the comparison arithmetic runs at the + # same precision for every backend. + "W": model.get_unmixing_matrix(0).astype(np.float64), + "A": model.get_mixing_matrix(0).astype(np.float64), + "ll_history": list(model.ll_history_), + "runtime_s": runtime_s, + } + + +def run_pytorch_amica( + data: np.ndarray, params: Dict, output_dir: Path, seed: int +) -> Dict: + """Run the PyTorch natural-gradient EM backend and collect results.""" + print("Running PyTorch AMICA (natural-gradient EM backend)...") + + # Set seed for reproducibility (AMICATorchNG also seeds its own init). + set_all_seeds(seed) + return _run_wrapper_amica("torch", data, params, seed) + + +def run_numpy_amica( + data: np.ndarray, params: Dict, output_dir: Path, seed: int +) -> Dict: + """Run the legacy NumPy backend (``AMICA_NumPy``) and collect results. + + ``params`` is canonical-keyed (``read_params_file``, issue #304); the + three settings this backend spells differently (``min_nd``, ``maxdecs``, + ``share_iter``) go through the backend's own ``_CANONICAL_TO_NUMPY_KEY`` + table rather than a copy of it. The backend computes in float64, so the + float32 sample is widened first, as its own parity test does. + """ + print("Running NumPy AMICA (legacy reference backend)...") + + _check_full_rank_comps(params, "NumPy") + + numpy_kwargs, applied = {}, set() + for key, value in params.items(): + numpy_key = _CANONICAL_TO_NUMPY_KEY.get(key, key) + if key in _DATA_LOCATION_KEYS or numpy_key not in _CONSUMED_KEYS: + continue + numpy_kwargs[numpy_key] = value + applied.add(key) + _warn_ignored(params, applied, "AMICA_NumPy", "NumPy") + + # Keep the backend's Fortran-style output (out.txt, the writestep + # checkpoints and the final amicaout files) next to this run's reports, + # as the reference run's is; the backend writes nothing without an outdir. + numpy_kwargs["outdir"] = str(output_dir / "numpy_run") + numpy_kwargs["seed"] = seed + model = AMICA_NumPy(**numpy_kwargs) + + start = time.perf_counter() + model.fit(data.astype(np.float64)) + runtime_s = time.perf_counter() - start + if not model.converged: + raise RuntimeError( + f"AMICA_NumPy fit ended degenerate (stop_reason={model.stop_reason!r})" + ) + + W = model.get_weights() + assert model.A is not None and model.comp_list is not None + return { + # ll[-1] is this backend's final_ll_ (it has no keep_best restore). + "final_ll": float(model.ll[-1]), + "final_iter": len(model.ll), + "W": W, + # get_weights() is the stored W transposed (issue #24 convention), so + # the true mixing is the stored A's model-0 columns transposed, the + # same composition AMICATorchNG.get_mixing_matrix returns. + "A": model.A[:, model.comp_list[:, 0]].T, + "ll_history": list(model.ll), + "runtime_s": runtime_s, } +def mlx_unavailable_reason() -> Optional[str]: + """Why the MLX backend cannot run here, or ``None`` when it can.""" + try: + import pamica.mlx_impl # noqa: F401 + except ImportError as exc: + return ( + f"--backend mlx needs the optional MLX backend, which failed to " + f"import ({exc}). MLX runs on Apple Silicon only; install it with " + "`uv sync --extra mlx`." + ) + return None + + +def run_mlx_amica(data: np.ndarray, params: Dict, output_dir: Path, seed: int) -> Dict: + """Run the MLX backend (``AMICA(backend="mlx")``, float32 on the Apple + GPU) and collect results.""" + print("Running MLX AMICA (Apple-GPU float32 backend)...") + return _run_wrapper_amica("mlx", data, params, seed) + + +_RUNNERS: Dict[str, Callable[[np.ndarray, Dict, Path, int], Dict]] = { + "torch": run_pytorch_amica, + "numpy": run_numpy_amica, + "mlx": run_mlx_amica, +} + + +def run_backend( + name: str, data: np.ndarray, params: Dict, output_dir: Path, seed: int +) -> Dict: + """Run backend ``name`` (one of :data:`BACKENDS`) on ``data`` with the + harness's ``params`` and return its results dict (``final_ll``, + ``final_iter``, ``W``, ``A``, ``ll_history``, ``runtime_s``).""" + if name not in _RUNNERS: + raise ValueError(f"unknown backend {name!r}; expected one of {BACKENDS}") + return _RUNNERS[name](data, params, output_dir, seed) + + def _amari_index(gain: np.ndarray) -> float: n = gain.shape[0] if n < 2: @@ -494,46 +662,49 @@ def amari_distance(Wa: np.ndarray, Wb: np.ndarray) -> float: return float((forward + backward) / 2) -def compare_results(fortran_results: Optional[Dict], pytorch_results: Dict) -> Dict: - """Compare results from both implementations.""" +def compare_results( + fortran_results: Optional[Dict], backend_results: Dict, label: str = "PyTorch" +) -> Dict: + """Compare a backend's results (labeled ``label`` in the printout) with + the reference run's.""" comparison = {} if fortran_results is None: - print("\nNo Fortran results to compare. Showing PyTorch results only:") - print(f" Final LL: {pytorch_results['final_ll']:.6f}") - print(f" Iterations: {pytorch_results['final_iter']}") + print(f"\nNo Fortran results to compare. Showing {label} results only:") + print(f" Final LL: {backend_results['final_ll']:.6f}") + print(f" Iterations: {backend_results['final_iter']}") return {"status": "fortran_unavailable"} # Compare log-likelihood fortran_ll = fortran_results.get("final_ll", 0) - pytorch_ll = pytorch_results["final_ll"] + backend_ll = backend_results["final_ll"] # Note: There may be a scaling difference - ll_ratio = pytorch_ll / fortran_ll if fortran_ll != 0 else float("inf") + ll_ratio = backend_ll / fortran_ll if fortran_ll != 0 else float("inf") comparison["ll_ratio"] = ll_ratio - comparison["ll_difference"] = abs(pytorch_ll - fortran_ll) + comparison["ll_difference"] = abs(backend_ll - fortran_ll) # Compare convergence speed comparison["fortran_iters"] = fortran_results.get("final_iter", 0) - comparison["pytorch_iters"] = pytorch_results["final_iter"] + comparison["backend_iters"] = backend_results["final_iter"] # Compare mixing/unmixing matrices (if available) - if "W" in fortran_results and "W" in pytorch_results: + if "W" in fortran_results and "W" in backend_results: W_fortran = fortran_results["W"] - W_pytorch = pytorch_results["W"] + W_backend = backend_results["W"] # Compute correlation between components - if W_fortran.shape == W_pytorch.shape: + if W_fortran.shape == W_backend.shape: # Normalize rows (components) W_fortran_norm = W_fortran / ( np.linalg.norm(W_fortran, axis=1, keepdims=True) + 1e-10 ) - W_pytorch_norm = W_pytorch / ( - np.linalg.norm(W_pytorch, axis=1, keepdims=True) + 1e-10 + W_backend_norm = W_backend / ( + np.linalg.norm(W_backend, axis=1, keepdims=True) + 1e-10 ) # Compute absolute correlations (components may have sign flip and permutation) - correlations = np.abs(W_fortran_norm @ W_pytorch_norm.T) + correlations = np.abs(W_fortran_norm @ W_backend_norm.T) # Find best matching components using Hungarian algorithm for optimal assignment from scipy.optimize import linear_sum_assignment @@ -553,17 +724,20 @@ def compare_results(fortran_results: Optional[Dict], pytorch_results: Dict) -> D # Store permutation for component matching comparison["component_permutation"] = col_ind + # The assignment-free second metric (see amari_distance). + comparison["amari_distance"] = amari_distance(W_fortran, W_backend) + # Also compare A matrices if available - if "A" in fortran_results and "A" in pytorch_results: + if "A" in fortran_results and "A" in backend_results: A_fortran = fortran_results["A"] - A_pytorch = pytorch_results["A"] + A_backend = backend_results["A"] - if A_fortran.shape == A_pytorch.shape: - # Apply same permutation to PyTorch A for fair comparison - A_pytorch_perm = A_pytorch[:, col_ind] + if A_fortran.shape == A_backend.shape: + # Apply same permutation to the backend's A for fair comparison + A_backend_perm = A_backend[:, col_ind] # Compute reconstruction error - A_diff = np.linalg.norm(A_fortran - A_pytorch_perm, "fro") + A_diff = np.linalg.norm(A_fortran - A_backend_perm, "fro") A_norm = np.linalg.norm(A_fortran, "fro") comparison["mixing_matrix_error"] = ( A_diff / A_norm if A_norm > 0 else float("inf") @@ -573,11 +747,14 @@ def compare_results(fortran_results: Optional[Dict], pytorch_results: Dict) -> D def print_comparison_report( - comparison: Dict, fortran_results: Optional[Dict], pytorch_results: Dict + comparison: Dict, + fortran_results: Optional[Dict], + backend_results: Dict, + label: str = "PyTorch", ): - """Print a formatted comparison report.""" + """Print a formatted comparison report for the backend labeled ``label``.""" print("\n" + "=" * 70) - print("VALIDATION REPORT: Fortran vs PyTorch AMICA") + print(f"VALIDATION REPORT: Fortran vs {label} AMICA") print("=" * 70) if comparison.get("status") == "fortran_unavailable": @@ -589,9 +766,9 @@ def print_comparison_report( print("-" * 40) if fortran_results: print(f" Fortran Final LL: {fortran_results.get('final_ll', 'N/A')}") - print(f" PyTorch Final LL: {pytorch_results['final_ll']:.6f}") + print(f" {label} Final LL: {backend_results['final_ll']:.6f}") if "ll_ratio" in comparison: - print(f" LL Ratio (PyTorch/Fortran): {comparison['ll_ratio']:.4f}") + print(f" LL Ratio ({label}/Fortran): {comparison['ll_ratio']:.4f}") print(f" LL Absolute Difference: {comparison['ll_difference']:.6f}") # Convergence comparison @@ -599,7 +776,7 @@ def print_comparison_report( print("-" * 40) if "fortran_iters" in comparison: print(f" Fortran Iterations: {comparison['fortran_iters']}") - print(f" PyTorch Iterations: {comparison['pytorch_iters']}") + print(f" {label} Iterations: {comparison['backend_iters']}") # Component correlation if "mean_correlation" in comparison: @@ -655,31 +832,119 @@ def print_comparison_report( print("\n" + "=" * 70) -def main(): +def _fmt(value, spec: str) -> str: + """``value`` formatted with ``spec``, or ``n/a`` when it is missing.""" + return "n/a" if value is None else format(value, spec) + + +def format_parity_summary( + fortran_results: Optional[Dict], rows: List[Tuple[str, Dict, Dict]] +) -> str: + """One Markdown table row per backend run, plus the reference's own row. + + ``rows`` holds ``(backend name, results, comparison)`` triples as produced + by :func:`run_backend` and :func:`compare_results`. Runtime is each fit's + wall-clock; the reference's includes process start-up and is + single-threaded (``max_threads 1``). + """ + lines = [ + "| backend | precision | iterations | final LL | abs. LL difference " + "| mean matched corr | min matched corr | Amari distance | runtime (s) |", + "|---|---|---:|---:|---:|---:|---:|---:|---:|", + ] + if fortran_results is not None: + lines.append( + "| Fortran (reference) | float64 | " + f"{_fmt(fortran_results.get('final_iter'), 'd')} | " + f"{_fmt(fortran_results.get('final_ll'), '.6f')} | | | | | " + f"{_fmt(fortran_results.get('runtime_s'), '.1f')} |" + ) + for name, results, comparison in rows: + lines.append( + f"| {_BACKEND_LABELS[name]} | {_BACKEND_PRECISION[name]} | " + f"{results['final_iter']} | {results['final_ll']:.6f} | " + f"{_fmt(comparison.get('ll_difference'), '.6f')} | " + f"{_fmt(comparison.get('mean_correlation'), '.4f')} | " + f"{_fmt(comparison.get('min_correlation'), '.4f')} | " + f"{_fmt(comparison.get('amari_distance'), '.4f')} | " + f"{results['runtime_s']:.1f} |" + ) + return "\n".join(lines) + + +def parse_backends(value: str) -> List[str]: + """Parse ``--backend``: one backend, a comma-separated list, or ``all``. + + Returns the names in the order given, without duplicates. + """ + if value.strip() == "all": + return list(BACKENDS) + names = [name.strip() for name in value.split(",")] + unknown = [name for name in names if name not in BACKENDS] + if unknown or not names: + raise argparse.ArgumentTypeError( + f"unknown backend(s) {unknown}; choose from {', '.join(BACKENDS)}, " + "a comma-separated list of them, or 'all'" + ) + return list(dict.fromkeys(names)) + + +def build_parser() -> argparse.ArgumentParser: + """The harness's command-line interface.""" parser = argparse.ArgumentParser(description="Validate AMICA implementations") parser.add_argument("--seed", type=int, default=42, help="Random seed") parser.add_argument("--max-iter", type=int, default=100, help="Maximum iterations") parser.add_argument("--output-dir", type=str, help="Output directory") + parser.add_argument( + "--backend", + type=parse_backends, + default=None, + metavar="{torch,numpy,mlx}[,...]|all", + help="pamica backend(s) to compare against the reference: torch (the " + "default), numpy, mlx (Apple Silicon, float32), a comma-separated list " + "such as torch,mlx, or all. Each backend gets its own report; passing " + "this flag also prints and saves a one-row-per-backend parity summary.", + ) parser.add_argument( "--skip-fortran", action="store_true", help="Skip Fortran comparison" ) parser.add_argument( "--native-engine", action="store_true", - help="Run the reference through pamica.native: resolve the binary via " - "PAMICA_NATIVE_BINARY or the cached/downloaded cross-platform release " - "binary, instead of the bundled macOS-only sample_data/amica15mac. This " - "is how to run the real Fortran reference on Linux/Windows/Apple Silicon.", + help="Use the native engine only: the default resolution order (see " + "default_reference_binary) without its fallback to the bundled " + "amica15mac, so a native binary that cannot be resolved skips the " + "Fortran comparison instead.", ) parser.add_argument( "--fortran-binary", type=str, default=None, - help="Explicit path to the AMICA reference binary (overrides the bundled " - "fixture; ignored when --native-engine is given).", + help="Explicit path to the AMICA reference binary (overrides the " + "native-engine resolution; ignored when --native-engine is given).", ) + return parser + + +def _report_file(output_dir: Path, backend: str) -> Path: + """Where a backend's report is saved. The PyTorch report keeps the name + the harness has always used.""" + if backend == "torch": + return output_dir / "validation_report.txt" + return output_dir / f"validation_report_{backend}.txt" - args = parser.parse_args() + +def main(argv: Optional[List[str]] = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + # The default run (no --backend) is exactly the historical torch-only run. + backends = args.backend if args.backend is not None else ["torch"] + if "mlx" in backends: + reason = mlx_unavailable_reason() + if reason is not None: + print(f"ERROR: {reason}", file=sys.stderr) + return 2 # Set up output directory if args.output_dir: @@ -700,8 +965,8 @@ def main(): # Update parameters params["max_iter"] = args.max_iter - # Resolve which reference binary to run: the bundled macOS fixture by - # default, or the cross-platform native-engine binary on request. + # Resolve which reference binary to run: the native engine by default + # (see default_reference_binary), or an explicit binary on request. fortran_binary = None if not args.skip_fortran and args.native_engine: from pamica.native import resolver @@ -718,33 +983,43 @@ def main(): elif args.fortran_binary: fortran_binary = Path(args.fortran_binary) - # Run Fortran implementation + # Run Fortran implementation (once, shared by every backend below) fortran_results = None if not args.skip_fortran: fortran_results = run_fortran_amica( data, params, output_dir, args.seed, binary_path=fortran_binary ) - # Run PyTorch implementation - pytorch_results = run_pytorch_amica(data, params, output_dir, args.seed) + rows = [] + for backend in backends: + label = _BACKEND_LABELS[backend] + results = run_backend(backend, data, params, output_dir, args.seed) - # Compare results - comparison = compare_results(fortran_results, pytorch_results) + # Compare results + comparison = compare_results(fortran_results, results, label) - # Print report - print_comparison_report(comparison, fortran_results, pytorch_results) + # Print report + print_comparison_report(comparison, fortran_results, results, label) - # Save comparison to file - report_file = output_dir / "validation_report.txt" - with open(report_file, "w") as f: - import sys + # Save comparison to file + report_file = _report_file(output_dir, backend) + with open(report_file, "w") as f, redirect_stdout(f): + print_comparison_report(comparison, fortran_results, results, label) - original_stdout = sys.stdout - sys.stdout = f - print_comparison_report(comparison, fortran_results, pytorch_results) - sys.stdout = original_stdout + print(f"\nReport saved to: {report_file}") + rows.append((backend, results, comparison)) - print(f"\nReport saved to: {report_file}") + if args.backend is not None: + summary = format_parity_summary(fortran_results, rows) + print( + "\nPARITY SUMMARY (seed={}, max_iter={}):".format( + args.seed, args.max_iter + ) + ) + print(summary) + summary_file = output_dir / "parity_summary.md" + summary_file.write_text(summary + "\n") + print(f"\nSummary saved to: {summary_file}") except Exception as e: print(f"\nERROR during validation: {e}") From aa15e55e5011723a0d8113a40dc496998c4c9647 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 23 Sep 2026 02:56:21 +0000 Subject: [PATCH 08/26] Rebuild paper.pdf --- paper.pdf | Bin 375736 -> 375736 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/paper.pdf b/paper.pdf index 610fed8225008564690e8d64639601fc760df247..9a770e4a136d65b4af9f62f18f5347a3256bebc9 100644 GIT binary patch delta 239 zcmdn-U2Mm9v4$4L7N#xCzdo`XS(%zy8CY&-{KWi{5yqST@iVh2oTK}N*&N2%p8bVc zf>FiLA|*N1#LP(7%skOp*TmS;RM#TeGD+7g+0wuiD4t}PlH9)hEA#f{Us>#SDH)lX z8CXVXD(L&>r?@1Rq$+5*SQ!}@0R@eW4ZxDy**CBTaJV_Unp&7UnK>F6TAH~T7`Paj kI+{Bgn_4(I8=E+rnY!93*br6{JKeC8S#JC89jt$u05T^;lK=n! delta 239 zcmdn-U2Mm9v4$4L7N#xCzdo`XTA7$w85(bA{KWi{5yqST@iVh2oTK}N*&N2%p8bVc zf>Fgd)x<10H8E8;(IO>T*96GZO-wSh&^1ajG)_%3GB8auPi$ZQm3jN}uPk=ElnhNw z42`2S74&`cQ(O{DQWZ2@tc(nd%nU4zj19n&+u1j;25`6;xms9Qx>}kznp!wHn;5!U ln7SFb8MqiZo4J}fyO}xLDcBHJ5 Date: Tue, 22 Sep 2026 21:11:34 -0700 Subject: [PATCH 09/26] Phase 10: write the sphere column-major (#337) * Write and read the EEGLAB sphere column-major (#336) * Fix S-order tests to expect column-major (#336) * Add cross-backend sphere export round-trip test (#336) * Document the sphere-order fix (#336) * Add native-binary sphere orientation test * Compare MLX sphere bytes bit-exactly * Fix sphere-order comments in write_amicaout * Warn about old asymmetric-sphere output * Correct scratch-history note on S order * Polish sphere-order wording in docs/tests --- .context/scratch_history.md | 7 + docs/changelog.md | 23 +++ docs/guides/eeglab.md | 4 + docs/guides/validation.md | 10 +- pamica/numpy_impl/data.py | 7 +- pamica/numpy_impl/load.py | 29 ++- pamica/tests/test_amicaout_reduced.py | 56 ++++-- pamica/tests/test_native_engine.py | 51 +++++ pamica/tests/test_sphere_export_order.py | 182 ++++++++++++++++++ .../torch_tests/test_amica_ng_wrapper.py | 12 +- 10 files changed, 346 insertions(+), 35 deletions(-) create mode 100644 pamica/tests/test_sphere_export_order.py diff --git a/.context/scratch_history.md b/.context/scratch_history.md index 11b23272..c2f0b2b1 100644 --- a/.context/scratch_history.md +++ b/.context/scratch_history.md @@ -108,6 +108,13 @@ use `order="F"` for those arrays. Diagnostic that nails the layout: read genuine internal-vs-true-unmixing transpose (#24) cancels against Fortran's column-major storage (`self.W` C-order bytes == Fortran column-major `W_true` bytes). `S` is symmetric (order-agnostic); `mean`/`gm`/`LL` are 1-D. + CORRECTION (2026-09-22, #336): "`S` is symmetric (order-agnostic)" is wrong. + Fortran writes `S` column-major like every other EEGLAB-read 2-D file; the + square branch wrote it C-order, which only looked "order-agnostic" because the + default zero-phase component analysis (ZCA) sphere is symmetric to ~1e-17. + With `do_approx_sphere=False` the sphere is genuinely asymmetric and the old + C-order write came back exactly transposed (measured `max|S_loaded - S| = 0.51` + on the bundled sample). Fixed in #336: `S` is now `order="F"` in both branches. - [SUPERSEDED by #159 -- this bullet's premise EXPIRED, read the update below] Remaining, deliberately-out-of-scope quirk: `loadmodout`/`load_results` still read the **W** file C-order, so the port's `mod.W` is the transpose of MATLAB's diff --git a/docs/changelog.md b/docs/changelog.md index 5a1d90e3..daae998f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,29 @@ Release notes are also published on the ## Unreleased +- **Fix: the EEGLAB export wrote an asymmetric sphere transposed (Phase 10 of epic #324, issue #336).** + `write_amicaout` (`pamica/numpy_impl/load.py`), the shared writer called from `write_amica_output` on + `AMICATorchNG`, `AMICAMLXNG` and the `AMICA` wrapper, and from the NumPy backend's own `_write_results`, + wrote the square sphere matrix `S` in C order, + while the Fortran reference and both readers + (EEGLAB's `loadmodout15.m` and pamica's `loadmodout`) read it column-major. + The default symmetric zero-phase component analysis (ZCA) sphere is its own transpose to about 1e-17, + so the bug moved only that many bytes there; + with `do_approx_sphere=False` the sphere is genuinely asymmetric, + and the exported sphere came back exactly transposed + (measured on the bundled sample, torch, 3 iterations: + before the fix `max|S_loaded - S| = 0.51`, `max|S_loaded - S.T| = 0.0`; + after, `max|S_loaded - S| = 0.0`, `max|S_loaded - S.T| = 0.51`). + `S` is now written column-major in both the square and rank-reduced branches, + and `load_results` reads a square sphere the same way. + **Action needed for existing output:** a full-rank directory written with `do_approx_sphere=False` + by an earlier pamica holds a C-order `S`. + EEGLAB always read that transposed (this fix does not change EEGLAB's own reading, only pamica's), + and pamica's corrected `loadmodout`/`load_results` now also read it transposed, with no error raised. + Regenerate any such directory by re-running the fit and `write_amica_output` again; + there is no on-disk version marker to detect the old layout, and this repo carries no + compatibility shim to read it automatically. + Directories from the default (symmetric) sphere and from a rank-reduced (`pcakeep`) fit are unaffected. - **Phase 6 of epic #324: the validation harness covers every backend (issue #315).** `validate_implementations.py --backend {torch,numpy,mlx}` (or a comma-separated list, or `all`) compares each backend against one Fortran reference run with the same settings; diff --git a/docs/guides/eeglab.md b/docs/guides/eeglab.md index 0ddb0414..a49bc94b 100644 --- a/docs/guides/eeglab.md +++ b/docs/guides/eeglab.md @@ -33,6 +33,10 @@ This writes the raw binary files EEGLAB's AMICA loader reads: For a single model the bytes are identical to the reference Fortran binary's `amicaout` files, so the directory is interchangeable with a native AMICA run. +A directory written with `do_approx_sphere=False` (a full-rank, genuinely +asymmetric `S`) by a pamica older than issue #336's fix has `S` transposed; +re-run `write_amica_output` to regenerate it (the default symmetric sphere and +a rank-reduced fit were unaffected). The MLX backend writes the same directory: an Apple-Silicon fit exports to EEGLAB directly, with no torch round trip (epic #278). The export is diff --git a/docs/guides/validation.md b/docs/guides/validation.md index 13c39e24..b02fd531 100644 --- a/docs/guides/validation.md +++ b/docs/guides/validation.md @@ -357,9 +357,13 @@ files (`gm`, `W`, `S`, `mean`, `c`, `alpha`, `mu`, `sbeta`, `rho`, `comp_list`, The round-trip is verified two ways: -- **Byte-level:** for a single model the written files are an exact float64 serialization of the fitted - parameters. `W` and the symmetric zero-phase component analysis (ZCA) sphere are byte-identical in C order; the non-square mixture - parameters and `c`/`comp_list` are column-major (Fortran layout), matching the reference `amicaout` files. +- **Byte-level:** for a single model the written files are an exact float64 serialization of the fitted parameters. + `W` is byte-identical in C order; + the sphere `S` and the non-square mixture parameters and `c`/`comp_list` are column-major (Fortran layout), + matching the reference `amicaout` files. + The default symmetric zero-phase component analysis (ZCA) sphere happens to be its own transpose to about 1e-17, + which is why an earlier column-major/C-order mismatch in the square-sphere write path went unnoticed, + until it was measured against an asymmetric (`do_approx_sphere=False`) sphere (issue #336). - **Reader-level:** the directory loads through `loadmodout15.m` (and its NumPy port `loadmodout`) with the expected shapes and the correct column-major layout. The MATLAB round-trip during development is what caught, and fixed, a column-major format bug in the mixture-parameter arrays. diff --git a/pamica/numpy_impl/data.py b/pamica/numpy_impl/data.py index 324f68e2..962fe911 100644 --- a/pamica/numpy_impl/data.py +++ b/pamica/numpy_impl/data.py @@ -284,13 +284,14 @@ def _read(name, dtype: type = np.float64): # The sphere is (nw, nx): square unless rank reduction kept nw < nx # dimensions, in which case write_amicaout pads it with zero rows to the - # reference's (nx, nx) record and writes it column-major (a square one is - # written C-order). Read it back the way it was written. + # reference's (nx, nx) record. Both the square and padded cases are written + # column-major (Fortran layout, matching the reference and loadmodout()), + # so read both back order="F" (issue #336). S = _read("S") if S is not None: nx = int(round(np.sqrt(len(S)))) if nx == nw: - results["sphere"] = S.reshape(nw, nw) + results["sphere"] = S.reshape(nw, nw, order="F") else: results["sphere"] = S.reshape(nx, nx, order="F")[:nw] diff --git a/pamica/numpy_impl/load.py b/pamica/numpy_impl/load.py index 4231d226..95850d8f 100644 --- a/pamica/numpy_impl/load.py +++ b/pamica/numpy_impl/load.py @@ -166,8 +166,16 @@ def _w(name, arr, dtype=np.float64, order="C"): f"W has {W.shape[2]} models but gm has {gm.size}; num_models disagree" ) + # mean/gm/LL are 1-D, so their order does not matter. _w("gm", gm) if A is not None: + # A is the one 2-D file here NOT written column-major: loadmodout15.m + # ignores it entirely (it derives A from W and S instead, per the + # docstring above), so nothing outside pamica ever reads these bytes. + # `_w`'s C-order default is kept, and `load_results` reads it back the + # same way (`data.py`'s plain `.reshape(...)`, no `order="F"`) -- the + # only contract this file has to satisfy is pamica-writes/pamica-reads + # self-consistency, not the Fortran/EEGLAB layout. _w("A", A) # W is the internal-backend unmixing (b = W.T @ x); Fortran/EEGLAB store the # true unmixing W_fortran = W.T with the model axis slowest, column-major @@ -177,13 +185,25 @@ def _w(name, arr, dtype=np.float64, order="C"): # this is byte-identical to the old plain C-order write, so single-model # output stays byte-compatible with the Fortran reference (issue #92); the # multi-model interleave it replaces was never MATLAB-readable (issue #159). - # The symmetric sphere S is order-agnostic; mean/gm/LL are 1-D. _w("W", np.asarray(W).transpose(2, 0, 1)) # Fortran always writes S at recl = 2*nbyte*nx*nx (amica15.f90:2423): the # array is allocated (nx, nx) and zero-filled, and a rank-reduced sphere # occupies only its first `numeigs` rows. Pad to that shape so a reduced fit # is readable by loadmodout15.m and by loadmodout() below, which both reshape # to (nx, nx) and slice [:num_pcs] (issue #164/#223). + # + # S is written column-major (order="F") in both branches, like every other + # EEGLAB-read 2-D file here (c, alpha, mu, sbeta, rho, comp_list below): that + # is what the Fortran reference writes and what both readers (loadmodout15.m + # and loadmodout() below) read. (A, above, is the one 2-D file that is NOT + # column-major, and for a different reason: EEGLAB never reads it.) The + # square branch used to write C order on the reasoning that the default + # zero-phase component analysis (ZCA) sphere is its own transpose (true to + # ~1e-17) so C order was "byte-identical to the Fortran reference" -- that + # reasoning was backwards: the reference itself is column-major, so C + # order only happened to match by the sphere's own symmetry, and diverges + # (transposed) whenever `do_approx_sphere=False` gives a genuinely + # asymmetric sphere (issue #336). sphere = np.asarray(sphere) if sphere.ndim != 2: raise ValueError(f"sphere must be 2-D (nw, nx); got shape {sphere.shape}") @@ -196,14 +216,9 @@ def _w(name, arr, dtype=np.float64, order="C"): if n_keep < n_in: padded = np.zeros((n_in, n_in), dtype=np.float64) padded[:n_keep] = sphere - # Column-major, because the padded array is not symmetric. The square - # branch below deliberately keeps its C-order write: the symmetric-ZCA - # sphere is its own transpose only to ~1e-17, so switching orders there - # would perturb bytes that are guaranteed identical to the Fortran - # reference (issue #92). _w("S", padded, order="F") else: - _w("S", sphere) + _w("S", sphere, order="F") _w("mean", mean) # The (num_mix, num_comps) mixture params and (num_comps, num_models) c / # comp_list are non-square, so their byte layout DOES depend on order: they diff --git a/pamica/tests/test_amicaout_reduced.py b/pamica/tests/test_amicaout_reduced.py index a9f42b39..a0cb80e2 100644 --- a/pamica/tests/test_amicaout_reduced.py +++ b/pamica/tests/test_amicaout_reduced.py @@ -55,13 +55,20 @@ def reduced_fit(rank_deficient: np.ndarray) -> AMICATorchNG: def test_sphere_is_padded_to_the_fortran_record_shape(reduced_fit, tmp_path): - """S on disk must be nx*nx values, not num_pcs*nx.""" + """S on disk must be nx*nx values, not num_pcs*nx, written column-major + (the padded branch has always used order="F"; issue #336 only fixed the + square branch).""" outdir = tmp_path / "amicaout" reduced_fit.write_amica_output(outdir) raw = np.fromfile(outdir / "S", dtype=np.float64) assert raw.size == NW * NW, ( f"S has {raw.size} values; EEGLAB's loadmodout15 expects {NW * NW}" ) + assert reduced_fit.sphere is not None + sphere = reduced_fit.sphere.cpu().numpy() + padded = np.zeros((NW, NW), dtype=np.float64) + padded[:RANK] = sphere + np.testing.assert_array_equal(raw, padded.ravel(order="F")) def test_loadmodout_reads_a_reduced_fit(reduced_fit, tmp_path): @@ -73,6 +80,8 @@ def test_loadmodout_reads_a_reduced_fit(reduced_fit, tmp_path): assert out.S.shape == (NW, NW) # Only the first num_pcs rows carry the sphere; the pad must be exactly zero. np.testing.assert_array_equal(out.S[RANK:], 0.0) + assert reduced_fit.sphere is not None + np.testing.assert_array_equal(out.S[:RANK], reduced_fit.sphere.cpu().numpy()) def test_sources_roundtrip_under_reduced_rank(reduced_fit, rank_deficient, tmp_path): @@ -105,27 +114,38 @@ def test_sources_roundtrip_under_reduced_rank(reduced_fit, rank_deficient, tmp_p assert np.all(np.isfinite(ratio.mean(axis=1))) -def test_square_sphere_bytes_are_unchanged(real_data, tmp_path): - """Full-rank output must stay byte-identical to the Fortran reference. - - The symmetric-ZCA sphere is its own transpose, so switching the writer to - column-major cannot move a byte here -- asserted rather than assumed, since - single-model output being byte-compatible with the reference is a standing - guarantee (issue #92). +@pytest.mark.parametrize("do_approx_sphere", [True, False]) +def test_sphere_is_written_and_read_column_major(real_data, tmp_path, do_approx_sphere): + """``S`` is written column-major, matching the Fortran reference and both + readers (EEGLAB's ``loadmodout15.m`` and pamica's ``loadmodout``), for a + symmetric (default) sphere and for a genuinely asymmetric one alike + (``do_approx_sphere=False``, issue #336). + + Before the fix, the square branch wrote ``S`` C-order: invisible for the + default zero-phase component analysis (ZCA) sphere, which is its own + transpose to about 1e-17, but with ``do_approx_sphere=False`` the exported + sphere came back exactly transposed (measured ``max|S_loaded - S| = 0.51``, + ``max|S_loaded - S.T| = 0.0`` on this sample). The asymmetry assertion below + makes sure this test cannot pass by accident on a sphere that happens to be + symmetric. """ - m = AMICATorchNG(n_channels=NW, seed=0, device="cpu") + m = AMICATorchNG( + n_channels=NW, seed=0, device="cpu", do_approx_sphere=do_approx_sphere + ) m.fit(real_data, max_iter=3, verbose=False) assert m.n_channels == NW + assert m.sphere is not None + sphere = m.sphere.cpu().numpy() + + if not do_approx_sphere: + assert np.abs(sphere - sphere.T).max() > 1e-3, ( + "exact sphere is unexpectedly symmetric; this test would not guard anything" + ) outdir = tmp_path / "amicaout" m.write_amica_output(outdir) on_disk = np.fromfile(outdir / "S", dtype=np.float64) - assert m.sphere is not None - sphere = m.sphere.cpu().numpy() - # Unchanged C-order write. The ZCA sphere is symmetric only to ~1e-17, so - # this is not interchangeable with a column-major write -- which is exactly - # why the writer leaves the square branch alone. - np.testing.assert_array_equal(on_disk, sphere.ravel(order="C")) - assert not np.array_equal(sphere.ravel(order="F"), sphere.ravel(order="C")), ( - "sphere is bit-symmetric here, so this test no longer guards anything" - ) + np.testing.assert_array_equal(on_disk, sphere.ravel(order="F")) + + loaded = loadmodout(outdir).S[:NW] + np.testing.assert_array_equal(loaded, sphere) diff --git a/pamica/tests/test_native_engine.py b/pamica/tests/test_native_engine.py index 52687dcd..5d843435 100644 --- a/pamica/tests/test_native_engine.py +++ b/pamica/tests/test_native_engine.py @@ -163,6 +163,57 @@ def test_native_engine_runs_and_produces_amica_output(real_data): assert np.all(np.isfinite(sources)) +@requires_binary +def test_native_engine_asymmetric_sphere_reads_the_right_orientation(real_data): + """``loadmodout`` must read the reference binary's own asymmetric sphere in + the right orientation (issue #336), checked without any dependence on + pamica's writer. ``do_approx_sphere=0`` is a plain kwarg: ``AMICANative`` + forwards any Fortran ``input.param`` field, and amica15.f90's param parser + has a ``case('do_approx_sphere')`` branch (amica15.f90:3448-3455) over the + exact sphering path (amica15.f90:495-508) -- not the default zero-phase + component analysis (ZCA) sphering -- which produces a genuinely asymmetric + sphere. + + The orientation check is that the loaded ``S`` whitens the covariance the + reference itself computed: ``S @ cov @ S.T == I`` to float64 round-off, + where ``cov`` is the population covariance of the mean-centered data + (``Stmp/cnt`` via ``DSYRK`` then a scale by ``1/cnt``, amica15.f90:360-389 -- + matching ``torch.cov(..., correction=0)``, not NumPy/``torch.cov``'s default + ``/(N-1)``). A transposed read would fail this (a general asymmetric + whitening matrix does not satisfy it in the other orientation), which the + second assertion below confirms is not a vacuous check. + """ + eng = AMICANative( + binary=_BINARY, n_models=1, n_mix=3, max_iter=8, threads=2, do_approx_sphere=0 + ) + eng.fit(real_data) + out = eng.output_ + assert out is not None + S = out.S[: out.num_pcs] + assert S.shape == (32, 32), "expected a full-rank (no pcakeep) sphere" + + assert np.abs(S - S.T).max() > 1e-3, ( + "do_approx_sphere=0 sphere is unexpectedly symmetric; this test would " + "not guard anything" + ) + + mean = real_data.mean(axis=1, keepdims=True) + Xc = real_data - mean + cov = (Xc @ Xc.T) / Xc.shape[1] + identity = np.eye(out.num_pcs) + + whitened = S @ cov @ S.T + max_err = np.abs(whitened - identity).max() + assert max_err < 1e-8, f"S @ cov @ S.T is not the identity: max err {max_err:.3e}" + + whitened_transposed = S.T @ cov @ S + max_err_t = np.abs(whitened_transposed - identity).max() + assert max_err_t > 1.0, ( + f"S.T @ cov @ S is unexpectedly close to the identity ({max_err_t:.3e}); " + "this check would not catch a transposed read" + ) + + @requires_binary def test_native_engine_param_aliases_and_multimodel(real_data): # n_models/n_mix aliases reach the Fortran num_models/num_mix_comps. Full data diff --git a/pamica/tests/test_sphere_export_order.py b/pamica/tests/test_sphere_export_order.py new file mode 100644 index 00000000..eb81fdca --- /dev/null +++ b/pamica/tests/test_sphere_export_order.py @@ -0,0 +1,182 @@ +"""Cross-backend round trip for the EEGLAB sphere export (issue #336). + +``write_amicaout`` writes the sphere matrix ``S`` column-major (Fortran +layout), matching the reference binary (amica15.f90 near :2423) and both +readers (EEGLAB's ``loadmodout15.m`` and pamica's ``loadmodout``). For the +default symmetric zero-phase component analysis (ZCA) sphere this was +invisible even under the old, wrong C-order write, because that sphere is its +own transpose to about 1e-17; with ``do_approx_sphere=False`` the sphere is +genuinely asymmetric, and a C-order write came back exactly transposed +(measured on the bundled sample, torch, 3 iterations: ``max|S_loaded - S| = +0.51``, ``max|S_loaded - S.T| = 0.0``). + +This module fits each backend briefly on the bundled real sample EEG, with +``do_approx_sphere`` both on and off and with and without rank reduction +(``pcakeep``), and checks that ``loadmodout`` (and, for the NumPy backend, +``load_results`` on its own output directory) returns exactly the sphere the +backend itself reports through ``get_sphere()``/``.sphere``. + +Real bundled sample EEG only, short fits (``.rules/testing.md``: no mocks, no +synthetic data). MLX is exercised through ``pytest.importorskip`` plus the +Apple-GPU guard, following ``pamica/tests/test_sensor_mixing_cross_backend.py``. +""" + +from pathlib import Path + +import numpy as np +import pytest + +from pamica import AMICA_NumPy +from pamica.numpy_impl.data import load_results +from pamica.numpy_impl.load import loadmodout +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +SEED = 42 +N_FRAMES = 8192 +MAX_ITER = 2 +BLOCK_SIZE = 4096 +PCAKEEP = 20 +CONFIGS = {"full": None, "reduced": PCAKEEP} + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +@pytest.fixture(scope="module") +def X() -> np.ndarray: + data = load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD) + return data.astype(np.float64)[:, :N_FRAMES] + + +def _mlx_class(): + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + if mlx_core.mx.default_device().type != mlx_core.mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + return mlx_core.AMICAMLXNG + + +def _fit_and_write( + backend: str, X: np.ndarray, do_approx_sphere: bool, pcakeep, outdir: Path +): + """Fit a short ``backend`` model and write its EEGLAB output to + ``outdir``. Returns ``(model, expected_sphere)``, where ``expected_sphere`` + is the array ``write_amica_output`` actually wrote to disk (so the + round-trip comparison below can be bit-exact), not necessarily what + ``get_sphere()`` reports -- see the MLX branch.""" + if backend == "numpy": + model = AMICA_NumPy( + use_tqdm=False, + seed=SEED, + max_iter=MAX_ITER, + block_size=BLOCK_SIZE, + pcakeep=pcakeep, + do_approx_sphere=do_approx_sphere, + outdir=str(outdir), + ) + model.fit(X) + assert model.converged, "fixture expects a converged (finite-LL) fit" + return model, model.sphere + + cls = AMICATorchNG if backend == "torch" else _mlx_class() + kwargs = {"device": "cpu"} if backend == "torch" else {} + data = X if backend == "torch" else X.astype(np.float32) + model = cls( + n_channels=NW, + seed=SEED, + block_size=BLOCK_SIZE, + pcakeep=pcakeep, + do_approx_sphere=do_approx_sphere, + **kwargs, + ) + model.fit(data, max_iter=MAX_ITER, verbose=False) + model.write_amica_output(outdir) + if backend == "mlx": + # write_amica_output writes np.array(self.sphere) (mlx_impl/core.py), + # the float32-cast sphere set in _preprocess; a float32->float64 upcast + # is exact (every float32 value is exactly representable in float64), + # so this is what loadmodout must reproduce bit-exactly, same as the + # other two backends. This is deliberately NOT get_sphere(), which + # returns the higher-precision float64 sphere computed just before + # that cast (_sphere_np) -- see the separate, explicitly-labeled + # float32-rounding assertion in the test below. + return model, np.array(model.sphere, dtype=np.float64) + return model, model.get_sphere() + + +@pytest.fixture(scope="module") +def fits(X, tmp_path_factory): + cache: dict = {} + + def get(backend: str, do_approx_sphere: bool, config: str): + key = (backend, do_approx_sphere, config) + if key not in cache: + outdir = tmp_path_factory.mktemp(f"{backend}_{do_approx_sphere}_{config}") + cache[key] = _fit_and_write( + backend, X, do_approx_sphere, CONFIGS[config], outdir + ) + (outdir,) + return cache[key] + + return get + + +@pytest.mark.parametrize("config", list(CONFIGS)) +@pytest.mark.parametrize("do_approx_sphere", [True, False]) +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_loadmodout_sphere_matches_backend(fits, backend, do_approx_sphere, config): + """``loadmodout`` reproduces the exact bytes ``write_amica_output`` wrote, + for every backend, bit-exactly (``assert_array_equal``) -- including MLX, + since the file holds a float32 sphere upcast to float64, and that upcast + is exact. + + The ``reduced`` (``pcakeep``) cells exercise the padded branch of + ``write_amicaout``, which was already column-major before issue #336's + fix (only the square branch was C order), so none of the six + ``reduced`` cells can fail on the old code; they stay in the matrix as a + same-shape regression guard, not because they demonstrate the fix. + ``mlx-True-full`` also cannot fail on the old code, for a different + reason: MLX's float32 cast rounds the default ZCA sphere's ~1e-17 + asymmetry to exactly zero, so a C-order read of a bit-symmetric matrix is + indistinguishable from a column-major one. The five cells that do fail + pre-fix are ``{torch,numpy}-{True,False}-full`` (the ZCA sphere is + symmetric only to ~1e-17, not bit-exact, so ``assert_array_equal`` still + catches the old C-order write even there) and ``mlx-False-full``. + """ + if backend == "mlx": + _mlx_class() + model, expected, outdir = fits(backend, do_approx_sphere, config) + + n = NW if CONFIGS[config] is None else PCAKEEP + assert expected.shape == (n, NW) + + if not do_approx_sphere and config == "full": + # Full-rank exact sphering is genuinely asymmetric -- exactly the case + # the old C-order write got wrong (it agreed with a symmetric sphere + # by coincidence). Guard that this test would actually catch a + # regression back to C order rather than passing vacuously. + assert np.abs(expected - expected.T).max() > 1e-3, ( + "sphere is unexpectedly symmetric here; this test would not guard anything" + ) + + out = loadmodout(outdir) + loaded = out.S[:n] + np.testing.assert_array_equal(loaded, expected) + + if backend == "mlx": + # Separate, explicitly-labeled relationship check: get_sphere() is not + # what got written to disk (see _fit_and_write's docstring), so it is + # not compared to `loaded` above. It differs from the exported sphere + # only by MLX's float32 rounding -- measured max_abs=1.4e-8, + # max_rel=5.9e-8 across do_approx_sphere x pcakeep on this fixture -- + # independent of the column-major bug this module guards (present + # equally for both do_approx_sphere values, before and after #336). + np.testing.assert_allclose(expected, model.get_sphere(), rtol=1e-6, atol=1e-7) + + if backend == "numpy": + r = load_results(outdir) + np.testing.assert_array_equal(r["sphere"], expected) diff --git a/pamica/tests/torch_tests/test_amica_ng_wrapper.py b/pamica/tests/torch_tests/test_amica_ng_wrapper.py index 2f78fd1e..74993036 100644 --- a/pamica/tests/torch_tests/test_amica_ng_wrapper.py +++ b/pamica/tests/torch_tests/test_amica_ng_wrapper.py @@ -167,18 +167,22 @@ def test_variance_order_requires_fit(): def test_write_amica_output_bytes(fitted_ng, tmp_path): """The written files are the model's exact float64 parameters: the on-disk - EEGLAB directory is a lossless serialization, not a lossy export (#92). W and - the symmetric sphere are byte-identical in C order; the non-square mixture - params and c/comp_list are column-major (Fortran layout), so read order="F". + EEGLAB directory is a lossless serialization, not a lossy export (#92). W is + byte-identical in C order; S is column-major (Fortran layout, issue #336 -- + the default zero-phase component analysis (ZCA) sphere is symmetric to + about 1e-17 so this was invisible before the fix); the non-square mixture + params and c/comp_list are column-major too, so read order="F". """ outdir = tmp_path / "amicaout" fitted_ng.write_amica_output(str(outdir)) ng = fitted_ng.model_ - for name, attr in [("gm", ng.gm), ("W", ng.W), ("S", ng.sphere), + for name, attr in [("gm", ng.gm), ("W", ng.W), ("mean", ng.mean)]: # fmt: skip got = np.fromfile(outdir / name).reshape(attr.shape) # C order np.testing.assert_array_equal(got, attr.cpu().numpy(), err_msg=name) + got_s = np.fromfile(outdir / "S").reshape(ng.sphere.shape, order="F") + np.testing.assert_array_equal(got_s, ng.sphere.cpu().numpy(), err_msg="S") for name, attr in [("c", ng.c), ("alpha", ng.alpha), ("mu", ng.mu), ("sbeta", ng.beta), ("rho", ng.rho)]: # fmt: skip got = np.fromfile(outdir / name).reshape(attr.shape, order="F") From 3627a6e2dd0361072ad9d212ee69dc33a923fd1b Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Tue, 22 Sep 2026 23:54:41 -0700 Subject: [PATCH 10/26] Phase 7: normalize component rows in doscaling (#340) * Add seeded native-binary oracle helper Writes a pamica state in the reference's load_* formats, runs the pinned v0.3.3 binary and reads its raw output arrays back, for element-wise oracle tests (issue #333). Used by the tests that follow. * Normalize component rows in doscaling Each model's stored A block is the transpose of the reference's mixing matrix, so a component is a block row. doscaling normalized stored columns, which is not a change of scale; torch, NumPy and MLX now rescale each component row with the matching mu/beta, exactly as amica15.f90:1843-1851 does (issue #333). Tested: invariance, cross-backend and doscaling=False byte-identity tests; seeded native oracle matches A/mu/sbeta to round-off. Tests that encoded the column rule are updated. * Retune overshoot recipes for the new trajectories With components rescaled, the aggressive-Newton recipes run monotone, so keep_best restores never fired (failures or silent skips). Add newtrate=3.0 and longer budgets where needed, move the pdftype=1 switch to kurt_start=6, pick n_mix=1 for the default min_dll stop, and check the keep_best recompute only after a restore. Tested: every retuned test passes and no longer skips. * Retune two MLX cross-backend test configs The variance-order guard rejected the 30-iteration fit (gap 5e-4), so run 50 (gap 2.4e-3). One two-model sample sat on the rejsig=2.0 threshold between float32 and float64; use rejsig=2.5. Tested: both tests pass for one and two models. * Count scalestep from 1 and validate it The reference parses scalestep but rescales every iteration. Keep it as a pamica extension counted from 1 (iterations s, 2s, ...), so the default 1 is the reference, and reject a non-integer or < 1 value at construction on every backend instead of dividing by zero mid-fit. Record it as row 13 of the differences page. Tested: cadence and validation tests on torch, NumPy and MLX. * Document component orientation in ADR 0006 ADR 0006 states the storage convention (components are rows of each stored block), the doscaling fix and the Phase 8 layout change, and amends ADR 0001; the issue #24 notes point to it. The changelog records the behavior change with the oracle numbers. * Make keep_best-under-reject tests non-vacuous Both the MLX and torch tests ran monotone fits, so final_ll_ equal to the last LL proved nothing. Use the overshoot recipe, require that it restores without do_reject and that the do_reject trajectory also ends below an earlier peak, then assert no restore. Tested: both tests pass; the old configs were monotone. * Check reject agreement up to float32 rounding Exact agreement of the rejected set is config luck: float32 and float64 per-sample LLs differ by up to ~0.1 by the second pass. Record each pass, require every disagreement to lie within the band measured on that pass, cap the band and the disagreement count, and add controls where a threshold or trajectory divergence fails. Tested: seeds 7 and 8, one and two models, at rejsig 2.0. * Note ADR 0003 figures predate the row rule * Rebuild paper.pdf * Derive MLX pin tolerances from float32 study * Check variance order against a float32 band * Band-check the pdftype1 reject test * Use semantic line breaks in ADR 0006 * Note Phase 8 aligns the initial A norm * Word pre-fix norm ranges as history * Note parity rows predate the row rule * Document when scalestep is validated * Say oracle maxima span backends and models * Mirror the zero-norm guard in Newton test * Test the rescale on Newton-reached states * Rescale a permuted comp_list across backends * Test zero- and NaN-norm rows stay untouched * Fail, not skip, on recipe preconditions * Never read stale or partial oracle output * Check out full history in pytest CI jobs * Fail byte-identity pins in CI, not skip * Use the CI-proven overshoot recipe in torch --------- Co-authored-by: github-actions[bot] --- .../0001-torch-backend-natural-gradient-em.md | 8 + .../decisions/0006-component-orientation.md | 98 +++ .context/decisions/README.md | 1 + .context/issue-24/drift_localization.md | 3 +- .context/issue-24/findings.md | 4 +- .github/workflows/ci.yml | 10 + .github/workflows/weekly-macos-slow.yml | 5 + docs/changelog.md | 26 + docs/guides/amica-differences.md | 4 +- docs/guides/validation.md | 6 +- pamica/mlx_impl/core.py | 98 ++- pamica/numpy_impl/core.py | 72 +- pamica/tests/mlx_tests/test_mlx_export.py | 14 +- pamica/tests/mlx_tests/test_mlx_fit_noop.py | 51 +- pamica/tests/mlx_tests/test_mlx_keepbest.py | 47 +- pamica/tests/mlx_tests/test_mlx_llt_stash.py | 17 +- pamica/tests/mlx_tests/test_mlx_mir.py | 23 +- pamica/tests/mlx_tests/test_mlx_reject.py | 65 +- pamica/tests/mlx_tests/test_mlx_transform.py | 154 ++-- pamica/tests/native_oracle.py | 274 +++++++ pamica/tests/test_doscaling_rows.py | 721 ++++++++++++++++++ pamica/tests/test_llt_stash.py | 25 +- .../tests/test_mlx_keepbest_cross_backend.py | 13 +- pamica/tests/test_mlx_reject_cross_backend.py | 259 +++++-- .../tests/test_mlx_transform_cross_backend.py | 186 ++++- .../torch_tests/test_amica_ng_wrapper.py | 38 +- pamica/tests/torch_tests/test_ng_backend.py | 107 ++- .../tests/torch_tests/test_ng_convergence.py | 116 +-- pamica/torch_impl/core.py | 71 +- paper.pdf | Bin 375736 -> 375736 bytes 30 files changed, 2146 insertions(+), 370 deletions(-) create mode 100644 .context/decisions/0006-component-orientation.md create mode 100644 pamica/tests/native_oracle.py create mode 100644 pamica/tests/test_doscaling_rows.py diff --git a/.context/decisions/0001-torch-backend-natural-gradient-em.md b/.context/decisions/0001-torch-backend-natural-gradient-em.md index dbcc6aa5..cad36b7e 100644 --- a/.context/decisions/0001-torch-backend-natural-gradient-em.md +++ b/.context/decisions/0001-torch-backend-natural-gradient-em.md @@ -72,3 +72,11 @@ is now the sole PyTorch backend, and the public `AMICA` interface wraps it direc (the `backend=` selector and the basic-backend-only `debug`/`output_dir` fit args were dropped). The Context/Alternatives sections above describe the pre-removal state and are retained as the historical record. + +## Addendum (2026-09-22, issue #333) + +The #24 fix left `A` stored with each model's block equal to the TRANSPOSE of the reference's per-model mixing matrix +(`get_mixing_matrix(h)` returns `A[:, comp_list[:, h]].T`), so a component is a ROW of its block, not a stored column. +[ADR 0006](0006-component-orientation.md) states that convention precisely, +records the `doscaling` defect it caused (stored columns were normalized; fixed in epic #324 Phase 7) +and the planned move to a component-row layout for `share_comps` (Phase 8, issue #334). diff --git a/.context/decisions/0006-component-orientation.md b/.context/decisions/0006-component-orientation.md new file mode 100644 index 00000000..97c7641f --- /dev/null +++ b/.context/decisions/0006-component-orientation.md @@ -0,0 +1,98 @@ +# ADR 0006: Components are rows of each stored mixing block + +**Status:** accepted +**Date:** 2026-09-22 +**Owner:** Seyed Yahya Shirazi + +Amends [ADR 0001](0001-torch-backend-natural-gradient-em.md), +which introduced the storage convention without naming its consequence for per-component operations. + +## Context + +Since issue #24 every array backend (PyTorch, NumPy, MLX) stores the mixing matrix `A` with shape `(n, n_comps)` and indexes each model's block by `comp_list`, +but each block holds the transpose of the reference's per-model mixing matrix: +`get_mixing_matrix(h)` returns `A[:, comp_list[:, h]].T`, which is the reference's `A(:, comp_list(:, h))`. +Source `i` of model `h` is therefore ROW `i` of the stored block `A[:, comp_list[:, h]]`, +while its density parameters live at `mu[:, comp_list[i, h]]`, `beta[:, comp_list[i, h]]` (the reference's `sbeta`), `alpha` and `rho`. +A stored COLUMN is not a component: it collects one sphered channel's loadings across the model's components. + +The reference's `doscaling` (amica15.f90:1843-1851) divides each component's mixing vector `A(:,k)` by its norm +and rescales `mu(:,k) *= norm`, `sbeta(:,k) /= norm`. +That is an exact change of scale: the source is multiplied by the norm, its density is rescaled to match, and the log-likelihood does not change. +Every backend instead normalized stored columns with that same compensation (issue #333), which is not a change of scale of any component. +A compensated rescale of stored row 5 changes the log-likelihood by exactly 0.0 on the sample EEG; +the same rescale of stored column 5 changes it by -3.9e-2. +Because `doscaling` is on by default, every default fit left the reference's trajectory from the first iteration: +seeded from the same initialization, the native binary and pamica differed by 7.2e-5 in `A` and 6.6e-5 in `mu` after one iteration, +while with `doscaling` off they agreed to 1e-15 and 1e-11. +Scale-blind endpoint metrics hid it (log-likelihood, Hungarian-matched correlation), +but fitted component norms ranged over [0.94, 1.07] in one-model fits and [0.05, 1.97] in two-model fits, +where the reference's are exactly 1. + +The same orientation error affects the `share_comps` metric and merge, which compare and tie stored columns (issue #334). + +## Decision + +Keep the storage convention for now and make every per-component operation act on block rows. +In this change (epic #324 Phase 7) `doscaling` normalizes, for every model `h` and source `i`, the row `A[i, comp_list[:, h]]`, +multiplies `mu[:, comp_list[i, h]]` by the row's norm and divides `beta[:, comp_list[i, h]]` by it, +leaving a zero-norm row untouched as the reference does. +The rule is identical in the three backends (`_rescale_components`) and runs where the reference runs it: +after the `A` update, before the unmixing matrices are rebuilt, and before a share merge. +The reference parses `scalestep` but never reads it and rescales every iteration; +pamica keeps `scalestep` as an extension, +now counted from 1 like the reference's other cadences (iterations `scalestep`, `2*scalestep`, ...), +so the default of 1 is the reference (row 14 of `docs/guides/amica-differences.md`). + +Epic #324 Phase 8 (issue #334) then changes the storage itself to component rows, +`A` of shape `(n_comps, n)` with `comp_list` indexing rows, +so a stored row is a component in every configuration, including merged ones. +Until then, a column merged by `share_comps` belongs to rows of several blocks, where the per-block rule is not a change of scale; +it is applied uniformly, model by model, and Phase 8 replaces it. + +## Consequences + +- Default trajectories change on every backend, toward the reference. + Seeded from pamica's initialization, `A`, `mu` and `sbeta` now match the native binary to float64 round-off after one and three natural-gradient iterations + (after one: `A` 5e-16, `mu` 8e-11, `sbeta` 1e-14; the column rule was 7e-5 off). + A 100-iteration Newton run against the seeded binary keeps its log-likelihood + (2.3e-4 to 2.1e-4 apart; the remaining gap is the Newton start, issue #335), + and the fitted component norms are exactly 1 instead of [0.94, 1.07]. + Two-model fits improve most: at 100 iterations the matched correlation with the seeded binary rises from 0.850 to 0.99998. +- Scale-blind results do not change materially: + log-likelihood, component directions, correlation and Amari distance against the bundled `amicaout` fixture move by about 1e-4. + Element-wise `A`, `mu` and `sbeta` now match the reference. +- `doscaling=False` is byte-identical to the previous code on every backend, pinned by an in-process comparison with the pre-fix classes. +- Saved models need no conversion: + the stored format is unchanged, and a model fitted before this change is still a valid model with non-unit component scales. + Refit only when comparing parameters element by element with the reference. +- Several test recipes that relied on a non-monotone trajectory (the `keep_best` overshoot, the `min_dll` stop) no longer overshoot at their old settings: + the column rule's perturbation was part of what made them overshoot. + They were retuned (see the Phase 7 pull request). + ADR 0003's `keep_best` variance figures were measured under the column rule and will be re-measured in the epic's final parity re-measurement. +- `scalestep > 1` now rescales on iterations `scalestep`, `2*scalestep`, ... instead of 1, `1+scalestep`, ...; + the default is unaffected. +- Remaining known difference: pamica does not normalize its drawn initial `A` + (the reference normalizes a drawn one, amica15.f90:818-819, but not a loaded one). + The two draws come from different random generators anyway, and the first iteration's rescale normalizes every component. + Epic #324 Phase 8 (issue #334) aligns it with the reference while it rewrites the storage layout and the initialization. + +## Alternatives considered + +- **Change the storage to component rows in the same change:** rejected for this phase. + The layout change touches the update, the accessors, persistence and the share code; + the rescale fix alone is small, + and landing it first gives Phase 8 a trajectory that is already reference-faithful to stay byte-identical to. +- **Keep the column rule and document it as a difference:** rejected. + It is not a change of scale, so it is not an alternative normalization but a perturbation of the fit, + and it made element-wise parity with the reference impossible. + +## Receipts + +- amica15.f90:1843-1851 (per-iteration `doscaling`), + :818-819 and :1039-1040 (normalization of a drawn initial and restart `A`), + :793-800 (a loaded `A` is used as is), + :3686-3688 (`scalestep` parsed, never read). +- Issues #333 (this change), #334 (Phase 8, component-row layout), #335 (Newton start), #24 (the storage convention), epic #324. +- `pamica/tests/test_doscaling_rows.py` (invariance, cross-backend, byte-identity and the gated native oracle), + `pamica/tests/native_oracle.py` (seeded reference runs). diff --git a/.context/decisions/README.md b/.context/decisions/README.md index 4ac81ef7..0995a247 100644 --- a/.context/decisions/README.md +++ b/.context/decisions/README.md @@ -30,3 +30,4 @@ Add new entries here as you create ADRs: - ADR 0003 - [Return the best-log-likelihood iterate from AMICATorchNG.fit](0003-best-iterate-safeguard.md) (accepted) - ADR 0004 - [Rank-deficient input handling and a relative eigenvalue floor](0004-rank-deficient-input-handling.md) (accepted) - ADR 0005 - [Restore the PCA residual in the MNE export](0005-restore-pca-residual-in-mne-export.md) (accepted) +- ADR 0006 - [Components are rows of each stored mixing block](0006-component-orientation.md) (accepted; amends ADR 0001) diff --git a/.context/issue-24/drift_localization.md b/.context/issue-24/drift_localization.md index 004458aa..ef6ff166 100644 --- a/.context/issue-24/drift_localization.md +++ b/.context/issue-24/drift_localization.md @@ -10,7 +10,8 @@ per-iteration M-step bug, not init-basin). This doc localizes the drifting term( The `-3.46` descent is a **transpose + multiply-side error in the natural-gradient A-update**, proven to machine precision and fixed. See `root_cause_Aupdate.py` (self-contained repro). -- The prototype stores `A` as **Fortran's A^T** (its "true unmixing = inv(A).T" convention). So +- The prototype stores `A` as **Fortran's A^T** (its "true unmixing = inv(A).T" convention; its + consequence, components are ROWS of each stored block, is ADR 0006, issue #333). So Fortran's step `A_fort <- A_fort - lr*A_fort@(I - G/dgm)` (G = g^T b) must become, transposed, `A <- A - lr*(I - G^T/dgm) @ A` (LEFT-multiply, TRANSPOSED direction). The prototype instead does `A <- A - lr*A @ (I - G/dgm)` (right-multiply, untransposed) -- wrong on **both** counts. diff --git a/.context/issue-24/findings.md b/.context/issue-24/findings.md index 3aca7475..d8e6403c 100644 --- a/.context/issue-24/findings.md +++ b/.context/issue-24/findings.md @@ -22,7 +22,9 @@ newt_ramp 10, newtrate 1.0, max_decs 3, ...) on CPU/float64. Two implementation facts made this work: - **Transpose:** the Python true unmixing is `inv(self.A).T` (= Fortran `W`), so the Fortran - mixing to load is `A_fortran = self.A.T`. + mixing to load is `A_fortran = self.A.T`. (2026-09-22: a component is therefore a ROW of + `self.A`'s block, not a column; see ADR 0006 and issue #333 for what that means for + `doscaling` and `share_comps`.) - **`load_sphere` is unusable** in the reference binary: its load path never allocates the `Stmp2` temp it later dereferences (amica17.f90:553) and segfaults. Worked around by NOT loading the sphere and letting Fortran compute its own -- it is the same symmetric ZCA diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4cdd3b5..7de0407d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,6 +121,11 @@ jobs: PYTORCH_ENABLE_MPS_FALLBACK: "1" steps: - uses: actions/checkout@v4 + # Full history: the byte-identity pins (test_doscaling_rows.py, + # mlx_tests/test_mlx_fit_noop.py) read older backend classes with + # `git show` and fail under CI when the commit is unreachable. + with: + fetch-depth: 0 - uses: astral-sh/setup-uv@v5 with: python-version: "3.12" @@ -190,6 +195,11 @@ jobs: PYTORCH_ENABLE_MPS_FALLBACK: "1" steps: - uses: actions/checkout@v4 + # Full history: the byte-identity pins (test_doscaling_rows.py, + # mlx_tests/test_mlx_fit_noop.py) read older backend classes with + # `git show` and fail under CI when the commit is unreachable. + with: + fetch-depth: 0 - uses: astral-sh/setup-uv@v5 with: python-version: "3.12" diff --git a/.github/workflows/weekly-macos-slow.yml b/.github/workflows/weekly-macos-slow.yml index 4e49d012..b7e25620 100644 --- a/.github/workflows/weekly-macos-slow.yml +++ b/.github/workflows/weekly-macos-slow.yml @@ -65,6 +65,11 @@ jobs: AMICA_RUN_FORTRAN: "1" steps: - uses: actions/checkout@v4 + # Full history: the byte-identity pins (test_doscaling_rows.py, + # mlx_tests/test_mlx_fit_noop.py) read older backend classes with + # `git show` and fail under CI when the commit is unreachable. + with: + fetch-depth: 0 - uses: astral-sh/setup-uv@v5 with: python-version: "3.12" diff --git a/docs/changelog.md b/docs/changelog.md index daae998f..68bf237e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,32 @@ Release notes are also published on the ## Unreleased +- **Phase 7 of epic #324: `doscaling` rescales components, as the reference does (issue #333).** + **Behavior change:** default fits on every backend (PyTorch, NumPy and MLX) now follow the reference's trajectory, + so their fitted parameters differ from those of earlier versions. + pamica stores each model's mixing block transposed relative to the reference (the issue #24 convention, now ADR 0006), + so a component is a row of the stored block, + but `doscaling` (on by default) normalized stored columns, which is not a change of scale of any component and perturbed every iteration. + It now divides each component's mixing vector by its norm and rescales that component's `mu` and `beta` to match, + an exact change of scale that leaves the log-likelihood unchanged. + - Seeded from pamica's initialization, `A`, `mu` and `sbeta` now match the native reference binary to float64 round-off + (after 1 iteration: `A` 5.0e-16, `mu` 7.8e-11, `sbeta` 1.1e-14, previously 7.2e-5, 6.6e-5 and 8.6e-5; + after 3: 2.7e-13, 8.6e-10 and 2.7e-11, previously 1.4e-3, 6.7e-3 and 2.2e-3). + - Fitted components now have unit norm, as in the reference; + previously, after 100 seeded iterations, norms ranged over [0.94, 1.07] with one model and [0.05, 1.97] with two. + - Scale-blind results barely move: against the bundled `amicaout` fixture after 200 iterations, + the log-likelihood goes from -3.401777 to -3.401673 (fixture: -3.401873), + the matched correlation from 0.99752 to 0.99740 and the Amari distance from 5.95e-3 to 6.14e-3. + Two-model fits improve most: after 100 seeded iterations, the matched correlation with the reference rises from 0.850 to 0.99998. + - `doscaling=False` is byte-identical to before on every backend. + Saved models load unchanged; refit only to compare parameters element by element with the reference. + - `scalestep`, which the reference parses but never reads (it rescales every iteration), stays a pamica extension + but now counts from 1: the rescale runs on iterations `scalestep`, `2*scalestep`, and so on, instead of 1, `1+scalestep`, and so on. + The default of 1 is unaffected (row 14 of the differences page). + With `doscaling` on, every backend's constructor now raises `ValueError` for a `scalestep` that is not an integer of at least 1; + `scalestep=0` used to fail mid-fit with a bare `ZeroDivisionError`. + - New test helper `pamica/tests/native_oracle.py` seeds the native binary from a pamica state through its `load_*` files, + for element-wise oracle tests (opt-in with `AMICA_RUN_FORTRAN=1`). - **Fix: the EEGLAB export wrote an asymmetric sphere transposed (Phase 10 of epic #324, issue #336).** `write_amicaout` (`pamica/numpy_impl/load.py`), the shared writer called from `write_amica_output` on `AMICATorchNG`, `AMICAMLXNG` and the `AMICA` wrapper, and from the NumPy backend's own `_write_results`, diff --git a/docs/guides/amica-differences.md b/docs/guides/amica-differences.md index b4ed4017..890d86b9 100644 --- a/docs/guides/amica-differences.md +++ b/docs/guides/amica-differences.md @@ -25,11 +25,13 @@ that is not listed, that is a bug worth | 11 | Reconstruction after rank reduction (`AMICAICA.apply`) | output has no representation of the discarded principal component analysis (PCA) subspace (sphere rows past `numeigs` are zero), so any back-projection drops it | the MNE export carries the full PCA basis, so `apply` restores the residual | MNE's own `ICA` does; the residual was never part of the independent component analysis (ICA) decomposition, so it is not ICA's to remove | `apply(..., n_pca_components=ica.n_components_)` | | 12 | `pcadb` | parsed (amica15.f90:3459-3461), never used | unset by default; when set alone, keeps the dimensions within `pcadb` dB of the largest eigenvalue; ignored when `pcakeep` is also set | a dB cut is a scale-free way to drop low-variance directions; letting `pcakeep` win preserves what a reference `input.param` that sets both (both bundled files do) means to the binary | leave `pcadb` unset (the default), or set `pcakeep` | | 13 | Preprocessing with `do_sphere=False` | divides each channel by its standard deviation and adds a log-determinant term to the likelihood (amica15.f90:516-526) | identity sphere with a zero log-determinant, on all three array backends: the data are fitted unscaled | not a deliberate choice: an existing divergence, found during epic #324 and not yet ported | none yet (issue #328) | +| 14 | `scalestep` | parsed (amica15.f90:3686), never used; rescales every iteration (:1843) | rescales every `scalestep` iterations counted from 1; default 1 matches the reference | pamica has always honored the keyword, and keeping it costs nothing; since issue #333 it counts from 1 like the reference's live cadences (`writestep`, `histstep`) instead of firing on the first iteration | `scalestep=1`, the default | Rows 1, 2 and 7 arrived with [ADR 0004](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0004-rank-deficient-input-handling.md); row 3 with ADR 0003; row 5 with issue #50, extended to the raw backends by issue #306; row 8 with issues #60 and #240; row 9 with issue #232; row 10 with issue #198; row 11 with issue #322 (ADR 0005); -row 12 with issue #323; row 13 is recorded, not yet resolved, by issue #328. +row 12 with issue #323; row 13 is recorded, not yet resolved, by issue #328; +row 14 with issue #333 (ADR 0006). Two `share_comps` details are pamica's own because the reference cannot decide them: the A-freeze window after a merge is anchored on `share_start` (the literal diff --git a/docs/guides/validation.md b/docs/guides/validation.md index b02fd531..7f882b58 100644 --- a/docs/guides/validation.md +++ b/docs/guides/validation.md @@ -22,7 +22,7 @@ Throughout, IC abbreviates independent component and LL log-likelihood. | Per-block sufficient statistics and one M-step | vs Fortran | bit-exact ($\sim\!10^{-15}$) | | Single-model solution (`do_newton=0`, $k\approx153$) | log-likelihood, component correlation vs Fortran | LL within ~0.0005 of $-3.6993$; correlation 0.998 | | Single-model solution (`do_newton=0`, bundled, $k\approx30$) | Amari distance vs Fortran | 0.006 | -| Every backend against the reference (harness defaults, bundled) | `validate_implementations.py --backend all`: PyTorch, NumPy and MLX each vs Fortran | LL within 3.2e-5; correlation 0.9992; Amari distance 0.004, for all three | +| Every backend against the reference (harness defaults, bundled) | `validate_implementations.py --backend all`: PyTorch, NumPy and MLX each vs Fortran | LL within 3.2e-5; correlation 0.9992; Amari distance 0.004, for all three (measured before issue #333's `doscaling` change; see [the per-backend rows](#parity-rows-per-backend)) | | Multi-model solution | distributional similarity over 20-run ensembles | indistinguishable from Fortran's own run-to-run spread ($p = 0.96$) | | Device and precision invariance | same independent components across CPU/CUDA/MPS/MLX, float32/float64, Linux/macOS | identical (1.000) across all eight torch/MLX combinations | | Cross-backend log-likelihood | converged LL across every backend | agree to ~3 significant digits (max pairwise ~0.003) | @@ -70,6 +70,10 @@ falling back, with a warning, to the bundled macOS x86_64 `amica15mac`, which ca ### Parity rows per backend +These rows predate epic #324 Phase 7 (issue #333), +which changed `doscaling` from normalizing stored columns to normalizing components, as the reference does; +they will be re-measured before the epic merges. + Measured on 2026-09-22 on an Apple M4 Pro (14 cores, 64 GB, macOS 27; MLX 0.32.0, PyTorch 2.12.1, NumPy 2.5.0) against the v0.3.3 release native engine (`amica15-macos-arm64`, SHA-256 `c8b2ac7f...`), with the harness defaults: diff --git a/pamica/mlx_impl/core.py b/pamica/mlx_impl/core.py index 73747838..40e979d8 100644 --- a/pamica/mlx_impl/core.py +++ b/pamica/mlx_impl/core.py @@ -91,6 +91,7 @@ import json import logging import math +import numbers import time import zipfile from typing import List, Optional, Sequence, Tuple @@ -554,6 +555,20 @@ class AMICAMLXNG: back to the input channels with :meth:`get_sensor_mixing_matrix`. ``fit(mir_step > 0)`` rejects an explicit reduction request up front, as AMICATorchNG does (see :meth:`_fit_once`). + + The rescale parameters (issue #333) likewise carry AMICATorchNG's names, + defaults, validation and semantics: + + ``doscaling`` (True) + Rescale each component's mixing vector (a row of its model's stored + ``A`` block) to unit norm, with the matching ``mu``/``beta`` rescale, + an exact change of scale (see :meth:`_rescale_components`). + ``scalestep`` (1) + Run the rescale on iterations ``scalestep``, ``2*scalestep``, ... + counted from 1; the default 1 rescales every iteration, as the + reference always does (it ignores ``scalestep``). Validated only when + ``doscaling`` is on (an integer >= 1, or the constructor raises + ``ValueError``); with ``doscaling`` off it is inert, never read. """ def __init__( @@ -744,6 +759,14 @@ def __init__( self.invsigmax = invsigmax self.doscaling = doscaling self.scalestep = scalestep + if doscaling and ( + isinstance(scalestep, bool) + or not isinstance(scalestep, numbers.Integral) + or scalestep < 1 + ): + # A zero cadence divides by zero mid-fit; a fractional one fires on + # no meaningful schedule. The reference never reads scalestep. + raise ValueError(f"scalestep must be an integer >= 1, got {scalestep!r}") # Component sharing (issue #263), same names/defaults/validation as # AMICATorchNG (torch_impl/core.py). OFF by default and inert for @@ -1721,12 +1744,12 @@ def _update_parameters(self, acc: dict, n_samples: int): # a SHARED column (#263) takes Fortran's responsibility-weighted average, # NOT a raw sum (a raw sum would over-step by the contributor count). A # merged-away column needs no special case: nothing scatters into it, so - # its zeta is 0 and its dAk is 0/tiny = 0, i.e. it takes no step. It is - # NOT a zero-norm column, and the rescale below does renormalize it like - # any other -- but by its own retained (already ~unit) norm, so that is a - # near-identity that perturbs it only at ULP scale. Hence - # test_merged_away_columns_keep_their_last_finite_value disables - # doscaling, to compare a frozen column exactly. + # its zeta is 0 and its dAk is 0/tiny = 0, i.e. it takes no step. The + # rescale below does not touch it either: it normalizes the rows of each + # model's block, and a merged-away column is in no model's block. + # (test_merged_away_columns_keep_their_last_finite_value disables + # doscaling; the column rule this replaced, issue #333, renormalized + # such a column at ULP scale.) # # The direction/dAk/gradient-norm computation below runs # UNCONDITIONALLY, not gated on _a_frozen(): Fortran computes dAk and @@ -1822,16 +1845,14 @@ def _update_parameters(self, acc: dict, n_samples: int): self.A = self.A - self.lrate * dAk - if self.doscaling and (self.iteration % self.scalestep == 0): - scale = mx.sqrt((self.A**2).sum(axis=0)) # (n_comps,) - # A zero-norm (collapsed) column is left untouched, not rescaled: - # safe_scale is 1 there, so A/beta are unchanged and mu*safe_scale - # keeps its prior value (matching AMICATorchNG's nonzero mask in - # _update_parameters -- using raw `scale` would zero mu instead). - safe_scale = mx.where(scale > 0, scale, mx.ones_like(scale)) - self.A = self.A / safe_scale - self.mu = self.mu * safe_scale - self.beta = self.beta / safe_scale + # The reference rescales every iteration (it parses ``scalestep`` but + # never reads it, amica15.f90:1843/3686); pamica keeps ``scalestep`` as + # an extension counted from 1 like the reference's other cadences + # (iterations s, 2s, ...), so the default 1 is the reference. The + # 1-based rule moves to ``schedule.every`` when epic #324 Phase 9 + # (issue #335) lands. + if self.doscaling and (self.iteration + 1) % self.scalestep == 0: + self._rescale_components() # rho is frozen for every non-GG family (self.dorho is False), so the # table it feeds (used only on the GG _log_pdf path) cannot have @@ -1845,6 +1866,51 @@ def _update_parameters(self, acc: dict, n_samples: int): self._refresh_lgamma_table() self._update_unmixing_matrices() + def _rescale_components(self) -> None: + """Rescale every component to a unit-norm mixing vector (Fortran + ``doscaling``, amica15.f90:1843-1851), an exact change of scale; the + same rule, order and zero-norm guard as + ``AMICATorchNG._rescale_components``. + + Source ``i`` of model ``h`` is ROW ``i`` of the stored block + ``A[:, comp_list[:, h]]`` (issue #24 convention), so that row is divided + by its norm while ``mu``/``beta`` at ``comp_list[i, h]`` are multiplied/ + divided by it, leaving the log-likelihood unchanged (the column rule + this replaced, issue #333, was not a change of scale). A zero-norm row + keeps its values: the scale is 1 there, so ``A``/``mu``/``beta`` are + unchanged rather than ``mu`` being zeroed. The new values are written + with ``mx.put_along_axis`` (an exact scatter), rebinding each attribute + to a new array as the rest of the M-step does, never mutating in place. + + Models are rescaled in order, one vectorized pass per model. Their + blocks are disjoint unless ``share_comps`` merged a column; a shared + stored column then belongs to rows of several blocks, where this + per-block rule is not well defined. It is applied uniformly anyway: the + component-row layout of epic #324 Phase 8 (issue #334) replaces it. + ADR 0006 records the convention. + """ + assert ( + self.A is not None + and self.mu is not None + and self.beta is not None + and self.comp_list is not None + ) + n = self.n_channels + for h in range(self.n_models): + idx = self.comp_list[:, h] + block = self.A[:, idx] # row i = source i of model h + norm = mx.sqrt((block**2).sum(axis=1)) # (n_channels,) + scale = mx.where(norm > 0, norm, mx.ones_like(norm)) + a_cols = mx.broadcast_to(idx[None, :], (n, n)) + self.A = mx.put_along_axis(self.A, a_cols, block / scale[:, None], axis=1) + d_cols = mx.broadcast_to(idx[None, :], (self.n_mix, n)) + self.mu = mx.put_along_axis( + self.mu, d_cols, self.mu[:, idx] * scale, axis=1 + ) + self.beta = mx.put_along_axis( + self.beta, d_cols, self.beta[:, idx] / scale, axis=1 + ) + # ------------------------------------------------------------------ # Adaptive PDF switch (issue #265; AMICATorchNG's #26 port) # ------------------------------------------------------------------ diff --git a/pamica/numpy_impl/core.py b/pamica/numpy_impl/core.py index 6a18ed40..9ef39baf 100644 --- a/pamica/numpy_impl/core.py +++ b/pamica/numpy_impl/core.py @@ -86,6 +86,7 @@ from scipy import linalg from scipy.special import digamma import logging +import numbers import time from pathlib import Path from typing import Dict, List, Optional, Tuple, Union @@ -304,6 +305,16 @@ def __init__( one WARNING, when ``do_sphere`` is False, as in the reference. See :mod:`pamica.rank`. + ``doscaling`` (True) and ``scalestep`` (1) carry AMICATorchNG's + names, defaults, validation and semantics (issue #333): each + component's mixing vector (a row of its model's stored ``A`` + block) is rescaled to unit norm, with the matching ``mu``/``beta`` + rescale, on iterations ``scalestep``, ``2*scalestep``, ... + counted from 1 (see :meth:`_rescale_components`). ``scalestep`` is + validated only when ``doscaling`` is on (an integer >= 1, or the + constructor raises ``ValueError``); with ``doscaling`` off it is + inert, never read. + ``outdir`` (None) is where the fit writes its ``out.txt`` log, its ``writestep`` checkpoints, its ``do_history`` snapshots and its final results, in the Fortran ``amicaout`` layout. The default @@ -427,6 +438,16 @@ def __init__( ) self.doscaling = params.get("doscaling", True) self.scalestep = params.get("scalestep", 1) + if self.doscaling and ( + isinstance(self.scalestep, bool) + or not isinstance(self.scalestep, numbers.Integral) + or self.scalestep < 1 + ): + # Same check and message as AMICATorchNG: a zero cadence divides by + # zero mid-fit; the reference never reads scalestep. + raise ValueError( + f"scalestep must be an integer >= 1, got {self.scalestep!r}" + ) self.do_sphere = params.get("do_sphere", True) self.do_mean = params.get("do_mean", True) self.do_approx_sphere = params.get("do_approx_sphere", True) @@ -2203,14 +2224,14 @@ def _update_parameters(self, updates: Dict): # (c was updated above, before the mixture/A updates, from dc_numer/dgm.) - # Rescale parameters if requested - if self.doscaling and self.iter % self.scalestep == 0: - for k in range(self.num_comps): - scale = np.sqrt(np.sum(self.A[:, k] ** 2)) - if scale > 0: - self.A[:, k] /= scale - self.mu[:, k] *= scale - self.beta[:, k] /= scale + # Rescale parameters if requested. The reference rescales every + # iteration (it parses ``scalestep`` but never reads it, + # amica15.f90:1843/3686); pamica keeps ``scalestep`` as an extension + # counted from 1 like the reference's other cadences (iterations s, 2s, + # ...), so the default 1 is the reference. The 1-based rule moves to + # ``schedule.every`` when epic #324 Phase 9 (issue #335) lands. + if self.doscaling and (self.iter + 1) % self.scalestep == 0: + self._rescale_components() # Update unmixing matrices self._update_unmixing_matrices() @@ -2224,6 +2245,41 @@ def _update_parameters(self, updates: Dict): # gates the separate final gradient-norm stop. self.nd.append(nd_value) + def _rescale_components(self) -> None: + """Rescale every component to a unit-norm mixing vector (Fortran + ``doscaling``, amica15.f90:1843-1851), an exact change of scale. + + Each model's stored block ``A[:, comp_list[:, h]]`` is the transpose of + the reference's per-model mixing matrix (issue #24 convention), so + source ``i`` of model ``h`` is ROW ``i`` of that block, the reference's + column ``A(:,k)``. Dividing that row by its norm scales source ``i`` up + by the norm; ``mu[:, comp_list[i, h]] *= norm`` and + ``beta[:, comp_list[i, h]] /= norm`` rescale its density to match, so + the log-likelihood is unchanged. Normalizing stored COLUMNS instead + (the rule before issue #333) is not a change of scale of any component + and perturbed the fit every iteration. A zero-norm row is left + untouched, as in the reference (``Anrmk > 0``). Same rule, order and + guard as ``AMICATorchNG._rescale_components``. + + Models are rescaled in order. Their blocks are disjoint unless + ``share_comps`` merged a column; a shared stored column then belongs to + rows of several blocks, where this per-block rule is not well defined. + It is applied uniformly anyway: the component-row layout of epic #324 + Phase 8 (issue #334) replaces it. ADR 0006 records the convention. + """ + assert self.A is not None and self.mu is not None and self.beta is not None + assert self.comp_list is not None + for h in range(self.num_models): + # comp_list[:, h] holds distinct indices within a model, so the + # fancy-index assignments below write every element exactly once. + idx = self.comp_list[:, h] + block = self.A[:, idx] # row i = source i of model h + norm = np.sqrt(np.sum(block**2, axis=1)) # (data_dim,) + scale = np.where(norm > 0, norm, 1.0) + self.A[:, idx] = block / scale[:, None] + self.mu[:, idx] = self.mu[:, idx] * scale + self.beta[:, idx] = self.beta[:, idx] / scale + def _a_frozen(self) -> bool: """Whether the A-update (and its lrate ramp) is held this iteration. diff --git a/pamica/tests/mlx_tests/test_mlx_export.py b/pamica/tests/mlx_tests/test_mlx_export.py index b8620030..c7ac173d 100644 --- a/pamica/tests/mlx_tests/test_mlx_export.py +++ b/pamica/tests/mlx_tests/test_mlx_export.py @@ -29,6 +29,7 @@ mx = pytest.importorskip("mlx.core") from pamica.mlx_impl import AMICAMLXNG # noqa: E402 (after the MLX importorskip) +from pamica.mlx_impl.core import _KEEP_BEST_TOL # noqa: E402 from pamica.numpy_impl.load import loadmodout # noqa: E402 SAMPLE_DIR = Path(__file__).resolve().parents[2] / "sample_data" @@ -150,18 +151,21 @@ def test_written_ll_ends_at_the_keep_best_restored_iterate(real_data): do_newton=True, newt_start=1, lrate=0.5, + newtrate=3.0, # overshoots since issue #333 (test_mlx_keepbest.py) use_min_dll=True, min_dll=1e-4, maxincs=2, use_grad_norm=False, ) m = AMICAMLXNG(n_channels=NW, **kwargs) - m.fit(real_data, max_iter=60, verbose=False) - if m.stop_reason in AMICAMLXNG._DEGENERATE_STOP_REASONS: - pytest.skip("aggressive run ended degenerate; not the case under test") + m.fit(real_data, max_iter=150, verbose=False) + assert m.stop_reason not in AMICAMLXNG._DEGENERATE_STOP_REASONS, ( + "the overshoot recipe ended degenerate: retune it" + ) assert m.final_ll_ is not None - if np.isclose(m.ll_history[-1], m.final_ll_): - pytest.skip("run was monotone; keep_best restore did not fire") + assert max(m.ll_history) - m.ll_history[-1] > _KEEP_BEST_TOL, ( + "the overshoot recipe no longer overshoots: retune it" + ) with tempfile.TemporaryDirectory() as d: m.write_amica_output(d) diff --git a/pamica/tests/mlx_tests/test_mlx_fit_noop.py b/pamica/tests/mlx_tests/test_mlx_fit_noop.py index 97c68544..4a3cf3dd 100644 --- a/pamica/tests/mlx_tests/test_mlx_fit_noop.py +++ b/pamica/tests/mlx_tests/test_mlx_fit_noop.py @@ -19,8 +19,16 @@ float32 is bit-reproducible run-to-run on ONE machine, so this needs no recorded constant at all, just the two classes agreeing with each other, here, now. + +Both fits run with ``doscaling=False``. Issue #333 (epic #324 Phase 7) +deliberately changed the rescale from stored columns to component rows, so a +``doscaling=True`` fit of the historical class no longer matches by design; +with the rescale off, every other part of the fit path is still compared bit +for bit against the pre-Phase-3 tip. The rescale itself is pinned by +``pamica/tests/test_doscaling_rows.py``. """ +import os import subprocess import sys import types @@ -75,14 +83,15 @@ def historical_amicamlxng(): this test module and removed afterward, so it cannot leak into any other test's import cache. - CI runs from a shallow (depth-1) checkout, where ``_EPIC_TIP`` is not - a reachable object and a bare ``git show`` fails with "bad object" -- - reproduced empirically on the macOS job. ``git fetch --depth 1`` that - one commit first (deepening the clone by exactly the object needed, - tolerating failure -- e.g. no network, or a remote that has since been - pruned) and only then read it; if the object is still unreachable, - skip loudly naming the shallow-clone cause rather than erroring the - whole module. + In a shallow (depth-1) clone ``_EPIC_TIP`` is not a reachable object + and a bare ``git show`` fails with "bad object" -- reproduced + empirically on the macOS job before the CI jobs checked out full + history (``fetch-depth: 0``). ``git fetch --depth 1`` that one commit + first (deepening the clone by exactly the object needed, tolerating + failure -- e.g. no network, or a remote that has since been pruned) and + only then read it; if the object is still unreachable, fail under CI + (the ``CI`` environment variable) and otherwise skip loudly naming the + shallow-clone cause rather than erroring the whole module. """ repo_root = Path(__file__).resolve().parents[3] subprocess.run( @@ -100,12 +109,18 @@ def historical_amicamlxng(): text=True, ) if result.returncode != 0: - pytest.skip( + reason = ( f"git object {_EPIC_TIP} is not reachable in this checkout " f"(likely a shallow/depth-1 clone that the fetch above could " f"not deepen -- e.g. no network or the remote history was " f"pruned); git show stderr: {result.stderr.strip()!r}" ) + # As in test_doscaling_rows.py: the CI jobs check out full history + # (fetch-depth: 0), so there an unreachable pin fails instead of + # silently dropping this regression guard. + if os.environ.get("CI"): + pytest.fail(reason) + pytest.skip(reason) module = types.ModuleType(_HISTORICAL_MODULE_NAME) module.__package__ = "pamica.mlx_impl" module.__name__ = _HISTORICAL_MODULE_NAME @@ -138,11 +153,16 @@ def historical_amicamlxng(): @pytest.mark.parametrize("n_models", [1, 2]) -def test_default_fit_is_bit_identical_to_the_pre_phase3_epic_tip( +def test_unscaled_fit_is_bit_identical_to_the_pre_phase3_epic_tip( real_data, historical_amicamlxng, n_models ): kwargs: dict[str, Any] = dict( - n_channels=NW, n_models=n_models, n_mix=NMIX, seed=42, block_size=BLOCK + n_channels=NW, + n_models=n_models, + n_mix=NMIX, + seed=42, + block_size=BLOCK, + doscaling=False, # the rescale changed deliberately (#333; docstring) ) old = historical_amicamlxng(**kwargs) @@ -162,13 +182,18 @@ def test_default_fit_is_bit_identical_to_the_pre_phase3_epic_tip( assert np.array_equal(a, b), f"{name}: diverged from the pre-phase-3 fit" -def test_default_fit_with_keep_best_off_is_also_bit_identical( +def test_unscaled_fit_with_keep_best_off_is_also_bit_identical( real_data, historical_amicamlxng ): """Same check with keep_best explicitly off, so the comparison does not depend on whichever safeguard branch a given seed happens to take.""" kwargs: dict[str, Any] = dict( - n_channels=NW, n_mix=NMIX, seed=7, block_size=BLOCK, keep_best=False + n_channels=NW, + n_mix=NMIX, + seed=7, + block_size=BLOCK, + keep_best=False, + doscaling=False, # the rescale changed deliberately (#333; docstring) ) old = historical_amicamlxng(**kwargs) old.fit(real_data, max_iter=8, verbose=False) diff --git a/pamica/tests/mlx_tests/test_mlx_keepbest.py b/pamica/tests/mlx_tests/test_mlx_keepbest.py index 5556f23d..ca971a6c 100644 --- a/pamica/tests/mlx_tests/test_mlx_keepbest.py +++ b/pamica/tests/mlx_tests/test_mlx_keepbest.py @@ -13,26 +13,33 @@ the PyTorch analogue -- MLX's float32 trajectory does not have to overshoot on the same config PyTorch's float64 one does): -* ``_FORCED_RESTORE_KWARGS`` (real EEG, first 4096 samples, ``max_iter=60``): +* ``_FORCED_RESTORE_KWARGS`` (real EEG, first 4096 samples, ``max_iter=150``): ``n_models=2, n_mix=3, seed=0, block_size=1024, do_newton=True, - newt_start=1, lrate=0.5, use_min_dll=True, min_dll=1e-4, maxincs=2, - use_grad_norm=False`` -- the same aggressive-Newton config + newt_start=1, lrate=0.5, newtrate=3.0, use_min_dll=True, min_dll=1e-4, + maxincs=2, use_grad_norm=False`` -- the same aggressive-Newton config ``test_ng_convergence.py::test_keep_best_restores_genuine_overshoot_under_min_dll_stop`` uses on PyTorch, and it reproduces a genuine overshoot here too: the fit - stops via ``min_dll`` at iteration 56 (57 recorded LLs), peaks at - ``ll_history[54]``, and ends ``best_ll - ll_history[-1] ~= 1.676e-4`` below + stops via ``min_dll`` at iteration 64 (65 recorded LLs), peaks at + ``ll_history[63]``, and ends ``best_ll - ll_history[-1] ~= 4.24e-4`` below that peak (measured on an Apple M4 Pro; the exact float32 value is machine-dependent, but the qualitative overshoot -- peak strictly above the - final two entries -- reproduces across seeds, see + final entry -- reproduces across seeds, see ``test_keep_best_restores_a_genuine_overshoot`` below for the live - measurement this module actually asserts against). + measurement this module actually asserts against). ``newtrate=3.0`` and the + longer budget date from issue #333: once ``doscaling`` rescaled components + instead of stored columns, the earlier ``newtrate=0.5``/60-iteration recipe + ran monotone to ``max_iter`` on every seed tried (0-11). * ``_FORCED_RESTORE_PDFTYPE1_KWARGS``: the ``pdftype=1``/``n_mix=1`` analogue - (seed 2, ``kurt_start=8``) chosen so the adaptive switcher's first kurtosis - re-evaluation (scheduled at iteration 7) fires strictly AFTER the peak - iterate (measured at iteration 5) -- the scenario + (seed 2, ``kurt_start=6``) chosen so the adaptive switcher's first kurtosis + re-evaluation (run at the end of iteration 5, after that iteration's E-step) + comes strictly AFTER the peak iterate (measured at iteration 5), while the + run lasts long enough for all five passes (it stops via ``min_dll`` at + iteration 12) -- the scenario ``test_pdftype1_forced_restore_rolls_back_n_kurt_done_with_pdtype`` needs to prove the restore rolls ``n_kurt_done`` back in step with ``pdtype``, not - just the floating-point arrays. + just the floating-point arrays. (``kurt_start=8`` served until issue #333; + the rescale fix moved that run's ``min_dll`` stop to iteration 8, after + only two passes.) The truncated-refit bit-identity check (both here and in the pdftype=1 test) uses ``max_iter=argmax`` where ``argmax = int(np.argmax(ll_history))``: a @@ -125,12 +132,13 @@ def _model(**kwargs: Any) -> AMICAMLXNG: do_newton=True, newt_start=1, lrate=0.5, + newtrate=3.0, use_min_dll=True, min_dll=1e-4, maxincs=2, use_grad_norm=False, ) -_FORCED_RESTORE_MAX_ITER = 60 +_FORCED_RESTORE_MAX_ITER = 150 # The pdftype=1 analogue, tuned so the first adaptive-switch pass lands after # the peak iterate (module docstring). @@ -147,7 +155,7 @@ def _model(**kwargs: Any) -> AMICAMLXNG: min_dll=1e-4, maxincs=2, use_grad_norm=False, - kurt_start=8, + kurt_start=6, num_kurt=5, kurt_int=1, ) @@ -356,12 +364,13 @@ def test_pdftype1_forced_restore_rolls_back_n_kurt_done_with_pdtype(real_data): which sources actually got switched (silent-failure risk this ports from ``AMICATorchNG``). - The recipe (module docstring) schedules the adaptive switcher's first - kurtosis pass at iteration 7, strictly after the measured peak at - iteration 5, so ``keep_best=False`` reaches ``n_kurt_done=5`` (all switch - passes ran) with a mixed pdtype, while the restored ``keep_best=True`` - model must be pinned at the pre-switch state: ``n_kurt_done=0`` and - ``pdtype`` unchanged from its all-super-Gaussian (code 1) init. + The recipe (module docstring) runs the adaptive switcher's first + kurtosis pass at the end of iteration 5, strictly after the E-step that + measures the peak at iteration 5, so ``keep_best=False`` reaches + ``n_kurt_done=5`` (all switch passes ran) with a mixed pdtype, while the + restored ``keep_best=True`` model must be pinned at the pre-switch state: + ``n_kurt_done=0`` and ``pdtype`` unchanged from its all-super-Gaussian + (code 1) init. """ x = real_data[:, :4096] on = _model(keep_best=True, **_FORCED_RESTORE_PDFTYPE1_KWARGS) diff --git a/pamica/tests/mlx_tests/test_mlx_llt_stash.py b/pamica/tests/mlx_tests/test_mlx_llt_stash.py index e9e29a38..942eaf96 100644 --- a/pamica/tests/mlx_tests/test_mlx_llt_stash.py +++ b/pamica/tests/mlx_tests/test_mlx_llt_stash.py @@ -24,6 +24,7 @@ mx = pytest.importorskip("mlx.core") from pamica.mlx_impl import AMICAMLXNG # noqa: E402 (after the MLX importorskip) +from pamica.mlx_impl.core import _KEEP_BEST_TOL # noqa: E402 SAMPLE_DIR = Path(__file__).resolve().parents[2] / "sample_data" DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" @@ -151,7 +152,8 @@ def test_llt_stash_is_none_before_any_estep_ran(): # Reuses the aggressive-Newton recipe test_mlx_keepbest.py measured to # genuinely overshoot on this backend (module docstring there): n_models=2, # seed=0, block_size=1024, do_newton=True, newt_start=1, lrate=0.5, -# use_min_dll=True, min_dll=1e-4, maxincs=2, use_grad_norm=False, max_iter=60. +# newtrate=3.0, use_min_dll=True, min_dll=1e-4, maxincs=2, +# use_grad_norm=False, max_iter=150 (newtrate and the budget since issue #333). _FORCED_RESTORE_KWARGS: dict[str, Any] = dict( n_models=2, n_mix=NMIX, @@ -160,6 +162,7 @@ def test_llt_stash_is_none_before_any_estep_ran(): do_newton=True, newt_start=1, lrate=0.5, + newtrate=3.0, use_min_dll=True, min_dll=1e-4, maxincs=2, @@ -173,12 +176,14 @@ def test_keep_best_restore_rolls_the_llt_stash_back(real_data): discarded last one (port of ``pamica/tests/test_llt_stash.py``'s torch-backend pin of the same name).""" m = AMICAMLXNG(n_channels=NW, **_FORCED_RESTORE_KWARGS) - m.fit(real_data, max_iter=60, verbose=False) - if m.stop_reason in AMICAMLXNG._DEGENERATE_STOP_REASONS: - pytest.skip("aggressive run ended degenerate; not the case under test") + m.fit(real_data, max_iter=150, verbose=False) + assert m.stop_reason not in AMICAMLXNG._DEGENERATE_STOP_REASONS, ( + "the overshoot recipe ended degenerate: retune it" + ) assert m.final_ll_ is not None - if np.isclose(m.ll_history[-1], m.final_ll_): - pytest.skip("run was monotone; keep_best restore did not fire") + assert max(m.ll_history) - m.ll_history[-1] > _KEEP_BEST_TOL, ( + "the overshoot recipe no longer overshoots: retune it" + ) assert m._llt_lt is not None and m._llt_lht is not None inv = _llt_invariant(m._llt_lt, m._llt_lt.size, NW) diff --git a/pamica/tests/mlx_tests/test_mlx_mir.py b/pamica/tests/mlx_tests/test_mlx_mir.py index 84764b72..1b561bf9 100644 --- a/pamica/tests/mlx_tests/test_mlx_mir.py +++ b/pamica/tests/mlx_tests/test_mlx_mir.py @@ -29,6 +29,7 @@ mx = pytest.importorskip("mlx.core") from pamica.mlx_impl import AMICAMLXNG # noqa: E402 (after the MLX importorskip) +from pamica.mlx_impl.core import _KEEP_BEST_TOL # noqa: E402 from pamica.metrics import mir as mir_metric # noqa: E402 from pamica.metrics import pairwise_mi # noqa: E402 @@ -358,28 +359,36 @@ def test_mir_history_survives_keep_best_restore(real_data): same forced-overshoot recipe as test_mlx_llt_stash.py, with mir_step=1 so a waypoint lands strictly inside the truncation window a buggy restore would damage (mirrors test_ng_convergence.py's - identically-named test and its mir_step=1 rationale).""" + identically-named test and its mir_step=1 rationale). + + Seed 1 rather than the recipe's seed 0 since issue #333: both overshoot, + but seed 0's restored iterate sits so close to its last one that their MIR + differ by only 4.9e-5 relative, under the 1e-4 float32 margin below; + seed 1's differ by 7.5e-4.""" m = AMICAMLXNG( n_channels=NW, n_models=2, n_mix=NMIX, - seed=0, + seed=1, block_size=BLOCK, do_newton=True, newt_start=1, lrate=0.5, + newtrate=3.0, # overshoots since issue #333 (test_mlx_keepbest.py) use_min_dll=True, min_dll=1e-4, maxincs=2, use_grad_norm=False, keep_best=True, ) - m.fit(real_data, max_iter=60, verbose=False, mir_step=1) - if m.stop_reason in AMICAMLXNG._DEGENERATE_STOP_REASONS: - pytest.skip("aggressive run ended degenerate; not the case under test") + m.fit(real_data, max_iter=150, verbose=False, mir_step=1) + assert m.stop_reason not in AMICAMLXNG._DEGENERATE_STOP_REASONS, ( + "the overshoot recipe ended degenerate: retune it" + ) assert m.final_ll_ is not None - if np.isclose(m.ll_history[-1], m.final_ll_): - pytest.skip("run was monotone; keep_best restore did not fire") + assert max(m.ll_history) - m.ll_history[-1] > _KEEP_BEST_TOL, ( + "the overshoot recipe no longer overshoots: retune it" + ) assert m.mir_history_, "test setup: mir_step recorded nothing" final_it = len(m.ll_history) - 1 diff --git a/pamica/tests/mlx_tests/test_mlx_reject.py b/pamica/tests/mlx_tests/test_mlx_reject.py index e27fc24d..cb9586a1 100644 --- a/pamica/tests/mlx_tests/test_mlx_reject.py +++ b/pamica/tests/mlx_tests/test_mlx_reject.py @@ -27,6 +27,7 @@ mx = pytest.importorskip("mlx.core") from pamica.mlx_impl import AMICAMLXNG # noqa: E402 (after the MLX importorskip) +from pamica.mlx_impl.core import _KEEP_BEST_TOL # noqa: E402 SAMPLE_DIR = Path(__file__).resolve().parents[2] / "sample_data" DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" @@ -229,29 +230,49 @@ def test_keep_best_inactive_reason_prefers_do_reject_when_both_are_on( assert "keep_best is inactive under share_comps" not in text +# The aggressive-Newton recipe that genuinely overshoots on this backend +# (test_mlx_keepbest.py's module docstring), plus a single rejection pass. +_OVERSHOOT_KWARGS: dict[str, Any] = dict( + n_models=2, + seed=0, + do_newton=True, + newt_start=1, + lrate=0.5, + newtrate=3.0, + use_min_dll=True, + min_dll=1e-4, + maxincs=2, + use_grad_norm=False, +) +_REJECT_KWARGS: dict[str, Any] = dict( + do_reject=True, rejsig=3.0, rejstart=5, rejint=5, maxrej=1 +) +_OVERSHOOT_MAX_ITER = 150 + + def test_keep_best_restore_never_fires_under_do_reject(real_data): - """Even the aggressive-Newton recipe known to overshoot under plain - keep_best (test_mlx_llt_stash.py) never restores when do_reject is on: - fit() always returns the last iterate there.""" - m = _model( - n_models=2, - seed=0, - do_newton=True, - newt_start=1, - lrate=0.5, - use_min_dll=True, - min_dll=1e-4, - maxincs=2, - use_grad_norm=False, - do_reject=True, - rejsig=3.0, - rejstart=5, - rejint=5, - maxrej=1, - ) - m.fit(real_data, max_iter=30, verbose=False) - if m.stop_reason in AMICAMLXNG._DEGENERATE_STOP_REASONS: - pytest.skip("run ended degenerate; not the case under test") + """A recipe that overshoots its own peak never restores when do_reject is + on: fit() returns the last iterate there. + + Non-vacuous on both counts, measured on an Apple M4 Pro. Without + do_reject the same fit restores (peak at iteration 63, stop at 64, 4.2e-4 + below the peak). With do_reject its own trajectory also ends below an + earlier peak (iteration 36, stop at 37, 2.0e-3 below), so a restore would + fire if the safeguard were active, and it does not. The previous version + (``newtrate`` 0.5, 30 iterations) ran monotone once issue #333 changed + ``doscaling`` to rescale components, so it could not have failed. + """ + plain = _model(keep_best=True, **_OVERSHOOT_KWARGS) + plain.fit(real_data, max_iter=_OVERSHOOT_MAX_ITER, verbose=False) + assert plain.stop_reason not in AMICAMLXNG._DEGENERATE_STOP_REASONS + assert max(plain.ll_history) - plain.ll_history[-1] > _KEEP_BEST_TOL + assert plain.final_ll_ == max(plain.ll_history) > plain.ll_history[-1] + + m = _model(keep_best=True, **_OVERSHOOT_KWARGS, **_REJECT_KWARGS) + m.fit(real_data, max_iter=_OVERSHOOT_MAX_ITER, verbose=False) + assert m.stop_reason not in AMICAMLXNG._DEGENERATE_STOP_REASONS + assert m.numrej >= 1 # rejection fired, so keep_best is inactive + assert max(m.ll_history) - m.ll_history[-1] > _KEEP_BEST_TOL assert m.final_ll_ == m.ll_history[-1] diff --git a/pamica/tests/mlx_tests/test_mlx_transform.py b/pamica/tests/mlx_tests/test_mlx_transform.py index cd8993ce..6cce089f 100644 --- a/pamica/tests/mlx_tests/test_mlx_transform.py +++ b/pamica/tests/mlx_tests/test_mlx_transform.py @@ -278,74 +278,93 @@ def test_get_rho_raises_on_force_set_nan_rho(): # # This test is the LOOSER canary that survives CI running a different Apple # GPU model: MLX float32 is bit-reproducible on one machine but not across -# GPU models (observed on CI: ll_history[0] -3.332018613 vs this machine's -# -3.332018852, a ~7e-8 relative difference -- not a fit-path regression). -# _REL_TOL (5e-6) is calibrated between that noise floor and a real change: -# ~50x the observed ~1e-7 cross-GPU spread, comfortably below the ~1e-6+ -# trajectory shift a genuine fit-path change produces on this same benchmark -# (issue #216's block_size default change alone shifted the trajectory -# ~1e-6, "inside parity tolerance" -- AGENTS.md). stop_reason and -# len(ll_history) are asserted exactly: they are a string and an int, so -# they carry no floating-point cross-machine risk at all. +# GPU models, whose kernels accumulate in a different order. stop_reason and +# len(ll_history) are asserted exactly: they are a string and an int, so they +# carry no floating-point cross-machine risk at all. +# +# Re-recorded on the same machine for issue #333 (epic #324 Phase 7), which +# deliberately changed doscaling from normalizing stored columns to +# normalizing components (rows of each model's block), as the reference does. +# The pre-#333 recording (reproduced exactly on this machine before the change) +# ended at final_ll_ -3.25075626373291 with A[0,0] 0.8831924, A[5,5] +# 0.95371497, A[10,20] -0.0037135077, A[31,31] 0.99918866, A[0,31] +# 0.032124873; ll_history[0] is unchanged (it is computed before the first +# rescale). _NOOP_PIN_LL_HISTORY = [ -3.3320186138153076, - -3.2827978134155273, - -3.2742865085601807, - -3.2692978382110596, - -3.26499080657959, - -3.2611570358276367, - -3.2578959465026855, - -3.2551658153533936, - -3.2528350353240967, - -3.25075626373291, + -3.282797336578369, + -3.2743287086486816, + -3.269350528717041, + -3.2649497985839844, + -3.260986089706421, + -3.2576255798339844, + -3.254916191101074, + -3.252664566040039, + -3.2506353855133057, ] -_NOOP_PIN_FINAL_LL = -3.25075626373291 +_NOOP_PIN_FINAL_LL = -3.2506353855133057 _NOOP_PIN_STOP_REASON = "max_iter" # A handful of representative A entries (two diagonal, two off-diagonal, one # corner), replacing the previous SHA-256 hash of the full A/W arrays: a # cross-machine hash can never match (any per-entry float32 noise flips it), -# but these entries under the same relative tolerance still catch a real -# fit-path change while surviving cross-GPU float32 noise. +# but these entries still catch a real fit-path change within tolerances that +# survive cross-GPU float32 noise. _NOOP_PIN_A_ENTRIES = { - (0, 0): 0.8831924, - (5, 5): 0.95371497, - (10, 20): -0.0037135077, - (31, 31): 0.99918866, - (0, 31): 0.032124873, + (0, 0): 0.9853508, + (5, 5): 0.99289405, + (10, 20): -0.0037210553, + (31, 31): 0.99681115, + (0, 31): 0.0327551, +} + +# Tolerances from a float32-rounding perturbation study, not hand-picked. The +# same fit ran on 48 copies of the input, each element multiplied by +# (1 + eps * z) with eps the float32 machine epsilon (1.19e-7) and z standard +# normal (numpy default_rng(20260922)): the scale of the accumulation-order +# differences between GPU models. +# Recorded below is the largest deviation from the unperturbed fit over those +# draws (Apple M4 Pro); each tolerance is _PIN_SAFETY times it. Both +# cross-GPU deviations seen on CI sat inside the study's maxima: ll_history[0] +# at ~7e-8 relative (study 7.2e-8) and A[0,0] at 9.2e-6 (study 1.9e-5). The +# earlier flat 5e-6 relative tolerance had no margin (the study's largest +# log-likelihood deviation is 5.0e-6) and failed on CI at A[0,0]. +# +# The canary still catches a real fit-path change. Issue #333's doscaling +# change moved A[0,0] by 1.0e-1 (1800x its tolerance), A[5,5] by 3.9e-2 +# (8800x), A[31,31] by 2.4e-3 (530x), A[0,31] by 6.3e-4 (11x), and +# ll_history[2] and [3] by 1.3e-5 and 1.6e-5 relative (15x and 12x). The +# discriminating power therefore rests on the three diagonal entries: A[10,20] +# (moved 7.5e-6, 1.3x) and ll_history[0] and [1] (unchanged or under 1x) sit +# inside float32 noise for that change and are kept as regression guards only. +_PIN_SAFETY = 3.0 +_STUDY_MAX_REL_DLL = [ + 7.16e-08, + 1.38e-06, + 2.91e-07, + 4.38e-07, + 2.63e-06, + 4.09e-06, + 4.98e-06, + 3.66e-06, + 3.23e-06, + 4.11e-06, +] +_STUDY_MAX_REL_DFINAL = 4.11e-06 +_STUDY_MAX_DA = { + (0, 0): 1.87e-05, + (5, 5): 1.49e-06, + (10, 20): 1.90e-06, + (31, 31): 1.49e-06, + (0, 31): 1.87e-05, } -# Recorded max(|A|) from the same run (A columns are ~unit-normalized by -# construction, so this sits near 1.0 regardless of seed/config). A[10, 20] -# above (-0.0037) is itself near zero, so a PER-ENTRY relative bound would -# collapse to an absolute tolerance of ~1.9e-8 there -- tighter than the -# ~1.1e-7 cross-GPU float32 noise CI actually observed, and exactly the -# near-zero-entry failure mode _max_rel_disagreement's docstring in -# test_mlx_transform_cross_backend.py explains for transform's output. Scale -# by this matrix-wide max instead, the same fix that function applies. -_NOOP_PIN_A_MAXABS = 0.99975544 -_REL_TOL = 5e-6 - - -def _assert_relclose(actual: float, expected: float, *, label: str) -> None: - diff = abs(actual - expected) - tol = _REL_TOL * abs(expected) - assert diff <= tol, ( - f"{label}: {actual!r} differs from the recorded {expected!r} by " - f"{diff:.3e} (relative {diff / abs(expected):.3e}), over the " - f"{_REL_TOL:.0e} cross-GPU tolerance" - ) -def _assert_matrix_scale_close(actual: float, expected: float, *, label: str) -> None: - """Like :func:`_assert_relclose`, but scaled by the matrix-wide - ``_NOOP_PIN_A_MAXABS`` rather than by ``expected`` itself -- appropriate - for a single entry of a matrix whose entries individually pass through - near zero, per the module-level comment above.""" +def _assert_within(actual: float, expected: float, tol: float, *, label: str) -> None: diff = abs(actual - expected) - tol = _REL_TOL * _NOOP_PIN_A_MAXABS assert diff <= tol, ( f"{label}: {actual!r} differs from the recorded {expected!r} by " - f"{diff:.3e}, over the {tol:.3e} matrix-scaled tolerance " - f"({_REL_TOL:.0e} x max|A|={_NOOP_PIN_A_MAXABS})" + f"{diff:.3e}, over its {tol:.3e} tolerance ({_PIN_SAFETY:g} x the " + "float32-rounding study's largest deviation)" ) @@ -360,15 +379,11 @@ def test_fit_path_is_unchanged_by_phase1(): Exact equality does not survive a different Apple GPU model, though (MLX float32 is bit-reproducible on one machine, not across models), so this CI-facing version checks agreement with the recorded M4 Pro values - instead: ``ll_history``/``final_ll_`` per-entry relative (safe -- their - magnitude, ~3.3, stays well clear of zero), the five ``A`` spot entries - scaled by the matrix-wide ``max|A|`` instead (``A[10, 20]`` is itself - near zero, so a per-entry relative bound there would be tighter than the - observed cross-GPU noise -- see the module-level comment). Both are - loose enough to survive cross-GPU float32 noise (~1e-7), tight enough to - still catch a genuine fit-path regression (~1e-6+, see the module-level - comment). ``stop_reason`` and the trajectory length are still exact -- - both machine-independent. + instead, each entry within _PIN_SAFETY times the largest deviation a + float32-rounding perturbation of the input produced (the study in the + module-level comment): ``ll_history``/``final_ll_`` relative, the five + ``A`` spot entries absolute. ``stop_reason`` and the trajectory length + are still exact -- both machine-independent. """ m = AMICAMLXNG(n_channels=NW, n_mix=NMIX, seed=SEED, block_size=BLOCK) m.fit(_real_data(4096), max_iter=10, verbose=False) @@ -376,11 +391,16 @@ def test_fit_path_is_unchanged_by_phase1(): assert m.stop_reason == _NOOP_PIN_STOP_REASON assert len(m.ll_history) == len(_NOOP_PIN_LL_HISTORY) - for it, (actual, expected) in enumerate(zip(m.ll_history, _NOOP_PIN_LL_HISTORY)): - _assert_relclose(actual, expected, label=f"ll_history[{it}]") + for it, (actual, expected, dev) in enumerate( + zip(m.ll_history, _NOOP_PIN_LL_HISTORY, _STUDY_MAX_REL_DLL) + ): + tol = _PIN_SAFETY * dev * abs(expected) + _assert_within(actual, expected, tol, label=f"ll_history[{it}]") assert m.final_ll_ is not None - _assert_relclose(m.final_ll_, _NOOP_PIN_FINAL_LL, label="final_ll_") + tol = _PIN_SAFETY * _STUDY_MAX_REL_DFINAL * abs(_NOOP_PIN_FINAL_LL) + _assert_within(m.final_ll_, _NOOP_PIN_FINAL_LL, tol, label="final_ll_") a = np.array(m.A, dtype=np.float32) for (i, j), expected in _NOOP_PIN_A_ENTRIES.items(): - _assert_matrix_scale_close(float(a[i, j]), expected, label=f"A[{i},{j}]") + tol = _PIN_SAFETY * _STUDY_MAX_DA[(i, j)] + _assert_within(float(a[i, j]), expected, tol, label=f"A[{i},{j}]") diff --git a/pamica/tests/native_oracle.py b/pamica/tests/native_oracle.py new file mode 100644 index 00000000..ab495406 --- /dev/null +++ b/pamica/tests/native_oracle.py @@ -0,0 +1,274 @@ +"""Seed the native reference binary from a pamica state and read back its raw output. + +A bit-level oracle for the M-step: pamica's own initialization (or any state +reached from it) is written in the reference's ``load_*`` file formats, the +binary runs a few iterations from exactly that state, and its raw output arrays +are read back without the variance ordering or normalization that ``loadmodout`` +applies. Any per-iteration difference then shows up element by element instead +of hiding behind scale-blind endpoint metrics (issue #333, epic #324 Phase 7). + +Not a test module (no ``test_`` prefix, so pytest does not collect it). Callers +gate on ``AMICA_RUN_FORTRAN=1`` like the other binary-driven tests. + +Reference facts this relies on (``amica15.f90``): + +* Layout: the reference mixing matrix ``A(nw, num_comps)`` holds source ``i`` of + model ``h`` in column ``comp_list(i, h)``. pamica stores each model's block + transposed (issue #24 convention, ADR 0006), so the reference block is the + transpose of pamica's stored block. +* Every ``load_*`` file is a raw little-endian float64 array in column-major + order under ``indir``. A loaded ``A`` is used as is (no normalization, + :793-800), unlike a drawn one (:818-819). +* ``load_sphere`` is not used: its load path dereferences an unallocated + temporary and crashes (``.context/issue-24/findings.md``). The binary computes + its own sphere, the same symmetric zero-phase component analysis (ZCA) sphere + pamica builds from the same data, so ``S`` is returned for the caller to check. +* ``load_comp_list`` reads the file named ``c``, not ``comp_list`` (:831, a + reference bug), so a comp_list seed and a ``c`` seed cannot be combined. With + ``comp_list`` given, :func:`run_seeded_reference` writes it to ``c`` and leaves + the bias at the binary's own zero initialization, which requires a zero ``c``. +""" + +from __future__ import annotations + +import os +import re +import shutil +import subprocess +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + +import numpy as np + +from pamica.native import resolver +from pamica.native.engine import _DEFAULT_PARAMS, _render_param + +# The release whose native binary the seeded oracles run (resolver cache, else a +# verified download); PAMICA_NATIVE_BINARY overrides it with a local build. +REFERENCE_VERSION = "v0.3.3" + +# Every output file :func:`run_seeded_reference` reads back. +_OUTPUT_FILES = ("A", "mu", "sbeta", "rho", "alpha", "gm", "c", "comp_list", "LL", "S") +# stdout lines of a run that did not carry the seeded state to the end +# (amica15.f90): an "Error: ..." before a bare ``stop``, which exits with +# status 0 (:264, :2452, :3077, ...), a NaN exit (:1054), or a NaN restart, +# which draws a fresh ``A`` and so discards the seed (:1021-1046, printed as +# "Reinitializaing"). A gfortran runtime error goes to stderr. +_STDOUT_FAILURE = re.compile(r"\berror\b|Got NaN|Reinitiali", re.IGNORECASE) +_STDERR_FAILURE = re.compile(r"Fortran runtime error", re.IGNORECASE) + + +@dataclass(frozen=True) +class SeedState: + """A model state in the reference's layout, ready for the ``load_*`` files. + + ``A`` is ``(nw, num_comps)`` with source ``i`` of model ``h`` in column + ``comp_list[i, h]``; ``mu``/``sbeta``/``rho``/``alpha`` are + ``(num_mix, num_comps)``, ``gm`` is ``(num_models,)``, ``c`` is + ``(nw, num_models)`` and ``mean`` is ``(nx,)``. ``comp_list`` is 0-based + ``(nw, num_models)``; ``None`` means the reference's default + ``comp_list(i, h) = (h-1)*nw + i``. + """ + + A: np.ndarray + mean: np.ndarray + mu: np.ndarray + sbeta: np.ndarray + rho: np.ndarray + alpha: np.ndarray + gm: np.ndarray + c: np.ndarray + comp_list: Optional[np.ndarray] = None + + +@dataclass(frozen=True) +class ReferenceOutput: + """The binary's raw output arrays, in the reference layout (see + :class:`SeedState`), plus the per-iteration ``LL`` and its sphere ``S``.""" + + A: np.ndarray + mu: np.ndarray + sbeta: np.ndarray + rho: np.ndarray + alpha: np.ndarray + gm: np.ndarray + c: np.ndarray + comp_list: np.ndarray + LL: np.ndarray + S: np.ndarray + stdout: str + + +def reference_mixing(A_stored: np.ndarray, comp_list: np.ndarray) -> np.ndarray: + """pamica's stored ``A`` in the reference layout: block ``h`` transposed. + + Only defined for a disjoint ``comp_list`` (no shared columns): in pamica's + current layout a merged column belongs to rows of several blocks, which has + no reference counterpart (issue #334). + """ + if np.unique(comp_list).size != comp_list.size: + raise ValueError("reference_mixing needs a comp_list without shared columns") + A_ref = np.empty_like(A_stored) + for h in range(comp_list.shape[1]): + idx = comp_list[:, h] + A_ref[:, idx] = A_stored[:, idx].T + return A_ref + + +def seed_from_torch(model) -> SeedState: + """The reference-layout state of an initialized (or fitted) ``AMICATorchNG``.""" + comp_list = model.comp_list.cpu().numpy() + return SeedState( + A=reference_mixing(model.A.cpu().numpy(), comp_list), + mean=model.mean.cpu().numpy().reshape(-1), + mu=model.mu.cpu().numpy(), + sbeta=model.beta.cpu().numpy(), + rho=model.rho.cpu().numpy(), + alpha=model.alpha.cpu().numpy(), + gm=model.gm.cpu().numpy(), + c=model.c.cpu().numpy(), + ) + + +def _write_f64(path: Path, arr: np.ndarray) -> None: + np.asarray(arr, dtype=" np.ndarray: + raw = np.fromfile(path, dtype=" ReferenceOutput: + """Run the reference binary for ``max_iter`` iterations from ``state``. + + ``data_file`` is the raw float32 column-major recording + (``nx x n_samples``) the pamica side fitted; it is linked into ``workdir``. + ``params`` override the engine defaults by Fortran name (``do_newton``, + ``doscaling``, ``block_size``, ...). + + A reused ``workdir`` is safe: its ``out`` and ``init`` directories are + removed first, so every file read back was written by this run. Raises + ``RuntimeError`` with the tails of the binary's stdout and stderr if it + exits nonzero, prints a failure line (see ``_STDOUT_FAILURE``), or leaves + any output file this helper reads unwritten. + """ + nw, num_comps = state.A.shape + num_mix = state.mu.shape[0] + num_models = state.gm.shape[0] + nx = state.mean.shape[0] + if nx != nw: + raise ValueError("seeded reference runs are full rank (nx == nw) only") + + workdir.mkdir(parents=True, exist_ok=True) + indir = workdir / "init" + out = workdir / "out" + # Old output or seed files from an earlier run must never be read back; + # the binary creates ``out`` itself (amica15.f90:82). + for stale in (out, indir): + if stale.exists(): + shutil.rmtree(stale) + indir.mkdir() + _write_f64(indir / "A", state.A) + _write_f64(indir / "mean", state.mean) + _write_f64(indir / "mu", state.mu) + _write_f64(indir / "sbeta", state.sbeta) + _write_f64(indir / "rho", state.rho) + _write_f64(indir / "alpha", state.alpha) + _write_f64(indir / "gm", state.gm) + if state.comp_list is None: + _write_f64(indir / "c", state.c) + load = {"load_c": 1, "load_comp_list": 0} + else: + if np.any(state.c != 0.0): + raise ValueError( + "load_comp_list reads the file 'c' (amica15.f90:831), so a " + "comp_list seed needs c == 0 (the binary's own initialization)" + ) + # Integer comp_list, 1-based, in a record of 2*nbyte*nw*num_models bytes. + rec = np.zeros(2 * nw * num_models, dtype="= 1 while the rescale is on; +5. (opt-in, ``AMICA_RUN_FORTRAN=1``) the native reference binary, seeded from + pamica's initialization, matches pamica's ``A``/``mu``/``sbeta`` after one + and three iterations to float64 round-off. + +Real bundled sample EEG only, no synthetic data or mocks (``.rules/testing.md``). +""" + +from __future__ import annotations + +import dataclasses +import os +import subprocess +import sys +import tempfile +import types +from pathlib import Path +from typing import Any, Dict, Tuple + +import numpy as np +import pytest +import torch +from scipy.special import logsumexp + +from pamica import AMICA_NumPy +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +NMIX = 3 +SEED = 42 +# The fitting slice for the always-on tests: long enough for real dynamics, +# short enough that a few iterations on every backend stay fast. +N_SLICE = 8192 + +# A float64 log-likelihood (per sample and channel, about -3.4) changed by a +# pure rescale moves by round-off only: measured 0 to 4.4e-16 (1 ULP) on the +# sample. The float32 MLX likelihood moves by float32 round-off: measured +# 3.5e-9 to 6e-8 (a float32 ULP at 3.4 is 2.4e-7). +LL_TOL_F64 = 1e-13 +LL_TOL_F32 = 1e-6 +# The old stored-column rule on the same 3-iteration states moved the +# log-likelihood by 1.0e-4 to 1.1e-4 (natural gradient) and 2.1e-4 to 2.2e-4 +# (with Newton steps), a compensated rescale of the wrong vectors: at least ten +# times this bound and a hundred times the float32 tolerance. +COLUMN_RULE_MIN_DLL = 1e-5 + +# A real state reached WITH Newton steps: Newton from the second iteration +# (0-based iteration 1). A positive-definite Newton step ramps lrate past the +# natural-gradient ceiling (the lrate default, 0.1), which the setup asserts; +# measured on every backend, one and two models: 0.3 after 3 iterations. +NEWTON: Dict[str, Any] = dict(do_newton=True, newt_start=1) +NG_LRATE_CEILING = 0.1 + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +@pytest.fixture(scope="module") +def real_slice() -> np.ndarray: + X = load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD) + return X.astype(np.float64)[:, :N_SLICE] + + +def _mlx_core(): + """The MLX backend module, or skip this test (never the whole module).""" + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + mx = mlx_core.mx + if mx.default_device().type != mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + return mlx_core + + +def _row_norm_dev(A: np.ndarray, comp_list: np.ndarray) -> float: + """Largest ``| ||row i of block h|| - 1 |`` over every component.""" + norms = [ + np.linalg.norm(A[:, comp_list[:, h]], axis=1) for h in range(comp_list.shape[1]) + ] + return float(np.abs(np.concatenate(norms) - 1.0).max()) + + +def _mean_ll(lht: np.ndarray, n_channels: int) -> float: + """Per-sample-per-channel log-likelihood from ``Lht`` (models x samples).""" + lht = np.asarray(lht, dtype=np.float64) + return float(logsumexp(lht, axis=0).sum() / (lht.shape[1] * n_channels)) + + +# --- 1. one pass is an exact change of scale, per backend ------------------- +# Each state is a real short unscaled fit, natural gradient only or with Newton +# steps (the Newton update moves A differently, so its rows are another test). +newton_param = pytest.mark.parametrize("newton", [False, True], ids=["ng", "newton"]) + + +@newton_param +@pytest.mark.parametrize("n_models", [1, 2]) +def test_torch_rescale_is_an_exact_change_of_scale(real_slice, n_models, newton): + m = AMICATorchNG( + n_channels=NW, + n_models=n_models, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + doscaling=False, + keep_best=False, + **(NEWTON if newton else {}), + ) + m.fit(real_slice, max_iter=3, verbose=False) + assert (m.lrate > NG_LRATE_CEILING) == newton, "test setup: Newton steps" + assert m.A is not None and m.mu is not None and m.beta is not None + assert m.comp_list is not None + comp_list = m.comp_list.numpy() + saved = {k: getattr(m, k).clone() for k in ("A", "mu", "beta")} + ll0 = _mean_ll(m.model_loglik(real_slice), NW) + # Unscaled rows drift well away from unit norm: the pass has work to do. + assert _row_norm_dev(m.A.numpy(), comp_list) > 1e-2 + + m._rescale_components() + m._update_unmixing_matrices() + assert abs(_mean_ll(m.model_loglik(real_slice), NW) - ll0) <= LL_TOL_F64 + assert _row_norm_dev(m.A.numpy(), comp_list) <= 1e-14 + + # The pre-#333 rule (normalize stored columns) on the same state. + for k, v in saved.items(): + setattr(m, k, v.clone()) + scale = torch.sqrt((m.A**2).sum(dim=0)) + m.A, m.mu, m.beta = m.A / scale, m.mu * scale, m.beta / scale + m._update_unmixing_matrices() + assert abs(_mean_ll(m.model_loglik(real_slice), NW) - ll0) > COLUMN_RULE_MIN_DLL + + +@newton_param +@pytest.mark.parametrize("n_models", [1, 2]) +def test_numpy_rescale_is_an_exact_change_of_scale( + real_slice, n_models, newton, tmp_path +): + m = AMICA_NumPy( + num_models=n_models, + num_mix=NMIX, + max_iter=3, + seed=SEED, + use_tqdm=False, + outdir=str(tmp_path), + do_opt_block=False, + block_size=8192, + doscaling=False, + writestep=10000, + **(NEWTON if newton else {}), + ) + m.fit(real_slice) + assert (m.lrate > NG_LRATE_CEILING) == newton, "test setup: Newton steps" + assert m.A is not None and m.mu is not None and m.beta is not None + assert m.comp_list is not None + comp_list = m.comp_list + A0, mu0, beta0 = m.A.copy(), m.mu.copy(), m.beta.copy() + ll0 = m._get_updates_and_likelihood()["ll"] + assert _row_norm_dev(A0, comp_list) > 1e-2 + + m._rescale_components() + m._update_unmixing_matrices() + assert abs(m._get_updates_and_likelihood()["ll"] - ll0) <= LL_TOL_F64 + assert m.A is not None + assert _row_norm_dev(m.A, comp_list) <= 1e-14 + + scale = np.sqrt(np.sum(A0**2, axis=0)) + m.A, m.mu, m.beta = A0 / scale, mu0 * scale, beta0 / scale + m._update_unmixing_matrices() + assert abs(m._get_updates_and_likelihood()["ll"] - ll0) > COLUMN_RULE_MIN_DLL + + +@newton_param +@pytest.mark.parametrize("n_models", [1, 2]) +def test_mlx_rescale_is_an_exact_change_of_scale(real_slice, n_models, newton): + mlx_core = _mlx_core() + mx = mlx_core.mx + m = mlx_core.AMICAMLXNG( + n_channels=NW, + n_models=n_models, + n_mix=NMIX, + seed=SEED, + doscaling=False, + keep_best=False, + **(NEWTON if newton else {}), + ) + m.fit(real_slice, max_iter=3, verbose=False) + assert (m.lrate > NG_LRATE_CEILING) == newton, "test setup: Newton steps" + comp_list = np.array(m.comp_list) + saved = {k: getattr(m, k) for k in ("A", "mu", "beta")} + ll0 = _mean_ll(m.model_loglik(real_slice), NW) + assert _row_norm_dev(np.array(m.A, dtype=np.float64), comp_list) > 1e-2 + + m._rescale_components() + m._update_unmixing_matrices() + assert abs(_mean_ll(m.model_loglik(real_slice), NW) - ll0) <= LL_TOL_F32 + assert _row_norm_dev(np.array(m.A, dtype=np.float64), comp_list) <= 1e-6 + + # MLX rebinds (never mutates) these arrays, so the saved ones are intact. + for k, v in saved.items(): + setattr(m, k, v) + scale = mx.sqrt((m.A**2).sum(axis=0)) + m.A, m.mu, m.beta = m.A / scale, m.mu * scale, m.beta / scale + m._update_unmixing_matrices() + assert abs(_mean_ll(m.model_loglik(real_slice), NW) - ll0) > COLUMN_RULE_MIN_DLL + + +# --- 2. one state, three backends, identical rescale ------------------------ +State = Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] + + +@pytest.fixture(scope="module") +def unscaled_state(real_slice) -> State: + """A real two-model state: ``A``, ``mu``, ``beta`` and ``comp_list`` of a + short unscaled fit, before any rescale.""" + t = AMICATorchNG( + n_channels=NW, + n_models=2, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + doscaling=False, + keep_best=False, + ) + t.fit(real_slice, max_iter=3, verbose=False) + assert t.A is not None and t.mu is not None and t.beta is not None + assert t.comp_list is not None + return ( + t.A.numpy().copy(), + t.mu.numpy().copy(), + t.beta.numpy().copy(), + t.comp_list.numpy().copy(), + ) + + +@pytest.fixture( + scope="module", + params=["disjoint", "permuted", "merged"], +) +def two_model_state(request, unscaled_state) -> State: + """``permuted`` shuffles each model's ``comp_list`` column, so the stored + columns of a block are neither contiguous nor in order (a general index + pattern for every backend's scatter). ``merged`` plants a share merge + (model 1's source 3 folded onto model 0's source 5 column, the way + ``identify_shared_comps`` folds a pair): the shared column then sits in + rows of both blocks, the one configuration where the per-block rule is not + a change of scale. Every backend must still apply it the same way (models + in order) until Phase 8 (issue #334) replaces it. + """ + A, mu, beta, comp_list = unscaled_state + comp_list = comp_list.copy() + if request.param == "permuted": + rng = np.random.default_rng(0) + for h in range(comp_list.shape[1]): + comp_list[:, h] = rng.permutation(comp_list[:, h]) + elif request.param == "merged": + comp_list[3, 1] = comp_list[5, 0] + return A, mu, beta, comp_list + + +def _rescaled(backend: str, state: State) -> State: + """``backend``'s own ``_rescale_components`` applied to a copy of + ``state`` (cast to float32 for MLX), as numpy arrays.""" + A, mu, beta, comp_list = state + if backend == "torch": + t = AMICATorchNG( + n_channels=NW, n_models=2, n_mix=NMIX, device="cpu", dtype=torch.float64 + ) + t.A, t.mu, t.beta = ( + torch.from_numpy(np.array(x, np.float64)) for x in (A, mu, beta) + ) + t.comp_list = torch.from_numpy(comp_list.copy()) + t._rescale_components() + assert t.A is not None and t.mu is not None and t.beta is not None + return t.A.numpy(), t.mu.numpy(), t.beta.numpy(), comp_list + if backend == "numpy": + n = AMICA_NumPy(num_models=2, num_mix=NMIX, use_tqdm=False) + n.A, n.mu, n.beta = (np.array(x, np.float64) for x in (A, mu, beta)) + n.comp_list = comp_list.copy() + n._rescale_components() + assert n.A is not None and n.mu is not None and n.beta is not None + return n.A, n.mu, n.beta, comp_list + mlx_core = _mlx_core() + mx = mlx_core.mx + x = mlx_core.AMICAMLXNG(n_channels=NW, n_models=2, n_mix=NMIX) + x.A, x.mu, x.beta = (mx.array(np.asarray(a, np.float32)) for a in (A, mu, beta)) + x.comp_list = mx.array(comp_list) + x._rescale_components() + return np.array(x.A), np.array(x.mu), np.array(x.beta), comp_list + + +def test_numpy_rescales_like_torch(two_model_state): + # Same float64 arithmetic; only the sum-of-squares association may differ. + got = _rescaled("numpy", two_model_state) + want = _rescaled("torch", two_model_state) + for g, w in zip(got[:3], want[:3]): + np.testing.assert_allclose(g, w, rtol=1e-14, atol=0) + + +def test_mlx_rescales_like_torch(two_model_state): + got = _rescaled("mlx", two_model_state) + # The float64 pass on the same float32-cast state is MLX's reference. + A, mu, beta, comp_list = two_model_state + f32 = tuple(a.astype(np.float32) for a in (A, mu, beta)) + want = _rescaled("torch", (*f32, comp_list)) + for g, w in zip(got[:3], want[:3]): + np.testing.assert_allclose(g, w, rtol=1e-6, atol=0) + + +@pytest.mark.parametrize("bad", [0.0, np.nan], ids=["zero", "nan"]) +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_a_zero_or_nan_norm_row_is_left_untouched(unscaled_state, backend, bad): + """The zero-norm guard (the reference's ``Anrmk > 0``): a component row + whose norm is zero, or NaN (``NaN > 0`` is false), keeps its row and its + ``mu``/``beta`` columns bit for bit, while every other row is rescaled + exactly as it is without the planted row (rows are independent).""" + A, mu, beta, comp_list = unscaled_state + h, i = 1, 7 # source 7 of model 1: a block other than the first + k = comp_list[i, h] + planted = A.copy() + planted[i, comp_list[:, h]] = bad + dtype = np.float32 if backend == "mlx" else np.float64 + before = tuple(np.asarray(x, dtype) for x in (planted, mu, beta)) + + got_A, got_mu, got_beta, _ = _rescaled(backend, (planted, mu, beta, comp_list)) + clean_A, clean_mu, clean_beta, _ = _rescaled(backend, unscaled_state) + + rows = np.zeros(A.shape, dtype=bool) + rows[i, comp_list[:, h]] = True + assert got_A[rows].tobytes() == before[0][rows].tobytes() + assert got_mu[:, k].tobytes() == before[1][:, k].tobytes() + assert got_beta[:, k].tobytes() == before[2][:, k].tobytes() + + others = np.arange(mu.shape[1]) != k + assert got_A[~rows].tobytes() == clean_A[~rows].tobytes() + assert got_mu[:, others].tobytes() == clean_mu[:, others].tobytes() + assert got_beta[:, others].tobytes() == clean_beta[:, others].tobytes() + # ...and those rows are unit norm, so the rescale really ran on them. + unit = 1e-6 if backend == "mlx" else 1e-14 + for hh in range(comp_list.shape[1]): + norms = np.linalg.norm(got_A[:, comp_list[:, hh]].astype(np.float64), axis=1) + keep = np.arange(NW) != i if hh == h else np.ones(NW, dtype=bool) + assert np.abs(norms[keep] - 1.0).max() <= unit + + +# --- 3. doscaling=False is byte-identical to the pre-fix code --------------- +# The epic #324 head this phase branched from: the last commit with the stored- +# column rule. A later phase that deliberately changes the doscaling=False +# trajectory moves this pin to its own base commit. +_PRE_FIX_COMMIT = "fb13d76de145419da9c89db94438d60aafbff444" +_BACKEND_MODULES = { + "torch": ("pamica/torch_impl/core.py", "pamica.torch_impl", "AMICATorchNG"), + "numpy": ("pamica/numpy_impl/core.py", "pamica.numpy_impl", "AMICA"), + "mlx": ("pamica/mlx_impl/core.py", "pamica.mlx_impl", "AMICAMLXNG"), +} + + +def _pre_fix_class(backend: str) -> Any: + """The backend class as it was at ``_PRE_FIX_COMMIT``, read with ``git + show`` and executed as a sibling module of the live package, so its + relative imports resolve against the live helper modules (this phase + changes none of them). The same construction as + ``mlx_tests/test_mlx_fit_noop.py``: both classes then run in one process on + one machine, so the comparison is exact without recorded constants.""" + path, package, cls_name = _BACKEND_MODULES[backend] + repo_root = Path(__file__).resolve().parents[2] + # A shallow clone may lack the commit: fetch just that object first (best + # effort, e.g. no network), and let `git show` be the real check. + subprocess.run( + ["git", "fetch", "origin", _PRE_FIX_COMMIT, "--depth", "1"], + cwd=repo_root, + capture_output=True, + text=True, + ) + result = subprocess.run( + ["git", "show", f"{_PRE_FIX_COMMIT}:{path}"], + cwd=repo_root, + capture_output=True, + text=True, + ) + if result.returncode != 0: + reason = ( + f"git object {_PRE_FIX_COMMIT[:7]} is not reachable in this checkout; " + f"git show stderr: {result.stderr.strip()!r}" + ) + # A skip in CI would silently drop this regression guard, and the CI + # jobs check out full history (fetch-depth: 0), so there it fails. + # Only a local shallow clone skips. + if os.environ.get("CI"): + pytest.fail(reason) + pytest.skip(reason) + name = f"{package}._pre_issue_333_core" + module = types.ModuleType(name) + module.__package__ = package + # The live path, so a data file read next to the module (NumPy's + # params.json, unchanged by this phase) resolves; the code object names + # the commit it came from. + module.__file__ = str(repo_root / path) + sys.modules[name] = module # dataclasses look the module up while building + try: + code = compile(result.stdout, f"", "exec") + exec(code, module.__dict__) + finally: + del sys.modules[name] + return getattr(module, cls_name) + + +def _fit_unscaled( + cls: Any, backend: str, n_models: int, X: np.ndarray, outdir: Path +) -> Dict[str, np.ndarray]: + """A short ``doscaling=False`` fit of ``cls``; its trajectory and every + fitted array, as float64/int numpy arrays.""" + if backend == "numpy": + model = cls( + num_models=n_models, + num_mix=NMIX, + max_iter=6, + seed=SEED, + use_tqdm=False, + outdir=str(outdir), + do_opt_block=False, + block_size=8192, + doscaling=False, + writestep=10000, + ) + model.fit(X) + ll = model.ll + else: + kwargs: Dict[str, Any] = dict( + n_channels=NW, n_models=n_models, n_mix=NMIX, seed=SEED, doscaling=False + ) + if backend == "torch": + kwargs.update(device="cpu", dtype=torch.float64) + model = cls(**kwargs) + model.fit(X, max_iter=6, verbose=False) + ll = model.ll_history + out = {"ll": np.asarray(ll, dtype=np.float64)} + for k in ("A", "W", "mu", "beta", "alpha", "rho", "gm", "c", "comp_list"): + v = getattr(model, k) + out[k] = v.cpu().numpy() if isinstance(v, torch.Tensor) else np.array(v) + return out + + +@pytest.mark.parametrize("n_models", [1, 2]) +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_doscaling_off_is_byte_identical_to_the_pre_fix_code( + real_slice, backend, n_models, tmp_path +): + """``doscaling=False`` never enters the rescale, so this fix must leave its + trajectory and every fitted array bit for bit where the pre-fix code put + them, on every backend.""" + new_cls: Any + if backend == "mlx": + new_cls = _mlx_core().AMICAMLXNG + else: + new_cls = AMICATorchNG if backend == "torch" else AMICA_NumPy + old_cls = _pre_fix_class(backend) + + old = _fit_unscaled(old_cls, backend, n_models, real_slice, tmp_path / "old") + new = _fit_unscaled(new_cls, backend, n_models, real_slice, tmp_path / "new") + changed = sorted(k for k in old if not np.array_equal(old[k], new[k])) + assert not changed, f"doscaling=False results changed: {changed}" + + +# --- 4. scalestep cadence, counted from 1 ----------------------------------- +def _fit_rows_dev(backend: str, scalestep: int, max_iter: int, X, tmp_path) -> float: + """Largest component-row norm deviation after a real two-model fit of + ``max_iter`` iterations (``keep_best`` off, so it is the last iterate).""" + if backend == "numpy": + n = AMICA_NumPy( + num_models=2, + num_mix=NMIX, + max_iter=max_iter, + seed=SEED, + use_tqdm=False, + outdir=str(tmp_path / f"s{scalestep}_k{max_iter}"), + do_opt_block=False, + block_size=4096, + scalestep=scalestep, + writestep=10000, + ) + n.fit(X) + assert n.A is not None and n.comp_list is not None + return _row_norm_dev(n.A, n.comp_list) + kwargs: Dict[str, Any] = dict( + n_channels=NW, + n_models=2, + n_mix=NMIX, + seed=SEED, + block_size=4096, + keep_best=False, + scalestep=scalestep, + ) + if backend == "torch": + m = AMICATorchNG(device="cpu", dtype=torch.float64, **kwargs) + m.fit(X, max_iter=max_iter, verbose=False) + assert m.A is not None and m.comp_list is not None + return _row_norm_dev(m.A.numpy(), m.comp_list.numpy()) + m = _mlx_core().AMICAMLXNG(**kwargs) + m.fit(X, max_iter=max_iter, verbose=False) + return _row_norm_dev(np.array(m.A, dtype=np.float64), np.array(m.comp_list)) + + +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +@pytest.mark.parametrize("scalestep", [1, 3]) +def test_scalestep_counts_iterations_from_one(real_slice, backend, scalestep, tmp_path): + """The reference rescales every iteration: it parses ``scalestep`` but never + reads it (amica15.f90:1843, :3686). pamica keeps ``scalestep`` as an + extension counted from 1 like the reference's other cadences, so the rescale + runs on 1-based iterations ``scalestep``, ``2*scalestep``, ...; the default 1 + is the reference. + + Observed on real fits of 1 to 6 iterations: right after a rescale iteration + every component row has unit norm, and one unscaled iteration already moves + the rows by 1.7e-2 to 9.5e-2 (measured). + """ + if backend == "mlx": + _mlx_core() + unit = 1e-6 if backend == "mlx" else 1e-14 + X = real_slice[:, :4096] + for k in range(1, 7): + dev = _fit_rows_dev(backend, scalestep, k, X, tmp_path) + if k % scalestep == 0: + assert dev <= unit, f"1-based iteration {k}: rows not rescaled ({dev})" + else: + assert dev > 1e-3, f"1-based iteration {k}: rows rescaled ({dev})" + + +def _construct(backend: str, outdir: Path, **kwargs: Any) -> Any: + if backend == "numpy": + return AMICA_NumPy( + num_models=1, num_mix=NMIX, use_tqdm=False, outdir=str(outdir), **kwargs + ) + if backend == "torch": + return AMICATorchNG(n_channels=NW, n_mix=NMIX, device="cpu", **kwargs) + return _mlx_core().AMICAMLXNG(n_channels=NW, n_mix=NMIX, **kwargs) + + +@pytest.mark.parametrize("bad", [0, -1, 2.5, True]) +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_every_backend_rejects_an_invalid_scalestep(backend, bad, tmp_path): + """A zero ``scalestep`` used to surface as a bare ``ZeroDivisionError`` + mid-fit on every backend; each constructor now rejects anything but an + integer >= 1, with one message, while the rescale is on.""" + with pytest.raises(ValueError, match="scalestep must be an integer >= 1"): + _construct(backend, tmp_path, doscaling=True, scalestep=bad) + # With the rescale off, scalestep is never read, so it is not validated. + off = _construct(backend, tmp_path, doscaling=False, scalestep=bad) + assert off.scalestep == bad + + +# --- 5. seeded native reference oracle (opt-in) ----------------------------- +# The reference's input.param optimizer (natural gradient only here: the Newton +# start is epic #324 Phase 9, issue #335) with the block size pinned on both +# sides. pamica keyword -> reference keyword where the names differ. +_OPT: Dict[str, Any] = dict( + block_size=512, + lrate=0.05, + lratefact=0.5, + rholrate=0.05, + rholratefact=0.5, + rho0=1.5, + minrho=1.0, + maxrho=2.0, + invsigmin=0.0, + invsigmax=100.0, + do_newton=False, + newt_start=50, + newtrate=1.0, + newt_ramp=10, +) +_REF_OPT: Dict[str, Any] = { + **{k: v for k, v in _OPT.items() if k != "do_newton"}, + "do_newton": 0, + "do_opt_block": 0, + "do_reject": 0, + "share_comps": 0, + "doscaling": 1, + "scalestep": 1, +} +# Tolerances per iteration count. The reference itself carries round-off that +# the ill-conditioned exact-EM mu update amplifies (mu of a low-mass mixture +# component), so each bound sits at the doscaling-OFF noise floor with margin. +# Measured maxima (doscaling on / off), each taken over both backends (PyTorch +# and NumPy) and both model counts, which is why they are wider than the +# single-configuration figures in the changelog and PR (PyTorch, one model: +# A 5.0e-16, mu 7.8e-11, sbeta 1.1e-14 after 1 iteration): +# 1 iteration: A 2.2e-15 / 2.4e-15, mu 7.8e-11 / 8.0e-11, sbeta 2.4e-14 +# 3 iterations: A 6.1e-12 / 3.1e-11, mu 3.6e-8 / 1.3e-8, sbeta 2.8e-10 +# The column rule this replaced was off by A 7.2e-5, mu 6.6e-5, sbeta 8.6e-5 +# after 1 iteration and 1.4e-3 / 6.7e-3 / 2.2e-3 after 3. +_ORACLE_TOL = { + 1: {"A": 1e-13, "mu": 1e-9, "sbeta": 1e-12, "LL": 1e-12}, + 3: {"A": 1e-10, "mu": 1e-6, "sbeta": 1e-8, "LL": 1e-11}, +} + + +@pytest.mark.skipif( + os.environ.get("AMICA_RUN_FORTRAN") != "1", + reason="opt-in Fortran-binary integration test (set AMICA_RUN_FORTRAN=1)", +) +@pytest.mark.parametrize("n_models", [1, 2]) +def test_doscaling_matches_the_seeded_reference(n_models, tmp_path): + """Seed the reference from pamica's initialization, run 1 and 3 iterations + with ``doscaling`` on, and compare ``A``/``mu``/``sbeta`` element by element. + + The two-model case seeds ``comp_list`` through ``load_comp_list`` (the + default one; ``c`` then starts at the reference's own zero, as pamica's + does), so the helper's comp_list path is exercised for Phase 8. + """ + from pamica.tests.native_oracle import ( + reference_mixing, + run_seeded_reference, + seed_from_torch, + ) + + X = load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD) + X = X.astype(np.float64) + + def torch_model() -> AMICATorchNG: + return AMICATorchNG( + n_channels=NW, + n_models=n_models, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + keep_best=False, + **_OPT, + ) + + init = torch_model() + init._preprocess(X) + init._initialize_parameters() + state = seed_from_torch(init) + assert init.comp_list is not None and init.sphere is not None + comp_list = init.comp_list.numpy() + if n_models > 1: + state = dataclasses.replace(state, comp_list=comp_list) + + for k, tol in _ORACLE_TOL.items(): + ref = run_seeded_reference( + state, + DATA_FILE, + tmp_path / f"k{k}", + n_samples=FIELD, + max_iter=k, + num_models=n_models, + **_REF_OPT, + ) + if n_models > 1: + assert "reading comp_list" in ref.stdout + np.testing.assert_array_equal(ref.comp_list, comp_list) + # Same data, same symmetric ZCA sphere (the reference computes its own). + assert np.abs(ref.S - init.sphere.numpy()).max() < 1e-12 + # The reference's components are unit norm, as pamica's rows now are. + assert np.abs(np.linalg.norm(ref.A, axis=0) - 1.0).max() < 1e-14 + + t = torch_model() + t.fit(X, max_iter=k, verbose=False) + assert t.A is not None and t.mu is not None and t.beta is not None + with tempfile.TemporaryDirectory() as td: + n = AMICA_NumPy( + num_models=n_models, + num_mix=NMIX, + max_iter=k, + seed=SEED, + use_tqdm=False, + outdir=td, + do_opt_block=False, + writestep=10000, + **_OPT, + ) + n.fit(X) + fits: Dict[str, tuple] = { + "torch": (t.A.numpy(), t.mu.numpy(), t.beta.numpy(), t.ll_history), + "numpy": (n.A, n.mu, n.beta, n.ll), + } + for name, (A, mu, beta, ll) in fits.items(): + errs = { + "A": np.abs(reference_mixing(A, comp_list) - ref.A).max(), + "mu": np.abs(mu - ref.mu).max(), + "sbeta": np.abs(beta - ref.sbeta).max(), + "LL": np.abs(np.asarray(ll) - ref.LL).max(), + } + over = {q: e for q, e in errs.items() if not e <= tol[q]} + assert not over, f"{name}, {k} iteration(s): {over} (bounds {tol})" + assert _row_norm_dev(A, comp_list) <= 1e-14 diff --git a/pamica/tests/test_llt_stash.py b/pamica/tests/test_llt_stash.py index c0a5e686..4ce3dc85 100644 --- a/pamica/tests/test_llt_stash.py +++ b/pamica/tests/test_llt_stash.py @@ -42,7 +42,7 @@ from pamica import AMICA_NumPy from pamica.numpy_impl.data import load_data_file from pamica.numpy_impl.load import loadmodout -from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.core import _KEEP_BEST_TOL, AMICATorchNG SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" _FDT = SAMPLE_DIR / "eeglab_data.fdt" @@ -387,19 +387,32 @@ def test_keep_best_restore_rolls_the_llt_stash_back(real_data, tmp_path): would still hold the discarded last iterate's values, which the second assertion rules out. """ + # The overshoot recipe of test_ng_convergence.py (same data, block size + # and settings, so the same trajectory): aggressive Newton that peaks and + # then stops via the loosened min_dll (peak at iteration 70, stop at 71). + # A fixed 60-iteration budget overshot on the development machine only; + # on the CI runners that run was monotone. m = _torch_fit( real_data, n_models=2, - max_iter=60, + max_iter=150, seed=0, do_newton=True, newt_start=1, lrate=0.5, + newtrate=3.0, + use_min_dll=True, + min_dll=1e-4, + maxincs=2, + use_grad_norm=False, + ) + assert m.stop_reason not in AMICATorchNG._DEGENERATE_STOP_REASONS, ( + "the overshoot recipe ended degenerate: retune it" + ) + assert m.final_ll_ is not None + assert max(m.ll_history) - m.ll_history[-1] > _KEEP_BEST_TOL, ( + "the overshoot recipe no longer overshoots: retune it" ) - if m.stop_reason in AMICATorchNG._DEGENERATE_STOP_REASONS: - pytest.skip("aggressive run ended degenerate; not the case under test") - if np.isclose(m.ll_history[-1], m.final_ll_): - pytest.skip("run was monotone; keep_best restore did not fire") assert m._llt_lt is not None and m._llt_lht is not None inv = _llt_invariant(m._llt_lt, m._llt_lt.size, NW) diff --git a/pamica/tests/test_mlx_keepbest_cross_backend.py b/pamica/tests/test_mlx_keepbest_cross_backend.py index 7ad07194..fe8049e3 100644 --- a/pamica/tests/test_mlx_keepbest_cross_backend.py +++ b/pamica/tests/test_mlx_keepbest_cross_backend.py @@ -69,7 +69,8 @@ def real_data() -> np.ndarray: # overshoot on PyTorch (test_ng_convergence.py's forcing recipe) and, # independently verified, on MLX too (test_mlx_keepbest.py's module # docstring) -- but this module does not rely on that coincidence holding -# forever, only on the CONTRACT below, which holds regardless. +# forever, only on the CONTRACT below, which holds regardless. (newtrate=3.0 +# and the 150-iteration budget since issue #333, as in those recipes.) _AGGRESSIVE_KWARGS: dict[str, Any] = dict( n_models=2, n_mix=NMIX, @@ -78,11 +79,13 @@ def real_data() -> np.ndarray: do_newton=True, newt_start=1, lrate=0.5, + newtrate=3.0, use_min_dll=True, min_dll=1e-4, maxincs=2, use_grad_norm=False, ) +_MAX_ITER = 150 def test_torch_keep_best_satisfies_the_max_contract(real_data): @@ -93,14 +96,14 @@ def test_torch_keep_best_satisfies_the_max_contract(real_data): keep_best=True, **_AGGRESSIVE_KWARGS, ) - ng.fit(real_data, max_iter=60, verbose=False) + ng.fit(real_data, max_iter=_MAX_ITER, verbose=False) assert ng.stop_reason not in ng._DEGENERATE_STOP_REASONS assert ng.final_ll_ == max(ng.ll_history) def test_mlx_keep_best_satisfies_the_max_contract(real_data): m = AMICAMLXNG(n_channels=NW, keep_best=True, **_AGGRESSIVE_KWARGS) - m.fit(real_data, max_iter=60, verbose=False) + m.fit(real_data, max_iter=_MAX_ITER, verbose=False) assert m.stop_reason not in m._DEGENERATE_STOP_REASONS assert m.final_ll_ == max(m.ll_history) @@ -116,11 +119,11 @@ def test_both_backends_disable_the_safeguard_under_share_comps(real_data): ng = AMICATorchNG( n_channels=NW, device="cpu", dtype=torch.float64, keep_best=True, **kwargs ) - ng.fit(real_data, max_iter=60, verbose=False) + ng.fit(real_data, max_iter=_MAX_ITER, verbose=False) assert ng.stop_reason not in ng._DEGENERATE_STOP_REASONS assert ng.final_ll_ == ng.ll_history[-1] m = AMICAMLXNG(n_channels=NW, keep_best=True, **kwargs) - m.fit(real_data, max_iter=60, verbose=False) + m.fit(real_data, max_iter=_MAX_ITER, verbose=False) assert m.stop_reason not in m._DEGENERATE_STOP_REASONS assert m.final_ll_ == m.ll_history[-1] diff --git a/pamica/tests/test_mlx_reject_cross_backend.py b/pamica/tests/test_mlx_reject_cross_backend.py index 44adb087..043d982f 100644 --- a/pamica/tests/test_mlx_reject_cross_backend.py +++ b/pamica/tests/test_mlx_reject_cross_backend.py @@ -14,17 +14,18 @@ pins ride here together. 1. **do_reject agreement**: same real data, same config, ``AMICATorchNG`` - (float64) and ``AMICAMLXNG`` (float32) must reject the SAME sample set. - This is the load-bearing evidence for the phase's design decision: MLX - reads the rejection statistic FROM the LLt stash instead of a second - forward pass (the NumPy backend's design, pre-empting AMICATorchNG's - open follow-up #298) -- if that were not mathematically equivalent to - torch's own ``_sample_ll`` forward-pass statistic, the two backends - would drift apart on which samples they drop. They do not, at a - generous margin (measured: identical rejected-sample sets at rejsig in - {2.0, 2.5, 3.0}, single- and 2-model), so the config below is chosen - with clear headroom rather than right at a borderline threshold (the - Phase 1/2 flakiness lesson). + (float64) and ``AMICAMLXNG`` (float32) must make the same rejection + decision on every sample that is not within float32 rounding of the + threshold. This is the load-bearing evidence for the phase's design + decision: MLX reads the rejection statistic FROM the LLt stash instead of + a second forward pass (the NumPy backend's design, pre-empting + AMICATorchNG's open follow-up #298) -- if that were not mathematically + equivalent to torch's own ``_sample_ll`` forward-pass statistic, the two + backends would drift apart on which samples they drop. Exact agreement of + the whole rejected set is not a property of float32, so the check is per + pass and explains every disagreement by the rounding measured on that + pass (issue #333 moved the trajectories enough to flip one borderline + sample of the earlier exact-set pin). 2. **do_reject x pdftype=1 agreement**: a PR review regression (``_choose_pdfs`` was called with the FULL sphered dataset on MLX, @@ -81,48 +82,206 @@ def _real_data(n_samples: int = 4096) -> np.ndarray: return data[:, :n_samples].astype(np.float64) -# --- do_reject: the same sample set, at a config with a clear margin ------- -@pytest.mark.parametrize("n_models", [1, 2]) -def test_reject_same_sample_set_across_backends(n_models): - """rejsig=2.0 (not right at any observed borderline): both backends - reject exactly the same sample indices, from the same seed/data/ - schedule. Measured to hold at rejsig in {2.0, 2.5, 3.0} during - development; 2.0 is kept as the assertion (generous margin, not tuned - to pass).""" +# --- do_reject: the same decisions, up to float32 rounding at the threshold - +class _RecordingTorch(AMICATorchNG): + """Records the input of each rejection pass, then runs the real pass. + + A pass-through spy, not a stub: ``_reject_outliers`` itself runs unchanged + on the real statistic, so the fit is exactly the production fit. It only + keeps what the backend decided on: the good set before the pass (sample + indices) and the per-sample log-likelihood over it, in the same order. + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.passes: list[tuple[np.ndarray, np.ndarray]] = [] + + def _reject_outliers(self, ll_vec: torch.Tensor) -> None: + assert self.good_idx is not None + self.passes.append((self.good_idx.numpy().copy(), ll_vec.numpy().copy())) + super()._reject_outliers(ll_vec) + + +class _RecordingMLX(AMICAMLXNG): + """The MLX twin of :class:`_RecordingTorch` (float32 statistic).""" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.passes: list[tuple[np.ndarray, np.ndarray]] = [] + + def _reject_outliers(self, ll_vec: np.ndarray) -> None: + assert self.good_idx is not None + self.passes.append((np.array(self.good_idx), np.array(ll_vec).copy())) + super()._reject_outliers(ll_vec) + + +def _torch_threshold(ll: np.ndarray, rejsig: float) -> float: + """``mean - rejsig * std`` (population std) in float64, with the same + torch operations as ``AMICATorchNG._reject_outliers``.""" + t = torch.from_numpy(ll) + mean = t.mean() + std = torch.sqrt((t.pow(2).mean() - mean.pow(2)).clamp_min(0.0)) + return float(mean - rejsig * std) + + +def _mlx_threshold(ll: np.ndarray, rejsig: float) -> float: + """The same threshold with the float32 numpy operations of + ``AMICAMLXNG._reject_outliers``.""" + mean = float(ll.mean()) + std = float(np.sqrt(max(float(np.mean(ll**2) - mean**2), 0.0))) + return mean - rejsig * std + + +# A float32 statistic can flip a decision only for samples within its own +# rounding of the threshold, so the band that explains a disagreement is +# measured per pass (below). These caps are what make a REAL divergence fail +# instead of being absorbed into a band it inflated: an orientation or +# trajectory bug makes the two per-sample log-likelihoods differ by the order +# of their spread, and a threshold bug moves the threshold by a sizable +# fraction of it. Measured over seeds 0-5, 7 and 8, one and two models, +# rejsig 2.0, 2.5 and 3.0 (Apple M4 Pro): the band was at most 3.6e-2 of the +# per-sample log-likelihood's standard deviation, held at most 14 samples +# (0.36% of a pass), and at most 1 sample ended up rejected by one backend and +# kept by the other (the band's worst sample, a two-model fit's second pass). +_MAX_BAND_OVER_STD = 0.1 +_MAX_BORDERLINE_FRACTION = 0.01 +_MAX_DISAGREEMENTS = 3 + + +def _rejection_violations( + m64: _RecordingTorch, m32: _RecordingMLX, rejsig64: float, rejsig32: float +) -> list[str]: + """Every way the two backends' rejections differ by more than float32 + rounding at the threshold (empty when they agree up to that rounding).""" + assert m64.good_idx is not None and m32.good_idx is not None + final64 = m64.good_idx.numpy() + final32 = np.array(m32.good_idx) + problems: list[str] = [] + if len(m64.passes) != len(m32.passes): + return [f"pass counts differ: {len(m64.passes)} vs {len(m32.passes)}"] + explained: set[int] = set() + for p, ((g64, l64), (g32, l32)) in enumerate(zip(m64.passes, m32.passes)): + after64 = m64.passes[p + 1][0] if p + 1 < len(m64.passes) else final64 + after32 = m32.passes[p + 1][0] if p + 1 < len(m32.passes) else final32 + t64 = _torch_threshold(l64, rejsig64) + t32 = _mlx_threshold(l32, rejsig32) + # The thresholds recomputed here are the ones each backend applied. + for name, g, ll, t, after in ( + ("torch", g64, l64, t64, after64), + ("mlx", g32, l32, t32, after32), + ): + if not np.array_equal(g[ll >= t], after): + problems.append(f"pass {p}: {name} did not apply mean - rejsig*std") + + common, i64, i32 = np.intersect1d(g64, g32, return_indices=True) + x64 = l64[i64] + diff = np.abs(x64 - l32[i32].astype(np.float64)) + # Measured on this pass: the largest float32-vs-float64 difference of + # a per-sample log-likelihood, plus the resulting threshold difference. + band = float(diff.max()) + abs(t64 - t32) + spread = float(np.std(x64)) + if band > _MAX_BAND_OVER_STD * spread: + problems.append( + f"pass {p}: the backends' statistics differ by {band:.3g}, " + f"{band / spread:.2g} of their spread (cap {_MAX_BAND_OVER_STD})" + ) + borderline = np.abs(x64 - t64) <= band + if borderline.mean() > _MAX_BORDERLINE_FRACTION: + problems.append( + f"pass {p}: {int(borderline.sum())} of {common.size} samples lie " + f"within {band:.3g} of the threshold" + ) + disagree = (l64[i64] >= t64) != (l32[i32] >= t32) + # Every decision outside the band is identical on both backends. + if (disagree & ~borderline).any(): + far = common[disagree & ~borderline] + problems.append(f"pass {p}: samples {far.tolist()} disagree off the band") + explained |= set(common[disagree].tolist()) + + rejected_by_one = set(final64.tolist()) ^ set(final32.tolist()) + if not rejected_by_one <= explained: + problems.append( + f"samples {sorted(rejected_by_one - explained)} differ in the final " + "good sets without a disagreeing pass decision" + ) + if len(rejected_by_one) > _MAX_DISAGREEMENTS: + problems.append( + f"{len(rejected_by_one)} samples rejected by one backend only " + f"(cap {_MAX_DISAGREEMENTS})" + ) + return problems + + +def _reject_fits( + n_models: int, seed: int, rejsig32: float = 2.0, seed32: int | None = None +) -> tuple[_RecordingTorch, _RecordingMLX]: X = _real_data() kwargs: dict[str, Any] = dict( n_channels=NW, n_models=n_models, n_mix=NMIX, - seed=7, block_size=BLOCK, do_reject=True, - rejsig=2.0, rejstart=2, rejint=3, maxrej=2, keep_best=True, ) + m32 = _RecordingMLX( + **kwargs, rejsig=rejsig32, seed=seed if seed32 is None else seed32 + ) + m32.fit(X, max_iter=10, verbose=False) + m64 = _RecordingTorch( + **kwargs, rejsig=2.0, seed=seed, device="cpu", dtype=torch.float64 + ) + m64.fit(X, max_iter=10, verbose=False) + return m64, m32 - mlx_model = AMICAMLXNG(**kwargs) - mlx_model.fit(X, max_iter=10, verbose=False) - torch_kwargs: dict[str, Any] = dict(kwargs, device="cpu", dtype=torch.float64) - torch_model = AMICATorchNG(**torch_kwargs) - torch_model.fit(X, max_iter=10, verbose=False) - - assert mlx_model.good_idx is not None and torch_model.good_idx is not None - mlx_set = set(np.array(mlx_model.good_idx).tolist()) - torch_set = set(torch_model.good_idx.numpy().tolist()) - - n_total = X.shape[1] - assert len(mlx_set) < n_total, "test setup: MLX rejected nothing" - assert len(torch_set) < n_total, "test setup: torch rejected nothing" - assert mlx_model.numrej == torch_model.numrej - assert mlx_set == torch_set, ( - f"rejected-sample sets diverged: {len(mlx_set ^ torch_set)} samples " - "differ between backends" - ) +@pytest.mark.parametrize("seed", [7, 8]) +@pytest.mark.parametrize("n_models", [1, 2]) +def test_reject_decisions_agree_up_to_float32_rounding(n_models, seed): + """Both backends make the same rejection decision on every sample, except + samples within float32 rounding of the threshold. + + Exact agreement of the rejected SET is not a property of the backends: the + float32 and float64 per-sample log-likelihoods differ by up to ~1e-1 by + the second pass (iteration 5), and a sample that close to the threshold + can land on either side. It held at rejsig=2.0 on seed 7 only until issue + #333 moved the trajectories, after which one two-model sample (of 357 + rejected) flipped. So each pass is checked on its own terms + (:func:`_rejection_violations`): the recomputed threshold is the one each + backend applied; every disagreement lies within the band measured on that + pass (the largest per-sample difference plus the threshold difference), + so every decision outside it is identical; the band is narrow and holds + few samples; and at most a few samples end up rejected by only one + backend. Seed 8's two-model fit is the widest band measured (14 + borderline samples, one disagreement). A threshold or trajectory + divergence fails it (``test_reject_agreement_check_catches_a_divergence``). + """ + m64, m32 = _reject_fits(n_models, seed) + assert len(m64.passes) == 2, "test setup: both rejection passes should run" + assert m64.good_idx is not None + assert m64.good_idx.numel() < 4096, "test setup: torch rejected nothing" + assert m32.numrej == m64.numrej + assert not _rejection_violations(m64, m32, 2.0, 2.0) + + +@pytest.mark.parametrize( + "divergence", ["threshold", "trajectory"], ids=["threshold", "trajectory"] +) +def test_reject_agreement_check_catches_a_divergence(divergence): + """The rounding allowance above cannot absorb a real divergence: a + threshold 0.2 standard deviations off (MLX at rejsig=2.2), or a + different trajectory (MLX from another seed, standing in for an + orientation or update bug), each violates it.""" + if divergence == "threshold": + m64, m32 = _reject_fits(2, 7, rejsig32=2.2) + problems = _rejection_violations(m64, m32, 2.0, 2.2) + else: + m64, m32 = _reject_fits(2, 7, seed32=8) + problems = _rejection_violations(m64, m32, 2.0, 2.0) + assert problems, "the agreement check accepted a real divergence" def test_reject_x_pdftype1_kurtosis_switch_matches_across_backends(): @@ -139,6 +298,19 @@ def test_reject_x_pdftype1_kurtosis_switch_matches_across_backends(): before kurt_start=3's first switch at iteration 3, so the switcher's FIRST call already sees a shrunken good set on both backends -- the scenario the bug needed. + + The good sets are checked with the same float32 band as + ``test_reject_decisions_agree_up_to_float32_rounding`` + (:func:`_rejection_violations`), not an equal rejected count, which a + single borderline sample on other hardware would break. This config's + band is far inside those caps: over seeds 0-5, 7 and 8, and over 12 + copies of the MLX input multiplied by (1 + eps * z) at seed 2 (eps the + float32 machine epsilon, z standard normal), it stayed within 1.2e-4 of + the per-sample log-likelihood's spread, held at most 1 sample, and no + decision differed. The pdtype decisions stay exact because the switch + has margin: at seed 2 the smallest |kurtosis| any switch saw is 1.24e-2, + 560 times the largest float32-vs-float64 kurtosis difference over those + perturbations (2.2e-5). """ X = _real_data() kwargs: dict[str, Any] = dict( @@ -159,19 +331,16 @@ def test_reject_x_pdftype1_kurtosis_switch_matches_across_backends(): keep_best=False, ) - mlx_model = AMICAMLXNG(**kwargs) + mlx_model = _RecordingMLX(**kwargs) mlx_model.fit(X, max_iter=15, verbose=False) torch_kwargs: dict[str, Any] = dict(kwargs, device="cpu", dtype=torch.float64) - torch_model = AMICATorchNG(**torch_kwargs) + torch_model = _RecordingTorch(**torch_kwargs) torch_model.fit(X, max_iter=15, verbose=False) assert mlx_model.numrej > 0, "test setup: no rejection fired" assert mlx_model.n_kurt_done > 0, "test setup: the switcher never ran" - assert mlx_model.good_idx is not None and torch_model.good_idx is not None - assert int(mlx_model.good_idx.size) == int(torch_model.good_idx.numel()), ( - "test setup: the two backends rejected a different number of samples" - ) + assert not _rejection_violations(torch_model, mlx_model, 2.0, 2.0) assert torch_model.pdtype is not None mlx_pdtype = np.array(mlx_model.pdtype) diff --git a/pamica/tests/test_mlx_transform_cross_backend.py b/pamica/tests/test_mlx_transform_cross_backend.py index 1734c9c0..00dfa79f 100644 --- a/pamica/tests/test_mlx_transform_cross_backend.py +++ b/pamica/tests/test_mlx_transform_cross_backend.py @@ -206,48 +206,162 @@ def test_accessors_match_float64_torch_twin(): assert err < 1e-4, f"model {h}: get_rho differs by {err:.3e}" -def test_variance_order_matches_float64_twin(): +# Float32 perturbation study behind the variance_order agreement check below: +# the same 2-model, 30-iteration MLX fit on the input and on 48 copies of it, +# each element multiplied by (1 + eps * z) with eps the float32 machine +# epsilon (1.19e-7) and z standard normal (numpy default_rng(20260922)), each +# compared with its float64 twin (98 per-model orders, Apple M4 Pro). Both +# backends compute the order host-side in float64, so the per-component +# variance difference comes only from MLX's float32 parameters (W and the +# sphere): at most 1.65e-6 relative. No pair of components fell inside the +# resulting band in any fit (the closest adjacent pair sat 34x outside its +# band), so no order ever differed; 35 of the 3038 adjacent gaps were under +# 1e-3 relative, none under 3e-5. +_SVAR_STUDY_MAX_REL = 1.65e-6 +_SVAR_SAFETY = 3.0 +# At most one in-band swap per model. From the study's gap density near zero +# (35 of 3038 adjacent gaps under 1e-3) and its widest band (2.9e-6), an +# adjacent pair lands inside the band with probability about 3e-5, so about +# 1e-3 in-band pairs are expected per 32-component model: one swap covers a +# rare near-tie on other hardware, while two in one model (about 5e-7) would +# signal a real ordering change, as would any swap outside the band. +_MAX_ORDER_SWAPS = 1 + + +def _per_component(order: np.ndarray, svar_sorted: np.ndarray) -> np.ndarray: + """Undo ``variance_order``'s sort: the variance of each source index.""" + assert np.array_equal(np.sort(order), np.arange(order.size)) + v = np.empty(order.size, dtype=np.float64) + v[order] = svar_sorted + return v + + +def _order_violations( + order32: np.ndarray, v32: np.ndarray, order64: np.ndarray, v64: np.ndarray +) -> list[str]: + """Every way the MLX order (``order32``, per-source variances ``v32``) + departs from the float64 twin's (``order64``, ``v64``) beyond float32 + rounding; empty when the two agree. + + A pair of sources may swap order only when the gap between their twin + variances lies inside the float32 band, the sum of the two sources' + measured ``|v32 - v64|``. The band's width is itself capped at + ``_SVAR_SAFETY`` times the study's largest per-source difference, and the + number of swapped pairs at ``_MAX_ORDER_SWAPS``. + """ + n = v64.size + d = np.abs(v32 - v64) + violations = [] + rel = d / v64 + cap = _SVAR_SAFETY * _SVAR_STUDY_MAX_REL + if rel.max() > cap: + violations.append( + f"source {int(rel.argmax())}'s variance differs by {rel.max():.2e} " + f"relative, over the {cap:.2e} float32 cap" + ) + rank32 = np.empty(n, dtype=np.int64) + rank32[order32] = np.arange(n) + rank64 = np.empty(n, dtype=np.int64) + rank64[order64] = np.arange(n) + i, j = np.triu_indices(n, k=1) + swapped = (rank32[i] - rank32[j]) * (rank64[i] - rank64[j]) < 0 + gap = np.abs(v64[i] - v64[j]) + band = d[i] + d[j] + wide = np.flatnonzero(swapped & (gap > band)) + if wide.size: + k = wide[np.argmax(gap[wide] / band[wide])] + violations.append( + f"{wide.size} swapped pairs lie outside the float32 band, worst " + f"sources {int(i[k])} and {int(j[k])}: gap {gap[k]:.3e} > band " + f"{band[k]:.3e}" + ) + n_swaps = int(swapped.sum()) + if n_swaps > _MAX_ORDER_SWAPS: + violations.append( + f"{n_swaps} swapped pairs, over the cap of {_MAX_ORDER_SWAPS}" + ) + return violations + + +def _variance_orders(model, ng, h: int): + """Both backends' order and per-source variances for model ``h``, after + checking that each order sorts its own variances in descending order.""" + order32, svar32 = model.variance_order(h, return_svar=True) + order64, svar64 = ng.variance_order(h, return_svar=True) + assert np.all(np.diff(svar32) <= 0) and np.all(np.diff(svar64) <= 0) + v32 = _per_component(order32, np.asarray(svar32, dtype=np.float64)) + v64 = _per_component(order64, np.asarray(svar64, dtype=np.float64)) + return order32, v32, order64, v64 + + +@pytest.fixture(scope="module") +def variance_order_fit(): + """One 2-model MLX fit and its float64 twin, shared by the + ``variance_order`` agreement test and its negative controls.""" + model = _fit_model(n_models=2, max_iter=30) + return model, _torch_twin(model) + + +def test_variance_order_matches_float64_twin(variance_order_fit): """``variance_order`` (issue #92, epic #278 polish round) agrees with the float64 twin on the actual ORDER, not just close variance values. - A near-tie between two sources' back-projected variance would make the - order genuinely ambiguous between float32 MLX and float64 torch even - though both computed the quantity correctly -- that risk is checked for, - not silently assumed away: the assertion below requires every consecutive - gap in the twin's own (float64) descending-sorted variances to clear - 1e-3 relative, well above the ~1e-4 to 1e-3 float32-vs-float64 - disagreement measured for ``get_mixing_matrix``/``get_rho`` above, so an - order mismatch here is a real regression rather than a coin flip on a - near-tied pair. A short (5-iteration) fit was tried first and rejected - for this reason -- its minimum gap was ~2e-4, inside the float32 noise - band -- so this test runs the fit longer (30 iterations) specifically to - reach a spectrum with real separation (measured minimum gap ~0.47%, - comfortably above the bound below); a shorter/noisier config is exactly - the kind of near-tie this assertion exists to catch and reject rather - than let the order check pass by luck. If a future data/config change - shrinks the gap back down, this assertion is designed to fail loudly (not - the order check) so the weaker config gets replaced rather than the order - check getting silently loosened. Multi-model, so ``model_idx`` routing is + The two orders must agree exactly except for swaps between sources whose + variance gap lies inside the float32 band measured here, with the band's + width and the number of swaps capped from the perturbation study recorded + above (see :func:`_order_violations`). This replaced a fixed 1e-3 + near-tie precondition on the twin's gaps: it assumed a float32 band about + 300 times wider than the measured one, and failed on a CI runner whose + GPU trajectory produced a 1.65e-4 gap, well clear of any float32 + ambiguity. ``test_variance_order_check_catches_a_divergence`` shows the + check fails on real divergences. Multi-model, so ``model_idx`` routing is exercised. """ - model = _fit_model(n_models=2, max_iter=30) - ng = _torch_twin(model) - + model, ng = variance_order_fit for h in range(2): - order_mlx, svar_mlx = model.variance_order(h, return_svar=True) - order_ng, svar_ng = ng.variance_order(h, return_svar=True) - - gaps = -np.diff(svar_ng) / np.maximum(svar_ng[:-1], svar_ng.max() * 1e-6) - assert gaps.min() > 1e-3, ( - f"model {h}: variance gaps too tight ({gaps.min():.2e}) for an " - "order comparison to be meaningful on this fit/config" - ) - - assert np.array_equal(order_mlx, order_ng), ( - f"model {h}: variance_order disagrees: {order_mlx} vs {order_ng}" - ) - err = _relerr(svar_mlx.astype(np.float64), svar_ng) - assert err < 1e-3, f"model {h}: variance_order svar differs by {err:.3e}" + violations = _order_violations(*_variance_orders(model, ng, h)) + assert not violations, f"model {h}: " + "; ".join(violations) + + +@pytest.mark.parametrize( + ("corruption", "expected"), + [ + ("reversed", "outside the float32 band"), + ("shuffled", "outside the float32 band"), + ("closest_pair_swap", "outside the float32 band"), + ("inflated_variance", "float32 cap"), + ], +) +def test_variance_order_check_catches_a_divergence( + variance_order_fit, corruption, expected +): + """Negative controls for :func:`_order_violations` on the real fit: the + MLX side's order reversed or shuffled; the closest adjacent pair whose + gap lies outside the float32 band swapped (the hardest real swap to + catch, and a single one, under the swap cap, so only the band can catch + it); or one source's variance moved by twice the float32 cap.""" + model, ng = variance_order_fit + order32, v32, order64, v64 = _variance_orders(model, ng, 0) + assert not _order_violations(order32, v32, order64, v64) + + if corruption == "reversed": + order32 = order32[::-1].copy() + elif corruption == "shuffled": + order32 = np.random.default_rng(0).permutation(order32) + elif corruption == "closest_pair_swap": + d = np.abs(v32 - v64)[order64] + gap = -np.diff(v64[order64]) + separated = np.flatnonzero(gap > d[:-1] + d[1:]) + k = int(separated[np.argmin(gap[separated])]) + pa, pb = (int(np.flatnonzero(order32 == src)[0]) for src in order64[k : k + 2]) + order32 = order32.copy() + order32[[pa, pb]] = order32[[pb, pa]] + else: + v32 = v32.copy() + v32[order32[0]] *= 1.0 + 2.0 * _SVAR_SAFETY * _SVAR_STUDY_MAX_REL + + violations = _order_violations(order32, v32, order64, v64) + assert any(expected in v for v in violations), violations def test_sensor_mixing_matrix_matches_twin_on_rank_reduced_fit(): diff --git a/pamica/tests/torch_tests/test_amica_ng_wrapper.py b/pamica/tests/torch_tests/test_amica_ng_wrapper.py index 74993036..de207a1b 100644 --- a/pamica/tests/torch_tests/test_amica_ng_wrapper.py +++ b/pamica/tests/torch_tests/test_amica_ng_wrapper.py @@ -17,7 +17,7 @@ from pamica.amica import AMICA from pamica.metrics import mir, pairwise_mi -from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.core import _KEEP_BEST_TOL, AMICATorchNG SAMPLE_DIR = Path(__file__).resolve().parents[2] / "sample_data" DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" @@ -240,21 +240,30 @@ def test_write_amica_output_ll_matches_kept_iterate(real_data, tmp_path): overshoot -- so a user reading mod.LL(end) in EEGLAB sees the loaded model's likelihood (review finding, #92).""" model = AMICA(n_models=2, n_mix=3, device="cpu", verbose=False) + # The overshoot recipe of test_ng_convergence.py (same data, block size + # and settings, so the same trajectory): it peaks at iteration 70 and stops + # via the loosened min_dll at 71. A fixed 60-iteration budget overshot on + # the development machine only; on the CI runners that run was monotone. model.fit( real_data[:, :4096], - max_iter=60, + max_iter=150, do_newton=True, newt_start=1, lrate=0.5, + newtrate=3.0, + use_min_dll=True, + min_dll=1e-4, + maxincs=2, + use_grad_norm=False, seed=0, block_size=1024, ) - if not model.is_fitted_: - pytest.skip("aggressive run ended degenerate; not the case under test") + assert model.is_fitted_, "the overshoot recipe ended degenerate: retune it" ng = model.model_ assert ng is not None and ng.final_ll_ is not None - if np.isclose(ng.ll_history[-1], ng.final_ll_): - pytest.skip("run was monotone; keep_best restore did not fire") + assert max(ng.ll_history) - ng.ll_history[-1] > _KEEP_BEST_TOL, ( + "the overshoot recipe no longer overshoots: retune it" + ) outdir = tmp_path / "amicaout" model.write_amica_output(str(outdir)) @@ -331,8 +340,7 @@ def test_loadmodout_sources_reproduce_live_transform_multimodel(real_data, tmp_p model = AMICA(n_models=2, n_mix=3, device="cpu", verbose=False) model.fit(real_data[:, :4096], max_iter=8, block_size=1024, seed=4) - if not model.is_fitted_: - pytest.skip("aggressive short fit ended degenerate; not the case under test") + assert model.is_fitted_, "the short fit ended degenerate: retune it" outdir = tmp_path / "amicaout" model.write_amica_output(str(outdir)) @@ -376,8 +384,7 @@ def test_written_w_bytes_are_genuine_fortran_layout(real_data, tmp_path): """ model = AMICA(n_models=2, n_mix=3, device="cpu", verbose=False) model.fit(real_data[:, :4096], max_iter=5, block_size=1024, seed=7) - if not model.is_fitted_: - pytest.skip("short fit ended degenerate; not the case under test") + assert model.is_fitted_, "the short fit ended degenerate: retune it" outdir = tmp_path / "amicaout" model.write_amica_output(str(outdir)) @@ -411,8 +418,7 @@ def test_load_results_returns_internal_w_multimodel(real_data, tmp_path): model = AMICA(n_models=2, n_mix=3, device="cpu", verbose=False) model.fit(real_data[:, :4096], max_iter=5, block_size=1024, seed=7) - if not model.is_fitted_: - pytest.skip("short fit ended degenerate; not the case under test") + assert model.is_fitted_, "the short fit ended degenerate: retune it" outdir = tmp_path / "amicaout" model.write_amica_output(str(outdir)) @@ -449,13 +455,13 @@ def test_loadmodout_sources_roundtrip_with_share_comps(real_data, tmp_path): share_iter=7, comp_thresh=0.85, ) - if not model.is_fitted_: - pytest.skip("short share_comps fit ended degenerate; not the case under test") + assert model.is_fitted_, "the short share_comps fit ended degenerate: retune it" ng = model.model_ assert ng is not None and ng.comp_list is not None and ng.gm is not None comp_list = ng.comp_list.detach().cpu().numpy() - if len(np.unique(comp_list)) == comp_list.size: - pytest.skip("no merge fired for this build; the sharing path is not exercised") + assert len(np.unique(comp_list)) < comp_list.size, ( + "no merge fired, so the sharing path is not exercised: retune the recipe" + ) outdir = tmp_path / "amicaout" model.write_amica_output(str(outdir)) diff --git a/pamica/tests/torch_tests/test_ng_backend.py b/pamica/tests/torch_tests/test_ng_backend.py index e1a043ca..cdaeb45b 100644 --- a/pamica/tests/torch_tests/test_ng_backend.py +++ b/pamica/tests/torch_tests/test_ng_backend.py @@ -489,7 +489,8 @@ def test_newton_posdef_mstep_composition(): (issue #21), so the posdef branch never fires in a plain fit. Here the finalized stats are forced positive definite to exercise that branch, and the resulting ``A`` is checked against an independent recomputation of the - exact composition (slice -> H -> ``A + lrate*(A@H)`` -> column rescale). + exact composition (slice -> H -> ``A + lrate*(A@H)`` -> component rescale: + each component is a ROW of the stored block, issue #333). """ data = _load_real_data() blk = 256 @@ -519,9 +520,10 @@ def forced(_acc): idx = ng.comp_list[:, 0] A_expected = A_before.clone() A_expected[:, idx] = A_before[:, idx] - lrate_after * (H.T @ A_before[:, idx]) - scale = torch.sqrt((A_expected**2).sum(dim=0)) - nz = scale > 0 - A_expected[:, nz] = A_expected[:, nz] / scale[nz] + block = A_expected[:, idx] + norm = torch.sqrt((block**2).sum(dim=1, keepdim=True)) + # The production zero-norm guard: a zero-norm row is left as it is. + A_expected[:, idx] = block / torch.where(norm > 0, norm, torch.ones_like(norm)) with mock.patch.object(ng, "_finalize_newton_stats", side_effect=forced): ng._update_parameters(acc, blk) @@ -1398,14 +1400,19 @@ def test_keep_best_snapshot_restore_roundtrip(): assert torch.equal(getattr(m, name), snap_val), name -def _multimodel_keep_best(seed: int, keep_best: bool) -> AMICATorchNG: +def _multimodel_keep_best(keep_best: bool) -> AMICATorchNG: + """The aggressive-Newton overshoot recipe of ``test_ng_convergence.py`` on + the same 4096 real samples, so the same trajectory: it peaks at iteration + 70 and stops via the loosened ``min_dll`` at 71. A fixed 60-iteration + budget without that stop overshot on the development machine only; on the + CI runners that run was monotone.""" m = AMICATorchNG( - n_channels=NW, n_models=2, n_mix=NMIX, seed=seed, device="cpu", - dtype=torch.float64, block_size=512, lrate=0.05, maxdecs=3, - do_newton=True, newt_start=50, newt_ramp=10, newtrate=1.0, - keep_best=keep_best, + n_channels=NW, n_models=2, n_mix=NMIX, seed=0, device="cpu", + dtype=torch.float64, block_size=1024, do_newton=True, newt_start=1, + lrate=0.5, newtrate=3.0, use_min_dll=True, min_dll=1e-4, maxincs=2, + use_grad_norm=False, keep_best=keep_best, ) # fmt: skip - m.fit(_load_real_data(), max_iter=100, verbose=False) + m.fit(_load_real_data()[:, :4096], max_iter=150, verbose=False) return m @@ -1417,11 +1424,18 @@ def test_keep_best_returns_within_tol_of_peak(): the raw ``ll_history[-1]``, which stays the true trajectory). keep_best does not change the optimization path, only which iterate is returned, so the ``keep_best=False`` run has the same trajectory but returns the (lower) last - iterate. seed 8 reaches the plateau where natural-gradient AMICA dips below - its own peak, so the restore branch runs; the invariants also hold if a - platform's BLAS makes the run monotone (see the explicit skip below).""" - on = _multimodel_keep_best(8, keep_best=True) - off = _multimodel_keep_best(8, keep_best=False) + iterate. + + Issue #333 changed the config. The #51 one (seed 8, ``lrate=0.05``, Newton + from iteration 50, 100 iterations on the whole record) dipped below its + own peak only because ``doscaling`` normalized stored columns; with + components rescaled it runs monotone (as do seeds 0 to 5), so the restore + branch never ran. The aggressive-Newton recipe used instead genuinely + overshoots. The recompute check now also comes after the overshoot guard: + it holds only for a restored iterate, whose parameters are the ones that + produced ``final_ll_``, not for a monotone run's post-update parameters.""" + on = _multimodel_keep_best(keep_best=True) + off = _multimodel_keep_best(keep_best=False) # keep_best does not alter the trajectory, only the returned iterate. assert on.ll_history == off.ll_history @@ -1434,35 +1448,57 @@ def test_keep_best_returns_within_tol_of_peak(): # The returned LL is within tolerance of the peak and never below the last. assert abs(on.final_ll_ - peak) <= _KEEP_BEST_TOL assert on.final_ll_ >= on.ll_history[-1] - # The returned parameters really sit at final_ll_ (recompute the E-step LL). - data = _load_real_data() - X_t = on._preprocess(data) + + # The restore branch must run, or this test is vacuous: fail, not skip. + assert peak - on.ll_history[-1] > _KEEP_BEST_TOL, ( + "the overshoot recipe no longer overshoots: retune it" + ) + # It did overshoot, so keep_best strictly beat return-last ... + assert on.final_ll_ > off.final_ll_ + # ... and the restored parameters really sit at final_ll_ (recompute the + # E-step LL). + X_t = on._preprocess(_load_real_data()[:, :4096]) acc = on._accumulate_blocks(X_t) ll_model = float(acc["ll"] / (X_t.shape[1] * NW)) assert abs(ll_model - on.final_ll_) < 1e-9 - # Make branch coverage visible rather than silently vacuous: if this run did - # not overshoot on this platform, the restore branch was not exercised. - if peak - on.ll_history[-1] <= _KEEP_BEST_TOL: - pytest.skip("seed 8 did not overshoot here; restore branch not exercised") - # It did overshoot, so keep_best strictly beat return-last. - assert on.final_ll_ > off.final_ll_ - @pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") def test_keep_best_inactive_under_reject(): """The safeguard is disabled under ``do_reject`` (the good-sample set, hence the LL normalization, changes across iterations, so per-iteration LLs are not comparable): ``final_ll_`` is exactly the last trajectory value, no restore - fires (issue #51).""" - data = _load_real_data() - m = AMICATorchNG( - n_channels=NW, n_models=2, n_mix=NMIX, seed=SEED, device="cpu", - dtype=torch.float64, block_size=512, do_reject=True, rejsig=2.0, - rejstart=2, rejint=3, maxrej=2, keep_best=True, + fires (issue #51). + + Non-vacuous on both counts: the recipe (the aggressive-Newton overshoot + recipe of ``test_ng_convergence.py``, ``_OVERSHOOT_KWARGS``, with its + loosened ``min_dll`` stop) restores without + ``do_reject`` (peak at iteration 70, stop at 71, 1.9e-4 below the peak), + and with ``do_reject`` its own trajectory also ends below an earlier peak + (iteration 57, stop at 59, 2.2e-4 below), so a restore would fire if the + safeguard were active. The previous config (``lrate=0.1`` natural + gradient, 12 iterations) was monotone, so it could not have failed. + """ + x = _load_real_data()[:, :4096] + kwargs: dict[str, Any] = dict( + n_channels=NW, n_models=2, n_mix=NMIX, seed=0, device="cpu", + dtype=torch.float64, block_size=1024, do_newton=True, newt_start=1, + lrate=0.5, newtrate=3.0, use_min_dll=True, min_dll=1e-4, maxincs=2, + use_grad_norm=False, keep_best=True, ) # fmt: skip - m.fit(data, max_iter=12, verbose=False) + plain = AMICATorchNG(**kwargs) + plain.fit(x, max_iter=150, verbose=False) + assert plain.stop_reason not in AMICATorchNG._DEGENERATE_STOP_REASONS + assert max(plain.ll_history) - plain.ll_history[-1] > _KEEP_BEST_TOL + assert plain.final_ll_ == max(plain.ll_history) > plain.ll_history[-1] + + m = AMICATorchNG( + **kwargs, do_reject=True, rejsig=3.0, rejstart=5, rejint=5, maxrej=1 + ) + m.fit(x, max_iter=150, verbose=False) + assert m.stop_reason not in AMICATorchNG._DEGENERATE_STOP_REASONS assert m.numrej >= 1 # rejection actually fired, so the good set changed + assert max(m.ll_history) - m.ll_history[-1] > _KEEP_BEST_TOL assert m.final_ll_ == m.ll_history[-1] # no best-iterate restore under reject @@ -1567,10 +1603,15 @@ def test_rholrate_ratchets_at_maxdecs_not_per_decrease(): several LL decreases. The surviving ``rholrate`` must have ratcheted exactly as often as ``newtrate`` (both gated on ``iter > newt_start`` at ``maxdecs``, matched 0.5 factor here), NOT once per decrease. + + ``newtrate=3.0`` since issue #333: with ``doscaling`` rescaling components + instead of stored columns, the ``newtrate=1.0`` run is monotone through all + 300 iterations (no decrease, so no ratchet to test); at 3.0 it decreases 6 + times and both ceilings ratchet twice. """ data = _load_real_data() m = _fresh_ng( - block_size=512, do_newton=True, newt_start=50, newtrate=1.0, lrate=0.05, + block_size=512, do_newton=True, newt_start=50, newtrate=3.0, lrate=0.05, lratefact=0.5, rholrate=0.05, rholratefact=0.5, maxdecs=3, ) # fmt: skip m.fit(data, max_iter=300, verbose=False) diff --git a/pamica/tests/torch_tests/test_ng_convergence.py b/pamica/tests/torch_tests/test_ng_convergence.py index 1b130d35..a2ddb7d4 100644 --- a/pamica/tests/torch_tests/test_ng_convergence.py +++ b/pamica/tests/torch_tests/test_ng_convergence.py @@ -330,13 +330,13 @@ def test_a_frozen_window_still_computes_fresh_grad_norm(real_data): (amica15.f90:1803). ``_update_parameters`` was refactored so the direction/dAk/ndtmpsum computation runs unconditionally too, with only the step itself gated on ``not self._a_frozen()``. This test proves both - halves of that refactor: the mixing matrix's per-column DIRECTION does not - move during a frozen iteration (only ``doscaling``'s unconditional - unit-norm rescale touches its magnitude -- that rescale is a separate, - non-frozen Fortran block, amica15.f90:1843-1854, so it is expected to - still apply), AND ndtmpsum is NOT a stale repeat of the pre-freeze value - across those same iterations (which would happen if the computation were - still skipped). + halves of that refactor: during a frozen iteration the mixing matrix takes + no gradient step -- it ends exactly where ``doscaling``'s unconditional + component rescale alone takes it (that rescale is a separate, non-frozen + Fortran block, amica15.f90:1843-1854, so it is expected to still apply), + while an unfrozen iteration does not -- AND ndtmpsum is NOT a stale repeat + of the pre-freeze value across those same iterations (which would happen if + the computation were still skipped). """ ng = _fresh_ng( n_models=2, @@ -352,23 +352,22 @@ def test_a_frozen_window_still_computes_fresh_grad_norm(real_data): ) trace = [] original = ng._update_parameters + # Applies the backend's own rescale to a copy of the pre-update A, i.e. A as + # it would be if this iteration took no gradient step at all. + probe = _fresh_ng(n_models=2) def spy(acc, n): - assert ng.A is not None + assert ng.A is not None and ng.mu is not None and ng.beta is not None + assert ng.comp_list is not None frozen = ng._a_frozen() - a_before = ng.A.clone() + probe.A, probe.mu, probe.beta = ng.A.clone(), ng.mu.clone(), ng.beta.clone() + probe.comp_list = ng.comp_list.clone() + probe._rescale_components() result = original(acc, n) - # Direction check robust to doscaling's per-column rescale: normalize - # both snapshots to unit columns before comparing, so only an actual - # gradient step (not a magnitude rescale) can fail this. - before_dir = a_before / a_before.norm(dim=0, keepdim=True) - after_dir = ng.A / ng.A.norm(dim=0, keepdim=True) trace.append( { "frozen": frozen, - "direction_changed": not torch.allclose( - before_dir, after_dir, atol=1e-12 - ), + "stepped": not torch.equal(ng.A, probe.A), "ndtmpsum": ng._ndtmpsum, } ) @@ -380,8 +379,10 @@ def spy(acc, n): frozen_iters = [t for t in trace if t["frozen"]] assert frozen_iters, "test setup: no frozen iteration occurred in this run" for t in frozen_iters: - assert not t["direction_changed"] + assert not t["stepped"] assert t["ndtmpsum"] is not None and math.isfinite(t["ndtmpsum"]) + # The check can fail: every unfrozen iteration does take a step. + assert all(t["stepped"] for t in trace if not t["frozen"]) # Fresh per-iteration values, not the same stale number repeated. assert len({round(v, 15) for v in (t["ndtmpsum"] for t in frozen_iters)}) > 1 # comp_used is exercised: the merge dropped at least one component. @@ -465,6 +466,24 @@ def test_grad_norm_floor_stop_leaves_wrapper_usable(real_data, tmp_path): # --- keep_best / do_reject interaction (early stopping must not break them) - +# The genuine-overshoot recipe (real 2-model data, aggressive Newton): it peaks +# and then stops via the loosened min_dll a couple of iterations later (see +# test_keep_best_restores_genuine_overshoot_under_min_dll_stop). +_OVERSHOOT_KWARGS: dict[str, Any] = dict( + n_models=2, + seed=0, + do_newton=True, + newt_start=1, + lrate=0.5, + newtrate=3.0, + block_size=1024, + use_min_dll=True, + min_dll=1e-4, + maxincs=2, + use_grad_norm=False, +) +_OVERSHOOT_MAX_ITER = 150 + def test_keep_best_restores_genuine_overshoot_under_min_dll_stop(real_data): """keep_best (issue #51) must actually exercise its restore branch, not @@ -477,25 +496,17 @@ def test_keep_best_restores_genuine_overshoot_under_min_dll_stop(real_data): This config is the known non-monotone recipe from ``test_write_amica_output_ll_matches_kept_iterate`` (issue #92, ``test_amica_ng_wrapper.py``: real 2-model data, aggressive - ``do_newton``/``lrate``), combined with a loosened ``min_dll`` so the run - stops via the NEW ``min_dll`` stop_reason a few iterations after its - peak, not via ``max_iter`` and not via a monotone approach to that peak. + ``do_newton``/``lrate``/``newtrate``), combined with a loosened ``min_dll`` + so the run stops via the NEW ``min_dll`` stop_reason a few iterations + after its peak, not via ``max_iter`` and not via a monotone approach to + that peak. ``newtrate=3.0`` and the 150-iteration budget were added for + issue #333: with ``doscaling`` rescaling components (not stored columns) + the old ``newtrate=0.5``/60-iteration recipe runs monotone to + ``max_iter``; this one peaks at iteration 70 and stops at 71. """ x = real_data[:, :4096] - ng = _fresh_ng( - n_models=2, - seed=0, - do_newton=True, - newt_start=1, - lrate=0.5, - block_size=1024, - use_min_dll=True, - min_dll=1e-4, - maxincs=2, - use_grad_norm=False, - keep_best=True, - ) - ng.fit(x, max_iter=60, verbose=False) + ng = _fresh_ng(**_OVERSHOOT_KWARGS, keep_best=True) + ng.fit(x, max_iter=_OVERSHOOT_MAX_ITER, verbose=False) assert ng.stop_reason == "min_dll" assert ng.final_ll_ == max(ng.ll_history) assert ng.final_ll_ in ng.ll_history @@ -678,17 +689,28 @@ def test_min_dll_stop_reachable_at_shipped_default_threshold(real_data): kind of seed/newt_start probing the other tests in this module already document doing), not a threshold change. + ``n_mix=1`` since issue #333. The three-component config this used before + stopped at iteration 326 only because the old ``doscaling`` normalized + stored columns, which perturbed every iteration; once it rescaled + components (an exact change of scale), that config kept gaining about 1e-8 + to 1e-7 per iteration through 2000 iterations without ever posting six + consecutive gains below 1e-9, and the native reference binary, seeded + from the same initialization, did the same. The one-component config + stops at iteration 368 on macOS-arm64 with its last gains between 8e-11 + and 9e-10, all far above the 7.6e-15 a raw, unnormalized comparison would + need (1e-9 / (4096 * 32)), so it still tells the two scales apart. + The budget is deliberately generous. The iteration at which the stop fires - varies by more than 3x with the BLAS in use: measured at 326 on macOS-arm64, - 412 on Linux-x86_64 with a CUDA-enabled torch build, and 1076 on the GitHub - Linux runner. An earlier version of this test used ``max_iter=500`` and + varies by more than 3x with the BLAS in use: measured (on the earlier + config) at 326 on macOS-arm64, 412 on Linux-x86_64 with a CUDA-enabled + torch build, and 1076 on the GitHub Linux runner. An earlier version of this test used ``max_iter=500`` and failed CI twice, first on the stop reason and then on a leftover ``len(ll_history) < 500`` bound (PR #213). The claim under test is that the default threshold is reachable at all, not that it is reached by any particular iteration, so both the budget and the bound track the budget rather than a constant fitted to one machine. """ - ng = _fresh_ng(seed=1, do_newton=True, newt_start=5, block_size=1024) + ng = _fresh_ng(seed=1, n_mix=1, do_newton=True, newt_start=5, block_size=1024) ng.fit(real_data[:, :4096], max_iter=2000, verbose=False) assert ng.stop_reason == "min_dll" # A real early stop rather than exhausting the budget. Bound against the @@ -775,20 +797,8 @@ def test_mir_history_survives_keep_best_restore(real_data): and the test would pass under the bug. Recording every iteration puts a waypoint strictly inside that window.""" x = real_data[:, :4096] - ng = _fresh_ng( - n_models=2, - seed=0, - do_newton=True, - newt_start=1, - lrate=0.5, - block_size=1024, - use_min_dll=True, - min_dll=1e-4, - maxincs=2, - use_grad_norm=False, - keep_best=True, - ) - ng.fit(x, max_iter=60, verbose=False, mir_step=1) + ng = _fresh_ng(**_OVERSHOOT_KWARGS, keep_best=True) + ng.fit(x, max_iter=_OVERSHOOT_MAX_ITER, verbose=False, mir_step=1) assert ng.stop_reason == "min_dll" assert ng.final_ll_ != ng.ll_history[-1] # the restore branch fired diff --git a/pamica/torch_impl/core.py b/pamica/torch_impl/core.py index a22ce4ba..9414d381 100644 --- a/pamica/torch_impl/core.py +++ b/pamica/torch_impl/core.py @@ -46,6 +46,7 @@ import logging import math +import numbers import time from typing import Dict, List, Optional, Sequence, Tuple, Union @@ -489,8 +490,15 @@ class AMICATorchNG: invsigmin, invsigmax : float Clamp bounds for the mixture scale parameter ``beta``. doscaling, scalestep : bool, int - Whether/how often to rescale ``A`` columns to unit norm each - iteration (with matching ``mu``/``beta`` rescale). + Whether/how often to rescale each component's mixing vector (a row of + its model's stored ``A`` block) to unit norm, with the matching + ``mu``/``beta`` rescale, an exact change of scale + (see :meth:`_rescale_components`). The rescale runs on iterations + ``scalestep``, ``2*scalestep``, ... counted from 1; the default 1 + rescales every iteration, as the reference always does (it ignores + ``scalestep``). ``scalestep`` is validated only when ``doscaling`` is + on (an integer >= 1, or the constructor raises ``ValueError``); with + ``doscaling`` off it is inert, never read. share_comps : bool, default=False Enable multi-model component sharing (Fortran ``share_comps`` / ``identify_shared_comps``, amica15.f90:1916): components that are @@ -762,6 +770,14 @@ def __init__( self.doscaling = doscaling self.scalestep = scalestep + if doscaling and ( + isinstance(scalestep, bool) + or not isinstance(scalestep, numbers.Integral) + or scalestep < 1 + ): + # A zero cadence divides by zero mid-fit; a fractional one fires on + # no meaningful schedule. The reference never reads scalestep. + raise ValueError(f"scalestep must be an integer >= 1, got {scalestep!r}") # Component sharing (Fortran share_comps / identify_shared_comps trigger # amica15.f90:1856, subroutine :1916-1963): periodically merge mixing @@ -1821,16 +1837,53 @@ def _update_parameters(self, acc: Dict[str, torch.Tensor], n_samples: int): self.A = self.A - self.lrate * dAk - if self.doscaling and (self.iteration % self.scalestep == 0): - assert self.A is not None and self.mu is not None and self.beta is not None - scale = torch.sqrt((self.A**2).sum(dim=0)) # (n_comps,) - nonzero = scale > 0 - self.A[:, nonzero] = self.A[:, nonzero] / scale[nonzero] - self.mu[:, nonzero] = self.mu[:, nonzero] * scale[nonzero] - self.beta[:, nonzero] = self.beta[:, nonzero] / scale[nonzero] + # The reference rescales every iteration (it parses ``scalestep`` but + # never reads it, amica15.f90:1843/3686); pamica keeps ``scalestep`` as + # an extension counted from 1 like the reference's other cadences + # (iterations s, 2s, ...), so the default 1 is the reference. The + # 1-based rule moves to ``schedule.every`` when epic #324 Phase 9 + # (issue #335) lands. + if self.doscaling and (self.iteration + 1) % self.scalestep == 0: + self._rescale_components() self._update_unmixing_matrices() + def _rescale_components(self) -> None: + """Rescale every component to a unit-norm mixing vector (Fortran + ``doscaling``, amica15.f90:1843-1851), an exact change of scale. + + Each model's stored block ``A[:, comp_list[:, h]]`` is the transpose of + the reference's per-model mixing matrix (issue #24 convention), so + source ``i`` of model ``h`` is ROW ``i`` of that block, the reference's + column ``A(:,k)``. Dividing that row by its norm scales source ``i`` up + by the norm; ``mu[:, comp_list[i, h]] *= norm`` and + ``beta[:, comp_list[i, h]] /= norm`` rescale its density to match, so + the log-likelihood is unchanged. Normalizing stored COLUMNS instead + (the rule before issue #333) is not a change of scale of any component + and perturbed the fit every iteration. A zero-norm row is left + untouched, as in the reference (``Anrmk > 0``). + + Models are rescaled in order. Their blocks are disjoint unless + ``share_comps`` merged a column; a shared stored column then belongs to + rows of several blocks, where this per-block rule is not well defined. + It is applied uniformly anyway: the component-row layout of epic #324 + Phase 8 (issue #334) replaces it. ADR 0006 records the convention. + """ + assert ( + self.A is not None + and self.mu is not None + and self.beta is not None + and self.comp_list is not None + ) + for h in range(self.n_models): + idx = self.comp_list[:, h] + block = self.A[:, idx] # row i = source i of model h + norm = torch.sqrt((block**2).sum(dim=1)) # (n_channels,) + scale = torch.where(norm > 0, norm, torch.ones_like(norm)) + self.A[:, idx] = block / scale.unsqueeze(1) + self.mu[:, idx] = self.mu[:, idx] * scale + self.beta[:, idx] = self.beta[:, idx] / scale + def _a_frozen(self) -> bool: """Whether the A-update (and its lrate ramp) is held this iteration. diff --git a/paper.pdf b/paper.pdf index 9a770e4a136d65b4af9f62f18f5347a3256bebc9..341265443be049a87d92b6bdb77050dfd9989f1d 100644 GIT binary patch delta 239 zcmdn-U2Mm9v4$4L7N#xCzdo{?S{YbcnV4>8{KWi{5yqST@iVh2oTK}N*&N2%p8bVc zf>FgF(Za~UG|foY%qYoB*Cg38NjJ&RI8`^zJk1;^Y-XOAn%KVlEA#f{Us>#SDVZ8r znwUmuD(L&>r?@1Rq$+5*SQ!}@nHg9b85@8lx3h0x4d8IIurP5oF)^?(u(Y(WFg7%I mHgvYIa56DAGBGi;G;y)CQ?MbdBzC%CC$rr4-8)$SG64WzH$;B` delta 239 zcmdn-U2Mm9v4$4L7N#xCzdo`XS(%zy8CY&-{KWi{5yqST@iVh2oTK}N*&N2%p8bVc zf>FiLA|*N1#LP(7%skOp*TmS;RM#TeGD+7g+0wuiD4t}PlH9)hEA#f{Us>#SDH)lX z8CXVXD(L&>r?@1Rq$+5*SQ!}@0R@eW4ZxDy**CBTaJV_Unp&7UnK>F6TAH~T7`Paj kI+{Bgn_4(I8=E+rnY!93*br6{JKeC8S#JC89jt$u05T^;lK=n! From 0930c0e68ec9e2bbef3d20d51cff4c03029248f2 Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Wed, 23 Sep 2026 02:13:10 -0700 Subject: [PATCH 11/26] Phase 9: align iteration-schedule gates with the reference (#338) * Count schedule gates from 1 like the reference Route every iteration-schedule gate in the torch, NumPy and MLX backends through a shared pamica/schedule.py. The Newton switch (iter .ge. newt_start), the numdecs reset on the switch-on iteration, the iter > newt_start ceiling ratchets, the outlier-rejection schedule and NumPy's restart-on-NaN window compared the 0-based loop index with the reference's 1-based setting and fired one iteration late. Share, kurtosis, A-freeze and writestep/histstep gates were already 1-based and now use the same helpers. Seeded against the v0.3.3 binary (newt_start=3, doscaling off, 6 iterations) the LL deviation drops from 1.2e-3 to 5.4e-11 (torch) and 2.1e-12 (NumPy). * Update schedule-dependent tests to 1-based gates Tests whose configurations or replays encoded the old 0-based newt_start/rejstart: the aggressive-Newton keep_best recipe moves from newt_start=1 to 2 (the exact pre-change trajectory its documented measurements came from), the LLt reject tests fire on iteration 6 of a 6-iteration fit, and the MLX ratchet/reset replays spell out the reference's 1-based arithmetic. Tested: the 11 affected files pass (179 passed, 1 skipped). * Test schedule gates against 1-based iterations Cross-backend real-fit tests (torch, NumPy, MLX): the first Newton step is iteration newt_start, the rho-rate ceiling ratchet opens only after it, the switch-on counter reset follows the reference's replay, rejection first fires on iteration rejstart, and NumPy's restart-on-NaN window covers the first restartiter iterations. Tested: 15 passed; against the pre-fix backends 10 of them fail. * Add gated native oracle for the Newton start Seeds the pinned v0.3.3 binary with pamica's own init (indir/load_*), newt_start=3, doscaling off, single-threaded, 6 iterations. Torch and NumPy match its LL trajectory and A to round-off (torch LL 5.4e-11, A 4.2e-10; NumPy LL 2.1e-12, A 1.1e-10) where the pre-fix code deviated by 1.25e-3 and 3.1e-2. A no-Newton control shows the compared window is Newton-sensitive. Tested with AMICA_RUN_FORTRAN=1: 3 passed; 2 fail on the pre-fix backends. * Document the 1-based schedule gates Changelog entry for issue #335: which gates moved, the native-oracle numbers before and after, the default-path cases that change, and how to reproduce an old trajectory. No row in docs/guides/amica-differences.md changes: the anchored share A-freeze is already recorded there. * Rebuild paper.pdf * Validate iteration-schedule settings One shared validator, schedule.validate_iteration_setting, called by all three constructors with identical messages: newt_start must be an integer >= 0 whether or not do_newton is on (it also gates the rho-rate ratchet), and rejstart an integer >= 1 when do_reject is on (with 1-based counting, rejstart <= 0 skipped the reference's unconditional first pass; NumPy accepted 0, torch and MLX any value). NumPy also validates restartiter >= 0, maxrestarts >= 0 and histstep >= 1 under do_history (histstep=0 was a bare ZeroDivisionError mid-fit), and documents that restartiter=0 disables restart-on-NaN. schedule.every and periodic_due raise ValueError for a non-positive interval. Docstrings: newton_active cites amica15.f90:1666, newt_start=0 and 1 are stated to fit identically, and the MLX newt_start/newtrate entries document the newtrate ratchet. Tested: the new validation, restart-window and interval tests pass (36 passed with the reject suites). * Reflow MLX schedule docstrings The share_start/share_iter and kurt_start entries no longer break mid-clause. No code change. * Record NumPy restart-on-NaN difference Row 14 of the differences guide: the reference allows maxrestarts+1 restarts and, because startover is never cleared (amica15.f90:1046), never calls update_params again after the first one (:1115-1122); the NumPy backend allows maxrestarts restarts and resumes fitting. PyTorch and MLX have no restart-on-NaN path and stop with nan_ll. * Correct the schedule changelog entry Use the measured pre-fix deviations (1.25e-3 over 6 iterations, 1.33e-3 over 100) in the changelog and the oracle test docstring, list restartiter among the settings that now count from 1, and add the new validation. * Test schedule gates at their boundaries The arithmetic test gains the =1 rows for every helper (newton_switches_on at 1 and 0, past_newton_start at newt_start and newt_start+1, rejection with rejstart=1/rejint=1 through maxrej exhaustion, every/periodic_due at 1, the restart window at 1 and 0). The first-Newton-step test runs at newt_start 1 and 5, and a new real-fit test on every backend shows newt_start=0 and 1 give bit-identical trajectories on an overshooting run whose ratchets fire. Tested: 12 passed. * Make the switch-on reset test discriminating The replay-based reset tests passed with the reset one iteration late too. The cross-backend test now uses a searched configuration (4096 frames, lrate=0.8, newt_ramp=1, maxdecs=3, newt_start=3) whose likelihood decreases on iterations 2, 3 and 4, where the reference rule predicts no ratchet and the one-late rule predicts one; the fitted lrate ceiling must match the reference. It fails on all three backends with only newton_switches_on reverted and on the pre-fix backends. The MLX twin, which could not discriminate, is removed in favor of it. Tested: 3 passed; 3 failed with the reset reverted. * Test the rho-rate gate at newt_start=20 A searched natural-gradient configuration (4096 frames, seed 4, lrate 0.5, maxdecs 2) completes a maxdecs cycle on iteration 21 on all three backends, so the rho-rate ratchet gate is now pinned at the shipped default: open for newt_start=20, closed for 21. This is the one default-path change of issue #335. Tested: 3 passed; 3 fail on the pre-fix backends. * Document the reset-test configuration search States the full search that selected the switch-on reset configuration, including the two-model hit found after the first commit. * Shift Phase 7 recipes to 1-based settings Phase 7 retuned its overshoot recipes under 0-based gating. With newt_start and rejstart counted from 1, n becomes n+1 so each documented trajectory reproduces bit for bit: the min_dll overshoot recipe (newt_start 2, newtrate 3.0) again peaks at 70 and stops at 71 on torch (1.9e-4 below the peak) and at 63/64 on MLX (4.2e-4); its do_reject variants move rejstart 5 to 6; the doscaling-rows NEWTON state uses newt_start=2; the min_dll-reachable test uses 6 and the slow rholrate ratchet test 51. Tested: the affected suites pass. * Route scalestep through the schedule module Phase 7's inline (iteration + 1) % scalestep rule becomes schedule.every in all three backends, and its scalestep check becomes schedule.validate_iteration_setting (same semantics and message; still only when doscaling is on), which drops the numbers imports it needed. Tested: test_doscaling_rows passes. * Re-search the newt_start=20 gate configuration Phase 7's component-row doscaling moved the old configuration's second ratchet off iteration 21, and the test's precondition guard failed as intended. The re-searched configuration (4096 frames, seed 1, lrate 0.6, maxdecs 3, newt_ramp 1) decreases at ll indices 4, 5, 8 and 9, 19, 20 on all three backends (smallest decrease 4.7e-4), ratcheting at 8 and 20. Tested: 3 passed. * Note the combined doscaling and Newton-start parity Changelog: with doscaling on (component rows, issue #333) the seeded 100-iteration Newton comparison stays within 3.9e-6 (torch) and 7.0e-6 (NumPy), where the #333 fix alone left 2.1e-4; scalestep now uses the shared schedule helper. --------- Co-authored-by: github-actions[bot] --- docs/changelog.md | 38 + docs/guides/amica-differences.md | 4 +- pamica/mlx_impl/core.py | 134 ++-- pamica/numpy_impl/core.py | 140 ++-- pamica/schedule.py | 154 ++++ pamica/tests/mlx_tests/test_mlx_export.py | 2 +- pamica/tests/mlx_tests/test_mlx_keepbest.py | 9 +- pamica/tests/mlx_tests/test_mlx_llt_stash.py | 7 +- pamica/tests/mlx_tests/test_mlx_mir.py | 2 +- pamica/tests/mlx_tests/test_mlx_newton.py | 110 +-- pamica/tests/mlx_tests/test_mlx_reject.py | 28 +- pamica/tests/test_doscaling_rows.py | 9 +- pamica/tests/test_llt_stash.py | 14 +- .../tests/test_mlx_keepbest_cross_backend.py | 2 +- pamica/tests/test_numpy_reject.py | 3 +- pamica/tests/test_schedule_gates.py | 712 ++++++++++++++++++ pamica/tests/test_schedule_native_oracle.py | 245 ++++++ .../torch_tests/test_amica_ng_wrapper.py | 2 +- pamica/tests/torch_tests/test_ng_backend.py | 14 +- .../tests/torch_tests/test_ng_convergence.py | 8 +- pamica/torch_impl/core.py | 113 +-- 21 files changed, 1450 insertions(+), 300 deletions(-) create mode 100644 pamica/schedule.py create mode 100644 pamica/tests/test_schedule_gates.py create mode 100644 pamica/tests/test_schedule_native_oracle.py diff --git a/docs/changelog.md b/docs/changelog.md index 68bf237e..1c5ba758 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -81,6 +81,44 @@ Release notes are also published on the `plot_model_comparison` skipped the sphere. They now plot the model's own sensor maps and sources (what `get_sensor_mixing_matrix` and `transform` return), checked against those accessors on a real fit. `load_results` also reads a rank-reduced fit's zero-padded sphere, which it used to reject. +- **Phase 9 of epic #324: iteration schedules count from 1, as the reference's do (issue #335).** + **Behavior change:** `newt_start`, `rejstart` and (on NumPy) `restartiter` now name iterations counted from 1, like the reference's `iter` (amica15.f90:949): + the first Newton M-step is the `newt_start`-th iteration's, the first rejection follows the `rejstart`-th, + and a non-finite likelihood restarts the fit only within the first `restartiter` iterations. + The PyTorch, NumPy and MLX backends compared their 0-based loop index with these 1-based settings, + so each of the following fired one iteration late or, for the restart window, covered one iteration too many: + - the Newton switch (`iter .ge. newt_start`), the decrease-counter reset on the switch-on iteration (`iter == newt_start`), + and the `maxdecs` ratchet of the rho-rate ceiling and of `newtrate` (`iter > newt_start`); + - the outlier-rejection schedule (`iter == rejstart`, then every `rejint` iterations); + - the NumPy backend's restart-on-NaN window (`iter .le. restartiter`). + + Measured against the pinned v0.3.3 native binary, seeded with pamica's own initialization, `doscaling` off and single-threaded: + with `newt_start=3`, the first 6 iterations now match to round-off, + the log-likelihood within 5.4e-11 (PyTorch) and 2.1e-12 (NumPy) and `A` within 4.2e-10 and 1.1e-10, + where they deviated by 1.25e-3 and 3.1e-2 before. + Over 100 iterations with the reference's own `newt_start=50`, the largest log-likelihood deviation drops from 1.33e-3 to 6.7e-6 (PyTorch) and 4.1e-6 (NumPy). + That is the floor this recording sets before Newton even starts: + one mixture component's shape sits at `rho=1`, where the location update divides by `|y|` and amplifies round-off from one iteration to the next. + With `doscaling` on as well (the default, component rows since issue #333), the same 100-iteration comparison stays within 3.9e-6 (PyTorch) and 7.0e-6 (NumPy); + with only the #333 fix it was 2.1e-4 apart, the gap ADR 0006 attributed to this Newton start. + - Default fits (`do_newton=False`, `do_reject=False`) change in two narrow cases only. + A `maxdecs` ratchet that completes on exactly iteration `newt_start + 1` (21 by default) now tightens the rho-rate ceiling, as the reference's does. + On NumPy, a non-finite likelihood on iteration `restartiter + 1` (11 by default) now ends the fit instead of restarting it. + - To reproduce a trajectory from before this change, add 1 to `newt_start` and `rejstart` (and to `restartiter` on NumPy). + - **New validation**, with the same message on every backend: `newt_start` must be an integer >= 0 whether or not `do_newton` is on + (it also gates the rho-rate ratchet), and `rejstart` an integer >= 1 when `do_reject` is on + (with 1-based counting, `rejstart <= 0` silently skipped the reference's unconditional first pass; NumPy used to accept 0, PyTorch and MLX any value). + On NumPy, `restartiter` and `maxrestarts` must be integers >= 0, and `histstep` an integer >= 1 when `do_history` is on + (`histstep=0` was a bare `ZeroDivisionError` mid-fit). + `restartiter=0` disables restart-on-NaN, as in the reference. + NumPy's restart-on-NaN recovery itself differs from the reference's, which never resumes fitting after a restart; + that is now recorded as row 15 of the differences guide. + - Every schedule gate now lives in one shared module, `pamica/schedule.py`, which all three backends call. + The share-merge, A-freeze, kurtosis-switch and `writestep`/`histstep` schedules already counted from 1 and are unchanged, + and `scalestep` (1-based since issue #333) uses the same helper and validator. + - `iteration`, `ll_history` and `mir_history_` keep their 0-based indexing. + - Tests: `pamica/tests/test_schedule_gates.py` observes each gate through real fits on all three backends, + and `pamica/tests/test_schedule_native_oracle.py` (opt-in, `AMICA_RUN_FORTRAN=1`) is the native-binary comparison above. - **Phase 4 of epic #324: backend selection in `AMICA` and `AMICAICA` (issue #313).** `AMICA` and `AMICAICA` gain a `backend` parameter: `"torch"` (the default, `AMICATorchNG`, float64 Fortran parity) or `"mlx"` (`AMICAMLXNG`, Apple GPU, float32 only). diff --git a/docs/guides/amica-differences.md b/docs/guides/amica-differences.md index 890d86b9..077de221 100644 --- a/docs/guides/amica-differences.md +++ b/docs/guides/amica-differences.md @@ -26,12 +26,14 @@ that is not listed, that is a bug worth | 12 | `pcadb` | parsed (amica15.f90:3459-3461), never used | unset by default; when set alone, keeps the dimensions within `pcadb` dB of the largest eigenvalue; ignored when `pcakeep` is also set | a dB cut is a scale-free way to drop low-variance directions; letting `pcakeep` win preserves what a reference `input.param` that sets both (both bundled files do) means to the binary | leave `pcadb` unset (the default), or set `pcakeep` | | 13 | Preprocessing with `do_sphere=False` | divides each channel by its standard deviation and adds a log-determinant term to the likelihood (amica15.f90:516-526) | identity sphere with a zero log-determinant, on all three array backends: the data are fitted unscaled | not a deliberate choice: an existing divergence, found during epic #324 and not yet ported | none yet (issue #328) | | 14 | `scalestep` | parsed (amica15.f90:3686), never used; rescales every iteration (:1843) | rescales every `scalestep` iterations counted from 1; default 1 matches the reference | pamica has always honored the keyword, and keeping it costs nothing; since issue #333 it counts from 1 like the reference's live cadences (`writestep`, `histstep`) instead of firing on the first iteration | `scalestep=1`, the default | +| 15 | Restart after a non-finite likelihood (NumPy backend only) | within the first `restartiter` iterations, redraws `A` up to `maxrestarts + 1` times (`numrestarts > maxrestarts` ends the run, amica15.f90:1022-1049); `startover` is set at :1046 and never cleared, so after the first restart the binary never calls `update_params` again (:1115-1122) and the redrawn parameters are never fitted | redraws `A` up to `maxrestarts` times within the same window (counted from 1; `restartiter=0` disables it) and resumes fitting after each restart, with the learning rates and likelihood history reset; the restart uses up that iteration of `max_iter` | the reference's recovery path cannot resume, so its count has nothing to recover into; a restart that fits is the evident intent. PyTorch and MLX have no restart-on-NaN path at all: they stop on the non-finite likelihood (`stop_reason="nan_ll"`), and their `n_restarts` (row 10) is a different mechanism | none for the stalled loop; `maxrestarts` one higher reproduces the reference's count, and `restartiter=0` stops on the first non-finite likelihood, as the reference does with `restartiter=0` | Rows 1, 2 and 7 arrived with [ADR 0004](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0004-rank-deficient-input-handling.md); row 3 with ADR 0003; row 5 with issue #50, extended to the raw backends by issue #306; row 8 with issues #60 and #240; row 9 with issue #232; row 10 with issue #198; row 11 with issue #322 (ADR 0005); row 12 with issue #323; row 13 is recorded, not yet resolved, by issue #328; -row 14 with issue #333 (ADR 0006). +row 14 with issue #333 (ADR 0006); +row 15 with issue #335, which also made the NumPy restart window count from 1. Two `share_comps` details are pamica's own because the reference cannot decide them: the A-freeze window after a merge is anchored on `share_start` (the literal diff --git a/pamica/mlx_impl/core.py b/pamica/mlx_impl/core.py index 40e979d8..26f7939f 100644 --- a/pamica/mlx_impl/core.py +++ b/pamica/mlx_impl/core.py @@ -91,7 +91,6 @@ import json import logging import math -import numbers import time import zipfile from typing import List, Optional, Sequence, Tuple @@ -104,6 +103,7 @@ from .. import blocktune from .. import restarts +from .. import schedule from ..metrics import mir as mir_metric from ..metrics import model_probability_from_loglik, pairwise_mi from ..numpy_impl.utils import identify_shared_components @@ -384,11 +384,11 @@ class AMICAMLXNG: returned model but trails in ``final_ll_``; see that attribute's comment (issue #269). ``share_start`` (100) / ``share_iter`` (100) - Sharing schedule: first iteration to attempt merges and the interval - between attempts. The A-update is held for the first 6 iterations of - every cycle (whether or not a merge fired) so the densities can settle; - ``share_iter`` must be ``> 6`` so that window never consumes the whole - cycle, and ``share_start`` must be ``>= 1``. + Sharing schedule: first iteration to attempt merges (counted from 1) + and the interval between attempts. The A-update is held for the first + 6 iterations of every cycle (whether or not a merge fired) so the + densities can settle; ``share_iter`` must be ``> 6`` so that window + never consumes the whole cycle, and ``share_start`` must be ``>= 1``. ``comp_thresh`` (0.99) Cosine-similarity cutoff, in the de-sphered (sensor-space) metric, above which two mixing columns are identified and merged. Must be in @@ -401,7 +401,7 @@ class AMICAMLXNG: ``do_newton`` (False) Precondition the ``A``/``W`` natural gradient with the approximate - Hessian once ``iteration >= newt_start`` (Fortran ``do_newton``: the + Hessian from iteration ``newt_start`` on (Fortran ``do_newton``: the 2x2 solve and its positive-definiteness guard at amica15.f90:1718-1741, the ramp and fallback at :1803-1816). Natural gradient alone plateaus short of the Fortran solution; the Newton step is what closes the gap. @@ -409,13 +409,23 @@ class AMICAMLXNG: default fit is bit-for-bit what it was before #264. ``newt_start`` (20) Iteration at which the Newton step switches on (natural gradient runs - before it, letting the mixture parameters settle first). It also gates - the ``rholrate`` ceiling ratchet, independently of ``do_newton`` - (Fortran amica15.f90:1067). + before it, letting the mixture parameters settle first). Counted from + 1, as the reference counts it (``iter .ge. newt_start``): the Newton + step is taken on the ``newt_start``-th iteration, i.e. once + ``iteration + 1 >= newt_start`` for the 0-based ``iteration`` + attribute (issue #335). ``newt_start=0`` fits exactly as ``1`` does, + Newton from the first iteration. It also gates the ``maxdecs`` ratchet + of the ``rholrate`` ceiling, independently of ``do_newton``, and under + Newton that of ``newtrate``, to iterations after ``newt_start`` + (Fortran amica15.f90:1067/1070), so it is validated (an integer + >= 0) whether or not ``do_newton`` is on. ``newtrate`` (0.5) Maximum learning rate the ramp climbs to while Newton is active and positive definite; the natural-gradient phase (and any fallback - iteration) is capped at ``lrate_cap`` instead. + iteration) is capped at ``lrate_cap`` instead. A ceiling itself: under + Newton it ratchets down by ``lratefact`` at each ``maxdecs`` cycle + completed after iteration ``newt_start`` (amica15.f90:1070), and is + reset to its constructor value at the start of every fit. ``newt_ramp`` (10) Denominator of the per-iteration learning-rate ramp toward the current ceiling: ``lrate = min(ceiling, lrate + min(1/newt_ramp, lrate))``. @@ -446,12 +456,13 @@ class AMICAMLXNG: (code 0). ``kurt_start`` (3) / ``num_kurt`` (5) / ``kurt_int`` (1) Adaptive-switch schedule (only used when ``pdftype=1``): first - iteration to re-estimate kurtosis, number of switch passes, and the - iteration interval between them. ``num_kurt=0`` disables switching (the - family stays at its super-Gaussian init). No bit-exact oracle -- the - reference's own switch is dead code (``do_choose_pdfs`` is set but - ``m2sum``/``m4sum`` are never accumulated, amica15.f90:608-615) -- so - this is behavior-validated on real data (ADR 0002). + iteration to re-estimate kurtosis (counted from 1), number of switch + passes, and the iteration interval between them. ``num_kurt=0`` + disables switching (the family stays at its super-Gaussian init). No + bit-exact oracle -- the reference's own switch is dead code + (``do_choose_pdfs`` is set but ``m2sum``/``m4sum`` are never + accumulated, amica15.f90:608-615) -- so this is behavior-validated on + real data (ADR 0002). The block-size search parameters (issue #232) likewise carry AMICATorchNG's names, defaults and semantics: @@ -506,8 +517,11 @@ class AMICAMLXNG: Reject a sample when its log-likelihood is below ``mean - rejsig * std`` (population std) over the current good set. ``rejstart`` (2) / ``rejint`` (3) / ``maxrej`` (1) - Rejection schedule: first iteration to reject, interval between - subsequent passes, and the maximum number of passes. + Rejection schedule: first iteration to reject (counted from 1, as the + reference counts it; issue #335), interval between subsequent passes, + and the maximum number of passes. ``rejstart`` must be an integer + >= 1 when ``do_reject`` is on: ``rejstart <= 0`` would silently skip + the reference's unconditional first pass. The best-iterate safeguard (issue #51, epic #278 Phase 2/#288) likewise carries AMICATorchNG's name, default and semantics: @@ -672,7 +686,9 @@ def __init__( # do_newton (amica15.f90:1067) -- so it stays meaningful for a # natural-gradient fit too. newtrate is a CEILING that ratchets down at # maxdecs during fit, so keep the constructor value for the per-fit reset - # in _initialize_parameters (as lrate0/rholrate0 do). + # in _initialize_parameters (as lrate0/rholrate0 do). Validated whether + # or not do_newton is on, for that same ratchet gate. + schedule.validate_iteration_setting("newt_start", newt_start, 0) self.newt_start = newt_start self.newtrate = newtrate self.newtrate0 = newtrate @@ -700,6 +716,9 @@ def __init__( raise ValueError(f"rejsig must be > 0, got {rejsig}") if maxrej < 0: raise ValueError(f"maxrej must be >= 0, got {maxrej}") + # Counted from 1, so rejstart <= 0 would silently disable the + # reference's unconditional ``iter == rejstart`` pass. + schedule.validate_iteration_setting("rejstart", rejstart, 1) self.numrej = 0 self.good_idx: Optional[mx.array] = None @@ -759,14 +778,10 @@ def __init__( self.invsigmax = invsigmax self.doscaling = doscaling self.scalestep = scalestep - if doscaling and ( - isinstance(scalestep, bool) - or not isinstance(scalestep, numbers.Integral) - or scalestep < 1 - ): + if doscaling: # A zero cadence divides by zero mid-fit; a fractional one fires on # no meaningful schedule. The reference never reads scalestep. - raise ValueError(f"scalestep must be an integer >= 1, got {scalestep!r}") + schedule.validate_iteration_setting("scalestep", scalestep, 1) # Component sharing (issue #263), same names/defaults/validation as # AMICATorchNG (torch_impl/core.py). OFF by default and inert for @@ -1680,7 +1695,9 @@ def _update_parameters(self, acc: dict, n_samples: int): # it one line later and lambda silently uses the updated mu: no error, no # NaN, just a subtly wrong Hessian (the torch backend's issue #24 bug, # pinned there and here by test_newton_finalize_uses_preupdate_mu). - newton_active = self.do_newton and self.iteration >= self.newt_start + newton_active = schedule.newton_active( + self.do_newton, self.iteration, self.newt_start + ) if newton_active: sigma2, lambda_, kappa = self._finalize_newton_stats(acc) @@ -1848,10 +1865,9 @@ def _update_parameters(self, acc: dict, n_samples: int): # The reference rescales every iteration (it parses ``scalestep`` but # never reads it, amica15.f90:1843/3686); pamica keeps ``scalestep`` as # an extension counted from 1 like the reference's other cadences - # (iterations s, 2s, ...), so the default 1 is the reference. The - # 1-based rule moves to ``schedule.every`` when epic #324 Phase 9 - # (issue #335) lands. - if self.doscaling and (self.iteration + 1) % self.scalestep == 0: + # (iterations s, 2s, ...; ``schedule.every``), so the default 1 is the + # reference. + if self.doscaling and schedule.every(self.iteration, self.scalestep): self._rescale_components() # rho is frozen for every non-GG family (self.dorho is False), so the @@ -2176,7 +2192,8 @@ def _a_frozen(self) -> bool: amica15.f90:1803). The window fires each cycle regardless of whether that cycle's :meth:`_identify_shared_comps` actually merged a pair. - Anchored on ``(itf - share_start) % share_iter`` so it stays aligned with + Anchored on ``share_start`` (:func:`pamica.schedule.share_freeze`, which + counts iterations from 1 like the reference) so it stays aligned with the merge schedule for any ``share_start``; the literal Fortran formula uses ``mod(iter, share_iter)`` (misaligned unless share_start is a multiple of share_iter, and a permanent freeze for ``share_iter <= 6``), @@ -2188,10 +2205,7 @@ def _a_frozen(self) -> bool: """ if not self.share_comps or self.n_models < 2: return False - itf = self.iteration + 1 # Fortran-style 1-indexed iteration - if itf < self.share_start: - return False - return (itf - self.share_start) % self.share_iter <= 5 + return schedule.share_freeze(self.iteration, self.share_start, self.share_iter) def _identify_shared_comps(self) -> None: """Merge near-collinear mixing columns across models (Fortran @@ -2697,7 +2711,8 @@ def _fit_once( best_snapshot = self._snapshot_params() # Whether rejection fires this iteration (Fortran schedule, - # amica15.f90:1142). Captured here, before _update_parameters, so + # amica15.f90:1136, with rejstart counted from 1 as the reference + # counts it; issue #335). Captured here, before _update_parameters, so # the statistic is the PRE-update per-sample log-likelihood -- # matching Fortran's ordering (loglik is filled in # get_updates_and_likelihood, before update_params runs). @@ -2713,16 +2728,8 @@ def _fit_once( # second pass in the first place. Fancy-indexed straight out of # the mx stash, so no host round-trip is needed to decide whether # to reject. - will_reject = ( - self.do_reject - and self.maxrej > 0 - and ( - it == self.rejstart - or ( - max(1, it - self.rejstart) % self.rejint == 0 - and self.numrej < self.maxrej - ) - ) + will_reject = schedule.rejection_due( + self.do_reject, it, self.rejstart, self.rejint, self.numrej, self.maxrej ) if will_reject: assert self.good_idx is not None and self._llt_ll is not None @@ -2821,9 +2828,9 @@ def _fit_once( # ``AMICATorchNG._fit_once``). Runs on the # kurt_start/num_kurt/kurt_int schedule using the just-updated W; # the new per-source families take effect from the next E-step. - # itf is the Fortran-style 1-indexed iteration. num_kurt=0 disables - # switching (the family stays at its pdftype=1 super-Gaussian - # init). Placed BEFORE the sharing hook below, matching + # kurt_start counts from 1, like every reference schedule. num_kurt=0 + # disables switching (the family stays at its pdftype=1 + # super-Gaussian init). Placed BEFORE the sharing hook below, matching # AMICATorchNG's source order -- component sharing does not # synchronize pdtype across merged columns (see # shared_components()), so running the switch first means a @@ -2834,11 +2841,7 @@ def _fit_once( # x pdftype=1 has no bit-exact oracle either way to pin against), so # a future accidental swap would not be caught by the suite. if self.do_choose_pdfs and self.n_kurt_done < self.num_kurt: - itf = it + 1 - if ( - itf >= self.kurt_start - and (itf - self.kurt_start) % self.kurt_int == 0 - ): + if schedule.periodic_due(it, self.kurt_start, self.kurt_int): self._choose_pdfs(X_use) self.n_kurt_done += 1 @@ -2855,14 +2858,11 @@ def _fit_once( # so a merge on the final iteration lands in the returned # A/W/comp_list but not in the ``ll_history``/``final_ll_`` value # appended just below -- see final_ll_'s comment (issue #269). - if self.share_comps: - itf = it + 1 - if ( - itf >= self.share_start - and (itf - self.share_start) % self.share_iter == 0 - ): - self._identify_shared_comps() - self._update_unmixing_matrices() + if self.share_comps and schedule.periodic_due( + it, self.share_start, self.share_iter + ): + self._identify_shared_comps() + self._update_unmixing_matrices() self.ll_history.append(ll) @@ -2980,9 +2980,11 @@ def _fit_once( numdecs += 1 if numdecs >= self.maxdecs: self.lrate_cap *= self.lratefact - if it > self.newt_start: + if schedule.past_newton_start(it, self.newt_start): self.rholrate *= self.rholratefact - if self.do_newton and it > self.newt_start: + if self.do_newton and schedule.past_newton_start( + it, self.newt_start + ): # The Newton ceiling ratchets on the same maxdecs # cadence as lrate_cap/rholrate (Fortran # amica15.f90:1056-1077), so a run that keeps @@ -3037,7 +3039,7 @@ def _fit_once( # describes the schedule now running: Fortran clears it on the # switch-on iteration (amica15.f90:1099-1102, # ``AMICATorchNG._fit_once``). - if self.do_newton and it == self.newt_start: + if schedule.newton_switches_on(self.do_newton, it, self.newt_start): numdecs = 0 if leave: diff --git a/pamica/numpy_impl/core.py b/pamica/numpy_impl/core.py index 9ef39baf..3a11edf6 100644 --- a/pamica/numpy_impl/core.py +++ b/pamica/numpy_impl/core.py @@ -86,13 +86,13 @@ from scipy import linalg from scipy.special import digamma import logging -import numbers import time from pathlib import Path from typing import Dict, List, Optional, Tuple, Union from tqdm import tqdm from .. import blocktune from .. import restarts +from .. import schedule from ..fortran_params import read_params_file from ..rank import ( MINEIG, @@ -305,6 +305,28 @@ def __init__( one WARNING, when ``do_sphere`` is False, as in the reference. See :mod:`pamica.rank`. + ``newt_start`` (20), ``rejstart`` (2), ``writestep`` (100) and + ``histstep`` (10) name iterations counted from 1, as the + reference's do (issue #335): Newton takes its first step on + iteration ``newt_start``, the first rejection follows iteration + ``rejstart``, and a checkpoint lands on every ``writestep``-th + iteration. ``newt_start`` must be an integer >= 0 (0 and 1 fit + identically), ``rejstart`` an integer >= 1 when ``do_reject`` is + on, and ``histstep`` an integer >= 1 when ``do_history`` is on, + or the constructor raises ``ValueError`` (for ``newt_start`` and + ``rejstart``, with the same messages as the PyTorch and MLX + backends). See :mod:`pamica.schedule`. + + ``restartiter`` (10) and ``maxrestarts`` (3) configure the + restart-on-NaN recovery (Fortran amica15.f90:1022-1050): a + non-finite likelihood within the first ``restartiter`` iterations, + counted from 1, redraws the mixing matrix and continues, up to + ``maxrestarts`` times; ``restartiter=0`` disables it, as in the + reference. Both must be integers >= 0. The restart count, and what + the fit does after a restart, differ from the reference; see + ``docs/guides/amica-differences.md``. The PyTorch and MLX backends + have no restart-on-NaN path: they stop on a non-finite likelihood. + ``doscaling`` (True) and ``scalestep`` (1) carry AMICATorchNG's names, defaults, validation and semantics (issue #333): each component's mixing vector (a row of its model's stored ``A`` @@ -360,6 +382,10 @@ def __init__( raise ValueError(f"max_iter must be >= 1, got {self.max_iter}") self.do_newton = params.get("do_newton", False) self.newt_start = params.get("newt_start", 20) + # Validated whether or not do_newton is on: newt_start also gates the + # rho-rate ceiling ratchet on the natural-gradient path + # (schedule.past_newton_start, amica15.f90:1067). + schedule.validate_iteration_setting("newt_start", self.newt_start, 0) self.newt_ramp = params.get("newt_ramp", 10) self.newtrate = params.get("newtrate", 0.5) self.do_reject = params.get("do_reject", False) @@ -374,15 +400,16 @@ def __init__( # reject-below-the-mean semantics (at 0 the threshold is the mean, so # ~half the samples drop every pass, and negative values invert it); # maxrej<0 is a sanity guard (it would just make rejection inert via - # the maxrej>0 schedule gate); rejstart<0 is nonsensical. + # the maxrej>0 schedule gate); rejstart counts from 1, so + # rejstart<=0 would silently disable the reference's unconditional + # ``iter == rejstart`` pass. if self.rejint < 1: raise ValueError(f"rejint must be >= 1, got {self.rejint}") if self.rejsig <= 0: raise ValueError(f"rejsig must be > 0, got {self.rejsig}") if self.maxrej < 0: raise ValueError(f"maxrej must be >= 0, got {self.maxrej}") - if self.rejstart < 0: - raise ValueError(f"rejstart must be >= 0, got {self.rejstart}") + schedule.validate_iteration_setting("rejstart", self.rejstart, 1) self.num_comps = params.get("num_comps", -1) self.lrate = params.get("lrate", 0.1) self.lrate0 = self.lrate @@ -398,6 +425,10 @@ def __init__( self.invsigmin = params.get("invsigmin", 1e-4) self.do_history = params.get("do_history", False) self.histstep = params.get("histstep", 10) + if self.do_history: + # A history snapshot fires on ``mod(iter, histstep) == 0``; 0 was a + # bare ZeroDivisionError on the first iteration of the fit. + schedule.validate_iteration_setting("histstep", self.histstep, 1) # Block-size search (issue #232). OFF by default, unlike Fortran, whose # header default is .true.: the choice is timing-based and therefore # machine-dependent, so a parity run has to be able to pin block_size. @@ -438,16 +469,10 @@ def __init__( ) self.doscaling = params.get("doscaling", True) self.scalestep = params.get("scalestep", 1) - if self.doscaling and ( - isinstance(self.scalestep, bool) - or not isinstance(self.scalestep, numbers.Integral) - or self.scalestep < 1 - ): + if self.doscaling: # Same check and message as AMICATorchNG: a zero cadence divides by # zero mid-fit; the reference never reads scalestep. - raise ValueError( - f"scalestep must be an integer >= 1, got {self.scalestep!r}" - ) + schedule.validate_iteration_setting("scalestep", self.scalestep, 1) self.do_sphere = params.get("do_sphere", True) self.do_mean = params.get("do_mean", True) self.do_approx_sphere = params.get("do_approx_sphere", True) @@ -468,10 +493,16 @@ def __init__( # (Fortran maxincs, amica17.f90:1087). self.maxincs = params.get("maxincs", 5) # Restart-on-NaN (Fortran amica15.f90:1022-1052): if the LL goes - # non-finite at iter <= restartiter, reinitialize and start over, up to - # maxrestarts times; a later NaN stops the fit (Fortran exits too). + # non-finite within the first restartiter iterations, counted from 1 as + # the reference counts them (``iter .le. restartiter``), reinitialize and + # start over, up to maxrestarts times; a later NaN stops the fit (Fortran + # exits too). restartiter=0 therefore disables the recovery, as in the + # reference. The restart count differs from the reference's; see + # docs/guides/amica-differences.md. self.restartiter = params.get("restartiter", 10) self.maxrestarts = params.get("maxrestarts", 3) + schedule.validate_iteration_setting("restartiter", self.restartiter, 0) + schedule.validate_iteration_setting("maxrestarts", self.maxrestarts, 0) self.numrestarts = 0 # Set by fit(): whether the fit ended usable, and the reason it stopped. # converged=False signals a terminal non-finite LL or non-finite fitted @@ -2080,7 +2111,9 @@ def _update_parameters(self, updates: Dict): self.rho[:, cols] = np.clip(new_rho, self.minrho, self.maxrho) # Update unmixing matrices - newton_active = self.do_newton and self.iter >= self.newt_start + newton_active = schedule.newton_active( + self.do_newton, self.iter, self.newt_start + ) if newton_active: # Finalize Newton curvature statistics (Fortran amica17.f90:1762-1776). # The dsigma2/dkappa/dlambda accumulators already carry the sbeta^2 @@ -2228,9 +2261,8 @@ def _update_parameters(self, updates: Dict): # iteration (it parses ``scalestep`` but never reads it, # amica15.f90:1843/3686); pamica keeps ``scalestep`` as an extension # counted from 1 like the reference's other cadences (iterations s, 2s, - # ...), so the default 1 is the reference. The 1-based rule moves to - # ``schedule.every`` when epic #324 Phase 9 (issue #335) lands. - if self.doscaling and (self.iter + 1) % self.scalestep == 0: + # ...; ``schedule.every``), so the default 1 is the reference. + if self.doscaling and schedule.every(self.iter, self.scalestep): self._rescale_components() # Update unmixing matrices @@ -2300,10 +2332,7 @@ def _a_frozen(self) -> bool: """ if not self.share_comps or self.num_models < 2: return False - itf = self.iter + 1 # Fortran-style 1-indexed iteration - if itf < self.share_start: - return False - return (itf - self.share_start) % self.share_int <= 5 + return schedule.share_freeze(self.iter, self.share_start, self.share_int) def _optimize(self): """Main optimization loop.""" @@ -2348,12 +2377,10 @@ def _optimize(self): for iter in iterator: self.iter = iter final_iter = iter - # Fortran-style 1-indexed iteration. Every schedule the reference - # expresses as `mod(iter, step)` (share_comps, writestep, - # histstep) is anchored on this, not on the 0-indexed loop - # counter, so an identical setting fires on the same iterations - # here, in AMICATorchNG, and in the binary. - itf = iter + 1 + # Every schedule below counts iterations from 1, as the reference + # does (pamica.schedule converts this 0-indexed loop counter), so + # an identical setting fires on the same iterations here, in + # AMICATorchNG, in AMICAMLXNG, and in the binary (issue #335). # Get updates and likelihood updates = self._get_updates_and_likelihood() @@ -2365,13 +2392,14 @@ def _optimize(self): # non-finite LL usually means an unlucky init, so redraw A and # start over, up to maxrestarts times, within the first # restartiter iterations (Fortran's absolute `iter <= restartiter` - # window; the iteration counter is not reset on restart here). A + # window over its 1-based counter; the iteration counter is not + # reset on restart here). A # later NaN falls through to _check_convergence, which stops # (Fortran exits too). if ( len(self.ll) > 0 and not np.isfinite(self.ll[-1]) - and iter <= self.restartiter + and schedule.within_restart_window(iter, self.restartiter) and self.numrestarts < self.maxrestarts ): self.numrestarts += 1 @@ -2426,24 +2454,20 @@ def _optimize(self): break # Reset the decrease counter when Newton turns on (Fortran - # amica17.f90:1105-1108). - if self.do_newton and iter == self.newt_start: + # amica15.f90:1099-1102). + if schedule.newton_switches_on(self.do_newton, iter, self.newt_start): numdecs = 0 - # Reject outliers if requested (Fortran amica17.f90:1142). The - # max(1, ...) clamp matches Fortran and AMICATorchNG: without it, - # Python's non-negative modulo makes (iter - rejstart) % rejint - # hit 0 for iter < rejstart, firing rejection before rejstart. - if ( - self.do_reject - and self.maxrej > 0 - and ( - (iter == self.rejstart) - or ( - (max(1, iter - self.rejstart) % self.rejint == 0) - and (self.numrej < self.maxrej) - ) - ) + # Reject outliers if requested (Fortran amica15.f90:1136; the + # schedule, including its max(1, ...) clamp, is shared with + # AMICATorchNG and AMICAMLXNG in pamica.schedule). + if schedule.rejection_due( + self.do_reject, + iter, + self.rejstart, + self.rejint, + self.numrej, + self.maxrej, ): self._reject_outliers() self.numrej += 1 @@ -2455,20 +2479,18 @@ def _optimize(self): # merged comp_list (Fortran runs identify_shared_comps before # get_unmixing_matrices, amica15.f90:1858,1863) -- otherwise the # next E-step would read a stale W while indexing the densities - # by the merged comp_list. itf is the Fortran-style 1-indexed - # iteration (itf, above), the same anchor AMICATorchNG uses, so - # an identical (share_start, share_int) fires on the same - # iterations in both backends and lines up with _a_frozen. + # by the merged comp_list. The schedule is pamica.schedule's, the + # one AMICATorchNG and AMICAMLXNG use, so an identical + # (share_start, share_int) fires on the same iterations in every + # backend and lines up with _a_frozen. # # This runs AFTER self.ll.append(updates["ll"]) inside # _update_parameters above, so a merge on the final iteration # lands in self.A/comp_list but not in the ll value already # stored -- see the final_ll_ note on self.ll's init (issue # #269). - if ( - self.share_comps - and itf >= self.share_start - and (itf - self.share_start) % self.share_int == 0 + if self.share_comps and schedule.periodic_due( + iter, self.share_start, self.share_int ): # Sensor-space maps (issue #258): pinv(sphere) @ A, matching # AMICATorchNG._identify_shared_comps so both backends make @@ -2505,14 +2527,14 @@ def _optimize(self): if ( self.outdir is not None and self.writestep > 0 - and itf % self.writestep == 0 + and schedule.every(iter, self.writestep) ): self._write_checkpoint("results") if ( self.outdir is not None and self.do_history - and itf % self.histstep == 0 + and schedule.every(iter, self.histstep) ): self._write_checkpoint("history") finally: @@ -2617,7 +2639,7 @@ def _check_convergence( numdecs += 1 if numdecs >= self.max_decs: self.lrate0 *= self.lratefact - if self.iter > self.newt_start: + if schedule.past_newton_start(self.iter, self.newt_start): # rho rate is a ceiling reset to rholrate0 each iteration # (Fortran amica15.f90:1806/1813); it ratchets ONLY here at maxdecs # (amica15.f90:1068), never per LL-decrease. The old per-decrease @@ -2625,7 +2647,9 @@ def _check_convergence( # reset that collapsed the rho rate and froze rho at a stale # shape (issue #193). self.rholrate *= self.rholratefact - if self.do_newton and self.iter > self.newt_start: + if self.do_newton and schedule.past_newton_start( + self.iter, self.newt_start + ): self.newtrate *= self.lratefact numdecs = 0 diff --git a/pamica/schedule.py b/pamica/schedule.py new file mode 100644 index 00000000..a71ef80a --- /dev/null +++ b/pamica/schedule.py @@ -0,0 +1,154 @@ +"""Iteration-schedule gates shared by every pamica backend (issue #335). + +The Fortran reference counts its iterations from 1 (``iter = 1`` before the main +loop, amica15.f90:949) and states every schedule against that counter: +``iter .ge. newt_start`` switches Newton on, ``iter == rejstart`` fires the +first rejection, ``mod(iter, writestep) == 0`` writes a checkpoint. pamica's fit +loops count from 0 (``for it in range(max_iter)``), and that 0-based index is +public (``iteration``, the ``mir_history_`` entries, log lines), so it stays as +it is. Every gate instead converts it through :func:`reference_iteration` here, +in one place, so the three backends cannot drift apart and no gate can compare +the 0-based index with a 1-based setting again: the Newton switch did exactly +that until issue #335, and started one iteration late in all three backends. + +Only the *decisions* live here: each backend still runs its own loop and calls +these predicates with its own counters (``.rules/backend_parity.md``). So does +the validation of the settings they read (:func:`validate_iteration_setting`), +which every backend constructor calls so the three reject the same values with +the same message. +""" + +from __future__ import annotations + +import numbers + +# The reference holds the A-update for the merge iteration and the 5 after it +# (``mod(iter, share_iter) > 5``, amica15.f90:1803). +_SHARE_FREEZE_ITERS = 6 + + +def reference_iteration(index: int) -> int: + """The reference's 1-based iteration number of a 0-based loop index.""" + return index + 1 + + +def validate_iteration_setting(name: str, value: object, minimum: int) -> None: + """Raise ``ValueError`` unless ``value`` is an integer of at least ``minimum``. + + For the settings that name an iteration or an interval (``newt_start``, + ``rejstart``, ``restartiter``, ``maxrestarts``, ``histstep``, + ``scalestep``): an integer in + the ``numbers.Integral`` sense, so numpy integers qualify, while ``bool`` + (which subclasses ``int``) and floats are rejected rather than coerced, as + :func:`pamica.rank.validate_pca_reduction` does for ``pcakeep``. + """ + if ( + isinstance(value, bool) + or not isinstance(value, numbers.Integral) + or value < minimum + ): + raise ValueError(f"{name} must be an integer >= {minimum}, got {value!r}") + + +def _check_interval(where: str, name: str, interval: int) -> None: + # A modulo by a non-positive interval is a bare ZeroDivisionError (0) or a + # silently wrong cadence (negative). The constructors validate the settings + # that reach these helpers; this is the backstop for one that did not. + if interval < 1: + raise ValueError( + f"{where}: {name} must be >= 1 (a schedule interval), got {interval!r}" + ) + + +def newton_active(do_newton: bool, index: int, newt_start: int) -> bool: + """Whether this iteration takes the Newton step (``iter .ge. newt_start``, + amica15.f90:1498/1651/1666/1719/1739/1804). + + ``newt_start`` counts from 1, as the reference's does: ``newt_start=50`` + takes the first Newton step on the 50th iteration, and ``newt_start=1`` on + the first. ``newt_start=0`` fits exactly as ``1`` does: Newton is active + from the first iteration either way, and the two differ only in gates that + cannot act on iteration 1 (:func:`newton_switches_on` clears a decrease + counter that is still 0 there, and no ``maxdecs`` ratchet can precede the + first likelihood comparison). + """ + return do_newton and reference_iteration(index) >= newt_start + + +def newton_switches_on(do_newton: bool, index: int, newt_start: int) -> bool: + """Whether this is the iteration Newton switches on, where the reference + clears the likelihood-decrease counter (``iter == newt_start``, + amica15.f90:1099-1102).""" + return do_newton and reference_iteration(index) == newt_start + + +def past_newton_start(index: int, newt_start: int) -> bool: + """Whether a ``maxdecs`` ratchet on this iteration also tightens the rho-rate + ceiling and, under Newton, ``newtrate`` (``iter > newt_start``, + amica15.f90:1067/1070). The rho-rate half applies whether or not Newton is + on; callers add ``do_newton`` for the ``newtrate`` half.""" + return reference_iteration(index) > newt_start + + +def rejection_due( + do_reject: bool, index: int, rejstart: int, rejint: int, numrej: int, maxrej: int +) -> bool: + """Whether outlier rejection fires after this iteration's update + (amica15.f90:1136). + + The ``max(1, ...)`` clamp is the reference's: without it, a non-negative + modulo would make ``iter - rejstart`` hit 0 before ``rejstart`` and fire + early. The ``iter == rejstart`` arm fires regardless of ``numrej``, as in + the reference. + """ + if not do_reject or maxrej <= 0: + return False + itf = reference_iteration(index) + return itf == rejstart or (max(1, itf - rejstart) % rejint == 0 and numrej < maxrej) + + +def periodic_due(index: int, start: int, interval: int) -> bool: + """Whether a schedule that runs every ``interval`` iterations from ``start`` + fires on this iteration: the share-merge pass (``iter .ge. share_start`` + and ``mod(iter - share_start, share_iter) == 0``, amica15.f90:1856), and the + extended-Infomax kurtosis switch on its ``kurt_start``/``kurt_int`` schedule + (a pamica port with no runnable reference, whose ``do_choose_pdfs`` is dead + code, so it reads the same way).""" + _check_interval("periodic_due", "interval", interval) + itf = reference_iteration(index) + return itf >= start and (itf - start) % interval == 0 + + +def share_freeze(index: int, share_start: int, share_iter: int) -> bool: + """Whether ``share_comps`` holds the A-update on this iteration: the merge + iteration and the 5 after it, in every ``share_iter`` cycle from + ``share_start``. + + Anchored on ``share_start`` so the window stays aligned with the merge + schedule (:func:`periodic_due`). The reference's literal + ``mod(iter, share_iter) > 5`` (amica15.f90:1803) misaligns unless + ``share_start`` is a multiple of ``share_iter``; that path is dead in the + reference, so this is a documented pamica decision + (``docs/guides/amica-differences.md``), not a parity constraint. Callers + gate it on ``share_comps`` with more than one model. + """ + itf = reference_iteration(index) + if itf < share_start: + return False + return (itf - share_start) % share_iter < _SHARE_FREEZE_ITERS + + +def every(index: int, step: int) -> bool: + """Whether a ``mod(iter, step) == 0`` cadence fires on this iteration + (``writestep``/``histstep``, amica15.f90:1124/1130; also pamica's + ``scalestep``, which the reference parses but never reads): the first time + at iteration ``step``, not after the first iteration.""" + _check_interval("every", "step", step) + return reference_iteration(index) % step == 0 + + +def within_restart_window(index: int, restartiter: int) -> bool: + """Whether a non-finite likelihood on this iteration may still restart from + a fresh draw (``iter .le. restartiter``, amica15.f90:1022): the first + ``restartiter`` iterations, so ``restartiter=0`` disables the recovery.""" + return reference_iteration(index) <= restartiter diff --git a/pamica/tests/mlx_tests/test_mlx_export.py b/pamica/tests/mlx_tests/test_mlx_export.py index c7ac173d..b5304b99 100644 --- a/pamica/tests/mlx_tests/test_mlx_export.py +++ b/pamica/tests/mlx_tests/test_mlx_export.py @@ -149,7 +149,7 @@ def test_written_ll_ends_at_the_keep_best_restored_iterate(real_data): seed=0, block_size=BLOCK, do_newton=True, - newt_start=1, + newt_start=2, lrate=0.5, newtrate=3.0, # overshoots since issue #333 (test_mlx_keepbest.py) use_min_dll=True, diff --git a/pamica/tests/mlx_tests/test_mlx_keepbest.py b/pamica/tests/mlx_tests/test_mlx_keepbest.py index ca971a6c..76dcbd45 100644 --- a/pamica/tests/mlx_tests/test_mlx_keepbest.py +++ b/pamica/tests/mlx_tests/test_mlx_keepbest.py @@ -15,10 +15,11 @@ * ``_FORCED_RESTORE_KWARGS`` (real EEG, first 4096 samples, ``max_iter=150``): ``n_models=2, n_mix=3, seed=0, block_size=1024, do_newton=True, - newt_start=1, lrate=0.5, newtrate=3.0, use_min_dll=True, min_dll=1e-4, + newt_start=2, lrate=0.5, newtrate=3.0, use_min_dll=True, min_dll=1e-4, maxincs=2, use_grad_norm=False`` -- the same aggressive-Newton config ``test_ng_convergence.py::test_keep_best_restores_genuine_overshoot_under_min_dll_stop`` - uses on PyTorch, and it reproduces a genuine overshoot here too: the fit + uses on PyTorch (``newt_start`` counts from 1 since issue #335, so ``2`` is + the run measured as ``newt_start=1`` before it), and it reproduces a genuine overshoot here too: the fit stops via ``min_dll`` at iteration 64 (65 recorded LLs), peaks at ``ll_history[63]``, and ends ``best_ll - ll_history[-1] ~= 4.24e-4`` below that peak (measured on an Apple M4 Pro; the exact float32 value is @@ -130,7 +131,7 @@ def _model(**kwargs: Any) -> AMICAMLXNG: seed=0, block_size=BLOCK, do_newton=True, - newt_start=1, + newt_start=2, lrate=0.5, newtrate=3.0, use_min_dll=True, @@ -149,7 +150,7 @@ def _model(**kwargs: Any) -> AMICAMLXNG: seed=2, block_size=BLOCK, do_newton=True, - newt_start=1, + newt_start=2, lrate=0.5, use_min_dll=True, min_dll=1e-4, diff --git a/pamica/tests/mlx_tests/test_mlx_llt_stash.py b/pamica/tests/mlx_tests/test_mlx_llt_stash.py index 942eaf96..862abc8c 100644 --- a/pamica/tests/mlx_tests/test_mlx_llt_stash.py +++ b/pamica/tests/mlx_tests/test_mlx_llt_stash.py @@ -151,16 +151,17 @@ def test_llt_stash_is_none_before_any_estep_ran(): # --- keep_best (issue #51) interaction --------------------------------- # Reuses the aggressive-Newton recipe test_mlx_keepbest.py measured to # genuinely overshoot on this backend (module docstring there): n_models=2, -# seed=0, block_size=1024, do_newton=True, newt_start=1, lrate=0.5, +# seed=0, block_size=1024, do_newton=True, newt_start=2, lrate=0.5, # newtrate=3.0, use_min_dll=True, min_dll=1e-4, maxincs=2, -# use_grad_norm=False, max_iter=150 (newtrate and the budget since issue #333). +# use_grad_norm=False, max_iter=150 (newtrate and the budget since issue #333; +# newt_start 2, counted from 1 since issue #335, is the run measured as 1). _FORCED_RESTORE_KWARGS: dict[str, Any] = dict( n_models=2, n_mix=NMIX, seed=0, block_size=BLOCK, do_newton=True, - newt_start=1, + newt_start=2, lrate=0.5, newtrate=3.0, use_min_dll=True, diff --git a/pamica/tests/mlx_tests/test_mlx_mir.py b/pamica/tests/mlx_tests/test_mlx_mir.py index 1b561bf9..0e9be1b9 100644 --- a/pamica/tests/mlx_tests/test_mlx_mir.py +++ b/pamica/tests/mlx_tests/test_mlx_mir.py @@ -372,7 +372,7 @@ def test_mir_history_survives_keep_best_restore(real_data): seed=1, block_size=BLOCK, do_newton=True, - newt_start=1, + newt_start=2, lrate=0.5, newtrate=3.0, # overshoots since issue #333 (test_mlx_keepbest.py) use_min_dll=True, diff --git a/pamica/tests/mlx_tests/test_mlx_newton.py b/pamica/tests/mlx_tests/test_mlx_newton.py index 60cd9dfa..3561bd15 100644 --- a/pamica/tests/mlx_tests/test_mlx_newton.py +++ b/pamica/tests/mlx_tests/test_mlx_newton.py @@ -117,13 +117,17 @@ def _ratchet_count(rate: float, rate0: float, factor: float) -> int: # # They are therefore data-driven: a probe fit whose ``newt_start`` sits past the # budget reports where the decreases actually land ON THE EXECUTING MACHINE, and -# the real ``newt_start`` is chosen from that. This works because the -# natural-gradient PREFIX is independent of ``newt_start``: for ``it < -# newt_start`` every branch that reads it (Newton activation, the two ``it > -# newt_start`` ceiling ratchets, the ``it == newt_start`` counter reset) is false -# on both sides, so the two runs are bit-identical there. The likelihood recorded -# AT ``it == newt_start`` is shared too -- ``fit`` computes it from the previous -# iteration's parameters, before the first Newton M-step runs. +# the real ``newt_start`` is chosen from that. ``newt_start`` counts from 1, as +# the reference's ``iter`` does (issue #335): Newton switches on at 0-based +# ``ll_history`` index ``newt_start - 1``. This works because the +# natural-gradient PREFIX is independent of ``newt_start``: before that index +# every branch that reads it (Newton activation, the two ``iter > newt_start`` +# ceiling ratchets, the ``iter == newt_start`` counter reset) is false on both +# sides, so the two runs are bit-identical there. The likelihood recorded AT +# index ``newt_start - 1`` is shared too -- ``fit`` computes it from the previous +# iteration's parameters, before the first Newton M-step runs. The replays below +# spell the reference's 1-based arithmetic out (``i + 1`` for index ``i``) +# rather than calling the implementation's own ``pamica.schedule``. _SCHEDULE_DATA = 8192 _SCHEDULE_ITERS = 120 @@ -174,9 +178,10 @@ def _fit_schedule(newt_start: int, do_newton: bool = True, max_iter: int | None def _ratchet_iterations(ll, maxdecs: int, newt_start: int | None = None) -> list[int]: - """Iterations at which the decrease counter completes a ``maxdecs`` cycle and - the ceilings ratchet. ``newt_start`` applies Fortran's counter reset on the - switch-on iteration; ``None`` replays without it (the counterfactual).""" + """``ll`` indices at which the decrease counter completes a ``maxdecs`` cycle + and the ceilings ratchet. ``newt_start`` applies Fortran's counter reset on + the switch-on iteration (``iter == newt_start`` over its 1-based ``iter``, + amica15.f90:1099); ``None`` replays without it (the counterfactual).""" numdecs, hits = 0, [] for i in range(1, len(ll)): if ll[i] < ll[i - 1]: @@ -184,7 +189,7 @@ def _ratchet_iterations(ll, maxdecs: int, newt_start: int | None = None) -> list if numdecs >= maxdecs: hits.append(i) numdecs = 0 - if newt_start is not None and i == newt_start: + if newt_start is not None and i + 1 == newt_start: numdecs = 0 return hits @@ -378,10 +383,11 @@ def test_fallback_ramps_toward_lrate_cap_and_counts(): def test_newtrate_ratchet_is_suppressed_before_newt_start(natural_gradient_prefix): """A ``maxdecs`` cycle completing BEFORE the Newton switch-on ratchets ``lrate_cap`` but must leave ``newtrate`` alone (Fortran - amica15.f90:1056-1077 gates it on ``it > newt_start``). + amica15.f90:1056-1077 gates it on ``iter > newt_start``). - ``newt_start`` is read off the probe trajectory -- one past the first ratchet - of the natural-gradient phase -- so a suppressed ratchet exists BY + ``newt_start`` is read off the probe trajectory -- the 1-based iteration of + the first ratchet of the natural-gradient phase, which ``iter > newt_start`` + therefore just excludes -- so a suppressed ratchet exists BY CONSTRUCTION on whatever machine is running, rather than by a constant that happened to straddle on the author's. The expected counts are then replayed from the run's own ``ll_history``, so what is pinned is the semantics, not a @@ -405,11 +411,11 @@ def test_newtrate_ratchet_is_suppressed_before_newt_start(natural_gradient_prefi m = _fit_schedule(newt_start) hits = _ratchet_iterations(m.ll_history, m.maxdecs, newt_start=newt_start) cap_ratchets = len(hits) - newt_ratchets = sum(1 for i in hits if i > newt_start) + newt_ratchets = sum(1 for i in hits if i + 1 > newt_start) - # Guaranteed by the choice of newt_start: the prefix ratchet at - # prefix_hits[0] < newt_start is shared with the probe bit-for-bit, so it - # ratchets lrate_cap while the gate holds newtrate. + # Guaranteed by the choice of newt_start: the prefix ratchet at index + # prefix_hits[0] (iteration newt_start, not past it) is shared with the + # probe bit-for-bit, so it ratchets lrate_cap while the gate holds newtrate. assert newt_ratchets < cap_ratchets, ( f"expected a ratchet before newt_start={newt_start} from the shared " f"prefix; got {hits}" @@ -434,7 +440,7 @@ def test_newtrate_ratchets_at_maxdecs_once_newton_runs(): "Newton running throughout; the DATA did not decrease often enough to " "exercise the ratchet" ) - assert all(i > m.newt_start for i in hits) # newt_start=0; i starts at 1 + assert all(i + 1 > m.newt_start for i in hits) # newt_start=0; i starts at 1 assert _ratchet_count(m.lrate_cap, m.lrate0, m.lratefact) == len(hits) assert _ratchet_count(m.newtrate, m.newtrate0, m.lratefact) == len(hits) assert m.newtrate < m.newtrate0 # non-vacuous: the ceiling actually moved @@ -449,72 +455,18 @@ def test_newtrate_never_ratchets_without_newton(natural_gradient_prefix): m = _fit_schedule(prefix_hits[0] + 1, do_newton=False) hits = _ratchet_iterations(m.ll_history, m.maxdecs, newt_start=None) - assert any(i > m.newt_start for i in hits), ( + assert any(i + 1 > m.newt_start for i in hits), ( "no ratchet landed past newt_start, so the do_newton gate is untested" ) assert _ratchet_count(m.lrate_cap, m.lrate0, m.lratefact) == len(hits) assert m.newtrate == m.newtrate0 -def test_numdecs_resets_when_newton_switches_on(natural_gradient_prefix): - """The decrease counter is cleared on the iteration Newton switches on - (Fortran amica15.f90:1099-1102), so a partially filled count from the - natural-gradient phase cannot ratchet the ceilings under the new schedule. - - Made observable in three data-driven steps, none of them a hardcoded - trajectory. First ``newt_start`` is placed at an iteration where the probe - shows the counter PARTIALLY filled -- that state is in the shared prefix, so - it holds on any machine. Then a full-budget fit at that ``newt_start`` is - scanned for the smallest budget at which the reset and no-reset replays - disagree; one exists as soon as the Newton phase decreases at all, because - the no-reset counter is strictly ahead and therefore completes its cycle - strictly earlier. Finally the fit is repeated at exactly that budget, where - the two hypotheses predict different ``lrate_cap`` values, and the observed - one has to match the reset prediction. Truncating is sound because the loop - is causal: iteration k depends only on the state after k-1, so a shorter - budget reproduces the same prefix. - """ - partial = None - numdecs = 0 - for i in range(1, len(natural_gradient_prefix)): - if natural_gradient_prefix[i] < natural_gradient_prefix[i - 1]: - numdecs += 1 - if numdecs >= _SCHEDULE_MAXDECS: - numdecs = 0 - if 0 < numdecs < _SCHEDULE_MAXDECS: - partial = i - break - assert partial is not None, ( - "the sample recording never left the decrease counter partially filled " - "in the natural-gradient phase, so the reset has nothing to clear: the " - "DATA, not the config, is the problem here" - ) - - full = _fit_schedule(partial) - ll = full.ll_history - budget = next( - ( - t - for t in range(2, len(ll) + 1) - if len(_ratchet_iterations(ll[:t], full.maxdecs, newt_start=partial)) - != len(_ratchet_iterations(ll[:t], full.maxdecs)) - ), - None, - ) - assert budget is not None, ( - f"over {len(ll)} iterations the two counter hypotheses never predicted " - f"different ratchet counts at newt_start={partial}; the DATA did not " - "decrease often enough in the Newton phase to expose the reset" - ) - - m = _fit_schedule(partial, max_iter=budget) - assert len(m.ll_history) == budget, "the truncated fit stopped early" - with_reset = _ratchet_iterations(m.ll_history, m.maxdecs, newt_start=partial) - without_reset = _ratchet_iterations(m.ll_history, m.maxdecs) - assert len(with_reset) != len(without_reset), ( - f"the reset is unobservable at budget {budget}: {with_reset} vs {without_reset}" - ) - assert _ratchet_count(m.lrate_cap, m.lrate0, m.lratefact) == len(with_reset) +# The switch-on counter reset (amica15.f90:1099) is pinned for MLX by the +# cross-backend ``test_schedule_gates.py:: +# test_newton_switch_on_clears_the_decrease_counter``. A replay test that lived +# here passed with the reset one iteration late too (issue #335 review): on the +# schedule fixture's trajectory the two rules predicted the same ratchets. def test_grad_norm_floor_fires_under_newton(): diff --git a/pamica/tests/mlx_tests/test_mlx_reject.py b/pamica/tests/mlx_tests/test_mlx_reject.py index cb9586a1..91fe2b03 100644 --- a/pamica/tests/mlx_tests/test_mlx_reject.py +++ b/pamica/tests/mlx_tests/test_mlx_reject.py @@ -61,10 +61,14 @@ def _model(**kwargs: Any) -> AMICAMLXNG: # --- constructor validation -------------------------------------------- def test_reject_param_validation(): - """Matches AMICATorchNG's validation exactly (torch_impl/core.py: - 679-685): rejint<1 would ZeroDivisionError in the reject schedule, - rejsig<=0 breaks the reject-below-mean semantics, maxrej<0 is a sanity - guard. rejstart is NOT validated (torch does not validate it either).""" + """Matches AMICATorchNG's validation exactly: rejint<1 would + ZeroDivisionError in the reject schedule, rejsig<=0 breaks the + reject-below-mean semantics, maxrej<0 is a sanity guard, and rejstart + counts from 1, so rejstart<=0 would silently skip the unconditional first + pass (issue #335; the cross-backend message check is + ``test_schedule_gates.py``).""" + with pytest.raises(ValueError, match="rejstart"): + _model(do_reject=True, rejstart=0) with pytest.raises(ValueError, match="rejint"): _model(do_reject=True, rejint=0) with pytest.raises(ValueError, match="rejsig"): @@ -99,10 +103,12 @@ def test_do_reject_false_leaves_good_idx_unset(real_data): # --- the reject schedule ------------------------------------------------- def test_rejection_shrinks_good_sample_set_on_the_expected_schedule(real_data): - """rejstart=2/rejint=3/maxrej=2: rejection fires at it=2 (unconditional) - and it=5 (max(1,5-2)%3==0, numrej<2), then is capped -- no more passes - at it=8/11 despite the modulo condition recurring, matching Fortran's - schedule (amica15.f90:1142) and the torch/NumPy backends' own tests.""" + """rejstart=2/rejint=3/maxrej=2: rejection fires at iteration 2 + (unconditional) and 5 (max(1,5-2)%3==0, numrej<2), then is capped -- no + more passes at 8/11 despite the modulo condition recurring, matching + Fortran's schedule (amica15.f90:1136, iterations counted from 1 as the + reference counts them, issue #335) and the torch/NumPy backends' own + tests.""" n_total = real_data.shape[1] m = _model( seed=42, @@ -232,11 +238,13 @@ def test_keep_best_inactive_reason_prefers_do_reject_when_both_are_on( # The aggressive-Newton recipe that genuinely overshoots on this backend # (test_mlx_keepbest.py's module docstring), plus a single rejection pass. +# newt_start and rejstart count from 1 since issue #335: 2 and 6 are the run +# measured as 1 and 5 before. _OVERSHOOT_KWARGS: dict[str, Any] = dict( n_models=2, seed=0, do_newton=True, - newt_start=1, + newt_start=2, lrate=0.5, newtrate=3.0, use_min_dll=True, @@ -245,7 +253,7 @@ def test_keep_best_inactive_reason_prefers_do_reject_when_both_are_on( use_grad_norm=False, ) _REJECT_KWARGS: dict[str, Any] = dict( - do_reject=True, rejsig=3.0, rejstart=5, rejint=5, maxrej=1 + do_reject=True, rejsig=3.0, rejstart=6, rejint=5, maxrej=1 ) _OVERSHOOT_MAX_ITER = 150 diff --git a/pamica/tests/test_doscaling_rows.py b/pamica/tests/test_doscaling_rows.py index 4bbcca35..b1f39b37 100644 --- a/pamica/tests/test_doscaling_rows.py +++ b/pamica/tests/test_doscaling_rows.py @@ -79,10 +79,11 @@ COLUMN_RULE_MIN_DLL = 1e-5 # A real state reached WITH Newton steps: Newton from the second iteration -# (0-based iteration 1). A positive-definite Newton step ramps lrate past the -# natural-gradient ceiling (the lrate default, 0.1), which the setup asserts; -# measured on every backend, one and two models: 0.3 after 3 iterations. -NEWTON: Dict[str, Any] = dict(do_newton=True, newt_start=1) +# (newt_start=2; it counts from 1 since issue #335). A positive-definite Newton +# step ramps lrate past the natural-gradient ceiling (the lrate default, 0.1), +# which the setup asserts; measured on every backend, one and two models: 0.3 +# after 3 iterations. +NEWTON: Dict[str, Any] = dict(do_newton=True, newt_start=2) NG_LRATE_CEILING = 0.1 pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") diff --git a/pamica/tests/test_llt_stash.py b/pamica/tests/test_llt_stash.py index 4ce3dc85..b690f762 100644 --- a/pamica/tests/test_llt_stash.py +++ b/pamica/tests/test_llt_stash.py @@ -286,7 +286,9 @@ def test_torch_and_numpy_stash_the_same_llt_on_matched_state(real_data, tmp_path # --- do_reject: the one reference-faithful break in the invariant ----------- def _reject_kwargs(rejstart): - """Fire exactly one rejection, on iteration ``rejstart`` (0-indexed). + """Fire exactly one rejection, on iteration ``rejstart`` (counted from 1, as + the reference counts it -- issue #335), so a fit of ``rejstart`` iterations + rejects on its own last one. ``rejint=3`` keeps the modulo arm of the schedule from firing earlier (both backends clamp ``max(1, iter - rejstart)``, so a ``rejint`` of 1 would @@ -318,7 +320,7 @@ def test_llt_invariant_breaks_when_rejection_fires_on_the_last_iteration( rather than pinned to a value -- what is asserted is that it is far above the summation tolerance and far below anything resembling a blow-up. """ - tm = _torch_fit(real_data, n_models=1, max_iter=6, **_reject_kwargs(5)) + tm = _torch_fit(real_data, n_models=1, max_iter=6, **_reject_kwargs(6)) assert len(tm.ll_history) == 6 and tm.numrej == 1 assert tm.good_idx is not None and int(tm.good_idx.numel()) < real_data.shape[1] assert tm._llt_lt is not None @@ -334,7 +336,7 @@ def test_llt_invariant_breaks_when_rejection_fires_on_the_last_iteration( n_models=1, max_iter=6, outdir=str(tmp_path / "rej"), - **_reject_kwargs(5), + **_reject_kwargs(6), ) assert len(nm.ll) == 6 and nm.numrej == 1 _, n_lt = nm._llt_arrays() @@ -352,7 +354,7 @@ def test_llt_invariant_returns_one_iteration_after_a_rejection(real_data, tmp_pa the rejected samples contribute 0 to both sides, so the equality is exact again. """ - tm = _torch_fit(real_data, n_models=1, max_iter=7, **_reject_kwargs(5)) + tm = _torch_fit(real_data, n_models=1, max_iter=7, **_reject_kwargs(6)) assert len(tm.ll_history) == 7 and tm.numrej == 1 assert tm.good_idx is not None and tm._llt_lt is not None n_good = int(tm.good_idx.numel()) @@ -364,7 +366,7 @@ def test_llt_invariant_returns_one_iteration_after_a_rejection(real_data, tmp_pa n_models=1, max_iter=7, outdir=str(tmp_path / "rej2"), - **_reject_kwargs(5), + **_reject_kwargs(6), ) assert len(nm.ll) == 7 and nm.numrej == 1 _, n_lt = nm._llt_arrays() @@ -398,7 +400,7 @@ def test_keep_best_restore_rolls_the_llt_stash_back(real_data, tmp_path): max_iter=150, seed=0, do_newton=True, - newt_start=1, + newt_start=2, lrate=0.5, newtrate=3.0, use_min_dll=True, diff --git a/pamica/tests/test_mlx_keepbest_cross_backend.py b/pamica/tests/test_mlx_keepbest_cross_backend.py index fe8049e3..60b53045 100644 --- a/pamica/tests/test_mlx_keepbest_cross_backend.py +++ b/pamica/tests/test_mlx_keepbest_cross_backend.py @@ -77,7 +77,7 @@ def real_data() -> np.ndarray: seed=0, block_size=BLOCK, do_newton=True, - newt_start=1, + newt_start=2, lrate=0.5, newtrate=3.0, use_min_dll=True, diff --git a/pamica/tests/test_numpy_reject.py b/pamica/tests/test_numpy_reject.py index 3d7b5c6d..c7193889 100644 --- a/pamica/tests/test_numpy_reject.py +++ b/pamica/tests/test_numpy_reject.py @@ -111,7 +111,8 @@ def test_rejection_does_not_spuriously_stop_fit(): assert len(ll) > model.rejstart + 2 # LL after the rejection is not below LL at the rejection iteration (dropping # below-average samples raises the mean of what remains; no spurious descent). - assert ll[-1] >= ll[model.rejstart] + # rejstart counts from 1 (issue #335), so that iteration is ll[rejstart - 1]. + assert ll[-1] >= ll[model.rejstart - 1] @pytest.mark.skipif(not _FDT.exists(), reason="sample data missing") diff --git a/pamica/tests/test_schedule_gates.py b/pamica/tests/test_schedule_gates.py new file mode 100644 index 00000000..0054f077 --- /dev/null +++ b/pamica/tests/test_schedule_gates.py @@ -0,0 +1,712 @@ +"""Iteration-schedule gates count from 1, as the reference does (issue #335). + +The reference's main loop starts at ``iter = 1`` (amica15.f90:949) and states +every schedule against that counter, while pamica's fit loops count from 0. Until +issue #335 the Newton switch (``iter .ge. newt_start``), the counter reset on the +switch-on iteration, the ``iter > newt_start`` ceiling ratchets, the rejection +schedule and NumPy's restart-on-NaN window all compared the 0-based index with +the 1-based setting, so each fired one iteration late in every backend. Every +gate now goes through :mod:`pamica.schedule`. + +Each gate is observed here through a backend's real fit on the bundled sample +EEG, with no mocks (``.rules/testing.md``): the likelihood trajectory, the +learning-rate ceilings a ratchet moves, and the rejection count. Cross-backend by +design (``.rules/backend_parity.md``): PyTorch and NumPy always run; MLX skips +per test (``pytest.importorskip`` plus an Apple-GPU guard), never the module. +The expected iterations are written out in the reference's own 1-based +arithmetic (``i + 1`` for 0-based index ``i``), not read back from +:mod:`pamica.schedule`, so a regression in that module cannot hide here. The +native-binary oracle for the same gates is ``test_schedule_native_oracle.py``. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import numpy as np +import pytest +import torch + +from pamica import schedule +from pamica.numpy_impl.core import AMICA as AMICA_NumPy +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +NMIX = 3 +SEED = 3 +BLOCK = 1024 +N_FRAMES = 8192 +BACKENDS = ["torch", "numpy", "mlx"] + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + +# An aggressive natural-gradient step that overshoots early on this recording, so +# the likelihood-decrease bookkeeping has something to count within a short +# budget. lrate=0.6/maxdecs=2/seed=3/8192 frames is test_mlx_newton.py's +# schedule fixture: it was chosen there by sweeping seeds, block sizes and +# sample counts, and here it completes the first maxdecs cycle at iteration 11 +# on all three backends. Every test below still reads the iterations it needs +# off the executing machine's own trajectory rather than trusting that number. +_OVERSHOOT: dict[str, Any] = dict(lrate=0.6, lratefact=0.5, maxdecs=2) +# The Newton-phase ceiling test_mlx_newton.py pairs with it: at newtrate=1.0 +# the post-switch-on trajectory can be monotone, leaving the reset unobservable. +_OVERSHOOT_NEWTRATE = 2.0 +_PROBE_ITERS = 40 +_NEWTON_ITERS = 120 +_ZERO_ONE_ITERS = 40 + + +@pytest.fixture(scope="module") +def X() -> np.ndarray: + return load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD).astype( + np.float64 + )[:, :N_FRAMES] + + +def _mlx_class(): + """``AMICAMLXNG``, or skip this test (never the whole module).""" + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + mx = mlx_core.mx + if mx.default_device().type != mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + return mlx_core.AMICAMLXNG + + +@dataclass +class _Run: + """What a fit leaves behind that the schedule decides, in one vocabulary.""" + + ll: list[float] + lrate: float + lrate_ceiling: float # lrate_cap (torch/MLX), lrate0 (NumPy) + lrate_ceiling0: float + rholrate: float + rholrate0: float + newtrate: float + newtrate0: float + numrej: int + n_good: int + n_newton_fallbacks: int | None # NumPy does not count them + + +def _fit( + backend: str, + x: np.ndarray, + max_iter: int, + tmp_path: Path, + seed: int = SEED, + **cfg: Any, +) -> _Run: + """One real fit on ``backend``; ``cfg`` uses the torch/MLX parameter names.""" + if backend == "numpy": + cfg = dict(cfg) + if "maxdecs" in cfg: + cfg["max_decs"] = cfg.pop("maxdecs") + m = AMICA_NumPy( + num_models=1, + num_mix=NMIX, + seed=seed, + block_size=BLOCK, + max_iter=max_iter, + use_tqdm=False, + do_opt_block=False, + writestep=10**7, + outdir=str(tmp_path / f"np_{max_iter}_{len(list(tmp_path.iterdir()))}"), + **cfg, + ) + m.fit(x) + assert m.num_good_samples is not None # set by every fit + return _Run( + ll=[float(v) for v in m.ll], + lrate=m.lrate, + lrate_ceiling=m.lrate0, + lrate_ceiling0=m._pristine_state["lrate0"], + rholrate=m.rholrate, + rholrate0=m.rholrate0, + newtrate=m.newtrate, + newtrate0=m._pristine_state["newtrate"], + numrej=m.numrej, + n_good=int(m.num_good_samples), + n_newton_fallbacks=None, + ) + if backend == "torch": + m = AMICATorchNG( + n_channels=NW, + n_mix=NMIX, + seed=seed, + block_size=BLOCK, + device="cpu", + dtype=torch.float64, + keep_best=False, + **cfg, + ) + else: + m = _mlx_class()( + n_channels=NW, + n_mix=NMIX, + seed=seed, + block_size=BLOCK, + keep_best=False, + **cfg, + ) + m.fit(x, max_iter=max_iter, verbose=False) + n_good = x.shape[1] if m.good_idx is None else int(np.asarray(m.good_idx).size) + return _Run( + ll=[float(v) for v in m.ll_history], + lrate=m.lrate, + lrate_ceiling=m.lrate_cap, + lrate_ceiling0=m.lrate0, + rholrate=m.rholrate, + rholrate0=m.rholrate0, + newtrate=m.newtrate, + newtrate0=m.newtrate0, + numrej=m.numrej, + n_good=n_good, + n_newton_fallbacks=m.n_newton_fallbacks, + ) + + +def _ratchet_indices( + ll: list[float], maxdecs: int, newt_start: int | None +) -> list[int]: + """0-based ``ll`` indices at which the decrease counter completes a + ``maxdecs`` cycle (amica15.f90:1056-1077), clearing it on the switch-on + iteration ``iter == newt_start`` of the reference's 1-based counter + (amica15.f90:1099) when ``newt_start`` is given.""" + numdecs, hits = 0, [] + for i in range(1, len(ll)): + if ll[i] < ll[i - 1]: + numdecs += 1 + if numdecs >= maxdecs: + hits.append(i) + numdecs = 0 + if newt_start is not None and i + 1 == newt_start: + numdecs = 0 + return hits + + +def _ratchets(rate: float, rate0: float, factor: float) -> int: + """How many ``*= factor`` ratchets took ``rate0`` to ``rate``.""" + k = round(math.log(rate / rate0) / math.log(factor)) if rate != rate0 else 0 + assert rate == pytest.approx(rate0 * factor**k), ( + f"{rate} is not {rate0} ratcheted by {factor} a whole number of times" + ) + return k + + +_PROBES: dict[str, _Run] = {} + + +def _probe(backend: str, x: np.ndarray, tmp_path: Path) -> _Run: + """The overshooting natural-gradient trajectory, with Newton enabled but + never switched on. Every ``newt_start`` shares it up to the switch-on + iteration, so the iterations a test needs are read off it. Cached per + backend: it is deterministic for a given machine.""" + if backend not in _PROBES: + _PROBES[backend] = _fit( + backend, + x, + _PROBE_ITERS, + tmp_path, + do_newton=True, + newt_start=10**6, + newtrate=_OVERSHOOT_NEWTRATE, + **_OVERSHOOT, + ) + return _PROBES[backend] + + +# --- the arithmetic itself --------------------------------------------------- + + +def test_every_gate_fires_on_the_reference_iteration(): + """Each predicate, fed 0-based loop indices, first fires on the index whose + 1-based reference iteration the reference names. + + Pure arithmetic on the predicates' own arguments -- no data stands in for + anything -- so it states the mapping in one place; the real-fit tests below + are what show the backends actually use it. + """ + idx = range(12) + + def first(pred) -> int: + return next(i for i in idx if pred(i)) + + def fires(pred) -> list[int]: + return [i for i in idx if pred(i)] + + # iter .ge. newt_start (amica15.f90:1804): the 5th iteration is index 4. + assert first(lambda i: schedule.newton_active(True, i, 5)) == 4 + assert not any(schedule.newton_active(False, i, 5) for i in idx) + # newt_start 0 and 1 both mean "from the first iteration". + assert first(lambda i: schedule.newton_active(True, i, 1)) == 0 + assert first(lambda i: schedule.newton_active(True, i, 0)) == 0 + # iter == newt_start (amica15.f90:1099): exactly once, on index 4; at + # newt_start=1 on the first iteration, and never at newt_start=0 (the + # reference's 1-based counter never equals 0). + assert fires(lambda i: schedule.newton_switches_on(True, i, 5)) == [4] + assert fires(lambda i: schedule.newton_switches_on(True, i, 1)) == [0] + assert fires(lambda i: schedule.newton_switches_on(True, i, 0)) == [] + assert fires(lambda i: schedule.newton_switches_on(False, i, 1)) == [] + # iter > newt_start (amica15.f90:1067): closed on iteration newt_start + # itself, open from the next one. + assert not schedule.past_newton_start(4, 5) # iteration 5 + assert schedule.past_newton_start(5, 5) # iteration 6 + assert first(lambda i: schedule.past_newton_start(i, 5)) == 5 + assert not schedule.past_newton_start(0, 1) # iteration 1 + assert schedule.past_newton_start(1, 1) # iteration 2 + assert first(lambda i: schedule.past_newton_start(i, 0)) == 0 + # Rejection (amica15.f90:1136) with rejstart=4, rejint=3: the 4th + # iteration (index 3), then every 3rd while numrej < maxrej (held at 0 + # here, so the modulo arm keeps firing). + assert [i for i in idx if schedule.rejection_due(True, i, 4, 3, 0, 2)] == [3, 6, 9] + assert not any(schedule.rejection_due(True, i, 4, 3, 0, 0) for i in idx) + assert not any(schedule.rejection_due(False, i, 4, 3, 0, 2) for i in idx) + + # rejstart=1, rejint=1: every iteration from the first until maxrej passes + # are spent, with numrej advancing as the fit loops advance it. + def passes(rejstart: int, rejint: int, maxrej: int) -> list[int]: + numrej, out = 0, [] + for i in idx: + if schedule.rejection_due(True, i, rejstart, rejint, numrej, maxrej): + out.append(i) + numrej += 1 + return out + + assert passes(1, 1, 3) == [0, 1, 2] + assert passes(1, 1, 1) == [0] + # The ``iter == rejstart`` arm is unconditional: it fires even once the + # modulo arm's budget is spent (here by an earlier modulo pass). + assert passes(4, 1, 1) == [0, 3] + # mod(iter, writestep) == 0 (amica15.f90:1124): the 5th and 10th, not the 1st. + assert [i for i in idx if schedule.every(i, 5)] == [4, 9] + assert fires(lambda i: schedule.every(i, 1)) == list(idx) + # Share merges from share_start=3 every 4 (amica15.f90:1856): 3, 7, 11. + assert [i for i in idx if schedule.periodic_due(i, 3, 4)] == [2, 6, 10] + assert fires(lambda i: schedule.periodic_due(i, 1, 1)) == list(idx) + # The A-freeze: the merge iteration and the 5 after it, anchored on + # share_start (a documented pamica decision), share_start=3, share_iter=8. + assert [i for i in idx if schedule.share_freeze(i, 3, 8)] == [ + 2, + 3, + 4, + 5, + 6, + 7, + 10, + 11, + ] + # iter .le. restartiter (amica15.f90:1022): the first 3 iterations; 0 turns + # the recovery off. + assert [i for i in idx if schedule.within_restart_window(i, 3)] == [0, 1, 2] + assert fires(lambda i: schedule.within_restart_window(i, 1)) == [0] + assert fires(lambda i: schedule.within_restart_window(i, 0)) == [] + + +@pytest.mark.parametrize("interval", [0, -2]) +def test_a_non_positive_interval_is_a_value_error_not_a_zero_division(interval): + """The constructors validate every interval they pass in; the shared helpers + still refuse a non-positive one by name instead of surfacing a bare + ``ZeroDivisionError`` (or, for a negative one, a silently wrong cadence).""" + with pytest.raises(ValueError, match=r"every: step must be >= 1"): + schedule.every(0, interval) + with pytest.raises(ValueError, match=r"periodic_due: interval must be >= 1"): + schedule.periodic_due(0, 1, interval) + + +# --- Newton switch ----------------------------------------------------------- + + +@pytest.mark.parametrize("newt_start", [1, 5]) +@pytest.mark.parametrize("backend", BACKENDS) +def test_newton_takes_its_first_step_on_iteration_newt_start( + backend, newt_start, X, tmp_path +): + """The first Newton M-step is iteration ``newt_start``'s (0-based index + ``newt_start - 1``), as ``iter .ge. newt_start`` puts it in the reference. + + Seen two ways on a real fit against its ``do_newton=False`` twin: the + likelihood trajectories are bit-identical through index ``newt_start - 1`` + (computed before that iteration's M-step) and first differ at index + ``newt_start``; and the learning rate has climbed the Newton ramp + (``min(newtrate, lrate + min(1/newt_ramp, lrate))``, amica15.f90:1805) once + per Newton iteration run, iterations ``newt_start`` through + ``newt_start + 2``, instead of sitting at its natural-gradient ceiling. + Before issue #335 the first difference was one index later and the ramp had + run twice. + """ + max_iter = newt_start + 2 + cfg: dict[str, Any] = dict( + lrate=0.05, newtrate=1.0, newt_ramp=10, newt_start=newt_start + ) + ng = _fit(backend, X, max_iter, tmp_path, do_newton=False, **cfg) + nt = _fit(backend, X, max_iter, tmp_path, do_newton=True, **cfg) + + assert len(ng.ll) == len(nt.ll) == max_iter + first_diff = next(i for i in range(max_iter) if ng.ll[i] != nt.ll[i]) + assert first_diff == newt_start, ( + f"the Newton fit first departs at ll index {first_diff}; the reference's " + f"first Newton M-step (iteration {newt_start}) moves ll index {newt_start}" + ) + + # The ramp arithmetic below assumes no likelihood decrease halved lrate and + # no Newton fallback retargeted it; both are facts about this data, checked + # rather than assumed. + assert all(b >= a for a, b in zip(nt.ll, nt.ll[1:])), "trajectory decreased" + if nt.n_newton_fallbacks is not None: + assert nt.n_newton_fallbacks == 0 + lrate = 0.05 + for _ in range(max_iter - newt_start + 1): # Newton iterations run + lrate = min(1.0, lrate + min(1.0 / 10, lrate)) + assert nt.lrate == pytest.approx(lrate, rel=1e-12) + assert ng.lrate == pytest.approx(0.05, rel=1e-12) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_newt_start_zero_fits_exactly_as_one(backend, X, tmp_path): + """``newt_start=0`` and ``newt_start=1`` give bit-identical fits, as the + docstrings state. + + Newton is active from the first iteration either way. The two settings + differ only in gates that cannot act on iteration 1: the switch-on reset + (``iter == newt_start``) fires there for ``1`` and never for ``0``, but it + clears a decrease counter that no comparison has touched yet; and the + ``iter > newt_start`` ratchet gate is closed on iteration 1 for ``1``, where + no ``maxdecs`` cycle can complete because iteration 1 has no predecessor to + compare against. Checked on the overshooting configuration, so ratchets + do fire later in the run and both of those gates are live. + """ + cfg: dict[str, Any] = dict( + do_newton=True, newtrate=_OVERSHOOT_NEWTRATE, **_OVERSHOOT + ) + zero = _fit(backend, X, _ZERO_ONE_ITERS, tmp_path, newt_start=0, **cfg) + one = _fit(backend, X, _ZERO_ONE_ITERS, tmp_path, newt_start=1, **cfg) + + assert zero.ll == one.ll + for name in ("lrate", "lrate_ceiling", "rholrate", "newtrate"): + assert getattr(zero, name) == getattr(one, name), name + # Not vacuous: the run completed maxdecs cycles past iteration 1, so the + # ratchet gate both settings read was exercised. + assert _ratchets(one.lrate_ceiling, one.lrate_ceiling0, 0.5) > 0 + assert _ratchets(one.newtrate, one.newtrate0, 0.5) > 0 + + +# --- the iter > newt_start ceiling ratchet ----------------------------------- + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_rho_rate_ratchet_opens_only_after_iteration_newt_start(backend, X, tmp_path): + """A ``maxdecs`` ratchet on iteration ``k`` (1-based) tightens the rho-rate + ceiling only when ``k > newt_start`` (amica15.f90:1067, whether or not Newton + is on), while the ``lrate`` ceiling ratchets regardless. + + ``k`` is read off the executing machine's own trajectory: the first ratchet + of the probe. Each fit then stops right after it. With ``newt_start = k`` + the gate is closed and the rho-rate ceiling must not move; with + ``newt_start = k - 1`` it is open and must move exactly once. Before issue + #335 the 0-based comparison kept the gate closed in both. + """ + probe = _probe(backend, X, tmp_path) + hits = _ratchet_indices(probe.ll, _OVERSHOOT["maxdecs"], None) + assert hits, "the probe never completed a maxdecs cycle; the DATA is the problem" + k = hits[0] + 1 # the reference iteration the first ratchet lands on + + for newt_start, rho_ratchets in ((k, 0), (k - 1, 1)): + run = _fit( + backend, + X, + k, + tmp_path, + do_newton=False, + newt_start=newt_start, + rholratefact=0.5, + **_OVERSHOOT, + ) + # Nothing before the ratchet reads newt_start, so this is the probe's + # own trajectory: the ratchet under test is the one located above. + assert run.ll == probe.ll[:k] + f = _OVERSHOOT["lratefact"] + assert _ratchets(run.lrate_ceiling, run.lrate_ceiling0, f) == 1 + assert _ratchets(run.rholrate, run.rholrate0, 0.5) == rho_ratchets, ( + f"newt_start={newt_start}: a ratchet on iteration {k} " + f"{'must' if rho_ratchets else 'must not'} tighten the rho-rate ceiling" + ) + assert run.newtrate == run.newtrate0 # do_newton=False never touches it + + +# A natural-gradient run whose second maxdecs cycle completes on iteration 21, +# one past the shipped default newt_start=20. Found by sweeping seeds 0-5, +# lrate 0.5/0.6/0.8, maxdecs 2/3, newt_ramp 10/1 and 4096/8192 frames for a +# ratchet at ll index 20, first on PyTorch and then checked on NumPy and MLX; +# re-searched when issue #333's component-row doscaling changed the default +# trajectories. This one decreases at ll indices 4, 5, 8 and 9, 19, 20 on all +# three backends, so it ratchets at 8 and 20; the smallest of those decreases +# is 4.7e-4, far above round-off, float32 included. +_DEFAULT_GATE_FRAMES = 4096 +_DEFAULT_GATE_SEED = 1 +_DEFAULT_GATE: dict[str, Any] = dict(lrate=0.6, lratefact=0.5, maxdecs=3, newt_ramp=1) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_rho_rate_ratchet_gate_at_the_shipped_default_newt_start(backend, X, tmp_path): + """The same gate at ``newt_start=20``, the default of every backend: a + ratchet on iteration 21 tightens the rho-rate ceiling, one on iteration 9 + does not, and with ``newt_start=21`` neither does. This is the one + default-path behavior issue #335 changes (``do_newton=False``).""" + x = X[:, :_DEFAULT_GATE_FRAMES] + runs = { + newt_start: _fit( + backend, + x, + 21, + tmp_path, + seed=_DEFAULT_GATE_SEED, + do_newton=False, + newt_start=newt_start, + rholratefact=0.5, + **_DEFAULT_GATE, + ) + for newt_start in (20, 21) + } + hits = _ratchet_indices(runs[21].ll, _DEFAULT_GATE["maxdecs"], None) + assert hits and hits[-1] == 20, ( + f"ratchets at ll indices {hits}; the configuration no longer completes a " + "cycle on iteration 21 on this DATA" + ) + assert runs[20].ll == runs[21].ll # nothing before the ratchet reads it + for newt_start, rho_ratchets in ((20, 1), (21, 0)): + run = runs[newt_start] + assert _ratchets(run.lrate_ceiling, run.lrate_ceiling0, 0.5) == len(hits) + assert _ratchets(run.rholrate, run.rholrate0, 0.5) == rho_ratchets, ( + f"newt_start={newt_start}" + ) + + +# --- the switch-on counter reset --------------------------------------------- + + +# The switch-on reset is observable only when the likelihood decreases right +# after the switch-on iteration while the decrease counter is partly filled, +# which asks the first Newton M-step to overshoot. A search on PyTorch, about 45 +# CPU-minutes (1 and 2 models; 4096 and 8192 frames; seeds 0-3; lrate 0.3 to +# 0.8; maxdecs 2 and 3; newt_ramp 10 and 1; newtrate 2 and 4; up to 8 candidate +# newt_start values per configuration, read off 60-iteration probes), found such +# runs only at lrate=0.8 with newt_start=3, where the natural gradient +# overshoots from the first iteration and the switch-on Hessian is not yet +# positive definite, so the step falls back to the natural gradient and +# overshoots again, plus one two-model run at lrate=0.5 and newt_start=11. This +# one-model configuration is the most robust of them: ``newt_ramp=1`` restores +# the rate after each halving, the decreases are 3.8e-3, 2.0e-2 and 3.0e-2, far +# above round-off, and the pattern holds on seeds 0-5 and all three backends. +_RESET_FRAMES = 4096 +_RESET: dict[str, Any] = dict( + do_newton=True, + newt_start=3, + newtrate=2.0, + newt_ramp=1, + lrate=0.8, + lratefact=0.5, + maxdecs=3, +) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_newton_switch_on_clears_the_decrease_counter(backend, X, tmp_path): + """The decrease counter is cleared on the iteration Newton switches on + (``iter == newt_start``, amica15.f90:1099), not one iteration later. + + With ``newt_start=3`` and ``maxdecs=3`` this 4-iteration fit decreases the + likelihood on iterations 2, 3 and 4 (``ll`` indices 1-3). The reference + clears the count of two after iteration 3, so the decrease on iteration 4 + starts a new count and nothing ratchets. Clearing it one iteration later, + as the 0-based comparison did before issue #335, lets that decrease + complete the cycle and ratchet the ``lrate`` ceiling. The fit's own + trajectory is replayed under both rules to confirm that they disagree + here, and the backend's ceiling must match the reference's. + """ + newt_start, maxdecs = _RESET["newt_start"], _RESET["maxdecs"] + run = _fit(backend, X[:, :_RESET_FRAMES], newt_start + 1, tmp_path, **_RESET) + + decreases = [i for i in range(1, len(run.ll)) if run.ll[i] < run.ll[i - 1]] + assert decreases == [1, 2, 3], ( + f"decreases at ll indices {decreases}; the configuration no longer " + "produces the pattern that exposes the reset on this DATA" + ) + # _ratchet_indices resets where ``i + 1 == newt_start``; passing + # newt_start + 1 replays the pre-fix rule, one iteration later. + reference = len(_ratchet_indices(run.ll, maxdecs, newt_start)) + one_late = len(_ratchet_indices(run.ll, maxdecs, newt_start + 1)) + assert (reference, one_late) == (0, 1) + assert _ratchets(run.lrate_ceiling, run.lrate_ceiling0, 0.5) == reference + + +# --- rejection --------------------------------------------------------------- + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_rejection_first_fires_on_iteration_rejstart(backend, X, tmp_path): + """With ``rejstart=4`` the first rejection pass follows the 4th iteration's + update (``iter == rejstart``, amica15.f90:1136): a 3-iteration fit has not + rejected, a 4-iteration fit has, once, and dropped samples. ``rejint=3`` + keeps the modulo arm quiet before ``rejstart`` (``max(1, iter - rejstart)`` + is 1 there). Before issue #335 the 4-iteration fit had not rejected yet. + """ + cfg: dict[str, Any] = dict( + do_reject=True, rejstart=4, rejint=3, maxrej=1, rejsig=2.0 + ) + before = _fit(backend, X, 3, tmp_path, **cfg) + assert before.numrej == 0 and before.n_good == X.shape[1] + + at = _fit(backend, X, 4, tmp_path, **cfg) + assert at.numrej == 1 + assert at.n_good < X.shape[1] + + +# --- NumPy's restart-on-NaN window -------------------------------------------- + + +class _NaNOnIteration(AMICA_NumPy): + """Poisons the real E-step's likelihood on one 0-based iteration. + + The sanctioned error-injection pattern (``.rules/testing.md``; the same + construction as ``test_sample_data.py``'s ``_InjectEarlyNaN``): every other + code path, the fit and the restart itself included, is the real one on real + data. Needed because a non-finite likelihood is not something the sample + recording produces on demand at a chosen iteration. + """ + + nan_iter = -1 + + def _get_updates_and_likelihood(self): + upd = super()._get_updates_and_likelihood() + if self.iter == self.nan_iter: + upd["ll"] = float("nan") + return upd + + +@pytest.mark.parametrize( + ("restartiter", "nan_iter", "restarts"), [(3, 2, 1), (3, 3, 0), (0, 0, 0)] +) +def test_numpy_restart_window_is_the_first_restartiter_iterations( + restartiter, nan_iter, restarts, X, tmp_path +): + """A non-finite likelihood restarts from a fresh draw only within the first + ``restartiter`` iterations (``iter .le. restartiter``, amica15.f90:1022). + + NumPy only: it is the one backend with a restart-on-NaN recovery (PyTorch + and MLX stop on a non-finite likelihood, and their best-of-N + ``n_restarts`` is a different mechanism). With ``restartiter=3``, a NaN on + the 3rd iteration (index 2) restarts and the fit recovers; a NaN on the 4th + (index 3) is past the window and ends the fit. ``restartiter=0`` disables + the recovery, as in the reference: a NaN on the very first iteration ends + the fit although ``maxrestarts`` would allow a restart. Before issue #335 + both the 4th iteration and, with ``restartiter=0``, the first still + restarted. + """ + m = _NaNOnIteration( + num_models=1, + num_mix=NMIX, + seed=SEED, + block_size=BLOCK, + max_iter=8, + use_tqdm=False, + do_opt_block=False, + writestep=10**7, + restartiter=restartiter, + maxrestarts=3, + outdir=str(tmp_path / "out"), + ) + m.nan_iter = nan_iter + m.fit(X) + assert m.numrestarts == restarts + assert m.converged is bool(restarts), m.stop_reason + if not restarts: + assert m.stop_reason is not None and "Non-finite" in m.stop_reason + assert len(m.ll) == nan_iter + 1 # stopped on the poisoned iteration + + +# --- validation of the settings the gates read ------------------------------ + + +def _construct(backend: str, **kwargs: Any): + """A backend built with ``kwargs`` (torch/MLX spelling), no data touched.""" + if backend == "numpy": + return AMICA_NumPy(use_tqdm=False, **kwargs) + if backend == "torch": + return AMICATorchNG(n_channels=NW, device="cpu", **kwargs) + return _mlx_class()(n_channels=NW, **kwargs) + + +@pytest.mark.parametrize( + ("setting", "value", "minimum", "extra"), + [ + # Checked whether or not Newton is on: it also gates the rho-rate + # ratchet on the natural-gradient path. + ("newt_start", -1, 0, {}), + ("newt_start", -1, 0, {"do_newton": True}), + ("newt_start", 2.5, 0, {}), + ("newt_start", True, 0, {}), + # Counted from 1: rejstart <= 0 would silently skip the reference's + # unconditional first pass. + ("rejstart", 0, 1, {"do_reject": True}), + ("rejstart", -2, 1, {"do_reject": True}), + ("rejstart", 2.0, 1, {"do_reject": True}), + ], +) +@pytest.mark.parametrize("backend", BACKENDS) +def test_schedule_settings_are_rejected_with_one_message( + backend, setting, value, minimum, extra +): + """Every backend refuses the same ``newt_start``/``rejstart`` values with + the same message (``pamica.schedule.validate_iteration_setting``).""" + with pytest.raises(ValueError) as exc: + _construct(backend, **{setting: value}, **extra) + assert str(exc.value) == f"{setting} must be an integer >= {minimum}, got {value!r}" + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_boundary_schedule_settings_are_accepted(backend): + """The smallest meaningful values construct, numpy integers included, and + ``rejstart`` is inert (unchecked) while ``do_reject`` is off, the pattern + ``share_start`` follows.""" + for kwargs in ( + {"newt_start": 0}, + {"newt_start": np.int64(3)}, + {"do_reject": True, "rejstart": 1}, + {"rejstart": 0}, + ): + _construct(backend, **kwargs) + + +@pytest.mark.parametrize( + ("kwargs", "message"), + [ + ( + {"do_history": True, "histstep": 0}, + "histstep must be an integer >= 1, got 0", + ), + ({"restartiter": -1}, "restartiter must be an integer >= 0, got -1"), + ({"maxrestarts": -1}, "maxrestarts must be an integer >= 0, got -1"), + ({"restartiter": 2.5}, "restartiter must be an integer >= 0, got 2.5"), + ], +) +def test_numpy_only_schedule_settings_are_validated(kwargs, message): + """The NumPy backend's own schedule settings (no PyTorch or MLX + counterpart): ``histstep=0`` with ``do_history`` on used to be a bare + ``ZeroDivisionError`` on the first iteration, and a negative + ``restartiter``/``maxrestarts`` has no meaning.""" + with pytest.raises(ValueError) as exc: + AMICA_NumPy(use_tqdm=False, **kwargs) + assert str(exc.value) == message + # The inert and boundary values construct. + AMICA_NumPy(use_tqdm=False, do_history=False, histstep=0) + AMICA_NumPy(use_tqdm=False, restartiter=0, maxrestarts=0) diff --git a/pamica/tests/test_schedule_native_oracle.py b/pamica/tests/test_schedule_native_oracle.py new file mode 100644 index 00000000..bdec0a25 --- /dev/null +++ b/pamica/tests/test_schedule_native_oracle.py @@ -0,0 +1,245 @@ +"""The first Newton iterations against the native reference binary (issue #335). + +Opt-in (``AMICA_RUN_FORTRAN=1``) like the other binary-driven tests. The +binary is the pinned v0.3.3 release asset from :mod:`pamica.native.resolver` +(``PAMICA_NATIVE_BINARY`` overrides it), run single-threaded so its own +reduction order is fixed. + +The binary is seeded with pamica's own initialization through its ``indir``/ +``load_*`` inputs (``A`` in the reference's layout is the stored block +transposed, the issue #24 mapping), so the two sides start from the same state +and differ only in their arithmetic. Newton switches on at ``newt_start=3`` and +``doscaling`` is off, which isolates the Newton schedule from the ``doscaling`` +axis fixed in issue #333. Over 6 iterations (four Newton M-steps) the +log-likelihood trajectory and the mixing matrix of both float64 backends match +the reference to round-off: + +======== ======================= ======================= +backend max abs LL deviation max abs ``A`` deviation +======== ======================= ======================= +PyTorch 5.4e-11 (1.25e-3 before) 4.2e-10 (3.1e-2 before) +NumPy 2.1e-12 (1.25e-3 before) 1.1e-10 (3.1e-2 before) +======== ======================= ======================= + +"Before" is the same run before issue #335, when Newton switched on one +iteration late. The deviations grow with the iteration count on this +recording, Newton or not: one mixture component's shape sits at ``rho=1``, +where the location update divides by ``|y|`` and amplifies round-off by +orders of magnitude per iteration. That is why the comparison stops at the +first Newton iterations rather than running to convergence. +""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path +from typing import Any + +import numpy as np +import pytest +import torch + +from pamica.native import resolver +from pamica.native.engine import _DEFAULT_PARAMS, _render_param +from pamica.numpy_impl.core import AMICA as AMICA_NumPy +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +NMIX = 3 +SEED = 42 +PINNED_BINARY = "v0.3.3" +NEWT_START = 3 +N_ITER = 6 +LL_ATOL = 1e-9 +A_ATOL = 1e-8 + +pytestmark = [ + pytest.mark.skipif( + os.environ.get("AMICA_RUN_FORTRAN") != "1", + reason="opt-in Fortran-binary integration test (set AMICA_RUN_FORTRAN=1)", + ), + pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing"), +] + +# One configuration for all three runs, in the torch spelling; the binary's and +# NumPy's spellings are derived from it below. +_CONFIG: dict[str, Any] = dict( + block_size=512, + lrate=0.05, + lratefact=0.5, + rholrate=0.05, + rholratefact=0.5, + rho0=1.5, + minrho=1.0, + maxrho=2.0, + invsigmin=0.0, + invsigmax=100.0, + newt_start=NEWT_START, + newtrate=1.0, + newt_ramp=10, + maxdecs=3, + doscaling=False, +) + + +@pytest.fixture(scope="module") +def X() -> np.ndarray: + # The whole record: the binary NaNs on a short slice at block size 512 + # (issue #292). + return load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD).astype( + np.float64 + ) + + +def _torch(do_newton: bool = True) -> AMICATorchNG: + return AMICATorchNG( + n_channels=NW, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + keep_best=False, + do_newton=do_newton, + **_CONFIG, + ) + + +def _write_f64(path: Path, arr) -> None: + np.asarray(arr, dtype=" np.ndarray: + arr = np.fromfile(path, dtype=" dict[str, np.ndarray]: + """Run the binary for ``N_ITER`` iterations from pamica's own seeded + initialization; return its log-likelihood trajectory and mixing matrix.""" + init = _torch() + init._preprocess(X) + init._initialize_parameters() + assert init.A is not None and init.mean is not None + indir = workdir / "init" + indir.mkdir() + _write_f64(indir / "A", init.A.numpy().T) # reference layout (issue #24) + _write_f64(indir / "mean", init.mean.numpy().reshape(-1)) + for name, value in ( + ("mu", init.mu), + ("sbeta", init.beta), + ("rho", init.rho), + ("alpha", init.alpha), + ("gm", init.gm), + ("c", init.c), + ): + assert value is not None + _write_f64(indir / name, value.numpy()) + + X.astype(np.float32).ravel(order="F").tofile(workdir / "data.fdt") + params = dict(_DEFAULT_PARAMS) + params.update( + {k: v for k, v in _CONFIG.items() if k not in ("maxdecs", "doscaling")} + ) + params.update( + data_dim=NW, + field_dim=X.shape[1], + num_models=1, + num_mix_comps=NMIX, + max_iter=N_ITER, + max_threads=1, + pcakeep=NW, + do_newton=1, + max_decs=_CONFIG["maxdecs"], + doscaling=int(_CONFIG["doscaling"]), + write_LLt=0, + writestep=10**6, + indir=str(indir), + load_mean=1, + load_sphere=0, + load_A=1, + load_mu=1, + load_beta=1, + load_rho=1, + load_alpha=1, + load_gm=1, + load_c=1, + ) + (workdir / "out").mkdir() + # ``files`` must come first: amica15.f90 stops if it parses other keys first. + param = {"files": "./data.fdt", "outdir": "./out/", **params} + (workdir / "input.param").write_text(_render_param(param)) + proc = subprocess.run( + [str(resolver.resolve(PINNED_BINARY)), "input.param"], + cwd=workdir, + capture_output=True, + text=True, + timeout=900, + ) + assert proc.returncode == 0, proc.stdout[-2000:] + proc.stderr[-2000:] + assert "Starting Newton" in proc.stdout, "the reference never switched Newton on" + ll = _read_f64(workdir / "out" / "LL") + return {"LL": ll[:N_ITER], "A": _read_f64(workdir / "out" / "A", (NW, NW))} + + +@pytest.fixture(scope="module") +def reference(X, tmp_path_factory) -> dict[str, np.ndarray]: + return _seeded_reference_run(X, tmp_path_factory.mktemp("native_newton")) + + +def test_torch_first_newton_iterations_match_the_reference(X, reference): + m = _torch().fit(X, max_iter=N_ITER, verbose=False) + assert len(m.ll_history) == N_ITER and m.n_newton_fallbacks == 0 + ll_err = np.abs(np.asarray(m.ll_history) - reference["LL"]).max() + a_err = np.abs(m.get_mixing_matrix(0) - reference["A"]).max() + assert ll_err < LL_ATOL, f"LL deviates by {ll_err:.2e}" + assert a_err < A_ATOL, f"A deviates by {a_err:.2e}" + + +def test_numpy_first_newton_iterations_match_the_reference(X, reference, tmp_path): + init = _torch() + init._preprocess(X) + init._initialize_parameters() + m = AMICA_NumPy( + num_models=1, + num_mix=NMIX, + seed=SEED, + max_iter=N_ITER, + use_tqdm=False, + do_opt_block=False, + writestep=10**7, + outdir=str(tmp_path / "out"), + do_newton=True, + max_decs=_CONFIG["maxdecs"], + **{k: v for k, v in _CONFIG.items() if k != "maxdecs"}, + ) + # Start from the same initialization the binary was seeded with: the NumPy + # backend draws its own from a different generator, and keeps any parameter + # already set when it initializes. + for name in ("A", "mu", "beta", "rho", "alpha", "gm", "c"): + value = getattr(init, name) + assert value is not None + setattr(m, name, value.numpy().copy()) + m.fit(X) + assert m.comp_list is not None and m.A is not None + assert len(m.ll) == N_ITER + ll_err = np.abs(np.asarray(m.ll) - reference["LL"]).max() + a_err = np.abs(m.A[:, m.comp_list[:, 0]].T - reference["A"]).max() + assert ll_err < LL_ATOL, f"LL deviates by {ll_err:.2e}" + assert a_err < A_ATOL, f"A deviates by {a_err:.2e}" + + +def test_the_window_compared_is_newton_sensitive(X, reference): + """Control: the same seeded run without Newton leaves the reference by + orders of magnitude more than the tolerance above, from the first iteration + a Newton step can reach (index ``NEWT_START``), so the agreement above is a + statement about the Newton schedule rather than about a window Newton never + touched.""" + m = _torch(do_newton=False).fit(X, max_iter=N_ITER, verbose=False) + dev = np.abs(np.asarray(m.ll_history) - reference["LL"]) + assert dev[:NEWT_START].max() < LL_ATOL # the shared natural-gradient prefix + assert dev[NEWT_START] > 1e4 * LL_ATOL diff --git a/pamica/tests/torch_tests/test_amica_ng_wrapper.py b/pamica/tests/torch_tests/test_amica_ng_wrapper.py index de207a1b..79620aab 100644 --- a/pamica/tests/torch_tests/test_amica_ng_wrapper.py +++ b/pamica/tests/torch_tests/test_amica_ng_wrapper.py @@ -248,7 +248,7 @@ def test_write_amica_output_ll_matches_kept_iterate(real_data, tmp_path): real_data[:, :4096], max_iter=150, do_newton=True, - newt_start=1, + newt_start=2, lrate=0.5, newtrate=3.0, use_min_dll=True, diff --git a/pamica/tests/torch_tests/test_ng_backend.py b/pamica/tests/torch_tests/test_ng_backend.py index cdaeb45b..c8817a96 100644 --- a/pamica/tests/torch_tests/test_ng_backend.py +++ b/pamica/tests/torch_tests/test_ng_backend.py @@ -1403,12 +1403,13 @@ def test_keep_best_snapshot_restore_roundtrip(): def _multimodel_keep_best(keep_best: bool) -> AMICATorchNG: """The aggressive-Newton overshoot recipe of ``test_ng_convergence.py`` on the same 4096 real samples, so the same trajectory: it peaks at iteration - 70 and stops via the loosened ``min_dll`` at 71. A fixed 60-iteration + 70 and stops via the loosened ``min_dll`` at 71 (``newt_start=2``, counted + from 1 since issue #335, is the run measured as 1 before it). A fixed 60-iteration budget without that stop overshot on the development machine only; on the CI runners that run was monotone.""" m = AMICATorchNG( n_channels=NW, n_models=2, n_mix=NMIX, seed=0, device="cpu", - dtype=torch.float64, block_size=1024, do_newton=True, newt_start=1, + dtype=torch.float64, block_size=1024, do_newton=True, newt_start=2, lrate=0.5, newtrate=3.0, use_min_dll=True, min_dll=1e-4, maxincs=2, use_grad_norm=False, keep_best=keep_best, ) # fmt: skip @@ -1482,7 +1483,7 @@ def test_keep_best_inactive_under_reject(): x = _load_real_data()[:, :4096] kwargs: dict[str, Any] = dict( n_channels=NW, n_models=2, n_mix=NMIX, seed=0, device="cpu", - dtype=torch.float64, block_size=1024, do_newton=True, newt_start=1, + dtype=torch.float64, block_size=1024, do_newton=True, newt_start=2, lrate=0.5, newtrate=3.0, use_min_dll=True, min_dll=1e-4, maxincs=2, use_grad_norm=False, keep_best=True, ) # fmt: skip @@ -1493,7 +1494,7 @@ def test_keep_best_inactive_under_reject(): assert plain.final_ll_ == max(plain.ll_history) > plain.ll_history[-1] m = AMICATorchNG( - **kwargs, do_reject=True, rejsig=3.0, rejstart=5, rejint=5, maxrej=1 + **kwargs, do_reject=True, rejsig=3.0, rejstart=6, rejint=5, maxrej=1 ) m.fit(x, max_iter=150, verbose=False) assert m.stop_reason not in AMICATorchNG._DEGENERATE_STOP_REASONS @@ -1607,11 +1608,12 @@ def test_rholrate_ratchets_at_maxdecs_not_per_decrease(): ``newtrate=3.0`` since issue #333: with ``doscaling`` rescaling components instead of stored columns, the ``newtrate=1.0`` run is monotone through all 300 iterations (no decrease, so no ratchet to test); at 3.0 it decreases 6 - times and both ceilings ratchet twice. + times and both ceilings ratchet twice. ``newt_start=51`` since issue #335, + which counts it from 1: the run measured as 50 before. """ data = _load_real_data() m = _fresh_ng( - block_size=512, do_newton=True, newt_start=50, newtrate=3.0, lrate=0.05, + block_size=512, do_newton=True, newt_start=51, newtrate=3.0, lrate=0.05, lratefact=0.5, rholrate=0.05, rholratefact=0.5, maxdecs=3, ) # fmt: skip m.fit(data, max_iter=300, verbose=False) diff --git a/pamica/tests/torch_tests/test_ng_convergence.py b/pamica/tests/torch_tests/test_ng_convergence.py index a2ddb7d4..ca4f9df2 100644 --- a/pamica/tests/torch_tests/test_ng_convergence.py +++ b/pamica/tests/torch_tests/test_ng_convergence.py @@ -468,12 +468,13 @@ def test_grad_norm_floor_stop_leaves_wrapper_usable(real_data, tmp_path): # The genuine-overshoot recipe (real 2-model data, aggressive Newton): it peaks # and then stops via the loosened min_dll a couple of iterations later (see -# test_keep_best_restores_genuine_overshoot_under_min_dll_stop). +# test_keep_best_restores_genuine_overshoot_under_min_dll_stop). newt_start +# counts from 1 since issue #335, so 2 is the trajectory measured as 1 before. _OVERSHOOT_KWARGS: dict[str, Any] = dict( n_models=2, seed=0, do_newton=True, - newt_start=1, + newt_start=2, lrate=0.5, newtrate=3.0, block_size=1024, @@ -710,7 +711,8 @@ def test_min_dll_stop_reachable_at_shipped_default_threshold(real_data): particular iteration, so both the budget and the bound track the budget rather than a constant fitted to one machine. """ - ng = _fresh_ng(seed=1, n_mix=1, do_newton=True, newt_start=5, block_size=1024) + # newt_start=6 counts from 1 (issue #335): the run measured as 5 before. + ng = _fresh_ng(seed=1, n_mix=1, do_newton=True, newt_start=6, block_size=1024) ng.fit(real_data[:, :4096], max_iter=2000, verbose=False) assert ng.stop_reason == "min_dll" # A real early stop rather than exhausting the budget. Bound against the diff --git a/pamica/torch_impl/core.py b/pamica/torch_impl/core.py index 9414d381..5a789b69 100644 --- a/pamica/torch_impl/core.py +++ b/pamica/torch_impl/core.py @@ -46,7 +46,6 @@ import logging import math -import numbers import time from typing import Dict, List, Optional, Sequence, Tuple, Union @@ -56,6 +55,7 @@ from .. import blocktune from .. import restarts +from .. import schedule from ..metrics import mir as mir_metric from ..metrics import model_probability_from_loglik, pairwise_mi from ..rank import ( @@ -432,15 +432,23 @@ class AMICATorchNG: (ceiling is ``lrate_cap`` for natural gradient, ``newtrate`` for Newton). do_newton : bool, default=False - Enable the Newton preconditioner for the ``A``/``W`` update once - ``iteration >= newt_start``. Ported from the Fortran reference + Enable the Newton preconditioner for the ``A``/``W`` update from + iteration ``newt_start`` on. Ported from the Fortran reference (``amica17.f90``): natural gradient alone plateaus well short of the Fortran solution, and the Newton step (a per-source-pair 2x2 solve preconditioning the natural gradient by an approximate Hessian) is what closes the gap. newt_start : int, default=20 Iteration at which the Newton step switches on (natural gradient is - used before it, letting the mixture parameters settle first). + used before it, letting the mixture parameters settle first). Counted + from 1, as the reference counts it (``iter .ge. newt_start``): the + Newton step is taken on the ``newt_start``-th iteration, i.e. once + ``iteration + 1 >= newt_start`` for the 0-based ``iteration`` + attribute (issue #335). ``newt_start=0`` fits exactly as ``1`` does, + Newton from the first iteration. It also gates the ``maxdecs`` ratchet + of the ``rholrate`` ceiling (and, under Newton, of ``newtrate``) to + iterations after ``newt_start`` (Fortran amica15.f90:1067/1070), so it + is validated (an integer >= 0) whether or not ``do_newton`` is on. newtrate : float, default=0.5 Maximum learning rate the ramp climbs to while Newton is active (the natural-gradient phase is capped at ``lrate``/``lrate0``). @@ -455,7 +463,10 @@ class AMICATorchNG: log-likelihood. rejstart, rejint, maxrej : int First rejection iteration, interval between rejections, and maximum - number of rejection passes (matching ``amica17.f90:1141-1146``). + number of rejection passes (matching ``amica15.f90:1136``). + ``rejstart`` counts from 1, as the reference counts it (issue #335), + and must be an integer >= 1 when ``do_reject`` is on: ``rejstart <= 0`` + would silently skip the reference's unconditional first pass. rho0, minrho, maxrho, rholrate : float Generalized-Gaussian shape-parameter initialization, clamp bounds, and learning rate. @@ -484,9 +495,9 @@ class AMICATorchNG: pre-#26 implementation. kurt_start, num_kurt, kurt_int : int Adaptive-switch schedule (only used when ``pdftype=1``): first iteration - to re-estimate kurtosis, number of switch passes, and the iteration - interval between them. ``num_kurt=0`` disables switching (the family - stays at its super-Gaussian init). + to re-estimate kurtosis (counted from 1), number of switch passes, and + the iteration interval between them. ``num_kurt=0`` disables switching + (the family stays at its super-Gaussian init). invsigmin, invsigmax : float Clamp bounds for the mixture scale parameter ``beta``. doscaling, scalestep : bool, int @@ -512,8 +523,9 @@ class AMICATorchNG: fires on the LAST fit iteration is reflected in the returned model but trails in ``final_ll_``; see that attribute's comment (issue #269). share_start, share_iter : int - Sharing schedule: first iteration to attempt merges and the interval - between attempts (Fortran ``share_start``/``share_iter``). The A-update + Sharing schedule: first iteration to attempt merges (counted from 1) + and the interval between attempts (Fortran + ``share_start``/``share_iter``). The A-update is held for the first 6 iterations of every cycle (independent of whether a merge fired) so densities can settle; ``share_iter`` must be ``> 6`` so that window never consumes the whole cycle. @@ -696,6 +708,10 @@ def __init__( self.newt_ramp = newt_ramp self.do_newton = do_newton + # Validated whether or not do_newton is on: newt_start also gates the + # rho-rate ceiling ratchet on the natural-gradient path + # (schedule.past_newton_start, amica15.f90:1067). + schedule.validate_iteration_setting("newt_start", newt_start, 0) self.newt_start = newt_start self.newtrate = newtrate self.newtrate0 = newtrate @@ -712,6 +728,9 @@ def __init__( raise ValueError(f"rejsig must be > 0, got {rejsig}") if maxrej < 0: raise ValueError(f"maxrej must be >= 0, got {maxrej}") + # Counted from 1, so rejstart <= 0 would silently disable the + # reference's unconditional ``iter == rejstart`` pass. + schedule.validate_iteration_setting("rejstart", rejstart, 1) self.rho0 = rho0 self.minrho = minrho @@ -770,14 +789,10 @@ def __init__( self.doscaling = doscaling self.scalestep = scalestep - if doscaling and ( - isinstance(scalestep, bool) - or not isinstance(scalestep, numbers.Integral) - or scalestep < 1 - ): + if doscaling: # A zero cadence divides by zero mid-fit; a fractional one fires on # no meaningful schedule. The reference never reads scalestep. - raise ValueError(f"scalestep must be an integer >= 1, got {scalestep!r}") + schedule.validate_iteration_setting("scalestep", scalestep, 1) # Component sharing (Fortran share_comps / identify_shared_comps trigger # amica15.f90:1856, subroutine :1916-1963): periodically merge mixing @@ -1673,7 +1688,9 @@ def _update_parameters(self, acc: Dict[str, torch.Tensor], n_samples: int): # moves mu (amica17.f90:1762-1774), and the NumPy port bakes it in at # accumulation time. Do it here, before self.mu is reassigned below, so # lambda uses this iteration's mu rather than the updated one. - newton_active = self.do_newton and self.iteration >= self.newt_start + newton_active = schedule.newton_active( + self.do_newton, self.iteration, self.newt_start + ) if newton_active: sigma2, lambda_, kappa = self._finalize_newton_stats(acc) @@ -1840,10 +1857,9 @@ def _update_parameters(self, acc: Dict[str, torch.Tensor], n_samples: int): # The reference rescales every iteration (it parses ``scalestep`` but # never reads it, amica15.f90:1843/3686); pamica keeps ``scalestep`` as # an extension counted from 1 like the reference's other cadences - # (iterations s, 2s, ...), so the default 1 is the reference. The - # 1-based rule moves to ``schedule.every`` when epic #324 Phase 9 - # (issue #335) lands. - if self.doscaling and (self.iteration + 1) % self.scalestep == 0: + # (iterations s, 2s, ...; ``schedule.every``), so the default 1 is the + # reference. + if self.doscaling and schedule.every(self.iteration, self.scalestep): self._rescale_components() self._update_unmixing_matrices() @@ -1894,7 +1910,8 @@ def _a_frozen(self) -> bool: amica15.f90:1803). The window fires each cycle regardless of whether that cycle's ``_identify_shared_comps`` actually merged a pair. - Anchored on ``(itf - share_start) % share_iter`` so it stays aligned with + Anchored on ``share_start`` (:func:`pamica.schedule.share_freeze`, which + counts iterations from 1 like the reference) so it stays aligned with the merge schedule for any ``share_start``; the literal Fortran formula uses ``mod(iter, share_iter)`` (misaligned unless share_start is a multiple of share_iter, and a permanent freeze for ``share_iter <= 6``), @@ -1906,10 +1923,7 @@ def _a_frozen(self) -> bool: """ if not self.share_comps or self.n_models < 2: return False - itf = self.iteration + 1 # Fortran-style 1-indexed iteration - if itf < self.share_start: - return False - return (itf - self.share_start) % self.share_iter <= 5 + return schedule.share_freeze(self.iteration, self.share_start, self.share_iter) def _identify_shared_comps(self) -> None: """Merge near-collinear mixing columns across models (Fortran @@ -2601,21 +2615,14 @@ def _fit_once( best_snapshot = self._snapshot_params() # Whether rejection fires this iteration (Fortran schedule, - # amica17.f90:1141-1146). Fortran rejects using the per-sample + # amica15.f90:1136, with rejstart counted from 1 as the reference + # counts it; issue #335). Fortran rejects using the per-sample # log-likelihood from THIS iteration's E-step, i.e. the PRE-update # parameters (loglik is stored in get_updates_and_likelihood before # update_params runs). Capture it here, before _update_parameters, # to match that ordering. - will_reject = ( - self.do_reject - and self.maxrej > 0 - and ( - it == self.rejstart - or ( - max(1, it - self.rejstart) % self.rejint == 0 - and self.numrej < self.maxrej - ) - ) + will_reject = schedule.rejection_due( + self.do_reject, it, self.rejstart, self.rejint, self.numrej, self.maxrej ) if will_reject: assert self.good_idx is not None @@ -2628,14 +2635,11 @@ def _fit_once( # Extended-Infomax adaptive PDF switch (Fortran do_choose_pdfs). Runs # on the kurt_start/num_kurt/kurt_int schedule using the just-updated # W; the new per-source families take effect from the next E-step. - # itf is the Fortran-style 1-indexed iteration. num_kurt=0 disables - # switching (the family stays at its pdftype=1 super-Gaussian init). + # kurt_start counts from 1, like every reference schedule. num_kurt=0 + # disables switching (the family stays at its pdftype=1 + # super-Gaussian init). if self.do_choose_pdfs and self.n_kurt_done < self.num_kurt: - itf = it + 1 - if ( - itf >= self.kurt_start - and (itf - self.kurt_start) % self.kurt_int == 0 - ): + if schedule.periodic_due(it, self.kurt_start, self.kurt_int): self._choose_pdfs(X_use) self.n_kurt_done += 1 @@ -2652,14 +2656,11 @@ def _fit_once( # so a merge on the final iteration lands in the returned # A/W/comp_list but not in the ``ll_history``/``final_ll_`` value # appended just below -- see final_ll_'s comment (issue #269). - if self.share_comps: - itf = it + 1 - if ( - itf >= self.share_start - and (itf - self.share_start) % self.share_iter == 0 - ): - self._identify_shared_comps() - self._update_unmixing_matrices() + if self.share_comps and schedule.periodic_due( + it, self.share_start, self.share_iter + ): + self._identify_shared_comps() + self._update_unmixing_matrices() self.ll_history.append(ll) @@ -2799,9 +2800,11 @@ def _fit_once( numdecs += 1 if numdecs >= self.maxdecs: self.lrate_cap *= self.lratefact - if it > self.newt_start: + if schedule.past_newton_start(it, self.newt_start): self.rholrate *= self.rholratefact - if self.do_newton and it > self.newt_start: + if self.do_newton and schedule.past_newton_start( + it, self.newt_start + ): self.newtrate *= self.lratefact numdecs = 0 @@ -2848,7 +2851,7 @@ def _fit_once( self.stop_reason = "grad_norm" leave = True - if self.do_newton and it == self.newt_start: + if schedule.newton_switches_on(self.do_newton, it, self.newt_start): numdecs = 0 if leave: From 5b6ae4f69eacf6aa18002ce336ed904f73438516 Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Wed, 23 Sep 2026 04:10:17 -0700 Subject: [PATCH 12/26] Phase 8: store components as rows and fix sharing (#342) * Add a loader for pre-change pamica in tests pamica/tests/pre_change.py imports the whole package at a pinned commit from git under a private name, so byte-identity tests can compare the live backends with the pre-change ones in one process. It fetches only when the object is missing (a --depth 1 fetch in a full clone makes it shallow), and fails under CI but skips locally when the commit is unreachable. * Store mixing matrix components as rows All three backends now hold A as (n_comps, n_channels): row k is the reference's column A(:, k), and model h's block is A[comp_list[:, h], :], the same matrix as before, so W = inv(block) is unchanged (issue #334). comp_list now indexes components in A as it does in the densities, so: - the share metric compares pinv(sphere) @ A.T, the components' scalp maps, not stored columns of each block; - a merge ties two components' mixing vectors and densities, and the gm-weighted dAk/zeta step averages one component across models; - doscaling rescales each component row once, shared or not; - accessors, W, the EEGLAB export (reference layout), load_results and the viz helpers follow the new layout. Fits without a merge are byte-identical on every backend; ndtmpsum moves by float round-off (it sums per component row, like the reference). Existing sharing, doscaling and MLX tests are updated to the row layout; two short sharing recipes are retuned because early scans now merge most components. Tested: full non-slow suite and slow tests. * Version saved models for the row layout The PyTorch state_dict is now format_version 4 and the MLX save format 2. A version 3 (PyTorch) or 1 (MLX) payload is converted through the shared pamica.component_layout.rows_from_legacy_columns: without loss when its comp_list is unmerged, and refused with a request to refit when a component id is shared across models, since that merge was made under the old column semantics. AMICA.save files (still version 2) go through the same path. Tested with test_component_rows_persistence.py (24 passed): round trips in the new formats, and payloads written by the pre-change classes loaded from git, converted or refused. * Refuse a stale multi-model A in load_results load_results now checks that each model's block of A inverts the W written beside it (max |A_h W_h - I| <= 1e-3; measured at most 3.8e-7 for float32 MLX exports and 1.1e-15 for float64). A multi-model directory written before issue #334 stored A with components as columns and reads back scrambled (residual 1.5); it is refused with the remedy instead of handing the viz helpers wrong sensor maps. Single-model files are the same bytes in both layouts and still load. * Test component rows and sharing semantics test_component_rows.py pins, on the sample EEG: - byte identity of every unshared configuration against the pre-change package (3627a6e) on PyTorch, NumPy and MLX: 1-3 models, doscaling, Newton, pdftype 0/1 and do_reject; - the metric compares get_sensor_mixing_matrix columns, a planted duplicate merges, and a merged group shares one mixing vector; - the EEGLAB A file is the reference layout, single-model exports are byte-identical, and load_results refuses a pre-change multi-model A; - opt-in (AMICA_RUN_FORTRAN=1): updates from a merged load_comp_list state match amica15mac to float64 round-off on PyTorch and NumPy, with doscaling on and off. All pass locally, including the gated oracle. * Document the component-row layout ADR 0007 records the row layout, the sharing semantics that follow from it, the persistence and export policy, and the measured evidence; ADR 0006 and 0001 are amended, and ADR 0006's initial-A normalization line now points to issue #341. The changelog, the differences page (sharing rows, a new section for issue #334, and row 15: the reference's single-precision density normalizers), and the backends, EEGLAB, validation and API pages describe the new behavior. mkdocs build --strict passes. * Reflow two sharing test docstrings Docstring text only; no test logic changes. * Skip MLX wrapper routes without MLX The merged-save refusal test's wrapper route fitted an MLX model without first checking MLX was installed, so it failed on the Linux runner. _wrapper_fit now performs the check for every wrapper route. Tested with MLX hidden: the new test files pass or skip. * Pin byte identity to the Phase 9 epic head The pre-change package is now 0930c0e, the epic head with Phase 9's schedule gates and without this phase, so the two sides differ by the layout change alone. The full byte-identity matrix (Newton included) and the persistence tests pass against it: 92 passed, 2 gated oracle tests skipped. * Stop tests from fetching pinned commits Tests that load historical code ran `git fetch origin --depth 1`, which in a full clone records a shallow boundary that gc can prune behind (issue #343). pre_change.py now only reads: it checks the pin with `git cat-file -e` and, when the commit is missing, fails under CI and otherwise skips with the command that fetches it (`git fetch origin `, or `git fetch --unshallow origin` in a shallow clone). Pins must be full SHAs. test_mlx_fit_noop.py, the last loader that fetched, now imports the whole package at its pin through the same helper; its fits are still bit-identical. test_pre_change_loader.py runs the loader behind a git wrapper that logs its arguments and execs the real git, and asserts no fetch ran and the shallow state is unchanged (a negative control with a real fetch trips the assertion). Tested: loader and fit-noop tests pass, also with CI set. * Record the single-precision density constants The reference writes its density normalizers as dble(), which gfortran reads in single precision: log(dble(1.772453851)) in the rho == 2 branch is 3.0e-8 above log(sqrt(pi)) (measured in the seeded oracle), and the Gaussian and cosh families' normalizers differ from pamica's double values by 3.7e-10, 2.0e-8 and -2.1e-8 (computed). Row 16 of the differences page records this, citing issue #344, which decides whether to adopt the reference's values. The torch and MLX comments that called _LOG_SQRT_2PI and _LOG_NORM_COSH_* bit-for-bit with the binary are corrected, and the merged-state oracle's maxrho=1.99 cites the issue. Comments and docs only. * Show early mass merges happen in the reference With share_comps on, the pinned binary's scan runs but never merges: Spinv2 is never allocated, so every similarity is NaN, even at comp_thresh=0. Its similarity (Spinv2 = Spinv^T Spinv) applied to its own state after 8 iterations from pamica's initialization merges exactly the pairs the torch and NumPy scans merge (32, 32 and 30 at 0.9, 0.95 and 0.99). In the recipe that went non-finite, the reference's update from pamica's post-scan states collapses in step: the second model's gm falls to 5.04e-3 on both after the first scan, and after the second reaches zero in one iteration and NaN in the next. Two gated tests pin this (AMICA_RUN_FORTRAN=1, both pass). The differences page, ADR 0007, the changelog, the validation table and the torch/MLX comments that called the scan unrunnable now say what it does. * Harden the pre-change loader and its guard pre_change.py now fails under CI, and otherwise skips, with its own message when git is not installed (instead of a raw FileNotFoundError) and when the tests do not run from a git checkout (instead of advising a fetch that cannot help). The repository root is a parameter, and a cached alias must map to the same commit or loading raises. The test's PATH wrapper now refuses fetch, pull and every depth or shallow option without running git, so the committed negative control (a fetch sent through _git) is caught by _assert_no_fetch and leaves the clone untouched. New tests cover missing git, a non-checkout root and alias reuse. Tested: 10 passed, also with CI set; the clone is not shallow afterward. * Validate comp_list dtype in the layout conversion rows_from_legacy_columns now refuses a non-integer comp_list (float or NaN ids) with its own ValueError before the bounds check, instead of letting it reach the indexing. test_component_layout.py adds direct unit tests on small arrays for every ValueError branch: the comp_list rank, the dtype, the A shape, an out-of-range id, an id repeated within a model, and an id shared across models (the refit message), plus a block-for-block conversion of an unmerged, permuted comp_list. Tested: 11 new tests pass; persistence tests unchanged. * Refuse a truncated A in load_results load_results now checks that the A file holds num_comps * nw values before reshaping it to (num_comps, nw), and names the file and both counts when it does not. Before, a length no reshape accepts surfaced as a bare NumPy reshape error, and one short by a whole component reshaped to the wrong width and failed later as a matrix-product error. Tested on truncated copies of a real two-model export (both cases). * Raise instead of assert in NumPy _write_results The fitted-A and outdir preconditions of AMICA_NumPy._write_results are now explicit RuntimeErrors, since assert is stripped under python -O. The comment above them also said the reference omits A from its output; the binary does write it, in the layout pamica now writes. Tested: NumPy write and load tests pass. * Widen the byte-identity matrix The unshared byte-identity test now also covers the Gaussian, logistic and sub-Gaussian cosh families (pdftype 2, 3 and 4, single model, n_mix 1 for family 4), four blocks of 1024 samples, a keep_best fit whose log-likelihood falls so the safeguard restores an earlier snapshot (asserted), and best-of-two restarts, on every backend that supports each. The NumPy backend has only the generalized Gaussian and no keep_best, so those cases run on PyTorch and MLX; the test says so and test_numpy_rejects_other_pdftypes pins the first reason. The helper now lets a configuration override its defaults, and compares final_ll_. Tested with CI=1: 66 passed. * Check the MLX double conversion on save again test_old_mlx_save_converts_losslessly now saves the converted model again, checks the file is format 2 with a component-row A, reloads it, and compares A and every output with the saved model, as the torch test does: a second load must not convert the already-converted A again. Tested: both recipes pass. * Fix the Spinv citation and sharing comments The torch _identify_shared_comps docstring cited Spinv's allocation at :551; it is amica15.f90:569. A test_mlx_newton docstring said merged-away columns and curvature not indexed by mixing column; with components as rows the words are component and mixing vector. Comments only. * Label the merged-oracle figures consistently ADR 0007 and the changelog now say the 3-iteration oracle figures are the worst of doscaling on and off, and give the old column semantics' error as the matching worst value (A 0.21, LL 4.3e-4, not 0.20 and 4e-4); the test comment lists both measurements. Docs and comments only. * Describe component-row sharing in the notes AGENTS.md and .context/progress_summary.md described the old column metric and merge as working. They now describe the component-row semantics (ADR 0007, #334): the metric compares de-sphered component mixing vectors, the merge ties components, and the reference binary's own scan never merges because Spinv2 is never allocated. Docs only. --- .../0001-torch-backend-natural-gradient-em.md | 8 + .../decisions/0006-component-orientation.md | 13 +- .../decisions/0007-component-row-layout.md | 94 ++ .context/decisions/README.md | 3 +- .context/progress_summary.md | 26 +- AGENTS.md | 24 +- benchmarks/benchmark_decompose.py | 2 +- docs/api/amica.md | 2 + docs/api/mlx-backend.md | 4 +- docs/changelog.md | 44 + docs/guides/amica-differences.md | 97 +- docs/guides/backends.md | 2 + docs/guides/eeglab.md | 5 + docs/guides/validation.md | 2 +- pamica/amica.py | 8 +- pamica/component_layout.py | 114 ++ pamica/mlx_impl/core.py | 287 ++-- pamica/numpy_impl/core.py | 198 +-- pamica/numpy_impl/data.py | 44 +- pamica/numpy_impl/load.py | 41 +- pamica/numpy_impl/utils.py | 68 +- pamica/numpy_impl/viz.py | 11 +- pamica/tests/mlx_tests/test_mlx_fit_noop.py | 108 +- pamica/tests/mlx_tests/test_mlx_inv_guard.py | 9 +- .../tests/mlx_tests/test_mlx_kitchen_sink.py | 13 +- pamica/tests/mlx_tests/test_mlx_newton.py | 6 +- .../tests/mlx_tests/test_mlx_persistence.py | 8 +- pamica/tests/mlx_tests/test_mlx_sharing.py | 46 +- pamica/tests/native_oracle.py | 33 +- pamica/tests/pre_change.py | 145 ++ pamica/tests/test_component_layout.py | 82 ++ pamica/tests/test_component_rows.py | 1229 +++++++++++++++++ .../tests/test_component_rows_persistence.py | 310 +++++ pamica/tests/test_doscaling_rows.py | 169 +-- pamica/tests/test_mlx_newton_cross_backend.py | 10 +- .../tests/test_mlx_sharing_cross_backend.py | 33 +- pamica/tests/test_numpy_share_comps.py | 60 +- pamica/tests/test_pre_change_loader.py | 212 +++ pamica/tests/torch_tests/test_ng_sharing.py | 33 +- pamica/torch_impl/core.py | 377 +++-- validate_implementations.py | 7 +- 41 files changed, 3262 insertions(+), 725 deletions(-) create mode 100644 .context/decisions/0007-component-row-layout.md create mode 100644 pamica/component_layout.py create mode 100644 pamica/tests/pre_change.py create mode 100644 pamica/tests/test_component_layout.py create mode 100644 pamica/tests/test_component_rows.py create mode 100644 pamica/tests/test_component_rows_persistence.py create mode 100644 pamica/tests/test_pre_change_loader.py diff --git a/.context/decisions/0001-torch-backend-natural-gradient-em.md b/.context/decisions/0001-torch-backend-natural-gradient-em.md index cad36b7e..9c1efe26 100644 --- a/.context/decisions/0001-torch-backend-natural-gradient-em.md +++ b/.context/decisions/0001-torch-backend-natural-gradient-em.md @@ -80,3 +80,11 @@ The #24 fix left `A` stored with each model's block equal to the TRANSPOSE of th [ADR 0006](0006-component-orientation.md) states that convention precisely, records the `doscaling` defect it caused (stored columns were normalized; fixed in epic #324 Phase 7) and the planned move to a component-row layout for `share_comps` (Phase 8, issue #334). + +## Addendum (2026-09-23, issue #334) + +[ADR 0007](0007-component-row-layout.md) replaces the storage this ADR introduced: +`A` is now `(n_comps, n)` with one component per row and `comp_list` indexing rows, +so each model's block, and every result without a `share_comps` merge, is unchanged, +while the share metric and merge act on components as the reference's do. +Saved models convert without loss unless a merge had fired. diff --git a/.context/decisions/0006-component-orientation.md b/.context/decisions/0006-component-orientation.md index 97c7641f..0700ce71 100644 --- a/.context/decisions/0006-component-orientation.md +++ b/.context/decisions/0006-component-orientation.md @@ -1,6 +1,6 @@ # ADR 0006: Components are rows of each stored mixing block -**Status:** accepted +**Status:** accepted; the storage part amended by [ADR 0007](0007-component-row-layout.md) **Date:** 2026-09-22 **Owner:** Seyed Yahya Shirazi @@ -44,11 +44,12 @@ pamica keeps `scalestep` as an extension, now counted from 1 like the reference's other cadences (iterations `scalestep`, `2*scalestep`, ...), so the default of 1 is the reference (row 14 of `docs/guides/amica-differences.md`). -Epic #324 Phase 8 (issue #334) then changes the storage itself to component rows, +Epic #324 Phase 8 (issue #334, [ADR 0007](0007-component-row-layout.md)) then changed the storage itself to component rows, `A` of shape `(n_comps, n)` with `comp_list` indexing rows, -so a stored row is a component in every configuration, including merged ones. -Until then, a column merged by `share_comps` belongs to rows of several blocks, where the per-block rule is not a change of scale; -it is applied uniformly, model by model, and Phase 8 replaces it. +so a stored row is a component in every configuration, including merged ones, +and the rescale acts on each component row once. +Until then, a column merged by `share_comps` belonged to rows of several blocks, where the per-block rule is not a change of scale; +it was applied uniformly, model by model. ## Consequences @@ -75,7 +76,7 @@ it is applied uniformly, model by model, and Phase 8 replaces it. - Remaining known difference: pamica does not normalize its drawn initial `A` (the reference normalizes a drawn one, amica15.f90:818-819, but not a loaded one). The two draws come from different random generators anyway, and the first iteration's rescale normalizes every component. - Epic #324 Phase 8 (issue #334) aligns it with the reference while it rewrites the storage layout and the initialization. + Aligning it changes default trajectories, so it is its own change, issue #341, not part of the layout change of Phase 8. ## Alternatives considered diff --git a/.context/decisions/0007-component-row-layout.md b/.context/decisions/0007-component-row-layout.md new file mode 100644 index 00000000..eb7ed2f1 --- /dev/null +++ b/.context/decisions/0007-component-row-layout.md @@ -0,0 +1,94 @@ +# ADR 0007: Store the mixing matrix with one component per row + +**Status:** accepted +**Date:** 2026-09-23 +**Owner:** Seyed Yahya Shirazi + +Amends [ADR 0006](0006-component-orientation.md), which named the per-block convention and fixed `doscaling` within it, +and [ADR 0001](0001-torch-backend-natural-gradient-em.md), which introduced the storage. + +## Context + +Since issue #24 every array backend stored `A` as `(n, n_comps)` and took model `h`'s block as `A[:, comp_list[:, h]]`, +the transpose of the reference's per-model mixing matrix, so a component was a ROW of its block +while the component ids in `comp_list`, and the density columns `mu[:, k]`, `beta[:, k]`, `alpha[:, k]`, `rho[:, k]`, indexed stored COLUMNS. +A stored column held one sphered channel's loadings across a model's components, not a component. +ADR 0006 made `doscaling` act on block rows, but `share_comps` still used the column ids for both of its steps (issue #334): + +- The metric compared de-sphered stored columns, not scalp maps. + On a two-model fit of the sample EEG (150 iterations) the |cosine| between a stored column and the true map `get_sensor_mixing_matrix(h)[:, i]` had median 0.27 and 0.39; + a planted exact duplicate component scored 0.74 and was not merged at `comp_thresh=0.99`. +- The merge tied one channel's loadings across two models instead of two components' mixing vectors, + and the `gm`-weighted `dAk/zeta` average then averaged those. + Forcing it on a planted identical pair changed the log-likelihood by -0.186, where the reference's fold changes it by exactly 0. + +The reference's own scan never merges (its `Spinv2` is declared but never allocated, so every similarity is NaN), +but its `load_comp_list` seeds a merged `comp_list`, and its update from that state is a bit-level oracle. + +## Decision + +Every array backend (PyTorch, NumPy, MLX) stores `A` as `(n_comps, n)`: +row `k` is component `k`'s sphered-space mixing vector, the reference's column `A(:, k)`, +and model `h`'s block is `A[comp_list[:, h], :]`, the same `n x n` matrix as before, so `W_h = inv(block)` is unchanged. +Everything that touches `A` follows: +the natural-gradient and Newton update scatter each model's step into the rows `comp_list` names (the `dAk/zeta` average now averages one component across the models that share it); +`doscaling` rescales each component row once, including a shared one, and leaves a merged-away row alone; +the share metric compares `pinv(sphere) @ A.T`, whose column `comp_list[i, h]` is column `i` of `get_sensor_mixing_matrix(h)`; +the merge re-points `comp_list` and retires the folded row, as the reference does, copying and averaging nothing. +The per-backend helper `_component_sensor_maps` returns the vectors the metric compares. + +Persistence converts or refuses, through one shared function (`pamica.component_layout.rows_from_legacy_columns`): +the PyTorch `state_dict` is `format_version` 4 and the MLX save format 2; +a version 3 (PyTorch) or version 1 (MLX) payload is converted without loss when its `comp_list` is unmerged +(every block carries over element for element), +and refused with a message to refit when a component id is shared across models, +because that merge was computed under the column semantics and has no component-row counterpart. +The `AMICA` wrapper's own save (version 2) wraps the backend payloads and goes through the same path. + +The EEGLAB export writes `A` in the reference's layout for every model count: +`(nw, num_comps)` column-major, the bytes of the component-row `A` in C order. +`loadmodout15.m` ignores this file; `load_results` reads it back +and refuses an `A` that does not invert the `W` beside it (a multi-model directory written before this change). + +## Consequences + +- Every configuration without a merge is byte-identical to the previous code on every backend: + one, two and three models, `doscaling` and Newton on and off, `pdftype` 0 and 1 (NumPy supports 0 only), and `do_reject`, + pinned against the pre-change package loaded from git. + The weight-gradient norm `ndtmpsum` moves by float round-off (2.2e-16 relative in float64, one float32 unit on MLX): + it now sums squares per component row, as the reference sums per component. +- Fits with `share_comps=True` where a merge fires change, toward the reference. + From a merged `load_comp_list` state the PyTorch and NumPy updates match the native binary to float64 round-off + (after 3 iterations, worst of `doscaling` on and off: `A` 1.5e-12, `mu` 3.7e-9, `sbeta` 1.8e-11, log-likelihood 4.7e-14, at the no-merge noise floor), + where the column semantics was off by 0.21 in `A` and 4.3e-4 in log-likelihood (worst of the same two). + End to end (2 models, 300 iterations, `comp_thresh=0.95`), the scan now merges three true pairs (map |cos| 0.956-0.971) + where the column metric merged pairs whose maps had |cos| 0.06, 0.35 and 0.55. +- Merging is earlier and more complete when the models are still similar: + both models start near the identity, so a scan in the first iterations now merges most components + (it compares their true maps; on the sample with seed 42, 32 of 32 at iteration 8 with `comp_thresh=0.95`, 24 with 0.99), + and a model that loses its responsibility can collapse. + The reference behaves the same way: its similarity on its own early state merges exactly the pairs pamica's does, + and its update from the same merged states loses the second model's responsibility in step with pamica's, to NaN where pamica's goes non-finite. + The reference's default `share_start` of 100 avoids this; several short test recipes were retuned. +- Saved models: unmerged saves load as before; saves with merged components must be refit. + Multi-model EEGLAB directories written before this change must be written again for `load_results`. +- Initial-`A` normalization is not part of this change: the reference normalizes a drawn initial `A` and pamica does not, + which changes default trajectories and is issue #341. + +## Alternatives considered + +- **Keep the column layout and translate component ids to block rows inside the share code:** + rejected. A component shared by two models sits in a row of two different blocks in that layout, + so the tied object cannot be stored once, and every future per-component operation would need the same translation. +- **Convert merged old saves by re-deriving the merge under the new metric:** + rejected. The old merge tied parameters that are not components, so there is no fitted state to convert; a refit is the only honest answer. +- **Leave the EEGLAB `A` in the old C-order layout:** rejected. The file then had no meaning outside pamica for several models, + and the reference's layout is also what `load_results` should read from a reference run. + +## Receipts + +- amica15.f90:1749-1761 (`dAk/zeta` over `comp_list`), :1843-1851 (`doscaling` over components), + :1916-1963 (`identify_shared_comps`), :825-846 (`load_comp_list`, which reads the file named `c`). +- Issues #334 (this change), #24 (the storage convention), #333 and ADR 0006 (`doscaling`), #341 (initial `A`), epic #324. +- `pamica/tests/test_component_rows.py` (byte identity, semantics, export, the gated merged-state oracle), + `pamica/tests/test_component_rows_persistence.py` (round trips, conversion, refusal). diff --git a/.context/decisions/README.md b/.context/decisions/README.md index 0995a247..84e50428 100644 --- a/.context/decisions/README.md +++ b/.context/decisions/README.md @@ -30,4 +30,5 @@ Add new entries here as you create ADRs: - ADR 0003 - [Return the best-log-likelihood iterate from AMICATorchNG.fit](0003-best-iterate-safeguard.md) (accepted) - ADR 0004 - [Rank-deficient input handling and a relative eigenvalue floor](0004-rank-deficient-input-handling.md) (accepted) - ADR 0005 - [Restore the PCA residual in the MNE export](0005-restore-pca-residual-in-mne-export.md) (accepted) -- ADR 0006 - [Components are rows of each stored mixing block](0006-component-orientation.md) (accepted; amends ADR 0001) +- ADR 0006 - [Components are rows of each stored mixing block](0006-component-orientation.md) (accepted; amends ADR 0001; storage amended by ADR 0007) +- ADR 0007 - [Store the mixing matrix with one component per row](0007-component-row-layout.md) (accepted; amends ADRs 0001 and 0006) diff --git a/.context/progress_summary.md b/.context/progress_summary.md index 8ee229dc..13b8f0ae 100644 --- a/.context/progress_summary.md +++ b/.context/progress_summary.md @@ -47,23 +47,21 @@ what remains as of the v0.1.0 preparation. `stop_reason_`; `transform` / `get_mixing_matrix` / `get_unmixing_matrix` / `save` raise a clear degenerate error instead of returning NaN sources. -### Component sharing (issue #60) -- `share_comps` ported to `AMICATorchNG`: on the `share_start`/`share_iter` schedule, components - that are near-collinear across different models (cosine angle of their de-sphered mixing columns - above `comp_thresh`) are merged into one shared mixing column and density, with an A-freeze for - ~6 iterations after each merge (Fortran `identify_shared_comps`, amica15.f90:1898). -- The M-step already sums sufficient statistics through `comp_list` (index_add), so shared - components update jointly; the A-update was refactored to accumulate columns as Fortran's - `gm`-weighted average (`dAk/zeta`, so shared columns are averaged, not summed) -- byte-identical - to the per-model update when unshared. +### Component sharing (issues #60, #334) +- `share_comps` runs on all three backends. Since #334 (ADR 0007) `A` stores one component per row, + so the metric compares de-sphered component mixing vectors (rows of `A` through `pinv(sphere)`, + the scalp maps) across models, and a merge above `comp_thresh` ties the two components: `comp_list` + is re-pointed and they share one mixing vector and density, with an A-freeze for 6 iterations + after each merge (Fortran `identify_shared_comps`, amica15.f90:1916). +- The `gm`-weighted `dAk/zeta` step averages a shared component over its models; byte-identical to + the per-model update when no merge fires. - `keep_best` (#51) is disabled under sharing (a merge changes the parameter count, so pre-/post-merge LLs are not comparable and restoring an earlier snapshot would revert the merge); `fit` returns the last, merged iterate like Fortran. -- OFF by default, and a no-op for `n_models=1`, so single-model (#24) and default multi-model (#27) - results are byte-for-byte unchanged (verified by the full torch suite). No bit-exact oracle: the - reference's `Spinv2` similarity metric is *declared but never allocated* (unrunnable, like the dead - `do_choose_pdfs`, #26), so the intended algorithm is implemented and behavior-validated. See - `tests/torch_tests/test_ng_sharing.py`. +- OFF by default, and a no-op for `n_models=1`. The reference binary's own scan never merges + (`Spinv2` is never allocated, so every similarity is NaN), but the update from a merged state + seeded through `load_comp_list` matches PyTorch and NumPy to float64 round-off + (`pamica/tests/test_component_rows.py`, opt-in with `AMICA_RUN_FORTRAN=1`). ### Structure and infrastructure - Module rename into `numpy_impl/` and `torch_impl/` with topic-based names (issue #34); the public diff --git a/AGENTS.md b/AGENTS.md index 426c87d4..c8a1878b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -146,18 +146,18 @@ dead code even in amica15 (the moment buffers are never accumulated), so the aut bit-exact oracle and is validated by real-data LL. `pdftype=0` stays the default and is byte-for-byte unchanged. See `.context/decisions/` and `pamica/tests/torch_tests/test_ng_pdf_families.py`. -**Component sharing (#60): DONE.** `share_comps` multi-model reassignment is ported to -`AMICATorchNG`: on the `share_start`/`share_iter` schedule, components near-collinear across -different models (cosine angle of their de-sphered mixing columns above `comp_thresh`) are merged -into one shared mixing column + density, with an A-freeze for ~6 iterations after each merge -(Fortran `identify_shared_comps`, amica15.f90:1916). The M-step already sums sufficient stats -through `comp_list`; the A-update was refactored to accumulate shared columns the same way -(byte-identical when unshared), and merged-away columns are frozen (avoiding 0/0 NaN that Fortran -tolerates behind its `comp_used` mask). OFF by default and a no-op for `n_models=1`, so single-model -(#24) and default multi-model (#27) parity stay byte-for-byte (full torch suite green). The A-update -is the Fortran `gm`-weighted average (`dAk/zeta`), so shared columns are averaged not summed. No -bit-exact oracle: the reference `Spinv2` metric is *declared but never allocated* (unrunnable, like -the dead `do_choose_pdfs`, #26), so it is behavior-validated (`tests/torch_tests/test_ng_sharing.py`). +**Component sharing (#60, #334): DONE.** `share_comps` runs on all three backends. Every backend +stores `A` as `(n_comps, n_channels)`, one component per row (ADR 0007, #334), so a `comp_list` id +names the same component in `A` and in the densities. On the `share_start`/`share_iter` schedule, +components whose de-sphered mixing vectors (rows of `A` mapped through `pinv(sphere)`, i.e. the +scalp maps) are near-collinear across models, with cosine above `comp_thresh`, are merged: the merge +re-points `comp_list`, so the two sources share one component (mixing vector and density), and the +`gm`-weighted `dAk/zeta` step averages that component over the models that share it +(Fortran `identify_shared_comps`, amica15.f90:1916). Merged-away rows are frozen. A-freeze for 6 +iterations after each merge. OFF by default; byte-identical when no merge fires. The reference +binary's own scan never merges (`Spinv2` is never allocated, so every similarity is NaN), but the +update from a merged state seeded through `load_comp_list` matches it to float64 round-off +(`pamica/tests/test_component_rows.py`, `AMICA_RUN_FORTRAN=1`). **Open (non-blocking, tracked):** - **Multi-model (#27): VALIDATED by distributional equivalence.** Multi-model AMICA is not diff --git a/benchmarks/benchmark_decompose.py b/benchmarks/benchmark_decompose.py index 2824d78f..605a6954 100644 --- a/benchmarks/benchmark_decompose.py +++ b/benchmarks/benchmark_decompose.py @@ -123,7 +123,7 @@ def _fit_mlx(data, iters): W_np = np.array(m.W) idx = np.array(m.comp_list)[:, 0].astype(int) W = W_np[0].T # (comps x channels), matching torch get_unmixing convention - A = A_np[:, idx].T + A = A_np[idx, :].T # component rows of model 0 (issue #334 layout) assert m.final_ll_ is not None return {"time": elapsed, "final_ll": float(m.final_ll_), "W": W, "A": A} diff --git a/docs/api/amica.md b/docs/api/amica.md index 4a639f6e..c4c22ce1 100644 --- a/docs/api/amica.md +++ b/docs/api/amica.md @@ -25,5 +25,7 @@ so the transform can be composed by hand: `save` writes `format_version` 2, which records the backend; `load` restores the model on that backend and still reads version 1 files, which predate backend selection and always hold a PyTorch model. +A model saved before issue #334, which changed how the mixing matrix is stored, is converted on load without loss; +one in which `share_comps` had merged components raises `ValueError` and must be refit. ::: pamica.AMICA diff --git a/docs/api/mlx-backend.md b/docs/api/mlx-backend.md index 92af83a0..280c3e80 100644 --- a/docs/api/mlx-backend.md +++ b/docs/api/mlx-backend.md @@ -23,7 +23,9 @@ Source extraction (`transform` and the `get_mixing_matrix`/`get_unmixing_matrix` `get_sensor_mixing_matrix`/`get_rho` accessors) and persistence (`state_dict`/`from_state_dict` and `.npz` `save`/`load`) are implemented (epic #278 Phase 1, issue #287); the `.npz` format is device- and framework-agnostic -(JSON-encoded config/extra plus native param arrays, no torch coupling). The +(JSON-encoded config/extra plus native param arrays, no torch coupling). +Since issue #334 the format is version 2, which stores the mixing matrix `A` with one component per row; +a version 1 save is converted on load, unless `share_comps` had merged components, which raises `ValueError`. The best-iterate safeguard (`keep_best`) is implemented (epic #278 Phase 2, issue #288). Outlier rejection (`do_reject`), the LLt-stash-backed scoring accessors (`model_loglik`/`model_probability`), the EEGLAB `write_amica_output` export, diff --git a/docs/changelog.md b/docs/changelog.md index 1c5ba758..ccbb5228 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,50 @@ Release notes are also published on the ## Unreleased +- **Fix: tests no longer make a full clone shallow (issue #343).** + Tests that load historical code ran `git fetch origin --depth 1` to reach the pinned commit; + in a full clone that records a shallow boundary, after which `git gc` can prune history. + Every such test now goes through `pamica/tests/pre_change.py`, which only reads the repository: + when the pinned commit is missing it fails under `CI` and otherwise skips, + naming the command that fetches it (`git fetch origin `, or `git fetch --unshallow origin` in a shallow clone). + `pamica/tests/test_pre_change_loader.py` runs the loader behind a logging `git` wrapper and asserts that nothing is fetched. + +- **Phase 8 of epic #324: `share_comps` compares and merges components (issue #334).** + Every backend (PyTorch, NumPy and MLX) now stores the mixing matrix with one component per row, + `A` of shape `(n_comps, n_channels)`, the reference's `A` transposed ([ADR 0007](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0007-component-row-layout.md)). + A component id in `comp_list` now names the same component in `A` as in the density parameters, + so the share metric compares the components' scalp maps (`get_sensor_mixing_matrix`) + and a merge ties the two components' mixing vectors and densities, as the reference's `identify_shared_comps` does. + Before, both steps used stored columns of each model's block, which are not components. + - **Behavior change: fits with `share_comps=True` in which a merge fires now differ.** + Refit them. + Seeded with a merged `comp_list` through the reference's `load_comp_list`, + the PyTorch and NumPy updates match the native binary to float64 round-off + (after 3 iterations, worst of `doscaling` on and off: `A` 1.5e-12, `mu` 3.7e-9, log-likelihood 4.7e-14), + where the previous code was off by 0.21 in `A` and 4.3e-4 in log-likelihood. + On the bundled sample (2 models, 300 iterations, `share_start=100`, `comp_thresh=0.95`) + the scan now merges three pairs whose maps agree (|cos| 0.956 to 0.971), ending at log-likelihood -3.3416, + where it merged pairs whose maps did not (|cos| 0.06, 0.35 and 0.55) and ended at -3.3484 (-3.3387 with sharing off). + Because the metric now sees how similar the two models still are early in a fit, a scan in the first iterations merges most components, + and a model left with few components of its own can then collapse. + The reference behaves the same way: its similarity on its own early state merges the same pairs, + and its update from the same merged states collapses in step (its own scan never merges, because its similarity is NaN). + The reference's default `share_start=100` avoids that. + - Every fit without a merge is byte-identical to before on every backend, sharing off or scheduled but not firing, + with one exception at float round-off: the weight-gradient norm (`ndtmpsum`), which now sums per component like the reference. + - **Persistence:** the PyTorch `state_dict` is now `format_version` 4 and the MLX save format 2. + An older save loads unchanged in content, converted without loss, unless `share_comps` had merged components: + such a save raises `ValueError` asking for a refit, because its merges were computed under the old semantics. + `AMICA.save` files (still `format_version` 2) go through the same conversion and refusal. + - **EEGLAB export:** the `A` file is now the reference's `A(nw, num_comps)` in column-major order for any number of models + (EEGLAB's `loadmodout15.m` ignores it; single-model files are byte-identical to before). + `pamica.numpy_impl.data.load_results` reads it in that layout + and refuses a multi-model directory written by an earlier version, whose `A` does not invert the `W` beside it; + write such a directory again from the fitted or reloaded model. + - Row 16 of the differences page records that the reference's density normalizers are single-precision literals: + in the exact-Gaussian branch (`rho == 2`) 3.0e-8 away from the one pamica uses, and up to 2.1e-8 in the `pdftype` 1, 2 and 4 families. + Issue #344 decides whether to adopt them; the comments that called pamica's constants bit-for-bit with the binary are corrected. + - **Phase 7 of epic #324: `doscaling` rescales components, as the reference does (issue #333).** **Behavior change:** default fits on every backend (PyTorch, NumPy and MLX) now follow the reference's trajectory, so their fitted parameters differ from those of earlier versions. diff --git a/docs/guides/amica-differences.md b/docs/guides/amica-differences.md index 077de221..387d1b9e 100644 --- a/docs/guides/amica-differences.md +++ b/docs/guides/amica-differences.md @@ -19,7 +19,7 @@ that is not listed, that is a bug worth | 5 | Degenerate fits | returns NaN sources, and writes them out on its `writestep` cadence | the `AMICA` wrapper, on either backend, refuses `transform`/`get_*`/`save`; the raw `AMICATorchNG`, `AMICAMLXNG` and NumPy `AMICA` backends now refuse their own output accessors on a degenerate fit too (issue #306), not just the wrapper; NumPy additionally reports `converged=False` with a `stop_reason`, refuses the final write, and skips each periodic checkpoint with a logged reason (leaving the last valid one on disk) | NaN sources silently poison downstream analysis, and `loadmodout` reads a NaN checkpoint back without complaint | — (see issues #50, #240 and #306) | | 6 | Precision | float64 | float64 (float32 on Apple GPUs) | Apple GPUs have no float64; float32 agrees to ~7 significant digits, not bit-parity | `dtype=torch.float64` | | 7 | Sensor-space maps | `Spinv` applied internally | `get_sensor_mixing_matrix()` | `get_mixing_matrix()` returns sphered-space `A`; switching its meaning by data conditioning would be worse | — | -| 8 | Columns merged away by `share_comps` | updated to NaN, then hidden by the `comp_used` mask | frozen at their last finite value (never divided) | a fit must not end holding NaN parameters, mask or no mask; the columns are dead either way | — (see issues #60, #240) | +| 8 | Components merged away by `share_comps` | mixing vector and density updated to NaN, then hidden by the `comp_used` mask | frozen at their last finite value (never divided, never rescaled) | a fit must not end holding NaN parameters, mask or no mask; the components are dead either way | — (see issues #60, #240, #334) | | 9 | Block-size search | on (`do_opt_block=1`), sweeps 128–1024, **aborts** if a candidate cannot allocate | off; sweeps 4096–32768; a candidate that cannot allocate is skipped and the fit continues | the choice is timing-based and therefore machine-dependent, which a parity run cannot have; Fortran's range sits far below where any pamica backend peaks; and running out of memory is a reason to use a smaller block, not to stop | `do_opt_block=True` (but pin `block_size` for a bit-for-bit comparison) | | 10 | Restarts across seeds | none (its `maxrestarts` only *recovers* from an early NaN) | available as `n_restarts`, **off by default** (`n_restarts=1`) | the weakest under-determined components are init-basin sensitive, so best-of-N buys robustness; but a default that ran N fits would change every result and cost N times as long | `n_restarts=1` (the default) | | 11 | Reconstruction after rank reduction (`AMICAICA.apply`) | output has no representation of the discarded principal component analysis (PCA) subspace (sphere rows past `numeigs` are zero), so any back-projection drops it | the MNE export carries the full PCA basis, so `apply` restores the residual | MNE's own `ICA` does; the residual was never part of the independent component analysis (ICA) decomposition, so it is not ICA's to remove | `apply(..., n_pca_components=ica.n_components_)` | @@ -27,21 +27,29 @@ that is not listed, that is a bug worth | 13 | Preprocessing with `do_sphere=False` | divides each channel by its standard deviation and adds a log-determinant term to the likelihood (amica15.f90:516-526) | identity sphere with a zero log-determinant, on all three array backends: the data are fitted unscaled | not a deliberate choice: an existing divergence, found during epic #324 and not yet ported | none yet (issue #328) | | 14 | `scalestep` | parsed (amica15.f90:3686), never used; rescales every iteration (:1843) | rescales every `scalestep` iterations counted from 1; default 1 matches the reference | pamica has always honored the keyword, and keeping it costs nothing; since issue #333 it counts from 1 like the reference's live cadences (`writestep`, `histstep`) instead of firing on the first iteration | `scalestep=1`, the default | | 15 | Restart after a non-finite likelihood (NumPy backend only) | within the first `restartiter` iterations, redraws `A` up to `maxrestarts + 1` times (`numrestarts > maxrestarts` ends the run, amica15.f90:1022-1049); `startover` is set at :1046 and never cleared, so after the first restart the binary never calls `update_params` again (:1115-1122) and the redrawn parameters are never fitted | redraws `A` up to `maxrestarts` times within the same window (counted from 1; `restartiter=0` disables it) and resumes fitting after each restart, with the learning rates and likelihood history reset; the restart uses up that iteration of `max_iter` | the reference's recovery path cannot resume, so its count has nothing to recover into; a restart that fits is the evident intent. PyTorch and MLX have no restart-on-NaN path at all: they stop on the non-finite likelihood (`stop_reason="nan_ll"`), and their `n_restarts` (row 10) is a different mechanism | none for the stalled loop; `maxrestarts` one higher reproduces the reference's count, and `restartiter=0` stops on the first non-finite likelihood, as the reference does with `restartiter=0` | +| 16 | Single-precision density normalizers | `log(dble(1.772453851))` in the exact-Gaussian branch of the generalized Gaussian (`rho == 2`, amica15.f90:1313) is a single-precision literal widened to double, 3.0e-8 above `log(sqrt(pi))`; the Gaussian and cosh families (`pdftype` 2, 4 and 1, :1333, :1359, :1371) use literals of the same kind | `0.5 * log(pi)` for `rho == 2`; the double-precision values of the decimal literals for the other families, which differ from the reference's in the log by 3.7e-10, 2.0e-8 and -2.1e-8 | not a deliberate choice: found during epic #324 Phase 8, while seeding the reference from warm states in which mixtures sit at `maxrho = 2`; each such mixture's log-density differs by 3.0e-8, weighted by its responsibility (2.8e-9 in the log-likelihood of one warm two-model state of the sample). The other families' offsets are computed from the literals, not yet measured against the binary | none yet (issue #344 decides whether to adopt the reference's values); the seeded oracles keep `maxrho` below 2 | Rows 1, 2 and 7 arrived with [ADR 0004](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0004-rank-deficient-input-handling.md); -row 3 with ADR 0003; row 5 with issue #50, extended to the raw backends by issue #306; row 8 with issues #60 and #240; +row 3 with ADR 0003; row 5 with issue #50, extended to the raw backends by issue #306; row 8 with issues #60, #240 and #334; row 9 with issue #232; row 10 with issue #198; row 11 with issue #322 (ADR 0005); row 12 with issue #323; row 13 is recorded, not yet resolved, by issue #328; row 14 with issue #333 (ADR 0006); -row 15 with issue #335, which also made the NumPy restart window count from 1. +row 15 with issue #335, which also made the NumPy restart window count from 1; +row 16 is recorded, not yet resolved, by issue #344. Two `share_comps` details are pamica's own because the reference cannot decide them: the A-freeze window after a merge is anchored on `share_start` (the literal `mod(iter, share_iter)` misaligns unless `share_start` is a multiple of -`share_iter`, and freezes A permanently for `share_iter <= 6`, which both array +`share_iter`, and freezes A permanently for `share_iter <= 6`, which the array backends reject up front), and the merge similarity metric has no bit-exact -oracle at all — the reference's `Spinv2` is declared but never allocated, so its -own reassignment is unrunnable. +oracle, because the reference's `Spinv2` is declared but never allocated. +Its scan still runs, but every similarity it computes is NaN, so a reference run with `share_comps` on never merges anything: +seeded from pamica's initialization, the pinned binary leaves `comp_list` unchanged +and reports 64 unique components after a scan at iteration 8, even at `comp_thresh=0`. +The merged state the scan would produce does have an oracle: +the reference's `load_comp_list` seeds a merged `comp_list`, and the PyTorch +and NumPy updates from such a state match the native binary to float64 round-off +([below](#component-sharing-compares-and-ties-components-issue-334)). ## 1. Relative rank threshold @@ -254,7 +262,7 @@ is simply absent from the constructor, rather than silently downgrading. One MLX failure mode used to be worse than loud — it was uncatchable. MLX 0.32's CPU-stream `mx.linalg.inv` does not raise a Python exception on a -singular per-model unmixing matrix `A[:, comp_list[:, h]]`: LAPACK's LU +singular per-model mixing block `A[comp_list[:, h], :]`: LAPACK's LU failure aborts the whole process (`libc++abi: ... [Inverse::eval_cpu] LU factorization failed`), which no `try`/`except` around `fit` can catch. Issue #274 closed that gap: `_update_unmixing_matrices` now condition-checks @@ -327,11 +335,12 @@ Component sharing was the other gap, closed by issue #263: `AMICAMLXNG` now take backend's names, defaults and validation, runs the same merge schedule and post-merge A-freeze, and exposes `comp_used`/`shared_components()`. It does not re-derive the merge metric — it calls the same -`identify_shared_components` kernel the NumPy backend uses, on host float64 -`pinv(sphere) @ A`, so all three backends decide identically from one fitted -state (`pamica/tests/test_mlx_sharing_cross_backend.py` pins that against +`identify_shared_components` kernel the NumPy backend uses, on the host float64 +sensor maps `pinv(sphere) @ A.T` (one column per component, issue #334), so all +three backends decide identically from one fitted state +(`pamica/tests/test_mlx_sharing_cross_backend.py` pins that against `AMICATorchNG`). -Row 8 of the "At a glance" table at the top of this page (merged-away columns +Row 8 of the "At a glance" table at the top of this page (merged-away components frozen at their last finite value, not left NaN behind the mask) holds in MLX as well. @@ -377,8 +386,8 @@ the moments that would drive it — so it is behavior-validated on real EEG, and ## Component sharing on rank-reduced fits `share_comps` merges components that are near-collinear *across models*, -comparing each pair of mixing columns after mapping them back to input-channel -(sensor) space. +comparing each pair of component mixing vectors after mapping them back to +input-channel (sensor) space. The PyTorch backend built that back-map with `inv(sphere)`, so it refused every rank-reduced or rank-deficient fit: with rank reduction active the sphere is `(n_kept, n_channels)` and has no inverse, and a square sphere fitted on @@ -399,13 +408,14 @@ well-conditioned data are unchanged; the bundled sample reproduces its previous The NumPy backend now reaches the merge decision from the same metric (issue #258): `identify_shared_components` takes the de-sphered sensor-space maps -directly -- `pinv(sphere) @ A`, the same back-map described above -- instead of -comparing columns of the sphered `A`. +directly -- `pinv(sphere) @ A.T` since issue #334, the same back-map described +above -- instead of comparing columns of the sphered `A`. The two backends therefore make the identical merge decision from the same fitted state (`pamica/tests/test_numpy_share_comps.py::test_numpy_merge_decision_matches_torch_backend`). The MLX backend calls that same kernel on the same host float64 inputs (issue #263), so all three agree. -On one real fitted 2-model state the top candidate cross-model pair measured +On one real fitted 2-model state (measured before issue #334, when the metric +still compared stored columns) the top candidate cross-model pair measured 0.992 cosine similarity in sensor space against 0.970 in the old sphered space -- close enough that, with the default `comp_thresh=0.99`, the two metrics disagree on whether that pair merges. A NumPy fit that shares @@ -413,6 +423,58 @@ components can therefore reach a different `comp_list` than it did before #258, even on a full-rank, well-conditioned sphere; only the `pinv`-vs-`inv` comparison two paragraphs above is unaffected by that swap. +## Component sharing compares and ties components (issue #334) + +The reference's mixing matrix `A(nw, num_comps)` holds component `k` in column `k`, +and `comp_list(i, h)` names the component source `i` of model `h` uses, +for its mixing vector and its density alike. +Every pamica backend now stores the same matrix transposed, `A` of shape `(n_comps, nw)`, +so a component id names the same component in `A` as in `mu`, `sbeta` (`beta`), `alpha` and `rho` +([ADR 0007](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0007-component-row-layout.md)). +Before issue #334 the backends stored each model's block transposed inside an `(nw, n_comps)` array +and indexed its COLUMNS by component id, but a stored column is one sphered channel's loadings across a model's components, not a component. +`share_comps` therefore compared and tied the wrong vectors: + +- The metric now compares the components' scalp maps: the vector it uses for source `i` of model `h` + is exactly column `i` of `get_sensor_mixing_matrix(h)`. + The old metric's vectors had median |cosine| 0.27 to 0.39 with those maps on a two-model fit of the sample, + so it merged pairs whose maps disagreed and missed pairs whose maps agreed. +- A merge now re-points `comp_list`, as the reference's does: + the two sources share one mixing vector and one density, nothing is copied or averaged at the merge, + and a merge of two identical components leaves the log-likelihood exactly unchanged (the old fold changed it by -0.186 on such a pair). + The `gm`-weighted `dAk/zeta` step then averages the models' steps for that one component. + +Seeded from a merged state through the reference's `load_comp_list`, +the PyTorch and NumPy updates match the native binary to float64 round-off after one and three iterations, +with `doscaling` on and off (`pamica/tests/test_component_rows.py`, opt-in with `AMICA_RUN_FORTRAN=1`). +On the bundled sample (2 models, 300 iterations, `share_start=100`, `comp_thresh=0.95`) +the scan merges three pairs whose maps agree (|cos| 0.956 to 0.971), +where the old metric merged three whose maps did not (|cos| 0.06, 0.35 and 0.55). + +Two consequences to know: + +- A scan early in a fit merges most components, and the reference's formula does the same. + Both models start near the identity, so their components stay near-collinear for the first iterations + (on the sample with 2 models and seed 42, a scan at iteration 8 merges all 32 at `comp_thresh=0.95` and 24 at 0.99; + one at iteration 20 merges 24 and 5). + The reference's own scan cannot show this (it never merges, above), + but its similarity with `Spinv2 = Spinv^T Spinv`, applied to the binary's own state after 8 iterations from pamica's initialization, + merges exactly the pairs the PyTorch and NumPy scans merge: 32, 32 and 30 at `comp_thresh` 0.9, 0.95 and 0.99. +- A model left with few components of its own then loses its responsibility, and can end in a non-finite fit. + In a short recipe (4096 samples, `share_start=8`, `share_iter=10`, `comp_thresh=0.9`), 28 merges at iteration 8 + dropped the second model's `gm` from 0.41 to 0.003, and one more merge at iteration 18 drove it to zero and the fit to NaN. + Seeded with the same merged states, the reference's update does the same: + the second model's `gm` falls to 0.005 within two iterations, as pamica's does from the same state, + and from the second merge it reaches zero in one iteration and the binary reports NaN and reinitializes. + This is the algorithm on models that have not yet separated, not a defect of the port. + The reference's default `share_start=100` avoids it; keep `share_start` well past the first iterations. +- Saved models: a PyTorch `state_dict` (now `format_version` 4) or MLX save (now format 2) from an earlier version + is converted without loss unless `share_comps` had merged components, + in which case loading raises `ValueError` and the model must be refit. + `AMICA.save` files go through the same conversion. + The EEGLAB export's `A` file is the reference's layout for any number of models; + `load_results` refuses a multi-model directory written by an earlier version. + ## `final_ll_` trails a final-iteration merge (issue #269) If a `share_comps` merge fires on the LAST fit iteration, the returned @@ -794,7 +856,8 @@ reference binary itself — the same category as the `do_choose_pdfs` and parameter file said. - (Already documented above:) `do_choose_pdfs`'s `pdftype=1` auto-switcher is declared but its moment buffers (`m2sum`/`m4sum`) are never accumulated, and - `share_comps`'s `Spinv2` similarity metric is declared but never allocated. + `share_comps`'s `Spinv2` similarity metric is declared but never allocated + (its scan runs, computes NaN similarities and never merges). One live (non-dead) divergence is worth recording explicitly rather than leaving it implicit in the unsupported-keys table: Fortran's `do_rho` can diff --git a/docs/guides/backends.md b/docs/guides/backends.md index 69f39e72..6a5dd834 100644 --- a/docs/guides/backends.md +++ b/docs/guides/backends.md @@ -44,6 +44,8 @@ Everything the wrappers offer works on both backends: and names any setting that backend cannot take in its "not applied" warning. - `save` records the backend (`format_version` 2), so `AMICA.load` restores the model on the backend that fit it. Files written before backend selection (version 1) still load, as PyTorch models. + The backend payload inside the file carries its own format version (see [the differences guide](amica-differences.md#component-sharing-compares-and-ties-components-issue-334)): + a model saved before issue #334 loads unchanged unless `share_comps` had merged components, in which case `load` asks for a refit. Loading an MLX model needs MLX installed and takes no `device`. The raw classes stay available for direct use (`from pamica.mlx_impl import AMICAMLXNG`), diff --git a/docs/guides/eeglab.md b/docs/guides/eeglab.md index a49bc94b..c9a01b82 100644 --- a/docs/guides/eeglab.md +++ b/docs/guides/eeglab.md @@ -27,6 +27,7 @@ This writes the raw binary files EEGLAB's AMICA loader reads: | `mean` | data mean | | `c` | per-model centers | | `alpha`, `mu`, `sbeta`, `rho` | source mixture-density parameters | +| `A` | mixing matrix in sphered space, one column per component (the reference's layout, for any number of models) | | `comp_list` | component ids (for component sharing) | | `LL` | log-likelihood per iteration | | `LLt` | per-timepoint, per-model log-likelihood, plus the per-timepoint total | @@ -37,6 +38,10 @@ A directory written with `do_approx_sphere=False` (a full-rank, genuinely asymmetric `S`) by a pamica older than issue #336's fix has `S` transposed; re-run `write_amica_output` to regenerate it (the default symmetric sphere and a rank-reduced fit were unaffected). +`loadmodout15.m` does not read `A`; pamica's own `load_results` does, +and refuses an `A` that does not invert the `W` beside it. +A multi-model directory written by a pamica older than issue #334 stored `A` in another layout +and must be written again to load there (a single-model `A` was already in the reference layout). The MLX backend writes the same directory: an Apple-Silicon fit exports to EEGLAB directly, with no torch round trip (epic #278). The export is diff --git a/docs/guides/validation.md b/docs/guides/validation.md index 7f882b58..dabbfccb 100644 --- a/docs/guides/validation.md +++ b/docs/guides/validation.md @@ -503,7 +503,7 @@ guarded to a no-op so the parity results above stay byte-for-byte unchanged. |---|---|---| | Best-iterate safeguard (`keep_best`, #51) | on by default | returns the highest-LL iterate; cuts multi-model LL sd from 12.7x to 2.0x Fortran's. Single-model parity stays bit-exact (monotone, no restore). ADR 0003 | | Per-model bias `c` update (#27) | on for `n_models>1` | Fortran `update_c`; per-block stats bit-exact; no-op for `n_models=1` | -| Component sharing (`share_comps`, #60) | off by default | Fortran `identify_shared_comps` ported; no bit-exact oracle (`Spinv2` is unrunnable), behavior-validated; byte-identical when unshared | +| Component sharing (`share_comps`, #60, #334) | off by default | Fortran `identify_shared_comps` ported; the scan itself has no bit-exact oracle (`Spinv2` is never allocated, so the reference's scan computes NaN similarities and never merges), but the update from a merged state seeded through the reference's `load_comp_list` matches the native binary to float64 round-off on PyTorch and NumPy (`test_component_rows.py`, opt-in with `AMICA_RUN_FORTRAN=1`); byte-identical when unshared | | Outlier rejection (`do_reject`, #123) | off by default | `good_idx` mechanism on all three backends (NumPy, PyTorch, MLX -- the last landed epic #278 Phase 3, #289); MLX/NumPy ports validated vs the PyTorch backend | | Degenerate-fit contract (#50) | always | the `AMICA` wrapper refuses a NaN or singular fit (`converged_` / `stop_reason_`); `transform`/`get_*`/`save` raise through the wrapper instead of returning NaN sources. This is a wrapper-level contract, not a raw-backend one -- calling a raw `AMICATorchNG`/`AMICAMLXNG`/`AMICA_NumPy` instance's `transform`/`get_*`/`save` directly, bypassing the wrapper, is not gated (tracked as issue #306). `write_amica_output` is the one exception: both the PyTorch and MLX backends gained the same degenerate/non-finite refusal directly on the raw class in this epic, since it has no wrapper equivalent to gate it. | | End-to-end workflow (#315) | always, PyTorch and MLX | average-referenced sample EEG with `pcakeep = n_channels - 1` through `AMICAICA`: 31 components, `get_sources` equal to `transform`, an exclusion that removes exactly one back-projection and keeps the residual, the EEGLAB export reloaded by `loadmodout`, `save`/`load`, an `input.param`-driven fit, and the two backends' sources Hungarian-matched with a minimum correlation of at least 0.999 (measured 0.999999999) | diff --git a/pamica/amica.py b/pamica/amica.py index ccdbebce..2d88bae6 100644 --- a/pamica/amica.py +++ b/pamica/amica.py @@ -1144,7 +1144,10 @@ def load( The file records which backend built the model (``format_version`` 2), and the model comes back on that backend. A ``format_version`` 1 file, written before backend selection existed (issue #313), holds a - PyTorch model by construction and still loads. + PyTorch model by construction and still loads. A backend payload + saved before issue #334 (components as columns of ``A``) is converted + on load, or refused with a request to refit when ``share_comps`` had + merged components (see ``AMICATorchNG.from_state_dict``). Parameters ---------- @@ -1165,7 +1168,8 @@ def load( ------ ValueError If the file's ``format_version`` is not 1 or 2, a section is - missing, or ``device`` is set for an MLX model. + missing, ``device`` is set for an MLX model, or the model was + saved before issue #334 with merged components. ImportError If the file holds an MLX model and MLX is not installed. """ diff --git a/pamica/component_layout.py b/pamica/component_layout.py new file mode 100644 index 00000000..38deb389 --- /dev/null +++ b/pamica/component_layout.py @@ -0,0 +1,114 @@ +"""Component-row storage of the mixing matrix, shared by every array backend +(issue #334, epic #324 Phase 8, ADR 0007). + +Every array backend (PyTorch, NumPy, MLX) stores the mixing matrix ``A`` with +shape ``(n_comps, n)``, one ROW per component: ``A[k, :]`` is component ``k``'s +mixing vector in the sphered space, the reference's column ``A(:, k)`` +(amica15.f90), and its density lives at ``mu[:, k]``, ``beta[:, k]`` (the +reference's ``sbeta``), ``alpha[:, k]`` and ``rho[:, k]``. ``comp_list[i, h]`` +names the component source ``i`` of model ``h`` uses, so model ``h``'s ``n x n`` +block is ``A[comp_list[:, h], :]``, whose row ``i`` is that source's mixing +vector. The backends invert that block for ``W`` (issue #24 convention: the +block is the transpose of the reference's ``A(:, comp_list(:, h))``). Globally, +``A`` is the reference's ``A`` transposed. + +Before issue #334 the backends stored ``A`` with shape ``(n, n_comps)`` and took +model ``h``'s block as ``A[:, comp_list[:, h]]``: the same ``n x n`` matrix, but +with the component ids indexing stored COLUMNS. A stored column held one +sphered channel's loadings across a model's components, not a component, so +``share_comps`` compared and tied the wrong vectors. For an unmerged +``comp_list`` (every component id used exactly once) that layout holds exactly +the same per-model blocks, so :func:`rows_from_legacy_columns` converts it +without loss. A merged one was computed under the wrong semantics and has no +counterpart in this layout; it is refused with a message to refit. + +Only the conversion policy lives here (``.rules/backend_parity.md``): the +PyTorch and MLX backends both call it when they read a pre-#334 save, so they +convert and refuse identically. The NumPy backend has no saved-model format of +its own. +""" + +from __future__ import annotations + +import numpy as np + + +def rows_from_legacy_columns( + A_legacy: np.ndarray, comp_list: np.ndarray, *, owner: str +) -> np.ndarray: + """Convert a pre-#334 ``A`` of shape ``(n, n_comps)`` to component rows. + + Model ``h``'s block is carried over element for element: + ``A_rows[comp_list[:, h], :] == A_legacy[:, comp_list[:, h]]``, so every + unmixing matrix, source and log-likelihood of the converted model is + exactly the saved one's. + + Parameters + ---------- + A_legacy : ndarray of shape (n, n_comps) + The mixing matrix as a pre-#334 ``state_dict`` stored it. + comp_list : ndarray of int, shape (n, n_models) + The saved component assignments; ``n_comps == n * n_models``. + owner : str + The class name the error messages name (``"AMICATorchNG"``, ...). + + Returns + ------- + ndarray of shape (n_comps, n) + The component-row ``A``, in ``A_legacy``'s dtype. + + Raises + ------ + ValueError + If the shapes disagree, ``comp_list`` is not an integer array, a + component id is out of range or repeated within one model (a malformed + payload), or a component id is shared by + two or more models: a ``share_comps`` merge computed on stored columns + (issue #334), which cannot be converted and needs a refit. + """ + A_legacy = np.asarray(A_legacy) + comp_list = np.asarray(comp_list) + if comp_list.ndim != 2: + raise ValueError( + f"malformed {owner} state: comp_list has shape {comp_list.shape}, " + "expected (n_channels, n_models)" + ) + if not np.issubdtype(comp_list.dtype, np.integer): + raise ValueError( + f"malformed {owner} state: comp_list has dtype {comp_list.dtype}, " + "expected integer component ids" + ) + n, n_models = comp_list.shape + n_comps = n * n_models + if A_legacy.shape != (n, n_comps): + raise ValueError( + f"malformed {owner} state: the pre-component-row A has shape " + f"{A_legacy.shape}, expected {(n, n_comps)} for comp_list of shape " + f"{comp_list.shape}" + ) + if comp_list.min() < 0 or comp_list.max() >= n_comps: + raise ValueError( + f"malformed {owner} state: comp_list holds ids outside [0, {n_comps})" + ) + for h in range(n_models): + if np.unique(comp_list[:, h]).size != n: + raise ValueError( + f"malformed {owner} state: model {h} uses a component id twice " + "in comp_list" + ) + ids, counts = np.unique(comp_list, return_counts=True) + shared = ids[counts > 1] + if shared.size: + raise ValueError( + f"This {owner} model was saved by a pamica version that stored the " + "mixing matrix with components as columns, after share_comps had " + f"merged {shared.size} component(s) across models. Those merges " + "compared and tied stored mixing columns, which are not components " + "(issue #334), so the fitted model cannot be converted. Refit it " + "with this version of pamica." + ) + A_rows = np.empty((n_comps, n), dtype=A_legacy.dtype) + for h in range(n_models): + idx = comp_list[:, h] + A_rows[idx, :] = A_legacy[:, idx] + return A_rows diff --git a/pamica/mlx_impl/core.py b/pamica/mlx_impl/core.py index 26f7939f..900de706 100644 --- a/pamica/mlx_impl/core.py +++ b/pamica/mlx_impl/core.py @@ -83,7 +83,12 @@ ``"grad_norm_floor"``), so a configuration moved from the PyTorch backend does the same work here. The gradient norm ``ndtmpsum`` is computed every iteration and masked by ``comp_used`` (Fortran amica15.f90:1761); without component sharing -every column is used, so the mask is all-True and drops out exactly. +every component is used, so the mask is all-True and drops out exactly. + +``A`` stores one component per ROW, shape ``(n_comps, n_channels)`` (issue +#334, :mod:`pamica.component_layout`, ADR 0007), exactly as in AMICATorchNG: +row ``k`` is the reference's column ``A(:, k)`` and model ``h``'s block is +``A[comp_list[:, h], :]``. """ from __future__ import annotations @@ -104,6 +109,7 @@ from .. import blocktune from .. import restarts from .. import schedule +from ..component_layout import rows_from_legacy_columns from ..metrics import mir as mir_metric from ..metrics import model_probability_from_loglik, pairwise_mi from ..numpy_impl.utils import identify_shared_components @@ -133,11 +139,12 @@ _LOG2 = math.log(2.0) _LOG4 = math.log(4.0) # logistic-family normalizer (amica15.f90:1346) -# Log-normalizers for the non-GG density families, using Fortran's exact literal -# constants (amica15.f90:1333/1359/1371) so the log-density matches the reference -# binary bit-for-bit: 2.506628274 = sqrt(2*pi) (Gaussian, pdtype 2); 4.132731354 / -# 1.858073988 = the sub-/super-Gaussian cosh normalizers (pdtype 4 / 1). Ported -# verbatim from the same-named ``pamica.torch_impl.core`` constants (policy 1). +# Log-normalizers for the non-GG density families, ported verbatim from the +# same-named ``pamica.torch_impl.core`` constants (policy 1): the double-precision +# values of the reference's decimal literals (amica15.f90:1333/1359/1371), which +# the binary reads in single precision, so its log-normalizers differ from these +# by 3.7e-10, 2.0e-8 and -2.1e-8 (docs/guides/amica-differences.md row 16; +# whether to adopt the single-precision values is issue #344). _LOG_SQRT_2PI = math.log(2.506628274) _LOG_NORM_COSH_SUB = math.log(4.132731354) _LOG_NORM_COSH_SUP = math.log(1.858073988) @@ -374,12 +381,14 @@ class AMICAMLXNG: Enable multi-model component sharing (Fortran ``share_comps`` / ``identify_shared_comps``, amica15.f90:1916): components that are near-collinear across different models are merged so they share one - mixing column and one density. Requires ``n_models >= 2`` (a model - cannot share with itself); accepted but inert otherwise. OFF by default, - so default fits are unchanged. There is no bit-exact oracle -- the + mixing vector (one row of ``A``) and one density. Requires + ``n_models >= 2`` (a model cannot share with itself); accepted but + inert otherwise. OFF by default, so default fits are unchanged. The reference's similarity metric is never initialized (like the dead - ``do_choose_pdfs``, #26) -- so this implements the intended algorithm, - validated by real-data behavior and against the PyTorch backend. A + ``do_choose_pdfs``, #26), so it has no bit-exact oracle; this implements + the intended algorithm on the component rows (issue #334), validated + against the PyTorch backend, whose update from a merged state matches + the reference. A merge that fires on the LAST fit iteration is reflected in the returned model but trails in ``final_ll_``; see that attribute's comment (issue #269). @@ -391,7 +400,8 @@ class AMICAMLXNG: never consumes the whole cycle, and ``share_start`` must be ``>= 1``. ``comp_thresh`` (0.99) Cosine-similarity cutoff, in the de-sphered (sensor-space) metric, above - which two mixing columns are identified and merged. Must be in + which two components' mixing vectors are identified and merged. Must be + in ``(0, 1]``. The de-sphering uses ``pinv(sphere)``, so sharing also works on rank-reduced and rank-deficient fits (issues #253, #221); see :meth:`_identify_shared_comps`. @@ -574,8 +584,8 @@ class AMICAMLXNG: defaults, validation and semantics: ``doscaling`` (True) - Rescale each component's mixing vector (a row of its model's stored - ``A`` block) to unit norm, with the matching ``mu``/``beta`` rescale, + Rescale each component's mixing vector (a row of the stored ``A``) to + unit norm, with the matching ``mu``/``beta`` rescale, an exact change of scale (see :meth:`_rescale_components`). ``scalestep`` (1) Run the rescale on iterations ``scalestep``, ``2*scalestep``, ... @@ -1039,13 +1049,14 @@ def _initialize_parameters(self): rng = np.random.RandomState(self.seed) n, m, ncomp, nmix = self.n_channels, self.n_models, self.n_comps, self.n_mix - # Per-model mixing blocks + comp_list mapping each (channel, model) to its - # column in A (identical RNG draw order to AMICATorchNG; for m=1 the loop - # runs once, so single-model init stays byte-for-byte). - A_np = np.zeros((n, ncomp)) + # Per-model mixing blocks + comp_list mapping each (source, model) to its + # component, a row of A (issue #334; identical RNG draw order to + # AMICATorchNG; for m=1 the loop runs once, so single-model init stays + # byte-for-byte). + A_np = np.zeros((ncomp, n)) comp_list_np = np.zeros((n, m), dtype=np.int64) for h in range(m): - A_np[:, h * n : (h + 1) * n] = np.eye(n) + 0.01 * (0.5 - rng.rand(n, n)) + A_np[h * n : (h + 1) * n, :] = np.eye(n) + 0.01 * (0.5 - rng.rand(n, n)) comp_list_np[:, h] = np.arange(h * n, (h + 1) * n) mu_np = np.zeros((nmix, ncomp)) @@ -1059,8 +1070,8 @@ def _initialize_parameters(self): self.A = mx.array(A_np.astype(np.float32)) self.comp_list = mx.array(comp_list_np) # (n_channels, n_models) int - # Every column is referenced by the default block comp_list; reset here - # (not only in __init__) so a re-fit cannot inherit a merged mask. + # Every component is referenced by the default block comp_list; reset + # here (not only in __init__) so a re-fit cannot inherit a merged mask. self._comp_used_arr = mx.array(np.ones(ncomp, dtype=bool)) self.mu = mx.array(mu_np.astype(np.float32)) self.alpha = mx.array(alpha_np.astype(np.float32)) @@ -1096,7 +1107,7 @@ def _refresh_lgamma_table(self): self._lgamma_table = mx.array(gammaln(1.0 + 1.0 / rho_np).astype(np.float32)) def _update_unmixing_matrices(self): - """Per-model ``W_h = inv(A[:, comp_list[:, h]])`` and the LL Jacobian + """Per-model ``W_h = inv(A[comp_list[:, h], :])`` and the LL Jacobian ``log|det W_h|``, on the CPU stream (MLX linalg is CPU-only), hoisted to once per iteration. ``W`` is ``(n_models, n, n)`` and ``_logdet_W`` is ``(n_models,)``. For n_models=1 this is ``inv(A)`` unchanged. @@ -1151,14 +1162,14 @@ def _update_unmixing_matrices(self): assert self.A is not None and self.comp_list is not None ws, logdets = [], [] for h in range(self.n_models): - A_h = self.A[:, self.comp_list[:, h]] + A_h = self.A[self.comp_list[:, h], :] a_h_np = np.array(A_h, dtype=np.float32, copy=False) finite_mask = np.isfinite(a_h_np) # A matrix with ZERO finite entries carries no signal to check -- # this is exactly the observed shape of a dead-model corruption # (a zero-responsibility model dividing by dgm==0 propagates # NaN/inf through the WHOLE per-model direction matrix, so all - # comp_list columns for that model go non-finite together, not + # of that model's component rows go non-finite together, not # just one entry -- confirmed on a real fitted 2-model dead-model # state). Skipping here reproduces the pre-guard behavior exactly: # mx.linalg.inv on a wholly non-finite A does not abort -- it @@ -1217,7 +1228,7 @@ def _update_unmixing_matrices(self): self.stop_reason = restarts.ERROR_STOP_REASON raise RuntimeError( f"Singular unmixing matrix for model {h} at iteration " - f"{self.iteration}: cond(A[:, comp_list[:, {h}]]) = " + f"{self.iteration}: cond(A[comp_list[:, {h}], :]) = " f"{cond:.3e} exceeds the float32 threshold " f"{_INV_COND_THRESHOLD:.1e} (MLX's CPU-stream inv " "would otherwise abort the process instead of " @@ -1751,22 +1762,21 @@ def _update_parameters(self, acc: dict, n_samples: int): used, mx.clip(new_rho, self.minrho, self.maxrho), self.rho ) - # Natural-gradient A-update. A is stored as Fortran's A^T, so the update - # is a LEFT-multiply by the transposed direction (as in - # ``AMICATorchNG._update_parameters``, #24 root cause). Each model's direction - # is scattered into its mixing columns as a gm-weighted average (Fortran - # dAk/zeta) using the PREVIOUS iteration's gm (gm_prev, see the snapshot above): - # for the default disjoint comp_list every column has one contributor, so - # gm cancels and n_models=1 is byte-for-byte the old `A - lrate*(dA.T@A)`; - # a SHARED column (#263) takes Fortran's responsibility-weighted average, - # NOT a raw sum (a raw sum would over-step by the contributor count). A - # merged-away column needs no special case: nothing scatters into it, so - # its zeta is 0 and its dAk is 0/tiny = 0, i.e. it takes no step. The - # rescale below does not touch it either: it normalizes the rows of each - # model's block, and a merged-away column is in no model's block. - # (test_merged_away_columns_keep_their_last_finite_value disables - # doscaling; the column rule this replaced, issue #333, renormalized - # such a column at ULP scale.) + # Natural-gradient A-update. A is stored as Fortran's A^T, one component + # per row (issue #334), so the update is a LEFT-multiply by the + # transposed direction (as in ``AMICATorchNG._update_parameters``, #24 + # root cause). Each model's step is scattered into its component rows as + # a gm-weighted average (Fortran dAk/zeta) using the PREVIOUS iteration's + # gm (gm_prev, see the snapshot above): for the default disjoint + # comp_list every component has one contributor, so gm cancels and + # n_models=1 is byte-for-byte the old `A - lrate*(dA.T@A)`; a SHARED + # component (#263) takes Fortran's responsibility-weighted average of the + # models' steps for its one mixing vector, NOT a raw sum (a raw sum would + # over-step by the contributor count). A merged-away component needs no + # special case: nothing scatters into its row, so its zeta is 0 and its + # dAk is 0/tiny = 0, i.e. it takes no step. The rescale below does not + # touch it either: it rescales the rows of each model's block, and a + # merged-away row is in no model's block. # # The direction/dAk/gradient-norm computation below runs # UNCONDITIONALLY, not gated on _a_frozen(): Fortran computes dAk and @@ -1805,9 +1815,9 @@ def _update_parameters(self, acc: dict, n_samples: int): zeta = mx.zeros((self.n_comps,), dtype=mx.float32) for h in range(self.n_models): idx = self.comp_list[:, h] - dAk = dAk.at[:, idx].add(gm_prev[h] * (directions[h].T @ self.A[:, idx])) + dAk = dAk.at[idx, :].add(gm_prev[h] * (directions[h].T @ self.A[idx, :])) zeta = zeta.at[idx].add(gm_prev[h] + mx.zeros((self.n_channels,))) - dAk = dAk / mx.maximum(zeta, tiny) + dAk = dAk / mx.maximum(zeta, tiny)[:, None] # Weight-gradient norm (Fortran ndtmpsum, amica15.f90:1760-1761): # ``sqrt(sum(dAk**2, mask=comp_used) / (nw*count(comp_used)))``, built @@ -1815,20 +1825,21 @@ def _update_parameters(self, acc: dict, n_samples: int): # applies it, exactly as Fortran does in accum_updates_and_likelihood # (:1749-1761) ahead of update_params' A step (:1803-1815). Read by # fit()'s two grad-norm checks (AMICATorchNG._update_parameters computes - # the same quantity). The comp_used mask matters only once share_comps - # has merged columns away: without sharing it is all-True, so the select - # returns nd unchanged and the count is n_comps, leaving this bit-for-bit - # the plain RMS over dAk. Kept as a lazy scalar (no .item() here) so it - # rides fit()'s single per-iteration mx.eval. + # the same quantity). One squared norm per component row, as the + # reference sums each component's column. The comp_used mask matters + # only once share_comps has merged components away: without sharing it + # is all-True, so the select returns nd unchanged and the count is + # n_comps, leaving this the plain RMS over dAk. Kept as a lazy scalar + # (no .item() here) so it rides fit()'s single per-iteration mx.eval. # # SELECT, not multiply-by-mask: 0*NaN is NaN, so a single non-finite - # column would poison the whole reduction, and a NaN ndtmpsum silently + # row would poison the whole reduction, and a NaN ndtmpsum silently # disables BOTH grad-norm stops (NaN <= min_nd is False), burning the # entire iteration budget with no diagnostic. mx.where drops the masked - # lanes structurally instead. Unreachable today (a merged-away column's + # lanes structurally instead. Unreachable today (a merged-away row's # dAk is exactly 0), but Phase 3's Newton direction feeds this same dAk. used_f = self._comp_used_arr.astype(mx.float32) - nd = (dAk**2).sum(axis=0) # (n_comps,) + nd = (dAk**2).sum(axis=1) # (n_comps,) nd = mx.where(self._comp_used_arr, nd, mx.zeros_like(nd)) self._nd_arr = mx.sqrt( nd.sum() / (self.n_channels * mx.maximum(used_f.sum(), 1.0)) @@ -1885,25 +1896,21 @@ def _update_parameters(self, acc: dict, n_samples: int): def _rescale_components(self) -> None: """Rescale every component to a unit-norm mixing vector (Fortran ``doscaling``, amica15.f90:1843-1851), an exact change of scale; the - same rule, order and zero-norm guard as + same rule, norms, order and zero-norm guard as ``AMICATorchNG._rescale_components``. - Source ``i`` of model ``h`` is ROW ``i`` of the stored block - ``A[:, comp_list[:, h]]`` (issue #24 convention), so that row is divided - by its norm while ``mu``/``beta`` at ``comp_list[i, h]`` are multiplied/ - divided by it, leaving the log-likelihood unchanged (the column rule - this replaced, issue #333, was not a change of scale). A zero-norm row - keeps its values: the scale is 1 there, so ``A``/``mu``/``beta`` are - unchanged rather than ``mu`` being zeroed. The new values are written - with ``mx.put_along_axis`` (an exact scatter), rebinding each attribute - to a new array as the rest of the M-step does, never mutating in place. - - Models are rescaled in order, one vectorized pass per model. Their - blocks are disjoint unless ``share_comps`` merged a column; a shared - stored column then belongs to rows of several blocks, where this - per-block rule is not well defined. It is applied uniformly anyway: the - component-row layout of epic #324 Phase 8 (issue #334) replaces it. - ADR 0006 records the convention. + Component ``k`` is row ``k`` of ``A`` (issue #334, ADR 0007), so that + row is divided by its norm while ``mu[:, k]``/``beta[:, k]`` are + multiplied/divided by it, leaving the log-likelihood unchanged (the + column rule issue #333 replaced was not a change of scale). A zero-norm + (or NaN-norm) row keeps its values: the scale is 1 there, so + ``A``/``mu``/``beta`` are unchanged rather than ``mu`` being zeroed. + Each component is rescaled exactly once, including one that + ``share_comps`` merged into several models, and a merged-away row + (in no model's block) keeps its scale of 1. The norms come from each + model's block ``A[comp_list[:, h], :]``, the same gathers as before + issue #334, and every attribute is rebound to a new array as the rest + of the M-step does, never mutated in place. """ assert ( self.A is not None @@ -1911,21 +1918,15 @@ def _rescale_components(self) -> None: and self.beta is not None and self.comp_list is not None ) - n = self.n_channels + scale = mx.ones((self.n_comps,), dtype=mx.float32) for h in range(self.n_models): idx = self.comp_list[:, h] - block = self.A[:, idx] # row i = source i of model h - norm = mx.sqrt((block**2).sum(axis=1)) # (n_channels,) - scale = mx.where(norm > 0, norm, mx.ones_like(norm)) - a_cols = mx.broadcast_to(idx[None, :], (n, n)) - self.A = mx.put_along_axis(self.A, a_cols, block / scale[:, None], axis=1) - d_cols = mx.broadcast_to(idx[None, :], (self.n_mix, n)) - self.mu = mx.put_along_axis( - self.mu, d_cols, self.mu[:, idx] * scale, axis=1 - ) - self.beta = mx.put_along_axis( - self.beta, d_cols, self.beta[:, idx] / scale, axis=1 - ) + norm = mx.sqrt((self.A[idx, :] ** 2).sum(axis=1)) # (n_channels,) + # A shared row gets the same norm from every block that holds it. + scale[idx] = mx.where(norm > 0, norm, mx.ones_like(norm)) + self.A = self.A / scale[:, None] + self.mu = self.mu * scale + self.beta = self.beta / scale # ------------------------------------------------------------------ # Adaptive PDF switch (issue #265; AMICATorchNG's #26 port) @@ -2207,15 +2208,26 @@ def _a_frozen(self) -> bool: return False return schedule.share_freeze(self.iteration, self.share_start, self.share_iter) + def _component_sensor_maps(self) -> np.ndarray: + """Every component's mixing vector in input-channel (sensor) space. + + ``pinv(sphere) @ A.T`` on the host in float64, shape ``(n_channels_in, + n_comps)``: column ``comp_list[i, h]`` is column ``i`` of + :meth:`get_sensor_mixing_matrix` for model ``h`` (issue #334). These + are the vectors the share metric compares. + """ + assert self.A is not None + return self._pinv_sphere() @ np.array(self.A, dtype=np.float64).T + def _identify_shared_comps(self) -> None: - """Merge near-collinear mixing columns across models (Fortran + """Merge near-collinear components across models (Fortran ``identify_shared_comps``, amica15.f90:1916). Two components (model ``h`` source ``i`` and model ``hh`` source ``ii``, - ``h < hh``) are identified when the angle between their mixing columns, - measured in the original (de-sphered) data space, is below the - ``comp_thresh`` cutoff; on a match ``cj`` is folded into ``ci``, so the - two share one mixing column and one density. + ``h < hh``) are identified when the angle between their mixing vectors + (rows of ``A``, issue #334), measured in the original (de-sphered) data + space, is below the ``comp_thresh`` cutoff; on a match ``cj`` is folded + into ``ci``, so the two share one mixing vector and one density. The decision itself is NOT reimplemented here: it runs :func:`pamica.numpy_impl.utils.identify_shared_components` on host @@ -2228,21 +2240,23 @@ def _identify_shared_comps(self) -> None: materialized by fit's per-iteration ``mx.eval``, so the host pull is cheap. - No bit-exact oracle: the reference's ``Spinv2`` metric is *declared* but - never *allocated* in ``amica15.f90``, so invoking the routine there would - read an unallocated array -- it is effectively unrunnable (cf. the dead - ``do_choose_pdfs`` switch, #26). This implements the intended algorithm - and is validated on real data, not against byte parity. + No bit-exact oracle for the metric: the reference's ``Spinv2`` is + *declared* but never *allocated* in ``amica15.f90``, so the routine + there reads an unallocated array, every similarity comes out NaN and it + never merges (cf. the dead ``do_choose_pdfs`` switch, #26). The merged + state it produces is checked against the reference through + ``load_comp_list`` on the float64 backends, and this backend is pinned + to AMICATorchNG. """ if self.n_models < 2: return assert self.A is not None and self.comp_list is not None # _pinv_sphere raises on a non-finite sphere, so the metric below can # only be garbage if A itself is (guarded per-pair inside the kernel). - atil = self._pinv_sphere() @ np.array(self.A, dtype=np.float64) + atil = self._component_sensor_maps() cl = np.array(self.comp_list) new_cl, new_used = identify_shared_components(atil, cl, self.comp_thresh) - # Each fold removes exactly one column from the referenced set, so the + # Each fold removes exactly one component from the referenced set, so the # drop in unique count IS the merge count (the kernel does not report it). merged = int(np.unique(cl).size - np.unique(new_cl).size) if merged: @@ -2302,8 +2316,9 @@ def comp_used(self) -> mx.array: """Boolean mask (n_comps,) of components still referenced by comp_list. A component drops out of use when it is folded into another by - :meth:`_identify_shared_comps`; unused columns receive no gradient and - are never read by the E-step. + :meth:`_identify_shared_comps`; an unused component (its row of ``A`` + and its density columns) receives no update and is never read by the + E-step. CACHED (set all-True at init, rewritten by each merge) rather than derived from ``comp_list`` on every read, which is how @@ -2323,14 +2338,17 @@ def shared_components(self) -> list: """Components shared across models by ``share_comps`` (issue #263). ``share_comps`` folds near-collinear components of different models onto - one shared mixing column + density, recorded as a repeated index in - ``comp_list``. Returns one group per shared column: a list of - ``(model_idx, source_idx)`` pairs that all reference it. Empty when no - component is shared across two or more models (always for one model, and - for a default multi-model fit with ``share_comps`` off). - - Note that a merge synchronizes only the mixture parameters routed - through ``comp_list`` (``mu``/``alpha``/``beta``/``rho``); the + one shared component (one row of ``A`` and one density), recorded as a + repeated index in ``comp_list``. Returns one group per shared + component: a list of ``(model_idx, source_idx)`` pairs that all + reference it, whose columns of :meth:`get_sensor_mixing_matrix` are + therefore identical. Empty when no component is shared across two or + more models (always for one model, and for a default multi-model fit + with ``share_comps`` off). + + Note that a merge synchronizes only the parameters routed through + ``comp_list`` (the mixing vector and ``mu``/``alpha``/``beta``/``rho``); + the per-source density *family* code ``pdtype`` is a separate array and is not synchronized (issue #265, matching ``AMICATorchNG.shared_components``), so under the adaptive switcher (``pdftype=1``) a shared pair can still @@ -2847,7 +2865,7 @@ def _fit_once( # Component sharing (Fortran identify_shared_comps schedule, # amica15.f90:1856): once per share_iter cycle from share_start, - # merge near-collinear mixing columns across models using the + # merge near-collinear components across models using the # just-updated A. Fortran runs identify_shared_comps BEFORE # get_unmixing_matrices (amica15.f90:1858,1863), so rebuild W from # the merged comp_list -- otherwise the next E-step would read a @@ -3264,8 +3282,10 @@ def get_pdftype(self, model_idx: int = 0) -> np.ndarray: return codes def get_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: - """True mixing matrix ``A_fort`` = (stored A)^T (issue #24 convention; - issue #287 port of ``AMICATorchNG.get_mixing_matrix``).""" + """True mixing matrix of model ``model_idx``: the reference's + ``A(:, comp_list(:, h))``, i.e. that model's component rows of the + stored ``A`` transposed (issue #24 convention; issue #334 layout; issue + #287 port of ``AMICATorchNG.get_mixing_matrix``).""" if self.A is None or self.comp_list is None: raise RuntimeError( "AMICAMLXNG.get_mixing_matrix() requires a fitted model; call " @@ -3273,7 +3293,7 @@ def get_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: ) self._check_model_idx(model_idx) self._check_usable("get the mixing matrix") - return np.array(self.A[:, self.comp_list[:, model_idx]].T) + return np.array(self.A[self.comp_list[:, model_idx], :].T) @property def n_channels_in(self) -> int: @@ -3311,7 +3331,7 @@ def get_sensor_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: ) self._check_model_idx(model_idx) self._check_usable("get the sensor mixing matrix") - A = np.array(self.A[:, self.comp_list[:, model_idx]].T, dtype=np.float64) + A = np.array(self.A[self.comp_list[:, model_idx], :].T, dtype=np.float64) return self._pinv_sphere() @ A def get_unmixing_matrix(self, model_idx: int = 0) -> np.ndarray: @@ -3865,7 +3885,9 @@ def write_amica_output(self, outdir) -> None: rho=np.array(self.rho), comp_list=np.array(self.comp_list), ll=ll, - A=np.array(self.A), + # The reference layout, (nw, num_comps) with component k in column + # k: the component-row A transposed (issue #334). + A=np.array(self.A).T, Lht=Lht, Lt=Lt, ) @@ -3913,10 +3935,15 @@ def write_amica_output(self, outdir) -> None: ) # fmt: skip # This backend owns its own format_version, independent of AMICATorchNG's - # (currently 3): the two payloads are never interchangeable (different + # (currently 4): the two payloads are never interchangeable (different # param layouts, no dtype/device fields here), so there is no reason for - # the version numbers to track each other. - _SAVE_FORMAT_VERSION = 1 + # the version numbers to track each other. Version 2 (issue #334) stores A + # with one component per row, shape (n_comps, n_channels); a version 1 + # payload stored it as (n_channels, n_comps) and is converted on load when + # unmerged, refused when share_comps had merged components + # (pamica.component_layout, ADR 0007). + _SAVE_FORMAT_VERSION = 2 + _COLUMN_LAYOUT_FORMAT_VERSION = 1 def state_dict(self) -> dict: """Serialize the fitted model to a plain, framework-agnostic dict. @@ -4091,12 +4118,20 @@ def from_state_dict(cls, state: dict) -> "AMICAMLXNG": Unlike ``AMICATorchNG.from_state_dict`` there is no ``device`` argument: this backend always runs on ``mx.default_device()``. + + A ``format_version`` 1 state (components as columns of ``A``, before + issue #334) loads unchanged in every other respect: its ``A`` is + converted to component rows without loss, unless ``share_comps`` had + merged components, which raises ``ValueError`` asking for a refit + (:func:`pamica.component_layout.rows_from_legacy_columns`). """ version = state.get("format_version") - if version != cls._SAVE_FORMAT_VERSION: + if version not in (cls._SAVE_FORMAT_VERSION, cls._COLUMN_LAYOUT_FORMAT_VERSION): raise ValueError( f"unsupported AMICAMLXNG state format_version: {version!r} " - f"(expected {cls._SAVE_FORMAT_VERSION})" + f"(expected {cls._SAVE_FORMAT_VERSION}, or " + f"{cls._COLUMN_LAYOUT_FORMAT_VERSION} from before the " + "component-row layout)" ) for section in ("config", "params", "extra"): if section not in state: @@ -4120,6 +4155,19 @@ def from_state_dict(cls, state: dict) -> "AMICAMLXNG": f"AMICAMLXNG constructor ({exc}); the payload may be " "truncated or from an incompatible version." ) from exc + if version == cls._COLUMN_LAYOUT_FORMAT_VERSION: + params = state["params"] + missing = [name for name in ("A", "comp_list") if name not in params] + if missing: + raise ValueError( + f"malformed AMICAMLXNG state: missing params {missing}" + ) + A_rows = rows_from_legacy_columns( + np.asarray(params["A"]), + np.asarray(params["comp_list"]), + owner="AMICAMLXNG", + ) + state = {**state, "params": {**params, "A": A_rows}} obj._load_params(state) return obj @@ -4162,7 +4210,7 @@ def _load_params(self, state: dict) -> None: # is the true input count, which a refit validates X against. self._n_input_channels = int(n_channels_in) expected_shapes = { - "A": (n, ncomp), + "A": (ncomp, n), "W": (m, n, n), "c": (n, m), "mu": (nmix, ncomp), @@ -4330,7 +4378,8 @@ def load(cls, filepath: str) -> "AMICAMLXNG": whose central directory or a member's compressed bytes were cut off) each raise a named ``ValueError`` naming what is wrong, rather than raising an opaque ``zipfile``/``numpy`` error or loading a silently - partial model. + partial model. A ``format_version`` 1 file (before issue #334) is + converted or refused exactly as :meth:`from_state_dict` describes. """ # Eagerly materialize every array the archive actually contains # INSIDE this try, so any corruption -- an unreadable zip (raised by @@ -4356,10 +4405,12 @@ def load(cls, filepath: str) -> "AMICAMLXNG": f"{section!r} (the file may be truncated or corrupted)." ) version = int(raw["format_version"]) - if version != cls._SAVE_FORMAT_VERSION: + if version not in (cls._SAVE_FORMAT_VERSION, cls._COLUMN_LAYOUT_FORMAT_VERSION): raise ValueError( f"unsupported AMICAMLXNG save format_version: {version!r} " - f"(expected {cls._SAVE_FORMAT_VERSION})" + f"(expected {cls._SAVE_FORMAT_VERSION}, or " + f"{cls._COLUMN_LAYOUT_FORMAT_VERSION} from before the " + "component-row layout)" ) config = json.loads(raw["config"].item()) extra = json.loads(raw["extra"].item()) diff --git a/pamica/numpy_impl/core.py b/pamica/numpy_impl/core.py index 3a11edf6..46f93eaa 100644 --- a/pamica/numpy_impl/core.py +++ b/pamica/numpy_impl/core.py @@ -599,7 +599,8 @@ def __init__( } # Initialize model parameters - self.A: Optional[np.ndarray] = None # Mixing matrix + # Mixing matrix, one component per row: (num_comps, data_dim) (#334) + self.A: Optional[np.ndarray] = None self.W: Optional[np.ndarray] = None # Unmixing matrix self.c: Optional[np.ndarray] = None # Bias terms self.mu: Optional[np.ndarray] = None # Means of mixture components @@ -825,8 +826,9 @@ def get_sensor_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: sensor maps when rank reduction has made the sphere non-square (issue #223). Mirrors ``AMICATorchNG.get_sensor_mixing_matrix``. - ``A`` here is the true mixing matrix, the stored ``A`` transposed: the - stored ``W = inv(stored A)`` is itself the transpose of the unmixing + ``A`` here is the true mixing matrix, the model's component rows of the + stored ``A`` transposed (issue #334 layout): the stored ``W = + inv(A[comp_list[:, h], :])`` is itself the transpose of the unmixing (:meth:`get_weights`, issue #24 convention). So the result inverts the spatial filter the sources come from, ``get_weights() @ sphere @ result == I``, and each column is one component's sensor map. @@ -834,7 +836,7 @@ def get_sensor_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: if self.sphere is None or self.A is None or self.comp_list is None: raise RuntimeError("Model has not been fitted yet; call fit() first.") self._check_usable("get the sensor mixing matrix") - A = self.A[:, self.comp_list[:, model_idx]].T + A = self.A[self.comp_list[:, model_idx], :].T return self._pinv_sphere() @ A def get_weights(self) -> np.ndarray: @@ -1367,16 +1369,18 @@ def _preprocess_data(self, data: np.ndarray): def _initialize_parameters(self): """Initialize all model parameters.""" assert self.data_dim is not None - # Initialize mixing/unmixing matrices + # Initialize mixing/unmixing matrices. A holds one component per row + # (issue #334, pamica.component_layout): model h's block is rows + # h*data_dim..(h+1)*data_dim-1 under the default comp_list below. if self.A is None: - self.A = np.zeros((self.data_dim, self.num_comps)) + self.A = np.zeros((self.num_comps, self.data_dim)) for h in range(self.num_models): if not hasattr(self, "fix_init") or not self.fix_init: - self.A[:, h * self.data_dim : (h + 1) * self.data_dim] = np.eye( + self.A[h * self.data_dim : (h + 1) * self.data_dim, :] = np.eye( self.data_dim ) + 0.01 * (0.5 - self.rng.rand(self.data_dim, self.data_dim)) else: - self.A[:, h * self.data_dim : (h + 1) * self.data_dim] = np.eye( + self.A[h * self.data_dim : (h + 1) * self.data_dim, :] = np.eye( self.data_dim ) @@ -2182,22 +2186,22 @@ def _update_parameters(self, updates: Dict): # # dAk is the gm-weighted average of the per-model directions mapped # through A (Fortran dAk/zeta, amica15.f90:1749-1761): each contributing - # model adds gm[h]/zeta of its direction to the shared column, where - # zeta = sum of gm over the models that reference the column. Weighted by - # gm_prev, the PRE-update model weights, because Fortran builds dAk before - # update_params reassigns gm (issue #219). + # model adds gm[h]/zeta of its step to the shared component's row, where + # zeta = sum of gm over the models that reference the component. Weighted + # by gm_prev, the PRE-update model weights, because Fortran builds dAk + # before update_params reassigns gm (issue #219). # # The weights are normalized per model (gm[h]/zeta) rather than summed and # then divided (Fortran's literal sum-then-divide, and AMICATorchNG's): - # mathematically the same average, but a column with a single contributor - # then has weight exactly gm[h]/gm[h] == 1.0, so the step is bit-identical - # to the pre-#242 per-model update instead of drifting by the ULP that a - # multiply-then-divide round trip can introduce. Every column has exactly - # one contributor unless share_comps merged one, so this keeps the default - # multi-model trajectory byte-for-byte. + # mathematically the same average, but a component with a single + # contributor then has weight exactly gm[h]/gm[h] == 1.0, so the step is + # bit-identical to the pre-#242 per-model update instead of drifting by + # the ULP that a multiply-then-divide round trip can introduce. Every + # component has exactly one contributor unless share_comps merged one, + # so this keeps the default multi-model trajectory byte-for-byte. # - # ndtmpsum is then the RMS of the used columns of dAk: - # ||dAk[:, used]|| / sqrt(nw * n_used), with NO lrate factor. Fortran + # ndtmpsum is then the RMS of the used rows of dAk: + # ||dAk[used, :]|| / sqrt(nw * n_used), with NO lrate factor. Fortran # measures the gradient direction before the step, not the applied update # lrate*dAk, and does not divide by lrate either (amica15.f90:1760-1761) -- # there is no missing factor here. @@ -2207,29 +2211,32 @@ def _update_parameters(self, updates: Dict): dAk = np.zeros_like(self.A) for h in range(self.num_models): # comp_list[:, h] holds distinct indices within a model - # (identify_shared_components never merges two columns that appear in - # the same model), so buffered `+=` on fancy indices cannot drop a - # contribution here. + # (identify_shared_components never merges two components that + # appear in the same model), so buffered `+=` on fancy indices cannot + # drop a contribution here. Row i of the model's step is source i's + # component, comp_list[i, h] (issue #334). idx = self.comp_list[:, h] weight = gm_prev[h] / np.maximum(zeta[idx], np.finfo(np.float64).tiny) - dAk[:, idx] += weight * np.dot(directions[h].T, self.A[:, idx]) + dAk[idx, :] += weight[:, None] * np.dot(directions[h].T, self.A[idx, :]) nd_value = float( - np.sqrt(np.sum(dAk[:, used] ** 2) / (self.data_dim * int(used.sum()))) + np.sqrt(np.sum(dAk[used, :] ** 2) / (self.data_dim * int(used.sum()))) ) - # A is stored as Fortran's A^T (true unmixing = W^T = inv(A)^T), so the - # Fortran step A_fort -= lrate*A_fort @ dir becomes A -= lrate*dir^T @ A + # A is stored as Fortran's A^T, one component per row (issue #334; true + # unmixing = W^T = inv(block)^T), so the Fortran step on a model's block + # A_fort(:, comp_list(:,h)) @ dir becomes dir^T @ A[comp_list[:, h], :] # (LEFT-multiply by the TRANSPOSED direction). Right-multiply by the # untransposed dir is invisible at the fixed point but sends the fit # downhill -- issue #24 root cause. # # ONE application of the averaged dAk (Fortran's single DAXPY, # amica15.f90:1807/1814), not the per-model loop this used to run: a - # column shared by two models took one step per contributing model, the - # second against an already-stepped A, which is a different operation - # from Fortran's single weighted average (issue #242). A merged-away - # column has no contributor, so its dAk stays exactly zero and it holds - # the value it was merged away with. When sharing holds A this iteration + # component shared by two models took one step per contributing model, + # the second against an already-stepped A, which is a different + # operation from Fortran's single weighted average (issue #242). A + # merged-away component has no contributor, so its dAk row stays + # exactly zero and it holds the value it was merged away with. When + # sharing holds A this iteration # (the post-merge settle window) the step is skipped entirely, along with # the lrate ramp and the Newton-fallback bookkeeping Fortran nests inside # the same guarded block (amica15.f90:1803), so a discarded Newton @@ -2277,40 +2284,76 @@ def _update_parameters(self, updates: Dict): # gates the separate final gradient-norm stop. self.nd.append(nd_value) + def _component_sensor_maps(self) -> np.ndarray: + """Every component's mixing vector in input-channel (sensor) space. + + ``pinv(sphere) @ A.T``, shape ``(data_dim_in, num_comps)``: column + ``comp_list[i, h]`` is column ``i`` of ``get_sensor_mixing_matrix(h)`` + (issue #334). These are the vectors the share metric compares. + """ + assert self.A is not None + return self._pinv_sphere() @ self.A.T + + def _identify_shared_comps(self) -> None: + """Merge near-collinear components across models (Fortran + ``identify_shared_comps``, amica15.f90:1916). + + Runs the shared kernel + :func:`pamica.numpy_impl.utils.identify_shared_components` on the + sensor-space component maps (:meth:`_component_sensor_maps`, issue + #258), the metric ``AMICATorchNG._identify_shared_comps`` computes, so + both backends make the same merge decision from the same fitted state. + A merge folds one component id into another in ``comp_list`` and + ``comp_used``; the caller rebuilds ``W``, as the reference's + ``get_unmixing_matrices`` follows the scan (amica15.f90:1858,1863). + """ + assert self.comp_list is not None + unique_before = int(np.unique(self.comp_list).size) + self.comp_list, self.comp_used = identify_shared_components( + self._component_sensor_maps(), self.comp_list, self.comp_thresh + ) + unique_after = int(np.unique(self.comp_list).size) + # identify_shared_components is stateless, so the merge log (matching + # AMICATorchNG's) is emitted here from the before/after unique-component + # count instead. + if unique_after < unique_before: + self.logger.info( + "Component sharing (iter %d): %d merge(s), %d unique components.", + self.iter, + unique_before - unique_after, + unique_after, + ) + def _rescale_components(self) -> None: """Rescale every component to a unit-norm mixing vector (Fortran ``doscaling``, amica15.f90:1843-1851), an exact change of scale. - Each model's stored block ``A[:, comp_list[:, h]]`` is the transpose of - the reference's per-model mixing matrix (issue #24 convention), so - source ``i`` of model ``h`` is ROW ``i`` of that block, the reference's - column ``A(:,k)``. Dividing that row by its norm scales source ``i`` up - by the norm; ``mu[:, comp_list[i, h]] *= norm`` and - ``beta[:, comp_list[i, h]] /= norm`` rescale its density to match, so - the log-likelihood is unchanged. Normalizing stored COLUMNS instead - (the rule before issue #333) is not a change of scale of any component - and perturbed the fit every iteration. A zero-norm row is left - untouched, as in the reference (``Anrmk > 0``). Same rule, order and - guard as ``AMICATorchNG._rescale_components``. - - Models are rescaled in order. Their blocks are disjoint unless - ``share_comps`` merged a column; a shared stored column then belongs to - rows of several blocks, where this per-block rule is not well defined. - It is applied uniformly anyway: the component-row layout of epic #324 - Phase 8 (issue #334) replaces it. ADR 0006 records the convention. + Component ``k`` is row ``k`` of ``A`` (issue #334, ADR 0007), the + reference's column ``A(:,k)``. Dividing that row by its norm scales the + source up by the norm; ``mu[:, k] *= norm`` and ``beta[:, k] /= norm`` + rescale its density to match, so the log-likelihood is unchanged. A + zero-norm row is left untouched, as in the reference (``Anrmk > 0``). + Each component is rescaled once, including a shared one, and a + merged-away row is left untouched. Same rule, norms and guard as + ``AMICATorchNG._rescale_components``. """ assert self.A is not None and self.mu is not None and self.beta is not None assert self.comp_list is not None + scale = np.ones(self.A.shape[0]) for h in range(self.num_models): - # comp_list[:, h] holds distinct indices within a model, so the - # fancy-index assignments below write every element exactly once. + # The per-model block, as the A-update and W gather it; a shared row + # gets the same norm from every block that holds it. Laid out + # column-major, numpy sums each row's squares column by column, the + # order the pre-#334 block (a column fancy-index, which numpy lays + # out column-major) was reduced in, so the default trajectory stays + # bit-identical; a row-major block would sum pairwise instead. idx = self.comp_list[:, h] - block = self.A[:, idx] # row i = source i of model h + block = np.asfortranarray(self.A[idx, :]) norm = np.sqrt(np.sum(block**2, axis=1)) # (data_dim,) - scale = np.where(norm > 0, norm, 1.0) - self.A[:, idx] = block / scale[:, None] - self.mu[:, idx] = self.mu[:, idx] * scale - self.beta[:, idx] = self.beta[:, idx] / scale + scale[idx] = np.where(norm > 0, norm, 1.0) + self.A = self.A / scale[:, None] + self.mu = self.mu * scale + self.beta = self.beta / scale def _a_frozen(self) -> bool: """Whether the A-update (and its lrate ramp) is held this iteration. @@ -2474,7 +2517,7 @@ def _optimize(self): # Share components if requested (Fortran identify_shared_comps # schedule, amica15.f90:1856): once per share_int cycle from - # share_start, merging near-collinear mixing columns across + # share_start, merging near-collinear components across # models using the just-updated A, then rebuilding W from the # merged comp_list (Fortran runs identify_shared_comps before # get_unmixing_matrices, amica15.f90:1858,1863) -- otherwise the @@ -2492,26 +2535,7 @@ def _optimize(self): if self.share_comps and schedule.periodic_due( iter, self.share_start, self.share_int ): - # Sensor-space maps (issue #258): pinv(sphere) @ A, matching - # AMICATorchNG._identify_shared_comps so both backends make - # the same merge decision from the same fitted state. - assert self.comp_list is not None - unique_before = int(np.unique(self.comp_list).size) - self.comp_list, self.comp_used = identify_shared_components( - self._pinv_sphere() @ self.A, self.comp_list, self.comp_thresh - ) - unique_after = int(np.unique(self.comp_list).size) - # identify_shared_components is stateless, so the merge log - # (matching AMICATorchNG's) is emitted here from the - # before/after unique-component count instead. - if unique_after < unique_before: - self.logger.info( - "Component sharing (iter %d): %d merge(s), %d unique " - "components.", - self.iter, - unique_before - unique_after, - unique_after, - ) + self._identify_shared_comps() self._update_unmixing_matrices() # Write intermediate results/history if requested, on Fortran's @@ -2770,14 +2794,18 @@ def _write_results(self): this iteration's M-step. See :meth:`_llt_arrays` for that ordering and its Fortran citation. """ - # A is written (Fortran output omits it; loadmodout derives A from W and - # S) only so load_results can restore it directly for the viz helpers. - # The Fortran 'nd' file (per-component weight-change history) is a - # different quantity from pamica's scalar self.nd, so it is not emitted - # (loadmodout treats 'nd' as optional). + # A is written in the reference's layout, as the binary writes it + # (loadmodout derives A from W and S instead); load_results reads it + # back for the viz helpers. The Fortran 'nd' file (per-component + # weight-change history) is a different quantity from pamica's scalar + # self.nd, so it is not emitted (loadmodout treats 'nd' as optional). from .load import write_amicaout - assert self.outdir is not None, "_write_results needs an outdir" + # Raises rather than asserts: ``assert`` is stripped under ``python -O``. + if self.outdir is None: + raise RuntimeError("_write_results needs an outdir") + if self.A is None: + raise RuntimeError("_write_results needs a fitted A; call fit() first") Lht, Lt = self._llt_arrays() write_amicaout( @@ -2793,7 +2821,9 @@ def _write_results(self): rho=self.rho, comp_list=self.comp_list, ll=np.asarray(self.ll), - A=self.A, + # The reference layout, (nw, num_comps) with component k in column + # k: the component-row A transposed (issue #334). + A=self.A.T, Lht=Lht, Lt=Lt, ) diff --git a/pamica/numpy_impl/data.py b/pamica/numpy_impl/data.py index 962fe911..65cbfe60 100644 --- a/pamica/numpy_impl/data.py +++ b/pamica/numpy_impl/data.py @@ -250,11 +250,18 @@ def _read(name, dtype: type = np.float64): W_fortran = W.reshape(nw, nw, num_models, order="F") results = {"gm": gm, "W": np.transpose(W_fortran, (1, 0, 2))} + # On disk A is the reference's A(nw, num_comps), column-major (issue #334, + # write_amicaout); the backends hold its transpose, one component per row, + # which is exactly a C-order read of the same bytes: (num_comps, nw). A = _read("A") if A is not None: - results["A"] = A.reshape( - len(A) // num_comps, num_comps - ) # (data_dim, num_comps) + if A.size != num_comps * nw: + raise ValueError( + f"The mixing matrix A in {indir} holds {A.size} values, expected " + f"{num_comps * nw} ({num_comps} components x {nw} channels, from " + "gm and W); the file may be truncated or from another run." + ) + results["A"] = A.reshape(num_comps, nw) # Mixture params are stored (num_mix, num_comps) column-major (Fortran names # 'sbeta'); reshape order="F" matches the write_amicaout writer and Fortran @@ -308,5 +315,36 @@ def _read(name, dtype: type = np.float64): f"Incomplete AMICA output in {indir}: missing {sorted(missing)}. " "The directory may be from an interrupted or partial run." ) + _check_mixing_inverts_unmixing(results, indir) return results + + +# Largest |A_h @ W_h - I| accepted from a written directory. W is written as +# the inverse of each model's block of A, so the residual is inversion +# round-off: measured on 60-iteration fits of the sample EEG with 1-3 models, at +# most 3.8e-7 for float32 MLX exports (sharing on or off) and 1.1e-15 for +# float64, while a multi-model A written in the pre-#334 component-column +# layout, read as component rows, gives 1.5. +_A_W_RESIDUAL_TOL = 1e-3 + + +def _check_mixing_inverts_unmixing(results: dict, indir: Path) -> None: + """Refuse an ``A`` that is not the inverse of the ``W`` written beside it. + + pamica before issue #334 wrote a multi-model ``A`` in its component-column + layout (C order), which reads back scrambled as component rows; a + single-model file is the same bytes in both layouts. Rather than hand the + viz helpers wrong sensor maps, fail with the remedy. + """ + A, W, comp_list = results["A"], results["W"], results["comp_list"] + for h in range(comp_list.shape[1]): + residual = np.abs(A[comp_list[:, h], :] @ W[:, :, h] - np.eye(W.shape[0])).max() + if not residual <= _A_W_RESIDUAL_TOL: + raise ValueError( + f"The mixing matrix A in {indir} does not invert the unmixing W " + f"of model {h} (max |A_h W_h - I| = {residual:.3g}). A " + "multi-model directory written by pamica before issue #334 " + "stored A with components as columns; write it again from the " + "fitted (or saved and reloaded) model with this version." + ) diff --git a/pamica/numpy_impl/load.py b/pamica/numpy_impl/load.py index 95850d8f..2a2077ea 100644 --- a/pamica/numpy_impl/load.py +++ b/pamica/numpy_impl/load.py @@ -120,8 +120,9 @@ def write_amicaout( ``(num_mix, num_comps)`` mixture params column-major) is laid out so EEGLAB's ``loadmodout15.m`` reads it correctly. The earlier model-interleaved ``W`` layout, which round-tripped through :func:`loadmodout` but was not - MATLAB-readable, was fixed in #159. (``A`` is exempt: ``loadmodout15`` derives - it from ``W``/``S`` and ignores the file; see below.) + MATLAB-readable, was fixed in #159. ``A``, which ``loadmodout15`` ignores + (it derives the mixing from ``W``/``S``), is written in the reference's + layout too, since issue #334. Parameters ---------- @@ -134,10 +135,13 @@ def write_amicaout( 0-based component ids; written 1-based to match the Fortran format. ll : array-like Per-iteration log-likelihood history. - A : array-like, optional - Mixing matrix. ``loadmodout15`` derives ``A`` from ``W`` and ``S`` and - ignores this file; it is written (when given) only so pamica's own - ``load_results`` can restore ``A`` directly for the viz helpers. + A : array-like of shape (nw, num_comps), optional + Mixing matrix in the reference's layout: component ``k``'s sphered-space + mixing vector in column ``k`` (the backends' component-row ``A`` + transposed, issue #334). Written column-major, so the file holds exactly + the bytes the reference writes for its ``A``. ``loadmodout15`` derives + ``A`` from ``W`` and ``S`` and ignores this file; pamica's own + ``load_results`` reads it back for the viz helpers. Lht : array-like of shape (num_models, n_samples), optional Per-model per-sample log-likelihood (Fortran ``modloglik``). Written together with ``Lt`` as the ``LLt`` file (issue #155); omitted (as @@ -169,14 +173,20 @@ def _w(name, arr, dtype=np.float64, order="C"): # mean/gm/LL are 1-D, so their order does not matter. _w("gm", gm) if A is not None: - # A is the one 2-D file here NOT written column-major: loadmodout15.m - # ignores it entirely (it derives A from W and S instead, per the - # docstring above), so nothing outside pamica ever reads these bytes. - # `_w`'s C-order default is kept, and `load_results` reads it back the - # same way (`data.py`'s plain `.reshape(...)`, no `order="F"`) -- the - # only contract this file has to satisfy is pamica-writes/pamica-reads - # self-consistency, not the Fortran/EEGLAB layout. - _w("A", A) + # The reference's A(nw, num_comps), column-major like every other 2-D + # file here (issue #334). loadmodout15.m ignores it (it derives A from W + # and S instead); `load_results` reads the same bytes in C order as the + # component-row A, (num_comps, nw), which is this array transposed. Before + # #334 this file was the backends' component-column A in C order: the + # same bytes for one model, a different layout for several. + A = np.asarray(A) + expected = (W.shape[0], W.shape[0] * W.shape[2]) + if A.shape != expected: + raise ValueError( + f"A must be the reference-layout (nw, num_comps) mixing matrix " + f"{expected}; got shape {A.shape}" + ) + _w("A", A, order="F") # W is the internal-backend unmixing (b = W.T @ x); Fortran/EEGLAB store the # true unmixing W_fortran = W.T with the model axis slowest, column-major # within each model. Moving the model axis to the front and writing C-order @@ -195,8 +205,7 @@ def _w(name, arr, dtype=np.float64, order="C"): # S is written column-major (order="F") in both branches, like every other # EEGLAB-read 2-D file here (c, alpha, mu, sbeta, rho, comp_list below): that # is what the Fortran reference writes and what both readers (loadmodout15.m - # and loadmodout() below) read. (A, above, is the one 2-D file that is NOT - # column-major, and for a different reason: EEGLAB never reads it.) The + # and loadmodout() below) read. (A, above, is column-major too.) The # square branch used to write C order on the reasoning that the default # zero-phase component analysis (ZCA) sphere is its own transpose (true to # ~1e-17) so C order was "byte-identical to the Fortran reference" -- that diff --git a/pamica/numpy_impl/utils.py b/pamica/numpy_impl/utils.py index 2bdcd41e..488a8ef5 100644 --- a/pamica/numpy_impl/utils.py +++ b/pamica/numpy_impl/utils.py @@ -56,36 +56,43 @@ def identify_shared_components(atil, comp_list, comp_thresh=0.99): Identify components that are shared between different models, in sensor space. Two components (model ``h`` source ``i`` and model ``hh`` source ``ii``, - ``h < hh``) are identified when the angle between their mixing columns, + ``h < hh``) are identified when the angle between their mixing vectors, measured in the original (de-sphered) data space, is below the ``comp_thresh`` cutoff:: t0 = |a . b| / (||a|| ||b||), a = atil[:, ci], b = atil[:, cj] - ``atil`` must already be the de-sphered (sensor-space) mixing columns -- - callers pass ``pinv(sphere) @ A``, the Fortran ``Spinv`` back-map - (amica15.f90:568-578) applied to the mixing matrix, mirroring - ``identify_shared_comps`` (amica15.f90:1916). This is a cross-backend - agreement contract (.rules/backend_parity.md): ``AMICATorchNG._identify_shared_comps`` - (torch_impl/core.py) computes the identical ``pinv(sphere) @ A`` metric, - so both backends make the same merge decision from the same fitted state - (see ``tests/test_numpy_share_comps.py::test_numpy_merge_decision_matches_torch_backend``). + with ``ci = comp_list[i, h]`` and ``cj = comp_list[ii, hh]``. ``atil`` must + already hold the de-sphered (sensor-space) component mixing vectors as + columns -- callers pass ``pinv(sphere) @ A.T``, the Fortran ``Spinv`` + back-map (amica15.f90:568-578) applied to the component rows of ``A`` + (issue #334), so column ``comp_list[i, h]`` is column ``i`` of that model's + ``get_sensor_mixing_matrix``. This mirrors ``identify_shared_comps`` + (amica15.f90:1916) and is a cross-backend agreement contract + (.rules/backend_parity.md): ``AMICATorchNG._identify_shared_comps`` + (torch_impl/core.py) computes the identical metric, so the backends make the + same merge decision from the same fitted state (see + ``tests/test_numpy_share_comps.py::test_numpy_merge_decision_matches_torch_backend``). Before issue #258 this function compared raw columns of the *sphered* ``A`` directly, which could disagree with the PyTorch backend under rank - reduction or PCA whitening, where the sphere is not orthonormal. + reduction or PCA whitening, where the sphere is not orthonormal; before + issue #334 its callers passed stored columns of a component-column layout, + which are not components. On a match, ``cj`` is folded into ``ci``: every ``comp_list`` entry equal - to ``cj`` is reassigned to ``ci``, so the two now share one mixing column - and one density. + to ``cj`` is reassigned to ``ci``, so the two sources now share component + ``ci``'s mixing vector and density (nothing is copied or averaged, as in + the reference). Greedy and order-dependent, matching the reference's quadruple loop. - Skips a pair already merged, or one whose two columns coexist in some + Skips a pair already merged, or one whose two components coexist in some single model (a model cannot share a component with itself). Parameters ---------- atil : ndarray of shape (data_dim_in, num_comps) - De-sphered (sensor-space) mixing columns, i.e. ``pinv(sphere) @ A``. + De-sphered (sensor-space) component mixing vectors, one per column, + i.e. ``pinv(sphere) @ A.T``. comp_list : ndarray of shape (data_dim, num_models) Component assignments. Not mutated -- a copy is merged and returned. comp_thresh : float @@ -114,7 +121,7 @@ def identify_shared_components(atil, comp_list, comp_thresh=0.99): t0 = np.abs(atil[:, ci] @ atil[:, cj]) / ( norms[ci] * norms[cj] + tiny ) - # NaN t0 (e.g. a zero-norm column) must NOT merge: + # NaN t0 (e.g. a zero-norm vector) must NOT merge: # `NaN >= thresh` is False either way, but guard finiteness # explicitly so a future rewrite of the comparison direction # cannot silently start merging on NaN. @@ -122,7 +129,7 @@ def identify_shared_components(atil, comp_list, comp_thresh=0.99): continue # A model cannot share a component with itself: skip if any - # single model already uses both columns. + # single model already uses both components. if any( (cl[:, k] == ci).any() and (cl[:, k] == cj).any() for k in range(num_models) @@ -132,10 +139,10 @@ def identify_shared_components(atil, comp_list, comp_thresh=0.99): cl[cl == cj] = ci # fold cj into ci everywhere # Derive comp_used from the final comp_list rather than tracking it during - # the merge loop. A fresh np.ones() per call forgot every column merged away - # in an earlier call: once comp_list is fully merged the ci == cj guard skips - # every pair, and the mask came back all-True while half the columns were - # dead (issue #240). Matches AMICATorchNG.comp_used, which is a property + # the merge loop. A fresh np.ones() per call forgot every component merged + # away in an earlier call: once comp_list is fully merged the ci == cj guard + # skips every pair, and the mask came back all-True while half the + # components were dead (issue #240). Matches AMICATorchNG.comp_used, which is a property # derived the same way. num_comps = atil.shape[1] comp_used = np.zeros(num_comps, dtype=bool) @@ -148,31 +155,30 @@ def get_unmixing_matrices(A, comp_list): """ Compute unmixing matrices from mixing matrix and component assignments. - For each model, constructs the unmixing matrix by inverting the appropriate - subset of the mixing matrix columns as specified by the component assignments. - The unmixing matrices are used to transform the mixed signals back into their - source components. + Model ``h``'s unmixing matrix is the inverse of its block of component rows, + ``inv(A[comp_list[:, h], :])`` (issue #334 layout; the stored ``W`` is the + transpose of the true unmixing, issue #24 convention). Parameters ---------- - A : ndarray - Mixing matrix - comp_list : ndarray - Component assignments + A : ndarray of shape (num_comps, data_dim) + Mixing matrix, one component per row. + comp_list : ndarray of shape (data_dim, num_models) + Component assignments. Returns ------- - W : ndarray + W : ndarray of shape (data_dim, data_dim, num_models) Unmixing matrices """ - data_dim = A.shape[0] + data_dim = A.shape[1] num_models = comp_list.shape[1] W = np.zeros((data_dim, data_dim, num_models)) for h in range(num_models): idx = comp_list[:, h] - W[:, :, h] = np.linalg.inv(A[:, idx]) + W[:, :, h] = np.linalg.inv(A[idx, :]) return W diff --git a/pamica/numpy_impl/viz.py b/pamica/numpy_impl/viz.py index ab219885..09ab463a 100644 --- a/pamica/numpy_impl/viz.py +++ b/pamica/numpy_impl/viz.py @@ -48,9 +48,10 @@ def _component_maps_and_sources( The same quantities :meth:`AMICA_NumPy.get_sensor_mixing_matrix` and :meth:`AMICA_NumPy.transform` return for the fitted model the directory was written from. ``load_results`` hands back the stored arrays, whose ``W`` is - the unmixing transposed and whose ``A`` is the mixing transposed (issue #24 - convention), so the maps are ``pinv(sphere) @ A[:, comps].T`` and the - sources are ``W.T @ (sphere @ (data - mean) - c)``. + the unmixing transposed and whose ``A`` holds one component per row (issue + #24 convention, issue #334 layout), so the maps are + ``pinv(sphere) @ A[comps, :].T`` and the sources are + ``W.T @ (sphere @ (data - mean) - c)``. Returns ------- @@ -67,7 +68,7 @@ def _component_maps_and_sources( ) comps = results["comp_list"][:, model_idx] sphere = results["sphere"] - maps = np.linalg.pinv(sphere) @ results["A"][:, comps].T + maps = np.linalg.pinv(sphere) @ results["A"][comps, :].T if data is None: return maps, None sphered = sphere @ (data - results["mean"][:, None]) @@ -269,7 +270,7 @@ def plot_component_sharing( # Create sharing matrix n_models = results["comp_list"].shape[1] - n_comps = results["A"].shape[1] + n_comps = results["A"].shape[0] sharing = np.zeros((n_comps, n_models)) for h in range(n_models): diff --git a/pamica/tests/mlx_tests/test_mlx_fit_noop.py b/pamica/tests/mlx_tests/test_mlx_fit_noop.py index 4a3cf3dd..2bcd9224 100644 --- a/pamica/tests/mlx_tests/test_mlx_fit_noop.py +++ b/pamica/tests/mlx_tests/test_mlx_fit_noop.py @@ -7,12 +7,11 @@ do_reject bookkeeping and the mir_step gate all have to be provably inert when unused, not just "probably fine". -This is verified directly, not by assumption: the pre-phase-3 -``pamica/mlx_impl/core.py`` is read from that git revision with ``git show`` -and executed as a SIBLING module of the live ``pamica.mlx_impl`` package (so -its ``from .. import blocktune`` etc. relative imports resolve against the -real, unmodified helper modules), giving two independent ``AMICAMLXNG`` -classes in the SAME process. Both then fit the same real data from the same +This is verified directly, not by assumption: the whole pamica package at +that revision is read from git and imported under a private name through the +shared loader ``pamica/tests/pre_change.py`` (which never fetches, issue +#343), giving two independent ``AMICAMLXNG`` classes in the SAME process, each +with its own helper modules. Both then fit the same real data from the same seed, and every fitted array is compared bit for bit -- a same-process comparison, per the Phase 1/2 lesson against recording cross-machine literals (``test_mlx_transform.py``'s ``_NOOP_PIN_*`` module comment): MLX @@ -26,12 +25,15 @@ with the rescale off, every other part of the fit path is still compared bit for bit against the pre-Phase-3 tip. The rescale itself is pinned by ``pamica/tests/test_doscaling_rows.py``. + +Issue #334 (epic #324 Phase 8) stores ``A`` with one component per row, so the +historical ``A`` (components as stored columns) is mapped onto rows with the +same lossless conversion a pre-#334 save goes through +(:func:`pamica.component_layout.rows_from_legacy_columns`) before the bit-for-bit +comparison; every per-model block, and so every other array, is unchanged. """ -import os -import subprocess -import sys -import types +import importlib from pathlib import Path from typing import Any @@ -40,7 +42,9 @@ mx = pytest.importorskip("mlx.core") +from pamica.component_layout import rows_from_legacy_columns # noqa: E402 from pamica.mlx_impl.core import AMICAMLXNG as CurrentAMICAMLXNG # noqa: E402 +from pamica.tests.pre_change import load_pre_change_package # noqa: E402 SAMPLE_DIR = Path(__file__).resolve().parents[2] / "sample_data" DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" @@ -51,8 +55,7 @@ # The epic tip this phase branched from (see the phase plan): the last # commit before issue #289's do_reject/LLt/export/MIR work landed. -_EPIC_TIP = "2e04006" -_HISTORICAL_MODULE_NAME = "pamica.mlx_impl._historical_phase3_noop_check" +_EPIC_TIP = "2e04006c85b96f3beb62dc12405479a05f2d5bc6" pytestmark = [ pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing"), @@ -72,66 +75,14 @@ def real_data() -> np.ndarray: @pytest.fixture(scope="module") -def historical_amicamlxng(): - """The pre-Phase-3 ``AMICAMLXNG`` class, loaded as a sibling module of - ``pamica.mlx_impl`` so its relative imports (``from .. import - blocktune`` etc.) resolve against the live, unmodified helper modules - -- exactly as they would have at that commit, since none of - blocktune/restarts/rank/numpy_impl.utils changed in this phase. - - Registered under a private name in ``sys.modules`` for the duration of - this test module and removed afterward, so it cannot leak into any - other test's import cache. - - In a shallow (depth-1) clone ``_EPIC_TIP`` is not a reachable object - and a bare ``git show`` fails with "bad object" -- reproduced - empirically on the macOS job before the CI jobs checked out full - history (``fetch-depth: 0``). ``git fetch --depth 1`` that one commit - first (deepening the clone by exactly the object needed, tolerating - failure -- e.g. no network, or a remote that has since been pruned) and - only then read it; if the object is still unreachable, fail under CI - (the ``CI`` environment variable) and otherwise skip loudly naming the - shallow-clone cause rather than erroring the whole module. - """ - repo_root = Path(__file__).resolve().parents[3] - subprocess.run( - ["git", "fetch", "origin", _EPIC_TIP, "--depth", "1"], - cwd=repo_root, - capture_output=True, - text=True, - ) # best-effort: a shallow CI checkout may not have `origin`, or may - # already have the object; either way the git show below is the real - # check, so a fetch failure here is not itself fatal. - result = subprocess.run( - ["git", "show", f"{_EPIC_TIP}:pamica/mlx_impl/core.py"], - cwd=repo_root, - capture_output=True, - text=True, +def historical_amicamlxng(tmp_path_factory): + """The pre-Phase-3 ``AMICAMLXNG`` class: the package at ``_EPIC_TIP``, + imported beside the live one. A clone without that commit fails under + ``CI`` and otherwise skips with the command that fetches it.""" + old = load_pre_change_package( + _EPIC_TIP, "pamica_pre289", tmp_path_factory.mktemp("pre289") ) - if result.returncode != 0: - reason = ( - f"git object {_EPIC_TIP} is not reachable in this checkout " - f"(likely a shallow/depth-1 clone that the fetch above could " - f"not deepen -- e.g. no network or the remote history was " - f"pruned); git show stderr: {result.stderr.strip()!r}" - ) - # As in test_doscaling_rows.py: the CI jobs check out full history - # (fetch-depth: 0), so there an unreachable pin fails instead of - # silently dropping this regression guard. - if os.environ.get("CI"): - pytest.fail(reason) - pytest.skip(reason) - module = types.ModuleType(_HISTORICAL_MODULE_NAME) - module.__package__ = "pamica.mlx_impl" - module.__name__ = _HISTORICAL_MODULE_NAME - module.__file__ = f"" - sys.modules[_HISTORICAL_MODULE_NAME] = module - try: - code = compile(result.stdout, module.__file__, "exec") - exec(code, module.__dict__) - yield module.AMICAMLXNG - finally: - del sys.modules[_HISTORICAL_MODULE_NAME] + return importlib.import_module(f"{old.__name__}.mlx_impl.core").AMICAMLXNG _PARAM_NAMES = ( @@ -152,6 +103,17 @@ def historical_amicamlxng(): # list is what makes that literally true rather than a subset. +def _historical(model: Any, name: str) -> np.ndarray: + """A fitted array of the historical model, ``A`` mapped onto the + component rows the live backend stores (issue #334).""" + value = np.array(getattr(model, name)) + if name == "A": + return rows_from_legacy_columns( + value, np.array(model.comp_list), owner="AMICAMLXNG" + ) + return value + + @pytest.mark.parametrize("n_models", [1, 2]) def test_unscaled_fit_is_bit_identical_to_the_pre_phase3_epic_tip( real_data, historical_amicamlxng, n_models @@ -177,7 +139,7 @@ def test_unscaled_fit_is_bit_identical_to_the_pre_phase3_epic_tip( assert old.final_ll_ == new.final_ll_ assert old.ll_history == new.ll_history for name in _PARAM_NAMES: - a = np.array(getattr(old, name)) + a = _historical(old, name) b = np.array(getattr(new, name)) assert np.array_equal(a, b), f"{name}: diverged from the pre-phase-3 fit" @@ -203,5 +165,5 @@ def test_unscaled_fit_with_keep_best_off_is_also_bit_identical( assert old.ll_history == new.ll_history for name in _PARAM_NAMES: np.testing.assert_array_equal( - np.array(getattr(old, name)), np.array(getattr(new, name)), err_msg=name + _historical(old, name), np.array(getattr(new, name)), err_msg=name ) diff --git a/pamica/tests/mlx_tests/test_mlx_inv_guard.py b/pamica/tests/mlx_tests/test_mlx_inv_guard.py index d5990476..d4ee4b05 100644 --- a/pamica/tests/mlx_tests/test_mlx_inv_guard.py +++ b/pamica/tests/mlx_tests/test_mlx_inv_guard.py @@ -199,7 +199,7 @@ def test_purely_nonfinite_a_flows_to_nan_params_not_abort(): mx.eval(model.A, model.W, model._logdet_W) cl = np.array(model.comp_list) - a_h1 = np.array(model.A)[:, cl[:, 1]] + a_h1 = np.array(model.A)[cl[:, 1], :] # model 1's component rows (#334) assert not np.any(np.isfinite(a_h1)), "model 1's A should be wholly non-finite" w1 = np.array(model.W)[1] @@ -217,9 +217,10 @@ def test_second_model_singular_names_model_one(): model, _ = _warm_model(n_models=2) cl = np.array(model.comp_list) a_np = np.array(model.A) - # Duplicate two columns WITHIN model 1's own comp_list selection. + # Duplicate two component rows WITHIN model 1's own comp_list selection + # (A holds one component per row, issue #334). rows = cl[:, 1] - a_np[:, rows[1]] = a_np[:, rows[0]] + a_np[rows[1], :] = a_np[rows[0], :] model.A = mx.array(a_np) model.iteration = 2 @@ -242,7 +243,7 @@ def test_guard_is_bit_identical_on_healthy_state(): ws_ref, logdets_ref = [], [] for h in range(model.n_models): - wh = mx.linalg.inv(model.A[:, model.comp_list[:, h]], stream=_CPU) + wh = mx.linalg.inv(model.A[model.comp_list[:, h], :], stream=_CPU) ws_ref.append(wh) logdets_ref.append(mx.linalg.slogdet(wh, stream=_CPU)[1]) w_ref = mx.stack(ws_ref, axis=0) diff --git a/pamica/tests/mlx_tests/test_mlx_kitchen_sink.py b/pamica/tests/mlx_tests/test_mlx_kitchen_sink.py index 97936861..6d226f4d 100644 --- a/pamica/tests/mlx_tests/test_mlx_kitchen_sink.py +++ b/pamica/tests/mlx_tests/test_mlx_kitchen_sink.py @@ -10,9 +10,12 @@ export), in one pass. This file is that one composition pin, not a substitute for the isolated tests. -Combination fitted: ``n_models=2``, ``share_comps=True`` (aggressive enough -to force a genuine merge, the same recipe as -``test_mlx_sharing.py::test_two_model_share_fit_completes_and_merges``), +Combination fitted: ``n_models=2``, ``share_comps=True`` (a genuine merge: +the winning restart's first scan, at iteration 8 with ``comp_thresh=0.99``, +merges six components; since issue #334 the metric compares the models' true +component maps, which are near-identical for the first few iterations, so the +earlier ``share_start=4``/``comp_thresh=0.9`` recipe merged 27 of 32 pairs and +the losing model's remaining components collapsed to a non-finite ``mu``), ``do_reject=True`` (a genuine rejection), ``n_restarts=2`` (a genuine restart-state round trip), ``pdftype=1`` (the adaptive kurtosis switcher, which requires ``n_mix=1``), and ``keep_best=True`` passed explicitly even @@ -70,9 +73,9 @@ def test_kitchen_sink_lifecycle(real_data, caplog): seed=7, block_size=BLOCK, share_comps=True, - share_start=4, + share_start=8, share_iter=8, - comp_thresh=0.9, + comp_thresh=0.99, do_reject=True, rejsig=2.0, rejstart=2, diff --git a/pamica/tests/mlx_tests/test_mlx_newton.py b/pamica/tests/mlx_tests/test_mlx_newton.py index 3561bd15..a953a590 100644 --- a/pamica/tests/mlx_tests/test_mlx_newton.py +++ b/pamica/tests/mlx_tests/test_mlx_newton.py @@ -676,10 +676,10 @@ def test_sharing_and_newton_fit_completes(): """``share_comps`` and Newton together: merges land inside the Newton phase and the fit stays finite. - A merged-away column receives no sufficient statistic, but the Newton - curvature is indexed by (model, SOURCE) rather than by mixing column, so it + A merged-away component receives no sufficient statistic, but the Newton + curvature is indexed by (model, SOURCE) rather than by mixing vector, so it never sees the 0/0 that the mixture updates mask -- both models' sources keep - full responsibility mass whichever column they point at. This drives that + full responsibility mass whichever component they point at. This drives that claim through a real fit at the shipped ``comp_thresh`` default, where a couple of genuinely near-collinear pairs merge. diff --git a/pamica/tests/mlx_tests/test_mlx_persistence.py b/pamica/tests/mlx_tests/test_mlx_persistence.py index eef22c81..51ca95d5 100644 --- a/pamica/tests/mlx_tests/test_mlx_persistence.py +++ b/pamica/tests/mlx_tests/test_mlx_persistence.py @@ -63,7 +63,7 @@ def _force_merged_column(model): cl = np.array(model.comp_list) kept, dead = int(cl[0, 0]), int(cl[0, 1]) a_np = np.array(model.A) - a_np[:, dead] = a_np[:, kept] + a_np[dead, :] = a_np[kept, :] # the retired component row (issue #334) model.A = mx.array(a_np) cl[cl == dead] = kept model.comp_list = mx.array(cl) @@ -177,7 +177,9 @@ def test_save_load_npz_layout(tmp_path): assert files == {"format_version", "config", "extra"} | set( AMICAMLXNG._PARAM_ARRAYS ) - assert int(data["format_version"]) == 1 + # 2 since issue #334 stored A with one component per row. + assert int(data["format_version"]) == 2 + assert data["A"].shape == (m.n_comps, m.n_channels) config = json.loads(data["config"].item()) assert config["n_channels"] == m.n_channels extra = json.loads(data["extra"].item()) @@ -426,7 +428,7 @@ def test_from_state_dict_rejects_shape_drift(): raises rather than failing later with a confusing matmul error.""" m = _fitted_model(n_models=1, max_iter=3) state = m.state_dict() - state["params"]["A"] = state["params"]["A"][:, :-1] # drop one column + state["params"]["A"] = state["params"]["A"][:, :-1] # drop one channel with pytest.raises(ValueError, match="shape"): AMICAMLXNG.from_state_dict(state) diff --git a/pamica/tests/mlx_tests/test_mlx_sharing.py b/pamica/tests/mlx_tests/test_mlx_sharing.py index f627a354..01852d57 100644 --- a/pamica/tests/mlx_tests/test_mlx_sharing.py +++ b/pamica/tests/mlx_tests/test_mlx_sharing.py @@ -84,7 +84,7 @@ def _force_merged_column(model): cl = np.array(model.comp_list) kept, dead = int(cl[0, 0]), int(cl[0, 1]) a_np = np.array(model.A) - a_np[:, dead] = a_np[:, kept] + a_np[dead, :] = a_np[kept, :] # the retired component row (issue #334) model.A = mx.array(a_np) cl[cl == dead] = kept model.comp_list = mx.array(cl) @@ -210,16 +210,21 @@ def _assert_share_result_consistent(model): cl = np.array(model.comp_list) assert cl.shape == (model.n_channels, model.n_models) assert cl.min() >= 0 and cl.max() < model.n_comps - assert np.array(model.A).shape == (model.n_channels, model.n_comps) + assert np.array(model.A).shape == (model.n_comps, model.n_channels) used = int(np.array(model.comp_used).sum()) assert used == np.unique(cl).size for group in model.shared_components(): - cols = {int(cl[i, h]) for h, i in group} - assert len(cols) == 1, "a shared group must reference exactly one column" + ids = {int(cl[i, h]) for h, i in group} + assert len(ids) == 1, "a shared group must reference exactly one component" assert len({h for h, _ in group}) >= 2, "sharing is across models" for h, i in group: assert 0 <= h < model.n_models and 0 <= i < model.n_channels + # One component, so one mixing vector (issue #334): every grouped + # source has the same column in its model's mixing matrix. + vecs = [model.get_mixing_matrix(h)[:, i] for h, i in group] + for vec in vecs[1:]: + np.testing.assert_array_equal(vec, vecs[0]) def test_merge_on_the_final_iteration_completes(): @@ -395,20 +400,29 @@ def test_second_identify_call_does_not_resurrect_merged_columns(): # --- (d) merged-away columns are frozen -------------------------------------- +def _component_values(model, name, ids): + """The values of components ``ids``: rows of ``A`` (issue #334), columns + of the density parameters.""" + value = np.array(getattr(model, name)) + return value[ids, :] if name == "A" else value[:, ids] + + def test_merged_away_columns_keep_their_last_finite_value(): - """A column no model references receives no sufficient statistics, so its - mixture update would be 0/0 and its ``dAk`` is exactly zero. It must freeze - at its last finite value rather than go NaN (which ``fit``'s ``nan_params`` - guard would then -- correctly -- abort on). ``doscaling`` is off so the - comparison is exact: the rescale pass renormalizes every column, dead ones - included, by a norm that is 1.0 only to within a ULP. + """A component no model references receives no sufficient statistics, so + its mixture update would be 0/0 and its ``dAk`` row is exactly zero. It + must freeze at its last finite value rather than go NaN (which ``fit``'s + ``nan_params`` guard would then -- correctly -- abort on). The rescale runs + (``doscaling`` on, the default): it gives a merged-away row, which is in no + model's block, a scale of exactly 1, so the comparison is exact. (Before + issue #334 this test turned ``doscaling`` off, because the rescale then + renormalized such a stored column at ULP scale.) """ - model, x_t = _warm_model(warmup=3, doscaling=False) + model, x_t = _warm_model(warmup=3) _force_merged_column(model) dead = ~np.array(model.comp_used) - assert dead.any(), "setup failed: no column was merged away" + assert dead.any(), "setup failed: no component was merged away" before = { - name: np.array(getattr(model, name))[:, dead] + name: _component_values(model, name, dead) for name in ("A", "mu", "alpha", "beta", "rho") } live_mu_before = np.array(model.mu)[:, ~dead] @@ -419,8 +433,10 @@ def test_merged_away_columns_keep_their_last_finite_value(): mx.eval(model.A, model.mu, model.alpha, model.beta, model.rho) for name, expected in before.items(): - actual = np.array(getattr(model, name))[:, dead] - assert np.all(np.isfinite(actual)), f"{name} went non-finite on a dead column" + actual = _component_values(model, name, dead) + assert np.all(np.isfinite(actual)), ( + f"{name} went non-finite on a dead component" + ) np.testing.assert_array_equal(actual, expected, err_msg=name) # The live columns did keep moving, so "unchanged" above means frozen, not # "nothing happened in these three iterations". diff --git a/pamica/tests/native_oracle.py b/pamica/tests/native_oracle.py index ab495406..0a3f968c 100644 --- a/pamica/tests/native_oracle.py +++ b/pamica/tests/native_oracle.py @@ -13,9 +13,9 @@ Reference facts this relies on (``amica15.f90``): * Layout: the reference mixing matrix ``A(nw, num_comps)`` holds source ``i`` of - model ``h`` in column ``comp_list(i, h)``. pamica stores each model's block - transposed (issue #24 convention, ADR 0006), so the reference block is the - transpose of pamica's stored block. + model ``h`` in column ``comp_list(i, h)``. pamica stores one component per + row (issue #334, ADR 0007), so the reference ``A`` is pamica's stored ``A`` + transposed, merged ``comp_list`` or not. * Every ``load_*`` file is a raw little-endian float64 array in column-major order under ``indir``. A loaded ``A`` is used as is (no normalization, :793-800), unlike a drawn one (:818-819). @@ -100,27 +100,20 @@ class ReferenceOutput: stdout: str -def reference_mixing(A_stored: np.ndarray, comp_list: np.ndarray) -> np.ndarray: - """pamica's stored ``A`` in the reference layout: block ``h`` transposed. - - Only defined for a disjoint ``comp_list`` (no shared columns): in pamica's - current layout a merged column belongs to rows of several blocks, which has - no reference counterpart (issue #334). - """ - if np.unique(comp_list).size != comp_list.size: - raise ValueError("reference_mixing needs a comp_list without shared columns") - A_ref = np.empty_like(A_stored) - for h in range(comp_list.shape[1]): - idx = comp_list[:, h] - A_ref[:, idx] = A_stored[:, idx].T - return A_ref +def reference_mixing(A_stored: np.ndarray) -> np.ndarray: + """pamica's stored component-row ``A`` in the reference layout + ``(nw, num_comps)``: its transpose (issue #334), for any ``comp_list``.""" + return np.ascontiguousarray(np.asarray(A_stored).T) def seed_from_torch(model) -> SeedState: - """The reference-layout state of an initialized (or fitted) ``AMICATorchNG``.""" - comp_list = model.comp_list.cpu().numpy() + """The reference-layout state of an initialized (or fitted) ``AMICATorchNG``. + + ``comp_list`` is left at the reference's default; pass a merged one with + ``dataclasses.replace`` (the ``load_comp_list`` path, which needs ``c == 0``). + """ return SeedState( - A=reference_mixing(model.A.cpu().numpy(), comp_list), + A=reference_mixing(model.A.cpu().numpy()), mean=model.mean.cpu().numpy().reshape(-1), mu=model.mu.cpu().numpy(), sbeta=model.beta.cpu().numpy(), diff --git a/pamica/tests/pre_change.py b/pamica/tests/pre_change.py new file mode 100644 index 00000000..50afb397 --- /dev/null +++ b/pamica/tests/pre_change.py @@ -0,0 +1,145 @@ +"""Import pamica as it was at an earlier commit, beside the live package. + +Byte-identity tests fit the live backends and the pre-change backends in one +process on one machine, so they can compare results bit for bit without recorded +constants. The whole package at that commit is extracted from git (its Python +sources and the two data files its modules read) and imported under a private +top-level name, so every relative import resolves inside the old tree: a helper +module that a later change edited (``numpy_impl/utils.py``, say) cannot leak +into the old backends, and nothing of the old tree leaks into the live one. + +This is the one place tests read historical code (issue #343), and it only +reads: it never fetches, and it never writes to the repository. A +``git fetch --depth 1`` run from a test once turned a developer's full clone +shallow, after which ``git gc`` could prune history. When the pinned commit is +missing, the calling test fails under ``CI`` (whose jobs check out full +history, ``fetch-depth: 0``) and is otherwise skipped with the command that +fetches it; the same holds, with its own message, when git is not installed or +the tests do not run from a git checkout. + +Not a test module (no ``test_`` prefix, so pytest does not collect it). +""" + +from __future__ import annotations + +import importlib +import io +import os +import subprocess +import sys +import tarfile +from pathlib import Path, PurePosixPath +from types import ModuleType +from typing import NoReturn + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[2] +# Non-Python files the package reads at run time: the NumPy backend's +# defaults and the reference source fortran_params.py parses its defaults from. +_DATA_FILES = frozenset({"pamica/numpy_impl/params.json", "pamica/amica15.f90"}) + + +def _unavailable(reason: str) -> NoReturn: + """Fail under ``CI`` (whose jobs check out full history with git), else skip.""" + if os.environ.get("CI"): + pytest.fail(reason) + pytest.skip(reason) + + +def _git(*args: str, root: Path = REPO_ROOT) -> subprocess.CompletedProcess: + """Run a read-only git command in ``root``.""" + try: + return subprocess.run(["git", *args], cwd=root, capture_output=True) + except FileNotFoundError: + _unavailable( + "git is not installed; these tests need git and the pamica history" + ) + + +def require_commit(commit: str, root: Path = REPO_ROOT) -> None: + """Fail under ``CI``, or skip, unless ``commit`` is in the clone at ``root``. + + ``commit`` must be a full 40-character SHA, so the message can name the + exact ``git fetch`` that brings it in. Outside a git checkout (pamica + installed from a wheel or an sdist, say) there is nothing to fetch into, so + that case gets its own message. + """ + if len(commit) != 40: + raise ValueError(f"pin a full 40-character commit SHA, got {commit!r}") + inside = _git("rev-parse", "--is-inside-work-tree", root=root) + if inside.returncode != 0 or inside.stdout.strip() != b"true": + _unavailable( + f"{root} is not a git checkout (pamica installed from a wheel or an " + "sdist, say), so the test cannot load the pre-change code; run it " + "from a clone of the pamica repository with its history" + ) + if _git("cat-file", "-e", f"{commit}^{{commit}}", root=root).returncode == 0: + return + shallow = _git("rev-parse", "--is-shallow-repository", root=root) + remedy = ( + "this clone is shallow; run `git fetch --unshallow origin`" + if shallow.stdout.strip() == b"true" + else f"run `git fetch origin {commit}`" + ) + _unavailable( + f"commit {commit[:7]} is not in this clone, so the test cannot load the " + f"pre-change code; {remedy} and rerun (tests never fetch, issue #343)" + ) + + +# The attribute a loaded package records its commit in, so a cached alias is +# never silently reused for a different commit. +_COMMIT_ATTR = "__pamica_pre_change_commit__" + + +def load_pre_change_package( + commit: str, alias: str, dest: Path, root: Path = REPO_ROOT +) -> ModuleType: + """Import the ``pamica`` package at ``commit`` as top-level ``alias``. + + The sources are written under ``dest / alias`` (a pytest temporary + directory). A second call with the same ``alias`` and ``commit`` returns + the package already imported; the same ``alias`` with another commit + raises ``ValueError``. :func:`require_commit` decides what happens when the + commit, git or the checkout at ``root`` is missing. + """ + if alias in sys.modules: + loaded = getattr(sys.modules[alias], _COMMIT_ATTR, None) + if loaded != commit: + raise ValueError( + f"module {alias!r} is already imported from commit {loaded!r}; " + f"load {commit[:7]} under a different alias" + ) + return sys.modules[alias] + require_commit(commit, root) + result = _git("archive", "--format=tar", commit, "pamica", root=root) + if result.returncode != 0: + raise RuntimeError( + f"git archive {commit[:7]} failed although the commit is present: " + f"{result.stderr.decode().strip()!r}" + ) + + package_dir = dest / alias + with tarfile.open(fileobj=io.BytesIO(result.stdout)) as tar: + for member in tar.getmembers(): + name = PurePosixPath(member.name) + if not member.isfile() or name.parts[1] in ("tests", "sample_data"): + continue + if name.suffix != ".py" and member.name not in _DATA_FILES: + continue + extracted = tar.extractfile(member) + assert extracted is not None # a regular file always has content + target = package_dir.joinpath(*name.parts[1:]) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(extracted.read()) + + # Only the top-level import needs the path: submodules resolve through the + # package's own __path__, which already points at ``package_dir``. + sys.path.insert(0, str(dest)) + try: + package = importlib.import_module(alias) + finally: + sys.path.remove(str(dest)) + setattr(package, _COMMIT_ATTR, commit) + return package diff --git a/pamica/tests/test_component_layout.py b/pamica/tests/test_component_layout.py new file mode 100644 index 00000000..5c9eb333 --- /dev/null +++ b/pamica/tests/test_component_layout.py @@ -0,0 +1,82 @@ +"""Validation of :func:`pamica.component_layout.rows_from_legacy_columns`. + +The conversion that loads a pre-#334 save (components as stored columns) into +the component-row layout. Its round trips on real fitted models are in +``test_component_rows_persistence.py``; these are the input checks, on small +arrays: every malformed payload and every merged ``comp_list`` must raise the +module's own ``ValueError`` instead of converting, or failing later with an +indexing error. +""" + +from __future__ import annotations + +import numpy as np +import pytest + +from pamica.component_layout import rows_from_legacy_columns + +N = 3 +# Two models, unmerged: model 0 uses components 0-2, model 1 uses 3-5, in a +# permuted order so the conversion cannot pass by treating ids as positions. +COMP_LIST = np.array([[0, 5], [2, 3], [1, 4]]) +A_LEGACY = np.arange(N * 2 * N, dtype=np.float64).reshape(N, 2 * N) / 7.0 + + +def _convert(A: np.ndarray = A_LEGACY, comp_list: np.ndarray = COMP_LIST): + return rows_from_legacy_columns(A, comp_list, owner="Probe") + + +def test_an_unmerged_payload_converts_block_for_block(): + A_rows = _convert() + assert A_rows.shape == (2 * N, N) + assert A_rows.dtype == A_LEGACY.dtype + for h in range(2): + idx = COMP_LIST[:, h] + np.testing.assert_array_equal(A_rows[idx, :], A_LEGACY[:, idx]) + + +def test_a_one_dimensional_comp_list_is_refused(): + with pytest.raises(ValueError, match=r"malformed Probe state: comp_list has shape"): + _convert(comp_list=COMP_LIST[:, 0]) + + +@pytest.mark.parametrize( + "comp_list", + [COMP_LIST.astype(np.float64), np.where(COMP_LIST == 4, np.nan, COMP_LIST)], + ids=["float-ids", "nan-id"], +) +def test_a_non_integer_comp_list_is_refused(comp_list): + with pytest.raises(ValueError, match=r"comp_list has dtype float64"): + _convert(comp_list=comp_list) + + +@pytest.mark.parametrize( + "A", + [A_LEGACY.T, A_LEGACY[:, :-1], A_LEGACY[None]], + ids=["transposed", "one-column-short", "three-dimensional"], +) +def test_a_mismatched_A_shape_is_refused(A): + with pytest.raises(ValueError, match=r"the pre-component-row A has shape"): + _convert(A=A) + + +@pytest.mark.parametrize("bad", [-1, 2 * N], ids=["negative", "past-the-end"]) +def test_an_out_of_range_id_is_refused(bad): + comp_list = COMP_LIST.copy() + comp_list[1, 1] = bad + with pytest.raises(ValueError, match=r"comp_list holds ids outside \[0, 6\)"): + _convert(comp_list=comp_list) + + +def test_an_id_repeated_within_a_model_is_refused(): + comp_list = COMP_LIST.copy() + comp_list[2, 1] = comp_list[0, 1] + with pytest.raises(ValueError, match=r"model 1 uses a component id twice"): + _convert(comp_list=comp_list) + + +def test_an_id_shared_across_models_asks_for_a_refit(): + comp_list = COMP_LIST.copy() + comp_list[0, 1] = comp_list[0, 0] # a share_comps merge: 5 folded into 0 + with pytest.raises(ValueError, match=r"merged 1 component\(s\).*Refit it"): + _convert(comp_list=comp_list) diff --git a/pamica/tests/test_component_rows.py b/pamica/tests/test_component_rows.py new file mode 100644 index 00000000..1b116bad --- /dev/null +++ b/pamica/tests/test_component_rows.py @@ -0,0 +1,1229 @@ +"""Components are rows of ``A`` in every backend (issue #334, epic #324 Phase 8, +ADR 0007). + +Before this change every array backend stored ``A`` as ``(n, n_comps)`` and took +model ``h``'s block as ``A[:, comp_list[:, h]]``, whose ROWS are the model's +components (issue #24 convention), while the component ids in ``comp_list`` +indexed stored COLUMNS. ``share_comps`` therefore compared and tied the wrong +vectors: its metric measured stored columns and its merge tied one sphered +channel's loadings across two models. ``A`` is now ``(n_comps, n)`` with one +component per row, so a component id names the same component everywhere, as in +the reference (whose ``A(:, k)`` is our row ``k``). + +Pinned here, cross-backend per ``.rules/backend_parity.md`` (PyTorch and NumPy +always run; MLX checks skip individually without MLX or an Apple GPU): + +1. every configuration without a merge is byte-identical to the pre-change code + (the package at commit ``0930c0e`` is loaded from git): one, two and three + models, ``doscaling`` and Newton on and off, every ``pdftype``, + ``do_reject``, several blocks, a ``keep_best`` restore and best-of-two + restarts, each on every backend that supports it; the old ``A`` maps onto + the new one through the conversion the persistence layer uses; +2. the semantics: (a) sources grouped by a real merge share one component map; + (b) a planted exact duplicate is merged at the default and at a strict + threshold, and the merge leaves the log-likelihood, ``transform`` and the + maps unchanged; (c) the metric compares exactly the sensor maps + ``get_sensor_mixing_matrix`` returns; (d) the pre-change column semantics + fails (a) to (c), so none of them is vacuous; +3. the ``A`` the EEGLAB export writes is the reference's layout for several + models, and single-model exports are unchanged byte for byte; +4. (opt-in, ``AMICA_RUN_FORTRAN=1``) the native reference binary, seeded with a + MERGED ``comp_list`` through ``load_comp_list``, matches the PyTorch and + NumPy updates from that state to float64 round-off; its own scan never + merges; and an early mass merge collapses its update exactly as it + collapses pamica's. + +Persistence of the new layout and conversion of old saves are in +``test_component_rows_persistence.py``. Real bundled sample EEG only, no +synthetic data or mocks (``.rules/testing.md``). +""" + +from __future__ import annotations + +import importlib +import os +from pathlib import Path +from typing import Any, Dict, List, Tuple + +import numpy as np +import pytest +import torch +from scipy.special import logsumexp + +from pamica import AMICA_NumPy +from pamica.component_layout import rows_from_legacy_columns +from pamica.numpy_impl.utils import identify_shared_components +from pamica.tests.pre_change import load_pre_change_package +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +NMIX = 3 +SEED = 42 + +# The epic #324 head this phase merges onto: the last commit with the +# component-column layout (Phase 7's doscaling fix and Phase 9's schedule +# gates included, so the two sides differ by this phase alone). +PRE_CHANGE_COMMIT = "0930c0e68ec9e2bbef3d20d51cff4c03029248f2" + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +@pytest.fixture(scope="module") +def real_data() -> np.ndarray: + X = load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD) + return X.astype(np.float64) + + +@pytest.fixture(scope="module") +def pre(tmp_path_factory) -> Any: + """The pamica package at ``PRE_CHANGE_COMMIT``, imported beside the live one.""" + return load_pre_change_package( + PRE_CHANGE_COMMIT, "pamica_pre334", tmp_path_factory.mktemp("pre334") + ) + + +def _mlx_core(): + """The MLX backend module, or skip this test (never the whole module).""" + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + mx = mlx_core.mx + if mx.default_device().type != mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + return mlx_core + + +def _live(backend: str) -> Any: + """The live backend class.""" + if backend == "torch": + return AMICATorchNG + if backend == "numpy": + return AMICA_NumPy + return _mlx_core().AMICAMLXNG + + +def _classes(backend: str, pre: Any) -> Tuple[Any, Any]: + """``(pre-change class, live class)`` for ``backend``.""" + live = _live(backend) + if backend == "torch": + return pre.torch_impl.core.AMICATorchNG, live + if backend == "numpy": + return pre.numpy_impl.core.AMICA, live + old = importlib.import_module(f"{pre.__name__}.mlx_impl.core").AMICAMLXNG + return old, live + + +def _np(value: Any) -> np.ndarray: + if isinstance(value, torch.Tensor): + return value.detach().cpu().numpy() + return np.array(value) + + +# --- 1. byte identity of every configuration without a merge ---------------- +# A short real slice: every configuration changes the trajectory within a few +# iterations (Newton from the third, rejection from the third), so each one +# exercises its own path. +_BYTE_ID_SAMPLES = 4096 +_BYTE_ID_ITERS = 6 + + +_ALL = ("torch", "numpy", "mlx") +# The NumPy backend implements only the generalized-Gaussian density (it raises +# NotImplementedError for any other pdftype; test_numpy_rejects_other_pdftypes +# below) and has no keep_best option, so those configurations run on PyTorch +# and MLX only. +_NOT_NUMPY = ("torch", "mlx") + + +def _byte_id_configs() -> List[Any]: + """``(backend, n_models, cfg)`` params, each configuration on every backend + that supports it (see ``_NOT_NUMPY``).""" + configs: List[Tuple[str, int, Dict[str, Any], Tuple[str, ...]]] = [] + for n_models in (1, 2): + for doscaling in (True, False): + for newton in (False, True): + for pdftype in (0, 1): + cfg = dict(doscaling=doscaling, do_newton=newton, pdftype=pdftype) + if newton: + cfg["newt_start"] = 2 + name = ( + f"m{n_models}-{'scale' if doscaling else 'noscale'}-" + f"{'newton' if newton else 'ng'}-pdf{pdftype}" + ) + configs.append( + (name, n_models, cfg, _ALL if pdftype == 0 else _NOT_NUMPY) + ) + newton: Dict[str, Any] = dict(do_newton=True, newt_start=2) + configs += [ + ( + "m2-reject-newton", + 2, + dict(newton, do_reject=True, rejstart=2, rejint=2), + _ALL, + ), + ("m3-scale-newton-pdf0", 3, newton, _ALL), + ("m3-noscale-ng-pdf1", 3, dict(doscaling=False, pdftype=1), _NOT_NUMPY), + # The other fixed density families (n_mix 1 for the single-component + # sub-Gaussian cosh+, family 4; see _fit_arrays). + ("m1-ng-pdf2", 1, dict(pdftype=2), _NOT_NUMPY), + ("m1-ng-pdf3", 1, dict(pdftype=3), _NOT_NUMPY), + ("m1-ng-pdf4", 1, dict(pdftype=4), _NOT_NUMPY), + # Four blocks of 1024 samples: the per-block accumulation. + ("m2-newton-4blocks", 2, dict(newton, block_size=1024), _ALL), + # The best-iterate safeguard, with a learning rate high enough that the + # log-likelihood falls, so it restores an earlier snapshot of A. + # (lrate 1.5: from 2.0 the float32 MLX fit goes non-finite.) + ("m2-keepbest-restores", 2, dict(keep_best=True, lrate=1.5), _NOT_NUMPY), + # Best-of-two restarts, which re-initializes A and keeps the winner. + ("m2-restarts", 2, dict(n_restarts=2), _ALL), + ] + return [ + pytest.param(backend, n_models, cfg, id=f"{backend}-{name}") + for name, n_models, cfg, backends in configs + for backend in backends + ] + + +def test_numpy_rejects_other_pdftypes(): + """The reason the non-GG byte-identity cases run on PyTorch and MLX only.""" + for pdftype in (1, 2, 3, 4): + with pytest.raises(NotImplementedError, match="pdftype=0"): + AMICA_NumPy(num_models=1, max_iter=1, use_tqdm=False, pdftype=pdftype) + + +def _fit_arrays( + backend: str, cls: Any, n_models: int, cfg: Dict[str, Any], X: np.ndarray, out: Path +) -> Dict[str, np.ndarray]: + """A short fit of ``cls``: its log-likelihood and gradient-norm record and + every fitted array, as numpy. ``cfg`` overrides the defaults here (one + block of the whole slice; ``keep_best`` off on PyTorch and MLX).""" + n_mix = 1 if cfg.get("pdftype", 0) in (1, 4) else NMIX + if backend == "numpy": + params: Dict[str, Any] = dict( + num_models=n_models, + num_mix=n_mix, + max_iter=_BYTE_ID_ITERS, + seed=SEED, + use_tqdm=False, + outdir=str(out), + do_opt_block=False, + block_size=_BYTE_ID_SAMPLES, + writestep=10000, + ) + params.update(cfg) + model = cls(**params) + model.fit(X) + ll, nd = model.ll, model.nd + else: + kwargs: Dict[str, Any] = dict( + n_channels=NW, + n_models=n_models, + n_mix=n_mix, + seed=SEED, + block_size=_BYTE_ID_SAMPLES, + keep_best=False, + ) + kwargs.update(cfg) + if backend == "torch": + kwargs.update(device="cpu", dtype=torch.float64) + model = cls(**kwargs) + model.fit(X, max_iter=_BYTE_ID_ITERS, verbose=False) + ll, nd = model.ll_history, [model._ndtmpsum] + arrays = {"ll": np.asarray(ll, dtype=np.float64), "nd": np.asarray(nd)} + names = ["A", "W", "mu", "beta", "alpha", "rho", "gm", "c", "comp_list"] + if backend != "numpy": + names.append("pdtype") + arrays["final_ll"] = np.asarray(model.final_ll_, dtype=np.float64) + for name in names: + arrays[name] = _np(getattr(model, name)) + return arrays + + +@pytest.mark.parametrize("backend, n_models, cfg", _byte_id_configs()) +def test_unshared_fits_are_byte_identical_to_the_column_layout( + pre, real_data, backend, n_models, cfg, tmp_path +): + """Without a merge every per-model block holds the same ``n x n`` matrix in + both layouts, so every trajectory and fitted array is bit for bit what the + pre-change code produced, the old ``A`` mapped onto component rows through + :func:`pamica.component_layout.rows_from_legacy_columns`. + + The one recorded quantity that moves is the weight-gradient norm + ``ndtmpsum``, by float round-off: it now sums squares per component row, as + the reference sums each component's column (``nd(iter,:) = + sum(dAk*dAk,1)``), where the column layout grouped them per stored column. + Measured: at most 2.2e-16 relative in float64 and 1.1e-7 (one float32 unit + in the last place) on MLX. It only feeds the gradient-norm stops, and no + stop fired in these fits. + """ + old_cls, new_cls = _classes(backend, pre) + X = real_data[:, :_BYTE_ID_SAMPLES] + old = _fit_arrays(backend, old_cls, n_models, cfg, X, tmp_path / "old") + new = _fit_arrays(backend, new_cls, n_models, cfg, X, tmp_path / "new") + + assert new["A"].shape == (NW * n_models, NW) + old["A"] = rows_from_legacy_columns(old["A"], old["comp_list"], owner="test") + changed = sorted(k for k in old if k != "nd" and not np.array_equal(old[k], new[k])) + assert not changed, f"results changed: {changed}" + # Six iterations ran, so no stop fired (the premise of the nd note above). + assert len(new["ll"]) == _BYTE_ID_ITERS + if cfg.get("keep_best"): + assert new["final_ll"] != new["ll"][-1], "setup: keep_best restored nothing" + eps = np.finfo(np.float32 if backend == "mlx" else np.float64).eps + np.testing.assert_allclose(new["nd"], old["nd"], rtol=float(2 * eps), atol=0) + + +# --- 2. semantics ---------------------------------------------------------------- +# A shared recipe whose one scan merges a few pairs on the short slice: both +# models start near the identity, so their components stay near-collinear for +# the first iterations. At iteration 8 and comp_thresh 0.99 the scan merges +# three pairs (measured, PyTorch); the column metric on the same trajectory +# would merge five. A partial merge is what makes (d) discriminate: once every +# pair is merged, both layouts hold two identical blocks. +_SHARE = dict(share_comps=True, share_start=8, share_iter=100, comp_thresh=0.99) +_SHARE_SAMPLES = 4096 +_SHARE_ITERS = 10 + + +def _share_fit(backend: str, cls: Any, X: np.ndarray, out: Path) -> Any: + """A real two-model share fit of ``cls`` whose scan at iteration 8 merges.""" + if backend == "numpy": + model = cls( + num_models=2, + num_mix=NMIX, + max_iter=_SHARE_ITERS, + seed=SEED, + use_tqdm=False, + outdir=str(out), + do_opt_block=False, + block_size=_SHARE_SAMPLES, + writestep=10000, + share_start=_SHARE["share_start"], + share_int=_SHARE["share_iter"], + share_comps=True, + comp_thresh=_SHARE["comp_thresh"], + ) + model.fit(X) + return model + kwargs: Dict[str, Any] = dict( + n_channels=NW, n_models=2, n_mix=NMIX, seed=SEED, block_size=_SHARE_SAMPLES + ) + if backend == "torch": + kwargs.update(device="cpu", dtype=torch.float64) + model = cls(**kwargs, **_SHARE) + model.fit(X, max_iter=_SHARE_ITERS, verbose=False) + return model + + +def _groups(comp_list: np.ndarray) -> List[List[Tuple[int, int]]]: + """``(model, source)`` groups sharing a component id across models.""" + groups = [] + for k in np.unique(comp_list): + src, mdl = np.where(comp_list == k) + if np.unique(mdl).size >= 2: + groups.append([(int(h), int(i)) for i, h in zip(src, mdl)]) + return groups + + +def _mixing(model: Any, h: int) -> np.ndarray: + """Model ``h``'s sphered-space mixing matrix, one source per column, for a + live or pre-change model of any backend (NumPy has no accessor).""" + if hasattr(model, "get_mixing_matrix"): + return np.asarray(model.get_mixing_matrix(h), dtype=np.float64) + A, comp_list = _np(model.A), _np(model.comp_list) + if A.shape[0] == comp_list.size: # component rows + return A[comp_list[:, h], :].T + return A[:, comp_list[:, h]].T # the pre-change column layout + + +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_sources_grouped_by_a_merge_share_one_component_map( + pre, real_data, backend, tmp_path +): + """(a) Sources a merge groups use ONE component, so their mixing vectors + and sensor maps are identical, and so are their densities. (d) The column + semantics fails this: its grouped sources keep different maps.""" + old_cls, new_cls = _classes(backend, pre) + X = real_data[:, :_SHARE_SAMPLES] + new = _share_fit(backend, new_cls, X, tmp_path / "new") + comp_list = _np(new.comp_list) + groups = _groups(comp_list) + assert groups, "setup: no merge fired" + for group in groups: + (h0, i0), rest = group[0], group[1:] + k = comp_list[i0, h0] + a0 = _mixing(new, h0)[:, i0] + s0 = new.get_sensor_mixing_matrix(h0)[:, i0] + for h, i in rest: + assert comp_list[i, h] == k + np.testing.assert_array_equal(_mixing(new, h)[:, i], a0) + # Same vector through two matrix products of different shape. + np.testing.assert_allclose( + new.get_sensor_mixing_matrix(h)[:, i], s0, rtol=1e-12, atol=0 + ) + + old = _share_fit(backend, old_cls, X, tmp_path / "old") + old_groups = _groups(_np(old.comp_list)) + assert old_groups, "setup: the column semantics merged nothing" + gaps = [ + np.abs(_mixing(old, h)[:, i] - _mixing(old, group[0][0])[:, group[0][1]]).max() + for group in old_groups + for h, i in group[1:] + ] + assert min(gaps) > 1e-2, f"the column semantics grouped identical maps: {gaps}" + + +# (b) A planted exact duplicate on a converged state. 150 natural-gradient and +# Newton iterations on the full sample leave the two models' best-matching +# components at |cos| 0.95-0.96 (issue #334), below the default threshold, so +# the planted pair is the only one a scan at 0.99 can merge (asserted). +_PLANT_ITERS = 150 + + +@pytest.fixture(scope="module") +def converged_two_model_state(real_data) -> Dict[str, Any]: + """A real two-model torch fit (sharing off) and its component-row state.""" + m = AMICATorchNG( + n_channels=NW, + n_models=2, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + do_newton=True, + ) + m.fit(real_data, max_iter=_PLANT_ITERS, verbose=False) + assert int(m.comp_used.sum()) == m.n_comps, "setup: a merge fired" + state: Dict[str, Any] = { + k: _np(getattr(m, k)).copy() for k in ("A", "mu", "beta", "rho", "alpha") + } + state.update( + { + k: _np(getattr(m, k)).copy() + for k in ("gm", "c", "comp_list", "mean", "sphere") + } + ) + maps = [m.get_sensor_mixing_matrix(h) for h in range(2)] + maps = [q / np.linalg.norm(q, axis=0) for q in maps] + cos = np.abs(maps[0].T @ maps[1]) + i, ii = np.unravel_index(int(np.argmax(cos)), cos.shape) + state["pair"] = (int(i), int(ii)) + state["best_cos"] = float(cos[i, ii]) + state["sldet"] = m.sldet + return state + + +def _plant(state: Dict[str, Any]) -> Dict[str, Any]: + """Make model 1's source ``ii`` exactly model 0's source ``i``: the same + component mixing vector (row) and density, the state the reference's + sharing is designed to detect.""" + planted = { + k: (v.copy() if isinstance(v, np.ndarray) else v) for k, v in state.items() + } + i, ii = state["pair"] + cl = state["comp_list"] + ci, cj = int(cl[i, 0]), int(cl[ii, 1]) + planted["A"][cj, :] = planted["A"][ci, :] + for name in ("mu", "beta", "rho", "alpha"): + planted[name][:, cj] = planted[name][:, ci] + planted["ci"], planted["cj"] = ci, cj + return planted + + +class _Probe: + """One backend holding a given state: its scan, log-likelihood, sources + and maps, through the backend's own methods.""" + + def __init__( + self, backend: str, cls: Any, state: Dict[str, Any], X: np.ndarray, out: Path + ): + self.backend, self.X = backend, X + if backend == "torch": + m = cls( + n_channels=NW, n_models=2, n_mix=NMIX, device="cpu", dtype=torch.float64 + ) + for k in ("mu", "beta", "rho", "alpha", "gm", "c", "mean", "sphere"): + setattr(m, k, torch.from_numpy(np.array(state[k], np.float64))) + m.A = torch.from_numpy(np.array(state["A"], np.float64)) + m.comp_list = torch.from_numpy(state["comp_list"].copy()) + m.sldet = state["sldet"] + m.pdtype = torch.zeros((NW, 2), dtype=torch.long) + m._sphere_pinv = None + elif backend == "numpy": + # One real iteration sizes the model and preprocesses the data; + # every parameter and the preprocessing are then replaced by the + # given state, so the sphere matches the other backends' exactly. + m = cls( + num_models=2, + num_mix=NMIX, + max_iter=1, + seed=SEED, + use_tqdm=False, + do_opt_block=False, + block_size=8192, + writestep=10**6, + outdir=str(out), + ) + m.fit(X) + m.mean = np.array(state["mean"], np.float64).reshape(-1, 1) + m.sphere = np.array(state["sphere"], np.float64) + m.sldet = state["sldet"] + m.data = m.sphere @ (X - m.mean) + m._sphere_pinv = None + for k in ("mu", "beta", "rho", "alpha", "gm", "c", "A"): + setattr(m, k, np.array(state[k], np.float64)) + m.comp_list = state["comp_list"].copy() + else: + mx = _mlx_core().mx + m = cls(n_channels=NW, n_models=2, n_mix=NMIX) + for k in ("mu", "beta", "rho", "alpha", "gm", "c", "A"): + setattr(m, k, mx.array(np.asarray(state[k], np.float32))) + m.comp_list = mx.array(state["comp_list"].copy()) + m.mean = mx.array(np.asarray(state["mean"], np.float32).reshape(-1, 1)) + m.sphere = mx.array(np.asarray(state["sphere"], np.float32)) + m._sphere_np = np.array(m.sphere, dtype=np.float64) + m._sphere_pinv = None + m.sldet = state["sldet"] + m.pdtype = mx.array(np.zeros((NW, 2), dtype=np.int32)) + used = np.zeros(2 * NW, dtype=bool) + used[np.unique(state["comp_list"])] = True + m._comp_used_arr = mx.array(used) + m._refresh_lgamma_table() + self.model = m + self.refresh() + + def refresh(self) -> None: + m = self.model + if self.backend == "numpy": + m.comp_used = np.zeros(2 * NW, dtype=bool) + m.comp_used[np.unique(m.comp_list)] = True + elif self.backend == "mlx": + mx = _mlx_core().mx + used = np.zeros(2 * NW, dtype=bool) + used[np.unique(np.array(m.comp_list))] = True + m._comp_used_arr = mx.array(used) + m._update_unmixing_matrices() + + @property + def comp_list(self) -> np.ndarray: + return _np(self.model.comp_list).copy() + + def set_comp_list(self, comp_list: np.ndarray) -> None: + m = self.model + if self.backend == "torch": + m.comp_list = torch.from_numpy(comp_list.copy()) + elif self.backend == "numpy": + m.comp_list = comp_list.copy() + else: + m.comp_list = _mlx_core().mx.array(comp_list.copy()) + self.refresh() + + def scan(self, thresh: float) -> None: + self.model.comp_thresh = thresh + self.model._identify_shared_comps() + self.refresh() + + def ll(self) -> float: + m = self.model + if self.backend == "numpy": + return float(m._get_updates_and_likelihood()["ll"]) + lht = np.asarray(m.model_loglik(self.X), dtype=np.float64) + return float(logsumexp(lht, axis=0).sum() / (lht.shape[1] * NW)) + + def sources(self, h: int) -> np.ndarray: + m = self.model + if self.backend == "numpy": + return m.transform(self.X)[m.comp_list[:, h], :, h] + return np.asarray(m.transform(self.X, model_idx=h)) + + def maps(self, h: int) -> np.ndarray: + return np.asarray(self.model.get_sensor_mixing_matrix(h)) + + +@pytest.mark.parametrize("thresh", [0.99, 1.0 - 1e-12], ids=["default", "strict"]) +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_a_planted_duplicate_is_merged_and_the_model_is_unchanged( + converged_two_model_state, real_data, backend, thresh, tmp_path +): + """(b) Plant the reference's notion of a shared component: model 1's source + ``ii`` set to exactly model 0's source ``i`` (mixing vector and density). + The scan must fold exactly that pair, and the fold must leave the model as + it was, because the tied parameters were already equal: log-likelihood, + ``transform`` and the maps are bit for bit unchanged (issue #334 measured + the column fold at -0.186 on such a pair, the reference's at exactly 0).""" + planted = _plant(converged_two_model_state) + ii = planted["pair"][1] + ci, cj = planted["ci"], planted["cj"] + probe = _Probe(backend, _live(backend), planted, real_data, tmp_path) + before = (probe.ll(), probe.sources(1), probe.maps(1)) + cl0 = probe.comp_list + + probe.scan(thresh) + cl = probe.comp_list + folded = np.argwhere(cl != cl0) + assert folded.tolist() == [[ii, 1]], f"expected only the planted pair: {folded}" + assert cl[ii, 1] == ci and cj not in cl + + after = (probe.ll(), probe.sources(1), probe.maps(1)) + assert after[0] == before[0] + np.testing.assert_array_equal(after[1], before[1]) + np.testing.assert_array_equal(after[2], before[2]) + + +def _column_state(state: Dict[str, Any]) -> Dict[str, Any]: + """A component-row state in the pre-change column layout (the inverse of + the persistence conversion, valid for an unmerged comp_list).""" + col = dict(state) + A_rows, cl = state["A"], state["comp_list"] + A_cols = np.empty((A_rows.shape[1], A_rows.shape[0]), dtype=A_rows.dtype) + for h in range(cl.shape[1]): + A_cols[:, cl[:, h]] = A_rows[cl[:, h], :] + col["A"] = A_cols + return col + + +def test_the_column_semantics_misses_and_breaks_a_planted_duplicate( + pre, converged_two_model_state, real_data, tmp_path +): + """(d) The same planted state under the pre-change column semantics: its + metric does not see the pair at the default threshold, and folding the pair + the column way changes the log-likelihood (issue #334: -0.186), so test (b) + distinguishes the two semantics.""" + planted = _plant(converged_two_model_state) + (i, ii), ci, cj = planted["pair"], planted["ci"], planted["cj"] + old_cls = pre.torch_impl.core.AMICATorchNG + old = _Probe("torch", old_cls, _column_state(planted), real_data, tmp_path) + ll0 = old.ll() + cl0 = old.comp_list + old.scan(0.99) + found = old.comp_list[ii, 1] == old.comp_list[i, 0] + assert not found, "the column metric found the planted pair" + cl = cl0.copy() + cl[cl == cj] = ci + old.set_comp_list(cl) + assert abs(old.ll() - ll0) > 1e-2 + + +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_the_metric_compares_the_sensor_maps( + converged_two_model_state, real_data, backend, tmp_path +): + """(c) The vector the scan compares for source ``i`` of model ``h`` is + column ``i`` of ``get_sensor_mixing_matrix(h)``, and the scan's decision is + the shared kernel's decision on those maps. (d) The pre-change metric + vectors (de-sphered stored columns) are not the maps at all.""" + probe = _Probe( + backend, _live(backend), converged_two_model_state, real_data, tmp_path + ) + vectors = probe.model._component_sensor_maps() + cl = probe.comp_list + for h in range(2): + maps = probe.maps(h) + # The same vectors through matrix products of different shape. + scale = np.abs(maps).max() + np.testing.assert_allclose( + vectors[:, cl[:, h]], maps, rtol=0, atol=1e-13 * scale + ) + + # The decision on a state where the default threshold merges nothing, at a + # threshold where it merges several pairs. + thresh = 0.9 + by_maps = np.empty((NW, 2 * NW)) + for h in range(2): + by_maps[:, cl[:, h]] = probe.maps(h) + want, _ = identify_shared_components(by_maps, cl, thresh) + probe.scan(thresh) + assert not np.array_equal(want, cl), "setup: nothing merged at 0.9" + np.testing.assert_array_equal(probe.comp_list, want) + + # The pre-change metric on the same fitted model: stored columns. + old_state = _column_state(converged_two_model_state) + spinv = np.linalg.pinv(old_state["sphere"]) + columns = spinv @ old_state["A"] + columns /= np.linalg.norm(columns, axis=0) + cos = [] + for h in range(2): + maps = spinv @ converged_two_model_state["A"][cl[:, h], :].T + maps /= np.linalg.norm(maps, axis=0) + cos.append(np.abs(np.sum(columns[:, cl[:, h]] * maps, axis=0))) + assert np.median(np.concatenate(cos)) < 0.6, "the column metric saw the maps" + + +# --- 3. the EEGLAB export writes the reference's A ------------------------------ +_EXPORT_FILES = ( + "gm", + "W", + "S", + "mean", + "c", + "alpha", + "mu", + "sbeta", + "rho", + "comp_list", + "LL", + "LLt", + "A", +) + + +def _export(backend: str, model: Any, out: Path) -> Path: + """The EEGLAB directory of a fitted model (NumPy's fit already wrote it).""" + if backend != "numpy": + model.write_amica_output(out) + return out + + +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_the_exported_A_is_the_reference_layout(real_data, backend, tmp_path): + """Two models, a real merge included: the ``A`` file is the reference's + ``A(nw, num_comps)`` in column-major order, so column ``comp_list[i, h]`` + read that way is source ``i`` of model ``h``, and the bytes are exactly the + component-row ``A`` in C order. ``load_results`` reads the rows back.""" + from pamica.numpy_impl.data import load_results + + model = _share_fit(backend, _live(backend), real_data[:, :_SHARE_SAMPLES], tmp_path) + comp_list = _np(model.comp_list) + assert _groups(comp_list), "setup: no merge fired" + out = _export(backend, model, tmp_path) + raw = np.fromfile(out / "A", dtype=" Path: + if backend == "numpy": + cls( + num_models=1, + num_mix=NMIX, + max_iter=_BYTE_ID_ITERS, + seed=SEED, + use_tqdm=False, + outdir=str(out), + do_opt_block=False, + block_size=_BYTE_ID_SAMPLES, + writestep=10000, + ).fit(X) + return out + kwargs: Dict[str, Any] = dict( + n_channels=NW, n_mix=NMIX, seed=SEED, block_size=_BYTE_ID_SAMPLES + ) + if backend == "torch": + kwargs.update(device="cpu", dtype=torch.float64) + model = cls(**kwargs) + model.fit(X, max_iter=_BYTE_ID_ITERS, verbose=False) + model.write_amica_output(out) + return out + + +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_single_model_export_is_byte_identical_to_the_pre_change_code( + pre, real_data, backend, tmp_path +): + """One model's ``A`` is the same bytes in both layouts, so every file of a + single-model export, ``A`` included, is what the pre-change code wrote.""" + old_cls, new_cls = _classes(backend, pre) + X = real_data[:, :_BYTE_ID_SAMPLES] + old = _write_single_model(backend, old_cls, X, tmp_path / "old") + new = _write_single_model(backend, new_cls, X, tmp_path / "new") + for name in _EXPORT_FILES: + assert (new / name).read_bytes() == (old / name).read_bytes(), name + + +def test_load_results_refuses_a_pre_change_multi_model_A(pre, real_data, tmp_path): + """A two-model directory the pre-change code wrote holds its component- + column ``A`` in C order, which read as component rows does not invert the + ``W`` beside it: ``load_results`` refuses it with the remedy instead of + handing the viz helpers scrambled maps. The same directory's other files, + and a single-model directory, are unaffected.""" + from pamica.numpy_impl.data import load_results + + X = real_data[:, :_BYTE_ID_SAMPLES] + kwargs = dict(n_channels=NW, n_mix=NMIX, seed=SEED, device="cpu") + old_cls = pre.torch_impl.core.AMICATorchNG + two = old_cls(n_models=2, **kwargs) + two.fit(X, max_iter=_BYTE_ID_ITERS, verbose=False) + two.write_amica_output(tmp_path / "two") + with pytest.raises(ValueError, match="does not invert the unmixing W"): + load_results(tmp_path / "two") + + one = old_cls(n_models=1, **kwargs) + one.fit(X, max_iter=_BYTE_ID_ITERS, verbose=False) + one.write_amica_output(tmp_path / "one") + np.testing.assert_array_equal(load_results(tmp_path / "one")["A"], _np(one.A)) + + +@pytest.mark.parametrize("keep", ["one-value-short", "one-component-short"]) +def test_load_results_refuses_a_truncated_A(real_data, tmp_path, keep): + """A truncated ``A`` file (a copy of a real two-model export) is refused + by name, whether the lost bytes leave a length that no reshape accepts or + one short by exactly one component, which would otherwise reshape and fail + later as a matrix-product error.""" + import shutil + + from pamica.numpy_impl.data import load_results + + model = AMICATorchNG(n_channels=NW, n_models=2, n_mix=NMIX, seed=SEED, device="cpu") + model.fit(real_data[:, :_BYTE_ID_SAMPLES], max_iter=_BYTE_ID_ITERS, verbose=False) + model.write_amica_output(tmp_path / "full") + assert load_results(tmp_path / "full")["A"].shape == (2 * NW, NW) + + shutil.copytree(tmp_path / "full", tmp_path / "cut") + raw = (tmp_path / "full" / "A").read_bytes() + lost = 8 if keep == "one-value-short" else 8 * NW + (tmp_path / "cut" / "A").write_bytes(raw[:-lost]) + with pytest.raises(ValueError, match=r"holds \d+ values, expected 2048"): + load_results(tmp_path / "cut") + + +# --- 4. seeded native reference oracle from a MERGED state (opt-in) ------------- +# The reference's input.param optimizer, natural gradient only (the Newton start +# is issue #335), with the block size pinned on both sides. ``maxrho`` sits just +# below 2 so no mixture reaches the reference's exact-Gaussian branch +# (``rho == 2``, amica15.f90:1309-1313), whose normalizer is a single-precision +# literal, ``log(dble(1.772453851))``, 3.0e-8 away from ``log(sqrt(pi))``: once a +# warm fit clamps a mixture there, every sample's log-density differs from +# pamica's exact one by that much times its responsibility, which is unrelated +# to the mixing layout and would swamp the round-off this test measures. (The +# Laplace branch, ``rho == 1``, is exact in both.) Whether pamica adopts the +# single-precision value is issue #344. +_OPT: Dict[str, Any] = dict( + block_size=512, + lrate=0.05, + lratefact=0.5, + rholrate=0.05, + rholratefact=0.5, + rho0=1.5, + minrho=1.0, + maxrho=1.99, # below the reference's single-precision rho == 2 branch (#344) + invsigmin=0.0, + invsigmax=100.0, + do_newton=False, + newt_start=50, + newtrate=1.0, + newt_ramp=10, +) +_REF_OPT: Dict[str, Any] = { + **{k: v for k, v in _OPT.items() if k != "do_newton"}, + "do_newton": 0, + "do_opt_block": 0, + "do_reject": 0, + "share_comps": 0, + "scalestep": 1, +} +_ORACLE_WARM_ITERS = 100 +# Per iteration count: bounds at the no-merge noise floor with margin, the same +# as test_doscaling_rows.py's oracle. Measured maxima over both backends and +# both doscaling settings, from the merged state (the same state with no merge +# in brackets, PyTorch, doscaling on): +# 1 iteration: A 1.3e-15, mu 2.3e-11, sbeta 1.4e-14, LL 8.9e-16 +# [A 1.1e-15, mu 2.3e-11, sbeta 1.3e-14, LL 1.8e-15] +# 3 iterations: A 1.5e-12, mu 3.7e-9, sbeta 1.8e-11, LL 4.7e-14 +# [A 1.9e-12, mu 1.4e-8, sbeta 1.9e-10, LL 8.0e-15] +# The column semantics from the same merged state is off by A 0.204 and 0.21 and +# LL 3.5e-4 and 4.3e-4 (doscaling on and off) after 3 iterations. +_MERGED_ORACLE_TOL = { + 1: {"A": 1e-13, "mu": 1e-9, "sbeta": 1e-12, "LL": 1e-13}, + 3: {"A": 1e-10, "mu": 1e-6, "sbeta": 1e-8, "LL": 1e-11}, +} + + +@pytest.fixture(scope="module") +def merged_seed(real_data) -> Dict[str, Any]: + """A real two-model state with one merge applied to ``comp_list`` exactly as + the scan folds it (the pair whose sensor maps agree best), ``c`` zeroed for + the reference's ``load_comp_list`` path.""" + warm = AMICATorchNG( + n_channels=NW, + n_models=2, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + keep_best=False, + **_OPT, + ) + warm.fit(real_data, max_iter=_ORACLE_WARM_ITERS, verbose=False) + maps = [warm.get_sensor_mixing_matrix(h) for h in range(2)] + maps = [q / np.linalg.norm(q, axis=0) for q in maps] + cos = np.abs(maps[0].T @ maps[1]) + i, ii = np.unravel_index(int(np.argmax(cos)), cos.shape) + default = _np(warm.comp_list).copy() + merged = default.copy() + merged[merged == merged[ii, 1]] = merged[i, 0] + seed: Dict[str, Any] = { + k: _np(getattr(warm, k)).copy() + for k in ("A", "mu", "beta", "rho", "alpha", "gm", "mean", "sphere") + } + seed.update(default=default, merged=merged, cos=float(cos[i, ii])) + # No mixture sits at rho == 2, where the reference's normalizer is a + # single-precision literal (see _OPT). + assert seed["rho"].max() < 2.0 + return seed + + +def _seeded_run( + model: Any, backend: str, seed: Dict[str, Any], X: np.ndarray, k: int, rows: bool +) -> Tuple[np.ndarray, List[np.ndarray], np.ndarray, np.ndarray]: + """Run ``k`` production iterations of ``model`` from the merged seed, in + the reference's load order: ``get_unmixing_matrices`` runs on the DEFAULT + ``comp_list`` and ``load_comp_list`` replaces it afterwards + (amica15.f90:825-833), so the first E-step unmixes with the pre-merge + blocks while indexing densities by the merged ``comp_list``. ``rows`` is + False for a pre-change class, which gets the seed's ``A`` in its column + layout. Returns the per-iteration log-likelihood, each model's mixing + matrix (the reference's ``A(:, comp_list(:, h))``), ``mu`` and ``sbeta``.""" + A = seed["A"] + if not rows: + A = _column_state({"A": A, "comp_list": seed["default"]})["A"] + lls: List[float] = [] + if backend == "torch": + X_t = model._preprocess(X) + model._initialize_parameters() + model.A = torch.from_numpy(A.copy()) + for name in ("mu", "beta", "rho", "alpha", "gm"): + setattr(model, name, torch.from_numpy(seed[name].copy())) + model.c = torch.zeros(NW, 2, dtype=torch.float64) + model.comp_list = torch.from_numpy(seed["default"].copy()) + model._update_unmixing_matrices() + model.comp_list = torch.from_numpy(seed["merged"].copy()) + n = X_t.shape[1] + for it in range(k): + model.iteration = it + acc = model._accumulate_blocks(X_t) + lls.append(float(acc["ll"]) / (n * NW)) + model._update_parameters(acc, n) + else: + model.fit(X) # sizes and preprocesses; the state is replaced below + model.mean = seed["mean"].reshape(-1, 1).copy() + model.sphere = seed["sphere"].copy() + model.data = model.sphere @ (X - model.mean) + model._sphere_pinv = None + model.A = A.copy() + for name in ("mu", "beta", "rho", "alpha", "gm"): + setattr(model, name, seed[name].copy()) + model.c = np.zeros((NW, 2)) + model.lrate, model.rholrate = _OPT["lrate"], _OPT["rholrate"] + model.ll, model.nd = [], [] + model.comp_list = seed["default"].copy() + model._update_unmixing_matrices() + model.comp_list = seed["merged"].copy() + model.comp_used = np.isin(np.arange(2 * NW), model.comp_list) + for it in range(k): + model.iter = it + model._update_parameters(model._get_updates_and_likelihood()) + lls = list(model.ll) + mixing = [_mixing(model, h) for h in range(2)] + return np.asarray(lls), mixing, _np(model.mu), _np(model.beta) + + +def _mixing_error( + mixing: List[np.ndarray], A_ref: np.ndarray, comp_list: np.ndarray +) -> float: + """Largest difference between each model's mixing matrix and the + reference's ``A(:, comp_list(:, h))``.""" + return max( + float(np.abs(mixing[h] - A_ref[:, comp_list[:, h]]).max()) + for h in range(comp_list.shape[1]) + ) + + +@pytest.mark.skipif( + os.environ.get("AMICA_RUN_FORTRAN") != "1", + reason="opt-in Fortran-binary integration test (set AMICA_RUN_FORTRAN=1)", +) +@pytest.mark.parametrize("doscaling", [True, False], ids=["scale", "noscale"]) +def test_updates_from_a_merged_state_match_the_seeded_reference( + pre, merged_seed, real_data, doscaling, tmp_path +): + """The reference's own scan is unrunnable (its ``Spinv2`` is never + allocated), but its ``load_comp_list`` seeds a merged ``comp_list``, and + the E-step, the ``gm``-weighted ``dAk/zeta`` average over the models that + share a component, the ``comp_used`` mask and the M-step then run on it. + PyTorch and NumPy match that to float64 round-off after one and three + iterations; the pre-change column semantics does not.""" + from pamica.tests.native_oracle import SeedState, run_seeded_reference + + seed = merged_seed + used = np.unique(seed["merged"]) + state = SeedState( + A=seed["A"].T.copy(), + mean=seed["mean"].reshape(-1), + mu=seed["mu"], + sbeta=seed["beta"], + rho=seed["rho"], + alpha=seed["alpha"], + gm=seed["gm"], + c=np.zeros((NW, 2)), + comp_list=seed["merged"], + ) + opt = dict(_OPT, doscaling=doscaling) + + def torch_model(cls: Any) -> Any: + return cls( + n_channels=NW, + n_models=2, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + keep_best=False, + **opt, + ) + + for k, tol in _MERGED_ORACLE_TOL.items(): + ref = run_seeded_reference( + state, + DATA_FILE, + tmp_path / f"k{k}", + n_samples=FIELD, + max_iter=k, + num_models=2, + doscaling=int(doscaling), + **_REF_OPT, + ) + assert "reading comp_list" in ref.stdout + np.testing.assert_array_equal(ref.comp_list, seed["merged"]) + assert np.abs(ref.S - seed["sphere"]).max() < 1e-12 + + numpy_model = AMICA_NumPy( + num_models=2, + num_mix=NMIX, + max_iter=1, + seed=SEED, + use_tqdm=False, + outdir=str(tmp_path / f"np{k}"), + do_opt_block=False, + writestep=10**6, + **opt, + ) + runs = { + "torch": _seeded_run( + torch_model(AMICATorchNG), "torch", seed, real_data, k, rows=True + ), + "numpy": _seeded_run(numpy_model, "numpy", seed, real_data, k, rows=True), + } + for name, (ll, mixing, mu, sbeta) in runs.items(): + errs = { + "A": _mixing_error(mixing, ref.A, seed["merged"]), + "mu": np.abs(mu[:, used] - ref.mu[:, used]).max(), + "sbeta": np.abs(sbeta[:, used] - ref.sbeta[:, used]).max(), + "LL": np.abs(ll - ref.LL).max(), + } + print(f"merged oracle doscaling={doscaling} {name} k={k}: {errs}") + over = {q: e for q, e in errs.items() if not e <= tol[q]} + assert not over, f"{name}, {k} iteration(s): {over} (bounds {tol})" + + if k == 3: + old = torch_model(pre.torch_impl.core.AMICATorchNG) + ll_old, mixing, _, _ = _seeded_run(old, "torch", seed, real_data, k, False) + old_err = _mixing_error(mixing, ref.A, seed["merged"]) + print( + f"merged oracle doscaling={doscaling} column semantics k=3: " + f"A {old_err:.3g}, LL {np.abs(ll_old - ref.LL).max():.3g}" + ) + assert old_err > 1e-2 + + +# --- 5. early mass merges behave the same in the reference (opt-in) ------------- +# The recipe the sharing tests used before issue #334 made the metric compare +# true component maps: 4096 samples, pamica's default optimizer, an early scan +# at a loose threshold. The first scan (iteration 8) merges 28 components and the +# one at iteration 18 one more; the second model then loses its responsibility +# and the fit goes non-finite at iteration 20. _COLLAPSE_REF spells pamica's +# defaults out for the binary, with no further scans, no A-freeze and no +# convergence stops, so both sides run the same updates from the same state. +_COLLAPSE_SAMPLES = 4096 +_COLLAPSE: Dict[str, Any] = dict( + n_channels=NW, + n_models=2, + n_mix=NMIX, + seed=7, + device="cpu", + dtype=torch.float64, + block_size=1024, + share_comps=True, + share_start=8, + share_iter=10, + comp_thresh=0.9, +) +_COLLAPSE_REF: Dict[str, Any] = dict( + block_size=1024, + do_opt_block=0, + lrate=0.1, + lratefact=0.5, + minlrate=1e-12, + rholrate=0.05, + rholratefact=0.1, + rho0=1.5, + minrho=1.0, + maxrho=2.0, + invsigmin=1e-4, + invsigmax=1000.0, + do_newton=0, + newt_ramp=10, + do_reject=0, + share_comps=0, + share_start=10**6, + share_iter=100, + use_min_dll=0, + use_grad_norm=0, +) + + +def _with(base: Dict[str, Any], **overrides: Any) -> Dict[str, Any]: + """``base`` with ``overrides`` applied (a copy).""" + merged: Dict[str, Any] = dict(base) + merged.update(overrides) + return merged + + +def _post_scan(model: AMICATorchNG) -> Dict[str, Any]: + """A fitted model's state as a seed: the merged ``comp_list``, the default + one the reference unmixes with first, and ``c`` zeroed (the reference's + ``load_comp_list`` path reads the file ``c``, so it needs ``c == 0``).""" + seed = { + k: _np(getattr(model, k)).copy() + for k in ("A", "mu", "beta", "rho", "alpha", "gm", "mean") + } + seed["merged"] = _np(model.comp_list).copy() + seed["default"] = np.stack([np.arange(NW), NW + np.arange(NW)], axis=1) + return seed + + +def _reference_seed(seed: Dict[str, Any]) -> Any: + from pamica.tests.native_oracle import SeedState + + return SeedState( + A=seed["A"].T.copy(), + mean=seed["mean"].reshape(-1), + mu=seed["mu"], + sbeta=seed["beta"], + rho=seed["rho"], + alpha=seed["alpha"], + gm=seed["gm"], + c=np.zeros((NW, 2)), + comp_list=seed["merged"], + ) + + +@pytest.mark.skipif( + os.environ.get("AMICA_RUN_FORTRAN") != "1", + reason="opt-in Fortran-binary integration test (set AMICA_RUN_FORTRAN=1)", +) +def test_the_reference_scan_never_merges(real_data, tmp_path): + """The reference's similarity weights by ``Spinv2``, which is never + allocated: its scan runs, every similarity is NaN, and nothing merges even + at ``comp_thresh=0``, where any finite similarity would merge.""" + from pamica.tests.native_oracle import run_seeded_reference + + init = AMICATorchNG(**_with(_COLLAPSE, share_comps=False)) + init._preprocess(real_data[:, :_COLLAPSE_SAMPLES]) + init._initialize_parameters() + seed = _post_scan(init) + ref = run_seeded_reference( + _reference_seed(seed), + DATA_FILE, + tmp_path, + n_samples=_COLLAPSE_SAMPLES, + max_iter=3, + num_models=2, + **_with(_COLLAPSE_REF, share_comps=1, share_start=2, comp_thresh=0.0), + ) + assert "Number of unique components" in ref.stdout, "the scan did not run" + np.testing.assert_array_equal(ref.comp_list, seed["default"]) + assert "Identifying component" not in ref.stdout + + +@pytest.mark.skipif( + os.environ.get("AMICA_RUN_FORTRAN") != "1", + reason="opt-in Fortran-binary integration test (set AMICA_RUN_FORTRAN=1)", +) +def test_an_early_mass_merge_collapses_the_reference_too(real_data, tmp_path): + """From pamica's state right after each early scan, the reference's update + loses the second model's responsibility exactly as pamica's does: after the + first scan both drop its ``gm`` from above 0.3 to below 0.01 within two + iterations, and after the second both drive it to zero in one iteration and + go non-finite in the next (the binary reports NaN and reinitializes). So + the collapse is the algorithm on models that have not separated, not a + defect of the port.""" + from pamica.tests.native_oracle import run_seeded_reference + + X = real_data[:, :_COLLAPSE_SAMPLES] + + def continuation(seed: Dict[str, Any], k: int) -> Tuple[np.ndarray, float]: + """``k`` iterations from ``seed`` in the reference's load order (see + ``_seeded_run``, whose accessors refuse the non-finite state this + reaches), with no scan and no A-freeze, like ``_COLLAPSE_REF``.""" + model = AMICATorchNG(**_with(_COLLAPSE, share_comps=False)) + X_t = model._preprocess(X) + model._initialize_parameters() + for name in ("A", "mu", "beta", "rho", "alpha", "gm"): + setattr(model, name, torch.from_numpy(seed[name].copy())) + model.c = torch.zeros(NW, 2, dtype=torch.float64) + model.comp_list = torch.from_numpy(seed["default"].copy()) + model._update_unmixing_matrices() + model.comp_list = torch.from_numpy(seed["merged"].copy()) + lls = [] + for it in range(k): + model.iteration = it + acc = model._accumulate_blocks(X_t) + lls.append(float(acc["ll"]) / (X_t.shape[1] * NW)) + model._update_parameters(acc, X_t.shape[1]) + return np.asarray(lls), float(_np(model.gm)[1]) + + first = AMICATorchNG(**_COLLAPSE) + first.fit(X, max_iter=8, verbose=False) # the scan runs on the last iteration + seed = _post_scan(first) + merges = 2 * NW - len(np.unique(seed["merged"])) + assert merges >= 20, f"setup: the first scan merged only {merges}" + assert seed["gm"][1] > 0.3, "setup: the second model had already collapsed" + ref = run_seeded_reference( + _reference_seed(seed), + DATA_FILE, + tmp_path / "first", + n_samples=_COLLAPSE_SAMPLES, + max_iter=2, + num_models=2, + **_COLLAPSE_REF, + ) + lls, gm2 = continuation(seed, 2) + print(f"after the first scan: gm2 reference {ref.gm[1]:.3e}, torch {gm2:.3e}") + assert np.isfinite(lls).all() and np.isfinite(ref.LL).all() + assert gm2 < 0.01 and ref.gm[1] < 0.01 + np.testing.assert_allclose(gm2, ref.gm[1], rtol=1e-2) + + second = AMICATorchNG(**_COLLAPSE) + second.fit(X, max_iter=18, verbose=False) # the second scan runs last + seed = _post_scan(second) + ref = run_seeded_reference( + _reference_seed(seed), + DATA_FILE, + tmp_path / "second1", + n_samples=_COLLAPSE_SAMPLES, + max_iter=1, + num_models=2, + **_COLLAPSE_REF, + ) + lls, gm2 = continuation(seed, 1) + assert ref.gm[1] == 0.0 and gm2 == 0.0 + with pytest.raises(RuntimeError, match="Reinitiali"): + run_seeded_reference( + _reference_seed(seed), + DATA_FILE, + tmp_path / "second2", + n_samples=_COLLAPSE_SAMPLES, + max_iter=2, + num_models=2, + **_COLLAPSE_REF, + ) + lls, _ = continuation(seed, 2) + assert np.isfinite(lls[0]) and not np.isfinite(lls[1]) diff --git a/pamica/tests/test_component_rows_persistence.py b/pamica/tests/test_component_rows_persistence.py new file mode 100644 index 00000000..14919c02 --- /dev/null +++ b/pamica/tests/test_component_rows_persistence.py @@ -0,0 +1,310 @@ +"""Persistence of the component-row ``A`` (issue #334, epic #324 Phase 8, ADR 0007). + +The PyTorch ``state_dict`` is ``format_version`` 4 and the MLX save format 2: +both store ``A`` as ``(n_comps, n)``, one component per row. A save from before +the change (PyTorch 3, MLX 1: ``A`` as ``(n, n_comps)`` with ``comp_list`` +indexing stored columns) is converted without loss when unmerged, and refused +with a message to refit when ``share_comps`` had merged components, because +that merge compared and tied stored columns. The ``AMICA`` wrapper's own save +(``format_version`` 2) wraps the backend payloads, so it converts and refuses +the same way. + +Pinned here, per backend and through the wrapper: new saves round-trip bit for +bit (single model, unmerged and merged multi-model); old saves, written by the +pre-change classes loaded from git (never hand-built), load into models whose +``transform`` and accessors reproduce the saved model exactly; an old merged +save is refused. Real bundled sample EEG only, no synthetic data or mocks. +""" + +from __future__ import annotations + +import importlib +from pathlib import Path +from typing import Any, Dict + +import numpy as np +import pytest +import torch + +from pamica import AMICA +from pamica.tests.pre_change import load_pre_change_package +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +NMIX = 3 +SEED = 42 +N_SAMPLES = 4096 +ITERS = 10 +# The epic #324 head Phase 8 merges onto: the last component-column layout. +PRE_CHANGE_COMMIT = "0930c0e68ec9e2bbef3d20d51cff4c03029248f2" + +# Fit recipes. "merged" merges a few pairs at iteration 8 under either layout's +# metric (both models are still near-collinear there); the refusal test checks +# that the pre-change fit really merged. +_RECIPES: Dict[str, Dict[str, Any]] = { + "single": dict(n_models=1), + "unmerged": dict(n_models=2), + "merged": dict( + n_models=2, + share_comps=True, + share_start=8, + share_iter=100, + comp_thresh=0.99, + ), +} +_REFIT = "Refit it with this version of pamica" + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +@pytest.fixture(scope="module") +def real_data() -> np.ndarray: + X = load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD) + return X.astype(np.float64)[:, :N_SAMPLES] + + +@pytest.fixture(scope="module") +def pre(tmp_path_factory) -> Any: + return load_pre_change_package( + PRE_CHANGE_COMMIT, "pamica_pre334", tmp_path_factory.mktemp("pre334") + ) + + +def _mlx_core(): + """The MLX backend module, or skip this test (never the whole module).""" + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + mx = mlx_core.mx + if mx.default_device().type != mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + return mlx_core + + +def _backend_fit(cls: Any, backend: str, recipe: str, X: np.ndarray) -> Any: + kwargs: Dict[str, Any] = dict( + n_channels=NW, n_mix=NMIX, seed=SEED, block_size=N_SAMPLES, **_RECIPES[recipe] + ) + if backend == "torch": + kwargs.update(device="cpu", dtype=torch.float64) + model = cls(**kwargs) + model.fit(X, max_iter=ITERS, verbose=False) + return model + + +def _wrapper_fit(cls: Any, backend: str, recipe: str, X: np.ndarray) -> Any: + """A wrapper fit; an ``"mlx"`` fit skips the calling test without MLX or an + Apple GPU, so no wrapper route can reach the backend's ImportError.""" + if backend == "mlx": + _mlx_core() + recipe_kwargs = dict(_RECIPES[recipe]) + n_models = recipe_kwargs.pop("n_models") + device = "cpu" if backend == "torch" else None + model = cls( + n_models=n_models, n_mix=NMIX, device=device, verbose=False, backend=backend + ) + model.fit(X, max_iter=ITERS, seed=SEED, block_size=N_SAMPLES, **recipe_kwargs) + return model + + +def _outputs(model: Any, X: np.ndarray, n_models: int) -> Dict[str, np.ndarray]: + """What a caller reads off a fitted model, per model: sources and the + three mixing/unmixing accessors, plus the sharing groups.""" + out: Dict[str, Any] = {} + for h in range(n_models): + out[f"transform{h}"] = np.asarray(model.transform(X, model_idx=h)) + out[f"mixing{h}"] = np.asarray(model.get_mixing_matrix(h)) + out[f"sensor{h}"] = np.asarray(model.get_sensor_mixing_matrix(h)) + out[f"unmixing{h}"] = np.asarray(model.get_unmixing_matrix(h)) + out["shared"] = model.shared_components() + return out + + +def _assert_same_outputs(got: Dict[str, Any], want: Dict[str, Any]) -> None: + assert got.keys() == want.keys() + for key in want: + if key == "shared": + assert got[key] == want[key] + else: + np.testing.assert_array_equal(got[key], want[key], err_msg=key) + + +def _merged(model: Any) -> bool: + cl = model.comp_list + cl = cl.cpu().numpy() if isinstance(cl, torch.Tensor) else np.array(cl) + return np.unique(cl).size < cl.size + + +# --- new-format round trips -------------------------------------------------- +@pytest.mark.parametrize("recipe", list(_RECIPES)) +def test_torch_state_dict_round_trip(real_data, recipe, tmp_path): + m = _backend_fit(AMICATorchNG, "torch", recipe, real_data) + assert _merged(m) == (recipe == "merged"), "setup: merge state" + state = m.state_dict() + assert state["format_version"] == 4 + assert tuple(state["params"]["A"].shape) == (m.n_comps, m.n_channels) + torch.save(state, tmp_path / "state.pt") + loaded = AMICATorchNG.from_state_dict( + torch.load(tmp_path / "state.pt", weights_only=True), device="cpu" + ) + assert loaded.A is not None and m.A is not None + assert torch.equal(loaded.A, m.A) + n_models = _RECIPES[recipe]["n_models"] + _assert_same_outputs( + _outputs(loaded, real_data, n_models), _outputs(m, real_data, n_models) + ) + + +@pytest.mark.parametrize("recipe", list(_RECIPES)) +def test_mlx_save_round_trip(real_data, recipe, tmp_path): + cls = _mlx_core().AMICAMLXNG + m = _backend_fit(cls, "mlx", recipe, real_data) + assert _merged(m) == (recipe == "merged"), "setup: merge state" + state = m.state_dict() + assert state["format_version"] == 2 + assert state["params"]["A"].shape == (m.n_comps, m.n_channels) + m.save(str(tmp_path / "model.npz")) + n_models = _RECIPES[recipe]["n_models"] + via_file = cls.load(str(tmp_path / "model.npz")) + via_dict = cls.from_state_dict(state) + want = _outputs(via_dict, real_data, n_models) + _assert_same_outputs(_outputs(via_file, real_data, n_models), want) + np.testing.assert_array_equal(np.array(via_file.A), np.array(m.A)) + # transform and the sphered-space accessors read persisted arrays only, so + # they match the in-memory model too (the sensor maps use the float32 + # sphere after a reload; see AMICAMLXNG._load_params). + for h in range(n_models): + np.testing.assert_array_equal( + want[f"transform{h}"], np.asarray(m.transform(real_data, model_idx=h)) + ) + np.testing.assert_array_equal(want[f"mixing{h}"], m.get_mixing_matrix(h)) + + +@pytest.mark.parametrize("recipe", list(_RECIPES)) +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_wrapper_save_round_trip(real_data, backend, recipe, tmp_path): + m = _wrapper_fit(AMICA, backend, recipe, real_data) + assert _merged(m.model_) == (recipe == "merged"), "setup: merge state" + path = str(tmp_path / "model.pt") + m.save(path) + loaded = AMICA.load(path, device="cpu" if backend == "torch" else None) + n_models = _RECIPES[recipe]["n_models"] + got = _outputs(loaded, real_data, n_models) + if backend == "torch": + _assert_same_outputs(got, _outputs(m, real_data, n_models)) + else: + # The MLX sensor maps use the float32 sphere after a reload, so compare + # against a second reload rather than the in-memory model. + _assert_same_outputs(got, _outputs(AMICA.load(path), real_data, n_models)) + + +# --- old saves: converted when unmerged, refused when merged ------------------ +def _old_class(pre: Any, backend: str) -> Any: + if backend == "torch": + return pre.torch_impl.core.AMICATorchNG + _mlx_core() + return importlib.import_module(f"{pre.__name__}.mlx_impl.core").AMICAMLXNG + + +@pytest.mark.parametrize("recipe", ["single", "unmerged"]) +def test_old_torch_state_dict_converts_losslessly(pre, real_data, recipe, tmp_path): + """A format_version 3 state (components as columns) loads into a model that + reproduces the saved model's sources and accessors exactly.""" + old = _backend_fit(_old_class(pre, "torch"), "torch", recipe, real_data) + state = old.state_dict() + assert state["format_version"] == 3 + torch.save(state, tmp_path / "old.pt") + loaded = AMICATorchNG.from_state_dict( + torch.load(tmp_path / "old.pt", weights_only=True), device="cpu" + ) + assert loaded.A is not None + assert tuple(loaded.A.shape) == (loaded.n_comps, loaded.n_channels) + n_models = _RECIPES[recipe]["n_models"] + _assert_same_outputs( + _outputs(loaded, real_data, n_models), _outputs(old, real_data, n_models) + ) + # And it saves again in the current format, round-tripping unchanged. + again = AMICATorchNG.from_state_dict(loaded.state_dict(), device="cpu") + assert loaded.state_dict()["format_version"] == 4 + assert again.A is not None + assert torch.equal(again.A, loaded.A) + + +@pytest.mark.parametrize("recipe", ["single", "unmerged"]) +def test_old_mlx_save_converts_losslessly(pre, real_data, recipe, tmp_path): + """A format_version 1 ``.npz`` (components as columns) loads into a model + that reproduces the saved model exactly: compared with the pre-change class + loading the same file, so both read the same float32 sphere.""" + old_cls = _old_class(pre, "mlx") + new_cls = _mlx_core().AMICAMLXNG + old = _backend_fit(old_cls, "mlx", recipe, real_data) + path = str(tmp_path / "old.npz") + old.save(path) + with np.load(path) as raw: + assert int(raw["format_version"]) == 1 + n_models = _RECIPES[recipe]["n_models"] + want = _outputs(old_cls.load(path), real_data, n_models) + loaded = new_cls.load(path) + _assert_same_outputs(_outputs(loaded, real_data, n_models), want) + converted = new_cls.from_state_dict(old.state_dict()) + _assert_same_outputs(_outputs(converted, real_data, n_models), want) + # And it saves again in the current format, and reloads unchanged: a + # second load must not convert the already-converted A again. + again_path = str(tmp_path / "again.npz") + loaded.save(again_path) + with np.load(again_path) as raw: + assert int(raw["format_version"]) == 2 + assert raw["A"].shape == (loaded.n_comps, loaded.n_channels) + again = new_cls.load(again_path) + np.testing.assert_array_equal(np.array(again.A), np.array(loaded.A)) + _assert_same_outputs(_outputs(again, real_data, n_models), want) + + +@pytest.mark.parametrize("recipe", ["single", "unmerged"]) +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_old_wrapper_save_converts_losslessly( + pre, real_data, backend, recipe, tmp_path +): + """The wrapper's own save wraps the backend payload, so an old wrapper file + goes through the same conversion.""" + old = _wrapper_fit(pre.AMICA, backend, recipe, real_data) + path = str(tmp_path / "old.pt") + old.save(path) + device = "cpu" if backend == "torch" else None + n_models = _RECIPES[recipe]["n_models"] + want = _outputs(pre.AMICA.load(path, device=device), real_data, n_models) + loaded = AMICA.load(path, device=device) + _assert_same_outputs(_outputs(loaded, real_data, n_models), want) + + +@pytest.mark.parametrize("route", ["state_dict", "wrapper"]) +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_old_merged_save_is_refused(pre, real_data, backend, route, tmp_path): + """A pre-change save whose ``share_comps`` merged components tied stored + columns across models; it has no component-row equivalent, so every load + route refuses it and says to refit.""" + if route == "wrapper": + old = _wrapper_fit(pre.AMICA, backend, "merged", real_data) + assert _merged(old.model_), "setup: the pre-change fit merged nothing" + path = str(tmp_path / "old.pt") + old.save(path) + with pytest.raises(ValueError, match=_REFIT): + AMICA.load(path, device="cpu" if backend == "torch" else None) + return + old = _backend_fit(_old_class(pre, backend), backend, "merged", real_data) + assert _merged(old), "setup: the pre-change fit merged nothing" + if backend == "torch": + with pytest.raises(ValueError, match=_REFIT): + AMICATorchNG.from_state_dict(old.state_dict(), device="cpu") + return + new_cls = _mlx_core().AMICAMLXNG + with pytest.raises(ValueError, match=_REFIT): + new_cls.from_state_dict(old.state_dict()) + path = str(tmp_path / "old.npz") + old.save(path) + with pytest.raises(ValueError, match=_REFIT): + new_cls.load(path) diff --git a/pamica/tests/test_doscaling_rows.py b/pamica/tests/test_doscaling_rows.py index b1f39b37..fc9526c0 100644 --- a/pamica/tests/test_doscaling_rows.py +++ b/pamica/tests/test_doscaling_rows.py @@ -2,8 +2,9 @@ Phase 7). pamica stores each model's mixing block transposed relative to the reference -(issue #24 convention, ADR 0006), so source ``i`` of model ``h`` is ROW ``i`` of -``A[:, comp_list[:, h]]``, with its density at ``mu[:, comp_list[i, h]]``. The +(issue #24 convention, ADR 0006), so source ``i`` of model ``h`` is a ROW of its +block, with its density at ``mu[:, comp_list[i, h]]``; since issue #334 (ADR +0007) ``A`` holds one component per row, ``A[comp_list[i, h], :]``. The reference's ``doscaling`` (amica15.f90:1843-1851) divides each component's mixing vector by its norm and multiplies/divides that component's ``mu``/``sbeta`` by it: an exact change of scale that leaves the log-likelihood unchanged. Before @@ -20,11 +21,12 @@ at unit norm, while the old column rule on the same state moves the log-likelihood by far more (so these tests catch the bug); 2. the three backends rescale one shared state identically, including a - permuted ``comp_list`` and a merged one, where the per-block rule is applied - uniformly until the component-row layout of Phase 8 (issue #334) replaces - it; a zero- or NaN-norm row is left untouched, as in the reference; -3. ``doscaling=False`` is byte-identical to the pre-fix code: each backend class - at commit ``fb13d76`` is loaded from git and fitted in the same process; + permuted ``comp_list`` and a merged one, where the shared component row is + rescaled once (issue #334); a zero- or NaN-norm row is left untouched, as in + the reference; +3. ``doscaling=False`` is byte-identical to the pre-fix code: the package at + commit ``fb13d76`` is loaded from git and each backend fitted in the same + process, its ``A`` mapped onto the component rows issue #334 introduced; 4. ``scalestep``, a pamica extension the reference parses but never reads, counts iterations from 1, so its default of 1 is the reference's every-iteration rescale, and every constructor rejects a ``scalestep`` that @@ -39,11 +41,9 @@ from __future__ import annotations import dataclasses +import importlib import os -import subprocess -import sys import tempfile -import types from pathlib import Path from typing import Any, Dict, Tuple @@ -53,6 +53,8 @@ from scipy.special import logsumexp from pamica import AMICA_NumPy +from pamica.component_layout import rows_from_legacy_columns +from pamica.tests.pre_change import load_pre_change_package from pamica.torch_impl.core import AMICATorchNG from pamica.torch_impl.utils import load_eeglab_data @@ -107,11 +109,27 @@ def _mlx_core(): def _row_norm_dev(A: np.ndarray, comp_list: np.ndarray) -> float: - """Largest ``| ||row i of block h|| - 1 |`` over every component.""" - norms = [ - np.linalg.norm(A[:, comp_list[:, h]], axis=1) for h in range(comp_list.shape[1]) - ] - return float(np.abs(np.concatenate(norms) - 1.0).max()) + """Largest ``| ||A[k, :]|| - 1 |`` over every component ``k`` a model uses + (one component per row, issue #334).""" + norms = np.linalg.norm(A[np.unique(comp_list), :], axis=1) + return float(np.abs(norms - 1.0).max()) + + +def _column_rule( + A: np.ndarray, mu: np.ndarray, beta: np.ndarray, comp_list: np.ndarray +) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """The pre-#333 rule: normalize every stored column of the pre-#334 + component-column layout, scaling ``mu``/``beta`` of the component id that + indexes the column to match -- a compensated rescale of vectors that are not + components. Takes and returns component-row arrays (an unmerged + ``comp_list``); the column layout is rebuilt block by block and converted + back with the persistence conversion.""" + A_cols = np.empty((A.shape[1], A.shape[0]), dtype=A.dtype) + for h in range(comp_list.shape[1]): + A_cols[:, comp_list[:, h]] = A[comp_list[:, h], :] + scale = np.sqrt(np.sum(A_cols**2, axis=0)) + A_rows = rows_from_legacy_columns(A_cols / scale, comp_list, owner="test") + return A_rows, mu * scale, beta / scale def _mean_ll(lht: np.ndarray, n_channels: int) -> float: @@ -156,10 +174,9 @@ def test_torch_rescale_is_an_exact_change_of_scale(real_slice, n_models, newton) assert _row_norm_dev(m.A.numpy(), comp_list) <= 1e-14 # The pre-#333 rule (normalize stored columns) on the same state. - for k, v in saved.items(): - setattr(m, k, v.clone()) - scale = torch.sqrt((m.A**2).sum(dim=0)) - m.A, m.mu, m.beta = m.A / scale, m.mu * scale, m.beta / scale + A0, mu0, beta0 = (saved[k].numpy() for k in ("A", "mu", "beta")) + cols = _column_rule(A0, mu0, beta0, comp_list) + m.A, m.mu, m.beta = (torch.from_numpy(x) for x in cols) m._update_unmixing_matrices() assert abs(_mean_ll(m.model_loglik(real_slice), NW) - ll0) > COLUMN_RULE_MIN_DLL @@ -197,8 +214,7 @@ def test_numpy_rescale_is_an_exact_change_of_scale( assert m.A is not None assert _row_norm_dev(m.A, comp_list) <= 1e-14 - scale = np.sqrt(np.sum(A0**2, axis=0)) - m.A, m.mu, m.beta = A0 / scale, mu0 * scale, beta0 / scale + m.A, m.mu, m.beta = _column_rule(A0, mu0, beta0, comp_list) m._update_unmixing_matrices() assert abs(m._get_updates_and_likelihood()["ll"] - ll0) > COLUMN_RULE_MIN_DLL @@ -230,10 +246,9 @@ def test_mlx_rescale_is_an_exact_change_of_scale(real_slice, n_models, newton): assert _row_norm_dev(np.array(m.A, dtype=np.float64), comp_list) <= 1e-6 # MLX rebinds (never mutates) these arrays, so the saved ones are intact. - for k, v in saved.items(): - setattr(m, k, v) - scale = mx.sqrt((m.A**2).sum(axis=0)) - m.A, m.mu, m.beta = m.A / scale, m.mu * scale, m.beta / scale + A0, mu0, beta0 = (np.array(saved[k]) for k in ("A", "mu", "beta")) + cols = _column_rule(A0, mu0, beta0, comp_list) + m.A, m.mu, m.beta = (mx.array(x) for x in cols) m._update_unmixing_matrices() assert abs(_mean_ll(m.model_loglik(real_slice), NW) - ll0) > COLUMN_RULE_MIN_DLL @@ -272,14 +287,13 @@ def unscaled_state(real_slice) -> State: params=["disjoint", "permuted", "merged"], ) def two_model_state(request, unscaled_state) -> State: - """``permuted`` shuffles each model's ``comp_list`` column, so the stored - columns of a block are neither contiguous nor in order (a general index - pattern for every backend's scatter). ``merged`` plants a share merge - (model 1's source 3 folded onto model 0's source 5 column, the way - ``identify_shared_comps`` folds a pair): the shared column then sits in - rows of both blocks, the one configuration where the per-block rule is not - a change of scale. Every backend must still apply it the same way (models - in order) until Phase 8 (issue #334) replaces it. + """``permuted`` shuffles each model's ``comp_list`` column, so the rows of + a block are neither contiguous nor in order (a general index pattern for + every backend's gather and scale). ``merged`` plants a share merge (model + 1's source 3 folded onto model 0's source 5 component, the way + ``identify_shared_comps`` folds a pair): the shared component row then sits + in both blocks and must be rescaled exactly once, and the merged-away row + not at all (issue #334). """ A, mu, beta, comp_list = unscaled_state comp_list = comp_list.copy() @@ -352,7 +366,7 @@ def test_a_zero_or_nan_norm_row_is_left_untouched(unscaled_state, backend, bad): h, i = 1, 7 # source 7 of model 1: a block other than the first k = comp_list[i, h] planted = A.copy() - planted[i, comp_list[:, h]] = bad + planted[k, :] = bad # component k is row k (issue #334) dtype = np.float32 if backend == "mlx" else np.float64 before = tuple(np.asarray(x, dtype) for x in (planted, mu, beta)) @@ -360,7 +374,7 @@ def test_a_zero_or_nan_norm_row_is_left_untouched(unscaled_state, backend, bad): clean_A, clean_mu, clean_beta, _ = _rescaled(backend, unscaled_state) rows = np.zeros(A.shape, dtype=bool) - rows[i, comp_list[:, h]] = True + rows[k, :] = True assert got_A[rows].tobytes() == before[0][rows].tobytes() assert got_mu[:, k].tobytes() == before[1][:, k].tobytes() assert got_beta[:, k].tobytes() == before[2][:, k].tobytes() @@ -371,10 +385,9 @@ def test_a_zero_or_nan_norm_row_is_left_untouched(unscaled_state, backend, bad): assert got_beta[:, others].tobytes() == clean_beta[:, others].tobytes() # ...and those rows are unit norm, so the rescale really ran on them. unit = 1e-6 if backend == "mlx" else 1e-14 - for hh in range(comp_list.shape[1]): - norms = np.linalg.norm(got_A[:, comp_list[:, hh]].astype(np.float64), axis=1) - keep = np.arange(NW) != i if hh == h else np.ones(NW, dtype=bool) - assert np.abs(norms[keep] - 1.0).max() <= unit + norms = np.linalg.norm(got_A.astype(np.float64), axis=1) + others_rows = np.arange(A.shape[0]) != k + assert np.abs(norms[others_rows] - 1.0).max() <= unit # --- 3. doscaling=False is byte-identical to the pre-fix code --------------- @@ -382,68 +395,32 @@ def test_a_zero_or_nan_norm_row_is_left_untouched(unscaled_state, backend, bad): # column rule. A later phase that deliberately changes the doscaling=False # trajectory moves this pin to its own base commit. _PRE_FIX_COMMIT = "fb13d76de145419da9c89db94438d60aafbff444" -_BACKEND_MODULES = { - "torch": ("pamica/torch_impl/core.py", "pamica.torch_impl", "AMICATorchNG"), - "numpy": ("pamica/numpy_impl/core.py", "pamica.numpy_impl", "AMICA"), - "mlx": ("pamica/mlx_impl/core.py", "pamica.mlx_impl", "AMICAMLXNG"), -} -def _pre_fix_class(backend: str) -> Any: - """The backend class as it was at ``_PRE_FIX_COMMIT``, read with ``git - show`` and executed as a sibling module of the live package, so its - relative imports resolve against the live helper modules (this phase - changes none of them). The same construction as - ``mlx_tests/test_mlx_fit_noop.py``: both classes then run in one process on - one machine, so the comparison is exact without recorded constants.""" - path, package, cls_name = _BACKEND_MODULES[backend] - repo_root = Path(__file__).resolve().parents[2] - # A shallow clone may lack the commit: fetch just that object first (best - # effort, e.g. no network), and let `git show` be the real check. - subprocess.run( - ["git", "fetch", "origin", _PRE_FIX_COMMIT, "--depth", "1"], - cwd=repo_root, - capture_output=True, - text=True, - ) - result = subprocess.run( - ["git", "show", f"{_PRE_FIX_COMMIT}:{path}"], - cwd=repo_root, - capture_output=True, - text=True, +@pytest.fixture(scope="module") +def pre_fix(tmp_path_factory) -> Any: + """The pamica package at ``_PRE_FIX_COMMIT``, imported beside the live one + (``pamica/tests/pre_change.py``): both then fit in one process on one + machine, so the comparison is exact without recorded constants.""" + return load_pre_change_package( + _PRE_FIX_COMMIT, "pamica_pre333", tmp_path_factory.mktemp("pre333") ) - if result.returncode != 0: - reason = ( - f"git object {_PRE_FIX_COMMIT[:7]} is not reachable in this checkout; " - f"git show stderr: {result.stderr.strip()!r}" - ) - # A skip in CI would silently drop this regression guard, and the CI - # jobs check out full history (fetch-depth: 0), so there it fails. - # Only a local shallow clone skips. - if os.environ.get("CI"): - pytest.fail(reason) - pytest.skip(reason) - name = f"{package}._pre_issue_333_core" - module = types.ModuleType(name) - module.__package__ = package - # The live path, so a data file read next to the module (NumPy's - # params.json, unchanged by this phase) resolves; the code object names - # the commit it came from. - module.__file__ = str(repo_root / path) - sys.modules[name] = module # dataclasses look the module up while building - try: - code = compile(result.stdout, f"", "exec") - exec(code, module.__dict__) - finally: - del sys.modules[name] - return getattr(module, cls_name) + + +def _pre_fix_class(pre_fix: Any, backend: str) -> Any: + if backend == "torch": + return pre_fix.torch_impl.core.AMICATorchNG + if backend == "numpy": + return pre_fix.numpy_impl.core.AMICA + return importlib.import_module(f"{pre_fix.__name__}.mlx_impl.core").AMICAMLXNG def _fit_unscaled( cls: Any, backend: str, n_models: int, X: np.ndarray, outdir: Path ) -> Dict[str, np.ndarray]: """A short ``doscaling=False`` fit of ``cls``; its trajectory and every - fitted array, as float64/int numpy arrays.""" + fitted array, as float64/int numpy arrays, ``A`` as component rows (a + pre-#334 class's is converted the way a pre-#334 save is).""" if backend == "numpy": model = cls( num_models=n_models, @@ -472,13 +449,15 @@ def _fit_unscaled( for k in ("A", "W", "mu", "beta", "alpha", "rho", "gm", "c", "comp_list"): v = getattr(model, k) out[k] = v.cpu().numpy() if isinstance(v, torch.Tensor) else np.array(v) + if out["A"].shape[0] != out["comp_list"].size: + out["A"] = rows_from_legacy_columns(out["A"], out["comp_list"], owner="test") return out @pytest.mark.parametrize("n_models", [1, 2]) @pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) def test_doscaling_off_is_byte_identical_to_the_pre_fix_code( - real_slice, backend, n_models, tmp_path + pre_fix, real_slice, backend, n_models, tmp_path ): """``doscaling=False`` never enters the rescale, so this fix must leave its trajectory and every fitted array bit for bit where the pre-fix code put @@ -488,7 +467,7 @@ def test_doscaling_off_is_byte_identical_to_the_pre_fix_code( new_cls = _mlx_core().AMICAMLXNG else: new_cls = AMICATorchNG if backend == "torch" else AMICA_NumPy - old_cls = _pre_fix_class(backend) + old_cls = _pre_fix_class(pre_fix, backend) old = _fit_unscaled(old_cls, backend, n_models, real_slice, tmp_path / "old") new = _fit_unscaled(new_cls, backend, n_models, real_slice, tmp_path / "new") @@ -712,7 +691,7 @@ def torch_model() -> AMICATorchNG: } for name, (A, mu, beta, ll) in fits.items(): errs = { - "A": np.abs(reference_mixing(A, comp_list) - ref.A).max(), + "A": np.abs(reference_mixing(A) - ref.A).max(), "mu": np.abs(mu - ref.mu).max(), "sbeta": np.abs(beta - ref.sbeta).max(), "LL": np.abs(np.asarray(ll) - ref.LL).max(), diff --git a/pamica/tests/test_mlx_newton_cross_backend.py b/pamica/tests/test_mlx_newton_cross_backend.py index 8d549dc2..dd9255fa 100644 --- a/pamica/tests/test_mlx_newton_cross_backend.py +++ b/pamica/tests/test_mlx_newton_cross_backend.py @@ -275,7 +275,7 @@ def _force_merged_column(model): cl = np.array(model.comp_list) kept, dead = int(cl[0, 0]), int(cl[0, 1]) a_np = np.array(model.A) - a_np[:, dead] = a_np[:, kept] + a_np[dead, :] = a_np[kept, :] # the retired component row (issue #334) model.A = mx.array(a_np) cl[cl == dead] = kept model.comp_list = mx.array(cl) @@ -288,15 +288,15 @@ def _force_merged_column(model): @pytest.mark.parametrize("merged", [False, True]) def test_multimodel_newton_mstep_matches_float64_twin(merged): - """Two models, with and without a shared mixing column, still match the + """Two models, with and without a shared component, still match the float64 twin through a full Newton M-step. Multi-model is where the curvature reduction's broadcast axis becomes observable (the NumPy backend's issue #267 crash), and ``merged=True`` adds - the ``share_comps`` interaction: one column carries both models' Newton - directions through the ``gm``-weighted ``dAk`` average, and one column is + the ``share_comps`` interaction: one component row carries both models' + Newton steps through the ``gm``-weighted ``dAk`` average, and one row is frozen. The curvature itself is indexed by (model, SOURCE) rather than by - mixing column, so a merge must NOT create a 0/0 there -- this is what pins + component, so a merge must NOT create a 0/0 there -- this is what pins that. """ model, x_t = _warm_model(5, n_models=2) diff --git a/pamica/tests/test_mlx_sharing_cross_backend.py b/pamica/tests/test_mlx_sharing_cross_backend.py index 71dfc70a..f4f59daa 100644 --- a/pamica/tests/test_mlx_sharing_cross_backend.py +++ b/pamica/tests/test_mlx_sharing_cross_backend.py @@ -13,7 +13,8 @@ 1. the merge DECISION (MLX calls the same ``pamica.numpy_impl.utils.identify_shared_components`` kernel the NumPy - backend uses, on the same host float64 ``pinv(sphere) @ A``); and + backend uses, on the same host float64 sensor maps ``pinv(sphere) @ A.T``, + one column per component, issue #334); and 2. the A-update's ``gm`` weighting, which Fortran takes from the PREVIOUS iteration (``dAk`` is accumulated at amica15.f90:1749-1761, before ``update_params`` reassigns ``gm`` at :1788). Issue #219 raised that ordering @@ -95,7 +96,7 @@ def _force_merged_column(model): cl = np.array(model.comp_list) kept, dead = int(cl[0, 0]), int(cl[0, 1]) a_np = np.array(model.A) - a_np[:, dead] = a_np[:, kept] + a_np[dead, :] = a_np[kept, :] # the retired component row (issue #334) model.A = mx.array(a_np) cl[cl == dead] = kept model.comp_list = mx.array(cl) @@ -147,8 +148,8 @@ def test_merge_decision_matches_the_torch_backend(): ``AMICATorchNG`` reach the identical merge decision. MLX does not re-derive the scan -- it calls the same NumPy kernel the NumPy - backend uses (issue #258) on host float64 ``pinv(sphere) @ A``, which is the - metric ``AMICATorchNG._identify_shared_comps`` computes inline. Driving both + backend uses (issue #258) on host float64 ``pinv(sphere) @ A.T``, which is + the metric ``AMICATorchNG._identify_shared_comps`` computes inline. Driving both from one state is what pins that equality. The threshold is read off the data so exactly one pair (the perturbed near-collinear one) is above it, making the comparison non-vacuous and its outcome exact. @@ -156,18 +157,19 @@ def test_merge_decision_matches_the_torch_backend(): model, x_t = _warm_model(warmup=3) cl = np.array(model.comp_list) i0, i1 = int(cl[0, 0]), int(cl[0, 1]) - # Perturb one cross-model column into NEAR- (not exact-) collinearity, so - # the decision itself is under test rather than a pre-merged state. + # Perturb one cross-model component (a row, issue #334) into NEAR- (not + # exact-) collinearity, so the decision itself is under test rather than a + # pre-merged state. a_np = np.array(model.A) rng = np.random.RandomState(0) - a_np[:, i1] = a_np[:, i0] + 1e-3 * rng.standard_normal(NW).astype(np.float32) + a_np[i1, :] = a_np[i0, :] + 1e-3 * rng.standard_normal(NW).astype(np.float32) model.A = mx.array(a_np) model._update_unmixing_matrices() mx.eval(model.A, model.W) # Separate the perturbed pair from the runner-up: any threshold between the # two merges exactly one pair, in either backend. - atil = model._pinv_sphere() @ np.array(model.A, dtype=np.float64) + atil = model._component_sensor_maps() norms = np.linalg.norm(atil, axis=0) cosines = sorted( abs(atil[:, int(cl[i, 0])] @ atil[:, int(cl[j, 1])]) @@ -204,15 +206,16 @@ def test_gm_prev_weighting_matches_torch(merged): MLX used the post-update ``gm``. Both backends run one iteration from one shared state, so only the update is compared. - ``merged=False`` is the disjoint-``comp_list`` case: every column has a + ``merged=False`` is the disjoint-``comp_list`` case: every component has a single contributor, so ``gm`` cancels exactly against ``zeta`` and the two weightings agree analytically -- reverting ``gm_prev`` there leaves the gap at 4.6e-8 (pure float32 noise), so that case CANNOT discriminate; it is kept as the plain one-iteration cross-backend pin. ``merged=True`` is where the - weighting is observable at all: a shared column averages two models' - directions with those weights. Measured on this state: 7.9e-7 with the fix - against 9.9e-3 with the post-update ``gm`` restored -- four orders of - magnitude, which is what the tolerance below discriminates on. + weighting is observable at all: a shared component row averages two models' + steps with those weights. Measured on this state (component-row layout, + issue #334): 6.3e-8 with the fix against 5.6e-3 with the post-update ``gm`` + restored -- five orders of magnitude, which is what the tolerance below + discriminates on. """ model, x_t = _warm_model(warmup=5, doscaling=False) if merged: @@ -226,8 +229,8 @@ def test_gm_prev_weighting_matches_torch(merged): assert ng.A is not None gap = np.abs(np.array(model.A, dtype=np.float64) - ng.A.numpy()).max() - # 1e-4: two orders below the post-update-gm failure (9.9e-3) and two above - # the float32-vs-float64 agreement this pair actually reaches (7.9e-7). + # 1e-4: 56x below the post-update-gm failure (5.6e-3) and three orders above + # the float32-vs-float64 agreement this pair actually reaches (6.3e-8). assert gap < 1e-4, f"A diverged from the float64 torch update by {gap:.2e}" # Same state, same mask: the gradient norm must agree too. This is what pins diff --git a/pamica/tests/test_numpy_share_comps.py b/pamica/tests/test_numpy_share_comps.py index fd8d4a50..305c7ca9 100644 --- a/pamica/tests/test_numpy_share_comps.py +++ b/pamica/tests/test_numpy_share_comps.py @@ -81,7 +81,7 @@ def _force_merged_column(model): """ kept = int(model.comp_list[0, 0]) dead = int(model.comp_list[0, 1]) - model.A[:, dead] = model.A[:, kept] + model.A[dead, :] = model.A[kept, :] # the retired component row (issue #334) model.comp_list[model.comp_list == dead] = kept model.comp_used = np.zeros(model.num_comps, dtype=bool) model.comp_used[np.unique(model.comp_list)] = True @@ -120,8 +120,9 @@ def test_comp_used_survives_a_second_identify_call(): A mask built during that loop comes back all-True. """ model = _shared_fit(max_iter=3, share_comps=False) - model.A[:, int(model.comp_list[0, 1])] = model.A[:, int(model.comp_list[0, 0])] - atil = model._pinv_sphere() @ model.A + # Source 0 of model 1 gets model 0's source-0 component (a row, issue #334). + model.A[int(model.comp_list[0, 1]), :] = model.A[int(model.comp_list[0, 0]), :] + atil = model._component_sensor_maps() comp_list_after, used_first = identify_shared_components( atil, model.comp_list.copy(), model.comp_thresh @@ -175,14 +176,16 @@ def test_sharing_leaves_finite_mixture_parameters(): def test_unused_columns_keep_their_last_finite_value(): - """Frozen, not zeroed: an unused column keeps the value it last held. + """Frozen, not zeroed: an unused component keeps the value it last held. The merge is forced rather than hoped for, so a stale all-True mask fails - here instead of skipping. ``doscaling`` is off so the comparison can be - exact: the rescale pass normalizes every column, dead ones included, which - multiplies their mu by a norm that is 1.0 only to within a ULP. + here instead of skipping. The rescale runs (``doscaling`` on, the + default): a merged-away row is in no model's block, so its scale is + exactly 1 and the comparison is exact. (Before issue #334 this test turned + ``doscaling`` off, because the rescale then renormalized such a stored + column at ULP scale.) """ - model = _shared_fit(max_iter=3, share_comps=False, doscaling=False) + model = _shared_fit(max_iter=3, share_comps=False) _, dead = _force_merged_column(model) unused = ~model.comp_used assert unused.any(), "setup failed: no column was merged away" @@ -346,15 +349,17 @@ def test_shared_column_takes_one_gm_weighted_step(): # Fortran: one dAk, gm-weighted across contributing models, divided by # zeta = sum_h gm[h], applied once (amica15.f90:1749-1761 build, DAXPY at # :1807/:1814). + # Row k of A is component k (issue #334), so model h's step + # dir_h^T @ A[comp_list[:, h], :] lands on the rows comp_list[:, h] names. dAk = np.zeros_like(A_before) zeta = np.zeros(model.num_comps) for h in range(model.num_models): zeta[model.comp_list[:, h]] += gm_before[h] for h in range(model.num_models): idx = model.comp_list[:, h] - dAk[:, idx] += gm_before[h] * np.dot(directions[h].T, A_before[:, idx]) + dAk[idx, :] += gm_before[h] * np.dot(directions[h].T, A_before[idx, :]) nonzero = zeta > 0 - dAk[:, nonzero] /= zeta[nonzero] + dAk[nonzero, :] /= zeta[nonzero][:, None] expected = A_before - lrate * dAk # Same arithmetic in a different association order, so the agreement is at @@ -365,24 +370,25 @@ def test_shared_column_takes_one_gm_weighted_step(): sequential = A_before.copy() for h in range(model.num_models): idx = model.comp_list[:, h] - sequential[:, idx] = sequential[:, idx] - lrate * np.dot( - directions[h].T, sequential[:, idx] + sequential[idx, :] = sequential[idx, :] - lrate * np.dot( + directions[h].T, sequential[idx, :] ) - assert np.abs(model.A[:, kept] - sequential[:, kept]).max() > 1e-3, ( + assert np.abs(model.A[kept, :] - sequential[kept, :]).max() > 1e-3, ( "the two A-update semantics are indistinguishable on this state, so the " "test above would pass with the per-model loop restored" ) def test_merged_away_column_does_not_move(): - """A column no contributor references gets exactly zero dAk, so it holds.""" - model = _shared_fit(max_iter=3, share_comps=False, doscaling=False) + """A component no contributor references gets exactly zero dAk, so its + row holds, the rescale included (its scale is exactly 1).""" + model = _shared_fit(max_iter=3, share_comps=False) _, dead = _force_merged_column(model) - dead_before = model.A[:, dead].copy() + dead_before = model.A[dead, :].copy() model._update_parameters(model._get_updates_and_likelihood()) - np.testing.assert_array_equal(model.A[:, dead], dead_before) + np.testing.assert_array_equal(model.A[dead, :], dead_before) # --- post-merge A-freeze (#242) --------------------------------------------- @@ -479,14 +485,14 @@ def test_shared_column_update_matches_the_torch_backend(): assert ng.A is not None A_torch = ng.A.numpy() - assert np.abs(model.A[:, kept] - A_before[:, kept]).max() > 1e-6, ( - "the shared column did not move, so this compares nothing" + assert np.abs(model.A[kept, :] - A_before[kept, :]).max() > 1e-6, ( + "the shared component did not move, so this compares nothing" ) - np.testing.assert_allclose(model.A[:, kept], A_torch[:, kept], rtol=0, atol=1e-12) + np.testing.assert_allclose(model.A[kept, :], A_torch[kept, :], rtol=0, atol=1e-12) np.testing.assert_allclose(model.A, A_torch, rtol=0, atol=1e-12) - # The dead column is frozen in both, but both still run it through their own - # rescale, whose column norm agrees only to a ULP across array libraries. - np.testing.assert_allclose(model.A[:, dead], A_torch[:, dead], rtol=0, atol=1e-12) + # The dead row is frozen in both, the rescale included (its scale is 1). + np.testing.assert_array_equal(model.A[dead, :], A_before[dead, :]) + np.testing.assert_array_equal(A_torch[dead, :], A_before[dead, :]) def test_forced_merge_fit_is_finite_in_both_backends(): @@ -561,7 +567,7 @@ def test_numpy_merge_decision_matches_torch_backend(): i0 = int(model.comp_list[0, 0]) i1 = int(model.comp_list[0, 1]) rng = np.random.RandomState(0) - model.A[:, i1] = model.A[:, i0] + 1e-3 * rng.standard_normal(model.A.shape[0]) + model.A[i1, :] = model.A[i0, :] + 1e-3 * rng.standard_normal(model.A.shape[1]) model._update_unmixing_matrices() thresh = 0.9 @@ -582,7 +588,7 @@ def test_numpy_merge_decision_matches_torch_backend(): ng.comp_thresh = thresh numpy_comp_list, _ = identify_shared_components( - model._pinv_sphere() @ model.A, model.comp_list.copy(), thresh + model._component_sensor_maps(), model.comp_list.copy(), thresh ) ng._identify_shared_comps() @@ -642,8 +648,8 @@ def test_zero_norm_column_is_not_merged_and_raises_no_warning(): model = _shared_fit(max_iter=3, share_comps=False) zero_idx = int(model.comp_list[0, 1]) other_idx = int(model.comp_list[0, 0]) - model.A[:, zero_idx] = 0.0 - atil = model._pinv_sphere() @ model.A + model.A[zero_idx, :] = 0.0 + atil = model._component_sensor_maps() with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") diff --git a/pamica/tests/test_pre_change_loader.py b/pamica/tests/test_pre_change_loader.py new file mode 100644 index 00000000..88fafbb9 --- /dev/null +++ b/pamica/tests/test_pre_change_loader.py @@ -0,0 +1,212 @@ +"""The historical-code loader never fetches or mutates the repository (issue #343). + +Tests used to run ``git fetch origin --depth 1`` to reach a pinned +commit, which in a full clone records a shallow boundary, after which ``git gc`` +can prune history. ``pamica/tests/pre_change.py`` now only reads. These tests put +a ``git`` wrapper first on ``PATH`` that logs its arguments, REFUSES any fetch +(it exits nonzero without running git, so not even a test of the guard can +touch the repository), and hands every other command to the real git, which +does its real work. They drive the loader through its outcomes (commit present; +missing locally; missing under ``CI``; no git; not a git checkout; an alias +reused for another commit) and assert that no fetch ran and the clone's +shallow state did not change. +""" + +from __future__ import annotations + +import os +import shutil +import stat +import subprocess +import sys +from pathlib import Path + +import pytest + +from pamica.tests import pre_change + +# The epic #324 head Phase 8 merges onto; any commit in the clone would do. +PRESENT = "0930c0e68ec9e2bbef3d20d51cff4c03029248f2" +# A well-formed SHA that names no object (the empty-tree SHA with its first +# character changed). +MISSING = "0b825dc642cb6eb9a060e54bf8d69288fbee4904" + +_REAL_GIT = shutil.which("git") +pytestmark = pytest.mark.skipif(_REAL_GIT is None, reason="git not installed") + + +# Arguments the wrapper refuses: anything that fetches or reshapes history. +_REFUSED = ("fetch", "pull", "--depth", "--deepen", "--shallow", "--unshallow") +_REFUSED_STATUS = 97 + + +@pytest.fixture +def git_log(tmp_path, monkeypatch) -> Path: + """Put a logging, fetch-refusing ``git`` wrapper first on ``PATH``; return + its log file.""" + assert _REAL_GIT is not None + bindir = tmp_path / "bin" + bindir.mkdir() + log = tmp_path / "git.log" + wrapper = bindir / "git" + wrapper.write_text( + "#!/bin/sh\n" + f"printf '%s\\n' \"$*\" >> '{log}'\n" + 'for arg in "$@"; do\n' + ' case "$arg" in\n' + " fetch|pull|--depth*|--deepen*|--shallow*|--unshallow)\n" + " echo 'git wrapper: refused (tests never fetch, issue #343)' >&2\n" + f" exit {_REFUSED_STATUS} ;;\n" + " esac\n" + "done\n" + f"exec '{_REAL_GIT}' \"$@\"\n" + ) + wrapper.chmod(wrapper.stat().st_mode | stat.S_IXUSR) + monkeypatch.setenv("PATH", f"{bindir}{os.pathsep}{os.environ['PATH']}") + return log + + +def _logged(log: Path) -> list[str]: + return log.read_text().splitlines() if log.exists() else [] + + +def _is_shallow() -> str: + return subprocess.run( + ["git", "rev-parse", "--is-shallow-repository"], + cwd=pre_change.REPO_ROOT, + capture_output=True, + text=True, + check=True, + ).stdout.strip() + + +def _assert_no_fetch(log: Path) -> None: + calls = _logged(log) + assert calls, "setup: the wrapper logged nothing, so git was not intercepted" + fetched = [ + call for call in calls if any(arg.startswith(_REFUSED) for arg in call.split()) + ] + assert not fetched, f"the loader fetched: {fetched}" + + +def test_loading_a_present_commit_only_reads(git_log, tmp_path): + if pre_change._git("cat-file", "-e", f"{PRESENT}^{{commit}}").returncode: + pre_change.require_commit(PRESENT) # fails under CI, skips locally + shallow_before = _is_shallow() + alias = "pamica_loader_check" + try: + package = pre_change.load_pre_change_package(PRESENT, alias, tmp_path) + assert package.__file__ is not None + assert Path(package.__file__).is_relative_to(tmp_path) + finally: + for name in [m for m in sys.modules if m == alias or m.startswith(alias + ".")]: + del sys.modules[name] + _assert_no_fetch(git_log) + assert any(call.startswith("archive") for call in _logged(git_log)) + assert _is_shallow() == shallow_before + + +def test_a_missing_commit_skips_locally_with_the_fetch_command(git_log, monkeypatch): + monkeypatch.delenv("CI", raising=False) + shallow_before = _is_shallow() + with pytest.raises(pytest.skip.Exception) as excinfo: + pre_change.require_commit(MISSING) + message = str(excinfo.value) + if shallow_before == "true": + assert "git fetch --unshallow origin" in message + else: + assert f"git fetch origin {MISSING}" in message + _assert_no_fetch(git_log) + assert _is_shallow() == shallow_before + + +def test_a_missing_commit_fails_under_ci(git_log, monkeypatch): + monkeypatch.setenv("CI", "true") + with pytest.raises(pytest.fail.Exception, match="is not in this clone"): + pre_change.require_commit(MISSING) + _assert_no_fetch(git_log) + + +def test_an_abbreviated_pin_is_rejected(): + with pytest.raises(ValueError, match="40-character"): + pre_change.require_commit(PRESENT[:7]) + + +def test_the_guard_catches_a_fetch_without_running_it(git_log): + """The negative control: a fetch sent through ``_git`` is logged, refused by + the wrapper before any git runs, flagged by ``_assert_no_fetch``, and the + clone's shallow state is unchanged.""" + shallow_before = _is_shallow() + result = pre_change._git("fetch", "origin", PRESENT, "--depth", "1") + assert result.returncode == _REFUSED_STATUS + assert b"refused" in result.stderr + with pytest.raises(AssertionError, match="the loader fetched"): + _assert_no_fetch(git_log) + assert _is_shallow() == shallow_before + + +@pytest.fixture +def no_git(tmp_path, monkeypatch) -> None: + """A ``PATH`` with no ``git`` on it.""" + empty = tmp_path / "empty_bin" + empty.mkdir() + monkeypatch.setenv("PATH", str(empty)) + + +def test_missing_git_skips_locally(no_git, monkeypatch): + monkeypatch.delenv("CI", raising=False) + with pytest.raises(pytest.skip.Exception, match="git is not installed"): + pre_change.require_commit(PRESENT) + + +def test_missing_git_fails_under_ci(no_git, monkeypatch): + monkeypatch.setenv("CI", "true") + with pytest.raises(pytest.fail.Exception, match="git is not installed"): + pre_change.require_commit(PRESENT) + + +@pytest.fixture +def not_a_checkout(tmp_path, monkeypatch) -> Path: + """A directory git cannot resolve to a repository, however ``tmp_path`` is + placed: the search for ``.git`` stops at its parent.""" + root = tmp_path / "unpacked" + root.mkdir() + monkeypatch.setenv("GIT_CEILING_DIRECTORIES", str(tmp_path)) + for var in ("GIT_DIR", "GIT_WORK_TREE"): + monkeypatch.delenv(var, raising=False) + return root + + +def test_outside_a_checkout_skips_without_fetch_advice( + git_log, not_a_checkout, monkeypatch, tmp_path +): + monkeypatch.delenv("CI", raising=False) + with pytest.raises(pytest.skip.Exception) as excinfo: + pre_change.load_pre_change_package( + PRESENT, "pamica_not_a_checkout", tmp_path, root=not_a_checkout + ) + message = str(excinfo.value) + assert "is not a git checkout" in message + assert "git fetch" not in message + assert "pamica_not_a_checkout" not in sys.modules + _assert_no_fetch(git_log) + + +def test_outside_a_checkout_fails_under_ci(not_a_checkout, monkeypatch): + monkeypatch.setenv("CI", "true") + with pytest.raises(pytest.fail.Exception, match="is not a git checkout"): + pre_change.require_commit(PRESENT, root=not_a_checkout) + + +def test_an_alias_is_never_reused_for_another_commit(tmp_path): + if pre_change._git("cat-file", "-e", f"{PRESENT}^{{commit}}").returncode: + pre_change.require_commit(PRESENT) # fails under CI, skips locally + alias = "pamica_alias_check" + try: + first = pre_change.load_pre_change_package(PRESENT, alias, tmp_path) + assert pre_change.load_pre_change_package(PRESENT, alias, tmp_path) is first + with pytest.raises(ValueError, match="different alias"): + pre_change.load_pre_change_package(MISSING, alias, tmp_path) + finally: + for name in [m for m in sys.modules if m == alias or m.startswith(alias + ".")]: + del sys.modules[name] diff --git a/pamica/tests/torch_tests/test_ng_sharing.py b/pamica/tests/torch_tests/test_ng_sharing.py index 83b82dca..5e53ab89 100644 --- a/pamica/tests/torch_tests/test_ng_sharing.py +++ b/pamica/tests/torch_tests/test_ng_sharing.py @@ -42,9 +42,11 @@ def real_data() -> np.ndarray: def _controlled_ng(A: torch.Tensor, n_channels: int, n_models: int) -> AMICATorchNG: - """An NG with a fixed identity sphere and default comp_list, whose A is the - given (n_channels, n_channels*n_models) matrix -- so the sharing metric is a - plain cosine similarity on A's columns. No fit needed.""" + """An NG with a fixed identity sphere and default comp_list, whose + component ``k`` has column ``k`` of the given (n_channels, + n_channels*n_models) matrix as its mixing vector -- stored as row ``k`` of + ``A`` (issue #334) -- so the sharing metric is a plain cosine similarity on + the given matrix's columns. No fit needed.""" ng = AMICATorchNG( n_channels=n_channels, n_models=n_models, @@ -60,7 +62,7 @@ def _controlled_ng(A: torch.Tensor, n_channels: int, n_models: int) -> AMICATorc for h in range(n_models): cl[:, h] = np.arange(h * n_channels, (h + 1) * n_channels) ng.comp_list = torch.from_numpy(cl) - ng.A = A.to(torch.float64) + ng.A = A.T.contiguous().to(torch.float64) return ng @@ -326,7 +328,13 @@ def test_two_model_share_fit_survives_merge(real_data): def test_sharing_reduces_unique_count_without_degrading_ll(real_data): """Enabling sharing on matched config strictly reduces the unique-component - count and does not materially degrade the log-likelihood.""" + count and does not materially degrade the log-likelihood. + + ``comp_thresh=0.99`` (five merges at iteration 8, one at 18): since issue + #334 the metric compares the two models' true component maps, which are + still near-identical at iteration 8, so the earlier ``comp_thresh=0.9`` + merged most of them and the losing model's remaining components collapsed + to a non-finite log-likelihood.""" x = real_data[:, :4096] common: dict[str, Any] = dict( n_channels=NW, @@ -340,7 +348,7 @@ def test_sharing_reduces_unique_count_without_degrading_ll(real_data): base = AMICATorchNG(**common) base.fit(x, max_iter=40) shared = AMICATorchNG( - **common, share_comps=True, share_start=8, share_iter=10, comp_thresh=0.9 + **common, share_comps=True, share_start=8, share_iter=10, comp_thresh=0.99 ) shared.fit(x, max_iter=40) assert int(base.comp_used.sum()) == base.n_comps @@ -362,15 +370,20 @@ def _assert_share_result_consistent(ng: AMICATorchNG) -> None: cl = ng.comp_list.cpu().numpy() assert cl.shape == (ng.n_channels, ng.n_models) assert cl.min() >= 0 and cl.max() < ng.n_comps - assert tuple(ng.A.shape) == (ng.n_channels, ng.n_comps) + assert tuple(ng.A.shape) == (ng.n_comps, ng.n_channels) used = int(ng.comp_used.sum()) assert used == len(np.unique(cl)) groups = ng.shared_components() for group in groups: - cols = {int(cl[i, h]) for h, i in group} - assert len(cols) == 1, "a shared group must reference exactly one column" + ids = {int(cl[i, h]) for h, i in group} + assert len(ids) == 1, "a shared group must reference exactly one component" assert len({h for h, _ in group}) >= 2, "sharing is across models" + # One component, so one mixing vector (issue #334): every grouped + # source has the same column in its model's mixing matrix. + vecs = [ng.get_mixing_matrix(h)[:, i] for h, i in group] + for vec in vecs[1:]: + np.testing.assert_array_equal(vec, vecs[0]) if ng.n_models == 2: # With two models the within-model guard caps a group at one source per # model, so every merge folds exactly one column away into one new pair. @@ -525,7 +538,7 @@ def test_pinv_matches_inv_on_a_fitted_full_rank_sphere(real_data): ng.fit(real_data[:, :4096], max_iter=10) assert ng.sphere is not None and ng.A is not None delta = ( - (torch.linalg.pinv(ng.sphere) @ ng.A - torch.linalg.inv(ng.sphere) @ ng.A) + (torch.linalg.pinv(ng.sphere) @ ng.A.T - torch.linalg.inv(ng.sphere) @ ng.A.T) .abs() .max() .item() diff --git a/pamica/torch_impl/core.py b/pamica/torch_impl/core.py index 5a789b69..396500c7 100644 --- a/pamica/torch_impl/core.py +++ b/pamica/torch_impl/core.py @@ -17,6 +17,9 @@ * ``W`` (and ``A``) are stored and mutated directly; ``W`` is recomputed from ``A`` once per iteration via a batched ``torch.linalg.inv`` (matching ``numpy_impl.utils.get_unmixing_matrices``), never via ``pinv`` in the hot path. +* ``A`` stores one component per ROW, shape ``(n_comps, n_channels)`` (issue + #334, :mod:`pamica.component_layout`, ADR 0007): row ``k`` is the reference's + column ``A(:, k)`` and model ``h``'s block is ``A[comp_list[:, h], :]``. * The E-step is vectorized over ``(model, mix, source)`` via broadcasting; the only Python loops are over models (typically 1-3) and over blocks. * Samples are processed in blocks and sufficient statistics are accumulated @@ -56,6 +59,7 @@ from .. import blocktune from .. import restarts from .. import schedule +from ..component_layout import rows_from_legacy_columns from ..metrics import mir as mir_metric from ..metrics import model_probability_from_loglik, pairwise_mi from ..rank import ( @@ -83,11 +87,17 @@ _LOG2 = math.log(2.0) _LOG4 = math.log(4.0) # logistic-family normalizer (amica15.f90:1346) -_HALF_LOG_PI = 0.5 * math.log(math.pi) -# Log-normalizers for the non-GG density families, using Fortran's exact literal -# constants (amica15.f90:1333/1359/1371) so the log-density matches the reference -# binary bit-for-bit: 2.506628274 = sqrt(2*pi) (Gaussian, pdtype 2); 4.132731354 / -# 1.858073988 = the sub-/super-Gaussian cosh normalizers (pdtype 4 / 1). +_HALF_LOG_PI = 0.5 * math.log(math.pi) # exact; the reference's differs (issue #344) +# Log-normalizers for the non-GG density families, from the decimal literals the +# reference writes (amica15.f90:1333/1359/1371): 2.506628274 = sqrt(2*pi) +# (Gaussian, pdtype 2); 4.132731354 / 1.858073988 = the sub-/super-Gaussian cosh +# normalizers (pdtype 4 / 1). These are the literals' double-precision values, +# not the binary's: gfortran reads ``dble(2.506628274)`` as a single-precision +# literal widened to double, so the binary's log-normalizers differ from these +# by 3.7e-10, 2.0e-8 and -2.1e-8 (computed from the literals; the same kind of +# literal in the rho == 2 branch, :1313, measurably moves the reference's +# log-likelihood). Recorded in docs/guides/amica-differences.md (row 16); +# whether to adopt the single-precision values is issue #344. _LOG_SQRT_2PI = math.log(2.506628274) _LOG_NORM_COSH_SUB = math.log(4.132731354) _LOG_NORM_COSH_SUP = math.log(1.858073988) @@ -120,6 +130,33 @@ # already IS the best, the gap is 0 < tol, and no restore fires. _KEEP_BEST_TOL = 1e-9 +# The last state_dict format_version that stored A with components as COLUMNS, +# shape (n_channels, n_comps) (issue #334). from_state_dict converts it. +_COLUMN_LAYOUT_FORMAT_VERSION = 3 + + +def _component_rows_state(state: dict) -> dict: + """A format_version 3 state with its ``A`` converted to component rows. + + Version 3 stored ``A`` as ``(n_channels, n_comps)`` with ``comp_list`` + indexing stored columns; every model's block carries over element for + element (:func:`pamica.component_layout.rows_from_legacy_columns`), so the + loaded model is exactly the saved one. A state whose ``comp_list`` shares + a component across models (a ``share_comps`` merge made under the column + semantics, issue #334) is refused with a message to refit. The input is not + modified. + """ + params = state["params"] + missing = [name for name in ("A", "comp_list") if name not in params] + if missing: + raise ValueError(f"malformed AMICATorchNG state: missing params {missing}") + A_rows = rows_from_legacy_columns( + torch.as_tensor(params["A"]).detach().cpu().numpy(), + torch.as_tensor(params["comp_list"]).detach().cpu().numpy(), + owner="AMICATorchNG", + ) + return {**state, "params": {**params, "A": torch.from_numpy(A_rows)}} + def _logcosh(x: torch.Tensor) -> torch.Tensor: """Numerically stable ``log cosh(x) = |x| - log2 + log1p(exp(-2|x|))``.""" @@ -412,10 +449,11 @@ class AMICATorchNG: RMS, over ``comp_used`` components only, of the per-iteration weight-update direction ``dAk`` (the natural-gradient/Newton step before the ``lrate`` scaling and before ``share_comps``'s A-freeze - may discard it) -- ``sqrt(sum(dAk**2, axis=0)[comp_used].sum() / - (n_channels * comp_used.sum()))``. The ``comp_used`` mask only - differs from all-True when ``share_comps`` has merged/frozen columns - (issue #60); it is a no-op otherwise. Computed every iteration + may discard it) -- ``sqrt(sum(dAk**2, axis=1)[comp_used].sum() / + (n_channels * comp_used.sum()))``, one squared norm per component + row, as the reference sums each component's column. The + ``comp_used`` mask only differs from all-True when ``share_comps`` + has merged components away (issue #60); it is a no-op otherwise. Computed every iteration regardless of ``use_grad_norm``/``use_min_dll`` (both stops read the same per-iteration value; Fortran computes ``ndtmpsum`` unconditionally too, in ``accum_updates_and_likelihood``, before either check runs). @@ -502,7 +540,7 @@ class AMICATorchNG: Clamp bounds for the mixture scale parameter ``beta``. doscaling, scalestep : bool, int Whether/how often to rescale each component's mixing vector (a row of - its model's stored ``A`` block) to unit norm, with the matching + the stored ``A``) to unit norm, with the matching ``mu``/``beta`` rescale, an exact change of scale (see :meth:`_rescale_components`). The rescale runs on iterations ``scalestep``, ``2*scalestep``, ... counted from 1; the default 1 @@ -514,12 +552,14 @@ class AMICATorchNG: Enable multi-model component sharing (Fortran ``share_comps`` / ``identify_shared_comps``, amica15.f90:1916): components that are near-collinear across different models are merged so they share one - mixing column and one density. Requires ``n_models >= 2`` (a model - cannot share with itself); a no-op otherwise. OFF by default, so - single-model (#24) and default multi-model (#27) results are unchanged. - There is no bit-exact oracle -- the reference's similarity metric is - never initialized (like ``do_choose_pdfs``, #26) -- so this implements - the intended algorithm, validated by real-data behavior. A merge that + mixing vector (one row of ``A``) and one density. Requires + ``n_models >= 2`` (a model cannot share with itself); a no-op + otherwise. OFF by default, so single-model (#24) and default + multi-model (#27) results are unchanged. The reference's similarity + metric is never initialized (like ``do_choose_pdfs``, #26), so the + metric has no bit-exact oracle; the merged state it produces does, and + the update from it matches the reference to round-off (issue #334, + see :meth:`_identify_shared_comps`). A merge that fires on the LAST fit iteration is reflected in the returned model but trails in ``final_ll_``; see that attribute's comment (issue #269). share_start, share_iter : int @@ -531,7 +571,8 @@ class AMICATorchNG: that window never consumes the whole cycle. comp_thresh : float, default=0.99 Cosine-similarity cutoff (in the de-sphered/sensor-space metric) above - which two mixing columns are identified and merged. The de-sphering uses + which two components' mixing vectors are identified and merged. The + de-sphering uses ``pinv(sphere)``, so sharing also works on rank-reduced and rank-deficient fits (issues #253, #221); see :meth:`_identify_shared_comps`. @@ -795,14 +836,15 @@ def __init__( schedule.validate_iteration_setting("scalestep", scalestep, 1) # Component sharing (Fortran share_comps / identify_shared_comps trigger - # amica15.f90:1856, subroutine :1916-1963): periodically merge mixing - # columns near-collinear across DIFFERENT models so they share one - # density and one mixing column. Multi-model only (a model cannot share - # with itself); OFF by default so single-model (#24) and default - # multi-model (#27) parity stay byte-for-byte. No bit-exact oracle -- the - # reference's Spinv2 metric is declared but never allocated, so its - # reassignment is unrunnable (the do_choose_pdfs situation, #26); this is - # the intended algorithm, validated by real-data behavior. + # amica15.f90:1856, subroutine :1916-1963): periodically merge + # components near-collinear across DIFFERENT models so they share one + # density and one mixing vector (one row of A, issue #334). Multi-model + # only (a model cannot share with itself); OFF by default so + # single-model (#24) and default multi-model (#27) parity stay + # byte-for-byte. The reference's Spinv2 metric is declared but never + # allocated, so its own scan never merges (every similarity is NaN; the + # do_choose_pdfs situation, #26); the merged state it would produce is + # checked against the reference through load_comp_list instead. self.share_comps = share_comps self.share_start = share_start self.share_iter = share_iter @@ -1081,9 +1123,11 @@ def _initialize_parameters(self): rng = np.random.RandomState(self.seed) n, m, ncomp, nmix = self.n_channels, self.n_models, self.n_comps, self.n_mix - A_np = np.zeros((n, ncomp), dtype=np.float64) + # One row per component (issue #334): model h's block is rows + # h*n..(h+1)*n-1, the same n x n draw the column layout held before. + A_np = np.zeros((ncomp, n), dtype=np.float64) for h in range(m): - A_np[:, h * n : (h + 1) * n] = np.eye(n) + 0.01 * (0.5 - rng.rand(n, n)) + A_np[h * n : (h + 1) * n, :] = np.eye(n) + 0.01 * (0.5 - rng.rand(n, n)) comp_list_np = np.zeros((n, m), dtype=np.int64) for h in range(m): @@ -1131,10 +1175,13 @@ def _initialize_parameters(self): self._update_unmixing_matrices() def _update_unmixing_matrices(self): - """Recompute W from A via direct (batched) inversion -- never pinv.""" + """Recompute W from A via direct (batched) inversion -- never pinv. + + Model ``h``'s block is the component rows ``A[comp_list[:, h], :]``. + """ assert self.A is not None and self.comp_list is not None A_stack = torch.stack( - [self.A[:, self.comp_list[:, h]] for h in range(self.n_models)], dim=0 + [self.A[self.comp_list[:, h], :] for h in range(self.n_models)], dim=0 ) try: W_stack = torch.linalg.inv(A_stack) @@ -1671,12 +1718,13 @@ def _update_parameters(self, acc: Dict[str, torch.Tensor], n_samples: int): # Component sharing (#60): a component that was merged away is no longer # referenced by comp_list, so no sufficient statistic accumulates into - # its column (dalpha_n/dmu_d/dbeta_d == 0) and the divisions below would - # be 0/0 = NaN. Update only USED columns and freeze the rest at their - # last finite value (Fortran carries NaN there harmlessly behind its - # comp_used mask; we keep them finite so save/the degenerate guard are - # not tripped). With the default full comp_list every column is used, so - # ``used`` is all-True and every update below is byte-for-byte unchanged. + # its density column (dalpha_n/dmu_d/dbeta_d == 0) and the divisions + # below would be 0/0 = NaN. Update only USED components and freeze the + # rest at their last finite value (Fortran carries NaN there harmlessly + # behind its comp_used mask; we keep them finite so save/the degenerate + # guard are not tripped). With the default full comp_list every + # component is used, so ``used`` is all-True and every update below is + # byte-for-byte unchanged. used = self.comp_used.unsqueeze(0) # (1, n_comps) self.alpha = torch.where( @@ -1695,7 +1743,7 @@ def _update_parameters(self, acc: Dict[str, torch.Tensor], n_samples: int): sigma2, lambda_, kappa = self._finalize_newton_stats(acc) # Exact-EM mixture location/scale (Fortran :1978/:1993). No lrate. - # ``used`` masks merged-away columns (no-op for the default comp_list). + # ``used`` masks merged-away components (no-op for the default comp_list). self.mu = torch.where(used, self.mu + acc["dmu_n"] / acc["dmu_d"], self.mu) self.beta = torch.where( used, @@ -1753,14 +1801,16 @@ def _update_parameters(self, acc: Dict[str, torch.Tensor], n_samples: int): ) # --- A / W update: natural gradient, optionally Newton-preconditioned. - # A is stored as Fortran's A^T (the true unmixing is W^T = inv(A)^T), so - # Fortran's A_fort -= lrate*A_fort @ dir becomes, transposed, - # A -= lrate*dir^T @ A (LEFT-multiply by the TRANSPOSED direction). The - # direction ``dir`` (natural gradient I - /dgm, or its Newton - # precondition) is built in Fortran's untransposed convention. Getting - # this wrong (right-multiply by the untransposed dir) is invisible at the - # fixed point but sends the free-running fit downhill -- issue #24 root - # cause (.context/issue-24/root_cause_Aupdate.py, machine-exact check). + # A is stored as Fortran's A^T (one component per row, issue #334; the + # true unmixing is W^T = inv(block)^T), so Fortran's per-model + # A_fort(:, comp_list(:,h)) @ dir becomes, transposed, + # dir^T @ A[comp_list[:, h], :] (LEFT-multiply by the TRANSPOSED + # direction). The direction ``dir`` (natural gradient I - /dgm, + # or its Newton precondition) is built in Fortran's untransposed + # convention. Getting this wrong (right-multiply by the untransposed + # dir) is invisible at the fixed point but sends the free-running fit + # downhill -- issue #24 root cause + # (.context/issue-24/root_cause_Aupdate.py, machine-exact check). # (newton_active / sigma2 / lambda_ / kappa were finalized above.) # # The direction/dAk/gradient-norm computation below runs UNCONDITIONALLY, @@ -1793,32 +1843,37 @@ def _update_parameters(self, acc: Dict[str, torch.Tensor], n_samples: int): directions.append(dA_h) # Accumulate each model's natural-gradient/Newton contribution per - # mixing COLUMN as a gm-WEIGHTED AVERAGE (Fortran dAk/zeta, - # amica15.f90:1749-1761): dAk = sum_h gm[h]*dir_h scattered by - # comp_list, zeta = sum_h gm[h] per column, then dAk /= zeta. For the - # default disjoint comp_list every column has exactly one contributor, - # so gm cancels (dAk = dir) and single-model (gm=[1]) is byte-for-byte - # unchanged; for a SHARED column (issue #60) the step is Fortran's - # responsibility-weighted average, NOT a raw sum (a raw sum would - # over-step by the contributor count and destabilize the fit). + # COMPONENT (a row of A) as a gm-WEIGHTED AVERAGE (Fortran dAk/zeta, + # amica15.f90:1749-1761): dAk = sum_h gm[h]*dir_h^T@block_h scattered + # into the rows comp_list names, zeta = sum_h gm[h] per component, then + # dAk /= zeta. For the default disjoint comp_list every component has + # exactly one contributor, so gm cancels (dAk = dir) and single-model + # (gm=[1]) is byte-for-byte unchanged; for a SHARED component (issue + # #60) the step is Fortran's responsibility-weighted average of the + # models' steps for that one mixing vector, NOT a raw sum (a raw sum + # would over-step by the contributor count and destabilize the fit). A + # merged-away component has no contributor, so its zeta is 0 and its + # dAk is 0/tiny = 0: its row takes no step. dAk = torch.zeros_like(self.A) zeta = torch.zeros(self.n_comps, dtype=self.dtype, device=self.device) for h in range(self.n_models): idx = self.comp_list[:, h] - dAk.index_add_(1, idx, gm_prev[h] * (directions[h].T @ self.A[:, idx])) + dAk.index_add_(0, idx, gm_prev[h] * (directions[h].T @ self.A[idx, :])) zeta.index_add_(0, idx, gm_prev[h].expand(idx.shape[0])) - dAk = dAk / zeta.clamp_min(torch.finfo(self.dtype).tiny) + dAk = dAk / zeta.clamp_min(torch.finfo(self.dtype).tiny).unsqueeze(1) # Weight-gradient norm (Fortran ndtmpsum, amica15.f90:1760-1761): - # ``sqrt(sum(dAk**2, mask=comp_used) / (nw*count(comp_used)))``. Read by - # fit()'s convergence checks (issue #207); the comp_used mask matters - # only when share_comps has merged/frozen columns (all-True otherwise, - # so ``comp_used_mask`` covers every column and this is a plain RMS over + # ``sqrt(sum(dAk**2, mask=comp_used) / (nw*count(comp_used)))``, one + # squared norm per component row, as the reference sums each + # component's column (``nd(iter,:) = sum(dAk*dAk,1)``). Read by fit()'s + # convergence checks (issue #207); the comp_used mask matters only when + # share_comps has merged components away (all-True otherwise, so + # ``comp_used_mask`` covers every component and this is a plain RMS over # dAk). Named distinctly from the ``used`` (1, n_comps) broadcast mask # above (alpha/mu/beta/rho updates) to avoid shadowing it. comp_used_mask = self.comp_used n_used = int(comp_used_mask.sum().item()) - nd = (dAk**2).sum(dim=0) # (n_comps,) + nd = (dAk**2).sum(dim=1) # (n_comps,) self._ndtmpsum = float( torch.sqrt( nd[comp_used_mask].sum() / (self.n_channels * max(n_used, 1)) @@ -1868,22 +1923,22 @@ def _rescale_components(self) -> None: """Rescale every component to a unit-norm mixing vector (Fortran ``doscaling``, amica15.f90:1843-1851), an exact change of scale. - Each model's stored block ``A[:, comp_list[:, h]]`` is the transpose of - the reference's per-model mixing matrix (issue #24 convention), so - source ``i`` of model ``h`` is ROW ``i`` of that block, the reference's - column ``A(:,k)``. Dividing that row by its norm scales source ``i`` up - by the norm; ``mu[:, comp_list[i, h]] *= norm`` and - ``beta[:, comp_list[i, h]] /= norm`` rescale its density to match, so - the log-likelihood is unchanged. Normalizing stored COLUMNS instead - (the rule before issue #333) is not a change of scale of any component - and perturbed the fit every iteration. A zero-norm row is left - untouched, as in the reference (``Anrmk > 0``). - - Models are rescaled in order. Their blocks are disjoint unless - ``share_comps`` merged a column; a shared stored column then belongs to - rows of several blocks, where this per-block rule is not well defined. - It is applied uniformly anyway: the component-row layout of epic #324 - Phase 8 (issue #334) replaces it. ADR 0006 records the convention. + Component ``k`` is row ``k`` of ``A`` (issue #334, ADR 0007), the + reference's column ``A(:,k)``. Dividing that row by its norm scales the + source up by the norm; ``mu[:, k] *= norm`` and ``beta[:, k] /= norm`` + rescale its density to match, so the log-likelihood is unchanged. + Normalizing anything but a component's mixing vector is not a change of + scale and perturbs the fit (issue #333, ADR 0006). A zero-norm row is + left untouched, as in the reference (``Anrmk > 0``), and so is a NaN + norm (``NaN > 0`` is false). + + Each component is rescaled exactly once, including one that + ``share_comps`` merged into several models. A merged-away row, which no + model reads, is left untouched: the reference's is NaN by then (its + ``dAk/zeta`` is ``0/0``), so its own ``Anrmk > 0`` skips it too. The + norms are taken from the per-model blocks ``A[comp_list[:, h], :]``, the + same ``n x n`` gathers the per-block rule of issue #333 used, so an + unshared ``comp_list`` rescales bit for bit as before. """ assert ( self.A is not None @@ -1891,14 +1946,15 @@ def _rescale_components(self) -> None: and self.beta is not None and self.comp_list is not None ) + scale = torch.ones(self.n_comps, dtype=self.dtype, device=self.device) for h in range(self.n_models): idx = self.comp_list[:, h] - block = self.A[:, idx] # row i = source i of model h - norm = torch.sqrt((block**2).sum(dim=1)) # (n_channels,) - scale = torch.where(norm > 0, norm, torch.ones_like(norm)) - self.A[:, idx] = block / scale.unsqueeze(1) - self.mu[:, idx] = self.mu[:, idx] * scale - self.beta[:, idx] = self.beta[:, idx] / scale + norm = torch.sqrt((self.A[idx, :] ** 2).sum(dim=1)) # (n_channels,) + # A shared row gets the same norm from every block that holds it. + scale[idx] = torch.where(norm > 0, norm, torch.ones_like(norm)) + self.A = self.A / scale.unsqueeze(1) + self.mu = self.mu * scale + self.beta = self.beta / scale def _a_frozen(self) -> bool: """Whether the A-update (and its lrate ramp) is held this iteration. @@ -1926,18 +1982,25 @@ def _a_frozen(self) -> bool: return schedule.share_freeze(self.iteration, self.share_start, self.share_iter) def _identify_shared_comps(self) -> None: - """Merge near-collinear mixing columns across models (Fortran + """Merge near-collinear components across models (Fortran ``identify_shared_comps``, amica15.f90:1916). Two components (model ``h`` source ``i`` and model ``hh`` source ``ii``, - ``h < hh``) are identified when the angle between their mixing columns, + ``h < hh``) are identified when the angle between their mixing vectors, measured in the original (de-sphered) data space, is below the ``comp_thresh`` cutoff:: - t0 = |a . b| / (||a|| ||b||), a = Spinv A[:,ci], b = Spinv A[:,cj] + t0 = |a . b| / (||a|| ||b||), a = Spinv A[ci, :], b = Spinv A[cj, :] - where ``Spinv = pinv(sphere)`` de-spheres the columns back to - input-channel (sensor) space, so the similarity compares scalp maps. + with ``ci = comp_list[i, h]`` and ``cj = comp_list[ii, hh]``. Row ``k`` of + ``A`` is component ``k``'s mixing vector (issue #334, ADR 0007), the + reference's column ``A(:, k)``, and ``Spinv = pinv(sphere)`` de-spheres + it back to input-channel (sensor) space, so ``a`` is exactly column + ``i`` of ``get_sensor_mixing_matrix(h)``: the similarity compares the + two components' scalp maps. (Before issue #334 the scan compared stored + COLUMNS of a component-column layout, which are not components.) The + reference weights its inner products with ``Spinv2``, which is this + cosine for ``Spinv2 = Spinv^T Spinv``. The pseudo-inverse -- not a true inverse -- is the faithful back-map: the reference carries exactly this, ``Spinv(nx, numeigs)``, whenever @@ -1950,42 +2013,48 @@ def _identify_shared_comps(self) -> None: merge decisions on well-conditioned data are unchanged. * Rank-reduced sphere ``(n_kept, n_channels)`` (issue #223), or a square sphere fitted on rank-deficient data (Maxwell-filtered MEG, - average-referenced EEG): ``pinv`` maps each column into the retained - sensor subspace instead of failing. For the reduced PCA sphere - ``S = D^-1/2 V_r^T`` this is ``pinv(S) = V_r D^1/2``, i.e. a + average-referenced EEG): ``pinv`` maps each component into the + retained sensor subspace instead of failing. For the reduced PCA + sphere ``S = D^-1/2 V_r^T`` this is ``pinv(S) = V_r D^1/2``, i.e. a de-whitening followed by the orthonormal ``U_r = V_r`` embedding proposed in issue #221; the embedding leaves the cosine untouched, so this is the same comparison the full-rank path makes, evaluated in the subspace the data actually occupy. - On a match, ``cj`` is folded into ``ci``: every ``comp_list`` entry equal - to ``cj`` is reassigned to ``ci``, so the two now share one mixing column - and one density (the M-step already accumulates every sufficient - statistic through ``comp_list`` via index_add, so shared components sum - automatically). + On a match, ``cj`` is folded into ``ci`` as the reference folds it: + every ``comp_list`` entry equal to ``cj`` is reassigned to ``ci``, so the + two sources now share component ``ci``'s mixing vector (row ``ci``) and + density, and nothing is copied or averaged at the merge itself. Row + ``cj`` is retired: no model reads it, the A-update gives it no step and + its density is frozen (see :meth:`_update_parameters`). The M-step + accumulates every sufficient statistic through ``comp_list`` via + index_add, so a shared component's statistics sum automatically. Greedy and order-dependent, matching the reference's quadruple loop. - Skips a pair already merged, or one whose two columns coexist in some + Skips a pair already merged, or one whose two components coexist in some single model (a model cannot share a component with itself). - No bit-exact oracle: ``Spinv2`` is *declared* in the reference headers - but never *allocated* anywhere in ``amica15.f90``/``amica17.f90`` (unlike - ``Spinv``, allocated at :551), so invoking ``identify_shared_comps`` with - ``share_comps=.true.`` would read an unallocated array through ``DGEMV`` - -- undefined behavior, most likely a crash, not a benign no-op. The - routine is effectively unrunnable in the reference (cf. the also-dead - ``do_choose_pdfs`` switch, #26), so this implements the intended - algorithm and validates it on real data, not against byte parity. + No bit-exact oracle for the metric: ``Spinv2`` is *declared* in the + reference headers but never *allocated* anywhere in + ``amica15.f90``/``amica17.f90`` (unlike ``Spinv``, allocated at + amica15.f90:569), so the pinned binary's scan reads an unallocated + array through ``DGEMV``. + It does not crash: every similarity comes out NaN, so it never merges, + at any ``comp_thresh`` (measured, epic #324 Phase 8; ``comp_list`` is + unchanged even at ``comp_thresh=0``). The merged STATE does have one: the + reference's ``load_comp_list`` seeds a merged ``comp_list`` directly, + and the update from it matches this backend to float64 round-off + (``pamica/tests/test_component_rows.py``). """ if self.n_models < 2: return assert self.A is not None and self.comp_list is not None # _pinv_sphere raises on a non-finite sphere, so the metric below can # only be garbage if A itself is (guarded per-pair in the scan). - spinv = self._pinv_sphere() - # De-sphered mixing columns in sensor space, on CPU for the small greedy - # scan (n_models^2 * n_channels^2 pairs; avoids per-element GPU syncs). - atil = (spinv @ self.A).detach().cpu().numpy() + # De-sphered component mixing vectors in sensor space (column k is + # component k), on CPU for the small greedy scan (n_models^2 * + # n_channels^2 pairs; avoids per-element GPU syncs). + atil = self._component_sensor_maps() norms = np.linalg.norm(atil, axis=0) cl = self.comp_list.detach().cpu().numpy().copy() # (nw, n_models) nw, m = cl.shape @@ -2001,12 +2070,12 @@ def _identify_shared_comps(self) -> None: t0 = abs(atil[:, ci] @ atil[:, cj]) / ( norms[ci] * norms[cj] + tiny ) - # NaN t0 (e.g. a zero-norm column) must NOT merge: + # NaN t0 (e.g. a zero-norm component) must NOT merge: # `NaN < thresh` is False, so guard finiteness explicitly. if not np.isfinite(t0) or t0 < self.comp_thresh: continue # A model cannot share a component with itself: skip if - # any single model already uses both columns. + # any single model already uses both components. if any( (cl[:, k] == ci).any() and (cl[:, k] == cj).any() for k in range(m) @@ -2023,6 +2092,18 @@ def _identify_shared_comps(self) -> None: int(np.unique(cl).size), ) + def _component_sensor_maps(self) -> np.ndarray: + """Every component's mixing vector in input-channel (sensor) space. + + ``pinv(sphere) @ A.T`` as a NumPy array of shape ``(n_channels_in, + n_comps)``: column ``comp_list[i, h]`` is column ``i`` of + :meth:`get_sensor_mixing_matrix` for model ``h`` (issue #334). These + are the vectors the share metric compares. Raises like + :meth:`_pinv_sphere` on a non-finite sphere. + """ + assert self.A is not None + return (self._pinv_sphere() @ self.A.T).detach().cpu().numpy() + def _pinv_sphere(self) -> torch.Tensor: """Cached ``pinv(sphere)``: the back-map from sphered to input-channel space. @@ -2063,8 +2144,9 @@ def comp_used(self) -> torch.Tensor: """Boolean mask (n_comps,) of components still referenced by comp_list. A component drops out of use when it is folded into another by - :meth:`_identify_shared_comps`; unused columns receive no gradient and - are never read by the E-step. Derived from ``comp_list`` (not stored). + :meth:`_identify_shared_comps`; an unused component (its row of ``A`` + and its density columns) receives no update and is never read by the + E-step. Derived from ``comp_list`` (not stored). Raises rather than asserts: ``assert`` is stripped under ``python -O``, which would turn a pre-fit read into an obscure ``NoneType`` error @@ -2645,7 +2727,7 @@ def _fit_once( # Component sharing (Fortran identify_shared_comps schedule, # amica15.f90:1856): once per share_iter cycle from share_start, - # merge near-collinear mixing columns across models using the + # merge near-collinear components across models using the # just-updated A. Fortran runs identify_shared_comps BEFORE # get_unmixing_matrices (amica15.f90:1858,1863), so rebuild W from the # merged comp_list -- otherwise the next E-step would read a stale W @@ -3110,7 +3192,10 @@ def transform(self, X: np.ndarray, model_idx: int = 0) -> np.ndarray: return S.cpu().numpy() def get_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: - """True mixing matrix ``A_fort`` = (stored A)^T (issue #24 convention).""" + """True mixing matrix of model ``model_idx``: the reference's + ``A(:, comp_list(:, h))``, i.e. that model's component rows of the + stored ``A`` transposed (issue #24 convention; issue #334 layout), so + column ``i`` is source ``i``'s mixing vector in the sphered space.""" if self.A is None or self.comp_list is None: raise RuntimeError( "AMICATorchNG.get_mixing_matrix() requires a fitted model; call " @@ -3118,7 +3203,7 @@ def get_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: ) self._check_model_idx(model_idx) self._check_usable("get the mixing matrix") - return self.A[:, self.comp_list[:, model_idx]].T.cpu().numpy() + return self.A[self.comp_list[:, model_idx], :].T.cpu().numpy() @property def n_channels_in(self) -> int: @@ -3156,7 +3241,7 @@ def get_sensor_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: ) self._check_model_idx(model_idx) self._check_usable("get the sensor mixing matrix") - A = self.A[:, self.comp_list[:, model_idx]].T + A = self.A[self.comp_list[:, model_idx], :].T return (self._pinv_sphere() @ A).cpu().numpy() def get_unmixing_matrix(self, model_idx: int = 0) -> np.ndarray: @@ -3526,14 +3611,17 @@ def shared_components(self) -> list: """Components shared across models by ``share_comps`` (issue #60). ``share_comps`` folds near-collinear components of different models onto - one shared mixing column + density, recorded as a repeated index in - ``comp_list``. Returns one group per shared column: a list of - ``(model_idx, source_idx)`` pairs that all reference it. Empty when no + one shared component (one row of ``A`` and one density), recorded as a + repeated index in ``comp_list``. Returns one group per shared + component: a list of ``(model_idx, source_idx)`` pairs that all + reference it, whose columns of :meth:`get_sensor_mixing_matrix` are + therefore identical. Empty when no component is shared across two or more models (always for one model, and for a default multi-model fit with ``share_comps`` off). - Note that a merge synchronizes only the mixture parameters routed through - ``comp_list`` (``mu``/``alpha``/``beta``/``rho``); the per-source density + Note that a merge synchronizes only the parameters routed through + ``comp_list`` (the mixing vector and ``mu``/``alpha``/``beta``/``rho``); + the per-source density *family* code ``pdtype`` is a separate tensor and is not synchronized, so under the adaptive switcher (``pdftype=1``) a shared pair can still report different :meth:`get_pdftype` codes. @@ -3727,7 +3815,9 @@ def _np(t): rho=_np(self.rho), comp_list=_np(self.comp_list), ll=ll, - A=_np(self.A), + # The reference layout, (nw, num_comps) with component k in column + # k: the component-row A transposed (issue #334). + A=_np(self.A).T, Lht=Lht, Lt=Lt, ) @@ -3749,6 +3839,10 @@ def _np(t): ) # fmt: skip # Integer tensors in _PARAM_TENSORS: keep their dtype on load, only move device. _INT_PARAM_TENSORS = ("comp_list", "pdtype") + # The state_dict format. 4 (issue #334) stores A with one component per + # row, shape (n_comps, n_channels); see from_state_dict for what older + # versions load as. + _STATE_FORMAT_VERSION = 4 # Stop reasons that mark a fit as degenerate (non-finite log-likelihood, or # -- only reachable under best-of-N restarts, issue #198 -- a fit that raised @@ -3911,7 +4005,7 @@ def state_dict(self) -> dict: "restart_stop_reasons_": list(self.restart_stop_reasons_), } return { - "format_version": 3, + "format_version": self._STATE_FORMAT_VERSION, "config": config, "params": params, "extra": extra, @@ -3926,23 +4020,34 @@ def from_state_dict( ``device`` overrides where the restored tensors live (the constructor picks a default when ``None``); ``dtype`` always comes from the saved ``config``. + + A ``format_version`` 3 state (components as columns of ``A``, before + issue #334) loads unchanged in every other respect: its ``A`` is + converted to component rows without loss, unless ``share_comps`` had + merged components, which raises ``ValueError`` asking for a refit + (:func:`pamica.component_layout.rows_from_legacy_columns`). """ - # format_version stays 3 here -- deliberately NOT bumped for issue - # #207, unlike PR #52's 1->2 (adaptive PDF) and PR #53's 2->3 - # (keep_best). The check below is strict equality, so bumping would - # break loading genuinely older (pre-#53) files for no reason: the - # five new config keys (use_min_dll/min_dll/maxincs/use_grad_norm/ - # min_nd) are additive-only, and a payload saved before #207 simply - # lacks them in its ``config`` dict, so ``cls(device=device, - # **config)`` below falls back to the constructor's own - # Fortran-faithful defaults for whichever keys are missing -- see + # format_version 4 (issue #334) stores A with one component per row, + # shape (n_comps, n_channels). A version 3 payload stored it as + # (n_channels, n_comps) with comp_list indexing columns; it is converted + # without loss when unmerged and refused when share_comps had merged + # components (pamica.component_layout, ADR 0007). Earlier bumps were + # PR #52's 1->2 (adaptive PDF) and PR #53's 2->3 (keep_best); versions 1 + # and 2 stay unreadable. Additive keys do not bump the version: issue + # #207's five convergence config keys (use_min_dll/min_dll/maxincs/ + # use_grad_norm/min_nd) are simply absent from an older payload's + # ``config`` dict, so ``cls(device=device, **config)`` below falls back + # to the constructor's own Fortran-faithful defaults for whichever keys + # are missing -- see # test_missing_convergence_keys_fall_back_to_fortran_defaults in # test_ng_convergence.py. version = state.get("format_version") - if version != 3: + if version not in (cls._STATE_FORMAT_VERSION, _COLUMN_LAYOUT_FORMAT_VERSION): raise ValueError( f"unsupported AMICATorchNG state format_version: {version!r} " - "(expected 3)" + f"(expected {cls._STATE_FORMAT_VERSION}, or " + f"{_COLUMN_LAYOUT_FORMAT_VERSION} from before the component-row " + "layout)" ) for section in ("config", "params", "extra"): if section not in state: @@ -3966,6 +4071,8 @@ def from_state_dict( f"AMICATorchNG constructor ({exc}); the payload may be " "truncated or from an incompatible version." ) from exc + if version == _COLUMN_LAYOUT_FORMAT_VERSION: + state = _component_rows_state(state) obj._load_params(state) return obj @@ -3979,10 +4086,10 @@ def _load_params(self, state: dict) -> None: # Guard against config/params drift: A and comp_list must match the # dimensions the constructor just derived, or transform()/the E-step # would fail later with a confusing matmul error far from load(). - if tuple(params["A"].shape) != (self.n_channels, self.n_comps): + if tuple(params["A"].shape) != (self.n_comps, self.n_channels): raise ValueError( f"restored A has shape {tuple(params['A'].shape)}, expected " - f"{(self.n_channels, self.n_comps)} for n_channels=" + f"{(self.n_comps, self.n_channels)} for n_channels=" f"{self.n_channels}, n_models={self.n_models}" ) if tuple(params["comp_list"].shape) != (self.n_channels, self.n_models): diff --git a/validate_implementations.py b/validate_implementations.py index 0d3c5f76..eeed1c1d 100644 --- a/validate_implementations.py +++ b/validate_implementations.py @@ -588,9 +588,10 @@ def run_numpy_amica( "final_iter": len(model.ll), "W": W, # get_weights() is the stored W transposed (issue #24 convention), so - # the true mixing is the stored A's model-0 columns transposed, the - # same composition AMICATorchNG.get_mixing_matrix returns. - "A": model.A[:, model.comp_list[:, 0]].T, + # the true mixing is the stored A's model-0 component rows transposed + # (issue #334 layout), the same composition + # AMICATorchNG.get_mixing_matrix returns. + "A": model.A[model.comp_list[:, 0], :].T, "ll_history": list(model.ll), "runtime_s": runtime_s, } From 724c39111349d027d0c7ebcf3111054283aa5a64 Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Wed, 23 Sep 2026 05:39:25 -0700 Subject: [PATCH 13/26] Phase 14: reject unknown NumPy options (#347) * Reject unknown AMICA_NumPy keyword arguments AMICA_NumPy(**kwargs) forwarded every keyword into a params dict read with params.get(...), so a typo (max_iters=50) or an option the legacy backend does not implement (keep_best=True) constructed silently and had no effect. __init__ now validates **kwargs before any other processing: an unrecognized name raises TypeError with a difflib-based suggestion, and a name implemented on the PyTorch backend (AMICATorchNG) but not this one raises TypeError naming it and pointing to AMICA(backend='torch'). The accepted-keyword set is derived from _CONSUMED_KEYS/_CANONICAL_TO_NUMPY_KEY (the same source the params-file routing uses) plus this constructor's own named parameters, so the two surfaces cannot drift apart. The canonical spelling of the three renamed settings (min_nd/maxdecs/share_iter) is now also accepted as a keyword argument directly, translated the same way a params file's setting already is; passing both spellings at once raises TypeError. * Add tests for AMICA_NumPy kwargs validation Real construction only, no mocks (no .fit() needed to exercise __init__'s validation): a typo raises TypeError with a suggestion; an unsupported cross-backend option raises the specific message; the three canonical-spelling aliases are accepted and their conflict is rejected; every documented NumPy option constructs, alone and all together; and a source scan proves _CONSUMED_KEYS matches every params.get(...)/raw_params.get(...) read site exactly, so the accepted-kwargs set cannot silently drift from what __init__ actually reads. * Document AMICA_NumPy kwargs rejection behavior change docs/changelog.md: Unreleased entry noting the legacy NumPy backend behavior change (unknown or unsupported options now raise instead of being ignored). docs/api/numpy-backend.md: a short paragraph mirroring the existing pdftype callout. * Improve AMICA_NumPy kwarg validation (review) PR #347 review follow-up, several items together since they touch the same functions: - files/data_dim/field_dim now work as constructor keywords, with the same meaning as in params_file, instead of passing the accepted- kwargs check but staying silently inert (fit() with no data still said "no 'files' configured" even after a caller set one via a kwarg). Conflicting values between params_file and a kwarg raise; the same value from both does not. - n_models/n_mix (AMICATorchNG's spelling of this backend's own num_models/num_mix) get a message naming the correct spelling instead of the generic (and here wrong) torch-only message. - n_channels gets its own message: it is inferred from the data passed to fit() on every backend, not a constructor keyword on any of them (the AMICA wrapper's own fit(**kwargs) excludes it too). - Every offending keyword in one call is named in a single error, however many different categories are mixed together. A first version raised on the first category found, so AMICA_NumPy(n_models=2, totally_bogus=1) named only n_models and dropped totally_bogus. - _torch_only_options() imports AMICATorchNG lazily, inside its own body, called only on the error path, so numpy_impl carries no module-level dependency on torch_impl. - Groups difflib/inspect with the other stdlib imports. - Hardens the source-scan test: fails loudly on a params[...] bracket read or a non-literal params.get(...) key, either of which would be invisible to the scan. - Adds typo-suggestion tests for params_file/use_tqdm alongside verbose. * Fix dropped keyword in AMICA.fit mlx check PR #347 review item 4: the same single-category drop AMICA_NumPy._reject_unknown_kwargs had (issue #346) existed here too -- backend='mlx' with fit(dtype=..., blocksize=...) named only 'dtype' (ValueError) and silently dropped 'blocksize' from the message, since the torch-only check raised before the unknown-keyword check ever ran. Both categories are computed before either is raised; a combination now names both in one TypeError. The two single-category messages are unchanged (still ValueError for torch-only alone, TypeError for unknown alone), so existing callers/tests keep working. * Document NumPy kwargs review follow-up docs/changelog.md: a Review follow-up sub-bullet under the Phase 14 entry. docs/api/numpy-backend.md: mentions files/data_dim/field_dim as keyword arguments and the n_models/n_mix/n_channels-specific messages. --- docs/api/numpy-backend.md | 27 + docs/changelog.md | 33 ++ pamica/amica.py | 39 +- pamica/numpy_impl/core.py | 324 +++++++++++- pamica/tests/test_numpy_kwargs.py | 704 ++++++++++++++++++++++++++ pamica/tests/test_wrapper_backends.py | 20 + 6 files changed, 1116 insertions(+), 31 deletions(-) create mode 100644 pamica/tests/test_numpy_kwargs.py diff --git a/docs/api/numpy-backend.md b/docs/api/numpy-backend.md index fe2e4695..16a568e4 100644 --- a/docs/api/numpy-backend.md +++ b/docs/api/numpy-backend.md @@ -25,4 +25,31 @@ This backend implements only the generalized-Gaussian source density the backend-differences table in [amica-differences.md](../guides/amica-differences.md). +`AMICA_NumPy(**kwargs)` rejects a keyword argument it does not recognize +(issue #346), before any other construction runs. +An unrecognized name raises `TypeError`, +with a `difflib`-based "did you mean" suggestion when a close match exists. +An option implemented on the PyTorch backend (`AMICATorchNG`) but not this one +(for example `keep_best`, `device`, `dtype`, the kurtosis-switch schedule) +also raises `TypeError`, naming the option and pointing to `AMICA(backend='torch')`. +`n_models`/`n_mix` (`AMICATorchNG`'s own spelling of this backend's +`num_models`/`num_mix`) get a message naming the correct spelling instead; +`n_channels` gets its own message, since it is inferred from the data passed +to `fit()` on every backend, torch included, not a constructor keyword on +any of them. +Every offending keyword is named in one error, however many different kinds +are mixed in the same call. +The three settings this backend spells differently from `AMICATorchNG` +(`min_nd`/`maxdecs`/`share_iter`) are accepted under either spelling, +the same as a params file already resolves them; +passing both spellings of the same setting at once raises `TypeError`. + +`files`, `data_dim` and `field_dim` -- data-location metadata `fit()` reads +when called with no data -- are also accepted as keyword arguments directly, +with the same meaning as in a params file, not only through `params_file=...` +(issue #346). +Setting both `params_file` and a keyword argument to a *different* value for +the same one of these three raises `TypeError`; the same value from both is +not a conflict. + ::: pamica.AMICA_NumPy diff --git a/docs/changelog.md b/docs/changelog.md index ccbb5228..aeacd0bd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,39 @@ Release notes are also published on the ## Unreleased +- **Phase 14 of epic #324: `AMICA_NumPy` rejects unknown/unsupported keyword arguments (issue #346).** + `AMICA_NumPy(**kwargs)` used to forward every keyword into a params dict read with `params.get(...)`, + so a typo (`max_iters=50`) or an option the legacy backend does not implement (`keep_best=True`) + constructed silently and had no effect, + unlike the PyTorch/MLX constructors, + which take explicit keyword parameters and already raised `TypeError` on an unknown name. + - **Behavior change (legacy NumPy backend only): an unrecognized or unsupported keyword argument now raises `TypeError` instead of being silently ignored.** + A typo raises `TypeError` naming the offending keyword(s), + with a `difflib`-based "did you mean" suggestion when a close match exists. + An option implemented on the PyTorch backend (`AMICATorchNG`) but not this one + (for example `keep_best`, `device`, `dtype`, the kurtosis-switch schedule) + raises `TypeError` naming the option and pointing to `AMICA(backend='torch')`. + - The three settings this backend spells differently from `AMICATorchNG` + (`min_nd`/`maxdecs`/`share_iter`, the canonical spelling a params file already resolves either way) + are now also accepted as keyword arguments directly, + translated to this backend's own attribute name the same way the params-file route already translates them. + Passing both spellings of the same setting at once raises `TypeError` naming both, + rather than picking one silently. + - The accepted-keyword set is derived from the same source the params-file routing uses + (`_CONSUMED_KEYS`/`_CANONICAL_TO_NUMPY_KEY`), + and the unsupported-option list is derived from `AMICATorchNG`'s own constructor signature, + so neither can drift from what the constructor actually reads. + - **Review follow-up:** `files`/`data_dim`/`field_dim` (data-location metadata) now work as keyword + arguments directly, with the same meaning as in `params_file`, instead of passing the check but + staying silently inert; `n_models`/`n_mix` (`AMICATorchNG`'s spelling of this backend's own + `num_models`/`num_mix`) get a message naming the correct spelling rather than the generic + (and here wrong) "use `AMICA(backend='torch')`" message; `n_channels` gets its own message, since + it is inferred from the data passed to `fit()` on every backend, not a constructor keyword on any + of them; and every offending keyword in one call is now named in a single error, however many + different kinds are mixed together (a first version named only the first category it found). + The `AMICA` wrapper's own `backend='mlx'` keyword check had the same single-category drop and is + fixed the same way. + - **Fix: tests no longer make a full clone shallow (issue #343).** Tests that load historical code ran `git fetch origin --depth 1` to reach the pinned commit; in a full clone that records a shallow boundary, after which `git gc` can prune history. diff --git a/pamica/amica.py b/pamica/amica.py index 2d88bae6..f80bfa37 100644 --- a/pamica/amica.py +++ b/pamica/amica.py @@ -494,16 +494,35 @@ def fit( _check_backend(self.backend, self.device) backend_cls = _backend_class(self.backend) ctor_params = _ctor_params(backend_cls) - if self.backend == "mlx": - torch_only = sorted(set(kwargs) & set(_TORCH_ONLY_PARAMS)) - if torch_only: - raise ValueError( - f"{torch_only} apply only to backend='torch'. The MLX " - "backend computes in float32 only (Apple GPUs have no " - "float64) on MLX's default device; use backend='torch' " - "for float64 Fortran-parity runs." - ) - unknown = sorted(set(kwargs) - ctor_params) + # Both categories computed before either is raised (PR #347 review + # item 4, the same fix as AMICA_NumPy's _reject_unknown_kwargs): a + # first version of this check raised on torch_only alone, so + # backend='mlx' with fit(dtype=..., blocksize=...) named only + # 'dtype' and silently dropped 'blocksize' from the message. + # unknown excludes torch_only so a torch-only name (which is not in + # ctor_params for backend='mlx' either) is not double-counted. + torch_only = ( + sorted(set(kwargs) & set(_TORCH_ONLY_PARAMS)) + if self.backend == "mlx" + else [] + ) + unknown = sorted(set(kwargs) - ctor_params - set(torch_only)) + if torch_only and unknown: + raise TypeError( + f"AMICA.fit got invalid keyword argument(s): {torch_only} " + "apply only to backend='torch' (the MLX backend computes in " + "float32 only, on its default device; use backend='torch' " + f"for float64 Fortran-parity runs), and {unknown} is " + "neither a fit() parameter nor a constructor keyword of " + f"{backend_cls.__name__}." + ) + if torch_only: + raise ValueError( + f"{torch_only} apply only to backend='torch'. The MLX " + "backend computes in float32 only (Apple GPUs have no " + "float64) on MLX's default device; use backend='torch' " + "for float64 Fortran-parity runs." + ) if unknown: raise TypeError( f"AMICA.fit got unexpected keyword argument(s) {unknown}: " diff --git a/pamica/numpy_impl/core.py b/pamica/numpy_impl/core.py index 46f93eaa..384156ce 100644 --- a/pamica/numpy_impl/core.py +++ b/pamica/numpy_impl/core.py @@ -82,14 +82,19 @@ Component Analyzers with Shared Components." 2012. """ -import numpy as np -from scipy import linalg -from scipy.special import digamma +import difflib +import functools +import inspect import logging import time from pathlib import Path from typing import Dict, List, Optional, Tuple, Union + +import numpy as np +from scipy import linalg +from scipy.special import digamma from tqdm import tqdm + from .. import blocktune from .. import restarts from .. import schedule @@ -201,6 +206,200 @@ } ) +# The canonical spelling of the three settings _CANONICAL_TO_NUMPY_KEY renames +# (min_nd, maxdecs, share_iter -- AMICATorchNG's own constructor spelling, and +# the name a params file's JSON_ALIAS_TO_CANONICAL/FORTRAN_TO_PAMICA_KEY +# resolve to). The params-file route already accepts either spelling and +# lands on the same setting (_read_numpy_keyed_params translates it); a +# keyword argument now does too (issue #346), so passing the canonical name +# directly -- as AMICATorchNG itself spells it -- is not treated as unknown. +# Passing both spellings of the same setting at once is ambiguous, so +# __init__ rejects that the same way fortran_params._apply_json_aliases +# rejects it for a params file (see _translate_canonical_kwargs). +_CANONICAL_ALIAS_KWARGS = frozenset(_CANONICAL_TO_NUMPY_KEY) + +# This constructor's own explicit (non-**kwargs) parameters. They can never +# actually land inside **kwargs when spelled correctly -- Python binds a +# matching keyword to the named parameter directly -- but are included in +# _ACCEPTED_KWARGS below so a typo of one (e.g. ``verbos=True``) is named and +# gets a "did you mean" suggestion instead of silently vanishing into the +# ignored params dict, which is exactly the failure mode issue #346 is about. +_CONSTRUCTOR_ONLY_KWARGS = frozenset({"params_file", "use_tqdm", "verbose"}) + +# Every keyword name AMICA_NumPy(**kwargs) accepts (issue #346). Derived from +# the same _CONSUMED_KEYS/_CANONICAL_TO_NUMPY_KEY the params-file routing +# uses, plus this constructor's own named parameters, so the kwargs-accepted +# set and the params-file-accepted set cannot drift apart. +_ACCEPTED_KWARGS = _CONSUMED_KEYS | _CANONICAL_ALIAS_KWARGS | _CONSTRUCTOR_ONLY_KWARGS + +# AMICATorchNG constructor names that mean the same setting as one of this +# backend's own names, just spelled differently -- not "unsupported" (PR +# #347 review): the generic torch-only message ("use AMICA(backend='torch')") +# would be actively wrong here, since this backend *does* support the +# setting, just under a different name. Keyed by the torch spelling, valued +# by this backend's spelling. +_ALTERNATE_SPELLING = { + "n_models": "num_models", + "n_mix": "num_mix", +} + +# n_channels gets its own message rather than landing in _ALTERNATE_SPELLING +# or the torch-only bucket below: it is not a settable hyperparameter on any +# backend's public fit path, torch included. AMICATorchNG's raw constructor +# takes it, but the AMICA wrapper's own fit(**kwargs) deliberately excludes +# it (amica.py's _ctor_params subtracts {"n_channels", "n_models", "n_mix", +# "device"} -- the wrapper infers it from the data and passes it itself), so +# "use AMICA(backend='torch')" would be wrong. This backend infers it from +# the data the same way. +_N_CHANNELS_OPTION = "n_channels" + + +@functools.cache +def _torch_only_options() -> frozenset: + """Constructor keywords ``AMICATorchNG`` accepts that this backend + genuinely does not implement at all (PR #347 review item 2/5) -- not a + spelling difference (``_ALTERNATE_SPELLING``) or a shape difference + (``_N_CHANNELS_OPTION``), which get their own messages in + ``_reject_unknown_kwargs`` instead of this one: + + - ``device``: this backend always runs on CPU; there is no device to + select. + - ``dtype``: this backend always computes in float64; there is no dtype + to choose. + - ``keep_best``: the best-iterate restore (issue #51) is not ported to + this backend's fit loop. + - ``kurt_start``/``num_kurt``/``kurt_int``: the kurtosis-switch schedule + for the extended-Infomax adaptive pdf family (``pdftype=1``) -- this + backend implements only the generalized-Gaussian density + (``pdftype=0``), so the switch schedule has nothing to switch (see the + ``pdftype != 0`` guard below). + + Computed from ``AMICATorchNG``'s own signature minus everything else + this backend recognizes, so it stays in sync with that constructor + instead of drifting as options are added there (rather than a hand- + picked list -- the assertion in ``TestKeySetMatchesReadSites`` that every + member here really has no NumPy equivalent is the actual anti-drift + check; this docstring is not it). + + Imports ``AMICATorchNG`` lazily and only on the error path (PR #347 + review item 5): ``__init__`` calls this only after already finding an + unrecognized keyword, so a normal, valid construction of + ``AMICA_NumPy`` never imports ``torch_impl`` -- ``numpy_impl`` keeps no + module-level dependency on the PyTorch backend. ``functools.cache`` + means the import, and the signature introspection, run at most once per + process. + """ + from ..torch_impl.core import AMICATorchNG + + return ( + frozenset(inspect.signature(AMICATorchNG).parameters) + - _ACCEPTED_KWARGS + - frozenset(_ALTERNATE_SPELLING) + - {_N_CHANNELS_OPTION} + ) + + +def _reject_unknown_kwargs(kwargs: Dict) -> None: + """Reject keyword arguments ``AMICA_NumPy(**kwargs)`` does not recognize + (issue #346), before any other constructor processing runs. + + Every offending name is categorized -- alternate spelling + (``_ALTERNATE_SPELLING``), ``n_channels``, genuinely torch-only + (``_torch_only_options()``), or unrecognized -- *before* anything is + raised, and every category present is named in ONE ``TypeError`` (PR + #347 review item 3): a first version of this function raised on the + first category it found, so ``AMICA_NumPy(n_models=2, totally_bogus=1)`` + named only ``n_models`` and silently dropped ``totally_bogus`` from the + message, which is the same silent-drop failure mode issue #346 itself is + about, just one layer in. + + ``TypeError`` throughout (matching the ``AMICA.fit`` cross-backend + keyword check in ``pamica/amica.py``'s single-category branches: every + unrecognized name here is a keyword argument this callable does not + accept, which is what ``TypeError`` means in Python's own calling + convention). Distinct from the existing ``pdftype != 0`` + ``NotImplementedError`` (a *value* of an accepted keyword this backend + cannot honor, not an unrecognized keyword). + + An unrecognized name with no close match, or one that is not one of the + special cases above, gets a "did you mean" suggestion + (``difflib.get_close_matches``) against every name this constructor does + accept, when one exists. + """ + unknown = sorted(set(kwargs) - _ACCEPTED_KWARGS) + if not unknown: + return + + alt_spelling = [name for name in unknown if name in _ALTERNATE_SPELLING] + n_channels = [name for name in unknown if name == _N_CHANNELS_OPTION] + torch_only_names = _torch_only_options() + torch_only = [name for name in unknown if name in torch_only_names] + categorized = set(alt_spelling) | set(n_channels) | set(torch_only) + remaining = [name for name in unknown if name not in categorized] + + segments = [] + if alt_spelling: + renames = ", ".join( + f"{name!r} -> {_ALTERNATE_SPELLING[name]!r}" for name in alt_spelling + ) + segments.append( + f"{alt_spelling} name the same setting AMICATorchNG spells that " + f"way; this backend uses its own name instead ({renames})" + ) + if n_channels: + segments.append( + "'n_channels' is inferred from the data passed to fit() on " + "every pamica backend, this one included; it is not a " + "constructor keyword here, nor on AMICA(backend='torch') " + "(AMICA.fit's own **kwargs surface excludes it too, for the " + "same reason)" + ) + if torch_only: + segments.append( + f"{torch_only} implemented on the PyTorch backend (AMICATorchNG) " + "but not the legacy NumPy backend; use AMICA(backend='torch') " + "for these options" + ) + if remaining: + hints = [] + for name in remaining: + match = difflib.get_close_matches(name, _ACCEPTED_KWARGS, n=1) + hints.append( + f"{name!r}" + (f" (did you mean {match[0]!r}?)" if match else "") + ) + segments.append( + f"unexpected keyword argument(s) {remaining}: {'; '.join(hints)}" + ) + + raise TypeError( + "AMICA_NumPy got invalid keyword argument(s): " + "; ".join(segments) + ) + + +def _translate_canonical_kwargs(kwargs: Dict) -> Dict: + """Map a keyword argument spelled with the canonical name + (``min_nd``/``maxdecs``/``share_iter``) to this backend's own attribute + name (``_CANONICAL_TO_NUMPY_KEY``), the same translation + ``_read_numpy_keyed_params`` already applies to a params file. + + Passing both spellings of the same setting at once is ambiguous -- which + one would silently win is an arbitrary choice -- so, mirroring + ``fortran_params._apply_json_aliases``'s identical guard for a params + file, this raises rather than picking one. + """ + conflicts = sorted( + (canonical, numpy_key) + for canonical, numpy_key in _CANONICAL_TO_NUMPY_KEY.items() + if canonical in kwargs and numpy_key in kwargs + ) + if conflicts: + names = ", ".join(f"{c!r}/{n!r}" for c, n in conflicts) + raise TypeError( + f"AMICA_NumPy got both the canonical and this backend's own " + f"spelling of the same setting ({names}); keep only one." + ) + return {_CANONICAL_TO_NUMPY_KEY.get(k, k): v for k, v in kwargs.items()} + def _read_numpy_keyed_params(params_file: Optional[Union[str, Path]]) -> Dict: """Parse a params file and map its canonical keys to this backend's own @@ -343,7 +542,47 @@ def __init__( writes no files at all, like the PyTorch and MLX backends; set it (here, in a params file, or with the CLI's ``--outdir``) to get the reference's on-disk output. + + ``min_nd``, ``maxdecs`` and ``share_iter`` -- the canonical + spelling of ``min_grad_norm``, ``max_decs`` and ``share_int`` + (this backend's own names), which matches ``AMICATorchNG``'s + constructor and the spelling a params file already resolves + either way -- are also accepted here directly (issue #346) and + translated to this backend's own attribute. Passing both + spellings of the same setting at once raises ``TypeError`` + rather than silently picking one. + + ``files``, ``data_dim`` and ``field_dim`` -- data-location + metadata (used by :meth:`fit` when it is called with no data), + normally read from ``params_file`` -- are also accepted here + directly, with the same meaning (issue #346). Setting both + ``params_file`` and a keyword argument to a *different* value + for the same one of these three raises ``TypeError``; the same + value from both is not a conflict. + + Raises + ------ + TypeError + If ``**kwargs`` holds a name this constructor does not recognize + (issue #346), or both ``params_file`` and a keyword argument set + ``files``/``data_dim``/``field_dim`` to different values. An + unrecognized name gets one of: the correct name, for + ``n_models``/``n_mix`` (this backend's own spelling of the same + setting); an explanation, for ``n_channels`` (inferred from the + data passed to :meth:`fit` on every backend, not a constructor + keyword on any of them); a name implemented on the PyTorch + backend but not this one (e.g. ``keep_best``, ``device``, + ``dtype``), pointing to ``AMICA(backend='torch')``; or a generic + "unexpected keyword argument" message with a ``difflib``-based + "did you mean" suggestion when one exists. Every offending name + is checked and named in ONE error, whatever mix of these applies. + Checked before any other keyword is applied, so a typo (e.g. + ``max_iters=50``) can no longer construct silently. """ + # Reject an unrecognized keyword before anything else runs (issue + # #346): a typo or a cross-backend-only option used to construct + # silently and have no effect. + _reject_unknown_kwargs(kwargs) # Store progress bar settings self.use_tqdm = use_tqdm self.verbose = verbose @@ -369,8 +608,12 @@ def __init__( unconsumed, ) - # Override with any provided parameters - params.update(kwargs) + # Override with any provided parameters. A canonical-spelling kwarg + # (min_nd/maxdecs/share_iter) is translated to this backend's own + # name first, exactly as a params file's setting already is (issue + # #346), so it takes effect instead of landing under a key nothing + # reads. + params.update(_translate_canonical_kwargs(kwargs)) # Store parameters self.num_models = params.get("num_models", 1) @@ -546,17 +789,54 @@ def __init__( self.outdir: Optional[Path] = None if outdir is None else Path(outdir) # Data-source config (used by fit() when called without explicit - # data), read off the same parsed dict as the hyperparameters above - # (issue #304: no second file read) -- files/data_dim/field_dim are - # data-specific, not hyperparameters, so load_default_params()/the - # _DATA_LOCATION_KEYS filter above strips them from `params`. - self._config_files = None - self._config_data_dim = None - self._config_field_dim = None - if params_file is not None: - self._config_files = raw_params.get("files") - self._config_data_dim = raw_params.get("data_dim") - self._config_field_dim = raw_params.get("field_dim") + # data): files/data_dim/field_dim are data-specific, not + # hyperparameters, so load_default_params()/the _DATA_LOCATION_KEYS + # filter above strips them out of `params`, and **kwargs is never + # routed through `params`/`raw_params` for these three -- they are + # read directly off `raw_params` (a params file, issue #304: no + # second file read) and off `kwargs` itself here instead. Accepting + # them as keyword arguments directly, with the same meaning as in a + # params file, is PR #347 review item 1: before this, a kwarg of one + # of these three names passed the unknown-kwarg check (they are in + # _CONSUMED_KEYS/_ACCEPTED_KWARGS) but was silently inert, so + # fit()'s no-data path still said "no 'files' configured" even + # though the caller had just set one. Setting both a params file and + # a keyword argument to different values is ambiguous, so this + # raises rather than picking one, following the same precedent as + # the min_nd/maxdecs/share_iter alias conflict above + # (_translate_canonical_kwargs) -- unlike that guard, this one is + # "different values", not "either given", since a params file and an + # explicit kwarg agreeing is not a conflict. + file_files = raw_params.get("files") if params_file is not None else None + file_data_dim = raw_params.get("data_dim") if params_file is not None else None + file_field_dim = ( + raw_params.get("field_dim") if params_file is not None else None + ) + kwarg_files = kwargs.get("files") + kwarg_data_dim = kwargs.get("data_dim") + kwarg_field_dim = kwargs.get("field_dim") + for name, from_file, from_kwarg in ( + ("files", file_files, kwarg_files), + ("data_dim", file_data_dim, kwarg_data_dim), + ("field_dim", file_field_dim, kwarg_field_dim), + ): + if ( + from_file is not None + and from_kwarg is not None + and from_file != from_kwarg + ): + raise TypeError( + f"AMICA_NumPy got {name!r} from both params_file and a " + f"keyword argument, with different values ({from_file!r} " + f"vs {from_kwarg!r}); keep only one." + ) + self._config_files = kwarg_files if kwarg_files is not None else file_files + self._config_data_dim = ( + kwarg_data_dim if kwarg_data_dim is not None else file_data_dim + ) + self._config_field_dim = ( + kwarg_field_dim if kwarg_field_dim is not None else file_field_dim + ) # Initialize random state self.seed = params.get("seed") @@ -900,14 +1180,16 @@ def fit(self, data: Optional[np.ndarray] = None) -> "AMICA": if data is None: if not self._config_files: raise ValueError( - "No data provided and no 'files' configured in params_file; " - "either pass data explicitly or set 'files'/'data_dim'/'field_dim'." + "No data provided and no 'files' configured (via " + "params_file or as a keyword argument); either pass " + "data explicitly or set 'files'/'data_dim'/'field_dim'." ) if self._config_data_dim is None or self._config_field_dim is None: raise ValueError( - "No data provided and 'data_dim'/'field_dim' are not both " - "configured in params_file; either pass data explicitly or " - "set 'files'/'data_dim'/'field_dim'." + "No data provided and 'data_dim'/'field_dim' are not " + "both configured (via params_file or as a keyword " + "argument); either pass data explicitly or set " + "'files'/'data_dim'/'field_dim'." ) if len(self._config_files) != len(self._config_field_dim): raise ValueError( diff --git a/pamica/tests/test_numpy_kwargs.py b/pamica/tests/test_numpy_kwargs.py new file mode 100644 index 00000000..6c0dd309 --- /dev/null +++ b/pamica/tests/test_numpy_kwargs.py @@ -0,0 +1,704 @@ +"""AMICA_NumPy(**kwargs) rejects unknown/unsupported keywords (issue #346). + +Before this fix ``AMICA_NumPy(**kwargs)`` forwarded every keyword into a +params dict read with ``params.get(...)``, so a typo (``max_iters=50``) or an +option the legacy backend does not implement (``keep_best=True``) constructed +silently and had no effect -- unlike the PyTorch/MLX constructors (explicit +keyword parameters, ``TypeError`` on an unknown name) and the params-file +reader (``pamica.fortran_params.read_params_file``, issue #304), which was +already loud about a setting it could not apply. + +``pamica.numpy_impl.core.__init__`` now validates ``**kwargs`` before any +other processing (``_reject_unknown_kwargs``). Every offending name is +categorized -- an alternate spelling of a setting this backend does support +(``n_models``/``n_mix``), ``n_channels`` (inferred from data on every +backend, not a constructor keyword on any of them), genuinely torch-only +(``keep_best``, ``device``, ``dtype``, the kurtosis-switch schedule), or +unrecognized with a ``difflib``-based "did you mean" suggestion -- and every +category present is named in ONE ``TypeError`` (PR #347 review item 3: a +first version of this raised on the first category it found, so +``AMICA_NumPy(n_models=2, totally_bogus=1)`` named only ``n_models`` and +dropped ``totally_bogus``). + +The three settings this backend spells differently from ``AMICATorchNG`` +(``min_nd``/``maxdecs``/``share_iter``, the canonical spelling a params file +already resolves either way) are also accepted as keyword arguments +directly, translated to this backend's own attribute name; passing both +spellings of the same setting at once is rejected as ambiguous, mirroring +``fortran_params._apply_json_aliases``'s identical guard for a params file. +``files``/``data_dim``/``field_dim`` (data-location metadata, normally read +from ``params_file``) are also accepted directly, with the same meaning (PR +#347 review item 1); setting both a params file and a keyword argument to +different values is rejected the same way. + +No mocks: every construction below is a real ``AMICA_NumPy(...)`` call. Most +need no data (no ``.fit()``, exercising only ``__init__``'s validation); the +``TestDataLocationKwargs`` class fits the real bundled sample EEG +(``pamica/sample_data/eeglab_data.fdt``, 32 channels x 30504 frames), which +``test_numpy_params_file.py``/``test_numpy_cli.py`` also use. +""" + +from __future__ import annotations + +import inspect +import json +import re +from pathlib import Path +from typing import Any, Dict + +import numpy as np +import pytest + +from pamica import AMICA_NumPy +from pamica.numpy_impl import core as numpy_core +from pamica.numpy_impl.core import ( + _ACCEPTED_KWARGS, + _ALTERNATE_SPELLING, + _CANONICAL_ALIAS_KWARGS, + _CANONICAL_TO_NUMPY_KEY, + _CONSTRUCTOR_ONLY_KWARGS, + _CONSUMED_KEYS, + _N_CHANNELS_OPTION, + _torch_only_options, +) +from pamica.torch_impl.core import AMICATorchNG + +_CORE_SOURCE = Path(numpy_core.__file__).resolve() +_SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +_DATA_FILE = _SAMPLE_DIR / "eeglab_data.fdt" +_NW = 32 +_FIELD = 30504 + + +def _construct(**kwargs: Any) -> AMICA_NumPy: + """Build ``AMICA_NumPy`` from a keyword dict whose keys are only known at + runtime (a parametrized name, or a loop variable). Typed ``**kwargs: Any`` + -- the same pattern ``test_schedule_gates.py``'s/``test_doscaling_rows.py``'s + own ``_construct`` already uses -- so a static checker does not + speculatively match a computed key against one of this constructor's own + explicitly named parameters (``params_file``/``use_tqdm``/``verbose``) + just because it cannot prove the key is not one of those names.""" + return AMICA_NumPy(**kwargs) + + +# --- Gate: the accepted-kwargs set cannot drift from the read sites -------- + + +class TestKeySetMatchesReadSites: + """``_CONSUMED_KEYS`` is exactly the set of keys ``__init__`` reads + (``params.get(...)``/``raw_params.get(...)``), a source scan rather than a + hand-maintained duplicate list, so the two cannot silently drift apart.""" + + def _init_body(self) -> str: + src = _CORE_SOURCE.read_text() + start = src.index(" def __init__(") + end = src.index(" def _setup_logging") + return src[start:end] + + def test_every_consumed_key_is_read_and_vice_versa(self): + """Depends entirely on every hyperparameter read in ``__init__`` + being spelled as a literal-string ``params.get("key")`` / + ``raw_params.get("key")`` call -- the only shape the regexes below + can extract a key name from. ``files``/``data_dim``/``field_dim`` + are the one exception, read off ``kwargs``/``raw_params`` directly + (not through ``params.get``, see PR #347 review item 1) and + exempted explicitly below, since they are already asserted present + in ``_CONSUMED_KEYS`` by construction (the frozenset literal).""" + body = self._init_body() + # raw_params.get(...) first, so its matches can be excluded from the + # plain params.get(...) scan below (the latter's regex would otherwise + # also match inside "raw_params.get(...)"). + raw_keys = set( + re.findall(r"raw_params\.get\(\s*['\"]([a-zA-Z0-9_]+)['\"]", body) + ) + plain_body = body.replace("raw_params.get(", "") + plain_keys = set( + re.findall(r"params\.get\(\s*['\"]([a-zA-Z0-9_]+)['\"]", plain_body) + ) + read_keys = raw_keys | plain_keys + + assert read_keys, "source scan found no params.get(...) calls at all" + exempt = {"files", "data_dim", "field_dim"} + assert read_keys - _CONSUMED_KEYS == set(), ( + f"read but not in _CONSUMED_KEYS: {sorted(read_keys - _CONSUMED_KEYS)}" + ) + assert _CONSUMED_KEYS - read_keys - exempt == set(), ( + "in _CONSUMED_KEYS but never read via params.get(...)/" + f"raw_params.get(...): {sorted(_CONSUMED_KEYS - read_keys - exempt)}" + ) + + def test_no_bracket_or_non_literal_reads_are_hiding_from_the_scan(self): + """PR #347 review item 6: the scan above is blind to a hyperparameter + read as ``params["key"]``/``raw_params["key"]`` (bracket access) or + as ``params.get(some_variable)`` (a non-literal key) -- either shape + would silently understate what ``_CONSUMED_KEYS`` actually covers, + with the scan above passing regardless. This fails loudly, naming + the offending snippet, if ``__init__`` ever reads a hyperparameter + that way instead of a literal ``params.get("key")``.""" + body = self._init_body() + + bracket_reads = re.findall(r"\b(?:raw_)?params\[[^\]]*\]", body) + assert not bracket_reads, ( + "__init__ reads params[...]/raw_params[...] by bracket access, " + f"invisible to the source-scan test above: {bracket_reads}. " + "Convert to params.get('key') or extend the scan." + ) + + # Every params.get(/raw_params.get( call site, regardless of its + # argument's shape, so a non-literal (computed/variable) key is + # caught even though the literal-key regexes above cannot extract + # its name at all. + for match in re.finditer(r"\b(raw_)?params\.get\(\s*(['\"]?)", body): + quote = match.group(2) + if quote not in ("'", '"'): + snippet = body[match.start() : match.start() + 60] + pytest.fail( + "__init__ has a params.get(...)/raw_params.get(...) call " + "whose key is not a literal string, invisible to the " + f"source-scan test above: {snippet!r}" + ) + + def test_accepted_kwargs_is_the_union_of_its_declared_sources(self): + """``_ACCEPTED_KWARGS`` cannot drift from its three declared sources + (issue #346): params-file keys, the two canonical aliases, and this + constructor's own named parameters.""" + assert _ACCEPTED_KWARGS == ( + _CONSUMED_KEYS | _CANONICAL_ALIAS_KWARGS | _CONSTRUCTOR_ONLY_KWARGS + ) + assert _CANONICAL_ALIAS_KWARGS == {"min_nd", "maxdecs", "share_iter"} + assert _CONSTRUCTOR_ONLY_KWARGS == {"params_file", "use_tqdm", "verbose"} + + def test_torch_only_options_derived_from_torch_signature(self): + """``_torch_only_options()`` is mechanically ``AMICATorchNG``'s own + signature minus what this backend accepts (directly, or under an + alternate spelling/``n_channels``'s own message), so it stays in + sync as ``AMICATorchNG`` gains options, rather than a hand-picked + list that can go stale.""" + expected = ( + frozenset(inspect.signature(AMICATorchNG).parameters) + - _ACCEPTED_KWARGS + - frozenset(_ALTERNATE_SPELLING) + - {_N_CHANNELS_OPTION} + ) + assert _torch_only_options() == expected + # Sanity: the options issue #346 names explicitly are really in it. + for name in ("keep_best", "device", "dtype"): + assert name in _torch_only_options() + # And nothing this backend actually does accept, under any name, + # leaked in. + assert _torch_only_options().isdisjoint(_ACCEPTED_KWARGS) + assert _torch_only_options().isdisjoint(_ALTERNATE_SPELLING) + assert _N_CHANNELS_OPTION not in _torch_only_options() + + def test_numpy_impl_core_has_no_module_level_torch_import(self): + """PR #347 review item 5: ``_torch_only_options()`` imports + ``AMICATorchNG`` lazily, inside its own function body, called only + on the error path -- so ``numpy_impl/core.py``'s own source carries + no module-level (top-of-file) dependency on ``torch_impl``. + + Not a runtime "torch never gets imported" check: ``import pamica`` + already imports ``torch_impl`` unconditionally, via ``amica.py`` + (the default-backend wrapper), regardless of anything in + ``numpy_impl`` -- so that claim would be false to test. What issue + #346/PR #347 actually changed is this module's OWN import graph: + before, ``from ..torch_impl.core import AMICATorchNG`` sat at the + top of this file; now the only mention of ``torch_impl`` anywhere + in the module is the deferred import inside + ``_torch_only_options``'s body, which this scans for directly.""" + src = _CORE_SOURCE.read_text() + fn_start = src.index("def _torch_only_options") + # Everything before _torch_only_options's own definition: the + # file's real header import block plus every module-level constant + # declared above it -- no "from ..torch_impl..." belongs there. + header = src[:fn_start] + top_level_torch_imports = [ + line + for line in header.splitlines() + if "torch_impl" in line and not line.strip().startswith("#") + ] + assert not top_level_torch_imports, ( + "numpy_impl/core.py imports torch_impl before _torch_only_options " + f"is even defined (i.e. at module level): {top_level_torch_imports}" + ) + # The one place torch_impl is actually imported: inside + # _torch_only_options's own body, not at module level. + fn_end = src.index("\ndef _reject_unknown_kwargs") + assert "from ..torch_impl.core import AMICATorchNG" in src[fn_start:fn_end] + # And nothing else in the whole file (a second, still-top-level + # occurrence elsewhere) imports it either. + rest = src[fn_end:] + assert "import" not in "\n".join( + line for line in rest.splitlines() if "torch_impl" in line + ) + + # Reason NumPy has no equivalent, one entry per name _torch_only_options() + # currently derives (PR #347 review item 2): a hardcoded, reasoned + # mapping checked against the mechanical derivation, so a name added to + # AMICATorchNG that this backend still lacks is caught with an + # explanation to update here, rather than silently absorbed into + # "torch-only" by the set difference alone. + _TORCH_ONLY_REASONS = { + "device": "this backend always runs on CPU; there is no device to select", + "dtype": "this backend always computes in float64; there is no dtype to choose", + "keep_best": ( + "the best-iterate restore (issue #51) is not ported to this " + "backend's fit loop" + ), + "kurt_start": ( + "the kurtosis-switch schedule (extended-Infomax adaptive pdf, " + "pdftype=1) has nothing to switch: this backend implements only " + "the generalized-Gaussian density, pdftype=0" + ), + "num_kurt": "the same kurt_start schedule, and reason, as above", + "kurt_int": "the same kurt_start schedule, and reason, as above", + } + + def test_every_torch_only_option_has_a_reason_and_no_numpy_equivalent(self): + assert set(self._TORCH_ONLY_REASONS) == _torch_only_options() + for name in self._TORCH_ONLY_REASONS: + assert name not in _CONSUMED_KEYS, ( + f"{name!r} has a NumPy-side consumer (params.get); it is not " + "torch-only, and belongs off this list" + ) + assert name not in _ALTERNATE_SPELLING + assert name != _N_CHANNELS_OPTION + + +# --- Gate: a typo raises TypeError with a suggestion ------------------------ + + +class TestTypoRaises: + def test_typo_raises_type_error_with_suggestion(self): + with pytest.raises(TypeError) as exc: + AMICA_NumPy(max_iters=50, use_tqdm=False) + message = str(exc.value) + assert "AMICA_NumPy got invalid keyword argument(s)" in message + assert "unexpected keyword argument(s)" in message + assert "max_iters" in message + assert "max_iter" in message # the suggested close match + + @pytest.mark.parametrize( + ("typo", "correct"), + [ + ("verbos", "verbose"), + ("param_file", "params_file"), + ("use_tqdmm", "use_tqdm"), + ], + ) + def test_typo_of_a_named_constructor_parameter_is_suggested(self, typo, correct): + """``verbose``/``use_tqdm``/``params_file`` never legitimately reach + **kwargs (Python binds a correctly spelled one directly), but a typo + of one of them does, and should be named rather than silently + vanishing into the ignored params dict (PR #347 review item 8 adds + params_file/use_tqdm alongside the original verbose coverage).""" + with pytest.raises(TypeError, match=correct): + _construct(**{typo: True}) + + def test_unknown_keyword_with_no_close_match_still_raises(self): + with pytest.raises(TypeError, match="unexpected keyword argument"): + AMICA_NumPy(totally_unrelated_nonsense_xyz=1, use_tqdm=False) + + def test_multiple_unknown_keywords_all_named(self): + with pytest.raises(TypeError) as exc: + AMICA_NumPy(max_iters=50, lrates=0.1, use_tqdm=False) + message = str(exc.value) + assert "max_iters" in message + assert "lrates" in message + + +# --- Gate: an unsupported cross-backend option raises the specific message -- + + +class TestUnsupportedCrossBackendOption: + @pytest.mark.parametrize("name", sorted(_torch_only_options())) + def test_torch_only_option_raises_specific_message(self, name): + with pytest.raises(TypeError) as exc: + _construct(use_tqdm=False, **{name: object()}) + message = str(exc.value) + assert "implemented on the PyTorch backend (AMICATorchNG)" in message + assert "but not the legacy NumPy backend" in message + assert name in message + assert "AMICA(backend='torch')" in message + + def test_keep_best_specifically(self): + """The option issue #346 names by example.""" + with pytest.raises(TypeError, match=r"keep_best.*AMICA\(backend='torch'\)"): + AMICA_NumPy(keep_best=True, use_tqdm=False) + + def test_device_and_dtype_specifically(self): + with pytest.raises(TypeError, match="device"): + AMICA_NumPy(device="cpu", use_tqdm=False) + with pytest.raises(TypeError, match="dtype"): + AMICA_NumPy(dtype="float64", use_tqdm=False) + + +# --- Gate: alternate spellings and n_channels get their own messages ------- +# (PR #347 review item 2: n_models/n_mix used to get the generic torch-only +# "use AMICA(backend='torch')" message, which is actively wrong -- this +# backend *does* support the setting, under its own name.) + + +class TestAlternateSpellingAndNChannels: + @pytest.mark.parametrize( + ("torch_name", "numpy_name"), + [("n_models", "num_models"), ("n_mix", "num_mix")], + ) + def test_alternate_spelling_names_the_correct_key(self, torch_name, numpy_name): + with pytest.raises(TypeError) as exc: + _construct(use_tqdm=False, **{torch_name: 2}) + message = str(exc.value) + assert torch_name in message + assert numpy_name in message + # Not the generic torch-only message: this setting IS supported. + assert "AMICA(backend='torch')" not in message + + def test_n_models_and_n_mix_are_not_torch_only(self): + assert "n_models" not in _torch_only_options() + assert "n_mix" not in _torch_only_options() + assert _ALTERNATE_SPELLING == {"n_models": "num_models", "n_mix": "num_mix"} + + def test_n_channels_gets_its_own_message(self): + with pytest.raises(TypeError) as exc: + AMICA_NumPy(n_channels=32, use_tqdm=False) + message = str(exc.value) + assert "n_channels" in message + assert "inferred from the data" in message + # Not the torch-only message either: pointing to + # AMICA(backend='torch') would be wrong here (that wrapper's own + # fit(**kwargs) excludes n_channels too). + assert "implemented on the PyTorch backend" not in message + + def test_n_channels_is_not_torch_only_or_alternate_spelling(self): + assert _N_CHANNELS_OPTION == "n_channels" + assert "n_channels" not in _torch_only_options() + assert "n_channels" not in _ALTERNATE_SPELLING + + +# --- Gate: every offending category in one call is named in ONE error ------ +# (PR #347 review item 3.) + + +class TestAllCategoriesNamedTogether: + @pytest.mark.parametrize( + ("kwargs", "must_contain"), + [ + ( + {"n_models": 2, "totally_bogus": 1}, + ["n_models", "num_models", "totally_bogus"], + ), + ( + {"n_models": 2, "keep_best": True}, + ["n_models", "num_models", "keep_best"], + ), + ( + {"n_models": 2, "n_channels": 8}, + ["n_models", "num_models", "n_channels", "inferred from the data"], + ), + ( + {"totally_bogus": 1, "keep_best": True}, + ["totally_bogus", "keep_best"], + ), + ( + {"totally_bogus": 1, "n_channels": 8}, + ["totally_bogus", "n_channels", "inferred from the data"], + ), + ( + {"keep_best": True, "n_channels": 8}, + ["keep_best", "n_channels", "inferred from the data"], + ), + ( + { + "n_models": 2, + "n_channels": 8, + "keep_best": True, + "totally_bogus": 1, + }, + ["n_models", "num_models", "n_channels", "keep_best", "totally_bogus"], + ), + ], + ) + def test_every_offending_key_is_named(self, kwargs, must_contain): + with pytest.raises(TypeError) as exc: + _construct(use_tqdm=False, **kwargs) + message = str(exc.value) + for needle in must_contain: + assert needle in message, f"{needle!r} missing from: {message}" + + def test_single_category_messages_are_unchanged_by_the_refactor(self): + """The combined-message machinery must not change what a single + category alone reports (regression guard for the item 3 fix).""" + with pytest.raises(TypeError) as exc: + AMICA_NumPy(keep_best=True, use_tqdm=False) + assert "n_models" not in str(exc.value) + assert "n_channels" not in str(exc.value) + + +# --- Gate: canonical-spelling kwargs are accepted, both-spellings rejected -- + + +class TestCanonicalAliasKwargs: + @pytest.mark.parametrize( + ("canonical", "numpy_key", "value"), + [ + ("min_nd", "min_grad_norm", 5e-6), + ("maxdecs", "max_decs", 7), + ("share_iter", "share_int", 42), + ], + ) + def test_canonical_spelling_takes_effect(self, canonical, numpy_key, value): + model = _construct(use_tqdm=False, **{canonical: value}) + assert getattr(model, numpy_key) == value + + @pytest.mark.parametrize( + ("canonical", "numpy_key"), + [ + ("min_nd", "min_grad_norm"), + ("maxdecs", "max_decs"), + ("share_iter", "share_int"), + ], + ) + def test_both_spellings_at_once_raises(self, canonical, numpy_key): + with pytest.raises( + TypeError, match=f"{canonical}.*{numpy_key}|{numpy_key}.*{canonical}" + ): + _construct(use_tqdm=False, **{canonical: 1, numpy_key: 2}) + + def test_canonical_to_numpy_key_matches_alias_set(self): + assert set(_CANONICAL_TO_NUMPY_KEY) == {"min_nd", "maxdecs", "share_iter"} + assert set(_CANONICAL_TO_NUMPY_KEY.values()) == { + "min_grad_norm", + "max_decs", + "share_int", + } + + +# --- Gate: files/data_dim/field_dim work as keywords, with real data ------- +# (PR #347 review item 1.) + + +@pytest.mark.skipif( + not _DATA_FILE.exists(), reason="bundled sample_data/eeglab_data.fdt missing" +) +class TestDataLocationKwargs: + """Before this fix, ``files``/``data_dim``/``field_dim`` passed the + unknown-kwarg check (already in ``_CONSUMED_KEYS``) but were silently + inert -- only a params file's own setting reached + ``_config_files``/``_config_data_dim``/``_config_field_dim`` -- so + ``fit()`` with no data still said "no 'files' configured" even though + the caller had just set one via a keyword argument. Real bundled sample + EEG only (32 channels x 30504 frames), no synthetic data.""" + + def _minimal_files_params_file(self, tmp_path: Path) -> Path: + payload = {"files": [str(_DATA_FILE)], "data_dim": _NW, "field_dim": [_FIELD]} + path = tmp_path / "files_only.json" + path.write_text(json.dumps(payload)) + return path + + def test_files_kwarg_loads_real_data_and_fits(self, tmp_path): + model = AMICA_NumPy( + files=[str(_DATA_FILE)], + data_dim=_NW, + field_dim=[_FIELD], + max_iter=3, + seed=0, + use_tqdm=False, + outdir=str(tmp_path / "kw_out"), + ) + model.fit() # no data argument: must load via the kwargs above + assert model.converged is True + assert len(model.ll) == 3 + assert np.isfinite(model.ll).all() + + def test_files_kwarg_matches_params_file_bit_for_bit(self, tmp_path): + """Same real data, same hyperparameters, same seed, loaded through + two different surfaces for files/data_dim/field_dim -- everything + else identical, so the two trajectories must be bit-for-bit equal.""" + params_file = self._minimal_files_params_file(tmp_path) + from_file = AMICA_NumPy( + params_file=str(params_file), + max_iter=3, + seed=0, + use_tqdm=False, + outdir=str(tmp_path / "file_out"), + ) + from_file.fit() + + from_kwargs = AMICA_NumPy( + files=[str(_DATA_FILE)], + data_dim=_NW, + field_dim=[_FIELD], + max_iter=3, + seed=0, + use_tqdm=False, + outdir=str(tmp_path / "kw_out"), + ) + from_kwargs.fit() + + assert from_file.converged is True and from_kwargs.converged is True + np.testing.assert_array_equal(from_file.ll, from_kwargs.ll) + assert from_file.A is not None and from_kwargs.A is not None + np.testing.assert_array_equal(from_file.A, from_kwargs.A) + np.testing.assert_array_equal(from_file.mu, from_kwargs.mu) + np.testing.assert_array_equal(from_file.sphere, from_kwargs.sphere) + + @pytest.mark.parametrize( + ("name", "kwarg_value"), + [("files", ["different.fdt"]), ("data_dim", _NW + 1), ("field_dim", [1])], + ) + def test_conflicting_value_with_params_file_raises( + self, tmp_path, name, kwarg_value + ): + params_file = self._minimal_files_params_file(tmp_path) + with pytest.raises(TypeError, match=name): + AMICA_NumPy( + params_file=str(params_file), + use_tqdm=False, + outdir=str(tmp_path / "out"), + **{name: kwarg_value}, + ) + + def test_same_value_from_both_sources_is_not_a_conflict(self, tmp_path): + params_file = self._minimal_files_params_file(tmp_path) + model = AMICA_NumPy( + params_file=str(params_file), + data_dim=_NW, # same value the file already sets + use_tqdm=False, + outdir=str(tmp_path / "out"), + ) + assert model._config_data_dim == _NW + assert model._config_files == [str(_DATA_FILE)] + assert model._config_field_dim == [_FIELD] + + def test_no_files_error_message_mentions_keyword_argument(self, tmp_path): + model = AMICA_NumPy(use_tqdm=False, outdir=str(tmp_path / "out")) + with pytest.raises(ValueError, match="as a keyword argument"): + model.fit() + + def test_no_data_dim_error_message_mentions_keyword_argument(self, tmp_path): + model = AMICA_NumPy( + files=[str(_DATA_FILE)], use_tqdm=False, outdir=str(tmp_path / "out") + ) + with pytest.raises(ValueError, match="as a keyword argument"): + model.fit() + + +# --- Gate: every documented NumPy option constructs ------------------------- + +# One representative, valid value per accepted keyword-only option (issue +# #346): every key in _CONSUMED_KEYS/_CANONICAL_ALIAS_KWARGS must appear here +# exactly once, enforced by test_values_cover_every_consumed_key below. +# pdftype must be exactly 0 (this backend implements only the +# generalized-Gaussian density); outdir/files/data_dim/field_dim are handled +# specially (outdir below, files/data_dim/field_dim in +# TestDataLocationKwargs above -- real data, not a placeholder value). +_REPRESENTATIVE_VALUES: Dict[str, Any] = { + "num_models": 2, + "num_mix": 2, + "max_iter": 5, + "do_newton": True, + "newt_start": 3, + "newt_ramp": 5, + "newtrate": 0.3, + "do_reject": True, + "rejsig": 2.5, + "rejstart": 3, + "rejint": 2, + "maxrej": 2, + "num_comps": 4, + "lrate": 0.05, + "minlrate": 1e-10, + "lratefact": 0.5, + "rho0": 1.3, + "minrho": 1.0, + "maxrho": 2.5, + "rholrate": 0.03, + "rholratefact": 0.2, + "invsigmax": 500.0, + "invsigmin": 1e-3, + "do_history": True, + "histstep": 5, + "do_opt_block": True, + "block_size": 4096, + "blk_min": 2048, + "blk_max": 16384, + "blk_step": 2048, + "share_comps": True, + "comp_thresh": 0.95, + "share_start": 50, + "share_int": 50, + "doscaling": True, + "scalestep": 2, + "do_sphere": True, + "do_mean": True, + "do_approx_sphere": True, + "pcakeep": 5, + "pcadb": 1e-3, + "mineig": 1e-8, + "mineig_rel": 1e-10, + "writestep": 50, + "max_decs": 4, + "maxincs": 3, + "restartiter": 5, + "maxrestarts": 2, + "min_dll": 1e-8, + "min_grad_norm": 1e-6, + "use_min_dll": True, + "use_grad_norm": True, + "pdftype": 0, + "seed": 123, + "n_restarts": 1, + "restart_seeds": [7], + # Canonical-spelling aliases (issue #346): also documented, also + # constructs, checked against their own (translated) attribute name. + "min_nd": 1e-6, + "maxdecs": 4, + "share_iter": 50, +} + +# outdir is exercised through the tmp_path-scoped test below instead of a +# hardcoded path (constructing with it creates the directory and an out.txt). +# files/data_dim/field_dim are exercised with real data in +# TestDataLocationKwargs above, not with a placeholder value here. +_SPECIAL_CASED_KWARGS = {"files", "data_dim", "field_dim", "outdir"} + +# The attribute this backend stores a canonical-spelling kwarg under. +_ATTR_FOR_KEY = {**_CANONICAL_TO_NUMPY_KEY} + + +def _attr_name(key: str) -> str: + return _ATTR_FOR_KEY.get(key, key) + + +class TestEveryDocumentedOptionConstructs: + def test_values_cover_every_consumed_key(self): + """The representative-value table itself does not drift from the + accepted-kwargs set: every non-special-cased key has an entry, and + every entry names a real accepted key.""" + documented = (_CONSUMED_KEYS | _CANONICAL_ALIAS_KWARGS) - _SPECIAL_CASED_KWARGS + assert set(_REPRESENTATIVE_VALUES) == documented + + @pytest.mark.parametrize("key", sorted(_REPRESENTATIVE_VALUES)) + def test_option_constructs_and_takes_effect(self, key, tmp_path): + value = _REPRESENTATIVE_VALUES[key] + model = _construct( + use_tqdm=False, outdir=str(tmp_path / f"out_{key}"), **{key: value} + ) + assert getattr(model, _attr_name(key)) == value + + def test_outdir_constructs_and_takes_effect(self, tmp_path): + target = tmp_path / "explicit_outdir" + model = AMICA_NumPy(use_tqdm=False, outdir=str(target)) + assert model.outdir == target + assert target.exists() + + def test_all_options_together_construct(self, tmp_path): + """Every non-special-cased option at once, not just one at a time -- + the combination the one-at-a-time loop above cannot catch (e.g. two + options whose validations only interact when both are set).""" + kwargs = dict(_REPRESENTATIVE_VALUES) + kwargs.pop("min_nd") # would conflict with min_grad_norm, also present + kwargs.pop("maxdecs") # would conflict with max_decs, also present + kwargs.pop("share_iter") # would conflict with share_int, also present + model = AMICA_NumPy(use_tqdm=False, outdir=str(tmp_path / "out_all"), **kwargs) + for key, value in kwargs.items(): + assert getattr(model, _attr_name(key)) == value diff --git a/pamica/tests/test_wrapper_backends.py b/pamica/tests/test_wrapper_backends.py index e90f688f..c374f47c 100644 --- a/pamica/tests/test_wrapper_backends.py +++ b/pamica/tests/test_wrapper_backends.py @@ -191,6 +191,26 @@ def test_unknown_fit_keyword_names_the_backend(X, backend): model.fit(X, max_iter=1, blocksize=512) +def test_torch_only_and_unknown_together_name_both(X): + """PR #347 review item 4: a first version of this check raised on the + torch-only category alone, so ``fit(dtype=..., blocksize=...)`` with + ``backend='mlx'`` named only ``dtype`` and silently dropped + ``blocksize`` from the message -- the same drop the NumPy backend's own + kwarg check had (issue #346). Both categories are now named in ONE + ``TypeError``, and the two single-category messages + (``test_dtype_with_the_mlx_backend_raises_before_fitting``, + ``test_unknown_fit_keyword_names_the_backend``) are unchanged.""" + model = _wrapper("mlx") + with pytest.raises(TypeError) as exc: + model.fit(X, max_iter=1, dtype=torch.float32, blocksize=512) + message = str(exc.value) + assert "dtype" in message + assert "apply only to backend='torch'" in message + assert "blocksize" in message + assert "neither a fit() parameter nor a constructor keyword" in message + assert model.model_ is None # nothing was built + + def test_backend_attribute_is_rechecked_at_fit(X): """A backend changed after construction goes through the same checks.""" model = AMICA(verbose=False) From 027cb0731dbaaa8e7a5795166af95ba7ff52c2cd Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Wed, 23 Sep 2026 08:42:22 -0700 Subject: [PATCH 14/26] Phase 11: follow the reference's iteration order (#348) * Split the M-step into direction and update Compute the natural-gradient/Newton step and its norm (the reference's dAk and ndtmpsum) in _update_direction and apply it in _update_parameters, in all three backends, sharing one set of intermediates. The fit loops call both back to back, so every fit is byte-identical (checked on 8 configurations per backend). * Follow the reference's iteration order Run the likelihood-decrease response and the stopping checks before the parameter update, and exit on a stop before any parameter moves, as amica15.f90:1015-1122 does, in all three backends. The step taken right after a decrease now uses the halved rate, and a stopped fit returns the parameters whose likelihood is ll_history[-1]. Keep the reference's working rho rate and its ceiling apart (rholrate, rholrate_cap), reset inside the A-update branch. Fits without decreases or stops are byte-identical; seeded oracle deviations drop from 1.4e-2 to 2.7e-5. * Hold A on the reference's schedule without sharing Apply the reference's A-update guard (iter >= share_start and mod(iter, share_iter) <= 5, amica15.f90:1803) in every fit, share_comps on or off, through schedule.share_freeze, replacing the anchored post-merge window. Every backend now rejects share_iter < 7 (NumPy: share_int) whether or not sharing is on, since a shorter cycle would never update A again. Default fits of 100 or more iterations change. * Add iteration-order tests and native oracle Always-on cross-backend tests for the freeze iterations, the decrease timing, stop semantics, keep_best pairing and share_iter validation, plus an opt-in comparison against the native binary (AMICA_RUN_FORTRAN=1). All pass; the native comparison fails on the pre-change code. * Retune tests to the reference's iteration order Existing tests that pinned the old order (update before the checks, a freeze gated on share_comps and anchored on share_start, one rho rate) now pin the reference's, or use recipes that still exercise what they test. Full non-slow suite and the gated native oracles pass. * Document the reference's iteration order ADR 0008, changelog entry with oracle and parity numbers, and the differences guide: the A-freeze is now the reference's arithmetic, and convergence stops return the parameters final_ll_ describes. mkdocs build --strict passes. * Test the rho-rate ceiling as its own value Rate tests that read rholrate as the ceiling now read rholrate_cap, the cross-backend state copies carry it, and a new test round-trips it through state_dict on PyTorch and MLX, including a save without the key. Affected tests pass, including the slow torch ratchet test. * Stop overshoot recipes on the first decrease The keep_best overshoot recipes stopped via min_dll=1e-4/maxincs=2, which under the reference order ended at the peak or below it depending on round-off: at the peak on the macOS CI runner, and in 1 to 2 of 12 data perturbations locally. maxincs=0 with min_dll=1e-8 stops them on their first likelihood decrease instead, which held in all 12 perturbations on PyTorch and MLX, with and without rejection. Affected tests pass. * Stop identically on non-finite values Every backend now keeps a non-finite likelihood out of its history (NumPy recorded it), stops before applying a non-finite step or gradient norm (new degenerate stop_reason nan_direction), and stops on non-finite parameters right after an update (nan_params, now also in PyTorch and NumPy). NumPy defers only an A/W corruption inside its restart window to restart-on-NaN, which can repair it. Cross-backend injection tests pass. * Clear numincs on a NumPy restart The reference's min_dll comparison with the NaN likelihood of its restart iteration is false, so it zeroes numincs (numdecs is left as it was). NumPy's restart carried the small-gain count across; it now clears it. The new test stops at index 8 as expected and at 6 without the fix. * Validate saved learning rates on load from_state_dict (and the MLX file load) now refuses a missing or non-finite lrate, lrate_cap, newtrate, rholrate or rholrate_cap with a ValueError naming the field; a missing rholrate no longer surfaces as a bare KeyError on PyTorch. Tested on PyTorch and MLX. * Validate share_start on every fit The A-freeze reads share_start whether or not share_comps is on, so every backend now requires an integer share_start >= 1 always. The share_iter message now says why a value below 7 is refused (A held permanently from share_start), and NumPy's names both of its spellings. Both checks are reached through a Fortran params file on all backends; tests pass. * Test every convergence stop's parameters The stop test now runs min_dll, grad_norm, grad_norm_floor and lrate_floor on every backend, each with a precondition on its stop_reason and a twin that runs the same iterations to max_iter. All 12 cases pass. * Test that a NumPy restart skips the update A pass-through recorder on _update_parameters shows the restarting iteration applies no update; the same fit of the pre-change code, loaded through pre_change.py as a control, updated on it. Passes. * Test MNE n_iter_ after each kind of stop to_mne_ica().n_iter_ equals iteration + 1 and len(ll_history) after a min_dll stop and after a max_iter fit, on PyTorch and MLX. Passes. * Take the oracle floor over two thread pairs The reference noise floor is now the larger deviation of a 2- or 4-thread binary run from the 1-thread run. The multi-threaded runs are not reproducible, so the docstring gives each floor's range over three runs. All four gated oracle cases pass in each run. * Fix docstrings and Fortran citations mir_history_ records no waypoint on a stopping iteration; MLX documents its rholrate/rholrate_cap pair as PyTorch does; NumPy fit() gets the iteration-order notes. Rejection is cited as amica15.f90:1136-1140 and the check ranges start at 1056 and 1019. Docstring and comment changes only. * Document the non-finite stops and validation ADR 0008 states the final_ll_ invariant with its keep_best qualifier and the review's changes; the changelog and differences guide cover the non-finite stops, share_start validation and saved-rate checks, with the two-pair oracle floors. AGENTS.md and the context notes describe the unconditional A-freeze. mkdocs build --strict passes. --- .context/decisions/0008-iteration-order.md | 77 ++ .context/decisions/README.md | 3 +- .context/plan.md | 5 +- .context/progress_summary.md | 6 +- AGENTS.md | 6 +- docs/changelog.md | 67 ++ docs/guides/amica-differences.md | 49 +- pamica/amica.py | 41 +- pamica/mlx_impl/core.py | 858 ++++++++++------- pamica/mne_compat/core.py | 5 +- pamica/numpy_impl/core.py | 752 +++++++++------ pamica/schedule.py | 88 +- pamica/tests/mlx_tests/test_mlx_backend.py | 27 +- pamica/tests/mlx_tests/test_mlx_export.py | 4 +- pamica/tests/mlx_tests/test_mlx_keepbest.py | 65 +- pamica/tests/mlx_tests/test_mlx_llt_stash.py | 11 +- pamica/tests/mlx_tests/test_mlx_mir.py | 34 +- pamica/tests/mlx_tests/test_mlx_reject.py | 20 +- pamica/tests/mlx_tests/test_mlx_sharing.py | 69 +- pamica/tests/mne_tests/test_mne_backends.py | 32 + pamica/tests/native_oracle.py | 17 + pamica/tests/test_component_rows.py | 58 +- pamica/tests/test_iteration_order.py | 587 ++++++++++++ .../test_iteration_order_native_oracle.py | 360 +++++++ pamica/tests/test_llt_stash.py | 13 +- .../tests/test_mlx_keepbest_cross_backend.py | 7 +- pamica/tests/test_mlx_newton_cross_backend.py | 1 + .../test_mlx_pdf_families_cross_backend.py | 1 + .../tests/test_mlx_sharing_cross_backend.py | 1 + pamica/tests/test_nonfinite_stops.py | 327 +++++++ pamica/tests/test_numpy_grad_norm.py | 11 +- pamica/tests/test_numpy_restarts.py | 1 + pamica/tests/test_numpy_share_comps.py | 84 +- pamica/tests/test_sample_data.py | 34 +- pamica/tests/test_schedule_gates.py | 159 +++- .../torch_tests/test_amica_ng_wrapper.py | 12 +- pamica/tests/torch_tests/test_ng_backend.py | 45 +- .../tests/torch_tests/test_ng_convergence.py | 71 +- pamica/tests/torch_tests/test_ng_sharing.py | 63 +- pamica/torch_impl/core.py | 897 +++++++++++------- 40 files changed, 3687 insertions(+), 1281 deletions(-) create mode 100644 .context/decisions/0008-iteration-order.md create mode 100644 pamica/tests/test_iteration_order.py create mode 100644 pamica/tests/test_iteration_order_native_oracle.py create mode 100644 pamica/tests/test_nonfinite_stops.py diff --git a/.context/decisions/0008-iteration-order.md b/.context/decisions/0008-iteration-order.md new file mode 100644 index 00000000..76098c3d --- /dev/null +++ b/.context/decisions/0008-iteration-order.md @@ -0,0 +1,77 @@ +# ADR 0008: Follow the reference's iteration order + +**Status:** accepted +**Date:** 2026-09-23 +**Owner:** Seyed Yahya Shirazi + +## Context + +Each iteration of the reference's main loop (amica15.f90:949-1142) computes the likelihood and the update direction, +then responds to a likelihood decrease (halves `lrate`, scales `rholrate`, ratchets the ceilings at `maxdecs`) and runs the stopping checks, +exits before any parameter update when a check fires (:1111), +and otherwise applies the update with the rates it just set (:1122). +All three pamica backends applied the update first and checked afterward, +so a decrease's halved rate took effect one iteration late (issue #339), +and a fit that stopped on a convergence check returned parameters one update past the likelihood it reported. +Separately, the reference holds the `A` update, the `lrate` ramp and the `rholrate` reset +on every iteration with `iter >= share_start` and `mod(iter, share_iter) <= 5` (:1803), +whether or not `share_comps` is on; +pamica applied a freeze only under `share_comps`, anchored on `share_start` (issue #345), +so every default fit of 100 or more iterations diverged from the reference at iterations 100-105. + +## Decision + +Every backend runs the reference's order: +E-step (likelihood, sufficient statistics, the step for `A` and its norm `ndtmpsum`), +then the non-finite check, the decrease response, `min_dll`, the gradient-norm stop and the Newton-switch reset, +then an exit before any update when a check fired, +otherwise the update with the rates just set, then checkpoints, then outlier rejection. +`_update_parameters` is split into `_update_direction` (the direction and its norms) and the update that applies it, sharing one set of intermediates. +The `A` update holds on exactly the reference's iterations, through `schedule.share_freeze`, for every fit; +`share_iter` (NumPy `share_int` or `share_iter`) below 7 and `share_start` below 1 are rejected in every backend whether or not sharing is on: +the first would hold `A` permanently from `share_start` on, the second would start the freeze on the first iteration. +The rho rate is split into a working rate (`rholrate`, scaled on each decrease and reset to its ceiling inside the `A` branch) +and its ceiling (`rholrate_cap`, ratcheted at `maxdecs`), the reference's `rholrate` and `rholrate0`. +Every backend meets a non-finite value the same way (issue #339 review): +a non-finite likelihood is never recorded in the history; +a non-finite step or gradient norm, which would pass both `<= min_nd` checks, stops the fit before the update (`nan_direction`); +non-finite parameters right after an update stop it at once (`nan_params`). +Both new reasons are degenerate, like `nan_ll`. + +## Consequences + +- Fits with no likelihood decrease, no convergence stop and fewer than `share_start` iterations are byte-identical to before on every backend. + Fits with a decrease, a convergence stop, or 100+ iterations at the default share settings change trajectory, toward the reference: + on a 30-iteration seeded run with eight decreases, the worst per-iteration log-likelihood gap to the binary fell from 1.4e-2 to 2.7e-5 (PyTorch) and 8.2e-5 (NumPy), + against a binary-versus-binary noise floor (1 thread against 2 or 4) of 4.1e-5 to 1.3e-4 over three runs; + on a 16-iteration freeze run the gap fell from 6.1e-3 to 6.6e-7, at a floor of 4.0e-7 to 4.3e-7. +- The invariant is that `final_ll_` is exactly the log-likelihood of the returned parameters whenever the fit ends on a convergence stop or a `keep_best` restore, + not that it equals `ll_history[-1]`: + on a convergence stop the returned parameters are the ones whose likelihood is `ll_history[-1]`, + and after a restore they are an earlier iterate's, whose likelihood is an earlier `ll_history` entry. + At `max_iter` the last iteration still takes its update, as in the reference, so there `final_ll_ == ll_history[-1]` is one update behind the returned parameters. + A convergence stop no longer runs a share scan, a kurtosis switch, a `mir_history_` waypoint or a rejection pass on its stopping iteration. +- `share_iter` values from 1 to 6, and `share_start` values below 1, that were accepted with sharing off now raise. + No bundled configuration or parameter file used one. +- A non-finite value that used to be applied or returned now ends the fit as degenerate: + a NaN step (PyTorch, NumPy and MLX), and non-finite parameters after the last update (PyTorch, which returned them as `max_iter`). + NumPy's history no longer ends with the NaN likelihood that stopped it, + its restart-on-NaN takes over a post-update corruption only when `A`/`W` alone went non-finite (a restart redraws only `A`), + and its restart clears `numincs`, as the reference's NaN comparison does. +- The PyTorch and MLX saves carry `rholrate_cap` as an additive field; a save without it loads with the ceiling equal to its saved `rholrate`. + Loading refuses a missing or non-finite learning rate with a `ValueError` naming the field. +- The MNE export's `n_iter_` is now `iteration + 1`, the number of E-steps that ran. + +## Alternatives considered + +- **Apply the halved rate one iteration late, as before, and document it:** the late rate compounds with the chaotic sensitivity of the fit, so the per-iteration gap to the binary grew from round-off to 1e-2 within a few iterations after the first decrease. +- **Keep the freeze anchored on `share_start` and gated on `share_comps`:** pamica's own arithmetic, chosen because the literal `mod(iter, share_iter)` misaligns when `share_start` is not a multiple of `share_iter`. + The literal arithmetic is unambiguous, costs nothing, and the reference applies it to every default fit that reaches iteration 100, so the anchored window was a divergence on the default path. +- **One rho rate, as before:** the reference scales `rholrate` on a decrease and resets it to `rholrate0` inside the `A` branch; a single variable cannot do both, + so the pamica ceiling ratchet and working-rate scaling could not both match. + +## Receipts + +- Issues #339 and #345; epic #324, Phase 11. +- amica15.f90:1019-1111 (checks and exit), :1122 (`update_params`), :1803-1816 (the `A` branch), :1136-1140 (rejection). +- `pamica/tests/test_iteration_order.py` and `pamica/tests/test_nonfinite_stops.py` (always-on, every backend) and `pamica/tests/test_iteration_order_native_oracle.py` (opt-in with `AMICA_RUN_FORTRAN=1`). diff --git a/.context/decisions/README.md b/.context/decisions/README.md index 84e50428..23293f0e 100644 --- a/.context/decisions/README.md +++ b/.context/decisions/README.md @@ -6,7 +6,7 @@ Architecture Decision Records (ADRs) capture significant decisions that shape th - One file per decision: `NNNN-short-kebab-title.md`, zero-padded to four digits. - `0000-template.md` is the template; copy it to start a new ADR. Do not edit `0000-template.md` itself. -- Number sequentially. The next ADR after `0007-...` is `0008-...`. +- Number sequentially. The next ADR after `0008-...` is `0009-...`. - Status flows `proposed` -> `accepted` -> (later) `superseded by ADR-NNNN`. Never delete an ADR; supersede it. - Keep each ADR short. If it grows past two screens, you are probably writing a design doc, not a decision. @@ -32,3 +32,4 @@ Add new entries here as you create ADRs: - ADR 0005 - [Restore the PCA residual in the MNE export](0005-restore-pca-residual-in-mne-export.md) (accepted) - ADR 0006 - [Components are rows of each stored mixing block](0006-component-orientation.md) (accepted; amends ADR 0001; storage amended by ADR 0007) - ADR 0007 - [Store the mixing matrix with one component per row](0007-component-row-layout.md) (accepted; amends ADRs 0001 and 0006) +- ADR 0008 - [Follow the reference's iteration order](0008-iteration-order.md) (accepted) diff --git a/.context/plan.md b/.context/plan.md index 8fbb46d4..b6405fa4 100644 --- a/.context/plan.md +++ b/.context/plan.md @@ -44,9 +44,10 @@ `.context/issue-51/`. - [x] Component sharing (issue #60): `share_comps` multi-model reassignment ported to `AMICATorchNG` (de-sphered cosine-similarity merge, `share_start`/`share_iter`/`comp_thresh` - schedule + A-freeze). OFF by default so single-model (#24)/default multi-model (#27) parity is + schedule). OFF by default so single-model (#24)/default multi-model (#27) parity is byte-for-byte. No bit-exact oracle (reference `Spinv2` metric is dead code, like #26); - behavior-validated. See `tests/torch_tests/test_ng_sharing.py`. + behavior-validated. See `tests/torch_tests/test_ng_sharing.py`. The A-freeze is not part of + sharing: it is the reference's schedule, applied to every fit (ADR 0008, #345). ### Priority 3: Testing & validation - [x] Real-data test suite exercising the PyTorch backend end-to-end (issue #7) - Phase 1, issue #10: diff --git a/.context/progress_summary.md b/.context/progress_summary.md index 13b8f0ae..7e6a5eea 100644 --- a/.context/progress_summary.md +++ b/.context/progress_summary.md @@ -51,8 +51,10 @@ what remains as of the v0.1.0 preparation. - `share_comps` runs on all three backends. Since #334 (ADR 0007) `A` stores one component per row, so the metric compares de-sphered component mixing vectors (rows of `A` through `pinv(sphere)`, the scalp maps) across models, and a merge above `comp_thresh` ties the two components: `comp_list` - is re-pointed and they share one mixing vector and density, with an A-freeze for 6 iterations - after each merge (Fortran `identify_shared_comps`, amica15.f90:1916). + is re-pointed and they share one mixing vector and density (Fortran `identify_shared_comps`, + amica15.f90:1916). The A-freeze is the reference's unconditional schedule (ADR 0008, #345): from + `share_start` on, `A` is held on every iteration with `mod(iter, share_iter) <= 5`, on every fit, + sharing on or off. - The `gm`-weighted `dAk/zeta` step averages a shared component over its models; byte-identical to the per-model update when no merge fires. - `keep_best` (#51) is disabled under sharing (a merge changes the parameter count, so pre-/post-merge diff --git a/AGENTS.md b/AGENTS.md index c8a1878b..5650fb74 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,8 +153,10 @@ components whose de-sphered mixing vectors (rows of `A` mapped through `pinv(sph scalp maps) are near-collinear across models, with cosine above `comp_thresh`, are merged: the merge re-points `comp_list`, so the two sources share one component (mixing vector and density), and the `gm`-weighted `dAk/zeta` step averages that component over the models that share it -(Fortran `identify_shared_comps`, amica15.f90:1916). Merged-away rows are frozen. A-freeze for 6 -iterations after each merge. OFF by default; byte-identical when no merge fires. The reference +(Fortran `identify_shared_comps`, amica15.f90:1916). Merged-away rows are frozen. The A-freeze is +the reference's schedule and applies to every fit, sharing on or off (ADR 0008, #345): from +`share_start` on, `A` is held on every iteration with `mod(iter, share_iter) <= 5` (100-105, +200-205, ... by default). Sharing is OFF by default; byte-identical when no merge fires. The reference binary's own scan never merges (`Spinv2` is never allocated, so every similarity is NaN), but the update from a merged state seeded through `load_comp_list` matches it to float64 round-off (`pamica/tests/test_component_rows.py`, `AMICA_RUN_FORTRAN=1`). diff --git a/docs/changelog.md b/docs/changelog.md index aeacd0bd..bcd76fd5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,73 @@ Release notes are also published on the ## Unreleased +- **Phase 11 of epic #324: every backend follows the reference's iteration order (issues #339 and #345).** + Each iteration of every backend (PyTorch, NumPy and MLX) now runs in the reference's order (amica15.f90:949-1142, + [ADR 0008](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0008-iteration-order.md)): + the E-step computes the log-likelihood and the update direction, + then the likelihood-decrease response and the stopping checks run, + a fit that stops leaves before any update, + and otherwise the parameters are updated with the rates those checks just set. + Fits with a likelihood decrease, fits that stop on a convergence check, and fits that reach `share_start` (iteration 100 by default) now end elsewhere, closer to the reference; + every other fit is byte-identical to before. + - **Behavior change: a likelihood decrease takes effect in the same iteration's update.** + Before, the halved `lrate` and the scaled rho rate reached the update one iteration late. + On a seeded 30-iteration run with eight decreases (`lrate=0.5`, no Newton, `doscaling` on), + the largest per-iteration log-likelihood gap to the native binary fell from 1.4e-2 to 2.7e-5 (PyTorch) and 8.2e-5 (NumPy), + and the largest gap in `A` from 0.22 to 8.1e-4 and 6.8e-4; + the binary against itself (1 thread against 2 or 4, whose reduction order is not reproducible from run to run) + differs by 4.1e-5 to 1.3e-4 in log-likelihood and 5.3e-4 to 1.2e-3 in `A` over three runs. + On a run that ratchets at `maxdecs`, the log-likelihood gap fell from 1.3e-2 to 6.6e-6 (PyTorch) and 8.6e-6 (NumPy), + and the gap in `A` from 0.14 to 3.0e-4 and 2.7e-4 (binary floors 5.7e-6 to 9.6e-6 and 1.4e-4 to 4.2e-4). + Every decrease now falls on the reference's iterations. + - **Behavior change: a fit that stops on a convergence check returns the parameters its `final_ll_` was computed from.** + On a `min_dll`, gradient-norm or `lrate`-floor stop, the stopping iteration used to take its update anyway, + so the returned parameters were one update past `final_ll_`, and the exported `LLt` one update behind them. + That iteration now also runs no share scan, kurtosis switch, `mir_history_` waypoint or rejection pass. + A fit that runs to `max_iter` still updates on its last iteration, as the reference does. + - **Behavior change: the reference's A-freeze applies to every fit.** + Once `iter >= share_start`, the reference holds the `A` update, its `lrate` ramp and its rho-rate reset + on every iteration with `mod(iter, share_iter) <= 5` (amica15.f90:1803), whether or not `share_comps` is on. + pamica held `A` only under `share_comps`, in a window counted from `share_start`. + So every default fit of 100 or more iterations now holds `A` on iterations 100-105 (and 200-205, and so on), as the reference does. + On a seeded 16-iteration run with `share_start=3` and `share_iter=10`, + the gap to the binary fell from 6.1e-3 to 6.6e-7 in log-likelihood (binary floor 4.0e-7 to 4.3e-7) and from 0.19 to 5.0e-6 in `A`. + - Parity on the bundled sample, before and after, on all three backends: + against the bundled 200-iteration reference output, the log-likelihood gap falls from 2.2e-4 to 2.3e-4 down to 1.2e-4 to 1.3e-4, + the mean matched component correlation rises from 0.9972-0.9973 to 0.9982-0.9983 (minimum 0.969-0.970 to 0.981-0.982), + and the Amari distance falls from 6.3e-3 to 4.8e-3. + The validation harness's 100-iteration run keeps its final log-likelihood (gap 2.6e-4 to 2.7e-4), + and its mean matched correlation rises from 0.9988 to 0.9991 (Amari distance 0.0044 to 0.0038), + because the reference holds `A` on its 100th iteration. + - **Behavior change: every backend stops the same way on a non-finite value.** + A non-finite log-likelihood is never recorded: the NumPy backend used to leave it as the last `self.ll` entry, + which PyTorch and MLX never did. + A non-finite update direction or gradient norm, which passes both `<= min_nd` checks, now stops the fit before the update + with the new degenerate `stop_reason` `"nan_direction"`; it used to be applied. + Non-finite parameters right after an update now stop PyTorch and NumPy as they stopped MLX (`"nan_params"`, same check and message), + so a corruption on the last iteration no longer ends as `max_iter`. + The `AMICA` wrapper treats both new reasons as degenerate (`converged_=False`, output refused). + The NumPy backend names them in its own prose vocabulary and reports `converged=False`; + inside its restart-on-NaN window it lets a restart take over only when `A`/`W` alone went non-finite, which a restart redraws. + Its restart also clears the small-gain count `numincs`, as the reference's NaN comparison does. + - **New validation:** `share_iter` (NumPy `share_int` or `share_iter`) must be an integer >= 7, + and `share_start` an integer >= 1, on every backend whether or not `share_comps` is on: + a `share_iter` below 7 would hold `A` permanently from `share_start` on, and `share_start=0` would start the freeze on the first iteration. + The same checks apply through a Fortran params file. + No bundled configuration used a smaller value. + Loading a PyTorch `state_dict` or an MLX save refuses a missing or non-finite learning rate with a `ValueError` naming the field. + - The rho learning rate is now two values, as in the reference: + the working rate `rholrate`, scaled on each decrease and reset to its ceiling by each `A` update, + and the ceiling `rholrate_cap`, ratcheted at `maxdecs`. + PyTorch and MLX saves store `rholrate_cap`; a save without it loads with the ceiling equal to its `rholrate`. + - Smaller consequences of the order: + the MNE export's `n_iter_` is now `iteration + 1`, the number of E-steps that ran (it was `max(iteration, 1)`); + an MLX fit that stops on non-finite parameters now records that iteration's log-likelihood; + NumPy's restart after a non-finite likelihood now happens before the update, and its outlier rejection after the checkpoint writes, as in the reference. + - Tests: `pamica/tests/test_iteration_order.py` checks the order, the decrease timing, the stop semantics of every convergence stop and the freeze through real fits on every backend, + `pamica/tests/test_nonfinite_stops.py` the non-finite stops by injection, + and `pamica/tests/test_iteration_order_native_oracle.py` (opt-in, `AMICA_RUN_FORTRAN=1`) is the native-binary comparison above. + - **Phase 14 of epic #324: `AMICA_NumPy` rejects unknown/unsupported keyword arguments (issue #346).** `AMICA_NumPy(**kwargs)` used to forward every keyword into a params dict read with `params.get(...)`, so a typo (`max_iters=50`) or an option the legacy backend does not implement (`keep_best=True`) diff --git a/docs/guides/amica-differences.md b/docs/guides/amica-differences.md index 387d1b9e..9521f704 100644 --- a/docs/guides/amica-differences.md +++ b/docs/guides/amica-differences.md @@ -16,7 +16,7 @@ that is not listed, that is a bug worth | 2 | Zero numerical rank | `numeigs = 0`, continues | `ValueError` naming cause and fix | fitting a zero-dimensional model is not a recoverable state | — (no reason to want it) | | 3 | Returned iterate | last EM iterate | highest-likelihood iterate (`keep_best`) | the lrate schedule is non-monotone; late Newton overshoots cut LL variance 12.7x → 2.0x | `keep_best=False` | | 4 | Newton | on (`do_newton=1`) | off | isolates the algorithm from initialization for parity work | `do_newton=True` | -| 5 | Degenerate fits | returns NaN sources, and writes them out on its `writestep` cadence | the `AMICA` wrapper, on either backend, refuses `transform`/`get_*`/`save`; the raw `AMICATorchNG`, `AMICAMLXNG` and NumPy `AMICA` backends now refuse their own output accessors on a degenerate fit too (issue #306), not just the wrapper; NumPy additionally reports `converged=False` with a `stop_reason`, refuses the final write, and skips each periodic checkpoint with a logged reason (leaving the last valid one on disk) | NaN sources silently poison downstream analysis, and `loadmodout` reads a NaN checkpoint back without complaint | — (see issues #50, #240 and #306) | +| 5 | Degenerate fits | returns NaN sources, and writes them out on its `writestep` cadence | the `AMICA` wrapper, on either backend, refuses `transform`/`get_*`/`save`; the raw `AMICATorchNG`, `AMICAMLXNG` and NumPy `AMICA` backends now refuse their own output accessors on a degenerate fit too (issue #306), not just the wrapper; NumPy additionally reports `converged=False` with a `stop_reason`, refuses the final write, and skips each periodic checkpoint with a logged reason (leaving the last valid one on disk). Every backend stops before a non-finite value is applied or returned: a non-finite likelihood (never recorded in the history), update direction (`nan_direction`) or parameter after an update (`nan_params`), where the reference applies a NaN step and exits on the next likelihood | NaN sources silently poison downstream analysis, and `loadmodout` reads a NaN checkpoint back without complaint | — (see issues #50, #240, #306 and #339) | | 6 | Precision | float64 | float64 (float32 on Apple GPUs) | Apple GPUs have no float64; float32 agrees to ~7 significant digits, not bit-parity | `dtype=torch.float64` | | 7 | Sensor-space maps | `Spinv` applied internally | `get_sensor_mixing_matrix()` | `get_mixing_matrix()` returns sphered-space `A`; switching its meaning by data conditioning would be worse | — | | 8 | Components merged away by `share_comps` | mixing vector and density updated to NaN, then hidden by the `comp_used` mask | frozen at their last finite value (never divided, never rescaled) | a fit must not end holding NaN parameters, mask or no mask; the components are dead either way | — (see issues #60, #240, #334) | @@ -30,19 +30,28 @@ that is not listed, that is a bug worth | 16 | Single-precision density normalizers | `log(dble(1.772453851))` in the exact-Gaussian branch of the generalized Gaussian (`rho == 2`, amica15.f90:1313) is a single-precision literal widened to double, 3.0e-8 above `log(sqrt(pi))`; the Gaussian and cosh families (`pdftype` 2, 4 and 1, :1333, :1359, :1371) use literals of the same kind | `0.5 * log(pi)` for `rho == 2`; the double-precision values of the decimal literals for the other families, which differ from the reference's in the log by 3.7e-10, 2.0e-8 and -2.1e-8 | not a deliberate choice: found during epic #324 Phase 8, while seeding the reference from warm states in which mixtures sit at `maxrho = 2`; each such mixture's log-density differs by 3.0e-8, weighted by its responsibility (2.8e-9 in the log-likelihood of one warm two-model state of the sample). The other families' offsets are computed from the literals, not yet measured against the binary | none yet (issue #344 decides whether to adopt the reference's values); the seeded oracles keep `maxrho` below 2 | Rows 1, 2 and 7 arrived with [ADR 0004](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0004-rank-deficient-input-handling.md); -row 3 with ADR 0003; row 5 with issue #50, extended to the raw backends by issue #306; row 8 with issues #60, #240 and #334; +row 3 with ADR 0003; row 5 with issue #50, extended to the raw backends by issue #306 and to non-finite steps and updates by issue #339; row 8 with issues #60, #240 and #334; row 9 with issue #232; row 10 with issue #198; row 11 with issue #322 (ADR 0005); row 12 with issue #323; row 13 is recorded, not yet resolved, by issue #328; row 14 with issue #333 (ADR 0006); row 15 with issue #335, which also made the NumPy restart window count from 1; row 16 is recorded, not yet resolved, by issue #344. -Two `share_comps` details are pamica's own because the reference cannot decide -them: the A-freeze window after a merge is anchored on `share_start` (the literal -`mod(iter, share_iter)` misaligns unless `share_start` is a multiple of -`share_iter`, and freezes A permanently for `share_iter <= 6`, which the array -backends reject up front), and the merge similarity metric has no bit-exact -oracle, because the reference's `Spinv2` is declared but never allocated. +The A-freeze is the reference's arithmetic, applied as it is (issue #345): +once `iter >= share_start`, every iteration with `mod(iter, share_iter) <= 5` holds the `A` update, +together with the `lrate` ramp and the `rholrate` reset that share its branch (amica15.f90:1803). +The reference applies it to every fit, one model or several, whether or not `share_comps` is on, +so with the defaults (`share_start = share_iter = 100`) every backend holds `A` on iterations 100-105, 200-205, and so on. +Until issue #345 pamica held `A` only under `share_comps` with two or more models, on each scan iteration and the five after it, counted from `share_start`. +The literal remainder starts the window on the scan iteration only when `share_start` is a multiple of `share_iter`, as with the defaults; +otherwise the window and the scan fall on different iterations, in the reference and in pamica alike. +A `share_iter` below 7 would leave every remainder at 5 or less, holding `A` permanently from `share_start` on, +so every backend rejects it, sharing on or off (the NumPy backend takes it as `share_int` or `share_iter`). +For the same reason every backend requires `share_start >= 1` whether or not sharing is on: +`share_start=0` would start the freeze on the first iteration. + +One `share_comps` detail is pamica's own because the reference cannot decide it: +the merge similarity metric has no bit-exact oracle, because the reference's `Spinv2` is declared but never allocated. Its scan still runs, but every similarity it computes is NaN, so a reference run with `share_comps` on never merges anything: seeded from pamica's initialization, the pinned binary leaves `comp_list` unchanged and reports 64 unique components after a scan at iteration 8, even at `comp_thresh=0`. @@ -333,7 +342,7 @@ that on the bundled sample). Statements elsewhere in these guides about the Component sharing was the other gap, closed by issue #263: `AMICAMLXNG` now takes `share_comps`/`share_start`/`share_iter`/`comp_thresh` with the PyTorch backend's names, defaults and validation, runs the same merge schedule and -post-merge A-freeze, and exposes `comp_used`/`shared_components()`. +A-freeze, and exposes `comp_used`/`shared_components()`. It does not re-derive the merge metric — it calls the same `identify_shared_components` kernel the NumPy backend uses, on the host float64 sensor maps `pinv(sphere) @ A.T` (one column per component, issue #334), so all @@ -461,11 +470,11 @@ Two consequences to know: but its similarity with `Spinv2 = Spinv^T Spinv`, applied to the binary's own state after 8 iterations from pamica's initialization, merges exactly the pairs the PyTorch and NumPy scans merge: 32, 32 and 30 at `comp_thresh` 0.9, 0.95 and 0.99. - A model left with few components of its own then loses its responsibility, and can end in a non-finite fit. - In a short recipe (4096 samples, `share_start=8`, `share_iter=10`, `comp_thresh=0.9`), 28 merges at iteration 8 - dropped the second model's `gm` from 0.41 to 0.003, and one more merge at iteration 18 drove it to zero and the fit to NaN. + In a short recipe (4096 samples, seed 20, `share_start=11`, `share_iter=11`, `comp_thresh=0.9`), 25 merges at iteration 11 + dropped the second model's `gm` from 0.53 to 6.0e-4 within two iterations, and the fit went non-finite at iteration 25. Seeded with the same merged states, the reference's update does the same: - the second model's `gm` falls to 0.005 within two iterations, as pamica's does from the same state, - and from the second merge it reaches zero in one iteration and the binary reports NaN and reinitializes. + the second model's `gm` falls to 6.0e-4 within two iterations, as pamica's does from the same state, + and from the state after the second scan (iteration 22) it reaches zero in one iteration and the binary reports NaN and reinitializes. This is the algorithm on models that have not yet separated, not a defect of the port. The reference's default `share_start=100` avoids it; keep `share_start` well past the first iterations. - Saved models: a PyTorch `state_dict` (now `format_version` 4) or MLX save (now format 2) from an earlier version @@ -492,6 +501,11 @@ behavior rather than fixed (`test_merge_on_the_final_iteration_completes` in each of `tests/torch_tests/test_ng_sharing.py`, `tests/test_numpy_share_comps.py` and `tests/mlx_tests/test_mlx_sharing.py`). +It can only happen on a fit that runs to `max_iter`. +A fit that stops on a convergence check (`min_dll`, the gradient norm, the `lrate` floor) +exits before that iteration's update and scan, as the reference does (amica15.f90:1111, issue #339), +so its returned parameters are the ones `final_ll_` was computed from. + One interaction worth knowing: the `keep_best` safeguard (row 3 above, implemented on PyTorch and, since epic #278 Phase 2, MLX) is disabled whenever `share_comps` is on, precisely because a merge changes the parameter @@ -519,8 +533,13 @@ Fortran's iteration runs `get_updates_and_likelihood` (amica15.f90:996), then parameters as they stood *before* the M-step whose `W`/`A` sit next to it. It is not the likelihood of the written decomposition; it is the likelihood of its immediate predecessor, and it is the E-step that produced the last entry of the -written `LL` trajectory. The relation that holds on both sides — on the -committed reference output as much as on pamica's — is +written `LL` trajectory. +The exception is a fit that stops on a convergence check: +the reference exits before that iteration's `update_params` (amica15.f90:1111), +and since issue #339 every pamica backend does too, +so the final write's `LLt` is the likelihood of the `W`/`A` beside it. +The relation that holds on both sides, on the +committed reference output as much as on pamica's, is ``` LLt[num_models, :].sum() / (n_good_samples * nw) == LL[-1] diff --git a/pamica/amica.py b/pamica/amica.py index f80bfa37..e744c14b 100644 --- a/pamica/amica.py +++ b/pamica/amica.py @@ -265,19 +265,25 @@ class AMICA: ``save`` refuse such a model (issue #50). converged_ : bool Whether the last ``fit`` ended on a usable stop rather than a degenerate - one (``stop_reason_`` not in ``nan_ll``/``singular_ll``). A degenerate fit - holds non-finite parameters and would produce NaN sources (issue #50). + one (``stop_reason_`` not in the backend's ``_DEGENERATE_STOP_REASONS``: + ``nan_ll``/``singular_ll``/``nan_direction``/``nan_params``). A + degenerate fit holds non-finite parameters, or stopped before applying + a non-finite step, and would produce NaN sources (issue #50). stop_reason_ : str or None Why the last ``fit`` stopped (the backend ``stop_reason``): ``"max_iter"``, ``"lrate_floor"``, ``"grad_norm_floor"``, ``"min_dll"``, - ``"grad_norm"``, ``"nan_ll"``, or ``"singular_ll"``. The last five are + ``"grad_norm"``, ``"nan_ll"``, ``"singular_ll"``, ``"nan_direction"`` + or ``"nan_params"``. ``"lrate_floor"`` to ``"grad_norm"`` are Fortran-faithful convergence stops (issue #207: ``lrate_floor``/ ``grad_norm_floor`` fire together as two halves of the same likelihood-decrease branch; ``min_dll``/``grad_norm`` are separate, - unconditional per-iteration checks); only ``nan_ll``/``singular_ll`` - are degenerate (see ``converged_``), plus each backend's own further - markers (MLX's ``"nan_params"``, and under best-of-N restarts the - marker of a restart that raised). None of these checks short- + unconditional per-iteration checks). The last four are degenerate (see + ``converged_``): a non-finite log-likelihood (``nan_ll``/ + ``singular_ll``), a non-finite update direction caught before it is + applied (``nan_direction``), or non-finite parameters right after an + update (``nan_params``); PyTorch and MLX use the same set (issue #339 + review), and under best-of-N restarts a restart that raised has its + own degenerate marker. None of these checks short- circuits on an earlier one in the same iteration, so under the shipped ``use_grad_norm=True`` default ``"grad_norm"`` always takes precedence over ``"grad_norm_floor"`` when both would apply -- @@ -286,11 +292,17 @@ class AMICA: docstring for the full explanation). ll_history_ : list Log-likelihood history during training (the true per-iteration - trajectory; may dip below its peak on a late overshoot) + trajectory; may dip below its peak on a late overshoot): entry ``i`` is + the likelihood of the parameters iteration ``i`` started from, recorded + before that iteration's checks and update. final_ll_ : float Log-likelihood of the *fitted* parameters (issue #51). Use this, not ``ll_history_[-1]``, as the model's log-likelihood: with the best-iterate safeguard the returned parameters can be an earlier, higher-LL iterate. + Exact after a convergence stop, which exits before that iteration's + update, as the reference does (issue #339); after ``max_iter`` it is + the likelihood one update before the returned parameters, also as in + the reference. mir_history_ : list Mutual Information Reduction (MIR) waypoint trajectory (issue #137), populated when ``fit`` is called @@ -301,6 +313,9 @@ class AMICA: Not index-aligned with ``ll_history_``: entry ``i`` is computed after iteration ``i``'s update, while ``ll_history_[i]`` is the likelihood of the parameters before it, so the two are one update apart (issue #161). + An iteration that ends the fit on a stop (a convergence check or a + degenerate value) takes no update and so records no waypoint: the last + waypoint then belongs to the iteration before it (issue #339). restart_seeds_ : list The seed each restart ran from (issue #198). One entry for a default ``n_restarts=1`` fit, ``n_restarts`` entries otherwise. @@ -1091,10 +1106,12 @@ def write_amica_output(self, outdir: str) -> None: #155) for a model that was just fit in this process, taken from the E-step stash (issue #157); a model restored via :meth:`load` carries no stash, so ``LLt`` is omitted for it (a warning is logged). As in the - reference, ``LLt`` is the E-step that produced ``final_ll_`` and is - therefore one M-step older than the ``W``/``A`` written beside it -- - see ``docs/guides/amica-differences.md``. Use :meth:`model_loglik` for - the log-likelihood of the written parameters. + reference, ``LLt`` is the E-step that produced ``final_ll_``: after a + fit that ran to ``max_iter`` it is therefore one M-step older than the + ``W``/``A`` written beside it, and after a convergence stop, which + exits before that iteration's update, it belongs to them -- see + ``docs/guides/amica-differences.md``. Use :meth:`model_loglik` for the + log-likelihood of the written parameters. Parameters ---------- diff --git a/pamica/mlx_impl/core.py b/pamica/mlx_impl/core.py index 900de706..c8ded8b5 100644 --- a/pamica/mlx_impl/core.py +++ b/pamica/mlx_impl/core.py @@ -98,7 +98,7 @@ import math import time import zipfile -from typing import List, Optional, Sequence, Tuple +from typing import List, NamedTuple, Optional, Sequence, Tuple # mlx ships as a compiled extension with no type stubs, so ty cannot resolve # it statically even when installed; scope the suppression to this one import. @@ -343,6 +343,50 @@ def _safe_int_cast(name: str, value: np.ndarray, dtype) -> np.ndarray: return rounded.astype(dtype) +# The learning rates a save carries (issue #339 review). rholrate_cap is +# additive: a save written before issue #339 kept one rho rate, the ceiling, +# under "rholrate", and a fit that never held A ends with the working rate +# equal to it. +_SAVED_RATES = ("lrate", "lrate_cap", "newtrate", "rholrate") + + +def _saved_rates(extra: dict, owner: str) -> dict: + """The saved learning rates, each a finite number, or a named ValueError. + + A missing rate is reported like a missing parameter tensor, and a + non-finite or non-numeric one is refused: a NaN rate would load silently + and turn the next refit's first update into NaN parameters. + """ + missing = [name for name in _SAVED_RATES if name not in extra] + if missing: + raise ValueError(f"malformed {owner} state: missing extra fields {missing}") + rates = {name: extra[name] for name in _SAVED_RATES} + rates["rholrate_cap"] = extra.get("rholrate_cap", extra["rholrate"]) + for name, value in rates.items(): + if ( + isinstance(value, bool) + or not isinstance(value, (int, float)) + or not math.isfinite(value) + ): + raise ValueError( + f"malformed {owner} state: extra field {name!r} must be a finite " + f"number, got {value!r}" + ) + return rates + + +class _UpdateStep(NamedTuple): + """The mixing-matrix step one E-step implies, computed before the + iteration's stopping checks and applied after them (the reference's + ``dAk``, built in ``accum_updates_and_likelihood``, amica15.f90:1749-1761, + before ``update_params``, :1782; ``AMICATorchNG``'s type of the same name). + See :meth:`AMICAMLXNG._update_direction`.""" + + dAk: mx.array # (n_comps, n_channels), before the lrate scaling + newton_active: bool # the Newton schedule is on this iteration + no_newt: bool # some model failed the positive-definiteness guard + + class AMICAMLXNG: """MLX natural-gradient EM backend, full torch-equivalent surface (#76/#81, epic #278). @@ -394,10 +438,16 @@ class AMICAMLXNG: comment (issue #269). ``share_start`` (100) / ``share_iter`` (100) Sharing schedule: first iteration to attempt merges (counted from 1) - and the interval between attempts. The A-update is held for the first - 6 iterations of every cycle (whether or not a merge fired) so the - densities can settle; ``share_iter`` must be ``> 6`` so that window - never consumes the whole cycle, and ``share_start`` must be ``>= 1``. + and the interval between attempts. They also set the reference's + A-freeze, which applies to every fit, sharing on or off (issue #345): + from iteration ``share_start`` on, the update of ``A`` (with its lrate + ramp) is held on every iteration whose number, counted from 1, has a + remainder of 0 to 5 modulo ``share_iter`` (amica15.f90:1803), so with + the defaults on iterations 100-105, 200-205, and so on. + Both are validated whether or not ``share_comps`` is on: + ``share_start`` must be an integer >= 1, and ``share_iter`` an + integer >= 7, or the freeze would hold A permanently from + ``share_start`` on. ``comp_thresh`` (0.99) Cosine-similarity cutoff, in the de-sphered (sensor-space) metric, above which two components' mixing vectors are identified and merged. Must be @@ -445,6 +495,20 @@ class AMICAMLXNG: ``n_newton_fallbacks`` counts it (as AMICATorchNG does), so an all-fallback run is visible without re-instrumenting. + The rho learning rate carries AMICATorchNG's names and semantics too: + + ``rholrate`` (0.05) / ``rholratefact`` (0.1) + Learning rate of the generalized-Gaussian shape ``rho``. As in the + reference (``rholrate``/``rholrate0``, amica15.f90:1063-1068), it has + a working value (``rholrate``), which each likelihood decrease + multiplies by ``rholratefact``, and a ceiling (``rholrate_cap``), + which the ``maxdecs`` ratchet multiplies by ``rholratefact`` after + ``newt_start``; every update of ``A`` resets the working value to the + ceiling before ``rho`` moves (:1806/:1813), so the decrease scaling + reaches ``rho`` only on an iteration on which ``A`` is held (see + ``share_iter``). Both are reset to the constructor value at the start + of every fit and saved with the model (issue #339). + The source-density family parameters (issue #265, porting AMICATorchNG's issue #26) likewise carry AMICATorchNG's names, defaults and semantics: @@ -735,8 +799,13 @@ def __init__( self.rho0 = rho0 self.minrho = minrho self.maxrho = maxrho + # Working rho rate and its ceiling, as AMICATorchNG keeps them (the + # reference's rholrate/rholrate0, amica15.f90:1063-1068, 1806/1813): + # a decrease scales the working rate, a maxdecs ratchet the ceiling, and + # every A update resets the working rate to the ceiling. self.rholrate0 = rholrate self.rholrate = rholrate + self.rholrate_cap = rholrate self.rholratefact = rholratefact # Source-density family selection (issue #265, porting AMICATorchNG's @@ -801,13 +870,12 @@ def __init__( self.share_start = share_start self.share_iter = share_iter self.comp_thresh = comp_thresh + # The A-freeze schedule reads share_start/share_iter whether or not + # share_comps is on (issue #345), so both are validated always, with + # AMICATorchNG's messages. + schedule.validate_share_start(share_start) + schedule.validate_share_iter(share_iter) if share_comps: - if share_start < 1: - raise ValueError(f"share_start must be >= 1, got {share_start}") - if share_iter <= 6: - # The A-freeze settle window is 6 iterations; a smaller cycle - # would freeze A permanently (never leaving room to update it). - raise ValueError(f"share_iter must be > 6, got {share_iter}") if not 0.0 < comp_thresh <= 1.0: raise ValueError(f"comp_thresh must be in (0, 1], got {comp_thresh}") @@ -859,11 +927,14 @@ def __init__( # index-aligned with ll_history: the entry for iteration i is # computed AFTER that iteration's _update_parameters, while # ll_history[i] is the likelihood of the parameters BEFORE it, so - # the two describe states one update apart (issue #161). + # the two describe states one update apart (issue #161). An + # iteration that ends the fit on a stop records no waypoint. self.mir_history_: list[tuple[int, float, float]] = [] # Log-likelihood of the returned parameters, set by fit() to # ll_history[-1], or to the best iterate's LL if the keep_best - # safeguard (issue #51) restores it -- see _fit_once. Under + # safeguard (issue #51) restores it -- see _fit_once, which also says + # when that is exact (a convergence stop) and when it trails the + # returned parameters by one update (max_iter, issue #339). Under # share_comps, if a merge fires on the LAST fit iteration, the # returned A/W/comp_list are already post-merge but final_ll_ still # reports the pre-merge log-likelihood -- the merge runs after that @@ -916,10 +987,10 @@ def __init__( None # scalar: log|det W|, refreshed per iter ) # Weight-gradient norm (Fortran ndtmpsum), recomputed every iteration by - # _update_parameters and read by fit()'s two grad-norm checks. Held as + # _update_direction and read by fit()'s two grad-norm checks. Held as # the unevaluated MLX scalar rather than a Python float (AMICATorchNG's - # eager ``_ndtmpsum`` float) so materializing it joins fit()'s single - # per-iteration mx.eval instead of adding a second sync; the + # eager ``_ndtmpsum`` float) so materializing it joins fit()'s + # per-iteration likelihood sync instead of adding one; the # ``_ndtmpsum`` property below is the float view the checks and # cross-backend tests read. self._nd_arr: Optional[mx.array] = None @@ -942,12 +1013,17 @@ def __init__( self._llt_lht: Optional[np.ndarray] = None self._llt_lt: Optional[np.ndarray] = None - # The last entry is only reachable under best-of-N restarts (issue #198): a - # restart whose fit raised rather than stopping, recorded as degenerate so a - # search in which every restart crashed still reports an unusable model. + # Stop reasons that mark a fit as degenerate, the same set as + # AMICATorchNG's: a non-finite log-likelihood ("nan_ll"/"singular_ll"), a + # non-finite update direction ("nan_direction"), non-finite parameters after + # an update ("nan_params"). The last entry is only reachable under best-of-N + # restarts (issue #198): a restart whose fit raised rather than stopping, + # recorded as degenerate so a search in which every restart crashed still + # reports an unusable model. _DEGENERATE_STOP_REASONS = ( "nan_ll", "singular_ll", + "nan_direction", "nan_params", restarts.ERROR_STOP_REASON, ) @@ -955,9 +1031,10 @@ def __init__( @property def _ndtmpsum(self) -> Optional[float]: """Latest weight-gradient norm as a host float (AMICATorchNG's - ``_ndtmpsum``), or None before the first M-step. Cheap after fit()'s - mx.eval has materialized it; forces evaluation otherwise, so a direct - ``_update_parameters`` call still reads the current iteration's value.""" + ``_ndtmpsum``), or None before the first step is built. Cheap after + fit()'s mx.eval has materialized it; forces evaluation otherwise, so a + direct ``_update_direction``/``_update_parameters`` call still reads the + current iteration's value.""" if self._nd_arr is None: return None return float(self._nd_arr.item()) @@ -1088,13 +1165,15 @@ def _initialize_parameters(self): self.n_kurt_done = 0 # Reset the mutable optimization state to the pristine constructor values - # (lrate_cap, newtrate and rholrate are ratcheted down during fit, and - # n_newton_fallbacks counts one fit), so a re-fit starts fresh -- - # AMICATorchNG does the same in ``_initialize_parameters``/``_fit_once``. + # (lrate/lrate_cap, newtrate and rholrate/rholrate_cap are annealed or + # ratcheted down during fit, and n_newton_fallbacks counts one fit), so + # a re-fit starts fresh -- AMICATorchNG does the same in + # ``_initialize_parameters``/``_fit_once``. self.lrate = self.lrate0 self.lrate_cap = self.lrate0 self.newtrate = self.newtrate0 self.rholrate = self.rholrate0 + self.rholrate_cap = self.rholrate0 self.n_newton_fallbacks = 0 self.iteration = 0 self._refresh_lgamma_table() @@ -1645,150 +1724,72 @@ def _newton_direction(self, dA_h, sigma2_h, lambda_h, kappa_h): posdef = bool(mx.all(mx.logical_or(valid, eye_bool)).item()) return H, posdef - def _update_parameters(self, acc: dict, n_samples: int): - """Exact-EM mixture updates + natural-gradient A-update, optionally - Newton-preconditioned (``AMICATorchNG._update_parameters``).""" - # Fortran builds dAk from the PREVIOUS iteration's model weights: gm is - # not reassigned until update_params (amica15.f90:1788+), after the - # dAk/zeta accumulation in accum_updates_and_likelihood (:1749-1761). - # Snapshot before overwriting, as AMICATorchNG does (the ordering - # question issue #219 raised, fixed there and now here); MLX arrays are - # immutable and gm is - # only ever rebound, so a plain rebinding is a safe snapshot (torch - # clones because its tensors could be written in place). Exactly gm for - # n_models=1 (both are 1.0) and canceling for a disjoint comp_list, so - # the single-model and unshared multi-model paths are unchanged. - assert self.gm is not None - gm_prev = self.gm - self.gm = acc["dgm"] / n_samples # (n_models,); == 1 for single model - tiny = float(np.finfo(np.float32).tiny) - - # Per-model data-space bias c[i,h] = sum_t v_h*x / sum_t v_h (Fortran - # update_c, as in ``AMICATorchNG._update_parameters``). Skipped for n_models=1 - # (v==1 => c is the zero data mean; the update would add a float-sum residual - # and break the #24 bit-exact single-model path). A dead model (dgm[h]==0) keeps - # its prior c rather than writing 0/0, and is surfaced (matching AMICATorchNG). - if self.n_models > 1: - dgm = acc["dgm"] - live = dgm > 0.0 - new_c = acc["dc_numer"] / mx.maximum(dgm, tiny)[None, :] - self.c = mx.where(live[None, :], new_c, self.c) - if not bool(mx.all(live).item()): - logger.warning( - "Zero-responsibility model(s) at iter %d; kept their prior " - "bias c (dead-model guard).", - self.iteration, - ) - - # Component sharing (#263): a component merged away by - # _identify_shared_comps is no longer referenced by comp_list, so no - # sufficient statistic accumulates into its column (dalpha_n/dmu_d/ - # dbeta_d == 0) and the divisions below would be 0/0 = NaN -- which the - # nan_params guard in fit() would (correctly) abort on. Update only USED - # columns and freeze the rest at their last finite value (Fortran carries - # NaN there behind its comp_used mask; keeping them finite matches - # AMICATorchNG and the NumPy backend, docs/guides/amica-differences.md - # row 8). With the default full comp_list every column is used, so - # ``used`` is all-True and every update below is bit-for-bit unchanged. - assert self._comp_used_arr is not None - used = self._comp_used_arr[None, :] # (1, n_comps) - - self.alpha = mx.where( - used, - acc["dalpha_n"] / acc["dalpha_n"].sum(axis=0, keepdims=True), - self.alpha, + def _update_direction(self, acc: dict) -> _UpdateStep: + """The natural-gradient or Newton step for ``A`` from this iteration's + sufficient statistics, and its norm, without changing any parameter + (``AMICATorchNG._update_direction``). + + The reference computes both in ``accum_updates_and_likelihood`` + (amica15.f90:1666-1761), with ``LL(iter)``, before the likelihood- + decrease response and the stopping checks read ``ndtmpsum`` and before + ``update_params`` applies the step; :meth:`fit` calls this first and + hands the result to :meth:`_update_parameters` once the checks have + run. Sets ``self._nd_arr`` (lazily; read through ``_ndtmpsum``). + + Everything here reads the parameters as the E-step saw them: the Newton + curvature folds in the pre-update ``mu``, and ``dAk`` weights the models + by the pre-update ``gm`` (the reference does not reassign ``gm`` until + ``update_params``, :1788; issue #219). MLX arrays are immutable and + every parameter is only ever rebound, so nothing here needs a copy. + """ + assert ( + self.mu is not None + and self.A is not None + and self.comp_list is not None + and self.gm is not None + and self._comp_used_arr is not None ) - + tiny = float(np.finfo(np.float32).tiny) # Finalize the Newton curvature with the PRE-update mu. Fortran folds the # mu^2 term into lambda during E-step accumulation, before the M-step - # moves mu (amica15.f90:1666-1680); doing it here -- between the alpha - # update and the mu reassignment below -- is what reproduces that. Move - # it one line later and lambda silently uses the updated mu: no error, no - # NaN, just a subtly wrong Hessian (the torch backend's issue #24 bug, - # pinned there and here by test_newton_finalize_uses_preupdate_mu). + # moves mu (amica15.f90:1666-1680); doing it here, before + # _update_parameters moves anything, is what reproduces that. Finalize + # after the mu update instead and lambda silently uses the updated mu: no + # error, no NaN, just a subtly wrong Hessian (the torch backend's issue + # #24 bug, pinned there and here by + # test_newton_finalize_uses_preupdate_mu). newton_active = schedule.newton_active( self.do_newton, self.iteration, self.newt_start ) if newton_active: sigma2, lambda_, kappa = self._finalize_newton_stats(acc) - self.mu = mx.where(used, self.mu + acc["dmu_n"] / acc["dmu_d"], self.mu) - self.beta = mx.where( - used, - mx.clip( - self.beta * mx.sqrt(acc["dbeta_n"] / acc["dbeta_d"]), - self.invsigmin, - self.invsigmax, - ), - self.beta, - ) - - # GG shape update with the 1/psi(1+1/rho) digamma factor (Fortran - # :2013-2014); digamma is computed host-side (MLX has none). A NaN here - # (e.g. from an upstream mu/beta blow-up) is reset to rho0 and surfaced, - # matching ``AMICATorchNG._update_parameters``, so it does not silently - # poison the lgamma table and every subsequent E-step. - # Deliberate divergence from ``AMICATorchNG._update_parameters``, which also - # skips the update when rho is pinned to a boundary (all 1.0 or all 2.0): - # that early-exit needs a host sync on a (n_mix, n_comps) reduction over - # rho every iteration. This backend does make a few scalar host syncs per - # iteration -- the dead-model check above, the rho-NaN canary below, and - # the Newton posdef flag -- each because a Python branch genuinely - # depends on the value; the rho-boundary exit is not one of those, since - # mx.clip clamps straight back to the boundary and the results are - # identical either way. So it would buy nothing but a sync -- unrelated to - # (and not fixed by) issue #265: pdftype != 0 is now supported, and for - # those families this whole block is skipped by the outer ``self.dorho`` - # gate below (rho is frozen, Fortran ``dorho=.false.``), which is the - # real reason the digamma work does not run there -- not an MLX - # limitation. See ``_get_block_updates`` for the matching ``drho_n`` - # accumulator gate (a work-only divergence from AMICATorchNG, which - # always accumulates and discards it). - if self.dorho: - drho = acc["drho_n"] / mx.maximum(acc["dalpha_n"], 1e-8) - rho_np = np.array(self.rho, dtype=np.float64) - psi = mx.array(digamma(1.0 + 1.0 / rho_np).astype(np.float32)) - new_rho = self.rho + self.rholrate * (1.0 - (self.rho / psi) * drho) - nan_mask = mx.isnan(new_rho) - if bool(mx.any(nan_mask).item()): - logger.warning( - "NaN in rho update at iter %d; resetting to rho0=%g.", - self.iteration, - self.rho0, - ) - new_rho = mx.where(nan_mask, self.rho0, new_rho) - # ``used`` freezes a merged-away column's rho, as for mu/alpha/beta. - self.rho = mx.where( - used, mx.clip(new_rho, self.minrho, self.maxrho), self.rho - ) - # Natural-gradient A-update. A is stored as Fortran's A^T, one component # per row (issue #334), so the update is a LEFT-multiply by the # transposed direction (as in ``AMICATorchNG._update_parameters``, #24 # root cause). Each model's step is scattered into its component rows as - # a gm-weighted average (Fortran dAk/zeta) using the PREVIOUS iteration's - # gm (gm_prev, see the snapshot above): for the default disjoint + # a gm-weighted average (Fortran dAk/zeta) using the PRE-update gm (see + # the docstring): for the default disjoint # comp_list every component has one contributor, so gm cancels and # n_models=1 is byte-for-byte the old `A - lrate*(dA.T@A)`; a SHARED # component (#263) takes Fortran's responsibility-weighted average of the # models' steps for its one mixing vector, NOT a raw sum (a raw sum would # over-step by the contributor count). A merged-away component needs no # special case: nothing scatters into its row, so its zeta is 0 and its - # dAk is 0/tiny = 0, i.e. it takes no step. The rescale below does not - # touch it either: it rescales the rows of each model's block, and a - # merged-away row is in no model's block. + # dAk is 0/tiny = 0, i.e. it takes no step. The rescale in + # _update_parameters does not touch it either: it rescales the rows of + # each model's block, and a merged-away row is in no model's block. # # The direction/dAk/gradient-norm computation below runs # UNCONDITIONALLY, not gated on _a_frozen(): Fortran computes dAk and # ndtmpsum every iteration in accum_updates_and_likelihood - # (amica15.f90:1749-1761), strictly before the separate, share-freeze - # guarded update_A block (:1803) that steps A. Only the step itself -- - # and the lrate ramp Fortran nests inside that same guarded block -- are - # conditional (issue #207: the grad-norm stop must see the true gradient - # magnitude every iteration, not only when A moves). _a_frozen() is - # always False with sharing off, so the default path is unchanged. + # (amica15.f90:1749-1761), strictly before the separate, freeze-guarded + # update_A block (:1803) that steps A. Only the step itself -- and the + # lrate ramp and rho-rate reset Fortran nests inside that same guarded + # block -- are conditional (issue #207: the grad-norm stop must see the + # true gradient magnitude every iteration, not only when A moves). # Newton only swaps out the per-model DIRECTION; the dAk/zeta scatter, - # the gradient norm and the freeze structure below are untouched by it + # the gradient norm and the freeze are untouched by it # (``AMICATorchNG._update_parameters``). A model whose curvature fails the # positive-definiteness guard falls back to its natural gradient for this # iteration, and -- as in Fortran -- ANY model falling back also sends @@ -1810,13 +1811,12 @@ def _update_parameters(self, acc: dict, n_samples: int): else: directions.append(dA_h) - assert self.A is not None and self.comp_list is not None dAk = mx.zeros_like(self.A) zeta = mx.zeros((self.n_comps,), dtype=mx.float32) for h in range(self.n_models): idx = self.comp_list[:, h] - dAk = dAk.at[idx, :].add(gm_prev[h] * (directions[h].T @ self.A[idx, :])) - zeta = zeta.at[idx].add(gm_prev[h] + mx.zeros((self.n_channels,))) + dAk = dAk.at[idx, :].add(self.gm[h] * (directions[h].T @ self.A[idx, :])) + zeta = zeta.at[idx].add(self.gm[h] + mx.zeros((self.n_channels,))) dAk = dAk / mx.maximum(zeta, tiny)[:, None] # Weight-gradient norm (Fortran ndtmpsum, amica15.f90:1760-1761): @@ -1833,10 +1833,10 @@ def _update_parameters(self, acc: dict, n_samples: int): # (no .item() here) so it rides fit()'s single per-iteration mx.eval. # # SELECT, not multiply-by-mask: 0*NaN is NaN, so a single non-finite - # row would poison the whole reduction, and a NaN ndtmpsum silently - # disables BOTH grad-norm stops (NaN <= min_nd is False), burning the - # entire iteration budget with no diagnostic. mx.where drops the masked - # lanes structurally instead. Unreachable today (a merged-away row's + # row would poison the whole reduction, and a NaN ndtmpsum would + # disable BOTH grad-norm stops (NaN <= min_nd is False) if fit() did not + # stop on it ("nan_direction"); a masked-away row must not trigger that + # stop. mx.where drops the masked lanes structurally instead. Unreachable today (a merged-away row's # dAk is exactly 0), but Phase 3's Newton direction feeds this same dAk. used_f = self._comp_used_arr.astype(mx.float32) nd = (dAk**2).sum(axis=1) # (n_comps,) @@ -1845,12 +1845,73 @@ def _update_parameters(self, acc: dict, n_samples: int): nd.sum() / (self.n_channels * mx.maximum(used_f.sum(), 1.0)) ) - # A-update. When sharing holds A this iteration (the post-merge settle - # window, Fortran amica15.f90:1803), skip the step -- the lrate ramp, the - # Newton-fallback bookkeeping, and the step itself -- so a discarded - # Newton direction cannot pollute the fallback counter. + return _UpdateStep(dAk, newton_active, no_newt) + + def _update_parameters( + self, acc: dict, n_samples: int, step: Optional[_UpdateStep] = None + ): + """Exact-EM mixture updates + natural-gradient A-update, optionally + Newton-preconditioned (``AMICATorchNG._update_parameters``). + + ``step`` is the mixing-matrix step :meth:`_update_direction` computed + from the same ``acc``: :meth:`fit` passes the one its stopping checks + already read, so nothing is computed twice. A direct call may omit it, + and the step is then computed here first, from the parameters as they + stand.""" + if step is None: + step = self._update_direction(acc) + # The step was built with the pre-update gm (_update_direction), so gm + # can be rebound now. + self.gm = acc["dgm"] / n_samples # (n_models,); == 1 for single model + tiny = float(np.finfo(np.float32).tiny) + + # Per-model data-space bias c[i,h] = sum_t v_h*x / sum_t v_h (Fortran + # update_c, as in ``AMICATorchNG._update_parameters``). Skipped for n_models=1 + # (v==1 => c is the zero data mean; the update would add a float-sum residual + # and break the #24 bit-exact single-model path). A dead model (dgm[h]==0) keeps + # its prior c rather than writing 0/0, and is surfaced (matching AMICATorchNG). + if self.n_models > 1: + dgm = acc["dgm"] + live = dgm > 0.0 + new_c = acc["dc_numer"] / mx.maximum(dgm, tiny)[None, :] + self.c = mx.where(live[None, :], new_c, self.c) + if not bool(mx.all(live).item()): + logger.warning( + "Zero-responsibility model(s) at iter %d; kept their prior " + "bias c (dead-model guard).", + self.iteration, + ) + + # Component sharing (#263): a component merged away by + # _identify_shared_comps is no longer referenced by comp_list, so no + # sufficient statistic accumulates into its column (dalpha_n/dmu_d/ + # dbeta_d == 0) and the divisions below would be 0/0 = NaN -- which the + # nan_params guard in fit() would (correctly) abort on. Update only USED + # columns and freeze the rest at their last finite value (Fortran carries + # NaN there behind its comp_used mask; keeping them finite matches + # AMICATorchNG and the NumPy backend, docs/guides/amica-differences.md + # row 8). With the default full comp_list every column is used, so + # ``used`` is all-True and every update below is bit-for-bit unchanged. + assert self._comp_used_arr is not None + used = self._comp_used_arr[None, :] # (1, n_comps) + + self.alpha = mx.where( + used, + acc["dalpha_n"] / acc["dalpha_n"].sum(axis=0, keepdims=True), + self.alpha, + ) + + # The A branch, where the reference has it: after gm/alpha/c and before + # mu/sbeta/rho (amica15.f90:1803-1816; ``AMICATorchNG._update_parameters``). + # On an iteration the reference holds A + # (:func:`pamica.schedule.share_freeze`), everything inside it is skipped + # together: the Newton-fallback bookkeeping (so a discarded Newton + # direction cannot pollute the fallback counter), the lrate ramp, the + # reset of the working rho rate to its ceiling, and the step itself. The + # step was built by _update_direction from the parameters the E-step + # saw, so taking it before the mixture updates changes nothing they read. if not self._a_frozen(): - if newton_active and no_newt: + if step.newton_active and step.no_newt: # Fortran prints "Hessian not positive definite, using natural # gradient" (amica15.f90:1809-1811). Surface the same signal so # an all-fallback run is visible without re-instrumenting. @@ -1861,8 +1922,10 @@ def _update_parameters(self, acc: dict, n_samples: int): ) # Learning-rate ramp: toward newtrate while Newton is active and - # stable, otherwise toward lrate_cap (Fortran amica15.f90:1803-1816). - if newton_active and not no_newt: + # stable, otherwise toward lrate_cap (Fortran amica15.f90:1803-1816), + # from this iteration's lrate, which a likelihood decrease has + # already halved (fit runs the response first, issue #339). + if step.newton_active and not step.no_newt: self.lrate = min( self.newtrate, self.lrate + min(1.0 / self.newt_ramp, self.lrate) ) @@ -1870,8 +1933,62 @@ def _update_parameters(self, acc: dict, n_samples: int): self.lrate = min( self.lrate_cap, self.lrate + min(1.0 / self.newt_ramp, self.lrate) ) + # rho moves at the ceiling (``rholrate = rholrate0``, :1806/:1813), + # so a decrease's scaling of the working rate reaches it only on a + # held iteration. + self.rholrate = self.rholrate_cap - self.A = self.A - self.lrate * dAk + self.A = self.A - self.lrate * step.dAk + + self.mu = mx.where(used, self.mu + acc["dmu_n"] / acc["dmu_d"], self.mu) + self.beta = mx.where( + used, + mx.clip( + self.beta * mx.sqrt(acc["dbeta_n"] / acc["dbeta_d"]), + self.invsigmin, + self.invsigmax, + ), + self.beta, + ) + + # GG shape update with the 1/psi(1+1/rho) digamma factor (Fortran + # :2013-2014); digamma is computed host-side (MLX has none). A NaN here + # (e.g. from an upstream mu/beta blow-up) is reset to rho0 and surfaced, + # matching ``AMICATorchNG._update_parameters``, so it does not silently + # poison the lgamma table and every subsequent E-step. + # Deliberate divergence from ``AMICATorchNG._update_parameters``, which also + # skips the update when rho is pinned to a boundary (all 1.0 or all 2.0): + # that early-exit needs a host sync on a (n_mix, n_comps) reduction over + # rho every iteration. This backend does make a few scalar host syncs per + # iteration -- the dead-model check above, the rho-NaN canary below, and + # the Newton posdef flag -- each because a Python branch genuinely + # depends on the value; the rho-boundary exit is not one of those, since + # mx.clip clamps straight back to the boundary and the results are + # identical either way. So it would buy nothing but a sync -- unrelated to + # (and not fixed by) issue #265: pdftype != 0 is now supported, and for + # those families this whole block is skipped by the outer ``self.dorho`` + # gate below (rho is frozen, Fortran ``dorho=.false.``), which is the + # real reason the digamma work does not run there -- not an MLX + # limitation. See ``_get_block_updates`` for the matching ``drho_n`` + # accumulator gate (a work-only divergence from AMICATorchNG, which + # always accumulates and discards it). + if self.dorho: + drho = acc["drho_n"] / mx.maximum(acc["dalpha_n"], 1e-8) + rho_np = np.array(self.rho, dtype=np.float64) + psi = mx.array(digamma(1.0 + 1.0 / rho_np).astype(np.float32)) + new_rho = self.rho + self.rholrate * (1.0 - (self.rho / psi) * drho) + nan_mask = mx.isnan(new_rho) + if bool(mx.any(nan_mask).item()): + logger.warning( + "NaN in rho update at iter %d; resetting to rho0=%g.", + self.iteration, + self.rho0, + ) + new_rho = mx.where(nan_mask, self.rho0, new_rho) + # ``used`` freezes a merged-away column's rho, as for mu/alpha/beta. + self.rho = mx.where( + used, mx.clip(new_rho, self.minrho, self.maxrho), self.rho + ) # The reference rescales every iteration (it parses ``scalestep`` but # never reads it, amica15.f90:1843/3686); pamica keeps ``scalestep`` as @@ -2184,28 +2301,21 @@ def _reject_outliers(self, ll_vec: np.ndarray) -> None: # Component sharing (issue #263; AMICATorchNG's #60 port) # ------------------------------------------------------------------ def _a_frozen(self) -> bool: - """Whether the A-update (and its lrate ramp) is held this iteration. - - A is frozen for the first 6 iterations of every ``share_iter``-length - window once ``iter >= share_start`` -- the merge iteration and the 5 - after it -- so the density parameters can settle onto any freshly merged - component before the mixing matrix moves again (Fortran A-freeze, - amica15.f90:1803). The window fires each cycle regardless of whether that - cycle's :meth:`_identify_shared_comps` actually merged a pair. - - Anchored on ``share_start`` (:func:`pamica.schedule.share_freeze`, which - counts iterations from 1 like the reference) so it stays aligned with - the merge schedule for any ``share_start``; the literal Fortran formula - uses ``mod(iter, share_iter)`` (misaligned unless share_start is a - multiple of share_iter, and a permanent freeze for ``share_iter <= 6``), - but that path is dead in the reference (see :meth:`_identify_shared_comps`) - so there is no parity constraint -- the constructor requires - ``share_iter > 6`` so the window never consumes the whole cycle. Gated - behind ``share_comps`` and ``n_models >= 2``, so with sharing off it is - always False and the validated default trajectory is untouched. + """Whether the reference holds the A update (with its lrate ramp and + rho-rate reset) this iteration: once ``iter >= share_start``, every + iteration with ``mod(iter, share_iter) <= 5``, counted from 1 + (amica15.f90:1803, :func:`pamica.schedule.share_freeze`; + ``AMICATorchNG._a_frozen``). + + The reference applies this whether or not ``share_comps`` is on and for + any number of models (issue #345), so this does too: with the defaults + ``share_start = share_iter = 100``, every fit of 100 or more iterations + holds A on iterations 100-105, 200-205, and so on. The reference's own + scan never merges (see :meth:`_identify_shared_comps`), so this + unconditional schedule is the only freeze it ever shows. The + constructor requires ``share_iter >= 7``, since a shorter cycle would + hold A for good (:func:`pamica.schedule.validate_share_iter`). """ - if not self.share_comps or self.n_models < 2: - return False return schedule.share_freeze(self.iteration, self.share_start, self.share_iter) def _component_sensor_maps(self) -> np.ndarray: @@ -2391,7 +2501,7 @@ def shared_components(self) -> list: # ... the schedule/counters a fit mutates ... "iteration", "ll_history", "final_ll_", "stop_reason", "n_newton_fallbacks", "n_kurt_done", - "lrate", "lrate_cap", "newtrate", "rholrate", + "lrate", "lrate_cap", "newtrate", "rholrate", "rholrate_cap", # ... the LLt stash and its materialized arrays (issue #157, epic # #278 Phase 3/#289) ... "_llt_logv", "_llt_ll", "_llt_lht", "_llt_lt", @@ -2457,8 +2567,8 @@ def fit( Records (index-aligned, always populated): ``restart_seeds_``, ``restart_lls_`` (NaN where a restart ended degenerate) and ``restart_stop_reasons_``; the winner is named in one INFO log line. A - degenerate restart (``nan_ll``/``singular_ll``/``nan_params``) is - excluded from selection but recorded; if every restart is degenerate the + degenerate restart (``nan_ll``/``singular_ll``/``nan_direction``/ + ``nan_params``) is excluded from selection but recorded; if every restart is degenerate the model is left holding the last one. ``mir_step``, as :meth:`_fit_once`, is passed through to every @@ -2543,6 +2653,25 @@ def _fit_once( """Run one fit (one initialization, one EM loop) -- what :meth:`fit` calls once per restart. ``X`` is ``(n_channels, n_samples)``. + Iteration order (issue #339), as in ``AMICATorchNG._fit_once``: each + iteration runs the E-step (its likelihood is appended to + ``ll_history``, and the step for ``A`` and its norm are built), then + the likelihood-decrease response and the stopping checks, and only + then, unless a check fired, the parameter update with the rates just + set. ``iteration`` is the 0-based index of the last iteration whose + E-step ran. A convergence stop (``"min_dll"``, ``"grad_norm"``, + ``"grad_norm_floor"``, ``"lrate_floor"``) takes no update on the + stopping iteration, so ``final_ll_`` (without a keep-best restore) is + exactly the log-likelihood of the returned parameters; a fit that runs + to ``max_iter`` takes the last iteration's update, so its + ``final_ll_ == ll_history[-1]`` is the likelihood one update before the + returned parameters, as in the reference. A ``"nan_direction"`` stop + (a non-finite step or gradient norm, caught before any check reads it) + and a ``"nan_params"`` stop (non-finite parameters right after an + update) both record their iteration's (finite) likelihood; a + ``"nan_ll"``/``"singular_ll"`` stop does not record the non-finite one. + All four are degenerate (``_DEGENERATE_STOP_REASONS``). + Under ``share_comps``, if a merge fires on the LAST iteration, the returned ``A``/``W``/``comp_list`` are already post-merge but ``final_ll_`` still reports the pre-merge log-likelihood; see that @@ -2552,12 +2681,12 @@ def _fit_once( ``LLt`` (``_llt_lht``/``_llt_lt``, written by :meth:`write_amica_output`) is the per-sample log-likelihood stashed by the E-step that produced ``final_ll_``, never a separate post-fit - forward pass -- so it is one M-step older than the returned ``W``/ - ``A`` (Fortran's own convention; see ``docs/guides/amica-differences.md``'s - "one M-step older" section) unless the ``keep_best`` safeguard - restored an earlier iterate, in which case the restored stash and the - restored parameters come from the same point in the loop and there is - no staleness at all. + forward pass -- so after a fit that ran to ``max_iter`` it is one + M-step older than the returned ``W``/``A`` (Fortran's own convention; + see ``docs/guides/amica-differences.md``'s "one M-step older" + section). After a convergence stop, or a ``keep_best`` restore of an + earlier iterate, the stash and the returned parameters come from the + same point in the loop and there is no staleness at all. ``mir_step`` (issue #137, epic #278 Phase 3/#289), if > 0, computes MIR from the current ``W``/``sphere`` every ``mir_step`` iterations @@ -2569,7 +2698,9 @@ def _fit_once( parameters, not ``mir_history_[-1]``. Not index-aligned with ``ll_history``: entry ``i`` is computed after iteration ``i``'s parameter update, while ``ll_history[i]`` is the likelihood of the - parameters before it, so the two are one update apart (issue #161). + parameters before it, so the two are one update apart (issue #161); + an iteration that ends the fit on a stop takes no update and records + no waypoint. Incompatible with PCA reduction, same as :meth:`mir` itself, and gated exactly as ``AMICATorchNG._fit_once`` gates it (issue #323): an explicit reduction request, ``pcakeep < n_channels`` or any @@ -2702,17 +2833,29 @@ def _fit_once( except ImportError: pass + # One iteration follows the reference's main loop (amica15.f90:949-1142, + # issue #339; ``AMICATorchNG._fit_once``): the E-step with LL(iter), the + # step and its norm; the likelihood-decrease response and the stopping + # checks; an exit BEFORE any parameter moves if a check fired; otherwise + # the update with the rates the response just set, then the remaining + # hooks and rejection. for it in rng: self.iteration = it X_use = X_t[:, self.good_idx] if self.do_reject else X_t n_use = X_use.shape[1] acc = self._accumulate_blocks(X_use, stash_llt=True) + # The step and its norm come from the same E-step and are read by + # the checks below (the reference builds both in + # accum_updates_and_likelihood, amica15.f90:1749-1761). Built lazily + # here so the sync below materializes them with the likelihood, one + # sync as before; a non-finite likelihood simply discards them. + step = self._update_direction(acc) ll_arr = acc["ll"] / (n_use * self.n_channels) # The stash scatter (_accumulate_blocks) is part of the same lazy # graph as ll_arr, so materializing both here costs exactly the # one sync this line already paid -- not a second one (issue #157). - mx.eval(ll_arr, self._llt_logv, self._llt_ll) + mx.eval(ll_arr, self._llt_logv, self._llt_ll, step.dAk, self._nd_arr) ll = float(ll_arr.item()) if not math.isfinite(ll): self.stop_reason = "nan_ll" if math.isnan(ll) else "singular_ll" @@ -2723,11 +2866,162 @@ def _fit_once( # Best-iterate safeguard (issue #51): remember the parameters that # produced this LL when it is the best seen, so a later overshoot - # does not leave the returned model below this peak. + # does not leave the returned model below this peak. Nothing has + # moved them since the E-step, so the snapshot pairs them with ll. if track_best and ll > best_ll: best_ll = ll best_snapshot = self._snapshot_params() + self.ll_history.append(ll) + + # A non-finite step or norm would pass both gradient-norm checks + # below (NaN <= min_nd is False) and then be applied, so stop on it + # here, before any check reads it and before the update, with the + # parameters whose (finite) likelihood was just recorded (as + # AMICATorchNG does). Both were materialized with the likelihood. + nd_now = self._ndtmpsum + if not ( + nd_now is not None + and math.isfinite(nd_now) + and bool(mx.all(mx.isfinite(step.dAk)).item()) + ): + self.stop_reason = "nan_direction" + logger.warning( + "Non-finite update direction (ndtmpsum %s) at iteration %d; " + "stopping before the update.", + nd_now, + it, + ) + break + + # Learning-rate control (Fortran amica15.f90:1051-1097): anneal the + # working lrate (and the working rho rate) on an LL decrease; ratchet + # the ceilings after maxdecs persistent decreases. All of it runs + # before this iteration's update, as in the reference, so the update + # below already uses the new rates. + # + # The working rho rate is scaled on every decrease, as the + # reference's is (:1063), but every update of A resets it to its + # ceiling before rho moves (_update_parameters, amica15.f90:1806/ + # 1813), so the scaling only reaches rho on an iteration on which A + # is held. It is not a monotone decay (the issue #195 collapse): + # nothing but the maxdecs ratchet lowers the ceiling. + # + # have_prev mirrors Fortran's outer ``if (iter > 1)`` + # (amica15.f90:1051), which wraps the decrease branch AND the two + # stops below, so none of the three can fire on the first iteration. + # + # PRECEDENCE NOTE (mirroring the same note in AMICATorchNG.fit): the + # three blocks are independent -- none is gated on ``leave`` already + # being True from an earlier block this same iteration, matching + # Fortran's own structure of independent ``leave = .true.`` + # assignments with no declared precedence. Whichever block runs LAST + # and finds its condition true wins the reported stop_reason, so with + # this source order the standalone grad_norm block always has final + # say; under the shipped use_grad_norm=True default that makes the + # decrease branch's "grad_norm_floor" unreachable as the FINAL reason + # (its condition is strictly narrower). Deliberately not restructured + # into an explicit precedence, to keep this a direct port of + # amica15.f90:1051-1098. + have_prev = len(self.ll_history) > 1 + leave = False + if have_prev and ll < self.ll_history[-2]: + if self.lrate <= self.minlrate: + logger.warning( + "lrate floor (%g) reached at iter %d; stopping.", + self.minlrate, + it, + ) + self.stop_reason = "lrate_floor" + leave = True + elif self._ndtmpsum is not None and self._ndtmpsum <= self.min_nd: + # Fortran amica15.f90:1058's ``.or. (ndtmpsum .le. min_nd)`` + # half of the decrease stop (issue #207 gap 3, #248 here): + # the same per-iteration value use_grad_norm reads below, so + # a run whose lrate oscillates instead of annealing still + # stops instead of burning the whole budget. + logger.warning( + "gradient-norm floor (%g) reached at iter %d on a " + "likelihood decrease; stopping.", + self.min_nd, + it, + ) + self.stop_reason = "grad_norm_floor" + leave = True + else: + self.lrate *= self.lratefact + self.rholrate *= self.rholratefact + numdecs += 1 + if numdecs >= self.maxdecs: + self.lrate_cap *= self.lratefact + if schedule.past_newton_start(it, self.newt_start): + self.rholrate_cap *= self.rholratefact + if self.do_newton and schedule.past_newton_start( + it, self.newt_start + ): + # The Newton ceiling ratchets on the same maxdecs + # cadence as lrate_cap/rholrate_cap (Fortran + # amica15.f90:1056-1077), so a run that keeps + # overshooting at newtrate anneals instead of + # oscillating there. + self.newtrate *= self.lratefact + numdecs = 0 + + # Small-likelihood-increase stop (Fortran amica15.f90:1078-1090, + # use_min_dll/min_dll/maxincs). Independent of the decrease branch + # above: it runs every iteration once have_prev, including iterations + # where the LL just decreased (a decrease is always "less than" a + # positive min_dll, so it also increments numincs there, matching + # Fortran exactly). numincs resets to 0 on any gain >= min_dll; stops + # only after MORE than maxincs *consecutive* small gains. + if have_prev and self.use_min_dll: + if ll - self.ll_history[-2] < self.min_dll: + numincs += 1 + if numincs > self.maxincs: + logger.warning( + "likelihood increasing by less than %g for more than " + "%d iterations; stopping at iter %d.", + self.min_dll, + self.maxincs, + it, + ) + self.stop_reason = "min_dll" + leave = True + else: + numincs = 0 + + # Weight-gradient-norm stop (Fortran amica15.f90:1091-1097, + # use_grad_norm/min_nd). Also independent of the decrease branch: + # this is the unconditional every-iteration check, as opposed to the + # decrease-gated grad_norm_floor above. + if ( + have_prev + and self.use_grad_norm + and self._ndtmpsum is not None + and self._ndtmpsum <= self.min_nd + ): + logger.warning( + "norm of weight gradient <= %g at iter %d; stopping.", + self.min_nd, + it, + ) + self.stop_reason = "grad_norm" + leave = True + + # Switching Newton on changes the step direction, so the decrease + # counter accumulated during the natural-gradient phase no longer + # describes the schedule now running: Fortran clears it on the + # switch-on iteration (amica15.f90:1099-1102, + # ``AMICATorchNG._fit_once``). + if schedule.newton_switches_on(self.do_newton, it, self.newt_start): + numdecs = 0 + + # Stop before this iteration's update, as the reference does + # (amica15.f90:1111, ahead of update_params at :1122): the returned + # parameters are the ones whose LL was just recorded. + if leave: + break + # Whether rejection fires this iteration (Fortran schedule, # amica15.f90:1136, with rejstart counted from 1 as the reference # counts it; issue #335). Captured here, before _update_parameters, so @@ -2755,14 +3049,13 @@ def _fit_once( else: reject_ll = None - self._update_parameters(acc, n_use) + self._update_parameters(acc, n_use, step) # One eval per iteration bounds the lazy graph to a single iteration's # worth of ops (the updated params feed the next accumulate). gm/c are # included so their dependency chain is materialized each iteration too # (c depends on the prior iteration's c), not left to grow unbounded. - # _nd_arr (the grad-norm stops' input) rides along here rather than - # being materialized inside _update_parameters, so reading it below - # costs no extra sync. + # _nd_arr (the grad-norm stops' input) was materialized with the + # likelihood above, before the checks read it. mx.eval( self.A, self.W, @@ -2772,7 +3065,6 @@ def _fit_once( self.rho, self.gm, self.c, - self._nd_arr, self._logdet_W, ) @@ -2782,10 +3074,10 @@ def _fit_once( # blow-up would otherwise complete as max_iter with silently NaN # parameters (the torch backend has state_dict as a backstop; the # MLX backend does not, so guard in fit()). Params are already - # materialized by the mx.eval above, so this is a cheap read. - # _nd_arr is included as defense in depth: a non-finite gradient norm - # silently disables both grad-norm stops (NaN <= min_nd is False), so - # it must not be the one quantity nothing checks. + # materialized by the mx.eval above, so this is a cheap read. The + # gradient norm is not among them: a non-finite one already stopped + # the fit before the update ("nan_direction", above). AMICATorchNG + # and the NumPy backend run the same check with the same message. checked = { "A": self.A, "mu": self.mu, @@ -2794,7 +3086,6 @@ def _fit_once( "rho": self.rho, "gm": self.gm, "c": self.c, - "ndtmpsum": self._nd_arr, # W and its log-determinant are DERIVED from A by # mx.linalg.inv/slogdet, so a non-finite value can reach the # caller while A itself is still finite -- and nothing else @@ -2824,10 +3115,8 @@ def _fit_once( params_finite = params_finite & mx.all(mx.isfinite(value)) if not bool(params_finite.item()): # Name the offenders. Everything here is already materialized, so - # the per-tensor reads add no mid-graph sync -- this is the MLX - # stand-in for AMICATorchNG's inline mu/beta/alpha canary - # (in ``AMICATorchNG._update_parameters``), which MLX cannot afford - # inside _update_parameters because it would sync the lazy graph. + # the per-tensor reads add no mid-graph sync (a check inside + # _update_parameters would sync the lazy graph mid-update). bad = [ name for name, value in checked.items() @@ -2872,18 +3161,16 @@ def _fit_once( # stale W (pre-merge comp_list) while indexing the densities by the # merged comp_list. No-op when share_comps is off or n_models == 1. # - # This runs AFTER ``ll`` (this iteration's LL) was captured above, + # This runs AFTER ``ll`` (this iteration's LL) was recorded above, # so a merge on the final iteration lands in the returned - # A/W/comp_list but not in the ``ll_history``/``final_ll_`` value - # appended just below -- see final_ll_'s comment (issue #269). + # A/W/comp_list but not in ``ll_history``/``final_ll_`` -- see + # final_ll_'s comment (issue #269). if self.share_comps and schedule.periodic_due( it, self.share_start, self.share_iter ): self._identify_shared_comps() self._update_unmixing_matrices() - self.ll_history.append(ll) - # MIR waypoint (issue #137), following AMICATorchNG's idiom. # Computed from the CURRENT W/sphere (just rebuilt above by # _update_parameters / the share_comps block) against the raw, @@ -2938,133 +3225,8 @@ def _fit_once( else: self.mir_history_.append((it, mir_nats, mir_var)) - # Learning-rate control (Fortran amica17.f90:1062-1108): anneal on an - # LL decrease; ratchet the ceilings after maxdecs persistent decreases. - # - # rholrate is a maxdecs-ratcheted CEILING, not a per-decrease-annealed - # working rate. Fortran resets rholrate=rholrate0 every iteration before - # the rho update (amica15.f90:1806/1813) and only tightens the rholrate0 - # ceiling at maxdecs (amica15.f90:1068, gated on iter > newt_start), so - # its per-decrease rholrate*=rholratefact (:1045) never reaches the rho - # update. rho has no ramp, so self.rholrate carries that ceiling directly - # (reset to rholrate0 at fit start, nothing re-inflates it) and must - # ratchet ONLY at maxdecs. The previous per-decrease decay collapsed the - # rho rate to ~1e-5 within a few hundred iterations and froze rho at a - # stale shape (issue #195, mirroring the torch/numpy fix in #193/#194). - # - # have_prev mirrors Fortran's outer ``if (iter > 1)`` - # (amica15.f90:1051), which wraps the decrease branch AND the two - # stops below, so none of the three can fire on the first iteration. - # - # PRECEDENCE NOTE (mirroring the same note in AMICATorchNG.fit): the - # three blocks are independent -- none is gated on ``leave`` already - # being True from an earlier block this same iteration, matching - # Fortran's own structure of independent ``leave = .true.`` - # assignments with no declared precedence. Whichever block runs LAST - # and finds its condition true wins the reported stop_reason, so with - # this source order the standalone grad_norm block always has final - # say; under the shipped use_grad_norm=True default that makes the - # decrease branch's "grad_norm_floor" unreachable as the FINAL reason - # (its condition is strictly narrower). Deliberately not restructured - # into an explicit precedence, to keep this a direct port of - # amica15.f90:1051-1098. - have_prev = len(self.ll_history) > 1 - leave = False - if have_prev and ll < self.ll_history[-2]: - if self.lrate <= self.minlrate: - logger.warning( - "lrate floor (%g) reached at iter %d; stopping.", - self.minlrate, - it, - ) - self.stop_reason = "lrate_floor" - leave = True - elif self._ndtmpsum is not None and self._ndtmpsum <= self.min_nd: - # Fortran amica15.f90:1058's ``.or. (ndtmpsum .le. min_nd)`` - # half of the decrease stop (issue #207 gap 3, #248 here): - # the same per-iteration value use_grad_norm reads below, so - # a run whose lrate oscillates instead of annealing still - # stops instead of burning the whole budget. - logger.warning( - "gradient-norm floor (%g) reached at iter %d on a " - "likelihood decrease; stopping.", - self.min_nd, - it, - ) - self.stop_reason = "grad_norm_floor" - leave = True - else: - self.lrate *= self.lratefact - numdecs += 1 - if numdecs >= self.maxdecs: - self.lrate_cap *= self.lratefact - if schedule.past_newton_start(it, self.newt_start): - self.rholrate *= self.rholratefact - if self.do_newton and schedule.past_newton_start( - it, self.newt_start - ): - # The Newton ceiling ratchets on the same maxdecs - # cadence as lrate_cap/rholrate (Fortran - # amica15.f90:1056-1077), so a run that keeps - # overshooting at newtrate anneals instead of - # oscillating there. - self.newtrate *= self.lratefact - numdecs = 0 - - # Small-likelihood-increase stop (Fortran amica15.f90:1078-1090, - # use_min_dll/min_dll/maxincs). Independent of the decrease branch - # above: it runs every iteration once have_prev, including iterations - # where the LL just decreased (a decrease is always "less than" a - # positive min_dll, so it also increments numincs there, matching - # Fortran exactly). numincs resets to 0 on any gain >= min_dll; stops - # only after MORE than maxincs *consecutive* small gains. - if have_prev and self.use_min_dll: - if ll - self.ll_history[-2] < self.min_dll: - numincs += 1 - if numincs > self.maxincs: - logger.warning( - "likelihood increasing by less than %g for more than " - "%d iterations; stopping at iter %d.", - self.min_dll, - self.maxincs, - it, - ) - self.stop_reason = "min_dll" - leave = True - else: - numincs = 0 - - # Weight-gradient-norm stop (Fortran amica15.f90:1091-1097, - # use_grad_norm/min_nd). Also independent of the decrease branch: - # this is the unconditional every-iteration check, as opposed to the - # decrease-gated grad_norm_floor above. - if ( - have_prev - and self.use_grad_norm - and self._ndtmpsum is not None - and self._ndtmpsum <= self.min_nd - ): - logger.warning( - "norm of weight gradient <= %g at iter %d; stopping.", - self.min_nd, - it, - ) - self.stop_reason = "grad_norm" - leave = True - - # Switching Newton on changes the step direction, so the decrease - # counter accumulated during the natural-gradient phase no longer - # describes the schedule now running: Fortran clears it on the - # switch-on iteration (amica15.f90:1099-1102, - # ``AMICATorchNG._fit_once``). - if schedule.newton_switches_on(self.do_newton, it, self.newt_start): - numdecs = 0 - - if leave: - break - # Outlier rejection, after the parameter update (Fortran order, - # amica15.f90:1141-1146) but using the pre-update per-sample LL + # amica15.f90:1136-1140) but using the pre-update per-sample LL # captured above. if will_reject: assert reject_ll is not None @@ -3783,8 +3945,9 @@ def write_amica_output(self, outdir) -> None: Also writes ``LLt`` (the per-sample/per-model log-likelihood, issue #155) for a model that was just :meth:`fit` in this process, from the stash the training E-step filled (issue #157) -- so, exactly as in - the reference, ``LLt`` is the E-step of the returned iterate and is - one M-step older than the ``W``/``A`` written beside it (see + the reference, ``LLt`` is the E-step of the returned iterate: one + M-step older than the ``W``/``A`` written beside it after a fit that ran + to ``max_iter``, their own after a convergence stop (see :meth:`_fit_once`'s docstring). A model restored via :meth:`from_state_dict`/:meth:`load` carries no stash, so ``LLt`` is omitted for it (a warning is logged) -- the rest of the output is @@ -3817,7 +3980,7 @@ def write_amica_output(self, outdir) -> None: if self.stop_reason in self._DEGENERATE_STOP_REASONS: raise RuntimeError( f"Refusing to write output for a degenerate model (stop_reason=" - f"{self.stop_reason!r}): fit() hit a non-finite log-likelihood " + f"{self.stop_reason!r}): fit() hit a non-finite value " f"at iteration {self.iteration}. Fix the instability (lower " f"lrate, disable Newton, or check data conditioning) before " f"writing." @@ -3825,7 +3988,7 @@ def write_amica_output(self, outdir) -> None: # Defense-in-depth, mirroring state_dict(): catch a non-finite # parameter even if stop_reason bookkeeping ever misses it. Also # neutralizes a stale LLt stash: a failed final iteration's - # _llt_lht/_llt_lt (from before the nan_params/nan_ll break) can no + # _llt_lht/_llt_lt (from before a degenerate break) can no # longer reach disk once this guard refuses the write outright. nonfinite = self._nonfinite_params() if nonfinite: @@ -3965,7 +4128,7 @@ def state_dict(self) -> dict: if self.stop_reason in self._DEGENERATE_STOP_REASONS: raise RuntimeError( f"Refusing to serialize a degenerate model (stop_reason=" - f"{self.stop_reason!r}): fit() hit a non-finite log-likelihood " + f"{self.stop_reason!r}): fit() hit a non-finite value " f"at iteration {self.iteration}. Fix the instability (lower " f"lrate, disable Newton, or check data conditioning) before " f"saving." @@ -4084,6 +4247,7 @@ def state_dict(self) -> dict: "lrate_cap": float(self.lrate_cap), "newtrate": float(self.newtrate), "rholrate": float(self.rholrate), + "rholrate_cap": float(self.rholrate_cap), # Per-restart records (issue #198): which seeds ran, what each # returned, and why each stopped. "restart_seeds_": list(self.restart_seeds_), @@ -4307,10 +4471,10 @@ def _load_params(self, state: dict) -> None: self.stop_reason = extra["stop_reason"] self.n_kurt_done = extra["n_kurt_done"] self.n_newton_fallbacks = extra["n_newton_fallbacks"] - self.lrate = extra["lrate"] - self.lrate_cap = extra["lrate_cap"] - self.newtrate = extra["newtrate"] - self.rholrate = extra["rholrate"] + # The rates, validated (a non-finite one is a named ValueError); + # rholrate_cap is additive, NOT in _EXTRA_KEYS, see _saved_rates. + for name, value in _saved_rates(extra, "AMICAMLXNG").items(): + setattr(self, name, value) self.restart_seeds_ = list(extra["restart_seeds_"]) self.restart_lls_ = list(extra["restart_lls_"]) self.restart_stop_reasons_ = list(extra["restart_stop_reasons_"]) diff --git a/pamica/mne_compat/core.py b/pamica/mne_compat/core.py index 345e8a7e..00e1795d 100644 --- a/pamica/mne_compat/core.py +++ b/pamica/mne_compat/core.py @@ -669,7 +669,10 @@ def to_mne_ica(self, model_idx: int = 0) -> _MNEICA: ica.pca_explained_variance_ = cov_evals ica.unmixing_matrix_ = unmixing ica.pre_whitener_ = self.pre_whitener_ - ica.n_iter_ = max(int(getattr(amica.model_, "iteration", 0)), 1) + # The number of iterations the fit ran: ``iteration`` is the 0-based + # index of the last one whose E-step ran (issue #339), which is also the + # reference's own ``iter`` count at a stop. + ica.n_iter_ = int(getattr(amica.model_, "iteration", 0)) + 1 # MNE's own fit sets these; read_ica_eeglab (the precedent for building # an ICA from an external decomposition) sets reject_=None. Without them # ICA.save()/plot_properties raise AttributeError. diff --git a/pamica/numpy_impl/core.py b/pamica/numpy_impl/core.py index 384156ce..f791ff17 100644 --- a/pamica/numpy_impl/core.py +++ b/pamica/numpy_impl/core.py @@ -88,7 +88,7 @@ import logging import time from pathlib import Path -from typing import Dict, List, Optional, Tuple, Union +from typing import Dict, List, NamedTuple, Optional, Tuple, Union import numpy as np from scipy import linalg @@ -114,6 +114,20 @@ logger = logging.getLogger(__name__) + +class _UpdateStep(NamedTuple): + """The mixing-matrix step one E-step implies, computed before the + iteration's stopping checks and applied after them (the reference's + ``dAk`` and ``ndtmpsum``, built in ``accum_updates_and_likelihood``, + amica15.f90:1749-1761, before ``update_params``, :1782). See + :meth:`AMICA._update_direction`.""" + + dAk: np.ndarray # (num_comps, data_dim), before the lrate scaling + newton_active: bool # the Newton schedule is on this iteration + no_newt: bool # some model failed the positive-definiteness guard + nd: float # the weight-gradient norm ndtmpsum + + # Data-location keys: not AMICA hyperparameters, so they are excluded from # load_default_params()'s return value. AMICA.__init__ reads files/data_dim/ # field_dim off the same parsed dict separately (see _config_files/ @@ -661,7 +675,13 @@ def __init__( self.rho0 = params.get("rho0", 1.5) self.minrho = params.get("minrho", 1.0) self.maxrho = params.get("maxrho", 2.0) + # Working rho rate, its ceiling and the constructor value, as the other + # backends keep them (the reference's rholrate/rholrate0, + # amica15.f90:1063-1068, 1806/1813): a decrease scales the working + # rate, a maxdecs ratchet the ceiling, and every A update resets the + # working rate to the ceiling. self.rholrate = params.get("rholrate", 0.05) + self.rholrate_cap = self.rholrate self.rholrate0 = self.rholrate self.rholratefact = params.get("rholratefact", 0.1) self.invsigmax = params.get("invsigmax", 1000.0) @@ -695,17 +715,17 @@ def __init__( self.comp_thresh = params.get("comp_thresh", 0.99) self.share_start = params.get("share_start", 100) self.share_int = params.get("share_int", 100) + # The reference's A-freeze reads share_start/share_int whether or not + # share_comps is on (issue #345), so both are validated always, with + # AMICATorchNG's messages (pamica.schedule): share_start >= 1, and a + # share_int below 7, which would hold A for every iteration of every + # cycle, is rejected under both of this backend's spellings. + schedule.validate_share_start(self.share_start) + schedule.validate_share_iter(self.share_int, "share_int/share_iter") if self.share_comps: - # Same validation (and the same reasons) as AMICATorchNG: the merge - # schedule is 1-indexed, and the post-merge A-freeze settle window is - # 6 iterations, so a share_int of 6 or less would hold A frozen for - # every iteration of every cycle -- a fit that silently never moves - # its mixing matrix again. comp_thresh is a cosine cutoff, so it is - # only meaningful in (0, 1]; at 0 every pair of columns merges. - if self.share_start < 1: - raise ValueError(f"share_start must be >= 1, got {self.share_start}") - if self.share_int <= 6: - raise ValueError(f"share_int must be > 6, got {self.share_int}") + # Same validation (and the same reason) as AMICATorchNG: comp_thresh + # is a cosine cutoff, so it is only meaningful in (0, 1]; at 0 every + # pair of columns merges. if not 0.0 < self.comp_thresh <= 1.0: raise ValueError( f"comp_thresh must be in (0, 1], got {self.comp_thresh}" @@ -875,6 +895,7 @@ def __init__( "lrate0": self.lrate0, "newtrate": self.newtrate, "rholrate": self.rholrate, + "rholrate_cap": self.rholrate_cap, "block_size": self.block_size, } @@ -932,7 +953,11 @@ def __init__( self.iter = 0 self.ll = [] # Log likelihood history # ``self.ll[-1]`` is this backend's equivalent of AMICATorchNG's - # ``final_ll_``: the LL of the returned model. Under ``share_comps``, + # ``final_ll_``: the LL of the returned model. Exactly so when the fit + # ended on a convergence stop, which exits before that iteration's + # update as the reference does (issue #339); after max_iter the last + # iteration's update follows its LL, so it is the LL one update before + # the returned parameters, also as in the reference. Under ``share_comps``, # if a merge fires on the LAST fit iteration, ``self.A``/``comp_list`` # are already post-merge but ``self.ll[-1]`` still reports the # pre-merge log-likelihood -- the merge runs after that iteration's LL @@ -1156,6 +1181,20 @@ def fit(self, data: Optional[np.ndarray] = None) -> "AMICA": Notes ----- + Iteration order (issue #339), as in ``AMICATorchNG.fit`` and + ``AMICAMLXNG.fit``: each iteration runs the E-step, then the + restart-on-NaN window, the likelihood-decrease response and the + stopping checks, and only then, unless a check fired, the update with + the rates just set, followed by the share merge, the checkpoints and + rejection. ``self.ll`` and ``self.nd`` record every iteration whose + likelihood was finite, and nothing else. A convergence stop takes no + update on its stopping iteration, so the returned parameters are the + ones whose likelihood is ``self.ll[-1]``; a fit that runs to + ``max_iter`` updates on its last iteration, as the reference does. A + non-finite likelihood outside the restart window, a non-finite update + direction, and non-finite parameters after an update each stop the fit + with ``converged=False`` (see ``_DEGENERATE_STOP_REASONS``). + Under ``share_comps``, if a merge fires on the LAST iteration, the returned ``A``/``comp_list`` are already post-merge but ``self.ll[-1]`` still reports the pre-merge log-likelihood -- the merge's effect on @@ -1427,16 +1466,26 @@ def _fit_once(self) -> None: # Main optimization loop self._optimize() - # Record the outcome: a terminal non-finite LL means the fit diverged - # (even restart-on-NaN could not recover), which callers/CLI must be - # able to detect rather than silently trusting model.A/W. - self.converged = len(self.ll) > 0 and bool(np.isfinite(self.ll[-1])) - if not self.converged: + # Record the outcome: a degenerate stop (a non-finite likelihood that + # restart-on-NaN could not recover, a non-finite update direction, or + # non-finite parameters after an update) means the fit diverged, which + # callers/CLI must be able to detect rather than silently trusting + # model.A/W. self.ll holds only finite values, so the stop reason, not + # its last entry, is what says so. + degenerate_stop = self._is_degenerate_stop(self.stop_reason) + self.converged = len(self.ll) > 0 and not degenerate_stop + if degenerate_stop: self.logger.error( - "AMICA did not converge: the log-likelihood is non-finite " - "(diverged after %d restart(s)); results were not written.", + "AMICA did not converge: %s (after %d restart(s)); results were " + "not written.", + self.stop_reason, self.numrestarts, ) + elif not self.converged: + self.logger.error( + "AMICA did not converge: no iteration completed; results were " + "not written." + ) else: # A finite likelihood is not on its own proof of a usable fit: a # parameter can go non-finite in a way the LL does not see (a @@ -1473,7 +1522,7 @@ def _fit_once(self) -> None: # ... the ratcheted learning-rate ceilings (this backend anneals lrate0 # and newtrate in place), the tuned block size, and the seed/RNG the # winning restart ran from. - "lrate", "lrate0", "newtrate", "rholrate", + "lrate", "lrate0", "newtrate", "rholrate", "rholrate_cap", "block_size", "seed", "rng", ) # fmt: skip # Written by the fit path but identical across the restarts of one fit() @@ -1484,6 +1533,26 @@ def _fit_once(self) -> None: "mean", "sphere", "sldet", "_sphere_pinv", ) # fmt: skip + # The stop reasons that mark a fit degenerate, in this backend's prose + # vocabulary: a non-finite likelihood (the PyTorch/MLX "nan_ll" and + # "singular_ll"), a non-finite update direction ("nan_direction"), + # non-finite parameters after an update ("nan_params", followed by ": " and + # the offenders' names, as the exit check's reason is), and a restart that + # raised. fit() reports converged=False for each (_is_degenerate_stop). + _NONFINITE_LL_REASON = "Non-finite likelihood (NaN/-inf) encountered" + _NONFINITE_DIRECTION_REASON = "Non-finite update direction (NaN/inf) encountered" + _NONFINITE_PARAMS_REASON = "Non-finite parameters after an update" + _DEGENERATE_STOP_REASONS = ( + _NONFINITE_LL_REASON, + _NONFINITE_DIRECTION_REASON, + _NONFINITE_PARAMS_REASON, + restarts.ERROR_STOP_REASON, + ) + + # What the post-update guard checks, in the order AMICAMLXNG's guard names + # them (it adds its cached log-determinant of W). + _POST_UPDATE_CHECKED = ("A", "mu", "alpha", "beta", "rho", "gm", "c", "W") + # Every parameter a caller can read back off disk or off the object. `A` and # `W` are the decomposition; `c`/`mu`/`alpha`/`beta`/`rho`/`gm` are the model # a downstream `loadmodout` reads. `sphere`/`mean` are preprocessing outputs @@ -1501,7 +1570,16 @@ def _fit_once(self) -> None: "mean", ) - def _nonfinite_params(self) -> List[str]: + @classmethod + def _is_degenerate_stop(cls, reason: Optional[str]) -> bool: + """Whether ``reason`` is one of :attr:`_DEGENERATE_STOP_REASONS`, + with or without the ``": names"`` suffix the parameter reason carries.""" + return reason is not None and any( + reason == known or reason.startswith(f"{known}: ") + for known in cls._DEGENERATE_STOP_REASONS + ) + + def _nonfinite_params(self, names: Optional[Tuple[str, ...]] = None) -> List[str]: """Names of fitted parameters currently holding a non-finite value. The single definition of "this state is safe to persist or report as a @@ -1511,11 +1589,13 @@ def _nonfinite_params(self) -> List[str]: non-finite rather than only *that* something did. Parameters not yet allocated (before ``_initialize_parameters``) are - skipped rather than treated as bad. + skipped rather than treated as bad. ``names`` narrows the sweep (the + post-update guard in :meth:`_optimize` passes + :attr:`_POST_UPDATE_CHECKED`); the default is :attr:`_FITTED_PARAMS`. """ return [ name - for name in self._FITTED_PARAMS + for name in (self._FITTED_PARAMS if names is None else names) if getattr(self, name, None) is not None and not np.all(np.isfinite(np.asarray(getattr(self, name)))) ] @@ -1750,8 +1830,10 @@ def _reinitialize_for_restart(self): self.good_idx = saved_good_idx self.num_good_samples = saved_num_good self.lrate = self.lrate0 - # rholrate is the (maxdecs-ratcheted) rho-rate ceiling; restore it to the - # pristine rholrate0 so a re-fit starts fresh (issue #193). + # rholrate_cap is the (maxdecs-ratcheted) rho-rate ceiling; restore it, + # and the working rate with it, to the pristine rholrate0 so a re-fit + # starts fresh (issue #193). + self.rholrate_cap = self.rholrate0 self.rholrate = self.rholrate0 self.ll = [] self.nd = [] @@ -2215,9 +2297,12 @@ def _llt_arrays(self) -> Tuple[Optional[np.ndarray], Optional[np.ndarray]]: (amica15.f90:996, 1122, 1124-1127), so the ``LLt`` written at any checkpoint is the E-step of the parameters as they stood BEFORE that iteration's M-step: one M-step older than the ``W``/``A`` written beside - it. pamica's loop has the same three steps in the same order, so - stashing reproduces that exactly, and the on-disk ``LLt`` is comparable - with the binary's. The invariant this pins down, on both sides, is + it. The same holds for the final write after a fit that ran to + ``max_iter``; after a convergence stop the loop exits before the + update (amica15.f90:1111, issue #339), so the final ``LLt`` is the + E-step of the written parameters themselves. pamica's loop has the + same steps in the same order, so stashing reproduces that exactly, and + the on-disk ``LLt`` is comparable with the binary's. The invariant this pins down, on both sides, is ``Lt.sum() / (num_good_samples * data_dim) == self.ll[-1]``. That equality holds bit for bit except on a write whose iteration also @@ -2251,152 +2336,32 @@ def _llt_arrays(self) -> Tuple[Optional[np.ndarray], Optional[np.ndarray]]: return None, None return self._llt_logv.T, self._llt_ll - def _update_parameters(self, updates: Dict): - """ - Update model parameters using computed updates. - - Parameters - ---------- - updates : dict - Dictionary containing parameter updates + def _update_direction(self, updates: Dict) -> _UpdateStep: + """The natural-gradient or Newton step for ``A`` from this iteration's + sufficient statistics, and its norm, without changing ``A`` or any + density parameter. + + The reference computes both in ``accum_updates_and_likelihood`` + (amica15.f90:1666-1761), with ``LL(iter)``, before the likelihood- + decrease response and the stopping checks read ``ndtmpsum`` and before + ``update_params`` applies the step. It finalizes the Newton curvature + (``sigma2``/``lambda_``/``kappa``) as it goes but records nothing: + :meth:`_optimize` appends the iteration's likelihood and the returned + norm to ``self.ll``/``self.nd`` once the likelihood is known to be + finite, as the PyTorch and MLX loops append ``ll_history``, and hands + the step to :meth:`_update_parameters` once the checks have run. """ assert ( self.data_dim is not None - and self.num_samples is not None - and self.c is not None - and self.mu is not None - and self.alpha is not None - and self.beta is not None - and self.rho is not None + and self.gm is not None and self.comp_list is not None and self.A is not None ) - # Fortran builds dAk from the model weights of the *previous* iteration: - # gm is not reassigned until update_params (amica15.f90:1788+), which runs - # after accum_updates_and_likelihood (:1731-1743). Snapshot it here so the - # nd block below weights by the same gm Fortran would (issue #219). - assert self.gm is not None - gm_prev = self.gm.copy() - - # Update model weights, normalizing by the number of samples the E-step - # actually summed over: the good set under do_reject, else all samples. - if self.do_reject: - assert self.num_good_samples is not None - self.gm = updates["dgm"] / self.num_good_samples - else: - self.gm = updates["dgm"] / self.num_samples - - # Per-model data-space bias c (Fortran's `update_c` flag, amica17.f90:1423- - # 1429 numerator / :1899-1901 division): c[i,h] = sum_t v_h*x / sum_t v_h, - # the responsibility-weighted data mean for model h (the E-step centers - # each model at its own mean, b = W(x - c)). Skipped for a single model to - # keep the issue #24 parity bit-exact: with v==1 the update would add a - # ~1e-13 float-sum residual of the (mean-removed) data. dgm[h] = sum_t v_h - # is the denominator (Fortran `dc_denom`). A dead model (dgm[h]==0) gives - # 0/0; keep its prior c so a NaN cannot poison the next iteration's - # cross-model softmax for every model (mirrors the mu/beta/rho guards). - if self.num_models > 1: - dgm = updates["dgm"] - live = dgm > 0.0 - new_c = ( - updates["dc_numer"] - / np.maximum(dgm, np.finfo(np.float64).tiny)[None, :] - ) - self.c = np.where(live[None, :], new_c, self.c) - if not np.all(live): - self.logger.warning( - "Zero-responsibility model(s) at iter %d; kept prior bias c " - "(dead-model guard).", - self.iter, - ) - - # A component merged away by share_comps is no longer referenced by - # comp_list, so no sufficient statistic accumulates into its column and - # its mixture divisions would be 0/0 = NaN. Update only the used columns - # and leave the rest frozen at their last finite value, as AMICATorchNG - # does (Fortran instead carries the NaN harmlessly behind its comp_used - # mask; keeping them finite means a fit cannot report success while - # holding NaN parameters, issue #240). - # - # The dead columns are INDEXED OUT rather than computed and masked: a - # 0/0 that is never evaluated raises no RuntimeWarning, so nothing has to - # be suppressed with np.errstate and a genuine 0/0 in a LIVE column (a - # component whose responsibility mass collapses to exactly zero) still - # warns, as it does with sharing off. ``cols`` is a full slice whenever - # every column is live -- always so with the default comp_list -- which - # keeps the ordinary path bit-identical and copy-free. (comp_used is None - # until fit() sets it up, and the M-step is exercised directly in tests - # before that happens, so fall back to "all used".) used = ( self.comp_used if self.comp_used is not None else np.ones(self.num_comps, dtype=bool) ) - cols = slice(None) if used.all() else np.flatnonzero(used) - - # Update mixture weights - dalpha_n = updates["dalpha_n"][:, cols] - self.alpha[:, cols] = dalpha_n / np.sum(dalpha_n, axis=0) - # Fortran has no alpha canary, but a collapsed live component gives the - # same 0/0 the mu/beta canary below reports; surface it here too so the - # origin is not lost to a later unattributable nan-LL stop. - if not np.all(np.isfinite(self.alpha)): - self.logger.warning( - "Non-finite alpha at iter %d (component responsibility mass " - "collapsed).", - self.iter, - ) - - # Exact-EM mixture location/scale (Fortran :1978/:1993). These are - # fixed-point updates -- mu += dmu_n/dmu_d, beta *= sqrt(dbeta_n/dbeta_d) - # -- NOT first-order gradient steps, so they carry no lrate. - dmu = updates["dmu_n"][:, cols] / updates["dmu_d"][:, cols] - self.mu[:, cols] = self.mu[:, cols] + dmu - self.beta[:, cols] = self.beta[:, cols] * np.sqrt( - updates["dbeta_n"][:, cols] / updates["dbeta_d"][:, cols] - ) - self.beta = np.clip(self.beta, self.invsigmin, self.invsigmax) - # Fortran keeps a live "NaN in sbeta!" canary here (amica17.f90:1996-2000); - # the exact-EM mu/beta divisions are unguarded (matching Fortran), so - # surface a non-finite value immediately instead of letting it propagate - # to a later, unattributable nan-LL stop. - if not np.all(np.isfinite(self.mu)) or not np.all(np.isfinite(self.beta)): - self.logger.warning( - "Non-finite mu/beta at iter %d (mixture component mass likely " - "collapsed).", - self.iter, - ) - - # GG shape update with the 1/psi(1+1/rho) digamma factor (Fortran - # :2013-2014); the divisor is the per-component responsibility mass - # dalpha_n (floored so a near-empty component cannot poison rho). A NaN - # is reset to rho0 -- but logged first, so the reset does not silently - # erase the failure origin. - # Restricted to the live columns for the same reason as mu/beta/alpha - # (and as AMICATorchNG, which masks rho with ``used`` too): a merged-away - # column has no responsibility mass, so its rho would ratchet up on the - # floored denominator every iteration instead of staying at the value it - # was merged away with. - if not np.all(self.rho == 1.0) and not np.all(self.rho == 2.0): - rho_cols = self.rho[:, cols] - drho = updates["drho_n"][:, cols] / np.maximum( - updates["dalpha_n"][:, cols], 1e-8 - ) - psi = digamma(1.0 + 1.0 / rho_cols) - new_rho = rho_cols + self.rholrate * (1.0 - (rho_cols / psi) * drho) - nan_mask = np.isnan(new_rho) - if nan_mask.any(): - self.logger.warning( - "NaN in rho update at iter %d for %d component(s); resetting " - "to rho0=%g.", - self.iter, - int(nan_mask.sum()), - self.rho0, - ) - new_rho = np.where(nan_mask, self.rho0, new_rho) - self.rho[:, cols] = np.clip(new_rho, self.minrho, self.maxrho) - - # Update unmixing matrices newton_active = schedule.newton_active( self.do_newton, self.iter, self.newt_start ) @@ -2470,8 +2435,9 @@ def _update_parameters(self, updates: Dict): # through A (Fortran dAk/zeta, amica15.f90:1749-1761): each contributing # model adds gm[h]/zeta of its step to the shared component's row, where # zeta = sum of gm over the models that reference the component. Weighted - # by gm_prev, the PRE-update model weights, because Fortran builds dAk - # before update_params reassigns gm (issue #219). + # by the PRE-update model weights (gm is not reassigned until + # _update_parameters), because Fortran builds dAk before update_params + # reassigns gm (issue #219). # # The weights are normalized per model (gm[h]/zeta) rather than summed and # then divided (Fortran's literal sum-then-divide, and AMICATorchNG's): @@ -2489,7 +2455,7 @@ def _update_parameters(self, updates: Dict): # there is no missing factor here. zeta = np.zeros(self.num_comps) for h in range(self.num_models): - zeta[self.comp_list[:, h]] += gm_prev[h] + zeta[self.comp_list[:, h]] += self.gm[h] dAk = np.zeros_like(self.A) for h in range(self.num_models): # comp_list[:, h] holds distinct indices within a model @@ -2498,18 +2464,126 @@ def _update_parameters(self, updates: Dict): # drop a contribution here. Row i of the model's step is source i's # component, comp_list[i, h] (issue #334). idx = self.comp_list[:, h] - weight = gm_prev[h] / np.maximum(zeta[idx], np.finfo(np.float64).tiny) + weight = self.gm[h] / np.maximum(zeta[idx], np.finfo(np.float64).tiny) dAk[idx, :] += weight[:, None] * np.dot(directions[h].T, self.A[idx, :]) nd_value = float( np.sqrt(np.sum(dAk[used, :] ** 2) / (self.data_dim * int(used.sum()))) ) + return _UpdateStep(dAk, newton_active, no_newt, nd_value) + + def _update_parameters(self, updates: Dict, step: Optional[_UpdateStep] = None): + """ + Update model parameters using computed updates. + + Parameters + ---------- + updates : dict + Dictionary containing parameter updates + step : _UpdateStep, optional + The mixing-matrix step :meth:`_update_direction` computed from the + same ``updates``: :meth:`_optimize` passes the one its stopping + checks already read, so nothing is computed twice. A direct call + may omit it, and it is then computed here first; neither path + records anything in ``self.ll``/``self.nd``, which only + :meth:`_optimize` appends to. + """ + assert ( + self.data_dim is not None + and self.num_samples is not None + and self.c is not None + and self.mu is not None + and self.alpha is not None + and self.beta is not None + and self.rho is not None + and self.comp_list is not None + and self.A is not None + ) + if step is None: + step = self._update_direction(updates) + # The step was built with the pre-update gm (_update_direction), so gm + # can be reassigned now (Fortran reassigns it in update_params, + # amica15.f90:1788, after accum_updates_and_likelihood; issue #219). + + # Update model weights, normalizing by the number of samples the E-step + # actually summed over: the good set under do_reject, else all samples. + if self.do_reject: + assert self.num_good_samples is not None + self.gm = updates["dgm"] / self.num_good_samples + else: + self.gm = updates["dgm"] / self.num_samples + + # Per-model data-space bias c (Fortran's `update_c` flag, amica17.f90:1423- + # 1429 numerator / :1899-1901 division): c[i,h] = sum_t v_h*x / sum_t v_h, + # the responsibility-weighted data mean for model h (the E-step centers + # each model at its own mean, b = W(x - c)). Skipped for a single model to + # keep the issue #24 parity bit-exact: with v==1 the update would add a + # ~1e-13 float-sum residual of the (mean-removed) data. dgm[h] = sum_t v_h + # is the denominator (Fortran `dc_denom`). A dead model (dgm[h]==0) gives + # 0/0; keep its prior c so a NaN cannot poison the next iteration's + # cross-model softmax for every model (mirrors the mu/beta/rho guards). + if self.num_models > 1: + dgm = updates["dgm"] + live = dgm > 0.0 + new_c = ( + updates["dc_numer"] + / np.maximum(dgm, np.finfo(np.float64).tiny)[None, :] + ) + self.c = np.where(live[None, :], new_c, self.c) + if not np.all(live): + self.logger.warning( + "Zero-responsibility model(s) at iter %d; kept prior bias c " + "(dead-model guard).", + self.iter, + ) + + # A component merged away by share_comps is no longer referenced by + # comp_list, so no sufficient statistic accumulates into its column and + # its mixture divisions would be 0/0 = NaN. Update only the used columns + # and leave the rest frozen at their last finite value, as AMICATorchNG + # does (Fortran instead carries the NaN harmlessly behind its comp_used + # mask; keeping them finite means a fit cannot report success while + # holding NaN parameters, issue #240). + # + # The dead columns are INDEXED OUT rather than computed and masked: a + # 0/0 that is never evaluated raises no RuntimeWarning, so nothing has to + # be suppressed with np.errstate and a genuine 0/0 in a LIVE column (a + # component whose responsibility mass collapses to exactly zero) still + # warns, as it does with sharing off. ``cols`` is a full slice whenever + # every column is live -- always so with the default comp_list -- which + # keeps the ordinary path bit-identical and copy-free. (comp_used is None + # until fit() sets it up, and the M-step is exercised directly in tests + # before that happens, so fall back to "all used".) + used = ( + self.comp_used + if self.comp_used is not None + else np.ones(self.num_comps, dtype=bool) + ) + cols = slice(None) if used.all() else np.flatnonzero(used) + + # Update mixture weights + dalpha_n = updates["dalpha_n"][:, cols] + self.alpha[:, cols] = dalpha_n / np.sum(dalpha_n, axis=0) + # Fortran has no alpha canary, but a collapsed live component gives the + # same 0/0 the mu/beta canary below reports; surface it here too so the + # origin is not lost to a later unattributable nan-LL stop. + if not np.all(np.isfinite(self.alpha)): + self.logger.warning( + "Non-finite alpha at iter %d (component responsibility mass " + "collapsed).", + self.iter, + ) + + # The A branch, where the reference has it: after gm/alpha/c and before + # mu/sbeta/rho (amica15.f90:1803-1816). + # # A is stored as Fortran's A^T, one component per row (issue #334; true # unmixing = W^T = inv(block)^T), so the Fortran step on a model's block # A_fort(:, comp_list(:,h)) @ dir becomes dir^T @ A[comp_list[:, h], :] - # (LEFT-multiply by the TRANSPOSED direction). Right-multiply by the - # untransposed dir is invisible at the fixed point but sends the fit - # downhill -- issue #24 root cause. + # (LEFT-multiply by the TRANSPOSED direction), which _update_direction + # built into step.dAk. Right-multiply by the untransposed dir is + # invisible at the fixed point but sends the fit downhill -- issue #24 + # root cause. # # ONE application of the averaged dAk (Fortran's single DAXPY, # amica15.f90:1807/1814), not the per-model loop this used to run: a @@ -2517,14 +2591,15 @@ def _update_parameters(self, updates: Dict): # the second against an already-stepped A, which is a different # operation from Fortran's single weighted average (issue #242). A # merged-away component has no contributor, so its dAk row stays - # exactly zero and it holds the value it was merged away with. When - # sharing holds A this iteration - # (the post-merge settle window) the step is skipped entirely, along with - # the lrate ramp and the Newton-fallback bookkeeping Fortran nests inside - # the same guarded block (amica15.f90:1803), so a discarded Newton - # direction cannot ratchet the learning rate. + # exactly zero and it holds the value it was merged away with. On an + # iteration the reference holds A (pamica.schedule.share_freeze), + # everything inside the branch is skipped together: the + # Newton-fallback message, the lrate ramp, the reset of the working rho + # rate to its ceiling and the step itself. The step was built from the + # parameters the E-step saw, so taking it before the mixture updates + # changes nothing they read. if not self._a_frozen(): - if newton_active and no_newt: + if step.newton_active and step.no_newt: # Fortran prints this whenever a model is not positive definite # (amica17.f90:1911-1913); surface it rather than falling back # silently. @@ -2533,7 +2608,10 @@ def _update_parameters(self, updates: Dict): self.iter, ) - if newton_active and not no_newt: + # The ramp starts from this iteration's lrate, which a likelihood + # decrease has already halved (_optimize runs the response before + # the update, as the reference does, issue #339). + if step.newton_active and not step.no_newt: self.lrate = min( self.newtrate, self.lrate + min(1.0 / self.newt_ramp, self.lrate) ) @@ -2541,8 +2619,61 @@ def _update_parameters(self, updates: Dict): self.lrate = min( self.lrate0, self.lrate + min(1.0 / self.newt_ramp, self.lrate) ) + # rho moves at the ceiling (``rholrate = rholrate0``, :1806/:1813), + # so a decrease's scaling of the working rate reaches it only on a + # held iteration. + self.rholrate = self.rholrate_cap - self.A = self.A - self.lrate * dAk + self.A = self.A - self.lrate * step.dAk + + # Exact-EM mixture location/scale (Fortran :1978/:1993). These are + # fixed-point updates -- mu += dmu_n/dmu_d, beta *= sqrt(dbeta_n/dbeta_d) + # -- NOT first-order gradient steps, so they carry no lrate. + dmu = updates["dmu_n"][:, cols] / updates["dmu_d"][:, cols] + self.mu[:, cols] = self.mu[:, cols] + dmu + self.beta[:, cols] = self.beta[:, cols] * np.sqrt( + updates["dbeta_n"][:, cols] / updates["dbeta_d"][:, cols] + ) + self.beta = np.clip(self.beta, self.invsigmin, self.invsigmax) + # Fortran keeps a live "NaN in sbeta!" canary here (amica17.f90:1996-2000); + # the exact-EM mu/beta divisions are unguarded (matching Fortran), so + # surface a non-finite value immediately instead of letting it propagate + # to a later, unattributable nan-LL stop. + if not np.all(np.isfinite(self.mu)) or not np.all(np.isfinite(self.beta)): + self.logger.warning( + "Non-finite mu/beta at iter %d (mixture component mass likely " + "collapsed).", + self.iter, + ) + + # GG shape update with the 1/psi(1+1/rho) digamma factor (Fortran + # :2013-2014); the divisor is the per-component responsibility mass + # dalpha_n (floored so a near-empty component cannot poison rho). A NaN + # is reset to rho0 -- but logged first, so the reset does not silently + # erase the failure origin. + # Restricted to the live columns for the same reason as mu/beta/alpha + # (and as AMICATorchNG, which masks rho with ``used`` too): a merged-away + # column has no responsibility mass, so its rho would ratchet up on the + # floored denominator every iteration instead of staying at the value it + # was merged away with. + if not np.all(self.rho == 1.0) and not np.all(self.rho == 2.0): + rho_cols = self.rho[:, cols] + drho = updates["drho_n"][:, cols] / np.maximum( + updates["dalpha_n"][:, cols], 1e-8 + ) + psi = digamma(1.0 + 1.0 / rho_cols) + new_rho = rho_cols + self.rholrate * (1.0 - (rho_cols / psi) * drho) + nan_mask = np.isnan(new_rho) + if nan_mask.any(): + self.logger.warning( + "NaN in rho update at iter %d for %d component(s); resetting " + "to rho0=%g.", + self.iter, + int(nan_mask.sum()), + self.rho0, + ) + new_rho = np.where(nan_mask, self.rho0, new_rho) + self.rho[:, cols] = np.clip(new_rho, self.minrho, self.maxrho) # (c was updated above, before the mixture/A updates, from dc_numer/dgm.) @@ -2557,15 +2688,6 @@ def _update_parameters(self, updates: Dict): # Update unmixing matrices self._update_unmixing_matrices() - # Store likelihood - self.ll.append(updates["ll"]) - - # The weight-gradient norm is computed above, before the A step, matching - # Fortran's ordering. _check_convergence uses it as the gradient floor in - # the decrease-stop condition regardless of use_grad_norm; the flag only - # gates the separate final gradient-norm stop. - self.nd.append(nd_value) - def _component_sensor_maps(self) -> np.ndarray: """Every component's mixing vector in input-channel (sensor) space. @@ -2638,25 +2760,18 @@ def _rescale_components(self) -> None: self.beta = self.beta / scale def _a_frozen(self) -> bool: - """Whether the A-update (and its lrate ramp) is held this iteration. - - A is frozen for the first 6 iterations of every ``share_int``-length - window once the Fortran-style iteration reaches ``share_start`` -- the - merge iteration and the 5 after it -- so the density parameters can - settle onto a freshly merged component before the mixing matrix moves - again (Fortran A-freeze, amica15.f90:1803). Identical mechanism, anchor - and duration as ``AMICATorchNG._a_frozen``; the window fires each cycle - whether or not that cycle's merge pass actually merged a pair, matching - both the reference and the PyTorch backend. - - Gated behind ``share_comps`` and ``num_models >= 2`` (a model cannot - share a component with itself), so with sharing off -- the default -- - this is always False and the validated trajectory is untouched. The - constructor rejects ``share_int <= 6``, so the window can never consume a - whole cycle and freeze A permanently. + """Whether the reference holds the A update (with its lrate ramp and + rho-rate reset) this iteration: once ``iter >= share_start``, every + iteration with ``mod(iter, share_int) <= 5``, counted from 1 + (amica15.f90:1803, :func:`pamica.schedule.share_freeze`), exactly as + ``AMICATorchNG._a_frozen`` decides it. + + The reference applies this whether or not ``share_comps`` is on and for + any number of models (issue #345), so this does too: with the defaults + ``share_start = share_int = 100``, every fit of 100 or more iterations + holds A on iterations 100-105, 200-205, and so on. The constructor + rejects ``share_int < 7``, since a shorter cycle would hold A for good. """ - if not self.share_comps or self.num_models < 2: - return False return schedule.share_freeze(self.iter, self.share_start, self.share_int) def _optimize(self): @@ -2707,36 +2822,80 @@ def _optimize(self): # an identical setting fires on the same iterations here, in # AMICATorchNG, in AMICAMLXNG, and in the binary (issue #335). - # Get updates and likelihood + # One iteration follows the reference's main loop + # (amica15.f90:949-1142, issue #339): the E-step, which yields + # LL(iter) and the step with its norm; the restart-on-NaN window; + # the likelihood-decrease response and the stopping checks; an + # exit BEFORE any parameter moves if a check fired; otherwise the + # update with the rates the response just set, then the share + # merge, the checkpoints and rejection. updates = self._get_updates_and_likelihood() + ll = float(updates["ll"]) + + # A non-finite likelihood is never recorded in self.ll, as it is + # never recorded in the PyTorch and MLX ll_history: the history + # stays the finite trajectory of the parameters the fit + # actually visited, and every backend's has the same length for + # the same event. A singular W makes logdet -> -inf (not NaN), so + # the guard is isfinite, not isnan. + if not np.isfinite(ll): + # Restart-on-NaN (Fortran amica15.f90:1022-1050): an early + # non-finite LL usually means an unlucky init, so redraw A + # and start over, up to maxrestarts times, within the first + # restartiter iterations (Fortran's absolute + # `iter <= restartiter` window over its 1-based counter; the + # iteration counter is not reset on restart here). Checked + # before the update, which the reference skips on a restart + # too (``startover``, :1115). + if ( + schedule.within_restart_window(iter, self.restartiter) + and self.numrestarts < self.maxrestarts + ): + self.numrestarts += 1 + self.logger.warning( + "Non-finite LL at iter %d; reinitializing and " + "starting over (restart %d of %d).", + iter + 1, + self.numrestarts, + self.maxrestarts, + ) + self._reinitialize_for_restart() + # The reference's checks still run on its restart + # iteration, and its min_dll comparison with the NaN + # likelihood is false (amica15.f90:1078-1090), so it + # zeroes numincs; its decrease comparison is false too, + # so numdecs is left as it was. Match that state, so + # small gains before the restart cannot count toward a + # min_dll stop after it (issue #339 review). + numincs = 0 + continue + # Past the window, or out of restarts: stop, as the + # reference does (amica15.f90:1052-1055). + self.logger.warning( + "Non-finite log-likelihood (%s) at iteration %d; stopping.", + ll, + iter, + ) + convergence_reason = self._NONFINITE_LL_REASON + break - # Update parameters - self._update_parameters(updates) - - # Restart-on-NaN (Fortran amica15.f90:1022-1050): an early - # non-finite LL usually means an unlucky init, so redraw A and - # start over, up to maxrestarts times, within the first - # restartiter iterations (Fortran's absolute `iter <= restartiter` - # window over its 1-based counter; the iteration counter is not - # reset on restart here). A - # later NaN falls through to _check_convergence, which stops - # (Fortran exits too). - if ( - len(self.ll) > 0 - and not np.isfinite(self.ll[-1]) - and schedule.within_restart_window(iter, self.restartiter) - and self.numrestarts < self.maxrestarts - ): - self.numrestarts += 1 + step = self._update_direction(updates) + self.ll.append(ll) + self.nd.append(step.nd) + + # A non-finite step or norm would pass both gradient-norm checks + # (NaN <= min_nd is False) and then be applied, so stop on it + # here, before any check reads it and before the update, with + # the parameters whose (finite) likelihood was just recorded. + if not (np.isfinite(step.nd) and np.all(np.isfinite(step.dAk))): self.logger.warning( - "Non-finite LL at iter %d; reinitializing and starting " - "over (restart %d of %d).", - iter + 1, - self.numrestarts, - self.maxrestarts, + "Non-finite update direction (ndtmpsum %s) at iteration " + "%d; stopping before the update.", + step.nd, + iter, ) - self._reinitialize_for_restart() - continue + convergence_reason = self._NONFINITE_DIRECTION_REASON + break # Calculate metrics for logging/progress elapsed_time = time.time() - start_time @@ -2770,32 +2929,66 @@ def _optimize(self): self.logger.info(detailed_log) # Check convergence (threads numdecs/numincs back so they - # accumulate across iterations, and ratchets the lrate ceiling). + # accumulate across iterations, and applies the likelihood- + # decrease response, including the ceiling ratchets). converged, reason, numdecs, numincs = self._check_convergence( numdecs, numincs ) - if converged: - convergence_reason = reason - break # Reset the decrease counter when Newton turns on (Fortran # amica15.f90:1099-1102). if schedule.newton_switches_on(self.do_newton, iter, self.newt_start): numdecs = 0 - # Reject outliers if requested (Fortran amica15.f90:1136; the - # schedule, including its max(1, ...) clamp, is shared with - # AMICATorchNG and AMICAMLXNG in pamica.schedule). - if schedule.rejection_due( - self.do_reject, - iter, - self.rejstart, - self.rejint, - self.numrej, - self.maxrej, - ): - self._reject_outliers() - self.numrej += 1 + # Stop before this iteration's update, as the reference does + # (amica15.f90:1111, ahead of update_params at :1122): the + # returned parameters are the ones whose likelihood is + # self.ll[-1]. + if converged: + convergence_reason = reason + break + + # Update parameters, with the rates the response above just set. + self._update_parameters(updates, step) + + # Surface a corrupted update (a collapsed mixture component, a + # singular inverse) on the iteration it happens, with the same + # check and message as AMICAMLXNG and AMICATorchNG: otherwise a + # corruption on the last iteration would end as a max_iter fit + # holding non-finite parameters. One difference, for this + # backend's restart-on-NaN, which redraws A (and so W) and keeps + # the mixture parameters, as the reference's does + # (amica15.f90:1026-1046): when only A/W went non-finite and the + # next iteration still falls inside the restart window with a + # restart left, the next E-step's non-finite likelihood restarts + # the fit, as the reference's would, so the loop goes on to it. + # A restart could not repair any other parameter, so those stop. + bad = self._nonfinite_params(self._POST_UPDATE_CHECKED) + if bad: + restartable = ( + set(bad) <= {"A", "W"} + and iter + 1 < self.max_iter + and schedule.within_restart_window(iter + 1, self.restartiter) + and self.numrestarts < self.maxrestarts + ) + if not restartable: + self.logger.warning( + "Non-finite %s at iter %d (a mixture component likely " + "collapsed); stopping.", + ", ".join(bad), + iter, + ) + convergence_reason = ( + f"{self._NONFINITE_PARAMS_REASON}: {', '.join(bad)}" + ) + break + self.logger.warning( + "Non-finite %s at iter %d (a mixture component likely " + "collapsed); the next iteration's restart-on-NaN will " + "reinitialize.", + ", ".join(bad), + iter, + ) # Share components if requested (Fortran identify_shared_comps # schedule, amica15.f90:1856): once per share_int cycle from @@ -2807,13 +3000,12 @@ def _optimize(self): # by the merged comp_list. The schedule is pamica.schedule's, the # one AMICATorchNG and AMICAMLXNG use, so an identical # (share_start, share_int) fires on the same iterations in every - # backend and lines up with _a_frozen. + # backend. # - # This runs AFTER self.ll.append(updates["ll"]) inside - # _update_parameters above, so a merge on the final iteration - # lands in self.A/comp_list but not in the ll value already - # stored -- see the final_ll_ note on self.ll's init (issue - # #269). + # This runs AFTER self.ll recorded this iteration's likelihood + # (above), so a merge on the final iteration lands + # in self.A/comp_list but not in the ll value already stored -- + # see the final_ll_ note on self.ll's init (issue #269). if self.share_comps and schedule.periodic_due( iter, self.share_start, self.share_int ): @@ -2843,6 +3035,23 @@ def _optimize(self): and schedule.every(iter, self.histstep) ): self._write_checkpoint("history") + + # Reject outliers if requested, after the update and the + # checkpoints, as the reference does (amica15.f90:1136; the + # schedule, including its max(1, ...) clamp, is shared with + # AMICATorchNG and AMICAMLXNG in pamica.schedule). The statistic + # is this iteration's pre-update per-sample likelihood + # (_last_ll_samples, captured in the E-step). + if schedule.rejection_due( + self.do_reject, + iter, + self.rejstart, + self.rejint, + self.numrej, + self.maxrej, + ): + self._reject_outliers() + self.numrej += 1 finally: # Close the progress bar if using tqdm if use_tqdm_progress: @@ -2880,8 +3089,11 @@ def _check_convergence( rho rate once ``iter > newt_start``; ``newtrate`` under Newton) and continues, which is what keeps a long run from oscillating and drifting past its converged solution (issue #41). The rho ceiling is - ``self.rholrate`` here (reset to ``rholrate0`` each fit), ratcheted only - at ``maxdecs`` -- never per-decrease (issue #193). The updated + ``self.rholrate_cap`` here (reset to ``rholrate0`` each fit), ratcheted + only at ``maxdecs`` (issue #193); each decrease scales the working + ``self.rholrate``, which every A update resets to the ceiling. It runs + before the iteration's update, as the reference's checks do, so the + update already uses the new rates (issue #339). The updated ``numdecs``/``numincs`` counters are returned so they accumulate across iterations (they previously did not). @@ -2906,14 +3118,10 @@ def _check_convergence( # Check for non-finite LL: a singular W makes logdet -> -inf (not NaN), # so guard on isfinite, not isnan alone, or a degenerate model would run - # to max_iter undetected. + # to max_iter undetected. _optimize stops on a non-finite likelihood + # before recording it, so this guards a direct caller's history. if not np.isfinite(self.ll[-1]): - return ( - True, - "Non-finite likelihood (NaN/-inf) encountered", - numdecs, - numincs, - ) + return (True, self._NONFINITE_LL_REASON, numdecs, numincs) # The remaining checks compare consecutive iterations; skip until there # are two LL values -- the first iteration, or the first iteration after @@ -2942,17 +3150,19 @@ def _check_convergence( numincs, ) self.lrate *= self.lratefact + # The working rho rate, as the reference scales it (:1063); every A + # update resets it to the ceiling before rho moves, so this reaches + # rho only on an iteration on which A is held. + self.rholrate *= self.rholratefact numdecs += 1 if numdecs >= self.max_decs: self.lrate0 *= self.lratefact if schedule.past_newton_start(self.iter, self.newt_start): - # rho rate is a ceiling reset to rholrate0 each iteration - # (Fortran amica15.f90:1806/1813); it ratchets ONLY here at maxdecs - # (amica15.f90:1068), never per LL-decrease. The old per-decrease - # self.rholrate *= rholratefact was a monotone decay with no - # reset that collapsed the rho rate and froze rho at a stale - # shape (issue #193). - self.rholrate *= self.rholratefact + # The rho-rate CEILING ratchets only here, at maxdecs + # (amica15.f90:1068), never per LL-decrease: a per-decrease + # decay of the rate rho actually uses, with no reset, collapsed + # the rho rate and froze rho at a stale shape (issue #193). + self.rholrate_cap *= self.rholratefact if self.do_newton and schedule.past_newton_start( self.iter, self.newt_start ): diff --git a/pamica/schedule.py b/pamica/schedule.py index a71ef80a..f43303a7 100644 --- a/pamica/schedule.py +++ b/pamica/schedule.py @@ -22,9 +22,12 @@ import numbers -# The reference holds the A-update for the merge iteration and the 5 after it -# (``mod(iter, share_iter) > 5``, amica15.f90:1803). -_SHARE_FREEZE_ITERS = 6 +# The reference steps A only when ``mod(iter, share_iter) > 5`` once +# ``iter >= share_start`` (amica15.f90:1803), so it holds A on the 6 iterations +# whose remainder is 0 to 5 in every cycle. +_SHARE_FREEZE_LAST_REMAINDER = 5 +# The smallest ``share_iter`` that leaves A an update in every cycle. +SHARE_ITER_MIN = _SHARE_FREEZE_LAST_REMAINDER + 2 def reference_iteration(index: int) -> int: @@ -32,22 +35,61 @@ def reference_iteration(index: int) -> int: return index + 1 -def validate_iteration_setting(name: str, value: object, minimum: int) -> None: +def validate_iteration_setting( + name: str, value: object, minimum: int, why: str = "" +) -> None: """Raise ``ValueError`` unless ``value`` is an integer of at least ``minimum``. For the settings that name an iteration or an interval (``newt_start``, ``rejstart``, ``restartiter``, ``maxrestarts``, ``histstep``, - ``scalestep``): an integer in + ``scalestep``, ``share_iter``): an integer in the ``numbers.Integral`` sense, so numpy integers qualify, while ``bool`` (which subclasses ``int``) and floats are rejected rather than coerced, as - :func:`pamica.rank.validate_pca_reduction` does for ``pcakeep``. + :func:`pamica.rank.validate_pca_reduction` does for ``pcakeep``. ``why``, + when given, is appended to the message to say what a smaller value would do. """ if ( isinstance(value, bool) or not isinstance(value, numbers.Integral) or value < minimum ): - raise ValueError(f"{name} must be an integer >= {minimum}, got {value!r}") + reason = f": {why}" if why else "" + raise ValueError( + f"{name} must be an integer >= {minimum}, got {value!r}{reason}" + ) + + +def validate_share_iter(value: object, name: str = "share_iter") -> None: + """Raise ``ValueError`` unless the A-freeze cycle leaves A room to move. + + Every backend validates it whether or not ``share_comps`` is on, because + the freeze it sets (:func:`share_freeze`) applies either way, from + ``share_start`` on, and whether a fit reaches ``share_start`` is not known + at construction. ``name`` is how the message names the keyword: the NumPy + backend takes both ``share_int`` and ``share_iter`` and names both. + """ + validate_iteration_setting( + name, + value, + SHARE_ITER_MIN, + why=( + f"below {SHARE_ITER_MIN}, every iteration's remainder is " + f"{_SHARE_FREEZE_LAST_REMAINDER} or less, so the reference's A-freeze " + "(amica15.f90:1803), which applies whether or not share_comps is " + "on, would hold the mixing matrix permanently from share_start on" + ), + ) + + +def validate_share_start(value: object) -> None: + """Raise ``ValueError`` unless ``share_start`` is an integer of at least 1. + + Validated whether or not ``share_comps`` is on, for the same reason as + :func:`validate_share_iter`: the A-freeze reads it on every fit, and + ``share_start=0`` would hold A from the very first iteration. Checked + with the plain message of the other iteration settings. + """ + validate_iteration_setting("share_start", value, 1) def _check_interval(where: str, name: str, interval: int) -> None: @@ -120,22 +162,26 @@ def periodic_due(index: int, start: int, interval: int) -> bool: def share_freeze(index: int, share_start: int, share_iter: int) -> bool: - """Whether ``share_comps`` holds the A-update on this iteration: the merge - iteration and the 5 after it, in every ``share_iter`` cycle from - ``share_start``. - - Anchored on ``share_start`` so the window stays aligned with the merge - schedule (:func:`periodic_due`). The reference's literal - ``mod(iter, share_iter) > 5`` (amica15.f90:1803) misaligns unless - ``share_start`` is a multiple of ``share_iter``; that path is dead in the - reference, so this is a documented pamica decision - (``docs/guides/amica-differences.md``), not a parity constraint. Callers - gate it on ``share_comps`` with more than one model. + """Whether the reference holds the mixing-matrix update on this iteration: + once ``iter >= share_start``, every iteration with + ``mod(iter, share_iter) <= 5`` (amica15.f90:1803, ``update_A .and. + ((iter < share_start) .or. (mod(iter,share_iter) > 5))``). + + The reference never checks ``share_comps`` here, so the freeze applies to + every fit, one model or several, sharing on or off (issue #345): with the + defaults ``share_start = share_iter = 100``, iterations 100-105, 200-205, + and so on. Inside the same branch the reference also ramps ``lrate`` and + resets the working ``rholrate`` to its ceiling, so a frozen iteration does + neither. The remainder is taken of ``iter`` itself, not of + ``iter - share_start``, so the window starts on the share-merge iteration + (:func:`periodic_due`) only when ``share_start`` is a multiple of + ``share_iter``, as with the defaults; that is the reference's arithmetic, + applied as is. ``share_iter`` below :data:`SHARE_ITER_MIN` would never + update A again (:func:`validate_share_iter`). """ + _check_interval("share_freeze", "share_iter", share_iter) itf = reference_iteration(index) - if itf < share_start: - return False - return (itf - share_start) % share_iter < _SHARE_FREEZE_ITERS + return itf >= share_start and itf % share_iter <= _SHARE_FREEZE_LAST_REMAINDER def every(index: int, step: int) -> bool: diff --git a/pamica/tests/mlx_tests/test_mlx_backend.py b/pamica/tests/mlx_tests/test_mlx_backend.py index 81d99eed..7e4869b5 100644 --- a/pamica/tests/mlx_tests/test_mlx_backend.py +++ b/pamica/tests/mlx_tests/test_mlx_backend.py @@ -391,9 +391,11 @@ def test_rholrate_ratchets_at_maxdecs_not_per_decrease(): """Issue #195 (mirrors #193/#194 for torch/numpy): the MLX rho learning rate is a maxdecs-ratcheted CEILING, not a per-LL-decrease monotone decay. - Fortran resets ``rholrate = rholrate0`` each iteration before the rho update - (amica15.f90:1806/1813) and only tightens the ceiling at ``maxdecs`` - (amica15.f90:1068, gated on ``iter > newt_start``). MLX previously decayed + Fortran scales the working ``rholrate`` on each decrease (amica15.f90:1063), + resets it to the ceiling ``rholrate0`` in every A update before the rho + update (amica15.f90:1806/1813), and only tightens the ceiling at ``maxdecs`` + (amica15.f90:1068, gated on ``iter > newt_start``). pamica names that + ceiling ``rholrate_cap`` (issue #339). MLX previously decayed the one ``rholrate`` on EVERY LL decrease with no reset, collapsing it toward ~1e-5 within a few hundred iterations and freezing rho at a stale shape. @@ -418,13 +420,13 @@ def test_rholrate_ratchets_at_maxdecs_not_per_decrease(): # The ceiling ratcheted a whole number of times at the maxdecs cadence # (rholrate0 * rholratefact**k), far fewer steps than one-per-decrease. - assert m.rholrate < m.rholrate0 - k = round(np.log(m.rholrate / m.rholrate0) / np.log(m.rholratefact)) - assert m.rholrate == pytest.approx(m.rholrate0 * m.rholratefact**k) + assert m.rholrate_cap < m.rholrate0 + k = round(np.log(m.rholrate_cap / m.rholrate0) / np.log(m.rholratefact)) + assert m.rholrate_cap == pytest.approx(m.rholrate0 * m.rholratefact**k) assert k <= n_dec // m.maxdecs + 1 # Guard against a regression to the old per-decrease decay (orders below). buggy = m.rholrate0 * (m.rholratefact**n_dec) - assert m.rholrate > buggy * 10 + assert m.rholrate_cap > buggy * 10 def test_rholrate_ceiling_ratchet_gated_on_newt_start(): @@ -446,20 +448,23 @@ def test_rholrate_ceiling_ratchet_gated_on_newt_start(): n_dec = sum(1 for i in range(1, len(ll)) if ll[i] < ll[i - 1]) assert n_dec >= m.maxdecs, "config did not exercise the decrease path" # Gated off for the whole run: the rho ceiling never moved. - assert m.rholrate == m.rholrate0 + assert m.rholrate_cap == m.rholrate0 def test_rholrate_ceiling_resets_at_fit_start(): """Issue #195: the rho-rate ceiling is reset to rholrate0 at fit start (``_initialize_parameters``, the same call ``fit`` makes), so a previously - ratcheted ``rholrate`` does not carry across a re-fit/restart -- parity with - the numpy backend's ``test_reinitialize_for_restart_resets_rho_ceiling``.""" + ratcheted ceiling (``rholrate_cap``), or a scaled working ``rholrate``, + does not carry across a re-fit/restart -- parity with the numpy backend's + ``test_reinitialize_for_restart_resets_rho_ceiling``.""" from pamica.mlx_impl import AMICAMLXNG m = AMICAMLXNG(n_channels=NW, n_mix=NMIX, seed=SEED) # Simulate a prior fit that ratcheted both ceilings down at maxdecs. - m.rholrate = m.rholrate0 * 0.25 + m.rholrate_cap = m.rholrate0 * 0.25 + m.rholrate = m.rholrate0 * 0.025 m.lrate_cap = m.lrate0 * 0.25 m._initialize_parameters() # fit-start reset + assert m.rholrate_cap == m.rholrate0 assert m.rholrate == m.rholrate0 assert m.lrate_cap == m.lrate0 diff --git a/pamica/tests/mlx_tests/test_mlx_export.py b/pamica/tests/mlx_tests/test_mlx_export.py index b5304b99..3ca27065 100644 --- a/pamica/tests/mlx_tests/test_mlx_export.py +++ b/pamica/tests/mlx_tests/test_mlx_export.py @@ -153,8 +153,8 @@ def test_written_ll_ends_at_the_keep_best_restored_iterate(real_data): lrate=0.5, newtrate=3.0, # overshoots since issue #333 (test_mlx_keepbest.py) use_min_dll=True, - min_dll=1e-4, - maxincs=2, + min_dll=1e-8, # stops on the first decrease (test_mlx_keepbest.py) + maxincs=0, use_grad_norm=False, ) m = AMICAMLXNG(n_channels=NW, **kwargs) diff --git a/pamica/tests/mlx_tests/test_mlx_keepbest.py b/pamica/tests/mlx_tests/test_mlx_keepbest.py index 76dcbd45..0aa70e4e 100644 --- a/pamica/tests/mlx_tests/test_mlx_keepbest.py +++ b/pamica/tests/mlx_tests/test_mlx_keepbest.py @@ -15,32 +15,39 @@ * ``_FORCED_RESTORE_KWARGS`` (real EEG, first 4096 samples, ``max_iter=150``): ``n_models=2, n_mix=3, seed=0, block_size=1024, do_newton=True, - newt_start=2, lrate=0.5, newtrate=3.0, use_min_dll=True, min_dll=1e-4, - maxincs=2, use_grad_norm=False`` -- the same aggressive-Newton config + newt_start=2, lrate=0.5, newtrate=3.0, use_min_dll=True, min_dll=1e-8, + maxincs=0, use_grad_norm=False`` -- the same aggressive-Newton config ``test_ng_convergence.py::test_keep_best_restores_genuine_overshoot_under_min_dll_stop`` uses on PyTorch (``newt_start`` counts from 1 since issue #335, so ``2`` is - the run measured as ``newt_start=1`` before it), and it reproduces a genuine overshoot here too: the fit - stops via ``min_dll`` at iteration 64 (65 recorded LLs), peaks at - ``ll_history[63]``, and ends ``best_ll - ll_history[-1] ~= 4.24e-4`` below - that peak (measured on an Apple M4 Pro; the exact float32 value is - machine-dependent, but the qualitative overshoot -- peak strictly above the - final entry -- reproduces across seeds, see - ``test_keep_best_restores_a_genuine_overshoot`` below for the live - measurement this module actually asserts against). ``newtrate=3.0`` and the - longer budget date from issue #333: once ``doscaling`` rescaled components - instead of stored columns, the earlier ``newtrate=0.5``/60-iteration recipe - ran monotone to ``max_iter`` on every seed tried (0-11). + the run measured as ``newt_start=1`` before it), and it reproduces a genuine + overshoot here too: ``maxincs=0`` with a ``min_dll`` far below every gain + stops the fit on its first likelihood decrease, at iteration 14 (15 + recorded LLs), ``best_ll - ll_history[-1] ~= 1.6e-3`` below the peak at + ``ll_history[13]`` (measured on an Apple M4 Pro; the exact float32 value is + machine-dependent, but the overshoot is built into the stop: across 12 + relative data perturbations of 1e-6 it stopped at 14, 2.7e-4 to 3.6e-3 + below the peak, every time; see ``test_keep_best_restores_a_genuine_overshoot`` + below for the live measurement this module actually asserts against). + ``newtrate=3.0`` dates from issue #333: once ``doscaling`` rescaled + components instead of stored columns, the earlier ``newtrate=0.5``/ + 60-iteration recipe ran monotone to ``max_iter`` on every seed tried (0-11). + The first-decrease stop dates from issue #339: the ``min_dll=1e-4``/ + ``maxincs=2`` stop before it ended wherever three small gains in a row fell, + below the peak or at it depending on round-off (at it in 1 of those 12). * ``_FORCED_RESTORE_PDFTYPE1_KWARGS``: the ``pdftype=1``/``n_mix=1`` analogue - (seed 2, ``kurt_start=6``) chosen so the adaptive switcher's first kurtosis - re-evaluation (run at the end of iteration 5, after that iteration's E-step) - comes strictly AFTER the peak iterate (measured at iteration 5), while the - run lasts long enough for all five passes (it stops via ``min_dll`` at - iteration 12) -- the scenario + (seed 3, ``kurt_start=7``) chosen so the adaptive switcher's first kurtosis + re-evaluation (run at the end of iteration 7, after that iteration's E-step) + comes strictly AFTER the E-step of the peak iterate (0-based ll index 6), + while the run lasts long enough for all five passes (it stops via ``min_dll`` + at ll index 19, 0.26 below the peak): the scenario ``test_pdftype1_forced_restore_rolls_back_n_kurt_done_with_pdtype`` needs to prove the restore rolls ``n_kurt_done`` back in step with ``pdtype``, not - just the floating-point arrays. (``kurt_start=8`` served until issue #333; - the rescale fix moved that run's ``min_dll`` stop to iteration 8, after - only two passes.) + just the floating-point arrays. (``kurt_start=8`` served until issue #333, + seed 2 with ``kurt_start=6`` until issue #339, whose earlier decrease + response no longer lets that run overshoot. Seeds 0-5 and ``kurt_start`` + 3-11 were searched; seed 1 with ``kurt_start=8`` also qualifies, and most + other overshooting runs end in a ``nan_params`` stop, which skips the + restore.) The truncated-refit bit-identity check (both here and in the pdftype=1 test) uses ``max_iter=argmax`` where ``argmax = int(np.argmax(ll_history))``: a @@ -76,8 +83,8 @@ ``n_restarts=2`` search must record ``restart_lls_[0]`` as the RESTORED best iterate's LL, not the raw last iterate -- i.e. the same value a standalone seed-0 fit's ``final_ll_`` reports, confirmed bit-identical in - the same process. The second restart (seed 1) is cheap: it converges in - under 60 iterations on the same recipe with no forcing needed. + the same process. The second restart (seed 1) is cheap: it stops on its + own first decrease, at iteration 16, on the same recipe. """ import logging @@ -135,8 +142,8 @@ def _model(**kwargs: Any) -> AMICAMLXNG: lrate=0.5, newtrate=3.0, use_min_dll=True, - min_dll=1e-4, - maxincs=2, + min_dll=1e-8, + maxincs=0, use_grad_norm=False, ) _FORCED_RESTORE_MAX_ITER = 150 @@ -147,7 +154,7 @@ def _model(**kwargs: Any) -> AMICAMLXNG: n_models=2, n_mix=1, pdftype=1, - seed=2, + seed=3, block_size=BLOCK, do_newton=True, newt_start=2, @@ -156,7 +163,7 @@ def _model(**kwargs: Any) -> AMICAMLXNG: min_dll=1e-4, maxincs=2, use_grad_norm=False, - kurt_start=6, + kurt_start=7, num_kurt=5, kurt_int=1, ) @@ -366,8 +373,8 @@ def test_pdftype1_forced_restore_rolls_back_n_kurt_done_with_pdtype(real_data): ``AMICATorchNG``). The recipe (module docstring) runs the adaptive switcher's first - kurtosis pass at the end of iteration 5, strictly after the E-step that - measures the peak at iteration 5, so ``keep_best=False`` reaches + kurtosis pass at the end of iteration 7, strictly after the E-step that + measures the peak (ll index 6), so ``keep_best=False`` reaches ``n_kurt_done=5`` (all switch passes ran) with a mixed pdtype, while the restored ``keep_best=True`` model must be pinned at the pre-switch state: ``n_kurt_done=0`` and ``pdtype`` unchanged from its all-super-Gaussian diff --git a/pamica/tests/mlx_tests/test_mlx_llt_stash.py b/pamica/tests/mlx_tests/test_mlx_llt_stash.py index 862abc8c..0dda5bfe 100644 --- a/pamica/tests/mlx_tests/test_mlx_llt_stash.py +++ b/pamica/tests/mlx_tests/test_mlx_llt_stash.py @@ -152,9 +152,10 @@ def test_llt_stash_is_none_before_any_estep_ran(): # Reuses the aggressive-Newton recipe test_mlx_keepbest.py measured to # genuinely overshoot on this backend (module docstring there): n_models=2, # seed=0, block_size=1024, do_newton=True, newt_start=2, lrate=0.5, -# newtrate=3.0, use_min_dll=True, min_dll=1e-4, maxincs=2, -# use_grad_norm=False, max_iter=150 (newtrate and the budget since issue #333; -# newt_start 2, counted from 1 since issue #335, is the run measured as 1). +# newtrate=3.0, use_min_dll=True, min_dll=1e-8, maxincs=0, +# use_grad_norm=False, max_iter=150 (newtrate since issue #333; newt_start 2, +# counted from 1 since issue #335, is the run measured as 1; the +# first-decrease stop since issue #339). _FORCED_RESTORE_KWARGS: dict[str, Any] = dict( n_models=2, n_mix=NMIX, @@ -165,8 +166,8 @@ def test_llt_stash_is_none_before_any_estep_ran(): lrate=0.5, newtrate=3.0, use_min_dll=True, - min_dll=1e-4, - maxincs=2, + min_dll=1e-8, + maxincs=0, use_grad_norm=False, ) diff --git a/pamica/tests/mlx_tests/test_mlx_mir.py b/pamica/tests/mlx_tests/test_mlx_mir.py index 0e9be1b9..9721d3ff 100644 --- a/pamica/tests/mlx_tests/test_mlx_mir.py +++ b/pamica/tests/mlx_tests/test_mlx_mir.py @@ -361,23 +361,28 @@ def test_mir_history_survives_keep_best_restore(real_data): buggy restore would damage (mirrors test_ng_convergence.py's identically-named test and its mir_step=1 rationale). - Seed 1 rather than the recipe's seed 0 since issue #333: both overshoot, - but seed 0's restored iterate sits so close to its last one that their MIR - differ by only 4.9e-5 relative, under the 1e-4 float32 margin below; - seed 1's differ by 7.5e-4.""" + Since issue #339 the min_dll stop exits before its own update, so the + last waypoint is the one right after the restored iterate, and the + recipe stops on its first likelihood decrease (``maxincs=0``, + ``min_dll=1e-8``): iteration 14, 1.6e-3 below the peak at 13. The MIR of + that one discarded step moves by 9.4e-4 relative, well past the 1e-4 + float32 margin below, and by 8.2e-4 to 1.1e-3 across 8 relative data + perturbations of 1e-6. Seed 1 with ``min_dll=1e-4``/``maxincs=2``, the + choice from issue #333 until then, moved it by 2.5e-5 under the new + order.""" m = AMICAMLXNG( n_channels=NW, n_models=2, n_mix=NMIX, - seed=1, + seed=0, block_size=BLOCK, do_newton=True, newt_start=2, lrate=0.5, newtrate=3.0, # overshoots since issue #333 (test_mlx_keepbest.py) use_min_dll=True, - min_dll=1e-4, - maxincs=2, + min_dll=1e-8, + maxincs=0, use_grad_norm=False, keep_best=True, ) @@ -392,13 +397,20 @@ def test_mir_history_survives_keep_best_restore(real_data): assert m.mir_history_, "test setup: mir_step recorded nothing" final_it = len(m.ll_history) - 1 + # A convergence stop exits before its iteration's update, as the reference + # does (issue #339), so that iteration records no waypoint; a max_iter fit + # updates, and records one, on its last iteration. + last_update_it = final_it if m.stop_reason == "max_iter" else final_it - 1 best_it = m.ll_history.index(m.final_ll_) - assert best_it < final_it, ( - "test setup: the restore must discard at least one iteration" + # Waypoint i comes from the parameters after iteration i's update, so + # every waypoint from best_it on was computed from parameters the restore + # discards (see test_ng_convergence.py's identically-named test). + assert best_it <= last_update_it, ( + "test setup: the restore must discard at least one waypoint" ) last_it, last_mir, _ = m.mir_history_[-1] - assert last_it == final_it, "the post-peak waypoints were dropped" - assert len(m.mir_history_) == final_it + 1, ( + assert last_it == last_update_it, "the post-peak waypoints were dropped" + assert len(m.mir_history_) == last_update_it + 1, ( "mir_history_ is not the full per-iteration trajectory" ) diff --git a/pamica/tests/mlx_tests/test_mlx_reject.py b/pamica/tests/mlx_tests/test_mlx_reject.py index 91fe2b03..a4dcd99b 100644 --- a/pamica/tests/mlx_tests/test_mlx_reject.py +++ b/pamica/tests/mlx_tests/test_mlx_reject.py @@ -248,8 +248,8 @@ def test_keep_best_inactive_reason_prefers_do_reject_when_both_are_on( lrate=0.5, newtrate=3.0, use_min_dll=True, - min_dll=1e-4, - maxincs=2, + min_dll=1e-8, + maxincs=0, use_grad_norm=False, ) _REJECT_KWARGS: dict[str, Any] = dict( @@ -263,12 +263,18 @@ def test_keep_best_restore_never_fires_under_do_reject(real_data): on: fit() returns the last iterate there. Non-vacuous on both counts, measured on an Apple M4 Pro. Without - do_reject the same fit restores (peak at iteration 63, stop at 64, 4.2e-4 + do_reject the same fit restores (peak at iteration 13, stop at 14, 1.6e-3 below the peak). With do_reject its own trajectory also ends below an - earlier peak (iteration 36, stop at 37, 2.0e-3 below), so a restore would - fire if the safeguard were active, and it does not. The previous version - (``newtrate`` 0.5, 30 iterations) ran monotone once issue #333 changed - ``doscaling`` to rescale components, so it could not have failed. + earlier peak (after the rejection pass at iteration 6: peak at 13, stop at + 14, 9.4e-3 below), so a restore would fire if the safeguard were active, + and it does not. Both stop on their first likelihood decrease + (``maxincs=0``, ``min_dll=1e-8``), so the overshoot does not depend on + round-off: across 12 relative data perturbations of 1e-6 it held in all, + 9.4e-3 to 1.1e-2 below the peak with do_reject. The ``min_dll=1e-4``/ + ``maxincs=2`` stop used until issue #339 ended at the peak in 2 of those + 12, and on the macOS CI runner. The version before that (``newtrate`` 0.5, + 30 iterations) ran monotone once issue #333 changed ``doscaling`` to + rescale components, so it could not have failed. """ plain = _model(keep_best=True, **_OVERSHOOT_KWARGS) plain.fit(real_data, max_iter=_OVERSHOOT_MAX_ITER, verbose=False) diff --git a/pamica/tests/mlx_tests/test_mlx_sharing.py b/pamica/tests/mlx_tests/test_mlx_sharing.py index 01852d57..1c2fbcf4 100644 --- a/pamica/tests/mlx_tests/test_mlx_sharing.py +++ b/pamica/tests/mlx_tests/test_mlx_sharing.py @@ -288,9 +288,10 @@ def test_convergence_stop_can_fire_while_A_is_frozen(): assert model.stop_reason == "grad_norm" # It stops on the first iteration that can stop at all (two LL values are # required, Fortran's `if (iter > 1)`), which is itf=2 -- inside the first - # freeze window [share_start, share_start + 5] = [1, 6]. + # freeze window, iterations 1-5 (from share_start, remainders 1-5 mod 8, + # amica15.f90:1803). itf = model.iteration + 1 - assert itf == 2 and model.share_start <= itf <= model.share_start + 5 + assert itf == 2 and itf >= model.share_start and itf % model.share_iter <= 5 assert model._a_frozen() is True, "the stop did not land inside the window" assert model._ndtmpsum is not None and model._ndtmpsum <= model.min_nd assert model.final_ll_ is not None and np.isfinite(model.final_ll_) @@ -446,14 +447,17 @@ def test_merged_away_columns_keep_their_last_finite_value(): # --- (e) the post-merge A-freeze window --------------------------------------- -def test_a_frozen_window_matches_the_torch_schedule(): - """Identical window to AMICATorchNG: the merge iteration and the 5 after.""" +@pytest.mark.parametrize("share_comps, n_models", [(True, 2), (False, 2), (False, 1)]) +def test_a_frozen_window_matches_the_torch_schedule(share_comps, n_models): + """Identical window to AMICATorchNG, the reference's ``iter >= + share_start`` and ``mod(iter, share_iter) <= 5`` (amica15.f90:1803, + 1-indexed), with sharing on or off and for any model count (issue #345).""" from pamica.mlx_impl import AMICAMLXNG model = AMICAMLXNG( n_channels=8, - n_models=2, - share_comps=True, + n_models=n_models, + share_comps=share_comps, share_start=10, share_iter=20, ) @@ -462,21 +466,20 @@ def frozen(itf): model.iteration = itf - 1 # itf is the Fortran-style 1-indexed iteration return model._a_frozen() - assert not any(frozen(i) for i in range(1, 10)) # before share_start - assert all(frozen(i) for i in range(10, 16)) # merge + 5 (residue 0..5) - assert not any(frozen(i) for i in range(16, 30)) # thawed rest of cycle - assert all(frozen(i) for i in range(30, 36)) # next cycle boundary + assert not any(frozen(i) for i in range(1, 20)) # remainder above 5 or early + assert all(frozen(i) for i in range(20, 26)) # remainder 0..5 + assert not any(frozen(i) for i in range(26, 40)) # thawed rest of cycle + assert all(frozen(i) for i in range(40, 46)) # next cycle -def test_a_frozen_is_off_for_a_single_model(): - """A model cannot share with itself, so sharing never freezes A there.""" +def test_a_frozen_applies_to_a_single_model_without_sharing(): + """The reference never checks share_comps in the A-update guard, so a + one-model fit with sharing off is held too (issue #345).""" from pamica.mlx_impl import AMICAMLXNG - model = AMICAMLXNG( - n_channels=8, n_models=1, share_comps=True, share_start=2, share_iter=8 - ) + model = AMICAMLXNG(n_channels=8, n_models=1, share_start=2, share_iter=8) model.iteration = 3 - assert model._a_frozen() is False + assert model._a_frozen() is True def test_freeze_holds_A_while_the_mixture_keeps_moving(): @@ -484,10 +487,16 @@ def test_freeze_holds_A_while_the_mixture_keeps_moving(): parameters and the gradient norm keep updating -- Fortran computes ``dAk``/ ``ndtmpsum`` in the accumulation pass, which runs whether or not the A step is taken (issue #207). A moves again at the sixth iteration after the merge. + + ``share_start`` is a multiple of ``share_iter``, as with the reference's + defaults, so the window (remainders 0-5, amica15.f90:1803) starts on the + merge iteration: iterations 7-12, thawed on 13. (It was 4 of 20 under the + anchored window issue #345 replaced, which the reference holds on 4 and 5 + only.) """ - share_start, share_iter = 4, 20 + share_start, share_iter = 7, 7 model, x_t = _warm_model( - warmup=share_start - 1, # iterations 0..2, so the next itf is share_start + warmup=share_start - 1, # iterations 0..5, so the next itf is share_start share_comps=True, share_start=share_start, share_iter=share_iter, @@ -501,7 +510,7 @@ def test_freeze_holds_A_while_the_mixture_keeps_moving(): mu_start = np.array(model.mu) lrate_start = model.lrate - frozen_iters = range(share_start - 1, share_start + 5) # itf = 4..9 + frozen_iters = range(share_start - 1, share_start + 5) # itf = 7..12 for it in frozen_iters: model.iteration = it assert model._a_frozen() is True @@ -538,9 +547,10 @@ def test_freeze_holds_A_while_the_mixture_keeps_moving(): ) def test_share_constructor_validation(kwargs, match): """Rejected up front, for the same reasons and with the same messages as - ``AMICATorchNG``. ``share_iter <= 6`` is the one that bites: the post-merge - A-freeze is 6 iterations long, so a shorter cycle would hold A frozen on - every iteration of every cycle and the mixing matrix would stop moving.""" + ``AMICATorchNG``. ``share_iter <= 6`` is the one that bites: the reference + holds A on every iteration whose remainder mod ``share_iter`` is 0 to 5, so + a shorter cycle would hold A frozen on every iteration of every cycle and + the mixing matrix would stop moving.""" from pamica.mlx_impl import AMICAMLXNG with pytest.raises(ValueError, match=match): @@ -548,12 +558,19 @@ def test_share_constructor_validation(kwargs, match): def test_share_settings_are_not_validated_when_sharing_is_off(): - """The validation is gated on ``share_comps``, as in AMICATorchNG, so a - default-constructed model carries the (unused) defaults untouched.""" + """The merge threshold is validated only with ``share_comps`` on, as in + AMICATorchNG, so it is carried untouched otherwise. The schedule is the + exception: the reference's A-freeze reads ``share_start`` and + ``share_iter`` whether or not sharing is on (issue #345), so both are + validated always.""" from pamica.mlx_impl import AMICAMLXNG - model = AMICAMLXNG(n_channels=8, n_models=2, share_start=0, share_iter=1) - assert model.share_comps is False and model.share_iter == 1 + model = AMICAMLXNG(n_channels=8, n_models=2, comp_thresh=0.0) + assert model.share_comps is False and model.comp_thresh == 0.0 + with pytest.raises(ValueError, match="share_iter must be an integer >= 7"): + AMICAMLXNG(n_channels=8, n_models=2, share_iter=1) + with pytest.raises(ValueError, match="share_start must be an integer >= 1"): + AMICAMLXNG(n_channels=8, n_models=2, share_start=0) def test_single_model_sharing_is_accepted_and_inert(): diff --git a/pamica/tests/mne_tests/test_mne_backends.py b/pamica/tests/mne_tests/test_mne_backends.py index 7c690f4a..f315feea 100644 --- a/pamica/tests/mne_tests/test_mne_backends.py +++ b/pamica/tests/mne_tests/test_mne_backends.py @@ -18,6 +18,7 @@ import sys from pathlib import Path +from typing import Any, Dict import numpy as np import pytest @@ -276,3 +277,34 @@ def test_degenerate_fit_builds_no_basis_and_refuses_export(raw, backend): for owner in (ica.amica_, ica.amica_.model_): with pytest.raises(RuntimeError, match="degenerate"): owner.get_sphere() + + +@pytest.mark.parametrize("stop", ["max_iter", "min_dll"]) +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_n_iter_counts_the_e_steps_that_ran(raw, backend, stop): + """The exported ``n_iter_`` is the number of iterations whose E-step ran: + ``iteration + 1``, which is also the length of ``ll_history``, whether the + fit ran to ``max_iter`` or stopped on a convergence check (which exits + before its iteration's update, issue #339). ``min_dll=1e-3`` with + ``maxincs=2`` stops a fit of this recording well inside 60 iterations.""" + if backend == "mlx": + _require_mlx() + kwargs: Dict[str, Any] = {"max_iter": MAX_ITER} + if stop == "min_dll": + kwargs = { + "max_iter": 60, + "use_min_dll": True, + "min_dll": 1e-3, + "maxincs": 2, + "use_grad_norm": False, + } + ica = AMICAICA(random_state=SEED, verbose=False, backend=backend).fit(raw, **kwargs) + assert ica.amica_ is not None and ica.amica_.model_ is not None + model = ica.amica_.model_ + assert model.stop_reason == stop, "setup: the fit ended another way" + exported = ica.to_mne_ica() + assert exported.n_iter_ == model.iteration + 1 == len(model.ll_history) + if stop == "max_iter": + assert exported.n_iter_ == MAX_ITER + else: + assert exported.n_iter_ < 60 diff --git a/pamica/tests/native_oracle.py b/pamica/tests/native_oracle.py index 0a3f968c..e0e6ce99 100644 --- a/pamica/tests/native_oracle.py +++ b/pamica/tests/native_oracle.py @@ -136,6 +136,23 @@ def _read_f64(path: Path, shape: tuple[int, ...]) -> np.ndarray: return raw[:need].reshape(shape, order="F") +def history_mixing(workdir: Path, nw: int, num_comps: int) -> dict[int, np.ndarray]: + """The reference-layout ``A`` the binary wrote after each iteration. + + A run with ``do_history=1`` and ``histstep=1`` passed to + :func:`run_seeded_reference` writes its state after every iteration's + update to ``out/history//`` (``write_history``, amica15.f90:2287-2324, + called at :1130). Keys are the reference's 1-based iterations; an iteration + that ends the run on a stop takes no update and writes no history. + """ + root = Path(workdir) / "out" / "history" + return { + int(d.name): _read_f64(d / "A", (nw, num_comps)) + for d in root.iterdir() + if d.name.isdigit() and (d / "A").is_file() + } + + def run_seeded_reference( state: SeedState, data_file: Path, diff --git a/pamica/tests/test_component_rows.py b/pamica/tests/test_component_rows.py index 1b116bad..aeba9ec9 100644 --- a/pamica/tests/test_component_rows.py +++ b/pamica/tests/test_component_rows.py @@ -132,6 +132,14 @@ def _np(value: Any) -> np.ndarray: _ALL = ("torch", "numpy", "mlx") +# Newton from the second iteration. newtrate at the natural-gradient rate (0.1) +# keeps the ramp saturated: the likelihood falls on the iteration after +# pdftype=1's density switch, and the halved rate is ramped straight back to +# 0.1, so it steps the same whether the decrease response runs before the +# update (the reference's order, issue #339) or after it (the order the +# pre-change code, and this layout comparison, predate). A ramp toward the +# default newtrate 0.5 would move that step, which is not a layout effect. +_NEWTON: Dict[str, Any] = dict(newt_start=2, newtrate=0.1) # The NumPy backend implements only the generalized-Gaussian density (it raises # NotImplementedError for any other pdftype; test_numpy_rejects_other_pdftypes # below) and has no keep_best option, so those configurations run on PyTorch @@ -149,7 +157,7 @@ def _byte_id_configs() -> List[Any]: for pdftype in (0, 1): cfg = dict(doscaling=doscaling, do_newton=newton, pdftype=pdftype) if newton: - cfg["newt_start"] = 2 + cfg.update(_NEWTON) name = ( f"m{n_models}-{'scale' if doscaling else 'noscale'}-" f"{'newton' if newton else 'ng'}-pdf{pdftype}" @@ -157,7 +165,7 @@ def _byte_id_configs() -> List[Any]: configs.append( (name, n_models, cfg, _ALL if pdftype == 0 else _NOT_NUMPY) ) - newton: Dict[str, Any] = dict(do_newton=True, newt_start=2) + newton: Dict[str, Any] = dict(do_newton=True, **_NEWTON) configs += [ ( "m2-reject-newton", @@ -175,9 +183,12 @@ def _byte_id_configs() -> List[Any]: # Four blocks of 1024 samples: the per-block accumulation. ("m2-newton-4blocks", 2, dict(newton, block_size=1024), _ALL), # The best-iterate safeguard, with a learning rate high enough that the - # log-likelihood falls, so it restores an earlier snapshot of A. - # (lrate 1.5: from 2.0 the float32 MLX fit goes non-finite.) - ("m2-keepbest-restores", 2, dict(keep_best=True, lrate=1.5), _NOT_NUMPY), + # log-likelihood falls, so it restores an earlier snapshot of A. At lrate + # 0.5 the one decrease is on the last iteration (by 2.4e-2 on PyTorch and + # MLX), whose update the restore discards, so the iteration order of + # issue #339 acts on nothing compared here; at 1.5, the value before + # that issue, the likelihood falls on iterations 2, 4, 5 and 6. + ("m2-keepbest-restores", 2, dict(keep_best=True, lrate=0.5), _NOT_NUMPY), # Best-of-two restarts, which re-initializes A and keeps the winner. ("m2-restarts", 2, dict(n_restarts=2), _ALL), ] @@ -912,15 +923,19 @@ def _seeded_run( setattr(model, name, seed[name].copy()) model.c = np.zeros((NW, 2)) model.lrate, model.rholrate = _OPT["lrate"], _OPT["rholrate"] + model.rholrate_cap = _OPT["rholrate"] model.ll, model.nd = [], [] model.comp_list = seed["default"].copy() model._update_unmixing_matrices() model.comp_list = seed["merged"].copy() model.comp_used = np.isin(np.arange(2 * NW), model.comp_list) + # Read from the E-step itself: only fit()'s loop records self.ll + # (issue #339 review), and a pre-change class records it in the update. for it in range(k): model.iter = it - model._update_parameters(model._get_updates_and_likelihood()) - lls = list(model.ll) + updates = model._get_updates_and_likelihood() + lls.append(float(updates["ll"])) + model._update_parameters(updates) mixing = [_mixing(model, h) for h in range(2)] return np.asarray(lls), mixing, _np(model.mu), _np(model.beta) @@ -1034,11 +1049,16 @@ def torch_model(cls: Any) -> Any: # --- 5. early mass merges behave the same in the reference (opt-in) ------------- -# The recipe the sharing tests used before issue #334 made the metric compare -# true component maps: 4096 samples, pamica's default optimizer, an early scan -# at a loose threshold. The first scan (iteration 8) merges 28 components and the -# one at iteration 18 one more; the second model then loses its responsibility -# and the fit goes non-finite at iteration 20. _COLLAPSE_REF spells pamica's +# The kind of recipe the sharing tests used before issue #334 made the metric +# compare true component maps: 4096 samples, pamica's default optimizer, an +# early scan at a loose threshold. The first scan (iteration 11) merges 25 +# components and the second model's gm falls from 0.53 to 6.0e-4 within two +# iterations; by the second scan (iteration 22, which merges nothing more) it +# is 2e-22, and the fit goes non-finite at iteration 25. Issue #345 moved the +# A-freeze to the reference's iterations (mod(iter, share_iter) <= 5), so +# share_start is a multiple of share_iter here, which puts each window on its +# scan iteration; the earlier recipe (seed 7, scans at 8 and 18) now goes +# non-finite at iteration 18, before its second scan. _COLLAPSE_REF spells pamica's # defaults out for the binary, with no further scans, no A-freeze and no # convergence stops, so both sides run the same updates from the same state. _COLLAPSE_SAMPLES = 4096 @@ -1046,13 +1066,13 @@ def torch_model(cls: Any) -> Any: n_channels=NW, n_models=2, n_mix=NMIX, - seed=7, + seed=20, device="cpu", dtype=torch.float64, block_size=1024, share_comps=True, - share_start=8, - share_iter=10, + share_start=11, + share_iter=11, comp_thresh=0.9, ) _COLLAPSE_REF: Dict[str, Any] = dict( @@ -1181,7 +1201,8 @@ def continuation(seed: Dict[str, Any], k: int) -> Tuple[np.ndarray, float]: return np.asarray(lls), float(_np(model.gm)[1]) first = AMICATorchNG(**_COLLAPSE) - first.fit(X, max_iter=8, verbose=False) # the scan runs on the last iteration + # The scan runs on the last iteration of each fit. + first.fit(X, max_iter=_COLLAPSE["share_start"], verbose=False) seed = _post_scan(first) merges = 2 * NW - len(np.unique(seed["merged"])) assert merges >= 20, f"setup: the first scan merged only {merges}" @@ -1202,7 +1223,10 @@ def continuation(seed: Dict[str, Any], k: int) -> Tuple[np.ndarray, float]: np.testing.assert_allclose(gm2, ref.gm[1], rtol=1e-2) second = AMICATorchNG(**_COLLAPSE) - second.fit(X, max_iter=18, verbose=False) # the second scan runs last + second.fit( + X, max_iter=_COLLAPSE["share_start"] + _COLLAPSE["share_iter"], verbose=False + ) + assert second.stop_reason == "max_iter", "the fit collapsed before its second scan" seed = _post_scan(second) ref = run_seeded_reference( _reference_seed(seed), diff --git a/pamica/tests/test_iteration_order.py b/pamica/tests/test_iteration_order.py new file mode 100644 index 00000000..931f5f13 --- /dev/null +++ b/pamica/tests/test_iteration_order.py @@ -0,0 +1,587 @@ +"""Each iteration follows the reference's order (issues #339 and #345). + +The reference's main loop (amica15.f90:949-1142) runs, per iteration, the +E-step (``LL(iter)``, the sufficient statistics and the step ``dAk`` with its +norm ``ndtmpsum``), then the likelihood-decrease response and the stopping +checks, exits on a stop before any parameter moves (:1111), and only then calls +``update_params`` (:1122) with the rates the response just set. Inside +``update_params`` it holds the mixing-matrix update, with its lrate ramp and the +reset of the working rho rate, whenever ``iter >= share_start`` and +``mod(iter, share_iter) <= 5`` (:1803), whether or not ``share_comps`` is on. + +Every claim is observed through real fits on the bundled sample EEG, with no +mocks (``.rules/testing.md``): the only instrumentation is a call-recording spy +on the real ``_update_parameters``, which calls straight through and never +substitutes a result. Cross-backend by design (``.rules/backend_parity.md``): +PyTorch and NumPy always run; MLX skips per test (``pytest.importorskip`` plus +an Apple-GPU guard), never the module. Expected iterations are computed here +from the reference's own arithmetic, not read back from :mod:`pamica.schedule`. +The native-binary oracles for the same behavior are +``test_iteration_order_native_oracle.py``. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict, List + +import numpy as np +import pytest +import torch + +from pamica.amica import AMICA +from pamica.numpy_impl.core import AMICA as AMICA_NumPy +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +PARAM_FILE = SAMPLE_DIR / "input.param" +NW = 32 +FIELD = 30504 +NMIX = 3 +SEED = 42 +BACKENDS = ["torch", "numpy", "mlx"] + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +@pytest.fixture(scope="module") +def X() -> np.ndarray: + return load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD).astype( + np.float64 + ) + + +def _mlx_class(): + """``AMICAMLXNG``, or skip this test (never the whole module).""" + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + mx = mlx_core.mx + if mx.default_device().type != mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + return mlx_core.AMICAMLXNG + + +def _model(backend: str, max_iter: int, tmp_path: Path, **cfg: Any) -> Any: + """An unfitted one-model backend; ``cfg`` uses the torch/MLX names.""" + if backend == "numpy": + # NumPy's spellings; it has no keep_best (it always returns the last + # iterate, which is what keep_best=False asks of the others). + renames = { + "share_iter": "share_int", + "maxdecs": "max_decs", + "min_nd": "min_grad_norm", + } + params = {renames.get(k, k): v for k, v in cfg.items() if k != "keep_best"} + return AMICA_NumPy( + num_models=1, + num_mix=NMIX, + seed=SEED, + max_iter=max_iter, + use_tqdm=False, + do_opt_block=False, + writestep=10**7, + outdir=str(tmp_path / f"np{len(list(tmp_path.iterdir()))}"), + **params, + ) + if backend == "torch": + return AMICATorchNG( + n_channels=NW, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + **cfg, + ) + return _mlx_class()(n_channels=NW, n_mix=NMIX, seed=SEED, **cfg) + + +def _fit(model: Any, backend: str, x: np.ndarray, max_iter: int) -> List[float]: + """Fit and return the per-iteration log-likelihood trajectory.""" + if backend == "numpy": + model.fit(x) + return [float(v) for v in model.ll] + model.fit(x, max_iter=max_iter, verbose=False) + return [float(v) for v in model.ll_history] + + +def _as_np(value: Any) -> np.ndarray: + if isinstance(value, torch.Tensor): + return value.detach().cpu().numpy().copy() + return np.array(value, copy=True) + + +def _iteration(model: Any, backend: str) -> int: + return model.iter if backend == "numpy" else model.iteration + + +def _record_updates(model: Any, backend: str, rescaled: bool = False) -> List[Dict]: + """Record every call of the real ``_update_parameters``. + + A call-recording spy (``.rules/testing.md``): it stores ``A`` and ``lrate`` + before and after, the step it was handed and the iteration, and calls the + real method with the same arguments. With ``rescaled``, it also stores what + the backend's own ``doscaling`` rescale alone would make of the pre-update + ``A``, computed on the live model and then undone, so nothing it touches + outlives the probe. + """ + records: List[Dict] = [] + original = model._update_parameters + + def spy(*args): + rec: Dict[str, Any] = { + "it": _iteration(model, backend), + "A_before": _as_np(model.A), + "lrate_before": float(model.lrate), + "dAk": _as_np(args[-1].dAk), + } + if rescaled: + saved = (model.A, model.mu, model.beta) + if backend == "torch": + model.A, model.mu, model.beta = (t.clone() for t in saved) + model._rescale_components() + rec["A_rescaled"] = _as_np(model.A) + model.A, model.mu, model.beta = saved + result = original(*args) + rec["A_after"] = _as_np(model.A) + rec["lrate_after"] = float(model.lrate) + records.append(rec) + return result + + model._update_parameters = spy + return records + + +def _reference_holds(itf: int, share_start: int, share_iter: int) -> bool: + """amica15.f90:1803: A is updated only if ``iter < share_start`` or + ``mod(iter, share_iter) > 5``, over the reference's 1-based ``iter``.""" + return not (itf < share_start or itf % share_iter > 5) + + +# --- #345: the A-freeze without sharing ------------------------------------ + + +# share_start=3, share_iter=10: iterations 3-5 and 10-15 are held. The stops +# are off so every scheduled iteration runs. +_FREEZE = dict(share_start=3, share_iter=10, use_min_dll=False, use_grad_norm=False) +_FREEZE_ITERS = 16 +_FREEZE_FRAMES = 4096 + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_a_is_held_on_exactly_the_reference_iterations_without_sharing( + backend, X, tmp_path +): + """With ``share_comps`` off and ``doscaling`` off, ``A`` is bit for bit + unchanged across exactly the iterations the reference holds, and moves on + every other one (issue #345). Before the fix it moved on all of them.""" + model = _model(backend, _FREEZE_ITERS, tmp_path, doscaling=False, **_FREEZE) + records = _record_updates(model, backend) + ll = _fit(model, backend, X[:, :_FREEZE_FRAMES], _FREEZE_ITERS) + assert len(ll) == len(records) == _FREEZE_ITERS, "the fit did not run through" + + held = [r["it"] + 1 for r in records if np.array_equal(r["A_after"], r["A_before"])] + expected = [ + itf for itf in range(1, _FREEZE_ITERS + 1) if _reference_holds(itf, 3, 10) + ] + assert expected == [3, 4, 5, 10, 11, 12, 13, 14, 15] + assert held == expected + for r in records: + if r["it"] + 1 not in expected: + assert np.abs(r["A_after"] - r["A_before"]).max() > 1e-4 + else: + # The ramp and the step are held together (amica15.f90:1804-1816). + assert r["lrate_after"] == r["lrate_before"] + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_a_held_iteration_only_rescales_with_doscaling_on(backend, X, tmp_path): + """With ``doscaling`` on (the default), a held iteration still renormalizes + each component, as the reference's rescale runs outside the guarded branch + (amica15.f90:1843): ``A`` then ends exactly where the backend's own rescale + alone takes the pre-update ``A``, a change of round-off size (the + reference's own ``A`` moves by up to 3.3e-16 on such iterations), and the + other iterations still take their step.""" + model = _model(backend, _FREEZE_ITERS, tmp_path, doscaling=True, **_FREEZE) + records = _record_updates(model, backend, rescaled=True) + ll = _fit(model, backend, X[:, :_FREEZE_FRAMES], _FREEZE_ITERS) + assert len(ll) == len(records) == _FREEZE_ITERS, "the fit did not run through" + + for r in records: + itf = r["it"] + 1 + if _reference_holds(itf, 3, 10): + np.testing.assert_array_equal(r["A_after"], r["A_rescaled"]) + eps = np.finfo(r["A_after"].dtype).eps + assert np.abs(r["A_after"] - r["A_before"]).max() <= 8 * eps + else: + assert np.abs(r["A_after"] - r["A_rescaled"]).max() > 1e-4 + + +# The shipped defaults, share_start = share_iter = 100: every fit of 100 or more +# iterations is held on 100-105. A short slice keeps it cheap; the stops are off +# so the fit reaches the window. +_DEFAULT_WINDOW_ITERS = 107 +_DEFAULT_WINDOW_FRAMES = 2048 + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_default_fits_hold_a_on_iterations_100_to_105(backend, X, tmp_path): + """The defaults hold ``A`` on iterations 100-105 of every fit, sharing off, + as the reference does (the bundled ``amicaout`` fixture shows it: its + ``log|det W|`` stays constant on iterations 101-106 of ``out.txt``).""" + model = _model( + backend, + _DEFAULT_WINDOW_ITERS, + tmp_path, + doscaling=False, + use_min_dll=False, + use_grad_norm=False, + ) + assert (model.share_start, _share_iter(model, backend)) == (100, 100) + records = _record_updates(model, backend) + ll = _fit(model, backend, X[:, :_DEFAULT_WINDOW_FRAMES], _DEFAULT_WINDOW_ITERS) + assert len(ll) == len(records) == _DEFAULT_WINDOW_ITERS + held = [r["it"] + 1 for r in records if np.array_equal(r["A_after"], r["A_before"])] + assert held == [100, 101, 102, 103, 104, 105] + + +def _share_iter(model: Any, backend: str) -> int: + return model.share_int if backend == "numpy" else model.share_iter + + +@pytest.mark.parametrize("share_comps", [False, True]) +@pytest.mark.parametrize("bad", [6, 1, 0, -3, 2.5, True]) +@pytest.mark.parametrize("backend", BACKENDS) +def test_every_backend_rejects_a_share_iter_below_7( + backend, bad, share_comps, tmp_path +): + """``share_iter <= 6`` would hold ``A`` on every iteration from + ``share_start`` on (every remainder is 0-5), and the freeze applies with + sharing off too, so every constructor rejects it either way, with one + message that says why. The NumPy backend takes both ``share_int`` and + ``share_iter`` and names both. 7 is accepted.""" + name = "share_int/share_iter" if backend == "numpy" else "share_iter" + head = f"{name} must be an integer >= 7, got {bad!r}: below 7, " + with pytest.raises(ValueError) as err: + _model(backend, 1, tmp_path, share_comps=share_comps, share_iter=bad) + assert str(err.value).startswith(head) + assert "hold the mixing matrix permanently from share_start on" in str(err.value) + assert "amica15.f90:1803" in str(err.value) + if backend == "numpy": + # The canonical spelling reaches the same check and message. + with pytest.raises(ValueError) as err: + AMICA_NumPy(use_tqdm=False, share_comps=share_comps, share_iter=bad) + assert str(err.value).startswith(head) + ok = _model(backend, 1, tmp_path, share_comps=share_comps, share_iter=7) + assert _share_iter(ok, backend) == 7 + + +@pytest.mark.parametrize( + ("setting", "value", "message"), + [ + ( + "share_iter", + 6, + "must be an integer >= 7, got 6: below 7", + ), + ("share_start", 0, "share_start must be an integer >= 1, got 0"), + ], +) +@pytest.mark.parametrize("backend", BACKENDS) +def test_a_params_file_reaches_the_share_validation( + backend, setting, value, message, X, tmp_path +): + """A Fortran ``input.param`` that sets ``share_iter`` below 7 or + ``share_start`` below 1 is refused by the same checks as a keyword, on + every backend: through ``AMICA.from_params_file`` (PyTorch, MLX), whose + backend is built in ``fit``, and through the NumPy constructor's + ``params_file``.""" + text = PARAM_FILE.read_text() + line = f"{setting} 100\n" + assert line in text, f"setup: {PARAM_FILE.name} no longer sets {setting} 100" + path = tmp_path / "input.param" + path.write_text(text.replace(line, f"{setting} {value}\n")) + if backend == "numpy": + with pytest.raises(ValueError, match=message): + AMICA_NumPy(params_file=str(path), use_tqdm=False) + return + if backend == "mlx": + _mlx_class() + wrapper = AMICA.from_params_file(str(path), backend=backend, verbose=False) + with pytest.raises(ValueError, match=message): + wrapper.fit(X[:, :512], max_iter=1) + + +# --- #339: the decrease response acts on the same iteration's update --------- + + +# A learning rate high enough that the likelihood falls within the first +# iterations: on 4096 samples the first decrease is ll index 7, by 8.8e-3, on +# PyTorch and MLX alike (PyTorch and NumPy share the trajectory). doscaling off +# and one model make each update exactly A <- A - rate * dAk, so the rate each +# update used can be read off A itself. +_DECREASE = dict( + lrate=0.6, lratefact=0.5, newt_ramp=10, doscaling=False, keep_best=False +) +_DECREASE_ITERS = 10 +_DECREASE_FRAMES = 4096 + + +def _ramp(rate: float, cap: float, newt_ramp: int) -> float: + """The natural-gradient ramp inside the A branch (amica15.f90:1812).""" + return min(cap, rate + min(1.0 / newt_ramp, rate)) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_the_step_after_a_decrease_already_uses_the_halved_rate(backend, X, tmp_path): + """On the iteration whose likelihood fell, the reference halves ``lrate`` + before that iteration's update, so the step it takes is ramped from the + halved rate (amica15.f90:1061-1062, then :1812-1814). Read off the real + step: the rate that took ``A`` to its new value is ``ramp(r / 2)``, where + ``r`` is the previous step's rate, not ``ramp(r)``, which is what the + previous order (halving after the update) took. Before issue #339 every + backend took ``ramp(r)`` here.""" + model = _model(backend, _DECREASE_ITERS, tmp_path, **_DECREASE) + records = _record_updates(model, backend) + ll = _fit(model, backend, X[:, :_DECREASE_FRAMES], _DECREASE_ITERS) + assert len(ll) == len(records) == _DECREASE_ITERS + + decreases = [i for i in range(1, len(ll)) if ll[i] < ll[i - 1]] + assert decreases, "the likelihood never fell: the configuration exposes nothing" + d = decreases[0] + assert d + 1 < _DECREASE_ITERS + + def rate(r: Dict) -> float: + # A_after = A_before - rate * dAk, so the rate is the projection. + step = r["A_before"] - r["A_after"] + return float(np.sum(step * r["dAk"]) / np.sum(r["dAk"] * r["dAk"])) + + rtol = 1e-4 if backend == "mlx" else 1e-12 + previous, taken = rate(records[d - 1]), rate(records[d]) + # The recorded lrate is the one the step used. + assert taken == pytest.approx(records[d]["lrate_after"], rel=rtol) + cap = _DECREASE["lrate"] + halved_first = _ramp(previous * _DECREASE["lratefact"], cap, 10) + halved_after = _ramp(previous, cap, 10) + assert abs(halved_first - halved_after) > 0.1 * halved_after, ( + "the two orders would take the same step here; nothing is discriminated" + ) + assert taken == pytest.approx(halved_first, rel=rtol) + # Before the decrease the rate had ramped to its ceiling and stayed there. + assert previous == pytest.approx(cap, rel=rtol) + + +# --- #339: a stop returns the parameters its final likelihood describes ------- + + +# One recipe per convergence stop, each on 4096 samples of the bundled +# recording, one model, seed 42, stopping well inside the budget on every +# backend that has the stop. "twin" is the override that switches the stop off +# without changing the trajectory before it, so the same iterations can be run +# to max_iter. NumPy reports both halves of the decrease branch +# (grad_norm_floor, lrate_floor) as one reason. +_OFF = dict(use_min_dll=False, use_grad_norm=False, keep_best=False) +_STOPS: Dict[str, Dict[str, Any]] = { + # Gains below 1e-3 for more than 2 iterations in a row. + "min_dll": dict( + cfg=dict(_OFF, use_min_dll=True, min_dll=1e-3, maxincs=2), + twin=dict(use_min_dll=False), + numpy="Converged: small likelihood increase", + ), + # ndtmpsum runs 0.02139 at index 14 and 0.02039 at 15 on every backend, so + # 0.0209 is crossed at 15 with 5e-4 to spare on both sides. + "grad_norm": dict( + cfg=dict(_OFF, use_grad_norm=True, min_nd=0.0209), + twin=dict(use_grad_norm=False), + numpy="Converged: small gradient norm", + ), + # lrate 0.5 overshoots within a few iterations; min_nd above any norm makes + # the first decrease stop through the ``ndtmpsum <= min_nd`` half. + "grad_norm_floor": dict( + cfg=dict(_OFF, lrate=0.5, min_nd=1.0), + twin=dict(min_nd=0.0), + numpy="Converged: minimum change threshold met", + ), + # The same overshoot with minlrate above lrate and a norm floor no norm + # reaches, so the first decrease stops through the ``lrate <= minlrate`` half. + "lrate_floor": dict( + cfg=dict(_OFF, lrate=0.5, minlrate=0.6, min_nd=0.0), + twin=dict(minlrate=1e-12), + numpy="Converged: minimum change threshold met", + ), +} +_STOP_ITERS = 60 +_STOP_FRAMES = 4096 + + +def _recomputed_ll(model: Any, backend: str, x: np.ndarray) -> float: + """The log-likelihood of the model's current parameters on ``x``, computed + by the backend's own E-step exactly as its fit loop computes it.""" + if backend == "numpy": + return float(model._get_updates_and_likelihood()["ll"]) + x_t = model._preprocess(x) + acc = model._accumulate_blocks(x_t) + n = x_t.shape[1] + return float((acc["ll"] / (n * model.n_channels)).item()) + + +@pytest.mark.parametrize("stop", list(_STOPS)) +@pytest.mark.parametrize("backend", BACKENDS) +def test_a_stopped_fit_returns_the_parameters_its_final_ll_describes( + backend, stop, X, tmp_path +): + """Every convergence stop (``min_dll``, ``grad_norm``, ``grad_norm_floor``, + ``lrate_floor``) exits before its iteration's update, as the reference does + (amica15.f90:1111), so the returned parameters reproduce the recorded + final log-likelihood exactly. Before issue #339 the stopping iteration + still took its update, so they were one update past it. + + A fit that runs the same iterations to ``max_iter``, with the stop switched + off, does take its last update, as the reference's does, so there the + recomputed value moves on: the equality is not one any parameters near the + end would pass.""" + recipe = _STOPS[stop] + x = X[:, :_STOP_FRAMES] + model = _model(backend, _STOP_ITERS, tmp_path, **recipe["cfg"]) + ll = _fit(model, backend, x, _STOP_ITERS) + if backend == "numpy": + assert model.stop_reason == recipe["numpy"], "setup: another stop fired" + final = float(model.ll[-1]) + else: + assert model.stop_reason == stop, "setup: another stop fired" + final = float(model.final_ll_) + assert final == ll[-1] + assert model.iteration == len(ll) - 1 + assert 2 < len(ll) < _STOP_ITERS, "setup: the stop fired at the edge" + assert _recomputed_ll(model, backend, x) == final + + twin_cfg = dict(recipe["cfg"], **recipe["twin"]) + ran = _model(backend, len(ll), tmp_path, **twin_cfg) + ll_ran = _fit(ran, backend, x, len(ll)) + assert ll_ran == ll # the same trajectory, without the stop + assert _recomputed_ll(ran, backend, x) != ll_ran[-1] + + +# The best-iterate safeguard (PyTorch and MLX; NumPy has none): two models, +# lrate 0.5 on one 4096-sample block (test_component_rows.py's +# m2-keepbest-restores recipe), whose only decrease is on the last of six +# iterations, by 2.4e-2, so the fit ends that far below its peak and restores it. +_RESTORE_ITERS = 6 + + +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_a_keep_best_restore_returns_the_parameters_of_its_ll(backend, X, tmp_path): + """The snapshot is taken right after the E-step, before any check or update + moves the parameters, so a restored model's own log-likelihood is exactly + the ``final_ll_`` it reports.""" + x = X[:, :4096] + if backend == "torch": + model = AMICATorchNG( + n_channels=NW, + n_models=2, + n_mix=NMIX, + seed=SEED, + block_size=4096, + device="cpu", + dtype=torch.float64, + keep_best=True, + lrate=0.5, + ) + else: + model = _mlx_class()( + n_channels=NW, + n_models=2, + n_mix=NMIX, + seed=SEED, + block_size=4096, + keep_best=True, + lrate=0.5, + ) + model.fit(x, max_iter=_RESTORE_ITERS, verbose=False) + ll = model.ll_history + assert model.stop_reason == "max_iter" + assert model.final_ll_ != ll[-1], "keep_best restored nothing" + assert model.final_ll_ == max(ll) + assert _recomputed_ll(model, backend, x) == model.final_ll_ + + +# --- #339: the rho-rate ceiling is saved with the model ----------------------- + + +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_the_rho_rate_ceiling_survives_a_save(backend, X, tmp_path): + """The rho rate is two values since issue #339, the working ``rholrate`` + and its ceiling ``rholrate_cap`` (the reference's ``rholrate0``). A save + round-trips both exactly; a save written before the split has only + ``rholrate``, which held the ceiling then, so it loads as the ceiling. + ``maxdecs=1`` with ``newt_start=0`` ratchets the ceiling on the first + decrease, so it no longer equals the pristine ``rholrate0``.""" + cfg = dict(_DECREASE, maxdecs=1, newt_start=0) + model = _model(backend, _DECREASE_ITERS, tmp_path, **cfg) + _fit(model, backend, X[:, :_DECREASE_FRAMES], _DECREASE_ITERS) + assert model.rholrate_cap < model.rholrate0, "the ceiling never ratcheted" + + def load(state: Dict[str, Any]) -> Any: + if backend == "torch": + return AMICATorchNG.from_state_dict(state, device="cpu") + return type(model).from_state_dict(state) + + state = model.state_dict() + restored = load(state) + assert restored.rholrate_cap == model.rholrate_cap + assert restored.rholrate == model.rholrate + + del state["extra"]["rholrate_cap"] + legacy = load(state) + assert legacy.rholrate_cap == state["extra"]["rholrate"] + + +@pytest.mark.parametrize( + "field", ["lrate", "lrate_cap", "newtrate", "rholrate", "rholrate_cap"] +) +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_a_save_with_a_nonfinite_rate_is_refused_by_name(backend, field, X, tmp_path): + """A NaN learning rate in a save would load silently and turn the next + refit's first update into NaN parameters, so loading refuses it with a + ``ValueError`` naming the field, through ``from_state_dict`` and, on MLX, + through a file written by ``save``.""" + model = _model(backend, 2, tmp_path, keep_best=False) + _fit(model, backend, X[:, :2048], 2) + + state = model.state_dict() + state["extra"][field] = float("nan") + with pytest.raises(ValueError, match=f"extra field '{field}' must be a finite"): + if backend == "torch": + AMICATorchNG.from_state_dict(state, device="cpu") + else: + type(model).from_state_dict(state) + + if backend == "mlx": + path = tmp_path / "model.npz" + model.save(str(path)) + with np.load(path, allow_pickle=False) as data: + raw = {name: data[name] for name in data.files} + extra = json.loads(raw["extra"].item()) + extra[field] = float("nan") + raw["extra"] = np.array(json.dumps(extra)) + np.savez_compressed(path, **raw) + with pytest.raises(ValueError, match=f"extra field '{field}' must be"): + type(model).load(str(path)) + + +@pytest.mark.parametrize("backend", ["torch", "mlx"]) +def test_a_save_without_its_rho_rate_is_refused_by_name(backend, X, tmp_path): + """A save missing ``rholrate`` is malformed, reported like a missing + parameter tensor rather than as a bare ``KeyError``.""" + model = _model(backend, 2, tmp_path, keep_best=False) + _fit(model, backend, X[:, :2048], 2) + state = model.state_dict() + del state["extra"]["rholrate"] + with pytest.raises(ValueError, match=r"missing extra fields \['rholrate'\]"): + if backend == "torch": + AMICATorchNG.from_state_dict(state, device="cpu") + else: + type(model).from_state_dict(state) diff --git a/pamica/tests/test_iteration_order_native_oracle.py b/pamica/tests/test_iteration_order_native_oracle.py new file mode 100644 index 00000000..dbf77eb1 --- /dev/null +++ b/pamica/tests/test_iteration_order_native_oracle.py @@ -0,0 +1,360 @@ +"""The iteration order against the native reference binary (issues #339, #345). + +Opt-in (``AMICA_RUN_FORTRAN=1``) like the other binary-driven tests. The binary +is the pinned v0.3.3 release asset (:mod:`pamica.tests.native_oracle`), seeded +with pamica's own initialization through its ``load_*`` inputs and run with +``do_history`` on, so its mixing matrix after every iteration can be compared +with PyTorch's and NumPy's, iteration by iteration, together with the +log-likelihood trajectory. + +The comparison bound is the reference's own round-off noise in the same setup, +measured in the same test: the binary is run again with 2 and with 4 threads +instead of 1, which only changes the order of its reductions, and the floor is +the larger of the two deviations from the 1-thread run. On this +recording that noise grows quickly (one mixture shape sits at ``rho=1``, where +the location update divides by ``|y|``), so it is the honest floor; each backend +must stay within 10 times it. The multi-threaded runs are not reproducible from +one run to the next (their reduction order is scheduled at run time), so the +floor varies; the table gives its range over three runs. Measured (maximum over +the trajectory, PyTorch / NumPy against the binary, which are reproducible, +then the binary against itself): + +===================== ========================================= ================================================ +configuration LL deviation ``A`` deviation +===================== ========================================= ================================================ +decreases, 30 its 2.7e-5 / 8.2e-5 (floor 4.1e-5 to 1.3e-4) 8.1e-4 / 6.8e-4 (floor 5.3e-4 to 1.2e-3) +maxdecs ratchet 6.6e-6 / 8.6e-6 (floor 5.7e-6 to 9.6e-6) 3.0e-4 / 2.7e-4 (floor 1.4e-4 to 4.2e-4) +A-freeze, doscaling 6.6e-7 / 4.4e-7 (floor 4.0e-7 to 4.3e-7) 5.0e-6 / 5.6e-6 (floor 4.3e-6 to 7.5e-6) +A-freeze, no scaling 4.0e-7 / 8.0e-7 (floor 4.3e-7 to 6.1e-7) 1.3e-5 / 1.2e-5 (floor 1.2e-5 to 1.5e-5) +===================== ========================================= ================================================ + +Before the fix the same comparisons were 1.4e-2 / 2.2e-1 (decreases), +1.3e-2 / 1.4e-1 (ratchet) and 6.1e-3 / 1.9e-1 (A-freeze), hundreds to tens of +thousands of times the floor; each test replays the pre-change PyTorch backend +(through :mod:`pamica.tests.pre_change`) as a control that the bound +discriminates. +""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any, Dict, List, Tuple + +import numpy as np +import pytest +import torch + +from pamica.numpy_impl.core import AMICA as AMICA_NumPy +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +NMIX = 3 +SEED = 42 +# The epic branch before issue #339: the order and freeze this change replaces. +PRE_CHANGE_COMMIT = "5b6ae4f69eacf6aa18002ce336ed904f73438516" +# Each backend within this many times the reference's own round-off noise. +NOISE_FACTOR = 10.0 +# The thread counts whose deviation from the 1-thread run sets the floor. +_FLOOR_THREADS = (2, 4) +# The pre-change code at least this many times the noise, so the bound above +# discriminates. +CONTROL_FACTOR = 100.0 + +pytestmark = [ + pytest.mark.skipif( + os.environ.get("AMICA_RUN_FORTRAN") != "1", + reason="opt-in Fortran-binary integration test (set AMICA_RUN_FORTRAN=1)", + ), + pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing"), +] + +# The reference's input.param optimizer, no Newton, block size pinned on both +# sides; pamica keyword -> reference keyword where the names differ. +_OPT: Dict[str, Any] = dict( + block_size=512, + lrate=0.05, + lratefact=0.5, + rholrate=0.05, + rholratefact=0.5, + rho0=1.5, + minrho=1.0, + maxrho=2.0, + invsigmin=0.0, + invsigmax=100.0, + do_newton=False, + newt_start=50, + newtrate=1.0, + newt_ramp=10, + maxdecs=5, + doscaling=True, +) +_CONFIGS: Dict[str, Dict[str, Any]] = { + # lrate 0.5 overshoots from iteration 7 on, 8 decreases in 30 iterations. + "decreases": dict(lrate=0.5), + # maxdecs=2 completes cycles that ratchet the lrate ceiling, and with + # newt_start=1 the rho-rate ceiling too (amica15.f90:1066-1068). + "ratchet": dict(lrate=0.5, maxdecs=2, newt_start=1), + # share_comps off: A is held on iterations 3-5 and 10-15 (amica15.f90:1803). + "freeze": dict(share_start=3, share_iter=10), + "freeze-noscaling": dict(share_start=3, share_iter=10, doscaling=False), +} + + +@pytest.fixture(scope="module") +def X() -> np.ndarray: + return load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD).astype( + np.float64 + ) + + +@pytest.fixture(scope="module") +def pre(tmp_path_factory): + """The package before this change, imported beside the live one.""" + from pamica.tests.pre_change import load_pre_change_package + + return load_pre_change_package( + PRE_CHANGE_COMMIT, "pamica_pre339", tmp_path_factory.mktemp("pre339") + ) + + +def _cfg(name: str) -> Dict[str, Any]: + return {**_OPT, **_CONFIGS[name]} + + +def _reference_params(cfg: Dict[str, Any]) -> Dict[str, Any]: + params = {k: v for k, v in cfg.items() if k not in ("maxdecs", "doscaling")} + params.update( + max_decs=cfg["maxdecs"], + do_newton=int(cfg["do_newton"]), + doscaling=int(cfg["doscaling"]), + do_opt_block=0, + do_reject=0, + share_comps=0, + scalestep=1, + do_history=1, + histstep=1, + ) + return params + + +def _torch(cls: Any, cfg: Dict[str, Any]) -> Any: + return cls( + n_channels=NW, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + keep_best=False, + **cfg, + ) + + +def _with_history(model: Any, get_a) -> List[np.ndarray]: + """``A`` in the reference layout after every real ``_update_parameters`` + call (a call-recording spy that calls straight through).""" + out: List[np.ndarray] = [] + original = model._update_parameters + + def spy(*args): + result = original(*args) + out.append(get_a()) + return result + + model._update_parameters = spy + return out + + +def _deviation( + ll: List[float], a: List[np.ndarray], ref_ll: np.ndarray, ref_a: Dict[int, Any] +) -> Tuple[np.ndarray, np.ndarray]: + """Per-iteration |LL| and max |A| deviation over the common iterations.""" + n = min(len(ll), len(ref_ll)) + dll = np.abs(np.asarray(ll[:n]) - ref_ll[:n]) + da = np.array( + [np.abs(a[i] - ref_a[i + 1]).max() for i in range(len(a)) if i + 1 in ref_a] + ) + return dll, da + + +def _decreases(ll) -> List[int]: + return [i for i in range(1, len(ll)) if ll[i] < ll[i - 1]] + + +def _run(name: str, X: np.ndarray, workdir: Path, max_iter: int, pre: Any) -> dict: + """The reference (1, 2 and 4 threads), PyTorch, NumPy and the pre-change + PyTorch, all from one seeded state.""" + from pamica.tests.native_oracle import ( + history_mixing, + reference_mixing, + run_seeded_reference, + seed_from_torch, + ) + + cfg = _cfg(name) + init = _torch(AMICATorchNG, cfg) + init._preprocess(X) + init._initialize_parameters() + state = seed_from_torch(init) + ref: Dict[int, Any] = {} + for threads in (1, *_FLOOR_THREADS): + wd = workdir / f"t{threads}" + out = run_seeded_reference( + state, + DATA_FILE, + wd, + n_samples=FIELD, + max_iter=max_iter, + threads=threads, + **_reference_params(cfg), + ) + n = int((out.LL != 0).sum()) + ref[threads] = (out.LL[:n], history_mixing(wd, NW, NW)) + assert np.abs(out.S - init.sphere.numpy()).max() < 1e-12 + + fits = {} + t = _torch(AMICATorchNG, cfg) + t_a = _with_history(t, lambda: reference_mixing(t.A.numpy())) + t.fit(X, max_iter=max_iter, verbose=False) + fits["torch"] = (list(t.ll_history), t_a) + + numpy_cfg = {k: v for k, v in cfg.items() if k not in ("maxdecs", "share_iter")} + if "share_iter" in cfg: + numpy_cfg["share_int"] = cfg["share_iter"] + n = AMICA_NumPy( + num_models=1, + num_mix=NMIX, + seed=SEED, + max_iter=max_iter, + use_tqdm=False, + do_opt_block=False, + writestep=10**7, + outdir=str(workdir / "np"), + max_decs=cfg["maxdecs"], + **numpy_cfg, + ) + # The NumPy backend draws its own initialization from a different + # generator and keeps any parameter already set, so seed it the same way. + for attr in ("A", "mu", "beta", "rho", "alpha", "gm", "c"): + setattr(n, attr, getattr(init, attr).numpy().copy()) + + def numpy_mixing() -> np.ndarray: + assert n.A is not None, "the NumPy backend was seeded with A above" + return reference_mixing(n.A) + + n_a = _with_history(n, numpy_mixing) + n.fit(X) + fits["numpy"] = ([float(v) for v in n.ll], n_a) + + old = _torch(pre.torch_impl.core.AMICATorchNG, cfg) + old_a = _with_history(old, lambda: reference_mixing(old.A.numpy())) + old.fit(X, max_iter=max_iter, verbose=False) + fits["pre-change"] = (list(old.ll_history), old_a) + return {"ref": ref, "fits": fits} + + +def _floor(run: dict) -> Tuple[float, float]: + """The reference's own round-off noise: the largest deviation of a 2- or + 4-thread run from the 1-thread run, so no single reduction order sets it.""" + ll1, a1 = run["ref"][1] + floor_ll = floor_a = 0.0 + for threads in _FLOOR_THREADS: + ll_t, a_t = run["ref"][threads] + dll, da = _deviation(list(ll_t), [a_t[i] for i in sorted(a_t)], ll1, a1) + floor_ll = max(floor_ll, float(dll.max())) + floor_a = max(floor_a, float(da.max())) + return floor_ll, floor_a + + +def _check_against_floor(run: dict) -> None: + ref_ll, ref_a = run["ref"][1] + floor_ll, floor_a = _floor(run) + assert 0.0 < floor_ll and 0.0 < floor_a, "the reference noise was not measured" + print(f"reference noise floor: LL {floor_ll:.2e}, A {floor_a:.2e}") + for backend in ("torch", "numpy"): + ll, a = run["fits"][backend] + assert len(ll) == len(ref_ll), f"{backend} ran {len(ll)} iterations" + dll, da = _deviation(ll, a, ref_ll, ref_a) + print(f"{backend}: LL {dll.max():.2e}, A {da.max():.2e}") + assert dll.max() <= NOISE_FACTOR * floor_ll, ( + f"{backend}: LL off by {dll.max():.2e}, reference noise {floor_ll:.2e}" + ) + assert da.max() <= NOISE_FACTOR * floor_a, ( + f"{backend}: A off by {da.max():.2e}, reference noise {floor_a:.2e}" + ) + ll, a = run["fits"]["pre-change"] + dll, da = _deviation(ll, a, ref_ll, ref_a) + print(f"pre-change: LL {dll.max():.2e}, A {da.max():.2e}") + assert max(dll.max() / floor_ll, da.max() / floor_a) > CONTROL_FACTOR, ( + "the pre-change code also sits at the noise floor: the configuration " + "does not exercise this change" + ) + + +# --- #339: the decrease response before the update -------------------------- + + +@pytest.mark.parametrize("name", ["decreases", "ratchet"]) +def test_decrease_response_matches_the_reference(name, X, pre, tmp_path): + """With the likelihood-decrease response ahead of the update, PyTorch and + NumPy see the reference's decreases on the same iterations and track its + likelihood and mixing matrix at its own round-off noise through 30 + iterations of overshooting (issue #339).""" + run = _run(name, X, tmp_path, 30, pre) + ref_ll, _ = run["ref"][1] + ref_decreases = _decreases(ref_ll) + assert len(ref_decreases) >= 3, "the configuration no longer overshoots" + if name == "ratchet": + # A maxdecs=2 cycle completes past newt_start=1: both ceilings ratchet. + numdecs, cycles = 0, 0 + for i in ref_decreases: + numdecs += 1 + if numdecs >= 2: + cycles, numdecs = cycles + 1, 0 + assert cycles >= 1 + for backend in ("torch", "numpy"): + ll, _ = run["fits"][backend] + assert _decreases(ll) == ref_decreases, backend + _check_against_floor(run) + + +# --- #345: the A-freeze without sharing --------------------------------------- + + +@pytest.mark.parametrize("name", ["freeze", "freeze-noscaling"]) +def test_a_freeze_without_sharing_matches_the_reference(name, X, pre, tmp_path): + """With ``share_comps`` off, the reference holds ``A`` on iterations 3-5 + and 10-15 (``share_start=3``, ``share_iter=10``), and so do PyTorch and + NumPy, tracking it at its own round-off noise (issue #345). + + Read straight off the binary's own history first: its ``A`` is unchanged + on exactly those iterations (to the rescale's round-off with + ``doscaling`` on), which is the reference behavior this change adopts. + """ + k = 16 + run = _run(name, X, tmp_path, k, pre) + expected = [i for i in range(1, k + 1) if i >= 3 and i % 10 <= 5] + assert expected == [3, 4, 5, 10, 11, 12, 13, 14, 15] + scaling = _cfg(name)["doscaling"] + + def held(steps: List[float]) -> List[int]: + # With doscaling on, a held iteration still renormalizes A: the + # reference's own A moves by up to 3.3e-16 there. + bound = 8 * np.finfo(np.float64).eps if scaling else 0.0 + return [i + 2 for i, step in enumerate(steps) if step <= bound] + + _, ref_a = run["ref"][1] + ref_steps = [np.abs(ref_a[i + 1] - ref_a[i]).max() for i in range(1, k)] + assert held(ref_steps) == expected + for backend in ("torch", "numpy"): + _, a = run["fits"][backend] + steps = [np.abs(a[i + 1] - a[i]).max() for i in range(k - 1)] + assert held(steps) == expected, backend + if not scaling: + for itf in expected: + np.testing.assert_array_equal(a[itf - 1], a[itf - 2]) + _check_against_floor(run) diff --git a/pamica/tests/test_llt_stash.py b/pamica/tests/test_llt_stash.py index b690f762..90b7627c 100644 --- a/pamica/tests/test_llt_stash.py +++ b/pamica/tests/test_llt_stash.py @@ -390,10 +390,11 @@ def test_keep_best_restore_rolls_the_llt_stash_back(real_data, tmp_path): assertion rules out. """ # The overshoot recipe of test_ng_convergence.py (same data, block size - # and settings, so the same trajectory): aggressive Newton that peaks and - # then stops via the loosened min_dll (peak at iteration 70, stop at 71). - # A fixed 60-iteration budget overshot on the development machine only; - # on the CI runners that run was monotone. + # and settings, so the same trajectory): aggressive Newton that + # maxincs=0/min_dll=1e-8 stop on its first likelihood decrease (peak at + # iteration 13, stop at 14). Endings left to the trajectory (a fixed + # 60-iteration budget, then min_dll=1e-4/maxincs=2) overshot on one + # machine and ended at the peak on another. m = _torch_fit( real_data, n_models=2, @@ -404,8 +405,8 @@ def test_keep_best_restore_rolls_the_llt_stash_back(real_data, tmp_path): lrate=0.5, newtrate=3.0, use_min_dll=True, - min_dll=1e-4, - maxincs=2, + min_dll=1e-8, + maxincs=0, use_grad_norm=False, ) assert m.stop_reason not in AMICATorchNG._DEGENERATE_STOP_REASONS, ( diff --git a/pamica/tests/test_mlx_keepbest_cross_backend.py b/pamica/tests/test_mlx_keepbest_cross_backend.py index 60b53045..9dcef7bf 100644 --- a/pamica/tests/test_mlx_keepbest_cross_backend.py +++ b/pamica/tests/test_mlx_keepbest_cross_backend.py @@ -70,7 +70,8 @@ def real_data() -> np.ndarray: # independently verified, on MLX too (test_mlx_keepbest.py's module # docstring) -- but this module does not rely on that coincidence holding # forever, only on the CONTRACT below, which holds regardless. (newtrate=3.0 -# and the 150-iteration budget since issue #333, as in those recipes.) +# since issue #333 and the first-decrease min_dll stop since issue #339, as in +# those recipes.) _AGGRESSIVE_KWARGS: dict[str, Any] = dict( n_models=2, n_mix=NMIX, @@ -81,8 +82,8 @@ def real_data() -> np.ndarray: lrate=0.5, newtrate=3.0, use_min_dll=True, - min_dll=1e-4, - maxincs=2, + min_dll=1e-8, + maxincs=0, use_grad_norm=False, ) _MAX_ITER = 150 diff --git a/pamica/tests/test_mlx_newton_cross_backend.py b/pamica/tests/test_mlx_newton_cross_backend.py index dd9255fa..0f7b6474 100644 --- a/pamica/tests/test_mlx_newton_cross_backend.py +++ b/pamica/tests/test_mlx_newton_cross_backend.py @@ -127,6 +127,7 @@ def _torch_twin(model, x_t, block_size: int = BLOCK): ng.lrate_cap = model.lrate_cap ng.newtrate = model.newtrate ng.rholrate = model.rholrate + ng.rholrate_cap = model.rholrate_cap ng.iteration = model.iteration # sldet cancels in the cross-model softmax, but carry it anyway so the two # states are identical in every field either backend reads. diff --git a/pamica/tests/test_mlx_pdf_families_cross_backend.py b/pamica/tests/test_mlx_pdf_families_cross_backend.py index def3e721..6a8dac05 100644 --- a/pamica/tests/test_mlx_pdf_families_cross_backend.py +++ b/pamica/tests/test_mlx_pdf_families_cross_backend.py @@ -240,6 +240,7 @@ def _torch_twin(model, x_t): ng._sphere_pinv = None ng.lrate = model.lrate ng.rholrate = model.rholrate + ng.rholrate_cap = model.rholrate_cap ng.iteration = model.iteration ng.sldet = model.sldet ng._update_unmixing_matrices() diff --git a/pamica/tests/test_mlx_sharing_cross_backend.py b/pamica/tests/test_mlx_sharing_cross_backend.py index f4f59daa..06c1775c 100644 --- a/pamica/tests/test_mlx_sharing_cross_backend.py +++ b/pamica/tests/test_mlx_sharing_cross_backend.py @@ -134,6 +134,7 @@ def _torch_twin(model, x_t): ng._sphere_pinv = None ng.lrate = model.lrate ng.rholrate = model.rholrate + ng.rholrate_cap = model.rholrate_cap ng.iteration = model.iteration # sldet cancels in the cross-model softmax, but carry it anyway so the two # states are identical in every field either backend reads. diff --git a/pamica/tests/test_nonfinite_stops.py b/pamica/tests/test_nonfinite_stops.py new file mode 100644 index 00000000..ec0ebca2 --- /dev/null +++ b/pamica/tests/test_nonfinite_stops.py @@ -0,0 +1,327 @@ +"""Every backend stops the same way on a non-finite value (issue #339 review). + +Three places a fit can meet a non-finite value, each with its own degenerate +``stop_reason``, the same in PyTorch and MLX and named in the NumPy backend's +prose vocabulary: + +* the E-step's log-likelihood (``nan_ll``): the fit stops before recording it, + so every backend's history is the finite trajectory of the parameters the fit + visited, with the same length for the same event; +* the update direction or its norm ``ndtmpsum`` (``nan_direction``): a NaN + passes both ``<= min_nd`` checks (the comparison is False), so without a + check it would be applied; the fit stops before the update instead; +* the parameters right after an update (``nan_params``): without a check, a + corruption on the last iteration would end as ``max_iter`` with non-finite + parameters. + +A non-finite value is not something the bundled recording produces on demand at +a chosen iteration, so each test uses the sanctioned error-injection pattern +(``.rules/testing.md``, as ``test_schedule_gates.py``'s ``_NaNOnIteration``): a +subclass that runs the real E-step, direction and update on real data and then +corrupts one value on one iteration. Everything else, the stops included, is +the real code. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, List + +import numpy as np +import pytest +import torch + +from pamica.amica import AMICA +from pamica.numpy_impl.core import AMICA as AMICA_NumPy +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +NMIX = 3 +SEED = 42 +FRAMES = 4096 +BACKENDS = ["torch", "numpy", "mlx"] + +# The NumPy backend's prose names for the PyTorch/MLX stop reasons. +_NUMPY_REASON = { + "nan_ll": AMICA_NumPy._NONFINITE_LL_REASON, + "nan_direction": AMICA_NumPy._NONFINITE_DIRECTION_REASON, + "nan_params": AMICA_NumPy._NONFINITE_PARAMS_REASON, +} + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +@pytest.fixture(scope="module") +def X() -> np.ndarray: + return load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD).astype( + np.float64 + )[:, :FRAMES] + + +def _nan_at_00(value: Any) -> np.ndarray: + assert value is not None, "injected before the parameter exists" + out = np.array(value, dtype=np.float64, copy=True) + out[0, 0] = np.nan + return out + + +class _TorchInject(AMICATorchNG): + """``AMICATorchNG`` that corrupts one value on iteration ``at``.""" + + kind = "" + at = -1 + + def _accumulate_blocks(self, X, stash_llt=False): + acc = super()._accumulate_blocks(X, stash_llt=stash_llt) + if self.kind == "ll" and self.iteration == self.at: + acc["ll"] = acc["ll"] * float("nan") + return acc + + def _update_direction(self, acc): + step = super()._update_direction(acc) + if self.iteration == self.at: + if self.kind == "dAk": + dAk = step.dAk.clone() + dAk[0, 0] = float("nan") + step = step._replace(dAk=dAk) + elif self.kind == "ndtmpsum": + self._ndtmpsum = float("nan") + return step + + def _update_parameters(self, acc, n_samples, step=None): + super()._update_parameters(acc, n_samples, step) + if self.kind == "params" and self.iteration == self.at: + assert self.mu is not None + mu = self.mu.clone() + mu[0, 0] = float("nan") + self.mu = mu + + +class _NumPyInject(AMICA_NumPy): + """The NumPy backend, corrupting one value on iteration ``at``.""" + + kind = "" + at = -1 + + def _get_updates_and_likelihood(self): + updates = super()._get_updates_and_likelihood() + if self.kind == "ll" and self.iter == self.at: + updates["ll"] = float("nan") + return updates + + def _update_direction(self, updates): + step = super()._update_direction(updates) + if self.iter == self.at: + if self.kind == "dAk": + step = step._replace(dAk=_nan_at_00(step.dAk)) + elif self.kind == "ndtmpsum": + step = step._replace(nd=float("nan")) + return step + + def _update_parameters(self, updates, step=None): + super()._update_parameters(updates, step) + if self.kind == "params" and self.iter == self.at: + self.mu = _nan_at_00(self.mu) + elif self.kind == "params_A" and self.iter == self.at: + # W is derived from A inside the update, so rebuild it the way the + # update does: a corrupted A reaches W, as it would for real. + self.A = _nan_at_00(self.A) + self._update_unmixing_matrices() + + +def _mlx_inject_class() -> Any: + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + mx = mlx_core.mx + if mx.default_device().type != mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + + class _MLXInject(mlx_core.AMICAMLXNG): + """``AMICAMLXNG`` that corrupts one value on iteration ``at``.""" + + kind = "" + at = -1 + + def _accumulate_blocks(self, X, stash_llt=False): + acc = super()._accumulate_blocks(X, stash_llt=stash_llt) + if self.kind == "ll" and self.iteration == self.at: + acc["ll"] = acc["ll"] * float("nan") + return acc + + def _update_direction(self, acc): + step = super()._update_direction(acc) + if self.iteration == self.at: + if self.kind == "dAk": + step = step._replace( + dAk=mx.array(_nan_at_00(np.array(step.dAk)), dtype=mx.float32) + ) + elif self.kind == "ndtmpsum": + self._nd_arr = mx.array(float("nan"), dtype=mx.float32) + return step + + def _update_parameters(self, acc, n_samples, step=None): + super()._update_parameters(acc, n_samples, step) + if self.kind == "params" and self.iteration == self.at: + self.mu = mx.array(_nan_at_00(np.array(self.mu)), dtype=mx.float32) + + return _MLXInject + + +def _fit(backend: str, x: np.ndarray, kind: str, at: int, max_iter: int, tmp_path): + """A one-model fit that corrupts ``kind`` on 0-based iteration ``at``.""" + if backend == "numpy": + numpy_model = _NumPyInject( + num_models=1, + num_mix=NMIX, + seed=SEED, + max_iter=max_iter, + use_tqdm=False, + do_opt_block=False, + writestep=10**7, + # The one backend with a restart-on-NaN recovery: off here, so the + # stops compare like for like (the recovery has its own test below). + restartiter=0, + outdir=str(tmp_path / "np"), + ) + numpy_model.kind = kind + numpy_model.at = at + return numpy_model.fit(x) + if backend == "torch": + model: Any = _TorchInject( + n_channels=NW, n_mix=NMIX, seed=SEED, device="cpu", dtype=torch.float64 + ) + else: + model = _mlx_inject_class()(n_channels=NW, n_mix=NMIX, seed=SEED) + model.kind = kind + model.at = at + model.fit(x, max_iter=max_iter, verbose=False) + return model + + +def _history(model: Any, backend: str) -> List[float]: + return [float(v) for v in (model.ll if backend == "numpy" else model.ll_history)] + + +def _assert_degenerate(model: Any, backend: str, reason: str) -> None: + """The stop is ``reason`` and every output path refuses the model.""" + if backend == "numpy": + # The parameter reason carries the offenders' names after a colon. + assert model.stop_reason is not None + assert model.stop_reason.split(": ")[0] == _NUMPY_REASON[reason] + assert type(model)._is_degenerate_stop(model.stop_reason) + assert model.converged is False + with pytest.raises(RuntimeError, match="degenerate"): + model.get_weights() + return + assert model.stop_reason == reason + assert reason in type(model)._DEGENERATE_STOP_REASONS + assert np.isnan(model.final_ll_) + with pytest.raises(RuntimeError, match="degenerate"): + model.state_dict() + # The AMICA wrapper classifies the fitted backend on the same path its own + # fit() and load() take. + wrapper = AMICA(n_models=1, n_mix=NMIX, backend=backend) + wrapper.model_ = model + wrapper._mirror_backend() + assert wrapper.converged_ is False and wrapper.is_fitted_ is False + with pytest.raises(RuntimeError, match="degenerate"): + wrapper.get_unmixing_matrix() + + +# --- 1. a non-finite likelihood is never recorded ----------------------------- + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_a_nonfinite_likelihood_leaves_the_same_finite_history(backend, X, tmp_path): + """A NaN likelihood on iteration 3 stops every backend there, with the + three likelihoods before it and nothing else in the history. Before the + issue #339 review the NumPy backend recorded the NaN as a fourth entry.""" + at = 3 + model = _fit(backend, X, "ll", at, max_iter=6, tmp_path=tmp_path) + history = _history(model, backend) + assert len(history) == at + assert np.all(np.isfinite(history)) + _assert_degenerate(model, backend, "nan_ll") + + +# --- 2. a non-finite direction is never applied ------------------------------- + + +@pytest.mark.parametrize("which", ["dAk", "ndtmpsum"]) +@pytest.mark.parametrize("backend", BACKENDS) +def test_a_nonfinite_direction_stops_before_the_update(backend, which, X, tmp_path): + """A NaN in the step, or in its norm, on the last iteration stops the fit + before the update: the finite likelihood of that iteration is recorded, the + returned parameters are the finite ones it describes, and the stop is the + degenerate ``nan_direction``. Before, a NaN norm passed both ``<= min_nd`` + checks and the step was applied, so a NaN step left non-finite parameters + behind a ``max_iter`` stop.""" + max_iter = 4 + at = max_iter - 1 + model = _fit(backend, X, which, at, max_iter=max_iter, tmp_path=tmp_path) + history = _history(model, backend) + assert len(history) == at + 1 + assert np.all(np.isfinite(history)) + assert model._nonfinite_params() == [] + _assert_degenerate(model, backend, "nan_direction") + + +# --- 3. non-finite parameters after an update stop the fit -------------------- + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_nonfinite_parameters_after_the_last_update_stop_the_fit(backend, X, tmp_path): + """A NaN in ``mu`` right after the last iteration's update ends the fit as + the degenerate ``nan_params``, not ``max_iter``, on every backend, with the + finite likelihood of that iteration recorded. Before, PyTorch returned + ``max_iter`` holding the NaN (MLX already stopped; NumPy caught it only at + exit, under a different reason).""" + max_iter = 4 + at = max_iter - 1 + model = _fit(backend, X, "params", at, max_iter=max_iter, tmp_path=tmp_path) + history = _history(model, backend) + assert len(history) == at + 1 + assert np.all(np.isfinite(history)) + assert "mu" in model._nonfinite_params() + _assert_degenerate(model, backend, "nan_params") + + +@pytest.mark.parametrize(("kind", "restarted"), [("params_A", True), ("params", False)]) +def test_numpy_restart_window_takes_only_what_a_restart_repairs( + kind, restarted, X, tmp_path +): + """The NumPy backend's restart-on-NaN keeps working where it can: a + restart redraws A (and so W) and keeps the mixture parameters, as the + reference's does (amica15.f90:1026-1046). So a non-finite A after an update + whose next iteration is inside the restart window goes on to that + iteration, whose non-finite likelihood restarts the fit, which then + finishes normally; a non-finite ``mu`` there, which no restart would + repair, stops as ``nan_params`` at once.""" + model = _NumPyInject( + num_models=1, + num_mix=NMIX, + seed=SEED, + max_iter=6, + use_tqdm=False, + do_opt_block=False, + writestep=10**7, + restartiter=10, + maxrestarts=3, + outdir=str(tmp_path / "np"), + ) + model.kind = kind + model.at = 1 + model.fit(X) + assert np.all(np.isfinite(model.ll)) + if restarted: + assert model.numrestarts == 1 + assert model.converged is True, model.stop_reason + else: + assert model.numrestarts == 0 + assert model.stop_reason == f"{_NUMPY_REASON['nan_params']}: mu" + assert len(model.ll) == 2 diff --git a/pamica/tests/test_numpy_grad_norm.py b/pamica/tests/test_numpy_grad_norm.py index db6fb898..443e1837 100644 --- a/pamica/tests/test_numpy_grad_norm.py +++ b/pamica/tests/test_numpy_grad_norm.py @@ -88,11 +88,12 @@ def test_grad_norm_uses_the_pre_step_mixing_matrix(): updates = model._get_updates_and_likelihood() assert model.A is not None # set by fit(); narrows Optional for the checker a_before = np.asarray(model.A).copy() - model._update_parameters(updates) + step = model._update_direction(updates) + model._update_parameters(updates, step) dAk = (a_before - np.asarray(model.A)) / model.lrate expected = np.sqrt(np.sum(dAk**2) / (model.data_dim * model.num_comps)) - assert np.isclose(model.nd[-1], expected, rtol=1e-8) + assert np.isclose(step.nd, expected, rtol=1e-8) # --- gm ordering under sharing (issue #219) --------------------------------- @@ -129,9 +130,9 @@ def _nd_for_pre_update_gm(model, updates, gm_pre): if getattr(model, name, None) is not None } model.gm = np.array(gm_pre, dtype=float) - model.nd = [] - model._update_parameters(updates) - nd = float(np.asarray(model.nd)[-1]) + step = model._update_direction(updates) + model._update_parameters(updates, step) + nd = float(step.nd) for name, value in saved.items(): setattr(model, name, value) return nd diff --git a/pamica/tests/test_numpy_restarts.py b/pamica/tests/test_numpy_restarts.py index 3a880e6b..339b5bf4 100644 --- a/pamica/tests/test_numpy_restarts.py +++ b/pamica/tests/test_numpy_restarts.py @@ -196,6 +196,7 @@ def test_a_restart_does_not_inherit_the_previous_restart_schedule(real_data, out assert model.lrate0 == reference.lrate0 assert model.newtrate == reference.newtrate assert model.rholrate == reference.rholrate + assert model.rholrate_cap == reference.rholrate_cap # --------------------------------------------------------------------------- diff --git a/pamica/tests/test_numpy_share_comps.py b/pamica/tests/test_numpy_share_comps.py index 305c7ca9..11ac76e6 100644 --- a/pamica/tests/test_numpy_share_comps.py +++ b/pamica/tests/test_numpy_share_comps.py @@ -48,12 +48,20 @@ def _real_data(n_samples: int = 4096) -> np.ndarray: def _shared_fit( max_iter: int = 10, share_comps: bool = True, - share_start: int = 2, + share_start: int = 8, share_int: int = 8, **kwargs, ): - """A 2-model fit with sharing on: merge at iteration ``share_start``, the - 6-iteration A-freeze, then iterations where A moves again.""" + """A 2-model fit with sharing on: merge at iteration ``share_start``, a + multiple of ``share_int`` as with the reference's defaults, so the + reference's A-freeze (iterations 8-13, issue #345) starts on the merge + iteration. + + With ``share_comps=False`` the freeze still applies (it does not depend on + sharing), but a 3-iteration fit ends before it, so the direct M-steps the + tests below take from that state, at iteration 3, move A. (With the + previous ``share_start=2`` they were held, and a test on A would have + compared a matrix that never moved.)""" model = AMICA( num_models=2, num_mix=3, @@ -103,9 +111,11 @@ def _force_merged_column(model): def test_share_constructor_validation(kwargs, match): """Rejected up front, and for the same reasons as AMICATorchNG. - ``share_int <= 6`` is the one that bites: the post-merge A-freeze window is - 6 iterations long, so a shorter cycle would hold A frozen on every iteration - of every cycle and the fit would silently stop moving its mixing matrix. + ``share_int <= 6`` is the one that bites: the reference holds A on every + iteration whose remainder mod ``share_int`` is 0 to 5, so a shorter cycle + would hold A frozen on every iteration of every cycle and the fit would + silently stop moving its mixing matrix. (That one is rejected with sharing + off too; see ``test_iteration_order.py``.) """ with pytest.raises(ValueError, match=match): AMICA(num_models=2, share_comps=True, use_tqdm=False, **kwargs) @@ -285,12 +295,13 @@ def test_checkpoints_never_persist_non_finite_parameters(tmp_path): """A mid-fit checkpoint must not write a degenerate state to disk. ``fit``'s final write is not the only write: ``writestep`` checkpoints run - inside the loop, and a state that goes non-finite early is still on the - object for every later checkpoint. Persisting it would leave a run whose - only on-disk artifact is corrupt -- ``loadmodout`` reads NaN back without + inside the loop. Persisting a non-finite state would leave a run whose only + on-disk artifact is corrupt -- ``loadmodout`` reads NaN back without complaint. The collapse lands at iteration 2 of 4 with ``writestep=1``, so - the first checkpoint is valid and every later one must be refused, loudly, - without disturbing what the valid one wrote. + the first checkpoint is valid. Since the issue #339 review the fit stops + right after that update (``nan_params``), before its checkpoint; the + checkpoint's own refusal stays as the backstop and is exercised directly + at the end, without disturbing what the valid checkpoint wrote. """ model = _collapsing_model(tmp_path, max_iter=4, collapse_iter=1, writestep=1) with pytest.warns(RuntimeWarning, match="invalid value"): @@ -308,12 +319,19 @@ def test_checkpoints_never_persist_non_finite_parameters(tmp_path): assert value is not None, f"{name} missing from the written output" assert np.all(np.isfinite(np.asarray(value))), f"{name} written non-finite" - # Refused loudly, naming the parameter. Once here rather than once per - # remaining iteration: the NaN mu makes the next likelihood non-finite, so - # restart-on-NaN takes over and its `continue` skips the checkpoint entirely. + # The fit stopped on the collapse, before that iteration's checkpoint. + assert model.stop_reason == f"{AMICA._NONFINITE_PARAMS_REASON}: mu" + log_text = (tmp_path / "out" / "out.txt").read_text() + assert "Skipping the results checkpoint" not in log_text + + # The checkpoint refuses a non-finite state loudly, naming the parameter, + # and leaves the valid checkpoint alone. + written = (tmp_path / "out" / "W").read_bytes() + assert model._write_checkpoint("results") is False log_text = (tmp_path / "out" / "out.txt").read_text() assert "Skipping the results checkpoint" in log_text assert "non-finite mu" in log_text + assert (tmp_path / "out" / "W").read_bytes() == written # --- one gm-weighted A step per shared column (#242) ------------------------ @@ -392,11 +410,14 @@ def test_merged_away_column_does_not_move(): # --- post-merge A-freeze (#242) --------------------------------------------- -def test_a_frozen_window_matches_the_torch_schedule(): - """Identical window to AMICATorchNG: the merge iteration and the 5 after.""" +@pytest.mark.parametrize("share_comps, num_models", [(True, 2), (False, 2), (False, 1)]) +def test_a_frozen_window_matches_the_torch_schedule(share_comps, num_models): + """Identical window to AMICATorchNG, the reference's ``iter >= share_start`` + and ``mod(iter, share_int) <= 5`` (amica15.f90:1803, 1-indexed), with + sharing on or off and for any model count (issue #345).""" model = AMICA( - num_models=2, - share_comps=True, + num_models=num_models, + share_comps=share_comps, share_start=10, share_int=20, use_tqdm=False, @@ -406,19 +427,18 @@ def frozen(itf): model.iter = itf - 1 # _a_frozen works in Fortran-style 1-indexed iters return model._a_frozen() - assert not any(frozen(i) for i in range(1, 10)) # before share_start - assert all(frozen(i) for i in range(10, 16)) # merge iteration + 5 - assert not any(frozen(i) for i in range(16, 30)) # A moves again - assert all(frozen(i) for i in range(30, 36)) # next cycle + assert not any(frozen(i) for i in range(1, 20)) # remainder above 5 or early + assert all(frozen(i) for i in range(20, 26)) # remainder 0..5 + assert not any(frozen(i) for i in range(26, 40)) # A moves again + assert all(frozen(i) for i in range(40, 46)) # next cycle -def test_a_frozen_is_off_for_a_single_model(): - """A model cannot share with itself, so sharing never freezes A there.""" - model = AMICA( - num_models=1, share_comps=True, share_start=1, share_int=8, use_tqdm=False - ) +def test_a_frozen_applies_to_a_single_model_without_sharing(): + """The reference never checks share_comps in the A-update guard, so a + one-model fit with sharing off is held too (issue #345).""" + model = AMICA(num_models=1, share_start=1, share_int=8, use_tqdm=False) model.iter = 0 - assert model._a_frozen() is False + assert model._a_frozen() is True def test_freeze_holds_A_but_still_measures_the_gradient(): @@ -438,13 +458,14 @@ def test_freeze_holds_A_but_still_measures_the_gradient(): assert model._a_frozen() is True A_before = model.A.copy() lrate_before = model.lrate - nd_count = len(model.nd) - model._update_parameters(model._get_updates_and_likelihood()) + updates = model._get_updates_and_likelihood() + step = model._update_direction(updates) + model._update_parameters(updates, step) np.testing.assert_array_equal(model.A, A_before) assert model.lrate == lrate_before # the ramp is held with the step - assert len(model.nd) == nd_count + 1 and model.nd[-1] > 0.0 + assert step.nd > 0.0 # --- cross-backend agreement (.rules/backend_parity.md) --------------------- @@ -475,6 +496,7 @@ def test_shared_column_update_matches_the_torch_backend(): ng.comp_list = torch.from_numpy(model.comp_list.copy()) ng.lrate = model.lrate ng.rholrate = model.rholrate + ng.rholrate_cap = model.rholrate_cap ng.iteration = model.iter ng._update_unmixing_matrices() assert int(ng.comp_used.sum()) == int(model.comp_used.sum()) diff --git a/pamica/tests/test_sample_data.py b/pamica/tests/test_sample_data.py index 8930b794..f2dee42c 100644 --- a/pamica/tests/test_sample_data.py +++ b/pamica/tests/test_sample_data.py @@ -662,9 +662,12 @@ def _get_updates_and_likelihood(self): model.fit(data) # Restarts are capped, the run stops on the persistent non-finite LL, and # the terminal failure is surfaced (converged=False), not silently ignored. + # The non-finite likelihood itself is never recorded (issue #339 review), + # so the history is empty: every iteration was non-finite. assert model.numrestarts == 2 assert model.converged is False - assert not np.isfinite(model.ll[-1]) + assert model.stop_reason == AMICA._NONFINITE_LL_REASON + assert model.ll == [] def test_check_convergence_ratchets_lrate_on_decrease(): @@ -674,9 +677,12 @@ def test_check_convergence_ratchets_lrate_on_decrease(): ratchet (#41). Drives the convergence handler directly with a decreasing LL history. - Also pins the #193 fix: the rho rate is a maxdecs-ratcheted CEILING, NOT a - per-decrease monotone decay. rholrate must stay untouched on a decrease below - max_decs and only ratchet when numdecs hits max_decs. + Also pins the #193 fix: the rho-rate ceiling (``rholrate_cap``) is + ratcheted only when numdecs hits max_decs, never per decrease. What each + decrease scales is the working rate ``rholrate``, as the reference's does + (amica15.f90:1063); every A update resets it to the ceiling before rho moves + (issue #339), so the per-decrease scaling is not the monotone decay #193 + removed. """ model = AMICA( num_models=1, @@ -692,15 +698,18 @@ def test_check_convergence_ratchets_lrate_on_decrease(): model.nd = [1.0] # gradient norm well above min_grad_norm (no floor stop) lrate0_before = model.lrate0 newtrate_before = model.newtrate + rholrate_cap_before = model.rholrate_cap rholrate_before = model.rholrate - # First decrease: numdecs -> 1 (below max_decs), so just reduce lrate. The rho - # ceiling must NOT move here -- pre-#193 it decayed on every decrease. + # First decrease: numdecs -> 1 (below max_decs), so just reduce the working + # rates. The rho ceiling must NOT move here -- pre-#193 it decayed on every + # decrease. model.ll = [-3.0, -3.1] conv, _, numdecs, numincs = model._check_convergence(0, 0) assert conv is False assert numdecs == 1 - assert model.rholrate == rholrate_before + assert model.rholrate_cap == rholrate_cap_before + assert model.rholrate == pytest.approx(rholrate_before * model.rholratefact) # Second consecutive decrease: numdecs hits max_decs=2 -> ratchet ceilings, # reset numdecs, and CONTINUE (not converged). @@ -710,13 +719,14 @@ def test_check_convergence_ratchets_lrate_on_decrease(): assert numdecs == 0 assert model.lrate0 == lrate0_before * 0.5 assert model.newtrate == newtrate_before * 0.5 - assert model.rholrate == pytest.approx(rholrate_before * model.rholratefact) + assert model.rholrate_cap == pytest.approx(rholrate_cap_before * model.rholratefact) def test_reinitialize_for_restart_resets_rho_ceiling(): """Issue #193: a mid-fit restart (non-finite LL, Fortran restartiter path) - resets the rho-rate ceiling to rholrate0, exactly like the lrate reset -- a - previously-ratcheted rholrate must not carry across the restart. + resets the rho-rate ceiling, and the working rate with it, to rholrate0, + exactly like the lrate reset -- a previously-ratcheted rho rate must not + carry across the restart. """ model = AMICA(num_models=1, num_mix=3, seed=0) model.data_dim = 4 @@ -727,7 +737,8 @@ def test_reinitialize_for_restart_resets_rho_ceiling(): model._initialize_parameters() # Simulate a fit that ratcheted both ceilings, then hit a non-finite LL. - model.rholrate = model.rholrate0 * 0.25 + model.rholrate_cap = model.rholrate0 * 0.25 + model.rholrate = model.rholrate0 * 0.025 model.lrate = model.lrate0 * 0.25 model.ll = [-3.0, -3.1] model.nd = [0.5] @@ -735,6 +746,7 @@ def test_reinitialize_for_restart_resets_rho_ceiling(): model._reinitialize_for_restart() # Both ceilings restored to pristine; history cleared for a fresh judgment. + assert model.rholrate_cap == model.rholrate0 assert model.rholrate == model.rholrate0 assert model.lrate == model.lrate0 assert model.ll == [] diff --git a/pamica/tests/test_schedule_gates.py b/pamica/tests/test_schedule_gates.py index 0054f077..bff48492 100644 --- a/pamica/tests/test_schedule_gates.py +++ b/pamica/tests/test_schedule_gates.py @@ -51,9 +51,11 @@ # the likelihood-decrease bookkeeping has something to count within a short # budget. lrate=0.6/maxdecs=2/seed=3/8192 frames is test_mlx_newton.py's # schedule fixture: it was chosen there by sweeping seeds, block sizes and -# sample counts, and here it completes the first maxdecs cycle at iteration 11 -# on all three backends. Every test below still reads the iterations it needs -# off the executing machine's own trajectory rather than trusting that number. +# sample counts, and here it completes the first maxdecs cycle at iteration 9 +# on all three backends (iteration 11 before issue #339 moved the decrease +# response ahead of the update). Every test below still reads the iterations it +# needs off the executing machine's own trajectory rather than trusting that +# number. _OVERSHOOT: dict[str, Any] = dict(lrate=0.6, lratefact=0.5, maxdecs=2) # The Newton-phase ceiling test_mlx_newton.py pairs with it: at newtrate=1.0 # the post-switch-on trajectory can be monotone, leaving the reset unobservable. @@ -89,7 +91,7 @@ class _Run: lrate: float lrate_ceiling: float # lrate_cap (torch/MLX), lrate0 (NumPy) lrate_ceiling0: float - rholrate: float + rholrate: float # the rho-rate ceiling: rholrate_cap on every backend rholrate0: float newtrate: float newtrate0: float @@ -130,7 +132,7 @@ def _fit( lrate=m.lrate, lrate_ceiling=m.lrate0, lrate_ceiling0=m._pristine_state["lrate0"], - rholrate=m.rholrate, + rholrate=m.rholrate_cap, rholrate0=m.rholrate0, newtrate=m.newtrate, newtrate0=m._pristine_state["newtrate"], @@ -165,7 +167,7 @@ def _fit( lrate=m.lrate, lrate_ceiling=m.lrate_cap, lrate_ceiling0=m.lrate0, - rholrate=m.rholrate, + rholrate=m.rholrate_cap, rholrate0=m.rholrate0, newtrate=m.newtrate, newtrate0=m.newtrate0, @@ -293,15 +295,16 @@ def passes(rejstart: int, rejint: int, maxrej: int) -> list[int]: # Share merges from share_start=3 every 4 (amica15.f90:1856): 3, 7, 11. assert [i for i in idx if schedule.periodic_due(i, 3, 4)] == [2, 6, 10] assert fires(lambda i: schedule.periodic_due(i, 1, 1)) == list(idx) - # The A-freeze: the merge iteration and the 5 after it, anchored on - # share_start (a documented pamica decision), share_start=3, share_iter=8. + # The A-freeze (amica15.f90:1803): from share_start=3 on, every iteration + # whose remainder mod share_iter=8 is 0 to 5 (3, 4, 5, then 8 to 13), + # whether or not share_comps is on (issue #345). assert [i for i in idx if schedule.share_freeze(i, 3, 8)] == [ 2, 3, 4, - 5, - 6, 7, + 8, + 9, 10, 11, ] @@ -448,11 +451,15 @@ def test_rho_rate_ratchet_opens_only_after_iteration_newt_start(backend, X, tmp_ # lrate 0.5/0.6/0.8, maxdecs 2/3, newt_ramp 10/1 and 4096/8192 frames for a # ratchet at ll index 20, first on PyTorch and then checked on NumPy and MLX; # re-searched when issue #333's component-row doscaling changed the default -# trajectories. This one decreases at ll indices 4, 5, 8 and 9, 19, 20 on all -# three backends, so it ratchets at 8 and 20; the smallest of those decreases -# is 4.7e-4, far above round-off, float32 included. +# trajectories, and again when issue #339 moved the decrease response ahead of +# the update (seed 1, the previous choice, now ratchets at 8 and 16). Seven +# configurations qualify on PyTorch; this one, seed 3, also does on NumPy and +# MLX (seed 4, the widest-margin PyTorch candidate, ratchets at 16 in float32). +# It decreases at ll indices 4, 5, 8 and 14, 15, 20 on all three backends, so it +# ratchets at 8 and 20; the smallest decrease is 2.4e-3 and the smallest +# likelihood step of the run 4.4e-4, far above round-off, float32 included. _DEFAULT_GATE_FRAMES = 4096 -_DEFAULT_GATE_SEED = 1 +_DEFAULT_GATE_SEED = 3 _DEFAULT_GATE: dict[str, Any] = dict(lrate=0.6, lratefact=0.5, maxdecs=3, newt_ramp=1) @@ -631,7 +638,111 @@ def test_numpy_restart_window_is_the_first_restartiter_iterations( assert m.converged is bool(restarts), m.stop_reason if not restarts: assert m.stop_reason is not None and "Non-finite" in m.stop_reason - assert len(m.ll) == nan_iter + 1 # stopped on the poisoned iteration + # Stopped on the poisoned iteration, whose non-finite likelihood is not + # recorded (as in the PyTorch/MLX ll_history, issue #339 review). + assert len(m.ll) == nan_iter + + +def test_numpy_restart_clears_the_small_gain_count_as_the_reference_does(X, tmp_path): + """The reference's checks still run on its restart iteration, and its + ``min_dll`` comparison with the NaN likelihood is false, so it zeroes + ``numincs`` (amica15.f90:1078-1090). With every gain counted as small + (``min_dll=10``) and ``maxincs=3``, two small gains before a restart on the + 4th iteration (index 3) must not count after it: the fit stops on the 5th + small gain after the restart, at index 8, not at index 6, where the gains + carried across the restart (the behavior before the issue #339 review) + would stop it.""" + m = _NaNOnIteration( + num_models=1, + num_mix=NMIX, + seed=SEED, + block_size=BLOCK, + max_iter=20, + use_tqdm=False, + do_opt_block=False, + writestep=10**7, + restartiter=10, + maxrestarts=3, + use_min_dll=True, + min_dll=10.0, + maxincs=3, + use_grad_norm=False, + outdir=str(tmp_path / "out"), + ) + m.nan_iter = 3 + m.fit(X) + assert m.numrestarts == 1 + assert m.stop_reason == "Converged: small likelihood increase" + # Indices 4 to 8 after the restart: one to start the history, then four + # small gains, the fourth of which exceeds maxincs=3. + assert m.iter == 8 + assert len(m.ll) == 5 + + +# The epic base before issue #339, the last NumPy loop that updated before its +# restart check. +_PRE_339 = "5b6ae4f69eacf6aa18002ce336ed904f73438516" + + +def _updates_around_a_restart(base: Any, X: np.ndarray, tmp_path: Path) -> tuple: + """Fit ``base`` with a NaN likelihood on iteration 2 inside the restart + window, recording the iteration of every ``_update_parameters`` call with a + pass-through recorder (it records, then calls the real method with the same + arguments). Returns the recorded iterations and the restart count.""" + + class _NaNOnTwo(base): + def _get_updates_and_likelihood(self): + upd = super()._get_updates_and_likelihood() + if self.iter == 2: + upd["ll"] = float("nan") + return upd + + m = _NaNOnTwo( + num_models=1, + num_mix=NMIX, + seed=SEED, + block_size=BLOCK, + max_iter=5, + use_tqdm=False, + do_opt_block=False, + writestep=10**7, + restartiter=10, + maxrestarts=3, + outdir=str(tmp_path / "out"), + ) + updated: list = [] + real_update = m._update_parameters + + def record(*args, **kwargs): + updated.append(m.iter) + return real_update(*args, **kwargs) + + m._update_parameters = record + m.fit(X) + return updated, m.numrestarts + + +def test_numpy_restart_iteration_applies_no_update(X, tmp_path, tmp_path_factory): + """The reference checks for a restart before ``update_params`` and skips + the update on a restarting iteration (``startover``, amica15.f90:1115-1122). + Since issue #339 the NumPy loop does the same: iteration 2, whose + likelihood is NaN, redraws A and applies no update, while every other + iteration updates. The same fit of the code before issue #339, the control, + updated on iteration 2 too, from the parameters whose likelihood was NaN.""" + updated, restarts = _updates_around_a_restart(AMICA_NumPy, X, tmp_path / "now") + assert restarts == 1 + assert updated == [0, 1, 3, 4] + + from pamica.tests.pre_change import load_pre_change_package + + pre = load_pre_change_package( + _PRE_339, "pamica_pre339_gates", tmp_path_factory.mktemp("pre339") + ) + old, old_restarts = _updates_around_a_restart( + pre.numpy_impl.core.AMICA, X, tmp_path / "pre" + ) + assert old_restarts == 1 + assert old == [0, 1, 2, 3, 4], "control: the old loop no longer updates first" # --- validation of the settings the gates read ------------------------------ @@ -660,14 +771,22 @@ def _construct(backend: str, **kwargs: Any): ("rejstart", 0, 1, {"do_reject": True}), ("rejstart", -2, 1, {"do_reject": True}), ("rejstart", 2.0, 1, {"do_reject": True}), + # Checked whether or not share_comps is on: the reference's A-freeze + # reads it on every fit, and 0 would hold A from the first iteration + # (issue #339 review). + ("share_start", 0, 1, {}), + ("share_start", -5, 1, {"share_comps": True}), + ("share_start", 2.5, 1, {}), + ("share_start", True, 1, {}), ], ) @pytest.mark.parametrize("backend", BACKENDS) def test_schedule_settings_are_rejected_with_one_message( backend, setting, value, minimum, extra ): - """Every backend refuses the same ``newt_start``/``rejstart`` values with - the same message (``pamica.schedule.validate_iteration_setting``).""" + """Every backend refuses the same ``newt_start``/``rejstart``/ + ``share_start`` values with the same message + (``pamica.schedule.validate_iteration_setting``).""" with pytest.raises(ValueError) as exc: _construct(backend, **{setting: value}, **extra) assert str(exc.value) == f"{setting} must be an integer >= {minimum}, got {value!r}" @@ -676,13 +795,15 @@ def test_schedule_settings_are_rejected_with_one_message( @pytest.mark.parametrize("backend", BACKENDS) def test_boundary_schedule_settings_are_accepted(backend): """The smallest meaningful values construct, numpy integers included, and - ``rejstart`` is inert (unchecked) while ``do_reject`` is off, the pattern - ``share_start`` follows.""" + ``rejstart`` is inert (unchecked) while ``do_reject`` is off. + ``share_start`` is not inert without sharing: the A-freeze reads it.""" for kwargs in ( {"newt_start": 0}, {"newt_start": np.int64(3)}, {"do_reject": True, "rejstart": 1}, {"rejstart": 0}, + {"share_start": 1}, + {"share_start": np.int64(1), "share_comps": True}, ): _construct(backend, **kwargs) diff --git a/pamica/tests/torch_tests/test_amica_ng_wrapper.py b/pamica/tests/torch_tests/test_amica_ng_wrapper.py index 79620aab..f5f4f019 100644 --- a/pamica/tests/torch_tests/test_amica_ng_wrapper.py +++ b/pamica/tests/torch_tests/test_amica_ng_wrapper.py @@ -241,9 +241,11 @@ def test_write_amica_output_ll_matches_kept_iterate(real_data, tmp_path): likelihood (review finding, #92).""" model = AMICA(n_models=2, n_mix=3, device="cpu", verbose=False) # The overshoot recipe of test_ng_convergence.py (same data, block size - # and settings, so the same trajectory): it peaks at iteration 70 and stops - # via the loosened min_dll at 71. A fixed 60-iteration budget overshot on - # the development machine only; on the CI runners that run was monotone. + # and settings, so the same trajectory): maxincs=0/min_dll=1e-8 stop it on + # its first likelihood decrease (peak at iteration 13, stop at 14). Endings + # left to the trajectory (a fixed 60-iteration budget, then + # min_dll=1e-4/maxincs=2) overshot on one machine and ended at the peak on + # another. model.fit( real_data[:, :4096], max_iter=150, @@ -252,8 +254,8 @@ def test_write_amica_output_ll_matches_kept_iterate(real_data, tmp_path): lrate=0.5, newtrate=3.0, use_min_dll=True, - min_dll=1e-4, - maxincs=2, + min_dll=1e-8, + maxincs=0, use_grad_norm=False, seed=0, block_size=1024, diff --git a/pamica/tests/torch_tests/test_ng_backend.py b/pamica/tests/torch_tests/test_ng_backend.py index c8817a96..efa44546 100644 --- a/pamica/tests/torch_tests/test_ng_backend.py +++ b/pamica/tests/torch_tests/test_ng_backend.py @@ -1058,6 +1058,7 @@ def test_state_dict_roundtrip_all_fields(): "lrate_cap", "newtrate", "rholrate", + "rholrate_cap", # issue #339: the rho-rate ceiling, beside the working rate "final_ll_", # issue #51: the returned iterate's LL survives a round-trip "keep_best", ): @@ -1401,16 +1402,18 @@ def test_keep_best_snapshot_restore_roundtrip(): def _multimodel_keep_best(keep_best: bool) -> AMICATorchNG: - """The aggressive-Newton overshoot recipe of ``test_ng_convergence.py`` on - the same 4096 real samples, so the same trajectory: it peaks at iteration - 70 and stops via the loosened ``min_dll`` at 71 (``newt_start=2``, counted - from 1 since issue #335, is the run measured as 1 before it). A fixed 60-iteration - budget without that stop overshot on the development machine only; on the - CI runners that run was monotone.""" + """The aggressive-Newton overshoot recipe of ``test_ng_convergence.py`` + (``_OVERSHOOT_KWARGS``) on the same 4096 real samples, so the same + trajectory: ``maxincs=0``/``min_dll=1e-8`` stop it on its first likelihood + decrease, iteration 14, 1.6e-3 below the peak at 13 (``newt_start=2``, + counted from 1 since issue #335, is the run measured as 1 before it). + Endings left to the trajectory were not portable: a fixed 60-iteration + budget, and later a ``min_dll=1e-4``/``maxincs=2`` stop, each overshot on + one machine and ended at the peak on another.""" m = AMICATorchNG( n_channels=NW, n_models=2, n_mix=NMIX, seed=0, device="cpu", dtype=torch.float64, block_size=1024, do_newton=True, newt_start=2, - lrate=0.5, newtrate=3.0, use_min_dll=True, min_dll=1e-4, maxincs=2, + lrate=0.5, newtrate=3.0, use_min_dll=True, min_dll=1e-8, maxincs=0, use_grad_norm=False, keep_best=keep_best, ) # fmt: skip m.fit(_load_real_data()[:, :4096], max_iter=150, verbose=False) @@ -1473,18 +1476,18 @@ def test_keep_best_inactive_under_reject(): Non-vacuous on both counts: the recipe (the aggressive-Newton overshoot recipe of ``test_ng_convergence.py``, ``_OVERSHOOT_KWARGS``, with its - loosened ``min_dll`` stop) restores without - ``do_reject`` (peak at iteration 70, stop at 71, 1.9e-4 below the peak), + first-decrease ``min_dll`` stop) restores without + ``do_reject`` (peak at iteration 13, stop at 14, 1.6e-3 below the peak), and with ``do_reject`` its own trajectory also ends below an earlier peak - (iteration 57, stop at 59, 2.2e-4 below), so a restore would fire if the - safeguard were active. The previous config (``lrate=0.1`` natural + (the one rejection pass at iteration 6, then peak at 13, stop at 14, + 9.3e-3 below), so a restore would fire if the safeguard were active. The previous config (``lrate=0.1`` natural gradient, 12 iterations) was monotone, so it could not have failed. """ x = _load_real_data()[:, :4096] kwargs: dict[str, Any] = dict( n_channels=NW, n_models=2, n_mix=NMIX, seed=0, device="cpu", dtype=torch.float64, block_size=1024, do_newton=True, newt_start=2, - lrate=0.5, newtrate=3.0, use_min_dll=True, min_dll=1e-4, maxincs=2, + lrate=0.5, newtrate=3.0, use_min_dll=True, min_dll=1e-8, maxincs=0, use_grad_norm=False, keep_best=True, ) # fmt: skip plain = AMICATorchNG(**kwargs) @@ -1594,14 +1597,16 @@ def test_rholrate_ratchets_at_maxdecs_not_per_decrease(): """Issue #193: the rho learning rate is a maxdecs-ratcheted *ceiling*, not a per-LL-decrease monotone decay. - Fortran resets ``rholrate = rholrate0`` every iteration before the rho update - (amica15.f90:1806/1813) and only tightens the ceiling at ``maxdecs`` - (amica15.f90:1068, gated on ``iter > newt_start``). torch previously decayed - ``rholrate`` on EVERY LL decrease with no reset, collapsing it to ~1e-5 within - a few hundred iterations and freezing rho at a stale shape. + Fortran scales the working ``rholrate`` on each decrease (amica15.f90:1063), + resets it to the ceiling ``rholrate0`` in every A update before the rho + update (amica15.f90:1806/1813), and only tightens the ceiling at ``maxdecs`` + (amica15.f90:1068, gated on ``iter > newt_start``). pamica names that + ceiling ``rholrate_cap`` (issue #339). torch previously decayed the one + ``rholrate`` on EVERY LL decrease with no reset, collapsing it to ~1e-5 + within a few hundred iterations and freezing rho at a stale shape. On the real sample data a long-enough Newton run overshoots and triggers - several LL decreases. The surviving ``rholrate`` must have ratcheted exactly + several LL decreases. The surviving ceiling must have ratcheted exactly as often as ``newtrate`` (both gated on ``iter > newt_start`` at ``maxdecs``, matched 0.5 factor here), NOT once per decrease. @@ -1627,8 +1632,8 @@ def test_rholrate_ratchets_at_maxdecs_not_per_decrease(): # rholrate and newtrate share the maxdecs ratchet schedule, so the surviving # rho ceiling ratcheted the same number of times as newtrate. - assert m.rholrate == pytest.approx(m.rholrate0 * (m.newtrate / m.newtrate0)) + assert m.rholrate_cap == pytest.approx(m.rholrate0 * (m.newtrate / m.newtrate0)) # The old per-decrease decay (rholrate0 * rholratefact**n_dec) sits orders of # magnitude below the fixed ceiling; guard against a regression to it. buggy = m.rholrate0 * (m.rholratefact**n_dec) - assert m.rholrate > buggy * 10 + assert m.rholrate_cap > buggy * 10 diff --git a/pamica/tests/torch_tests/test_ng_convergence.py b/pamica/tests/torch_tests/test_ng_convergence.py index ca4f9df2..0d264ccb 100644 --- a/pamica/tests/torch_tests/test_ng_convergence.py +++ b/pamica/tests/torch_tests/test_ng_convergence.py @@ -103,20 +103,22 @@ def _predict_grad_norm_stop(nd_history: list, min_nd: float) -> Optional[int]: def _fit_with_nd_history(ng: AMICATorchNG, data: np.ndarray, **fit_kwargs) -> list: - """Fit ``ng``, recording ``ndtmpsum`` after every ``_update_parameters`` - call. This is an observational spy on the real method (calls straight - through via ``side_effect``, does not replace or bypass any computation) - -- the same pattern ``test_ng_backend.py`` already uses to inspect - intermediate state (e.g. ``test_newton_finalize_uses_preupdate_mu``).""" + """Fit ``ng``, recording ``ndtmpsum`` after every ``_update_direction`` + call, the one that computes it, every iteration the fit runs (including + the one a stop ends on, which takes no update). This is an observational + spy on the real method (calls straight through via ``side_effect``, does + not replace or bypass any computation) -- the same pattern + ``test_ng_backend.py`` already uses to inspect intermediate state (e.g. + ``test_newton_finalize_uses_preupdate_mu``).""" nd_history: list = [] - original = ng._update_parameters + original = ng._update_direction - def spy(acc, n): - result = original(acc, n) + def spy(acc): + result = original(acc) nd_history.append(ng._ndtmpsum) return result - with mock.patch.object(ng, "_update_parameters", side_effect=spy): + with mock.patch.object(ng, "_update_direction", side_effect=spy): ng.fit(data, verbose=False, **fit_kwargs) return nd_history @@ -356,14 +358,14 @@ def test_a_frozen_window_still_computes_fresh_grad_norm(real_data): # it would be if this iteration took no gradient step at all. probe = _fresh_ng(n_models=2) - def spy(acc, n): + def spy(acc, n, step): assert ng.A is not None and ng.mu is not None and ng.beta is not None assert ng.comp_list is not None frozen = ng._a_frozen() probe.A, probe.mu, probe.beta = ng.A.clone(), ng.mu.clone(), ng.beta.clone() probe.comp_list = ng.comp_list.clone() probe._rescale_components() - result = original(acc, n) + result = original(acc, n, step) trace.append( { "frozen": frozen, @@ -466,8 +468,11 @@ def test_grad_norm_floor_stop_leaves_wrapper_usable(real_data, tmp_path): # --- keep_best / do_reject interaction (early stopping must not break them) - -# The genuine-overshoot recipe (real 2-model data, aggressive Newton): it peaks -# and then stops via the loosened min_dll a couple of iterations later (see +# The genuine-overshoot recipe (real 2-model data, aggressive Newton). With +# maxincs=0 and min_dll=1e-8 the min_dll stop fires on the first iteration +# whose gain is below 1e-8, which on this trajectory is its first likelihood +# decrease: the overshoot is built into the stop, not left to where a chaotic +# trajectory happens to end (see # test_keep_best_restores_genuine_overshoot_under_min_dll_stop). newt_start # counts from 1 since issue #335, so 2 is the trajectory measured as 1 before. _OVERSHOOT_KWARGS: dict[str, Any] = dict( @@ -479,8 +484,8 @@ def test_grad_norm_floor_stop_leaves_wrapper_usable(real_data, tmp_path): newtrate=3.0, block_size=1024, use_min_dll=True, - min_dll=1e-4, - maxincs=2, + min_dll=1e-8, + maxincs=0, use_grad_norm=False, ) _OVERSHOOT_MAX_ITER = 150 @@ -498,12 +503,16 @@ def test_keep_best_restores_genuine_overshoot_under_min_dll_stop(real_data): ``test_write_amica_output_ll_matches_kept_iterate`` (issue #92, ``test_amica_ng_wrapper.py``: real 2-model data, aggressive ``do_newton``/``lrate``/``newtrate``), combined with a loosened ``min_dll`` - so the run stops via the NEW ``min_dll`` stop_reason a few iterations - after its peak, not via ``max_iter`` and not via a monotone approach to - that peak. ``newtrate=3.0`` and the 150-iteration budget were added for - issue #333: with ``doscaling`` rescaling components (not stored columns) - the old ``newtrate=0.5``/60-iteration recipe runs monotone to - ``max_iter``; this one peaks at iteration 70 and stops at 71. + so the run stops via the NEW ``min_dll`` stop_reason right after its peak, + not via ``max_iter`` and not via a monotone approach to that peak. + ``newtrate=3.0`` dates from issue #333: with ``doscaling`` rescaling + components (not stored columns) the old ``newtrate=0.5``/60-iteration + recipe runs monotone to ``max_iter``. ``maxincs=0``/``min_dll=1e-8`` date + from issue #339: the ``min_dll=1e-4``/``maxincs=2`` stop ended wherever + three small gains in a row fell, below the peak or at it depending on + round-off (it ended at its peak on the macOS CI runner, and in 1 of 12 + relative data perturbations of 1e-13 here). This one stops on the first + likelihood decrease, iteration 14, 1.6e-3 below the peak at 13, in all 12. """ x = real_data[:, :4096] ng = _fresh_ng(**_OVERSHOOT_KWARGS, keep_best=True) @@ -792,7 +801,7 @@ def test_mir_history_survives_keep_best_restore(real_data): with ``mir_step=1`` added. ``mir_step=1`` is load-bearing, not incidental. The ``min_dll``/``maxincs`` - stop halts one or two iterations past the peak, so with any coarser step + stop halts one iteration past the peak, so with any coarser step the last waypoint lands *before* the best iterate and the window a truncating restore would damage is never sampled -- a restore that dropped every waypoint after the best iterate would leave this fixture unchanged @@ -813,16 +822,26 @@ def test_mir_history_survives_keep_best_restore(real_data): # stopping iteration; the earlier equality-with-a-multiple-of-5 form was # satisfied by a truncating restore as well as a correct one. final_it = len(ng.ll_history) - 1 + # The min_dll stop exits before its iteration's update, as the reference + # does (issue #339), so that iteration records no waypoint: the last one + # belongs to the iteration before it. + last_update_it = final_it - 1 assert ng.final_ll_ is not None best_it = ng.ll_history.index(ng.final_ll_) - assert best_it < final_it, ( - "test setup: the restore must discard at least one iteration, or " + # Waypoint i is computed after iteration i's update, from the parameters + # whose likelihood is ll_history[i + 1]; the restore returns the ones + # measured at ll_history[best_it], so every waypoint from best_it on was + # computed from parameters it discards. A restore that rolled + # mir_history_ back with its snapshot (taken at best_it's E-step, before + # waypoint best_it exists) would end it at best_it - 1. + assert best_it <= last_update_it, ( + "test setup: the restore must discard at least one waypoint, or " "there is no truncation window to guard" ) - assert last_it == final_it, "the post-peak waypoints were dropped" + assert last_it == last_update_it, "the post-peak waypoints were dropped" # The count is what a partial truncation would move even if the last entry # happened to survive. - assert len(ng.mir_history_) == final_it + 1, ( + assert len(ng.mir_history_) == last_update_it + 1, ( "mir_history_ is not the full per-iteration trajectory" ) diff --git a/pamica/tests/torch_tests/test_ng_sharing.py b/pamica/tests/torch_tests/test_ng_sharing.py index 5e53ab89..d352cf1e 100644 --- a/pamica/tests/torch_tests/test_ng_sharing.py +++ b/pamica/tests/torch_tests/test_ng_sharing.py @@ -72,10 +72,22 @@ def _controlled_ng(A: torch.Tensor, n_channels: int, n_models: int) -> AMICATorc def test_single_model_byte_identical_with_share_toggled(real_data): """Sharing is a no-op for n_models=1, so a single-model fit must be byte-for-byte identical with share_comps on vs off (the gm-weighted A-update - reduces to the plain update since gm=[1] cancels exactly).""" + reduces to the plain update since gm=[1] cancels exactly). + + Both fits carry the same share_start/share_iter: the reference's A-freeze + reads them whether or not sharing is on (issue #345), so with share_iter=8 + both hold A on iteration 8, and only share_comps differs between them.""" x = real_data[:, :4096] off = AMICA(n_models=1, n_mix=3, device="cpu", verbose=False) - off.fit(x, max_iter=8, seed=42, block_size=1024, do_newton=True) + off.fit( + x, + max_iter=8, + seed=42, + block_size=1024, + do_newton=True, + share_start=7, + share_iter=8, + ) on = AMICA(n_models=1, n_mix=3, device="cpu", verbose=False) on.fit( x, @@ -182,14 +194,18 @@ def test_comp_thresh_one_merges_only_exact_duplicates(): # --- freeze schedule -------------------------------------------------------- -def test_a_frozen_window(): - """A is frozen for the merge iteration + 5 after it, thawed for the rest of - each cycle, and frozen again at the next cycle boundary.""" +@pytest.mark.parametrize("share_comps, n_models", [(True, 2), (False, 2), (False, 1)]) +def test_a_frozen_window(share_comps, n_models): + """The reference's arithmetic, ``iter >= share_start`` and + ``mod(iter, share_iter) <= 5`` (amica15.f90:1803, 1-indexed), for any model + count and with sharing on or off (issue #345). The remainder is of ``iter`` + itself, so with ``share_start=10`` (not a multiple of 20) nothing is held + until iteration 20; the window is not anchored on share_start.""" ng = AMICATorchNG( n_channels=4, - n_models=2, + n_models=n_models, device="cpu", - share_comps=True, + share_comps=share_comps, share_start=10, share_iter=20, ) @@ -198,23 +214,22 @@ def frozen(itf): ng.iteration = itf - 1 # itf is the Fortran-style 1-indexed iteration return ng._a_frozen() - assert not any(frozen(i) for i in range(1, 10)) # before share_start - assert all(frozen(i) for i in range(10, 16)) # merge + 5 (residue 0..5) - assert not any(frozen(i) for i in range(16, 30)) # thawed rest of cycle - assert all(frozen(i) for i in range(30, 36)) # next cycle boundary - - -def test_a_frozen_off_for_single_model(): - ng = AMICATorchNG( - n_channels=4, - n_models=1, - device="cpu", - share_comps=True, - share_start=2, - share_iter=8, - ) - ng.iteration = 3 - assert ng._a_frozen() is False + assert not any(frozen(i) for i in range(1, 20)) # before/at share_start + assert all(frozen(i) for i in range(20, 26)) # remainder 0..5 + assert not any(frozen(i) for i in range(26, 40)) # remainder 6..19 + assert all(frozen(i) for i in range(40, 46)) # next cycle + + +def test_a_frozen_window_starting_inside_a_cycle(): + """share_start inside the 0..5 remainder band holds A from share_start to + the band's end, as the reference's arithmetic does: 3, 4, 5, then 10-15.""" + ng = AMICATorchNG(n_channels=4, device="cpu", share_start=3, share_iter=10) + held = [] + for itf in range(1, 21): + ng.iteration = itf - 1 + if ng._a_frozen(): + held.append(itf) + assert held == [3, 4, 5, 10, 11, 12, 13, 14, 15, 20] # --- validation ------------------------------------------------------------- diff --git a/pamica/torch_impl/core.py b/pamica/torch_impl/core.py index 396500c7..296493f1 100644 --- a/pamica/torch_impl/core.py +++ b/pamica/torch_impl/core.py @@ -50,7 +50,7 @@ import logging import math import time -from typing import Dict, List, Optional, Sequence, Tuple, Union +from typing import Dict, List, NamedTuple, Optional, Sequence, Tuple, Union import numpy as np import torch @@ -315,6 +315,53 @@ def _log_pdf_only( return log_pdf, az_rho +# The learning rates a save carries (issue #339 review). rholrate_cap is +# additive: a save written before issue #339 kept one rho rate, the ceiling, +# under "rholrate", and a fit that never held A ends with the working rate +# equal to it. +_SAVED_RATES = ("lrate", "lrate_cap", "newtrate", "rholrate") + + +def _saved_rates(extra: dict, owner: str) -> dict: + """The saved learning rates, each a finite number, or a named ValueError. + + A missing rate is reported like a missing parameter tensor, and a + non-finite or non-numeric one is refused: a NaN rate would load silently + and turn the next refit's first update into NaN parameters. + """ + missing = [name for name in _SAVED_RATES if name not in extra] + if missing: + raise ValueError(f"malformed {owner} state: missing extra fields {missing}") + rates = {name: extra[name] for name in _SAVED_RATES} + rates["rholrate_cap"] = extra.get("rholrate_cap", extra["rholrate"]) + for name, value in rates.items(): + if ( + isinstance(value, bool) + or not isinstance(value, (int, float)) + or not math.isfinite(value) + ): + raise ValueError( + f"malformed {owner} state: extra field {name!r} must be a finite " + f"number, got {value!r}" + ) + return rates + + +class _UpdateStep(NamedTuple): + """The mixing-matrix step one E-step implies, computed before the + iteration's stopping checks and applied after them. + + The reference builds it in ``accum_updates_and_likelihood`` + (amica15.f90:1749-1761), beside ``LL(iter)`` and the gradient norm + ``ndtmpsum``, before ``update_params`` (:1782) takes the step. See + :meth:`AMICATorchNG._update_direction`. + """ + + dAk: torch.Tensor # (n_comps, n_channels), before the lrate scaling + newton_active: bool # the Newton schedule is on this iteration + no_newt: bool # some model failed the positive-definiteness guard + + class AMICATorchNG: """ Natural-gradient EM AMICA, ported from ``pamica.numpy_impl.core.AMICA``. @@ -382,7 +429,10 @@ class AMICATorchNG: lratefact : float, default=0.5 Factor by which ``lrate`` (and the ceiling ``lrate_cap``/``newtrate``) are annealed when the log-likelihood decreases; see ``fit`` for the - Fortran-style ``numdecs``/``maxdecs`` ratchet. + Fortran-style ``numdecs``/``maxdecs`` ratchet. The response runs on + the iteration whose E-step saw the decrease, before that iteration's + update, so the step it takes already uses the halved rate, as in the + reference (amica15.f90:1056-1122, issue #339). maxdecs : int, default=5 Number of consecutive log-likelihood decreases after which the learning-rate *ceiling* is ratcheted down (Fortran ``maxdecs``). @@ -448,8 +498,8 @@ class AMICATorchNG: check). Matches Fortran's ``ndtmpsum`` (amica15.f90:1760-1761): the RMS, over ``comp_used`` components only, of the per-iteration weight-update direction ``dAk`` (the natural-gradient/Newton step - before the ``lrate`` scaling and before ``share_comps``'s A-freeze - may discard it) -- ``sqrt(sum(dAk**2, axis=1)[comp_used].sum() / + before the ``lrate`` scaling and before the reference's A-freeze + (see ``share_iter``) may discard it) -- ``sqrt(sum(dAk**2, axis=1)[comp_used].sum() / (n_channels * comp_used.sum()))``, one squared norm per component row, as the reference sums each component's column. The ``comp_used`` mask only differs from all-True when ``share_comps`` @@ -507,7 +557,13 @@ class AMICATorchNG: would silently skip the reference's unconditional first pass. rho0, minrho, maxrho, rholrate : float Generalized-Gaussian shape-parameter initialization, clamp bounds, - and learning rate. + and learning rate. As in the reference, the rate has a working value + (``rholrate``), which each likelihood decrease multiplies by + ``rholratefact``, and a ceiling (``rholrate_cap``), which the + ``maxdecs`` ratchet multiplies by ``rholratefact`` after + ``newt_start``; every update of ``A`` resets the working value to the + ceiling before ``rho`` moves, so the decrease scaling reaches ``rho`` + only on an iteration on which ``A`` is held (see ``share_iter``). keep_best : bool, default=True Return the highest-log-likelihood iterate instead of the last one (issue #51). The lrate schedule is non-monotone (it anneals only after @@ -565,10 +621,16 @@ class AMICATorchNG: share_start, share_iter : int Sharing schedule: first iteration to attempt merges (counted from 1) and the interval between attempts (Fortran - ``share_start``/``share_iter``). The A-update - is held for the first 6 iterations of every cycle (independent of whether - a merge fired) so densities can settle; ``share_iter`` must be ``> 6`` so - that window never consumes the whole cycle. + ``share_start``/``share_iter``). They also set the reference's A-freeze, + which applies to every fit, sharing on or off (issue #345): from + iteration ``share_start`` on, the update of ``A`` (with its lrate ramp) + is held on every iteration whose number, counted from 1, has a + remainder of 0 to 5 modulo ``share_iter`` (amica15.f90:1803), so with + the defaults on iterations 100-105, 200-205, and so on. + Both are validated whether or not ``share_comps`` is on: + ``share_start`` must be an integer >= 1, and ``share_iter`` an + integer >= 7, or the freeze would hold A permanently from + ``share_start`` on. comp_thresh : float, default=0.99 Cosine-similarity cutoff (in the de-sphered/sensor-space metric) above which two components' mixing vectors are identified and merged. The @@ -736,7 +798,7 @@ def __init__( # amica15_header.f90:24/74 flags default True): the small- # likelihood-increase stop (use_min_dll/min_dll/maxincs) and the # weight-gradient-norm stop (use_grad_norm/min_nd). See fit() for the - # per-iteration checks and _update_parameters for the ndtmpsum + # per-iteration checks and _update_direction for the ndtmpsum # computation these both read. if maxincs < 0: raise ValueError(f"maxincs must be >= 0, got {maxincs}") @@ -776,7 +838,14 @@ def __init__( self.rho0 = rho0 self.minrho = minrho self.maxrho = maxrho + # The reference keeps a working rho rate (``rholrate``) and its ceiling + # (``rholrate0``): a likelihood decrease scales the working rate, a + # maxdecs ratchet the ceiling, and every A update resets the working + # rate to the ceiling before the rho update (amica15.f90:1063-1068, + # 1806/1813). ``rholrate_cap`` is that ceiling (as ``lrate_cap`` is + # lrate's); ``rholrate0`` keeps the constructor value for a refit. self.rholrate = rholrate + self.rholrate_cap = rholrate self.rholrate0 = rholrate self.rholratefact = rholratefact @@ -852,13 +921,11 @@ def __init__( # Cached sphere pseudo-inverse (issues #223, #253): the sensor-space # back-map, shared by get_sensor_mixing_matrix and the sharing metric. self._sphere_pinv = None + # The A-freeze schedule reads share_start/share_iter whether or not + # share_comps is on (issue #345), so both are validated always. + schedule.validate_share_start(share_start) + schedule.validate_share_iter(share_iter) if share_comps: - if share_start < 1: - raise ValueError(f"share_start must be >= 1, got {share_start}") - if share_iter <= 6: - # The A-freeze settle window is 6 iterations; a smaller cycle - # would freeze A permanently (never leaving room to update it). - raise ValueError(f"share_iter must be > 6, got {share_iter}") if not 0.0 < comp_thresh <= 1.0: raise ValueError(f"comp_thresh must be in (0, 1], got {comp_thresh}") @@ -911,7 +978,10 @@ def __init__( # keep_best, ``ll_history`` stays the true per-iteration trajectory # (which can include a late overshoot), while ``final_ll_`` is the LL of # the iterate fit() actually kept -- use this, not ``ll_history[-1]``, as - # the model's fitted log-likelihood. Set by fit(). + # the model's fitted log-likelihood. Set by fit(). Exact for a fit that + # ended on a convergence stop, which exits before that iteration's + # update (issue #339); after max_iter it is the LL one update before + # the returned parameters, as in the reference (see _fit_once). # # Under share_comps, a merge that fires on the LAST fit iteration is # reflected in the returned A/W/comp_list but NOT in final_ll_: the @@ -938,7 +1008,8 @@ def __init__( # ll_history[i] is the likelihood of the parameters BEFORE it (the # E-step accumulator that produced the update). The two therefore # describe states one update apart, so zipping them by index compares - # different parameters (issue #161). + # different parameters (issue #161). An iteration that ends the fit on + # a stop takes no update and records no waypoint. self.mir_history_: list[tuple[int, float, float]] = [] # Outlier-rejection bookkeeping (set up in fit()). @@ -954,8 +1025,8 @@ def __init__( self.n_newton_fallbacks = 0 # Weight-gradient-norm (Fortran ndtmpsum), recomputed every iteration by - # _update_parameters and read by fit()'s convergence checks (issue #207). - # None before the first _update_parameters call. + # _update_direction and read by fit()'s convergence checks (issue #207). + # None before the first _update_direction call. self._ndtmpsum: Optional[float] = None # Populated by fit()/_initialize_parameters(). @@ -1165,12 +1236,14 @@ def _initialize_parameters(self): self.n_kurt_done = 0 # Reset the mutable optimization state to the pristine constructor - # values (lrate_cap, newtrate, rholrate are ratcheted down during - # fit; restore them so a re-fit starts fresh). + # values (lrate/lrate_cap, newtrate and rholrate/rholrate_cap are + # annealed or ratcheted down during fit; restore them so a re-fit + # starts fresh). self.lrate = self.lrate0 self.lrate_cap = self.lrate0 self.newtrate = self.newtrate0 self.rholrate = self.rholrate0 + self.rholrate_cap = self.rholrate0 self.iteration = 0 self._update_unmixing_matrices() @@ -1653,10 +1726,126 @@ def _newton_direction(self, dA_h, sigma2_h, lambda_h, kappa_h): posdef = bool(valid[offdiag].all().item()) return H, posdef - def _update_parameters(self, acc: Dict[str, torch.Tensor], n_samples: int): + def _update_direction(self, acc: Dict[str, torch.Tensor]) -> _UpdateStep: + """The natural-gradient or Newton step for ``A`` from this iteration's + sufficient statistics, and its norm, without changing any parameter. + + The reference computes both in ``accum_updates_and_likelihood`` + (amica15.f90:1666-1761), with ``LL(iter)``, before the likelihood- + decrease response and the stopping checks read ``ndtmpsum`` and before + ``update_params`` applies the step; :meth:`fit` calls this first and + hands the result to :meth:`_update_parameters` once the checks have + run. Sets ``self._ndtmpsum``, the norm those checks read. + + Everything here reads the parameters as the E-step saw them: the Newton + curvature folds in the pre-update ``mu`` (as the reference does during + accumulation, amica15.f90:1666-1680), and ``dAk`` weights the models by + the pre-update ``gm`` (the reference does not reassign ``gm`` until + ``update_params``, :1788; issue #219). + """ + assert ( + self.mu is not None + and self.A is not None + and self.comp_list is not None + and self.gm is not None + ) + # Finalize the Newton curvature with the PRE-update mu. Fortran folds the + # mu^2 term into lambda during E-step accumulation, before the M-step + # moves mu (amica17.f90:1762-1774), and the NumPy port bakes it in at + # accumulation time. Nothing has moved mu yet when this runs. + newton_active = schedule.newton_active( + self.do_newton, self.iteration, self.newt_start + ) + if newton_active: + sigma2, lambda_, kappa = self._finalize_newton_stats(acc) + + # A is stored as Fortran's A^T (one component per row, issue #334; the + # true unmixing is W^T = inv(block)^T), so Fortran's per-model + # A_fort(:, comp_list(:,h)) @ dir becomes, transposed, + # dir^T @ A[comp_list[:, h], :] (LEFT-multiply by the TRANSPOSED + # direction). The direction ``dir`` (natural gradient I - /dgm, + # or its Newton precondition) is built in Fortran's untransposed + # convention. Getting this wrong (right-multiply by the untransposed + # dir) is invisible at the fixed point but sends the free-running fit + # downhill -- issue #24 root cause + # (.context/issue-24/root_cause_Aupdate.py, machine-exact check). + # + # Computed every iteration, including one on which A is held: Fortran + # computes dAk and ndtmpsum in accum_updates_and_likelihood + # (amica15.f90:1749-1761), before and apart from the guarded A step in + # update_params (:1803), and the grad-norm stop needs the true gradient + # magnitude every iteration (issue #207). + eye = torch.eye(self.n_channels, dtype=self.dtype, device=self.device) + directions = [] + no_newt = False + for h in range(self.n_models): + dA_h = -acc["dWtmp"][:, :, h] / acc["dgm"][h] + eye # I - /dgm + if newton_active: + H, posdef = self._newton_direction( + dA_h, sigma2[:, h], lambda_[:, h], kappa[:, h] + ) + if posdef: + directions.append(H) + else: + no_newt = True + directions.append(dA_h) # fall back to natural gradient + else: + directions.append(dA_h) + + # Accumulate each model's natural-gradient/Newton contribution per + # COMPONENT (a row of A) as a gm-WEIGHTED AVERAGE (Fortran dAk/zeta, + # amica15.f90:1749-1761): dAk = sum_h gm[h]*dir_h^T@block_h scattered + # into the rows comp_list names, zeta = sum_h gm[h] per component, then + # dAk /= zeta. For the default disjoint comp_list every component has + # exactly one contributor, so gm cancels (dAk = dir) and single-model + # (gm=[1]) is byte-for-byte unchanged; for a SHARED component (issue + # #60) the step is Fortran's responsibility-weighted average of the + # models' steps for that one mixing vector, NOT a raw sum (a raw sum + # would over-step by the contributor count and destabilize the fit). A + # merged-away component has no contributor, so its zeta is 0 and its + # dAk is 0/tiny = 0: its row takes no step. gm is still the pre-update + # weight here (see the docstring). + dAk = torch.zeros_like(self.A) + zeta = torch.zeros(self.n_comps, dtype=self.dtype, device=self.device) + for h in range(self.n_models): + idx = self.comp_list[:, h] + dAk.index_add_(0, idx, self.gm[h] * (directions[h].T @ self.A[idx, :])) + zeta.index_add_(0, idx, self.gm[h].expand(idx.shape[0])) + dAk = dAk / zeta.clamp_min(torch.finfo(self.dtype).tiny).unsqueeze(1) + + # Weight-gradient norm (Fortran ndtmpsum, amica15.f90:1760-1761): + # ``sqrt(sum(dAk**2, mask=comp_used) / (nw*count(comp_used)))``, one + # squared norm per component row, as the reference sums each + # component's column (``nd(iter,:) = sum(dAk*dAk,1)``). Read by fit()'s + # convergence checks (issue #207); the comp_used mask matters only when + # share_comps has merged components away (all-True otherwise, so + # ``comp_used_mask`` covers every component and this is a plain RMS over + # dAk). + comp_used_mask = self.comp_used + n_used = int(comp_used_mask.sum().item()) + nd = (dAk**2).sum(dim=1) # (n_comps,) + self._ndtmpsum = float( + torch.sqrt( + nd[comp_used_mask].sum() / (self.n_channels * max(n_used, 1)) + ).item() + ) + return _UpdateStep(dAk, newton_active, no_newt) + + def _update_parameters( + self, + acc: Dict[str, torch.Tensor], + n_samples: int, + step: Optional[_UpdateStep] = None, + ): """Apply the M-step parameter update, matching ``pamica.AMICA._update_parameters`` (natural-gradient and Newton). + ``step`` is the mixing-matrix step :meth:`_update_direction` computed + from the same ``acc``: :meth:`fit` passes the one its stopping checks + already read, so nothing is computed twice. A direct call may omit it, + and the step is then computed here first, from the parameters as they + stand. + ``n_samples`` is the number of samples that fed the accumulators (the good-sample count when ``do_reject`` is active), so ``gm`` and the reported log-likelihood are normalized by the effective sample count. @@ -1680,16 +1869,10 @@ def _update_parameters(self, acc: Dict[str, torch.Tensor], n_samples: int): and self.A is not None and self.comp_list is not None ) - # Fortran builds dAk from the previous iteration's model weights: gm is - # not reassigned until update_params (amica15.f90:1788+), after the - # dAk/zeta accumulation in accum_updates_and_likelihood (:1749-1761). - # Snapshot before overwriting so - # dAk -- which both drives the A-update below and reports ndtmpsum, unlike - # numpy_impl where it is only the diagnostic -- weights the way Fortran - # does (the ordering question raised by issue #219). Cloned rather than aliased: gm is only ever rebound - # today, but an in-place write elsewhere would silently corrupt this. - assert self.gm is not None - gm_prev = self.gm.clone() + if step is None: + step = self._update_direction(acc) + # The step was built with the pre-update gm (_update_direction), so gm + # can be reassigned now. self.gm = acc["dgm"] / n_samples # Per-model data-space bias (Fortran's `update_c` flag, amica17.f90:1423- @@ -1731,16 +1914,45 @@ def _update_parameters(self, acc: Dict[str, torch.Tensor], n_samples: int): used, acc["dalpha_n"] / acc["dalpha_n"].sum(dim=0, keepdim=True), self.alpha ) - # Finalize the Newton curvature with the PRE-update mu. Fortran folds the - # mu^2 term into lambda during E-step accumulation, before the M-step - # moves mu (amica17.f90:1762-1774), and the NumPy port bakes it in at - # accumulation time. Do it here, before self.mu is reassigned below, so - # lambda uses this iteration's mu rather than the updated one. - newton_active = schedule.newton_active( - self.do_newton, self.iteration, self.newt_start - ) - if newton_active: - sigma2, lambda_, kappa = self._finalize_newton_stats(acc) + # The A branch, where the reference has it: after gm/alpha/c and before + # mu/sbeta/rho (amica15.f90:1803-1816). On an iteration the reference + # holds A (:func:`pamica.schedule.share_freeze`), everything inside the + # branch is skipped together: the Newton-fallback bookkeeping (so a + # discarded Newton direction cannot pollute the fallback counter), the + # lrate ramp, the reset of the working rho rate to its ceiling, and the + # step itself. The step was built by _update_direction from the + # parameters the E-step saw, so taking it before the mixture updates + # changes nothing they read. + if not self._a_frozen(): + if step.newton_active and step.no_newt: + # Fortran prints "Hessian not positive definite, using natural + # gradient" (amica15.f90:1809-1811). Surface the same signal so an + # all-fallback run (issue #21) is visible without re-instrumenting. + self.n_newton_fallbacks += 1 + logger.warning( + "Newton not positive definite at iter %d; using natural gradient.", + self.iteration, + ) + + # Learning-rate ramp: toward newtrate while Newton is active and + # stable, otherwise toward the lrate ceiling (Fortran + # amica15.f90:1804-1815). It starts from this iteration's lrate, which + # a likelihood decrease has already halved (fit runs the response + # before this update, as the reference does, issue #339). + if step.newton_active and not step.no_newt: + self.lrate = min( + self.newtrate, self.lrate + min(1.0 / self.newt_ramp, self.lrate) + ) + else: + self.lrate = min( + self.lrate_cap, self.lrate + min(1.0 / self.newt_ramp, self.lrate) + ) + # The rho update below uses the ceiling (``rholrate = rholrate0``, + # :1806/:1813), so a decrease's per-iteration scaling of the working + # rho rate reaches the rho update only on a held iteration. + self.rholrate = self.rholrate_cap + + self.A = self.A - self.lrate * step.dAk # Exact-EM mixture location/scale (Fortran :1978/:1993). No lrate. # ``used`` masks merged-away components (no-op for the default comp_list). @@ -1754,20 +1966,12 @@ def _update_parameters(self, acc: Dict[str, torch.Tensor], n_samples: int): ), self.beta, ) - # Fortran keeps a live "NaN in sbeta!" canary here (amica17.f90:1996-2000). # The exact-EM mu/beta divisions are unguarded (matching Fortran, whose own - # mu/beta guard is commented out), so surface a non-finite value here - # instead of letting it propagate to a later, unattributable nan-LL stop. - if ( - not torch.isfinite(self.mu).all() - or not torch.isfinite(self.beta).all() - or not torch.isfinite(self.alpha).all() - ): - logger.warning( - "Non-finite mu/beta/alpha at iter %d (a mixture component's mass " - "likely collapsed).", - self.iteration, - ) + # mu/beta guard is commented out; it keeps a "NaN in sbeta!" canary, + # amica17.f90:1996-2000). fit() checks every parameter right after this + # update and stops, naming the offenders, on the iteration a value goes + # non-finite (stop_reason "nan_params"), so the failure is attributed + # where it happens rather than at a later nan-LL stop. # GG shape update with the 1/psi(1+1/rho) digamma factor (Fortran # :2013-2014); the divisor is the per-component responsibility mass @@ -1800,115 +2004,6 @@ def _update_parameters(self, acc: Dict[str, torch.Tensor], n_samples: int): used, torch.clamp(new_rho, self.minrho, self.maxrho), self.rho ) - # --- A / W update: natural gradient, optionally Newton-preconditioned. - # A is stored as Fortran's A^T (one component per row, issue #334; the - # true unmixing is W^T = inv(block)^T), so Fortran's per-model - # A_fort(:, comp_list(:,h)) @ dir becomes, transposed, - # dir^T @ A[comp_list[:, h], :] (LEFT-multiply by the TRANSPOSED - # direction). The direction ``dir`` (natural gradient I - /dgm, - # or its Newton precondition) is built in Fortran's untransposed - # convention. Getting this wrong (right-multiply by the untransposed - # dir) is invisible at the fixed point but sends the free-running fit - # downhill -- issue #24 root cause - # (.context/issue-24/root_cause_Aupdate.py, machine-exact check). - # (newton_active / sigma2 / lambda_ / kappa were finalized above.) - # - # The direction/dAk/gradient-norm computation below runs UNCONDITIONALLY, - # not gated on _a_frozen(): Fortran computes dAk and ndtmpsum every - # iteration in accum_updates_and_likelihood (amica15.f90:1749-1761), - # strictly before the LATER, separate update_A block (amica15.f90:1803) - # that actually steps A and is guarded by the share-freeze window. Only - # the step itself -- and the lrate ramp / Newton-fallback bookkeeping - # that Fortran nests inside that same guarded block -- are conditional on - # ``not self._a_frozen()`` (issue #207: the grad-norm stop needs - # ndtmpsum to reflect the true gradient magnitude every iteration, not - # just the iterations where A actually moves). _a_frozen() is always - # False when sharing is off, so the default path recomputes exactly what - # it always did, just with the gate narrowed. - eye = torch.eye(self.n_channels, dtype=self.dtype, device=self.device) - directions = [] - no_newt = False - for h in range(self.n_models): - dA_h = -acc["dWtmp"][:, :, h] / acc["dgm"][h] + eye # I - /dgm - if newton_active: - H, posdef = self._newton_direction( - dA_h, sigma2[:, h], lambda_[:, h], kappa[:, h] - ) - if posdef: - directions.append(H) - else: - no_newt = True - directions.append(dA_h) # fall back to natural gradient - else: - directions.append(dA_h) - - # Accumulate each model's natural-gradient/Newton contribution per - # COMPONENT (a row of A) as a gm-WEIGHTED AVERAGE (Fortran dAk/zeta, - # amica15.f90:1749-1761): dAk = sum_h gm[h]*dir_h^T@block_h scattered - # into the rows comp_list names, zeta = sum_h gm[h] per component, then - # dAk /= zeta. For the default disjoint comp_list every component has - # exactly one contributor, so gm cancels (dAk = dir) and single-model - # (gm=[1]) is byte-for-byte unchanged; for a SHARED component (issue - # #60) the step is Fortran's responsibility-weighted average of the - # models' steps for that one mixing vector, NOT a raw sum (a raw sum - # would over-step by the contributor count and destabilize the fit). A - # merged-away component has no contributor, so its zeta is 0 and its - # dAk is 0/tiny = 0: its row takes no step. - dAk = torch.zeros_like(self.A) - zeta = torch.zeros(self.n_comps, dtype=self.dtype, device=self.device) - for h in range(self.n_models): - idx = self.comp_list[:, h] - dAk.index_add_(0, idx, gm_prev[h] * (directions[h].T @ self.A[idx, :])) - zeta.index_add_(0, idx, gm_prev[h].expand(idx.shape[0])) - dAk = dAk / zeta.clamp_min(torch.finfo(self.dtype).tiny).unsqueeze(1) - - # Weight-gradient norm (Fortran ndtmpsum, amica15.f90:1760-1761): - # ``sqrt(sum(dAk**2, mask=comp_used) / (nw*count(comp_used)))``, one - # squared norm per component row, as the reference sums each - # component's column (``nd(iter,:) = sum(dAk*dAk,1)``). Read by fit()'s - # convergence checks (issue #207); the comp_used mask matters only when - # share_comps has merged components away (all-True otherwise, so - # ``comp_used_mask`` covers every component and this is a plain RMS over - # dAk). Named distinctly from the ``used`` (1, n_comps) broadcast mask - # above (alpha/mu/beta/rho updates) to avoid shadowing it. - comp_used_mask = self.comp_used - n_used = int(comp_used_mask.sum().item()) - nd = (dAk**2).sum(dim=1) # (n_comps,) - self._ndtmpsum = float( - torch.sqrt( - nd[comp_used_mask].sum() / (self.n_channels * max(n_used, 1)) - ).item() - ) - - # A-update. When sharing holds A this iteration (the post-merge settle - # window, Fortran amica15.f90:1803), skip the step -- lrate ramp, - # Newton-fallback bookkeeping, and the DAXPY itself -- so a discarded - # Newton direction cannot pollute the fallback counter. - if not self._a_frozen(): - if newton_active and no_newt: - # Fortran prints "Hessian not positive definite, using natural - # gradient" (amica15.f90:1809-1811). Surface the same signal so an - # all-fallback run (issue #21) is visible without re-instrumenting. - self.n_newton_fallbacks += 1 - logger.warning( - "Newton not positive definite at iter %d; using natural gradient.", - self.iteration, - ) - - # Learning-rate ramp: toward newtrate while Newton is active and - # stable, otherwise toward lrate0 (Fortran amica15.f90:1804-1815). - # Ramped after mu/beta/rho (exact-EM, lrate-free) and before A. - if newton_active and not no_newt: - self.lrate = min( - self.newtrate, self.lrate + min(1.0 / self.newt_ramp, self.lrate) - ) - else: - self.lrate = min( - self.lrate_cap, self.lrate + min(1.0 / self.newt_ramp, self.lrate) - ) - - self.A = self.A - self.lrate * dAk - # The reference rescales every iteration (it parses ``scalestep`` but # never reads it, amica15.f90:1843/3686); pamica keeps ``scalestep`` as # an extension counted from 1 like the reference's other cadences @@ -1957,28 +2052,20 @@ def _rescale_components(self) -> None: self.beta = self.beta / scale def _a_frozen(self) -> bool: - """Whether the A-update (and its lrate ramp) is held this iteration. - - A is frozen for the first 6 iterations of every ``share_iter``-length - window once ``iter >= share_start`` -- i.e. the merge iteration and the 5 - after it -- so the density parameters can settle onto any freshly merged - component before the mixing matrix moves again (Fortran A-freeze, - amica15.f90:1803). The window fires each cycle regardless of whether that - cycle's ``_identify_shared_comps`` actually merged a pair. - - Anchored on ``share_start`` (:func:`pamica.schedule.share_freeze`, which - counts iterations from 1 like the reference) so it stays aligned with - the merge schedule for any ``share_start``; the literal Fortran formula - uses ``mod(iter, share_iter)`` (misaligned unless share_start is a - multiple of share_iter, and a permanent freeze for ``share_iter <= 6``), - but that path is dead in the reference (see :meth:`_identify_shared_comps`) - so there is no parity constraint -- the constructor requires - ``share_iter > 6`` so the window never consumes the whole cycle. Gated - behind ``share_comps`` and ``n_models >= 2``, so with sharing off it is - always False and the validated default trajectory is untouched. + """Whether the reference holds the A update (with its lrate ramp and + rho-rate reset) this iteration: once ``iter >= share_start``, every + iteration with ``mod(iter, share_iter) <= 5``, counted from 1 + (amica15.f90:1803, :func:`pamica.schedule.share_freeze`). + + The reference applies this whether or not ``share_comps`` is on and for + any number of models (issue #345), so this does too: with the defaults + ``share_start = share_iter = 100``, every fit of 100 or more iterations + holds A on iterations 100-105, 200-205, and so on. The reference's own + scan never merges (see :meth:`_identify_shared_comps`), so this + unconditional schedule is the only freeze it ever shows. The + constructor requires ``share_iter >= 7``, since a shorter cycle would + hold A for good (:func:`pamica.schedule.validate_share_iter`). """ - if not self.share_comps or self.n_models < 2: - return False return schedule.share_freeze(self.iteration, self.share_start, self.share_iter) def _identify_shared_comps(self) -> None: @@ -2279,7 +2366,7 @@ def _restore_params(self, snapshot: Dict[str, object]) -> None: # ... the schedule/counters a fit mutates (the state_dict extras) ... "iteration", "ll_history", "final_ll_", "stop_reason", "mir_history_", "n_newton_fallbacks", "n_kurt_done", "numrej", "good_idx", "_ndtmpsum", - "lrate", "lrate_cap", "newtrate", "rholrate", + "lrate", "lrate_cap", "newtrate", "rholrate", "rholrate_cap", # ... the LLt stash and its materialized arrays (issue #157) ... "_llt_logv", "_llt_ll", "_llt_lht", "_llt_lt", # ... the tuned block size (do_opt_block re-times per restart) and the @@ -2486,7 +2573,9 @@ def _fit_once( ``mir_history_[-1]``. Not index-aligned with ``ll_history``: entry ``i`` is computed after iteration ``i``'s parameter update, while ``ll_history[i]`` is the likelihood of the parameters - before it, so the two are one update apart (issue #161). + before it, so the two are one update apart (issue #161). An + iteration that ends the fit on a stop takes no update, so it has + no waypoint either. Incompatible with PCA reduction, same as :meth:`mir` itself. This upfront gate only sees an explicit reduction request: ``pcakeep < n_channels`` or any ``pcadb`` while sphering (``pcakeep >= @@ -2504,6 +2593,37 @@ def _fit_once( Notes ----- + Iteration order (issue #339). Each iteration follows the reference's + main loop (amica15.f90:949-1142): the E-step computes the likelihood + of the current parameters, which is appended to ``ll_history``, and + the step for ``A`` with its norm; then the likelihood-decrease + response (``lrate`` halved, the working ``rholrate`` scaled, + ``maxdecs`` ratchets) and the stopping checks run; then, unless a check + fired, the parameters are updated with the rates just set. So after + ``fit``: + + * ``iteration`` is the 0-based index of the last iteration whose + E-step ran, and ``ll_history`` has ``iteration + 1`` entries (one + fewer after a ``nan_ll``/``singular_ll`` stop, whose likelihood is + not recorded). + * On a convergence stop (``stop_reason`` ``"min_dll"``, + ``"grad_norm"``, ``"grad_norm_floor"`` or ``"lrate_floor"``) the + stopping iteration takes no update, as in the reference, so the + returned parameters are the ones whose likelihood is + ``ll_history[-1]``, and ``final_ll_`` (without a keep-best restore) + is exactly their log-likelihood. ``iteration`` updates were applied. + * A ``"nan_direction"`` stop (a non-finite step or gradient norm, + caught before any check reads it or the update applies it) and a + ``"nan_params"`` stop (non-finite parameters right after an update) + both record their iteration's finite likelihood. Both are + degenerate (``_DEGENERATE_STOP_REASONS``), like ``"nan_ll"`` and + ``"singular_ll"``, so ``final_ll_`` is NaN and the model is refused + by every output path. + * At ``max_iter`` (``stop_reason == "max_iter"``) the last iteration + does take its update, as in the reference, so ``max_iter`` updates + were applied and ``final_ll_ == ll_history[-1]`` is the likelihood + of the parameters one update before the returned ones. + Under ``share_comps``, if a merge fires on the LAST iteration, the returned ``A``/``W``/``comp_list`` are already post-merge but ``final_ll_`` still reports the pre-merge log-likelihood -- the merge's @@ -2524,23 +2644,27 @@ def _fit_once( the same normalization ``ll_history`` uses. Three consequences worth stating plainly: - * Without a keep-best restore, ``final_ll_`` is ``ll_history[-1]``, the - LL of the parameters as they stood *before* the last M-step -- so the - exported ``LLt`` is one M-step older than the exported ``W``/``A``. - That is the reference's own behavior (Fortran fills ``modloglik`` - during iteration i's E-step, ``update_params`` then moves the - parameters, and ``write_output`` writes both -- amica15.f90:996, - 1122, 1124-1127), adopted deliberately so pamica's on-disk ``LLt`` is + * Without a keep-best restore, ``final_ll_`` is ``ll_history[-1]``. + After a fit that ran to ``max_iter``, that is the LL of the + parameters as they stood *before* the last M-step -- so the exported + ``LLt`` is one M-step older than the exported ``W``/``A``. That is + the reference's own behavior (Fortran fills ``modloglik`` during + iteration i's E-step, ``update_params`` then moves the parameters, + and ``write_output`` writes both -- amica15.f90:996, 1122, + 1124-1127), adopted deliberately so pamica's on-disk ``LLt`` is comparable with the binary's. It is verifiable on the committed reference output: ``sum(Lt)/(N*nw)`` there equals its ``LL[-1]`` - exactly, not the LL of the ``W`` written next to it. + exactly, not the LL of the ``W`` written next to it. After a + convergence stop no M-step follows the last E-step (the reference + exits first too, :1111), so ``LLt`` and ``final_ll_`` belong to the + exported ``W``/``A`` themselves. * With a keep-best restore (issue #51), the restore rolls the stash back with the parameters, so ``LLt`` is the restored iterate's own E-step -- the one that measured ``best_ll`` -- and not the discarded - last iterate's. Because ``_snapshot_params`` is taken *before* that - iteration's M-step, the restored parameters and the restored ``LLt`` - come from the same point in the loop, so there is no staleness at all - in this case. + last iterate's. Because ``_snapshot_params`` is taken right after + that E-step, before any check or update, the restored parameters and + the restored ``LLt`` come from the same point in the loop, so there + is no staleness at all in this case. * The one case where the equality above does NOT hold is a ``do_reject`` fit whose rejection fires on its own last executed iteration: ``ll`` was normalized over the good set as it stood @@ -2663,6 +2787,15 @@ def _fit_once( "earlier snapshot would undo the merge", ) + # One iteration follows the reference's main loop (amica15.f90:949-1142, + # issue #339): (1) the E-step, which yields LL(iter), the sufficient + # statistics and the step with its norm ndtmpsum; (2) the likelihood- + # decrease response and the stopping checks, which read those; (3) an + # exit, BEFORE any parameter moves, if a check fired; otherwise (4) the + # update, with the rates the response just set, then the iteration's + # remaining hooks and rejection. So a stop returns the parameters whose + # likelihood is ll_history[-1], and the step right after a decrease is + # already taken at the halved rate. iterator = tqdm(range(max_iter), desc="AMICA-NG", disable=not verbose) for it in iterator: self.iteration = it @@ -2671,13 +2804,12 @@ def _fit_once( n_use = X_use.shape[1] acc = self._accumulate_blocks(X_use, stash_llt=True) - # Log-likelihood of the CURRENT (pre-update) parameters: acc["ll"] is - # this iteration's E-step total, computed before _update_parameters - # moves the parameters. A singular W makes logdet -> -inf (not NaN), - # so guard on isfinite, not isnan alone: a -inf LL would otherwise - # sail past as a mere "decrease" and the run would "complete" - # (stop_reason=max_iter) on a degenerate model. Checking here, before - # the update, stops on the last finite parameters instead of + # Log-likelihood of the CURRENT parameters: acc["ll"] is this + # iteration's E-step total. A singular W makes logdet -> -inf (not + # NaN), so guard on isfinite, not isnan alone: a -inf LL would + # otherwise sail past as a mere "decrease" and the run would + # "complete" (stop_reason=max_iter) on a degenerate model. Checking + # here, before the update, stops on the last parameters instead of # overwriting them with a garbage update first. ll = (acc["ll"] / (n_use * self.n_channels)).item() if not math.isfinite(ll): @@ -2689,145 +2821,60 @@ def _fit_once( ) break + # The step and its norm, from the same E-step, before any check reads + # ndtmpsum (the reference computes both in + # accum_updates_and_likelihood, amica15.f90:1749-1761). + step = self._update_direction(acc) + # Best-iterate safeguard (issue #51): remember the parameters that # produced this LL when it is the best seen, so a later overshoot - # does not leave the returned model below this peak. + # does not leave the returned model below this peak. Nothing has + # moved them since the E-step, so the snapshot pairs them with ll. if track_best and ll > best_ll: best_ll = ll best_snapshot = self._snapshot_params() - # Whether rejection fires this iteration (Fortran schedule, - # amica15.f90:1136, with rejstart counted from 1 as the reference - # counts it; issue #335). Fortran rejects using the per-sample - # log-likelihood from THIS iteration's E-step, i.e. the PRE-update - # parameters (loglik is stored in get_updates_and_likelihood before - # update_params runs). Capture it here, before _update_parameters, - # to match that ordering. - will_reject = schedule.rejection_due( - self.do_reject, it, self.rejstart, self.rejint, self.numrej, self.maxrej - ) - if will_reject: - assert self.good_idx is not None - reject_ll = self._sample_ll(self.good_idx, X_t) - else: - reject_ll = None - - self._update_parameters(acc, n_use) - - # Extended-Infomax adaptive PDF switch (Fortran do_choose_pdfs). Runs - # on the kurt_start/num_kurt/kurt_int schedule using the just-updated - # W; the new per-source families take effect from the next E-step. - # kurt_start counts from 1, like every reference schedule. num_kurt=0 - # disables switching (the family stays at its pdftype=1 - # super-Gaussian init). - if self.do_choose_pdfs and self.n_kurt_done < self.num_kurt: - if schedule.periodic_due(it, self.kurt_start, self.kurt_int): - self._choose_pdfs(X_use) - self.n_kurt_done += 1 - - # Component sharing (Fortran identify_shared_comps schedule, - # amica15.f90:1856): once per share_iter cycle from share_start, - # merge near-collinear components across models using the - # just-updated A. Fortran runs identify_shared_comps BEFORE - # get_unmixing_matrices (amica15.f90:1858,1863), so rebuild W from the - # merged comp_list -- otherwise the next E-step would read a stale W - # (pre-merge comp_list) while indexing the densities by the merged - # comp_list. No-op when share_comps is off or n_models == 1. - # - # This runs AFTER ``ll`` (this iteration's LL) was captured above, - # so a merge on the final iteration lands in the returned - # A/W/comp_list but not in the ``ll_history``/``final_ll_`` value - # appended just below -- see final_ll_'s comment (issue #269). - if self.share_comps and schedule.periodic_due( - it, self.share_start, self.share_iter - ): - self._identify_shared_comps() - self._update_unmixing_matrices() - self.ll_history.append(ll) - # MIR waypoint (issue #137), following the NumPy backend's - # writestep/histstep idiom (numpy_impl/core.py). Computed from the - # CURRENT W/sphere (just rebuilt above by _update_parameters / - # the share_comps block) against the raw, un-preprocessed X. - # - # A failed waypoint must never kill the fit. `metrics.mir` raises on - # a near-singular unmixing, and a near-singular W mid-fit is a - # transient the natural gradient can pass through (the same - # condition is only a warning on the training path, see - # numpy_impl/core.py's logdet_W check). Letting that propagate would - # let a purely diagnostic flag destroy an otherwise-recoverable - # decomposition. Warn and record NaN instead: the gap stays visible - # in mir_history_ rather than being silently absent, so a plotted - # trajectory shows a hole exactly where the transient was. - # - # ValueError vs LinAlgError get different treatment (PR #318 - # review): a ValueError (PCA reduction, or metrics.mir's own - # near-singular-unmixing check) reflects the fit's GEOMETRY -- - # the sphere shape or the current unmixing's conditioning as a - # structural fact -- not a one-off numerical hiccup, so it will - # keep firing identically on every remaining scheduled waypoint - # of a long fit. Warn once, then stop scheduling waypoints for - # the rest of THIS fit (mir_history_ simply gets no more - # entries -- every one it would have gotten is the same NaN - # anyway, so nothing is lost). LinAlgError stays per-waypoint: - # it is the genuinely transient case the comment above already - # describes, which the natural gradient can pass through. - if mir_waypoints_disabled: - pass - elif mir_step > 0 and it % mir_step == 0: - try: - mir_nats, mir_var = self.mir(X) - except np.linalg.LinAlgError as exc: - logger.warning( - "MIR waypoint failed at iter %d (%s: %s); recording NaN " - "and continuing. The fit itself is unaffected.", - it, - type(exc).__name__, - exc, - ) - mir_nats = mir_var = float("nan") - self.mir_history_.append((it, mir_nats, mir_var)) - except ValueError as exc: - logger.warning( - "MIR waypoint failed at iter %d (%s: %s); this " - "condition will not resolve mid-fit, so MIR " - "waypoints are now disabled for the rest of this " - "fit (mir_history_ gets no further entries). The " - "fit itself is unaffected.", - it, - type(exc).__name__, - exc, - ) - self.mir_history_.append((it, float("nan"), float("nan"))) - mir_waypoints_disabled = True - else: - self.mir_history_.append((it, mir_nats, mir_var)) + # A non-finite step or norm would pass both gradient-norm checks + # below (NaN <= min_nd is False) and then be applied, so stop on it + # here, before any check reads it and before the update, with the + # parameters whose (finite) likelihood was just recorded. + if not ( + self._ndtmpsum is not None + and math.isfinite(self._ndtmpsum) + and bool(torch.isfinite(step.dAk).all()) + ): + self.stop_reason = "nan_direction" + logger.warning( + "Non-finite update direction (ndtmpsum %s) at iteration %d; " + "stopping before the update.", + self._ndtmpsum, + it, + ) + break - # Learning-rate control, ported from Fortran (amica17.f90:1062-1108). + # Learning-rate control, ported from Fortran (amica15.f90:1051-1097). # Natural-gradient/Newton ascent is not monotonic at a fixed rate: - # when the log-likelihood decreases, anneal the working lrate. If - # decreases persist for maxdecs iterations, ratchet the *ceilings* - # down (lrate_cap; newtrate once Newton is running; and the rho rate) - # so the per-iteration ramp can no longer re-inflate lrate back to the - # overshooting value -- without this the ramp and a one-shot halving - # just oscillate and the LL drifts down. + # when the log-likelihood decreases, anneal the working lrate (and the + # working rho rate). If decreases persist for maxdecs iterations, + # ratchet the *ceilings* down (lrate_cap; rholrate_cap and, under + # Newton, newtrate once past newt_start) so the per-iteration ramp can + # no longer re-inflate lrate back to the overshooting value -- without + # this the ramp and a one-shot halving just oscillate and the LL + # drifts down. All of it runs before this iteration's update, as in + # the reference, so the update below already uses the new rates. # - # rholrate is a CEILING here, not a per-decrease-annealed working - # rate. Fortran resets rholrate=rholrate0 each iteration before the - # rho update (amica15.f90:1806/1813) and only tightens the rholrate0 - # ceiling at maxdecs (amica15.f90:1068, gated on iter>newt_start), so - # its per-decrease rholrate*=rholratefact (:1045) is always overwritten - # by the reset and never reaches the rho update. rho has no ramp, so - # self.rholrate carries that ceiling directly (reset to rholrate0 each - # fit, nothing re-inflates it) and must ratchet ONLY at maxdecs. The - # previous per-decrease self.rholrate*=rholratefact was a monotone - # decay with no reset that collapsed the rho rate to ~1e-5 within a few - # hundred iterations and froze rho at a stale shape (issue #193). + # The working rho rate is scaled on every decrease, as the + # reference's is (:1063), but every update of A resets it to its + # ceiling before rho moves (_update_parameters, amica15.f90:1806/ + # 1813), so the scaling only reaches rho on an iteration on which A + # is held. It is not a monotone decay (the issue #193 collapse): + # nothing but the maxdecs ratchet lowers the ceiling. # have_prev mirrors Fortran's outer ``if (iter > 1)`` (amica15.f90:1051), # which wraps the decrease branch AND the two stops below: none of # the three checks can fire on the first iteration (no LL(iter-1) - # yet) or before ll_history has two entries after a restart. + # yet). # # PRECEDENCE NOTE (PR #213 review, issue #207): the three blocks # below (decrease branch; min_dll; grad_norm) are independent -- @@ -2879,11 +2926,12 @@ def _fit_once( leave = True else: self.lrate *= self.lratefact + self.rholrate *= self.rholratefact numdecs += 1 if numdecs >= self.maxdecs: self.lrate_cap *= self.lratefact if schedule.past_newton_start(it, self.newt_start): - self.rholrate *= self.rholratefact + self.rholrate_cap *= self.rholratefact if self.do_newton and schedule.past_newton_start( it, self.newt_start ): @@ -2936,11 +2984,149 @@ def _fit_once( if schedule.newton_switches_on(self.do_newton, it, self.newt_start): numdecs = 0 + # Stop before this iteration's update, as the reference does + # (``if (leave) exit``, amica15.f90:1111, ahead of update_params at + # :1122): the returned parameters are the ones whose LL was just + # recorded, so final_ll_ describes them exactly. if leave: break + # Whether rejection fires this iteration (Fortran schedule, + # amica15.f90:1136, with rejstart counted from 1 as the reference + # counts it; issue #335). Fortran rejects using the per-sample + # log-likelihood from THIS iteration's E-step, i.e. the PRE-update + # parameters (loglik is stored in get_updates_and_likelihood before + # update_params runs). Capture it here, before _update_parameters, + # to match that ordering. + will_reject = schedule.rejection_due( + self.do_reject, it, self.rejstart, self.rejint, self.numrej, self.maxrej + ) + if will_reject: + assert self.good_idx is not None + reject_ll = self._sample_ll(self.good_idx, X_t) + else: + reject_ll = None + + self._update_parameters(acc, n_use, step) + + # Surface a corrupted update (a collapsed mixture component, a + # singular inverse) on the iteration it happens, with AMICAMLXNG's + # check and message. The likelihood check above only sees a + # corruption through the NEXT iteration's E-step, so one on the last + # iteration would otherwise end as stop_reason="max_iter" with + # non-finite parameters. One sync: the per-parameter flags are + # stacked and reduced together, and the names are only read out once + # that reduction has failed. + post_update = [getattr(self, name) for name in self._POST_UPDATE_CHECKED] + if not bool( + torch.stack( + [torch.isfinite(value).all() for value in post_update] + ).all() + ): + bad = [ + name + for name, value in zip(self._POST_UPDATE_CHECKED, post_update) + if not bool(torch.isfinite(value).all()) + ] + logger.warning( + "Non-finite %s at iter %d (a mixture component likely " + "collapsed); stopping.", + ", ".join(bad), + it, + ) + self.stop_reason = "nan_params" + break + + # Extended-Infomax adaptive PDF switch (Fortran do_choose_pdfs). Runs + # on the kurt_start/num_kurt/kurt_int schedule using the just-updated + # W; the new per-source families take effect from the next E-step. + # kurt_start counts from 1, like every reference schedule. num_kurt=0 + # disables switching (the family stays at its pdftype=1 + # super-Gaussian init). + if self.do_choose_pdfs and self.n_kurt_done < self.num_kurt: + if schedule.periodic_due(it, self.kurt_start, self.kurt_int): + self._choose_pdfs(X_use) + self.n_kurt_done += 1 + + # Component sharing (Fortran identify_shared_comps schedule, + # amica15.f90:1856): once per share_iter cycle from share_start, + # merge near-collinear components across models using the + # just-updated A. Fortran runs identify_shared_comps BEFORE + # get_unmixing_matrices (amica15.f90:1858,1863), so rebuild W from the + # merged comp_list -- otherwise the next E-step would read a stale W + # (pre-merge comp_list) while indexing the densities by the merged + # comp_list. No-op when share_comps is off or n_models == 1. + # + # This runs AFTER ``ll`` (this iteration's LL) was recorded above, + # so a merge on the final iteration lands in the returned + # A/W/comp_list but not in ``ll_history``/``final_ll_`` -- see + # final_ll_'s comment (issue #269). + if self.share_comps and schedule.periodic_due( + it, self.share_start, self.share_iter + ): + self._identify_shared_comps() + self._update_unmixing_matrices() + + # MIR waypoint (issue #137), following the NumPy backend's + # writestep/histstep idiom (numpy_impl/core.py). Computed from the + # CURRENT W/sphere (just rebuilt above by _update_parameters / + # the share_comps block) against the raw, un-preprocessed X. + # + # A failed waypoint must never kill the fit. `metrics.mir` raises on + # a near-singular unmixing, and a near-singular W mid-fit is a + # transient the natural gradient can pass through (the same + # condition is only a warning on the training path, see + # numpy_impl/core.py's logdet_W check). Letting that propagate would + # let a purely diagnostic flag destroy an otherwise-recoverable + # decomposition. Warn and record NaN instead: the gap stays visible + # in mir_history_ rather than being silently absent, so a plotted + # trajectory shows a hole exactly where the transient was. + # + # ValueError vs LinAlgError get different treatment (PR #318 + # review): a ValueError (PCA reduction, or metrics.mir's own + # near-singular-unmixing check) reflects the fit's GEOMETRY -- + # the sphere shape or the current unmixing's conditioning as a + # structural fact -- not a one-off numerical hiccup, so it will + # keep firing identically on every remaining scheduled waypoint + # of a long fit. Warn once, then stop scheduling waypoints for + # the rest of THIS fit (mir_history_ simply gets no more + # entries -- every one it would have gotten is the same NaN + # anyway, so nothing is lost). LinAlgError stays per-waypoint: + # it is the genuinely transient case the comment above already + # describes, which the natural gradient can pass through. + if mir_waypoints_disabled: + pass + elif mir_step > 0 and it % mir_step == 0: + try: + mir_nats, mir_var = self.mir(X) + except np.linalg.LinAlgError as exc: + logger.warning( + "MIR waypoint failed at iter %d (%s: %s); recording NaN " + "and continuing. The fit itself is unaffected.", + it, + type(exc).__name__, + exc, + ) + mir_nats = mir_var = float("nan") + self.mir_history_.append((it, mir_nats, mir_var)) + except ValueError as exc: + logger.warning( + "MIR waypoint failed at iter %d (%s: %s); this " + "condition will not resolve mid-fit, so MIR " + "waypoints are now disabled for the rest of this " + "fit (mir_history_ gets no further entries). The " + "fit itself is unaffected.", + it, + type(exc).__name__, + exc, + ) + self.mir_history_.append((it, float("nan"), float("nan"))) + mir_waypoints_disabled = True + else: + self.mir_history_.append((it, mir_nats, mir_var)) + # Outlier rejection, after the parameter update (Fortran order, - # amica17.f90:1141-1146) but using the pre-update per-sample LL + # amica15.f90:1136-1140) but using the pre-update per-sample LL # captured above. if will_reject: assert reject_ll is not None @@ -3725,9 +3911,10 @@ def write_amica_output(self, outdir) -> None: Also writes ``LLt`` (the per-sample/per-model log-likelihood, issue #155) for a model that was just ``fit()`` in this process, from the stash the training E-step filled (issue #157) -- so, exactly as in the - reference, ``LLt`` is the E-step of the returned iterate and is one - M-step older than the ``W``/``A`` written beside it (see ``fit``'s - Notes). A model restored via :meth:`from_state_dict` carries no stash, + reference, ``LLt`` is the E-step of the returned iterate: one M-step + older than the ``W``/``A`` written beside it after a fit that ran to + ``max_iter``, their own after a convergence stop (see + :meth:`_fit_once`'s Notes). A model restored via :meth:`from_state_dict` carries no stash, so ``LLt`` is omitted for it (a warning is logged) -- the rest of the output is unaffected. @@ -3752,7 +3939,7 @@ def write_amica_output(self, outdir) -> None: if self.stop_reason in self._DEGENERATE_STOP_REASONS: raise RuntimeError( f"Refusing to write output for a degenerate model (stop_reason=" - f"{self.stop_reason!r}): fit() hit a non-finite log-likelihood at " + f"{self.stop_reason!r}): fit() hit a non-finite value at " f"iteration {self.iteration}. Fix the instability (lower lrate, " f"disable Newton, or check data conditioning) before writing." ) @@ -3844,12 +4031,23 @@ def _np(t): # versions load as. _STATE_FORMAT_VERSION = 4 - # Stop reasons that mark a fit as degenerate (non-finite log-likelihood, or - # -- only reachable under best-of-N restarts, issue #198 -- a fit that raised - # before it could finish). Such a model yields NaN sources, so state_dict() - # refuses to persist it rather than let it round-trip silently - # (silent-failure review, PR #44). - _DEGENERATE_STOP_REASONS = ("nan_ll", "singular_ll", restarts.ERROR_STOP_REASON) + # Stop reasons that mark a fit as degenerate: a non-finite log-likelihood + # ("nan_ll"/"singular_ll"), a non-finite update direction ("nan_direction"), + # non-finite parameters after an update ("nan_params"), or -- only reachable + # under best-of-N restarts, issue #198 -- a fit that raised before it could + # finish. Such a model yields NaN sources or was never fitted to the end, so + # state_dict() refuses to persist it rather than let it round-trip silently + # (silent-failure review, PR #44). The same set as AMICAMLXNG's. + _DEGENERATE_STOP_REASONS = ( + "nan_ll", + "singular_ll", + "nan_direction", + "nan_params", + restarts.ERROR_STOP_REASON, + ) + # What fit()'s post-update guard checks, in the order AMICAMLXNG's guard + # names them (it adds its cached log-determinant of W). + _POST_UPDATE_CHECKED = ("A", "mu", "alpha", "beta", "rho", "gm", "c", "W") def state_dict(self) -> dict: """Serialize the fitted model to a plain, device-agnostic dict. @@ -3873,7 +4071,7 @@ def state_dict(self) -> dict: if self.stop_reason in self._DEGENERATE_STOP_REASONS: raise RuntimeError( f"Refusing to serialize a degenerate model (stop_reason=" - f"{self.stop_reason!r}): fit() hit a non-finite log-likelihood at " + f"{self.stop_reason!r}): fit() hit a non-finite value at " f"iteration {self.iteration}. Fix the instability (lower lrate, " f"disable Newton, or check data conditioning) before saving." ) @@ -3996,6 +4194,7 @@ def state_dict(self) -> dict: "lrate_cap": float(self.lrate_cap), "newtrate": float(self.newtrate), "rholrate": float(self.rholrate), + "rholrate_cap": float(self.rholrate_cap), # Per-restart records (issue #198): which seeds ran, what each # returned, and why each stopped. Persisted so a reloaded best-of-N # model can still say how its parameters were chosen instead of @@ -4124,10 +4323,10 @@ def _load_params(self, state: dict) -> None: self.numrej = extra["numrej"] good_idx = extra["good_idx"] self.good_idx = None if good_idx is None else good_idx.to(self.device) - self.lrate = extra["lrate"] - self.lrate_cap = extra["lrate_cap"] - self.newtrate = extra["newtrate"] - self.rholrate = extra["rholrate"] + # The rates, validated first (a missing or non-finite one is a named + # ValueError); rholrate_cap is additive, see _saved_rates. + for name, value in _saved_rates(extra, "AMICATorchNG").items(): + setattr(self, name, value) # Additive-only, like the issue #207 config keys: a payload written # before issue #198 simply has no restart records, and an empty search # is the honest description of a single-fit model saved back then. From 0138a717422cb44936f5e771bb354541e2406a5d Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Wed, 23 Sep 2026 11:10:21 -0700 Subject: [PATCH 15/26] Phase 13: use the reference's single-precision constants (#349) * Use the reference's single-precision constants The reference writes its density normalizers as default-kind literals widened with dble, so the binary uses their float32 roundings. All three backends now read those values, and epsdble's, from one shared module, pamica/reference_constants.py; MLX holds the rho == 2 normalizer in its lgamma table. The literal-formula tests and the benchmark's copy now use the reference's values. Tested: pdf-family and backend test files pass. * Compare pre-change fits at the old constants The byte-identity tests against code from before issue #344 (component rows, doscaling off, the MLX pre-Phase-3 tip) now give the live backend its old density constants through pre_change.use_pre_344_constants, so they still isolate the change each was written for: their two-model and non-GG fits reach the changed normalizers. Tested: the three files pass. * Pin the constants against the reference test_reference_constants.py checks each shared constant against the float32 rounding of its literal on the cited reference line (exact rational arithmetic), pins the sweep of amica15.f90 and its header, checks that no backend keeps a copy, checks every backend's rho == 2 log-density, and (opt-in) seeds the native binary for pdftype 2, 4 and 1. Tested: 23 passed with AMICA_RUN_FORTRAN=1. * Run the merged oracle at rho == 2 The merged-state oracle no longer holds maxrho at 1.99: its warm seed now has mixtures clamped at 2, it asserts that, and the code before issue #344 is replayed from the same state as a control (off by 2.8e-9 in log-likelihood after 1 iteration, against 2.2e-15 now). Tolerances unchanged. Tested with AMICA_RUN_FORTRAN=1. * Document the single-precision constants Changelog entry with the oracle numbers; the differences guide gains a section on the constants, and row 16 now records the one kind of single-precision literal pamica keeps at its decimal value (defaults of input.param keys); the validation guide notes the new family oracle. Tested: mkdocs build --strict, typos. * Note the reference's unset lrate0 in row 16 Without an lrate key the reference never sets its ceiling lrate0, so its lrate drops to 0 after the first iteration (checked against the pinned binary). Row 16 now says so and uses comp_thresh as the example. Tested: mkdocs build --strict, typos. * Skip MLX before patching its constants On a host without MLX the byte-identity tests imported the MLX module to patch it before the check that skips them. Resolve the classes first. Tested: the two files pass with MLX and skip its cases without. * Remove the unused compute_log_pdf numpy_impl.pdf.compute_log_pdf had no callers in the package, tests or docs. Tested: the pdf and viz tests pass. * Plot the fit's density in compute_pdf compute_pdf, which viz.plot_pdf_fits draws, now takes its normalizers from pamica.reference_constants, including the reference's single-precision sqrt(pi) at rho == 2, so it draws the density the fit uses. Its docstring says it is a plotting helper with the legacy pdftype numbering. Tested: the pdf, viz and constants tests pass. * Scan every backend module for copies The no-copy test now scans every .py file of torch_impl, numpy_impl and mlx_impl with comments and strings blanked, and flags the linear forms (sqrt(pi), sqrt(2*pi), ...) as well as the log forms and the literals. A new test checks that it flags every definition this change removed. Tested: passes; fails on the old pdf.py sqrt(pi) line (checked by hand). * Assert the family held in the oracle The family oracle now asserts that every source kept its family and no kurtosis switch ran (n_kurt_done == 0), for pdftype=1 with num_kurt=0 too, in the live and the pre-change runs. Tested with AMICA_RUN_FORTRAN=1. * Check the pre-#344 constant substitution A control test: every constant use_pre_344_constants patches differs from the live value, the patch takes effect, it covers every changed constant each backend module reads, and on MLX it restores the pre-change rho == 2 table entry. Tested: passes on all three backends. * Note the plotting helper in the changelog compute_pdf now draws the fit's density and compute_log_pdf is gone. Tested: mkdocs build --strict, typos. --- benchmarks/reproduce_table1.py | 8 +- docs/changelog.md | 30 + docs/guides/amica-differences.md | 46 +- docs/guides/validation.md | 6 + pamica/mlx_impl/core.py | 61 +- pamica/numpy_impl/core.py | 21 +- pamica/numpy_impl/pdf.py | 75 +-- pamica/reference_constants.py | 71 ++ pamica/tests/mlx_tests/test_mlx_fit_noop.py | 18 +- pamica/tests/pre_change.py | 40 ++ pamica/tests/test_amica.py | 6 +- pamica/tests/test_component_rows.py | 78 ++- pamica/tests/test_doscaling_rows.py | 11 +- .../test_mlx_pdf_families_cross_backend.py | 10 +- pamica/tests/test_pamica.py | 6 +- pamica/tests/test_reference_constants.py | 607 ++++++++++++++++++ pamica/tests/torch_tests/test_ng_backend.py | 4 +- .../tests/torch_tests/test_ng_pdf_families.py | 11 +- pamica/torch_impl/core.py | 63 +- 19 files changed, 1005 insertions(+), 167 deletions(-) create mode 100644 pamica/reference_constants.py create mode 100644 pamica/tests/test_reference_constants.py diff --git a/benchmarks/reproduce_table1.py b/benchmarks/reproduce_table1.py index 639a81a8..9d1ffa83 100644 --- a/benchmarks/reproduce_table1.py +++ b/benchmarks/reproduce_table1.py @@ -595,10 +595,12 @@ def amari_pair(a: np.ndarray, b: np.ndarray) -> float: # (matches test_ng_backend.py::test_sufficient_stats_match_numpy_reference). # --------------------------------------------------------------------------- +# The reference's log-normalizers: each default-kind literal is rounded to +# single precision before ``dble`` widens it (issue #344); 4.0 is exact. _LOG4 = math.log(4.0) -_LSQ2PI = math.log(2.506628274) -_LNSUB = math.log(4.132731354) -_LNSUP = math.log(1.858073988) +_LSQ2PI = math.log(float(np.float32(2.506628274))) +_LNSUB = math.log(float(np.float32(4.132731354))) +_LNSUP = math.log(float(np.float32(1.858073988))) def _fortran_z0(y: np.ndarray, code: int) -> np.ndarray: diff --git a/docs/changelog.md b/docs/changelog.md index bcd76fd5..44027186 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,36 @@ Release notes are also published on the ## Unreleased +- **Phase 13 of epic #324: every backend uses the reference's single-precision constants (issue #344).** + The reference writes several density normalizers as default-kind Fortran literals widened with `dble`, for example `log(dble(2.506628274))`, + so the binary uses the float32 rounding of each decimal, not the decimal. + pamica used the decimals' double values. + The PyTorch, NumPy and MLX backends now take the reference's values from one module, `pamica/reference_constants.py` + ([the differences guide](guides/amica-differences.md#single-precision-constants-issue-344) has the table). + - **Behavior change: fits in which a mixture reaches `rho == 2` move slightly, toward the reference.** + The default `maxrho = 2` clamps mixtures there, where the reference's normalizer, `log(dble(1.772453851))`, is 3.0e-8 above the `0.5 * log(pi)` pamica used, + so default fits of the generalized Gaussian take this branch once a mixture reaches the clamp + (on 4096 samples of the bundled recording, a two-model fit gets there on its fifth iteration). + Seeded with a warm two-model state that has mixtures at `rho == 2`, the PyTorch and NumPy updates now match the native binary to float64 round-off: + after three iterations the log-likelihood differs by 1.6e-13, `A` by 3.8e-13 and `mu` by 5.7e-9, + where the previous code was off by 1.2e-8, 3.4e-8 and 8.4e-3. + - **Behavior change: the Gaussian (`pdftype` 2) and the sub- and super-Gaussian cosh families (`pdftype` 4 and 1) report a different log-likelihood**, + lower by 3.7e-10 and 2.0e-8 and higher by 2.1e-8, which now matches the binary's to 2.7e-15. + Their parameter updates move only by round-off, since a family's normalizer shifts every mixture alike. + - The underflow guard of the rho update, `epsdble`, is likewise the reference's `1.0e-16` in single precision (1.0000000168623835e-16). + - The NumPy plotting helper `pamica.numpy_impl.pdf.compute_pdf`, which `viz.plot_pdf_fits` draws, takes its normalizers from the same module, + so it draws the density the fit uses; its unused companion `compute_log_pdf` is removed. + - Row 16 of the differences page now records the one kind of single-precision literal pamica keeps at its decimal value: + the compiled-in defaults of `input.param` keys, which the binary uses only when the key is missing + (then its `comp_thresh` is 0.9900000095); a value given in `input.param` is read as double, and pamica's native engine gives every one. + - Tests: `pamica/tests/test_reference_constants.py` pins every constant against the float32 rounding of its literal on the cited reference line, + computed by exact rational arithmetic; pins the sweep of both reference sources; checks that no backend keeps its own copy; + and (opt-in, `AMICA_RUN_FORTRAN=1`) seeds the native binary for `pdftype` 2, 4 and 1. + The merged-state oracle in `pamica/tests/test_component_rows.py` no longer holds `maxrho` at 1.99. + The tests that compare a live backend bit for bit with code from before this change + (`test_component_rows.py`, `test_doscaling_rows.py` and `mlx_tests/test_mlx_fit_noop.py`) + give the live backend its old constants first, so they still isolate the change they were written for. + - **Phase 11 of epic #324: every backend follows the reference's iteration order (issues #339 and #345).** Each iteration of every backend (PyTorch, NumPy and MLX) now runs in the reference's order (amica15.f90:949-1142, [ADR 0008](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0008-iteration-order.md)): diff --git a/docs/guides/amica-differences.md b/docs/guides/amica-differences.md index 9521f704..b5187980 100644 --- a/docs/guides/amica-differences.md +++ b/docs/guides/amica-differences.md @@ -27,7 +27,7 @@ that is not listed, that is a bug worth | 13 | Preprocessing with `do_sphere=False` | divides each channel by its standard deviation and adds a log-determinant term to the likelihood (amica15.f90:516-526) | identity sphere with a zero log-determinant, on all three array backends: the data are fitted unscaled | not a deliberate choice: an existing divergence, found during epic #324 and not yet ported | none yet (issue #328) | | 14 | `scalestep` | parsed (amica15.f90:3686), never used; rescales every iteration (:1843) | rescales every `scalestep` iterations counted from 1; default 1 matches the reference | pamica has always honored the keyword, and keeping it costs nothing; since issue #333 it counts from 1 like the reference's live cadences (`writestep`, `histstep`) instead of firing on the first iteration | `scalestep=1`, the default | | 15 | Restart after a non-finite likelihood (NumPy backend only) | within the first `restartiter` iterations, redraws `A` up to `maxrestarts + 1` times (`numrestarts > maxrestarts` ends the run, amica15.f90:1022-1049); `startover` is set at :1046 and never cleared, so after the first restart the binary never calls `update_params` again (:1115-1122) and the redrawn parameters are never fitted | redraws `A` up to `maxrestarts` times within the same window (counted from 1; `restartiter=0` disables it) and resumes fitting after each restart, with the learning rates and likelihood history reset; the restart uses up that iteration of `max_iter` | the reference's recovery path cannot resume, so its count has nothing to recover into; a restart that fits is the evident intent. PyTorch and MLX have no restart-on-NaN path at all: they stop on the non-finite likelihood (`stop_reason="nan_ll"`), and their `n_restarts` (row 10) is a different mechanism | none for the stalled loop; `maxrestarts` one higher reproduces the reference's count, and `restartiter=0` stops on the first non-finite likelihood, as the reference does with `restartiter=0` | -| 16 | Single-precision density normalizers | `log(dble(1.772453851))` in the exact-Gaussian branch of the generalized Gaussian (`rho == 2`, amica15.f90:1313) is a single-precision literal widened to double, 3.0e-8 above `log(sqrt(pi))`; the Gaussian and cosh families (`pdftype` 2, 4 and 1, :1333, :1359, :1371) use literals of the same kind | `0.5 * log(pi)` for `rho == 2`; the double-precision values of the decimal literals for the other families, which differ from the reference's in the log by 3.7e-10, 2.0e-8 and -2.1e-8 | not a deliberate choice: found during epic #324 Phase 8, while seeding the reference from warm states in which mixtures sit at `maxrho = 2`; each such mixture's log-density differs by 3.0e-8, weighted by its responsibility (2.8e-9 in the log-likelihood of one warm two-model state of the sample). The other families' offsets are computed from the literals, not yet measured against the binary | none yet (issue #344 decides whether to adopt the reference's values); the seeded oracles keep `maxrho` below 2 | +| 16 | Defaults of `input.param` keys | the compiled-in defaults are single-precision literals widened to double (amica15_header.f90:66-74), so a key missing from `input.param` takes the float32 rounding of its default: `comp_thresh` is 0.9900000095, and likewise `mineig`, `minlrate`, `rholrate`, `rholratefact`, `invsigmin`, `min_dll`, `min_grad_norm` and `lrate` (0.1000000015, printed by the binary; without the key its ceiling `lrate0` is never set, and the binary's `lrate` drops to 0 after the first iteration); a value given in `input.param` is read as double | the decimal values (`lrate=0.1`), given or defaulted | they are user inputs, and the reference reads any value it is given as double; pamica's native engine and the seeded oracles write every one of these keys, so they never meet the compiled-in values. The constants the reference hard-codes the same way are its values in every backend since issue #344 ([below](#single-precision-constants-issue-344)) | pass the float32 rounding, for example `lrate=float(numpy.float32(0.1))` | Rows 1, 2 and 7 arrived with [ADR 0004](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0004-rank-deficient-input-handling.md); row 3 with ADR 0003; row 5 with issue #50, extended to the raw backends by issue #306 and to non-finite steps and updates by issue #339; row 8 with issues #60, #240 and #334; @@ -35,7 +35,7 @@ row 9 with issue #232; row 10 with issue #198; row 11 with issue #322 (ADR 0005) row 12 with issue #323; row 13 is recorded, not yet resolved, by issue #328; row 14 with issue #333 (ADR 0006); row 15 with issue #335, which also made the NumPy restart window count from 1; -row 16 is recorded, not yet resolved, by issue #344. +row 16 with issue #344, which adopted the reference's hard-coded single-precision constants. The A-freeze is the reference's arithmetic, applied as it is (issue #345): once `iter >= share_start`, every iteration with `mod(iter, share_iter) <= 5` holds the `A` update, @@ -847,6 +847,48 @@ which also covers the reference's own `load_*` warm-start keywords. Construct a new `AMICA_NumPy` instance for each fit, as the NumPy command-line interface and the validation harness do, or pass `restart_seeds=[seed]`. +## Single-precision constants (issue #344) + +This section records a parity fix, not a difference. +The reference writes several constants as default-kind Fortran literals, which are single precision, widened to double with `dble`. +The compiler rounds the decimal to float32, so the binary uses that rounding, not the decimal: +`log(dble(2.506628274))` is the log of 2.5066282749176025. +pamica used the decimals' double values until issue #344. +Every backend now uses the reference's values, defined once in `pamica/reference_constants.py`: + +| Where | Reference | Value in the binary | Against the value used before | +|---|---|---|---| +| Generalized Gaussian at `rho == 2` | `log(dble(1.772453851))`, amica15.f90:1313 | log of 1.7724539041519165 | 3.0e-8 above `0.5 * log(pi)` | +| Gaussian, `pdftype` 2 | `log(dble(2.506628274))`, amica15.f90:1333 | log of 2.5066282749176025 | 3.7e-10 above | +| Sub-Gaussian cosh, `pdftype` 4 | `log(dble(4.132731354))`, amica15.f90:1359 | log of 4.1327314376831055 | 2.0e-8 above | +| Super-Gaussian cosh, `pdftype` 1 | `log(dble(1.858073988))`, amica15.f90:1371 | log of 1.8580739498138428 | 2.1e-8 below | +| Guard of the rho update, `epsdble` | `1.0e-16`, amica15_header.f90:73, used at amica15.f90:1558 | 1.0000000168623835e-16 | 1.7e-24 above | + +The other normalizers, `log(dble(2.0))` and the logistic family's `log(dble(4.0))`, are exact in single precision. +A log-normalizer that is larger by some amount lowers the log-density of its mixtures by that amount. +The default `maxrho = 2` clamps mixtures at exactly 2, so default fits take the first row, +and their log-likelihood and updates move with the responsibility of those mixtures. +Seeded with a warm two-model state that has mixtures at `rho == 2`, +the PyTorch and NumPy updates now match the native binary to float64 round-off +(log-likelihood 2.2e-15 after one iteration and 1.6e-13 after three), +where the previous code was off by 2.8e-9 and 1.2e-8 in log-likelihood and by up to 8.4e-3 in `mu` after three iterations, +in a low-mass mixture whose update amplifies any difference (`pamica/tests/test_component_rows.py`). +Seeded with pamica's initialization for `pdftype` 2, 4 and 1, +the PyTorch log-likelihood matches the binary's to 2.7e-15, +where it was off by exactly each literal's rounding (`pamica/tests/test_reference_constants.py`). +The MLX backend casts the same values to float32; +at `rho == 2` its host-side `lgamma` table holds the reference's normalizer. + +Three kinds of single-precision literal keep their decimal values in pamica: + +- the defaults of `input.param` keys (row 16 of the table at the top); +- the scales of the random initialization, `0.05`, `0.1` and `0.01` (amica15.f90:756, 771, 814 and 1035): + pamica draws its initialization from its own random generator, so the reference's cannot be reproduced either way, + and the parity tests seed the binary with pamica's initialization; +- the starting values of the rejection pass's running maximum and minimum log-likelihood (amica15.f90:2210-2211), which pamica does not need. + +`pamica/tests/test_reference_constants.py` pins the sweep of both reference sources that found these. + ## Unmapped Fortran keywords This page's own contract ("anything not on this page is intended to match the diff --git a/docs/guides/validation.md b/docs/guides/validation.md index dabbfccb..ecbc627d 100644 --- a/docs/guides/validation.md +++ b/docs/guides/validation.md @@ -140,6 +140,12 @@ AMICA models each source with one of the reference's five `pdftype` density fami For every family other than the default generalized Gaussian, the vectorized log-density and score reproduce the literal `amica15.f90` expressions to float64 precision (test bound $<10^{-12}$, observed $\sim\!10^{-15}$): the source model is not an approximation of the Fortran one, it is the same function. +That includes the normalizing constants at the precision the binary uses them: +the reference writes them as single-precision literals widened to double, and every backend has used those values since issue #344 +([the differences guide](amica-differences.md#single-precision-constants-issue-344)). +Seeded with pamica's initialization, the native binary's log-likelihood for families 2, 4 and 1 matches PyTorch's to $3\times10^{-15}$ after one and three iterations, +where the decimals' double values had been off by $3.7\times10^{-10}$, $2.0\times10^{-8}$ and $-2.1\times10^{-8}$ +(`pamica/tests/test_reference_constants.py`, opt-in with `AMICA_RUN_FORTRAN=1`). The generalized Gaussian has no closed-form literal to compare against, since its score depends on the adaptive shape $\rho$, so the default family is validated by the single-model parity above instead. The oracle column below records which check applies to each family: diff --git a/pamica/mlx_impl/core.py b/pamica/mlx_impl/core.py index c8ded8b5..42029c47 100644 --- a/pamica/mlx_impl/core.py +++ b/pamica/mlx_impl/core.py @@ -122,6 +122,19 @@ validate_pca_reduction, ) +# The density normalizers and the rho-update guard, at the values the binary +# uses: its single-precision literals widened to double (issue #344), cast to +# float32 where they meet the float32 arrays. +from ..reference_constants import ( + EPSDBLE, + LOG2, + LOG4, + LOG_NORM_COSH_SUB, + LOG_NORM_COSH_SUP, + LOG_SQRT_2PI, + LOG_SQRT_PI, +) + logger = logging.getLogger(__name__) # Human-readable names for the ``pdftype``/``pdtype`` source-density family @@ -137,20 +150,6 @@ 4: "sub_gaussian_cosh", } -_LOG2 = math.log(2.0) -_LOG4 = math.log(4.0) # logistic-family normalizer (amica15.f90:1346) -# Log-normalizers for the non-GG density families, ported verbatim from the -# same-named ``pamica.torch_impl.core`` constants (policy 1): the double-precision -# values of the reference's decimal literals (amica15.f90:1333/1359/1371), which -# the binary reads in single precision, so its log-normalizers differ from these -# by 3.7e-10, 2.0e-8 and -2.1e-8 (docs/guides/amica-differences.md row 16; -# whether to adopt the single-precision values is issue #344). -_LOG_SQRT_2PI = math.log(2.506628274) -_LOG_NORM_COSH_SUB = math.log(4.132731354) -_LOG_NORM_COSH_SUP = math.log(1.858073988) -# Fortran epsdble: zero the rho*ln|y| term when |y|^rho underflows below this -# (amica17.f90:1570), matching AMICATorchNG. -_EPSDBLE = 1e-16 # MLX linalg runs on the CPU stream only (float32-accurate); the GPU stream # raises "not yet supported on the GPU" for inv/slogdet/eigh/solve. _CPU = mx.cpu @@ -224,7 +223,7 @@ def _logcosh(x: mx.array) -> mx.array: precision (~1e-4 absolute; measured 2.9e-5 at ``x=1000``) out to at least 1e3 (policy 3; no ``mlx.nn`` import).""" ax = mx.abs(x) - return ax - _LOG2 + mx.log1p(mx.exp(-2.0 * ax)) + return ax - LOG2 + mx.log1p(mx.exp(-2.0 * ax)) def _score(y: mx.array, rho: mx.array, pdtype: Optional[mx.array] = None) -> mx.array: @@ -275,8 +274,10 @@ def _log_pdf( ``pdtype is None`` (the ``pdftype=0`` fast path): byte-for-byte the pre-#265 ``_log_pdf_gg`` body -- ``lgamma_table = lgamma(1+1/rho)`` (precomputed host-side; MLX has no ``lgamma``) makes the uniform GG form - reduce to the exact Laplace (rho=1) and Gaussian (rho=2) log-densities, and - ``az_rho`` is returned for the rho-update accumulator (policy 2). + reduce to the Laplace (rho=1) log-density, and to the reference's Gaussian + (rho=2) one, whose single-precision normalizer + :meth:`AMICAMLXNG._refresh_lgamma_table` puts in the table; ``az_rho`` is + returned for the rho-update accumulator (policy 2). Otherwise selects per source among the fixed families (Fortran ``z0`` select, amica15.f90:1333/1346/1359/1371), in AMICATorchNG's nesting order @@ -289,15 +290,15 @@ def _log_pdf( """ abs_y = mx.abs(y) az_rho = mx.power(abs_y, rho) # reused by the rho-update accumulator (GG only) - log_pdf_gg = -az_rho - _LOG2 - lgamma_table + log_pdf_gg = -az_rho - LOG2 - lgamma_table if pdtype is None: return log_pdf_gg, az_rho - log_pdf_2 = -0.5 * y * y - _LOG_SQRT_2PI # Gaussian - log_pdf_3 = -2.0 * _logcosh(0.5 * y) - _LOG4 # logistic (sech^2) + log_pdf_2 = -0.5 * y * y - LOG_SQRT_2PI # Gaussian + log_pdf_3 = -2.0 * _logcosh(0.5 * y) - LOG4 # logistic (sech^2) lc = _logcosh(y) - log_pdf_4 = -0.5 * y * y + lc - _LOG_NORM_COSH_SUB # sub-Gaussian cosh+ - log_pdf_1 = -0.5 * y * y - lc - _LOG_NORM_COSH_SUP # super-Gaussian cosh- + log_pdf_4 = -0.5 * y * y + lc - LOG_NORM_COSH_SUB # sub-Gaussian cosh+ + log_pdf_1 = -0.5 * y * y - lc - LOG_NORM_COSH_SUP # super-Gaussian cosh- log_pdf = mx.where( pdtype == 2, log_pdf_2, @@ -1181,9 +1182,19 @@ def _initialize_parameters(self): def _refresh_lgamma_table(self): """Recompute ``lgamma(1+1/rho)`` host-side (MLX has no lgamma). Called at - init and after every rho update. Cheap: rho is ``(n_mix, n_comps)``.""" + init and after every rho update. Cheap: rho is ``(n_mix, n_comps)``. + + ``_log_pdf`` subtracts ``LOG2 + table``. At ``rho == 2`` the reference + takes its exact-Gaussian branch, whose normalizer is its own + single-precision literal (amica15.f90:1313, issue #344), so the entry + there is ``LOG_SQRT_PI - LOG2`` rather than ``lgamma(1.5)``: the same + normalizer the PyTorch and NumPy backends subtract, cast to float32. At + ``rho == 1`` the Laplace branch's ``log(2)`` is exact and + ``lgamma(2) == 0``, so the general form already matches it.""" rho_np = np.array(self.rho, dtype=np.float64) - self._lgamma_table = mx.array(gammaln(1.0 + 1.0 / rho_np).astype(np.float32)) + table = gammaln(1.0 + 1.0 / rho_np) + table = np.where(rho_np == 2.0, LOG_SQRT_PI - LOG2, table) + self._lgamma_table = mx.array(table.astype(np.float32)) def _update_unmixing_matrices(self): """Per-model ``W_h = inv(A[comp_list[:, h], :])`` and the LL Jacobian @@ -1472,7 +1483,7 @@ def zeros(): if self.dorho: logab = rho_h * mx.log(mx.maximum(mx.abs(y), tiny)) - logab = mx.where(az_rho < _EPSDBLE, mx.zeros_like(logab), logab) + logab = mx.where(az_rho < EPSDBLE, mx.zeros_like(logab), logab) drho_n = drho_n.at[:, idx].add((u * (az_rho * logab)).sum(0).T) g = (beta_h * ufp).sum(-1) # (batch, n_channels) diff --git a/pamica/numpy_impl/core.py b/pamica/numpy_impl/core.py index f791ff17..5004abb3 100644 --- a/pamica/numpy_impl/core.py +++ b/pamica/numpy_impl/core.py @@ -106,6 +106,10 @@ numerical_rank, validate_pca_reduction, ) + +# The density normalizers and the rho-update guard, at the values the binary +# uses: its single-precision literals widened to double (issue #344). +from ..reference_constants import EPSDBLE, LOG2, LOG_SQRT_PI from .utils import ( gammaln, identify_shared_components, @@ -1864,17 +1868,18 @@ def _compute_log_pdf( """ if rho == 1.0: # Laplace distribution - log_pdf = -np.abs(y) - np.log(2.0) + log_pdf = -np.abs(y) - LOG2 pdf = np.exp(log_pdf) dpdf = -np.sign(y) * pdf elif rho == 2.0: - # Gaussian distribution - log_pdf = -y * y - 0.5 * np.log(np.pi) + # Gaussian distribution, with the reference's single-precision + # normalizer log(dble(1.772453851)) (amica15.f90:1313, issue #344) + log_pdf = -y * y - LOG_SQRT_PI pdf = np.exp(log_pdf) dpdf = -2 * y * pdf else: # Generalized Gaussian distribution - log_pdf = -np.power(np.abs(y), rho) - np.log(2.0) - gammaln(1.0 + 1.0 / rho) + log_pdf = -np.power(np.abs(y), rho) - LOG2 - gammaln(1.0 + 1.0 / rho) pdf = np.exp(log_pdf) dpdf = -rho * np.power(np.abs(y), rho - 1) * np.sign(y) * pdf @@ -2243,10 +2248,10 @@ def _get_block_updates(self, X: np.ndarray) -> Dict: ay = np.abs(y) ayrho = np.power(ay, self.rho[j, k]) logab = self.rho[j, k] * np.log(np.maximum(ay, tiny)) - # Fortran zeros the term when |y|^rho < epsdble=1e-16 - # (amica17.f90:1570 / amica17_header.f90:73), not at denormal - # underflow; use 1e-16 to match, not np.finfo.tiny. - logab = np.where(ayrho < 1e-16, 0.0, logab) + # Fortran zeros the term when |y|^rho < epsdble + # (amica15.f90:1558 / amica15_header.f90:73), not at denormal + # underflow; use its value to match, not np.finfo.tiny. + logab = np.where(ayrho < EPSDBLE, 0.0, logab) updates["drho_n"][j, k] += np.sum(u * ayrho * logab) if self.do_newton: diff --git a/pamica/numpy_impl/pdf.py b/pamica/numpy_impl/pdf.py index 5cb18a2f..03b1e97c 100644 --- a/pamica/numpy_impl/pdf.py +++ b/pamica/numpy_impl/pdf.py @@ -1,8 +1,16 @@ """ -Probability Density Function (PDF) implementations for AMICA. +Source densities for plotting a fitted model. -This module implements various probability density functions used in the AMICA -algorithm for modeling source distributions. The supported PDFs are: +This is a plotting helper, not a fit path: :func:`compute_pdf` evaluates a +fitted source density in linear space for ``viz.plot_pdf_fits``. The fit itself +uses the log-space density in ``core.py`` (``AMICA._compute_log_pdf``), and the +generalized Gaussian here draws the same density: its normalizers come from +:mod:`pamica.reference_constants`, which every backend uses, including the +reference's single-precision ``sqrt(pi)`` at ``rho == 2`` (issue #344). + +The ``pdftype`` codes below are this module's legacy numbering, not the +reference's ``pdftype``: here 1 is the generalized Gaussian (the reference's 0), +and 2, 3 and 4 are densities no backend fits. The supported PDFs are: 1. Generalized Gaussian Distribution (GGD): p(x) = exp(-|x|^ρ) / (2Γ(1+1/ρ)) @@ -29,6 +37,8 @@ import numpy as np from scipy import special +from ..reference_constants import LOG2, LOG_SQRT_2PI, LOG_SQRT_PI + def compute_pdf( y: np.ndarray, rho: float, pdftype: int = 1 @@ -37,10 +47,8 @@ def compute_pdf( Compute PDF value and its derivative for given activation values. For each supported PDF type, computes both the probability density p(y) - and its derivative dp(y)/dy. These are used in the AMICA algorithm for: - 1. Computing data likelihood during model fitting - 2. Gradient calculations for parameter updates - 3. Estimating component responsibilities + and its derivative dp(y)/dy, for plotting (see the module docstring; the + fit uses ``AMICA._compute_log_pdf``). The shape parameter ρ controls the distribution's properties: - For GGD: Controls tail heaviness (1=Laplace, 2=Gaussian) @@ -54,7 +62,7 @@ def compute_pdf( rho : float Shape parameter pdftype : int - PDF type: + PDF type, in this module's legacy numbering (not the reference's): 1: Generalized Gaussian 2: Logistic 3: Generalized Logistic @@ -70,12 +78,13 @@ def compute_pdf( if pdftype == 1: # Generalized Gaussian if rho == 1.0: - # Laplace distribution - pdf = np.exp(-np.abs(y)) / 2.0 + # Laplace distribution, the fit's exp(-|y| - log 2) + pdf = np.exp(-np.abs(y) - LOG2) dpdf = -np.sign(y) * pdf elif rho == 2.0: - # Gaussian distribution - pdf = np.exp(-y * y) / np.sqrt(np.pi) + # Gaussian distribution, with the reference's single-precision + # normalizer log(dble(1.772453851)) (amica15.f90:1313), as in the fit + pdf = np.exp(-y * y - LOG_SQRT_PI) dpdf = -2 * y * pdf else: # General case: p(y) = exp(-|y|^rho) / (2 * Gamma(1 + 1/rho)). @@ -106,9 +115,9 @@ def compute_pdf( dpdf = pdf * (1 - (rho + 1) * exp_y / (1 + exp_y)) elif pdftype == 4: - # Gaussian mixture - pdf1 = np.exp(-0.5 * y * y) / np.sqrt(2 * np.pi) - pdf2 = np.exp(-0.5 * y * y / rho) / np.sqrt(2 * np.pi * rho) + # Gaussian mixture, with the log sqrt(2*pi) the Gaussian family uses + pdf1 = np.exp(-0.5 * y * y - LOG_SQRT_2PI) + pdf2 = np.exp(-0.5 * y * y / rho - LOG_SQRT_2PI - 0.5 * np.log(rho)) pdf = 0.5 * (pdf1 + pdf2) dpdf = -0.5 * (y * pdf1 + y * pdf2 / rho) @@ -118,42 +127,6 @@ def compute_pdf( return pdf, dpdf -def compute_log_pdf( - y: np.ndarray, rho: float, pdftype: int = 1 -) -> tuple[np.ndarray, np.ndarray]: - """ - Compute logarithm of PDF value and its derivative. - - Working in log space provides better numerical stability, especially for: - 1. Very small probability densities - 2. Product of probabilities (becomes sum of log probabilities) - 3. Computing likelihood ratios - - The derivative of log PDF (score function) is used in natural gradient - calculations for more efficient optimization. - - Parameters - ---------- - y : ndarray - Activation values - rho : float - Shape parameter - pdftype : int - PDF type (see compute_pdf for details) - - Returns - ------- - log_pdf : ndarray - Log PDF values - dlog_pdf : ndarray - Log PDF derivatives - """ - pdf, dpdf = compute_pdf(y, rho, pdftype) - log_pdf = np.log(pdf) - dlog_pdf = dpdf / pdf - return log_pdf, dlog_pdf - - def choose_pdf_type(data: np.ndarray, rho: float = 1.5) -> int: """ Choose best PDF type based on data statistics. diff --git a/pamica/reference_constants.py b/pamica/reference_constants.py new file mode 100644 index 00000000..d732d128 --- /dev/null +++ b/pamica/reference_constants.py @@ -0,0 +1,71 @@ +"""The reference's hard-coded numeric constants, at the precision it uses them. + +``amica15.f90`` writes several constants as default-kind Fortran literals, which +are single precision, and widens them to double with ``dble`` (for example +``log(dble(2.506628274))``): the compiler rounds the decimal to float32, and +``dble`` widens that float32 value exactly. The binary therefore uses the +float32 rounding of each decimal, not the decimal itself (issue #344). The same +holds for a default-kind literal that initializes a double-precision variable +in ``amica15_header.f90``, such as ``epsdble = 1.0e-16``. + +Every backend takes these values from this module, so they cannot drift apart +(``.rules/backend_parity.md``). The PyTorch and NumPy backends compute in +float64 and use them as they are; the MLX backend computes in float32 and casts +them there. + +A literal that is a dyadic rational (``dble(2.0)``, ``dble(4.0)``) is exact in +single precision, so its constant is the exact double value. Values the +reference reads from ``input.param`` (``lrate``, ``maxrho``, ``comp_thresh`` +and the rest) are user inputs that its parser reads as double, so they are not +constants and are not here. +""" + +import math + +import numpy as np + + +def single_precision_literal(value: float) -> float: + """The value of a Fortran default-kind real literal once widened to double. + + That is the float32 rounding of ``value``, returned as a float64. Rounding + the decimal to float64 first and then to float32 could round twice; the + pinned tests check, with exact rational arithmetic, that it does not for + any literal used here. + """ + return float(np.float32(value)) + + +# --- log-normalizers of the source densities (amica15.f90:1295-1372) ----------- +# Each family's log-density is ``-cost - log(norm)``, with ``norm`` written as a +# literal in the reference. + +# ``log(dble(2.0))``: the Laplace (rho == 1, :1307) and general generalized +# Gaussian (:1324) branches. Exact. +LOG2 = math.log(2.0) + +# ``log(dble(4.0))``: the logistic family, pdtype 3 (:1346). Exact. +LOG4 = math.log(4.0) + +# ``log(dble(1.772453851))``: the generalized Gaussian at exactly rho == 2 +# (:1313), whose normalizer is sqrt(pi). The float32 rounding is 1.7724539041..., +# so this is 3.0e-8 above 0.5 * log(pi). +LOG_SQRT_PI = math.log(single_precision_literal(1.772453851)) + +# ``log(dble(2.506628274))``: the Gaussian family, pdtype 2 (:1333), sqrt(2*pi). +# 3.7e-10 above the log of the decimal. +LOG_SQRT_2PI = math.log(single_precision_literal(2.506628274)) + +# ``log(dble(4.132731354))``: the sub-Gaussian cosh family, pdtype 4 (:1359). +# 2.0e-8 above the log of the decimal. +LOG_NORM_COSH_SUB = math.log(single_precision_literal(4.132731354)) + +# ``log(dble(1.858073988))``: the super-Gaussian cosh family, pdtype 1 (:1371). +# 2.1e-8 below the log of the decimal. +LOG_NORM_COSH_SUP = math.log(single_precision_literal(1.858073988)) + +# --- the rho update --------------------------------------------------------------- +# ``epsdble = 1.0e-16`` (amica15_header.f90:73, never read from input.param): the +# rho-update accumulator zeros its ``|y|^rho * log(|y|^rho)`` term where +# ``|y|^rho`` falls below this (amica15.f90:1558-1560). +EPSDBLE = single_precision_literal(1.0e-16) diff --git a/pamica/tests/mlx_tests/test_mlx_fit_noop.py b/pamica/tests/mlx_tests/test_mlx_fit_noop.py index 2bcd9224..8cf91d59 100644 --- a/pamica/tests/mlx_tests/test_mlx_fit_noop.py +++ b/pamica/tests/mlx_tests/test_mlx_fit_noop.py @@ -31,6 +31,13 @@ same lossless conversion a pre-#334 save goes through (:func:`pamica.component_layout.rows_from_legacy_columns`) before the bit-for-bit comparison; every per-model block, and so every other array, is unchanged. + +Issue #344 (epic #324 Phase 13) gave every backend the reference's +single-precision density constants, and these fits reach ``rho == 2``, where +the generalized Gaussian's normalizer is one of them, so the live class runs +with the constants it had before that change +(:func:`pamica.tests.pre_change.use_pre_344_constants`); the new values are +pinned against the reference by ``pamica/tests/test_reference_constants.py``. """ import importlib @@ -44,7 +51,10 @@ from pamica.component_layout import rows_from_legacy_columns # noqa: E402 from pamica.mlx_impl.core import AMICAMLXNG as CurrentAMICAMLXNG # noqa: E402 -from pamica.tests.pre_change import load_pre_change_package # noqa: E402 +from pamica.tests.pre_change import ( # noqa: E402 + load_pre_change_package, + use_pre_344_constants, +) SAMPLE_DIR = Path(__file__).resolve().parents[2] / "sample_data" DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" @@ -116,8 +126,9 @@ def _historical(model: Any, name: str) -> np.ndarray: @pytest.mark.parametrize("n_models", [1, 2]) def test_unscaled_fit_is_bit_identical_to_the_pre_phase3_epic_tip( - real_data, historical_amicamlxng, n_models + real_data, historical_amicamlxng, n_models, monkeypatch ): + use_pre_344_constants(monkeypatch, "mlx") kwargs: dict[str, Any] = dict( n_channels=NW, n_models=n_models, @@ -145,10 +156,11 @@ def test_unscaled_fit_is_bit_identical_to_the_pre_phase3_epic_tip( def test_unscaled_fit_with_keep_best_off_is_also_bit_identical( - real_data, historical_amicamlxng + real_data, historical_amicamlxng, monkeypatch ): """Same check with keep_best explicitly off, so the comparison does not depend on whichever safeguard branch a given seed happens to take.""" + use_pre_344_constants(monkeypatch, "mlx") kwargs: dict[str, Any] = dict( n_channels=NW, n_mix=NMIX, diff --git a/pamica/tests/pre_change.py b/pamica/tests/pre_change.py index 50afb397..6a3c4b12 100644 --- a/pamica/tests/pre_change.py +++ b/pamica/tests/pre_change.py @@ -24,6 +24,7 @@ import importlib import io +import math import os import subprocess import sys @@ -143,3 +144,42 @@ def load_pre_change_package( sys.path.remove(str(dest)) setattr(package, _COMMIT_ATTR, commit) return package + + +# The density constants every backend used before issue #344 (epic #324 Phase +# 13): the double values of the reference's decimal literals, where the binary +# uses their single-precision roundings (pamica.reference_constants). For +# ``LOG_SQRT_PI`` this is the exact 0.5 * log(pi) the PyTorch and NumPy +# backends used at rho == 2; the MLX backend's table then holds +# float32(0.5 * log(pi) - log(2)), which equals the float32 lgamma(1.5) it held. +_PRE_344_CONSTANTS = { + "LOG_SQRT_PI": 0.5 * math.log(math.pi), + "LOG_SQRT_2PI": math.log(2.506628274), + "LOG_NORM_COSH_SUB": math.log(4.132731354), + "LOG_NORM_COSH_SUP": math.log(1.858073988), + "EPSDBLE": 1e-16, +} +# The live module of each backend and the constants it reads. +_PRE_344_USERS = { + "torch": ("pamica.torch_impl.core", tuple(_PRE_344_CONSTANTS)), + "numpy": ("pamica.numpy_impl.core", ("LOG_SQRT_PI", "EPSDBLE")), + "mlx": ("pamica.mlx_impl.core", tuple(_PRE_344_CONSTANTS)), +} + + +def use_pre_344_constants(monkeypatch: pytest.MonkeyPatch, backend: str) -> None: + """Give the live ``backend`` the density constants it used before issue #344. + + Issue #344 replaced them with the reference's single-precision values, + which moves every fit in which a generalized-Gaussian mixture reaches + ``rho == 2`` and every fit of the Gaussian and cosh families. A test that + compares a live backend bit for bit with code from before that change, to + show that some other change left results alone, calls this first, so the + comparison isolates that other change. Every live code path still runs; + only these constants take their old values, for the rest of the test. + ``test_reference_constants.py`` pins the new values against the reference. + """ + module_name, names = _PRE_344_USERS[backend] + module = importlib.import_module(module_name) + for name in names: + monkeypatch.setattr(module, name, _PRE_344_CONSTANTS[name]) diff --git a/pamica/tests/test_amica.py b/pamica/tests/test_amica.py index 23abcc3f..d93c739d 100644 --- a/pamica/tests/test_amica.py +++ b/pamica/tests/test_amica.py @@ -65,9 +65,11 @@ def test_pdf_computation(self): np.testing.assert_allclose(pdf, np.exp(-np.abs(y)) / 2.0) np.testing.assert_allclose(dpdf, -np.sign(y) * pdf) - # Test Gaussian distribution + # Test Gaussian distribution, normalized by the reference's + # single-precision sqrt(pi), 1.772453851 rounded to float32 + # (amica15.f90:1313, issue #344) pdf, dpdf = compute_pdf(y, rho=2.0) - np.testing.assert_allclose(pdf, np.exp(-y * y) / np.sqrt(np.pi)) + np.testing.assert_allclose(pdf, np.exp(-y * y) / float(np.float32(1.772453851))) np.testing.assert_allclose(dpdf, -2 * y * pdf) # NOTE: the former synthetic-data source-recovery test (test_full_amica) was diff --git a/pamica/tests/test_component_rows.py b/pamica/tests/test_component_rows.py index aeba9ec9..3bede5fe 100644 --- a/pamica/tests/test_component_rows.py +++ b/pamica/tests/test_component_rows.py @@ -53,7 +53,7 @@ from pamica import AMICA_NumPy from pamica.component_layout import rows_from_legacy_columns from pamica.numpy_impl.utils import identify_shared_components -from pamica.tests.pre_change import load_pre_change_package +from pamica.tests.pre_change import load_pre_change_package, use_pre_344_constants from pamica.torch_impl.core import AMICATorchNG from pamica.torch_impl.utils import load_eeglab_data @@ -68,6 +68,9 @@ # component-column layout (Phase 7's doscaling fix and Phase 9's schedule # gates included, so the two sides differ by this phase alone). PRE_CHANGE_COMMIT = "0930c0e68ec9e2bbef3d20d51cff4c03029248f2" +# The epic head before issue #344 (Phase 13): component rows, but the exact +# rho == 2 normalizer instead of the reference's single-precision one. +PRE_344_COMMIT = "027cb0731dbaaa8e7a5795166af95ba7ff52c2cd" pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") @@ -86,6 +89,14 @@ def pre(tmp_path_factory) -> Any: ) +@pytest.fixture(scope="module") +def pre344(tmp_path_factory) -> Any: + """The pamica package at ``PRE_344_COMMIT``, imported beside the live one.""" + return load_pre_change_package( + PRE_344_COMMIT, "pamica_pre344", tmp_path_factory.mktemp("pre344") + ) + + def _mlx_core(): """The MLX backend module, or skip this test (never the whole module).""" mlx_core = pytest.importorskip( @@ -256,7 +267,7 @@ def _fit_arrays( @pytest.mark.parametrize("backend, n_models, cfg", _byte_id_configs()) def test_unshared_fits_are_byte_identical_to_the_column_layout( - pre, real_data, backend, n_models, cfg, tmp_path + pre, real_data, backend, n_models, cfg, tmp_path, monkeypatch ): """Without a merge every per-model block holds the same ``n x n`` matrix in both layouts, so every trajectory and fitted array is bit for bit what the @@ -270,8 +281,14 @@ def test_unshared_fits_are_byte_identical_to_the_column_layout( Measured: at most 2.2e-16 relative in float64 and 1.1e-7 (one float32 unit in the last place) on MLX. It only feeds the gradient-norm stops, and no stop fired in these fits. + + The live backend runs with the density constants it had before issue #344, + which the pre-change code predates: the two-model fits reach ``rho == 2`` + by the fifth iteration, and the Gaussian and cosh families use their + normalizers on every one. """ - old_cls, new_cls = _classes(backend, pre) + old_cls, new_cls = _classes(backend, pre) # skips MLX without MLX + use_pre_344_constants(monkeypatch, backend) X = real_data[:, :_BYTE_ID_SAMPLES] old = _fit_arrays(backend, old_cls, n_models, cfg, X, tmp_path / "old") new = _fit_arrays(backend, new_cls, n_models, cfg, X, tmp_path / "new") @@ -797,15 +814,11 @@ def test_load_results_refuses_a_truncated_A(real_data, tmp_path, keep): # --- 4. seeded native reference oracle from a MERGED state (opt-in) ------------- # The reference's input.param optimizer, natural gradient only (the Newton start -# is issue #335), with the block size pinned on both sides. ``maxrho`` sits just -# below 2 so no mixture reaches the reference's exact-Gaussian branch -# (``rho == 2``, amica15.f90:1309-1313), whose normalizer is a single-precision -# literal, ``log(dble(1.772453851))``, 3.0e-8 away from ``log(sqrt(pi))``: once a -# warm fit clamps a mixture there, every sample's log-density differs from -# pamica's exact one by that much times its responsibility, which is unrelated -# to the mixing layout and would swamp the round-off this test measures. (The -# Laplace branch, ``rho == 1``, is exact in both.) Whether pamica adopts the -# single-precision value is issue #344. +# is issue #335), with the block size pinned on both sides. The warm state has +# mixtures clamped at ``maxrho = 2``, so the E-step takes the reference's +# exact-Gaussian branch (``rho == 2``, amica15.f90:1309-1313), whose normalizer +# is the single-precision literal ``log(dble(1.772453851))``; every backend uses +# that value since issue #344. (Until then this oracle held ``maxrho`` at 1.99.) _OPT: Dict[str, Any] = dict( block_size=512, lrate=0.05, @@ -814,7 +827,7 @@ def test_load_results_refuses_a_truncated_A(real_data, tmp_path, keep): rholratefact=0.5, rho0=1.5, minrho=1.0, - maxrho=1.99, # below the reference's single-precision rho == 2 branch (#344) + maxrho=2.0, invsigmin=0.0, invsigmax=100.0, do_newton=False, @@ -835,12 +848,16 @@ def test_load_results_refuses_a_truncated_A(real_data, tmp_path, keep): # as test_doscaling_rows.py's oracle. Measured maxima over both backends and # both doscaling settings, from the merged state (the same state with no merge # in brackets, PyTorch, doscaling on): -# 1 iteration: A 1.3e-15, mu 2.3e-11, sbeta 1.4e-14, LL 8.9e-16 -# [A 1.1e-15, mu 2.3e-11, sbeta 1.3e-14, LL 1.8e-15] -# 3 iterations: A 1.5e-12, mu 3.7e-9, sbeta 1.8e-11, LL 4.7e-14 -# [A 1.9e-12, mu 1.4e-8, sbeta 1.9e-10, LL 8.0e-15] -# The column semantics from the same merged state is off by A 0.204 and 0.21 and -# LL 3.5e-4 and 4.3e-4 (doscaling on and off) after 3 iterations. +# 1 iteration: A 1.3e-15, mu 1.7e-12, sbeta 1.3e-14, LL 2.2e-15 +# [A 1.1e-15, mu 1.7e-12, sbeta 1.2e-14, LL 0] +# 3 iterations: A 3.8e-13, mu 5.7e-9, sbeta 2.9e-11, LL 1.6e-13 +# [A 5.1e-14, mu 3.3e-9, sbeta 1.9e-12, LL 1.3e-14] +# The column semantics from the same merged state is off by A 0.203 and 0.209 and +# LL 3.5e-4 and 4.3e-4 (doscaling on and off) after 3 iterations. The code +# before issue #344, with the exact rho == 2 normalizer, is off by A 2.6e-10, +# mu 1.8e-8, sbeta 5.0e-9 and LL 2.8e-9 after 1 iteration, and by A 3.4e-8, +# mu 8.4e-3, sbeta 2.6e-6 and LL 1.2e-8 after 3 (maxima over doscaling on and +# off). _MERGED_ORACLE_TOL = { 1: {"A": 1e-13, "mu": 1e-9, "sbeta": 1e-12, "LL": 1e-13}, 3: {"A": 1e-10, "mu": 1e-6, "sbeta": 1e-8, "LL": 1e-11}, @@ -875,9 +892,9 @@ def merged_seed(real_data) -> Dict[str, Any]: for k in ("A", "mu", "beta", "rho", "alpha", "gm", "mean", "sphere") } seed.update(default=default, merged=merged, cos=float(cos[i, ii])) - # No mixture sits at rho == 2, where the reference's normalizer is a - # single-precision literal (see _OPT). - assert seed["rho"].max() < 2.0 + # Mixtures sit at rho == 2, where the reference's normalizer is a + # single-precision literal (see _OPT), so the oracle exercises that branch. + assert (seed["rho"] == 2.0).any() return seed @@ -957,7 +974,7 @@ def _mixing_error( ) @pytest.mark.parametrize("doscaling", [True, False], ids=["scale", "noscale"]) def test_updates_from_a_merged_state_match_the_seeded_reference( - pre, merged_seed, real_data, doscaling, tmp_path + pre, pre344, merged_seed, real_data, doscaling, tmp_path ): """The reference's own scan is unrunnable (its ``Spinv2`` is never allocated), but its ``load_comp_list`` seeds a merged ``comp_list``, and @@ -1037,6 +1054,21 @@ def torch_model(cls: Any) -> Any: over = {q: e for q, e in errs.items() if not e <= tol[q]} assert not over, f"{name}, {k} iteration(s): {over} (bounds {tol})" + # The code before issue #344 from the same state: pamica's exact rho == 2 + # normalizer against the reference's single-precision one, the gap the + # maxrho = 1.99 workaround hid. + old344 = torch_model(pre344.torch_impl.core.AMICATorchNG) + ll, mixing, mu, sbeta = _seeded_run(old344, "torch", seed, real_data, k, True) + errs = { + "A": _mixing_error(mixing, ref.A, seed["merged"]), + "mu": np.abs(mu[:, used] - ref.mu[:, used]).max(), + "sbeta": np.abs(sbeta[:, used] - ref.sbeta[:, used]).max(), + "LL": np.abs(ll - ref.LL).max(), + } + print(f"merged oracle doscaling={doscaling} before #344 k={k}: {errs}") + assert all(errs[q] > tol[q] for q in errs), errs + assert errs["LL"] > 10 * tol["LL"] + if k == 3: old = torch_model(pre.torch_impl.core.AMICATorchNG) ll_old, mixing, _, _ = _seeded_run(old, "torch", seed, real_data, k, False) diff --git a/pamica/tests/test_doscaling_rows.py b/pamica/tests/test_doscaling_rows.py index fc9526c0..acefc837 100644 --- a/pamica/tests/test_doscaling_rows.py +++ b/pamica/tests/test_doscaling_rows.py @@ -54,7 +54,7 @@ from pamica import AMICA_NumPy from pamica.component_layout import rows_from_legacy_columns -from pamica.tests.pre_change import load_pre_change_package +from pamica.tests.pre_change import load_pre_change_package, use_pre_344_constants from pamica.torch_impl.core import AMICATorchNG from pamica.torch_impl.utils import load_eeglab_data @@ -457,17 +457,20 @@ def _fit_unscaled( @pytest.mark.parametrize("n_models", [1, 2]) @pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) def test_doscaling_off_is_byte_identical_to_the_pre_fix_code( - pre_fix, real_slice, backend, n_models, tmp_path + pre_fix, real_slice, backend, n_models, tmp_path, monkeypatch ): """``doscaling=False`` never enters the rescale, so this fix must leave its trajectory and every fitted array bit for bit where the pre-fix code put - them, on every backend.""" + them, on every backend. The live backend runs with the density constants it + had before issue #344, which the pre-fix code predates (the two-model fits + reach ``rho == 2``).""" new_cls: Any if backend == "mlx": - new_cls = _mlx_core().AMICAMLXNG + new_cls = _mlx_core().AMICAMLXNG # skips without MLX else: new_cls = AMICATorchNG if backend == "torch" else AMICA_NumPy old_cls = _pre_fix_class(pre_fix, backend) + use_pre_344_constants(monkeypatch, backend) old = _fit_unscaled(old_cls, backend, n_models, real_slice, tmp_path / "old") new = _fit_unscaled(new_cls, backend, n_models, real_slice, tmp_path / "new") diff --git a/pamica/tests/test_mlx_pdf_families_cross_backend.py b/pamica/tests/test_mlx_pdf_families_cross_backend.py index 6a8dac05..83b7317b 100644 --- a/pamica/tests/test_mlx_pdf_families_cross_backend.py +++ b/pamica/tests/test_mlx_pdf_families_cross_backend.py @@ -56,11 +56,13 @@ ] # Fortran log-normalizer literals (amica15.f90:1333/1346/1359/1371), matching -# torch_tests/test_ng_pdf_families.py. +# torch_tests/test_ng_pdf_families.py. The binary rounds each default-kind +# literal to single precision before ``dble`` widens it (issue #344), so the +# reference's values are the float32 roundings; 4.0 is exact. _LOG4 = math.log(4.0) -_LSQ2PI = math.log(2.506628274) -_LNSUB = math.log(4.132731354) -_LNSUP = math.log(1.858073988) +_LSQ2PI = math.log(float(np.float32(2.506628274))) +_LNSUB = math.log(float(np.float32(4.132731354))) +_LNSUP = math.log(float(np.float32(1.858073988))) def _real_data(n_samples: int = 4096) -> np.ndarray: diff --git a/pamica/tests/test_pamica.py b/pamica/tests/test_pamica.py index f46e4f44..82b89678 100644 --- a/pamica/tests/test_pamica.py +++ b/pamica/tests/test_pamica.py @@ -74,9 +74,11 @@ def test_pdf_computation(): npt.assert_allclose(pdf, np.exp(-np.abs(y)) / 2.0) npt.assert_allclose(dpdf, -np.sign(y) * pdf) - # Test Gaussian distribution + # Test Gaussian distribution, normalized by the reference's + # single-precision sqrt(pi), 1.772453851 rounded to float32 + # (amica15.f90:1313, issue #344) pdf, dpdf = compute_pdf(y, rho=2.0) - npt.assert_allclose(pdf, np.exp(-y * y) / np.sqrt(np.pi)) + npt.assert_allclose(pdf, np.exp(-y * y) / float(np.float32(1.772453851))) npt.assert_allclose(dpdf, -2 * y * pdf) diff --git a/pamica/tests/test_reference_constants.py b/pamica/tests/test_reference_constants.py new file mode 100644 index 00000000..07f76175 --- /dev/null +++ b/pamica/tests/test_reference_constants.py @@ -0,0 +1,607 @@ +"""The reference's single-precision constants (issue #344, epic #324 Phase 13). + +``amica15.f90`` writes several density normalizers as default-kind Fortran +literals widened with ``dble``, for example ``log(dble(2.506628274))``: the +compiler rounds the decimal to float32, so the binary uses that rounding, not +the decimal. ``epsdble = 1.0e-16`` (``amica15_header.f90:73``) is the same kind +of literal. pamica used the decimals' double values, which moved every mixture +at ``rho == 2`` (the default ``maxrho`` clamp) by 3.0e-8 in its log-density, +and the Gaussian and cosh families by 3.7e-10, 2.0e-8 and -2.1e-8. All three +backends now read the reference's values from :mod:`pamica.reference_constants`. + +Pinned here, cross-backend per ``.rules/backend_parity.md`` (PyTorch and NumPy +always run; the MLX checks skip individually without MLX or an Apple GPU): + +1. every shared constant is the float32 rounding of the literal on the cited + reference line, computed here by exact rational arithmetic, independently of + the module under test; the dyadic literals are exact; +2. the sweep of both reference sources, as executable documentation: the set + of default-kind literals that are not exact in single precision, and what + each one is (a density normalizer, an initialization scale, a sentinel, a + default of an ``input.param`` key, or a declared variable nothing reads); +3. no module of the three backend packages keeps a copy of any of these + constants, in log or linear form, and the scanner flags every definition + this change removed; +4. every backend's ``rho == 2`` log-density uses the reference's normalizer, + and the NumPy plotting helper draws the density the fit uses; +5. (opt-in, ``AMICA_RUN_FORTRAN=1``) the native reference binary, seeded from + pamica's initialization for the Gaussian and the two cosh families, matches + PyTorch to float64 round-off after one and three iterations, where the code + before this change is off by exactly each literal's rounding. + ``test_component_rows.py`` runs the ``rho == 2`` oracle from a warm + two-model state; +6. ``pre_change.use_pre_344_constants``, which the byte-identity tests against + older code call, changes every constant it patches and covers every changed + constant each backend reads. + +Real bundled sample EEG only, no synthetic data or mocks (``.rules/testing.md``). +The density checks evaluate the formulas on a grid of activations, as +``torch_tests/test_ng_pdf_families.py`` does. +""" + +from __future__ import annotations + +import importlib +import io +import math +import os +import re +import tokenize +from fractions import Fraction +from pathlib import Path +from typing import Any, Dict + +import numpy as np +import pytest +import torch + +import pamica.reference_constants as rc +from pamica.numpy_impl.core import AMICA as AMICA_NumPy +from pamica.numpy_impl.pdf import compute_pdf +from pamica.tests import pre_change +from pamica.tests.pre_change import load_pre_change_package +from pamica.torch_impl.core import AMICATorchNG, _log_pdf_and_deriv, _log_pdf_only +from pamica.torch_impl.utils import load_eeglab_data + +PACKAGE = Path(__file__).resolve().parents[1] +REFERENCE = PACKAGE / "amica15.f90" +REFERENCE_HEADER = PACKAGE / "amica15_header.f90" +SAMPLE_DIR = PACKAGE / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +SEED = 42 + +# The epic #324 head this phase is based on: the last commit with the decimal +# literals' double values. +PRE_CHANGE_COMMIT = "027cb0731dbaaa8e7a5795166af95ba7ff52c2cd" + + +def _float32_rounding(literal: str) -> float: + """The float32 value nearest the decimal ``literal``, ties to even, as the + Fortran compiler rounds a default-kind literal: exact rational arithmetic, + so no intermediate float64 rounding and nothing shared with the module under + test. Normal positive values only, which every literal here is.""" + x = Fraction(literal) + assert x > 0 + e = x.numerator.bit_length() - x.denominator.bit_length() + if Fraction(2) ** e > x: + e -= 1 + assert Fraction(2) ** e <= x < Fraction(2) ** (e + 1) + assert -126 <= e <= 127, "not a normal float32" + scale = Fraction(2) ** (23 - e) # 24 significant bits + return float(Fraction(round(x * scale)) / scale) + + +def _source_line(path: Path, line: int) -> str: + return path.read_text().splitlines()[line - 1] + + +# --- 1. each constant is the float32 rounding of its reference literal --------- +# (constant, reference line, the text on that line, the literal) +_LOG_NORMALIZERS = [ + ("LOG_SQRT_PI", 1313, "log(dble(1.772453851))", "1.772453851"), + ("LOG_SQRT_2PI", 1333, "log(dble(2.506628274))", "2.506628274"), + ("LOG_NORM_COSH_SUB", 1359, "log(dble(4.132731354))", "4.132731354"), + ("LOG_NORM_COSH_SUP", 1371, "log(dble(1.858073988))", "1.858073988"), +] +# The same constants against the double value pamica used before this change: +# log(float32(literal)) - log(literal), and for rho == 2 against 0.5 * log(pi), +# which is what the PyTorch and NumPy backends used there. +_OFFSETS = { + "LOG_SQRT_PI": (0.5 * math.log(math.pi), 3.0e-8), + "LOG_SQRT_2PI": (math.log(2.506628274), 3.7e-10), + "LOG_NORM_COSH_SUB": (math.log(4.132731354), 2.0e-8), + "LOG_NORM_COSH_SUP": (math.log(1.858073988), -2.1e-8), +} + + +@pytest.mark.parametrize( + "name,line,text,literal", _LOG_NORMALIZERS, ids=[n for n, *_ in _LOG_NORMALIZERS] +) +def test_log_normalizer_is_the_reference_literal_in_single_precision( + name, line, text, literal +): + assert text in _source_line(REFERENCE, line), f"amica15.f90:{line} moved" + value = getattr(rc, name) + assert value == math.log(_float32_rounding(literal)) + # Not the double value of the decimal: the rounding is what differs. + assert _float32_rounding(literal) != float(literal) + old, offset = _OFFSETS[name] + assert value - old == pytest.approx(offset, rel=0.05) + + +def test_epsdble_is_the_reference_literal_in_single_precision(): + header = _source_line(REFERENCE_HEADER, 73) + assert "epsdble = 1.0e-16" in header, "amica15_header.f90:73 moved" + assert "where (tmpy(bstrt:bstp) < epsdble)" in _source_line(REFERENCE, 1558) + assert rc.EPSDBLE == _float32_rounding("1.0e-16") + assert rc.EPSDBLE != 1e-16 + + +@pytest.mark.parametrize( + "name,line,text,value", + [ + ("LOG2", 1308, "log(dble(2.0))", 2.0), + ("LOG2", 1324, "log(dble(2.0))", 2.0), + ("LOG4", 1346, "log(dble(4.0))", 4.0), + ], +) +def test_dyadic_normalizers_are_exact(name, line, text, value): + """A dyadic literal is exact in single precision, so its constant is the + exact double value.""" + assert text in _source_line(REFERENCE, line), f"amica15.f90:{line} moved" + assert _float32_rounding(repr(value)) == value + assert getattr(rc, name) == math.log(value) + + +@pytest.mark.parametrize( + "literal", ["1.772453851", "2.506628274", "4.132731354", "1.858073988", "1.0e-16"] +) +def test_the_helper_does_not_round_twice(literal): + """``single_precision_literal`` rounds through float64; for every literal it + is used on, that lands on the correctly rounded float32 value.""" + assert rc.single_precision_literal(float(literal)) == _float32_rounding(literal) + + +# --- 2. the sweep, as executable documentation ---------------------------------- +_DBLE_LITERAL = re.compile(r"dble\(\s*(-?\d+\.\d*(?:[eE][-+]?\d+)?)\s*\)") +_REAL_LITERAL = re.compile(r"(? bool: + magnitude = literal.lstrip("-") + if Fraction(magnitude) == 0: + return False + return _float32_rounding(magnitude) != float(magnitude) + + +def test_every_inexact_single_precision_literal_in_the_reference_is_accounted_for(): + """``amica15.f90`` writes every real constant in its code as ``dble(...)`` of + a default-kind literal, never a bare or double-precision literal. The ones + that are not exact in single precision are exactly these.""" + found: Dict[int, str] = {} + for n, code in _code_lines(REFERENCE): + stripped = _DBLE_LITERAL.sub("", code) + assert not _REAL_LITERAL.search(stripped), f"amica15.f90:{n}: {code.strip()}" + for literal in _DBLE_LITERAL.findall(code): + if _inexact(literal): + assert n not in found + found[n] = literal + assert found == { + # Density normalizers: pamica.reference_constants. + 1313: "1.772453851", + 1333: "2.506628274", + 1359: "4.132731354", + 1371: "1.858073988", + # Initialization scales of the drawn mu, sbeta and A (and A's redraw on + # a restart). pamica draws from its own generator, so it cannot + # reproduce the reference's initialization, and these are not adopted. + 756: "0.05", + 771: "0.1", + 814: "0.01", + 1035: "0.01", + # Starting values of the rejection pass's running max and min. + 2210: "-999999999.0", + 2211: "999999999.0", + } + + +def test_every_inexact_default_in_the_reference_header_is_accounted_for(): + """``amica15_header.f90`` initializes double-precision variables with + default-kind literals. The ones that are not exact in single precision are + defaults of ``input.param`` keys (read as double when the key is given), + ``epsdble`` (hard-coded, now shared) or variables nothing reads.""" + found: Dict[str, str] = {} + for n, code in _code_lines(REFERENCE_HEADER): + if not code.lstrip().lower().startswith("double precision"): + continue + for name, literal in re.findall( + r"(\w+)\s*=\s*(-?[\d.]+(?:[eE][-+]?\d+)?)", code + ): + if _inexact(literal): + found[name] = literal + read_from_input_param = { + "mineig": "1.0e-15", + "lrate": "0.1", + "minlrate": "1.0e-12", + "rholrate": "0.05", + "rholrate0": "0.05", + "rholratefact": "0.1", + "invsigmin": "0.0001", + "comp_thresh": "0.99", + "min_dll": "1.0e-9", + "min_nd": "1.0e-7", + } + never_read = { + "mincond": "1.0e-15", + "maxdble": "1.0e32", + "mineigv": "1.0e-15", + "minhess": "1.0e-5", + } + assert found == {**read_from_input_param, **never_read, "epsdble": "1.0e-16"} + reference = REFERENCE.read_text() + # variable -> (input.param key, the variable its case reads into) + reads = { + "lrate": ("lrate", "lrate0"), # then lrate = lrate0 + "rholrate": ("rholrate", "rholrate0"), # then rholrate = rholrate0 + "rholrate0": ("rholrate", "rholrate0"), + "min_nd": ("min_grad_norm", "min_nd"), + } + for name in read_from_input_param: + key, target = reads.get(name, (name, name)) + case = re.search(rf"case\('{key}'\)(.*?)(?=\n\s*case\()", reference, re.S) + assert case is not None, key + assert re.search(rf"read\(tmparg,'\([^)]*\)'\)\s*{target}\b", case.group(1)) + if name != target: + assert re.search(rf"\b{name} = {target}\b", case.group(1)), name + for name in never_read: + uses = [ + n for n, code in _code_lines(REFERENCE) if re.search(rf"\b{name}\b", code) + ] + assert not uses, f"{name} is read at amica15.f90:{uses}" + + +# --- 3. no backend keeps its own copy -------------------------------------------- +# Every Python module of the three backend packages, not just their cores (the +# NumPy plotting helper pdf.py once carried its own sqrt(pi)). +_BACKEND_PACKAGES = ("torch_impl", "numpy_impl", "mlx_impl") +_BACKEND_FILES = sorted( + path for package in _BACKEND_PACKAGES for path in (PACKAGE / package).rglob("*.py") +) +_MOD = r"(?:math|np|numpy|torch|mx)" +_PI = rf"{_MOD}\.pi\b" +_TWO = r"2(?:\.0*)?" +# A constant written into a backend instead of imported: the decimal literals +# (and truncations of them), epsdble, and the normalizers in log or linear form. +_COPIES = re.compile( + "|".join( + [ + r"1\.77245|2\.50662|4\.13273|1\.85807", + r"\b1(?:\.0*)?e-16\b", + rf"log\(\s*{_PI}\s*\)", # log(pi) + rf"log\(\s*{_TWO}\s*\*\s*{_PI}", # log(2*pi...) + rf"log\(\s*{_PI}\s*\*\s*{_TWO}\b", # log(pi*2) + rf"log\(\s*(?:{_MOD}\.)?sqrt\(", # log(sqrt(...)) + r"log\(\s*[24](?:\.0*)?\s*\)", # log(2), log(4.0) + rf"sqrt\(\s*{_PI}\s*\)", # sqrt(pi) + rf"sqrt\(\s*{_TWO}\s*\*\s*{_PI}", # sqrt(2*pi), sqrt(2 * np.pi * rho) + rf"sqrt\(\s*{_PI}\s*\*\s*{_TWO}\b", # sqrt(pi*2) + rf"{_PI}\s*\*\*\s*0?\.5", # pi ** 0.5 + ] + ) +) +# Code that the scanner must flag: the definitions this change removed. +_OLD_DEFINITIONS = [ + "_HALF_LOG_PI = 0.5 * math.log(math.pi)", + "_LOG_SQRT_2PI = math.log(2.506628274)", + "_LOG_NORM_COSH_SUB = math.log(4.132731354)", + "_LOG_NORM_COSH_SUP = math.log(1.858073988)", + "_EPSDBLE = 1e-16", + "_LOG2 = math.log(2.0)", + "_LOG4 = math.log(4.0)", + "log_pdf = -np.abs(y) - np.log(2.0)", + "log_pdf = -y * y - 0.5 * np.log(np.pi)", + "logab = np.where(ayrho < 1e-16, 0.0, logab)", + "pdf = np.exp(-y * y) / np.sqrt(np.pi)", + "pdf1 = np.exp(-0.5 * y * y) / np.sqrt(2 * np.pi)", + "pdf2 = np.exp(-0.5 * y * y / rho) / np.sqrt(2 * np.pi * rho)", +] + + +def _python_code(source: str) -> list[str]: + """``source``'s lines with comments, strings and docstrings blanked out, so + prose that names a constant is not mistaken for a copy of it.""" + lines = [list(line) for line in source.splitlines(keepends=True)] + prose = {tokenize.COMMENT, tokenize.STRING} + if hasattr(tokenize, "FSTRING_MIDDLE"): + prose.add(tokenize.FSTRING_MIDDLE) + for tok in tokenize.generate_tokens(io.StringIO(source).readline): + if tok.type not in prose: + continue + (row, col), (end_row, end_col) = tok.start, tok.end + for r in range(row, end_row + 1): + line = lines[r - 1] + stop = end_col if r == end_row else len(line) + for c in range(col if r == row else 0, stop): + if line[c] != "\n": + line[c] = " " + return ["".join(line) for line in lines] + + +@pytest.mark.parametrize("code", _OLD_DEFINITIONS) +def test_the_copy_scanner_flags_the_removed_definitions(code): + """The scanner below is not vacuous: it flags each definition this change + removed from the backends.""" + assert _COPIES.search(_python_code(code + "\n")[0]) + + +@pytest.mark.parametrize( + "path", _BACKEND_FILES, ids=[str(p.relative_to(PACKAGE)) for p in _BACKEND_FILES] +) +def test_no_backend_keeps_a_copy_of_the_constants(path): + copies = [ + f"{n}: {line.strip()}" + for n, line in enumerate(_python_code(path.read_text()), 1) + if _COPIES.search(line) + ] + assert not copies, f"{path.relative_to(PACKAGE)} defines its own: {copies}" + + +@pytest.mark.parametrize("package", _BACKEND_PACKAGES) +def test_every_backend_imports_the_shared_constants(package): + assert "reference_constants import" in (PACKAGE / package / "core.py").read_text() + + +# --- 4. the rho == 2 branch uses the reference's normalizer, on every backend --- +_Y = np.linspace(-8.0, 8.0, 65) +_Y = _Y[np.abs(_Y) > 1e-3] + + +def test_torch_and_numpy_rho2_log_density_is_the_references(): + """The exact-Gaussian branch of the generalized Gaussian (amica15.f90:1313) + subtracts ``log(dble(1.772453851))``; PyTorch and NumPy now match that + formula bit for bit, and each other.""" + expected = -_Y * _Y - math.log(_float32_rounding("1.772453851")) + y = torch.from_numpy(_Y.copy()) + rho = torch.full_like(y, 2.0) + lp_only, _ = _log_pdf_only(y, rho) + lp_full, _ = _log_pdf_and_deriv(y, rho) + np.testing.assert_array_equal(lp_only.numpy(), expected) + np.testing.assert_array_equal(lp_full.numpy(), expected) + numpy_model = AMICA_NumPy(use_tqdm=False) + lp_numpy, _ = numpy_model._compute_log_pdf(_Y, 2.0) + np.testing.assert_array_equal(lp_numpy, expected) + + +@pytest.mark.parametrize("rho", [1.0, 1.5, 2.0]) +def test_the_plotted_density_is_the_fits(rho): + """``viz.plot_pdf_fits`` draws ``compute_pdf``, which must be the density + the fit uses, including the reference's normalizer at ``rho == 2``.""" + fit_log_pdf, _ = AMICA_NumPy(use_tqdm=False)._compute_log_pdf(_Y, rho) + plotted, _ = compute_pdf(_Y, rho) + np.testing.assert_allclose(plotted, np.exp(fit_log_pdf), rtol=1e-13, atol=0) + + +def test_mlx_rho2_normalizer_is_the_references(): + """MLX folds the generalized Gaussian's normalizer into a host-side table, + ``log2 + table``, with ``table = lgamma(1 + 1/rho)`` (MLX has no lgamma). + At ``rho == 2`` the table holds the reference's normalizer instead, so the + float32 log-density is the reference's formula cast to float32.""" + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + mx = mlx_core.mx + if mx.default_device().type != mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + from scipy.special import gammaln + + m = mlx_core.AMICAMLXNG(n_channels=NW, n_mix=3) + rho = np.array([[1.0], [1.5], [2.0]]) + m.rho = mx.array(rho.astype(np.float32)) + m._refresh_lgamma_table() + table = np.array(m._lgamma_table) + assert table.dtype == np.float32 + assert table[0, 0] == 0.0 # lgamma(2): the Laplace branch's log(2) alone + assert table[1, 0] == np.float32(gammaln(1.0 + 1.0 / 1.5)) + assert table[2, 0] == np.float32(rc.LOG_SQRT_PI - rc.LOG2) + assert table[2, 0] != np.float32(gammaln(1.5)) + + y32 = mx.array(_Y.astype(np.float32)) + rho2 = mx.full(y32.shape, 2.0) + lp, _ = mlx_core._log_pdf(y32, rho2, mx.full(y32.shape, table[2, 0])) + expected = -_Y * _Y - math.log(_float32_rounding("1.772453851")) + np.testing.assert_allclose(np.array(lp, dtype=np.float64), expected, rtol=1e-6) + + +# --- 5. seeded native reference oracle, per family (opt-in) ---------------------- +# The reference's input.param optimizer, natural gradient only, with the block +# size pinned on both sides (the same as test_doscaling_rows.py's oracle). +_OPT: Dict[str, Any] = dict( + block_size=512, + lrate=0.05, + lratefact=0.5, + rholrate=0.05, + rholratefact=0.5, + rho0=1.5, + minrho=1.0, + maxrho=2.0, + invsigmin=0.0, + invsigmax=100.0, + do_newton=False, + newt_start=50, + newtrate=1.0, + newt_ramp=10, +) +_REF_OPT: Dict[str, Any] = { + **{k: v for k, v in _OPT.items() if k != "do_newton"}, + "do_newton": 0, + "do_opt_block": 0, + "do_reject": 0, + "share_comps": 0, + "doscaling": 1, + "scalestep": 1, +} +# (pdftype, n_mix, the constant whose rounding the pre-change code missed) +_FAMILIES = [ + (2, 3, "LOG_SQRT_2PI"), + (4, 1, "LOG_NORM_COSH_SUB"), + (1, 1, "LOG_NORM_COSH_SUP"), +] +# Per iteration count, the doscaling oracle's round-off bounds. Measured maxima +# over the three families (PyTorch against the binary): +# 1 iteration: A 9.8e-16, mu 1.5e-14, sbeta 1.4e-14, LL 2.7e-15 +# 3 iterations: A 1.9e-15, mu 1.8e-14, sbeta 1.6e-14, LL 2.7e-15 +# Before this change the A, mu and sbeta errors were the same and the +# log-likelihood was off by 3.7e-10, 2.0e-8 and -2.1e-8 (pdftype 2, 4 and 1). +_ORACLE_TOL = { + 1: {"A": 1e-13, "mu": 1e-9, "sbeta": 1e-12, "LL": 1e-12}, + 3: {"A": 1e-10, "mu": 1e-6, "sbeta": 1e-8, "LL": 1e-11}, +} + + +@pytest.fixture(scope="module") +def pre344(tmp_path_factory) -> Any: + """The pamica package at ``PRE_CHANGE_COMMIT``, imported beside the live one.""" + return load_pre_change_package( + PRE_CHANGE_COMMIT, "pamica_pre344", tmp_path_factory.mktemp("pre344") + ) + + +@pytest.mark.skipif( + os.environ.get("AMICA_RUN_FORTRAN") != "1", + reason="opt-in Fortran-binary integration test (set AMICA_RUN_FORTRAN=1)", +) +@pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") +@pytest.mark.parametrize( + "pdftype,n_mix,constant", _FAMILIES, ids=[f"pdftype{p}" for p, *_ in _FAMILIES] +) +def test_family_matches_the_seeded_reference( + pre344, pdftype, n_mix, constant, tmp_path +): + """Seed the reference with pamica's initialization for one fixed density + family, run one and three iterations, and compare ``A``/``mu``/``sbeta`` + and the log-likelihood. The family's normalizer shifts every sample's + log-density by the same amount, so it moves the log-likelihood and nothing + else: the code before this change is off by exactly the literal's rounding + in the log-likelihood, and PyTorch now matches to round-off.""" + from pamica.tests.native_oracle import ( + reference_mixing, + run_seeded_reference, + seed_from_torch, + ) + + X = load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD) + X = X.astype(np.float64) + # The reference never switches a family (its do_choose_pdfs is set at + # amica15.f90:612-613 and never read), so pdftype=1 holds code 1. + extra = dict(num_kurt=0) if pdftype == 1 else {} + + def torch_model(cls: Any) -> Any: + return cls( + n_channels=NW, + n_mix=n_mix, + pdftype=pdftype, + seed=SEED, + device="cpu", + dtype=torch.float64, + keep_best=False, + **extra, + **_OPT, + ) + + init = torch_model(AMICATorchNG) + init._preprocess(X) + init._initialize_parameters() + state = seed_from_torch(init) + assert init.sphere is not None + offset = getattr(rc, constant) - _OFFSETS[constant][0] + + for k, tol in _ORACLE_TOL.items(): + ref = run_seeded_reference( + state, + DATA_FILE, + tmp_path / f"k{k}", + n_samples=FIELD, + max_iter=k, + pdftype=pdftype, + **_REF_OPT, + ) + family = re.search(r"pdf type =\s*(\d+)", ref.stdout) + assert family is not None and int(family.group(1)) == pdftype + assert np.abs(ref.S - init.sphere.numpy()).max() < 1e-12 + + t = torch_model(AMICATorchNG) + t.fit(X, max_iter=k, verbose=False) + assert t.A is not None and t.mu is not None and t.beta is not None + # The precondition: every source kept the family the reference runs + # (for pdftype=1, no kurtosis switch ran, so none left code 1). + assert t.pdtype is not None and np.all(t.pdtype.numpy() == pdftype) + assert t.n_kurt_done == 0 + errs = { + "A": np.abs(reference_mixing(t.A.numpy()) - ref.A).max(), + "mu": np.abs(t.mu.numpy() - ref.mu).max(), + "sbeta": np.abs(t.beta.numpy() - ref.sbeta).max(), + "LL": np.abs(np.asarray(t.ll_history) - ref.LL).max(), + } + print(f"pdftype={pdftype} k={k}: {errs}") + over = {q: e for q, e in errs.items() if not e <= tol[q]} + assert not over, f"pdftype={pdftype}, {k} iteration(s): {over} (bounds {tol})" + + # The pre-change code: the same trajectory, its log-likelihood shifted + # by the literal's rounding (pamica's log-density was higher by it). + old = torch_model(pre344.torch_impl.core.AMICATorchNG) + old.fit(X, max_iter=k, verbose=False) + assert np.all(old.pdtype.numpy() == pdftype) and old.n_kurt_done == 0 + shift = np.asarray(old.ll_history) - ref.LL + print(f"pdftype={pdftype} k={k} pre-change: LL - reference = {shift}") + assert np.abs(shift - offset).max() <= tol["LL"] + assert abs(offset) > 10 * tol["LL"] + + +# --- 6. the pre-#344 substitution the byte-identity tests use -------------------- +@pytest.mark.parametrize("backend", ["torch", "numpy", "mlx"]) +def test_the_pre_344_substitution_changes_every_constant_it_patches( + backend, monkeypatch +): + """``pre_change.use_pre_344_constants`` gives a live backend the constants + it had before this change, so the tests that compare it bit for bit with + older code isolate what they were written for. A patch that changed + nothing would let those tests pass vacuously, so every patched constant + must differ from the live value, the patch must take effect, and it must + cover every changed constant the backend module reads.""" + module_name, names = pre_change._PRE_344_USERS[backend] + if backend == "mlx": + pytest.importorskip( + module_name, reason="MLX not installed (Apple Silicon only)" + ) + module = importlib.import_module(module_name) + old = pre_change._PRE_344_CONSTANTS + read = {name for name in old if hasattr(module, name)} + assert read == set(names), f"{backend} reads {read}, the patch covers {names}" + for name in names: + assert getattr(module, name) == getattr(rc, name) + assert getattr(module, name) != old[name], name + pre_change.use_pre_344_constants(monkeypatch, backend) + for name in names: + assert getattr(module, name) == old[name], name + if backend == "mlx": + # The patched rho == 2 table entry is the float32 lgamma(1.5) the + # pre-change code held. + from scipy.special import gammaln + + mx = module.mx + m = module.AMICAMLXNG(n_channels=NW, n_mix=1) + m.rho = mx.array(np.array([[2.0]], dtype=np.float32)) + m._refresh_lgamma_table() + assert np.array(m._lgamma_table)[0, 0] == np.float32(gammaln(1.5)) diff --git a/pamica/tests/torch_tests/test_ng_backend.py b/pamica/tests/torch_tests/test_ng_backend.py index efa44546..8a44b085 100644 --- a/pamica/tests/torch_tests/test_ng_backend.py +++ b/pamica/tests/torch_tests/test_ng_backend.py @@ -280,7 +280,9 @@ def test_forward_ll_per_source_factorization_order(): abs_y = np.abs(y) gg = -(abs_y ** rho[k][None, :]) - np.log(2.0) - sp.gammaln(1.0 + 1.0 / rho[k]) lap = -abs_y - np.log(2.0) - gau = -y * y - 0.5 * np.log(np.pi) + # The reference's rho == 2 normalizer, log(dble(1.772453851)): the + # literal rounded to single precision (amica15.f90:1313, issue #344). + gau = -y * y - np.log(float(np.float32(1.772453851))) log_pdf = np.where(rho[k] == 2.0, gau, np.where(rho[k] == 1.0, lap, gg)) z0[:, :, k] = np.log(alpha[k])[None, :] + np.log(beta[k])[None, :] + log_pdf diff --git a/pamica/tests/torch_tests/test_ng_pdf_families.py b/pamica/tests/torch_tests/test_ng_pdf_families.py index 830a1204..8d5e1829 100644 --- a/pamica/tests/torch_tests/test_ng_pdf_families.py +++ b/pamica/tests/torch_tests/test_ng_pdf_families.py @@ -25,11 +25,14 @@ NW = 32 FIELD = 30504 -# Fortran log-normalizer literals (amica15.f90:1333/1346/1359/1371). +# Fortran log-normalizer literals (amica15.f90:1333/1346/1359/1371). The +# binary rounds each default-kind literal to single precision before ``dble`` +# widens it (issue #344), so the reference's values are the float32 roundings; +# 4.0 is exact. _LOG4 = math.log(4.0) -_LSQ2PI = math.log(2.506628274) -_LNSUB = math.log(4.132731354) -_LNSUP = math.log(1.858073988) +_LSQ2PI = math.log(float(np.float32(2.506628274))) +_LNSUB = math.log(float(np.float32(4.132731354))) +_LNSUP = math.log(float(np.float32(1.858073988))) def _load_real_data() -> np.ndarray: diff --git a/pamica/torch_impl/core.py b/pamica/torch_impl/core.py index 296493f1..650a685c 100644 --- a/pamica/torch_impl/core.py +++ b/pamica/torch_impl/core.py @@ -70,6 +70,18 @@ pca_reduction_requested, validate_pca_reduction, ) + +# The density normalizers and the rho-update guard, at the values the binary +# uses: its single-precision literals widened to double (issue #344). +from ..reference_constants import ( + EPSDBLE, + LOG2, + LOG4, + LOG_NORM_COSH_SUB, + LOG_NORM_COSH_SUP, + LOG_SQRT_2PI, + LOG_SQRT_PI, +) from .utils import setup_device logger = logging.getLogger(__name__) @@ -85,25 +97,6 @@ 4: "sub_gaussian_cosh", } -_LOG2 = math.log(2.0) -_LOG4 = math.log(4.0) # logistic-family normalizer (amica15.f90:1346) -_HALF_LOG_PI = 0.5 * math.log(math.pi) # exact; the reference's differs (issue #344) -# Log-normalizers for the non-GG density families, from the decimal literals the -# reference writes (amica15.f90:1333/1359/1371): 2.506628274 = sqrt(2*pi) -# (Gaussian, pdtype 2); 4.132731354 / 1.858073988 = the sub-/super-Gaussian cosh -# normalizers (pdtype 4 / 1). These are the literals' double-precision values, -# not the binary's: gfortran reads ``dble(2.506628274)`` as a single-precision -# literal widened to double, so the binary's log-normalizers differ from these -# by 3.7e-10, 2.0e-8 and -2.1e-8 (computed from the literals; the same kind of -# literal in the rho == 2 branch, :1313, measurably moves the reference's -# log-likelihood). Recorded in docs/guides/amica-differences.md (row 16); -# whether to adopt the single-precision values is issue #344. -_LOG_SQRT_2PI = math.log(2.506628274) -_LOG_NORM_COSH_SUB = math.log(4.132731354) -_LOG_NORM_COSH_SUP = math.log(1.858073988) -# Fortran's epsdble (amica17_header.f90:73): the drho-numerator underflow guard -# zeros the rho*ln|y| term when |y|^rho falls below this, matching amica17.f90:1570. -_EPSDBLE = 1e-16 # Best-iterate safeguard (issue #51). The lrate schedule is deliberately # non-monotone: both NG and Fortran anneal the rate only *after* an LL decrease, @@ -161,7 +154,7 @@ def _component_rows_state(state: dict) -> dict: def _logcosh(x: torch.Tensor) -> torch.Tensor: """Numerically stable ``log cosh(x) = |x| - log2 + log1p(exp(-2|x|))``.""" ax = x.abs() - return ax - _LOG2 + torch.log1p(torch.exp(-2.0 * ax)) + return ax - LOG2 + torch.log1p(torch.exp(-2.0 * ax)) def _log_pdf_and_deriv( @@ -186,13 +179,13 @@ def _log_pdf_and_deriv( abs_y = y.abs() sign_y = torch.sign(y) - log_pdf_lap = -abs_y - _LOG2 + log_pdf_lap = -abs_y - LOG2 dpdf_lap = -sign_y * torch.exp(log_pdf_lap) - log_pdf_gau = -y * y - _HALF_LOG_PI + log_pdf_gau = -y * y - LOG_SQRT_PI dpdf_gau = -2.0 * y * torch.exp(log_pdf_gau) - log_pdf_gg = -abs_y.pow(rho) - _LOG2 - torch.lgamma(1.0 + 1.0 / rho) + log_pdf_gg = -abs_y.pow(rho) - LOG2 - torch.lgamma(1.0 + 1.0 / rho) dpdf_gg = -rho * abs_y.pow(rho - 1.0) * sign_y * torch.exp(log_pdf_gg) is_lap = rho == 1.0 @@ -207,11 +200,11 @@ def _log_pdf_and_deriv( # Non-GG families (amica15.f90:1327-1371). Each is `-cost - log_norm`, and # dpdf = -fp * exp(log_pdf). - log_pdf_2 = -0.5 * y * y - _LOG_SQRT_2PI # Gaussian - log_pdf_3 = -2.0 * _logcosh(0.5 * y) - _LOG4 # logistic (sech^2) + log_pdf_2 = -0.5 * y * y - LOG_SQRT_2PI # Gaussian + log_pdf_3 = -2.0 * _logcosh(0.5 * y) - LOG4 # logistic (sech^2) lc = _logcosh(y) - log_pdf_4 = -0.5 * y * y + lc - _LOG_NORM_COSH_SUB # sub-Gaussian cosh+ - log_pdf_1 = -0.5 * y * y - lc - _LOG_NORM_COSH_SUP # super-Gaussian cosh- + log_pdf_4 = -0.5 * y * y + lc - LOG_NORM_COSH_SUB # sub-Gaussian cosh+ + log_pdf_1 = -0.5 * y * y - lc - LOG_NORM_COSH_SUP # super-Gaussian cosh- log_pdf = torch.where( pdtype == 2, @@ -287,20 +280,20 @@ def _log_pdf_only( abs_y = y.abs() az_rho = abs_y.pow(rho) # |y|^rho, reused by the rho-update accumulator - log_pdf_lap = -abs_y - _LOG2 - log_pdf_gau = -y * y - _HALF_LOG_PI - log_pdf_gg = -az_rho - _LOG2 - torch.lgamma(1.0 + 1.0 / rho) + log_pdf_lap = -abs_y - LOG2 + log_pdf_gau = -y * y - LOG_SQRT_PI + log_pdf_gg = -az_rho - LOG2 - torch.lgamma(1.0 + 1.0 / rho) log_pdf = torch.where( rho == 2.0, log_pdf_gau, torch.where(rho == 1.0, log_pdf_lap, log_pdf_gg) ) if pdtype is None: return log_pdf, az_rho - log_pdf_2 = -0.5 * y * y - _LOG_SQRT_2PI # Gaussian - log_pdf_3 = -2.0 * _logcosh(0.5 * y) - _LOG4 # logistic (sech^2) + log_pdf_2 = -0.5 * y * y - LOG_SQRT_2PI # Gaussian + log_pdf_3 = -2.0 * _logcosh(0.5 * y) - LOG4 # logistic (sech^2) lc = _logcosh(y) - log_pdf_4 = -0.5 * y * y + lc - _LOG_NORM_COSH_SUB # sub-Gaussian cosh+ - log_pdf_1 = -0.5 * y * y - lc - _LOG_NORM_COSH_SUP # super-Gaussian cosh- + log_pdf_4 = -0.5 * y * y + lc - LOG_NORM_COSH_SUB # sub-Gaussian cosh+ + log_pdf_1 = -0.5 * y * y - lc - LOG_NORM_COSH_SUP # super-Gaussian cosh- log_pdf = torch.where( pdtype == 2, log_pdf_2, @@ -1486,7 +1479,7 @@ def zeros(*shape): ay = y.abs() ayrho = azrho_list[h] # |y|^rho reused from _forward (issue #63) logab = rho_h.unsqueeze(0) * torch.log(ay.clamp_min(tiny)) # rho*ln|y| - logab = torch.where(ayrho < _EPSDBLE, torch.zeros_like(logab), logab) + logab = torch.where(ayrho < EPSDBLE, torch.zeros_like(logab), logab) drho_n.index_add_(1, idx, (u * (ayrho * logab)).sum(0).T) g = (beta_h * ufp).sum(-1) # g_i = sum_j sbeta*ufp (:1493) From 09b824736aca230344033ec46fffcbd31e825f58 Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Wed, 23 Sep 2026 12:33:35 -0700 Subject: [PATCH 16/26] Phase 12: normalize the drawn initial mixing matrix (#350) * Run the native binary from its own draw run_drawn_reference runs the binary with nothing loaded and a fixed seed; with max_iter=0 it writes its drawn initialization. The run and read-back are shared with run_seeded_reference. * Normalize the drawn initial mixing matrix Every backend now draws its initial A through the shared pamica.initialization.initial_mixing: per model, 0.01*(0.5-u) with the diagonal set to one and each component divided by its norm, as the reference does (amica15.f90:805-823), including the NumPy restart redraw. A supplied or loaded A is used as is. Tested in later commits. * Start pre-change classes from the new init The byte-identity pins (fb13d76, 0930c0e, 2e04006) and the #339 oracle control now fit the historical class from the live normalized initial A through pre_change.with_normalized_initial_mixing, so each still isolates its own change. Tested: all those byte-identity tests pass. * Test the normalized initial mixing matrix Always-on cross-backend start checks with a pre-change control, the supplied/loaded A paths, the NumPy restart redraw, and two gated native oracles. Tested: 34 passed with AMICA_RUN_FORTRAN=1. * Re-search the MLX pdftype=1 restore recipe The normalized initial A made seed 3 at lrate 0.5 climb monotonically to max_iter, so the restore was never exercised. Seed 6 at lrate 0.4 (kurt_start 7) overshoots by 0.17 after all five switch passes, on all 12 perturbed inputs. Tested: test_mlx_keepbest.py passes. * Re-record the MLX fit-path canary The normalized initial A moved the recorded trajectory (ll_history[0] by 1.55e-5). Pins re-recorded on the M4 Pro and tolerances re-derived from the same 48-draw float32 perturbation study, floored at one float32 unit: no draw moved ll_history[0]. Tested: the canary passes. * Seed the doscaling oracle from a conditioned state From seed 42's normalized init, one sample sits 2.3e-7 from a mixture center; the mu update amplifies the 1e-14 sphere round-off to 1.3e-9 (PyTorch and NumPy differ by 7.9e-10), over the 1e-9 bound. Seed 45 stays 7x under every bound; a new precondition asserts the backends' own disagreement sits 3x below each bound. Tested with the binary. * Re-search the early-merge collapse recipe From its normalized initial A, seed 20's second model keeps gm 1.2e-3 after the second scan instead of collapsing to zero in one iteration. Seed 23 (of 23, 32, 40, 41 in seeds 0-49) collapses as the test needs. Tested with the binary (AMICA_RUN_FORTRAN=1). * Document the normalized initial mixing matrix Changelog entry with the measured behavior change, ADR 0006's remaining difference marked resolved (and ADR 0007's pointer), the differences guide's init wording and the re-searched collapse recipe. mkdocs build --strict passes. * Re-seed the MLX MIR restore test From its normalized initial A, seed 0's discarded step moved the MIR by 2.1e-4 relative (3.4e-5 to 2.1e-3 over 8 perturbed inputs), inside the 1e-4 margin on the CI GPU. Seed 7 moves it by 2.3e-3 to 2.9e-3 on every input. Tested: test_mlx_mir.py passes. * List the re-seeded MLX MIR test in the changelog * Refuse a zero-norm component in normalization normalize_components now raises ValueError on a zero or non-finite row norm instead of returning NaN. The reference path cannot reach it (the unit diagonal keeps every norm >= 1). Tested: zero and NaN rows. * Check a supplied NumPy A at fit start A supplied A (or one left by a previous fit) now raises ValueError on a wrong shape, a non-finite entry or a singular model block, through the shared validate_supplied_mixing. Before, it raised LinAlgError deep in the fit, or (NaN) ended with no stop reason. Tested: all three. * Test NumPy fix_init and the wrapper round trip fix_init (an attribute) starts from the stacked identity and leaves the generator untouched; AMICA save/load restores the PyTorch A bit for bit. Tested: test_initial_mixing.py passes. * Separate the freeze from the collapse figures The 9.0e-4 gm drop is pamica's own fit, whose A is held on those iterations; the 4.2e-3 both sides reach comes from the freeze-free continuation. Docs and test comment now say so. Docs only. * Tighten three comments from the review ADR 0006 marks the init difference as former; the _FLOOR_MARGIN comment names the worse of doscaling on and off; pre_change notes a component is a block row in both layouts. Comments only. * Record the supplied-A check in the changelog * Sphere both oracle sides with the reference S The merged-state oracle now sphers pamica's data with the binary's own sphere, so both sides start from the same bits after the Phase 12 and 13 merge. Tested: gated component-row oracles pass. * Start the pre-344 control from the new init The pre-#344 control class predates #341, so it now starts from the normalized initial A the seeded reference uses. Tested: gated family oracles pass. --- .../decisions/0006-component-orientation.md | 14 +- .../decisions/0007-component-row-layout.md | 4 +- docs/changelog.md | 42 ++ docs/guides/amica-differences.md | 14 +- pamica/initialization.py | 159 +++++ pamica/mlx_impl/core.py | 13 +- pamica/numpy_impl/core.py | 47 +- pamica/tests/mlx_tests/test_mlx_fit_noop.py | 16 +- pamica/tests/mlx_tests/test_mlx_keepbest.py | 29 +- pamica/tests/mlx_tests/test_mlx_mir.py | 20 +- pamica/tests/mlx_tests/test_mlx_transform.py | 124 ++-- pamica/tests/native_oracle.py | 111 ++- pamica/tests/pre_change.py | 105 ++- pamica/tests/test_component_rows.py | 93 ++- pamica/tests/test_doscaling_rows.py | 88 ++- pamica/tests/test_initial_mixing.py | 673 ++++++++++++++++++ .../test_iteration_order_native_oracle.py | 8 +- pamica/tests/test_reference_constants.py | 10 +- pamica/torch_impl/core.py | 8 +- 19 files changed, 1409 insertions(+), 169 deletions(-) create mode 100644 pamica/initialization.py create mode 100644 pamica/tests/test_initial_mixing.py diff --git a/.context/decisions/0006-component-orientation.md b/.context/decisions/0006-component-orientation.md index 0700ce71..cc9f8a55 100644 --- a/.context/decisions/0006-component-orientation.md +++ b/.context/decisions/0006-component-orientation.md @@ -73,10 +73,15 @@ it was applied uniformly, model by model. ADR 0003's `keep_best` variance figures were measured under the column rule and will be re-measured in the epic's final parity re-measurement. - `scalestep > 1` now rescales on iterations `scalestep`, `2*scalestep`, ... instead of 1, `1+scalestep`, ...; the default is unaffected. -- Remaining known difference: pamica does not normalize its drawn initial `A` +- Former known difference, now resolved: pamica did not normalize its drawn initial `A` (the reference normalizes a drawn one, amica15.f90:818-819, but not a loaded one). The two draws come from different random generators anyway, and the first iteration's rescale normalizes every component. - Aligning it changes default trajectories, so it is its own change, issue #341, not part of the layout change of Phase 8. + Aligning it changes default trajectories, so it was its own change, issue #341, not part of the layout change of Phase 8. + Epic #324 Phase 12 made it: every backend draws its initial `A` through `pamica.initialization.initial_mixing`, + which sets each block's diagonal to one and normalizes every component as the reference does, the NumPy restart redraw included, + and a supplied or loaded `A` is still used as is. + Default fits start from a different point and end close to where they did (the first rescale used to do the normalizing); + with `doscaling` off the change reaches the whole fit. ## Alternatives considered @@ -94,6 +99,7 @@ it was applied uniformly, model by model. :818-819 and :1039-1040 (normalization of a drawn initial and restart `A`), :793-800 (a loaded `A` is used as is), :3686-3688 (`scalestep` parsed, never read). -- Issues #333 (this change), #334 (Phase 8, component-row layout), #335 (Newton start), #24 (the storage convention), epic #324. +- Issues #333 (this change), #334 (Phase 8, component-row layout), #335 (Newton start), #341 (Phase 12, initial `A`), #24 (the storage convention), epic #324. - `pamica/tests/test_doscaling_rows.py` (invariance, cross-backend, byte-identity and the gated native oracle), - `pamica/tests/native_oracle.py` (seeded reference runs). + `pamica/tests/native_oracle.py` (seeded reference runs), + `pamica/tests/test_initial_mixing.py` (the initial `A`, with its gated oracles). diff --git a/.context/decisions/0007-component-row-layout.md b/.context/decisions/0007-component-row-layout.md index eb7ed2f1..607b9568 100644 --- a/.context/decisions/0007-component-row-layout.md +++ b/.context/decisions/0007-component-row-layout.md @@ -72,8 +72,8 @@ and refuses an `A` that does not invert the `W` beside it (a multi-model directo The reference's default `share_start` of 100 avoids this; several short test recipes were retuned. - Saved models: unmerged saves load as before; saves with merged components must be refit. Multi-model EEGLAB directories written before this change must be written again for `load_results`. -- Initial-`A` normalization is not part of this change: the reference normalizes a drawn initial `A` and pamica does not, - which changes default trajectories and is issue #341. +- Initial-`A` normalization is not part of this change: the reference normalizes a drawn initial `A` and pamica did not, + which changes default trajectories and is issue #341 (epic #324 Phase 12, see ADR 0006). ## Alternatives considered diff --git a/docs/changelog.md b/docs/changelog.md index 44027186..d128ba22 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,48 @@ Release notes are also published on the ## Unreleased +- **Phase 12 of epic #324: the drawn initial mixing matrix has unit-norm components, as in the reference (issue #341).** + The reference draws each model's block of `A` as `0.01 * (0.5 - u)`, sets its diagonal to one + and divides every component by its Euclidean norm (amica15.f90:805-823); + its restart after a non-finite likelihood redraws the same way (:1026-1044). + pamica drew `I + 0.01 * (0.5 - u)` and never normalized it, + so its initial components had norms up to about 5e-3 away from one. + Every backend (PyTorch, NumPy and MLX) now draws its initial `A` through one shared function, + `pamica.initialization.initial_mixing`, which follows the reference's recipe, + and the NumPy restart after a non-finite likelihood redraws through it too. + The generator is called exactly as before, so `mu` and `beta` start from the same values. + A supplied or loaded `A` is used as is, as the reference uses a loaded one (:793-802): + an `A` set on a NumPy model before `fit`, a NumPy refit, a PyTorch `state_dict`, a saved `AMICA` model and an MLX save. + - **Behavior change (NumPy backend): a supplied initial `A` is checked at fit start.** + An `A` set before `fit`, or left by a previous fit on the same instance, now raises `ValueError` naming the problem + when its shape is not `(num_comps, n_channels)` (one row per component), when it holds a non-finite entry, + or when a model's block is numerically singular. + Before, a wrong shape or a singular block raised `LinAlgError` from deep in the fit, + and a NaN entry ended the fit with no stop reason. + The PyTorch and MLX backends take an `A` only through their saves, which are already validated. + - **Behavior change: every fit from a drawn `A` starts from a different point.** + Normalizing is not a compensated rescale, so it changes the first E-step and the trajectory after it. + With `doscaling` on (the default), the first rescale used to normalize the components after the first iteration anyway, + so default fits end close to where they did: + on the bundled sample (PyTorch, seed 42, 100 iterations), the first log-likelihood moves by 2.7e-5 with one model and 1.7e-6 with two, + and the final one by 1.8e-6 and 4.5e-4. + With `doscaling=False` the initial scale was never corrected, so the change reaches the whole fit + (the final log-likelihood moves by 6.6e-7 and 9.2e-4). + The validation harness (100 iterations, all three backends, against the native binary from its own initialization) is unchanged at its precision: + mean matched correlation 0.9991 and Amari distance 0.0038 before and after, + and a log-likelihood gap of 2.7e-4 to 2.8e-4 (2.6e-4 to 2.7e-4 before). + - The draw itself still cannot match the reference's, whose generator is gfortran's `random_number`. + Two new gated oracles check the two halves instead: + the binary's own drawn initialization (written by a run with `max_iter=0`) has unit-norm components with the recipe's shape, + and the binary, loaded with pamica's draw before normalization and with its `A` update off, + normalizes it to pamica's initial `A` within 4.4e-16. + - Tests: `pamica/tests/test_initial_mixing.py` checks the recipe, the start of every fit on every backend + (with a control that fails on the code before this change), the supplied and loaded paths and the refused ones, NumPy's `fix_init`, the NumPy restart redraw, and the two gated oracles. + The byte-identity tests that pin earlier changes against older commits now start the older code from the new initial `A` + (`pamica.tests.pre_change.with_normalized_initial_mixing`), while the live side keeps its pre-#344 constants (Phase 13), + and still pass bit for bit. + Data-driven tests whose trajectories moved were re-searched or re-recorded: + the MLX `pdftype=1` restore recipe, the MLX MIR restore test, the MLX fit-path canary, the seed of the `doscaling` native oracle, and the early-merge collapse oracle. - **Phase 13 of epic #324: every backend uses the reference's single-precision constants (issue #344).** The reference writes several density normalizers as default-kind Fortran literals widened with `dble`, for example `log(dble(2.506628274))`, so the binary uses the float32 rounding of each decimal, not the decimal. diff --git a/docs/guides/amica-differences.md b/docs/guides/amica-differences.md index b5187980..d8f11205 100644 --- a/docs/guides/amica-differences.md +++ b/docs/guides/amica-differences.md @@ -470,11 +470,13 @@ Two consequences to know: but its similarity with `Spinv2 = Spinv^T Spinv`, applied to the binary's own state after 8 iterations from pamica's initialization, merges exactly the pairs the PyTorch and NumPy scans merge: 32, 32 and 30 at `comp_thresh` 0.9, 0.95 and 0.99. - A model left with few components of its own then loses its responsibility, and can end in a non-finite fit. - In a short recipe (4096 samples, seed 20, `share_start=11`, `share_iter=11`, `comp_thresh=0.9`), 25 merges at iteration 11 - dropped the second model's `gm` from 0.53 to 6.0e-4 within two iterations, and the fit went non-finite at iteration 25. - Seeded with the same merged states, the reference's update does the same: - the second model's `gm` falls to 6.0e-4 within two iterations, as pamica's does from the same state, - and from the state after the second scan (iteration 22) it reaches zero in one iteration and the binary reports NaN and reinitializes. + In a short recipe (4096 samples, seed 23, `share_start=11`, `share_iter=11`, `comp_thresh=0.9`), 28 merges at iteration 11 + dropped the second model's `gm` from 0.57 to 9.0e-4 within two iterations + (in pamica's own fit, whose `A` is held on those iterations, amica15.f90:1803), and the fit went non-finite in iteration 23. + Seeded with the same merged states and run without the `A`-freeze, the reference's update behaves the same way: + from the state after the first scan it drops the second model's `gm` to 4.2e-3 within two iterations, + matching pamica's update rule run without the freeze from that state (not the 9.0e-4 above, which includes the freeze), + and from the state after the second scan (iteration 22) it reaches zero in one iteration, as pamica's does, and the binary reports NaN and reinitializes. This is the algorithm on models that have not yet separated, not a defect of the port. The reference's default `share_start=100` avoids it; keep `share_start` well past the first iterations. - Saved models: a PyTorch `state_dict` (now `format_version` 4) or MLX save (now format 2) from an earlier version @@ -662,7 +664,7 @@ so they fit the data unscaled. **Consequences.** A `do_sphere=False` fit is not trajectory-comparable to the binary's: -the natural-gradient EM is not scale-invariant through its `A = I + noise` initialization and its step sizes, so the two fit differently scaled data. +the natural-gradient EM is not scale-invariant through its fixed near-identity initialization (unit-norm components, whatever the data's scale) and its step sizes, so the two fit differently scaled data. The reported log-likelihood also differs by the log-determinant term. That term looks inconsistent in the reference itself: `log|det S|` for `S = diag(1/sqrt(var_i))` is `-0.5 * sum_i log(var_i)`, diff --git a/pamica/initialization.py b/pamica/initialization.py new file mode 100644 index 00000000..039359fb --- /dev/null +++ b/pamica/initialization.py @@ -0,0 +1,159 @@ +"""The reference's initial mixing matrix, shared by every array backend +(issue #341, epic #324 Phase 12). + +The reference draws each model's ``nw x nw`` block of ``A`` uniformly around +zero, sets the block's diagonal to one and divides every component by its +Euclidean norm (amica15.f90:805-823); its restart after a non-finite likelihood +redraws the same way (:1026-1044). With ``fix_init`` it starts from the +identity instead, whose components already have unit norm. A loaded ``A`` +(``load_A``, :793-802) is used as is, without normalization. + +Every array backend (PyTorch, NumPy, MLX) draws its initial ``A`` here, in +float64, so the three agree bit for bit (MLX casts the result to float32), and +each draws from its NumPy generator in the order it always did: one ``n x n`` +uniform block per model, before ``mu`` and ``beta``. A backend that honors a +supplied ``A`` (the NumPy backend initializes a parameter only while it is +``None``) never calls this for it, so a supplied ``A`` is not normalized either. + +The draw itself cannot match the reference's: the binary draws from gfortran's +``random_number``, pamica from ``numpy.random.RandomState``. Both fill component +``i`` from the ``i``-th run of ``n`` values in the stream (the reference's +``Wtmp`` is column-major and its columns are components; pamica's draw is +row-major and its rows are components, ADR 0007). +""" + +from __future__ import annotations + +import numpy as np + +# The reference's off-diagonal draw is 0.01 * (0.5 - u), u uniform on [0, 1): +# every off-diagonal entry of a block lies in (-0.005, 0.005] before the +# normalization (amica15.f90:814). +OFF_DIAGONAL_SCALE = 0.01 + + +def draw_initial_block(rng: np.random.RandomState, n: int) -> np.ndarray: + """One model's ``n x n`` block before normalization. + + ``0.01 * (0.5 - u)`` from one ``rng.rand(n, n)`` call, with the diagonal + then set to exactly one (amica15.f90:812-816). Row ``i`` is component ``i``. + """ + block = OFF_DIAGONAL_SCALE * (0.5 - rng.rand(n, n)) + block[np.diag_indices(n)] = 1.0 + return block + + +def normalize_components(block: np.ndarray) -> np.ndarray: + """``block`` with every row (component) divided by its Euclidean norm, + ``A(:,k) / sqrt(sum(A(:,k)*A(:,k)))`` in the reference (amica15.f90:818-819). + + The reference has no zero-norm guard here, and needs none: a block from + :func:`draw_initial_block` has a unit diagonal, so every norm is at least 1. + A direct call on any other block could still divide by a zero or + non-finite norm, so that raises instead of returning NaN rows; it cannot + happen on the reference's path, so the arithmetic there is unchanged. + + Raises + ------ + ValueError + If a row's norm is zero or not finite. + """ + norm = np.sqrt(np.sum(block * block, axis=1)) + bad = np.flatnonzero(~(np.isfinite(norm) & (norm > 0))) + if bad.size: + raise ValueError( + f"cannot normalize component row(s) {bad.tolist()}: their norm is " + "zero or not finite" + ) + return block / norm[:, None] + + +def initial_mixing( + rng: np.random.RandomState, n: int, n_models: int, *, fix_init: bool = False +) -> np.ndarray: + """The initial component-row ``A`` of shape ``(n_models * n, n)``. + + Model ``h``'s block is rows ``h*n`` to ``(h+1)*n - 1``, the default + ``comp_list``. For each model in turn (amica15.f90:805-823): with + ``fix_init`` the identity, drawing nothing; otherwise + :func:`draw_initial_block` normalized by :func:`normalize_components`. + + Parameters + ---------- + rng : numpy.random.RandomState + The generator to draw from; it advances by ``n * n`` values per model + (none with ``fix_init``). + n : int + Components per model (the kept channel count). + n_models : int + Number of models. + fix_init : bool, default False + Start every model from the identity, as the reference's ``fix_init``. + + Returns + ------- + ndarray of shape (n_models * n, n), float64 + Every row has unit norm. + """ + A = np.zeros((n_models * n, n)) + for h in range(n_models): + rows = slice(h * n, (h + 1) * n) + if fix_init: + A[rows, :] = np.eye(n) + else: + A[rows, :] = normalize_components(draw_initial_block(rng, n)) + return A + + +def validate_supplied_mixing( + A: np.ndarray, shape: tuple, comp_list: np.ndarray, *, owner: str +) -> None: + """Refuse an initial ``A`` that a fit could not start from. + + A backend that honors a supplied initial ``A`` uses it as is, like the + reference's loaded ``A`` (amica15.f90:793-802), so it is checked here + instead: at fit start, with a message naming the problem, rather than as + a ``LinAlgError`` or a non-finite likelihood deep in the fit. + + Parameters + ---------- + A : array_like + The supplied initial mixing matrix, one component per row. + shape : tuple of int + The shape the fit needs, ``(n_comps, n)``. + comp_list : ndarray of int, shape (n, n_models) + The component each source of each model uses; model ``h``'s block is + ``A[comp_list[:, h], :]``. + owner : str + The class name the messages name. + + Raises + ------ + ValueError + If ``A`` has the wrong shape, holds a non-finite entry, or a model's + block is numerically singular (``numpy.linalg.matrix_rank`` below its + size), so that its unmixing matrix cannot be computed. + """ + A = np.asarray(A) + if A.shape != tuple(shape): + raise ValueError( + f"{owner}: the supplied initial A has shape {A.shape}, expected " + f"{tuple(shape)}: one row per component, each as long as the kept " + "channel count (a mixing matrix stored with components as columns " + "must be transposed)" + ) + bad_rows = np.flatnonzero(~np.isfinite(A).all(axis=1)) + if bad_rows.size: + raise ValueError( + f"{owner}: the supplied initial A has non-finite entries in " + f"component row(s) {bad_rows.tolist()}" + ) + n = comp_list.shape[0] + for h in range(comp_list.shape[1]): + rank = int(np.linalg.matrix_rank(A[comp_list[:, h], :])) + if rank < n: + raise ValueError( + f"{owner}: model {h}'s block of the supplied initial A (component " + f"rows {comp_list[:, h].tolist()}) is numerically singular " + f"(rank {rank} of {n}), so its unmixing matrix cannot be computed" + ) diff --git a/pamica/mlx_impl/core.py b/pamica/mlx_impl/core.py index 42029c47..90fd5d68 100644 --- a/pamica/mlx_impl/core.py +++ b/pamica/mlx_impl/core.py @@ -110,6 +110,7 @@ from .. import restarts from .. import schedule from ..component_layout import rows_from_legacy_columns +from ..initialization import initial_mixing from ..metrics import mir as mir_metric from ..metrics import model_probability_from_loglik, pairwise_mi from ..numpy_impl.utils import identify_shared_components @@ -1127,14 +1128,14 @@ def _initialize_parameters(self): rng = np.random.RandomState(self.seed) n, m, ncomp, nmix = self.n_channels, self.n_models, self.n_comps, self.n_mix - # Per-model mixing blocks + comp_list mapping each (source, model) to its - # component, a row of A (issue #334; identical RNG draw order to - # AMICATorchNG; for m=1 the loop runs once, so single-model init stays - # byte-for-byte). - A_np = np.zeros((ncomp, n)) + # Per-model mixing blocks, drawn and normalized to unit-norm components + # as the reference does (issue #341, pamica.initialization), in float64 + # before the float32 cast; comp_list maps each (source, model) to its + # component, a row of A (issue #334). Identical RNG draw order to + # AMICATorchNG. + A_np = initial_mixing(rng, n, m) comp_list_np = np.zeros((n, m), dtype=np.int64) for h in range(m): - A_np[h * n : (h + 1) * n, :] = np.eye(n) + 0.01 * (0.5 - rng.rand(n, n)) comp_list_np[:, h] = np.arange(h * n, (h + 1) * n) mu_np = np.zeros((nmix, ncomp)) diff --git a/pamica/numpy_impl/core.py b/pamica/numpy_impl/core.py index 5004abb3..0b62c0eb 100644 --- a/pamica/numpy_impl/core.py +++ b/pamica/numpy_impl/core.py @@ -99,6 +99,7 @@ from .. import restarts from .. import schedule from ..fortran_params import read_params_file +from ..initialization import initial_mixing, validate_supplied_mixing from ..rank import ( MINEIG, MINEIG_REL, @@ -1735,27 +1736,39 @@ def _preprocess_data(self, data: np.ndarray): def _initialize_parameters(self): """Initialize all model parameters.""" assert self.data_dim is not None - # Initialize mixing/unmixing matrices. A holds one component per row - # (issue #334, pamica.component_layout): model h's block is rows - # h*data_dim..(h+1)*data_dim-1 under the default comp_list below. - if self.A is None: - self.A = np.zeros((self.num_comps, self.data_dim)) - for h in range(self.num_models): - if not hasattr(self, "fix_init") or not self.fix_init: - self.A[h * self.data_dim : (h + 1) * self.data_dim, :] = np.eye( - self.data_dim - ) + 0.01 * (0.5 - self.rng.rand(self.data_dim, self.data_dim)) - else: - self.A[h * self.data_dim : (h + 1) * self.data_dim, :] = np.eye( - self.data_dim - ) - # Initialize component assignments self.comp_list = np.zeros((self.data_dim, self.num_models), dtype=int) self.comp_used = np.ones(self.num_comps, dtype=bool) for h in range(self.num_models): self.comp_list[:, h] = np.arange(h * self.data_dim, (h + 1) * self.data_dim) + # Initialize mixing/unmixing matrices. A holds one component per row + # (issue #334, pamica.component_layout): model h's block is rows + # h*data_dim..(h+1)*data_dim-1 under the default comp_list above. A + # drawn A is normalized to unit-norm components as the reference does + # (issue #341, pamica.initialization), here and in the restart after a + # non-finite likelihood, which redraws through this method. An A + # supplied before fit() (or left by a previous fit on this instance) is + # used as is, like the reference's loaded A, once it is checked: a + # wrong shape, a non-finite entry or a singular model block raises + # ValueError here instead of failing deep in the fit. + if self.A is None: + drawn = initial_mixing( + self.rng, + self.data_dim, + self.num_models, + fix_init=bool(getattr(self, "fix_init", False)), + ) + self.A = np.zeros((self.num_comps, self.data_dim)) + self.A[: drawn.shape[0], :] = drawn + else: + validate_supplied_mixing( + self.A, + (self.num_comps, self.data_dim), + self.comp_list, + owner="AMICA_NumPy", + ) + # Outlier-rejection state (do_reject): start with every sample good # (good_idx = all indices), mirroring AMICATorchNG. num_good_samples # drives the gm/LL normalization and equals num_samples until the first @@ -1813,7 +1826,9 @@ def _reinitialize_for_restart(self): Matches Fortran's restart path (amica15.f90:1026-1046): it re-draws *only* the mixing matrix ``A`` (from the already-advanced RNG, a new - random basin) and recomputes ``comp_list``/``W``; the last-successful + random basin), normalizes each redrawn component to unit norm as the + reference does (:1039-1040, issue #341), and recomputes + ``comp_list``/``W``; the last-successful mixture parameters (``mu``/``alpha``/``beta``/``rho``/``gm``/``c``) are kept, not cold-reset. The learning rate and the LL/gradient-norm history are reset so the restarted run is judged from scratch; preprocessing diff --git a/pamica/tests/mlx_tests/test_mlx_fit_noop.py b/pamica/tests/mlx_tests/test_mlx_fit_noop.py index 8cf91d59..424e6f01 100644 --- a/pamica/tests/mlx_tests/test_mlx_fit_noop.py +++ b/pamica/tests/mlx_tests/test_mlx_fit_noop.py @@ -32,6 +32,13 @@ (:func:`pamica.component_layout.rows_from_legacy_columns`) before the bit-for-bit comparison; every per-model block, and so every other array, is unchanged. +Issue #341 (epic #324 Phase 12) normalizes the drawn initial ``A`` to unit-norm +components, as the reference does, which changes every trajectory from its +first E-step. The historical class therefore starts from the live normalized +initial ``A`` +(:func:`pamica.tests.pre_change.with_normalized_initial_mixing`), and every +other part of the fit path is still compared bit for bit. + Issue #344 (epic #324 Phase 13) gave every backend the reference's single-precision density constants, and these fits reach ``rho == 2``, where the generalized Gaussian's normalizer is one of them, so the live class runs @@ -54,6 +61,7 @@ from pamica.tests.pre_change import ( # noqa: E402 load_pre_change_package, use_pre_344_constants, + with_normalized_initial_mixing, ) SAMPLE_DIR = Path(__file__).resolve().parents[2] / "sample_data" @@ -87,12 +95,14 @@ def real_data() -> np.ndarray: @pytest.fixture(scope="module") def historical_amicamlxng(tmp_path_factory): """The pre-Phase-3 ``AMICAMLXNG`` class: the package at ``_EPIC_TIP``, - imported beside the live one. A clone without that commit fails under - ``CI`` and otherwise skips with the command that fetches it.""" + imported beside the live one, starting from the live normalized initial + ``A`` (issue #341; module docstring). A clone without that commit fails + under ``CI`` and otherwise skips with the command that fetches it.""" old = load_pre_change_package( _EPIC_TIP, "pamica_pre289", tmp_path_factory.mktemp("pre289") ) - return importlib.import_module(f"{old.__name__}.mlx_impl.core").AMICAMLXNG + mlx_core = importlib.import_module(f"{old.__name__}.mlx_impl.core") + return with_normalized_initial_mixing(mlx_core.AMICAMLXNG) _PARAM_NAMES = ( diff --git a/pamica/tests/mlx_tests/test_mlx_keepbest.py b/pamica/tests/mlx_tests/test_mlx_keepbest.py index 0aa70e4e..abe082ad 100644 --- a/pamica/tests/mlx_tests/test_mlx_keepbest.py +++ b/pamica/tests/mlx_tests/test_mlx_keepbest.py @@ -35,19 +35,24 @@ ``maxincs=2`` stop before it ended wherever three small gains in a row fell, below the peak or at it depending on round-off (at it in 1 of those 12). * ``_FORCED_RESTORE_PDFTYPE1_KWARGS``: the ``pdftype=1``/``n_mix=1`` analogue - (seed 3, ``kurt_start=7``) chosen so the adaptive switcher's first kurtosis - re-evaluation (run at the end of iteration 7, after that iteration's E-step) - comes strictly AFTER the E-step of the peak iterate (0-based ll index 6), - while the run lasts long enough for all five passes (it stops via ``min_dll`` - at ll index 19, 0.26 below the peak): the scenario + (seed 6, ``lrate=0.4``, ``kurt_start=7``) chosen so the adaptive switcher's + first kurtosis re-evaluation (run at the end of iteration 7, after that + iteration's E-step) comes strictly AFTER the E-step of the peak iterate + (0-based ll index 6), while the run lasts long enough for all five passes + (it stops via ``min_dll`` at ll index 18, 0.17 below the peak): the scenario ``test_pdftype1_forced_restore_rolls_back_n_kurt_done_with_pdtype`` needs to prove the restore rolls ``n_kurt_done`` back in step with ``pdtype``, not just the floating-point arrays. (``kurt_start=8`` served until issue #333, - seed 2 with ``kurt_start=6`` until issue #339, whose earlier decrease - response no longer lets that run overshoot. Seeds 0-5 and ``kurt_start`` - 3-11 were searched; seed 1 with ``kurt_start=8`` also qualifies, and most - other overshooting runs end in a ``nan_params`` stop, which skips the - restore.) + seed 2 with ``kurt_start=6`` until issue #339, and seed 3 with + ``lrate=0.5`` until issue #341, whose normalized initial ``A`` makes that run + climb monotonically to ``max_iter``. At ``lrate=0.5``, seeds 0-11 and + ``kurt_start`` 3-11 were searched and only seed 1 with ``kurt_start=11`` + qualified, after its log-likelihood fell by 8.6e3; most other overshooting + runs end in a ``nan_direction`` or ``nan_params`` stop, which skips the + restore. The same search at ``lrate`` 0.3, 0.4, 0.6 and 0.7 found eight runs + with a sane overshoot (0.06 to 0.19), each of which qualified on all 12 + relative data perturbations of 1e-6; this one keeps ``kurt_start=7`` and + the peak at index 6.) The truncated-refit bit-identity check (both here and in the pdftype=1 test) uses ``max_iter=argmax`` where ``argmax = int(np.argmax(ll_history))``: a @@ -154,11 +159,11 @@ def _model(**kwargs: Any) -> AMICAMLXNG: n_models=2, n_mix=1, pdftype=1, - seed=3, + seed=6, block_size=BLOCK, do_newton=True, newt_start=2, - lrate=0.5, + lrate=0.4, use_min_dll=True, min_dll=1e-4, maxincs=2, diff --git a/pamica/tests/mlx_tests/test_mlx_mir.py b/pamica/tests/mlx_tests/test_mlx_mir.py index 9721d3ff..e18588a7 100644 --- a/pamica/tests/mlx_tests/test_mlx_mir.py +++ b/pamica/tests/mlx_tests/test_mlx_mir.py @@ -356,7 +356,7 @@ def test_mir_history_survives_keep_best_restore(real_data): """mir_history_ is a TRUE trajectory that a keep_best restore does NOT rewrite: its last entry is computed from the pre-restore, discarded parameters, not the restored ones fit() actually returns. Uses the - same forced-overshoot recipe as test_mlx_llt_stash.py, with + same forced-overshoot recipe as test_mlx_llt_stash.py (from seed 7), with mir_step=1 so a waypoint lands strictly inside the truncation window a buggy restore would damage (mirrors test_ng_convergence.py's identically-named test and its mir_step=1 rationale). @@ -364,17 +364,21 @@ def test_mir_history_survives_keep_best_restore(real_data): Since issue #339 the min_dll stop exits before its own update, so the last waypoint is the one right after the restored iterate, and the recipe stops on its first likelihood decrease (``maxincs=0``, - ``min_dll=1e-8``): iteration 14, 1.6e-3 below the peak at 13. The MIR of - that one discarded step moves by 9.4e-4 relative, well past the 1e-4 - float32 margin below, and by 8.2e-4 to 1.1e-3 across 8 relative data - perturbations of 1e-6. Seed 1 with ``min_dll=1e-4``/``maxincs=2``, the - choice from issue #333 until then, moved it by 2.5e-5 under the new - order.""" + ``min_dll=1e-8``): iteration 15, 2.5e-2 below the peak at 14. The MIR of + that one discarded step moves by 2.5e-3 relative, well past the 1e-4 + float32 margin below, and by 2.3e-3 to 2.9e-3 across 8 relative data + perturbations of 1e-6. Seed 0 served until issue #341: from its + normalized initial A the step moved the MIR by only 2.1e-4 (3.4e-5 to + 2.1e-3 across the same perturbations, and inside the margin on a CI + GPU), where it had moved it by 9.4e-4 before; of seeds 0-11, seed 7 has + the largest smallest shift over the perturbations. Seed 1 with + ``min_dll=1e-4``/``maxincs=2``, the choice from issue #333 until issue + #339, moved it by 2.5e-5 under the new order.""" m = AMICAMLXNG( n_channels=NW, n_models=2, n_mix=NMIX, - seed=0, + seed=7, block_size=BLOCK, do_newton=True, newt_start=2, diff --git a/pamica/tests/mlx_tests/test_mlx_transform.py b/pamica/tests/mlx_tests/test_mlx_transform.py index 6cce089f..a90c8edf 100644 --- a/pamica/tests/mlx_tests/test_mlx_transform.py +++ b/pamica/tests/mlx_tests/test_mlx_transform.py @@ -290,19 +290,29 @@ def test_get_rho_raises_on_force_set_nan_rho(): # 0.95371497, A[10,20] -0.0037135077, A[31,31] 0.99918866, A[0,31] # 0.032124873; ll_history[0] is unchanged (it is computed before the first # rescale). +# +# Re-recorded again on the same machine for issue #341 (epic #324 Phase 12), +# which deliberately normalizes each drawn initial component to unit norm, as +# the reference does. The pre-#341 recording (reproduced exactly on this +# machine before the change) was ll_history -3.3320186138153076, +# -3.282797336578369, -3.2743287086486816, -3.269350528717041, +# -3.2649497985839844, -3.260986089706421, -3.2576255798339844, +# -3.254916191101074, -3.252664566040039, -3.2506353855133057 (the last also +# final_ll_) with A[0,0] 0.9853508, A[5,5] 0.99289405, A[10,20] +# -0.0037210553, A[31,31] 0.99681115, A[0,31] 0.0327551. _NOOP_PIN_LL_HISTORY = [ - -3.3320186138153076, - -3.282797336578369, - -3.2743287086486816, - -3.269350528717041, - -3.2649497985839844, - -3.260986089706421, - -3.2576255798339844, - -3.254916191101074, - -3.252664566040039, - -3.2506353855133057, + -3.332034111022949, + -3.282749891281128, + -3.2743053436279297, + -3.2693262100219727, + -3.264932155609131, + -3.260972738265991, + -3.2576169967651367, + -3.254908800125122, + -3.2526557445526123, + -3.2506275177001953, ] -_NOOP_PIN_FINAL_LL = -3.2506353855133057 +_NOOP_PIN_FINAL_LL = -3.2506275177001953 _NOOP_PIN_STOP_REASON = "max_iter" # A handful of representative A entries (two diagonal, two off-diagonal, one # corner), replacing the previous SHA-256 hash of the full A/W arrays: a @@ -310,11 +320,11 @@ def test_get_rho_raises_on_force_set_nan_rho(): # but these entries still catch a real fit-path change within tolerances that # survive cross-GPU float32 noise. _NOOP_PIN_A_ENTRIES = { - (0, 0): 0.9853508, - (5, 5): 0.99289405, - (10, 20): -0.0037210553, - (31, 31): 0.99681115, - (0, 31): 0.0327551, + (0, 0): 0.98533404, + (5, 5): 0.992889, + (10, 20): -0.0037056825, + (31, 31): 0.9968118, + (0, 31): 0.03281356, } # Tolerances from a float32-rounding perturbation study, not hand-picked. The @@ -323,48 +333,63 @@ def test_get_rho_raises_on_force_set_nan_rho(): # normal (numpy default_rng(20260922)): the scale of the accumulation-order # differences between GPU models. # Recorded below is the largest deviation from the unperturbed fit over those -# draws (Apple M4 Pro); each tolerance is _PIN_SAFETY times it. Both -# cross-GPU deviations seen on CI sat inside the study's maxima: ll_history[0] -# at ~7e-8 relative (study 7.2e-8) and A[0,0] at 9.2e-6 (study 1.9e-5). The -# earlier flat 5e-6 relative tolerance had no margin (the study's largest -# log-likelihood deviation is 5.0e-6) and failed on CI at A[0,0]. +# draws (Apple M4 Pro); each tolerance is _PIN_SAFETY times it, and never less +# than _PIN_SAFETY float32 units in the last place of the recorded value, the +# resolution of a float32 result. That floor matters for ll_history[0] alone: +# since issue #341 no draw moved it at all (nor any of 192 draws of the same +# generator), while a GPU with another accumulation order can move it by one +# unit, as CI once did (7e-8 relative). Both cross-GPU deviations seen on CI +# before issue #341 sat inside that study's maxima: ll_history[0] at ~7e-8 +# relative (study 7.2e-8) and A[0,0] at 9.2e-6 (study 1.9e-5). The earlier +# flat 5e-6 relative tolerance had no margin (that study's largest +# log-likelihood deviation was 5.0e-6) and failed on CI at A[0,0]. The same +# study with 192 draws (its first 48 are these) stays inside every tolerance +# below: its largest deviations are at most 1.8 times these (ll_history[5]). # # The canary still catches a real fit-path change. Issue #333's doscaling -# change moved A[0,0] by 1.0e-1 (1800x its tolerance), A[5,5] by 3.9e-2 -# (8800x), A[31,31] by 2.4e-3 (530x), A[0,31] by 6.3e-4 (11x), and -# ll_history[2] and [3] by 1.3e-5 and 1.6e-5 relative (15x and 12x). The -# discriminating power therefore rests on the three diagonal entries: A[10,20] -# (moved 7.5e-6, 1.3x) and ll_history[0] and [1] (unchanged or under 1x) sit -# inside float32 noise for that change and are kept as regression guards only. +# change moved A[0,0] by 1.0e-1, A[5,5] by 3.9e-2, A[31,31] by 2.4e-3 and +# A[0,31] by 6.3e-4, far outside the tolerances of the time, while A[10,20] +# and ll_history[0] and [1] sat inside float32 noise for that change. Issue +# #341's normalized initial A moved ll_history[0] by 1.55e-5 (22 times its +# tolerance below), ll_history[1] to [4] by 4.1 to 8.5 times theirs and A[0,31] +# by 5.8 times; the later log-likelihoods and the other A entries moved inside +# their tolerances, so they are regression guards only for that change. _PIN_SAFETY = 3.0 _STUDY_MAX_REL_DLL = [ - 7.16e-08, - 1.38e-06, - 2.91e-07, - 4.38e-07, - 2.63e-06, - 4.09e-06, - 4.98e-06, - 3.66e-06, - 3.23e-06, - 4.11e-06, + 0.0, + 1.09e-06, + 5.83e-07, + 2.92e-07, + 2.19e-07, + 1.83e-06, + 4.68e-06, + 6.67e-06, + 4.91e-06, + 4.03e-06, ] -_STUDY_MAX_REL_DFINAL = 4.11e-06 +_STUDY_MAX_REL_DFINAL = 4.03e-06 _STUDY_MAX_DA = { - (0, 0): 1.87e-05, - (5, 5): 1.49e-06, - (10, 20): 1.90e-06, - (31, 31): 1.49e-06, - (0, 31): 1.87e-05, + (0, 0): 1.76e-05, + (5, 5): 1.79e-06, + (10, 20): 1.34e-05, + (31, 31): 1.25e-06, + (0, 31): 3.36e-06, } +def _tolerance(study_dev: float, expected: float) -> float: + """``_PIN_SAFETY`` times the study's absolute deviation, floored at one + float32 unit in the last place of ``expected`` (the study's resolution).""" + ulp = float(np.spacing(np.float32(abs(expected)))) + return _PIN_SAFETY * max(study_dev, ulp) + + def _assert_within(actual: float, expected: float, tol: float, *, label: str) -> None: diff = abs(actual - expected) assert diff <= tol, ( f"{label}: {actual!r} differs from the recorded {expected!r} by " f"{diff:.3e}, over its {tol:.3e} tolerance ({_PIN_SAFETY:g} x the " - "float32-rounding study's largest deviation)" + "float32-rounding study's largest deviation, or of one float32 unit)" ) @@ -381,7 +406,8 @@ def test_fit_path_is_unchanged_by_phase1(): this CI-facing version checks agreement with the recorded M4 Pro values instead, each entry within _PIN_SAFETY times the largest deviation a float32-rounding perturbation of the input produced (the study in the - module-level comment): ``ll_history``/``final_ll_`` relative, the five + module-level comment), and never within less than _PIN_SAFETY float32 + units in the last place: ``ll_history``/``final_ll_`` relative, the five ``A`` spot entries absolute. ``stop_reason`` and the trajectory length are still exact -- both machine-independent. """ @@ -394,13 +420,15 @@ def test_fit_path_is_unchanged_by_phase1(): for it, (actual, expected, dev) in enumerate( zip(m.ll_history, _NOOP_PIN_LL_HISTORY, _STUDY_MAX_REL_DLL) ): - tol = _PIN_SAFETY * dev * abs(expected) + tol = _tolerance(dev * abs(expected), expected) _assert_within(actual, expected, tol, label=f"ll_history[{it}]") assert m.final_ll_ is not None - tol = _PIN_SAFETY * _STUDY_MAX_REL_DFINAL * abs(_NOOP_PIN_FINAL_LL) + tol = _tolerance( + _STUDY_MAX_REL_DFINAL * abs(_NOOP_PIN_FINAL_LL), _NOOP_PIN_FINAL_LL + ) _assert_within(m.final_ll_, _NOOP_PIN_FINAL_LL, tol, label="final_ll_") a = np.array(m.A, dtype=np.float32) for (i, j), expected in _NOOP_PIN_A_ENTRIES.items(): - tol = _PIN_SAFETY * _STUDY_MAX_DA[(i, j)] + tol = _tolerance(_STUDY_MAX_DA[(i, j)], expected) _assert_within(float(a[i, j]), expected, tol, label=f"A[{i},{j}]") diff --git a/pamica/tests/native_oracle.py b/pamica/tests/native_oracle.py index e0e6ce99..c9ae2d69 100644 --- a/pamica/tests/native_oracle.py +++ b/pamica/tests/native_oracle.py @@ -216,11 +216,7 @@ def run_seeded_reference( rec.tofile(indir / "c") load = {"load_c": 0, "load_comp_list": 1} - link = workdir / "data.fdt" - if link.is_symlink() or link.exists(): - link.unlink() - link.symlink_to(Path(data_file).resolve()) - + _link_data(workdir, data_file) param = { # `files` must come first: amica15.f90 stops if other keys precede it. "files": "./data.fdt", @@ -247,6 +243,109 @@ def run_seeded_reference( **load, **params, } + return _run_reference( + param, + workdir, + nw=nw, + num_comps=num_comps, + num_mix=num_mix, + num_models=num_models, + max_iter=max_iter, + threads=threads, + timeout=timeout, + binary=binary, + ) + + +def run_drawn_reference( + data_file: Path, + workdir: Path, + *, + nw: int, + n_samples: int, + num_models: int, + num_mix: int, + seed: int, + max_iter: int = 0, + threads: int = 2, + timeout: float = 600.0, + binary: Optional[Path] = None, + **params: object, +) -> ReferenceOutput: + """Run the reference binary from its OWN drawn initialization. + + Nothing is loaded: the binary draws ``A`` (and ``mu``, ``sbeta``) with + gfortran's ``random_number``, seeded deterministically from ``seed`` + (amica15.f90:222-238). With ``max_iter=0`` its loop exits before the first + iteration (:955) and ``write_output`` writes the initialization itself, so + the returned ``A`` is the drawn initial mixing matrix (issue #341). + Full rank only (``nw`` channels, ``pcakeep = nw``); ``params`` override + the engine defaults by Fortran name. Raises like + :func:`run_seeded_reference`. + """ + num_comps = nw * num_models + workdir.mkdir(parents=True, exist_ok=True) + # Old output from an earlier run must never be read back. + out = workdir / "out" + if out.exists(): + shutil.rmtree(out) + _link_data(workdir, data_file) + param = { + # `files` must come first: amica15.f90 stops if other keys precede it. + "files": "./data.fdt", + "outdir": "./out/", + **_DEFAULT_PARAMS, + "data_dim": nw, + "field_dim": n_samples, + "pcakeep": nw, + "num_models": num_models, + "num_mix_comps": num_mix, + "max_iter": max_iter, + "max_threads": threads, + "write_LLt": 0, + "writestep": max_iter + 1, + "seed": seed, + **params, + } + return _run_reference( + param, + workdir, + nw=nw, + num_comps=num_comps, + num_mix=num_mix, + num_models=num_models, + max_iter=max_iter, + threads=threads, + timeout=timeout, + binary=binary, + ) + + +def _link_data(workdir: Path, data_file: Path) -> None: + """Link the raw recording into ``workdir`` as ``data.fdt``.""" + link = workdir / "data.fdt" + if link.is_symlink() or link.exists(): + link.unlink() + link.symlink_to(Path(data_file).resolve()) + + +def _run_reference( + param: dict, + workdir: Path, + *, + nw: int, + num_comps: int, + num_mix: int, + num_models: int, + max_iter: int, + threads: int, + timeout: float, + binary: Optional[Path], +) -> ReferenceOutput: + """Write ``param`` as ``input.param`` in ``workdir``, run the binary there + and read its raw output back (full rank: the sphere is ``nw x nw``). Raises + as :func:`run_seeded_reference` describes.""" + out = workdir / "out" (workdir / "input.param").write_text(_render_param(param)) exe = binary if binary is not None else resolver.resolve(REFERENCE_VERSION) @@ -279,6 +378,6 @@ def run_seeded_reference( c=_read_f64(out / "c", (nw, num_models)), comp_list=comp_list.reshape(nw, num_models, order="F") - 1, LL=_read_f64(out / "LL", (max_iter,)), - S=_read_f64(out / "S", (nx, nx)), + S=_read_f64(out / "S", (nw, nw)), stdout=proc.stdout, ) diff --git a/pamica/tests/pre_change.py b/pamica/tests/pre_change.py index 6a3c4b12..ff557437 100644 --- a/pamica/tests/pre_change.py +++ b/pamica/tests/pre_change.py @@ -17,6 +17,15 @@ fetches it; the same holds, with its own message, when git is not installed or the tests do not run from a git checkout. +Every commit loaded here predates two changes that move every trajectory: +the normalized initial mixing matrix of issue #341 (epic #324 Phase 12) and +the reference's single-precision density constants of issue #344 (Phase 13). +A byte-identity test that pins a later change against such a commit adapts +both sides, so the comparison isolates the change it was written for: +:func:`with_normalized_initial_mixing` starts the pre-change class from the +live initialization, and :func:`use_pre_344_constants` gives the live backend +its old constants. + Not a test module (no ``test_`` prefix, so pytest does not collect it). """ @@ -31,8 +40,9 @@ import tarfile from pathlib import Path, PurePosixPath from types import ModuleType -from typing import NoReturn +from typing import Any, NoReturn +import numpy as np import pytest REPO_ROOT = Path(__file__).resolve().parents[2] @@ -183,3 +193,96 @@ def use_pre_344_constants(monkeypatch: pytest.MonkeyPatch, backend: str) -> None module = importlib.import_module(module_name) for name in names: monkeypatch.setattr(module, name, _PRE_344_CONSTANTS[name]) + + +def with_normalized_initial_mixing(cls: Any) -> Any: + """A subclass of the pre-change backend class ``cls`` that starts from the + normalized initial ``A`` the live backends draw (issue #341). + + Every commit the byte-identity tests load predates epic #324 Phase 12, + whose backends normalize each drawn initial component to unit norm, as the + reference does (amica15.f90:818-819). That changes every trajectory from its + first E-step, so a test that pins a LATER behavior against such a commit + (the layout of issue #334, the rescale of issue #333, ...) fits the + pre-change class from the same initial ``A`` as the live code; everything + else the pre-change class does is its own, unmodified. ``cls`` is the + ``AMICATorchNG``, ``AMICAMLXNG`` or NumPy ``AMICA`` class of a package from + :func:`load_pre_change_package`. + + The pre-change class draws exactly as it always did, so every later draw + (``mu``, ``beta``, a restart) is unchanged; only the drawn ``A`` is then + replaced, in the class's own storage layout (component rows, or the + component-column layout before issue #334) and dtype: + + * PyTorch and MLX draw each fit's ``A`` first from a fresh + ``numpy.random.RandomState(seed)``, so the replacement is the live + :func:`pamica.initialization.initial_mixing` from that generator. + * NumPy draws from its running generator and only while ``A`` is ``None`` + (a supplied ``A`` is kept, and the restart after a non-finite likelihood + redraws through the same method), so the replacement normalizes the + class's own draw: off-diagonal entries of ``I + 0.01 * (0.5 - u)`` are + exactly the live draw's ``0.01 * (0.5 - u)``, and the diagonal is set to + one as in :func:`pamica.initialization.draw_initial_block`. + """ + from pamica.initialization import initial_mixing, normalize_components + + # The backend module the class (or the pre-change class it derives from, + # for a test's own subclass) was defined in. + backend, module = "", None + for base in cls.__mro__: + parts = base.__module__.split(".") + if len(parts) == 3 and parts[1:] in ( + ["torch_impl", "core"], + ["mlx_impl", "core"], + ["numpy_impl", "core"], + ): + backend, module = parts[1], sys.modules[base.__module__] + break + if module is None: + raise ValueError(f"{cls!r} is not a pamica backend class") + + def blocks(shape: tuple, n: int, m: int) -> list: + """Each model's block of an ``A`` of ``shape`` as index tuples.""" + if shape == (m * n, n): # component rows (issue #334 on) + return [(slice(h * n, (h + 1) * n), slice(None)) for h in range(m)] + if shape == (n, m * n): # component columns (before issue #334) + return [(slice(None), slice(h * n, (h + 1) * n)) for h in range(m)] + raise ValueError(f"unexpected A shape {shape} for {m} model(s) of {n}") + + if backend == "numpy_impl": + + class NumPyNormalizedInit(cls): + def _initialize_parameters(self): + drawn = self.A is None and not getattr(self, "fix_init", False) + super()._initialize_parameters() + if drawn: + A = np.array(self.A, dtype=np.float64) + n, m = self.data_dim, self.num_models + for idx in blocks(A.shape, n, m): + # A component is a row of the per-model block in both + # layouts; only the axis of the full A that carves the + # block out differs (columns before #334, rows after). + block = A[idx].copy() + block[np.diag_indices(n)] = 1.0 + A[idx] = normalize_components(block) + self.A = A + self._update_unmixing_matrices() + + return NumPyNormalizedInit + + class NormalizedInit(cls): + def _initialize_parameters(self): + super()._initialize_parameters() + n, m = self.n_channels, self.n_models + rows = initial_mixing(np.random.RandomState(self.seed), n, m) + shape = tuple(self.A.shape) + A = np.empty(shape) + for h, idx in enumerate(blocks(shape, n, m)): + A[idx] = rows[h * n : (h + 1) * n, :] + if backend == "torch_impl": + self.A = module.torch.from_numpy(A).to(self.A.device, self.A.dtype) + else: + self.A = module.mx.array(A.astype(np.float32)) + self._update_unmixing_matrices() + + return NormalizedInit diff --git a/pamica/tests/test_component_rows.py b/pamica/tests/test_component_rows.py index 3bede5fe..2db2f499 100644 --- a/pamica/tests/test_component_rows.py +++ b/pamica/tests/test_component_rows.py @@ -14,7 +14,8 @@ always run; MLX checks skip individually without MLX or an Apple GPU): 1. every configuration without a merge is byte-identical to the pre-change code - (the package at commit ``0930c0e`` is loaded from git): one, two and three + (the package at commit ``0930c0e`` is loaded from git, and started from the + live normalized initial ``A`` of issue #341): one, two and three models, ``doscaling`` and Newton on and off, every ``pdftype``, ``do_reject``, several blocks, a ``keep_best`` restore and best-of-two restarts, each on every backend that supports it; the old ``A`` maps onto @@ -53,7 +54,11 @@ from pamica import AMICA_NumPy from pamica.component_layout import rows_from_legacy_columns from pamica.numpy_impl.utils import identify_shared_components -from pamica.tests.pre_change import load_pre_change_package, use_pre_344_constants +from pamica.tests.pre_change import ( + load_pre_change_package, + use_pre_344_constants, + with_normalized_initial_mixing, +) from pamica.torch_impl.core import AMICATorchNG from pamica.torch_impl.utils import load_eeglab_data @@ -118,14 +123,20 @@ def _live(backend: str) -> Any: def _classes(backend: str, pre: Any) -> Tuple[Any, Any]: - """``(pre-change class, live class)`` for ``backend``.""" + """``(pre-change class, live class)`` for ``backend``. + + The pre-change class starts from the live normalized initial ``A`` (issue + #341, epic #324 Phase 12, which changed every trajectory through the + initialization alone), so the two sides differ by the layout alone. + """ live = _live(backend) if backend == "torch": - return pre.torch_impl.core.AMICATorchNG, live - if backend == "numpy": - return pre.numpy_impl.core.AMICA, live - old = importlib.import_module(f"{pre.__name__}.mlx_impl.core").AMICAMLXNG - return old, live + old = pre.torch_impl.core.AMICATorchNG + elif backend == "numpy": + old = pre.numpy_impl.core.AMICA + else: + old = importlib.import_module(f"{pre.__name__}.mlx_impl.core").AMICAMLXNG + return with_normalized_initial_mixing(old), live def _np(value: Any) -> np.ndarray: @@ -899,7 +910,13 @@ def merged_seed(real_data) -> Dict[str, Any]: def _seeded_run( - model: Any, backend: str, seed: Dict[str, Any], X: np.ndarray, k: int, rows: bool + model: Any, + backend: str, + seed: Dict[str, Any], + X: np.ndarray, + k: int, + rows: bool, + sphere: np.ndarray, ) -> Tuple[np.ndarray, List[np.ndarray], np.ndarray, np.ndarray]: """Run ``k`` production iterations of ``model`` from the merged seed, in the reference's load order: ``get_unmixing_matrices`` runs on the DEFAULT @@ -907,14 +924,21 @@ def _seeded_run( (amica15.f90:825-833), so the first E-step unmixes with the pre-merge blocks while indexing densities by the merged ``comp_list``. ``rows`` is False for a pre-change class, which gets the seed's ``A`` in its column - layout. Returns the per-iteration log-likelihood, each model's mixing - matrix (the reference's ``A(:, comp_list(:, h))``), ``mu`` and ``sbeta``.""" + layout. The data are sphered with ``sphere``, the one input the reference + computes for itself (its ``load_sphere`` path is broken, see + ``native_oracle.py``); pass the reference's own ``S`` so both sides start + from the same bits. Returns the per-iteration log-likelihood, each model's + mixing matrix (the reference's ``A(:, comp_list(:, h))``), ``mu`` and + ``sbeta``.""" A = seed["A"] if not rows: A = _column_state({"A": A, "comp_list": seed["default"]})["A"] lls: List[float] = [] if backend == "torch": - X_t = model._preprocess(X) + model._preprocess(X) + model.sphere = torch.from_numpy(sphere.copy()) + model._sphere_pinv = None + X_t = model.sphere @ (torch.from_numpy(X) - model.mean.reshape(-1, 1)) model._initialize_parameters() model.A = torch.from_numpy(A.copy()) for name in ("mu", "beta", "rho", "alpha", "gm"): @@ -932,7 +956,7 @@ def _seeded_run( else: model.fit(X) # sizes and preprocesses; the state is replaced below model.mean = seed["mean"].reshape(-1, 1).copy() - model.sphere = seed["sphere"].copy() + model.sphere = sphere.copy() model.data = model.sphere @ (X - model.mean) model._sphere_pinv = None model.A = A.copy() @@ -1037,11 +1061,13 @@ def torch_model(cls: Any) -> Any: writestep=10**6, **opt, ) + # Both sides from the reference's own sphere (see _seeded_run). + S = ref.S runs = { "torch": _seeded_run( - torch_model(AMICATorchNG), "torch", seed, real_data, k, rows=True + torch_model(AMICATorchNG), "torch", seed, real_data, k, True, S ), - "numpy": _seeded_run(numpy_model, "numpy", seed, real_data, k, rows=True), + "numpy": _seeded_run(numpy_model, "numpy", seed, real_data, k, True, S), } for name, (ll, mixing, mu, sbeta) in runs.items(): errs = { @@ -1058,7 +1084,9 @@ def torch_model(cls: Any) -> Any: # normalizer against the reference's single-precision one, the gap the # maxrho = 1.99 workaround hid. old344 = torch_model(pre344.torch_impl.core.AMICATorchNG) - ll, mixing, mu, sbeta = _seeded_run(old344, "torch", seed, real_data, k, True) + ll, mixing, mu, sbeta = _seeded_run( + old344, "torch", seed, real_data, k, True, S + ) errs = { "A": _mixing_error(mixing, ref.A, seed["merged"]), "mu": np.abs(mu[:, used] - ref.mu[:, used]).max(), @@ -1071,7 +1099,9 @@ def torch_model(cls: Any) -> Any: if k == 3: old = torch_model(pre.torch_impl.core.AMICATorchNG) - ll_old, mixing, _, _ = _seeded_run(old, "torch", seed, real_data, k, False) + ll_old, mixing, _, _ = _seeded_run( + old, "torch", seed, real_data, k, False, S + ) old_err = _mixing_error(mixing, ref.A, seed["merged"]) print( f"merged oracle doscaling={doscaling} column semantics k=3: " @@ -1083,22 +1113,29 @@ def torch_model(cls: Any) -> Any: # --- 5. early mass merges behave the same in the reference (opt-in) ------------- # The kind of recipe the sharing tests used before issue #334 made the metric # compare true component maps: 4096 samples, pamica's default optimizer, an -# early scan at a loose threshold. The first scan (iteration 11) merges 25 -# components and the second model's gm falls from 0.53 to 6.0e-4 within two -# iterations; by the second scan (iteration 22, which merges nothing more) it -# is 2e-22, and the fit goes non-finite at iteration 25. Issue #345 moved the -# A-freeze to the reference's iterations (mod(iter, share_iter) <= 5), so -# share_start is a multiple of share_iter here, which puts each window on its -# scan iteration; the earlier recipe (seed 7, scans at 8 and 18) now goes -# non-finite at iteration 18, before its second scan. _COLLAPSE_REF spells pamica's -# defaults out for the binary, with no further scans, no A-freeze and no -# convergence stops, so both sides run the same updates from the same state. +# early scan at a loose threshold. The first scan (iteration 11) merges 28 +# components and the second model's gm falls in the fit (whose A is held on +# those iterations) from 0.57 to 9.0e-4 within two iterations, and to 4.2e-3 +# in the freeze-free continuation the test runs on both sides; by the second +# scan (iteration 22, which merges nothing more) it is 2.4e-4 in the fit, and +# the fit's parameters go non-finite in iteration 23. Issue #345 +# moved the A-freeze to the reference's iterations (mod(iter, share_iter) <= 5), +# so share_start is a multiple of share_iter here, which puts each window on its +# scan iteration; the earlier recipe (seed 7, scans at 8 and 18) went +# non-finite at iteration 18, before its second scan. Seed 20 served until +# issue #341, whose normalized initial A leaves its second model at gm 1.2e-3 +# after the second scan and 1.7e-3 one iteration later, not collapsed; of seeds +# 0-49, 23, 32, 40 and 41 collapse as described (with share_start 10, none of +# 0-29; with 12, seeds 11 and 27), and many others go non-finite before the +# second scan. _COLLAPSE_REF spells pamica's defaults out for the binary, with +# no further scans, no A-freeze and no convergence stops, so both sides run the +# same updates from the same state. _COLLAPSE_SAMPLES = 4096 _COLLAPSE: Dict[str, Any] = dict( n_channels=NW, n_models=2, n_mix=NMIX, - seed=20, + seed=23, device="cpu", dtype=torch.float64, block_size=1024, diff --git a/pamica/tests/test_doscaling_rows.py b/pamica/tests/test_doscaling_rows.py index acefc837..2ac35598 100644 --- a/pamica/tests/test_doscaling_rows.py +++ b/pamica/tests/test_doscaling_rows.py @@ -26,7 +26,9 @@ the reference; 3. ``doscaling=False`` is byte-identical to the pre-fix code: the package at commit ``fb13d76`` is loaded from git and each backend fitted in the same - process, its ``A`` mapped onto the component rows issue #334 introduced; + process from the live normalized initial ``A`` (issue #341), the live one + at its pre-#344 density constants, its ``A`` mapped onto the component rows + issue #334 introduced; 4. ``scalestep``, a pamica extension the reference parses but never reads, counts iterations from 1, so its default of 1 is the reference's every-iteration rescale, and every constructor rejects a ``scalestep`` that @@ -54,7 +56,11 @@ from pamica import AMICA_NumPy from pamica.component_layout import rows_from_legacy_columns -from pamica.tests.pre_change import load_pre_change_package, use_pre_344_constants +from pamica.tests.pre_change import ( + load_pre_change_package, + use_pre_344_constants, + with_normalized_initial_mixing, +) from pamica.torch_impl.core import AMICATorchNG from pamica.torch_impl.utils import load_eeglab_data @@ -393,7 +399,11 @@ def test_a_zero_or_nan_norm_row_is_left_untouched(unscaled_state, backend, bad): # --- 3. doscaling=False is byte-identical to the pre-fix code --------------- # The epic #324 head this phase branched from: the last commit with the stored- # column rule. A later phase that deliberately changes the doscaling=False -# trajectory moves this pin to its own base commit. +# trajectory moves this pin to its own base commit. Issue #341 (Phase 12) +# changed every trajectory through the initial A alone, so the pre-fix class +# starts from the live normalized initial A instead +# (``with_normalized_initial_mixing``): from the same start, everything else +# the doscaling=False path does is still the pre-fix code's, bit for bit. _PRE_FIX_COMMIT = "fb13d76de145419da9c89db94438d60aafbff444" @@ -408,11 +418,15 @@ def pre_fix(tmp_path_factory) -> Any: def _pre_fix_class(pre_fix: Any, backend: str) -> Any: + """The pre-fix class, starting from the live initial ``A`` (issue #341).""" if backend == "torch": - return pre_fix.torch_impl.core.AMICATorchNG - if backend == "numpy": - return pre_fix.numpy_impl.core.AMICA - return importlib.import_module(f"{pre_fix.__name__}.mlx_impl.core").AMICAMLXNG + cls = pre_fix.torch_impl.core.AMICATorchNG + elif backend == "numpy": + cls = pre_fix.numpy_impl.core.AMICA + else: + mlx_core = importlib.import_module(f"{pre_fix.__name__}.mlx_impl.core") + cls = mlx_core.AMICAMLXNG + return with_normalized_initial_mixing(cls) def _fit_unscaled( @@ -461,9 +475,9 @@ def test_doscaling_off_is_byte_identical_to_the_pre_fix_code( ): """``doscaling=False`` never enters the rescale, so this fix must leave its trajectory and every fitted array bit for bit where the pre-fix code put - them, on every backend. The live backend runs with the density constants it - had before issue #344, which the pre-fix code predates (the two-model fits - reach ``rho == 2``).""" + them, on every backend, from the same initial ``A`` (issue #341). The live + backend runs with the density constants it had before issue #344, which the + pre-fix code predates (the two-model fits reach ``rho == 2``).""" new_cls: Any if backend == "mlx": new_cls = _mlx_core().AMICAMLXNG # skips without MLX @@ -595,20 +609,38 @@ def test_every_backend_rejects_an_invalid_scalestep(backend, bad, tmp_path): "scalestep": 1, } # Tolerances per iteration count. The reference itself carries round-off that -# the ill-conditioned exact-EM mu update amplifies (mu of a low-mass mixture -# component), so each bound sits at the doscaling-OFF noise floor with margin. -# Measured maxima (doscaling on / off), each taken over both backends (PyTorch -# and NumPy) and both model counts, which is why they are wider than the -# single-configuration figures in the changelog and PR (PyTorch, one model: -# A 5.0e-16, mu 7.8e-11, sbeta 1.1e-14 after 1 iteration): -# 1 iteration: A 2.2e-15 / 2.4e-15, mu 7.8e-11 / 8.0e-11, sbeta 2.4e-14 -# 3 iterations: A 6.1e-12 / 3.1e-11, mu 3.6e-8 / 1.3e-8, sbeta 2.8e-10 +# the ill-conditioned exact-EM mu update amplifies (a sample close to a mixture +# center weighs |y|^(rho-2) in its denominator), so each bound sits at the +# round-off floor of the seeded state with margin. Measured maxima (doscaling +# on / off), each taken over both backends (PyTorch and NumPy) and both model +# counts, from ORACLE_SEED's initialization: +# 1 iteration: A 2.1e-15 / 2.3e-15, mu 1.3e-10 / 1.3e-10, sbeta 2.4e-14 +# 3 iterations: A 3.6e-12 / 1.1e-11, mu 1.4e-7 / 4.3e-8, sbeta 1.1e-10 # The column rule this replaced was off by A 7.2e-5, mu 6.6e-5, sbeta 8.6e-5 -# after 1 iteration and 1.4e-3 / 6.7e-3 / 2.2e-3 after 3. +# after 1 iteration and 1.4e-3 / 6.7e-3 / 2.2e-3 after 3 (measured from seed +# 42's initialization before issue #341; PyTorch, one model, the new rule was +# off by A 5.0e-16, mu 7.8e-11, sbeta 1.1e-14 after 1 iteration there). _ORACLE_TOL = { 1: {"A": 1e-13, "mu": 1e-9, "sbeta": 1e-12, "LL": 1e-12}, 3: {"A": 1e-10, "mu": 1e-6, "sbeta": 1e-8, "LL": 1e-11}, } +# The seeded state's round-off floor, measured in the test as the largest +# disagreement between PyTorch and NumPy (the same float64 arithmetic in two +# operation orders), must sit this many times below each bound, as the worse +# of the doscaling on and off deviations the bounds were set on did (3.2 to 42 +# times below): a state whose floor reaches a bound cannot test that bound. +_FLOOR_MARGIN = 3.0 +# The seed of the oracle's initialization. Seed 42 served until issue #341: from +# its normalized initial A, the two-model state puts one sample 2.3e-7 from a +# mixture center, and after one iteration PyTorch and NumPy disagree with each +# other by 7.9e-10 in mu and with the reference by 5.1e-10 and 1.3e-9, at and +# over the bound (the spheres of all three differ by 1e-14, and the mu update +# amplifies that). Of seeds 42 and 45-56, only 45 and 47 keep every deviation +# within its bound (at 0.14 and 0.33 of it); the others exceed one by up to 34 +# times, their two backends as far apart as each is from the reference. With +# the pre-#341 initialization, the one-iteration mu deviation of PyTorch stayed +# below 2.7e-10 on seeds 42-49. +ORACLE_SEED = 45 @pytest.mark.skipif( @@ -638,7 +670,7 @@ def torch_model() -> AMICATorchNG: n_channels=NW, n_models=n_models, n_mix=NMIX, - seed=SEED, + seed=ORACLE_SEED, device="cpu", dtype=torch.float64, keep_best=False, @@ -680,7 +712,7 @@ def torch_model() -> AMICATorchNG: num_models=n_models, num_mix=NMIX, max_iter=k, - seed=SEED, + seed=ORACLE_SEED, use_tqdm=False, outdir=td, do_opt_block=False, @@ -692,6 +724,20 @@ def torch_model() -> AMICATorchNG: "torch": (t.A.numpy(), t.mu.numpy(), t.beta.numpy(), t.ll_history), "numpy": (n.A, n.mu, n.beta, n.ll), } + # Precondition: the state's own round-off floor leaves room under + # every bound (_FLOOR_MARGIN). + (tA, tmu, tbeta, tll), (nA, nmu, nbeta, nll) = fits.values() + floor = { + "A": np.abs(tA - nA).max(), + "mu": np.abs(tmu - nmu).max(), + "sbeta": np.abs(tbeta - nbeta).max(), + "LL": np.abs(np.asarray(tll) - np.asarray(nll)).max(), + } + high = {q: f for q, f in floor.items() if not f * _FLOOR_MARGIN <= tol[q]} + assert not high, ( + f"setup: PyTorch and NumPy disagree by {high} after {k} iteration(s) " + f"from seed {ORACLE_SEED}, too close to the bounds {tol} to test them" + ) for name, (A, mu, beta, ll) in fits.items(): errs = { "A": np.abs(reference_mixing(A) - ref.A).max(), diff --git a/pamica/tests/test_initial_mixing.py b/pamica/tests/test_initial_mixing.py new file mode 100644 index 00000000..d783e15d --- /dev/null +++ b/pamica/tests/test_initial_mixing.py @@ -0,0 +1,673 @@ +"""Every drawn initial component has unit norm, as in the reference (issue #341, +epic #324 Phase 12). + +The reference draws each model's block of ``A`` as ``0.01 * (0.5 - u)``, sets +its diagonal to one and divides every component (a column of its ``A``, a row +of pamica's, ADR 0007) by its Euclidean norm (amica15.f90:805-823); its restart +after a non-finite likelihood redraws the same way (:1026-1044). A loaded ``A`` +is used as is (:793-802), and ``fix_init`` starts from the identity. Before +this change pamica drew ``I + 0.01 * (0.5 - u)``, whose components have norms +within about 5e-3 of one, so the first E-step, and with ``doscaling`` off the +whole fit, ran on a scale the reference never uses. + +Pinned here, cross-backend per ``.rules/backend_parity.md`` (PyTorch and NumPy +always run; MLX checks skip individually without MLX or an Apple GPU): + +1. the shared draw (:func:`pamica.initialization.initial_mixing`) is the + reference's recipe, written out below loop for loop from amica15.f90, and + its normalization refuses a zero or non-finite component instead of + returning NaN; +2. every backend starts every fit (one and two models, a single fit and each + of best-of-two restarts) from that draw: unit-norm components, the same + bits in PyTorch and NumPy and their float32 cast in MLX, unchanged up to + the first update; the same checks fail on the pre-change code (commit + ``027cb07``, loaded from git), whose components are off by more than 1e-3; +3. a supplied or loaded ``A`` is used bit for bit (a NumPy ``A`` set before + ``fit``, a NumPy refit, a PyTorch ``state_dict``, a saved ``AMICA`` wrapper, + an MLX save): only a drawn ``A`` is normalized; the NumPy backend's + ``fix_init`` starts from the identity and draws nothing; and a supplied + NumPy ``A`` that no fit could start from (wrong shape, a non-finite entry, + a singular model block) is refused at fit start (the PyTorch and MLX + backends take an ``A`` only through their validated saves); +4. the NumPy restart after a non-finite likelihood redraws unit-norm + components from the running generator (the sanctioned error-injection + pattern of ``.rules/testing.md`` poisons one real likelihood); +5. (opt-in, ``AMICA_RUN_FORTRAN=1``) the native binary's own drawn + initialization has unit-norm components with the recipe's shape, and the + binary, loaded with pamica's draw BEFORE its normalization, normalizes it + to pamica's initial ``A`` to float64 round-off. + +A bit-level oracle of the whole initialization is not possible: the binary +draws from gfortran's ``random_number`` and pamica from NumPy's +``RandomState``, and the binary normalizes only a draw of its own (a loaded +``A`` is used as is, and ``fix_init`` draws nothing). Oracle 5 therefore checks +the two halves separately: the binary's draw path normalizes (on its own +draw), and its normalization arithmetic, which is the same expression in its +initialization (:818-819) and in its per-iteration rescale (:1845-1847), +reproduces pamica's on pamica's draw, reached through ``load_A`` with the +``A`` update off so that the rescale is the only change to ``A``. + +Real bundled sample EEG only, no synthetic data or mocks +(``.rules/testing.md``). The only instrumentation is a call-recording spy on +the real ``_initialize_parameters`` and ``_update_parameters``, which calls +straight through and never substitutes a result. +""" + +from __future__ import annotations + +import dataclasses +import importlib +import os +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np +import pytest +import torch + +from pamica import AMICA, AMICA_NumPy +from pamica.initialization import ( + draw_initial_block, + initial_mixing, + normalize_components, +) +from pamica.tests.pre_change import load_pre_change_package +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +SAMPLE_DIR = Path(__file__).resolve().parent.parent / "sample_data" +DATA_FILE = SAMPLE_DIR / "eeglab_data.fdt" +NW = 32 +FIELD = 30504 +NMIX = 3 +SEED = 42 +N_SLICE = 4096 +BACKENDS = ["torch", "numpy", "mlx"] + +# The epic #324 head this phase branched from: the last commit whose backends +# drew I + 0.01 * (0.5 - u) without normalizing it. +PRE_CHANGE_COMMIT = "027cb0731dbaaa8e7a5795166af95ba7ff52c2cd" + +# Unit-norm bounds. Measured over 500 seeds (two models of 32): the float64 +# rows are within 4.4e-16 (2 units in the last place) of norm one, and their +# float32 cast (MLX) within 3.0e-8 (a quarter of a float32 unit); the pre-change +# draw's rows sit 1e-4 to 5e-3 away. +UNIT_TOL = {np.dtype(np.float64): 1e-15, np.dtype(np.float32): 1.2e-7} +# A pre-change row deviates by at least this much somewhere in every draw (the +# largest deviation over its 32 rows is 4e-3 to 5e-3 on the seeds used here). +PRE_CHANGE_MIN_DEV = 1e-3 + +pytestmark = pytest.mark.skipif(not DATA_FILE.exists(), reason="sample data missing") + + +@pytest.fixture(scope="module") +def X() -> np.ndarray: + data = load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD) + return data.astype(np.float64)[:, :N_SLICE] + + +@pytest.fixture(scope="module") +def pre(tmp_path_factory) -> Any: + """The package before this change, imported beside the live one.""" + return load_pre_change_package( + PRE_CHANGE_COMMIT, "pamica_pre341", tmp_path_factory.mktemp("pre341") + ) + + +def _mlx_core(): + """The MLX backend module, or skip this test (never the whole module).""" + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + mx = mlx_core.mx + if mx.default_device().type != mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + return mlx_core + + +def _live(backend: str) -> Any: + if backend == "torch": + return AMICATorchNG + if backend == "numpy": + return AMICA_NumPy + return _mlx_core().AMICAMLXNG + + +def _pre_change(backend: str, pre: Any) -> Any: + if backend == "torch": + return pre.torch_impl.core.AMICATorchNG + if backend == "numpy": + return pre.numpy_impl.core.AMICA + _mlx_core() # skip without MLX, as for the live class + return importlib.import_module(f"{pre.__name__}.mlx_impl.core").AMICAMLXNG + + +def _np(value: Any) -> np.ndarray: + if isinstance(value, torch.Tensor): + return value.detach().cpu().numpy().copy() + return np.array(value) + + +def _reference_recipe(rng: np.random.RandomState, n: int, n_models: int) -> np.ndarray: + """amica15.f90:805-823 written out loop for loop, NumPy's generator in + place of gfortran's and components as rows (ADR 0007): per model, one + ``n x n`` draw ``0.01 * (0.5 - u)``; then per component, the diagonal + entry set to one and the component divided by ``sqrt(sum(a * a))``.""" + blocks = [] + for _ in range(n_models): + a = 0.01 * (0.5 - rng.rand(n, n)) + for i in range(n): + a[i, i] = 1.0 + a[i, :] = a[i, :] / np.sqrt(np.sum(a[i, :] * a[i, :])) + blocks.append(a) + return np.vstack(blocks) + + +def _row_norm_dev(A: np.ndarray) -> float: + norms = np.linalg.norm(np.asarray(A, dtype=np.float64), axis=1) + return float(np.abs(norms - 1.0).max()) + + +# --- 1. the shared draw is the reference's recipe ----------------------------- +@pytest.mark.parametrize("n_models", [1, 2, 3]) +def test_the_shared_draw_is_the_reference_recipe(n_models): + """Bit for bit, over several seeds, including the generator's position + afterward (``n * n`` values per model, so the ``mu`` and ``beta`` draws + that follow are the ones they always were). With ``fix_init`` the draw is + the identity and consumes nothing (amica15.f90:806-810).""" + for seed in (0, 7, SEED): + got_rng, want_rng = np.random.RandomState(seed), np.random.RandomState(seed) + A = initial_mixing(got_rng, NW, n_models) + assert A.shape == (n_models * NW, NW) and A.dtype == np.float64 + np.testing.assert_array_equal(A, _reference_recipe(want_rng, NW, n_models)) + assert _row_norm_dev(A) <= UNIT_TOL[A.dtype] + assert got_rng.rand() == want_rng.rand() + + rng, untouched = np.random.RandomState(seed), np.random.RandomState(seed) + fixed = initial_mixing(rng, NW, n_models, fix_init=True) + np.testing.assert_array_equal(fixed, np.vstack([np.eye(NW)] * n_models)) + assert rng.rand() == untouched.rand() + + +@pytest.mark.parametrize("bad", [0.0, np.nan], ids=["zero", "nan"]) +def test_normalizing_a_zero_or_nan_component_raises(bad): + """The reference's normalization has no zero-norm guard because its draw + cannot need one (the unit diagonal keeps every norm at least 1). A direct + call on another block must not divide by a zero or NaN norm silently.""" + block = draw_initial_block(np.random.RandomState(SEED), NW) + block[5, :] = bad + with pytest.raises(ValueError, match=r"component row\(s\) \[5\]"): + normalize_components(block) + + +# --- 2. every backend starts every fit from that draw ------------------------- +Record = Tuple[Optional[int], np.ndarray, np.ndarray] + + +def _record_starts(model: Any, monkeypatch) -> List[list]: + """Spy on the real ``_initialize_parameters`` and ``_update_parameters``: + for each initialization, ``[seed, A after it, A at the entry of the next + update]``. Both spies call straight through.""" + records: List[list] = [] + initialize, update = model._initialize_parameters, model._update_parameters + + def initialize_spy(*args, **kwargs): + result = initialize(*args, **kwargs) + records.append([model.seed, _np(model.A), None]) + return result + + def update_spy(*args, **kwargs): + if records and records[-1][2] is None: + records[-1][2] = _np(model.A) + return update(*args, **kwargs) + + monkeypatch.setattr(model, "_initialize_parameters", initialize_spy) + monkeypatch.setattr(model, "_update_parameters", update_spy) + return records + + +def _fit_recording( + cls: Any, + backend: str, + n_models: int, + n_restarts: int, + X: np.ndarray, + tmp_path: Path, + monkeypatch, +) -> List[list]: + """A real two-iteration fit of ``cls`` from ``SEED`` with the start spies.""" + if backend == "numpy": + model = cls( + num_models=n_models, + num_mix=NMIX, + max_iter=2, + seed=SEED, + n_restarts=n_restarts, + use_tqdm=False, + outdir=str(tmp_path), + do_opt_block=False, + block_size=N_SLICE, + writestep=10**6, + ) + records = _record_starts(model, monkeypatch) + model.fit(X) + return records + kwargs: Dict[str, Any] = dict( + n_channels=NW, + n_models=n_models, + n_mix=NMIX, + seed=SEED, + n_restarts=n_restarts, + block_size=N_SLICE, + ) + if backend == "torch": + kwargs.update(device="cpu", dtype=torch.float64) + model = cls(**kwargs) + records = _record_starts(model, monkeypatch) + model.fit(X, max_iter=2, verbose=False) + return records + + +def _check_starts(backend: str, n_models: int, n_restarts: int, records) -> None: + """Each fit started from the reference recipe for its own seed: unit-norm + components, the float64 recipe's bits (their float32 cast on MLX), and the + same ``A`` when the first update came.""" + assert [r[0] for r in records] == [SEED + i for i in range(n_restarts)] + for seed, start, first_update in records: + assert first_update is not None, "no update ran" + np.testing.assert_array_equal(start, first_update) + assert _row_norm_dev(start) <= UNIT_TOL[start.dtype], ( + f"seed {seed}: initial components off unit norm by " + f"{_row_norm_dev(start):.2e}" + ) + want = _reference_recipe(np.random.RandomState(seed), NW, n_models) + if backend == "mlx": + want = want.astype(np.float32) + np.testing.assert_array_equal(start, want) + + +@pytest.mark.parametrize("n_restarts", [1, 2]) +@pytest.mark.parametrize("n_models", [1, 2]) +@pytest.mark.parametrize("backend", BACKENDS) +def test_every_fit_starts_from_unit_norm_components( + backend, n_models, n_restarts, X, tmp_path, monkeypatch +): + """Unit-norm drawn components before the first update, on every backend, + for a single fit and for each of best-of-two restarts. The recipe check + also makes the backends agree: PyTorch and NumPy start from the same + float64 bits, MLX from their float32 cast.""" + records = _fit_recording( + _live(backend), backend, n_models, n_restarts, X, tmp_path, monkeypatch + ) + _check_starts(backend, n_models, n_restarts, records) + + +@pytest.mark.parametrize("n_models", [1, 2]) +@pytest.mark.parametrize("backend", BACKENDS) +def test_the_pre_change_code_fails_the_start_checks( + pre, backend, n_models, X, tmp_path, monkeypatch +): + """The control: the code before this change drew ``I + 0.01 * (0.5 - u)`` + and never normalized it, so the same checks fail on it, on the norm.""" + records = _fit_recording( + _pre_change(backend, pre), backend, n_models, 2, X, tmp_path, monkeypatch + ) + for _, start, _ in records: + assert _row_norm_dev(start) > PRE_CHANGE_MIN_DEV + with pytest.raises(AssertionError, match="off unit norm"): + _check_starts(backend, n_models, 2, records) + + +# --- 3. a supplied or loaded A is used as is ---------------------------------- +@pytest.fixture(scope="module") +def fitted_unscaled(X) -> Dict[str, np.ndarray]: + """A real ``A`` whose components are far from unit norm: three iterations + of a two-model fit with ``doscaling`` off.""" + t = AMICATorchNG( + n_channels=NW, + n_models=2, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + doscaling=False, + keep_best=False, + ) + t.fit(X, max_iter=3, verbose=False) + A = _np(t.A) + assert _row_norm_dev(A) > 1e-2, "setup: the fitted components are unit norm" + return {"A": A, "comp_list": _np(t.comp_list)} + + +def _numpy_model(outdir: Path, **kwargs: Any) -> Any: + params: Dict[str, Any] = dict( + num_models=2, + num_mix=NMIX, + max_iter=1, + seed=SEED, + use_tqdm=False, + outdir=str(outdir), + do_opt_block=False, + block_size=N_SLICE, + writestep=10**6, + ) + params.update(kwargs) + return AMICA_NumPy(**params) + + +def test_numpy_uses_an_A_supplied_before_fit_as_is( + fitted_unscaled, X, tmp_path, monkeypatch +): + """The NumPy backend initializes a parameter only while it is ``None``, so + an ``A`` set before ``fit`` is its starting point, like the reference's + loaded ``A`` (amica15.f90:793-802): used bit for bit, not normalized.""" + supplied = fitted_unscaled["A"] + model = _numpy_model(tmp_path) + model.A = supplied.copy() + records = _record_starts(model, monkeypatch) + model.fit(X) + assert len(records) == 1 + _, start, first_update = records[0] + assert start.tobytes() == supplied.tobytes() + assert first_update.tobytes() == supplied.tobytes() + + +def test_numpy_refit_continues_from_its_fitted_A_as_is(X, tmp_path, monkeypatch): + """A second ``fit`` on the same NumPy instance starts from the first fit's + final ``A`` (``docs/guides/amica-differences.md``, issue #312), which + ``doscaling`` off leaves far from unit norm: used bit for bit.""" + model = _numpy_model(tmp_path, max_iter=3, doscaling=False) + model.fit(X) + assert model.A is not None + fitted = model.A.copy() + assert _row_norm_dev(fitted) > 1e-2 + records = _record_starts(model, monkeypatch) + model.fit(X) + assert len(records) == 1 + assert records[0][1].tobytes() == fitted.tobytes() + + +def test_a_torch_state_dict_restores_A_as_is(X): + """A PyTorch ``state_dict`` restores the fitted ``A`` bit for bit.""" + t = AMICATorchNG( + n_channels=NW, + n_models=2, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + doscaling=False, + keep_best=False, + ) + t.fit(X, max_iter=3, verbose=False) + fitted = _np(t.A) + assert _row_norm_dev(fitted) > 1e-2 + restored = AMICATorchNG.from_state_dict(t.state_dict(), device="cpu") + assert _np(restored.A).tobytes() == fitted.tobytes() + + +def test_an_mlx_save_restores_A_as_is(X, tmp_path): + """An MLX save, through ``state_dict`` and through the file, restores the + fitted ``A`` bit for bit.""" + cls = _mlx_core().AMICAMLXNG + m = cls(n_channels=NW, n_models=2, n_mix=NMIX, seed=SEED, doscaling=False) + m.fit(X, max_iter=3, verbose=False) + fitted = _np(m.A) + assert _row_norm_dev(fitted) > 1e-2 + assert _np(cls.from_state_dict(m.state_dict()).A).tobytes() == fitted.tobytes() + path = tmp_path / "model.npz" + m.save(str(path)) + assert _np(cls.load(str(path)).A).tobytes() == fitted.tobytes() + + +def test_a_wrapper_save_restores_A_as_is(X, tmp_path): + """The ``AMICA`` wrapper's ``save``/``load`` restores the fitted PyTorch + ``A`` bit for bit.""" + m = AMICA(n_models=2, n_mix=NMIX, device="cpu", verbose=False) + m.fit(X, max_iter=3, seed=SEED, doscaling=False, keep_best=False) + assert m.model_ is not None + fitted = _np(m.model_.A) + assert _row_norm_dev(fitted) > 1e-2 + path = tmp_path / "model.pt" + m.save(str(path)) + loaded = AMICA.load(str(path), device="cpu") + assert loaded.model_ is not None + assert _np(loaded.model_.A).tobytes() == fitted.tobytes() + + +def test_numpy_fix_init_starts_from_the_identity(X, tmp_path, monkeypatch): + """The NumPy backend's ``fix_init`` starts every model from the identity, + whose components already have unit norm, and draws nothing: not ``A``, + and not ``mu`` or ``beta`` either (amica15.f90:754, :767, :806-810). It is + an attribute, not a constructor keyword or a params-file key (neither + accepts it, issue #346 and ``FORTRAN_UNSUPPORTED_KEYS``).""" + with pytest.raises(TypeError, match="fix_init"): + _numpy_model(tmp_path, fix_init=True) + model = _numpy_model(tmp_path) + model.fix_init = True + records = _record_starts(model, monkeypatch) + model.fit(X) + assert len(records) == 1 + _, start, first_update = records[0] + identity = np.vstack([np.eye(NW)] * 2) + assert start.tobytes() == identity.tobytes() + assert first_update.tobytes() == identity.tobytes() + assert model.rng.rand() == np.random.RandomState(SEED).rand() + + +def _supplied_misfits(fitted: np.ndarray) -> Dict[str, Tuple[np.ndarray, str]]: + """Supplied two-model ``A`` values no fit can start from, each with the + message it must raise: a component-column ``A`` (the layout before issue + #334), a NaN entry, and an all-zero component row.""" + nan = fitted.copy() + nan[3, 7] = np.nan + zero = fitted.copy() + zero[NW + 5, :] = 0.0 + return { + "columns": ( + np.ascontiguousarray(fitted.T), + r"has shape \(32, 64\), expected \(64, 32\)", + ), + "nan": (nan, r"non-finite entries in component row\(s\) \[3\]"), + "zero-row": ( + zero, + r"model 1's block .* numerically singular \(rank 31 of 32\)", + ), + } + + +@pytest.mark.parametrize("case", ["columns", "nan", "zero-row"]) +def test_numpy_refuses_a_supplied_A_no_fit_can_start_from( + case, fitted_unscaled, X, tmp_path, monkeypatch +): + """Checked at fit start, before any E-step, with ``ValueError`` naming the + problem instead of a ``LinAlgError`` or a non-finite likelihood deep in the + fit. The valid ``A`` they are made from fits (test above).""" + A, message = _supplied_misfits(fitted_unscaled["A"])[case] + model = _numpy_model(tmp_path) + model.A = A + steps: List[int] = [] + forward = model._get_updates_and_likelihood + + def spy(): + steps.append(model.iter) + return forward() + + monkeypatch.setattr(model, "_get_updates_and_likelihood", spy) + with pytest.raises(ValueError, match=message): + model.fit(X) + assert steps == [] and model.ll == [] + + +# --- 4. the NumPy restart after a non-finite likelihood ----------------------- +def _nan_on_iteration(base: Any, nan_iter: int) -> Any: + """``base`` with its real E-step likelihood poisoned on one 0-based + iteration: the sanctioned error-injection pattern (``.rules/testing.md``, + as ``test_schedule_gates.py``'s ``_NaNOnIteration``). Everything else, the + restart included, is the real code on real data.""" + + class NaNOnIteration(base): + def _get_updates_and_likelihood(self): + updates = super()._get_updates_and_likelihood() + if self.iter == nan_iter: + updates["ll"] = float("nan") + return updates + + return NaNOnIteration + + +def _numpy_restart_starts( + cls: Any, n_models: int, X: np.ndarray, tmp_path: Path, monkeypatch +) -> List[list]: + """The starts of a real NumPy fit whose second likelihood is NaN, inside + the restart window: the initial draw and the restart's redraw.""" + model = _nan_on_iteration(cls, 1)( + num_models=n_models, + num_mix=NMIX, + max_iter=4, + seed=SEED, + use_tqdm=False, + outdir=str(tmp_path), + do_opt_block=False, + block_size=N_SLICE, + writestep=10**6, + restartiter=10, + maxrestarts=3, + ) + records = _record_starts(model, monkeypatch) + model.fit(X) + assert model.numrestarts == 1 + assert len(records) == 2, "setup: the NaN did not restart the fit" + return records + + +@pytest.mark.parametrize("n_models", [1, 2]) +def test_the_numpy_restart_redraws_unit_norm_components( + n_models, X, tmp_path, monkeypatch +): + """The redraw continues the running generator past the first fit's ``A``, + ``mu`` and ``beta`` draws, and normalizes like the initial draw + (amica15.f90:1039-1040).""" + records = _numpy_restart_starts(AMICA_NumPy, n_models, X, tmp_path, monkeypatch) + (_, first, _), (_, redraw, redraw_update) = records + assert redraw_update is not None + np.testing.assert_array_equal(redraw, redraw_update) + assert _row_norm_dev(redraw) <= UNIT_TOL[redraw.dtype] + assert not np.array_equal(redraw, first) + + rng = np.random.RandomState(SEED) + np.testing.assert_array_equal(first, _reference_recipe(rng, NW, n_models)) + n_comps = NW * n_models + for _ in range(n_comps): + rng.rand(NMIX) # mu, one component at a time + rng.rand(NMIX, n_comps) # beta + np.testing.assert_array_equal(redraw, _reference_recipe(rng, NW, n_models)) + + +def test_the_pre_change_numpy_restart_redrew_unnormalized_components( + pre, X, tmp_path, monkeypatch +): + """The control: the pre-change redraw is off unit norm.""" + records = _numpy_restart_starts( + pre.numpy_impl.core.AMICA, 2, X, tmp_path, monkeypatch + ) + assert _row_norm_dev(records[1][1]) > PRE_CHANGE_MIN_DEV + + +# --- 5. the native reference binary (opt-in) ---------------------------------- +run_fortran = pytest.mark.skipif( + os.environ.get("AMICA_RUN_FORTRAN") != "1", + reason="opt-in Fortran-binary integration test (set AMICA_RUN_FORTRAN=1)", +) + + +def _off_diagonal_ratios(A: np.ndarray, n: int) -> np.ndarray: + """``a[j] / a[i]`` over every component ``a`` (a row) with its own + diagonal entry ``a[i]`` (``i`` = its index within its model), ``j != i``: + the draw before normalization, whose off-diagonal entries lie in + (-0.005, 0.005].""" + ratios = [] + for k, a in enumerate(A): + i = k % n + assert np.argmax(np.abs(a)) == i, f"component {k}: diagonal not largest" + ratios.append(np.delete(a, i) / a[i]) + return np.concatenate(ratios) + + +@run_fortran +@pytest.mark.parametrize("seed", [1, SEED]) +@pytest.mark.parametrize("n_models", [1, 2]) +def test_the_reference_draws_unit_norm_components(n_models, seed, tmp_path): + """The binary's own drawn initialization (``max_iter=0`` writes it before + any iteration): every component has unit norm, its diagonal entry is its + largest, and the other entries relative to it lie within the draw's + half-width 0.005. pamica's draw has the same shape (measured on the + binary: norms within 4.4e-16 of one, ratios up to 0.004998).""" + from pamica.tests.native_oracle import reference_mixing, run_drawn_reference + + ref = run_drawn_reference( + DATA_FILE, + tmp_path, + nw=NW, + n_samples=FIELD, + num_models=n_models, + num_mix=NMIX, + seed=seed, + ) + assert ref.LL.size == 0 # no iteration ran + # The reference layout's columns are components: pamica's rows. + components = reference_mixing(ref.A) + assert _row_norm_dev(components) <= UNIT_TOL[np.dtype(np.float64)] + half_width = 0.01 * 0.5 + assert np.abs(_off_diagonal_ratios(components, NW)).max() <= half_width + + ours = initial_mixing(np.random.RandomState(seed), NW, n_models) + assert np.abs(_off_diagonal_ratios(ours, NW)).max() <= half_width + + +@run_fortran +@pytest.mark.parametrize("n_models", [1, 2]) +def test_the_reference_normalizes_pamicas_draw_as_pamica_does(n_models, tmp_path): + """The binary loaded with pamica's draw BEFORE normalization (``load_A``, + used as is) and every other parameter of pamica's initialization, with the + ``A`` update off, rescales each component in its first iteration with the + same expression its initialization normalizes with (amica15.f90:1845-1847 + and :818-819): the result is pamica's initial ``A`` to float64 round-off + (measured: 4.4e-16, two units in the last place, for one and two models), + while the loaded draw was off unit norm by 1.8e-4.""" + from pamica.tests.native_oracle import ( + reference_mixing, + run_seeded_reference, + seed_from_torch, + ) + + full = load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD) + init = AMICATorchNG( + n_channels=NW, + n_models=n_models, + n_mix=NMIX, + seed=SEED, + device="cpu", + dtype=torch.float64, + ) + init._preprocess(full.astype(np.float64)) + init._initialize_parameters() + rng = np.random.RandomState(SEED) + drawn = np.vstack([draw_initial_block(rng, NW) for _ in range(n_models)]) + assert _row_norm_dev(drawn) > 1e-5, "setup: the draw is already unit norm" + state = dataclasses.replace(seed_from_torch(init), A=reference_mixing(drawn)) + + ref = run_seeded_reference( + state, + DATA_FILE, + tmp_path, + n_samples=FIELD, + max_iter=1, + num_models=n_models, + update_A=0, + doscaling=1, + do_newton=0, + ) + assert init.A is not None + err = np.abs(ref.A - reference_mixing(_np(init.A))).max() + assert err <= UNIT_TOL[np.dtype(np.float64)], f"off by {err:.2e}" diff --git a/pamica/tests/test_iteration_order_native_oracle.py b/pamica/tests/test_iteration_order_native_oracle.py index dbf77eb1..ab0946a1 100644 --- a/pamica/tests/test_iteration_order_native_oracle.py +++ b/pamica/tests/test_iteration_order_native_oracle.py @@ -32,7 +32,9 @@ 1.3e-2 / 1.4e-1 (ratchet) and 6.1e-3 / 1.9e-1 (A-freeze), hundreds to tens of thousands of times the floor; each test replays the pre-change PyTorch backend (through :mod:`pamica.tests.pre_change`) as a control that the bound -discriminates. +discriminates. The control starts from the same normalized initial ``A`` as the +live backends (issue #341, which changed every trajectory through the +initialization alone), so it differs from them by the iteration order alone. """ from __future__ import annotations @@ -250,7 +252,9 @@ def numpy_mixing() -> np.ndarray: n.fit(X) fits["numpy"] = ([float(v) for v in n.ll], n_a) - old = _torch(pre.torch_impl.core.AMICATorchNG, cfg) + from pamica.tests.pre_change import with_normalized_initial_mixing + + old = _torch(with_normalized_initial_mixing(pre.torch_impl.core.AMICATorchNG), cfg) old_a = _with_history(old, lambda: reference_mixing(old.A.numpy())) old.fit(X, max_iter=max_iter, verbose=False) fits["pre-change"] = (list(old.ll_history), old_a) diff --git a/pamica/tests/test_reference_constants.py b/pamica/tests/test_reference_constants.py index 07f76175..a714d737 100644 --- a/pamica/tests/test_reference_constants.py +++ b/pamica/tests/test_reference_constants.py @@ -559,8 +559,14 @@ def torch_model(cls: Any) -> Any: assert not over, f"pdftype={pdftype}, {k} iteration(s): {over} (bounds {tol})" # The pre-change code: the same trajectory, its log-likelihood shifted - # by the literal's rounding (pamica's log-density was higher by it). - old = torch_model(pre344.torch_impl.core.AMICATorchNG) + # by the literal's rounding (pamica's log-density was higher by it). It + # predates the normalized initial A of issue #341, so it starts from + # the live one, as the reference here does (the seeded state). + old = torch_model( + pre_change.with_normalized_initial_mixing( + pre344.torch_impl.core.AMICATorchNG + ) + ) old.fit(X, max_iter=k, verbose=False) assert np.all(old.pdtype.numpy() == pdftype) and old.n_kurt_done == 0 shift = np.asarray(old.ll_history) - ref.LL diff --git a/pamica/torch_impl/core.py b/pamica/torch_impl/core.py index 650a685c..a7de986c 100644 --- a/pamica/torch_impl/core.py +++ b/pamica/torch_impl/core.py @@ -60,6 +60,7 @@ from .. import restarts from .. import schedule from ..component_layout import rows_from_legacy_columns +from ..initialization import initial_mixing from ..metrics import mir as mir_metric from ..metrics import model_probability_from_loglik, pairwise_mi from ..rank import ( @@ -1188,10 +1189,9 @@ def _initialize_parameters(self): n, m, ncomp, nmix = self.n_channels, self.n_models, self.n_comps, self.n_mix # One row per component (issue #334): model h's block is rows - # h*n..(h+1)*n-1, the same n x n draw the column layout held before. - A_np = np.zeros((ncomp, n), dtype=np.float64) - for h in range(m): - A_np[h * n : (h + 1) * n, :] = np.eye(n) + 0.01 * (0.5 - rng.rand(n, n)) + # h*n..(h+1)*n-1. Drawn and normalized to unit-norm components as the + # reference does (issue #341, pamica.initialization). + A_np = initial_mixing(rng, n, m) comp_list_np = np.zeros((n, m), dtype=np.int64) for h in range(m): From 3fbe1abb83032dcc3d0eb8658120d27287fd9e33 Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Wed, 23 Sep 2026 15:30:03 -0700 Subject: [PATCH 17/26] Phase 16: audit the documentation against the epic (#353) * Delete the dead legacy pdf code in numpy_impl/pdf.py choose_pdf_type had no callers, and compute_pdf's pdftype 2-4 branches were unreachable: every caller uses the generalized Gaussian. Tested with the pdf, reference-constant and viz suites. * Correct public docstrings against the epic code Rewrap docstring lines that began with an issue number, which the API pages rendered as headings; state the sphered-space shapes of the wrapper's mixing and unmixing accessors; name restart_error among the degenerate stops; drop the claim that natural gradient alone falls short of the reference; fix the NumPy n_channels and doscaling wording. Checked on the bundled sample (accessor shapes and composition). * Say that maxdecs counts decreases, not consecutive ones The reference resets its decrease count only at each ratchet and when Newton switches on (amica15.f90:1062-1076), as every backend does. * Describe the reference's iteration order in the concept pages How AMICA works now walks one iteration as the code runs it: the E-step, the likelihood-decrease response and stopping checks, the exit before any update, then the update with Newton, the A-freeze windows and the doscaling rescale. The false claim that every iteration raises the likelihood is gone, and the flow figure shows the checks before the update. What is AMICA covers the unit-norm scale convention. * Fix the EEGLAB guide's scalp-map example and multi-model note The variance-order example took scalp maps from the sphered-space get_mixing_matrix; it now uses get_sensor_mixing_matrix. The multi-model note still said the per-model layout differs from the reference's, which #159 and #334 fixed. The guide index lists all four guides. * Correct the backends guide on devices, CUDA float32 and scope Auto device selection picks MPS first and the wrapper redirects it to CPU for float64; the raw AMICATorchNG raises there instead (checked on this host). CUDA float32 is about as fast as float64, not faster, as the same page says above. The native backend is listed, the wrappers' two backends and their parameter-file route are stated, and a stale 'sweep is being finalized' note points to the published sweeps. * Document the raw PyTorch backend's automatic device choice With device=None it picks MPS first and, at the float64 default, raises on an Apple machine; the AMICA wrapper redirects to CPU. Checked on this host. * Update install, accessor and precision notes on the entry pages pamica is on PyPI (0.3.3), so getting started no longer says a release is planned, and says unreleased changes need a source install. The README's get_mixing_matrix comment called the sphered-space matrix scalp maps; the examples now use get_sensor_mixing_matrix for maps. The home page no longer calls float32 faster, the final_ll_ note covers the max_iter case, and the NumPy CLI takes either parameter-file format. * Correct the API overview and backend pages List AMICANative in the public import surface; install extras with uv from PyPI or a source checkout; correct the NumPy page's n_channels claim (the raw PyTorch and MLX constructors take it) and list what that backend lacks; drop the viz page's reference to the closed #159 as an open question; replace em-dashes in list items. * Say the reference compiles Newton off, not on amica15_header.f90 declares do_newton = .false.; only the bundled input.param turns it on. The Newton docstrings and the concept page said the reference runs Newton by default. * Correct the differences guide's Newton row and backend table The reference compiles do_newton off (only the bundled input.param turns it on); row 5 names singular_ll; the backend table gains the variance_order, scoring and wrapper rows and the wrapper .pt save; an obsolete pre-#334 comparison of two retired sharing metrics is replaced by the current single-kernel account; the LLt exception names the last iteration; table cells say none instead of an em-dash. * Bring the validation guide's non-numeric claims up to date The degenerate-fit row still said raw backends were ungated (#306 is done) and named only the likelihood stops; the EEGLAB section still said multi-model files are not in the reference layout. Parity figures are left to #351. * Make the Unreleased changelog one coherent account Group the phase entries by what changed (fitting, MLX and the wrappers, the MNE wrapper, persistence and exports, fixes, removals, docs) and open with a warning that default fits differ from 0.3.3 and why. Drop the entries a later phase superseded (Phase 8's pending #344 note, the polish round's harness and Phase 1's remaining-gap notes), merge the duplicate pdftype-default entries, correct the n_channels claim, and mark measurements taken before later phases. No figure is changed. * Map the shared modules and the epic's fitting changes in AGENTS.md Add rank, schedule, initialization, component_layout and reference_constants (with blocktune, restarts and the params reader) as the shared-decision modules, plus mne_compat, native and metrics; name amica15.f90 as the reference source; add a status line for the reference-fidelity changes and the #306/#339 degenerate-fit extensions; shorten the epic #278 history sentence. * Bring the context plan and progress summary up to date Record epic #324's phases and the reference-fidelity changes, the #306/#339 degenerate-fit extensions and the #334 sharing oracle; close the stale release items (repository transfer, docs deploy, PyPI and Zenodo are done) and the #15 coverage item. * Document the opt-in native-binary tests and MNE's pre-whitener The testing page names AMICA_RUN_FORTRAN=1 and the mne_tests directory; the MNE page's source formula includes the per-channel-type scaling the export applies. * Say shared component, not shared column, in AMICAICA's docstring * Note that doscaling is on by default in What is AMICA * Avoid em-dashes in the plan's updated phase headings * Mention the post-update non-finite stop in How AMICA works * Reword the paper's framing in nuanced terms Drop negation-contrast and candor framing outside the Validation section, count releases as of today (nine, six on PyPI), and describe the MEG status as end-to-end use without measured parity. * Drop candor framing in the differences guide and ADR 0007 * Rebuild paper.pdf * State the complete transform in get_unmixing_matrix The docstring called W @ get_sphere() the full transform; transform also subtracts the mean before sphering and the model center after. The MIR docstrings and the metrics page now call W @ sphere the linear part. Checked on the bundled sample with two models: the full formula matches transform exactly, W @ sphere @ X is off by up to 4.2, and MIR is unchanged by a shift of the data. * Say which default fits #335 and #334 reach, in the changelog The opening warning said the schedule gates and component rows matter only with Newton, rejection, restarts or sharing; they also reach plain default fits in narrow cases (a maxdecs ratchet past newt_start, and the per-component ndtmpsum sum near a grad-norm stop). The stopping iteration's skipped steps are listed in code order (kurtosis switch, share scan, MIR waypoint, rejection), in ADR 0008 too, and the #304 entry says the fit applies the file's settings. * Replace the two em-dashes left in the validation guide Prose only; no figure on those lines changed. * Say that fit applies from_params_file's settings from_params_file stores the file's settings on the instance; fit passes them to the backend constructor as defaults and emits the warning. * State effects directly in the prose this PR added Reword contrast framings in the concept page, home page, viz page and module docstring, and the differences guide's opening, per the style rule: say what the code does and its known effect. --------- Co-authored-by: github-actions[bot] --- .../decisions/0007-component-row-layout.md | 2 +- .context/decisions/0008-iteration-order.md | 2 +- .context/plan.md | 43 +- .context/progress_summary.md | 30 +- AGENTS.md | 37 +- README.md | 12 +- docs/api/index.md | 18 +- docs/api/metrics.md | 11 +- docs/api/mlx-backend.md | 3 +- docs/api/mne-compat.md | 4 +- docs/api/native-backend.md | 4 +- docs/api/numpy-backend.md | 12 +- docs/api/viz.md | 11 +- docs/assets/figures/amica-algorithm-flow.svg | 69 +- docs/changelog.md | 727 +++++++++--------- docs/concepts/how-amica-works.md | 145 ++-- docs/concepts/index.md | 10 +- docs/concepts/what-is-amica.md | 29 +- docs/concepts/what-is-ica.md | 11 +- docs/development/testing.md | 15 +- docs/getting-started.md | 31 +- docs/guides/amica-differences.md | 51 +- docs/guides/backends.md | 39 +- docs/guides/eeglab.md | 17 +- docs/guides/index.md | 14 +- docs/guides/validation.md | 15 +- docs/index.md | 17 +- pamica/amica.py | 90 ++- pamica/mlx_impl/core.py | 61 +- pamica/mne_compat/core.py | 2 +- pamica/numpy_impl/core.py | 11 +- pamica/numpy_impl/pdf.py | 186 +---- pamica/torch_impl/core.py | 70 +- pamica/viz.py | 12 +- paper.md | 54 +- paper.pdf | Bin 375736 -> 375572 bytes 36 files changed, 982 insertions(+), 883 deletions(-) diff --git a/.context/decisions/0007-component-row-layout.md b/.context/decisions/0007-component-row-layout.md index 607b9568..51e816fe 100644 --- a/.context/decisions/0007-component-row-layout.md +++ b/.context/decisions/0007-component-row-layout.md @@ -81,7 +81,7 @@ and refuses an `A` that does not invert the `W` beside it (a multi-model directo rejected. A component shared by two models sits in a row of two different blocks in that layout, so the tied object cannot be stored once, and every future per-component operation would need the same translation. - **Convert merged old saves by re-deriving the merge under the new metric:** - rejected. The old merge tied parameters that are not components, so there is no fitted state to convert; a refit is the only honest answer. + rejected. The old merge tied parameters that are not components, so there is no fitted state to convert, and a refit is required. - **Leave the EEGLAB `A` in the old C-order layout:** rejected. The file then had no meaning outside pamica for several models, and the reference's layout is also what `load_results` should read from a reference run. diff --git a/.context/decisions/0008-iteration-order.md b/.context/decisions/0008-iteration-order.md index 76098c3d..f7d7df62 100644 --- a/.context/decisions/0008-iteration-order.md +++ b/.context/decisions/0008-iteration-order.md @@ -50,7 +50,7 @@ Both new reasons are degenerate, like `nan_ll`. on a convergence stop the returned parameters are the ones whose likelihood is `ll_history[-1]`, and after a restore they are an earlier iterate's, whose likelihood is an earlier `ll_history` entry. At `max_iter` the last iteration still takes its update, as in the reference, so there `final_ll_ == ll_history[-1]` is one update behind the returned parameters. - A convergence stop no longer runs a share scan, a kurtosis switch, a `mir_history_` waypoint or a rejection pass on its stopping iteration. + A convergence stop no longer runs a kurtosis switch, a share scan, a `mir_history_` waypoint or a rejection pass on its stopping iteration. - `share_iter` values from 1 to 6, and `share_start` values below 1, that were accepted with sharing off now raise. No bundled configuration or parameter file used one. - A non-finite value that used to be applied or returned now ends the fit as degenerate: diff --git a/.context/plan.md b/.context/plan.md index b6405fa4..3363286a 100644 --- a/.context/plan.md +++ b/.context/plan.md @@ -36,6 +36,8 @@ (`stop_reason` nan_ll/singular_ll) as usable. `fit` sets `is_fitted_` only on a converged fit and exposes `converged_`/`stop_reason_`; `transform`/`get_mixing_matrix`/`get_unmixing_matrix`/ `save` raise a clear degenerate error (mirroring `state_dict`) instead of returning NaN sources. + Extended to the raw backends' accessors (#306) and to non-finite steps and updates + (`nan_direction`/`nan_params`, #339). - [x] Best-iterate safeguard (issue #51): `AMICATorchNG.fit` returns the highest-LL iterate (`keep_best`, `final_ll_`), not the last, so a late Newton-fallback overshoot no longer leaves the model below a peak it reached. Root cause was return-last, not a bad basin (the sole @@ -44,10 +46,11 @@ `.context/issue-51/`. - [x] Component sharing (issue #60): `share_comps` multi-model reassignment ported to `AMICATorchNG` (de-sphered cosine-similarity merge, `share_start`/`share_iter`/`comp_thresh` - schedule). OFF by default so single-model (#24)/default multi-model (#27) parity is - byte-for-byte. No bit-exact oracle (reference `Spinv2` metric is dead code, like #26); - behavior-validated. See `tests/torch_tests/test_ng_sharing.py`. The A-freeze is not part of - sharing: it is the reference's schedule, applied to every fit (ADR 0008, #345). + schedule), on all three backends. OFF by default. The reference's scan never merges + (`Spinv2` is dead code, like #26), but since #334 (component-row `A`, ADR 0007) the update + from a merged `load_comp_list` state matches the binary to float64 round-off. See + `tests/torch_tests/test_ng_sharing.py` and `tests/test_component_rows.py`. The A-freeze is + not part of sharing: it is the reference's schedule, applied to every fit (ADR 0008, #345). ### Priority 3: Testing & validation - [x] Real-data test suite exercising the PyTorch backend end-to-end (issue #7) - Phase 1, issue #10: @@ -55,7 +58,7 @@ - [x] Integration tests comparing against Fortran outputs (`tests/torch_tests/`) - Phase 1, issue #10 - [ ] Numerical-stability regression tests (mincond/minlog/maxdble/mineig) - [ ] Edge cases (single channel, single sample) -- [ ] `AMICA.save`/`load` and `plot_components` coverage (issue #15) +- [x] `AMICA.save`/`load` and `plot_components` coverage (issue #15, closed) - [x] Performance benchmark: NG runtime vs Fortran binary (#77/#84): CUDA float64 ~4.5x over a 16-thread CPU; MLX ~7x on Apple Silicon; runtime criterion met (see below) @@ -74,6 +77,16 @@ - [x] Runtime within 2-3x of Fortran (met/exceeded: CUDA float64 ~4.5x faster, MLX ~7x, #77/#84) - [x] Real (non-mock) test suite green +### Epic #324: MLX first-class, end to end (plan of record: `.context/issue-324/plan.md`) +- [x] Phases 1-14: MLX `pcakeep`/`pcadb` (#323), params-file reader for every backend (#304), + wrapper backend selection (#313), raw accessor guards (#306), every backend in + `validate_implementations.py` (#315), `doscaling` rows (#333), component rows and sharing + (#334), 1-based schedules (#335), column-major sphere export (#336), the reference's + iteration order and A-freeze (#339, #345), single-precision constants (#344), strict + NumPy options (#346), normalized initial `A` (#341); the PCA residual (#322) on `dev`. +- [~] Phase 15: re-measure every parity figure (#351). +- [~] Phase 16: documentation audit against the finished epic (#352). + ## Notes - Correctness is defined by parity with the Fortran binary, not by convergence alone. - Detailed feature status: `feature_parity.md`. NumPy-to-PyTorch migration is complete (see AGENTS.md). @@ -96,18 +109,17 @@ will be hosted at `eeglab.org/pyAMICA`. and fixed a column-major mixture-param format bug (see [[amica92-eeglab-dropin]] / `.context/scratch_history.md`). -### Phase R2: Documentation — content DONE, standup remains +### Phase R2: Documentation (done) - [x] MkDocs Material site + concepts/API/guides, `docs` extra, `docs.yml` Pages workflow, and community health files (CONTRIBUTING, CODE_OF_CONDUCT, CITATION.cff) built in #97; README de-WIP'd and modernized in #102. `site_url: https://eeglab.org/pyAMICA/` (numpy docstrings, git-revision-date fallback). -- [ ] **Standup (needs the transfer):** deploy Pages at `eeglab.org/pyAMICA` under the - sccn org custom domain (`sccn/pyAMICA` project Pages at the `/pamica` subpath). +- [x] **Standup:** Pages deploy at `https://eeglab.org/pAMICA/` from `sccn/pAMICA` + (`mkdocs.yml` `site_url`, `docs.yml`). -### Phase R3: Transfer to github.com/sccn — REMAINS (user) -- [ ] GitHub repo transfer (preserves issues/PRs/stars/history; auto-redirects old - URLs). Post-transfer: update badge/repo URLs (README, CITATION.cff, paper.md), - wire up the `eeglab.org/pyAMICA` docs deploy target. +### Phase R3: Transfer to github.com/sccn (done) +- [x] The repository lives at `github.com/sccn/pAMICA`; README badges and the docs deploy target + point there. ### Phase R4: JOSS paper — DONE - [x] `paper.md` + `paper.bib` (PR #102, #101): summary, statement of need, validation, @@ -116,10 +128,9 @@ will be hosted at `eeglab.org/pyAMICA`. (corresponding), Delorme, Makeig. See [[amica-release-readiness]]. ### Remaining before actual JOSS submission (user) -- [ ] R3 transfer + R2 docs standup (above). -- [ ] Archived release (Zenodo/Software Heritage) with a matching version. -- [ ] PyPI distribution name (the name `pyamica`/`pamica` is taken on PyPI; the - `import pamica` name is unaffected; deferred to release). +- [x] R3 transfer + R2 docs standup (above). +- [x] Archived release (Zenodo DOI badge in README) with a matching version. +- [x] PyPI distribution: `pamica` is published (0.3.3 is the latest release). - [ ] Fill the `paper.md` corresponding-author ORCID / confirm co-author details at submission (ORCIDs set: Shirazi 0000-0001-5557-259X, Delorme 0000-0002-0799-3557, Makeig 0000-0002-9048-8438). diff --git a/.context/progress_summary.md b/.context/progress_summary.md index 7e6a5eea..817e1d3e 100644 --- a/.context/progress_summary.md +++ b/.context/progress_summary.md @@ -2,7 +2,7 @@ Internal status snapshot for the PyTorch AMICA effort. The living source of truth for parity status is the **Known Issues** section of `../AGENTS.md`; this file summarizes what has landed and -what remains as of the v0.1.0 preparation. +what remains, as of epic #324 (after v0.3.3). User-facing detail is in `docs/changelog.md`. ## Delivered @@ -41,11 +41,14 @@ what remains as of the v0.1.0 preparation. matched 100-iter budget. Single-model #24 parity stays bit-exact (monotone => no restore). See ADR 0003. -### Degenerate-fit contract (issue #50) -- The `AMICA` wrapper no longer treats a degenerate fit (`stop_reason` nan_ll / singular_ll) as - usable. `fit` sets `is_fitted_` only on a converged fit and exposes `converged_` / - `stop_reason_`; `transform` / `get_mixing_matrix` / `get_unmixing_matrix` / `save` raise a clear - degenerate error instead of returning NaN sources. +### Degenerate-fit contract (issues #50, #306, #339) +- The `AMICA` wrapper no longer treats a degenerate fit (`stop_reason` nan_ll / singular_ll / + nan_direction / nan_params, or restart_error under best-of-N) as usable. `fit` sets `is_fitted_` + only on a non-degenerate stop and exposes `converged_` / `stop_reason_`; `transform` / `get_*` / + `write_amica_output` / `save` raise a clear degenerate error instead of returning NaN sources. +- Since #306 the raw `AMICATorchNG`, `AMICAMLXNG` and `AMICA_NumPy` accessors refuse a degenerate + fit too; since #339 every backend stops on a non-finite likelihood, update direction or + parameter before using it. ### Component sharing (issues #60, #334) - `share_comps` runs on all three backends. Since #334 (ADR 0007) `A` stores one component per row, @@ -65,9 +68,20 @@ what remains as of the v0.1.0 preparation. seeded through `load_comp_list` matches PyTorch and NumPy to float64 round-off (`pamica/tests/test_component_rows.py`, opt-in with `AMICA_RUN_FORTRAN=1`). +### MLX first-class and reference fidelity (epic #324) +- `AMICA(backend="mlx")` and `AMICAICA(backend="mlx")` run every wrapper feature on MLX (#313); + MLX has explicit `pcakeep`/`pcadb` (#323); one params-file reader serves every backend (#304). +- Default fits on every backend follow the reference more closely, so trajectories differ from + 0.3.3: the reference's iteration order and unconditional A-freeze (#339, #345, ADR 0008), + `doscaling` of component rows (#333, ADR 0006), component-row storage and correct sharing (#334, + ADR 0007), 1-based schedule gates (#335), a normalized initial `A` (#341) and the reference's + single-precision constants (#344), each decided in one shared module (`schedule.py`, + `component_layout.py`, `initialization.py`, `reference_constants.py`, with `rank.py`). +- `AMICAICA.apply` restores the PCA residual of rank-reduced fits (#322, on `dev`). + ### Structure and infrastructure - Module rename into `numpy_impl/` and `torch_impl/` with topic-based names (issue #34); the public - import surface (`from pamica import AMICA, AMICA_NumPy, AMICATorchNG`) is stable. + import surface (`from pamica import AMICA, AMICA_NumPy, AMICATorchNG, AMICANative`) is stable. - UV is the canonical environment (`pyproject.toml` + `uv.lock`); the legacy conda env is retired. - CI is live and green on `main`: ruff lint/format, pytest (excluding slow/Fortran-binary parity), and a build + clean-env import matrix on Python 3.12 and 3.13. Typos check green. @@ -78,7 +92,7 @@ what remains as of the v0.1.0 preparation. correlation 0.9992, Amari 0.004; rows and bars in `docs/guides/validation.md`), pinned by the `AMICA_RUN_FORTRAN`-gated test in `test_fortran_param_forwarding.py`. -## Remaining before / around v0.1.0 +## Remaining - **Performance benchmark:** DONE. The "runtime within 2-3x of Fortran" success criterion has been measured and met/exceeded: CUDA float64 ~4.5x over a 16-thread CPU, MLX ~7x on Apple Silicon diff --git a/AGENTS.md b/AGENTS.md index 5650fb74..be7c1577 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,14 +2,14 @@ ## Project Context **Purpose:** Python implementation of AMICA (Adaptive Mixture Independent Component Analysis) that reproduces the results of the reference Fortran binary. Targets EEG/EMG source separation with GPU/MPS/CPU support. -**Tech Stack:** Python 3.12+, PyTorch (primary backend, MPS/CUDA/CPU), NumPy/SciPy (legacy backend), matplotlib. Reference implementation is Fortran (`amica17.f90`, `funmod2.f90`). +**Tech Stack:** Python 3.12+, PyTorch (primary backend, MPS/CUDA/CPU), NumPy/SciPy (legacy backend), matplotlib. Reference implementation is Fortran (`amica15.f90`, the source of the validation binary; `funmod2.f90`). **Architecture:** The scikit-learn-style `AMICA` interface wraps `AMICATorchNG` (`torch_impl/core.py`) by default, or `AMICAMLXNG` (`mlx_impl/core.py`) with `backend="mlx"` (#313); `AMICATorchNG` is the natural-gradient EM port that reaches Fortran parity (Newton, exact-EM mixture updates, symmetric-ZCA sphere, Jacobian LL). This is the single PyTorch backend: the earlier Adam/autograd backends (`AMICATorch`, `AMICATorchV2`) and their mixture/optimizer/PDF helper modules were removed in issue #32 as superseded. The legacy NumPy implementation (`numpy_impl/core.py`, retained as `AMICA_NumPy`) carries the same parity fixes plus baralpha and outlier rejection (`do_reject`, ported from the PyTorch backend's `good_idx` mechanism in issue #123). Correctness is defined by parity with the Fortran binary, validated by `validate_implementations.py`. ## Architecture Map ``` pamica/ ├── amica.py # Main scikit-learn-style AMICA interface (wraps AMICATorchNG, or AMICAMLXNG with backend="mlx") -├── __init__.py # Exposes AMICA (PyTorch), AMICA_NumPy (legacy), numpy_impl, torch_impl +├── __init__.py # Exposes AMICA, AMICATorchNG, AMICA_NumPy (legacy), AMICANative, metrics, viz plots ├── torch_impl/ # PyTorch backend │ ├── core.py # Natural-gradient EM port (AMICATorchNG); Fortran-parity, primary backend │ └── utils.py # Preprocessing (sphering, PCA), device selection @@ -18,10 +18,19 @@ pamica/ ├── numpy_impl/ # Legacy NumPy reference (topic-named modules, issue #34) │ ├── core.py # AMICA_NumPy (incl. inlined Newton); pdf.py, data.py, load.py, viz.py, utils.py, cli.py │ └── ... -├── blocktune.py # Shared block-size auto-tuner policy (#232, all backends) -├── restarts.py # Shared best-of-N restart policy (#198, all backends) -├── fortran_params.py # Shared params-file reader (JSON + Fortran input.param, #132/#304) for AMICA.from_params_file, AMICA_NumPy and the NumPy CLI -├── amica17.f90, funmod2.f90 # Fortran reference source (read-only, for parity) +├── mne_compat/core.py # AMICAICA: MNE Raw/Epochs wrapper over AMICA (either backend), to_mne_ica, PCA residual (#322) +├── native/ # AMICANative: runs the released Fortran binary as a backend (engine.py, resolver.py) +├── metrics/, viz.py # MIR/PMI metrics and backend-agnostic plots +│ # Shared decisions every array backend calls (.rules/backend_parity.md): +├── rank.py # numerical rank and pcakeep/pcadb policy (#223, #323) +├── schedule.py # 1-based iteration gates: Newton, rejection, share/A-freeze, scalestep (#335, #345) +├── initialization.py # the reference's normalized initial A (#341) +├── component_layout.py # component-row A and legacy-save conversion (#334, ADR 0007) +├── reference_constants.py # the reference's single-precision density constants (#344) +├── blocktune.py # block-size auto-tuner policy (#232) +├── restarts.py # best-of-N restart policy (#198) +├── fortran_params.py # params-file reader (JSON + Fortran input.param, #132/#304) for every backend +├── amica15.f90, funmod2.f90 # Fortran reference source (read-only, for parity; amica17.f90 is a later GG-only trim) ├── sample_data/ # Sample EEG data + Fortran binary (amica15mac) └── tests/ # Tests, incl. tests/torch_tests/ (vs-Fortran parity) @@ -88,11 +97,9 @@ non-GG pdf families (#265, including the adaptive switcher; see `.context/issue- ported; source extraction (`transform` and the mixing/unmixing/`rho` accessors) and persistence (`state_dict`/`.npz` save-load) landed in epic #278 Phase 1 (#287); the best-iterate safeguard (`keep_best`) landed in Phase 2 (#288); outlier rejection, the LLt-stash-backed scoring accessors, -the EEGLAB export, and MIR/PMI landed in Phase 3 (#289); `variance_order` (the EEGLAB -back-projected-variance component order) landed in the epic's post-Phase-3 polish round, ahead of -merge to `dev`, closing the one accessor gap Phase 3 left open -- epic #278 is complete; the -remaining MLX gap vs the PyTorch backend is none, other than float32-only precision (Apple GPUs -have no float64). +the EEGLAB export, and MIR/PMI landed in Phase 3 (#289), and `variance_order` in the polish round. +With explicit `pcakeep`/`pcadb` (#323) and the wrappers' `backend="mlx"` (#313, epic #324), the only +remaining MLX gap vs the PyTorch backend is float32-only precision (Apple GPUs have no float64). ## Key Files - **Main interface:** `pamica/amica.py` (thin wrapper over `AMICATorchNG`, or `AMICAMLXNG` with @@ -118,6 +125,11 @@ have no float64). the bundled sample (LL within 3.2e-5, correlation 0.9992, Amari 0.004; rows and bars in `docs/guides/validation.md`), pinned by the `AMICA_RUN_FORTRAN`-gated test in `test_fortran_param_forwarding.py`. +- Epic #324 aligned every backend's default fit with the reference, so default trajectories differ + from 0.3.3 (changelog warning): the reference's per-iteration order, with the exit before the update + and the A-freeze on every fit (ADR 0008), `doscaling` of component rows (ADR 0006), component-row + storage (ADR 0007), 1-based schedule gates, a normalized initial `A` and the reference's + single-precision constants, each decided once in a shared module (map above). - Newton and exact-EM updates are implemented in `AMICATorchNG` and the legacy NumPy `numpy_impl/core.py` (both Fortran-faithful). Adaptive PDF (#26) is DONE (all five `pdftype` families + ext-Infomax switcher); full multi-model matching (#27) is validated by distributional equivalence. @@ -131,7 +143,8 @@ correlation ~0.997, > 0.95 gate cleared; root cause in `.context/issue-24/`). Al stability (posdef, 0 fallbacks), backend consolidation (#32/#31), NumPy CLI save/load format (#30), NG save/load persistence (#36), and the degenerate-fit contract (#50: the `AMICA` wrapper marks a degenerate fit unusable via `converged_`/`stop_reason_` and refuses `transform`/`get_*`/`save`, -instead of returning NaN sources). +instead of returning NaN sources; since #306 the raw backends refuse their own accessors too, and +since #339 every backend stops on a non-finite likelihood, direction or parameter before using it). **Adaptive-PDF selection: DONE (#26).** `AMICATorchNG` now supports all five `amica15.f90` source-density families via `pdftype`: 0 generalized Gaussian (default, unchanged), 2 Gaussian, diff --git a/README.md b/README.md index 945425c1..4dc74491 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,10 @@ AMICA (Adaptive Mixture ICA) is an advanced blind source separation algorithm th ## Installation -The canonical environment is [uv](https://docs.astral.sh/uv/): +Released versions are on PyPI: `uv add pamica` (or `uv pip install pamica`). +The development version, with the changes listed as unreleased in the +[changelog](https://eeglab.org/pAMICA/changelog/), installs from source; the +canonical environment is [uv](https://docs.astral.sh/uv/): ```bash git clone https://github.com/sccn/pAMICA.git @@ -41,7 +44,7 @@ uv run pytest # optional: run the tests ``` The optional Apple-GPU backend (MLX, Apple Silicon only) installs with the `mlx` -extra: `uv pip install mlx`. +extra: `uv sync --extra mlx` from source, or `uv add "pamica[mlx]"`. ## Usage @@ -56,7 +59,7 @@ from pamica import AMICA model = AMICA(n_models=1, n_mix=3).fit(X) sources = model.transform(X) # (n_sources, n_samples) -A = model.get_mixing_matrix() # sensor-space scalp maps +maps = model.get_sensor_mixing_matrix() # scalp maps, (n_channels, n_sources) order = model.variance_order() # EEGLAB IC order (IC1 = highest variance) ``` @@ -93,7 +96,8 @@ mod = loadmodout15('amicaout'); % components in EEGLAB variance order ### Legacy NumPy CLI -The NumPy reference backend keeps a JSON-driven command-line interface: +The NumPy reference backend keeps a command-line interface, driven by a +pamica JSON parameter file or a Fortran `input.param`: ```bash python -m pamica.numpy_impl.cli params.json --outdir results diff --git a/docs/api/index.md b/docs/api/index.md index 220d2c00..7ca421e9 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -3,21 +3,23 @@ The public import surface is stable: ```python -from pamica import AMICA, AMICA_NumPy, AMICATorchNG +from pamica import AMICA, AMICA_NumPy, AMICANative, AMICATorchNG ``` -- **[`AMICA`](amica.md)** — the main scikit-learn-style interface. +- **[`AMICA`](amica.md)**: the main scikit-learn-style interface. Wraps a natural-gradient EM backend: PyTorch by default, or MLX with `backend="mlx"`. Start here. -- **[`AMICATorchNG`](torch-backend.md)** — the PyTorch natural-gradient EM +- **[`AMICATorchNG`](torch-backend.md)**: the PyTorch natural-gradient EM backend (Fortran parity). The `AMICA` interface delegates to this class by default. -- **[`pamica.metrics`](metrics.md)** — separation-quality metrics (`mir`, +- **[`pamica.metrics`](metrics.md)**: separation-quality metrics (`mir`, `pairwise_mi`, `block_diagonal_order`) as free functions over plain arrays. Also reachable as `AMICA.mir`/`AMICA.pmi` on a fitted model. -- **[`pamica.viz`](viz.md)** — backend-agnostic plots over a written +- **[`pamica.viz`](viz.md)**: backend-agnostic plots over a written `amicaout` directory. -- **[`AMICA_NumPy`](numpy-backend.md)** — the legacy NumPy reference +- **[`AMICA_NumPy`](numpy-backend.md)**: the legacy NumPy reference implementation, retained as an oracle and for its command-line interface. +- **[`AMICANative`](native-backend.md)**: the Fortran reference binary itself, + run on your data as a fourth backend. The optional Apple-Silicon GPU backend is imported separately and is not part of the default import surface: @@ -26,7 +28,7 @@ the default import surface: from pamica.mlx_impl import AMICAMLXNG # requires the `mlx` extra ``` -- **[`AMICAMLXNG`](mlx-backend.md)** — the optional Apple-GPU (MLX) backend; the +- **[`AMICAMLXNG`](mlx-backend.md)**: the optional Apple-GPU (MLX) backend; the fastest option on Apple Silicon (float32). `AMICA(backend="mlx")` builds it without this import. @@ -36,6 +38,6 @@ The optional MNE-Python wrapper is likewise imported explicitly: from pamica.mne_compat import AMICAICA # requires the `mne` extra ``` -- **[`AMICAICA`](mne-compat.md)** — fit AMICA from an MNE `Raw`/`Epochs` and +- **[`AMICAICA`](mne-compat.md)**: fit AMICA from an MNE `Raw`/`Epochs` and interoperate with `mne.preprocessing.ICA` (`get_sources`, `apply`, `plot_components`, `to_mne_ica`), on either backend (`backend="mlx"`). diff --git a/docs/api/metrics.md b/docs/api/metrics.md index 62729dea..beb485ad 100644 --- a/docs/api/metrics.md +++ b/docs/api/metrics.md @@ -4,14 +4,15 @@ Quality metrics for a decomposition, as free functions over plain arrays. They are backend-agnostic and independent of any fitted model object, so they work on sources from any source. -- **`mir`** — Mutual Information Reduction, in nats: how much mutual information +- **`mir`**: Mutual Information Reduction, in nats: how much mutual information a linear unmixing removes from the data. Higher is a better separation. It - needs the **full raw-data-to-sources transform** (the unmixing composed with + needs the **linear map from raw data to sources** (the unmixing composed with the sphering matrix), which must be square and invertible, since the estimate - includes a log-Jacobian term. -- **`pairwise_mi`** — the symmetric mutual-information matrix between sources. + includes a log-Jacobian term. The mean and per-model center that `transform` + subtracts are offsets, and MIR does not depend on them. +- **`pairwise_mi`**: the symmetric mutual-information matrix between sources. Its diagonal is each source's own entropy, not a mutual information. -- **`block_diagonal_order`** — a permutation that clusters mutually dependent +- **`block_diagonal_order`**: a permutation that clusters mutually dependent components near the diagonal, for reading structure out of a `pairwise_mi` matrix. diff --git a/docs/api/mlx-backend.md b/docs/api/mlx-backend.md index 280c3e80..7dcee029 100644 --- a/docs/api/mlx-backend.md +++ b/docs/api/mlx-backend.md @@ -67,7 +67,8 @@ and is not part of the default `import pamica` surface from pamica.mlx_impl import AMICAMLXNG # requires the `mlx` extra ``` -Install it with `uv pip install mlx` or the `mlx` extra (`pip install pamica[mlx]`). +Install it with the `mlx` extra (`uv add "pamica[mlx]"`, or `uv sync --extra mlx` from a source checkout) +or with `uv pip install mlx`. Because it computes in float32, use the [PyTorch backend](torch-backend.md) on CUDA/CPU for float64 Fortran-parity runs. diff --git a/docs/api/mne-compat.md b/docs/api/mne-compat.md index 5977a502..87e3c0fb 100644 --- a/docs/api/mne-compat.md +++ b/docs/api/mne-compat.md @@ -10,7 +10,7 @@ MNE is an optional dependency, so `import pamica` never requires it. Install the extra and import the wrapper explicitly: ```bash -pip install pamica[mne] +uv add "pamica[mne]" # or, from a source checkout: uv sync --extra mne ``` ```python @@ -84,7 +84,7 @@ The wrapper's `get_sources`, `apply`, `get_components`, `plot_components` and `plot_sources` delegate to this object, so they reproduce `AMICA.transform` exactly: MNE computes sources as -`unmixing_matrix_ @ pca_components_[:n_components_] @ (X - pca_mean_)`, and +`unmixing_matrix_ @ pca_components_[:n_components_] @ (X / pre_whitener_ - pca_mean_)`, and the export maps pamica's mean, symmetric-ZCA sphere and unmixing into those matrices (writing the sphere as `V diag(1/√e) Vᵀ` with `V` orthonormal so MNE's scalp maps come out in channel space). The equivalence diff --git a/docs/api/native-backend.md b/docs/api/native-backend.md index 9f289a29..07fa49a9 100644 --- a/docs/api/native-backend.md +++ b/docs/api/native-backend.md @@ -25,8 +25,8 @@ sources = model.transform(X) # source activations, EEGLAB variance order `fit` writes the data and a full `input.param` (so it does not depend on an installed `sample_data`), runs the binary, and loads the result with -`loadmodout`, exposed as `model.output_` (an -[`AmicaOutput`](numpy-backend.md)). `n_models` and `n_mix` are friendly aliases; +`loadmodout`, exposed as `model.output_` (a +`pamica.numpy_impl.load.AmicaOutput`). `n_models` and `n_mix` are friendly aliases; any Fortran `input.param` field (`max_iter`, `lrate`, `pdftype`, `do_newton`, ...) can be passed as a keyword. A collapsed fit (non-finite weights) is raised as a clear degenerate-fit error rather than an opaque SVD failure. diff --git a/docs/api/numpy-backend.md b/docs/api/numpy-backend.md index 16a568e4..5705d3d4 100644 --- a/docs/api/numpy-backend.md +++ b/docs/api/numpy-backend.md @@ -21,9 +21,10 @@ Without one (the default) a fit writes nothing, like the PyTorch and MLX backend This backend implements only the generalized-Gaussian source density (`pdftype=0`); `AMICA_NumPy(pdftype=...)` with any other value raises -`NotImplementedError` at construction instead of silently doing nothing -- see -the backend-differences table in -[amica-differences.md](../guides/amica-differences.md). +`NotImplementedError` at construction instead of silently doing nothing. +It also has no best-iterate safeguard (`keep_best`), Mutual Information Reduction (MIR) diagnostic, +`variance_order` or per-sample scoring accessors, and a second `fit` on the same instance continues from the first; +see the [backend-differences table](../guides/amica-differences.md#backend-differences). `AMICA_NumPy(**kwargs)` rejects a keyword argument it does not recognize (issue #346), before any other construction runs. @@ -34,9 +35,8 @@ An option implemented on the PyTorch backend (`AMICATorchNG`) but not this one also raises `TypeError`, naming the option and pointing to `AMICA(backend='torch')`. `n_models`/`n_mix` (`AMICATorchNG`'s own spelling of this backend's `num_models`/`num_mix`) get a message naming the correct spelling instead; -`n_channels` gets its own message, since it is inferred from the data passed -to `fit()` on every backend, torch included, not a constructor keyword on -any of them. +`n_channels` gets its own message: this backend, like the `AMICA` wrapper, +infers it from the data passed to `fit()`. Every offending keyword is named in one error, however many different kinds are mixed in the same call. The three settings this backend spells differently from `AMICATorchNG` diff --git a/docs/api/viz.md b/docs/api/viz.md index ec8e71c8..f1909d45 100644 --- a/docs/api/viz.md +++ b/docs/api/viz.md @@ -7,15 +7,16 @@ Top-level, backend-agnostic plots for a fitted model's output accept an optional `ax`/`axes` to draw on, instead of returning `None` and mutating pyplot's global state. -- **`plot_pmi_heatmap`** — a components-by-components pairwise mutual-information +- **`plot_pmi_heatmap`**: a components-by-components pairwise mutual-information heatmap (see `pamica.metrics.pairwise_mi`), reordered to cluster related components near the diagonal. -- **`plot_model_probability`** — for a multi-model fit, two stacked panels: each +- **`plot_model_probability`**: for a multi-model fit, two stacked panels: each model's posterior probability over time, and the log-likelihood of the most probable model at each timepoint. -A per-component scalp-topography plot is not included yet: deriving source -activations from a loaded `AmicaOutput` depends on an unsettled `W` convention -question, tracked in [#159](https://github.com/sccn/pAMICA/issues/159). +A per-component scalp-topography plot is not included: the one upstream +reference it could be checked against (EEGLAB's `pop_topohistplot`) is broken +on current EEGLAB, so it is left out until it can be verified. For scalp maps, +use the MNE wrapper's [`plot_components`](mne-compat.md). ```python from pamica import plot_pmi_heatmap, plot_model_probability diff --git a/docs/assets/figures/amica-algorithm-flow.svg b/docs/assets/figures/amica-algorithm-flow.svg index 5f832970..70fd99b5 100644 --- a/docs/assets/figures/amica-algorithm-flow.svg +++ b/docs/assets/figures/amica-algorithm-flow.svg @@ -1,4 +1,4 @@ - + @@ -9,26 +9,49 @@ - - - - - - - - Center + Whiten - (ZCA sphere) - - E-step: - responsibilities - - M-step: source-density params (exact EM) - + unmixing W - (natural gradient -> Newton) - - converged? - - Best-iterate solution - no - yes + + + + + + + + + + + + Center + whiten + (ZCA sphere) + + + 1. E-step: responsibilities, + log-likelihood, + update direction + + + 2. Checks: likelihood + decreased? reduce the + learning rate; stop? + + + stopping + check fired? + + + 3. Update (M-step): + densities, weights: + exact EM + W: natural gradient + or Newton, held in + A-freeze windows + rescale components + + + Exit before any update: + best-iterate solution + + no + yes + next iteration + (up to max_iter) diff --git a/docs/changelog.md b/docs/changelog.md index d128ba22..c6bb3253 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,7 +5,36 @@ Release notes are also published on the ## Unreleased -- **Phase 12 of epic #324: the drawn initial mixing matrix has unit-norm components, as in the reference (issue #341).** +MLX becomes a first-class backend, reachable through every wrapper feature (epic #324, completing the raw backend of epic #278), +and every backend's fitting follows the Fortran reference more closely. + +!!! warning "Default fits differ from 0.3.3" + A default fit on any backend (PyTorch, NumPy or MLX) follows a different trajectory than in 0.3.3, + so its fitted parameters differ, and results computed with an earlier version do not reproduce bit for bit. + Five changes, each aligning a step with the reference and each described under + [Fitting follows the reference](#fitting-follows-the-reference-every-backend), reach every default fit: + `doscaling` normalizes each component's mixing vector, where it used to normalize stored columns (issue #333); + the drawn initial mixing matrix has unit-norm components (issue #341); + each iteration runs in the reference's order, so a likelihood decrease takes effect in the same iteration and a convergence stop returns the parameters its likelihood was computed from (issue #339); + the reference's A-freeze holds the mixing update on iterations 100-105, 200-205, and so on, of every fit (issue #345); + and the density normalizers are the reference's single-precision constants (issue #344). + Two more reach default fits in narrow cases. + With schedule gates counted from 1 (issue #335), a `maxdecs` ratchet that completes on iteration `newt_start + 1` tightens the rho-rate ceiling whether or not Newton is on, + and on NumPy a non-finite likelihood on iteration `restartiter + 1` ends the fit. + With components stored as rows (issue #334), the gradient norm `ndtmpsum` is summed per component, + which moves it by float round-off and can change a gradient-norm stop that falls within that round-off of `min_nd`. + Fits with Newton, outlier rejection or merging `share_comps` change further through the same two issues. + Seeded from the same initialization, `A`, `mu` and `sbeta` now match the native binary to float64 round-off over the first iterations; + before, they departed from its trajectory on the first iteration. + Scale-blind results, such as the log-likelihood, the component maps and the matched correlation with the reference, moved by small amounts in the measurements reported below. + To compare parameters element by element with the reference or with an earlier fit, refit. + Saved models load, converted where the storage changed; + `share_comps` models in which components had merged are refused and must be refit + ([Persistence](#persistence-and-exports)). + +### Fitting follows the reference (every backend) + +- **The drawn initial mixing matrix has unit-norm components, as in the reference** (issue #341, epic #324 Phase 12). The reference draws each model's block of `A` as `0.01 * (0.5 - u)`, sets its diagonal to one and divides every component by its Euclidean norm (amica15.f90:805-823); its restart after a non-finite likelihood redraws the same way (:1026-1044). @@ -17,13 +46,6 @@ Release notes are also published on the The generator is called exactly as before, so `mu` and `beta` start from the same values. A supplied or loaded `A` is used as is, as the reference uses a loaded one (:793-802): an `A` set on a NumPy model before `fit`, a NumPy refit, a PyTorch `state_dict`, a saved `AMICA` model and an MLX save. - - **Behavior change (NumPy backend): a supplied initial `A` is checked at fit start.** - An `A` set before `fit`, or left by a previous fit on the same instance, now raises `ValueError` naming the problem - when its shape is not `(num_comps, n_channels)` (one row per component), when it holds a non-finite entry, - or when a model's block is numerically singular. - Before, a wrong shape or a singular block raised `LinAlgError` from deep in the fit, - and a NaN entry ended the fit with no stop reason. - The PyTorch and MLX backends take an `A` only through their saves, which are already validated. - **Behavior change: every fit from a drawn `A` starts from a different point.** Normalizing is not a compensated rescale, so it changes the first E-step and the trajectory after it. With `doscaling` on (the default), the first rescale used to normalize the components after the first iteration anyway, @@ -32,9 +54,16 @@ Release notes are also published on the and the final one by 1.8e-6 and 4.5e-4. With `doscaling=False` the initial scale was never corrected, so the change reaches the whole fit (the final log-likelihood moves by 6.6e-7 and 9.2e-4). - The validation harness (100 iterations, all three backends, against the native binary from its own initialization) is unchanged at its precision: + When this change landed, the validation harness (100 iterations, all three backends, against the native binary from its own initialization) was unchanged at its precision: mean matched correlation 0.9991 and Amari distance 0.0038 before and after, and a log-likelihood gap of 2.7e-4 to 2.8e-4 (2.6e-4 to 2.7e-4 before). + - **Behavior change (NumPy backend): a supplied initial `A` is checked at fit start.** + An `A` set before `fit`, or left by a previous fit on the same instance, now raises `ValueError` naming the problem + when its shape is not `(num_comps, n_channels)` (one row per component), when it holds a non-finite entry, + or when a model's block is numerically singular. + Before, a wrong shape or a singular block raised `LinAlgError` from deep in the fit, + and a NaN entry ended the fit with no stop reason. + The PyTorch and MLX backends take an `A` only through their saves, which are already validated. - The draw itself still cannot match the reference's, whose generator is gfortran's `random_number`. Two new gated oracles check the two halves instead: the binary's own drawn initialization (written by a run with `max_iter=0`) has unit-norm components with the recipe's shape, @@ -43,49 +72,20 @@ Release notes are also published on the - Tests: `pamica/tests/test_initial_mixing.py` checks the recipe, the start of every fit on every backend (with a control that fails on the code before this change), the supplied and loaded paths and the refused ones, NumPy's `fix_init`, the NumPy restart redraw, and the two gated oracles. The byte-identity tests that pin earlier changes against older commits now start the older code from the new initial `A` - (`pamica.tests.pre_change.with_normalized_initial_mixing`), while the live side keeps its pre-#344 constants (Phase 13), + (`pamica.tests.pre_change.with_normalized_initial_mixing`), while the live side keeps its pre-#344 constants, and still pass bit for bit. Data-driven tests whose trajectories moved were re-searched or re-recorded: the MLX `pdftype=1` restore recipe, the MLX MIR restore test, the MLX fit-path canary, the seed of the `doscaling` native oracle, and the early-merge collapse oracle. -- **Phase 13 of epic #324: every backend uses the reference's single-precision constants (issue #344).** - The reference writes several density normalizers as default-kind Fortran literals widened with `dble`, for example `log(dble(2.506628274))`, - so the binary uses the float32 rounding of each decimal, not the decimal. - pamica used the decimals' double values. - The PyTorch, NumPy and MLX backends now take the reference's values from one module, `pamica/reference_constants.py` - ([the differences guide](guides/amica-differences.md#single-precision-constants-issue-344) has the table). - - **Behavior change: fits in which a mixture reaches `rho == 2` move slightly, toward the reference.** - The default `maxrho = 2` clamps mixtures there, where the reference's normalizer, `log(dble(1.772453851))`, is 3.0e-8 above the `0.5 * log(pi)` pamica used, - so default fits of the generalized Gaussian take this branch once a mixture reaches the clamp - (on 4096 samples of the bundled recording, a two-model fit gets there on its fifth iteration). - Seeded with a warm two-model state that has mixtures at `rho == 2`, the PyTorch and NumPy updates now match the native binary to float64 round-off: - after three iterations the log-likelihood differs by 1.6e-13, `A` by 3.8e-13 and `mu` by 5.7e-9, - where the previous code was off by 1.2e-8, 3.4e-8 and 8.4e-3. - - **Behavior change: the Gaussian (`pdftype` 2) and the sub- and super-Gaussian cosh families (`pdftype` 4 and 1) report a different log-likelihood**, - lower by 3.7e-10 and 2.0e-8 and higher by 2.1e-8, which now matches the binary's to 2.7e-15. - Their parameter updates move only by round-off, since a family's normalizer shifts every mixture alike. - - The underflow guard of the rho update, `epsdble`, is likewise the reference's `1.0e-16` in single precision (1.0000000168623835e-16). - - The NumPy plotting helper `pamica.numpy_impl.pdf.compute_pdf`, which `viz.plot_pdf_fits` draws, takes its normalizers from the same module, - so it draws the density the fit uses; its unused companion `compute_log_pdf` is removed. - - Row 16 of the differences page now records the one kind of single-precision literal pamica keeps at its decimal value: - the compiled-in defaults of `input.param` keys, which the binary uses only when the key is missing - (then its `comp_thresh` is 0.9900000095); a value given in `input.param` is read as double, and pamica's native engine gives every one. - - Tests: `pamica/tests/test_reference_constants.py` pins every constant against the float32 rounding of its literal on the cited reference line, - computed by exact rational arithmetic; pins the sweep of both reference sources; checks that no backend keeps its own copy; - and (opt-in, `AMICA_RUN_FORTRAN=1`) seeds the native binary for `pdftype` 2, 4 and 1. - The merged-state oracle in `pamica/tests/test_component_rows.py` no longer holds `maxrho` at 1.99. - The tests that compare a live backend bit for bit with code from before this change - (`test_component_rows.py`, `test_doscaling_rows.py` and `mlx_tests/test_mlx_fit_noop.py`) - give the live backend its old constants first, so they still isolate the change they were written for. - -- **Phase 11 of epic #324: every backend follows the reference's iteration order (issues #339 and #345).** +- **Every backend follows the reference's iteration order** (issues #339 and #345, epic #324 Phase 11). Each iteration of every backend (PyTorch, NumPy and MLX) now runs in the reference's order (amica15.f90:949-1142, - [ADR 0008](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0008-iteration-order.md)): + [ADR 0008](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0008-iteration-order.md); + [How AMICA works](concepts/how-amica-works.md#the-algorithm) walks through it): the E-step computes the log-likelihood and the update direction, then the likelihood-decrease response and the stopping checks run, a fit that stops leaves before any update, and otherwise the parameters are updated with the rates those checks just set. Fits with a likelihood decrease, fits that stop on a convergence check, and fits that reach `share_start` (iteration 100 by default) now end elsewhere, closer to the reference; - every other fit is byte-identical to before. + every other fit is byte-identical to the code before this change. - **Behavior change: a likelihood decrease takes effect in the same iteration's update.** Before, the halved `lrate` and the scaled rho rate reached the update one iteration late. On a seeded 30-iteration run with eight decreases (`lrate=0.5`, no Newton, `doscaling` on), @@ -99,7 +99,7 @@ Release notes are also published on the - **Behavior change: a fit that stops on a convergence check returns the parameters its `final_ll_` was computed from.** On a `min_dll`, gradient-norm or `lrate`-floor stop, the stopping iteration used to take its update anyway, so the returned parameters were one update past `final_ll_`, and the exported `LLt` one update behind them. - That iteration now also runs no share scan, kurtosis switch, `mir_history_` waypoint or rejection pass. + That iteration now also runs no kurtosis switch, share scan, `mir_history_` waypoint or rejection pass. A fit that runs to `max_iter` still updates on its last iteration, as the reference does. - **Behavior change: the reference's A-freeze applies to every fit.** Once `iter >= share_start`, the reference holds the `A` update, its `lrate` ramp and its rho-rate reset @@ -108,12 +108,12 @@ Release notes are also published on the So every default fit of 100 or more iterations now holds `A` on iterations 100-105 (and 200-205, and so on), as the reference does. On a seeded 16-iteration run with `share_start=3` and `share_iter=10`, the gap to the binary fell from 6.1e-3 to 6.6e-7 in log-likelihood (binary floor 4.0e-7 to 4.3e-7) and from 0.19 to 5.0e-6 in `A`. - - Parity on the bundled sample, before and after, on all three backends: - against the bundled 200-iteration reference output, the log-likelihood gap falls from 2.2e-4 to 2.3e-4 down to 1.2e-4 to 1.3e-4, - the mean matched component correlation rises from 0.9972-0.9973 to 0.9982-0.9983 (minimum 0.969-0.970 to 0.981-0.982), - and the Amari distance falls from 6.3e-3 to 4.8e-3. - The validation harness's 100-iteration run keeps its final log-likelihood (gap 2.6e-4 to 2.7e-4), - and its mean matched correlation rises from 0.9988 to 0.9991 (Amari distance 0.0044 to 0.0038), + - Parity on the bundled sample when this change landed, before and after, on all three backends: + against the bundled 200-iteration reference output, the log-likelihood gap fell from 2.2e-4 to 2.3e-4 down to 1.2e-4 to 1.3e-4, + the mean matched component correlation rose from 0.9972-0.9973 to 0.9982-0.9983 (minimum 0.969-0.970 to 0.981-0.982), + and the Amari distance fell from 6.3e-3 to 4.8e-3. + The validation harness's 100-iteration run kept its final log-likelihood (gap 2.6e-4 to 2.7e-4), + and its mean matched correlation rose from 0.9988 to 0.9991 (Amari distance 0.0044 to 0.0038), because the reference holds `A` on its 100th iteration. - **Behavior change: every backend stops the same way on a non-finite value.** A non-finite log-likelihood is never recorded: the NumPy backend used to leave it as the last `self.ll` entry, @@ -143,161 +143,34 @@ Release notes are also published on the - Tests: `pamica/tests/test_iteration_order.py` checks the order, the decrease timing, the stop semantics of every convergence stop and the freeze through real fits on every backend, `pamica/tests/test_nonfinite_stops.py` the non-finite stops by injection, and `pamica/tests/test_iteration_order_native_oracle.py` (opt-in, `AMICA_RUN_FORTRAN=1`) is the native-binary comparison above. - -- **Phase 14 of epic #324: `AMICA_NumPy` rejects unknown/unsupported keyword arguments (issue #346).** - `AMICA_NumPy(**kwargs)` used to forward every keyword into a params dict read with `params.get(...)`, - so a typo (`max_iters=50`) or an option the legacy backend does not implement (`keep_best=True`) - constructed silently and had no effect, - unlike the PyTorch/MLX constructors, - which take explicit keyword parameters and already raised `TypeError` on an unknown name. - - **Behavior change (legacy NumPy backend only): an unrecognized or unsupported keyword argument now raises `TypeError` instead of being silently ignored.** - A typo raises `TypeError` naming the offending keyword(s), - with a `difflib`-based "did you mean" suggestion when a close match exists. - An option implemented on the PyTorch backend (`AMICATorchNG`) but not this one - (for example `keep_best`, `device`, `dtype`, the kurtosis-switch schedule) - raises `TypeError` naming the option and pointing to `AMICA(backend='torch')`. - - The three settings this backend spells differently from `AMICATorchNG` - (`min_nd`/`maxdecs`/`share_iter`, the canonical spelling a params file already resolves either way) - are now also accepted as keyword arguments directly, - translated to this backend's own attribute name the same way the params-file route already translates them. - Passing both spellings of the same setting at once raises `TypeError` naming both, - rather than picking one silently. - - The accepted-keyword set is derived from the same source the params-file routing uses - (`_CONSUMED_KEYS`/`_CANONICAL_TO_NUMPY_KEY`), - and the unsupported-option list is derived from `AMICATorchNG`'s own constructor signature, - so neither can drift from what the constructor actually reads. - - **Review follow-up:** `files`/`data_dim`/`field_dim` (data-location metadata) now work as keyword - arguments directly, with the same meaning as in `params_file`, instead of passing the check but - staying silently inert; `n_models`/`n_mix` (`AMICATorchNG`'s spelling of this backend's own - `num_models`/`num_mix`) get a message naming the correct spelling rather than the generic - (and here wrong) "use `AMICA(backend='torch')`" message; `n_channels` gets its own message, since - it is inferred from the data passed to `fit()` on every backend, not a constructor keyword on any - of them; and every offending keyword in one call is now named in a single error, however many - different kinds are mixed together (a first version named only the first category it found). - The `AMICA` wrapper's own `backend='mlx'` keyword check had the same single-category drop and is - fixed the same way. - -- **Fix: tests no longer make a full clone shallow (issue #343).** - Tests that load historical code ran `git fetch origin --depth 1` to reach the pinned commit; - in a full clone that records a shallow boundary, after which `git gc` can prune history. - Every such test now goes through `pamica/tests/pre_change.py`, which only reads the repository: - when the pinned commit is missing it fails under `CI` and otherwise skips, - naming the command that fetches it (`git fetch origin `, or `git fetch --unshallow origin` in a shallow clone). - `pamica/tests/test_pre_change_loader.py` runs the loader behind a logging `git` wrapper and asserts that nothing is fetched. - -- **Phase 8 of epic #324: `share_comps` compares and merges components (issue #334).** - Every backend (PyTorch, NumPy and MLX) now stores the mixing matrix with one component per row, - `A` of shape `(n_comps, n_channels)`, the reference's `A` transposed ([ADR 0007](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0007-component-row-layout.md)). - A component id in `comp_list` now names the same component in `A` as in the density parameters, - so the share metric compares the components' scalp maps (`get_sensor_mixing_matrix`) - and a merge ties the two components' mixing vectors and densities, as the reference's `identify_shared_comps` does. - Before, both steps used stored columns of each model's block, which are not components. - - **Behavior change: fits with `share_comps=True` in which a merge fires now differ.** - Refit them. - Seeded with a merged `comp_list` through the reference's `load_comp_list`, - the PyTorch and NumPy updates match the native binary to float64 round-off - (after 3 iterations, worst of `doscaling` on and off: `A` 1.5e-12, `mu` 3.7e-9, log-likelihood 4.7e-14), - where the previous code was off by 0.21 in `A` and 4.3e-4 in log-likelihood. - On the bundled sample (2 models, 300 iterations, `share_start=100`, `comp_thresh=0.95`) - the scan now merges three pairs whose maps agree (|cos| 0.956 to 0.971), ending at log-likelihood -3.3416, - where it merged pairs whose maps did not (|cos| 0.06, 0.35 and 0.55) and ended at -3.3484 (-3.3387 with sharing off). - Because the metric now sees how similar the two models still are early in a fit, a scan in the first iterations merges most components, - and a model left with few components of its own can then collapse. - The reference behaves the same way: its similarity on its own early state merges the same pairs, - and its update from the same merged states collapses in step (its own scan never merges, because its similarity is NaN). - The reference's default `share_start=100` avoids that. - - Every fit without a merge is byte-identical to before on every backend, sharing off or scheduled but not firing, - with one exception at float round-off: the weight-gradient norm (`ndtmpsum`), which now sums per component like the reference. - - **Persistence:** the PyTorch `state_dict` is now `format_version` 4 and the MLX save format 2. - An older save loads unchanged in content, converted without loss, unless `share_comps` had merged components: - such a save raises `ValueError` asking for a refit, because its merges were computed under the old semantics. - `AMICA.save` files (still `format_version` 2) go through the same conversion and refusal. - - **EEGLAB export:** the `A` file is now the reference's `A(nw, num_comps)` in column-major order for any number of models - (EEGLAB's `loadmodout15.m` ignores it; single-model files are byte-identical to before). - `pamica.numpy_impl.data.load_results` reads it in that layout - and refuses a multi-model directory written by an earlier version, whose `A` does not invert the `W` beside it; - write such a directory again from the fitted or reloaded model. - - Row 16 of the differences page records that the reference's density normalizers are single-precision literals: - in the exact-Gaussian branch (`rho == 2`) 3.0e-8 away from the one pamica uses, and up to 2.1e-8 in the `pdftype` 1, 2 and 4 families. - Issue #344 decides whether to adopt them; the comments that called pamica's constants bit-for-bit with the binary are corrected. - -- **Phase 7 of epic #324: `doscaling` rescales components, as the reference does (issue #333).** +- **`doscaling` rescales components, as the reference does** (issue #333, epic #324 Phase 7). **Behavior change:** default fits on every backend (PyTorch, NumPy and MLX) now follow the reference's trajectory, so their fitted parameters differ from those of earlier versions. - pamica stores each model's mixing block transposed relative to the reference (the issue #24 convention, now ADR 0006), - so a component is a row of the stored block, + Each model's mixing block was stored transposed relative to the reference (the issue #24 convention, ADR 0006), + so a component was a row of the stored block, but `doscaling` (on by default) normalized stored columns, which is not a change of scale of any component and perturbed every iteration. It now divides each component's mixing vector by its norm and rescales that component's `mu` and `beta` to match, an exact change of scale that leaves the log-likelihood unchanged. + Since issue #334 (below) a component is a row of the stored `A` itself. - Seeded from pamica's initialization, `A`, `mu` and `sbeta` now match the native reference binary to float64 round-off (after 1 iteration: `A` 5.0e-16, `mu` 7.8e-11, `sbeta` 1.1e-14, previously 7.2e-5, 6.6e-5 and 8.6e-5; after 3: 2.7e-13, 8.6e-10 and 2.7e-11, previously 1.4e-3, 6.7e-3 and 2.2e-3). - Fitted components now have unit norm, as in the reference; previously, after 100 seeded iterations, norms ranged over [0.94, 1.07] with one model and [0.05, 1.97] with two. - - Scale-blind results barely move: against the bundled `amicaout` fixture after 200 iterations, - the log-likelihood goes from -3.401777 to -3.401673 (fixture: -3.401873), + - Scale-blind results barely moved when this change landed: against the bundled `amicaout` fixture after 200 iterations, + the log-likelihood went from -3.401777 to -3.401673 (fixture: -3.401873), the matched correlation from 0.99752 to 0.99740 and the Amari distance from 5.95e-3 to 6.14e-3. - Two-model fits improve most: after 100 seeded iterations, the matched correlation with the reference rises from 0.850 to 0.99998. - - `doscaling=False` is byte-identical to before on every backend. + Two-model fits improve most: after 100 seeded iterations, the matched correlation with the reference rose from 0.850 to 0.99998. + - `doscaling=False` was byte-identical to the code before this change on every backend. Saved models load unchanged; refit only to compare parameters element by element with the reference. - `scalestep`, which the reference parses but never reads (it rescales every iteration), stays a pamica extension but now counts from 1: the rescale runs on iterations `scalestep`, `2*scalestep`, and so on, instead of 1, `1+scalestep`, and so on. - The default of 1 is unaffected (row 14 of the differences page). + The default of 1 is unaffected (row 14 of the [differences guide](guides/amica-differences.md#at-a-glance)). With `doscaling` on, every backend's constructor now raises `ValueError` for a `scalestep` that is not an integer of at least 1; `scalestep=0` used to fail mid-fit with a bare `ZeroDivisionError`. - New test helper `pamica/tests/native_oracle.py` seeds the native binary from a pamica state through its `load_*` files, for element-wise oracle tests (opt-in with `AMICA_RUN_FORTRAN=1`). -- **Fix: the EEGLAB export wrote an asymmetric sphere transposed (Phase 10 of epic #324, issue #336).** - `write_amicaout` (`pamica/numpy_impl/load.py`), the shared writer called from `write_amica_output` on - `AMICATorchNG`, `AMICAMLXNG` and the `AMICA` wrapper, and from the NumPy backend's own `_write_results`, - wrote the square sphere matrix `S` in C order, - while the Fortran reference and both readers - (EEGLAB's `loadmodout15.m` and pamica's `loadmodout`) read it column-major. - The default symmetric zero-phase component analysis (ZCA) sphere is its own transpose to about 1e-17, - so the bug moved only that many bytes there; - with `do_approx_sphere=False` the sphere is genuinely asymmetric, - and the exported sphere came back exactly transposed - (measured on the bundled sample, torch, 3 iterations: - before the fix `max|S_loaded - S| = 0.51`, `max|S_loaded - S.T| = 0.0`; - after, `max|S_loaded - S| = 0.0`, `max|S_loaded - S.T| = 0.51`). - `S` is now written column-major in both the square and rank-reduced branches, - and `load_results` reads a square sphere the same way. - **Action needed for existing output:** a full-rank directory written with `do_approx_sphere=False` - by an earlier pamica holds a C-order `S`. - EEGLAB always read that transposed (this fix does not change EEGLAB's own reading, only pamica's), - and pamica's corrected `loadmodout`/`load_results` now also read it transposed, with no error raised. - Regenerate any such directory by re-running the fit and `write_amica_output` again; - there is no on-disk version marker to detect the old layout, and this repo carries no - compatibility shim to read it automatically. - Directories from the default (symmetric) sphere and from a rank-reduced (`pcakeep`) fit are unaffected. -- **Phase 6 of epic #324: the validation harness covers every backend (issue #315).** - `validate_implementations.py --backend {torch,numpy,mlx}` (or a comma-separated list, or `all`) - compares each backend against one Fortran reference run with the same settings; - the default remains `torch` and prints the same report as before. - NumPy receives the settings through its own key-translation table, PyTorch and MLX through `AMICA(backend=...)`; - an explicit `--backend` also prints and saves a one-row-per-backend summary with runtimes (`parity_summary.md`), - and `--backend mlx` without MLX exits with status 2 and the install hint. - On the bundled sample all three backends meet the reference bar (log-likelihood within 3.2e-5, matched correlation 0.9992, Amari distance 0.004); - the rows and each backend's expected bar are in the validation guide, pinned by an `AMICA_RUN_FORTRAN`-gated test. - - The getting-started page gains a short Apple Silicon (MLX) route that links to the backends guide's full workflow, - and the differences page records two existing divergences: - `do_sphere=False` fits unscaled data where the reference divides each channel by its standard deviation (issue #328), - and a second `fit` on the same `AMICA_NumPy` instance continues from the first (related to issue #312). - - **Behavior change (legacy NumPy backend):** `AMICA_NumPy` writes files only when given an `outdir`. - Its default was `./output`, so every fit wrote `out.txt` at construction, `writestep` checkpoints and its final results into the caller's working directory. - The default is now `outdir=None`, which writes nothing, as the PyTorch and MLX backends never do unless asked. - An explicit `outdir` (keyword, params file, or the command-line interface's `--outdir`, which still defaults to `output`) writes exactly what it did before. - - **Fix:** `AMICA_NumPy.get_sensor_mixing_matrix` returned `pinv(sphere)` times the stored mixing matrix without the transpose the PyTorch and MLX backends apply, - so its columns were the rows of the true mixing matrix rather than the components' sensor maps - (about 10% away from the PyTorch backend's maps after five iterations on the bundled sample, and not an inverse of `get_weights() @ sphere`). - It now matches the PyTorch backend to round-off (4.6e-12 relative), pinned by a cross-backend test. - The NumPy backend's fit, `transform`, `get_weights` and EEGLAB export were not affected. - - **Fix:** the legacy plotting helpers in `pamica.numpy_impl.viz` had the same orientation slip. - `plot_components` drew rows of the mixing matrix as mixing vectors, and it and `plot_pdf_fits` formed activations from the raw data with no mean removal, no sphere and no transpose; - `plot_model_comparison` skipped the sphere. - They now plot the model's own sensor maps and sources (what `get_sensor_mixing_matrix` and `transform` return), checked against those accessors on a real fit. - `load_results` also reads a rank-reduced fit's zero-padded sphere, which it used to reject. -- **Phase 9 of epic #324: iteration schedules count from 1, as the reference's do (issue #335).** +- **Iteration schedules count from 1, as the reference's do** (issue #335, epic #324 Phase 9). **Behavior change:** `newt_start`, `rejstart` and (on NumPy) `restartiter` now name iterations counted from 1, like the reference's `iter` (amica15.f90:949): the first Newton M-step is the `newt_start`-th iteration's, the first rejection follows the `rejstart`-th, and a non-finite likelihood restarts the fit only within the first `restartiter` iterations. @@ -335,61 +208,258 @@ Release notes are also published on the - `iteration`, `ll_history` and `mir_history_` keep their 0-based indexing. - Tests: `pamica/tests/test_schedule_gates.py` observes each gate through real fits on all three backends, and `pamica/tests/test_schedule_native_oracle.py` (opt-in, `AMICA_RUN_FORTRAN=1`) is the native-binary comparison above. -- **Phase 4 of epic #324: backend selection in `AMICA` and `AMICAICA` (issue #313).** +- **`share_comps` compares and merges components; `A` stores one component per row** (issue #334, epic #324 Phase 8). + Every backend (PyTorch, NumPy and MLX) now stores the mixing matrix with one component per row, + `A` of shape `(n_comps, n_channels)`, the reference's `A` transposed ([ADR 0007](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0007-component-row-layout.md)). + A component id in `comp_list` now names the same component in `A` as in the density parameters, + so the share metric compares the components' scalp maps (`get_sensor_mixing_matrix`) + and a merge ties the two components' mixing vectors and densities, as the reference's `identify_shared_comps` does. + Before, both steps used stored columns of each model's block, which are not components. + - **Behavior change: fits with `share_comps=True` in which a merge fires now differ.** + Refit them. + Seeded with a merged `comp_list` through the reference's `load_comp_list`, + the PyTorch and NumPy updates match the native binary to float64 round-off + (after 3 iterations, worst of `doscaling` on and off: `A` 1.5e-12, `mu` 3.7e-9, log-likelihood 4.7e-14), + where the previous code was off by 0.21 in `A` and 4.3e-4 in log-likelihood. + On the bundled sample (2 models, 300 iterations, `share_start=100`, `comp_thresh=0.95`) + the scan now merges three pairs whose maps agree (|cos| 0.956 to 0.971), ending at log-likelihood -3.3416, + where it merged pairs whose maps did not (|cos| 0.06, 0.35 and 0.55) and ended at -3.3484 (-3.3387 with sharing off). + Because the metric now sees how similar the two models still are early in a fit, a scan in the first iterations merges most components, + and a model left with few components of its own can then collapse. + The reference behaves the same way: its similarity on its own early state merges the same pairs, + and its update from the same merged states collapses in step (its own scan never merges, because its similarity is NaN). + The reference's default `share_start=100` avoids that. + - Every fit without a merge is byte-identical to the code before this change on every backend, sharing off or scheduled but not firing, + with one exception at float round-off: the weight-gradient norm (`ndtmpsum`), which now sums per component like the reference. + - Persistence and the EEGLAB `A` file change with the layout; see [Persistence and exports](#persistence-and-exports). +- **Every backend uses the reference's single-precision constants** (issue #344, epic #324 Phase 13). + The reference writes several density normalizers as default-kind Fortran literals widened with `dble`, for example `log(dble(2.506628274))`, + so the binary uses the float32 rounding of each decimal, not the decimal. + pamica used the decimals' double values. + The PyTorch, NumPy and MLX backends now take the reference's values from one module, `pamica/reference_constants.py` + ([the differences guide](guides/amica-differences.md#single-precision-constants-issue-344) has the table). + - **Behavior change: fits in which a mixture reaches `rho == 2` move slightly, toward the reference.** + The default `maxrho = 2` clamps mixtures there, where the reference's normalizer, `log(dble(1.772453851))`, is 3.0e-8 above the `0.5 * log(pi)` pamica used, + so default fits of the generalized Gaussian take this branch once a mixture reaches the clamp + (on 4096 samples of the bundled recording, a two-model fit gets there on its fifth iteration). + Seeded with a warm two-model state that has mixtures at `rho == 2`, the PyTorch and NumPy updates now match the native binary to float64 round-off: + after three iterations the log-likelihood differs by 1.6e-13, `A` by 3.8e-13 and `mu` by 5.7e-9, + where the previous code was off by 1.2e-8, 3.4e-8 and 8.4e-3. + - **Behavior change: the Gaussian (`pdftype` 2) and the sub- and super-Gaussian cosh families (`pdftype` 4 and 1) report a different log-likelihood**, + lower by 3.7e-10 and 2.0e-8 and higher by 2.1e-8, which now matches the binary's to 2.7e-15. + Their parameter updates move only by round-off, since a family's normalizer shifts every mixture alike. + - The underflow guard of the rho update, `epsdble`, is likewise the reference's `1.0e-16` in single precision (1.0000000168623835e-16). + - The NumPy plotting helper `pamica.numpy_impl.pdf.compute_pdf`, which `viz.plot_pdf_fits` draws, takes its normalizers from the same module, + so it draws the density the fit uses; its unused companion `compute_log_pdf` is removed. + - Row 16 of the differences guide records the one kind of single-precision literal pamica keeps at its decimal value: + the compiled-in defaults of `input.param` keys, which the binary uses only when the key is missing + (then its `comp_thresh` is 0.9900000095); a value given in `input.param` is read as double, and pamica's native engine gives every one. + - Tests: `pamica/tests/test_reference_constants.py` pins every constant against the float32 rounding of its literal on the cited reference line, + computed by exact rational arithmetic; pins the sweep of both reference sources; checks that no backend keeps its own copy; + and (opt-in, `AMICA_RUN_FORTRAN=1`) seeds the native binary for `pdftype` 2, 4 and 1. + The merged-state oracle in `pamica/tests/test_component_rows.py` no longer holds `maxrho` at 1.99. + The tests that compare a live backend bit for bit with code from before this change + (`test_component_rows.py`, `test_doscaling_rows.py` and `mlx_tests/test_mlx_fit_noop.py`) + give the live backend its old constants first, so they still isolate the change they were written for. + +### MLX as a first-class backend, through every wrapper + +- **Backend selection in `AMICA` and `AMICAICA`** (issue #313, epic #324 Phase 4). `AMICA` and `AMICAICA` gain a `backend` parameter: `"torch"` (the default, `AMICATorchNG`, float64 Fortran parity) or `"mlx"` (`AMICAMLXNG`, Apple GPU, float32 only). - Every wrapper feature now runs on MLX: - `fit` with any backend keyword (including `pcakeep`, which closes #323's remaining item), `from_params_file(..., backend="mlx")` (#304's MLX path), + Every wrapper feature runs on MLX: + `fit` with any backend keyword (including `pcakeep`), `from_params_file(..., backend="mlx")`, the #50 degenerate-fit contract, `save`/`load`, the EEGLAB export and the MNE path. An unknown backend raises `ValueError`, `backend="mlx"` without MLX installed raises `ImportError` at construction, and `device` or a `dtype` fit keyword with `backend="mlx"` raises `ValueError`, since MLX runs only on its default device in float32. `import pamica` still never imports MLX. + The [backends guide](guides/backends.md#selecting-a-backend) has the rules and an Apple Silicon workflow. - The keywords `fit` takes from `**kwargs` and from a params file are derived from the selected backend class's own signature, and the "not applied" warning names that class. A keyword the selected backend does not take now raises `TypeError` from `AMICA.fit` itself, naming the backend, - instead of from the backend constructor. + instead of from the backend constructor; + every offending keyword of one call is named in that one error (issue #346 review). - The degenerate-fit contract uses each backend class's own `_DEGENERATE_STOP_REASONS`, so an MLX fit that stops on `nan_params` is refused like a PyTorch `nan_ll` fit. - - **Save format version 2:** `AMICA.save` records the backend in `wrapper["backend"]`, - and `AMICA.load` restores the model on that backend. - An MLX model's numpy arrays are stored as CPU tensors of the same dtype, so the file still loads with `torch.load(weights_only=True)`. - Version 1 files, written before this change, still load (always as PyTorch models). - Loading an MLX file needs MLX installed and `device=None`. - - **Fix:** a numpy scalar in the backend's config or fit record (for example `fit(X, seed=np.int64(42))`) is now saved as the equivalent Python number. - `AMICA.save` used to write such a file, which `AMICA.load` then refused (the `weights_only` unpickler rejects numpy scalars). - Any other value that loading could not read back now raises `TypeError` at save time. - **New accessors:** `get_sphere()`, `get_mean()` and `get_model_center(model_idx)` on `AMICATorchNG`, `AMICAMLXNG` and `AMICA`, - with the same names and shapes on both backends and float64 arrays from both. - The torch and MLX ones carry the degenerate-fit guard of the issue #306 entry below. + with the same names and shapes on both backends and float64 arrays from both, + guarded against degenerate fits like the other accessors (issue #306, below). `AMICA` also gains `get_sensor_mixing_matrix()`, which both backends already had. - `AMICAICA` reads the fitted mean, sphere and centers through those accessors, with no backend-specific array calls. An MLX export is float32-consistent (sources agree with the MLX `transform` within float32 tolerance), while `apply` with nothing excluded still returns the input to float64 round-off. A degenerate `AMICAICA` fit now leaves `pca_components_`/`pca_explained_variance_` as `None`; it was never exportable. -- **Raw backend accessors now guard against degenerate fits and bad input shape** (issue #306, epic #324 Phase 5). +- **Explicit `pcakeep`/`pcadb` on MLX, with one validation policy for every backend** (issue #323, epic #324 Phase 1). + `AMICAMLXNG` gains `pcakeep` and `pcadb` with `AMICATorchNG`'s names, defaults (`None`), position, validation and precedence. + They go through the shared `pamica.rank` policy, so all three array backends keep the same rank and build the same sphere + (cross-backend test on the bundled sample: torch vs NumPy sphere within 1e-10 relative, MLX within float32 rounding). + `fit(mir_step > 0)` gains the same upfront reduction gate and message as the PyTorch backend. + - **Behavior change: invalid `pcakeep`/`pcadb` raise `ValueError` at construction on every backend.** + `pcakeep` must be an integer of at least 1 (a `bool` or a float is rejected) and `pcadb` a finite number greater than 0; + a value assigned to the attribute after construction fails at fit time. + The PyTorch and NumPy backends used to accept these silently: + `pcakeep=-3` sliced from the end and fitted 29 of 32 sources on the bundled sample, `pcakeep=2.7` truncated to 2, + and `pcakeep=0` or `pcadb <= 0` ran to a degenerate `nan_ll` fit. + Because construction validates, a saved PyTorch model whose config carries such a value + (only possible before this change) fails to load with the same `ValueError`. + Setting both stays valid: `pcakeep` takes precedence and `pcadb` is ignored (one INFO log line), + as in the reference, which parses `pcadb` but never uses it. + - **Behavior change: `pcakeep`/`pcadb` with `do_sphere=False` are ignored with one warning.** + No backend reduces without sphering, as in the reference, which keeps every dimension there (amica15.f90:527). + The request used to be dropped silently, and the PyTorch `mir_step` gate still refused it. + Every backend now logs one WARNING at construction, and the gate no longer counts it as a reduction request. + - **Behavior change: `mir_step` no longer rejects `pcakeep >= n_channels`.** + The PyTorch backend's upfront gate refused any explicit `pcakeep`, + including the bundled `input.param`'s `pcakeep 32` on the 32-channel sample, which reduces nothing; + `AMICA.from_params_file("input.param").fit(X, mir_step=1)` raised. + The gate now rejects only a real request (`pcakeep` below the channel count, or any `pcadb`, while sphering), + identically on the PyTorch and MLX backends. +- **One parameter-file reader for every backend** (issue #304, epic #324 Phase 3). + `pamica/fortran_params.py` gains `read_params_file`, the single params-file entry point every backend uses. + It content-sniffs JSON vs. the literal Fortran `input.param` text and returns pamica's canonical keys either way. + A JSON file's own alias spellings (`min_grad_norm`, `max_decs`, `numrej`, `num_mix_comps`, `share_int`) + are translated to the canonical/constructor names through one table, `JSON_ALIAS_TO_CANONICAL`; + a file carrying both an alias and its canonical key raises `ValueError` naming both. + `writestep`/`do_history`/`histstep` are translated (identity) keys: + the legacy NumPy backend implements periodic on-disk checkpointing under these names, + and the reader translates what any backend supports; + the PyTorch and MLX backends have no such mechanism yet (issue #312), so `AMICA.fit` names them as not applied. + The [parameter-files section](guides/validation.md#parameter-files) has the tables. + - **Behavior change:** a fit from `AMICA.from_params_file` now applies `sample_params.json`'s own + `max_decs`/`min_grad_norm`/`share_int` settings (as `maxdecs`/`min_nd`/`share_iter`). + Under their raw JSON spelling they matched neither a named `fit()` parameter nor an `AMICATorchNG` keyword, + so they were only named in the "not applied" warning. + - **Behavior change (legacy NumPy backend):** `AMICA_NumPy(params_file=...)` accepts the literal Fortran `input.param` text format, not just JSON; + a non-JSON file used to raise a raw `json.JSONDecodeError`. + A params-file setting this backend does not consume is named in one `logger.warning` instead of silently vanishing. + The NumPy CLI (`python -m pamica.numpy_impl.cli`) accepts both formats too, through the same reader, instead of its own separate `json.load`. + - **Breaking change (legacy NumPy backend):** `AMICA_NumPy.from_json_file` is renamed to `from_params_file` + (matching the wrapper's classmethod name, and accepting both formats), with no alias left behind. + - **Breaking change (legacy NumPy backend):** `AMICA_NumPy(pdftype=...)` with anything other than `0` raises `NotImplementedError` at construction: + this backend implements only the generalized-Gaussian source density, and used to ignore the setting silently + (`pdftype` was read but never consulted by the fit path). + The constructor's default and the bundled `numpy_impl/params.json`'s both changed from `1` to `0` to match; + since the value was never read, no previously passing fit's numerics change. + - `validate_implementations.py`'s own JSON-schema-to-Fortran-keyword alias table + (`_FORTRAN_ALIASES`) is composed from the two shared tables (`JSON_ALIAS_TO_CANONICAL`, then `PAMICA_KEY_TO_FORTRAN_KEY`) + instead of one hand-maintained entry, and its `max_decs` special case is gone: + `load_sample_data` reads through `read_params_file`, so canonical keys reach `run_pytorch_amica`'s `ng_kwargs` automatically. +- **Raw backend accessors guard against degenerate fits and bad input shape** (issue #306, epic #324 Phase 5). **Behavior change:** - `AMICATorchNG`, `AMICAMLXNG` and the legacy NumPy `AMICA` backend's fitted-output accessors + `AMICATorchNG`, `AMICAMLXNG` and the legacy NumPy backend's fitted-output accessors (`transform`, `get_mixing_matrix`, `get_unmixing_matrix`, `get_sensor_mixing_matrix`, `get_rho`, `get_pdftype`, `shared_components`, `variance_order`, `model_loglik`, `model_probability`, - `mir` and `pmi` on torch/MLX, plus Phase 4's `get_sphere`, `get_mean` and `get_model_center`; + `mir` and `pmi` on torch/MLX, plus `get_sphere`, `get_mean` and `get_model_center`; `transform`, `get_weights` and `get_sensor_mixing_matrix` on NumPy) - now raise `RuntimeError` when called on a fit the backend itself classified as degenerate, + raise `RuntimeError` when called on a fit the backend itself classified as degenerate, or when a fitted parameter holds a non-finite value, instead of silently returning NaN-tainted output. The accessors that take data (`transform`, `model_loglik`, `model_probability`, `mir` and `pmi`) - now also validate that the input is a 2D array with the model's fitted input channel count, - raising the same named `ValueError` that `fit()` already raises for the identical mistake, + also validate that the input is a 2D array with the model's fitted input channel count, + raising the same named `ValueError` that `fit()` raises for the identical mistake, instead of a raw matmul/broadcast error. - `model_probability` also now tells apart a NaN log-likelihood (numerical corruption) - from every model underflowing to `-inf` (an extreme outlier), where it previously reported both the same way. + `model_probability` also tells apart a NaN log-likelihood (numerical corruption) + from every model underflowing to `-inf` (an extreme outlier), where it used to report both the same way. `AMICATorchNG.from_state_dict` and `AMICAMLXNG.from_state_dict` - now raise `ValueError` naming the payload as the culprit when the saved config does not match the constructor, + raise `ValueError` naming the payload as the culprit when the saved config does not match the constructor, such as a missing or unexpected key, chaining the original `TypeError` instead of letting it propagate bare. This closes the gap the `AMICA` wrapper's own degenerate-fit guard (issue #50) never covered: - a caller using a raw backend directly now gets the same protection. - See `docs/guides/amica-differences.md` row 5. -- **`AMICAICA.apply` restores the PCA residual of rank-reduced fits** (issue #322, epic #324). + a caller using a raw backend directly gets the same protection. + See row 5 of the [differences guide](guides/amica-differences.md#at-a-glance). +- **The validation harness covers every backend** (issue #315, epic #324 Phase 6). + `validate_implementations.py --backend {torch,numpy,mlx}` (or a comma-separated list, or `all`) + compares each backend against one Fortran reference run with the same settings; + the default remains `torch` and prints the same report as before. + NumPy receives the settings through its own key-translation table, PyTorch and MLX through `AMICA(backend=...)`; + an explicit `--backend` also prints and saves a one-row-per-backend summary with runtimes (`parity_summary.md`), + and `--backend mlx` without MLX exits with status 2 and the install hint. + When the harness landed, before the fitting changes above, all three backends met the reference bar on the bundled sample + (log-likelihood within 3.2e-5, matched correlation 0.9992, Amari distance 0.004); + the [validation guide](guides/validation.md#parity-rows-per-backend) has the current rows and each backend's expected bar, + pinned by an `AMICA_RUN_FORTRAN`-gated test. + An end-to-end workflow test (`pamica/tests/mne_tests/test_end_to_end_workflow.py`) runs a per-session workflow on both wrapper backends: + average-referenced EEG with `pcakeep = n_channels - 1`, `AMICAICA`, the EEGLAB export and reload, `save`/`load` and an `input.param`-driven fit. + - The getting-started page gains a short Apple Silicon (MLX) route that links to the backends guide's full workflow, + and the differences guide records two existing divergences: + `do_sphere=False` fits unscaled data where the reference divides each channel by its standard deviation (issue #328), + and a second `fit` on the same `AMICA_NumPy` instance continues from the first (related to issue #312). + - **Behavior change (legacy NumPy backend):** `AMICA_NumPy` writes files only when given an `outdir`. + Its default was `./output`, so every fit wrote `out.txt` at construction, `writestep` checkpoints and its final results into the caller's working directory. + The default is now `outdir=None`, which writes nothing, as the PyTorch and MLX backends never do unless asked. + An explicit `outdir` (keyword, params file, or the command-line interface's `--outdir`, which still defaults to `output`) writes exactly what it did before. + - **Fix:** `AMICA_NumPy.get_sensor_mixing_matrix` returned `pinv(sphere)` times the stored mixing matrix without the transpose the PyTorch and MLX backends apply, + so its columns were the rows of the true mixing matrix rather than the components' sensor maps + (about 10% away from the PyTorch backend's maps after five iterations on the bundled sample, and not an inverse of `get_weights() @ sphere`). + It now matches the PyTorch backend to round-off (4.6e-12 relative), pinned by a cross-backend test. + The NumPy backend's fit, `transform`, `get_weights` and EEGLAB export were not affected. + - **Fix:** the legacy plotting helpers in `pamica.numpy_impl.viz` had the same orientation slip. + `plot_components` drew rows of the mixing matrix as mixing vectors, and it and `plot_pdf_fits` formed activations from the raw data with no mean removal, no sphere and no transpose; + `plot_model_comparison` skipped the sphere. + They now plot the model's own sensor maps and sources (what `get_sensor_mixing_matrix` and `transform` return), checked against those accessors on a real fit. + `load_results` also reads a rank-reduced fit's zero-padded sphere, which it used to reject. +- **`AMICA_NumPy` rejects unknown and unsupported keyword arguments** (issue #346, epic #324 Phase 14). + `AMICA_NumPy(**kwargs)` used to forward every keyword into a params dict read with `params.get(...)`, + so a typo (`max_iters=50`) or an option the legacy backend does not implement (`keep_best=True`) + constructed silently and had no effect, + unlike the PyTorch/MLX constructors, which take explicit keyword parameters and raise `TypeError` on an unknown name. + - **Behavior change (legacy NumPy backend only): an unrecognized or unsupported keyword argument raises `TypeError` instead of being silently ignored.** + A typo raises `TypeError` naming the offending keyword(s), + with a `difflib`-based "did you mean" suggestion when a close match exists. + An option implemented on the PyTorch backend (`AMICATorchNG`) but not this one + (for example `keep_best`, `device`, `dtype`, the kurtosis-switch schedule) + raises `TypeError` naming the option and pointing to `AMICA(backend='torch')`. + `n_models`/`n_mix` (`AMICATorchNG`'s spelling of this backend's `num_models`/`num_mix`) get a message naming the correct spelling, + and `n_channels` its own message, since this backend, like the `AMICA` wrapper, infers it from the data passed to `fit()`. + Every offending keyword in one call is named in a single error, however many different kinds are mixed together. + - The three settings this backend spells differently from `AMICATorchNG` + (`min_nd`/`maxdecs`/`share_iter`, the canonical spelling a params file already resolves either way) + are also accepted as keyword arguments directly, + translated to this backend's own attribute name the same way the params-file route translates them. + Passing both spellings of the same setting at once raises `TypeError` naming both, rather than picking one silently. + - `files`/`data_dim`/`field_dim` (data-location metadata) work as keyword arguments directly, with the same meaning as in `params_file`; + a params file and a keyword argument that set one of them to different values raise `TypeError`. + - The accepted-keyword set is derived from the same source the params-file routing uses + (`_CONSUMED_KEYS`/`_CANONICAL_TO_NUMPY_KEY`), + and the unsupported-option list from `AMICATorchNG`'s own constructor signature, + so neither can drift from what the constructor actually reads. +- **The MLX backend gains `AMICATorchNG`'s full surface** (epic #278). + With it, the only difference between the raw MLX and PyTorch backends is precision (float32 only on Apple GPUs). + - **`transform` and save/load** (issue #287, epic #278 Phase 1): + source extraction (`transform`, plus the `get_mixing_matrix`/`get_unmixing_matrix`/`get_sensor_mixing_matrix`/`get_rho` accessors, + mirroring `AMICATorchNG`'s issue #24/#27/#142/#223 conventions) + and persistence (`state_dict`/`from_state_dict`, plus a device- and framework-agnostic `.npz` `save`/`load`: + `config`/`extra` as JSON-encoded scalars, params as native arrays, no torch coupling, no pickle). + `transform` derives the unmixing composition from MLX's own `_forward` rather than transcribing torch's tensor layout: + MLX's `W` is `(n_models, n, n)`, not torch's `(n, n, n_models)`. + Fitting was untouched (a default fit was bit-identical to before this phase). + - **`keep_best` best-iterate safeguard** (issue #288, epic #278 Phase 2): + `fit` tracks the highest-log-likelihood iterate and, if the run ends more than `_KEEP_BEST_TOL` (1e-9, the same constant as `AMICATorchNG`) below that peak, + restores it instead of returning the last iterate. + Same name, default (`keep_best=True`) and semantics as the PyTorch backend, + including its inactivity under `share_comps` and `do_reject`. + `ll_history` is never rewritten; only `final_ll_` and the twelve fitted-parameter arrays roll back. + Persisted additively in `state_dict`'s config (a payload without the key loads with the default). + - **Outlier rejection, the LLt stash, the EEGLAB export and MIR/PMI** (issue #289, epic #278 Phase 3): + the LLt stash (issue #157), filled per block by the E-step (never a second forward pass) and rolled back on a `keep_best` restore; + `do_reject` (issue #123's `good_idx` mechanism), with the rejection statistic read from the stash rather than a second forward pass, + the NumPy backend's design, ahead of `AMICATorchNG`'s open follow-up to drop its own extra `_sample_ll` pass (issue #298); + `model_loglik`/`model_probability` (issue #141); + `write_amica_output` (issue #92), a thin adapter over the shared `numpy_impl.load.write_amicaout`; + and `mir`/`pmi` plus `fit(mir_step=...)` waypoints (issue #137), including the #300 fitted-geometry PCA guard. + Rejection state (`numrej`/`good_idx`) persists additively in `state_dict`'s `extra`; + `mir_history_` stays out of both the `keep_best` snapshot and `state_dict` (a diagnostic trajectory, not a fitted parameter). + A default fit (`do_reject` off, `mir_step=0`) was unaffected, verified bit-identical to the code before this phase, + and MLX and PyTorch reject the same sample set on the same real data and configuration. + `write_amica_output` also gained `state_dict`'s two-layer degenerate/non-finite refusal guard, on both the MLX and PyTorch backends: + a caller using either backend class directly could previously write a NaN model to disk silently. + - **`variance_order`** (issue #92, the epic's polish round): the EEGLAB back-projected-variance component order, + validated on real data against a float64 `AMICATorchNG` twin holding identical fitted parameters + (the component order matches exactly on a configuration with non-degenerate variance gaps). + +### MNE wrapper + +- **`AMICAICA.apply` restores the PCA residual of rank-reduced fits** (issue #322). **Behavior change for `pcakeep`/`pcadb` and rank-deficient fits:** `AMICAICA.fit` now computes the full orthonormal PCA basis once (`pca_components_`, `n_channels x n_channels`, with `pca_explained_variance_`), @@ -402,37 +472,58 @@ Release notes are also published on the pass `n_pca_components=ica.n_components_` to `apply` for the reference's rank-reduced reconstruction. Sources, component maps and the log-likelihood are unchanged, and full-rank fits export bit-identically. `to_mne_ica` logs the residual's dimension and the opt-out at INFO. - Recorded in ADR 0005 and `docs/guides/amica-differences.md`. -- **MLX backend: explicit `pcakeep`/`pcadb`** (issue #323, epic #324 Phase 1). - `AMICAMLXNG` gained `pcakeep` and `pcadb` with `AMICATorchNG`'s names, defaults (`None`), position, validation and precedence. - They go through the shared `pamica.rank` policy, so all three array backends keep the same rank and build the same sphere - (cross-backend test on the bundled sample: torch vs NumPy sphere within 1e-10 relative, MLX within float32 rounding). - `fit(mir_step > 0)` gained the same upfront reduction gate and message as the PyTorch backend. - Both parameters persist additively in `state_dict()["config"]`: - a payload written before this change loads with `None`, and there is no `format_version` bump. -- **Behavior change: invalid `pcakeep`/`pcadb` now raise `ValueError` at construction on every backend.** - `pcakeep` must be an integer of at least 1 (a `bool` or a float is rejected) and `pcadb` a finite number greater than 0; - a value assigned to the attribute after construction fails at fit time. - The PyTorch and NumPy backends used to accept these silently: - `pcakeep=-3` sliced from the end and fitted 29 of 32 sources on the bundled sample, `pcakeep=2.7` truncated to 2, - and `pcakeep=0` or `pcadb <= 0` ran to a degenerate `nan_ll` fit. - Because construction validates, a saved PyTorch model whose config carries such a value - (only possible before this change) now fails to load with the same `ValueError`. - Setting both stays valid: `pcakeep` takes precedence and `pcadb` is ignored (one INFO log line), - as in the reference, which parses `pcadb` but never uses it. - `state_dict` now stores both as plain `int`/`float`, - so a numpy-scalar request survives `AMICA.save`/`load` (whose `weights_only` load refuses numpy scalars). -- **Behavior change: `pcakeep`/`pcadb` with `do_sphere=False` are ignored with one warning.** - No backend reduces without sphering, as in the reference, which keeps every dimension there (amica15.f90:527). - The request used to be dropped silently, and the PyTorch `mir_step` gate still refused it. - Every backend now logs one WARNING at construction, and the gate no longer counts it as a reduction request. -- **Behavior change: `mir_step` no longer rejects `pcakeep >= n_channels`.** - The PyTorch backend's upfront gate refused any explicit `pcakeep`, - including the bundled `input.param`'s `pcakeep 32` on the 32-channel sample, which reduces nothing; - `AMICA.from_params_file("input.param").fit(X, mir_step=1)` raised. - The gate now rejects only a real request (`pcakeep` below the channel count, or any `pcadb`, while sphering), - identically on the PyTorch and MLX backends. -- **Fix: refits and best-of-N restarts after a rank reduction** (PyTorch and MLX backends). + Recorded in ADR 0005 and the [differences guide](guides/amica-differences.md#amicaicaapply-restores-the-pca-residual-issue-322). + +### Persistence and exports + +- **Saved models convert to the component-row layout** (issue #334). + The PyTorch `state_dict` is now `format_version` 4 and the MLX save format 2. + An older save loads unchanged in content, converted without loss, unless `share_comps` had merged components: + such a save raises `ValueError` asking for a refit, because its merges were computed under the old semantics. +- **`AMICA.save` format version 2 records the backend** (issue #313). + `AMICA.save` records the backend in `wrapper["backend"]`, and `AMICA.load` restores the model on that backend. + An MLX model's numpy arrays are stored as CPU tensors of the same dtype, so the file still loads with `torch.load(weights_only=True)`. + Version 1 files, written before this change, still load (always as PyTorch models), + and both versions go through the component-row conversion and refusal above. + Loading an MLX file needs MLX installed and `device=None`. + - **Fix:** a numpy scalar in the backend's config or fit record (for example `fit(X, seed=np.int64(42))`) is now saved as the equivalent Python number. + `AMICA.save` used to write such a file, which `AMICA.load` then refused (the `weights_only` unpickler rejects numpy scalars). + Any other value that loading could not read back now raises `TypeError` at save time. +- **Additive fields in the backend saves.** + PyTorch and MLX saves store `rholrate_cap` (issue #339), a payload without it loading with the ceiling equal to its saved `rholrate`. + MLX saves gain `pcakeep`/`pcadb` in `state_dict()["config"]` (issue #323), a payload without them loading with `None`, and no `format_version` bump. + Both backends store `pcakeep`/`pcadb` as plain `int`/`float`, so a numpy-scalar request survives `AMICA.save`/`load`. + Loading a PyTorch `state_dict` or an MLX save refuses a missing or non-finite learning rate with a `ValueError` naming the field. +- **EEGLAB export: the `A` file is in the reference's layout for any number of models** (issue #334). + It is the reference's `A(nw, num_comps)` in column-major order + (EEGLAB's `loadmodout15.m` ignores it; single-model files are byte-identical to before). + `pamica.numpy_impl.data.load_results` reads it in that layout + and refuses a multi-model directory written by an earlier version, whose `A` does not invert the `W` beside it; + write such a directory again from the fitted or reloaded model. +- **Fix: the EEGLAB export wrote an asymmetric sphere transposed** (issue #336, epic #324 Phase 10). + `write_amicaout` (`pamica/numpy_impl/load.py`), the shared writer called from `write_amica_output` on + `AMICATorchNG`, `AMICAMLXNG` and the `AMICA` wrapper, and from the NumPy backend's own `_write_results`, + wrote the square sphere matrix `S` in C order, + while the Fortran reference and both readers (EEGLAB's `loadmodout15.m` and pamica's `loadmodout`) read it column-major. + The default symmetric zero-phase component analysis (ZCA) sphere is its own transpose to about 1e-17, + so the bug moved only that many bytes there; + with `do_approx_sphere=False` the sphere is genuinely asymmetric, and the exported sphere came back exactly transposed + (measured on the bundled sample, torch, 3 iterations: + before the fix `max|S_loaded - S| = 0.51`, `max|S_loaded - S.T| = 0.0`; + after, `max|S_loaded - S| = 0.0`, `max|S_loaded - S.T| = 0.51`). + `S` is now written column-major in both the square and rank-reduced branches, + and `load_results` reads a square sphere the same way. + **Action needed for existing output:** a full-rank directory written with `do_approx_sphere=False` + by an earlier pamica holds a C-order `S`. + EEGLAB always read that transposed (this fix does not change EEGLAB's own reading, only pamica's), + and pamica's corrected `loadmodout`/`load_results` now also read it transposed, with no error raised. + Regenerate any such directory by re-running the fit and `write_amica_output` again; + there is no on-disk version marker to detect the old layout, and this repository carries no compatibility shim to read it automatically. + Directories from the default (symmetric) sphere and from a rank-reduced (`pcakeep`) fit are unaffected. + +### Other fixes + +- **Refits and best-of-N restarts after a rank reduction** (PyTorch and MLX backends). A rank reduction, explicit or automatic (`mineig`/`mineig_rel`, for example on Maxwell-filtered MEG), shrinks the model's `n_channels` to the kept rank, and the next fit validated its data against that shrunk count. A second `fit()` on the same instance, and the second restart of any `n_restarts > 1` fit, @@ -440,125 +531,35 @@ Release notes are also published on the Every fit now starts from the constructor's input channel count, including a fit on a model reloaded from `state_dict`/`save`. The NumPy backend was not affected. -- **Phase 3 of epic #324: shared params-file reader for every backend (issue #304).** - `pamica/fortran_params.py` gains `read_params_file`, the single params-file entry point - every backend now uses. - It content-sniffs JSON vs. the literal Fortran `input.param` text and returns pamica's - canonical keys either way. - A JSON file's own alias spellings (`min_grad_norm`, `max_decs`, `numrej`, `num_mix_comps`, - `share_int`) are translated to the canonical/constructor names via one table, - `JSON_ALIAS_TO_CANONICAL`; - a file carrying both an alias and its canonical key raises `ValueError` naming both. - `writestep`/`do_history`/`histstep` moved from `FORTRAN_UNSUPPORTED_KEYS` to translated - (identity) keys -- the legacy NumPy backend already implements periodic on-disk - checkpointing under these names, and the reader now translates what any backend supports - rather than only what its one production caller (the PyTorch wrapper) did; - torch/MLX have no matching mechanism yet (issue #312). - - **Behavior change:** `AMICA.from_params_file` now applies `sample_params.json`'s own - `max_decs`/`min_grad_norm`/`share_int` settings (as `maxdecs`/`min_nd`/`share_iter`) - instead of only naming them in the "not applied" warning. - Their raw JSON spelling previously matched neither a named `fit()` parameter nor an - `AMICATorchNG` keyword, so they were only named in the "not applied" warning rather - than actually applied. - - **Behavior change (legacy NumPy backend):** `AMICA_NumPy(params_file=...)` now accepts - the literal Fortran `input.param` text format, not just JSON -- previously a non-JSON - file raised a raw `json.JSONDecodeError`. - A params-file setting this backend does not consume is now named in one - `logger.warning` instead of silently vanishing. - The NumPy CLI (`python -m pamica.numpy_impl.cli`) accepts both formats too, through - the same reader, instead of its own separate `json.load`. - - **Breaking change (legacy NumPy backend):** `AMICA_NumPy.from_json_file` is renamed to - `from_params_file` (matching the PyTorch wrapper's classmethod name, and now accepting - both formats), with no alias left behind. - - **Breaking change (legacy NumPy backend):** `AMICA_NumPy(pdftype=...)` with anything - other than `0` now raises `NotImplementedError` at construction -- this backend - implements only the generalized-Gaussian source density, and previously silently - ignored the setting (`pdftype` is read but never consulted by the fit path). - `numpy_impl/params.json`'s bundled default `pdftype` changed `1` -> `0` to match; - since the value was never read, no previously-passing fit's numerics change. - - `validate_implementations.py`'s own JSON-schema-to-Fortran-keyword alias table - (`_FORTRAN_ALIASES`) is now composed from the two shared tables (`JSON_ALIAS_TO_CANONICAL` - then `PAMICA_KEY_TO_FORTRAN_KEY`) instead of one hand-maintained entry, and its - `max_decs` special case is gone -- `load_sample_data` reads through `read_params_file`, - so canonical keys reach `run_pytorch_amica`'s `ng_kwargs` automatically. -- **Epic #278 polish round: audit-driven fixes ahead of merge to `dev`.** - `AMICAMLXNG` gained `variance_order` (issue #92), the EEGLAB - back-projected-variance component order, closing the one accessor gap the - epic's feature-parity audit found in an otherwise-complete Phase 3 - (validated on real data against a float64 `AMICATorchNG` twin holding - identical fitted parameters: the component order matches exactly on a - config with non-degenerate variance gaps). The audit also found several - stale/misattributed doc claims, fixed here: `docs/guides/amica-differences.md` - now attributes the degenerate-fit refusal to the `AMICA` wrapper rather than - the raw PyTorch backend (the raw-backend gap is issue #306), and gained an - "Unmapped Fortran keywords" section pointing to `fortran_params.py`'s - `FORTRAN_UNSUPPORTED_KEYS` as the enumeration, naming three keywords dead in - the reference itself (`filter_length`/`dft_length`/`decwindow`), and - recording the `do_rho`-vs-`pdftype` divergence explicitly; `AGENTS.md` and - `.context/progress_summary.md` no longer claim `validate_implementations.py` - covers the NumPy/MLX backends (it is torch-vs-Fortran only; extending it is - issue #315); `.context/feature_parity.md` and `.context/progress_summary.md` - no longer claim the runtime-vs-Fortran benchmark is unmeasured or that issue - #15 (save/load and `plot_components` test coverage) is open, both stale - since earlier work landed; and `fortran_params.py`'s unsupported-keys table - no longer claims `writestep`/`do_history`/`histstep` have no pamica - equivalent -- they are implemented on the legacy NumPy backend, just not - yet on torch/MLX (issue #312). Also aligned the legacy NumPy backend's - inert `pdftype` constructor default to 0, matching torch/MLX (the value is - never read, so this is a documentation-equivalent surface fix, not a - behavior change). -- **MLX backend: outlier rejection, LLt stash, EEGLAB export, MIR/PMI** - (issue #289, epic #278 Phase 3 -- see the polish-round entry above for the - `variance_order` accessor gap this phase left open and the epic-completing - follow-up). `AMICAMLXNG` gains: - the LLt stash (issue #157), filled per-block by the E-step (never a second - forward pass) and rolled back on a `keep_best` restore; `do_reject` - (issue #123's `good_idx` mechanism), with the rejection statistic read FROM - the stash rather than a second forward pass -- the NumPy backend's design, - pre-empting `AMICATorchNG`'s open follow-up to drop its own extra - `_sample_ll` pass (issue #298); `model_loglik`/`model_probability` - (issue #141); `write_amica_output` (issue #92), a thin adapter over the - shared `numpy_impl.load.write_amicaout` with MLX's `W` transposed from - `(n_models, n, n)` to the writer's `(nw, nw, num_models)` contract; and - `mir`/`pmi` plus `fit(mir_step=...)` waypoints (issue #137), including the - #300 fitted-geometry PCA guard. `do_reject`/rejection state (`numrej`/ - `good_idx`) persists additively in `state_dict`'s `extra` (`extra.get` - fallback for pre-Phase-3 payloads); `mir_history_` stays out of both the - `keep_best` snapshot and `state_dict` (a diagnostic trajectory, not a - fitted parameter). A default fit (`do_reject` off, `mir_step=0`) is - unaffected, verified bit-identical to the epic tip before this phase. - Cross-backend agreement (same real data/config, MLX vs PyTorch reject the - same sample set) is the evidence for the stash-based design decision. - `write_amica_output` also gained `state_dict`'s two-layer degenerate/ - non-finite refusal guard, on **both** the MLX and PyTorch backends: a - caller using either backend class directly (bypassing the `AMICA` - wrapper's own usability gate) could previously write a NaN model to disk - silently. -- **MLX backend: `keep_best` best-iterate safeguard** (issue #288, epic #278 - Phase 2). `AMICAMLXNG` gained the #51 best-iterate restore: `fit` now tracks - the highest-log-likelihood iterate and, if the run ends more than - `_KEEP_BEST_TOL` (1e-9, same constant as `AMICATorchNG`) below that peak, - restores it instead of returning the last iterate. Same name, default - (`keep_best=True`) and semantics as the PyTorch backend, including the - `share_comps` interaction (a merge changes the parameter count, so the - safeguard is inactive under sharing -- `do_reject` joined that exclusion - when it landed in Phase 3, see above). `ll_history` is never rewritten; only - `final_ll_` and the twelve fitted-parameter arrays roll back. Persisted - additively in `state_dict`'s config (no format_version bump -- a Phase-1-era - payload without the key loads with the default). -- **MLX backend: `transform` and save/load** (issue #287, epic #278 Phase 1). - `AMICAMLXNG` gained source extraction (`transform`, plus the - `get_mixing_matrix`/`get_unmixing_matrix`/`get_sensor_mixing_matrix`/ - `get_rho` accessors, mirroring `AMICATorchNG`'s issue #24/#27/#142/#223 - conventions) and persistence (`state_dict`/`from_state_dict`, plus a - device- and framework-agnostic `.npz` `save`/`load` -- `config`/`extra` as - JSON-encoded scalars, params as native arrays, no torch coupling, no - pickle). `transform` derives the unmixing composition from MLX's own - `_forward` rather than transcribing torch's tensor layout: MLX's `W` is - `(n_models, n, n)`, not torch's `(n, n, n_models)`. Fitting is untouched - (`_fit_once` and its call graph are unmodified; a default fit is - bit-identical to before this phase). Remaining MLX gap -- outlier rejection - + LLt/MIR (Phase 3, #289) -- is tracked under epic #278. +- **Tests no longer make a full clone shallow** (issue #343). + Tests that load historical code ran `git fetch origin --depth 1` to reach the pinned commit; + in a full clone that records a shallow boundary, after which `git gc` can prune history. + Every such test now goes through `pamica/tests/pre_change.py`, which only reads the repository: + when the pinned commit is missing it fails under `CI` and otherwise skips, + naming the command that fetches it (`git fetch origin `, or `git fetch --unshallow origin` in a shallow clone). + `pamica/tests/test_pre_change_loader.py` runs the loader behind a logging `git` wrapper and asserts that nothing is fetched. + +### Removed + +- **Dead legacy code in `pamica.numpy_impl.pdf`** (issue #352). + `choose_pdf_type`, which nothing called, is removed, + and `compute_pdf` loses its `pdftype` argument and the branches for three legacy densities no backend fits: + `compute_pdf(y, rho)` draws the generalized Gaussian, the only density the NumPy backend fits, + and every existing call already used it. + +### Documentation + +- **The documentation describes the finished epic** (issue #352, epic #324 Phase 16). + The concept pages walk one iteration in the reference's order + (E-step, the likelihood-decrease response and the stopping checks, the exit before any update, then the update, + with the Newton start, the A-freeze windows and the `doscaling` rescale), + and the algorithm-flow figure shows the checks before the update. + The API pages no longer render docstring lines that began with an issue number as headings, + and the entry pages give the PyPI install. + During epic #278's polish round the differences guide gained its + [Unmapped Fortran keywords](guides/amica-differences.md#unmapped-fortran-keywords) section, + which names three keywords that are dead in the reference itself (`filter_length`/`dft_length`/`decwindow`) + and records the `do_rho`-vs-`pdftype` divergence. ## 0.3.3 diff --git a/docs/concepts/how-amica-works.md b/docs/concepts/how-amica-works.md index 191bbd6e..e1c32ec8 100644 --- a/docs/concepts/how-amica-works.md +++ b/docs/concepts/how-amica-works.md @@ -1,8 +1,7 @@ # How AMICA works AMICA fits its [generative model](what-is-amica.md) by **maximum likelihood**: -it searches for the unmixing matrices, biases, model weights, and source-density -parameters that make the observed data most probable. +it searches for the unmixing matrices, biases, model weights, and source-density parameters that make the observed data most probable. ## The objective @@ -17,45 +16,67 @@ y_{hi}(t) = \mathbf{w}_{hi}^{\top}\big(\mathbf{x}(t)-\mathbf{c}_h\big), $$ over all parameters $\{\mathbf{W}_h, \mathbf{c}_h, \gamma_h, -\alpha_{hij}, \mu_{hij}, \beta_{hij}, \rho_{hij}\}$. The $|\det\mathbf{W}_h|$ -term is what keeps the unmixing matrices from collapsing to zero. +\alpha_{hij}, \mu_{hij}, \beta_{hij}, \rho_{hij}\}$. +The $|\det\mathbf{W}_h|$ term is what keeps the unmixing matrices from collapsing to zero. ## The algorithm -Because the model has hidden assignments (which model, and which mixture -component, generated each sample), it is fit with -**expectation-maximization (EM)**, wrapped in a preprocessing and update loop. +Because the model has hidden assignments (which model, and which mixture component, generated each sample), +it is fit with **expectation-maximization (EM)**: a preprocessing step, then a loop of iterations. +Each iteration runs in the same order as the Fortran reference: +the E-step, then the checks, then either an exit or the parameter update. -![AMICA pipeline: center and whiten the data, then iterate E-step and M-step (natural-gradient and Newton updates) until the log-likelihood converges.](../assets/figures/amica-algorithm-flow.svg){ width=760 } +![AMICA fitting loop: center and whiten the data; then each iteration runs the E-step, checks the log-likelihood (reducing the learning rate after a decrease), exits before any update if a stopping check fired, and otherwise updates the parameters and loops back.](../assets/figures/amica-algorithm-flow.svg){ width=760 } /// caption -The AMICA fitting loop: preprocessing, then alternating E- and M-steps until the -log-likelihood converges. +The AMICA fitting loop: preprocessing, then iterations of E-step, checks and update, +until a stopping check fires or the iteration budget runs out. /// ### Preprocessing -The data are centered (mean removed) and **whitened** with a symmetric (ZCA) -sphering matrix, matching the Fortran reference. Whitening removes second-order -correlations so the iterations only have to resolve higher-order structure. +The data are centered (mean removed) and **whitened** with a symmetric zero-phase component analysis (ZCA) sphering matrix, matching the Fortran reference. +Whitening removes second-order correlations so the iterations only have to resolve higher-order structure. +When the data are rank-deficient, or a principal component analysis (PCA) reduction is requested with `pcakeep`, +the sphering also reduces the data to the dimensions that are kept, and the model has that many sources. -### E-step (responsibilities) +### 1. E-step -Holding the parameters fixed, AMICA computes the posterior probabilities of the -hidden assignments for each sample: the probability that model $h$ generated it, -and, within each source, the probability that mixture component $j$ produced the -activation. These *responsibilities* are the expected sufficient statistics used -by the M-step. +Holding the parameters fixed, AMICA computes the posterior probabilities of the hidden assignments for each sample: +the probability that model $h$ generated it, +and, within each source, the probability that mixture component $j$ produced the activation. +These *responsibilities* are the expected sufficient statistics the update needs. +The same pass over the data gives the log-likelihood of the current parameters +and the direction in which the unmixing matrices should move, together with its size, the gradient norm. -### M-step (parameter updates) +### 2. Checks -Holding the responsibilities fixed, AMICA updates the parameters: +Before anything is updated, the iteration looks at what the E-step found. -- **Source-density parameters** ($\alpha, \mu, \beta, \rho$) are updated with the - exact expectation-maximization closed-form expressions (a digamma equation for - the shape $\rho$). -- **Unmixing matrices** are updated with the **natural gradient**, which - accounts for the geometry of the space of matrices and converges far faster - than the ordinary gradient: +- **A log-likelihood decrease.** + The update steps do not guarantee that the likelihood rises: a step can overshoot. + When the likelihood falls from one iteration to the next, AMICA halves the learning rate of the unmixing update (by `lratefact`), + so this iteration's step is already the smaller one. + Every `maxdecs` decreases it also lowers the ceiling the learning rate can climb back to, + so the rate cannot keep returning to the value that overshot. +- **Stopping checks.** + The fit stops when the likelihood has risen by less than `min_dll` for more than `maxincs` iterations in a row, + when the gradient norm falls to `min_nd`, + or when a decrease finds the learning rate already at its floor (`minlrate`). + A non-finite log-likelihood or update direction also ends the fit, and marks it degenerate: + every output method then refuses the model, so no NaN sources reach an analysis. + +The likelihood and gradient checks cannot fire on the first iteration, which has no earlier likelihood to compare with. + +### 3. Exit, or update + +If a check fired, the fit leaves here, before any parameter moves, +so the parameters it returns are the ones whose likelihood it has just computed. +Otherwise the M-step updates the parameters, with the learning rates the checks have just set: + +- **Model weights, biases and mixture weights** ($\gamma_h$, $\mathbf{c}_h$, $\alpha$) take their exact expectation-maximization values, + closed-form functions of the responsibilities. +- **Unmixing matrices** move by the **natural gradient**, + which accounts for the geometry of the space of matrices and converges far faster than the ordinary gradient: $$ \Delta \mathbf{W}_h \;\propto\; @@ -65,23 +86,55 @@ $$ where $\langle\cdot\rangle$ is the responsibility-weighted average over samples and $\mathbf{g}$ is the **score function** of the source density, - $g_i(y) = -\,\partial \log p_i(y)/\partial y$. For a single generalized - Gaussian component this is $g(y) = \rho\,\beta^{\rho}\,|y|^{\rho-1}\,\mathrm{sign}(y)$; - for a mixture it is the responsibility-weighted combination of its components' - scores. At the optimum the bracket vanishes, i.e. the sources are decorrelated - from their own scores, a statement of independence. - -- **Newton update.** Once the iterates are close, AMICA switches the unmixing - update to a **Newton step** (using the per-source curvature of the - likelihood), which sharpens convergence near the optimum. pamica keeps this - step positive-definite for stability. + $g_i(y) = -\,\partial \log p_i(y)/\partial y$. + For a single generalized Gaussian component this is $g(y) = \rho\,\beta^{\rho}\,|y|^{\rho-1}\,\mathrm{sign}(y)$; + for a mixture it is the responsibility-weighted combination of its components' scores. + At the optimum the bracket vanishes, that is, the sources are decorrelated from their own scores, a statement of independence. + The step is scaled by the learning rate, which climbs back toward its ceiling a little on every update. + +- **Newton step.** + With Newton enabled, from iteration `newt_start` on, AMICA replaces the natural-gradient direction with a **Newton step**, + which uses the per-source curvature of the likelihood and converges faster near the optimum. + An iteration whose curvature is not positive definite falls back to the natural gradient, which keeps the step stable. + Newton is off by default, as in the reference binary's compiled default; + pass `do_newton=True` to turn it on, as the reference's bundled `input.param` does. +- **A-freeze windows.** + From iteration `share_start` on, AMICA holds the unmixing update, and its learning-rate climb, + on the six iterations that start at each multiple of `share_iter`: + with the defaults (`share_start = share_iter = 100`), on iterations 100-105, 200-205, and so on. + The windows give the models time to settle after component sharing merges components, + but the reference applies them to every fit, with or without sharing, and so does pamica. + The other parameters keep updating during a window. +- **Source-density parameters** ($\mu, \beta, \rho$) take their exact expectation-maximization values: + closed-form expressions for the locations and scales, + and a digamma equation for the shape $\rho$, stepped by its own learning rate. +- **Rescaling (`doscaling`).** + ICA cannot tell a large source with a narrow density from a small source with a wide one. + After the update, AMICA divides each component's mixing vector by its norm + and rescales that component's density location and scale to match. + This is an exact change of scale: the log-likelihood does not change, and every component's mixing vector has unit norm. + +An update that leaves any parameter non-finite ends the fit at once, also marked degenerate. +After the update come the optional steps: +the extended-Infomax switch between super- and sub-Gaussian densities (`pdftype=1`), +the component-sharing scan (`share_comps`), +and outlier rejection (`do_reject`). +Then the next iteration starts with its E-step. ### Convergence and the returned solution -Each EM iteration increases the log-likelihood until it converges. Because the -learning-rate schedule is not strictly monotone, pamica returns the -**highest-likelihood iterate** it visited (the *best-iterate* safeguard) rather -than the last one, and reports its likelihood as `final_ll_`. +The log-likelihood rises over a fit, but not at every iteration: +a step can overshoot, which is what the decrease response corrects. +A fit ends on a stopping check or when it reaches `max_iter`. +After a stopping check, the returned parameters are exactly those whose likelihood was last computed. +At `max_iter` the last iteration still takes its update, as in the reference, +so the last likelihood computed belongs to the parameters one update earlier. + +Because the learning-rate schedule is not monotone, pamica by default returns the **highest-likelihood iterate** it visited +(the *best-iterate* safeguard, `keep_best`), which is the last one unless the fit ended below an earlier peak, +and reports its likelihood as `final_ll_`. +The reference has no such safeguard, +and pamica turns it off under component sharing and outlier rejection, where likelihoods from different iterations are not comparable. ![Log-likelihood versus iteration on the real sample EEG, rising and converging.](../assets/figures/ll-convergence.png){ width=640 } /// caption @@ -91,8 +144,8 @@ and converges toward the reference solution. ## Relationship to the Fortran reference -Every M-step update in pamica is derived to match the AMICA reference Fortran -implementation, and on real sample EEG the natural-gradient backend reaches the -same solution (log-likelihood and Hungarian-matched component correlation). See -[Validation & Parity](../guides/validation.md) for the acceptance criteria and -how cross-backend equivalence depends on data adequacy. +Every step above follows the AMICA reference Fortran implementation (`amica15`), in the same order within each iteration, +and on real sample EEG the natural-gradient backend reaches the same solution +(log-likelihood and Hungarian-matched component correlation). +See [Validation & Parity](../guides/validation.md) for the acceptance criteria and how cross-backend equivalence depends on data adequacy, +and [pamica vs. AMICA](../guides/amica-differences.md) for every place pamica deliberately differs. diff --git a/docs/concepts/index.md b/docs/concepts/index.md index 76eb9c5d..f35c64ad 100644 --- a/docs/concepts/index.md +++ b/docs/concepts/index.md @@ -3,14 +3,14 @@ This section explains the ideas behind pamica from the ground up, for readers new to independent component analysis. -- **[What is ICA?](what-is-ica.md)** — the blind source separation problem, the +- **[What is ICA?](what-is-ica.md)**: the blind source separation problem, the linear mixing model, and why statistical independence and non-Gaussianity make it solvable. -- **[What is AMICA?](what-is-amica.md)** — how AMICA extends ICA with *adaptive* +- **[What is AMICA?](what-is-amica.md)**: how AMICA extends ICA with *adaptive* source densities (mixtures of generalized Gaussians) and *multiple* ICA models. -- **[How AMICA works](how-amica-works.md)** — the log-likelihood objective and - the expectation-maximization algorithm (natural-gradient and Newton updates) - that fits it. +- **[How AMICA works](how-amica-works.md)**: the log-likelihood objective and + the expectation-maximization algorithm that fits it, one iteration at a time + (E-step, checks, update). If you just want to run a decomposition, start with [Getting Started](../getting-started.md) instead. diff --git a/docs/concepts/what-is-amica.md b/docs/concepts/what-is-amica.md index 821f0f7b..38341413 100644 --- a/docs/concepts/what-is-amica.md +++ b/docs/concepts/what-is-amica.md @@ -51,10 +51,18 @@ An adaptive source density (solid) as a weighted sum of generalized Gaussian mixture components (dashed). /// -pamica supports all five source-density families of the reference -implementation (generalized Gaussian is the default), plus an extended-Infomax -switcher that flips each source between super- and sub-Gaussian by the sign of -its kurtosis. +pamica supports all five source-density families of the reference implementation +(generalized Gaussian is the default), +plus an extended-Infomax switcher that flips each source between super- and sub-Gaussian by the sign of its kurtosis. +The legacy NumPy backend fits the generalized Gaussian only. + +AMICA settles ICA's scale ambiguity +([What ICA cannot pin down](what-is-ica.md#what-ica-cannot-pin-down)) by convention: +each component's mixing vector is kept at unit norm, +and the source's amplitude is carried by the locations $\mu_{ij}$ and scales $\beta_{ij}$ of its density. +AMICA restores that convention after every update by rescaling each component (`doscaling`, on by default), +an exact change of scale that leaves the likelihood unchanged +(see [How AMICA works](how-amica-works.md#3-exit-or-update)). ## Idea 2: multiple ICA models @@ -78,12 +86,15 @@ $\mathbf{w}_{hi}^{\top}$ is the $i$-th row of $\mathbf{W}_h$, and the $|\det \mathbf{W}_h|$ (Jacobian) term accounts for the change of variables from $\mathbf{x}$ to the sources. -With $H = 1$ this reduces to ordinary ICA with adaptive source densities; that -single-model case is the default and reaches bit-level parity with the Fortran -reference. +With $H = 1$ this reduces to ordinary ICA with adaptive source densities. +That single-model case is the default, +and it is where pamica's parity with the Fortran reference is established +(see [Validation & Parity](../guides/validation.md)). -AMICA also supports **shared components** across models (merging near-collinear -sources) and **outlier rejection**. These are off by default. +AMICA also supports **shared components** across models +(components whose scalp maps are nearly identical in two models are merged, so the models share one mixing vector and one density) +and **outlier rejection** (samples whose likelihood is far below the rest are dropped from the fit). +Both are off by default. Next: [How AMICA works](how-amica-works.md), which fits this model by maximizing its likelihood. diff --git a/docs/concepts/what-is-ica.md b/docs/concepts/what-is-ica.md index 41c897b0..ce587f7b 100644 --- a/docs/concepts/what-is-ica.md +++ b/docs/concepts/what-is-ica.md @@ -79,8 +79,8 @@ In practice the data are first **centered** (mean removed) and **whitened** (also called *sphering*): linearly transformed so the channels are uncorrelated and have unit variance. Whitening removes all second-order structure, reducing the remaining ICA problem to finding an orthogonal rotation, which is both -faster and better conditioned. pamica uses a symmetric (ZCA) whitening that -matches the Fortran reference. +faster and better conditioned. pamica uses a symmetric zero-phase component +analysis (ZCA) whitening that matches the Fortran reference. ## What ICA cannot pin down @@ -92,9 +92,10 @@ irreducible: scalar can move between a column of $\mathbf{A}$ and the corresponding source. These do not affect the usefulness of the components; they only mean component -*indices* and *scaling* are conventions, not ground truth. pamica follows the -EEGLAB conventions for ordering and sign where it matters (see -[Validation & Parity](../guides/validation.md)). +*indices* and *scaling* are conventions, not ground truth. AMICA keeps each +component's mixing vector at unit norm (see [What is AMICA?](what-is-amica.md)), +and pamica's EEGLAB export and `variance_order` give EEGLAB's component order +(see [EEGLAB interoperability](../guides/eeglab.md)). Next: [What is AMICA?](what-is-amica.md), which relaxes the fixed-source-density assumption and adds multiple models. diff --git a/docs/development/testing.md b/docs/development/testing.md index 29b3024f..e48d7a31 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -12,8 +12,14 @@ basis for correctness tests: no test is better than a fake passing test. uv run pytest # full suite uv run pytest --cov # with coverage uv run pytest pamica/tests/torch_tests/ # PyTorch-vs-Fortran parity tests +uv run pytest -m "not slow" # skip the slow tests, as CI does +AMICA_RUN_FORTRAN=1 uv run pytest # also run the tests that execute the native binary ``` +Tests that run the Fortran binary, including the element-wise oracles that seed it from a pamica state +(`pamica/tests/native_oracle.py`), are opt-in with `AMICA_RUN_FORTRAN=1`; +the weekly macOS workflow runs them. + ## Working directory The suite runs from a temporary working directory, not the repository: @@ -27,10 +33,11 @@ Coverage reports still land in the repository root. ## Layout -- `pamica/tests/` — end-to-end and interface tests. -- `pamica/tests/torch_tests/` — natural-gradient backend parity, PDF families, +- `pamica/tests/`: end-to-end and interface tests, and the cross-backend tests every shared behavior needs. +- `pamica/tests/torch_tests/`: natural-gradient backend parity, PDF families, component sharing, float32 stability, and edge cases. -- `pamica/tests/mlx_tests/` — MLX backend tests (Apple Silicon). -- `validate_implementations.py` — cross-implementation validation harness +- `pamica/tests/mlx_tests/`: MLX backend tests (Apple Silicon). +- `pamica/tests/mne_tests/`: the MNE wrapper, including the end-to-end workflow test. +- `validate_implementations.py`: cross-implementation validation harness (Hungarian component matching against Fortran; `--backend` selects torch, numpy, mlx, a list, or `all`). diff --git a/docs/getting-started.md b/docs/getting-started.md index ebe815d8..ac4644a8 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,6 +5,19 @@ pamica uses [UV](https://docs.astral.sh/uv/) for environment and dependency management. +### From the Python Package Index (PyPI) + +Released versions are on PyPI as `pamica`: + +```bash +uv add pamica # in a uv project +uv pip install pamica # or into an existing environment +``` + +The optional extras install the same way, for example `uv add "pamica[mlx]"` or `uv add "pamica[mne]"`. +The changes listed under "Unreleased" in the [changelog](changelog.md) are not in a release yet; +install from source to use them. + ### From source ```bash @@ -13,9 +26,6 @@ cd pAMICA uv sync # install the project and its dependencies into .venv ``` -A packaged release on the Python Package Index (PyPI) is planned; until then, -install from source as above. - ### Optional Apple Silicon backend (MLX) The MLX backend runs on the Apple Silicon graphics processing unit (GPU). @@ -48,8 +58,9 @@ amica.fit(X, max_iter=100) # Unmixed sources and the mixing/unmixing matrices S = amica.transform(X) -A = amica.get_mixing_matrix(0) # mixing matrix for model 0 -W = amica.get_unmixing_matrix(0) # unmixing matrix for model 0 +A = amica.get_mixing_matrix(0) # model 0's mixing matrix, sphered space +W = amica.get_unmixing_matrix(0) # model 0's unmixing matrix, sphered space +maps = amica.get_sensor_mixing_matrix(0) # scalp maps, input-channel space print("final log-likelihood:", amica.final_ll_) ``` @@ -59,6 +70,9 @@ print("final log-likelihood:", amica.final_ll_) higher-likelihood iterate, so `final_ll_` is the log-likelihood of the *returned* model. `ll_history_` is the true per-iteration trajectory and may dip below its peak on a late overshoot. + As in the reference, a fit that stops on a convergence check returns the + parameters `final_ll_` was computed from, while one that runs to `max_iter` + takes one more update after its last likelihood. ## Choosing a device and precision @@ -100,6 +114,7 @@ and the measured precision ([Precision on the MLX backend](guides/backends.md#pr ## Next steps -- [Backends & Devices](guides/backends.md) — CUDA / CPU / MLX and float32 vs float64. -- [Validation & Parity](guides/validation.md) — comparing against the Fortran reference. -- [API Reference](api/index.md) — full parameter and method documentation. +- [Backends & Devices](guides/backends.md): CUDA / CPU / MLX and float32 vs float64. +- [Validation & Parity](guides/validation.md): comparing against the Fortran reference. +- [pamica vs. AMICA](guides/amica-differences.md): every deliberate difference from the reference. +- [API Reference](api/index.md): full parameter and method documentation. diff --git a/docs/guides/amica-differences.md b/docs/guides/amica-differences.md index d8f11205..08cfdd20 100644 --- a/docs/guides/amica-differences.md +++ b/docs/guides/amica-differences.md @@ -6,20 +6,20 @@ reference is how correctness is defined here. This page lists every place pamica Anything not on this page is intended to match the reference. If you find a difference that is not listed, that is a bug worth -[reporting](https://github.com/sccn/pAMICA/issues) — not a documented choice. +[reporting](https://github.com/sccn/pAMICA/issues). ## At a glance | # | Area | Fortran AMICA | pamica default | Why | Restore reference | |---|---|---|---|---|---| | 1 | Rank threshold | absolute floor `mineig=1e-15` | relative floor `mineig_rel=1e-12` | the absolute floor is unit-dependent: MEG in Tesla yields rank 0, and average-referenced EEG is detected by luck | `mineig_rel=None` | -| 2 | Zero numerical rank | `numeigs = 0`, continues | `ValueError` naming cause and fix | fitting a zero-dimensional model is not a recoverable state | — (no reason to want it) | +| 2 | Zero numerical rank | `numeigs = 0`, continues | `ValueError` naming cause and fix | fitting a zero-dimensional model is not a recoverable state | none (no reason to want it) | | 3 | Returned iterate | last EM iterate | highest-likelihood iterate (`keep_best`) | the lrate schedule is non-monotone; late Newton overshoots cut LL variance 12.7x → 2.0x | `keep_best=False` | -| 4 | Newton | on (`do_newton=1`) | off | isolates the algorithm from initialization for parity work | `do_newton=True` | -| 5 | Degenerate fits | returns NaN sources, and writes them out on its `writestep` cadence | the `AMICA` wrapper, on either backend, refuses `transform`/`get_*`/`save`; the raw `AMICATorchNG`, `AMICAMLXNG` and NumPy `AMICA` backends now refuse their own output accessors on a degenerate fit too (issue #306), not just the wrapper; NumPy additionally reports `converged=False` with a `stop_reason`, refuses the final write, and skips each periodic checkpoint with a logged reason (leaving the last valid one on disk). Every backend stops before a non-finite value is applied or returned: a non-finite likelihood (never recorded in the history), update direction (`nan_direction`) or parameter after an update (`nan_params`), where the reference applies a NaN step and exits on the next likelihood | NaN sources silently poison downstream analysis, and `loadmodout` reads a NaN checkpoint back without complaint | — (see issues #50, #240, #306 and #339) | +| 4 | Newton | off in the compiled defaults (amica15_header.f90:19); the bundled `input.param` turns it on (`do_newton 1`) | off | the compiled default; Newton-off fits isolate the algorithm from initialization for parity work | `do_newton=True`, as the bundled `input.param` sets | +| 5 | Degenerate fits | returns NaN sources, and writes them out on its `writestep` cadence | the `AMICA` wrapper, on either backend, refuses `transform`/`get_*`/`save`; the raw `AMICATorchNG`, `AMICAMLXNG` and NumPy `AMICA` backends refuse their own output accessors on a degenerate fit too (issue #306); NumPy additionally reports `converged=False` with a `stop_reason`, refuses the final write, and skips each periodic checkpoint with a logged reason (leaving the last valid one on disk). Every backend stops before a non-finite value is applied or returned: a non-finite likelihood (`nan_ll`, or `singular_ll` for an infinite one; never recorded in the history), update direction (`nan_direction`) or parameter after an update (`nan_params`), where the reference applies a NaN step and exits on the next likelihood | NaN sources silently poison downstream analysis, and `loadmodout` reads a NaN checkpoint back without complaint | none (see issues #50, #240, #306 and #339) | | 6 | Precision | float64 | float64 (float32 on Apple GPUs) | Apple GPUs have no float64; float32 agrees to ~7 significant digits, not bit-parity | `dtype=torch.float64` | -| 7 | Sensor-space maps | `Spinv` applied internally | `get_sensor_mixing_matrix()` | `get_mixing_matrix()` returns sphered-space `A`; switching its meaning by data conditioning would be worse | — | -| 8 | Components merged away by `share_comps` | mixing vector and density updated to NaN, then hidden by the `comp_used` mask | frozen at their last finite value (never divided, never rescaled) | a fit must not end holding NaN parameters, mask or no mask; the components are dead either way | — (see issues #60, #240, #334) | +| 7 | Sensor-space maps | `Spinv` applied internally | `get_sensor_mixing_matrix()` | `get_mixing_matrix()` returns sphered-space `A`; switching its meaning by data conditioning would be worse | none | +| 8 | Components merged away by `share_comps` | mixing vector and density updated to NaN, then hidden by the `comp_used` mask | frozen at their last finite value (never divided, never rescaled) | a fit must not end holding NaN parameters, mask or no mask; the components are dead either way | none (see issues #60, #240, #334) | | 9 | Block-size search | on (`do_opt_block=1`), sweeps 128–1024, **aborts** if a candidate cannot allocate | off; sweeps 4096–32768; a candidate that cannot allocate is skipped and the fit continues | the choice is timing-based and therefore machine-dependent, which a parity run cannot have; Fortran's range sits far below where any pamica backend peaks; and running out of memory is a reason to use a smaller block, not to stop | `do_opt_block=True` (but pin `block_size` for a bit-for-bit comparison) | | 10 | Restarts across seeds | none (its `maxrestarts` only *recovers* from an early NaN) | available as `n_restarts`, **off by default** (`n_restarts=1`) | the weakest under-determined components are init-basin sensitive, so best-of-N buys robustness; but a default that ran N fits would change every result and cost N times as long | `n_restarts=1` (the default) | | 11 | Reconstruction after rank reduction (`AMICAICA.apply`) | output has no representation of the discarded principal component analysis (PCA) subspace (sphere rows past `numeigs` are zero), so any back-projection drops it | the MNE export carries the full PCA basis, so `apply` restores the residual | MNE's own `ICA` does; the residual was never part of the independent component analysis (ICA) decomposition, so it is not ICA's to remove | `apply(..., n_pca_components=ica.n_components_)` | @@ -225,8 +225,11 @@ so the params-file reader, the degenerate-fit contract, `.pt` `save`/`load`, the | `keep_best` best-iterate restore | yes | no | yes | n/a | | `n_restarts` best-of-N restarts | yes | yes | yes | n/a | | Mutual Information Reduction (MIR) diagnostic | yes | no | yes | n/a | -| Persistence | `state_dict` + EEGLAB `amicaout` export | EEGLAB `amicaout` | `state_dict`/`.npz` `save`-`load` + EEGLAB `amicaout` export | EEGLAB `amicaout` | -| Fortran `input.param` reader | yes (`AMICA.from_params_file`, #132) | yes (`AMICA_NumPy(params_file=...)` / `from_params_file`, #304) | yes (`AMICA.from_params_file(..., backend="mlx")`) | native | +| `variance_order` (EEGLAB component order) | yes | no (issue #317) | yes | applied by `loadmodout15` on load | +| Per-sample scoring (`model_loglik`/`model_probability`) | yes | no | yes | n/a | +| Through the `AMICA` and `AMICAICA` wrappers | yes (`backend="torch"`, the default) | no, used directly | yes (`backend="mlx"`) | no, used directly as `AMICANative` | +| Persistence | `state_dict`, `AMICA.save` `.pt` + EEGLAB `amicaout` export | EEGLAB `amicaout` | `state_dict`/`.npz` `save`-`load`, `AMICA.save` `.pt` + EEGLAB `amicaout` export | EEGLAB `amicaout` | +| Parameter file (JSON or Fortran `input.param`) | yes (`AMICA.from_params_file`, #132) | yes (`AMICA_NumPy(params_file=...)` / `from_params_file`, #304) | yes (`AMICA.from_params_file(..., backend="mlx")`) | the binary reads `input.param`; `AMICANative` takes its keys as keywords | | A second `fit` on the same instance | starts from a fresh initialization | continues from the previous fit ([see below](#refitting-a-numpy-instance-continues-from-the-previous-fit-issue-312)) | starts from a fresh initialization | every run starts fresh, or from `load_*` files | The NumPy row's "GG only" (generalized Gaussian, GG) corrects an earlier @@ -412,25 +415,15 @@ m.shared_components() # groups of (model, source) pai For a full-rank square sphere `pinv` equals `inv` to about 1e-15, far below the `comp_thresh` decision boundary (0.99 by default), so merge decisions on -well-conditioned data are unchanged; the bundled sample reproduces its previous -`comp_list` and log-likelihood bit for bit. - -The NumPy backend now reaches the merge decision from the same metric (issue -#258): `identify_shared_components` takes the de-sphered sensor-space maps -directly -- `pinv(sphere) @ A.T` since issue #334, the same back-map described -above -- instead of comparing columns of the sphered `A`. -The two backends therefore make the identical merge decision from the same -fitted state (`pamica/tests/test_numpy_share_comps.py::test_numpy_merge_decision_matches_torch_backend`). -The MLX backend calls that same kernel on the same host float64 inputs (issue -#263), so all three agree. -On one real fitted 2-model state (measured before issue #334, when the metric -still compared stored columns) the top candidate cross-model pair measured -0.992 cosine similarity in sensor space against 0.970 in the old sphered -space -- close enough that, with the default `comp_thresh=0.99`, the two -metrics disagree on whether that pair merges. A NumPy fit that shares -components can therefore reach a different `comp_list` than it did before -#258, even on a full-rank, well-conditioned sphere; only the `pinv`-vs-`inv` -comparison two paragraphs above is unaffected by that swap. +well-conditioned data are unchanged; when that change landed, the bundled +sample reproduced its previous `comp_list` and log-likelihood bit for bit. + +All three backends reach the merge decision through one kernel, `identify_shared_components` +(the NumPy backend since issue #258, MLX since issue #263), +on the same host float64 sensor-space maps, `pinv(sphere) @ A.T` since issue #334, +so they make the identical merge decision from the same fitted state +(`pamica/tests/test_numpy_share_comps.py::test_numpy_merge_decision_matches_torch_backend` +and `pamica/tests/test_mlx_sharing_cross_backend.py`). ## Component sharing compares and ties components (issue #334) @@ -548,7 +541,7 @@ LLt[num_models, :].sum() / (n_good_samples * nw) == LL[-1] ``` It holds bit for bit with one reference-faithful exception: a `do_reject` fit -whose rejection fires on the same iteration as the write. `LL(iter)` is +whose rejection fires on its last iteration, just before the final write. `LL(iter)` is normalized over the good set as it stood *before* that rejection (amica15.f90:1770), and `reject_data` then shrinks `numgoodsum` (amica15.f90:2252) and zeroes the rejected samples' `modloglik`/`loglik` @@ -742,7 +735,7 @@ are additionally capped by `n_samples` and by a conservative estimate of one block's peak, so the search usually finds its ceiling without having to walk into a failure at all. -One consequence worth stating plainly: on the NumPy backend this flag used to +One consequence for earlier NumPy results: on that backend this flag used to default to **on** (following Fortran's header) while its sweep ran over 128–1024, so every NumPy fit quietly re-tuned itself to a small block and ignored the `block_size` it was given. It is now off by default there too, and diff --git a/docs/guides/backends.md b/docs/guides/backends.md index 6a5dd834..4f8fca52 100644 --- a/docs/guides/backends.md +++ b/docs/guides/backends.md @@ -1,7 +1,8 @@ # Backends & Devices pamica ships one primary PyTorch backend behind the [`AMICA`](../api/amica.md) -interface, plus an optional Apple-GPU backend and a legacy NumPy reference. +interface, plus an optional Apple-GPU backend, a legacy NumPy reference, and +the Fortran reference binary itself as a native backend. ## Backends @@ -9,7 +10,8 @@ interface, plus an optional Apple-GPU backend and a legacy NumPy reference. |---|---|---| | PyTorch natural-gradient EM | [`AMICATorchNG`](../api/torch-backend.md) | **Default.** Fortran-parity backend; CUDA / CPU, and float32 on MPS. | | MLX (Apple GPU) | [`AMICAMLXNG`](../api/mlx-backend.md) (`pamica.mlx_impl`) | Optional Apple-Silicon GPU backend; float32 only. | -| NumPy reference | [`AMICA_NumPy`](../api/numpy-backend.md) | Legacy oracle + CLI; carries the same parity fixes. | +| NumPy reference | [`AMICA_NumPy`](../api/numpy-backend.md) | Legacy oracle + CLI; carries the same parity fixes, generalized Gaussian only. | +| Native Fortran | [`AMICANative`](../api/native-backend.md) | Runs the reference binary on your data; the parity oracle. | The `AMICA` wrapper and the MNE wrapper `AMICAICA` build `AMICATorchNG` by default and `AMICAMLXNG` with `backend="mlx"` (see [Selecting a backend](#selecting-a-backend)). @@ -40,7 +42,13 @@ Everything the wrappers offer works on both backends: MLX always runs on its default device (the Apple GPU) in float32, so `AMICA(backend="mlx", device=...)` and `fit(..., dtype=...)` on an MLX model raise `ValueError`. - The default stays `"torch"`, the float64 Fortran-parity path; there is no automatic selection. -- `AMICA.from_params_file(path, backend="mlx")` applies the file's settings through `AMICAMLXNG`'s own constructor, +- The wrappers build only these two backends. + The legacy NumPy backend and the native binary are used directly, as `AMICA_NumPy` (which reads a parameter file itself, `params_file=`) + and `AMICANative` (which takes Fortran `input.param` keys as keywords); see [the backend table](amica-differences.md#backend-differences). +- `AMICA.from_params_file` is the wrapper's parameter-file entry point; + `AMICAICA` takes its settings as `fit` keywords instead. +- `AMICA.from_params_file(path, backend="mlx")` stores the file's settings on the instance; + `fit` then passes them to `AMICAMLXNG`'s own constructor as defaults (an explicit `fit` keyword wins) and names any setting that backend cannot take in its "not applied" warning. - `save` records the backend (`format_version` 2), so `AMICA.load` restores the model on the backend that fit it. Files written before backend selection (version 1) still load, as PyTorch models. @@ -49,7 +57,8 @@ Everything the wrappers offer works on both backends: Loading an MLX model needs MLX installed and takes no `device`. The raw classes stay available for direct use (`from pamica.mlx_impl import AMICAMLXNG`), -but the wrappers are the supported route: they add the degenerate-fit contract, the params-file reader and the MNE export. +but the wrappers are the supported route: they add the `converged_`/`stop_reason_` record, the params-file reader, `.pt` `save`/`load` and the MNE export. +The raw classes refuse output from a degenerate fit too (issue #306). ### Precision on the MLX backend @@ -107,18 +116,20 @@ so its export omits the `LLt` file (with a warning). With the PyTorch backend, `AMICA(device=...)` accepts `"cuda"`, `"cpu"`, `"mps"`, or `None` (auto): -- **`None` (auto)** — selects CUDA if available, else CPU. An auto-selected MPS - device is redirected to CPU because the parity default is float64, which MPS - cannot represent. -- **`"cuda"`** — the bit-safe path for float64 Fortran parity on NVIDIA GPUs. -- **`"mps"`** — requires `dtype=torch.float32`. Note that PyTorch-MPS is not a +- **`None` (auto)**: picks MPS, then CUDA, then CPU, whichever is available first. + For a float64 fit (the default) the wrapper redirects an MPS pick to CPU, because MPS cannot represent float64, + so on a Mac a default fit runs on the CPU and a `dtype=torch.float32` fit on MPS. + The raw `AMICATorchNG` does not redirect: with `device=None` and float64 on a Mac its constructor raises `ValueError`, + so pass `device="cpu"` there. +- **`"cuda"`**: the bit-safe path for float64 Fortran parity on NVIDIA GPUs. +- **`"mps"`**: requires `dtype=torch.float32`. Note that PyTorch-MPS is not a performance win for AMICA (see below); prefer the MLX backend on Apple hardware. ## Precision: float64 vs float32 -- **float64** — the default; required for Fortran-parity runs. CUDA float64 +- **float64**: the default; required for Fortran-parity runs. CUDA float64 agrees with the CPU log-likelihood to ~5 significant digits. -- **float32** — required on the Apple GPUs (MPS/MLX have no float64) and +- **float32**: required on the Apple GPUs (MPS/MLX have no float64) and ~7-significant-digit, not float64-parity. It is not a general speedup: CUDA is overhead-bound so float32 is about as fast as float64, while on CPU float32 is modestly faster and scales better across cores. The Apple-GPU speed win comes @@ -144,7 +155,7 @@ Measured on real 70-channel EEG (see the project benchmarks and remains the recommendation over `device="mps"` on Apple hardware. See [Block-size sensitivity](validation.md#block-size-sensitivity) for the full sweep. - On **NVIDIA**, CUDA float64 is the bit-safe path (~4.5x over a 16-thread CPU, - warmed); float32 is faster still. + warmed); float32 is about as fast, since CUDA is overhead-bound at EEG scale. - On **CPU**, intra-op threads are workload-limited; around 4 threads was the sweet spot in the measured laptop sweep, with 8+ regressing. @@ -153,5 +164,5 @@ All backends agree on the log-likelihood to ~3 significant digits on real data. !!! note "Cross-backend equivalence and data adequacy" Whether two backends recover the *same* independent components depends on how well-determined the decomposition is (the data-adequacy factor - `k = frames / channels^2`). See [Validation & Parity](validation.md); the - full data-size sweep is being finalized. + `k = frames / channels^2`). See the channel and frame sweeps in + [Validation & Parity](validation.md#data-adequacy-and-cross-backend-equivalence). diff --git a/docs/guides/eeglab.md b/docs/guides/eeglab.md index c9a01b82..bb6398d6 100644 --- a/docs/guides/eeglab.md +++ b/docs/guides/eeglab.md @@ -96,16 +96,21 @@ which ranks sources by the same back-projected variance (IC1 = highest): ```python order = model.variance_order() # source indices, highest variance first -A = model.get_mixing_matrix()[:, order] # scalp maps in EEGLAB order +A = model.get_sensor_mixing_matrix()[:, order] # scalp maps in EEGLAB order W = model.get_unmixing_matrix()[order] # unmixing rows in EEGLAB order ``` Pass `return_svar=True` to also get the per-component variances. +`get_sensor_mixing_matrix()` gives the maps in input-channel space; +`get_mixing_matrix()` is the sphered-space matrix, which is not a scalp map. + ## Multi-model note -Single-model output files are byte-identical in layout to a native AMICA run. For -`n_models > 1` the per-model axis layout is self-consistent (it round-trips -through `loadmodout15` and pamica's own reader) but is not byte-identical to a -native multi-model AMICA run; see the multi-model equivalence discussion in -[Validation & Parity](validation.md). +Every file is written in the reference's layout for any number of models: +`W` with the model axis slowest (issue #159) and `A` with one column per component (issue #334), +so `loadmodout15` reads a multi-model directory the same way it reads a native one. +The values themselves are another matter: +multi-model AMICA is not partition-identifiable, +so two runs, native or pamica, do not produce the same models; +see the multi-model discussion in [Validation & Parity](validation.md#multi-model-distributional-similarity). diff --git a/docs/guides/index.md b/docs/guides/index.md index d5246a64..c4bfc1cb 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -3,9 +3,15 @@ The user guide covers how to run pamica in practice and how its results relate to the reference implementation. -- **[Backends & Devices](backends.md)** — the available compute backends - (PyTorch natural-gradient EM, optional MLX, legacy NumPy), device selection - (CUDA / CPU / MPS), float32 vs float64, and performance guidance on real EEG. -- **[Validation & Parity](validation.md)** — how correctness is defined as +- **[Backends & Devices](backends.md)**: the available compute backends + (PyTorch natural-gradient EM, optional MLX, legacy NumPy), how to select one + through `AMICA` and `AMICAICA`, device selection (CUDA / CPU / MPS), float32 + vs float64, and performance guidance on real EEG. +- **[EEGLAB interoperability](eeglab.md)**: writing a fit as an EEGLAB + `amicaout` directory and reading it with `loadmodout15`. +- **[Validation & Parity](validation.md)**: how correctness is defined as parity with the Fortran reference, the validation harness, and how cross-backend equivalence depends on data adequacy. +- **[Differences vs AMICA](amica-differences.md)**: every place pamica + deliberately behaves differently from the Fortran reference, why, and how to + restore the reference behavior. diff --git a/docs/guides/validation.md b/docs/guides/validation.md index ecbc627d..66cdd00a 100644 --- a/docs/guides/validation.md +++ b/docs/guides/validation.md @@ -27,7 +27,7 @@ Throughout, IC abbreviates independent component and LL log-likelihood. | Device and precision invariance | same independent components across CPU/CUDA/MPS/MLX, float32/float64, Linux/macOS | identical (1.000) across all eight torch/MLX combinations | | Cross-backend log-likelihood | converged LL across every backend | agree to ~3 significant digits (max pairwise ~0.003) | | EEGLAB output | `write_amica_output` round-trip through `loadmodout15` | single-model bytes are an exact serialization; loads with correct layout | -| Degenerate fits | NaN or singular log-likelihood | refused, never returned as NaN sources | +| Degenerate fits | non-finite log-likelihood, update direction or parameters | refused, never returned as NaN sources | ## The validation harness @@ -379,8 +379,9 @@ The round-trip is verified two ways: and fixed, a column-major format bug in the mixture-parameter arrays. `variance_order()` reproduces EEGLAB's IC ordering (IC1 = highest back-projected variance) in Python without a -disk round-trip. For `n_models > 1` the layout is self-consistent and round-trips through both readers, but is -not byte-identical to a native multi-model run (see the multi-model discussion above). +disk round-trip. For `n_models > 1` every file is also in the reference's layout (`W` since issue #159, `A` since issue #334), +and the directory round-trips through both readers; the values differ from any one native run +only because multi-model AMICA is not partition-identifiable (see the multi-model discussion above). Full usage is in the [EEGLAB interoperability guide](eeglab.md); tests are in `pamica/tests/torch_tests/test_amica_ng_wrapper.py`. ## Performance across backends @@ -511,7 +512,7 @@ guarded to a no-op so the parity results above stay byte-for-byte unchanged. | Per-model bias `c` update (#27) | on for `n_models>1` | Fortran `update_c`; per-block stats bit-exact; no-op for `n_models=1` | | Component sharing (`share_comps`, #60, #334) | off by default | Fortran `identify_shared_comps` ported; the scan itself has no bit-exact oracle (`Spinv2` is never allocated, so the reference's scan computes NaN similarities and never merges), but the update from a merged state seeded through the reference's `load_comp_list` matches the native binary to float64 round-off on PyTorch and NumPy (`test_component_rows.py`, opt-in with `AMICA_RUN_FORTRAN=1`); byte-identical when unshared | | Outlier rejection (`do_reject`, #123) | off by default | `good_idx` mechanism on all three backends (NumPy, PyTorch, MLX -- the last landed epic #278 Phase 3, #289); MLX/NumPy ports validated vs the PyTorch backend | -| Degenerate-fit contract (#50) | always | the `AMICA` wrapper refuses a NaN or singular fit (`converged_` / `stop_reason_`); `transform`/`get_*`/`save` raise through the wrapper instead of returning NaN sources. This is a wrapper-level contract, not a raw-backend one -- calling a raw `AMICATorchNG`/`AMICAMLXNG`/`AMICA_NumPy` instance's `transform`/`get_*`/`save` directly, bypassing the wrapper, is not gated (tracked as issue #306). `write_amica_output` is the one exception: both the PyTorch and MLX backends gained the same degenerate/non-finite refusal directly on the raw class in this epic, since it has no wrapper equivalent to gate it. | +| Degenerate-fit contract (#50, #306, #339) | always | a fit that stops on a non-finite log-likelihood (`nan_ll`/`singular_ll`), update direction (`nan_direction`) or parameters (`nan_params`) is marked unusable (`converged_=False`, with `stop_reason_`), and every output path refuses it instead of returning NaN sources: the `AMICA` wrapper's `transform`/`get_*`/`write_amica_output`/`save`, and since issue #306 the raw `AMICATorchNG`, `AMICAMLXNG` and `AMICA_NumPy` output accessors too (`pamica/tests/test_backend_guards.py`, `pamica/tests/test_nonfinite_stops.py`). | | End-to-end workflow (#315) | always, PyTorch and MLX | average-referenced sample EEG with `pcakeep = n_channels - 1` through `AMICAICA`: 31 components, `get_sources` equal to `transform`, an exclusion that removes exactly one back-projection and keeps the residual, the EEGLAB export reloaded by `loadmodout`, `save`/`load`, an `input.param`-driven fit, and the two backends' sources Hungarian-matched with a minimum correlation of at least 0.999 (measured 0.999999999) | Tests live under `pamica/tests/`: `torch_tests/test_ng_backend.py`, `torch_tests/test_ng_sharing.py`, `torch_tests/test_amica_ng_wrapper.py`, `test_numpy_reject.py`, and `mne_tests/test_end_to_end_workflow.py`. @@ -543,8 +544,8 @@ backend-differences guide), taking `AMICATorchNG`'s off-by-default. Which of them actually ends a fit: -- **`min_dll` normally wins**, at iteration 326-1076 depending on the BLAS build - — but only when `max_iter` is large enough to let it. At `AMICATorchNG`'s +- **`min_dll` normally wins**, at iteration 326-1076 depending on the BLAS build, + when `max_iter` is large enough to let it. At `AMICATorchNG`'s default `max_iter=100` the fit always ends on `max_iter` before `min_dll` can fire, so the default PyTorch run is iteration-limited, not converged. Raise `max_iter` if you want the likelihood stop to be the one that decides. @@ -568,7 +569,7 @@ configuration, its own gradient norm oscillates rather than shrinking: It then plateaus at 1.0-1.65e-5 out to iteration 5073 without ever crossing the `1e-7` threshold, which sits about two orders of magnitude below the reference's -own floor. The Python backends plateau roughly two orders higher again — near +own floor. The Python backends plateau roughly two orders higher again: near a fixed point `dAk` tends to zero, so the norm is measuring a near-total cancellation where floating-point and BLAS ordering differences dominate what is left. diff --git a/docs/index.md b/docs/index.md index e0d3df81..bbd11db7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,13 +26,16 @@ Hungarian-matched component correlation both agree with Fortran on real EEG. ## Quick links -- [Getting Started](getting-started.md) — install and run your first decomposition. -- [Backends & Devices](guides/backends.md) — pick CUDA / CPU / MLX and float32 vs float64. -- [Validation & Parity](guides/validation.md) — how correctness is defined and checked. -- [API Reference](api/index.md) — the `AMICA` interface and the backend classes. +- [Getting Started](getting-started.md): install and run your first decomposition. +- [Backends & Devices](guides/backends.md): pick CUDA / CPU / MLX and float32 vs float64. +- [Validation & Parity](guides/validation.md): how correctness is defined and checked. +- [pamica vs. AMICA](guides/amica-differences.md): every deliberate difference from the reference. +- [API Reference](api/index.md): the `AMICA` interface and the backend classes. !!! note "Precision and parity" The natural-gradient backend computes in float64 for Fortran parity. Apple - MPS cannot represent float64, so parity runs use CPU or CUDA; float32 is - faster (and required on MPS/MLX) but is ~7-significant-digit, not - float64-parity. Use float64 for reference-parity runs. + MPS cannot represent float64, so parity runs use CPU or CUDA. float32 is + required on MPS/MLX and carries about seven significant digits; it runs + about as fast as float64 on CUDA and modestly faster on CPU, and the + Apple-GPU speed comes from the MLX backend. Use float64 for + reference-parity runs. diff --git a/pamica/amica.py b/pamica/amica.py index e744c14b..494ba2da 100644 --- a/pamica/amica.py +++ b/pamica/amica.py @@ -112,8 +112,8 @@ def _backend_class(backend: str) -> "type[AMICATorchNG] | type[AMICAMLXNG]": @functools.cache def _ctor_params(backend_cls: type) -> frozenset: """Constructor keywords ``fit()`` accepts from ``**kwargs`` and from a - parameter file, for one backend class (issue #132 review item 2, issue - #313). + parameter file, for one backend class (issue #132 review item 2, + issue #313). Derived from the class signature, so each backend gets exactly its own keywords: a file setting only AMICATorchNG accepts is applied to a torch @@ -260,15 +260,18 @@ class AMICA: The underlying backend model is_fitted_ : bool Whether a *usable* model is available. ``fit`` sets this True only when - the fit converged normally; a degenerate fit (see ``converged_``) leaves - it False, and ``transform``/``get_mixing_matrix``/``get_unmixing_matrix``/ - ``save`` refuse such a model (issue #50). + the fit ended on a non-degenerate stop (a convergence stop or + ``max_iter``); a degenerate fit (see ``converged_``) leaves it False, + and every output method (``transform``, the ``get_*`` accessors, + ``write_amica_output``, ``save``) refuses such a model (issue #50). converged_ : bool Whether the last ``fit`` ended on a usable stop rather than a degenerate one (``stop_reason_`` not in the backend's ``_DEGENERATE_STOP_REASONS``: - ``nan_ll``/``singular_ll``/``nan_direction``/``nan_params``). A - degenerate fit holds non-finite parameters, or stopped before applying - a non-finite step, and would produce NaN sources (issue #50). + ``nan_ll``/``singular_ll``/``nan_direction``/``nan_params``, or + ``restart_error`` under best-of-N restarts). A ``max_iter`` stop counts + as usable. A degenerate fit holds non-finite parameters, or stopped + before applying a non-finite step, and would produce NaN sources + (issue #50). stop_reason_ : str or None Why the last ``fit`` stopped (the backend ``stop_reason``): ``"max_iter"``, ``"lrate_floor"``, ``"grad_norm_floor"``, ``"min_dll"``, @@ -282,8 +285,8 @@ class AMICA: ``singular_ll``), a non-finite update direction caught before it is applied (``nan_direction``), or non-finite parameters right after an update (``nan_params``); PyTorch and MLX use the same set (issue #339 - review), and under best-of-N restarts a restart that raised has its - own degenerate marker. None of these checks short- + review), and under best-of-N restarts a restart that raised is + recorded as ``"restart_error"``, also degenerate. None of these checks short- circuits on an earlier one in the same iteration, so under the shipped ``use_grad_norm=True`` default ``"grad_norm"`` always takes precedence over ``"grad_norm_floor"`` when both would apply -- @@ -452,8 +455,8 @@ def fit( ``pcakeep``, ``use_min_dll``, ``min_dll``, ``maxincs``, ``use_grad_norm``, ``min_nd`` -- the issue #207 convergence stops, Fortran-faithful defaults ``True``/``1e-9``/``5``/``True``/``1e-7`` -- or - ``do_opt_block``/``blk_min``/``blk_max``/``blk_step``, the issue - #232 block-size search, off by default) -- the backend's tunables + ``do_opt_block``/``blk_min``/``blk_max``/``blk_step``, the + issue #232 block-size search, off by default) -- the backend's tunables are constructor arguments, not fit() kwargs. A keyword the selected backend's constructor does not take raises ``TypeError``. @@ -467,14 +470,14 @@ def fit( that never heard of restarts. Rank-deficient input (Maxwell-filtered MEG, average-referenced or - interpolated EEG) is handled by ``mineig``/``mineig_rel`` (issue - #223): the model is sized to the detected numerical rank and + interpolated EEG) is handled by ``mineig``/``mineig_rel`` + (issue #223): the model is sized to the detected numerical rank and :meth:`get_sensor_mixing_matrix` maps components back to input channels. ``mineig`` is an absolute eigenvalue floor and so unit-dependent; pass ``mineig_rel`` for data far from unit scale. - When the instance was built via :meth:`from_params_file` (issue - #132), any of the parameters above -- named or in ``**kwargs`` -- + When the instance was built via :meth:`from_params_file` + (issue #132), any of the parameters above -- named or in ``**kwargs`` -- left unset here falls back to that file's translated value instead of the hard-coded default; an explicitly passed argument always wins over the file. Settings the file carries that match neither a @@ -637,8 +640,8 @@ def _mirror_backend(self) -> None: routes expose the same surface. A degenerate stop (one of the backend class's own ``_DEGENERATE_STOP_REASONS``) holds non-finite parameters and would return NaN sources, so it is not a usable model: - ``is_fitted_`` stays False and the output methods refuse it (issue - #50), while ``stop_reason_``/``converged_`` stay set for inspection. + ``is_fitted_`` stays False and the output methods refuse it + (issue #50), while ``stop_reason_``/``converged_`` stay set for inspection. """ backend = self.model_ assert backend is not None @@ -730,7 +733,11 @@ def fit_transform(self, X: np.ndarray, **fit_params) -> np.ndarray: def get_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: """ - Get the mixing matrix A. + Get the mixing matrix A in the sphered space. + + Column ``i`` is source ``i``'s mixing vector after sphering, the + reference's ``A(:, comp_list(:, h))``. For scalp maps in input-channel + space use :meth:`get_sensor_mixing_matrix`. Parameters ---------- @@ -740,7 +747,7 @@ def get_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: Returns ------- A : np.ndarray - Mixing matrix of shape (n_channels, n_sources) + Mixing matrix of shape (n_sources, n_sources) """ self._check_usable("get the mixing matrix") assert self.model_ is not None @@ -749,7 +756,12 @@ def get_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: def get_unmixing_matrix(self, model_idx: int = 0) -> np.ndarray: """ - Get the unmixing matrix W. + Get the unmixing matrix W, which acts on sphered data. + + :meth:`transform` applies it after centering and sphering: + ``S = W @ (get_sphere() @ (X - mean[:, None]) - c[:, None])``, with + ``mean = get_mean()`` and ``c = get_model_center(model_idx)``, so + ``W @ get_sphere()`` is only the linear part of that map. Parameters ---------- @@ -759,7 +771,7 @@ def get_unmixing_matrix(self, model_idx: int = 0) -> np.ndarray: Returns ------- W : np.ndarray - Unmixing matrix of shape (n_sources, n_channels) + Unmixing matrix of shape (n_sources, n_sources) """ self._check_usable("get the unmixing matrix") assert self.model_ is not None @@ -882,8 +894,10 @@ def mir( """ Mutual Information Reduction (issue #137) of the fitted unmixing on ``X``. - Composes the full raw-data-to-sources transform (unmixing @ sphere) - the documented way and delegates to :func:`pamica.metrics.mir`. + Composes the linear part of the raw-data-to-sources transform + (unmixing @ sphere) and delegates to :func:`pamica.metrics.mir`. MIR + is shift-invariant, so the mean and center that :meth:`transform` + subtracts do not change it. Parameters ---------- @@ -1054,8 +1068,8 @@ def shared_components(self) -> list: Delegates to the backend's ``shared_components`` (see :meth:`AMICATorchNG.shared_components`): one group of - ``(model_idx, source_idx)`` pairs per shared column; empty when nothing - is shared. + ``(model_idx, source_idx)`` pairs per shared component; empty when + nothing is shared. """ self._check_usable("get the shared components") assert self.model_ is not None @@ -1096,14 +1110,16 @@ def write_amica_output(self, outdir: str) -> None: Emits the raw binary files that EEGLAB's ``loadmodout15.m`` reads (``W``, ``S``, ``gm``, ``mean``, ``c``, ``alpha``, ``mu``, ``sbeta``, ``rho``, - ``comp_list``, ``LL``), so a pamica fit drops directly into an EEGLAB - workflow (``mod = loadmodout15(outdir)``). ``loadmodout15`` applies the + ``comp_list``, ``LL``), plus the sphered-space mixing matrix ``A``, so + a pamica fit drops directly into an EEGLAB workflow + (``mod = loadmodout15(outdir)``). ``loadmodout15`` applies the variance-ordering and normalization on load, so no manual re-ordering or - sign-flipping is needed. Single-model output is byte-compatible with the - Fortran reference (issue #92). + sign-flipping is needed. Every file is in the Fortran reference's + layout for any number of models, so single-model output is + byte-compatible with the reference (issue #92). - Also writes ``LLt`` (the per-sample/per-model log-likelihood, issue - #155) for a model that was just fit in this process, taken from the + Also writes ``LLt`` (the per-sample/per-model log-likelihood, + issue #155) for a model that was just fit in this process, taken from the E-step stash (issue #157); a model restored via :meth:`load` carries no stash, so ``LLt`` is omitted for it (a warning is logged). As in the reference, ``LLt`` is the E-step that produced ``final_ll_``: after a @@ -1289,12 +1305,10 @@ def from_params_file(cls, params_file: str, **kwargs) -> "AMICA": Both formats are read through :func:`pamica.fortran_params. read_params_file` (issue #304), which also applies pamica's JSON schema's own alias spellings (``min_grad_norm``/``max_decs``/ - ``share_int``/...) to the canonical/constructor names -- so a - ``sample_params.json`` fit now applies its ``max_decs``/ - ``min_grad_norm``/``share_int`` settings, which previously matched - neither a named ``fit()`` parameter nor an ``AMICATorchNG`` keyword - under their raw JSON spelling and were only named in the "not - applied" warning rather than applied. See that function and + ``share_int``/...) to the canonical/constructor names, so a + ``sample_params.json`` fit applies its ``max_decs``/ + ``min_grad_norm``/``share_int`` settings as ``maxdecs``/``min_nd``/ + ``share_iter``. See that function and :func:`pamica.fortran_params.read_fortran_param_file` for the Fortran-side key-mapping table and the deliberately-unmapped keys they warn about rather than silently drop. diff --git a/pamica/mlx_impl/core.py b/pamica/mlx_impl/core.py index 90fd5d68..5c954632 100644 --- a/pamica/mlx_impl/core.py +++ b/pamica/mlx_impl/core.py @@ -392,9 +392,11 @@ class _UpdateStep(NamedTuple): class AMICAMLXNG: """MLX natural-gradient EM backend, full torch-equivalent surface (#76/#81, epic #278). - Parameters mirror the subset of :class:`AMICATorchNG` that is supported; - the same ``seed`` produces the same initial parameters as the PyTorch/NumPy - backends, so cross-backend equivalence is testable. + Parameters are :class:`AMICATorchNG`'s, with the same names, defaults and + validation, except ``device`` and ``dtype``: MLX runs on its default + device (the Apple GPU) in float32 only. The same ``seed`` produces the same + initial parameters as the PyTorch/NumPy backends, so cross-backend + equivalence is testable. The convergence-stop parameters (issue #248) carry AMICATorchNG's names, defaults and semantics exactly, so the two backends stop on the same @@ -414,8 +416,8 @@ class AMICAMLXNG: regardless of ``use_grad_norm``. Under the shipped defaults ``"grad_norm"`` shadows ``"grad_norm_floor"`` -- see AMICATorchNG's ``use_grad_norm`` docstring, whose precedence note applies verbatim. - ``min_nd`` is not reachable on small recordings in any backend (issue - #218); the Fortran-faithful default is kept rather than retuned. + ``min_nd`` is not reachable on small recordings in any backend + (issue #218); the Fortran-faithful default is kept rather than retuned. All three checks require two log-likelihood values, so none can fire on the first iteration (Fortran's ``if (iter > 1)``, amica15.f90:1051). @@ -465,9 +467,10 @@ class AMICAMLXNG: Precondition the ``A``/``W`` natural gradient with the approximate Hessian from iteration ``newt_start`` on (Fortran ``do_newton``: the 2x2 solve and its positive-definiteness guard at amica15.f90:1718-1741, - the ramp and fallback at :1803-1816). Natural gradient alone plateaus - short of the Fortran solution; the Newton step is what closes the gap. - OFF by default, and every accumulator it needs is gated on it, so a + the ramp and fallback at :1803-1816), which converges faster near the + optimum. OFF by default, as in the reference's compiled default (the + bundled reference ``input.param`` turns it on), and every accumulator + it needs is gated on it, so a default fit is bit-for-bit what it was before #264. ``newt_start`` (20) Iteration at which the Newton step switches on (natural gradient runs @@ -566,8 +569,8 @@ class AMICAMLXNG: ``n_restarts`` (1) Number of independent fits to run from different seeds, keeping the one with the highest ``final_ll_``. ``1`` (the default) bypasses the restart - machinery entirely, so a default fit is bit-for-bit what it was before - #198. ``n_restarts > 1`` requires a base ``seed`` (or explicit + machinery entirely, so a default fit is bit-for-bit what it was + before #198. ``n_restarts > 1`` requires a base ``seed`` (or explicit ``restart_seeds``) so the winner can be reproduced, and costs ``n_restarts`` times as long (restarts run serially). This is a pamica extension: Fortran has no search over seeds. See @@ -3458,8 +3461,8 @@ def get_pdftype(self, model_idx: int = 0) -> np.ndarray: def get_mixing_matrix(self, model_idx: int = 0) -> np.ndarray: """True mixing matrix of model ``model_idx``: the reference's ``A(:, comp_list(:, h))``, i.e. that model's component rows of the - stored ``A`` transposed (issue #24 convention; issue #334 layout; issue - #287 port of ``AMICATorchNG.get_mixing_matrix``).""" + stored ``A`` transposed (issue #24 convention; issue #334 layout; + issue #287 port of ``AMICATorchNG.get_mixing_matrix``).""" if self.A is None or self.comp_list is None: raise RuntimeError( "AMICAMLXNG.get_mixing_matrix() requires a fitted model; call " @@ -3565,8 +3568,8 @@ def get_model_center(self, model_idx: int = 0) -> np.ndarray: """Model ``model_idx``'s center ``c`` in sphered space, shape ``(n_channels,)`` (port of ``AMICATorchNG.get_model_center``). - The per-model offset :meth:`transform` subtracts after sphering (issue - #27). Identically zero for a single-model fit, since the ``c`` update + The per-model offset :meth:`transform` subtracts after sphering + (issue #27). Identically zero for a single-model fit, since the ``c`` update is gated to ``n_models > 1``. Returned as an independent float64 copy of the stored float32 values. """ @@ -3691,8 +3694,8 @@ def _pca_reduction_requested(self, n_channels: int) -> bool: """Whether the explicit ``pcakeep``/``pcadb`` asks to fit fewer than ``n_channels`` dimensions (port of ``AMICATorchNG._pca_reduction_requested``; - both delegate to :func:`pamica.rank.pca_reduction_requested`, issue - #323). + both delegate to :func:`pamica.rank.pca_reduction_requested`, + issue #323). ``n_channels`` is the channel count of the data being fitted, not ``self.n_channels``, which :meth:`_preprocess` shrinks to the kept @@ -3726,7 +3729,7 @@ def mir( """Mutual Information Reduction (issue #137) of this model's unmixing on ``X``. - Composes the full raw-data-to-sources transform ``W_fort @ sphere`` + Composes the linear part of the raw-data-to-sources transform, ``W_fort @ sphere`` -- i.e. ``get_unmixing_matrix(model_idx) @ sphere`` -- and delegates to :func:`pamica.metrics.mir`. MIR is shift-invariant, so the data-space mean/``c`` centering :meth:`transform` applies is @@ -3751,8 +3754,8 @@ def mir( Raises ------ RuntimeError - If the model is unfitted, or the fit ended degenerate (issue - #306). + If the model is unfitted, or the fit ended degenerate + (issue #306). ValueError If ``X`` is not a 2D array of the fitted input channel count, or if the fitted sphere is rank-reduced (non-square): whether from @@ -3805,8 +3808,8 @@ def pmi( Raises ------ RuntimeError - If the model is unfitted, or the fit ended degenerate (issue - #306), both via :meth:`transform`. + If the model is unfitted, or the fit ended degenerate + (issue #306), both via :meth:`transform`. ValueError If ``X`` is not a 2D array of the fitted input channel count (via :meth:`transform`). @@ -3848,8 +3851,8 @@ def model_loglik(self, X: np.ndarray) -> np.ndarray: Raises ------ RuntimeError - If the model is unfitted, or the fit ended degenerate (issue - #306). + If the model is unfitted, or the fit ended degenerate + (issue #306). ValueError If ``X`` is not a 2D array of the fitted input channel count, or contains non-finite (NaN/Inf) values. @@ -3864,8 +3867,8 @@ def model_loglik(self, X: np.ndarray) -> np.ndarray: def _model_loglik_unchecked(self, X: np.ndarray) -> np.ndarray: """Core ``Lht`` computation for :meth:`model_loglik`, with no - degenerate-fit guard or shape validation of its own (issue #306 PR - #329 review; port of ``AMICATorchNG._model_loglik_unchecked``): + degenerate-fit guard or shape validation of its own (issue #306 + PR #329 review; port of ``AMICATorchNG._model_loglik_unchecked``): :meth:`model_loglik` and :meth:`model_probability` each do their own single guard + shape check, with their own action wording, then both call this -- so the guard no longer runs twice on a @@ -3909,8 +3912,8 @@ def model_probability(self, X: np.ndarray) -> np.ndarray: Raises ------ RuntimeError - If the model is unfitted, or the fit ended degenerate (issue - #306). + If the model is unfitted, or the fit ended degenerate + (issue #306). ValueError If ``X`` is not a 2D array of the fitted input channel count, if ``X`` is non-finite, if every model underflows to ``-inf`` @@ -3954,8 +3957,8 @@ def write_amica_output(self, outdir) -> None: order. Single-model output is byte-compatible with the Fortran reference. - Also writes ``LLt`` (the per-sample/per-model log-likelihood, issue - #155) for a model that was just :meth:`fit` in this process, from the + Also writes ``LLt`` (the per-sample/per-model log-likelihood, + issue #155) for a model that was just :meth:`fit` in this process, from the stash the training E-step filled (issue #157) -- so, exactly as in the reference, ``LLt`` is the E-step of the returned iterate: one M-step older than the ``W``/``A`` written beside it after a fit that ran diff --git a/pamica/mne_compat/core.py b/pamica/mne_compat/core.py index 00e1795d..8c5c97a6 100644 --- a/pamica/mne_compat/core.py +++ b/pamica/mne_compat/core.py @@ -929,7 +929,7 @@ def get_rho(self, *, model_idx: int = 0) -> np.ndarray: def shared_components(self) -> list: """Components shared across models by ``share_comps`` (issue #60). - One group of ``(model_idx, component_idx)`` pairs per shared column; + One group of ``(model_idx, component_idx)`` pairs per shared component; empty when nothing is shared (always so for a single model or a default multi-model fit with ``share_comps`` off). """ diff --git a/pamica/numpy_impl/core.py b/pamica/numpy_impl/core.py index 0b62c0eb..8ed74311 100644 --- a/pamica/numpy_impl/core.py +++ b/pamica/numpy_impl/core.py @@ -69,7 +69,7 @@ See Also -------- -pdf : PDF implementations +pdf : Generalized Gaussian density for plotting utils : Utility functions viz : Visualization tools cli : Command-line interface @@ -547,8 +547,8 @@ def __init__( ``doscaling`` (True) and ``scalestep`` (1) carry AMICATorchNG's names, defaults, validation and semantics (issue #333): each - component's mixing vector (a row of its model's stored ``A`` - block) is rescaled to unit norm, with the matching ``mu``/``beta`` + component's mixing vector (a row of the stored ``A``, issue #334) + is rescaled to unit norm, with the matching ``mu``/``beta`` rescale, on iterations ``scalestep``, ``2*scalestep``, ... counted from 1 (see :meth:`_rescale_components`). ``scalestep`` is validated only when ``doscaling`` is on (an integer >= 1, or the @@ -588,8 +588,9 @@ def __init__( unrecognized name gets one of: the correct name, for ``n_models``/``n_mix`` (this backend's own spelling of the same setting); an explanation, for ``n_channels`` (inferred from the - data passed to :meth:`fit` on every backend, not a constructor - keyword on any of them); a name implemented on the PyTorch + data passed to :meth:`fit`, here and in the ``AMICA`` wrapper, + which builds the raw PyTorch and MLX constructors with it); a + name implemented on the PyTorch backend but not this one (e.g. ``keep_best``, ``device``, ``dtype``), pointing to ``AMICA(backend='torch')``; or a generic "unexpected keyword argument" message with a ``difflib``-based diff --git a/pamica/numpy_impl/pdf.py b/pamica/numpy_impl/pdf.py index 03b1e97c..df77c7aa 100644 --- a/pamica/numpy_impl/pdf.py +++ b/pamica/numpy_impl/pdf.py @@ -1,59 +1,34 @@ """ -Source densities for plotting a fitted model. +Source density for plotting a fitted model. This is a plotting helper, not a fit path: :func:`compute_pdf` evaluates a -fitted source density in linear space for ``viz.plot_pdf_fits``. The fit itself -uses the log-space density in ``core.py`` (``AMICA._compute_log_pdf``), and the -generalized Gaussian here draws the same density: its normalizers come from -:mod:`pamica.reference_constants`, which every backend uses, including the -reference's single-precision ``sqrt(pi)`` at ``rho == 2`` (issue #344). - -The ``pdftype`` codes below are this module's legacy numbering, not the -reference's ``pdftype``: here 1 is the generalized Gaussian (the reference's 0), -and 2, 3 and 4 are densities no backend fits. The supported PDFs are: - -1. Generalized Gaussian Distribution (GGD): - p(x) = exp(-|x|^ρ) / (2Γ(1+1/ρ)) - - ρ=1: Laplace distribution (heavy-tailed) - - ρ=2: Gaussian distribution - - Other ρ: Interpolates between Laplace and Gaussian - -2. Logistic Distribution: - p(x) = exp(x) / (1 + exp(x))^2 - - Symmetric, slightly heavier tails than Gaussian - -3. Generalized Logistic Distribution: - p(x) = ρ exp(x) / (1 + exp(x))^(ρ+1) - - Asymmetric, allows modeling skewed distributions - -4. Gaussian Mixture: - p(x) = 0.5[N(0,1) + N(0,ρ)] - - Bimodal, good for modeling multimodal data - -The choice of PDF can significantly impact separation quality. The module includes -functionality to automatically select appropriate PDFs based on data statistics. +fitted generalized Gaussian source density in linear space for +``viz.plot_pdf_fits``. The fit itself uses the log-space density in ``core.py`` +(``AMICA._compute_log_pdf``), and this module draws the same density: its +normalizers come from :mod:`pamica.reference_constants`, which every backend +uses, including the reference's single-precision ``sqrt(pi)`` at ``rho == 2`` +(issue #344). + +The generalized Gaussian with shape ``rho`` is +``p(y) = exp(-|y|^rho) / (2 Gamma(1 + 1/rho))``: ``rho == 1`` is the Laplace +density, ``rho == 2`` the Gaussian, and values between them interpolate. It is +the only source density the NumPy backend fits (``pdftype=0``). """ import numpy as np from scipy import special -from ..reference_constants import LOG2, LOG_SQRT_2PI, LOG_SQRT_PI +from ..reference_constants import LOG2, LOG_SQRT_PI -def compute_pdf( - y: np.ndarray, rho: float, pdftype: int = 1 -) -> tuple[np.ndarray, np.ndarray]: +def compute_pdf(y: np.ndarray, rho: float) -> tuple[np.ndarray, np.ndarray]: """ - Compute PDF value and its derivative for given activation values. - - For each supported PDF type, computes both the probability density p(y) - and its derivative dp(y)/dy, for plotting (see the module docstring; the - fit uses ``AMICA._compute_log_pdf``). + Generalized Gaussian density and its derivative at ``y``. - The shape parameter ρ controls the distribution's properties: - - For GGD: Controls tail heaviness (1=Laplace, 2=Gaussian) - - For Gen. Logistic: Controls asymmetry - - For Gaussian Mixture: Controls variance ratio of components + Computes the probability density p(y) and its derivative dp(y)/dy for + plotting (see the module docstring; the fit uses + ``AMICA._compute_log_pdf``). The shape ``rho`` controls tail heaviness: + 1 is the Laplace density and 2 the Gaussian. Parameters ---------- @@ -61,12 +36,6 @@ def compute_pdf( Activation values rho : float Shape parameter - pdftype : int - PDF type, in this module's legacy numbering (not the reference's): - 1: Generalized Gaussian - 2: Logistic - 3: Generalized Logistic - 4: Gaussian Mixture Returns ------- @@ -75,102 +44,27 @@ def compute_pdf( dpdf : ndarray PDF derivatives """ - if pdftype == 1: - # Generalized Gaussian - if rho == 1.0: - # Laplace distribution, the fit's exp(-|y| - log 2) - pdf = np.exp(-np.abs(y) - LOG2) - dpdf = -np.sign(y) * pdf - elif rho == 2.0: - # Gaussian distribution, with the reference's single-precision - # normalizer log(dble(1.772453851)) (amica15.f90:1313), as in the fit - pdf = np.exp(-y * y - LOG_SQRT_PI) - dpdf = -2 * y * pdf - else: - # General case: p(y) = exp(-|y|^rho) / (2 * Gamma(1 + 1/rho)). - # gamma, NOT gammaln: the Fortran reference computes this in LOG - # space (`- gamln(1+1/rho) - log(2)`, amica15.f90:1323-1324), where - # log-gamma is correct; transcribing that gamln into this - # linear-space expression divided by log(Gamma(...)) instead of - # Gamma(...). That is negative for every rho in (1, 2) -- at the - # default rho0=1.5 the "density" integrated to -8.82 -- so the - # curve was wrong for every rho outside the special-cased 1 and 2. - # The fit path was never affected: it uses core.py's own log-space - # _compute_log_pdf, which mirrors the Fortran correctly. - pdf = np.exp(-np.power(np.abs(y), rho)) / ( - 2.0 * special.gamma(1.0 + 1.0 / rho) - ) - dpdf = -rho * np.power(np.abs(y), rho - 1) * np.sign(y) * pdf - - elif pdftype == 2: - # Logistic distribution - exp_y = np.exp(y) - pdf = exp_y / np.square(1 + exp_y) - dpdf = pdf * (1 - exp_y) / (1 + exp_y) - - elif pdftype == 3: - # Generalized logistic - exp_y = np.exp(y) - pdf = rho * exp_y / np.power(1 + exp_y, rho + 1) - dpdf = pdf * (1 - (rho + 1) * exp_y / (1 + exp_y)) - - elif pdftype == 4: - # Gaussian mixture, with the log sqrt(2*pi) the Gaussian family uses - pdf1 = np.exp(-0.5 * y * y - LOG_SQRT_2PI) - pdf2 = np.exp(-0.5 * y * y / rho - LOG_SQRT_2PI - 0.5 * np.log(rho)) - pdf = 0.5 * (pdf1 + pdf2) - dpdf = -0.5 * (y * pdf1 + y * pdf2 / rho) - + if rho == 1.0: + # Laplace distribution, the fit's exp(-|y| - log 2) + pdf = np.exp(-np.abs(y) - LOG2) + dpdf = -np.sign(y) * pdf + elif rho == 2.0: + # Gaussian distribution, with the reference's single-precision + # normalizer log(dble(1.772453851)) (amica15.f90:1313), as in the fit + pdf = np.exp(-y * y - LOG_SQRT_PI) + dpdf = -2 * y * pdf else: - raise ValueError(f"Unknown PDF type: {pdftype}") + # General case: p(y) = exp(-|y|^rho) / (2 * Gamma(1 + 1/rho)). + # gamma, NOT gammaln: the Fortran reference computes this in LOG + # space (`- gamln(1+1/rho) - log(2)`, amica15.f90:1323-1324), where + # log-gamma is correct; transcribing that gamln into this + # linear-space expression divided by log(Gamma(...)) instead of + # Gamma(...). That is negative for every rho in (1, 2) -- at the + # default rho0=1.5 the "density" integrated to -8.82 -- so the + # curve was wrong for every rho outside the special-cased 1 and 2. + # The fit path was never affected: it uses core.py's own log-space + # _compute_log_pdf, which mirrors the Fortran correctly. + pdf = np.exp(-np.power(np.abs(y), rho)) / (2.0 * special.gamma(1.0 + 1.0 / rho)) + dpdf = -rho * np.power(np.abs(y), rho - 1) * np.sign(y) * pdf return pdf, dpdf - - -def choose_pdf_type(data: np.ndarray, rho: float = 1.5) -> int: - """ - Choose best PDF type based on data statistics. - - Uses higher-order statistics to characterize the data distribution: - 1. Kurtosis: Measures tail heaviness - - kurt ≈ 0: Gaussian-like - - kurt > 0: Heavier tails - - kurt < 0: Lighter tails - 2. Skewness: Measures asymmetry - - skew ≈ 0: Symmetric - - skew ≠ 0: Asymmetric - - Selection criteria: - - Near-Gaussian (low kurt & skew): Generalized Gaussian - - Heavy tails (high kurt): Logistic - - High asymmetry: Generalized Logistic - - Otherwise: Gaussian Mixture - - Parameters - ---------- - data : ndarray - Data samples - rho : float - Initial shape parameter - - Returns - ------- - pdftype : int - Selected PDF type - """ - # Compute statistics - kurt = np.mean(data**4) / np.square(np.mean(data**2)) - 3 - skew = np.mean(data**3) / np.power(np.mean(data**2), 1.5) - - if abs(kurt) < 0.5 and abs(skew) < 0.5: - # Close to Gaussian - return 1 - elif kurt > 2: - # Heavy tailed - return 2 - elif abs(skew) > 1: - # Asymmetric - return 3 - else: - # Multimodal - return 4 diff --git a/pamica/torch_impl/core.py b/pamica/torch_impl/core.py index a7de986c..ad158302 100644 --- a/pamica/torch_impl/core.py +++ b/pamica/torch_impl/core.py @@ -17,8 +17,8 @@ * ``W`` (and ``A``) are stored and mutated directly; ``W`` is recomputed from ``A`` once per iteration via a batched ``torch.linalg.inv`` (matching ``numpy_impl.utils.get_unmixing_matrices``), never via ``pinv`` in the hot path. -* ``A`` stores one component per ROW, shape ``(n_comps, n_channels)`` (issue - #334, :mod:`pamica.component_layout`, ADR 0007): row ``k`` is the reference's +* ``A`` stores one component per ROW, shape ``(n_comps, n_channels)`` + (issue #334, :mod:`pamica.component_layout`, ADR 0007): row ``k`` is the reference's column ``A(:, k)`` and model ``h``'s block is ``A[comp_list[:, h], :]``. * The E-step is vectorized over ``(model, mix, source)`` via broadcasting; the only Python loops are over models (typically 1-3) and over blocks. @@ -428,8 +428,10 @@ class AMICATorchNG: update, so the step it takes already uses the halved rate, as in the reference (amica15.f90:1056-1122, issue #339). maxdecs : int, default=5 - Number of consecutive log-likelihood decreases after which the - learning-rate *ceiling* is ratcheted down (Fortran ``maxdecs``). + Number of log-likelihood decreases after which the learning-rate + *ceiling* is ratcheted down (Fortran ``maxdecs``). The decreases need + not be consecutive: the count resets only at each ratchet and when + Newton switches on, as in the reference (amica15.f90:1062-1076). use_min_dll : bool, default=True Enable the small-likelihood-increase stop (Fortran ``use_min_dll``, amica15_header.f90:24/74; amica15.f90:1078-1090): once the per- @@ -515,11 +517,13 @@ class AMICATorchNG: Newton). do_newton : bool, default=False Enable the Newton preconditioner for the ``A``/``W`` update from - iteration ``newt_start`` on. Ported from the Fortran reference - (``amica17.f90``): natural gradient alone plateaus well short of the - Fortran solution, and the Newton step (a per-source-pair 2x2 solve - preconditioning the natural gradient by an approximate Hessian) is - what closes the gap. + iteration ``newt_start`` on, ported from the Fortran reference: a + per-source-pair 2x2 solve that preconditions the natural gradient by + an approximate Hessian, which converges faster near the optimum. An + iteration whose Hessian is not positive definite falls back to the + natural gradient (counted in ``n_newton_fallbacks``). Off by default, + as in the reference's compiled default; the bundled reference + ``input.param`` turns it on (``do_newton 1``). newt_start : int, default=20 Iteration at which the Newton step switches on (natural gradient is used before it, letting the mixture parameters settle first). Counted @@ -579,8 +583,7 @@ class AMICATorchNG: super-Gaussian (code 1) and sub-Gaussian (code 4) cosh densities by kurtosis sign. For every non-GG family the GG shape update is frozen (Fortran ``dorho=.false.``); the single-component families 1/4 (and the - adaptive mode) require ``n_mix=1``. ``pdftype=0`` is byte-for-byte the - pre-#26 implementation. + adaptive mode) require ``n_mix=1``. kurt_start, num_kurt, kurt_int : int Adaptive-switch schedule (only used when ``pdftype=1``): first iteration to re-estimate kurtosis (counted from 1), number of switch passes, and @@ -686,9 +689,13 @@ class AMICATorchNG: Explicit per-restart seeds; must have exactly ``n_restarts`` entries. When omitted the seeds are ``seed, seed + 1, ..., seed + n_restarts - 1``. device : str or torch.device, optional - Compute device for the block loop. Preprocessing (mean/cov/eigh) is - always done in float64 on CPU regardless of device, since eigh is - not reliably supported on MPS. + Compute device for the block loop. ``None`` picks MPS, then CUDA, + then CPU, whichever is available first; MPS has no float64, so with + the default ``dtype`` on an Apple machine the constructor raises + ``ValueError`` and ``device="cpu"`` must be passed (the + :class:`~pamica.AMICA` wrapper redirects an automatic MPS pick to CPU + itself). Preprocessing (mean/cov/eigh) is always done in float64 on + CPU regardless of device, since eigh is not reliably supported on MPS. dtype : torch.dtype, default=torch.float64 Parameter/computation dtype. float64 is the parity default (Fortran bit-parity) and ~4.5x on CUDA over CPU (issue #63). float32 converges on @@ -2417,8 +2424,9 @@ def fit( ``restart_lls_`` (NaN where a restart ended degenerate) and ``restart_stop_reasons_``. The winner is named in one INFO log line. - A restart that ends degenerate (``nan_ll``/``singular_ll``) is excluded - from selection but recorded. If *every* restart is degenerate the model + A restart that ends degenerate (any stop in + ``_DEGENERATE_STOP_REASONS``, including ``"restart_error"`` for a + restart that raised) is excluded from selection but recorded. If *every* restart is degenerate the model is left holding the last one, so issue #50's degenerate-fit contract applies exactly as it does to a single degenerate fit. """ @@ -3471,8 +3479,8 @@ def get_mean(self) -> np.ndarray: def get_model_center(self, model_idx: int = 0) -> np.ndarray: """Model ``model_idx``'s center ``c`` in sphered space, shape ``(n_channels,)``. - The per-model offset :meth:`transform` subtracts after sphering (issue - #27). Identically zero for a single-model fit, since the ``c`` update + The per-model offset :meth:`transform` subtracts after sphering + (issue #27). Identically zero for a single-model fit, since the ``c`` update is gated to ``n_models > 1``. Returned as an independent float64 copy whatever the fit ``dtype``. """ @@ -3529,7 +3537,7 @@ def mir( ) -> Tuple[float, float]: """Mutual Information Reduction (issue #137) of this model's unmixing on ``X``. - Composes the full raw-data-to-sources transform ``W_fort @ sphere`` -- + Composes the linear part of the raw-data-to-sources transform, ``W_fort @ sphere`` -- i.e. ``get_unmixing_matrix(model_idx) @ sphere`` -- and delegates to :func:`pamica.metrics.mir`. MIR is shift-invariant, so the data-space mean/``c`` centering ``transform`` applies is irrelevant here. @@ -3551,8 +3559,8 @@ def mir( Raises ------ RuntimeError - If the model is unfitted, or the fit ended degenerate (issue - #306). + If the model is unfitted, or the fit ended degenerate + (issue #306). ValueError If ``X`` is not a 2D array of the fitted input channel count, or if the fitted sphere is rank-reduced (non-square): whether from @@ -3604,8 +3612,8 @@ def pmi( Raises ------ RuntimeError - If the model is unfitted, or the fit ended degenerate (issue - #306), both via :meth:`transform`. + If the model is unfitted, or the fit ended degenerate + (issue #306), both via :meth:`transform`. ValueError If ``X`` is not a 2D array of the fitted input channel count (via :meth:`transform`). @@ -3645,8 +3653,8 @@ def model_loglik(self, X: np.ndarray) -> np.ndarray: Raises ------ RuntimeError - If the model is unfitted, or the fit ended degenerate (issue - #306). + If the model is unfitted, or the fit ended degenerate + (issue #306). ValueError If ``X`` is not a 2D array of the fitted input channel count, or contains non-finite (NaN/Inf) values. @@ -3661,8 +3669,8 @@ def model_loglik(self, X: np.ndarray) -> np.ndarray: def _model_loglik_unchecked(self, X: np.ndarray) -> np.ndarray: """Core ``Lht`` computation for :meth:`model_loglik`, with no - degenerate-fit guard or shape validation of its own (issue #306 PR - #329 review): :meth:`model_loglik` and :meth:`model_probability` each + degenerate-fit guard or shape validation of its own (issue #306 + PR #329 review): :meth:`model_loglik` and :meth:`model_probability` each do their own single guard + shape check, with their own action wording, then both call this -- so the guard no longer runs twice (~2x cost) on a :meth:`model_probability` call, which used to run @@ -3708,8 +3716,8 @@ def model_probability(self, X: np.ndarray) -> np.ndarray: Raises ------ RuntimeError - If the model is unfitted, or the fit ended degenerate (issue - #306). + If the model is unfitted, or the fit ended degenerate + (issue #306). ValueError If ``X`` is not a 2D array of the fitted input channel count, if ``X`` is non-finite, if every model underflows to ``-inf`` @@ -3901,8 +3909,8 @@ def write_amica_output(self, outdir) -> None: parameters are written in fit order. Single-model output is byte-compatible with the Fortran reference. - Also writes ``LLt`` (the per-sample/per-model log-likelihood, issue - #155) for a model that was just ``fit()`` in this process, from the + Also writes ``LLt`` (the per-sample/per-model log-likelihood, + issue #155) for a model that was just ``fit()`` in this process, from the stash the training E-step filled (issue #157) -- so, exactly as in the reference, ``LLt`` is the E-step of the returned iterate: one M-step older than the ``W``/``A`` written beside it after a fit that ran to diff --git a/pamica/viz.py b/pamica/viz.py index 1320ec96..75bba522 100644 --- a/pamica/viz.py +++ b/pamica/viz.py @@ -15,12 +15,12 @@ clean-room posture for GPL code. Every quantity they draw is pinned to a MATLAB oracle; see ``.context/issue-136/matlab_viz_verification.md``. -A per-component scalp-topography view is deliberately NOT here yet: deriving -source activations from a loaded ``AmicaOutput`` depends on a ``W`` convention -that turned out to be broken in the loader itself, tracked as issue #159. It -was also the only planned plot with no working upstream reference -(``pop_topohistplot`` is broken on current EEGLAB), so nothing external could -have caught a wrong activation space. It is cut rather than shipped unverified. +A per-component scalp-topography view is deliberately NOT here: it was the +only planned plot with no working upstream reference (``pop_topohistplot`` is +broken on current EEGLAB), so nothing external could catch a wrong activation +space. It is left out until it can be verified; the loader ``W`` convention it +also depended on was fixed in issue #159. The MNE wrapper's +``AMICAICA.plot_components`` draws scalp maps through MNE instead. """ from collections.abc import Sequence diff --git a/paper.md b/paper.md index 5c161804..83a7341d 100644 --- a/paper.md +++ b/paper.md @@ -31,13 +31,13 @@ bibliography: paper.bib Independent Component Analysis (ICA) separates electroencephalographic and magnetoencephalographic (EEG/MEG) recordings into maximally independent sources that isolate brain, muscle, and artifact activities for downstream analysis [@makeig1995independent; @vigario1997independent; @iversen2019megeeg]. Adaptive Mixture ICA (AMICA) [@palmer2006super; @palmer2007modeling; @palmer2012amica] models each source with a flexible, self-adjusting probability density and lets several ICA models coexist, one per segment of a recording. -Among the algorithms benchmarked by @delorme2012independent it recovers the least dependent and most dipolar decompositions, -and therefore the most physiologically interpretable ones. -Its reference implementation, by Jason Palmer, is a Fortran program distributed as a compiled binary callable from MATLAB/EEGLAB: -awkward to install, restricted to the central processing unit (CPU), and unusable from Python. +Among the algorithms benchmarked by @delorme2012independent it returned the least dependent and most dipolar decompositions, +properties that study links to physiological interpretability. +Its reference implementation, by Jason Palmer, is a Fortran program distributed as a compiled binary and usually run from MATLAB/EEGLAB; +it runs only on the central processing unit (CPU) and has no Python interface. `pamica` reproduces the reference Fortran results within numerical tolerance while running on the CPU, NVIDIA graphics processing units (GPUs, via CUDA), and Apple GPUs (through Apple's MLX array framework [@mlx2023]). -It is a complete reimplementation on PyTorch [@paszke2019pytorch], NumPy [@harris2020array], and SciPy [@virtanen2020scipy] rather than a wrapper around the binary, +It reimplements the algorithm on PyTorch [@paszke2019pytorch], NumPy [@harris2020array], and SciPy [@virtanen2020scipy], and exposes a scikit-learn-style estimator under a BSD-3-Clause license. It writes the format EEGLAB's AMICA loader reads, so established MATLAB tooling consumes its results unchanged. The software is at (archived at doi:10.5281/zenodo.21312148). @@ -45,7 +45,7 @@ The software is at (archived at doi:10.5281/zen # Statement of need AMICA decompositions are well suited to equivalent-dipole source localization and automated component classification [@piontonachini2019iclabel]. -Yet the reference implementation is MATLAB-only Fortran, an increasing obstacle as neuroimaging analysis moves toward Python, for example MNE-Python [@gramfort2013meg]: +Yet the reference implementation is used mainly through MATLAB, a growing obstacle as neuroimaging analysis moves toward Python, for example MNE-Python [@gramfort2013meg]: modern pipelines need an AMICA that runs natively in Python and on a GPU, and that is validated to reproduce the Fortran reference numerically. General-purpose Python ICA implementations do not fill this gap. @@ -54,35 +54,37 @@ while Picard [@ablin2018faster] offers faster-converging maximum-likelihood ICA; none implement AMICA's mixture of models, adaptive generalized-Gaussian densities, or Newton updates, so none can reproduce its decompositions. `pamica` is for analysts who want AMICA-quality decompositions in Python, for anyone with a GPU who wants faster runs than the CPU-only binary, and for methodologists who need a transparent reference to build on. -Validation to date is on EEG; the algorithm is modality-agnostic but MEG is untested. +The parity measurements reported here use EEG. +The algorithm itself is modality-agnostic, and an external user has fit rank-reduced, Maxwell-filtered MEG with it end to end, +but parity with the reference has not yet been measured on MEG. # State of the field -`pamica` complements rather than replaces the reference Fortran AMICA used with EEGLAB [@delorme2004eeglab]: +`pamica` is designed to work alongside the reference Fortran AMICA used with EEGLAB [@delorme2004eeglab]: it keeps the same output format, adds a Python API with GPU support, and can run the reference Fortran itself through a bundled dependency-free native build. Three other Python AMICA reimplementations have appeared as of August 2026 [@huberty2025amicapython; @esmaeili2025amica; @herforth2026pyamica], oriented toward MNE-Python; `pamica` adds a scikit-learn-style array API, output in EEGLAB's exact on-disk layout, an MLX backend, and an optional MNE-Python wrapper. -Its Fortran-parity validation goes further than theirs: score functions exact to floating-point resolution against the literal Fortran expressions, -and a distributional framework for the non-identifiable multi-model case. +Its validation includes score functions checked against the literal Fortran expressions to floating-point resolution, +and a distributional comparison for the non-identifiable multi-model case. # Software design -The governing decision was to treat numerical parity with Palmer's Fortran, rather than convergence to some independent solution, as the definition of correctness. -The rest follows from it. -Wrapping the binary would have secured parity for free but inherited its CPU-only, MATLAB-facing design; reimplementing put parity at risk. -`pamica` does both, porting the algorithm natively and also shipping the reference binary as a runnable engine, -so users can reproduce the parity claims on their own data and hardware. -For the same reason the port follows the reference's natural-gradient [@amari1998natural] expectation-maximization (EM) formulation rather than an automatic-differentiation optimizer: -an Adam/autograd backend was written early and then deleted, because it converged to different optima and made the name "AMICA" ambiguous. +The central design decision was to define correctness as numerical parity with Palmer's Fortran, +and most of the other choices follow from it. +Wrapping the binary would have secured parity but inherited its CPU-only, MATLAB-facing design, while a reimplementation must demonstrate parity. +`pamica` ports the algorithm natively and also ships the reference binary as a runnable engine, +so users can check the parity claims on their own data and hardware. +For the same reason the port follows the reference's natural-gradient [@amari1998natural] expectation-maximization (EM) formulation. +An early Adam/autograd backend was removed because it converged to different optima, which made results labeled "AMICA" hard to compare with the reference. The port covers exact-EM mixture updates, a positive-definite Newton step [@palmer2008newton], symmetric sphering, the five source-density families, a mixture of ICA models, component sharing, and the mutual-information metrics used to score separation quality [@frank2023optimal]. Three array backends (PyTorch, MLX, NumPy) implement the same algorithm behind a common estimator API, with the reference Fortran a fourth. -The duplication is deliberate: NumPy stays readable as an executable specification, -and MLX exists because it is the more efficient path on Apple GPUs: PyTorch's Metal backend trails MLX across the entire block-size sweep, -and at `pamica`'s shipped defaults trails even the CPU it runs beside (see the [documentation](https://eeglab.org/pAMICA/guides/validation/)). -Double precision is the default because parity demands it; single precision is available for Apple GPUs, which have no float64. +NumPy is kept as a readable executable specification. +MLX is included because it is the more efficient path on Apple GPUs: in the block-size sweep, PyTorch's Metal backend was slower than MLX at every size, +and at `pamica`'s shipped defaults slower than the CPU it runs beside (see the [documentation](https://eeglab.org/pAMICA/guides/validation/)). +Double precision is the default because the parity comparisons need it; single precision is available for Apple GPUs, which have no float64. # Validation @@ -131,17 +133,17 @@ The harness and sample data ship with the package, and every row of Table 1 but # Research impact statement -AMICA is not a new method: its source model and estimation were published in 2006-2008 [@palmer2006super; @palmer2007modeling; @palmer2008newton], +AMICA's source model and estimation were published in 2006-2008 [@palmer2006super; @palmer2007modeling; @palmer2008newton], and it ranked first of the twenty-two algorithms benchmarked by @delorme2012independent. MNE-Python is publicly weighing which of the Python ports to adopt (`mne-tools/mne-python` issue 13819), where an MNE core developer and the author of a competing port lean toward `pamica`, since this Center develops AMICA and maintains the reference Fortran and EEGLAB. -`sccn/pAMICA` has been public since 2021, with public issues, pull requests, seven releases (four on PyPI), and a Zenodo archive. +`sccn/pAMICA` has been public since 2021, with public issues, pull requests, nine releases (six on PyPI), and a Zenodo archive. Three researchers outside the author group have filed issues, two mid-analysis on their own data: a 236-channel, 8.3-million-sample EEG decomposition (issue 207) and Maxwell-filtered 306-channel MEG (issue 221); the third is adopting AMICA for a multiverse analysis in MNE-Python (issue 206). -Integration is underway, not complete, into this Center's Python preprocessing and into NEMAR [@delorme2022nemar], the public archive we operate: 800 datasets, ~40,000 participants, 55 TB. +Integration into this Center's Python preprocessing and into NEMAR [@delorme2022nemar], the public archive we operate (800 datasets, ~40,000 participants, 55 TB), is in progress. # AI usage disclosure @@ -154,9 +156,9 @@ The instructions given to them are public in the repository (`AGENTS.md`, `CLAUD **Human oversight.** The authors made the design decisions and take responsibility for the result. Parity as the correctness criterion, the natural-gradient EM formulation, the backend architecture, -the distributional treatment of the multi-model case, and the acceptance thresholds were chosen by the authors, not by a model. +the distributional treatment of the multi-model case, and the acceptance thresholds were chosen by the authors. Every AI-assisted change was reviewed by a human before merge and run through the parity harness, -which scores output against the reference binary on real recordings rather than generated fixtures. +which scores output against the reference binary on real recordings. The reported numbers came from running the software and were checked against their run records, as was every bibliographic entry. # Acknowledgments diff --git a/paper.pdf b/paper.pdf index 341265443be049a87d92b6bdb77050dfd9989f1d..77e8d2aeb63581ad65f3fb903bc436c87b0e86d8 100644 GIT binary patch delta 37514 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 From 24244b7c059dc80ee76d03bc33b30a3945de3b80 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 23 Sep 2026 22:31:04 +0000 Subject: [PATCH 18/26] Rebuild paper.pdf --- paper.pdf | Bin 375572 -> 375572 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/paper.pdf b/paper.pdf index 77e8d2aeb63581ad65f3fb903bc436c87b0e86d8..a0750bebd72b4f59447ca5ef880e0b9c44efa7c6 100644 GIT binary patch delta 243 zcmbR8O>D|Hv4$4L7N#xC|2}dUSs5ExnVLpTzxbY6V!PZY<_nDMP_gM>KQo)cIfh@D z&0(DF`CphN7*)(oEt7!2P}jo3!bsP|z`#`3($vIEH#yZL8OTVsOfs}+U;UMN`|7VO zqC1t0j17PmX)5UZ=BKzMmZU0ZxL6q(7?~MZ8W|gbCAU9X$9kE=&CtNy#m(Hp(8S5o p(%jh8!p+Ux%-G1y%-Gr7z}eB+(oVsKu#(tG+;ZC`cd-6u0suB5MyLP) delta 243 zcmbR8O>D|Hv4$4L7N#xC|2}dUTA7$wnHok-zxbY6V!PZY<_nDMP_gM>KQo)cIfh@D z&0(DF`CphN7*&kS%o8nuKo^LObWMOf-K12LWL*=(6hn&?^TgD|RFn4AUzxYB{>mb{ zQ_0Z81Za__g1&Elic4Zis)B}#m63swnSrH|u@P8u`=fQNmpR Date: Wed, 23 Sep 2026 22:05:31 -0700 Subject: [PATCH 19/26] Phase 17: unify the defaults across entry points (#355) * Take the wrapper's fit defaults from the backend AMICA.fit restated max_iter/lrate/do_mean/do_sphere/do_newton, and its lrate (0.05, runamica15.m's) differed from every backend's 0.1. It now reads them from the selected backend's signatures, so AMICA and AMICAICA fits without lrate run at 0.1. Tests for torch and MLX: resolved defaults, a default wrapper fit equals the default backend fit, and one set of constructor defaults across the backends. * Move the MPS/float64 device fallback into AMICATorchNG AMICATorchNG(n_channels) with default arguments raised ValueError on Apple Silicon: device=None picked MPS, which has no float64. The constructor now uses the CPU for that case and logs a warning; the wrapper and AMICA.load pass device through and drop their copy. An explicit device='mps' at float64 still raises. Torch-only (MLX has no device choice). Tested on MPS: raw construction and fit, float32 auto, explicit MPS, load with device=None. * Remove the dead pamica/data package-data entry No pamica/data directory exists. Built with uv build: the wheel's file list is unchanged, it imports from a clean venv, and numpy_impl/params.json is inside. * Document pamica's defaults and the backend device choice Add a defaults table to the differences guide: pamica against the compiled amica15 binary and EEGLAB's runamica15.m, why pamica follows the binary, and how to reproduce an EEGLAB run. Update the pages that stated the wrapper's lrate or the raw backend's MPS raise. mkdocs build --strict passes. * Add the Phase 17 changelog entries The wrapper lrate change joins the 'Default fits differ from 0.3.3' warning; entries for the backend device fallback and the package-data cleanup. * Give AMICA_NumPy the shared default settings params.json set do_newton on and max_iter 2000, the only shared settings where NumPy differed from AMICATorchNG; it now sets them off and 100, as does the constructor's max_iter fallback (reached by the CLI). Fits shorter than newt_start are byte-identical. New test_default_settings.py holds NumPy and MLX to every shared AMICATorchNG default (the MLX check moves there from test_wrapper_backends.py). * Correct the degenerate-fit test's measured stop The comment said torch stops on nan_ll at iteration 3; both backends stop on nan_params with iteration 2 in all 16 measured runs, at either lrate. The test asserts only a degenerate stop. * Document the NumPy backend's shared defaults Defaults section, validation guide's stops table, NumPy API page, and a changelog entry (NumPy joins the opening warning's defaults line). * Give AMICANative pamica's shared defaults The engine wrote the bundled input.param's values (lrate 0.05, Newton on, max_iter 2000, block_size 512, ...). It now takes every setting the binary has a keyword for from AMICATorchNG's signature through FORTRAN_TO_PAMICA_KEY; binary-only knobs are unchanged. The default block is pamica's 8192 samples split over max_threads and capped at the data, since the binary's block_size is per thread and a block longer than the data gives an all-NaN fit (#292). The native oracles now start from the bundled input.param itself (byte-identical files). Tested with the v0.3.3 binary: native, oracle and default-settings tests pass. * Document AMICANative's shared defaults AMICANative joins the defaults table's pamica column; the section and the native API page cover the settings it cannot write, the per-thread block size, and running the binary as an input.param configures it. The changelog records the behavior change. * Say where AMICANative's default pcakeep comes from --- README.md | 9 +- docs/api/amica.md | 5 + docs/api/mne-compat.md | 3 +- docs/api/native-backend.md | 16 ++ docs/api/numpy-backend.md | 6 + docs/api/torch-backend.md | 7 + docs/changelog.md | 84 +++++++++ docs/getting-started.md | 8 +- docs/guides/amica-differences.md | 130 ++++++++++++- docs/guides/backends.md | 8 +- docs/guides/validation.md | 23 +-- pamica/amica.py | 117 ++++++------ pamica/native/engine.py | 171 ++++++++++------- pamica/numpy_impl/cli.py | 2 +- pamica/numpy_impl/core.py | 2 +- pamica/numpy_impl/params.json | 4 +- .../mne_tests/test_end_to_end_workflow.py | 3 +- pamica/tests/mne_tests/test_mne_backends.py | 19 +- pamica/tests/native_oracle.py | 39 +++- pamica/tests/test_default_settings.py | 178 ++++++++++++++++++ pamica/tests/test_llt_stash.py | 3 +- pamica/tests/test_native_engine.py | 15 +- pamica/tests/test_pamica.py | 5 +- pamica/tests/test_schedule_native_oracle.py | 7 +- pamica/tests/test_wrapper_backends.py | 61 +++++- .../torch_tests/test_amica_ng_wrapper.py | 89 ++++++++- pamica/torch_impl/core.py | 23 ++- pyproject.toml | 2 +- validate_implementations.py | 4 +- 29 files changed, 857 insertions(+), 186 deletions(-) create mode 100644 pamica/tests/test_default_settings.py diff --git a/README.md b/README.md index 4dc74491..a722e54a 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,8 @@ order = model.variance_order() # EEGLAB IC order (IC1 = highest variance) ### Backends and precision -The wrapper auto-selects a device and computes in float64 for Fortran parity. +The wrapper auto-selects a device and computes in float64 for Fortran parity; +on a Mac, where the Metal Performance Shaders (MPS) device has no float64, a default fit runs on the CPU. - CPU and CUDA (float64) are bit-reproducible; use them for parity runs. - float32 (about 7 significant digits, not parity) is required on the Apple GPUs @@ -94,6 +95,12 @@ model.write_amica_output("amicaout") # gm, W, S, mean, c, alpha, mu, sbeta, rh mod = loadmodout15('amicaout'); % components in EEGLAB variance order ``` +pamica's defaults follow the compiled amica15 binary (`lrate` 0.1, Newton off), +and EEGLAB's `runamica15.m` sets its own (`lrate` 0.05, Newton on). +To rerun an EEGLAB decomposition, fit from the `input.param` that `runamica15.m` wrote beside its output: +`AMICA.from_params_file("amicaouttmp/input.param").fit(X)`. +The [defaults table](https://eeglab.org/pAMICA/guides/amica-differences/#default-settings-issue-354) lists every setting in the three sources. + ### Legacy NumPy CLI The NumPy reference backend keeps a command-line interface, driven by a diff --git a/docs/api/amica.md b/docs/api/amica.md index c4c22ce1..3a71ee93 100644 --- a/docs/api/amica.md +++ b/docs/api/amica.md @@ -18,6 +18,11 @@ model = AMICA.load("model.pt") # restored on the MLX backend `device` and a `dtype` fit keyword apply to the PyTorch backend only and raise `ValueError` with `backend="mlx"`; `backend="mlx"` without MLX installed raises `ImportError`. +`fit`'s `max_iter`, `lrate`, `do_mean`, `do_sphere` and `do_newton` default to the selected backend's own values (issue #354), +so `AMICA().fit(X)` runs the same fit as the backend class with its defaults: +`lrate=0.1`, the compiled amica15 default, where EEGLAB's `runamica15.m` uses 0.05. +[Default settings](../guides/amica-differences.md#default-settings-issue-354) compares every default with the compiled binary's and EEGLAB's. + `get_sphere()`, `get_mean()` and `get_model_center(model_idx)` return the fitted preprocessing as float64 arrays on either backend, so the transform can be composed by hand: `transform(X)` is `W @ (sphere @ (X - mean[:, None]) - c[:, None])` with `W = get_unmixing_matrix()`. diff --git a/docs/api/mne-compat.md b/docs/api/mne-compat.md index 87e3c0fb..8c8a04df 100644 --- a/docs/api/mne-compat.md +++ b/docs/api/mne-compat.md @@ -31,7 +31,8 @@ clean = ica.apply(raw.copy(), exclude=[0, 3]) `fit` accepts a `Raw` or `Epochs` (epochs are concatenated along time, as MNE's own ICA does), any MNE `picks` selector, and forwards remaining keywords -(`max_iter`, `lrate`, `do_newton`, ...) to [`AMICA.fit`](amica.md). It rejects +(`max_iter`, `lrate`, `do_newton`, ...) to [`AMICA.fit`](amica.md), +so a setting left out takes the backend's default, as with `AMICA` (for example `lrate=0.1`). It rejects non-finite input, supports principal component analysis (PCA) reduction (`pcakeep`/`pcadb`) and rank-deficient data (see [Rank-reduced fits and the PCA residual](#rank-reduced-fits-and-the-pca-residual)), diff --git a/docs/api/native-backend.md b/docs/api/native-backend.md index 07fa49a9..35c42b17 100644 --- a/docs/api/native-backend.md +++ b/docs/api/native-backend.md @@ -31,6 +31,22 @@ any Fortran `input.param` field (`max_iter`, `lrate`, `pdftype`, `do_newton`, ...) can be passed as a keyword. A collapsed fit (non-finite weights) is raised as a clear degenerate-fit error rather than an opaque SVD failure. +The `input.param` it writes carries pamica's shared defaults (issue #354), +the values the PyTorch, MLX and NumPy backends default to, under the binary's keywords: +`lrate` 0.1, Newton off, `max_iter` 100, `do_opt_block` off, `pdftype` 0, and so on +([Default settings](../guides/amica-differences.md#default-settings-issue-354) has the full table). +The binary's `block_size` counts one thread's share of a block, and it processes no block at all, +writing an all-NaN fit, when `max_threads * block_size` exceeds the samples (issue #292). +Unless you pass `block_size`, the engine writes pamica's 8192-sample block, capped at the data's length, divided by `max_threads` (10 unless given), +so a default run keeps pamica's block and never meets that case; a `block_size` you pass is written as is. +pamica settings the binary has no keyword for, such as `keep_best` and `mineig_rel`, are not written, +so the binary returns its last iterate and applies the absolute `mineig` floor. +Keys only the binary reads (`max_threads`, `byte_size`, `writestep`, the `load_*` and `update_*` switches) keep the bundled `input.param`'s values. +Before issue #354 the engine wrote the bundled `pamica/sample_data/input.param`'s settings instead +(`lrate` 0.05, Newton on from iteration 50, `max_iter` 2000, `block_size` 512). +To run as that file or an EEGLAB-written one configures the binary, pass the file's settings as keywords +([Reproducing an EEGLAB run](../guides/amica-differences.md#reproducing-an-eeglab-run) shows how). + ## Binary resolution and caching On the first run for a given host, the engine downloads the matching release diff --git a/docs/api/numpy-backend.md b/docs/api/numpy-backend.md index 5705d3d4..a7cab1ef 100644 --- a/docs/api/numpy-backend.md +++ b/docs/api/numpy-backend.md @@ -15,6 +15,12 @@ the same shared reader. Settings the file carries that this backend does not consume are named in one `logger.warning` rather than silently dropped. +Its defaults come from the bundled `pamica/numpy_impl/params.json`, read by the constructor, +and match the PyTorch and MLX backends' ([Default settings](../guides/amica-differences.md#default-settings-issue-354)): +since issue #354 a default fit runs without Newton and stops at `max_iter=100`, +where it used to turn Newton on and run 2000 iterations. +A `params_file` replaces the bundled file, and a keyword argument overrides either. + It writes files only when given an `outdir` (keyword, params file, or the CLI's `--outdir`, default `output`): there it writes its `out.txt` log, its `writestep` checkpoints and its final results in the Fortran `amicaout` layout. Without one (the default) a fit writes nothing, like the PyTorch and MLX backends. diff --git a/docs/api/torch-backend.md b/docs/api/torch-backend.md index d39ea878..035df28a 100644 --- a/docs/api/torch-backend.md +++ b/docs/api/torch-backend.md @@ -5,4 +5,11 @@ mixture updates, symmetric-ZCA sphere, Jacobian log-likelihood). The [`AMICA`](amica.md) interface delegates to this class; use it directly for lower-level control. +Its defaults are the ones the wrapper uses +(compared with the compiled binary's and EEGLAB's in [Default settings](../guides/amica-differences.md#default-settings-issue-354)). +With `device=None` on a Mac, the constructor runs a float64 model on the CPU, because Metal Performance Shaders (MPS) cannot represent float64, +and logs a warning, +so `AMICATorchNG(n_channels)` with default arguments runs on Apple Silicon (issue #354). +An explicit `device="mps"` needs `dtype=torch.float32` and raises `ValueError` without it. + ::: pamica.AMICATorchNG diff --git a/docs/changelog.md b/docs/changelog.md index c6bb3253..aba5f488 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -18,6 +18,15 @@ and every backend's fitting follows the Fortran reference more closely. each iteration runs in the reference's order, so a likelihood decrease takes effect in the same iteration and a convergence stop returns the parameters its likelihood was computed from (issue #339); the reference's A-freeze holds the mixing update on iterations 100-105, 200-205, and so on, of every fit (issue #345); and the density normalizers are the reference's single-precision constants (issue #344). + Every pamica entry point now also shares one set of defaults (issue #354, [Defaults and device selection](#defaults-and-device-selection)), + which moves default fits in three of them: + an `AMICA()` or `AMICAICA()` fit that sets no `lrate` now runs at 0.1, the backends' default, where it ran at 0.05, + which raises the final log-likelihood of a default 100-iteration fit on the bundled sample by 0.008; + a default `AMICA_NumPy` fit now runs without Newton and stops at 100 iterations, like the other backends, + where it switched Newton on at iteration 20 and ran up to 2000 iterations; + and a default `AMICANative` run gives the binary these shared settings, where it gave it the bundled `input.param`'s + (`lrate` 0.05, Newton on, `max_iter` 2000, `block_size` 512). + The raw PyTorch and MLX backends already used these values. Two more reach default fits in narrow cases. With schedule gates counted from 1 (issue #335), a `maxdecs` ratchet that completes on iteration `newt_start + 1` tightens the rho-rate ceiling whether or not Newton is on, and on NumPy a non-finite likelihood on iteration `restartiter + 1` ends the fit. @@ -457,6 +466,78 @@ and every backend's fitting follows the Fortran reference more closely. validated on real data against a float64 `AMICATorchNG` twin holding identical fitted parameters (the component order matches exactly on a configuration with non-degenerate variance gaps). +### Defaults and device selection + +- **`AMICA` and `AMICAICA` take their `fit` defaults from the backend** (issue #354, epic #324 Phase 17). + **Behavior change: an `AMICA()` or `AMICAICA()` fit that sets no `lrate` now runs at 0.1, where it ran at 0.05.** + The wrapper's 0.05 was the value of EEGLAB's `runamica15.m`, + while `AMICATorchNG`, `AMICAMLXNG` and `AMICA_NumPy` default to 0.1, the compiled amica15 default (amica15_header.f90:68), + so `AMICA().fit(X)` and `AMICATorchNG(n_channels).fit(X)` ran at different learning rates. + `AMICA.fit` now reads the defaults of `max_iter`, `lrate`, `do_mean`, `do_sphere` and `do_newton` from the selected backend's signatures, + so the wrapper and the backend cannot drift apart again; + the other four already agreed, so only `lrate` moves. + `AMICAICA` forwards its keywords to `AMICA.fit` and has no default of its own, so it follows. + An explicit `lrate`, or one from a parameter file (both bundled files set 0.05), is unaffected. + On the bundled sample (PyTorch, seed 42, the default 100 iterations), a default wrapper fit now ends at log-likelihood -3.42768, where it ended at -3.43566, + and its sources match the earlier fit's with a mean Hungarian-matched correlation of 0.983 (minimum 0.931). + Pass `lrate=0.05` to reproduce an earlier default wrapper fit. + The differences guide gains a [table of the defaults](guides/amica-differences.md#default-settings-issue-354) of pamica, the compiled binary and `runamica15.m`, + and says how to reproduce an EEGLAB run from the `input.param` it wrote. + - Tests: `pamica/tests/test_wrapper_backends.py` checks on both backends that the wrapper resolves the backend's own defaults, + that a default wrapper fit is the default backend fit (the same learning rate and log-likelihood trajectory), + and that an explicit `lrate` still takes precedence; + a cross-backend test (now in `pamica/tests/test_default_settings.py`) holds every constructor default that `AMICATorchNG` and `AMICAMLXNG` share equal, + since the defaults table gives one pamica column for them. + `pamica/tests/mne_tests/test_mne_backends.py` checks that an `AMICAICA` fit runs at the backend's default. + No existing test depended on the old default; + the figures quoted in two torch-against-MLX test docstrings were measured again at the new one. +- **`AMICA_NumPy` defaults to the other backends' settings** (issue #354). + **Behavior change: a default NumPy fit now runs without Newton and stops at 100 iterations, like the PyTorch and MLX backends.** + The bundled `pamica/numpy_impl/params.json`, which the NumPy constructor reads for its defaults, set `do_newton` on and `max_iter` to 2000, + the only two settings it shares with `AMICATorchNG` on which the two disagreed; + it now sets `do_newton` off and `max_iter` to 100, + and the constructor's fallback for a parameter file without `max_iter` is 100 too (it was 2000), which also reaches the NumPy CLI. + A fit that sets these, directly or through its own parameter file, is unaffected; + pass `do_newton=True` and `max_iter=2000` to reproduce an earlier default NumPy fit. + Fits shorter than `newt_start` (20 by default) iterations are byte-identical, since Newton had not started in them. + - Tests: `pamica/tests/test_default_settings.py` holds every setting `AMICA_NumPy` shares with `AMICATorchNG` to `AMICATorchNG`'s default, + read through a default NumPy construction and through the backend's own `params.json` loader, + and holds `AMICAMLXNG`'s constructor and `fit` defaults to `AMICATorchNG`'s (moved from `test_wrapper_backends.py`). + `pamica/tests/test_pamica.py::test_amica_initialization`, which pinned the old `max_iter`, now expects the shared default; + no other test relied on the old values, since every NumPy fit that runs past `newt_start` in the suite sets `do_newton` itself. +- **`AMICANative` gives the binary pamica's shared defaults** (issue #354). + **Behavior change: a native run that does not set them now runs at `lrate` 0.1, without Newton, for at most 100 iterations.** + The engine wrote the bundled `pamica/sample_data/input.param`'s settings as its defaults: + `lrate` 0.05, `minlrate` 1e-8, `maxdecs` 3, Newton on from iteration 50 at `newtrate` 1.0, `max_iter` 2000, `block_size` 512, + `rholratefact` 0.5, `invsigmin` 0, `invsigmax` 100, `mineig` 1e-12, `numrej` 3 and `pcadb` 30. + It now takes the default of every setting the binary has a keyword for from `AMICATorchNG`'s signature, through the shared keyword table of `pamica.fortran_params`, + so the four entry points cannot drift apart. + pamica settings without a binary keyword, such as `keep_best` and `mineig_rel`, are not written, and neither are settings whose default is `None` (`pcadb`, `seed`). + The binary's `block_size` counts one thread's share of a block and leaves an all-NaN fit when `max_threads * block_size` exceeds the samples (issue #292), + so unless `block_size` is given the engine writes pamica's 8192-sample block, capped at the data's length, divided by `max_threads` + (819 with the default 10 threads on the bundled sample, where 8192 as is would process nothing). + Keys only the binary reads (`max_threads`, `byte_size`, `writestep`, the `load_*` and `update_*` switches) keep their earlier values. + To run as the bundled or an EEGLAB-written `input.param` configures the binary, pass the file's settings as keywords + ([Reproducing an EEGLAB run](guides/amica-differences.md#reproducing-an-eeglab-run)). + - Tests: `pamica/tests/test_default_settings.py` builds the engine's `input.param` without a binary, reads it back through `pamica.fortran_params.read_params_file`, + and holds every setting it carries to `AMICATorchNG`'s default and every writable setting to being written. + The native-binary oracles (`pamica/tests/native_oracle.py` and `test_schedule_native_oracle.py`) relied on the old defaults; + they now start from the bundled `input.param` itself, the configuration they were measured against, and write byte-identical parameter files. + `test_native_engine.py::test_native_engine_degenerate_fit_raises_clearly` took its NaN weights from the binary's zero-block case at the old `block_size` 512 on 2048 samples, + which the new default avoids, so it pins `block_size=512`; the other native-engine tests pass at the new defaults. +- **The raw `AMICATorchNG` runs a default construction on the CPU on Apple Silicon** (issue #354). + `AMICATorchNG(n_channels)` with default arguments raised `ValueError` on every Mac with Metal Performance Shaders (MPS): + automatic device selection picks MPS, which cannot represent the float64 default. + When `device=None` picks MPS for a float64 model, the constructor now uses the CPU and logs a warning, as the `AMICA` wrapper did. + The wrappers and `AMICA.load` now pass `device` through and rely on the backend, and the wrapper's own copy of the fallback is removed. + An explicit `device="mps"` at float64 still raises `ValueError`, and `device=None` with `dtype=torch.float32` still picks MPS. + The warning now comes from the `pamica.torch_impl.core` logger, and the wrapper no longer also prints it when `verbose=True`. + The MLX backend has no device choice of this kind, so the change is PyTorch-only. + - Tests: `pamica/tests/torch_tests/test_amica_ng_wrapper.py` constructs and fits the raw backend with default arguments, + checks that `dtype=torch.float32` keeps MPS and that an explicit `device="mps"` at float64 raises, + and loads a saved model with `device=None`. + The tests that need MPS skip without it; the macOS CI runner has it. + ### MNE wrapper - **`AMICAICA.apply` restores the PCA residual of rank-reduced fits** (issue #322). @@ -541,6 +622,9 @@ and every backend's fitting follows the Fortran reference more closely. ### Removed +- **The `package-data` entry for a `pamica/data` directory** (issue #354). + `pyproject.toml` listed `"pamica" = ["data/*"]`, and no such directory exists, so the entry matched nothing. + The built wheel holds the same files as before, `pamica/numpy_impl/params.json` included. - **Dead legacy code in `pamica.numpy_impl.pdf`** (issue #352). `choose_pdf_type`, which nothing called, is removed, and `compute_pdf` loses its `pdftype` argument and the branches for three legacy densities no backend fits: diff --git a/docs/getting-started.md b/docs/getting-started.md index ac4644a8..dfe41931 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -78,8 +78,9 @@ print("final log-likelihood:", amica.final_ll_) `AMICA` auto-selects a device. Because the backend computes in float64 for Fortran parity and Apple's Metal Performance Shaders (MPS) device cannot represent float64, -an auto-selected MPS device is redirected to the central processing unit (CPU); -pass `device="mps"` with `dtype=torch.float32` to run on MPS explicitly. +an auto-selected MPS device is redirected to the central processing unit (CPU), with a logged warning; +the PyTorch backend does this itself, so the raw `AMICATorchNG` chooses the same way. +Pass `device="mps"` with `dtype=torch.float32` to run on MPS explicitly. On Apple Silicon the MLX backend below is faster than MPS. See [Backends & Devices](guides/backends.md) for the full matrix and performance guidance. @@ -116,5 +117,6 @@ and the measured precision ([Precision on the MLX backend](guides/backends.md#pr - [Backends & Devices](guides/backends.md): CUDA / CPU / MLX and float32 vs float64. - [Validation & Parity](guides/validation.md): comparing against the Fortran reference. -- [pamica vs. AMICA](guides/amica-differences.md): every deliberate difference from the reference. +- [pamica vs. AMICA](guides/amica-differences.md): every deliberate difference from the reference, + including pamica's [default settings](guides/amica-differences.md#default-settings-issue-354) beside the compiled binary's and EEGLAB's. - [API Reference](api/index.md): full parameter and method documentation. diff --git a/docs/guides/amica-differences.md b/docs/guides/amica-differences.md index 08cfdd20..c894403d 100644 --- a/docs/guides/amica-differences.md +++ b/docs/guides/amica-differences.md @@ -28,6 +28,7 @@ that is not listed, that is a bug worth | 14 | `scalestep` | parsed (amica15.f90:3686), never used; rescales every iteration (:1843) | rescales every `scalestep` iterations counted from 1; default 1 matches the reference | pamica has always honored the keyword, and keeping it costs nothing; since issue #333 it counts from 1 like the reference's live cadences (`writestep`, `histstep`) instead of firing on the first iteration | `scalestep=1`, the default | | 15 | Restart after a non-finite likelihood (NumPy backend only) | within the first `restartiter` iterations, redraws `A` up to `maxrestarts + 1` times (`numrestarts > maxrestarts` ends the run, amica15.f90:1022-1049); `startover` is set at :1046 and never cleared, so after the first restart the binary never calls `update_params` again (:1115-1122) and the redrawn parameters are never fitted | redraws `A` up to `maxrestarts` times within the same window (counted from 1; `restartiter=0` disables it) and resumes fitting after each restart, with the learning rates and likelihood history reset; the restart uses up that iteration of `max_iter` | the reference's recovery path cannot resume, so its count has nothing to recover into; a restart that fits is the evident intent. PyTorch and MLX have no restart-on-NaN path at all: they stop on the non-finite likelihood (`stop_reason="nan_ll"`), and their `n_restarts` (row 10) is a different mechanism | none for the stalled loop; `maxrestarts` one higher reproduces the reference's count, and `restartiter=0` stops on the first non-finite likelihood, as the reference does with `restartiter=0` | | 16 | Defaults of `input.param` keys | the compiled-in defaults are single-precision literals widened to double (amica15_header.f90:66-74), so a key missing from `input.param` takes the float32 rounding of its default: `comp_thresh` is 0.9900000095, and likewise `mineig`, `minlrate`, `rholrate`, `rholratefact`, `invsigmin`, `min_dll`, `min_grad_norm` and `lrate` (0.1000000015, printed by the binary; without the key its ceiling `lrate0` is never set, and the binary's `lrate` drops to 0 after the first iteration); a value given in `input.param` is read as double | the decimal values (`lrate=0.1`), given or defaulted | they are user inputs, and the reference reads any value it is given as double; pamica's native engine and the seeded oracles write every one of these keys, so they never meet the compiled-in values. The constants the reference hard-codes the same way are its values in every backend since issue #344 ([below](#single-precision-constants-issue-344)) | pass the float32 rounding, for example `lrate=float(numpy.float32(0.1))` | +| 17 | Default settings | the compiled defaults (amica15_header.f90); EEGLAB's `runamica15.m` writes its own choices into `input.param` | the compiled defaults, except `pdftype=0`, `block_size=8192` and `max_iter=100` (and the settings of rows 1, 3 and 9) | the binary falls back to its compiled values for any key an `input.param` leaves out; the three exceptions are explained in [Default settings](#default-settings-issue-354) | fit from the run's `input.param` with `AMICA.from_params_file`, or pass its values ([Reproducing an EEGLAB run](#reproducing-an-eeglab-run)) | Rows 1, 2 and 7 arrived with [ADR 0004](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0004-rank-deficient-input-handling.md); row 3 with ADR 0003; row 5 with issue #50, extended to the raw backends by issue #306 and to non-finite steps and updates by issue #339; row 8 with issues #60, #240 and #334; @@ -35,7 +36,8 @@ row 9 with issue #232; row 10 with issue #198; row 11 with issue #322 (ADR 0005) row 12 with issue #323; row 13 is recorded, not yet resolved, by issue #328; row 14 with issue #333 (ADR 0006); row 15 with issue #335, which also made the NumPy restart window count from 1; -row 16 with issue #344, which adopted the reference's hard-coded single-precision constants. +row 16 with issue #344, which adopted the reference's hard-coded single-precision constants; +row 17 with issue #354, which made the wrappers take the backends' defaults. The A-freeze is the reference's arithmetic, applied as it is (issue #345): once `iter >= share_start`, every iteration with `mod(iter, share_iter) <= 5` holds the `A` update, @@ -60,6 +62,132 @@ the reference's `load_comp_list` seeds a merged `comp_list`, and the PyTorch and NumPy updates from such a state match the native binary to float64 round-off ([below](#component-sharing-compares-and-ties-components-issue-334)). +## Default settings (issue #354) + +Three sources set AMICA's defaults: +pamica's own, the compiled amica15 binary's (`pamica/amica15_header.f90`), +and those of EEGLAB's `runamica15.m`, the MATLAB front end that writes an `input.param` and runs the binary +(read here from sccn/amica at commit 509c8be). +pamica's column holds for every pamica entry point since issue #354: +`AMICATorchNG` and `AMICAMLXNG`; +the `AMICA` and `AMICAICA` wrappers on either backend, which take their defaults from the backend they build; +the legacy NumPy backend (`AMICA_NumPy`), which reads them from `pamica/numpy_impl/params.json` +(before issue #354 that file turned Newton on and set `max_iter` to 2000); +and [`AMICANative`](../api/native-backend.md), which runs the binary itself and writes them into the `input.param` it gives it +(before issue #354 it wrote the bundled `pamica/sample_data/input.param`'s values). +`AMICA_NumPy` has no `keep_best` ([Backend differences](#backend-differences)). +`AMICANative` writes every setting the binary has a keyword for; +the binary has none for `keep_best` or `mineig_rel`, +so a native run returns the last iterate and applies the absolute `mineig` floor, as the reference does (rows 1 and 3). +The binary's `block_size` counts one thread's share of a block (amica15.f90:1215-1227), +and a block of `max_threads * block_size` samples longer than the data leaves it no block to process and an all-NaN fit (issue #292), +so `AMICANative` writes pamica's 8192-sample block, capped at the data's length, divided by `max_threads`: +819 with its 10 threads on a recording of 8192 samples or more. +Bold marks a pamica default that differs from the compiled one. +The line numbers are those of `amica15_header.f90` and `runamica15.m`. + +| Setting (`input.param` key) | pamica | Compiled amica15 | EEGLAB `runamica15.m` | +|---|---|---|---| +| `lrate` | 0.1 | 0.1 (:68) | 0.05 (:163) | +| `minlrate` | 1e-12 | 1e-12 (:68) | 1e-8 (:164) | +| `lratefact` | 0.5 | 0.5 (:68) | 0.5 (:165) | +| `maxdecs` (`max_decs`) | 5 | 5 (:111) | 3 (:197) | +| `do_newton` | off | off (:19) | on (:177) | +| `newt_start` | 20 | 20 (:87) | 50 (:178) | +| `newtrate` | 0.5 | 0.5 (:73) | 1.0 (:179) | +| `newt_ramp` | 10 | 10 (:87) | 10 (:180) | +| `pdftype` | **0** | 1 (:93) | 0 (:150) | +| `block_size` | **8192** | 128 (:81) | 128 (:142) | +| `do_opt_block` | **off** (row 9) | on (:20), over 128-1024 in steps of 128 (:76-77) | off (:143) | +| `max_iter` | **100** | none: declared without a value (:93), so `input.param` must set it | 2000 (:151) | +| `n_mix` (`num_mix_comps`) | 3 | 3 (:76) | 3 (:149) | +| `rholrate` | 0.05 | 0.05 (:68) | 0.05 (:167) | +| `rholratefact` | 0.1 | 0.1 (:70) | 0.5 (:171) | +| `invsigmin` | 1e-4 | 1e-4 (:72) | 1e-8 (:206), which it writes with `%f` as `0.000000` (:845) | +| `invsigmax` | 1000 | 1000 (:69) | 100 (:205) | +| `mineig` | 1e-15, with the relative floor `mineig_rel=1e-12` taking precedence (row 1) | 1e-15 (:66) | 1e-12 (:153) | +| `do_mean` | on | on (:14) | on (:220) | +| `do_sphere` | on | on (:14) | on (:221) | +| `doscaling` | on | on (:14) | on (:224) | +| `share_comps` | off | off (:18) | off (:158) | +| `share_start` | 100 | 100 (:76) | 100 (:160) | +| `share_iter` | 100 | 100 (:76) | 100 (:161, as `share_int`) | +| `comp_thresh` | 0.99 | 0.99 (:74) | 0.99 (:159) | +| `do_reject` | off | off (:20) | off (:182) | +| `use_min_dll` | on | on (:24) | on (:188) | +| `min_dll` | 1e-9 | 1e-9 (:74) | 1e-9 (:189) | +| `use_grad_norm` | on | on (:24) | on (:190) | +| `min_nd` (`min_grad_norm`) | 1e-7 | 1e-7 (:74) | 1e-7 (:191) | +| `keep_best` | on (row 3) | no such setting | no such setting | + +The compiled values are single-precision literals widened to double (row 16), so the binary's `lrate` default is 0.1000000015; +the table gives the decimals. +`runamica15.m` writes `invsigmin` with six decimal places, so the binary it runs sees `invsigmin 0.000000`, +as the bundled `pamica/sample_data/input.param` shows. + +pamica follows the compiled binary because its values are the reference's own defaults: +the binary falls back to them for any key an `input.param` leaves out, whichever front end wrote the file. +`runamica15.m` is EEGLAB's front end, and it writes its own choices for the keys it knows. +Besides `do_opt_block` (row 9), three defaults depart from the compiled ones: + +- **`pdftype=0`**, the generalized Gaussian mixture, as in `runamica15.m`. + The compiled `pdftype=1` is the extended-Infomax switcher between two single-component densities, + which reads only the first mixture component (pamica requires `n_mix=1` for it), + while all three sources default to three mixture components. +- **`block_size=8192`**, for throughput: + every backend is dispatch-bound at small blocks ([Block-size sensitivity](validation.md#block-size-sensitivity)). + The trajectory moves by about 1e-6 with `block_size`, + so a run compared bit for bit with the binary sets the same `block_size` on both sides. +- **`max_iter=100`**, where the compiled binary has no default and `runamica15.m` uses 2000. + On a recording the size of the bundled sample, a default fit therefore ends on `max_iter` before the likelihood stop can fire + ([Which convergence criterion actually stops a fit](validation.md#which-convergence-criterion-actually-stops-a-fit)). + +Rows 1 and 3 add two settings of pamica's own, the relative rank floor `mineig_rel` and the best-iterate restore `keep_best`. + +### Reproducing an EEGLAB run + +`runamica15.m` writes the settings of each run into `input.param` in its output directory, +so the file records what the binary was given, `invsigmin 0.000000` included; +the bundled `pamica/sample_data/input.param` is one such file. +Fit from it with [`AMICA.from_params_file`](../api/amica.md): + +```python +from pamica import AMICA + +# X: the data the EEGLAB run fitted, shape (n_channels, n_samples) +model = AMICA.from_params_file("/path/to/amicaouttmp/input.param") +model.fit(X) +``` + +`fit` applies every setting the file carries, and a keyword passed to `fit` still takes precedence. +Without the file, pass `runamica15.m`'s values from the table as `fit` keywords, for example +`fit(X, lrate=0.05, minlrate=1e-8, maxdecs=3, do_newton=True, newt_start=50, newtrate=1.0, rholratefact=0.5, invsigmin=0.0, invsigmax=100.0, mineig=1e-12, block_size=128, max_iter=2000)`. +Either way, the pamica-only settings keep their defaults unless you set them: +pass `mineig_rel=None` for the file's absolute `mineig` floor (row 1) +and `keep_best=False` to return the last iterate, as the binary does (row 3). +pamica draws its own random initialization (the reference's generator is gfortran's), +so a run with the same settings agrees with the EEGLAB run to the tolerances in [Single-model parity](validation.md#single-model-parity) +and does not reproduce it bit for bit; +with Newton on, the weakest components can settle in a different optimum +([Newton-enabled runs](validation.md#newton-enabled-runs-and-the-initialization-basin)). + +To run the reference binary itself as such a file configures it, +pass the file's settings to [`AMICANative`](../api/native-backend.md), which forwards any `input.param` keyword; +its own defaults are pamica's column, so a keyword the file leaves out takes pamica's value, where the binary run by `runamica15.m` would take its compiled one: + +```python +from pathlib import Path + +from pamica import AMICANative + +settings = {} +for line in Path("/path/to/amicaouttmp/input.param").read_text().splitlines(): + key, _, value = line.partition(" ") + if key and key not in ("files", "outdir", "data_dim", "field_dim"): + settings[key] = value.strip() # written back verbatim +model = AMICANative(**settings).fit(X) # X: the data the EEGLAB run fitted +``` + ## 1. Relative rank threshold The reference decides how many dimensions are real with an absolute floor on covariance diff --git a/docs/guides/backends.md b/docs/guides/backends.md index 4f8fca52..ca292801 100644 --- a/docs/guides/backends.md +++ b/docs/guides/backends.md @@ -117,12 +117,12 @@ so its export omits the `LLt` file (with a warning). With the PyTorch backend, `AMICA(device=...)` accepts `"cuda"`, `"cpu"`, `"mps"`, or `None` (auto): - **`None` (auto)**: picks MPS, then CUDA, then CPU, whichever is available first. - For a float64 fit (the default) the wrapper redirects an MPS pick to CPU, because MPS cannot represent float64, + For a float64 fit (the default) the `AMICATorchNG` constructor moves an MPS pick to the CPU and logs a warning, because MPS cannot represent float64, so on a Mac a default fit runs on the CPU and a `dtype=torch.float32` fit on MPS. - The raw `AMICATorchNG` does not redirect: with `device=None` and float64 on a Mac its constructor raises `ValueError`, - so pass `device="cpu"` there. + The wrappers pass `device` through to it, so `AMICA`, `AMICAICA`, `AMICA.load` and the raw `AMICATorchNG` all choose the same way (issue #354). - **`"cuda"`**: the bit-safe path for float64 Fortran parity on NVIDIA GPUs. -- **`"mps"`**: requires `dtype=torch.float32`. Note that PyTorch-MPS is not a +- **`"mps"`**: requires `dtype=torch.float32`, and raises `ValueError` at the float64 default. + Note that PyTorch-MPS is not a performance win for AMICA (see below); prefer the MLX backend on Apple hardware. ## Precision: float64 vs float32 diff --git a/docs/guides/validation.md b/docs/guides/validation.md index 66cdd00a..ac46302f 100644 --- a/docs/guides/validation.md +++ b/docs/guides/validation.md @@ -126,7 +126,7 @@ and the backend converges to the binary's solution within ~0.005 log-likelihood ### Newton-enabled runs and the initialization basin The comparison above disables Newton (`do_newton=0`) to isolate the algorithm from its starting point. -With Newton enabled (`do_newton=1`, the default), agreement at the full 2000-iteration budget depends on the initialization, not on any dynamics difference between the backends. +With Newton enabled (`do_newton=1`, as in the bundled parameter files and EEGLAB's `runamica15.m`; pamica's own default is off), agreement at the full 2000-iteration budget depends on the initialization, not on any dynamics difference between the backends. From an *identical* initialization (the same starting mixing matrix and densities fed to both), `pamica` and Fortran converge to the same solution: mean Hungarian-matched correlation ~0.997 with no collapsed components on the full 70-channel recording, the residual being floating-point summation-order noise between two implementations rather than an algorithmic gap. From *independent* random initializations the picture differs, because the two backends' random number generators do not share a state, so a fixed seed does not map to a matched start. @@ -522,20 +522,21 @@ Tests live under `pamica/tests/`: `torch_tests/test_ng_backend.py`, `torch_tests AMICA ships four stops. On recordings the size of the bundled sample, only two of them fire, and it is worth knowing which before concluding that one is broken. -Two of the four defaults differ between the backends, so read the column that -matches the entry point you use. `AMICA_NumPy` resolves its defaults from the -bundled `pamica/numpy_impl/params.json`; `AMICA`/`AMICATorchNG` and `AMICAMLXNG` -take theirs from the constructor signature (`max_iter` from `fit`); Fortran -compiles in the values in `amica15_header.f90` and the bundled +The pamica backends share these defaults (issue #354): +`AMICA`/`AMICATorchNG` and `AMICAMLXNG` take theirs from the constructor signature (`max_iter` from `fit`), +and `AMICA_NumPy` resolves the same values from the bundled `pamica/numpy_impl/params.json`, +which before issue #354 turned Newton on and ran 2000 iterations. +Fortran compiles in the values in `amica15_header.f90`, and the bundled `pamica/sample_data/input.param` overrides several. +The [defaults table](amica-differences.md#default-settings-issue-354) of the differences guide lists every setting across these sources. | Stop | `AMICA` / `AMICATorchNG` | `AMICA_NumPy` | `AMICAMLXNG` (MLX) | Fortran (compiled / `input.param`) | |---|---|---|---|---| -| `max_iter` | **100** | 2000 | **100** | none / 2000 | +| `max_iter` | **100** | **100** | **100** | none / 2000 | | `min_dll` (`use_min_dll`) | on, `1e-9` | on, `1e-9` | on, `1e-9` | on, `1e-9` | | `min_nd` (`use_grad_norm`) | on, `1e-7` | on, `1e-7` (named `min_grad_norm`) | on, `1e-7` | on, `1e-7` | | `minlrate` (`lrate_floor`) | `1e-12` | `1e-12` | `1e-12` | `1e-12` / `1e-8` | -| `do_newton` | **off** | **on** | **off** | off / on | +| `do_newton` | **off** | **off** | **off** | off / on | The MLX column dates from issue #248, which ported both stops to that backend; before it, an MLX fit had no convergence criterion at all and always ran to @@ -545,9 +546,9 @@ backend-differences guide), taking `AMICATorchNG`'s off-by-default. Which of them actually ends a fit: - **`min_dll` normally wins**, at iteration 326-1076 depending on the BLAS build, - when `max_iter` is large enough to let it. At `AMICATorchNG`'s - default `max_iter=100` the fit always ends on `max_iter` before `min_dll` can - fire, so the default PyTorch run is iteration-limited, not converged. Raise + when `max_iter` is large enough to let it. At the default + `max_iter=100` the fit always ends on `max_iter` before `min_dll` can + fire, so a default run on any pamica backend is iteration-limited, not converged. Raise `max_iter` if you want the likelihood stop to be the one that decides. - **`min_nd` never fires** on a recording this size, in any of the four implementations. This is the subject of the rest of this section. diff --git a/pamica/amica.py b/pamica/amica.py index 494ba2da..0085aaab 100644 --- a/pamica/amica.py +++ b/pamica/amica.py @@ -19,18 +19,13 @@ import numpy as np import torch -from .torch_impl import AMICATorchNG, setup_device +from .torch_impl import AMICATorchNG if TYPE_CHECKING: from .mlx_impl import AMICAMLXNG logger = logging.getLogger(__name__) -# AMICATorchNG's default parameter dtype, derived from its signature so the -# wrapper's MPS/float64 fallback below stays in lockstep if that default ever -# changes (rather than duplicating the literal). -_NG_DEFAULT_DTYPE = inspect.signature(AMICATorchNG).parameters["dtype"].default - # fit()'s own named parameters that a parameter-file dict can default. _FIT_NAMED_PARAMS = {"max_iter", "lrate", "do_mean", "do_sphere", "do_newton"} @@ -49,9 +44,9 @@ ) # Sentinel distinguishing "caller did not pass this fit() argument" from -# "caller explicitly passed the same value as the hard default", so a +# "caller explicitly passed the same value as the backend's default", so a # from_params_file default can be overridden by an explicit call-site value -# without also being masked by fit()'s own hard-coded defaults. +# without also being masked by the backend defaults fit() falls back to. _UNSET = object() @@ -131,6 +126,25 @@ def _ctor_params(backend_cls: type) -> frozenset: ) +@functools.cache +def _fit_defaults(backend_cls: "type[AMICATorchNG] | type[AMICAMLXNG]") -> dict: + """The defaults of ``fit()``'s named parameters, for one backend class + (issue #354). + + Read from the backend's own signatures, so the wrapper cannot drift from + it: ``max_iter`` from ``backend_cls.fit``, and ``lrate``/``do_mean``/ + ``do_sphere``/``do_newton`` from the constructor. An argument left unset + in :meth:`AMICA.fit`, and absent from a parameter file, takes this value, + so ``AMICA().fit(X)`` runs exactly as ``backend_cls(n_channels).fit(X)``. + """ + ctor = inspect.signature(backend_cls).parameters + defaults = {name: ctor[name].default for name in _FIT_NAMED_PARAMS - {"max_iter"}} + defaults["max_iter"] = ( + inspect.signature(backend_cls.fit).parameters["max_iter"].default + ) + return defaults + + # AMICA.save's payload version (issue #313). Version 2 records the backend # in wrapper["backend"]; version 1 (written before backend selection) is still # read, and holds a torch state dict by construction. @@ -228,12 +242,13 @@ class AMICA: n_mix : int, default=3 Number of mixture components per source device : str or torch.device, optional - Device to use ('cuda', 'mps', 'cpu', or None for auto). With ``None`` - (auto), an auto-selected MPS device is redirected to CPU because the - backend computes in float64 for Fortran parity and MPS cannot - represent it; pass ``dtype=torch.float32`` (with ``device="mps"``) to - run on MPS instead. PyTorch backend only: with ``backend="mlx"`` it - must stay ``None``. + Device to use ('cuda', 'mps', 'cpu', or None for auto), passed to + :class:`AMICATorchNG`. With ``None`` (auto), the backend moves an + auto-selected MPS device to the CPU with a logged warning, because it + computes in float64 for Fortran parity and MPS cannot represent it; + pass ``dtype=torch.float32`` (with ``device="mps"``) to run on MPS + instead. PyTorch backend only: with ``backend="mlx"`` it must stay + ``None``. verbose : bool, default=True Whether to show progress during fitting backend : {"torch", "mlx"}, default="torch" @@ -386,32 +401,6 @@ def __init__( # None for an instance built directly via AMICA(...). self._file_params: Optional[dict] = None - def _select_device(self, ng_dtype) -> Union[str, torch.device]: - """Resolve the torch compute device, applying the MPS/float64 fallback. - - PyTorch backend only; MLX has no device choice. ``AMICATorchNG`` - defaults to float64 for Fortran parity, which MPS cannot represent. - When the device was auto-selected (the user did not - pin one) and resolved to MPS for a float64 run, fall back to CPU so the - default config runs instead of crashing. CUDA supports float64, so only - MPS needs this. An explicit ``device="mps"`` is left untouched and - surfaces ``AMICATorchNG``'s own ValueError; users wanting MPS pass - ``dtype=torch.float32`` too. - """ - device = setup_device() if self.device is None else self.device - dev_type = getattr(device, "type", device) - if self.device is None and dev_type == "mps" and ng_dtype == torch.float64: - device = torch.device("cpu") - msg = ( - "AMICA uses float64 for Fortran parity; MPS lacks float64 " - "support, so falling back to CPU. Pass dtype=torch.float32 " - "with device='mps' to run on MPS." - ) - logger.warning(msg) - if self.verbose: - print(msg) - return device - def fit( self, X: np.ndarray, @@ -426,14 +415,22 @@ def fit( """ Fit AMICA model to data. + The defaults of ``max_iter``, ``lrate``, ``do_mean``, ``do_sphere`` + and ``do_newton`` are the selected backend's own, read from its + signatures (issue #354), so ``AMICA().fit(X)`` fits exactly as the + backend class does with its defaults. Both backends default to the + values listed below. + Parameters ---------- X : np.ndarray Input data of shape (n_channels, n_samples) max_iter : int, default=100 Maximum number of iterations - lrate : float, default=0.05 - Learning rate + lrate : float, default=0.1 + Initial and maximum natural-gradient learning rate, the compiled + amica15 default. EEGLAB's ``runamica15.m`` uses 0.05; see the + defaults table in ``docs/guides/amica-differences.md``. do_mean : bool, default=True Whether to remove mean from data do_sphere : bool, default=True @@ -479,7 +476,7 @@ def fit( When the instance was built via :meth:`from_params_file` (issue #132), any of the parameters above -- named or in ``**kwargs`` -- left unset here falls back to that file's translated value instead - of the hard-coded default; an explicitly passed argument always + of the backend's default; an explicitly passed argument always wins over the file. Settings the file carries that match neither a named ``fit()`` parameter nor a constructor keyword of the selected backend (data-location metadata like ``files``/ @@ -556,17 +553,18 @@ def fit( # the file carries that apply to neither surface are named in one # warning rather than silently discarded. file_params = self._file_params or {} + backend_defaults = _fit_defaults(backend_cls) - def _file_default(explicit, name, hard_default): + def _file_default(explicit, name): if explicit is not _UNSET: return explicit - return file_params.get(name, hard_default) + return file_params.get(name, backend_defaults[name]) - max_iter = _file_default(max_iter, "max_iter", 100) - lrate = _file_default(lrate, "lrate", 0.05) - do_mean = _file_default(do_mean, "do_mean", True) - do_sphere = _file_default(do_sphere, "do_sphere", True) - do_newton = _file_default(do_newton, "do_newton", False) + max_iter = _file_default(max_iter, "max_iter") + lrate = _file_default(lrate, "lrate") + do_mean = _file_default(do_mean, "do_mean") + do_sphere = _file_default(do_sphere, "do_sphere") + do_newton = _file_default(do_newton, "do_newton") if file_params: for key, value in file_params.items(): @@ -590,13 +588,12 @@ def _file_default(explicit, name, hard_default): print(f"Fitting AMICA with {n_channels} channels, {n_samples} samples") print(f"Models: {self.n_models}, Mixture components: {self.n_mix}") - # Torch device (with the MPS/float64 parity fallback, see - # _select_device); MLX always runs on MLX's default device. + # The torch device as given (None picks one inside AMICATorchNG, + # which also moves an automatic MPS pick for a float64 fit to the + # CPU, issue #354); MLX always runs on MLX's default device. placement: dict[str, Any] = {} if self.backend == "torch": - placement["device"] = self._select_device( - kwargs.get("dtype", _NG_DEFAULT_DTYPE) - ) + placement["device"] = self.device # Build and train the backend on a LOCAL reference first, and only # publish it to self (and derive the fitted-state attributes) once @@ -1207,7 +1204,7 @@ def load( Path to a file written by :meth:`save`. device : str or torch.device, optional Device to place a restored PyTorch model on. With ``None`` (auto), - the same MPS/float64 fallback as :meth:`fit` applies so a float64 + the backend constructor chooses, as in :meth:`fit`, so a float64 parity model never lands on MPS. An MLX model always loads onto MLX's default device, so it must stay ``None`` for one. @@ -1273,11 +1270,9 @@ def load( ) state = payload["backend"] if backend == "torch": - # Resolve the device using the persisted backend dtype so the same - # MPS/float64 fallback as fit() applies to an auto-selected device. - ng_dtype = getattr(torch, state["config"]["dtype"]) - resolved_device = model._select_device(ng_dtype) - model.model_ = AMICATorchNG.from_state_dict(state, device=resolved_device) + # device=None lets the constructor choose from the saved dtype, so + # a float64 model's automatic MPS pick moves to the CPU as in fit(). + model.model_ = AMICATorchNG.from_state_dict(state, device=device) else: model.model_ = _mlx_backend_class().from_state_dict( _mlx_state_from_payload(state, filepath) diff --git a/pamica/native/engine.py b/pamica/native/engine.py index 57738f59..8351e16d 100644 --- a/pamica/native/engine.py +++ b/pamica/native/engine.py @@ -11,6 +11,8 @@ from __future__ import annotations +import functools +import inspect import subprocess import tempfile from pathlib import Path @@ -18,62 +20,26 @@ import numpy as np +from ..fortran_params import FORTRAN_TO_PAMICA_KEY from ..numpy_impl.load import AmicaOutput, loadmodout from . import resolver -# Full default parameter set (mirrors sample_data/input.param) so the engine does -# not depend on the sample data being installed. ``files``/``outdir``/``data_dim``/ -# ``field_dim`` are set per fit; everything else is a tunable default. Keys are the -# Fortran param names; friendly aliases are mapped in ``fit``. -_DEFAULT_PARAMS: dict[str, object] = { - "block_size": 512, - "do_opt_block": 0, - "blk_min": 256, - "blk_step": 256, - "blk_max": 1024, - "num_models": 1, +# Settings the binary reads that no pamica backend has: its thread count, the +# raw-data format, its output files and checkpoint cadence, the checkpoint +# loading switches and the per-family update switches. They keep the values of +# the bundled sample_data/input.param, as before issue #354. doPCA, +# field_blocksize and load_W are keys that file carries and this amica15.f90 +# does not parse (pamica.fortran_params warns about them the same way). +_NATIVE_ONLY_PARAMS: dict[str, object] = { "max_threads": 10, - "use_min_dll": 1, - "min_dll": 1e-09, - "use_grad_norm": 1, - "min_grad_norm": 1e-07, - "num_mix_comps": 3, - "pdftype": 0, - "max_iter": 2000, "num_samples": 1, "field_blocksize": 1, "do_history": 0, "histstep": 10, - "share_comps": 0, - "share_start": 100, - "comp_thresh": 0.99, - "share_iter": 100, - "lrate": 0.05, - "minlrate": 1e-08, - "mineig": 1e-12, - "lratefact": 0.5, - "rholrate": 0.05, - "rho0": 1.5, - "minrho": 1.0, - "maxrho": 2.0, - "rholratefact": 0.5, - "kurt_start": 3, - "num_kurt": 5, - "kurt_int": 1, - "do_newton": 1, - "newt_start": 50, - "newt_ramp": 10, - "newtrate": 1.0, - "do_reject": 0, - "numrej": 3, - "rejsig": 3.0, - "rejstart": 2, - "rejint": 3, "writestep": 20, "write_nd": 0, "write_LLt": 1, "decwindow": 1, - "max_decs": 3, "fix_init": 0, "update_A": 1, "update_c": 1, @@ -81,8 +47,6 @@ "update_alpha": 1, "update_mu": 1, "update_beta": 1, - "invsigmax": 100.0, - "invsigmin": 0.0, "do_rho": 1, "load_rej": 0, "load_W": 0, @@ -93,16 +57,54 @@ "load_beta": 0, "load_rho": 0, "load_comp_list": 0, - "do_mean": 1, - "do_sphere": 1, "doPCA": 1, - "pcakeep": 0, - "pcadb": 30.0, "byte_size": 4, - "doscaling": 1, - "scalestep": 1, } +# The two canonical pamica keys (pamica.fortran_params) that AMICATorchNG +# spells differently in its constructor. +_TORCH_SPELLING = {"num_models": "n_models", "num_mix": "n_mix"} + + +@functools.cache +def _shared_default_params() -> dict[str, object]: + """pamica's shared defaults under the binary's keywords (issue #354). + + Every keyword of ``pamica.fortran_params.FORTRAN_TO_PAMICA_KEY`` whose + pamica setting ``AMICATorchNG`` has takes ``AMICATorchNG``'s default + (``max_iter`` from its ``fit``), the same value the other backends hold + (``pamica/tests/test_default_settings.py``). A setting whose default is + ``None`` (``pcakeep``, ``pcadb``, ``seed``) is left out of this table; + ``_input_params`` then fills ``pcakeep`` with the data's channel count. + pamica settings with no binary keyword (``keep_best``, ``mineig_rel``, + ``n_restarts``, ...) have nothing to write. Then the native-only knobs. + Imported lazily: the torch backend's signature is the shared source. + """ + from ..torch_impl.core import AMICATorchNG + + shared = { + name: param.default + for name, param in inspect.signature(AMICATorchNG).parameters.items() + } + shared["max_iter"] = ( + inspect.signature(AMICATorchNG.fit).parameters["max_iter"].default + ) + params: dict[str, object] = {} + for fortran_key, pamica_key in FORTRAN_TO_PAMICA_KEY.items(): + if fortran_key == "num_mix": # the binary's second spelling of num_mix_comps + continue + value = shared.get(_TORCH_SPELLING.get(pamica_key, pamica_key)) + if value is not None: + params[fortran_key] = value + params.update(_NATIVE_ONLY_PARAMS) + return params + + +def _default_params() -> dict[str, object]: + """A fresh copy of :func:`_shared_default_params`, for a caller to modify.""" + return dict(_shared_default_params()) + + # Friendly kwarg -> Fortran param-name aliases (match the Python backends' names). _ALIASES = {"n_models": "num_models", "n_mix": "num_mix_comps"} @@ -138,6 +140,25 @@ class AMICANative: Any Fortran ``input.param`` field (or a friendly alias: ``n_models``, ``n_mix``), overriding the defaults; e.g. ``max_iter``, ``lrate``, ``pdftype``, ``do_newton``. + + Notes + ----- + The defaults are pamica's shared ones since issue #354, the values the + PyTorch, MLX and NumPy backends default to, under the binary's keywords + (``lrate`` 0.1, Newton off, ``max_iter`` 100, ``do_opt_block`` off, + ``pdftype`` 0, ...). pamica settings the binary has no keyword for, such + as ``keep_best`` and ``mineig_rel``, are not written, so the binary + returns its last iterate and uses the absolute ``mineig`` floor. + + The binary's ``block_size`` counts one thread's share of a block, and it + processes no block at all when ``max_threads * block_size`` exceeds the + samples (an all-NaN fit, issue #292). Unless ``block_size`` is given, the + engine therefore writes pamica's 8192-sample block, capped at the data's + length, divided by ``max_threads`` (10 unless given): 819 for a long + recording. A given ``block_size`` is written as is. Before issue #354 the defaults were the bundled + ``sample_data/input.param``'s (``lrate`` 0.05, Newton on from iteration + 50, ``max_iter`` 2000, ``block_size`` 512); pass that file's settings as + keywords to run it (see ``docs/api/native-backend.md``). """ def __init__( @@ -165,22 +186,45 @@ def _resolve_binary(self) -> Path: return self.binary return resolver.resolve(self.version) - def fit(self, X: np.ndarray, **params: object) -> "AMICANative": - """Run AMICA on ``X`` (shape ``(n_channels, n_samples)``) and store the - result as ``self.output_`` (an ``AmicaOutput``).""" - X = np.asarray(X) - if X.ndim != 2: - raise ValueError(f"X must be 2-D (n_channels, n_samples); got {X.shape}") - n_channels, n_samples = X.shape - - merged = dict(_DEFAULT_PARAMS) + def _input_params( + self, n_channels: int, n_samples: int, params: dict[str, object] + ) -> dict[str, object]: + """The ``input.param`` settings :meth:`fit` writes for data of this + shape: pamica's shared defaults, then the constructor's and ``fit``'s + keywords (friendly aliases mapped), then the data's dimensions.""" + merged = _default_params() + given = set() for src in (self.params, params): for key, value in src.items(): merged[_ALIASES.get(key, key)] = value + given.add(_ALIASES.get(key, key)) + if "block_size" not in given: + # pamica's block_size counts the samples of one block; the + # binary's counts one thread's share of it, and it runs + # n_samples // (max_threads * block_size) blocks, so a block larger + # than the data would leave it none and an all-NaN fit (issue + # #292). The default is therefore pamica's block, at most the + # data, split over the threads. + # int(str(...)): a value passed as a file's text ("10") reads the same. + threads = int(str(merged["max_threads"])) + block = min(int(str(merged["block_size"])), n_samples) + merged["block_size"] = max(1, block // threads) merged["data_dim"] = n_channels merged["field_dim"] = n_samples if not merged.get("pcakeep"): merged["pcakeep"] = n_channels # default: keep all components + # `files` must come first: amica15.f90 hard-stops if it parses other + # keys before the data file. Dict insertion order preserves that. + return {"files": "./data.fdt", "outdir": "./amicaout/", **merged} + + def fit(self, X: np.ndarray, **params: object) -> "AMICANative": + """Run AMICA on ``X`` (shape ``(n_channels, n_samples)``) and store the + result as ``self.output_`` (an ``AmicaOutput``).""" + X = np.asarray(X) + if X.ndim != 2: + raise ValueError(f"X must be 2-D (n_channels, n_samples); got {X.shape}") + n_channels, n_samples = X.shape + param = self._input_params(n_channels, n_samples, params) binary = self._resolve_binary() @@ -188,7 +232,7 @@ def fit(self, X: np.ndarray, **params: object) -> "AMICANative": work = Path(td) # AMICA reads the data as raw byte_size floats in column-major order # (numpy_impl/data.py: reshape order="F"); write it that way. - byte_size = merged["byte_size"] + byte_size = param["byte_size"] dtype = ( np.float32 if isinstance(byte_size, int) and byte_size == 4 @@ -198,9 +242,6 @@ def fit(self, X: np.ndarray, **params: object) -> "AMICANative": outdir = work / "amicaout" outdir.mkdir() - # `files` must come first: amica15.f90 hard-stops if it parses other - # keys before the data file. Dict insertion order preserves that. - param = {"files": "./data.fdt", "outdir": "./amicaout/", **merged} (work / "input.param").write_text(_render_param(param)) env = None diff --git a/pamica/numpy_impl/cli.py b/pamica/numpy_impl/cli.py index 9bbb9c09..1a33abb9 100644 --- a/pamica/numpy_impl/cli.py +++ b/pamica/numpy_impl/cli.py @@ -23,7 +23,7 @@ Optional parameters can be included in the file: - num_models: Number of models (default: 1) - num_mix: Number of mixture components (default: 3) -- max_iter: Maximum iterations (default: 2000) +- max_iter: Maximum iterations (default: 100) And many others as documented in the AMICA class. """ diff --git a/pamica/numpy_impl/core.py b/pamica/numpy_impl/core.py index 8ed74311..94101355 100644 --- a/pamica/numpy_impl/core.py +++ b/pamica/numpy_impl/core.py @@ -640,7 +640,7 @@ def __init__( if self.num_models < 1: raise ValueError(f"num_models must be >= 1, got {self.num_models}") self.num_mix = params.get("num_mix", 3) - self.max_iter = params.get("max_iter", 2000) + self.max_iter = params.get("max_iter", 100) if self.max_iter < 1: raise ValueError(f"max_iter must be >= 1, got {self.max_iter}") self.do_newton = params.get("do_newton", False) diff --git a/pamica/numpy_impl/params.json b/pamica/numpy_impl/params.json index c293f70a..24342846 100644 --- a/pamica/numpy_impl/params.json +++ b/pamica/numpy_impl/params.json @@ -8,10 +8,10 @@ "num_models": 1, "num_mix": 3, - "max_iter": 2000, + "max_iter": 100, "num_comps": -1, - "do_newton": true, + "do_newton": false, "newt_start": 20, "newt_ramp": 10, "newtrate": 0.5, diff --git a/pamica/tests/mne_tests/test_end_to_end_workflow.py b/pamica/tests/mne_tests/test_end_to_end_workflow.py index f91e258c..60b9773b 100644 --- a/pamica/tests/mne_tests/test_end_to_end_workflow.py +++ b/pamica/tests/mne_tests/test_end_to_end_workflow.py @@ -245,7 +245,8 @@ def test_input_param_drives_the_fit(raw, backend): def test_torch_and_mlx_find_the_same_components(raw, fitted): """Same data, seed and settings on both backends: the same number of components and the same sources (Hungarian-matched |corr|, the bar of the - other torch-vs-MLX suites; measured min 0.999999999).""" + other torch-vs-MLX suites; measured min 0.999999997 at the default lrate, + 0.1 since issue #354).""" t, m = fitted("torch"), fitted("mlx") assert t.n_components_ == m.n_components_ == N_KEEP matched, _ = _matched_abs_corr( diff --git a/pamica/tests/mne_tests/test_mne_backends.py b/pamica/tests/mne_tests/test_mne_backends.py index f315feea..913ebaae 100644 --- a/pamica/tests/mne_tests/test_mne_backends.py +++ b/pamica/tests/mne_tests/test_mne_backends.py @@ -16,6 +16,7 @@ the construction checks run everywhere. """ +import inspect import sys from pathlib import Path from typing import Any, Dict @@ -151,6 +152,17 @@ def test_mlx_rejects_a_dtype(raw): assert ica.amica_ is None +@pytest.mark.parametrize("fixture", ["torch_keep", "mlx_keep"]) +def test_fit_without_lrate_uses_the_backend_default(fixture, request): + """``AMICAICA.fit`` forwards its keywords to ``AMICA.fit`` and sets no + ``lrate`` of its own, so a fit that passes none runs at the backend's + default, as ``AMICA`` does (issue #354).""" + fitted = request.getfixturevalue(fixture) + backend = fitted.amica_.model_ + default = inspect.signature(type(backend)).parameters["lrate"].default + assert backend.lrate0 == default + + # --- the MLX fit and its export --------------------------------------------------- def test_mlx_fit_builds_the_mlx_backend(mlx_keep): AMICAMLXNG = _require_mlx() @@ -237,8 +249,11 @@ def test_mlx_and_torch_export_the_same_basis(mlx_keep, torch_keep): # least 1e6 on unit-variance sphered data, the densities collapse (the next # log-likelihood is about -4e8) and the real fit path diverges. Measured over # seeds 0/1/2/42, 4096 samples and the full recording, with and without -# pcakeep=20, max_iter=10: torch stops on nan_ll at iteration 3 and MLX on -# nan_params at iteration 2, 16 of 16 runs each. +# pcakeep=20, max_iter=10, at lrate 0.05 and at the default 0.1 (issue #354): +# both backends end degenerate in 16 of 16 runs, on nan_params with the +# backend's 0-based ``iteration`` at 2. The test asserts only that the stop +# is one of the backend's degenerate reasons, so it does not depend on which +# one or when. INVSIGMIN, INVSIGMAX = 1e6, 1e7 diff --git a/pamica/tests/native_oracle.py b/pamica/tests/native_oracle.py index c9ae2d69..8e91bb7a 100644 --- a/pamica/tests/native_oracle.py +++ b/pamica/tests/native_oracle.py @@ -42,12 +42,41 @@ import numpy as np from pamica.native import resolver -from pamica.native.engine import _DEFAULT_PARAMS, _render_param +from pamica.native.engine import _render_param # The release whose native binary the seeded oracles run (resolver cache, else a # verified download); PAMICA_NATIVE_BINARY overrides it with a local build. REFERENCE_VERSION = "v0.3.3" +_BUNDLED_PARAM_FILE = ( + Path(__file__).resolve().parents[1] / "sample_data" / "input.param" +) +# The keys each run sets for itself. +_PER_RUN_KEYS = frozenset({"files", "outdir", "data_dim", "field_dim"}) + + +def _bundled_reference_params() -> dict[str, object]: + """The bundled ``sample_data/input.param`` settings, verbatim, without the + keys each run sets. + + Every oracle run starts from this configuration, the one the oracles were + written and measured against (``runamica15.m``'s run of the bundled + sample): ``lrate`` 0.05, Newton on from iteration 50, ``block_size`` 512, + and so on, which each caller then overrides where its protocol needs to. + Until issue #354 ``AMICANative``'s own defaults were these values; they are + now pamica's shared defaults, so the oracles pin the file instead. Values + stay the file's strings, which ``_render_param`` writes back unchanged. + """ + params: dict[str, object] = {} + for line in _BUNDLED_PARAM_FILE.read_text().splitlines(): + key, _, value = line.strip().partition(" ") + if key and not key.startswith("#") and key not in _PER_RUN_KEYS: + params[key] = value.strip() + return params + + +REFERENCE_RUN_PARAMS: dict[str, object] = _bundled_reference_params() + # Every output file :func:`run_seeded_reference` reads back. _OUTPUT_FILES = ("A", "mu", "sbeta", "rho", "alpha", "gm", "c", "comp_list", "LL", "S") # stdout lines of a run that did not carry the seeded state to the end @@ -169,7 +198,7 @@ def run_seeded_reference( ``data_file`` is the raw float32 column-major recording (``nx x n_samples``) the pamica side fitted; it is linked into ``workdir``. - ``params`` override the engine defaults by Fortran name (``do_newton``, + ``params`` override :data:`REFERENCE_RUN_PARAMS` by Fortran name (``do_newton``, ``doscaling``, ``block_size``, ...). A reused ``workdir`` is safe: its ``out`` and ``init`` directories are @@ -221,7 +250,7 @@ def run_seeded_reference( # `files` must come first: amica15.f90 stops if other keys precede it. "files": "./data.fdt", "outdir": "./out/", - **_DEFAULT_PARAMS, + **REFERENCE_RUN_PARAMS, "indir": "./init", "data_dim": nx, "field_dim": n_samples, @@ -280,7 +309,7 @@ def run_drawn_reference( iteration (:955) and ``write_output`` writes the initialization itself, so the returned ``A`` is the drawn initial mixing matrix (issue #341). Full rank only (``nw`` channels, ``pcakeep = nw``); ``params`` override - the engine defaults by Fortran name. Raises like + :data:`REFERENCE_RUN_PARAMS` by Fortran name. Raises like :func:`run_seeded_reference`. """ num_comps = nw * num_models @@ -294,7 +323,7 @@ def run_drawn_reference( # `files` must come first: amica15.f90 stops if other keys precede it. "files": "./data.fdt", "outdir": "./out/", - **_DEFAULT_PARAMS, + **REFERENCE_RUN_PARAMS, "data_dim": nw, "field_dim": n_samples, "pcakeep": nw, diff --git a/pamica/tests/test_default_settings.py b/pamica/tests/test_default_settings.py new file mode 100644 index 00000000..d47ed45f --- /dev/null +++ b/pamica/tests/test_default_settings.py @@ -0,0 +1,178 @@ +"""Every pamica backend defaults to the same settings (issue #354). + +The defaults table in ``docs/guides/amica-differences.md`` gives one pamica +column for ``AMICATorchNG``, ``AMICAMLXNG``, ``AMICA_NumPy`` and the wrappers +that build the first two. These tests hold the backends to it: every setting a +backend shares with ``AMICATorchNG`` has ``AMICATorchNG``'s default. + +``AMICATorchNG`` and ``AMICAMLXNG`` declare their defaults in their +signatures. ``AMICA_NumPy`` reads its own from the bundled +``pamica/numpy_impl/params.json`` in its constructor (falling back to values in +code for keys the file does not carry), so it is compared through a default +construction, with its own spelling tables mapping the names. NumPy and torch +always run (``.rules/backend_parity.md``); the MLX test skips without MLX or an +Apple GPU. + +``AMICANative`` runs the reference binary, so its defaults are the +``input.param`` it writes. That file is built without a binary and read back +through ``pamica.fortran_params.read_params_file``, the reader every pamica +parameter-file path uses, so each binary keyword is compared under the pamica +name it translates to. +""" + +import inspect + +import pytest + +from pamica import AMICA_NumPy, AMICANative, AMICATorchNG +from pamica.fortran_params import FORTRAN_TO_PAMICA_KEY, read_params_file +from pamica.native.engine import _render_param +from pamica.numpy_impl import core as numpy_core + +# Settings AMICA.fit names itself; max_iter is a fit() argument on the +# torch and MLX backends and a constructor setting on NumPy. +_FIT_MAX_ITER = "max_iter" + + +def _torch_defaults() -> dict: + """AMICATorchNG's defaults, plus its fit()'s max_iter.""" + defaults = { + name: param.default + for name, param in inspect.signature(AMICATorchNG).parameters.items() + if name != "n_channels" + } + defaults[_FIT_MAX_ITER] = ( + inspect.signature(AMICATorchNG.fit).parameters[_FIT_MAX_ITER].default + ) + return defaults + + +def _numpy_name(name: str) -> str: + """This setting's spelling on the NumPy backend, from its own tables.""" + return numpy_core._ALTERNATE_SPELLING.get( + name, numpy_core._CANONICAL_TO_NUMPY_KEY.get(name, name) + ) + + +def test_numpy_shares_every_torch_default(): + """A default ``AMICA_NumPy()``, which reads the bundled params.json, holds + AMICATorchNG's default for every setting the two share. Before issue #354 + params.json turned Newton on and ran 2000 iterations.""" + torch_defaults = _torch_defaults() + unsupported = numpy_core._torch_only_options() + shared = sorted(set(torch_defaults) - unsupported) + model = AMICA_NumPy() + + missing = [name for name in shared if not hasattr(model, _numpy_name(name))] + assert missing == [], f"NumPy has no attribute for {missing}" + differing = { + name: (getattr(model, _numpy_name(name)), torch_defaults[name]) + for name in shared + if getattr(model, _numpy_name(name)) != torch_defaults[name] + } + assert differing == {}, "NumPy default != torch default: " + repr(differing) + # The comparison reaches the settings this issue moved. + assert {"do_newton", _FIT_MAX_ITER, "lrate"} <= set(shared) + + +def test_numpy_params_json_carries_the_shared_defaults(): + """The bundled file itself, read by the backend's own loader, sets the + moved values: a code fallback cannot mask a stale entry in the file.""" + params = numpy_core.load_default_params() + torch_defaults = _torch_defaults() + for name in ("do_newton", _FIT_MAX_ITER, "lrate"): + assert params[_numpy_name(name)] == torch_defaults[name], name + + +def _require_mlx(): + mlx_core = pytest.importorskip( + "pamica.mlx_impl.core", reason="MLX not installed (Apple Silicon only)" + ) + mx = mlx_core.mx + if mx.default_device().type != mx.DeviceType.gpu: + pytest.skip("no Apple GPU") + return mlx_core.AMICAMLXNG + + +def test_mlx_shares_every_torch_default(): + """Every keyword AMICATorchNG and AMICAMLXNG share has the same default + (only torch has device/dtype), and so does fit()'s max_iter.""" + mlx_cls = _require_mlx() + torch_params = inspect.signature(AMICATorchNG).parameters + mlx_params = inspect.signature(mlx_cls).parameters + assert set(torch_params) - set(mlx_params) == {"device", "dtype"} + assert set(mlx_params) <= set(torch_params) + differing = { + name: (torch_params[name].default, mlx_params[name].default) + for name in mlx_params + if torch_params[name].default != mlx_params[name].default + } + assert differing == {} + mlx_max_iter = inspect.signature(mlx_cls.fit).parameters[_FIT_MAX_ITER].default + assert mlx_max_iter == _torch_defaults()[_FIT_MAX_ITER] + + +# The canonical pamica keys (read_params_file's output) that AMICATorchNG +# spells differently in its constructor. +_TORCH_SPELLING = {"num_models": "n_models", "num_mix": "n_mix"} +NW, N_SAMPLES = 32, 30504 + + +def _native_settings(tmp_path) -> dict: + """The input.param a default ``AMICANative().fit`` writes for 32-channel + data, read back under pamica's canonical names.""" + param = AMICANative()._input_params(NW, N_SAMPLES, {}) + path = tmp_path / "input.param" + path.write_text(_render_param(param)) + return read_params_file(path) + + +def test_native_input_param_shares_every_torch_default(tmp_path): + """Before issue #354 the engine wrote the bundled input.param's values + (lrate 0.05, Newton on, max_iter 2000, block_size 512, ...).""" + torch_defaults = _torch_defaults() + settings = _native_settings(tmp_path) + compared = {} + for key, value in settings.items(): + name = _TORCH_SPELLING.get(key, key) + # pcakeep is the data's channel count, which keeps every dimension: + # what pamica's default of None means. block_size is one thread's + # share of a block in the binary, checked below. + if name in torch_defaults and name not in ("pcakeep", "block_size"): + compared[name] = (value, torch_defaults[name]) + differing = {k: v for k, v in compared.items() if v[0] != v[1]} + assert differing == {}, "native default != torch default: " + repr(differing) + assert settings["pcakeep"] == NW + assert {"lrate", "do_newton", "max_iter", "pdftype"} <= set(compared) + + +def test_native_block_is_pamicas_block_split_over_the_threads(): + """The binary processes n_samples // (max_threads * block_size) blocks + of max_threads * block_size samples (amica15.f90:1215-1227), so the + engine writes pamica's block divided by the thread count; one capped at + the data's length still leaves the binary a block (issue #292).""" + block = _torch_defaults()["block_size"] + param = AMICANative()._input_params(NW, N_SAMPLES, {}) + threads, per_thread = param["max_threads"], param["block_size"] + assert isinstance(threads, int) and isinstance(per_thread, int) + assert block - threads < per_thread * threads <= block + short = AMICANative()._input_params(NW, 2048, {})["block_size"] + assert isinstance(short, int) and 0 < short * threads <= 2048 + given = AMICANative(block_size=512)._input_params(NW, N_SAMPLES, {}) + assert given["block_size"] == 512 # a given block_size is written as is + + +def test_native_input_param_writes_every_setting_it_can(tmp_path): + """Every pamica setting with a binary keyword and a non-None default is + written; the ones without a keyword (keep_best, mineig_rel, ...) cannot + be, and the None defaults (pcadb, seed) are left to the binary.""" + torch_defaults = _torch_defaults() + settings = _native_settings(tmp_path) + writable = { + _TORCH_SPELLING.get(key, key) for key in set(FORTRAN_TO_PAMICA_KEY.values()) + } & set(torch_defaults) + expected = {name for name in writable if torch_defaults[name] is not None} + written = {_TORCH_SPELLING.get(key, key) for key in settings} + assert expected <= written, sorted(expected - written) + for name in ("keep_best", "mineig_rel", "n_restarts", "pcadb", "seed"): + assert name not in written, name diff --git a/pamica/tests/test_llt_stash.py b/pamica/tests/test_llt_stash.py index 90b7627c..a4882937 100644 --- a/pamica/tests/test_llt_stash.py +++ b/pamica/tests/test_llt_stash.py @@ -179,7 +179,8 @@ def test_native_binary_llt_is_the_last_estep(tmp_path): Opt-in (``AMICA_RUN_FORTRAN=1``) like the other binary-driven tests, so the default suite does not depend on a runnable ``amica15mac``. Uses the whole record, as the native-engine tests do: the binary NaNs on a short slice at - its default 512 block size (issue #292), which would test nothing here. + block size 512 (issue #292, the bundled input.param's size and the + engine's default until issue #354), which would test nothing here. """ from pamica import AMICANative diff --git a/pamica/tests/test_native_engine.py b/pamica/tests/test_native_engine.py index 5d843435..bce5fa90 100644 --- a/pamica/tests/test_native_engine.py +++ b/pamica/tests/test_native_engine.py @@ -217,8 +217,8 @@ def test_native_engine_asymmetric_sphere_reads_the_right_orientation(real_data): @requires_binary def test_native_engine_param_aliases_and_multimodel(real_data): # n_models/n_mix aliases reach the Fortran num_models/num_mix_comps. Full data - # + enough iterations so the 2-model fit converges (a too-short multi-model run - # collapses -- exercised separately below). + # + enough iterations for a finite 2-model fit (a slice too short for its + # block ends with NaN weights -- exercised separately below). eng = AMICANative(binary=_BINARY, n_models=2, n_mix=3, max_iter=15, threads=2) eng.fit(real_data) assert eng.output_ is not None @@ -229,10 +229,13 @@ def test_native_engine_param_aliases_and_multimodel(real_data): @requires_binary def test_native_engine_degenerate_fit_raises_clearly(real_data): - # A too-short multi-model fit collapses to non-finite weights; the engine must - # report that as a degenerate fit, not let loadmodout's pinv raise an opaque - # SVD error (cf. the #50 degenerate-fit contract). - eng = AMICANative(binary=_BINARY, n_models=2, max_iter=3, threads=2) + # A run that ends with non-finite weights must be reported as a degenerate + # fit, not let loadmodout's pinv raise an opaque SVD error (cf. the #50 + # degenerate-fit contract). block_size=512 on 2048 samples produces one: + # the binary runs 2048 // (max_threads * 512) = 0 blocks and writes NaN + # weights (issue #292). The engine's own default block leaves it a block + # (issue #354), so this test pins the value that fails. + eng = AMICANative(binary=_BINARY, n_models=2, max_iter=3, threads=2, block_size=512) with pytest.raises(RuntimeError, match="degenerate"): eng.fit(real_data[:, :2048]) diff --git a/pamica/tests/test_pamica.py b/pamica/tests/test_pamica.py index 82b89678..338dc954 100644 --- a/pamica/tests/test_pamica.py +++ b/pamica/tests/test_pamica.py @@ -37,7 +37,10 @@ def test_amica_initialization(): # Test default initialization model = AMICA() assert model.num_models == 1 - assert model.max_iter == 2000 + # The other backends' defaults since issue #354 (params.json had 2000 and + # Newton on); test_default_settings.py checks every shared setting. + assert model.max_iter == 100 + assert model.do_newton is False # Test custom parameters model = AMICA(num_models=2, max_iter=500, do_newton=True) diff --git a/pamica/tests/test_schedule_native_oracle.py b/pamica/tests/test_schedule_native_oracle.py index bdec0a25..d69635f6 100644 --- a/pamica/tests/test_schedule_native_oracle.py +++ b/pamica/tests/test_schedule_native_oracle.py @@ -41,7 +41,8 @@ import torch from pamica.native import resolver -from pamica.native.engine import _DEFAULT_PARAMS, _render_param +from pamica.native.engine import _render_param +from pamica.tests.native_oracle import REFERENCE_RUN_PARAMS from pamica.numpy_impl.core import AMICA as AMICA_NumPy from pamica.torch_impl.core import AMICATorchNG from pamica.torch_impl.utils import load_eeglab_data @@ -141,7 +142,9 @@ def _seeded_reference_run(X: np.ndarray, workdir: Path) -> dict[str, np.ndarray] _write_f64(indir / name, value.numpy()) X.astype(np.float32).ravel(order="F").tofile(workdir / "data.fdt") - params = dict(_DEFAULT_PARAMS) + # The bundled input.param, the configuration this oracle was measured + # against (AMICANative's defaults until issue #354). + params = dict(REFERENCE_RUN_PARAMS) params.update( {k: v for k, v in _CONFIG.items() if k not in ("maxdecs", "doscaling")} ) diff --git a/pamica/tests/test_wrapper_backends.py b/pamica/tests/test_wrapper_backends.py index c374f47c..169fe79a 100644 --- a/pamica/tests/test_wrapper_backends.py +++ b/pamica/tests/test_wrapper_backends.py @@ -410,6 +410,62 @@ def test_params_file_warning_names_the_torch_backend(X, caplog): assert not set(keys) & set(inspect.signature(AMICATorchNG).parameters) +# --- the wrapper's defaults are the backend's (issue #354) ------------------------ +_FIT_NAMED = ("max_iter", "lrate", "do_mean", "do_sphere", "do_newton") + + +def _signature_defaults(cls) -> dict: + """The defaults of the five settings ``AMICA.fit`` names, read off + ``cls`` itself: ``max_iter`` from ``cls.fit``, the rest from ``cls``.""" + ctor = inspect.signature(cls).parameters + fit = inspect.signature(cls.fit).parameters + return { + name: (fit if name == "max_iter" else ctor)[name].default for name in _FIT_NAMED + } + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_wrapper_resolves_the_backends_own_defaults(backend): + """Until issue #354 the wrapper restated these defaults, and its lrate + (0.05) differed from every backend's (0.1).""" + from pamica.amica import _fit_defaults + + cls = _backend_class(backend) + assert _fit_defaults(cls) == _signature_defaults(cls) + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_default_wrapper_fit_is_the_default_backend_fit(X, backend): + """``AMICA().fit(X)``, with none of the five named settings passed, runs + the same fit as the backend class built with its own defaults (the full + default ``max_iter``, so on a shorter slice to stay cheap).""" + cls = _backend_class(backend) + placement = {"device": "cpu"} if backend == "torch" else {} + data = X[:, :4096] + model = _wrapper(backend) + model.fit(data, seed=SEED) + raw = cls(n_channels=NW, seed=SEED, **placement) + raw.fit(data, verbose=False) + + b = model.model_ + assert b is not None + defaults = _signature_defaults(cls) + assert b.lrate0 == raw.lrate0 == defaults["lrate"] + assert b.do_mean is raw.do_mean is defaults["do_mean"] + assert b.do_sphere is raw.do_sphere is defaults["do_sphere"] + assert b.do_newton is raw.do_newton is defaults["do_newton"] + assert len(model.ll_history_) == len(raw.ll_history) <= defaults["max_iter"] + assert model.ll_history_ == raw.ll_history + assert model.stop_reason_ == raw.stop_reason + + +@pytest.mark.parametrize("backend", BACKENDS) +def test_explicit_lrate_overrides_the_backend_default(X, backend): + model = _wrapper(backend) + model.fit(X, max_iter=1, lrate=0.05, seed=SEED) + assert model.model_ is not None and model.model_.lrate0 == 0.05 + + # --- pcakeep through the wrapper on MLX (#323's remaining bullet) --------------------- def test_pcakeep_through_the_mlx_wrapper(X): _require_mlx() @@ -685,8 +741,9 @@ def test_torch_and_mlx_wrappers_find_the_same_components(real_data, pcakeep): number of components, the same sources (Hungarian-matched |corr| at the bar of test_pca_reduction_cross_backend.py, which this reuses), and the same log-likelihood to MLX's float32 bar. Measured on the full sample, 10 - iterations: min matched |corr| 0.99999996 full rank / 0.99999993 at - pcakeep=20, identity matching, LL difference 1.3e-6 / 4.8e-6.""" + iterations at the default lrate (0.1 since issue #354): min matched |corr| + 0.99999995 both full rank and at pcakeep=20, identity matching, LL + difference 2.2e-6 / 1.4e-6.""" from pamica.tests.test_pca_reduction_cross_backend import _matched_abs_corr kwargs = {} if pcakeep is None else {"pcakeep": pcakeep} diff --git a/pamica/tests/torch_tests/test_amica_ng_wrapper.py b/pamica/tests/torch_tests/test_amica_ng_wrapper.py index f5f4f019..aceec867 100644 --- a/pamica/tests/torch_tests/test_amica_ng_wrapper.py +++ b/pamica/tests/torch_tests/test_amica_ng_wrapper.py @@ -3,7 +3,9 @@ These cover the wiring the wrapper adds on top of ``AMICATorchNG``: the save/load round-trip (issue #36) and the device-selection fallback that keeps the float64 parity default from crashing on Apple Silicon (MPS cannot -represent float64). Real sample EEG data only (no synthetic/mock). +represent float64). Since issue #354 that fallback lives in the backend's +constructor, so its tests here cover the raw ``AMICATorchNG`` as well as the +wrapper that relies on it. Real sample EEG data only (no synthetic/mock). """ import logging @@ -104,20 +106,95 @@ def test_ng_load_rejects_unknown_version(fitted_ng, tmp_path): AMICA.load(path) +_HAS_MPS = torch.backends.mps.is_available() +_BACKEND_LOGGER = "pamica.torch_impl.core" + + +def _fallback_warnings(caplog) -> list: + return [ + r + for r in caplog.records + if r.name == _BACKEND_LOGGER and "auto-selected MPS" in r.getMessage() + ] + + def test_ng_default_device_avoids_mps_float64(real_data, caplog): """The default float64 NG config must not crash when the auto-selected - device is MPS; the wrapper falls back to CPU (regression for #29).""" + device is MPS; the backend falls back to CPU, and the wrapper relies on it + (regression for #29, moved into the backend by issue #354).""" model = AMICA(n_models=1, n_mix=3, verbose=False) # device=None - with caplog.at_level(logging.WARNING, logger="pamica.amica"): + with caplog.at_level(logging.WARNING, logger=_BACKEND_LOGGER): model.fit(real_data[:, :2048], max_iter=2, block_size=1024, seed=42) # float64 parity runs must never land on MPS. assert isinstance(model.model_, AMICATorchNG) assert model.model_.device.type in ("cpu", "cuda") - if torch.backends.mps.is_available(): + if _HAS_MPS: assert model.model_.device.type == "cpu" - # The downgrade must be announced even with verbose=False (not silent). - assert any("float64" in r.message for r in caplog.records) + # The downgrade must be announced even with verbose=False (not silent), + # once per fit. + assert len(_fallback_warnings(caplog)) == 1 + + +# --- the raw backend's automatic device choice (issue #354) -------------------------- +@pytest.mark.skipif(not _HAS_MPS, reason="the fallback needs an MPS device") +def test_raw_backend_default_construction_uses_cpu_on_mps(caplog): + """``AMICATorchNG(n_channels)`` with default arguments raised ValueError on + every Apple Silicon Mac: auto-selection picks MPS, which cannot hold the + float64 default. It now resolves to the CPU and says so.""" + with caplog.at_level(logging.WARNING, logger=_BACKEND_LOGGER): + model = AMICATorchNG(n_channels=NW) + assert model.device.type == "cpu" + assert model.dtype == torch.float64 + (record,) = _fallback_warnings(caplog) + assert record.levelno == logging.WARNING + assert "dtype=torch.float32" in record.getMessage() + + +@pytest.mark.skipif(not _HAS_MPS, reason="the fallback needs an MPS device") +def test_raw_backend_default_fit_runs_on_cpu_on_mps(real_data): + """The fallback holds through a fit, which places every tensor on the + resolved device.""" + model = AMICATorchNG(n_channels=NW, block_size=1024, seed=42) + model.fit(real_data[:, :2048], max_iter=2, verbose=False) + assert model.stop_reason == "max_iter" + assert model.W is not None + assert model.W.device.type == "cpu" and model.W.dtype == torch.float64 + + +@pytest.mark.skipif(not _HAS_MPS, reason="needs an MPS device to auto-select") +def test_raw_backend_float32_auto_selection_keeps_mps(caplog): + with caplog.at_level(logging.WARNING, logger=_BACKEND_LOGGER): + model = AMICATorchNG(n_channels=NW, dtype=torch.float32) + assert model.device.type == "mps" + assert _fallback_warnings(caplog) == [] + + +def test_raw_backend_explicit_mps_float64_raises(): + """A caller who names MPS gets the error instead of a move to the CPU. + Raised at construction, before any tensor is placed, so no MPS hardware + is needed.""" + with pytest.raises(ValueError, match="MPS does not support float64"): + AMICATorchNG(n_channels=NW, device="mps") + + +def test_load_without_a_device_uses_the_backend_choice(fitted_ng, tmp_path, caplog): + """``AMICA.load(path)`` with ``device=None`` rebuilds the model through the + backend constructor, so a float64 model lands where a fresh one would: the + CPU on a host whose automatic choice is MPS.""" + path = str(tmp_path / "model.pt") + fitted_ng.save(path) + with caplog.at_level(logging.WARNING, logger=_BACKEND_LOGGER): + loaded = AMICA.load(path) + assert isinstance(loaded.model_, AMICATorchNG) + assert loaded.model_.dtype == torch.float64 + assert loaded.model_.device.type in ("cpu", "cuda") + if _HAS_MPS: + assert loaded.model_.device.type == "cpu" + assert len(_fallback_warnings(caplog)) == 1 + np.testing.assert_array_equal( + loaded.get_unmixing_matrix(), fitted_ng.get_unmixing_matrix() + ) def test_ng_explicit_mps_float64_raises(real_data): diff --git a/pamica/torch_impl/core.py b/pamica/torch_impl/core.py index ad158302..33d1c2c2 100644 --- a/pamica/torch_impl/core.py +++ b/pamica/torch_impl/core.py @@ -690,12 +690,13 @@ class AMICATorchNG: When omitted the seeds are ``seed, seed + 1, ..., seed + n_restarts - 1``. device : str or torch.device, optional Compute device for the block loop. ``None`` picks MPS, then CUDA, - then CPU, whichever is available first; MPS has no float64, so with - the default ``dtype`` on an Apple machine the constructor raises - ``ValueError`` and ``device="cpu"`` must be passed (the - :class:`~pamica.AMICA` wrapper redirects an automatic MPS pick to CPU - itself). Preprocessing (mean/cov/eigh) is always done in float64 on - CPU regardless of device, since eigh is not reliably supported on MPS. + then CPU, whichever is available first. MPS has no float64, so when + ``None`` picks MPS for a float64 run (the default ``dtype``) the + constructor uses the CPU instead and logs a warning; with + ``dtype=torch.float32``, ``None`` keeps MPS. An explicit + ``device="mps"`` with float64 raises ``ValueError``. Preprocessing + (mean/cov/eigh) is always done in float64 on CPU regardless of device, + since eigh is not reliably supported on MPS. dtype : torch.dtype, default=torch.float64 Parameter/computation dtype. float64 is the parity default (Fortran bit-parity) and ~4.5x on CUDA over CPU (issue #63). float32 converges on @@ -962,6 +963,16 @@ def __init__( if device is None: device = setup_device() + # MPS has no float64, so an automatic MPS pick for a float64 run + # (the parity default) moves to the CPU instead of raising below. + # An explicit device="mps" is the caller's choice and still raises. + if device.type == "mps" and dtype == torch.float64: + logger.warning( + "AMICATorchNG: auto-selected MPS cannot represent float64 " + "(the parity default), so running on CPU. Pass " + "dtype=torch.float32 with device='mps' to run on MPS." + ) + device = torch.device("cpu") elif isinstance(device, str): device = torch.device(device) self.device = device diff --git a/pyproject.toml b/pyproject.toml index 3de2ee6f..3747c5b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ Repository = "https://github.com/sccn/pAMICA" packages = ["pamica", "pamica.numpy_impl", "pamica.torch_impl", "pamica.mlx_impl", "pamica.metrics", "pamica.native", "pamica.mne_compat"] # numpy_impl/params.json is the NumPy backend's default parameter file, loaded at # runtime via importlib/__file__, so it must ship in the wheel. -package-data = {"pamica" = ["data/*"], "pamica.numpy_impl" = ["params.json"]} +package-data = {"pamica.numpy_impl" = ["params.json"]} [dependency-groups] dev = [ diff --git a/validate_implementations.py b/validate_implementations.py index eeed1c1d..f523a482 100644 --- a/validate_implementations.py +++ b/validate_implementations.py @@ -466,8 +466,8 @@ def _run_wrapper_amica(backend: str, data: np.ndarray, params: Dict, seed: int) ``"mlx"``) and collect results. The two backends take the same constructor keywords, so the canonical - params map onto both the same way; the wrapper's own ``fit`` resolves the - PyTorch device (with its MPS/float64 -> CPU fallback). + params map onto both the same way; ``AMICATorchNG`` resolves the PyTorch + device itself (with its MPS/float64 -> CPU fallback, issue #354). """ if backend == "torch": backend_cls, short = AMICATorchNG, "NG" From 880d71d1dccc5c78bcd5483c17ad9e84fc35d200 Mon Sep 17 00:00:00 2001 From: "Seyed (Yahya) Shirazi" Date: Wed, 23 Sep 2026 23:52:49 -0700 Subject: [PATCH 20/26] Phase 15: re-measure parity under the finished epic (#356) * Add issue-351 Newton independent-seed study script * Add hallu driver for the Newton seed study * Record harness, same-start and fixture parity runs * Record share_comps counts and float32 agreement runs * Update share_comps merge counts in differences guide * Re-state the harness rows with same-start parity * Add an MLX float32 option to the Newton seed study * Mark the issue-27 and issue-51 records as pre-epic * Update the harness figures in AGENTS and progress summary * Add ensemble scripts and dimsweep LL records * Add the hallu CUDA dimsweep LL record * Re-state the cross-backend LL table with the chaos caveat * Reword the harness and cross-backend notes * Pin the ensemble lrate in reproduce_table1 * Add basin and pre-epic ensemble scripts * Record the bundled tier, basin, ensemble and MLX Newton runs * Add the equivalence-margin check on saved ensembles * Record the seeded keep_best ensemble * Add the re-measured keep_best figures to ADR 0003 * Name the pre-change code precisely in validation guide * Regenerate the multi-model ensemble figure * Rebuild paper.pdf * Re-state the bundled single-model and multi-model results * Update keep_best and multi-model figures in AGENTS and guides * Update the fixture parity figures in AGENTS and progress summary * Record the bundled tier wall-clock on the M4 Pro * Update the cross-backend LL claim in the backends guide * Record the Amari pairing-correction measurement * Update paper Table 1 bundled rows and the float32 paragraph * Clarify the benchmark excerpt in the paper * Rebuild paper.pdf * Record the gated oracle and full-suite runs * Add the Phase 15 re-measurement to the changelog * Record the Newton independent-seed fits * Record the same-session 70-channel timing check * Record the unseeded-start check of the issue-145 binary * Mark the sections of the validation guide that predate the epic * Pin every reference setting in the Table 1 and run scripts * Re-state the Newton-enabled basin results * Give both matching seeds in the paper * Rebuild paper.pdf * Document the ensemble's invsigmin difference * Smooth the ensemble protocol sentence * Address the docs-claims review findings * Address the paper review findings * Give each ensemble distribution its own line style * Label the external single-model figures as pre-epic * Reflow the README parity sentence * Record the external tier run * Fill the external-tier figures and fix rounding slips * Rebuild paper.pdf * Save the pinned parameter files before Phase 17 * Re-check the reference pins after Phase 17 * Pin do_approx_sphere in the reference settings * Record the test runs after the Phase 17 merge * Add the issue-351 findings record * Reword two remaining contrast phrasings in the paper * Rebuild paper.pdf --------- Co-authored-by: github-actions[bot] --- .../decisions/0003-best-iterate-safeguard.md | 49 + .../multimodel_distributional_equivalence.md | 7 + .context/issue-351/_reference_settings.py | 52 + .context/issue-351/bundled_single_basins.py | 114 + .context/issue-351/equivalence_check.py | 100 + .context/issue-351/findings.md | 158 + .context/issue-351/fixture_parity.py | 82 + .context/issue-351/harness_gap.py | 194 + .context/issue-351/keep_best_ensemble.py | 236 + .context/issue-351/multimodel_ensemble.py | 287 ++ .../multimodel_ensemble_distributions.pdf | Bin 0 -> 43454 bytes .../multimodel_ensemble_distributions.png | Bin 0 -> 159225 bytes .context/issue-351/multimodel_pamica_fits.py | 62 + .context/issue-351/multimodel_summary.json | 70 + .../multimodel_summary_pre_epic_e38aa11.json | 70 + .context/issue-351/newton_seeds.py | 324 ++ .context/issue-351/orientation_check.py | 85 + .context/issue-351/per_run_detail.csv | 41 + .../per_run_detail_pre_epic_e38aa11.csv | 41 + .context/issue-351/pin_check.py | 232 + .context/issue-351/pin_run_check.py | 114 + .context/issue-351/precision_agreement.py | 54 + .../bundled_basins/bundled_single_basins.json | 560 +++ .../bundled_basins/bundled_single_basins.npz | Bin 0 -> 124574 bytes .../issue-351/raw/bundled_basins/stdout.log | 33 + .../issue-351/raw/dimsweep/dimsweep_cuda.json | 80 + .../issue-351/raw/dimsweep/dimsweep_cuda.log | 9 + .../raw/dimsweep/dimsweep_mac_ll.json | 127 + .../raw/dimsweep/dimsweep_mac_ll.log | 15 + .../issue-351/raw/dimsweep/timing70_head.json | 55 + .../dimsweep/timing70_pre_epic_e38aa11.json | 55 + .../issue-351/raw/fixture_parity_head.txt | 3 + .context/issue-351/raw/gated_tests.log | 2 + .../raw/gated_tests_post_phase17.log | 109 + .context/issue-351/raw/harness_gap_head.json | 65 + .context/issue-351/raw/harness_gap_head.log | 131 + .../raw/harness_gap_pre_epic_e38aa11.json | 21 + .../raw/harness_gap_pre_epic_e38aa11.log | 30 + .../fortran_run/fortran_output/out.txt | 108 + .../fortran_run/input.param | 82 + .../parity_summary.md | 6 + .../raw/harness_pre_epic_e38aa11/stdout.log | 240 + .../validation_report.txt | 33 + .../validation_report_mlx.txt | 33 + .../validation_report_numpy.txt | 33 + .../fortran_run/fortran_output/out.txt | 108 + .../raw/harness_run1/fortran_run/input.param | 82 + .../raw/harness_run1/parity_summary.md | 6 + .../issue-351/raw/harness_run1/stdout.log | 240 + .../raw/harness_run1/validation_report.txt | 33 + .../harness_run1/validation_report_mlx.txt | 33 + .../harness_run1/validation_report_numpy.txt | 33 + .../fortran_run/fortran_output/out.txt | 108 + .../raw/harness_run2/fortran_run/input.param | 82 + .../raw/harness_run2/parity_summary.md | 6 + .../issue-351/raw/harness_run2/stdout.log | 240 + .../raw/harness_run2/validation_report.txt | 33 + .../harness_run2/validation_report_mlx.txt | 33 + .../harness_run2/validation_report_numpy.txt | 33 + .../issue-351/raw/keep_best/ensemble.json | 71 + .context/issue-351/raw/keep_best/ensemble.npz | Bin 0 -> 98750 bytes .context/issue-351/raw/keep_best/stdout.log | 93 + .context/issue-351/raw/keep_best/verify.json | 18 + .../raw/multimodel/equivalence_check.json | 68 + .../raw/multimodel/pairing_correction.txt | 1 + .../pre_epic_e38aa11_vs_bundled_fortran.npz | Bin 0 -> 656654 bytes .../raw/multimodel/pre_epic_stdout.log | 30 + .../issue-351/raw/newton/fortran_fixinit.npz | Bin 0 -> 95652 bytes .../issue-351/raw/newton/fortran_seed1.npz | Bin 0 -> 95652 bytes .../issue-351/raw/newton/fortran_seed2.npz | Bin 0 -> 95652 bytes .context/issue-351/raw/newton/gpu_queue2.log | 2 + .../raw/newton/newton_seeds_report.json | 238 + .../issue-351/raw/newton/pamica_fixinit.npz | Bin 0 -> 96478 bytes .../issue-351/raw/newton/pamica_seed13.npz | Bin 0 -> 96478 bytes .../issue-351/raw/newton/pamica_seed42.npz | Bin 0 -> 96478 bytes .../issue-351/raw/newton/pamica_seed7.npz | Bin 0 -> 96478 bytes .../raw/newton_mlx/pamica_seed42_mlx.npz | Bin 0 -> 68314 bytes .../raw/newton_mlx/pamica_seed7_mlx.npz | Bin 0 -> 68506 bytes .context/issue-351/raw/newton_mlx/stdout.log | 4 + .context/issue-351/raw/orientation_check.txt | 10 + .context/issue-351/raw/pin_check.json | 34 + .../pin_check_pinned_params_pre_phase17.json | 10 + .../issue-351/raw/pin_check_post_phase17.json | 82 + .../raw/pin_run_check_post_phase17.json | 128 + .../issue-351/raw/precision_agreement.json | 20 + .context/issue-351/raw/pytest_full.log | 52 + .../raw/pytest_full_post_phase17.log | 4063 +++++++++++++++++ .../raw/seedcheck_issue145_binary/run1.log | 127 + .../raw/seedcheck_issue145_binary/run2.log | 127 + .../seedcheck_issue145_binary/seedcheck.txt | 6 + .../raw/share_comps/share_comps_counts.json | 117 + .context/issue-351/raw/share_comps/stdout.log | 140 + .../bundled_multimodel_ensemble.npz | Bin 0 -> 656654 bytes .../issue-351/raw/table1_bundled/report.json | 59 + .../issue-351/raw/table1_bundled/stdout.log | 99 + .../issue-351/raw/table1_external/report.json | 28 + .../issue-351/raw/table1_external/stdout.log | 42 + .context/issue-351/run_newton_hallu.sh | 34 + .context/issue-351/share_comps_counts.py | 229 + .context/issue-51/best_iterate_safeguard.md | 6 + .context/progress_summary.md | 23 +- AGENTS.md | 43 +- README.md | 7 +- benchmarks/README_dimsweep.md | 25 +- benchmarks/reproduce_table1.py | 66 +- docs/assets/figures/multimodel-ensemble.png | Bin 147783 -> 159225 bytes docs/changelog.md | 9 + docs/guides/amica-differences.md | 16 +- docs/guides/backends.md | 4 +- docs/guides/validation.md | 336 +- paper.md | 90 +- paper.pdf | Bin 375572 -> 385666 bytes 112 files changed, 11629 insertions(+), 200 deletions(-) create mode 100644 .context/issue-351/_reference_settings.py create mode 100644 .context/issue-351/bundled_single_basins.py create mode 100644 .context/issue-351/equivalence_check.py create mode 100644 .context/issue-351/findings.md create mode 100644 .context/issue-351/fixture_parity.py create mode 100644 .context/issue-351/harness_gap.py create mode 100644 .context/issue-351/keep_best_ensemble.py create mode 100644 .context/issue-351/multimodel_ensemble.py create mode 100644 .context/issue-351/multimodel_ensemble_distributions.pdf create mode 100644 .context/issue-351/multimodel_ensemble_distributions.png create mode 100644 .context/issue-351/multimodel_pamica_fits.py create mode 100644 .context/issue-351/multimodel_summary.json create mode 100644 .context/issue-351/multimodel_summary_pre_epic_e38aa11.json create mode 100644 .context/issue-351/newton_seeds.py create mode 100644 .context/issue-351/orientation_check.py create mode 100644 .context/issue-351/per_run_detail.csv create mode 100644 .context/issue-351/per_run_detail_pre_epic_e38aa11.csv create mode 100644 .context/issue-351/pin_check.py create mode 100644 .context/issue-351/pin_run_check.py create mode 100644 .context/issue-351/precision_agreement.py create mode 100644 .context/issue-351/raw/bundled_basins/bundled_single_basins.json create mode 100644 .context/issue-351/raw/bundled_basins/bundled_single_basins.npz create mode 100644 .context/issue-351/raw/bundled_basins/stdout.log create mode 100644 .context/issue-351/raw/dimsweep/dimsweep_cuda.json create mode 100644 .context/issue-351/raw/dimsweep/dimsweep_cuda.log create mode 100644 .context/issue-351/raw/dimsweep/dimsweep_mac_ll.json create mode 100644 .context/issue-351/raw/dimsweep/dimsweep_mac_ll.log create mode 100644 .context/issue-351/raw/dimsweep/timing70_head.json create mode 100644 .context/issue-351/raw/dimsweep/timing70_pre_epic_e38aa11.json create mode 100644 .context/issue-351/raw/fixture_parity_head.txt create mode 100644 .context/issue-351/raw/gated_tests.log create mode 100644 .context/issue-351/raw/gated_tests_post_phase17.log create mode 100644 .context/issue-351/raw/harness_gap_head.json create mode 100644 .context/issue-351/raw/harness_gap_head.log create mode 100644 .context/issue-351/raw/harness_gap_pre_epic_e38aa11.json create mode 100644 .context/issue-351/raw/harness_gap_pre_epic_e38aa11.log create mode 100644 .context/issue-351/raw/harness_pre_epic_e38aa11/fortran_run/fortran_output/out.txt create mode 100644 .context/issue-351/raw/harness_pre_epic_e38aa11/fortran_run/input.param create mode 100644 .context/issue-351/raw/harness_pre_epic_e38aa11/parity_summary.md create mode 100644 .context/issue-351/raw/harness_pre_epic_e38aa11/stdout.log create mode 100644 .context/issue-351/raw/harness_pre_epic_e38aa11/validation_report.txt create mode 100644 .context/issue-351/raw/harness_pre_epic_e38aa11/validation_report_mlx.txt create mode 100644 .context/issue-351/raw/harness_pre_epic_e38aa11/validation_report_numpy.txt create mode 100644 .context/issue-351/raw/harness_run1/fortran_run/fortran_output/out.txt create mode 100644 .context/issue-351/raw/harness_run1/fortran_run/input.param create mode 100644 .context/issue-351/raw/harness_run1/parity_summary.md create mode 100644 .context/issue-351/raw/harness_run1/stdout.log create mode 100644 .context/issue-351/raw/harness_run1/validation_report.txt create mode 100644 .context/issue-351/raw/harness_run1/validation_report_mlx.txt create mode 100644 .context/issue-351/raw/harness_run1/validation_report_numpy.txt create mode 100644 .context/issue-351/raw/harness_run2/fortran_run/fortran_output/out.txt create mode 100644 .context/issue-351/raw/harness_run2/fortran_run/input.param create mode 100644 .context/issue-351/raw/harness_run2/parity_summary.md create mode 100644 .context/issue-351/raw/harness_run2/stdout.log create mode 100644 .context/issue-351/raw/harness_run2/validation_report.txt create mode 100644 .context/issue-351/raw/harness_run2/validation_report_mlx.txt create mode 100644 .context/issue-351/raw/harness_run2/validation_report_numpy.txt create mode 100644 .context/issue-351/raw/keep_best/ensemble.json create mode 100644 .context/issue-351/raw/keep_best/ensemble.npz create mode 100644 .context/issue-351/raw/keep_best/stdout.log create mode 100644 .context/issue-351/raw/keep_best/verify.json create mode 100644 .context/issue-351/raw/multimodel/equivalence_check.json create mode 100644 .context/issue-351/raw/multimodel/pairing_correction.txt create mode 100644 .context/issue-351/raw/multimodel/pre_epic_e38aa11_vs_bundled_fortran.npz create mode 100644 .context/issue-351/raw/multimodel/pre_epic_stdout.log create mode 100644 .context/issue-351/raw/newton/fortran_fixinit.npz create mode 100644 .context/issue-351/raw/newton/fortran_seed1.npz create mode 100644 .context/issue-351/raw/newton/fortran_seed2.npz create mode 100644 .context/issue-351/raw/newton/gpu_queue2.log create mode 100644 .context/issue-351/raw/newton/newton_seeds_report.json create mode 100644 .context/issue-351/raw/newton/pamica_fixinit.npz create mode 100644 .context/issue-351/raw/newton/pamica_seed13.npz create mode 100644 .context/issue-351/raw/newton/pamica_seed42.npz create mode 100644 .context/issue-351/raw/newton/pamica_seed7.npz create mode 100644 .context/issue-351/raw/newton_mlx/pamica_seed42_mlx.npz create mode 100644 .context/issue-351/raw/newton_mlx/pamica_seed7_mlx.npz create mode 100644 .context/issue-351/raw/newton_mlx/stdout.log create mode 100644 .context/issue-351/raw/orientation_check.txt create mode 100644 .context/issue-351/raw/pin_check.json create mode 100644 .context/issue-351/raw/pin_check_pinned_params_pre_phase17.json create mode 100644 .context/issue-351/raw/pin_check_post_phase17.json create mode 100644 .context/issue-351/raw/pin_run_check_post_phase17.json create mode 100644 .context/issue-351/raw/precision_agreement.json create mode 100644 .context/issue-351/raw/pytest_full.log create mode 100644 .context/issue-351/raw/pytest_full_post_phase17.log create mode 100644 .context/issue-351/raw/seedcheck_issue145_binary/run1.log create mode 100644 .context/issue-351/raw/seedcheck_issue145_binary/run2.log create mode 100644 .context/issue-351/raw/seedcheck_issue145_binary/seedcheck.txt create mode 100644 .context/issue-351/raw/share_comps/share_comps_counts.json create mode 100644 .context/issue-351/raw/share_comps/stdout.log create mode 100644 .context/issue-351/raw/table1_bundled/bundled_multimodel_ensemble.npz create mode 100644 .context/issue-351/raw/table1_bundled/report.json create mode 100644 .context/issue-351/raw/table1_bundled/stdout.log create mode 100644 .context/issue-351/raw/table1_external/report.json create mode 100644 .context/issue-351/raw/table1_external/stdout.log create mode 100644 .context/issue-351/run_newton_hallu.sh create mode 100644 .context/issue-351/share_comps_counts.py diff --git a/.context/decisions/0003-best-iterate-safeguard.md b/.context/decisions/0003-best-iterate-safeguard.md index cd86b446..4dd982c7 100644 --- a/.context/decisions/0003-best-iterate-safeguard.md +++ b/.context/decisions/0003-best-iterate-safeguard.md @@ -76,10 +76,59 @@ latter joined in Phase 3, issue #289) -- see `pamica/mlx_impl/core.py`'s `_snapshot_params`/`_restore_params` and `pamica/tests/mlx_tests/test_mlx_keepbest.py`. +## Re-measured after epic #324 (2026-09-23, issue #351) + +Epic #324 changed the default trajectory of every backend (component-row +`doscaling` #333, 1-based schedule gates #335, the reference's iteration order +and unconditional `A`-freeze #339/#345, the normalized initial `A` #341, +single-precision constants #344), so the figures above describe code that no +longer ships. The ensemble was re-run with the #51 protocol +(`.context/issue-351/keep_best_ensemble.py`: the sample EEG, `n_models=2`, the +#51 keyword set, N = 20, pamica seeds 0-19), with two changes: +the reference is the pinned v0.3.3 native binary, seeded 0-19 and +single-threaded (the #51 runs used the clock-seeded `amica15mac`), and each +fit runs 300 iterations once, with the 100- and 200-iteration values read from +its trajectory (checked equal to separate 100-iteration fits, with `keep_best` +on and off, on two seeds on both sides). + +| budget | Fortran mean (sd) | pamica return-last mean (sd) | pamica `keep_best` mean (sd) | sd ratio | pamica minus Fortran | Kolmogorov-Smirnov (KS) p | restores | +|---:|---:|---:|---:|---:|---:|---:|---:| +| 100 | -3.3550 (0.0029) | -3.3542 (0.0029) | -3.3542 (0.0029) | 1.0x | +0.0008 | 0.83 | 0 of 20 | +| 200 | -3.3418 (0.0030) | -3.3416 (0.0022) | -3.3416 (0.0022) | 0.75x | +0.0002 | 0.57 | 0 of 20 | +| 300 | -3.3392 (0.0027) | -3.3391 (0.0020) | -3.3391 (0.0020) | 0.75x | +0.0001 | 0.98 | 1 of 20 | + +- The late overshoots that motivated this decision do not occur in these + fits: the largest likelihood decrease in any of the 20 pamica trajectories + is 1.2e-5, and none of them used a natural-gradient fallback. At 100 + iterations the return-last and `keep_best` distributions are identical, and + their spread equals the reference's (sd ratio 1.0x, where #51 measured 12.7x + and 2.0x). Phase 7 of the epic (#340) traced most of the old overshoots to + the column-rule `doscaling` (#333). +- A restore fired in one of the 20 fits, and only at the 300-iteration budget: seed 3, + whose best iterate (iteration 299) beat its last by 4.2e-6. +- The mean gap is gone at every budget: pamica's mean lies 8.1e-4 above the + reference's at 100 iterations, 2.1e-4 at 200 and 1.0e-4 at 300, where #51 + measured -0.009 at 100 and attributed it to convergence speed. The + bundled tier's clock-seeded ensemble (`benchmarks/reproduce_table1.py`, + `AMICA` seeds 1-20) agrees: -3.3541 against -3.3543, KS p 0.83. Refitting + that ensemble's pamica half with the code before the epic's changes to the fit (e38aa11) against + the same 20 reference fits gives -3.3627 (sd 0.006, KS p 1e-5), close to + the #27/#51 figures. Seven of those 20 fits stopped early on `min_dll`, + whose check counted likelihood dips as small gains until #339 (their mean + -3.3679), and the 13 that ran the full 100 iterations average -3.3600, so + the old gap came partly from the early stops and partly from the update + rule of that code. +- The decision stands: `keep_best` stays on by default. With these fits it + rarely acts, and when it does the gain is small, but a run that ends below + its peak (a likelihood decrease near the end, or a Newton fallback) still + returns its best iterate. + ## Receipts - `pamica/torch_impl/amica_torch_ng.py` (`keep_best`, `_snapshot_params`/ `_restore_params`, `final_ll_`, `_KEEP_BEST_TOL`). - `pamica/tests/torch_tests/test_ng_backend.py::test_keep_best_*`. - `.context/issue-51/ensemble_ll.py` (Fortran-vs-NG LL ensemble, real data). +- `.context/issue-351/keep_best_ensemble.py` and `raw/keep_best/` (the + re-measurement above), `.context/issue-351/findings.md`. - Fortran schedule: `pamica/amica15.f90:1038-1058` (anneal-on-decrease). diff --git a/.context/issue-27/multimodel_distributional_equivalence.md b/.context/issue-27/multimodel_distributional_equivalence.md index 01ef0b32..4f9f2229 100644 --- a/.context/issue-27/multimodel_distributional_equivalence.md +++ b/.context/issue-27/multimodel_distributional_equivalence.md @@ -1,5 +1,12 @@ # Multi-model AMICA parity: distributional equivalence to Fortran (issue #27) +> **Historical record.** These ensembles predate epic #324, which changed every +> backend's default trajectory (issues #333, #335, #339, #341, #344, #345). +> The re-measured study under the finished epic, with the same protocol against +> the pinned v0.3.3 native binary, is in `.context/issue-351/findings.md` +> (script `.context/issue-351/multimodel_ensemble.py`, which reuses this +> directory's analysis and figure code). + **Bottom line.** For multi-model AMICA (`n_models > 1`), the natural-gradient PyTorch backend (`AMICATorchNG`) is validated against the Fortran reference at the level that is actually well-posed: its **ensemble of solutions is diff --git a/.context/issue-351/_reference_settings.py b/.context/issue-351/_reference_settings.py new file mode 100644 index 00000000..ac1f46a5 --- /dev/null +++ b/.context/issue-351/_reference_settings.py @@ -0,0 +1,52 @@ +"""The reference settings of ``benchmarks/reproduce_table1.py``, shared by this +directory's scripts so that every native-binary run passes its protocol's +settings explicitly (a change of ``AMICANative``'s defaults cannot move them). + +``REFERENCE_SETTINGS`` is read from the script's source without importing it, +so a script that imports an older pamica tree can use it too. +""" + +from __future__ import annotations + +import ast +import importlib.util +import sys +from pathlib import Path +from typing import Any + +_TABLE1 = Path(__file__).resolve().parents[2] / "benchmarks/reproduce_table1.py" +_NAME = "reproduce_table1_351" + + +def _literal(name: str) -> dict[str, Any]: + for node in ast.parse(_TABLE1.read_text()).body: + if ( + isinstance(node, ast.AnnAssign) + and isinstance(node.target, ast.Name) + and node.target.id == name + and node.value is not None + ): + return dict(ast.literal_eval(node.value)) + raise LookupError(f"{name} not found in {_TABLE1}") + + +REFERENCE_SETTINGS: dict[str, Any] = _literal("REFERENCE_SETTINGS") + + +def _table1(): + if _NAME in sys.modules: + return sys.modules[_NAME] + spec = importlib.util.spec_from_file_location(_NAME, _TABLE1) + assert spec is not None and spec.loader is not None + mod = importlib.util.module_from_spec(spec) + sys.modules[_NAME] = mod + spec.loader.exec_module(mod) + return mod + + +def single_model_reference_kwargs(max_iter: int) -> dict[str, Any]: + return _table1().single_model_reference_kwargs(max_iter) + + +def multimodel_reference_kwargs(max_iter: int) -> dict[str, Any]: + return _table1().multimodel_reference_kwargs(max_iter) diff --git a/.context/issue-351/bundled_single_basins.py b/.context/issue-351/bundled_single_basins.py new file mode 100644 index 00000000..f076d28b --- /dev/null +++ b/.context/issue-351/bundled_single_basins.py @@ -0,0 +1,114 @@ +"""Basins of the bundled tier's single-model comparison (issue #351 item 2). + +``benchmarks/reproduce_table1.py --tier bundled`` compares five pamica fits +(seeds 301-305) with five clock-seeded reference fits, one pair per seed, on +the bundled sample (32 channels, k ~ 30), Newton off, 2000 iterations, and +reports only the means. This script repeats that single-model protocol with +the unmixing matrices kept, so every pair can be compared: the same pamica +seeds (deterministic, so they reproduce the tier's pamica fits) and seeded +reference runs (``seed`` 1..N, reproducible), with the tier's keyword sets. + + uv run python .context/issue-351/bundled_single_basins.py OUT.npz [N_FORTRAN] [--threads T] +""" + +from __future__ import annotations + +import argparse +import importlib.util +import itertools +import json +import sys +from pathlib import Path + +import numpy as np + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[1] + + +def _table1(): + spec = importlib.util.spec_from_file_location( + "reproduce_table1_basins", REPO / "benchmarks/reproduce_table1.py" + ) + assert spec is not None and spec.loader is not None + mod = importlib.util.module_from_spec(spec) + sys.modules["reproduce_table1_basins"] = mod + spec.loader.exec_module(mod) + return mod + + +def fortran_kwargs(t1, seed: int, threads: int) -> dict: + """``AMICANative`` keywords of the reference runs: the tier's single-model + settings, every one explicit, plus the seed.""" + return dict( + threads=threads, max_threads=threads, timeout=3600, seed=seed, + **t1.single_model_reference_kwargs(2000), + ) # fmt: skip + + +def main() -> None: + p = argparse.ArgumentParser() + p.add_argument("out", type=Path) + p.add_argument("n_fortran", nargs="?", type=int, default=10) + p.add_argument("--threads", type=int, default=4) + p.add_argument("--pamica-seeds", default="301,302,303,304,305") + a = p.parse_args() + t1 = _table1() + from pamica import AMICA, AMICANative + + data, _ = t1.load_bundled_data() + binary = t1.resolve_binary(None, "v0.3.3") + runs: dict[str, dict] = {} + for s in range(1, a.n_fortran + 1): + eng = AMICANative(binary=binary, **fortran_kwargs(t1, s, a.threads)) + eng.fit(data) + assert eng.output_ is not None + runs[f"fortran_seed{s}"] = { + "W": eng.output_.W[:, :, 0], + "ll": float(eng.output_.LL[-1]), + } + print(f"fortran seed {s}: LL={runs[f'fortran_seed{s}']['ll']:.6f}", flush=True) + for s in [int(x) for x in a.pamica_seeds.split(",") if x]: + model = AMICA(n_models=1, n_mix=3, device="cpu", verbose=False) + model.fit( + data, max_iter=2000, lrate=0.05, do_mean=True, do_sphere=True, + do_approx_sphere=True, do_newton=False, seed=s, block_size=512, + minlrate=1e-8, lratefact=0.5, maxdecs=3, newt_start=50, newt_ramp=10, + newtrate=1.0, rho0=1.5, minrho=1.0, maxrho=2.0, rholrate=0.05, + rholratefact=0.5, invsigmin=0.0, invsigmax=100.0, doscaling=True, + scalestep=1, + ) # fmt: skip + runs[f"pamica_seed{s}"] = { + "W": model.get_unmixing_matrix(0), + "ll": float(model.final_ll_), + "iters": len(model.ll_history_), + "stop": str(model.stop_reason_), + } + print( + f"pamica seed {s}: LL={model.final_ll_:.6f} iters={len(model.ll_history_)} " + f"stop={model.stop_reason_}", + flush=True, + ) + names = list(runs) + np.savez( + a.out, + names=np.array(names), + W=np.stack([runs[k]["W"] for k in names]), + ll=np.array([runs[k]["ll"] for k in names]), + ) + rep: dict = {"ll": {k: runs[k]["ll"] for k in names}, "pairs": {}} + for x, y in itertools.combinations(names, 2): + c = t1.xcorr(runs[x]["W"], runs[y]["W"]) + rep["pairs"][f"{x} vs {y}"] = { + "mean_corr": float(c.mean()), + "min_corr": float(c.min()), + "amari": float(t1.amari_distance(runs[x]["W"], runs[y]["W"])), + } + for k in ("iters", "stop"): + rep[k] = {n: runs[n][k] for n in names if k in runs[n]} + a.out.with_suffix(".json").write_text(json.dumps(rep, indent=2)) + print(json.dumps(rep["ll"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/.context/issue-351/equivalence_check.py b/.context/issue-351/equivalence_check.py new file mode 100644 index 00000000..2fa3bd1b --- /dev/null +++ b/.context/issue-351/equivalence_check.py @@ -0,0 +1,100 @@ +"""The ±0.05 equivalence margin of the #27 multi-model study, on saved ensembles +(issue #351 item 3). No refitting. + +The #27 record states its acceptance criterion as "between not worse than +within-Fortran; TOST within a margin" of ±0.05 on the mean Hungarian-matched +correlation. Its first TOST was computed on the 190/400 pairwise values as if +they were independent, which the record later retired as pseudoreplicated (each +run appears in ~39 pairs) in favor of the run-level permutation test. For each +ensemble given, this script reports, for the correlation and the Amari distance: + +* ``diff``: mean(between) - mean(within-Fortran), and whether it lies inside + ±0.05 (the documented descriptive check, stated for the correlation only); +* ``pairwise_tost_p``: the retired pairwise TOST with the same margin, for + continuity with the old record (its p-value is not valid, as the record says); +* ``run_bootstrap_90ci``: a run-level bootstrap 90% interval of ``diff`` + (runs resampled with replacement within each implementation, 5000 draws), + which respects the shared-run dependence; an interval inside ±0.05 is the + run-level analogue of the TOST at the 5% level. + + uv run python .context/issue-351/equivalence_check.py OUT.json NAME=PATH.npz [NAME=PATH.npz ...] +""" + +from __future__ import annotations + +import importlib.util +import json +import sys +from pathlib import Path + +import numpy as np +from scipy import stats + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[1] +DELTA = 0.05 + + +def _load(path: Path, name: str): + spec = importlib.util.spec_from_file_location(name, path) + assert spec is not None and spec.loader is not None + mod = importlib.util.module_from_spec(spec) + sys.modules[name] = mod + spec.loader.exec_module(mod) + return mod + + +def analyze(Fs: np.ndarray, Gs: np.ndarray, metric) -> dict: + allW = np.concatenate([Fs, Gs]) + m, n = len(allW), len(Fs) + P = np.zeros((m, m)) + for i in range(m): + for j in range(i + 1, m): + P[i, j] = P[j, i] = metric(allW[i], allW[j]) + F = np.arange(n) + G = np.arange(n, m) + wF = P[np.ix_(F, F)][np.triu_indices(n, 1)] + bt = P[np.ix_(G, F)].ravel() + diff = float(bt.mean() - wF.mean()) + se = np.sqrt(bt.var(ddof=1) / bt.size + wF.var(ddof=1) / wF.size) + p_tost = float( + max(stats.norm.sf((diff + DELTA) / se), stats.norm.cdf((diff - DELTA) / se)) + ) + rng = np.random.default_rng(0) + boot = [] + for _ in range(5000): + f = rng.choice(F, n, replace=True) + g = rng.choice(G, len(G), replace=True) + # within-Fortran over distinct draws only (a run is not paired with itself) + sub = P[np.ix_(f, f)] + mask = f[:, None] != f[None, :] + w = sub[np.triu(mask, 1)].mean() + b = P[np.ix_(g, f)].mean() + boot.append(b - w) + lo, hi = np.percentile(boot, [5, 95]) + return { + "diff_between_minus_within_fortran": diff, + "inside_pm_0.05": bool(abs(diff) < DELTA), + "pairwise_tost_p_retired": p_tost, + "run_bootstrap_90ci": [float(lo), float(hi)], + "run_bootstrap_90ci_inside_pm_0.05": bool(lo > -DELTA and hi < DELTA), + } + + +def main() -> None: + out = Path(sys.argv[1]) + ama27 = _load(REPO / ".context/issue-27/amari_distance.py", "ama27_eq") + rep = {} + for arg in sys.argv[2:]: + name, path = arg.split("=", 1) + d = np.load(path) + rep[name] = { + "corr": analyze(d["Fs"], d["Gs"], ama27.xcorr), + "amari": analyze(d["Fs"], d["Gs"], ama27.model_amari), + } + print(name, json.dumps(rep[name], indent=1), flush=True) + out.write_text(json.dumps(rep, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/.context/issue-351/findings.md b/.context/issue-351/findings.md new file mode 100644 index 00000000..04255762 --- /dev/null +++ b/.context/issue-351/findings.md @@ -0,0 +1,158 @@ +# Phase 15 findings: parity re-measured under epic #324 (issue #351) + +Recorded 2026-09-23. +Every user-facing parity figure of pamica, the Python port of Adaptive Mixture Independent Component Analysis (AMICA), +was re-measured with the library code of epic #324 at 09b8247 (with Phase 16's documentation, #353) +against the pinned v0.3.3 native reference binary (`pamica.native.resolver`). +No library code changed in this phase. +Phase 17 (#354, bb22df6) was merged afterwards; the reference settings are pinned so that its default changes leave these protocols as measured (see "Pins after Phase 17"). + +Hosts: +- Mac: Apple M4 Pro, 14 cores, 64 GB, macOS 27; MLX 0.32.0, torch 2.12.1, NumPy 2.5.0. +- hallu: Linux, 32 cores, NVIDIA RTX 4090, torch 2.12.1+cu130; shared, with the reference binary kept to 8-12 threads in total. + +"Before" figures are those the documents quoted before this phase. +Where a before figure was re-measured in this phase, it used e38aa11 (Phase 6, the last commit before the epic's changes to the fit), run from a separate tree. +Paths are relative to `.context/issue-351/`. +Log-likelihood is abbreviated LL; correlations are Hungarian-matched absolute correlations of unmixing rows. + +## Old and new figures + +| Figure | Before | Now | Command | Commit | Host | Raw record | +|---|---|---|---|---|---|---| +| Harness, 100 iterations, seed 42, independent starts: LL gap PyTorch / NumPy / MLX | 2.9e-5 / 2.9e-5 / 3.2e-5 | 2.71e-4 / 2.72e-4 / 2.76e-4 | `validate_implementations.py --backend all` | 09b8247 (before: e38aa11) | Mac | `raw/harness_run1`, `raw/harness_run2`, `raw/harness_pre_epic_e38aa11` | +| Harness: mean / min correlation, Amari distance (PyTorch) | 0.9992 / 0.9935, 0.0037 | 0.9991 / 0.9918, 0.0038 (NumPy 0.9991 / 0.9917) | same | same | Mac | `raw/harness_run1` | +| Harness seed spread over 8 seeds: reference LL standard deviation (SD); cross-pair LL gap median (range) | not measured | 2.6e-4 (pamica 2.7e-4); 3.5e-4 (1.7e-5 to 8.2e-4) | `harness_gap.py ROOT OUT.json --spread ...` | 09b8247 | Mac | `raw/harness_gap_head.json` | +| Harness from a shared start: LL gap, correlation, Amari | 2.35e-4, 0.99999, 5.8e-4 (e38aa11) | 1.6e-6, 0.99999993, 3.9e-5 | `harness_gap.py` | 09b8247, e38aa11 | Mac | `raw/harness_gap_head.json`, `raw/harness_gap_pre_epic_e38aa11.json` | +| Harness runtimes, reference / PyTorch / NumPy / MLX (s) | 10.1 / 17.9 / 32.5 / 3.2 (same session) | 10.0 / 18.1 / 32.6 / 3.0 | harness | 09b8247, e38aa11 | Mac | `raw/harness_run1`, `raw/harness_pre_epic_e38aa11` | +| 200-iteration `amicaout` fixture: LL gap PyTorch / NumPy / MLX; correlation (min); Amari | 2.2-2.3e-4; 0.9973; 6.3e-3 (before Phase 11) | 1.42e-4 / 1.32e-4 / 1.44e-4; 0.9983 (0.982-0.983); 4.8e-3 | `fixture_parity.py ROOT BACKEND` | 09b8247 | Mac | `raw/fixture_parity_head.txt` | +| MLX float32 vs PyTorch float64 from a shared start, 100 / 200 iterations: LL gap, correlation, Amari | not characterized | 5.0e-6, 0.99999991, 5.0e-5 / 2.4e-6, 0.9999998, 6.3e-5 | `precision_agreement.py` | 09b8247 | Mac | `raw/precision_agreement.json` | +| Table 1, bundled single model: Amari over 5 run pairs | 0.006 | 0.0109 (per seed 0.0031, 0.0037, 0.0380, 0.0029, 0.0069); reference vs itself 0.0185 | `benchmarks/reproduce_table1.py --tier bundled` | 09b8247 | Mac CPU, 2842 s | `raw/table1_bundled` | +| Bundled single model, 10 seeded reference runs against the 5 pamica fits: Amari (max), correlation | not measured | 0.0044 (0.0083), 0.9988 over 50 pairs; reference vs itself 0.0054 (0.0101), 0.9985 over 45; pamica vs itself 0.0039, 0.9991 over 10 | `bundled_single_basins.py` | 09b8247 | Mac | `raw/bundled_basins` | +| Score functions; sufficient statistics | ~1e-15 | log-density 8.9e-16, score 1.8e-15; 4.5e-13 absolute (3e-16 relative) | `reproduce_table1.py --tier bundled` | 09b8247 | Mac | `raw/table1_bundled/report.json` | +| Multi-model correlation: cross; within-reference; within-pamica | 0.65; 0.64 | 0.632; 0.626; 0.638; difference +0.006, permutation p 0.88, run bootstrap 90% interval -0.002 to 0.014 | `reproduce_table1.py --tier bundled`, `equivalence_check.py` | 09b8247 | Mac | `raw/table1_bundled/report.json`, `raw/multimodel/equivalence_check.json` | +| Multi-model Amari: cross; within-reference; within-pamica | 0.163; 0.174 (p > 0.999) | 0.172; 0.166; 0.176; difference +0.005, p 0.051, interval 0.002 to 0.009 | same | 09b8247 | Mac | same | +| Multi-model ensemble LL: reference; pamica | -3.3539; -3.3629 (Kolmogorov-Smirnov (KS) p 6e-5) | -3.3543 (SD 0.002); -3.3541 (SD 0.003); KS p 0.83. The pre-epic pamica refit against the same reference fits gives -3.3627 (0.006), KS p 1e-5 | same, and `multimodel_pamica_fits.py` at e38aa11 | 09b8247, e38aa11 | Mac | `raw/table1_bundled`, `raw/multimodel` | +| keep_best: pamica/reference LL SD ratio at 100 iterations | 12.7x without, 2.0x with | 1.0x with and without; 1 restore in 20 fits (at 300 iterations, gain 4.2e-6); 0 Newton fallbacks | `keep_best_ensemble.py run`, `verify`, `report` | 09b8247 | Mac, reference seeded 0-19, 1 thread | `raw/keep_best` | +| Mean LL gap (pamica minus reference) at 100 / 200 / 300 iterations | -0.009 / 0 / +0.002 | +8.1e-4 / +2.1e-4 / +1.0e-4 (KS p 0.83 / 0.57 / 0.98) | same | 09b8247 | Mac | `raw/keep_best/ensemble.json` | +| Cross-backend LL after 25 iterations (dimsweep) | maximum pairwise ~0.003 | 1e-5 at 32 and 48 channels; 1.1e-3 at 70 channels (k≈6); NumPy (unseeded) 4.2e-3 | `benchmarks/benchmark_dimsweep.py` | 09b8247 | Mac, hallu | `raw/dimsweep` | +| Dimsweep timing at 70 channels, e38aa11 / 09b8247 (ms per iteration) | MLX 33.3, CPU float64 190.8, MPS 165.1 | MLX 33.1, CPU float64 189.4, MPS 166.7 | same | e38aa11, 09b8247 | Mac (quiet) | `raw/dimsweep/timing70_*.json` | +| Newton, independent seeds, 70 channels, 2000 iterations | seed 42: 10-11 of 70 components below 0.9; other seeds ~0.995 | see the Newton table below | `run_newton_hallu.sh`, `newton_seeds.py compare` | 09b8247 | hallu (CUDA float64; reference 10 threads) | `raw/newton` | +| Newton from a shared start: correlation (min), Amari, LL gap | 0.9974 (0.9475) | 0.9999998 (0.9999964), 3.8e-5, 7.6e-8 | `newton_seeds.py pamica/fortran --fixinit` | 09b8247 | hallu | `raw/newton` | +| `share_comps` end to end (300 iterations, threshold 0.95) | 3 merges, LL -3.3416 | 1 merge at iteration 100 (\|cos\| 0.970), LL -3.3410 (-3.3393 without sharing); 63 of 64 components | `share_comps_counts.py` | 09b8247 | Mac | `raw/share_comps` | +| Early scans at iteration 8 / 20, thresholds 0.95 and 0.99 | 32, 24 / 24, 5 | 30, 17 / 22, 5 | same | 09b8247 | Mac | `raw/share_comps` | +| Reference-formula scan on the reference's own state | 32, 32, 30 | 32, 32, 29 (identical `comp_list`s; 8-iteration states agree to 5.2e-6 in A and 1.6e-7 in LL) | same | 09b8247 | Mac | `raw/share_comps` | +| Table 1 external tier LL (on002718, 5 seeds) | gap ~0.0003 at -3.6993 | reference -3.699346 (SD 6.5e-5), pamica -3.699341 (4.1e-5), gap 5.6e-6 | `reproduce_table1.py --tier external --threads 16` | 09b8247 | hallu, CUDA float64 (lead's run, 26185 s) | `raw/table1_external` | +| Table 1 external correlation; Amari | 0.998 (reference vs itself ~0.999) | 0.99965 (SD 0.00048, min component 0.9602), reference vs itself 0.99827 (min 0.9167); Amari 0.00126 (SD 0.00062), reference vs itself 0.00249 | same | 09b8247 | hallu | `raw/table1_external/report.json` | + +External tier per seed (mean correlation / min correlation / Amari): +201 0.9999 / 0.9996 / 0.0008; 202 0.9987 / 0.9602 / 0.0024; 203 0.9999 / 0.9985 / 0.0012; 204 1.0000 / 0.9998 / 0.0007; 205 0.9998 / 0.9955 / 0.0012. +The reference fits took 1972-2559 s and the pamica fits 1886-5013 s. + +Newton pairs (mean / min correlation / components below 0.9): + +| Pair | Mean | Min | Below 0.9 | +|---|---:|---:|---:| +| pamica 42 vs reference 1 | 0.963 | 0.667 | 8 | +| pamica 42 vs reference 2 | 0.986 | 0.858 | 3 | +| pamica 13 vs reference 1 | 0.996 | 0.946 | 0 | +| pamica 13 vs reference 2 | 0.981 | 0.773 | 3 | +| pamica 7 vs reference 1 | 0.995 | 0.943 | 0 | +| pamica 7 vs reference 2 | 0.982 | 0.786 | 3 | +| reference 1 vs reference 2 | 0.985 | 0.841 | 2 | +| pamica 42 vs pamica 13 | 0.962 | 0.658 | 7 | + +Final LLs: reference seed 1 -3.697804 and seed 2 -3.697709; pamica seed 42 -3.697569, 13 -3.697816 and 7 -3.697812; shared start -3.697812. +The shared-start reference run matches reference seed 1 at 0.996 (min 0.955). +MLX float32 (supplementary, Mac): seed 7 stopped on the `lrate` floor after 952 iterations at LL -3.698091 (CUDA float64 -3.697913 at the same iteration), after 121 decreases; +it matches CUDA seed 7 at 0.985 (min 0.907) and reference seed 1 at 0.983 (min 0.905). +MLX seed 42 (928 iterations) matches CUDA seed 42 at 0.955, with 10 components below 0.9 (`raw/newton_mlx`). + +## Harness: why the independent-start gap grew + +The harness fits pamica and the reference from independent random starts at one seed, +so its LL gap is one draw from the start-to-start spread. +Over 8 seeds the reference's own final LL has SD 2.6e-4 and range 8.0e-4, pamica's SD 2.7e-4 and range 8.0e-4, +and the cross-implementation gaps have median 3.5e-4 (1.7e-5 to 8.2e-4). +Before the epic, seed 42 happened to fall near the low end of that spread (2.9e-5); +the epic changed the initial draw (the normalized initial mixing matrix, #341) and the trajectory, and seed 42 now falls near its middle (2.71e-4). +The lead reproduced the binary's LL at seeds 42, 6 and 3. +From a shared start, the reference loaded with pamica's initial state (`native_oracle.run_seeded_reference`), the gap is 1.6e-6, with correlation 0.99999993 and Amari 3.9e-5; +the same comparison at e38aa11 gives 2.35e-4, 0.99999 and 5.8e-4. +The shared state's orientation was checked by the first-iteration LL, which agrees to 9e-16 as loaded and differs by 1.3e-5 when transposed (`raw/orientation_check.txt`). +The documents now give the independent-start figure with the reference's spread beside it, and the shared-start figure. + +## 70-channel dimsweep: round-off sensitivity + +At 32 and 48 channels the backends that share a start agree to 1e-5 in LL after 25 iterations. +At 70 channels on the 30,000-frame excerpt, k = frames / channels² ≈ 6, the five seeded backends spread over 1.1e-3: +MLX -3.21643, CPU float64 -3.21746, MPS -3.21638, CUDA float64 -3.21723, CUDA float32 -3.21689. +The lead's perturbation check measured the sensitivity directly: +changing one sample of the 70-channel data by 1e-9 µV changes the CPU float64 LL at iteration 25 by 4.4e-4, +so differences of this size arise from round-off amplification on data this poorly determined. +The NumPy backend is unseeded in this benchmark and differs by 4.2e-3 (-3.22054). + +## Protocol changes and why + +1. Harness: unchanged. + The seed-spread and shared-start diagnostics (`harness_gap.py`) were added because the single-seed independent-start gap moved tenfold and needed an interpretation. +2. `benchmarks/reproduce_table1.py`: every reference setting is spelled out in `REFERENCE_SETTINGS`. + These are the bundled `input.param` values, which `AMICANative`'s defaults mirrored when Table 1 was measured, plus `do_approx_sphere 1`. + The ensemble's pamica fit pins `lrate=0.05`. + Phase 17 moved the `AMICANative` and wrapper defaults, and the pins keep Table 1's protocol as it was measured. +3. Bundled single-model tier: the five-pair protocol is unchanged. + The 50-pair basin study was added because one of the five reference runs (seed 303, LL -3.4006 against about -3.3996 for the others) sets the five-pair Amari mean. +4. Multi-model ensemble: the protocol is unchanged (20 fits per implementation, 2 models, 3 mixture components, 100 iterations, bundled sample; the reference draws its own clock-based seeds). + The ±0.05 margin is checked with a run-level bootstrap 90% interval of the difference. + The earlier pairwise two one-sided tests (TOST) treated 190 or 400 pairwise values as independent, although each run appears in about 39 pairs; the #27 record had already retired it in favor of the run-level permutation test. + The multi-model Amari distance takes the better of the two model pairings. + Over the 780 run pairs of the new ensemble that choice lowers the mean by 0.0185 (median 0) and swaps the pairing in 333 pairs (`raw/multimodel/pairing_correction.txt`). + The reference runs with `invsigmin` 0.0 and pamica with 1e-8; `validation.md` states this. +5. keep_best ensemble: the reference is seeded 0-19 and single-threaded, so each run is reproducible (the #51 runs were clock-seeded). + The 100- and 200-iteration budgets are read from prefixes of each 300-iteration run. + Separate 100-iteration runs and pamica histories agree exactly with those prefixes (`raw/keep_best/verify.json`, all differences 0.0). +6. Newton: pamica on CUDA float64 at seeds 42, 13 and 7, against the pinned v0.3.3 binary seeded 1 and 2. + The data are the full 70-channel ds002718 recording, rounded to float32. + The early stops are off on both sides, so both run 2000 iterations. + A shared-start pair (A = I, mu = linspace(-1, 1, 3), sbeta = 1, rho = 1.5, alpha = 1/3) replaces #145's matched-initialization run. + The #145 comparison used the gfortran `amica15_linux` build (SHA 43b9f0e3...), which draws a new start on every unseeded run: + two runs differ after one iteration in LL (-3.51144 against -3.51323) and in A (max 9.6e-3; `raw/seedcheck_issue145_binary`). + Both reference-vs-reference figures are therefore reported. +7. Dimsweep and `share_comps`: unchanged; the counts script re-runs the documented scans. + +## Why the multi-model LL gap closed + +The earlier -0.009 ensemble gap came mostly from the code before the epic. +Refitting the pamica half with e38aa11 against the same 20 reference fits gives -3.3627 (SD 0.006, KS p 1e-5). +7 of those 20 fits stopped early on the pre-#339 `min_dll` check (mean -3.3679). +The 13 full-length fits average -3.3600; the difference that remains is attributed to that code's update rule. +With the epic's code the ensemble gap is +2e-4 (KS p 0.83). +The seeded keep_best ensemble shows the same at matched budgets: +8.1e-4, +2.1e-4 and +1.0e-4 at 100, 200 and 300 iterations, with equal or smaller SD for pamica. +The documents no longer attribute the gap to convergence speed. +On Amari distance, the pre-epic ensemble was tighter (within-pamica 0.151, cross 0.160, p 0.998); the current one is slightly wider than the reference's own (0.176 and 0.172 against 0.166, p 0.051). + +## share_comps details + +End to end, 300 iterations at threshold 0.95: 1 merge at iteration 100 (|cos| 0.970), LL -3.3410; -3.3393 with sharing off; 63 of 64 components kept. +Early scans: at iteration 8, 30 merges (0.95) and 17 (0.99); at iteration 20, 22 and 5; at 0.9, 31 and 30. +The early mass-merge collapse: 28 merges, gm2 0.572 to 2.18e-3 to 9.0e-4, second scan 2.44e-4, a non-finite LL at iteration 23. +The gated freeze-free oracle gives 4.222e-3 on both sides. + +## Pins after Phase 17 + +Before Phase 17, pinned and unpinned calls wrote byte-identical `input.param` files at all 8 native call sites, with a negative control (`pin_check.py`, `raw/pin_check.json`). +The pinned files were saved (`raw/pin_check_pinned_params_pre_phase17.json`). +After the merge of bb22df6: +- The 3 seeded-runner calls are byte-identical. +- The 5 `AMICANative` calls write the same settings in Phase 17's key order, plus `do_approx_sphere 1`, the binary's compiled value (`amica15_header.f90:13`); nothing else is added, removed or changed (`raw/pin_check_post_phase17.json`). +- `pin_run_check.py` ran the binary on the pre- and post-Phase-17 forms of the file for the single-model, multi-model and Newton configurations: bundled sample, seeded, single-threaded, 20 or 60 iterations. + All 13 numeric output files are byte-identical, and `out.txt` matches once the per-iteration wall-clock times are removed (`raw/pin_run_check_post_phase17.json`). +- `REFERENCE_SETTINGS` now pins `do_approx_sphere` as well. +- Without the pins, 15 settings of the native calls would take Phase 17's defaults, among them `lrate` 0.1, `do_newton` 0, `newt_start` 20, `invsigmin` 1e-4 and `max_decs` 5. + +## Gates + +Before and after the Phase 17 merge: +- ruff check and format, `ty check .`, typos and `mkdocs build --strict` are clean. +- pytest (`CI=1`, 8 workers, no coverage): 1955 passed, 40 skipped, 3 xfailed before the merge; 1974 passed, 40 skipped, 3 xfailed after it (`raw/pytest_full.log`, `raw/pytest_full_post_phase17.log`). +- `AMICA_RUN_FORTRAN=1` on `test_fortran_param_forwarding.py` and the two early-merge oracles: 37 passed both times (`raw/gated_tests.log`, `raw/gated_tests_post_phase17.log`). diff --git a/.context/issue-351/fixture_parity.py b/.context/issue-351/fixture_parity.py new file mode 100644 index 00000000..d18bff9e --- /dev/null +++ b/.context/issue-351/fixture_parity.py @@ -0,0 +1,82 @@ +"""Parity against the bundled ``amicaout`` fixture (200 reference iterations of +the bundled macOS binary, ``input.param`` settings, unseeded), for every +backend. The protocol of the epic #324 Phase 7 and Phase 11 before/after +figures (PR 340 and PR 348), kept here as the issue #351 run record: +each backend fits the sample through ``input.param`` for K iterations, seed 42, +and is compared with the fixture's W @ S and its LL at iteration K. + + uv run python .context/issue-351/fixture_parity.py ROOT BACKEND [K] + +ROOT is a pamica source tree (the worktree, or an older checkout).""" + +import sys +import tempfile +import time + +root = sys.argv[1] +sys.path.insert(0, root) +import importlib.util # noqa: E402 + +import numpy as np # noqa: E402 +from scipy.optimize import linear_sum_assignment # noqa: E402 + +import pamica # noqa: E402 + +assert pamica.__file__.startswith(root), pamica.__file__ +from pamica.torch_impl.utils import load_eeglab_data # noqa: E402 + +backend = sys.argv[2] +K = int(sys.argv[3]) if len(sys.argv) > 3 else 200 +SD = root + "/pamica/sample_data/" +vspec = importlib.util.spec_from_file_location( + "vi", root + "/validate_implementations.py" +) +vi = importlib.util.module_from_spec(vspec) +vspec.loader.exec_module(vi) +X = load_eeglab_data(SD + "eeglab_data.fdt", data_dim=32, field_dim=30504).astype( + np.float64 +) +AO = SD + "amicaout/" +W_ref = np.fromfile(AO + "W", np.float64).reshape(32, 32, order="F") +S_ref = np.fromfile(AO + "S", np.float64).reshape(32, 32, order="F") +LL_ref = np.fromfile(AO + "LL", np.float64) +LL_ref = LL_ref[LL_ref != 0] + +t0 = time.time() +if backend == "numpy": + from pamica.numpy_impl.core import AMICA as AMICA_NumPy + + with tempfile.TemporaryDirectory() as td: + m = AMICA_NumPy( + params_file=SD + "input.param", + max_iter=K, + seed=42, + use_tqdm=False, + outdir=td, + writestep=10**6, + ) + m.fit(X) + Wp = m.get_weights() @ m.sphere + ll = float(m.ll[-1]) + n_it = len(m.ll) + stop = m.stop_reason +else: + from pamica import AMICA + + a = AMICA.from_params_file(SD + "input.param", backend=backend) + a.fit(X, max_iter=K, seed=42) + mm = a.model_ + Wp = np.asarray(mm.get_unmixing_matrix(0)) @ np.asarray(mm.get_sphere()) + ll = float(mm.final_ll_) + n_it = len(mm.ll_history) + stop = mm.stop_reason +Wr = W_ref @ S_ref +an = Wp / np.linalg.norm(Wp, axis=1, keepdims=True) +bn = Wr / np.linalg.norm(Wr, axis=1, keepdims=True) +c = np.abs(an @ bn.T) +ri, ci = linear_sum_assignment(1 - c) +print( + f"{backend} K={K}: LL {ll:.6f} fixture {LL_ref[K - 1]:.6f} |dLL|={abs(ll - LL_ref[K - 1]):.2e} " + f"corr={c[ri, ci].mean():.6f} min={c[ri, ci].min():.4f} amari={vi.amari_distance(Wp, Wr):.3e} " + f"stop={stop} iters={n_it} ({time.time() - t0:.0f}s)" +) diff --git a/.context/issue-351/harness_gap.py b/.context/issue-351/harness_gap.py new file mode 100644 index 00000000..8b275f40 --- /dev/null +++ b/.context/issue-351/harness_gap.py @@ -0,0 +1,194 @@ +"""Why the harness's 100-iteration log-likelihood gap moved (issue #351). + +``validate_implementations.py --backend all`` runs each pamica backend and the +reference binary from their OWN initializations with the same seed number +(numpy's ``RandomState(42)`` for pamica, gfortran's ``random_number`` seeded +with 42 for the binary), so the two start from different points. Its +log-likelihood difference after 100 iterations mixes two things: how far apart +the two starting points leave the fits at iteration 100, and any difference in +the update rule. This script separates them on the harness's own settings. + +1. Same start: pamica's seed-42 initialization is written into the binary's + ``load_*`` files (``pamica/tests/native_oracle.py``) and both sides run the + harness's 100 iterations from it, single-threaded. What remains is the + update rule's difference. +2. Start-to-start spread: the binary and pamica each run from several seeds. + The spread of their final log-likelihoods is what the harness's + independent-start difference is drawn from. + +``ROOT`` is a pamica source tree: the worktree, or a checkout of an older +commit (run it from the worktree's environment; the tree at ``ROOT`` is +imported in place of the installed package, and ``native_oracle`` is read from +this worktree so an old tree without it still works):: + + uv run python .context/issue-351/harness_gap.py ROOT OUT.json [--spread 1,2,3] +""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +import sys +import tempfile +import time +from pathlib import Path + +HERE = Path(__file__).resolve().parent +WORKTREE = HERE.parents[1] + + +def _load(path: Path, name: str): + spec = importlib.util.spec_from_file_location(name, path) + assert spec is not None and spec.loader is not None + mod = importlib.util.module_from_spec(spec) + sys.modules[name] = mod + spec.loader.exec_module(mod) + return mod + + +def main() -> None: + p = argparse.ArgumentParser() + p.add_argument("root", type=Path) + p.add_argument("out", type=Path) + p.add_argument("--seed", type=int, default=42) + p.add_argument("--spread", default="", help="comma-separated extra seeds") + p.add_argument( + "--binary", + type=Path, + default=Path.home() / ".cache/pamica/bin/v0.3.3/amica15-macos-arm64", + ) + a = p.parse_args() + root = a.root.resolve() + sys.path.insert(0, str(root)) + + import numpy as np # noqa: E402 + + import pamica # noqa: E402 + + assert pamica.__file__.startswith(str(root)), pamica.__file__ + from pamica.torch_impl import AMICATorchNG # noqa: E402 + + vi = _load(root / "validate_implementations.py", "vi_under_test") + oracle = _load(WORKTREE / "pamica/tests/native_oracle.py", "native_oracle_351") + + data, params = vi.load_sample_data() + params = dict(params) + params["max_iter"] = 100 + X = data.astype(np.float64) + nw, n = X.shape + fdt = root / "pamica/sample_data/eeglab_data.fdt" + report: dict = {"root": str(root), "binary": str(a.binary), "seed": a.seed} + + # --- 1. same start -------------------------------------------------------- + t0 = time.time() + fit = vi.run_pytorch_amica(data, params, Path(tempfile.mkdtemp()), a.seed) + torch_dt = time.time() - t0 + init = AMICATorchNG( + n_channels=nw, + n_models=1, + n_mix=params.get("num_mix", 3), + rho0=params["rho0"], + seed=a.seed, + device="cpu", + ) + init._preprocess(X) + init._initialize_parameters() + state = oracle.seed_from_torch(init) + # The harness's reference settings: the input.param it writes, minus the + # keys the seeded runner sets itself. + with tempfile.TemporaryDirectory() as td: + ref_free = vi.run_fortran_amica(data, params, Path(td), a.seed, a.binary) + written = (Path(td) / "fortran_run" / "input.param").read_text().splitlines() + skip = { + "files", "outdir", "indir", "seed", "max_threads", "data_dim", + "field_dim", "pcakeep", "max_iter", "num_models", "num_mix_comps", + "writestep", "write_LLt", "num_comps", "load_W", "doPCA", + "field_blocksize", + } # fmt: skip + ref_params = {} + for line in written: + parts = line.split() + if ( + len(parts) >= 2 + and parts[0] not in skip + and not parts[0].startswith("load_") + ): + ref_params[parts[0]] = parts[1] + with tempfile.TemporaryDirectory() as td: + ref = oracle.run_seeded_reference( + state, fdt, Path(td), n_samples=n, max_iter=100, threads=1, + binary=a.binary, **ref_params, + ) # fmt: skip + W_ref = np.linalg.inv(ref.A) # reference A: component k in column k + ref_ll = ref.LL[ref.LL != 0] + same = { + "pamica_final_ll": float(fit["final_ll"]), + "reference_from_pamica_init_final_ll": float(ref_ll[-1]), + "abs_ll_difference": abs(float(fit["final_ll"]) - float(ref_ll[-1])), + "mean_matched_corr": None, + "min_matched_corr": None, + "amari": float(vi.amari_distance(W_ref, fit["W"])), + "pamica_runtime_s": torch_dt, + } + cmp = vi.compare_results( + {"final_ll": ref_ll[-1], "W": W_ref, "final_iter": 100}, fit + ) + same["mean_matched_corr"] = float(cmp["mean_correlation"]) + same["min_matched_corr"] = float(cmp.get("min_correlation", np.nan)) + free = vi.compare_results(ref_free, fit) + report["independent_start"] = { + "reference_final_ll": float(ref_free["final_ll"]), + "pamica_final_ll": float(fit["final_ll"]), + "abs_ll_difference": float(free["ll_difference"]), + "mean_matched_corr": float(free["mean_correlation"]), + "amari": float(vi.amari_distance(ref_free["W"], fit["W"])), + } + report["same_start"] = same + + # --- 2. start-to-start spread ---------------------------------------------- + seeds = [a.seed] + [int(s) for s in a.spread.split(",") if s] + if len(seeds) > 1: + F, G = {}, {} + for s in seeds: + with tempfile.TemporaryDirectory() as td: + F[s] = vi.run_fortran_amica(data, params, Path(td), s, a.binary) + G[s] = ( + fit + if s == a.seed + else vi.run_pytorch_amica(data, params, Path(tempfile.mkdtemp()), s) + ) + f_ll = np.array([F[s]["final_ll"] for s in seeds]) + g_ll = np.array([G[s]["final_ll"] for s in seeds]) + gaps = np.array([abs(g - f) for g in g_ll for f in f_ll]) + corr_fg = [ + vi.compare_results(F[fs], G[gs])["mean_correlation"] + for fs in seeds + for gs in seeds + ] + corr_ff = [ + vi.compare_results(F[x], F[y])["mean_correlation"] + for i, x in enumerate(seeds) + for y in seeds[i + 1 :] + ] + report["spread"] = { + "seeds": seeds, + "reference_final_ll": f_ll.tolist(), + "pamica_final_ll": g_ll.tolist(), + "reference_ll_sd": float(f_ll.std()), + "pamica_ll_sd": float(g_ll.std()), + "reference_ll_range": float(np.ptp(f_ll)), + "pamica_ll_range": float(np.ptp(g_ll)), + "mean_ll_difference_of_means": float(abs(g_ll.mean() - f_ll.mean())), + "all_pairs_abs_gap_median": float(np.median(gaps)), + "all_pairs_abs_gap_min_max": [float(gaps.min()), float(gaps.max())], + "corr_pamica_vs_reference_mean": float(np.mean(corr_fg)), + "corr_reference_vs_reference_mean": float(np.mean(corr_ff)), + } + a.out.parent.mkdir(parents=True, exist_ok=True) + a.out.write_text(json.dumps(report, indent=2)) + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/.context/issue-351/keep_best_ensemble.py b/.context/issue-351/keep_best_ensemble.py new file mode 100644 index 00000000..b1ed3c5f --- /dev/null +++ b/.context/issue-351/keep_best_ensemble.py @@ -0,0 +1,236 @@ +"""Seeded multi-model log-likelihood ensemble against the pinned binary +(issue #351 items 3 and 5): ADR 0003's ``keep_best`` figures and the paper's +"convergence speed" explanation, re-measured under epic #324. + +Protocol of ``.context/issue-51/ensemble_ll.py`` (ADR 0003): the bundled +sample EEG, ``n_models=2``, three mixtures, the reference's ``input.param`` +schedule (``lrate`` 0.05, Newton from iteration 50, ``newtrate`` 1.0, block +size 512), N = 20 fits per implementation, pamica seeds 0-19 on +``AMICATorchNG`` with the #51 keyword set. Changes, each recorded in +``findings.md``: + +* the reference is the pinned v0.3.3 native binary through ``AMICANative``, + seeded 0-19 and single-threaded, so every run is reproducible (#51 used the + clock-seeded bundled ``amica15mac``); +* each fit runs the longest budget (300 iterations) once, and the 100- and + 200-iteration figures are read from the prefix of its trajectory: a fit's + iterations do not depend on ``max_iter``, and ``verify`` checks that on the + first seeds (separate 100-iteration fits with ``keep_best`` on and off give + exactly the prefix's values, on both sides). + +At a budget of K iterations, "return-last" is ``ll_history[K-1]`` (the +likelihood ``keep_best=False`` reports) and ``keep_best`` is the best of the +first K (restored when it beats the last by more than ``_KEEP_BEST_TOL``). + + uv run python .context/issue-351/keep_best_ensemble.py run OUT.npz [N] + uv run python .context/issue-351/keep_best_ensemble.py verify OUT.json [n] + uv run python .context/issue-351/keep_best_ensemble.py report OUT.npz +""" + +from __future__ import annotations + +import json +import sys +import time +from pathlib import Path + +import numpy as np +from scipy import stats + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[1] +FDT = REPO / "pamica/sample_data/eeglab_data.fdt" +NW, FIELD = 32, 30504 +BUDGETS = (100, 200, 300) +DELTA_LL = 0.01 # the #51 TOST margin on the mean LL + + +def load_data() -> np.ndarray: + from pamica.torch_impl.utils import load_eeglab_data + + return load_eeglab_data(str(FDT), data_dim=NW, field_dim=FIELD).astype(np.float64) + + +def fortran_kwargs(max_iter: int, seed: int) -> dict: + """``AMICANative`` keywords of the reference runs, every protocol setting + explicit (``_reference_settings``), seeded and single-threaded.""" + from _reference_settings import multimodel_reference_kwargs + + return dict( + threads=1, max_threads=1, timeout=3600, seed=seed, + **multimodel_reference_kwargs(max_iter), + ) # fmt: skip + + +def run_fortran(data: np.ndarray, seed: int, max_iter: int) -> np.ndarray: + from pamica import AMICANative + from pamica.native import resolver + + eng = AMICANative( + **fortran_kwargs(max_iter, seed), binary=resolver.resolve("v0.3.3") + ) + eng.fit(data) + assert eng.output_ is not None + ll = np.asarray(eng.output_.LL, dtype=np.float64) + return ll[ll != 0] + + +def run_ng(data: np.ndarray, seed: int, max_iter: int, keep_best: bool = True): + from pamica.torch_impl import AMICATorchNG + + ng = AMICATorchNG( + n_channels=NW, n_models=2, n_mix=3, block_size=512, lrate=0.05, minlrate=1e-8, + lratefact=0.5, maxdecs=3, do_newton=True, newt_start=50, newt_ramp=10, + newtrate=1.0, rho0=1.5, minrho=1.0, maxrho=2.0, rholrate=0.05, + rholratefact=0.5, invsigmin=1e-8, invsigmax=100.0, doscaling=True, + scalestep=1, seed=seed, device="cpu", keep_best=keep_best, + ) # fmt: skip + ng.fit(data, max_iter=max_iter, verbose=False) + return ng + + +def at_budget(hist: np.ndarray, k: int, tol: float) -> tuple[float, float, bool]: + """(return-last, keep_best, restored) at a budget of ``k`` iterations.""" + h = hist[:k] + last, best = float(h[-1]), float(h.max()) + restored = best - last > tol + return last, (best if restored else last), restored + + +def cmd_run(out: Path, n: int) -> None: + from pamica.torch_impl.core import _KEEP_BEST_TOL + + data = load_data() + kmax = max(BUDGETS) + F_hist = np.full((n, kmax), np.nan) + G_hist = np.full((n, kmax), np.nan) + G_final = np.full(n, np.nan) + G_stop, G_fallbacks = [], [] + t0 = time.time() + for k in range(n): + f = run_fortran(data, k, kmax) + F_hist[k, : f.size] = f + ng = run_ng(data, k, kmax) + h = np.asarray(ng.ll_history, dtype=np.float64) + G_hist[k, : h.size] = h + G_final[k] = ng.final_ll_ + G_stop.append(str(ng.stop_reason)) + G_fallbacks.append(int(ng.n_newton_fallbacks)) + print( + f"seed {k}: Fortran LL[{f.size}]={f[-1]:.4f} NG iters={h.size} " + f"last={h[-1]:.4f} final={ng.final_ll_:.4f} stop={ng.stop_reason} " + f"fallbacks={ng.n_newton_fallbacks} ({time.time() - t0:.0f}s)", + flush=True, + ) + np.savez( + out, F_hist=F_hist, G_hist=G_hist, G_final=G_final, + G_stop=np.array(G_stop), G_fallbacks=np.array(G_fallbacks), + keep_best_tol=_KEEP_BEST_TOL, budgets=np.array(BUDGETS), + ) # fmt: skip + cmd_report(out) + + +def cmd_verify(out: Path, n: int) -> None: + """Separate 100-iteration fits equal the prefix of a 300-iteration fit.""" + from pamica.torch_impl.core import _KEEP_BEST_TOL + + data = load_data() + rows = [] + for k in range(n): + f300 = run_fortran(data, k, 300) + f100 = run_fortran(data, k, 100) + g300 = np.asarray(run_ng(data, k, 300).ll_history) + g100_on = run_ng(data, k, 100, keep_best=True) + g100_off = run_ng(data, k, 100, keep_best=False) + last, kb, restored = at_budget(g300, 100, _KEEP_BEST_TOL) + rows.append( + { + "seed": k, + "fortran_ll100_separate_minus_prefix": float(f100[-1] - f300[99]), + "ng_history_prefix_max_abs_diff": float( + np.abs(np.asarray(g100_on.ll_history) - g300[:100]).max() + ), + "ng_keep_best_off_final_minus_prefix_last": float( + g100_off.final_ll_ - last + ), + "ng_keep_best_on_final_minus_prefix_best": float( + g100_on.final_ll_ - kb + ), + "restored_at_100": bool(restored), + } + ) + print(rows[-1], flush=True) + out.write_text(json.dumps(rows, indent=2)) + + +def _tost(F: np.ndarray, G: np.ndarray) -> float: + diff = G.mean() - F.mean() + se = np.sqrt(G.var(ddof=1) / G.size + F.var(ddof=1) / F.size) + return float( + max( + stats.norm.sf((diff + DELTA_LL) / se), + stats.norm.cdf((diff - DELTA_LL) / se), + ) + ) + + +def cmd_report(out: Path) -> dict: + d = np.load(out) + F_hist, G_hist, tol = d["F_hist"], d["G_hist"], float(d["keep_best_tol"]) + n = F_hist.shape[0] + rep: dict = {"n": n, "keep_best_tol": tol, "budgets": {}} + for K in BUDGETS: + F = np.array([F_hist[i, :K][~np.isnan(F_hist[i, :K])][-1] for i in range(n)]) + last, best, restored = [], [], [] + for i in range(n): + h = G_hist[i][~np.isnan(G_hist[i])] + a, b, r = at_budget(h, min(K, h.size), tol) + last.append(a) + best.append(b) + restored.append(r) + G_last, G_best = np.array(last), np.array(best) + entry = { + "fortran_mean": float(F.mean()), + "fortran_sd": float(F.std()), + "return_last_mean": float(G_last.mean()), + "return_last_sd": float(G_last.std()), + "keep_best_mean": float(G_best.mean()), + "keep_best_sd": float(G_best.std()), + "sd_ratio_return_last": float(G_last.std() / F.std()), + "sd_ratio_keep_best": float(G_best.std() / F.std()), + "gap_return_last": float(G_last.mean() - F.mean()), + "gap_keep_best": float(G_best.mean() - F.mean()), + "ks_p_keep_best": float(stats.ks_2samp(G_best, F).pvalue), + "ks_p_return_last": float(stats.ks_2samp(G_last, F).pvalue), + "tost_p_keep_best": _tost(F, G_best), + "n_restored": int(np.sum(restored)), + "restored_seeds": [int(i) for i in np.flatnonzero(restored)], + "max_restore_gain": float((G_best - G_last).max()), + } + rep["budgets"][str(K)] = entry + F100 = np.array([F_hist[i, 99] for i in range(n)]) + rep["keep_best_gap_to_fortran_at_100"] = { + str(K): rep["budgets"][str(K)]["keep_best_mean"] - float(F100.mean()) + for K in BUDGETS + } + rep["ng_stop_reasons"] = sorted(set(d["G_stop"].tolist())) + rep["ng_newton_fallbacks_total"] = int(d["G_fallbacks"].sum()) + out.with_suffix(".json").write_text(json.dumps(rep, indent=2)) + print(json.dumps(rep, indent=2)) + return rep + + +def main() -> None: + cmd, out = sys.argv[1], Path(sys.argv[2]) + if cmd == "run": + cmd_run(out, int(sys.argv[3]) if len(sys.argv) > 3 else 20) + elif cmd == "verify": + cmd_verify(out, int(sys.argv[3]) if len(sys.argv) > 3 else 2) + elif cmd == "report": + cmd_report(out) + else: + raise SystemExit(f"unknown command {cmd!r}") + + +if __name__ == "__main__": + main() diff --git a/.context/issue-351/multimodel_ensemble.py b/.context/issue-351/multimodel_ensemble.py new file mode 100644 index 00000000..6248b74c --- /dev/null +++ b/.context/issue-351/multimodel_ensemble.py @@ -0,0 +1,287 @@ +"""Multi-model distributional-equivalence study (issue #27), re-run under epic +#324 (issue #351 item 3). Same shape as ``.context/issue-27/multimodel_ensemble.py`` +and ``amari_distance.py``, whose analysis functions and figure it reuses: +N = 20 reference and N = 20 pamica fits of the bundled sample EEG +(``n_models=2``, three mixtures, 100 iterations, Newton from iteration 50), +compared by the within-Fortran, within-pamica and between distributions of the +Hungarian-matched correlation and the Amari distance, with the run-level +permutation test and a Kolmogorov-Smirnov test on the final log-likelihoods. + +The fits are ``benchmarks/reproduce_table1.py``'s bundled-tier ensemble (so the +paper's Table 1 rows and the figure come from the same 40 fits): the pinned +v0.3.3 native binary through ``AMICANative`` (clock-seeded, as the #27 +reference runs were) and ``AMICA`` seeds 1-20 with the #27 keyword set. + + # analyze an existing ensemble (the bundled tier's .npz) + uv run python .context/issue-351/multimodel_ensemble.py --from-npz PATH + # or fit a new one first (writes ensemble.npz next to this script) + uv run python .context/issue-351/multimodel_ensemble.py [N] [--threads T] + +Writes ``multimodel_summary.json`` and ``per_run_detail.csv`` next to this +script and the figure to ``docs/assets/figures/multimodel-ensemble.png``. +""" + +from __future__ import annotations + +import argparse +import csv +import importlib.util +import json +import shutil +import sys +from pathlib import Path + +import matplotlib +import numpy as np +from scipy import stats + +matplotlib.use("Agg") +import matplotlib.pyplot as plt # noqa: E402 + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[1] +FIGURE = REPO / "docs/assets/figures/multimodel-ensemble.png" +NW = 32 + + +def _load(path: Path, name: str): + spec = importlib.util.spec_from_file_location(name, path) + assert spec is not None and spec.loader is not None + mod = importlib.util.module_from_spec(spec) + sys.modules[name] = mod + spec.loader.exec_module(mod) + return mod + + +def fit_ensemble(n: int, threads: int) -> dict[str, np.ndarray]: + table1 = _load(REPO / "benchmarks/reproduce_table1.py", "reproduce_table1_351") + device = table1.resolve_device("cpu") + binary = table1.resolve_binary(None, "v0.3.3") + data, _ = table1.load_bundled_data() + _, raw = table1.run_multimodel_ensemble(data, n, 100, threads, device, binary) + np.savez(HERE / "ensemble.npz", **raw) + return raw + + +# The figure of `.context/issue-27/multimodel_ensemble.py`, with a line style +# for each distribution besides its color (the #27 record keeps its own copy). +C_FORT, C_NG, C_BET = "#0072B2", "#E69F00", "#009E73" # Okabe-Ito + + +def figure(within_F, within_G, between, F_ll, G_ll, diff, p_perm, ks, out): + # Sized to its ACTUAL print footprint, not a big on-screen canvas: paper.md + # embeds this at width=100% of a ~5.36in single-column page (measured from the + # compiled paper.pdf), so figsize is set to that width directly -- LaTeX then + # displays it near 1:1 instead of shrinking a much larger canvas down to fit, + # which previously collapsed every font to an unreadable ~3pt in the printed + # PDF even though it looked fine on screen (figure-qa print-scale finding). + plt.rcParams.update( + {"font.size": 7, "axes.spines.top": False, "axes.spines.right": False} + ) + fig, (axA, axB) = plt.subplots(1, 2, figsize=(5.36, 4.2)) + bins = np.linspace(0.5, 1.0, 26) + for arr, col, ls, lab in [ + (within_F, C_FORT, "-", "within-Fortran"), + (within_G, C_NG, "--", "within-pamica"), + (between, C_BET, ":", "between (pamica-Fortran)"), + ]: + axA.hist(arr, bins=bins, density=True, color=col, alpha=0.25) + axA.hist( + arr, bins=bins, density=True, histtype="step", color=col, ls=ls, lw=2, + label=lab, + ) # fmt: skip + axA.axvline(arr.mean(), color=col, ls=ls, lw=1.2) + axA.set_xlabel("Hungarian-matched cross-correlation\n(stacked 2x32 components)") + axA.set_ylabel("density") + axA.set_title( + "A Partition-agreement distributions", + loc="left", + fontweight="bold", + fontsize=8, + ) + bins_ll = np.linspace( + min(F_ll.min(), G_ll.min()) - 0.005, max(F_ll.max(), G_ll.max()) + 0.005, 24 + ) + for arr, col, ls, lab in [ + (F_ll, C_FORT, "-", "Fortran"), + (G_ll, C_NG, "--", "pamica"), + ]: + axB.hist(arr, bins=bins_ll, density=True, color=col, alpha=0.25) + axB.hist( + arr, bins=bins_ll, density=True, histtype="step", color=col, ls=ls, lw=2, + label=lab, + ) # fmt: skip + axB.axvline(arr.mean(), color=col, ls=ls, lw=1.2) + axB.set_xlabel("final log-likelihood\n(mean per sample-channel)") + axB.set_ylabel("density") + axB.set_title( + "B Likelihood distributions", loc="left", fontweight="bold", fontsize=8 + ) + + # Both the legend and the stats box previously sat inside the axes and ended up + # overlapping the histogram bars (and each other) no matter where they were + # anchored -- with 3 distributions filling most of the plotted range, there was + # no empty pocket big enough to hold either. Reserve a fixed bottom margin for + # both instead and place them with figure-fraction (not axes-fraction) + # coordinates: axes-fraction anchors turned out to depend on the final axes + # height that tight_layout picks, which isn't known in advance and caused the + # legend/text to collide with the xlabel above it. get_position() gives each + # axes' true horizontal center after the layout below is fixed, so this keeps + # each panel's legend/text under its own histogram, not bleeding into the + # other panel. + fig.subplots_adjust(top=0.80, bottom=0.42, left=0.11, right=0.97, wspace=0.45) + cx_a = sum(axA.get_position().intervalx) / 2 + cx_b = sum(axB.get_position().intervalx) / 2 + + handles_a, labels_a = axA.get_legend_handles_labels() + fig.legend( + handles_a, labels_a, frameon=False, fontsize=6, + loc="upper center", bbox_to_anchor=(cx_a, 0.31), + ) # fmt: skip + fig.text( + cx_a, + 0.20, + f"mean corr. (run pairs)\n" + f"within-Fortran: {within_F.mean():.3f} (n={len(within_F)})\n" + f"within-pamica: {within_G.mean():.3f} (n={len(within_G)})\n" + f"between: {between.mean():.3f} (n={len(between)})\n" + f"diff: {diff:+.3f} (margin +/-0.05)\n" + f"perm. p={p_perm:.2f}", + ha="center", + va="top", + fontsize=5.5, + bbox=dict(boxstyle="round", fc="white", ec="0.7", alpha=0.95), + ) + + handles_b, labels_b = axB.get_legend_handles_labels() + fig.legend( + handles_b, labels_b, frameon=False, fontsize=6, + loc="upper center", bbox_to_anchor=(cx_b, 0.31), + ) # fmt: skip + fig.text( + cx_b, + 0.23, + f"mean final LL\n" + f"Fortran: {F_ll.mean():.4f} (sd {F_ll.std():.3f})\n" + f"pamica: {G_ll.mean():.4f} (sd {G_ll.std():.3f})\n" + f"gap: {abs(F_ll.mean() - G_ll.mean()):.4f} (100-iter budget)\n" + f"KS p={ks:.2f}" + if ks >= 0.01 + else f"KS p={ks:.0e}", + ha="center", + va="top", + fontsize=5.5, + bbox=dict(boxstyle="round", fc="white", ec="0.7", alpha=0.9), + ) + + fig.text( + 0.5, + 0.90, + "Vertical lines mark each distribution's mean, in its line style.", + ha="center", + fontsize=6.5, + style="italic", + ) + fig.suptitle( + "Multi-model AMICA (n_models=2): pamica vs Fortran ensembles, real sample EEG", + fontweight="bold", + fontsize=8.5, + y=0.97, + ) + fig.savefig( + out / "multimodel_ensemble_distributions.png", bbox_inches="tight", dpi=300 + ) + fig.savefig(out / "multimodel_ensemble_distributions.pdf", bbox_inches="tight") + + +def main() -> None: + p = argparse.ArgumentParser() + p.add_argument("n", nargs="?", type=int, default=20) + p.add_argument("--from-npz", type=Path) + p.add_argument("--threads", type=int, default=4) + p.add_argument("--tag", default="", help="suffix for the output file names") + p.add_argument("--no-figure", action="store_true") + a = p.parse_args() + sfx = f"_{a.tag}" if a.tag else "" + ens27 = _load(REPO / ".context/issue-27/multimodel_ensemble.py", "ens27") + ama27 = _load(REPO / ".context/issue-27/amari_distance.py", "ama27") + + d = dict(np.load(a.from_npz)) if a.from_npz else fit_ensemble(a.n, a.threads) + Fs, Gs, F_ll, G_ll = d["Fs"], d["Gs"], d["F_ll"], d["G_ll"] + n = len(Fs) + + within_F = ens27.pairwise(Fs, Fs, True) + within_G = ens27.pairwise(Gs, Gs, True) + between = ens27.pairwise(Gs, Fs, False) + diff, p_perm = ens27.perm_test_not_worse(Fs, Gs) + ks = float(stats.ks_2samp(G_ll, F_ll).pvalue) + + summary: dict = { + "source": str(a.from_npz) if a.from_npz else "fit here", + "n_fortran": int(len(Fs)), + "n_pamica": int(len(Gs)), + "ll": { + "fortran_mean": float(F_ll.mean()), + "fortran_sd": float(F_ll.std()), + "pamica_mean": float(G_ll.mean()), + "pamica_sd": float(G_ll.std()), + "pamica_minus_fortran": float(G_ll.mean() - F_ll.mean()), + "ks_p": ks, + "mann_whitney_p": float(stats.mannwhitneyu(G_ll, F_ll).pvalue), + }, + } + for name, metric, higher_is_worse in ( + ("corr", ama27.xcorr, False), + ("amari", ama27.model_amari, True), + ): + wF = ama27.pairwise(Fs, Fs, True, metric) + wG = ama27.pairwise(Gs, Gs, True, metric) + bt = ama27.pairwise(Gs, Fs, False, metric) + summary[name] = { + "within_Fortran": {"mean": float(wF.mean()), "sd": float(wF.std())}, + "within_pamica": {"mean": float(wG.mean()), "sd": float(wG.std())}, + "between": {"mean": float(bt.mean()), "sd": float(bt.std())}, + "between_minus_within_Fortran": float(bt.mean() - wF.mean()), + "perm_p_not_worse": float( + ama27.perm_test_not_worse(Fs, Gs, metric, higher_is_worse) + ), + "range_within_Fortran": [float(wF.min()), float(wF.max())], + "range_within_pamica": [float(wG.min()), float(wG.max())], + "range_between": [float(bt.min()), float(bt.max())], + } + (HERE / f"multimodel_summary{sfx}.json").write_text(json.dumps(summary, indent=2)) + print(json.dumps(summary, indent=2)) + + corr_detail = { + (r["implementation"], r["run"]): r + for r in ama27.per_run_detail(Fs, Gs, ama27.xcorr) + } + with open(HERE / f"per_run_detail{sfx}.csv", "w", newline="") as f: + w = csv.writer(f) + w.writerow( + [ + "implementation", "run", "final_ll", "corr_within", "corr_between", + "amari_within", "amari_between", + ] + ) # fmt: skip + for row in ama27.per_run_detail(Fs, Gs, ama27.model_amari): + key = (row["implementation"], row["run"]) + c = corr_detail[key] + ll = (F_ll if row["implementation"] == "Fortran" else G_ll)[row["run"]] + w.writerow( + [ + row["implementation"], row["run"], f"{ll:.4f}", + f"{c['within']:.4f}", f"{c['between']:.4f}", + f"{row['within']:.4f}", f"{row['between']:.4f}", + ] + ) # fmt: skip + + if a.no_figure: + return + figure(within_F, within_G, between, F_ll, G_ll, diff, p_perm, ks, HERE) + shutil.copyfile(HERE / "multimodel_ensemble_distributions.png", FIGURE) + print(f"figure -> {FIGURE} (n={n} each)") + + +if __name__ == "__main__": + main() diff --git a/.context/issue-351/multimodel_ensemble_distributions.pdf b/.context/issue-351/multimodel_ensemble_distributions.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c8422d858c2fd1bb86c4e2bddff84fdd4001ec45 GIT binary patch literal 43454 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%0AT_dzjT0gd{xk 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 %O6 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

91>u719W9|Bmvb!g^En-<}(e?4ZZB>JsS?-_5 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

MTZjD^=nktfcuN>0d5ME~wnn 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

  • Ys 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 literal 0 HcmV?d00001 diff --git a/.context/issue-351/multimodel_pamica_fits.py b/.context/issue-351/multimodel_pamica_fits.py new file mode 100644 index 00000000..43f561ff --- /dev/null +++ b/.context/issue-351/multimodel_pamica_fits.py @@ -0,0 +1,62 @@ +"""Refit the pamica side of the bundled tier's multi-model ensemble with the +pamica tree at ROOT (issue #351 item 3), for a comparison against the SAME +reference fits: the reference half is copied from ``--fortran-npz`` (the +bundled tier's ``bundled_multimodel_ensemble.npz``), the pamica half is fitted +here with ``reproduce_table1.run_multimodel_ensemble``'s keyword set (seeds +1-20, 100 iterations). With ROOT at the code before epic #324 this separates the +epic's effect on the ensemble from the draw of the reference runs. + + uv run python .context/issue-351/multimodel_pamica_fits.py ROOT --fortran-npz PATH OUT.npz +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import numpy as np + + +def main() -> None: + p = argparse.ArgumentParser() + p.add_argument("root", type=Path) + p.add_argument("out", type=Path) + p.add_argument("--fortran-npz", type=Path, required=True) + p.add_argument("--n", type=int, default=20) + a = p.parse_args() + root = a.root.resolve() + sys.path.insert(0, str(root)) + import pamica + from pamica import AMICA + from pamica.torch_impl.utils import load_eeglab_data + + assert pamica.__file__.startswith(str(root)), pamica.__file__ + data = load_eeglab_data( + str(root / "pamica/sample_data/eeglab_data.fdt"), + data_dim=32, + field_dim=30504, + dtype=np.float32, + ).astype(np.float64) + Gs, G_ll = [], [] + for i in range(a.n): + model = AMICA(n_models=2, n_mix=3, device="cpu", verbose=False) + model.fit( + data, max_iter=100, lrate=0.05, do_newton=True, seed=1 + i, + block_size=512, minlrate=1e-8, lratefact=0.5, maxdecs=3, newt_start=50, + newt_ramp=10, newtrate=1.0, rho0=1.5, minrho=1.0, maxrho=2.0, + rholrate=0.05, rholratefact=0.5, invsigmin=1e-8, invsigmax=100.0, + doscaling=True, scalestep=1, + ) # fmt: skip + assert model.converged_, model.stop_reason_ + Gs.append( + np.vstack([model.get_unmixing_matrix(0), model.get_unmixing_matrix(1)]) + ) + G_ll.append(float(model.final_ll_)) + print(f"pamica {i + 1}/{a.n}: LL={G_ll[-1]:.4f}", flush=True) + f = np.load(a.fortran_npz) + np.savez(a.out, Fs=f["Fs"], F_ll=f["F_ll"], Gs=np.array(Gs), G_ll=np.array(G_ll)) + + +if __name__ == "__main__": + main() diff --git a/.context/issue-351/multimodel_summary.json b/.context/issue-351/multimodel_summary.json new file mode 100644 index 00000000..a8a1a601 --- /dev/null +++ b/.context/issue-351/multimodel_summary.json @@ -0,0 +1,70 @@ +{ + "source": ".context/issue-351/raw/table1_bundled/bundled_multimodel_ensemble.npz", + "n_fortran": 20, + "n_pamica": 20, + "ll": { + "fortran_mean": -3.354345258775286, + "fortran_sd": 0.002081060017571174, + "pamica_mean": -3.354145177582788, + "pamica_sd": 0.002921024726455666, + "pamica_minus_fortran": 0.00020008119249803613, + "ks_p": 0.8319696107963263, + "mann_whitney_p": 0.5792183523134169 + }, + "corr": { + "within_Fortran": { + "mean": 0.6255295314440862, + "sd": 0.036300560820058544 + }, + "within_pamica": { + "mean": 0.6376031888481765, + "sd": 0.0424716014598481 + }, + "between": { + "mean": 0.6318832725487383, + "sd": 0.04158240732116439 + }, + "between_minus_within_Fortran": 0.006353741104652144, + "perm_p_not_worse": 0.8836558172091395, + "range_within_Fortran": [ + 0.5684137031783317, + 0.868054776369684 + ], + "range_within_pamica": [ + 0.5775638757483479, + 0.8051347220921952 + ], + "range_between": [ + 0.5644991772631343, + 0.8445546340425321 + ] + }, + "amari": { + "within_Fortran": { + "mean": 0.16630054685403053, + "sd": 0.017003169113571776 + }, + "within_pamica": { + "mean": 0.17623288658259578, + "sd": 0.025328776779805127 + }, + "between": { + "mean": 0.17155089714011276, + "sd": 0.02186823769645278 + }, + "between_minus_within_Fortran": 0.005250350286082228, + "perm_p_not_worse": 0.05089745512724364, + "range_within_Fortran": [ + 0.0716072110880876, + 0.1935216447461059 + ], + "range_within_pamica": [ + 0.09071592396865505, + 0.21715382793807797 + ], + "range_between": [ + 0.08186463933601121, + 0.20506707922833445 + ] + } +} \ No newline at end of file diff --git a/.context/issue-351/multimodel_summary_pre_epic_e38aa11.json b/.context/issue-351/multimodel_summary_pre_epic_e38aa11.json new file mode 100644 index 00000000..4398c6da --- /dev/null +++ b/.context/issue-351/multimodel_summary_pre_epic_e38aa11.json @@ -0,0 +1,70 @@ +{ + "source": ".context/issue-351/raw/multimodel/pre_epic_e38aa11_vs_bundled_fortran.npz", + "n_fortran": 20, + "n_pamica": 20, + "ll": { + "fortran_mean": -3.354345258775286, + "fortran_sd": 0.002081060017571174, + "pamica_mean": -3.362741544256477, + "pamica_sd": 0.006099420690041798, + "pamica_minus_fortran": -0.00839628548119098, + "ks_p": 9.54696510144592e-06, + "mann_whitney_p": 4.540079639937965e-06 + }, + "corr": { + "within_Fortran": { + "mean": 0.6255295314440862, + "sd": 0.036300560820058544 + }, + "within_pamica": { + "mean": 0.6658225162510475, + "sd": 0.04776583649133302 + }, + "between": { + "mean": 0.6426448010574329, + "sd": 0.037979911735524674 + }, + "between_minus_within_Fortran": 0.01711526961334675, + "perm_p_not_worse": 0.99950002499875, + "range_within_Fortran": [ + 0.5684137031783317, + 0.868054776369684 + ], + "range_within_pamica": [ + 0.5826450828558657, + 0.8196652526198319 + ], + "range_between": [ + 0.5766351978381686, + 0.8006844979819607 + ] + }, + "amari": { + "within_Fortran": { + "mean": 0.16630054685403053, + "sd": 0.017003169113571776 + }, + "within_pamica": { + "mean": 0.15050186128469903, + "sd": 0.01972676938294031 + }, + "between": { + "mean": 0.1601145477064069, + "sd": 0.0172986921233092 + }, + "between_minus_within_Fortran": -0.006185999147623633, + "perm_p_not_worse": 0.9975001249937503, + "range_within_Fortran": [ + 0.0716072110880876, + 0.1935216447461059 + ], + "range_within_pamica": [ + 0.09310945460851341, + 0.18542160260896953 + ], + "range_between": [ + 0.09827879191432547, + 0.19832864865760144 + ] + } +} \ No newline at end of file diff --git a/.context/issue-351/newton_seeds.py b/.context/issue-351/newton_seeds.py new file mode 100644 index 00000000..e260f383 --- /dev/null +++ b/.context/issue-351/newton_seeds.py @@ -0,0 +1,324 @@ +"""Newton-enabled single-model runs on the full ds002718 sub-002 recording +(issue #351 item 4a, re-running the issue #145 protocol under epic #324). + +The paper's claim: with Newton enabled (``do_newton=1``) and independent seeds, +one pamica seed of three (seed 42) put ten of seventy components in a different +basin, the other two matched the reference at ~0.99, and a matched +initialization (A = I, mu = [-1, 0, 1], sbeta = 1, rho = rho0, alpha = 1/3, +c = 0 on both sides) restored ~0.997. The issue #145 records are +``.context/issue-145/full_data_reproduction.md`` and ``same_init_result.md``; +this script repeats that protocol with the code at the epic head and the +pinned v0.3.3 native binary. + +Protocol, as in #145: + +* data: the full 70-channel x 747,750-frame recording (k ~ 153), rounded to + float32 as the #145 runs read it (``data.fdt``) and as the binary reads it; +* pamica: ``AMICATorchNG`` on CUDA float64, 2000 iterations, the #145 keyword + set (``fit_torch_full.py``: ``lrate=0.05``, ``do_newton=True``, + ``newt_start=50``, ``newt_ramp=10``, ``newtrate=1.0``, ``lratefact=0.5``, + ``rholrate=0.05``, ``rholratefact=0.5``, ``maxdecs=3``, ``block_size=512``; + everything else at the constructor default), seeds 42, 7 and 13; the early + stops are off here so both sides run the full budget, as they did in #145 + (pamica had no ``min_dll`` stop then, and every #145 fit ran 2000 iterations); +* reference: two independent fits of the native binary with the ``input.param`` + settings, ``do_newton=1``, 2000 iterations, early stops off (as the #145 + load-path run had them), here seeded (1 and 2) so the run is reproducible, + where #145 used clock-seeded runs; +* same-init: both sides from the deterministic start above, the binary through + its ``load_*`` path. + +Subcommands (each writes one ``.npz`` under ``--out-dir``):: + + python newton_seeds.py pamica --seed 42 --data DATA.npy --out-dir OUT + python newton_seeds.py pamica --fixinit --data DATA.npy --out-dir OUT + python newton_seeds.py fortran --seed 1 --threads 10 --data DATA.npy --out-dir OUT + python newton_seeds.py fortran --fixinit --threads 10 --data DATA.npy --out-dir OUT + python newton_seeds.py compare --out-dir OUT +""" + +from __future__ import annotations + +import argparse +import itertools +import json +import time +from pathlib import Path + +import numpy as np +from scipy.optimize import linear_sum_assignment + +MAX_ITER = 2000 +NMIX = 3 +PAMICA_KW = dict( + n_mix=NMIX, lrate=0.05, do_mean=True, do_sphere=True, do_approx_sphere=True, + do_newton=True, newt_start=50, newt_ramp=10, newtrate=1.0, lratefact=0.5, + rholrate=0.05, rholratefact=0.5, maxdecs=3, block_size=512, + use_min_dll=False, use_grad_norm=False, +) # fmt: skip +# The reference's settings (every one explicit, from _reference_settings): +# Newton on, early stops off, as the #145 load-path run had them. +FORTRAN_OVERRIDES = dict(do_newton=1, use_min_dll=0, use_grad_norm=0, block_size=512) + + +def fortran_kwargs(max_iter: int) -> dict: + """``AMICANative`` settings of the independent-start reference fits.""" + from _reference_settings import REFERENCE_SETTINGS + + return { + **REFERENCE_SETTINGS, "num_models": 1, "num_mix_comps": NMIX, + "max_iter": max_iter, **FORTRAN_OVERRIDES, + } # fmt: skip + + +def fixinit_kwargs() -> dict: + """``run_seeded_reference`` settings of the same-start reference fit.""" + from _reference_settings import REFERENCE_SETTINGS + + return {**REFERENCE_SETTINGS, **FORTRAN_OVERRIDES} + + +def load_data(path: Path) -> np.ndarray: + """The recording rounded to float32 (what the binary reads), in float64.""" + return np.load(path).astype(np.float32).astype(np.float64) + + +def fixinit_state(nw: int, rho0: float = 1.5) -> dict[str, np.ndarray]: + """The deterministic start of the #145 same-init test (reference layout: + ``mu``/``sbeta``/``rho``/``alpha`` are ``(num_mix, num_comps)``).""" + return { + "A": np.eye(nw), + "mu": np.tile(np.linspace(-1.0, 1.0, NMIX)[:, None], (1, nw)), + "sbeta": np.ones((NMIX, nw)), + "rho": np.full((NMIX, nw), rho0), + "alpha": np.full((NMIX, nw), 1.0 / NMIX), + "gm": np.ones(1), + "c": np.zeros((nw, 1)), + } + + +def run_pamica( + data: np.ndarray, seed: int | None, fixinit: bool, device: str, max_iter: int +) -> dict: + import torch + + from pamica.torch_impl.core import AMICATorchNG + + nw = data.shape[0] + + class FixInitNG(AMICATorchNG): + """The #145 deterministic start in place of the drawn one.""" + + def _initialize_parameters(self): + super()._initialize_parameters() + st = fixinit_state(self.n_channels, self.rho0) + dev, dt = self.device, self.dtype + self.A = torch.from_numpy(st["A"]).to(dev, dt) # symmetric: rows == cols + self.mu = torch.from_numpy(st["mu"]).to(dev, dt) + self.beta = torch.from_numpy(st["sbeta"]).to(dev, dt) + self.rho = torch.from_numpy(st["rho"]).to(dev, dt) + self.alpha = torch.from_numpy(st["alpha"]).to(dev, dt) + self.c = torch.from_numpy(st["c"]).to(dev, dt) + self._update_unmixing_matrices() + + if device == "mlx": + # Supplementary float32 run (not part of the #145 protocol): the MLX + # backend with the same keywords and the same drawn start as the + # float64 PyTorch fit of this seed. + if fixinit: + raise SystemExit("--fixinit is PyTorch-only") + from pamica.mlx_impl import AMICAMLXNG + + m = AMICAMLXNG(n_channels=nw, n_models=1, seed=seed, **PAMICA_KW) + else: + cls = FixInitNG if fixinit else AMICATorchNG + m = cls( + n_channels=nw, n_models=1, seed=0 if fixinit else seed, device=device, + dtype=torch.float64, **PAMICA_KW, + ) # fmt: skip + t0 = time.time() + m.fit(data, max_iter=max_iter, verbose=False) + dt = time.time() - t0 + return { + "W": np.asarray(m.get_unmixing_matrix(0)), + "S": np.asarray(m.get_sphere()), + "ll_history": np.asarray(m.ll_history), + "final_ll": float(m.final_ll_), + "stop_reason": str(m.stop_reason), + "n_newton_fallbacks": int(m.n_newton_fallbacks), + "end_newtrate": float(m.newtrate), + "seconds": dt, + } + + +def run_fortran( + data: np.ndarray, + seed: int | None, + fixinit: bool, + threads: int, + work: Path, + max_iter: int, +) -> dict: + from pamica.native import resolver + from pamica.native.engine import AMICANative + + binary = resolver.resolve("v0.3.3") + t0 = time.time() + if not fixinit: + eng = AMICANative( + binary=binary, threads=threads, max_threads=threads, timeout=6 * 3600, + seed=seed, **fortran_kwargs(max_iter), + ) # fmt: skip + eng.fit(data) + out = eng.output_ + assert out is not None + W = out.W[:, :, 0] + S = out.S + ll = np.asarray(out.LL) + else: + from pamica.tests.native_oracle import SeedState, run_seeded_reference + + nw, n_samples = data.shape + fdt = work / "data_f32.fdt" + if not fdt.exists(): + np.ascontiguousarray(data.T).astype(" np.ndarray: + """Hungarian-matched |corr| per component (rows of the unmixing matrices), + as in the #145 ``compare_local.py``.""" + a = Wa / (np.linalg.norm(Wa, axis=1, keepdims=True) + 1e-12) + b = Wb / (np.linalg.norm(Wb, axis=1, keepdims=True) + 1e-12) + c = np.abs(a @ b.T) + r, cc = linear_sum_assignment(1 - c) + return c[r, cc] + + +def summ(m: np.ndarray) -> dict: + return { + "mean": float(m.mean()), + "min": float(m.min()), + "n_below_0.9": int((m < 0.9).sum()), + "worst5": [round(float(x), 3) for x in np.sort(m)[:5]], + } + + +def compare(out_dir: Path) -> dict: + runs = { + p.stem: dict(np.load(p, allow_pickle=True)) + for p in sorted(out_dir.glob("*.npz")) + } + # Sphered-space unmixing rows, as #145 compared them: both sides compute + # the same symmetric ZCA sphere from the same float32-rounded data. + Wt = {k: v["W"] for k, v in runs.items()} + report: dict = { + "runs": {k: {"final_ll": float(v["final_ll"])} for k, v in runs.items()} + } + fort = sorted(k for k in Wt if k.startswith("fortran_seed")) + pam = sorted( + k for k in Wt if k.startswith("pamica_seed") and not k.endswith("_mlx") + ) + pairs = {} + for a, b in itertools.combinations(fort, 2): + pairs[f"{a} vs {b}"] = summ(matched(Wt[a], Wt[b])) + for a in pam: + for b in fort: + pairs[f"{a} vs {b}"] = summ(matched(Wt[a], Wt[b])) + for a, b in itertools.combinations(pam, 2): + pairs[f"{a} vs {b}"] = summ(matched(Wt[a], Wt[b])) + if "pamica_fixinit" in Wt and "fortran_fixinit" in Wt: + pairs["pamica_fixinit vs fortran_fixinit"] = summ( + matched(Wt["pamica_fixinit"], Wt["fortran_fixinit"]) + ) + for b in fort: + pairs[f"fortran_fixinit vs {b}"] = summ( + matched(Wt["fortran_fixinit"], Wt[b]) + ) + for a in pam: + pairs[f"pamica_fixinit vs {a}"] = summ(matched(Wt["pamica_fixinit"], Wt[a])) + for k in sorted(Wt): + if k.endswith("_mlx") and k[: -len("_mlx")] in Wt: + pairs[f"{k} vs {k[: -len('_mlx')]}"] = summ( + matched(Wt[k], Wt[k[: -len("_mlx")]]) + ) + for b in fort: + pairs[f"{k} vs {b}"] = summ(matched(Wt[k], Wt[b])) + report["pairs"] = pairs + for k, v in runs.items(): + extra = { + x: v[x].item() + for x in ("stop_reason", "n_newton_fallbacks", "end_newtrate", "seconds") + if x in v + } + report["runs"][k].update( + { + x: (y if not isinstance(y, bytes) else y.decode()) + for x, y in extra.items() + } + ) + return report + + +def main() -> None: + p = argparse.ArgumentParser() + p.add_argument("cmd", choices=["pamica", "fortran", "compare"]) + p.add_argument("--seed", type=int) + p.add_argument("--fixinit", action="store_true") + p.add_argument("--data", type=Path) + p.add_argument("--out-dir", type=Path, required=True) + p.add_argument("--threads", type=int, default=10) + p.add_argument("--device", default="cuda", help="cuda, cpu, or mlx (float32)") + # Smoke-test knobs only; the protocol is the full recording at 2000 iterations. + p.add_argument("--max-iter", type=int, default=MAX_ITER) + p.add_argument("--frames", type=int, default=None) + a = p.parse_args() + a.out_dir.mkdir(parents=True, exist_ok=True) + if a.cmd == "compare": + rep = compare(a.out_dir) + (a.out_dir / "newton_seeds_report.json").write_text(json.dumps(rep, indent=2)) + print(json.dumps(rep, indent=2)) + return + data = load_data(a.data) + if a.frames: + data = data[:, : a.frames] + tag = "fixinit" if a.fixinit else f"seed{a.seed}" + if a.device == "mlx": + tag += "_mlx" + if a.cmd == "pamica": + res = run_pamica(data, a.seed, a.fixinit, a.device, a.max_iter) + else: + res = run_fortran(data, a.seed, a.fixinit, a.threads, a.out_dir, a.max_iter) + np.savez(a.out_dir / f"{a.cmd}_{tag}.npz", **res) + print( + f"{a.cmd} {tag}: final_ll={res['final_ll']:.6f} iters={len(res['ll_history'])} " + + " ".join( + f"{k}={res[k]}" + for k in ("stop_reason", "n_newton_fallbacks", "end_newtrate") + if k in res + ) + + f" time={res['seconds']:.0f}s", + flush=True, + ) + + +if __name__ == "__main__": + main() diff --git a/.context/issue-351/orientation_check.py b/.context/issue-351/orientation_check.py new file mode 100644 index 00000000..46b03923 --- /dev/null +++ b/.context/issue-351/orientation_check.py @@ -0,0 +1,85 @@ +"""Check that ``native_oracle.seed_from_torch`` writes a tree's initialization in +the orientation the binary reads (issue #351, for ``harness_gap.py``). + +For the tree at ``ROOT``, pamica's seed-42 initialization is written into the +binary's ``load_*`` files twice: as ``seed_from_torch`` writes it (the stored +``A`` transposed) and with the stored ``A`` itself. The binary runs ONE +iteration from each; its iteration-1 log-likelihood is the E-step at the loaded +state, before any update, so the right orientation reproduces pamica's own +iteration-1 log-likelihood to round-off and the wrong one does not. + + uv run python .context/issue-351/orientation_check.py ROOT +""" + +from __future__ import annotations + +import dataclasses +import importlib.util +import sys +import tempfile +from pathlib import Path + +HERE = Path(__file__).resolve().parent +WORKTREE = HERE.parents[1] +BINARY = Path.home() / ".cache/pamica/bin/v0.3.3/amica15-macos-arm64" + + +def main() -> None: + from _reference_settings import REFERENCE_SETTINGS + + root = Path(sys.argv[1]).resolve() + sys.path.insert(0, str(root)) + import numpy as np + + import pamica + + assert pamica.__file__.startswith(str(root)), pamica.__file__ + from pamica.torch_impl import AMICATorchNG + + spec = importlib.util.spec_from_file_location( + "native_oracle_351", WORKTREE / "pamica/tests/native_oracle.py" + ) + assert spec is not None and spec.loader is not None + oracle = importlib.util.module_from_spec(spec) + sys.modules["native_oracle_351"] = oracle # dataclasses need the module + spec.loader.exec_module(oracle) + vspec = importlib.util.spec_from_file_location( + "vi_351", root / "validate_implementations.py" + ) + assert vspec is not None and vspec.loader is not None + vi = importlib.util.module_from_spec(vspec) + vspec.loader.exec_module(vi) + + data, params = vi.load_sample_data() + X = data.astype(np.float64) + nw, n = X.shape + kw = dict(n_channels=nw, n_models=1, n_mix=3, rho0=params["rho0"], seed=42) + fit = AMICATorchNG(block_size=512, lrate=0.05, device="cpu", **kw) + fit.fit(X, max_iter=1, verbose=False) + pamica_ll1 = float(fit.ll_history[0]) + + init = AMICATorchNG(device="cpu", **kw) + init._preprocess(X) + init._initialize_parameters() + state = oracle.seed_from_torch(init) + stored = dataclasses.replace(state, A=np.ascontiguousarray(state.A.T)) + fdt = root / "pamica/sample_data/eeglab_data.fdt" + out = {} + for name, st in (("seed_from_torch", state), ("stored A untransposed", stored)): + with tempfile.TemporaryDirectory() as td: + ref = oracle.run_seeded_reference( + st, fdt, Path(td), n_samples=n, max_iter=1, threads=1, binary=BINARY, + **{**REFERENCE_SETTINGS, "block_size": 512, "do_newton": 1, + "use_min_dll": 0, "use_grad_norm": 0}, + ) # fmt: skip + out[name] = float(ref.LL[0]) + asym = float(np.abs(state.A - state.A.T).max()) + print(f"root {root}") + print(f"max |A - A.T| of the initial A: {asym:.3e}") + print(f"pamica iteration-1 LL: {pamica_ll1:.15f}") + for name, ll in out.items(): + print(f"binary iteration-1 LL, {name}: {ll:.15f} (diff {ll - pamica_ll1:+.3e})") + + +if __name__ == "__main__": + main() diff --git a/.context/issue-351/per_run_detail.csv b/.context/issue-351/per_run_detail.csv new file mode 100644 index 00000000..d6b2a79d --- /dev/null +++ b/.context/issue-351/per_run_detail.csv @@ -0,0 +1,41 @@ +implementation,run,final_ll,corr_within,corr_between,amari_within,amari_between +Fortran,0,-3.3540,0.6169,0.6061,0.1710,0.1853 +Fortran,1,-3.3526,0.6348,0.6508,0.1615,0.1593 +Fortran,2,-3.3535,0.6145,0.6064,0.1756,0.1876 +Fortran,3,-3.3512,0.6250,0.6356,0.1655,0.1684 +Fortran,4,-3.3511,0.6290,0.6424,0.1654,0.1663 +Fortran,5,-3.3556,0.6118,0.6047,0.1707,0.1853 +Fortran,6,-3.3531,0.6268,0.6334,0.1610,0.1648 +Fortran,7,-3.3506,0.6238,0.6247,0.1700,0.1765 +Fortran,8,-3.3560,0.6332,0.6471,0.1643,0.1696 +Fortran,9,-3.3529,0.6344,0.6458,0.1606,0.1648 +Fortran,10,-3.3541,0.6057,0.6019,0.1782,0.1876 +Fortran,11,-3.3531,0.6353,0.6534,0.1627,0.1631 +Fortran,12,-3.3559,0.6182,0.6147,0.1680,0.1796 +Fortran,13,-3.3580,0.6497,0.6503,0.1590,0.1642 +Fortran,14,-3.3577,0.6270,0.6343,0.1637,0.1679 +Fortran,15,-3.3574,0.6315,0.6322,0.1653,0.1715 +Fortran,16,-3.3553,0.6101,0.6119,0.1701,0.1772 +Fortran,17,-3.3555,0.6181,0.6287,0.1698,0.1718 +Fortran,18,-3.3539,0.6428,0.6764,0.1596,0.1537 +Fortran,19,-3.3554,0.6222,0.6371,0.1638,0.1664 +pamica,0,-3.3507,0.6507,0.6421,0.1703,0.1671 +pamica,1,-3.3530,0.6491,0.6338,0.1687,0.1687 +pamica,2,-3.3541,0.6264,0.6206,0.1829,0.1785 +pamica,3,-3.3550,0.6277,0.6324,0.1860,0.1730 +pamica,4,-3.3570,0.6390,0.6358,0.1804,0.1706 +pamica,5,-3.3593,0.5984,0.6025,0.1983,0.1880 +pamica,6,-3.3500,0.6608,0.6492,0.1605,0.1615 +pamica,7,-3.3535,0.6266,0.6175,0.1823,0.1786 +pamica,8,-3.3593,0.6177,0.6205,0.1859,0.1751 +pamica,9,-3.3551,0.6587,0.6442,0.1633,0.1644 +pamica,10,-3.3497,0.6385,0.6269,0.1747,0.1749 +pamica,11,-3.3517,0.6291,0.6259,0.1800,0.1752 +pamica,12,-3.3545,0.6608,0.6499,0.1642,0.1631 +pamica,13,-3.3532,0.6591,0.6456,0.1638,0.1665 +pamica,14,-3.3531,0.6237,0.6252,0.1823,0.1733 +pamica,15,-3.3528,0.6357,0.6361,0.1795,0.1704 +pamica,16,-3.3588,0.6256,0.6219,0.1789,0.1720 +pamica,17,-3.3515,0.6408,0.6326,0.1784,0.1742 +pamica,18,-3.3581,0.6173,0.6221,0.1868,0.1751 +pamica,19,-3.3525,0.6663,0.6528,0.1574,0.1609 diff --git a/.context/issue-351/per_run_detail_pre_epic_e38aa11.csv b/.context/issue-351/per_run_detail_pre_epic_e38aa11.csv new file mode 100644 index 00000000..580e95a2 --- /dev/null +++ b/.context/issue-351/per_run_detail_pre_epic_e38aa11.csv @@ -0,0 +1,41 @@ +implementation,run,final_ll,corr_within,corr_between,amari_within,amari_between +Fortran,0,-3.3540,0.6169,0.6207,0.1710,0.1713 +Fortran,1,-3.3526,0.6348,0.6673,0.1615,0.1495 +Fortran,2,-3.3535,0.6145,0.6204,0.1756,0.1718 +Fortran,3,-3.3512,0.6250,0.6377,0.1655,0.1605 +Fortran,4,-3.3511,0.6290,0.6501,0.1654,0.1569 +Fortran,5,-3.3556,0.6118,0.6239,0.1707,0.1695 +Fortran,6,-3.3531,0.6268,0.6380,0.1610,0.1557 +Fortran,7,-3.3506,0.6238,0.6404,0.1700,0.1637 +Fortran,8,-3.3560,0.6332,0.6518,0.1643,0.1583 +Fortran,9,-3.3529,0.6344,0.6542,0.1606,0.1559 +Fortran,10,-3.3541,0.6057,0.6181,0.1782,0.1714 +Fortran,11,-3.3531,0.6353,0.6603,0.1627,0.1531 +Fortran,12,-3.3559,0.6182,0.6346,0.1680,0.1644 +Fortran,13,-3.3580,0.6497,0.6595,0.1590,0.1547 +Fortran,14,-3.3577,0.6270,0.6442,0.1637,0.1577 +Fortran,15,-3.3574,0.6315,0.6408,0.1653,0.1592 +Fortran,16,-3.3553,0.6101,0.6239,0.1701,0.1640 +Fortran,17,-3.3555,0.6181,0.6360,0.1698,0.1631 +Fortran,18,-3.3539,0.6428,0.6888,0.1596,0.1448 +Fortran,19,-3.3554,0.6222,0.6423,0.1638,0.1568 +pamica,0,-3.3709,0.7094,0.6720,0.1382,0.1561 +pamica,1,-3.3565,0.6670,0.6399,0.1520,0.1595 +pamica,2,-3.3573,0.6429,0.6343,0.1513,0.1539 +pamica,3,-3.3742,0.6451,0.6277,0.1476,0.1541 +pamica,4,-3.3665,0.6768,0.6507,0.1546,0.1643 +pamica,5,-3.3550,0.6353,0.6230,0.1676,0.1711 +pamica,6,-3.3611,0.6846,0.6574,0.1399,0.1534 +pamica,7,-3.3691,0.6798,0.6441,0.1494,0.1646 +pamica,8,-3.3581,0.6311,0.6266,0.1673,0.1722 +pamica,9,-3.3636,0.6926,0.6587,0.1440,0.1603 +pamica,10,-3.3656,0.6773,0.6453,0.1483,0.1633 +pamica,11,-3.3569,0.6343,0.6227,0.1594,0.1664 +pamica,12,-3.3586,0.6708,0.6453,0.1487,0.1562 +pamica,13,-3.3650,0.6939,0.6608,0.1425,0.1587 +pamica,14,-3.3752,0.6945,0.6566,0.1410,0.1580 +pamica,15,-3.3552,0.6617,0.6412,0.1535,0.1602 +pamica,16,-3.3597,0.6445,0.6304,0.1604,0.1640 +pamica,17,-3.3660,0.6758,0.6464,0.1434,0.1543 +pamica,18,-3.3641,0.6294,0.6200,0.1554,0.1577 +pamica,19,-3.3564,0.6696,0.6499,0.1455,0.1543 diff --git a/.context/issue-351/pin_check.py b/.context/issue-351/pin_check.py new file mode 100644 index 00000000..b4856bf8 --- /dev/null +++ b/.context/issue-351/pin_check.py @@ -0,0 +1,232 @@ +"""Check that pinning every reference setting explicitly is a no-op today +(issue #351): each native-binary call of ``benchmarks/reproduce_table1.py`` and +of this directory's scripts writes a byte-identical ``input.param`` with the +explicit settings and with the keywords it passed before the pin (which relied +on ``AMICANative``'s defaults, then the bundled ``input.param`` values). + +The binary is never run: ``subprocess.run`` in the engine and the seeded runner +of ``pamica/tests/native_oracle.py`` are replaced by functions that record the +parameter file and stop. + + uv run python .context/issue-351/pin_check.py OUT.json + +With ``--dump PATH`` it also saves the pinned calls' parameter files, and with +``--against PATH`` it compares them with a saved dump instead, so a change of +``AMICANative``'s defaults (epic #324 Phase 17) can be checked to leave every +pinned call's settings unchanged. Phase 17 reorders the keys and adds +``do_approx_sphere 1``, the binary's compiled value, so this mode reports the +settings added, removed and changed as well as byte identity; +``pin_run_check.py`` runs the binary on both forms. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import numpy as np + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[1] +sys.path.insert(0, str(HERE)) + + +class _Stop(Exception): + pass + + +def native_param(**kw) -> str: + """The input.param ``AMICANative(**kw).fit`` would run the binary with.""" + import pamica.native.engine as engine + + captured: dict[str, str] = {} + + def fake_run(cmd, cwd=None, **_): + captured["param"] = (Path(cwd) / "input.param").read_text() + raise _Stop + + real = engine.subprocess.run + engine.subprocess.run = fake_run + try: + engine.AMICANative(binary=sys.executable, **kw).fit( + np.zeros((32, 64), dtype=np.float64) + ) + except _Stop: + pass + finally: + engine.subprocess.run = real + return captured["param"] + + +def seeded_param(state_models: int = 1, **kw) -> str: + """The parameter dict ``run_seeded_reference(state, ..., **kw)`` would run.""" + import pamica.tests.native_oracle as oracle + + captured: dict[str, str] = {} + + def fake_run_reference(param, workdir, **_): + from pamica.native.engine import _render_param + + captured["param"] = _render_param(param) + raise _Stop + + nw, nmix = 32, 3 + num_models = state_models + state = oracle.SeedState( + A=np.eye(nw * num_models)[:nw].copy() if num_models == 1 else np.ones((nw, nw * num_models)), + mean=np.zeros(nw), + mu=np.zeros((nmix, nw * num_models)), + sbeta=np.ones((nmix, nw * num_models)), + rho=np.ones((nmix, nw * num_models)), + alpha=np.ones((nmix, nw * num_models)) / nmix, + gm=np.ones(num_models) / num_models, + c=np.zeros((nw, num_models)), + ) # fmt: skip + real = oracle._run_reference + oracle._run_reference = fake_run_reference + try: + import tempfile + + with tempfile.TemporaryDirectory() as td: + oracle.run_seeded_reference( + state, REPO / "pamica/sample_data/eeglab_data.fdt", Path(td), + n_samples=64, max_iter=kw.pop("max_iter", 5), threads=1, + binary=Path(sys.executable), **kw, + ) # fmt: skip + except _Stop: + pass + finally: + oracle._run_reference = real + return captured["param"] + + +def _settings(text: str) -> dict[str, str]: + """An ``input.param`` text as {key: value}; a repeated key is an error.""" + out: dict[str, str] = {} + for line in text.splitlines(): + if line.strip(): + key, _, value = line.partition(" ") + if key in out: + raise ValueError(f"repeated key {key!r}") + out[key] = value.strip() + return out + + +def settings_diff(saved: str, now: str) -> dict: + """How two ``input.param`` texts differ as settings, ignoring key order.""" + a, b = _settings(saved), _settings(now) + return { + "same_key_order": list(a) == list(b), + "added": {k: b[k] for k in b.keys() - a.keys()}, + "removed": {k: a[k] for k in a.keys() - b.keys()}, + "changed": {k: [a[k], b[k]] for k in a.keys() & b.keys() if a[k] != b[k]}, + } + + +def main() -> None: + import _reference_settings as rs + import bundled_single_basins as basins + import keep_best_ensemble as kb + import newton_seeds as ns + from pamica.tests.test_component_rows import _REF_OPT + + t1 = rs._table1() + checks = { + # (before: the keywords each call passed before the pin, after: now) + "reproduce_table1 single-model sweep": ( + dict(threads=4, max_threads=4, timeout=3600, n_models=1, n_mix=3, + max_iter=2000, do_newton=0, use_min_dll=0, use_grad_norm=0, + block_size=512), + dict(threads=4, max_threads=4, timeout=3600, + **t1.single_model_reference_kwargs(2000)), + ), + "reproduce_table1 multi-model ensemble": ( + dict(threads=4, max_threads=4, timeout=1800, n_models=2, n_mix=3, + max_iter=100), + dict(threads=4, max_threads=4, timeout=1800, + **t1.multimodel_reference_kwargs(100)), + ), + "keep_best_ensemble reference": ( + dict(threads=1, max_threads=1, timeout=3600, n_models=2, n_mix=3, + max_iter=300, seed=3), + kb.fortran_kwargs(300, 3), + ), + "bundled_single_basins reference": ( + dict(threads=3, max_threads=3, timeout=3600, n_models=1, n_mix=3, + max_iter=2000, do_newton=0, use_min_dll=0, use_grad_norm=0, + block_size=512, seed=4), + basins.fortran_kwargs(t1, 4, 3), + ), + "newton_seeds reference": ( + dict(threads=10, max_threads=10, timeout=6 * 3600, seed=1, + num_models=1, num_mix_comps=3, max_iter=2000, do_newton=1, + use_min_dll=0, use_grad_norm=0, block_size=512), + dict(threads=10, max_threads=10, timeout=6 * 3600, seed=1, + **ns.fortran_kwargs(2000)), + ), + } # fmt: skip + out = {} + pinned: dict[str, str] = {} + for name, (before, after) in checks.items(): + a, b = native_param(**before), native_param(**after) + pinned[name] = b + out[name] = {"identical": a == b, "lines": a.count("\n")} + seeded = { + "newton_seeds same-start reference": ( + dict(max_iter=2000, do_newton=1, use_min_dll=0, use_grad_norm=0, + block_size=512), + dict(max_iter=2000, **ns.fixinit_kwargs()), + ), + "orientation_check": ( + dict(max_iter=1, block_size=512, do_newton=1, use_min_dll=0, + use_grad_norm=0), + dict(max_iter=1, **{**rs.REFERENCE_SETTINGS, "block_size": 512, + "do_newton": 1, "use_min_dll": 0, + "use_grad_norm": 0}), + ), + "share_comps_counts part c": ( + dict(max_iter=8, num_models=2, + **{**_REF_OPT, "share_start": 8, "share_iter": 100, "share_comps": 0}), + dict(max_iter=8, num_models=2, + **{**rs.REFERENCE_SETTINGS, **_REF_OPT, "share_start": 8, + "share_iter": 100, "share_comps": 0}), + ), + } # fmt: skip + for name, (before, after) in seeded.items(): + nm = before.get("num_models", 1) + a = seeded_param(state_models=nm, **before) + b = seeded_param(state_models=nm, **after) + pinned[name] = b + out[name] = {"identical": a == b, "lines": a.count("\n")} + args = sys.argv[1:] + if "--dump" in args: + Path(args[args.index("--dump") + 1]).write_text(json.dumps(pinned, indent=2)) + if "--against" in args: + saved = json.loads(Path(args[args.index("--against") + 1]).read_text()) + out = { + name: { + "pinned_identical_to_saved": pinned[name] == saved[name], + **settings_diff(saved[name], pinned[name]), + "unpinned_identical_to_pinned": out[name]["identical"], + } + for name in pinned + } + # Byte identity, or the same settings in another order plus keys at + # the binary's compiled value (pin_run_check.py runs both forms). + ok = all( + v["pinned_identical_to_saved"] + or (not v["changed"] and not v["removed"] + and v["added"] == {"do_approx_sphere": "1"}) + for v in out.values() + ) # fmt: skip + else: + ok = all(v["identical"] for v in out.values()) + Path(args[0]).write_text(json.dumps(out, indent=2)) + print(json.dumps(out, indent=2)) + if not ok: + raise SystemExit("a pinned call writes a different input.param") + + +if __name__ == "__main__": + main() diff --git a/.context/issue-351/pin_run_check.py b/.context/issue-351/pin_run_check.py new file mode 100644 index 00000000..636c68ce --- /dev/null +++ b/.context/issue-351/pin_run_check.py @@ -0,0 +1,114 @@ +"""Run the reference binary on both forms of a pinned ``input.param`` +(issue #351, after epic #324 Phase 17). + +Phase 17 (#354) rebuilt ``AMICANative``'s parameter table, so a pinned call +now writes its keys in a new order and adds ``do_approx_sphere 1``, the value +compiled into the binary (``amica15_header.f90``), with every other key and +value unchanged (``pin_check.py --against``); ``REFERENCE_SETTINGS`` in +``benchmarks/reproduce_table1.py`` now pins that key too. The binary reads +keys by name, so the two files should give the same run. This script checks that on the +bundled sample: for each pinned configuration it lets ``AMICANative`` write +its current ``input.param``, runs the binary on it, then runs it again in a +copy of the work directory whose ``input.param`` has the pre-Phase-17 key +order and no ``do_approx_sphere`` line, and compares every output file byte +for byte, except that the per-iteration wall-clock times are removed from +``out.txt`` first. Seeded and single-threaded, so each run is deterministic. + + uv run python .context/issue-351/pin_run_check.py OUT.json +""" + +from __future__ import annotations + +import filecmp +import json +import re +import shutil +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) + +SAVED = HERE / "raw/pin_check_pinned_params_pre_phase17.json" +# The end of each iteration line of out.txt, e.g. "( 0.19 s, 0.0 h)". +TIMING = re.compile(r"\(\s*[0-9.]+ s,\s*[0-9.]+ h\)") + + +def old_form(new_text: str, old_text: str) -> str: + """``new_text``'s lines in ``old_text``'s key order, without the key + Phase 17 added; keys ``old_text`` lacks (``seed``) keep their place last.""" + lines = {ln.split(None, 1)[0]: ln for ln in new_text.splitlines() if ln.strip()} + lines.pop("do_approx_sphere") + order = [ln.split(None, 1)[0] for ln in old_text.splitlines() if ln.strip()] + keys = [k for k in order if k in lines] + [k for k in lines if k not in order] + return "\n".join(lines[k] for k in keys) + "\n" + + +def compare_run(name: str, kwargs: dict, data, saved: dict) -> dict: + import pamica.native.engine as engine + + real = engine.subprocess.run + record: dict = {} + + def both_forms(cmd, cwd=None, **kw): + work = Path(cwd) + twin = work.parent / (work.name + "_old_form") + shutil.copytree(work, twin) + new_text = (work / "input.param").read_text() + (twin / "input.param").write_text(old_form(new_text, saved[name])) + record["new_has_approx"] = "do_approx_sphere 1" in new_text + proc_old = real(cmd, cwd=twin, **kw) + proc = real(cmd, cwd=cwd, **kw) + a, b = work / "amicaout", twin / "amicaout" + files = sorted(p.name for p in a.iterdir()) + binary = [f for f in files if f != "out.txt"] + match, mismatch, errors = filecmp.cmpfiles(a, b, binary, shallow=False) + logs = [TIMING.sub("", (d / "out.txt").read_text()) for d in (a, b)] + (match if logs[0] == logs[1] else mismatch).append("out.txt (times removed)") + record.update( + files=files, identical=match, different=mismatch, missing=errors, + returncodes=[proc.returncode, proc_old.returncode], + ) # fmt: skip + shutil.rmtree(twin) + return proc + + engine.subprocess.run = both_forms + try: + engine.AMICANative(**kwargs).fit(data) + finally: + engine.subprocess.run = real + record["all_identical"] = ( + not record["different"] and not record["missing"] and bool(record["identical"]) + ) + return record + + +def main() -> None: + import _reference_settings as rs + import keep_best_ensemble as kb + import newton_seeds as ns + + data = kb.load_data() + saved = json.loads(SAVED.read_text()) + t1 = rs._table1() + one = dict(threads=1, max_threads=1, timeout=3600, seed=1) + configs = { + "reproduce_table1 single-model sweep": { + **one, **t1.single_model_reference_kwargs(20)}, + "reproduce_table1 multi-model ensemble": { + **one, **t1.multimodel_reference_kwargs(20)}, + "newton_seeds reference": {**one, **ns.fortran_kwargs(60)}, + } # fmt: skip + out = {name: compare_run(name, kw, data, saved) for name, kw in configs.items()} + Path(sys.argv[1]).write_text(json.dumps(out, indent=2)) + print( + json.dumps( + {k: (v["all_identical"], len(v["identical"])) for k, v in out.items()} + ) + ) + if not all(v["all_identical"] for v in out.values()): + raise SystemExit("the two forms of a pinned input.param gave different runs") + + +if __name__ == "__main__": + main() diff --git a/.context/issue-351/precision_agreement.py b/.context/issue-351/precision_agreement.py new file mode 100644 index 00000000..b50ff3c4 --- /dev/null +++ b/.context/issue-351/precision_agreement.py @@ -0,0 +1,54 @@ +"""Component-level float32 agreement at a matched iteration budget (issue #351 +item 4c): MLX (float32) against PyTorch (float64) from the same start. + +Both backends draw their initialization from numpy's ``RandomState(seed)``, so +with the same seed they start from the same point up to MLX's float32 cast, and +any difference after K iterations is float32 arithmetic. The harness settings +(``sample_params.json`` through ``validate_implementations._run_wrapper_amica``) +at K = 100 (the harness budget) and 200 (the fixture budget), seed 42. + + uv run python .context/issue-351/precision_agreement.py OUT.json +""" + +from __future__ import annotations + +import importlib.util +import json +import sys +from pathlib import Path + +import numpy as np + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[1] + + +def main() -> None: + spec = importlib.util.spec_from_file_location( + "vi_351p", REPO / "validate_implementations.py" + ) + assert spec is not None and spec.loader is not None + vi = importlib.util.module_from_spec(spec) + spec.loader.exec_module(vi) + data, params = vi.load_sample_data() + out = {} + for k in (100, 200): + p = dict(params, max_iter=k) + t = vi._run_wrapper_amica("torch", data, p, 42) + m = vi._run_wrapper_amica("mlx", data, p, 42) + cmp = vi.compare_results(t, m, label="MLX") + out[str(k)] = { + "torch_final_ll": float(t["final_ll"]), + "mlx_final_ll": float(m["final_ll"]), + "abs_ll_difference": float(abs(t["final_ll"] - m["final_ll"])), + "mean_matched_corr": float(cmp["mean_correlation"]), + "min_matched_corr": float(cmp["min_correlation"]), + "amari": float(vi.amari_distance(t["W"], m["W"])), + "max_abs_W_diff": float(np.abs(t["W"] - m["W"]).max()), + } + Path(sys.argv[1]).write_text(json.dumps(out, indent=2)) + print(json.dumps(out, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/.context/issue-351/raw/bundled_basins/bundled_single_basins.json b/.context/issue-351/raw/bundled_basins/bundled_single_basins.json new file mode 100644 index 00000000..948d896b --- /dev/null +++ b/.context/issue-351/raw/bundled_basins/bundled_single_basins.json @@ -0,0 +1,560 @@ +{ + "ll": { + "fortran_seed1": -3.399644280997763, + "fortran_seed2": -3.399446053777136, + "fortran_seed3": -3.3996402544421502, + "fortran_seed4": -3.3996119263160636, + "fortran_seed5": -3.399563131051804, + "fortran_seed6": -3.3995541089444816, + "fortran_seed7": -3.399617603804792, + "fortran_seed8": -3.3995827076502567, + "fortran_seed9": -3.3998076574695872, + "fortran_seed10": -3.3998408175881214, + "pamica_seed301": -3.3997216530763565, + "pamica_seed302": -3.399558597670594, + "pamica_seed303": -3.3996768215064535, + "pamica_seed304": -3.399518642876667, + "pamica_seed305": -3.399432015696039 + }, + "pairs": { + "fortran_seed1 vs fortran_seed2": { + "mean_corr": 0.9973838178304947, + "min_corr": 0.9833377930758486, + "amari": 0.0074416676119647635 + }, + "fortran_seed1 vs fortran_seed3": { + "mean_corr": 0.9989427216190532, + "min_corr": 0.9916487468589162, + "amari": 0.004623320291597859 + }, + "fortran_seed1 vs fortran_seed4": { + "mean_corr": 0.999372784429559, + "min_corr": 0.9957432465186093, + "amari": 0.0036354243637478677 + }, + "fortran_seed1 vs fortran_seed5": { + "mean_corr": 0.999033626255555, + "min_corr": 0.993034701641012, + "amari": 0.004970984085623771 + }, + "fortran_seed1 vs fortran_seed6": { + "mean_corr": 0.9996709661698624, + "min_corr": 0.9968596716414584, + "amari": 0.002607841801349828 + }, + "fortran_seed1 vs fortran_seed7": { + "mean_corr": 0.9988894018489317, + "min_corr": 0.9921465133433931, + "amari": 0.0050044187434843115 + }, + "fortran_seed1 vs fortran_seed8": { + "mean_corr": 0.9992426681703461, + "min_corr": 0.9941112768865812, + "amari": 0.004326540803187227 + }, + "fortran_seed1 vs fortran_seed9": { + "mean_corr": 0.9987262157772459, + "min_corr": 0.9879189709291721, + "amari": 0.005368950672020232 + }, + "fortran_seed1 vs fortran_seed10": { + "mean_corr": 0.9987029946463601, + "min_corr": 0.9836560358967542, + "amari": 0.004577045376639574 + }, + "fortran_seed1 vs pamica_seed301": { + "mean_corr": 0.9988197782134436, + "min_corr": 0.9925473290369876, + "amari": 0.004493081767497086 + }, + "fortran_seed1 vs pamica_seed302": { + "mean_corr": 0.9996232269976436, + "min_corr": 0.9970666514513028, + "amari": 0.0026077025843725234 + }, + "fortran_seed1 vs pamica_seed303": { + "mean_corr": 0.9998760370088206, + "min_corr": 0.9993005371750217, + "amari": 0.001651445174441888 + }, + "fortran_seed1 vs pamica_seed304": { + "mean_corr": 0.9984774064870074, + "min_corr": 0.9894225634478668, + "amari": 0.005147511117384538 + }, + "fortran_seed1 vs pamica_seed305": { + "mean_corr": 0.9989797499059779, + "min_corr": 0.994136773775746, + "amari": 0.004700049082804198 + }, + "fortran_seed2 vs fortran_seed3": { + "mean_corr": 0.9990637987741886, + "min_corr": 0.9927341373252119, + "amari": 0.004704936426180726 + }, + "fortran_seed2 vs fortran_seed4": { + "mean_corr": 0.9981576718699423, + "min_corr": 0.987098725237102, + "amari": 0.006352106280226102 + }, + "fortran_seed2 vs fortran_seed5": { + "mean_corr": 0.9988945163070959, + "min_corr": 0.9900752075964804, + "amari": 0.0045919722544712875 + }, + "fortran_seed2 vs fortran_seed6": { + "mean_corr": 0.9984511926614246, + "min_corr": 0.990159284635876, + "amari": 0.005775851282398327 + }, + "fortran_seed2 vs fortran_seed7": { + "mean_corr": 0.9989580398485283, + "min_corr": 0.9935704632586921, + "amari": 0.0047472126870288 + }, + "fortran_seed2 vs fortran_seed8": { + "mean_corr": 0.9967476787217107, + "min_corr": 0.9749520396175718, + "amari": 0.008379231447818235 + }, + "fortran_seed2 vs fortran_seed9": { + "mean_corr": 0.9967465716554912, + "min_corr": 0.9692128653877078, + "amari": 0.007910240403602466 + }, + "fortran_seed2 vs fortran_seed10": { + "mean_corr": 0.9943219341832437, + "min_corr": 0.9378651203429902, + "amari": 0.010081459366705963 + }, + "fortran_seed2 vs pamica_seed301": { + "mean_corr": 0.9988355109676916, + "min_corr": 0.9918783082330488, + "amari": 0.0048316508763874624 + }, + "fortran_seed2 vs pamica_seed302": { + "mean_corr": 0.9981044794948167, + "min_corr": 0.9885362597692511, + "amari": 0.005841226562580723 + }, + "fortran_seed2 vs pamica_seed303": { + "mean_corr": 0.9977177294812547, + "min_corr": 0.9828253940390324, + "amari": 0.006062072042578845 + }, + "fortran_seed2 vs pamica_seed304": { + "mean_corr": 0.9990591880211106, + "min_corr": 0.9930293495753705, + "amari": 0.003981188020323598 + }, + "fortran_seed2 vs pamica_seed305": { + "mean_corr": 0.9988763951838573, + "min_corr": 0.9940098117862647, + "amari": 0.005304718261202596 + }, + "fortran_seed3 vs fortran_seed4": { + "mean_corr": 0.999396828999206, + "min_corr": 0.9939018563863432, + "amari": 0.003729894131341692 + }, + "fortran_seed3 vs fortran_seed5": { + "mean_corr": 0.9995027948739312, + "min_corr": 0.9965289679019256, + "amari": 0.003915647203107725 + }, + "fortran_seed3 vs fortran_seed6": { + "mean_corr": 0.9995373733080833, + "min_corr": 0.997313115630426, + "amari": 0.0034475029006144115 + }, + "fortran_seed3 vs fortran_seed7": { + "mean_corr": 0.999737951212694, + "min_corr": 0.9988851323264738, + "amari": 0.002894253487469939 + }, + "fortran_seed3 vs fortran_seed8": { + "mean_corr": 0.9984126558389312, + "min_corr": 0.9850535534433025, + "amari": 0.006228034795063001 + }, + "fortran_seed3 vs fortran_seed9": { + "mean_corr": 0.9980125990212064, + "min_corr": 0.9797064543658351, + "amari": 0.006562078495015843 + }, + "fortran_seed3 vs fortran_seed10": { + "mean_corr": 0.9961808307110858, + "min_corr": 0.9540472986110117, + "amari": 0.007816293587526967 + }, + "fortran_seed3 vs pamica_seed301": { + "mean_corr": 0.9998087338508995, + "min_corr": 0.9986467519039831, + "amari": 0.0020338897938263706 + }, + "fortran_seed3 vs pamica_seed302": { + "mean_corr": 0.9992934120660634, + "min_corr": 0.9950641208037472, + "amari": 0.0038377943779466252 + }, + "fortran_seed3 vs pamica_seed303": { + "mean_corr": 0.9989548528144083, + "min_corr": 0.9912030864194291, + "amari": 0.004001457338479523 + }, + "fortran_seed3 vs pamica_seed304": { + "mean_corr": 0.9995758585079988, + "min_corr": 0.9975891319405777, + "amari": 0.0031725247657017723 + }, + "fortran_seed3 vs pamica_seed305": { + "mean_corr": 0.999321832795217, + "min_corr": 0.9965494031999469, + "amari": 0.004237758146630131 + }, + "fortran_seed4 vs fortran_seed5": { + "mean_corr": 0.9991202381494935, + "min_corr": 0.9943376613161212, + "amari": 0.004807896584858074 + }, + "fortran_seed4 vs fortran_seed6": { + "mean_corr": 0.9996532699778209, + "min_corr": 0.9969838420201728, + "amari": 0.0028179627570559616 + }, + "fortran_seed4 vs fortran_seed7": { + "mean_corr": 0.999592631816905, + "min_corr": 0.9968492901763694, + "amari": 0.003229051969471713 + }, + "fortran_seed4 vs fortran_seed8": { + "mean_corr": 0.9994733805072307, + "min_corr": 0.9969951845046999, + "amari": 0.00382415179094113 + }, + "fortran_seed4 vs fortran_seed9": { + "mean_corr": 0.9991819595533001, + "min_corr": 0.9948901078793091, + "amari": 0.0046200497354493275 + }, + "fortran_seed4 vs fortran_seed10": { + "mean_corr": 0.9978787919021165, + "min_corr": 0.9789276715722082, + "amari": 0.006155067102876148 + }, + "fortran_seed4 vs pamica_seed301": { + "mean_corr": 0.9995596280306486, + "min_corr": 0.9965788934035755, + "amari": 0.0029616145944367013 + }, + "fortran_seed4 vs pamica_seed302": { + "mean_corr": 0.9997192789317055, + "min_corr": 0.9980140310776281, + "amari": 0.0024300833561121888 + }, + "fortran_seed4 vs pamica_seed303": { + "mean_corr": 0.9994433846754331, + "min_corr": 0.9945940089354077, + "amari": 0.0028600874996276768 + }, + "fortran_seed4 vs pamica_seed304": { + "mean_corr": 0.9990326222134043, + "min_corr": 0.9924562885109357, + "amari": 0.0042666370792989235 + }, + "fortran_seed4 vs pamica_seed305": { + "mean_corr": 0.9989924331252956, + "min_corr": 0.9937442592620741, + "amari": 0.0048758452512213096 + }, + "fortran_seed5 vs fortran_seed6": { + "mean_corr": 0.9995812314178413, + "min_corr": 0.9978058116755557, + "amari": 0.0038239318706220038 + }, + "fortran_seed5 vs fortran_seed7": { + "mean_corr": 0.999514078092274, + "min_corr": 0.9961493736965917, + "amari": 0.0035298370569453114 + }, + "fortran_seed5 vs fortran_seed8": { + "mean_corr": 0.9979474199591788, + "min_corr": 0.9861032521379883, + "amari": 0.007113548565940528 + }, + "fortran_seed5 vs fortran_seed9": { + "mean_corr": 0.9977092745441969, + "min_corr": 0.9812674458487556, + "amari": 0.0069307479557897305 + }, + "fortran_seed5 vs fortran_seed10": { + "mean_corr": 0.9963040748261923, + "min_corr": 0.9572839861586194, + "amari": 0.008444862394737896 + }, + "fortran_seed5 vs pamica_seed301": { + "mean_corr": 0.9990798748391737, + "min_corr": 0.991812490346674, + "amari": 0.004365215155725142 + }, + "fortran_seed5 vs pamica_seed302": { + "mean_corr": 0.999052425177928, + "min_corr": 0.9938682616228671, + "amari": 0.004544902710188987 + }, + "fortran_seed5 vs pamica_seed303": { + "mean_corr": 0.9991326202143925, + "min_corr": 0.9925599414827744, + "amari": 0.004078852790282991 + }, + "fortran_seed5 vs pamica_seed304": { + "mean_corr": 0.9996321800932771, + "min_corr": 0.9975972592892737, + "amari": 0.0026105539199796604 + }, + "fortran_seed5 vs pamica_seed305": { + "mean_corr": 0.9990525101394004, + "min_corr": 0.9950573779105458, + "amari": 0.0052706226767407416 + }, + "fortran_seed6 vs fortran_seed7": { + "mean_corr": 0.9994787394752752, + "min_corr": 0.9963826816875206, + "amari": 0.003853602008941948 + }, + "fortran_seed6 vs fortran_seed8": { + "mean_corr": 0.9991179565875554, + "min_corr": 0.9937033424068504, + "amari": 0.004220774931521121 + }, + "fortran_seed6 vs fortran_seed9": { + "mean_corr": 0.9984704041532737, + "min_corr": 0.9891222741067596, + "amari": 0.006042614553299116 + }, + "fortran_seed6 vs fortran_seed10": { + "mean_corr": 0.9976792573949571, + "min_corr": 0.9715053015020583, + "amari": 0.006057537480594763 + }, + "fortran_seed6 vs pamica_seed301": { + "mean_corr": 0.9993817084503793, + "min_corr": 0.9943323743499128, + "amari": 0.003463736140921489 + }, + "fortran_seed6 vs pamica_seed302": { + "mean_corr": 0.9997483557019242, + "min_corr": 0.9978549303692951, + "amari": 0.0020593122926749535 + }, + "fortran_seed6 vs pamica_seed303": { + "mean_corr": 0.9996293319744426, + "min_corr": 0.9960185370855396, + "amari": 0.0023946050179204526 + }, + "fortran_seed6 vs pamica_seed304": { + "mean_corr": 0.9992764975353268, + "min_corr": 0.9954589447119667, + "amari": 0.003994141572805168 + }, + "fortran_seed6 vs pamica_seed305": { + "mean_corr": 0.9995429848429997, + "min_corr": 0.9972313656240595, + "amari": 0.0033531200996181135 + }, + "fortran_seed7 vs fortran_seed8": { + "mean_corr": 0.998433337743135, + "min_corr": 0.9895361968690808, + "amari": 0.00643518217927485 + }, + "fortran_seed7 vs fortran_seed9": { + "mean_corr": 0.9986835321774921, + "min_corr": 0.9858556378309332, + "amari": 0.004858183652285637 + }, + "fortran_seed7 vs fortran_seed10": { + "mean_corr": 0.9964049035371767, + "min_corr": 0.9617609445475178, + "amari": 0.008067803373905285 + }, + "fortran_seed7 vs pamica_seed301": { + "mean_corr": 0.9997786684436386, + "min_corr": 0.9985262254188575, + "amari": 0.0022860058517429073 + }, + "fortran_seed7 vs pamica_seed302": { + "mean_corr": 0.999259598279419, + "min_corr": 0.9947097558602807, + "amari": 0.004074054728796885 + }, + "fortran_seed7 vs pamica_seed303": { + "mean_corr": 0.999112423908114, + "min_corr": 0.9938442748708238, + "amari": 0.0038845591225042837 + }, + "fortran_seed7 vs pamica_seed304": { + "mean_corr": 0.999566253303679, + "min_corr": 0.9967154659035612, + "amari": 0.0029108383790811586 + }, + "fortran_seed7 vs pamica_seed305": { + "mean_corr": 0.9988982189664617, + "min_corr": 0.9933732362767999, + "amari": 0.005252643833358741 + }, + "fortran_seed8 vs fortran_seed9": { + "mean_corr": 0.9989541892283986, + "min_corr": 0.994169254451218, + "amari": 0.005974994529053787 + }, + "fortran_seed8 vs fortran_seed10": { + "mean_corr": 0.9986423997841374, + "min_corr": 0.9895995779298575, + "amari": 0.005617472432972744 + }, + "fortran_seed8 vs pamica_seed301": { + "mean_corr": 0.9986557250966162, + "min_corr": 0.9891814682631886, + "amari": 0.005360023028224558 + }, + "fortran_seed8 vs pamica_seed302": { + "mean_corr": 0.9996343047588356, + "min_corr": 0.9971255038386381, + "amari": 0.002738872715799867 + }, + "fortran_seed8 vs pamica_seed303": { + "mean_corr": 0.9990941929024237, + "min_corr": 0.9947858081838797, + "amari": 0.00442126148088693 + }, + "fortran_seed8 vs pamica_seed304": { + "mean_corr": 0.9975578815618564, + "min_corr": 0.9828843222612923, + "amari": 0.006881352991437963 + }, + "fortran_seed8 vs pamica_seed305": { + "mean_corr": 0.9985258946057936, + "min_corr": 0.9884589663297982, + "amari": 0.0051706321119112855 + }, + "fortran_seed9 vs fortran_seed10": { + "mean_corr": 0.9984767540249583, + "min_corr": 0.9918408069705098, + "amari": 0.006276540536856508 + }, + "fortran_seed9 vs pamica_seed301": { + "mean_corr": 0.9986041275933438, + "min_corr": 0.9848170213311357, + "amari": 0.004646159725441516 + }, + "fortran_seed9 vs pamica_seed302": { + "mean_corr": 0.9989926477063218, + "min_corr": 0.9939290507647376, + "amari": 0.004879920174524821 + }, + "fortran_seed9 vs pamica_seed303": { + "mean_corr": 0.9990787139815815, + "min_corr": 0.990651633823239, + "amari": 0.004116590256502387 + }, + "fortran_seed9 vs pamica_seed304": { + "mean_corr": 0.9972738179906191, + "min_corr": 0.9774399285182953, + "amari": 0.006847851485397489 + }, + "fortran_seed9 vs pamica_seed305": { + "mean_corr": 0.9973536019809848, + "min_corr": 0.9826030980586804, + "amari": 0.007641576909828345 + }, + "fortran_seed10 vs pamica_seed301": { + "mean_corr": 0.9963857293380367, + "min_corr": 0.960681248582251, + "amari": 0.007001875424024999 + }, + "fortran_seed10 vs pamica_seed302": { + "mean_corr": 0.9981676337703574, + "min_corr": 0.9782688764002065, + "amari": 0.005010091281096943 + }, + "fortran_seed10 vs pamica_seed303": { + "mean_corr": 0.9987120734130099, + "min_corr": 0.9847804527627908, + "amari": 0.004349191154381497 + }, + "fortran_seed10 vs pamica_seed304": { + "mean_corr": 0.9953235058707853, + "min_corr": 0.9509878778759614, + "amari": 0.008321524192129484 + }, + "fortran_seed10 vs pamica_seed305": { + "mean_corr": 0.9965803501903417, + "min_corr": 0.961306274186901, + "amari": 0.007279637650825507 + }, + "pamica_seed301 vs pamica_seed302": { + "mean_corr": 0.9993632507318939, + "min_corr": 0.9963030562962545, + "amari": 0.00336599590754251 + }, + "pamica_seed301 vs pamica_seed303": { + "mean_corr": 0.9989130161076951, + "min_corr": 0.992290719846548, + "amari": 0.003655795362540219 + }, + "pamica_seed301 vs pamica_seed304": { + "mean_corr": 0.9992686078972955, + "min_corr": 0.9936924638973679, + "amari": 0.0035167304795695625 + }, + "pamica_seed301 vs pamica_seed305": { + "mean_corr": 0.9990620599367581, + "min_corr": 0.9926554749691798, + "amari": 0.004254937488855155 + }, + "pamica_seed302 vs pamica_seed303": { + "mean_corr": 0.999584203402772, + "min_corr": 0.9972710767947442, + "amari": 0.0025009291903615597 + }, + "pamica_seed302 vs pamica_seed304": { + "mean_corr": 0.9986041716955749, + "min_corr": 0.9917773094805747, + "amari": 0.0047337704633078695 + }, + "pamica_seed302 vs pamica_seed305": { + "mean_corr": 0.999244106003941, + "min_corr": 0.9953375977547575, + "amari": 0.0037017057962124723 + }, + "pamica_seed303 vs pamica_seed304": { + "mean_corr": 0.9985917475017295, + "min_corr": 0.98881749714067, + "amari": 0.004088939800788652 + }, + "pamica_seed303 vs pamica_seed305": { + "mean_corr": 0.9988930115217927, + "min_corr": 0.9930351766442601, + "amari": 0.004499529337141232 + }, + "pamica_seed304 vs pamica_seed305": { + "mean_corr": 0.9991082793499348, + "min_corr": 0.9952202525241588, + "amari": 0.004619937201301882 + } + }, + "iters": { + "pamica_seed301": 2000, + "pamica_seed302": 2000, + "pamica_seed303": 2000, + "pamica_seed304": 2000, + "pamica_seed305": 2000 + }, + "stop": { + "pamica_seed301": "max_iter", + "pamica_seed302": "max_iter", + "pamica_seed303": "max_iter", + "pamica_seed304": "max_iter", + "pamica_seed305": "max_iter" + } +} \ No newline at end of file diff --git a/.context/issue-351/raw/bundled_basins/bundled_single_basins.npz b/.context/issue-351/raw/bundled_basins/bundled_single_basins.npz new file mode 100644 index 0000000000000000000000000000000000000000..0468157f8e84fccb54147e9b33d7e6fa24438469 GIT binary patch literal 124574 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
    6&l;kvTnqycs@-UlZK`;|CqPLJ$WwbGk-xmHZs2 zb`&qR8H^y!RHG}a?8%^I=i}I~F5~FtkvZ1rmLBBQD)EHUx)WW{UEt1XSVH}*iY{Jt z#M_&r){=CjA3VzZenZx;9jRC+s)1Z~$U8Au(>uF_0>o-I?_Qh%4u4OEZ?SS<(fLPd zMYE>!DRlhP-2`doy8Tg+F0%|*6M z?$;o?oPVst#a-apekYohn_I+Z;W}5!R0}-m*v2>{$($uXNM(4v20i^0`!Ifn600(1 z{L_-`1Cn2#?11yzAU-1gX-Pd7wl>|xweRx+C`)tMiS20M+%u(bJkLn~_`pPVXc`?p zxRzKfu1EZsLchgQqDAWc=SiVmNOQrS7FxCHFQ6R!5F(%?(q#FiDg zSYWvgbGxI?404hwj(zrSCmfZjk+hTC0$ksQOp8k>aC!IxgZRx=r0}njsf^?egfoLb zX51b|jtRgDpE&m=$1IHPStX>Jn#z%9j_=Oz2@FO|}Uz;o5TacJd#T_?&Oj{o5)yx{syf z+j$E3`*%Y~dDRj~ti^oxH#?BqYLTu8@o{9`{U(;KtwA(Kp&zp|R7IjQz~B$h;!!;#;2~ zpt*VKlbR9-wswEgtxfj-=LhGXn6a&Zw|W&FN;=CRiiw+OrKvHOC)2WP72za*;0f(^ z+d}cLv^*E9hS1+)pZxn7t*HIC?3CcbI1sH=EEzZHLcCG%Y0>;72o_iz+ylRX!yPwL zv*tKqondb719x`p8MwqbK)e=G&Gx%~lYH_woRgbRv4BpU57=Z28UU&744S9+&!Kkv zP!s8ogXmMXp8S^SFmjQV9dhJfN8N!%AD<#hyu3P=+fFMC_Xd@oS3U6qJ)mUWP^y_h zyIJ>Dgo&=8ryqnG)hVfQICX*YzQ3b@#e5e7w~`$yA7+=2xzi6)4ssp+w08q2eXL=R z4X1~uAL^d`pPS~#77U;5+;%?ks) zl(3qe@f6)p3fvgntF<7{1`o~u7pJ8;4yGm|pV*A9p?#bu_lCS6J#mUh#Wgw==*VNK zdU5_CbiPF{oRMP;-M8Em0Lgd#I@?{>vy)VKSXAcf$FE}`b@Gzbm)Sn_H_0+#?Hf1r zgre#f`uad_-u{W8kQH$5WCn}n76U%~E9$>}UX<`b&NDU3%cTGQY4h5KLNQn{eo^M} zYZ@4~UOeViT?EXvu7;EdF=4FbpVq!Lj!s-->>A7&M!ek?s&&)Th=V(1?_4%DWSBqL zks-c>p86Qio-QW5GOHKn5@z#&d3CbUDx(a1o{`sui9N{pJZsy37WDXajCkxzISL}b zJ^k@!d@|UVznbqx&UJ3K$F*Xcy3lie#jV!U#1A5_x|w^i80~xD!=d+i6*zuVKkMB; zgdY8jQ*QY{4aZWJFLZ0Ozzs`*`tAKo=+zB|nQ(PBC_$rI|Ils~T~vK3Qop+go$MZ} z-S=z*jhudLtL52`D3m=<9pj%Rc^y$97t2{t!&-b>TapHE*vEmIUgCRAd7-LrvrfJ{ zl$m47h+lJ&&7VV>8gtwc_wv6_c+UQDv{F5k@Z1G#AAOwyerPEW7JEnynbaBpw?Mdfs(-(c@2tap4lFXS zXMkLS9sBCuZh(DeCtTTn0NTIQbYm1eF!0D#T{E9H(0I#ck|})(S!mh)%G?-4b6&=a z$xh|S9S7HH*mL1`-uk*Sa~nt`?M!I*LNDRoKJ0n2vj=s!KT184(ux$7Sd+uQ_kkO( zc;SI(GdT1%X&9?ZpcAC6QmTBO)e%5ob zp$yb~X^SltBlDXgHLL$bmw|I#MUZLr5cn%+V{$H1M#5eLd#iq9zB?Sl!KV2L^6pyw~KmI}kO`i%@iAk*k zxqCb6?{m=L;tPx>iafON++;}L_rs$oP49-|=0!%#{z>BcDe4ihtFuer$)z9c{ycNx zSIH*&Z($(PSZM;?J(@&!U9}qRRBNgWBpx`igGHft-ZTLVE0(=d5Bty=x`^-(u9WcZ zZlNZnhBb7&QvJ2Z4OaN|q;^d6CN<7B=ImdXDMwCL6P9Ds;~@U;iM{b#-@)776<79G z<0zWv^GXvxJxugOFC6l=k#Uqa%S3VwNI%x+r@4a;H-#M3S!e45*Z*nWqv>h~kyjI+ zohCUWw<{fjD~f9%X{UUQ)IH+;vUqmh>pdkF;VvD~e)|v6F3ZtF> zMQEn34Iu-D^02B09q8}znEC80k+`sMc1w@63CQzK4Hf>C)nb zGmU+=(}WB4)x6+CoD)K{A59xSP%Wd33mK-$$GpEE*(?BZ<|YSmH1Q}MjMRlZ ztH`{gx4SN53(+}?pZP)FLo}&(rmZ%aF?U0?{#BVqB(q-2+Bw9}6erC7ZE*nF`bIelY$;Y^tg8FZx^M&KDP4i0B18Lnyi?S!mP(zq}9iNl}jMuma1lF+*G<)j^%%Ndwpb=9tZK>o^nRUS$h_R`1n zw(Tkq_Q=%Udr}47WgPljcUcY=Eyhg5=V?KQR;Fl*VS0Rs-97B$AUAdtyAd>XQU*%W zWbCcplEA-&@25UVql4KEDu$X*x!~DS^^+FS%b?UvtK`v*EwHcHcy?Kni+KO62eMNY zA&0p1l`z7W`S<)%s6 z9E9SIw@x+_{=d8$S4&CDIw&X_i5a}U4aQ5dn`KV%!ei%Gj~uu;i+&p%+W&Tw6OK>T z3jHnK02-rpExUTRLC>*Q!*5Cjpw<0DogKu}(bOa#6j(?6gzv&5N*Y%|wQ?{=QJY_a*5_%pT)T8O$%cR~I?@UwAZ zUUqmI{Ep3d^gD7El}^<?E?4V+sPVx(qmZno0Y+FR)A!J2niroj0M&Q&cff!)J%Qfq zByV1v%+Mc4gBg@RhXPA(93_;p#63ul8{+iy%ukTs@^70rslS!r%}}XE-F?(BxOLq0 zxe`0BZe6vWRgr*2_g^|)@ZSOn`a8AS94Rp>uBqz&#stj|^#NU=lhWA7EEHD1>gi&&UM}cP%VKKv zwg)`S`qRAhn|RK}UmUzdMS}-!HEQ3AyM@lYPCEAE7!{QNmtdiLg6u;}#7?v74I!oV z=X)CR#bLF2a{H7&DvxkU;=^+yo4 zcCPKYWxxwBcZnwkcrn3m3eTl_3)j%*W_(dD$yc9Y{2u=E*$jgH(R!B0B_Pd*7fi6k1;<0~nUeqR^9|r7y z!&F zRG#!dP+xifi_*j<;*ve|b9W{!{CcJ~ZS}`8s59KW@Lrr8{~+fzhB^Uk*PmrrCk67mgxZ=E4U0Knh%4mua8HTMPd7eLUe= zPLKJg^i-A~_JYsoyLG1Ai@xdlGMkwn~!1l6E6lD9bSUpvMI7*av>! zb&VFL+%0`E;>`sO19}V$q{h&jFz;=J%o(5+cjBc-x)O}MFhcQ-@V+lk?cbYA^75_8 z=QDIPR*}DQ&KM=}0SL(Xm+SA}1Y19|9DflWh+lY6y1C9Gx^e9*19vPdv`g-VZ_{>Q zT1jq|3%7Y8tBFZKh*$&iedWx!>-#3kpE;?`dV>ev+BG@Y#Loa-W3G&S44gx59`?c> z+B>kFl6UZ_M`X`uF0a_}ayMLTGk<-?dlnR)QzWvb6Z<{#OAUmA1${DcCgFW9cVL29MKQ#$;O=9$7j zQvn=)T*31AR1BcI9{Fy}oCn{?`t8puFNO^_SuV$%5WvS?*L!52rG{sPqr_#X`jN}_ zmEr#RAh0)Gtmlk&C3+G0NG0J8100_MJ(|-@_(HKwLXM3HoV4n&xOtTe`Z`gr?5O3% z1D{ipZ;~FZyLrsCb59Srx;n=$eog|WiFv=EdMgbbpE;gR`rVFhBN&C!z zC_%hLzvatIa_J5lNm>T?N$(=wE{?*FBbxOlXwWt96lh6i z^ymCL11-N^zkTAl4s|l$Gm_rffj1r{5~zn@d6;k;2lw-P-~peRiYLa6EbF*&XldOA z`}HLg%0_3wy}R~o!LnHH0r@obOK1iz#&uM*mQW*CnjDTVksQKNjd9EiItB?Bmlv4W z36Sjq3JZ>V{lMnKK>GLWW2j&5rFMH2%W>If>*tUY@5;S^?FZrb_0bOMWJ4{XI`4#B+|_6QZldJT|p3MwU&A@oyQf4&7?ZZGsYlROElOUz} zUGtLmy>Ng*%T6?62axKIIx`C|0hW&zCQTS`Qqigl@09{6s(-YH=}rVl8+G67?rA(! z>P4(t;Vw6Fxjx-FdU+D&jc@EMM{Gc;xzYYEYfdzS?*pOI2?d&3+rQ@efE~T?T2)V! zaSXPH-}%qi?gyB^@$IRk*D~a`6?UJ-^1~@f_nt;4V_C-szRGlh2cReYm%o1Z2IO5i z)*#De#5i@jEDnS-fN5!Lk(-zvd5w3kBhi@$31$6|{n3>G<>ONNa-T>P>7>4*Z8tXr zaAS$wp);N!HL`P6#zLrr&B;&egSD4lyC@2RD87U|5Pl~_M+!Q%7X1%;xZ7~Ic z;{V`wDwaNC zRE}}axlR5IDlGrKZi)2*_^+|f@d)j~)Md)=jUUe=Tmr8WpV?q@NRO4S;dL$qk63-u z-6zF># z(X=ToC8}}~E8dSsfo8z`$;&DmAjm1{b+#8CGLf`-d4GcpjqsN8i`8!ds;&!$FD@^D zFZJ$dC)P8vwKq7Txgmy%<+gp+`$UZz6P2#K$vXh;H2K#=DEGh=y~0NkjML7+{^PRE zTQa12Hh%GL4jsyVQI@w|ha7#_wQByGl?9RGU~Ho*W=1b={=)V7FrvfvmvkcQb^*uK z(_JHDD#Y(~Lkw*fIXclF(8McFiTLS~Qa*U(nsM2&6^?DMnU%KZ^`U~?N{S~z!^yWbS3O#836MF{axDI%aTQ}jv)?=0B zL@7krVEjq48OBBLEw;Lk`2jV5eDyA>pN50Y=gBoG=@Cv>T@lhHGKA-1LDs#Q4zNE= zmz12j0-sbih@w~XfzC}P#xFN9J+2LaNHzxa&6e(Xl;ALAmQQ{j>w@{cT6SwgCy$^; zys3u>@i7oSa%2=9T?NrK2CRNhSwMdJBGji%XJN*7 z2;`f7k!8>+11@8D$su{Gkh=aalbRVnnm;#jC+Xf2{PC|%SjLDJ-Br{#xBiat*2&&! zUeeeEaoUz|8%ns47Vg$(C}-OlaLfb`fYJH>Ffjw}F$9r}4Hyuhb zU-)Aqm><0&6jtESItgn0u3dQ2IuD;Q**qmm=ztrYcma3q4?&(u;sF8aAfU0dOOWZI zLSrv-6{~qIfD|th|MI?082zk-_=fNgz|G~xe@>bfP2w>WxM+a!TV;tSkEyZqMWo40 z>kD#3w$NX`z-iI?Gx9QsIM{3t$3YnMgEx#E!m(0Im*NFKr6{OlC<9nffx7AS&84F6adnYl> zZxkG|+Pl>$&?086a%L-@R0xeLPusl6E_|S|^Rnk?A5b=dw0c%@#P^#LmC!I98t|v{ zGTAW>*0yNmG3w7?xn)PmRD4X=xE44$YeI(%gkG8Vyi9~-+N5nRERDe(Wr~phl*>V` z0ri%(3?<_HHRRn50(|s0BTjkc_8HJiWBl^^=M?yGBVoadjsg)HkIU~c@KOO@kJ4c;JXbZjBbKrUMN(@% zjbCI&J_WiOtA}IU<=56UTepwlv)Q1Ak?<4X{^p#EN7MkIInXmRvd00wE2{>_(j>@A zl1rRuAs+HT<9GVc`XTu5k`J>J11TzHa39sB*aB`QmuUwc{)N_eJZF)R{|fQ$fb|d#kh+_*9Uxw zR(a5oKRFK!0y_b@Hil!AIs$frRw*n&n2xNV$1B22i}X@#RdCV@Akpm9Ru12((J;K9 zhtaDHs9v~Y5I0>0(2!&?lJi-C+o4D`HP+|rGoIhcXvW#>5zu0|J<)&|H~}dLy}B;EdLvzzJ6Pk4h>Nv|7yHS zk8CZTP`xm0f>soEb3u;ys95g-G5z{7{6^_1fxkeB+@A>3{Q8mrA+h!^*%>~Cy57UL z$ZMIkDHJ2+9E?*#S1LYx(`774$H9B`8hauf!%IFni%Po z9(hH0n-SHxVjwEaEr3v0UwwFdgAv_Lx2mvo$MXNLwR6|7-kN)g{2?LRXE62Zl*h&# zF>>x?WhZX*1SalTH|1FnVLS-jx&9=2gx8fF=*90>+qpuMZZARdmbb%_&EW?KE|h}5q~YY918Ip`#$I08iBr9 zlb%6?jZlf$!H%_R3!2j<1UJ-@pi+9uKV53~;rAK6J-&_>AUmBkdhrPz>PSo~e{&KK z(fn1L$@K-xyRg~13rAtSdV&MN!i{I}ua+RWUB>~;*13u=N=$(?zxAAYi(7w{WT1=LT(%9fb1+11D~XuA8yxNiP?|yD#Xgw_+lEjlcmBGb;dYJ@eM@NE)zaZ3Z4nG4=L+x@@dl?bs z`gEev%PSyWwXpP7#3a1TmE89_yc#(0ThcgD?Ze?w^_y9yWQeHEk@|C{1*k|T{`DHy z3V5M&)c~iq13%)4j~F3cP}x*$SbAy`_LRw-E#I7i79LZvAF)0G>rM_l&%1wMjNOYA z$6<{BG;Q@~D~}Eld#FV}l0OSM5^%V9&2|{q@ae-y@fq-VPb8xsvIePF6&0#JZejfX z!R-0dX?U`cm9Jb=2nFez_*6Xb5Wn>ian6-JxXVjF8gcvwR9v(gl00KTv2yVA%Lkax z>j_0}hc^S_f&G1|Y=r0=oSvB8w>BVK;}oM0x8bWhKiPi9t-^;@FSYhbhe1g?F`L}v zAuPI^A--^q67?4}T4432Miy>ASU9%D-gl;a3Z1EWu+Mb6!{#X^GS|rbG>mr#yrt`q zJ9}9J<*erWt~hrCmD-n5i&$=Tuj%45-S!UfZ^HNz(r^kx<;-3Qf5!5`>8lpLG{i`P z5=&}^%n|gjQ4d=f-iM+uB(LC%n}BSDX2m_KK`KR zB^Izxj|j284P$GiMSaZgRgp&Q!A2hDf?kY&JsfPCnH;bUy%>&b=?pak+}wwk{e!9i~zs?+ogtXk2qei_Di+0n?;!0cl%b5>=# zN`&bMqIW9^3N~Tnghtn$T8vxLckzh~_8ijx4J<8$eg4wU;k{_G2g5g zK4NIjNd_O{q2cKb%`#fl$mqrvqwDq}_$Ebka7-}?slB?g9mRKHj82R?xmp(V9GZ@P z`0^M$y3|i<<~$2yf)S+6kO-iIXc6NxaZX;CBCu+^klMc5nUlhf9aGu*ll-% z*c+=!g_u9%71v7wu_7$@PpU|TizY;m>I(w@7WD&RZH+t^F#^O$W$<{a9OD7a1r3!x zAVPRqW?GwjWtGq2Z8IDev9}d@HVY$Fywprgmf`0| zs-!vcB*@7-??H)80<^n*FTw0&87SA>@V!eu2|H+B{aC{8$t%ah1?-ixK#Zx9yDNVm z1fA66;nc~|UE|5CwEOso+Dqd5kFkEZH1bX{V`3DTk=RSZMC^Vwo$}4WvKdU%?COkS ze(5mfzh^61SpHuM18_PnLXP==%-)O}Q0NO%-fT^XoQmI7lQi6g`Y);?C1W=sdXEfzLC0U98sx;@`BP8oe+Bu?+0GzC#0) z)Es8*BV<6e&+PXe^PIrv`hBVm@>7uNq0)OI;z>waPE**AakN+xE6aO%r@^a}bh0|t z0g#vQO3;px6d7HV_8?z8hPb7mM^e~%(dizwXbRmbY*_j%6NE>AuFE={p1#IIYP!e1 z$J|?hWp8TU413|BPGJF?GFg29{nIgMq_zUQCp1JZxeP*DqJ{+G>+9f4Rbihrb1h)) z_|&j#z6&a&2A3QyFy9@`&Klp-IlyAY5hsQ5|08m5{IwxFfV1kXg3AxP0K?wo9nIHk zfY!coj%KPD^l(+QJhwT5e#_c-Bt-||?lHYg_~0xIEBuk+LP~@{XQn|@K5R~?9uv^3 zhUsAc1@C@-hMm(JPH(!>V0}6te%3{PAVcw6U+l*7F923jejhRHe2@1p;Cmx0#&_aq zxwbjDg3WIf#uJq=?z$Fz`gi0ME>G^G#*g~}!@Xa){lF=>`)<9{nM=3KGRS+wt~!iqKyr^6o-lu0f(~ibmG~HM zm7~?+ONloQYD7Z20*5vbd4tcvYAU!4;55W1 z5Iy6%goifi96nIh?}t)BmKTHAw}9!<*-O4%613RUmEGKJ84k2e-ISJMLblJwdoE&} zmuU7GmuMA?<5%ol{fTB1{G~kR5BM<$vI6XJ!W2{R?qtK1_k|-!-9JgWi}^MG>6cYW%m>r+E#H=O0a(S; z6|0?m1JkC_w_S$zA-Q*Mpv})ez#u8^^!JSnAa+hJYtwoY`07tA&-CrWS!uf=Wxg|b z-bm*-vS0?bn#{?XH&CE8NAiBsTfIew>ee-GH+<0ZQzorF-eRFi#-_3x|KlxXi?ybTYI@knZ z0ml0Z>?ohe>#WVBDL^(P_gb6fB69DhX&k>1H^OwMvhCc#IfRXhtx;l91SOnVFqM!L zK-$ha?4kwu5RvWr8Z=S>nSCHfLAZ>M*cm6r`(e8I)sO#bCov9yXPQ@1wH_go+e|68 z&MJj0iP}6NJuiXW(I3i*FujJj^LUPvaxh>$S=X#isOZqe7qQCc^aYXbSEdSq@V77>ftc9y1_QWmNTN#q@Nz#6 zP}09iE4#4+V&67AIvQ*PRYxzh-NIG@C99y#h1abhB%W~K+k09fgP>k zIQVrYLXVoI?nYU35u@C<@36JIZ^Df(VcY=$#@EEF9dpz@gh9S#H8RgAk*B`tMpW#l zAV`%en_K({2vn<(as<;Ngo1(WQ-8NW@Q;MdYf&4Rf0tW>v7H_vsFmQ`O`ZnYDqDw& zlqAT|qlU(KEfRF(Fz%9C_a8v5&(pH2iOs_aPj+kDNzu`X$O^)yMJORB89au~tL8Li z44lm=kow5at>YEgePAgR)16O1YmgX@NR=AxyUU_zfb|?a`NmxpCc}Zu7vAK_YGy_z zo-Xk{2%toYKdj1Bs1cxD0_=~-+PASdR8|nn+BOsuz5hlhR{&f84i{cw{gDX@S6%$6 zIFVV6Cm+)Acu@X_@65NYXi!a@-xssX)ac=sa`-J~3at09DW<~Q~rlJXBCL~^WrC89Cz zvL6eL8y|K*+2U_VIQhqh{@IIEH~WtgS$;-1d+n4K>2FrxpjjhE&(e^H2lz~gMq#ol zBE*i?{k;-!0lQa@al1(iJUIhZ!wyu>^v|GcR(xJT#sFAWs$+1>;1ss=3|fbw#~k_WG75r18MKUML4Fe%D)1J^`|zLgB%X|rQO zT6ZZl=SK+8+^Ze35ZnKlW$$EMp2tVW)Z*>=Vj0l;x8d{ho1?JpVke7qTM5|dy)qq< z{1;l@5Bq75jOCS&E=peIV@8esozciplc8FVo>}s4v7=pO-^ddZ$Dx9Q)(ci4HuSqI z`z7eXi}n(inK+MgphLSx_XgQd0P-M&@0;`q{6)Pt8LmAEY$iHy6GWlV@oC?4XHF92 zvR;xKF%1RkI`@M2_YYp=M>CDAoiWz)DkqR%zr%|5kXiCPS;9lsUUA^#H|&A^+42{6 zJ_sWfN4ou!u_6dBt|heR*~l68T<+r<4jC^T^=!h}QK2k8S{}51 zfVu2z@*dERH+;_fRTPox4CEzvxe2~f-xqmbUk{dc^j?S*pF?N{f9N1iga|HRxhgJf z54^)h2?t6T_lE64HMJip>cGfLIIlpBT(4G7Hf5XwaVzinA6HC*w8fqw^(V|I-=Low z=6!(=(qY--=hz(9@rJz z0X?e6&Kwi?qXY`-KeD&h#<-*nHEztKJ3yXJRYuo>77++&3HZ%Tgl?z1%TW1ELC%hZ zHPzWkIC$HC`))`(-Lv@3x zjYp%%ktAoNxL8FFaozU6aC?IeiPW1Yd2y2rJ$F_twAOwEL_6#m&#;Qb=QVqu#}(<3 zIo#umL;J_j-$=*cXYn!2@f;nFtYbto^y$+gqwx?c!aT(Y%!eGqur@S>%?n81%GGRQ zyur=;X70t9PxG~Xd%|kwA;=H;?t2^C@9k~PpWM>w0Xyn=akaQbVCL6&{Xh^0NgZsT zcCHS>H~20W^89I$+CYVVY33a$#P|SY2@b+bK~Hp7ISG){byLDi^pk+Va8&$G(kXoW zJo%~u1v9ET91xYm&xk~Rx0=H@WJMk-G&>3z9)e434Z!ynDH7*Wr*MvI8zv7?pmO&o z0GswXza9~Bq)_CZ3)O!z$ZLNL_U%iDdMa5I6YH^~cPten$XeKtGIp z#Bc&Ta{Uh{mrr1U^X5ji^bv4-KfF;kSOaXn$6Wl{(hI#~i1(h5QXp!>Y_VAX9?I>K zZ^c$fDXx*k_Z-|@|kRn@EDtfBiyE#pg@2|ql4SRSPx4; zuj{{egY?KGF_~ibB`HKPYB}(ZNh5gToFc@A zqF=7J^Q&-UdAlVIvFmwja6nA-sjvV!+GzYqnXZQvxi2|S*Hll7%D&JG;=y<$!mRBg ziIcnVRxNHJLWddo+3Hhbqp%NKo{7qsl8Pd4R6lA&>o6iz=8+n&lzEV4t*6Le+W#}SI@vZY7{5n(rF$_ zfj%DIPIP^b_1g+9T;Zge1qtG|L5E$SwTD^pBzB^PPW`SdikY4i?eeEGUCbe<+Ta2_0Un6qqjC1^5Sbx7Z@7 z5c$oz-&@$ZapR$8FW)~3l*p#>p4s;Sc=LOG;d{v=`0iwc(4+nsYQ8UW=={rpye!qO zNmE`2}{&pW5RWk1(Ikw0_XdhuHl4=$EGe^>bP@CB%5r80%q6 z6JS3*=Qj=l?x(!^-h$m%Dr348<<@}7Chw723HwDwFPT*6Y(sN5p>#ZR0W?PB;T?gg9Z0Zor(){`ElPHC z?xVx%2}En_l$dY-gf7h1&yKNvh|7}O3LUBJ$eR!9*Jm0=VFLbV=fE5+m-$MLWpIZc zHRv8fa((-N5=~d6Kn@8qsRz|XIoMIlpMj2&>9oib9;>gc@>ma&_siAH&1kD>js)HiQKD`Mt@0i%)V|mEU zwZ#mT#0?mK{XnNp1|KD0(yHD!{s)~j#d&Z1#QMJ`7q^8Iuzn5~1lU z+`@RQ?6>z%G$x^Q;GBk63g&C$#;3{CB16JDxOu#lR~vGDeCOWg^nH{0sN0*?b&!+P-EWQOQwneRh9qR?@2cS z7QgbQb8^rljkoijdtSo$8^yJ2c@p?2d4=E=QHJx#{k4*{K++r-*t|Kx?n;IrA{2Sj z81Ke^pik%bAPG7%d`|talOH*VT>IG8bO56L&YM4Sr$xLuEnGd|1Z3}xKfGRs11oZu zef^J#kXwFc+B`NpK$g>djS=H)>4*NLiI^XQckJGO-=XaVr(rpPy&a6`Pl}AHFgOG$ zaSEFM=;%?S6{C>x~vV9nhQb<%cFGJ8zGgX2Y4?Rj}7*LEq zhX3VX`$QTfg4pkCN!t+@!jO%L(JssfrAbkyIeU%)&7PHgQDY#0{<3q946_;rja21U zIv*Bbww{4+SwE(mQLsvi$Q^(srRfsioC&}jNPJ$il?q)O(z;!Y`PvKh&NurJw?oOc zO!^dI6f!QS9qotGqBxz;UOq#Z&+zW3Q!KV$X=J(?G!|1JqXQQtUkgsbznny3?M4Jh zRG3Vy$Qs6_?Tqm$N+&@+;Vuxkr%yvIyR_>MM>pZXtNkazVgJw5FBqp|`u=$f6Djk< z6(~Z9YZbn~hUF$39nbi&-a3NZ%a5#gp|(j<;#)HYRDEYV*?t1!aAhvFGHh%>rN?rn z|1u|_c1+~EQLFQa{KxJ?Lc&f+s?Uc1a*-Le)_qYN9XbKr2A8FKv#1e?oLr?P3(Q~o zgq>5jY7-JFZYHZ+?E>9o-q=f>BnaC^c~Z~}J<9mEgSV~?2czGzvt0L?1Dm4R??tVT z!5fn>2wDsL?PxzFqVa*dz}iY}AxU^|i_7Di$oa-1+o# z_cVaz!zn7-0%-z(QaiQTozal6rx8KgtkYUWm{lIx`>rKJ3ji~ zI9;e|$6qLJT}1t?j1rk9?|F131@p(}&En>EaPXkG^A8W!r-NL)@+hhK07w+6I5WHc zgLK^?i#zmWh!>5=xz_aqu$_5fQ9f@4T9KMQ(>^0YSVE#{@@E)OC-klI{0cwvmtjOG zF@phZ-qsaX4t2PMrX@XXUJ~u{IXp0F|yD3F2~g!6}up%~VXTOQi=XBwB<3-9fQw4XsuVSEkT<`#fb6_a^!f@_G|h6GCXH9Mv+~E>F&SS zK9{YZ04voq{#FKV^h>Go`}dDK!6qu<$RUQ!AtfhYy9X1X(kb{ z1pn^em&BB4@S1%0hctTh;eqbZX91Tmz0U}4|H=$05ES~V~x!io8}@)op;{+(g@N=CDQCP{u| zum8Bqyy6sG*WCA^8p1=Dl|;r(vHdSd^;X&=Y63LMNFrOdWge>HZ#ik}lcVj9mkPP6 zFn(2u5YM}%BT%-HFaIC+3RGTrX}gW(W6wR_;I2~H18#2xRS26fy-!8*6MVi2LtW~$ z-o>8+gQb)slI=wp7GoaT?pz0V73pHCuf8mpK(ciO|1VUr{zrG*BLZhp}Js!-1PISRZ)YgMpv#*1>BT&3xwnsIV3y=JUh9|G=B?`kS-AFrX1+ ze5<%|jQ^jSl-rK=V5ZkB%A__iA`1;+=2Z+!VADhVH@m1bMenN5`doIJCWJ z<}2O`BDIZ=t^Edpt9pihH!%%jX57J3O1%j!{#{IOzJaa3K0Q7+wCT`rC#N2@7CdBI zxIcIB`T;hN9%?7YbUBih;$weWBCOYCbSpj_0`E`PI?a8~pbd|lMUnu<$F7~Kzojt> zTd%%UPd8fuZV_!Ou^nSjX8Gr`=T|ImDPsN7VYLF(*;!r?C&BW4#>D>I&9tch|Q1|$-(l>e9e|G50AlH&ma9ZfE4Mho%%Z!wgnHj#g}z{ zw8An@;@fnX&!W{0zyI3#B`{Gj|Li1=0M)eHnRmzef(zRHWN#d?{PBTnjXl#bxMgI^ zf4n*f%$Un8j)UewsFR8QjwLoPHn7s_7@(k$1O`cWDka!{pM3sr$v$|+e=Lv4&mhSTcPhn<8aVp{ecC>(Hfg4 z{1+}b1JC;i=Xt4Z0fWd>b@xY@FAVqY+HI5&B^R^1rkU0UQaG2!-qsvL(?JF!^)&)? zV`X$S7oP~N!do&ER_q0dkuqAAz4%DVCnB8E8Xf|bGc{k+WBc84KaKU5S#XyS1{^<` zhE&1a(kjgTz(Z6`O=^q@9SPPixbu7$l#@QX)^%qG-1wci*eJUPsTU(%a%wlgZ_Wz# zzB^c-Q-H91u?s2c+pe@>;8+7A@Xo?jmZxEElt&2^UxITgqz~=av7V&OD=vvxE}tH+ zOzNiG3eX*S-x!~d^>ygHU^gXTLgH;3Xm@{3gQd&D$|>0gAl;_u;(KhJ=B(J0H7dYI z41WFeythn>)SPQ=4t`w$v^tizW@R?OFUe~&6*7Oo5c3c{%XJELMq>Fp`f?4L9{f5c z$6thTClWL{7dK#L*g4^u3{2lk8@|tzwF~ny6E>0pF>c(mGjoqG+n}RO$jISB7kKF8 zD9u?k4n10kuY+(>G|6@U8Mihb8ug91e?l<_28;Fte5hff)FcNVTHlql|8X@D`8JKt@{GoTyuLJmrD zC-C+F*PXC{N$eq*XFedq^8e9P6c;*%pbd`u=$h3O&`^4d79|e?Ifi)sFD|5rsH~6= zkH;}2@u?K~<&E_L$X#1W;9P_M_FN8li}BHNpOf${j3Xl$soTLRy97z6|H%zyU_6%% z0Ts#E9^liRP|+i?g5^S^K8s_V0qc_Lm0qTG5arco)~ZwsjQz)dkcsaCTPMrlWV(H@ z+Slr>e19IW_tW=39h?PqwULg7Hx3|f`?~9szn!4+ylU&T+8Vh1bvzOUC76GSrh-fL z2s&;aU-kC+3%4SzOm3vlLSgf{bbA6Kj{>(Dq#OYi{(tpayf(Fw?S&D z?@Qs9r%*mkNJQanKNuIhUmXxI1s?`>xzX+7A>~gq?1fd1;Ep%9>$hksyy**mMR)Y09IC3hKtw+wlI-9w@AGN9d+9RZT&Wc=SIdPjlKeX8tM(x6 z#qwiQ^Iq`$EQwCev>s%scjH|Q+6A$}yqg_`hwz#E9*q~#8N5kw=ktcl45Ye!5);@= zg@&`o5QlXR!r&j~@wY6u!S&OP%r|M%kTO@3ELN!#Pz?km8FFnx*+e_DSLzt2!auyy zkOs@QY^B~{scVH-aB}Jy?h7C_tAxb`)BR+5?>&BQI04?QvF|S4s)Sxvs&O$pvw*Ch z-zq1$3A(RjI-?yQA={Ift|zX`P%QMC%NWK}D^PKak7?e43Q1-BAAOfWfY2SM-Q$1o zKl25?z3fyl6`6T;%P<`bR)1DJMt%eDN5kroWN|Q0Nx0hcZ3P%feB82WlLm^Pc>cYX zk`CIwP&CPYsR2gOH-{)JQ=nVOp4*{97I3JSB4gH10ijC4|ArP{z`2k2Gzw(XL8FUU z&(ii6VALnDH$?3Q<}(&8pPGFF8yZh}YW_Hb&b7yGITBxh#P88}SsO9HbvEN+Ok57| z=aqha*ChmUWUyRw3U-HwUq~T?wl{2aeI2vZ{1LVcPa9qM^%^7$kPnZP#{j10e8T(n zvG5{~O44(4H|Xi$Jh|xO2eJcN2494Qf#dDJuTk56P$Qvm?07g2UOup%*jy`wb`GO& zANN!O5uIYm(`%&wy!*rAW?BRYB~8^%zUM%FwaLx}{}$LKK=3?Ou^N2k{6YVF6b0so zX`BhHWl+P9*xK|%1CV%ivFs(lxJ*w`qgL7Mq|cf&acsKP1ur0#*4(xgI$eL(SjXR|tjcVR`o9 z!MjWE;6(m?{IH#8KwW-aKBhhsj1o8K#P0=zb^57bcdH2?%$CdVeefO7&?(%ngXQo9 zFZIdrpF+5;+zY$gqaY_)gqsxhz7*ik4ZQOG1M5iJueloLLj99CKpmI^Jq-<~cjZ!H zOv(ihZR0{vnW|!_n~)5WkKSF_;_3xkDF$IzB>zA%>Hp4loO(bF_aR%@P8n=H4Bevb zZihWwhDwH)TS2SM?l(cT5-2m_pyp=V3RTQWSy{Ed0T!`{F z#duNzo{KxYC;2ZGp5>RA+LfR{_QKD_Ah|lw?K6=T>{t<)LHLR2X9`o_^zm*4U-03s8o7V zq1Wv-y0EAc7+0#+Imq(^28(|`?2^Rxt#BiJp+q(C{gOk}?C}p!w9AY;+-imES@l-} zjz*#FZb0@Wl}s$A;ko)LJQZ3`dk+QtHH6W-gk@jPN`d&NWV()%j{pdp)%?nChP-~H zjOzm5K-1Ew*Ot0BWZF{P4(oM>Qy2g?~qcz;o- zhdBYSHufK7!U4R$J~6rJa8~DYUfD?+yjk8Obz`s^jCajGj(kuKF01l*?uKFC(NUd- z>qY`hZ;Md66P5;6M&r(@+fD+;!*6$QE+&JitR4|&gL0t!9`CwUT`rvE7XCtgryQu8 zpLI=`6@o7v5>A;LrEvfK<$+QEOnC5#e*c9^4RC!^vgdiJ8wzi@pS>b)2K)1YX%9ui zpzz`px2bj^6w}eF?os;#+AE2^qHI+lHr#Q-PBj%~k2lpqlWurJ8@KPWoCW;ZXq^e3 z{sXqJ?&XPJO9MXV-8DY+d4pZ12Q6x!2f*I9yYjdCN?Q;<3SC!J(=#DjwglhYSG0b;Mz?I4a3zuRK zc&1frZkkyL62=K0O5-&Fe@FcA$%;DQb?u?t#%2UmypVmaJFfwj`%WE~KJx)tSsld# zEj6HZ?8#~G+aYjA&w14VU>aHUwG1|O4>LTkh=N4{D|MGM zN@0nLz4DK;@34uUPkLOc23|6I$<0F&3fDE5Ei5m0!FT8K=~UZ$!K3TEPI4xhkSY;$ z`iT67RX^iw{it5Sq&G}|DnHkOfRS*1ZTv#WMw@+(QllMMl%N-_S^9x;!Y7wb1dS1qJg|7IfyJenUoE4^JnO4QWSnAuI71XU0f5(D!2Kd16rk z-WSrO41DW=d%xaj$A=_<)X3S(q_uT$TEuglV6qkZ(fKhj61BnJs9^$p|F7WE_gp5560h=UVLCP2JKi? zmKDMvpj)XX@Kdh_ePJ&g&g-M_TzFjRXOjwW7;)_Fuw4g_Te){quW|Re zK|KFjej$uyzcXNRTnw#qyD#robO9yd8#94|Z5W5PIAC}QyC=Lj=O1@23wB;>rma^0 z2gHjHN${wPN_6Z1@*X+j1BnkrMl!qaAQ% zrx|icps)_VnBZG=CDgScC5npp1+IzyXT4Yd8PEjoH52NmLnhI?t};&DFyZD^rLcd0 z;A4~ak^M*?NSi}Fwzl>Y1eZ)tii$UY7S*fG2ccEa_^+9yXn!3@lHwp9$9mBU*O()g z_v2x3q92)AWj%;pCSSPa)dfkslAFDg3&24_!VLa&2Ta&_#PM!=93D%c=lA(Xfk(sl zij3nzkn^yh?kBGu#+{Dm*Bkr_jYRD3(ng2Elg`!VS6M&cL!uDQo45vuzjQv)>02pm z>Q@mB#PbDdQYsB<*`07&e5nGp$L=#~X}V8`BjEP}kzY%XzJspk7h?lls$pM``f@>C z8B`Qh)0dfS0=n}%K21KEFig$x(Hox}c-uK>45w8IZvW(|OY#-#UBur-i0W2`)=UVVa7R}8i&Dq;Z7z>~Lr*gY-d7X@ui)&D3u?{KRBH;h|Jp^{P*g-Tka zB9g}_Ba+O7C{j{pS=l2ZGkfp79eX^Ey*Xq?W-1ERmkQQQ=6J-uPPmvHd}N?C5UDUfYK7SfnEKOx9k9qoh2X~1Kb=Z&7xfzb(wRE+o64Iz zN}CJkK3LpKJDq_^drpj>`>zA<3|z^}nkR^etJZ(F9GgH@a4|mr(jf5lvT1%#ug0pu zC`Vr2X;?0P&$sFk2IaJ_BHkNqkUsu~%T}}jlzdhv+*XO+*aM6^!UEp|v;DKmW8^Y; zd(3H+uBQn!d}Quec)OrPWF;k?z7z~@dj@FhHvx;LW@&D8F?7f5I`-JC7YueCZxz)` zLb>%Maf83{kaGHrpPqO(2I_nW*jNt2mbq;aOA>j0UWp}H~7t<7|jH^)gKV>YbJ+~DY3iX|-))$ih6E;2{(2g3@*!&N-PbqV zT`1mEyt$w>fYy7z#OZ8xL5XwD`QZIz{H~H>_vLFQw2GwOnqW_cFUAksEDgi)ZBiA- zmlqTG{)u76U9Lp*+^7|3{MdoNm9rI-^&0TLLMuaeY!wLK5?eG99)|a_kB&>d9|o`L zi&2^H<3Rdj3S$9H0_g6!d2V#15gm_tEK1l0)xxr!L)7Q zxv~1o->06qZ`Q`B)n*{K|Fx*iq87~D$*ub>wi1qHr54hJI)Ko8P>iu8@$)bJ+?*<9 zcvSPo;mtoy@OgXc2E9}w)Z9sy<4h(akKNs@<6rFY?JwHouc0IGmsh{>zmj%1Z>!RH z@lq>15p-Oa^yoz9i+WjMJWVj*U86f7)Butdc0MDq?Qnsbb7|~%B?!;hXKvah!UKu2 z?<%6%FzbDZv${PBJhNUjrF?z|;|ABgn0UJ&`RwV%VyO@C*Y=w_a>V0uo>A|cxf(q1 zde4r@N4=17=-F-)xn>;n3~s9+D&+-u{#sa2$WT_BGi#|D1Y=TfFQ};N5`E%2oH6kv z$W7Iy&d?ixl$sr`+|(tgW3Q)F?wSE#h1f@nZ{|Sym|*2NZ5|BolcX`L^h7D4u#5JA zsaQ=V)GgcFj{?N&i{^=Y{m1MRYxcioqoR)*G+z>fK#R z_Hp&FptEa->UahAe?N2exHlR6`Q>fikJO?6&0g#JXZf(1RPT}$@D5HeRjTvvjzAJm3YfK@MM-#2RaDz&ogZ(aE`{BkpD2~icwlNI^*Ox|zbQNle%!crQ8Lbd} z*;-1i>yr(Jmwa9x?~Q`%Ps%ysxk};cV6}F87ctLRaZ(s!%3)HjuB+Ii9JLcqmEKJq zfSC#&rjL|JT=sY?Z6KV6dO`E&iq?*HOr-JfHUWX*VdN8Mv``&YX0KY{ki?&XXfFng*!tG%*Ft)}inD1!9g~A)* zE+(x=$Mi18XI~RaTuGl9{cjL|dyY#TRmw)GY%1G|wpO%!O|@A-%&U^d_j4=G5c|_V zmkshxb%UHBZ+!gmLX-lx*-Wn)xWFWI@W{V9fZ=$>ZX+^Mw%sdjJ5!Aw^MCGrQEtSj zfBA_%)S0L(edkHr-A?p>U|`WWRE0$n$302sYoKRPgDU4u0S1dprz!m`M{zjVarss` z9G2vdDpARR5LaoH(57O{dHAJ2)es>$=TL8_3XzC>qP)^AqXsWiGzQAU((#O1AN8AG zW=P=)HO%nM1VcZCju0`%MH#o}EC(38%1EmN%=-j_YR z;2)WfI#kbhz$wDt8eUUf;zP#sq-106QzZO#=M8^eVhrx$3rV_(hU_x`TCLcDC;z}d!7~APBR*QEVp}l| zvCRgTX_a8&-;{%Uzm?(7ZaJ&3S``@XKeB!N9tAp-m&y!StB_LDiGd+q@NeyjtJ|ly z@NV$b@iQVNc+Ml4U*b*!Sf5o^PdbnQf^81B&VKL0U1HiX$&O7&kj4!af6?o2 zkn7O*?(?9yv|2F7nFQZR!iOw<#X6Sw9yU|AI6uBqj8`kK#744qz|`Z2@R~>Y7(76~ zU%h<_X75J03)ppoPbHT_M9e$T7+HZNmpX_4%i%lcAwwFp<$JABP3*my5`B0f+v% z*nHy_T)gl$d?%4h{@5VfCR)1>w;LO+2#p5fGsoPbQTsBGQNDOMD)%)wRHiRg8JA&k z!hof5S3g{xRnmN{(t-35mm_bfM8TA`T%7Dz3i?-$ME?700^x70%Qw|s z7;)nj_chr%EcbQ~J@dXB1x9KwnSay7T2G?{ohu!9{QlUzZBzYNDbHiFM(##^%PXC1 zDFv{XPy3*3WDI6b)10Gg^~3&o=c?#0q3F3sX{^tu3dH4}^Sscj#cO#?7F4$=5Vb?= zgl1|JoY^Dhx!lS75kX|}Ci!(k=D<6W**O%A%q{l!|MOtB9Rv&~X z2lVTLdcjI6sieoG3ri1))7`mH4|K ztA|rYT<_F%dZ0jSeAsPQ45t3p&lNn`j7Nitvv>Y!06mGyTWfAb`1YUF=U{5Bt8DGIpZ((J7PQmQIkr zbdBZLVj1+_&|&I7)`0Xe(a*#T+`(PY^@Ma!0$3X!O7AylMAjm9D-zcnnqO_=xwO%Q z@uFi={@;t?k?v#LCO*oVt=1Iud9R&v%S10lYLLgvg zex)*hC-QgaeL7{GgzKNy9{X%B!j(|Hf?RQTU^wO7^KYsW4;v1M+r5c^g&!G`7qzN! zyZN7jeWUr<{ed>vBi$cSOn8&-Rv2nbh-#GYZ$zD!s`8a=WqA0ygMZECN_bW>m5|*P zf+@!?t}*EqK?Z}!e|+pdINir9_Bb{dyUm?_GZ+gYpS#ufMszPkYa96t3zLvrV#Oj` z*&nY8()EzMt1(x^bh*OhBV5|#d(-Ya2DYBklZ(_9P;W|`zOHVIiiiAr>O7qwch{$< z=Z=g(jQPGp!>*0!^FxH5d&Ctq*cHtKc6j0029FV;=2FyGp?UF{4bk)9>D5S`HiB>c z=T1fJ>+2&97XQ{1L~psr6gk^WAmwK!?I-fr&hTP_42;F{XA5}{t z$oQWB{LHNYzOi4q^{1}|>ZZgyVuboJs_d_*e`h7Sk|m#d*CgRsULG55K?R)hRVcYG z)r`OB7iTZb%|ZNDh5uGjD1LD+I8Ekm!4or1m+Yik;aZ~OmSsgVc5wbYDy}&Iv3x~3 z>C8n?@kRNmG1D_-jh@i3y_x|#BSUR#K9FHy$t!TctOBFPi;pFL3qapY))T-Sjm>mr zrN<8kgHiQ$+eWKmd=YZ<@jA0FIL>|y*RTr1n$x43J?G4jKll4f?`Wb%TgV|NxjX|n ztfF{-$`xbZ?q?5w)MSGZRbuA%ZHOPL-ly=ts>h3F-tG%dBxtSWJRkAV7J|xkq^~Tt zVzt>C)Q5sknc4SSy>r^pAkQUh93Bj|=fW zIGc^X<`j4my%N!%a^rDRP&5v)9N>J{Qihjzdi=M4TRT(@J0I*1O9h)Z9}QA`g3&ww zilq}b*S9Zsdubd^vN9&1w z#|qm;wJa3eBSH0u(gCcMuTG5+`{4lbzbuxQYH;5@{u8HP4Pfx&owp3e%7Oi<#KQw( zvG}Com&u{i#D17%-}t@*<)C@2_^yY11}afx4>$hT2+qKdxPWTTd;MZpkF2_Q%(aT8Zm3mwXS{{B;ZM~QQy8k#xzw8Ua zjL8Yoq+enm`Kr2aO$2B(kaHgY%glq$G2JCiW~{H@u=ydtC(>0)&_6^Ajpf5&G1OOSe-|6s-We(;dm zR>BrpjLYAA9!#fKpx#NU;uG)7V54<)vQbtU7QQxgFKO4p`Aarm|GGEAm&=N@hsSEb ziu22`2t*)hhs1K->n5yPy49j(m;;T6E3GWsqfwJ4+5Igk9SmnXJ%88?fodlok<1y3 zMj9M`q+JF0SpCV8vH<2FptAQ@7iqAKQT&0wp;bs@P zFo@&*3+t<8aNrls($3m?OkY16{VlEoEXsGX`rn^~4|fkgUvBC^sp_ zsA&@T6y*IdU5kOfvc(5-L~h)4@VeM$O9Y(wZqv~&SOR(k2W-<-jIwX0SmU4AVr$60 z8qOZ#Ubi;-z3hD{u87u_8vJ|=@o%1O*ZtQI#Xl_*CK($b@FR2bt@#F&YPFML$uESd zQL{Dvphi@i6Pi4Drw*Nkf`cV{IzjRBdgPDgCQzoflB0yzgX&6GH|2RQHg;M#bd}~H z;q9=D6^X*%2OW#N`Y7PGKfW>+3UTR$h3KkE7rcFMf&v~r=zDJbclRWr>utZSesV`G zKKtR-dy@r$6!BllLCG%ASChK!Z7>a4PVb8%ItC!1WDg}esvbqce$-4$)FJJzmMYt= z0N^la&Qy_40A=qa={?{3;Xn-ypY-uEG>f}@rm`*vWRc^X5QiUS) z=Sdxm&5hN!KdA-gg@)Rg`|apinx|xt?+zc%Ywvpfw*jADeXGtNoQM1ta)W7+i5S@1 zEFrR31pcZr5Bcj;&@-%LBO|Mr=-n~?68+5y-kJ2fX|@x4C>hs{^IY#xL}=ecwp|TI z{0xlTmez)JJ7rfM%vRvY8TF$j|FU3)kV&WA$}s)&6?*uQgW+2*F8X!m;e;>aYFPmp zoesa8&d+T@XPx2uVe-?kv{AT4_FaVavvHXtR3ETSu!iqNeF_|@oy$s8N{2W_v#oFU z>R>+c(}v6A2;}CUs-ee9c;xM2tSFueOXqe*{uh}JopQOasUyfxDq^PlJu(?X)@aMP zwX;C@Vn4fbS~B#RxqSGw^b$M!*va%^=^)aqzRT@LEF{*xR&d?-9I7M@yJ$K?A>z`o zyeH>#ppMQzsxw3A|GvUP>D&=;MD@%|X>tyP-DmhZ9{d`g?3>Ql-fo9_N$!j(<}Mh! za`5Qgv!N*WpOn8%%u8Y(8a_!g5)Qw+gJXBcMxxPHmWYal6^>J~sVAD;A;{wwV+89P zAgW>>nHmhj8>$Rz+tp8yoimW8bF2v4GE0{^lB&SV>vjGg%Tk!V`?;AJJl=`T;Fb4SaN-)x7Gadb1xg=}82lqT+Zx38_F zw->>|21e70K&dw6&{6L`8U6V2wWdm$UTNUhWQC&_Tj>dNB<^-P!&d&lUeN$8A6_U9JNHG@Dy=cwFW67F0l ztC#JM!~RP%M^CbsAl);++t}WS?jw)w9|Z=WPZWp0e$`#j05%6gzW`^OEuCNM-oPLA zueMin3KAE@dl;{$Lwd_rqB61P`Pf=~g%nbRzvLrMo2-Rk*2CE6%WD(J6I^Z59p8#y zM25@yCW(8SE04CsNH*SIINPKln~ISIHJ?tOC*HfYZC{UC5c|SgzXm>>9Dt4l$s~(= zt;l)nOaS3jggL=u?B|#&aHqlCvIZ2K6*yUY&Oh;Ga;M zM`ef1#21`_@=EO-aZTU`8)-tq8;ZxdvL&reiU!$&Y*@uZ6Bi zOZ?68Du^{M{3;;&3gdd$*sok@#j4B-!BU$%twq3AkjU@HmR5#9uapB;*9 zSMuT5@d4h1f1&u<%7IVoN-3~0f`%5l5yI^VHed+flH8>-7iKCj1@Jm&6 zr(f@DK;ysVA&nN9*i2?uDf3D}Q+B@7iw0##VQB5yu2F()r=EMv5d9E0jqdrbIo*J} zafY{RSV|!6OStIv$S$;;?*5^5Z~|Kf{Pi-96CE4fN{Nd@DL8ZT&-w4pZg|%(>w8@> z2{b)OQB*1sV6GS8x!Bl-DV!`@EgC5>p05?WZ`1<^^n0^H&Uhf#m#jXLgU0=pZ za;4x68~X1qU$b#$SXFsgy9wu-j}`_i5}d<oH+@KO@|WIL_4M6Liw zulHk#Ik~7Or*n5wH ztY%Rjd+xM>l=kYfEqWu}hlf+=cNZd=>I$88 z6zm8X57oVx4b~izFRy4#z~9EVL2S#ZV9_3YZBE}Fv@SG^C*%%+TT;G!3}+ei$f`9# zP!{CrzPUSEM1p`un`?y~RmjfYQXf}W4>V>rl+_(|*m>l_yN~WgC>+SVxT@TTbgsv~ zd0wD^Wa{;=tk<(~?iqRiQd%ko?UvL1cW{p22*NqtU0>meeH7Z0#{D?V8S*jgk{{T< zdh>jKq#epxVjF0BTVZir^q9=WVmy?+qdKpx6mE@wbP2F-22Z~AtCP!}=u zt)SRRq5jrbjh*#{u`D|)Axn;{?#WaVvHZG z9VmgB?b-GV9d;=5#ismERUw>Gk2MJU+6v9llf#j`jRc<+otI%1jGK_+&eYM2&J*{Q za%k+~ydTMYd5jE=9}=@vEk@zD^v@Z}YYMhEm8y(d7ZLBB59V=ui{Z%UV-kyzedx!r zitr(k`Bbz+9(pM8CfR=|uis>C+9y zN0mXBSk9H=ll!#uf;MSLtzIjt%umKCrMbM2V?H?Z;&k27fIj@R=dW~S!~nbvH!<;Q z$-pk7Zy_r}b*TDiD}h(m4ZZK}zsdEs5mv{HGNz3Q-`@_o$KOP|VQag~i^5%lz|+n= zS}ReHMwf4oG`S4{txI53OzzLR;Z2_2`r%nwy|;>GnIzc@pJivC-NecE__z( zhI8Csgnv8LVdBn5^SdTQ{-eDOEGgX|gi(hA;siAhnV^a!7i*$AZ zbsOe(8rblX1ks)49g~z!IdojH>=Y+51T0N?KCY=&S%&by+^I@8TMh3;06&;SO|UF4quI0=|Q2!LGq81NmPieGW2j82aX~m z&gJ?dm^3cf`zLL=PVlwW<3g@DTzqRUBPH#NosL`^9M#3R$^IbL=0h`z?Y<=O+_Dt? zR+`*SDZ0a`0rx)4C@zS#JYJ3$e|2xCj@}?J;|LaL3K)ehoJi5YX#vVjMx*ubDeo113TI-jjf{;X^>a zo*H<_w*_^aZzy=bP676^ljk++YVfeaxxb!zQF4re zw#IdAe`NdNRQE{EdxDQ~qurgzs#gZgPAT0vJ$-QRhvIW(gK7}#4mh;)p+7$4{v5yj zE(mTI_P5338=UhWeB4?Y2A5uB-DYVU!v-zmKHF2 zpZj9{U?RA0IpJ+Pu0G|e zBwri?5mok-heuk0-^{|U$*2ZIA8PxLd}u)$ll{-TN_yZ!+qX_Tsz&^_b*wLPdkG$= z=PhA6+k}U`XufUfRN}KujoVYkBVd?4cALg;5=5j`y}YE0!CIk1<^oZ3b%na`n#Jt_ z*!bbP`xJj6T$(9ypermw`I+K#ThFTD+>iB(w=MIa!sw?W4|58#8a(|}^r;wSV&l)bu;fff^4Um&)UH>#tuk_^4gIKO^%XKZ{1 z96kKk!T(A%EQI}9xJMlY{<-!OqGdr~dCx>Y!CzH-Zahzu5{ zFRkLv*5MniJ0E?ka)1;fY;SD*4vxJjzxCWC7T?}++hF}rjijTg;Z5!BXjkJ~{z9b% zL~Jz9JG_|#K7L{5S~E|ua@|cAtx^Yf|0x}8D+(j@1TGWH#Z36+!)E5OCmQtZc&WBA zmV&s~HDj}tI{YyI-z_e$GFbjjE2CXsjv=>bzaJSMApD?nCBZpyxc%y_(I;7Hcx?CX zG&O>!lBxA$A4sT2!{(KhebIf`=(IPl+$

    jD*v=7c}4qy{NeYVd~rdl8A78aDBKAY*}0dh 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

    Xwb_e2@?E?aH?mKzU4#^TLgo+Xfh zO41$kt-zX5G?Tko4Qg+8^4JN-p~D8-wWRx=z$>|tVsNAhREkrlZ%miraQ-x{DkJex z9S+%5{fh^jOIL7u-U#i1WeGk74H&P;+IHu2Fj`ZTW$hxl#Z8@(G97+9+_p427CD#$ z@y%RWM?W=ztnp=q)X`e7V?Srfd)OC0xpC1EPA$q@c554>w*awSZMU`u*TDP-nZ1Xf zjQ~aFJv;Hp9$c=Vj}ASW2W`!VG7WX}K=RC@5{+Fqw(ram%X#XF^H-D$q^=L*5Ji}u zaCi&6*t0pH^{)m5o3h0YL=V9mn|U*RmTb5aF)NqO6OV_YSJW2To`7Img>2`p3REqY z-$AMdAlSjS;D0e2f3zeB4HV>n+CElYxK@m`0W)0DB;RmaS=7O&ya2=EBif(qBqHxz z_~MJbq(@cw{==r0{ct~j)tBpYHQJkMv3)x=2m>OT@18ag zZV3~g*6EWe_|o*+Noo?LH+Wq&Hu{+K@?UjcknJdjAp^-Q$&00scroQzvvenFh$@Dc zh(^KbH*MNn&+G9+;eDBsvNqg!GWWYJuN{BwP+zJ!8Ub&-bKX31C;O<+F!R~FCh(WL z{Op8U2T*ZM+gmedga6ep*Md?q2)~hi+m?N#|0~MmeBb;duIzK_;QWX1N2WX8Wv~kF z(6UEv@K>YE-fe7uPnN=pJx?b?SsjjM9rfRt-Glb|ahJ@E$lu*W%i0eKWTSzco78aECa;AGuFTN~*;` zsjc5-MtUa=;<7C=q9jLC@xpfUTsPH}Z4`*#-MNaAI1y z8v|cGQ#!jZ2hKVEHmE$@h4NQQ8!QiuV}S4B%NzmkLE)icu=MF%pbAWCHk`hT*N)P< zbd1GdoGu8;>CAp|;N}O_U8aLr%DJ|7Lv( ze!SE*%6BFm^~LBCcwC4dx|@nv^4-|9x&77A0+3fz|%jh>alU;-L0MrnegnT zVp~ZF;R1QEj0l?+;O;l3J{;t|F5Q(_{3e#{a0F{81V{9sA)nSZ^|QSY*ZKS4cyK-0 zWvL%6B6;iv$Kwa-)q8-;IV)4{RwZ2HDm}mBuRF?;#pSS$U@)W9d(^%{{FZYaI*jI_ z;B)yf2@lthz0vCm$;_c}&H53`oM9Uj(ck;;lb#ngOYga?bITsGJdM~Qt20q;SXy!J zdJg{9-kl!hhuE9Dp|p274L^BuxIE_ukog*I zeWYdp;(1eZr@bohsUznZC#DI=-|o{;BH;vUY3s)$nwufDNuK-eR4we{tLuzEMe^E9 zL6HM=?;%y_EK9#jA-o<+;xBnv5A*d#L;B>rxu8 zb1UJ|xWKg8Mk8LizkK;JLnm<2^suDdtV5pYN6W_z@=#y>CiS*IYDX?b`*EhDzD)`nyD{($E zAB5V)ZWgoVfTM*Q!+m z#yIjW*{%5%K_P5O_%EZ~o+0n6;K}#)WuELr)SoEta|OryoEi^ORn3$rj@c))$JRqaX}zBXPhj&iSuyVSc+jtkd9)PP9je=M!=RoGbF z?7uPyZQUL(pd1S(``tKHpQM4}kVhMV0_Dng~YAbxyAX7y?#QqMh%Xc>2Nxq{#Ryu zGO#nv{q5=kfMV2L}#PtIeDbiJux8p{axA-v1!YYlLT#i!4_twd_?!`H69PXVf< zmr{8X`ys91RWN&hAnLs^9)5SQ8b6yKczr*NSdJ6c zieaoZd17+71RvIOR&`DH!77LHsIpEBipol>GVvs!+}=+m);EYQdI(EGTx-zo#5v1< zU%K(bA{$k!t1s?iH43_%LVQtb*~CzV7@)l=W8p{qv1=x0G;95d4%;d7{O8?55S7~P z-S~fJ*BKO5wr(5AN>&sU6-g~hcEAypjR*>Yhzdv(L^6_t1O)^EMY1R&QOS~XXoCHb zGfhqc1Bk+41Yrm|$lbg*w_eq&`|Iwi-K+YXeX6?m=|20M@B7wT#AdX~+BwfUfcgJp zHoerGR@i9O$v+g?2p;XGclqar;HRT&pqOqhq_OJRZILiPHScG{} zz?CATtZJVPyE4VOB*i*mKhY~a-371P#P;#gJ+;VdSTSg94EqM|;4^=Jl?+wfktD=G zMm`@FPs9!0?z{i&}PK)*bO&{h(N%6k`d!wuJWUE>T9^dqzvCD#G8I|mJ zchAH8%XAM-*-TU$Ia+3P{sG);=%+CK=z?|GlS0(Y9N5b9N?CKQfxuw>Le4w!Ao7_% zezqCcbxMzVDHYX2Yt9$LY4jSNsU8~n!}1x-2a9m++R=v^J$K*yt#Sb9c(T`jh*Tgx zTTMajllhQ+;TuR`{y#dgPi{=79zIFr{@B@727L{uVp<%r|FCY(8>5w0q_HDvk|w<# zS>25K*7NN?a#-IZ$h$KW)}n`G?EKB(SW}6H5njLSoFAuz-{^oPqy85C#7OY(X|}!R zTMWTD<3jtTOQHAvra+838Q65s$Gh2Ag14cOg6UL0Y7kwbRr*j2mZqsj>KA%Y$TKMh zot9***S7gAULqU&+eOg^J2#-QuD()dLLPFS|8mT|tP476IqrRYjdeLC=a-5uCV_-7 z6CZi727dn{bSW3thl>PPnFU>MfplB!?Mi6?uK2w*!p?bcYxViwVVLuIhP4*{#1IY6Xcl>f_7_11;?ljEJMJ1ie@_uJC zkl6;mHCtOH?7w(fW`>f3JOw9Pvhn?G!0$R$W{Y)DPn*rg_!R4-()&hD*hC@Pi5zXW zYl)~^NXH;ys0!I$HMSey=Yy6-9eeKIZh^F%@UwS%n5D<%7tL=-IPC(JX4{q**6JW$Efg3>UJA*)kwKvsox zcax?QBE+5_qVdIbLIzD89c6m~GKm7u47551U~#*p=Vnq<|-x2Q?7+9+H% z1s(f7mR5!9bNds!6~iQ3fTP`QgW*>_;ueuRe+QL<@wzOi8 zs3v1xEVJFG862;+W@yc3Xh22!#(ZTQG2lqo9xE@_-efUQc#8vq!2{tw$Rfe8ik;9mG`J{w_5zo7` zp<0&dd(7WZQmkRI0L0VclQC5^0KJiOY2c$?Q{0Y8R>l?ZN^LHV&+l z^LhB=_uyI->7r?sqFaWhMFkf}!U{oT{GfG*Pg$ z69pxxmf6WyBel+w(-VtW|39%a_~@7~^3u`H9A=9}eGJ3QO8EQNwap!43!4Ss$9y{F zlxEObs5_w?P=VT}dU^XfOHp&o6G?foH>hitfoz52QDs(-dRV@MpDm!ybpHvVXI z=a;sIaBMr|za5(kFiZyTxgVttQb5RHbSBldNRVxLxf#hp#ZUZU(`<%gOi zGMqH|PJjGI9yHPQD(wmkfD~emUw!UV*j3|v#J;m0+$HXC7!Ou}wpo++uD@#0%M?b2 zagi7hUK8kz`k9B~9SY;-40({b#o5;HO!@H2u0ycvOEEeqW*+!+t{a`RX*5hOs{##e z%7I5RPvMGSRjf959UQfzoIVo%0qqG9ZPn9$i>~U5v_B87M1?FLYzNUpAXCI``wu+; zc^R4F zZ7?ZIii`CwLb)Rx!l$e);AZ`4UPFd#bW-p03kF)u|L>MdHa<#4wf8(n3~phbcu0HM zb)zu&ljUHnjq!7I{`lv($Zy3+MTv3C-3{xQvy2p|(iwx@*=1R-R)D-$8E)gD^~m6n zsess?6s(V`w_GDogghtfW9_C|P*k_%;3{1|c&%hRe*LWq$_=P&Pxw-RZCOX)xJwl} z;8Uy}%-Rg~UpXc0XWt{ax040g#26TGJE2s#y9Kg;3Vl9e+KLzy#|(7zn?YHN)SnnN zfV>N8X6!?-UgdjRUIQ^Z*ve#6H)6_0J)b|Vx1VXi^{%75SEyuAtcWAuNDG33H|~_5 zq$rS6(N|b>h(poxq`oZP5)e(1en8<3K`g8Ec^*L#SZC0*`6tmH;ytrlRtNymJ2}-) zVV!mvHA32D!E#7JV_DhvaeaeCHF@5o4%QR$*vt7FK>o{CkTb5Y4DgJd)xG9~B9_u0 z?;U7`cK$7?@v?kmP0k6T9v?#Mp%?x2f7+ts=G{Jq_j`d&VrcgW*7X`q`1M?Uuoe~X zUpySDGmp5vt&VY)G$Quf<0nNYus_kT&!Z%|B6K=xbB)~$`?fI;{g}3CLYD;swqC?p zqP#ZwlOOqVV1xUPW!FRsbXq;Um>`h=iq?JigncSO^OnMc(DF9)LwIAsl8p7wzQx9| z>%>7Hk&$U&j*47E^aN5@E8z8PlKr8AJjCIU5MObN3jQ}K=Y-k%(ADZAK3|!`a9n?v z|Fz^gFc9u?syderAy;^M=jA$4YEj|n{GWAbl@LnrxljXA_hm1*GYx_Q?|hpb&ht-b zJ6#tmN`M>lJJQ6*T972Yt@fMcN)!{6cZV?s^V-O}CvC9qM--31;8o^+_-eWHSm@?7 zY7o*@{(GbkZW;WVU~g(f?~HPoih8Tj_os}^p%N6xOIs7Dxtk9&1W_S-GwcI4r5fIX z^OkqX79kcrMX2QP+LXthQp6|tL@iM-6c{;f(aSC5f`ZT8{!d3!!AY%Y7o$oJVx#Ts z@yNi>k50RnWIp*oxT@%xC}Yh3cl)KR_N5GtNd`>JGvj)J&*sz?fu?}90Kp}4Z-{} z<0X4B@2N~Ad)OeW_ChtTKfgyv?~+K+!g)vL!dd&J zaje5wJ$Qx*`yPyDH21ILI^%m*vQr-QaAP{?;%^=u=tX5t;po*VWS7ayx$j*EEc25h z?Zo?Far`>*&~h5uEu}eNkoFSob^Dc>hIJOu*I+jvydIoH0Vtl%=kh@J)G@{^+zR`_&=}ckbkqWKCr6YecQ>jkB^Ca0yBR&6l-8=le!qWFjyGQs zEP)T9qSKu?zWP_Ie3u3GOE6{-OBM^kami-i(~_hTP&h#`m&9>k!)BXmcXuizrPIsMxQ@o+qc!O-*E*DB)~XtIs~hc1crsQ=m_dF8-JXuV14w%!16!dqdfRl&w8{}?8l;XbD4pctNGxx>|s|DWfYHi|Vf!`mMmK75fl2xgv%&xkah0bOUjEpO5G42=#mIrIC zFd8_e6E!jJldiN=!`S;{tilGP)!E~=dKjOG+Zj+X_KERQrZAdSA5c!g7?dH=s*X{r z@?%3VMt75tK{<@phth5{W7JZo*&$B*e=|n4d{os&OCW4_=6~jB8~kXtUmZ;BEnK&! z@c$nsXQuYaZ<>a%-RZYS{?q(uXz2)q8~^E$?Q^p|``ex2-{Ud*(Nw>u`R7b&snh?@ RE?NRD;U(Vp@x%7V_!oi|T>Ah3 literal 0 HcmV?d00001 diff --git a/.context/issue-351/raw/bundled_basins/stdout.log b/.context/issue-351/raw/bundled_basins/stdout.log new file mode 100644 index 00000000..76fc716c --- /dev/null +++ b/.context/issue-351/raw/bundled_basins/stdout.log @@ -0,0 +1,33 @@ +Fortran reference binary: /Users/yahya/.cache/pamica/bin/v0.3.3/amica15-macos-arm64 (native/v0.3.3) +fortran seed 1: LL=-3.399644 +fortran seed 2: LL=-3.399446 +fortran seed 3: LL=-3.399640 +fortran seed 4: LL=-3.399612 +fortran seed 5: LL=-3.399563 +fortran seed 6: LL=-3.399554 +fortran seed 7: LL=-3.399618 +fortran seed 8: LL=-3.399583 +fortran seed 9: LL=-3.399808 +fortran seed 10: LL=-3.399841 +pamica seed 301: LL=-3.399722 iters=2000 stop=max_iter +pamica seed 302: LL=-3.399559 iters=2000 stop=max_iter +pamica seed 303: LL=-3.399677 iters=2000 stop=max_iter +pamica seed 304: LL=-3.399519 iters=2000 stop=max_iter +pamica seed 305: LL=-3.399432 iters=2000 stop=max_iter +{ + "fortran_seed1": -3.399644280997763, + "fortran_seed2": -3.399446053777136, + "fortran_seed3": -3.3996402544421502, + "fortran_seed4": -3.3996119263160636, + "fortran_seed5": -3.399563131051804, + "fortran_seed6": -3.3995541089444816, + "fortran_seed7": -3.399617603804792, + "fortran_seed8": -3.3995827076502567, + "fortran_seed9": -3.3998076574695872, + "fortran_seed10": -3.3998408175881214, + "pamica_seed301": -3.3997216530763565, + "pamica_seed302": -3.399558597670594, + "pamica_seed303": -3.3996768215064535, + "pamica_seed304": -3.399518642876667, + "pamica_seed305": -3.399432015696039 +} diff --git a/.context/issue-351/raw/dimsweep/dimsweep_cuda.json b/.context/issue-351/raw/dimsweep/dimsweep_cuda.json new file mode 100644 index 00000000..ca7e49d2 --- /dev/null +++ b/.context/issue-351/raw/dimsweep/dimsweep_cuda.json @@ -0,0 +1,80 @@ +{ + "platform": { + "machine": "x86_64", + "system": "Linux", + "nproc": 32, + "torch": "2.12.1+cu130", + "cuda_device": "NVIDIA GeForce RTX 4090", + "loadavg": [ + 33.0, + 36.2, + 32.2 + ] + }, + "config": { + "data": "/home/yahya/epic324-runs/p15/ds002718_sub-002_eeg70.npy", + "channels": "32,48,70", + "montage": null, + "samples": 30000, + "iters": 25, + "repeats": 3, + "backends": "torch-cuda-f64,torch-cuda-f32", + "out": "/home/yahya/epic324-runs/p15/dimsweep_cuda.json", + "report": null, + "n_models": 1, + "share": false, + "fortran_bin": "/home/yahya/pamica-p15/benchmarks/fortran/amica15", + "fortran_threads": null, + "threads": null + }, + "rows": [ + { + "config": "m1", + "channels": 32, + "backend": "torch-cuda-f64", + "threads": null, + "ms_per_iter": 125.49756260123104, + "final_ll": -3.2848901731348024 + }, + { + "config": "m1", + "channels": 32, + "backend": "torch-cuda-f32", + "threads": null, + "ms_per_iter": 145.12465924024582, + "final_ll": -3.2848782539367676 + }, + { + "config": "m1", + "channels": 48, + "backend": "torch-cuda-f64", + "threads": null, + "ms_per_iter": 90.71042256429791, + "final_ll": -3.2083838687794355 + }, + { + "config": "m1", + "channels": 48, + "backend": "torch-cuda-f32", + "threads": null, + "ms_per_iter": 105.01488755922765, + "final_ll": -3.208394765853882 + }, + { + "config": "m1", + "channels": 70, + "backend": "torch-cuda-f64", + "threads": null, + "ms_per_iter": 123.88060091994703, + "final_ll": -3.217233763497051 + }, + { + "config": "m1", + "channels": 70, + "backend": "torch-cuda-f32", + "threads": null, + "ms_per_iter": 137.2088565211743, + "final_ll": -3.2168900966644287 + } + ] +} \ No newline at end of file diff --git a/.context/issue-351/raw/dimsweep/dimsweep_cuda.log b/.context/issue-351/raw/dimsweep/dimsweep_cuda.log new file mode 100644 index 00000000..44180635 --- /dev/null +++ b/.context/issue-351/raw/dimsweep/dimsweep_cuda.log @@ -0,0 +1,9 @@ +platform: {'machine': 'x86_64', 'system': 'Linux', 'nproc': 32, 'torch': '2.12.1+cu130', 'cuda_device': 'NVIDIA GeForce RTX 4090', 'loadavg': [33.0, 36.2, 32.2]} +data (70, 60000) | config m1 | channels [32, 48, 70] | samples 30000 | iters 25 x 3 | ['torch-cuda-f64', 'torch-cuda-f32'] + [m1] 32ch torch-cuda-f64 125.50 ms/it LL=-3.28489 + [m1] 32ch torch-cuda-f32 145.12 ms/it LL=-3.28488 + [m1] 48ch torch-cuda-f64 90.71 ms/it LL=-3.20838 + [m1] 48ch torch-cuda-f32 105.01 ms/it LL=-3.20839 + [m1] 70ch torch-cuda-f64 123.88 ms/it LL=-3.21723 + [m1] 70ch torch-cuda-f32 137.21 ms/it LL=-3.21689 +wrote /home/yahya/epic324-runs/p15/dimsweep_cuda.json diff --git a/.context/issue-351/raw/dimsweep/dimsweep_mac_ll.json b/.context/issue-351/raw/dimsweep/dimsweep_mac_ll.json new file mode 100644 index 00000000..bf128205 --- /dev/null +++ b/.context/issue-351/raw/dimsweep/dimsweep_mac_ll.json @@ -0,0 +1,127 @@ +{ + "platform": { + "machine": "arm64", + "system": "Darwin", + "nproc": 14, + "torch": "2.12.1", + "loadavg": [ + 10.3, + 15.3, + 12.1 + ] + }, + "config": { + "data": "benchmarks/data/ds002718_sub-002_eeg70.npy", + "channels": "32,48,70", + "montage": null, + "samples": 30000, + "iters": 25, + "repeats": 1, + "backends": "mlx-f32,torch-cpu-f64,torch-mps-f32,numpy-cpu-f64", + "out": ".context/issue-351/raw/dimsweep/dimsweep_mac_ll.json", + "report": null, + "n_models": 1, + "share": false, + "fortran_bin": "/Users/yahya/Documents/git/mlx-fc-phase15-final-parity/benchmarks/fortran/amica15", + "fortran_threads": null, + "threads": null + }, + "rows": [ + { + "config": "m1", + "channels": 32, + "backend": "mlx-f32", + "threads": null, + "ms_per_iter": 35.301941679790616, + "final_ll": -3.2848825454711914 + }, + { + "config": "m1", + "channels": 32, + "backend": "torch-cpu-f64", + "threads": null, + "ms_per_iter": 141.085026639048, + "final_ll": -3.2848893716905923 + }, + { + "config": "m1", + "channels": 32, + "backend": "torch-mps-f32", + "threads": null, + "ms_per_iter": 302.2509750002064, + "final_ll": -3.2848832607269287 + }, + { + "config": "m1", + "channels": 32, + "backend": "numpy-cpu-f64", + "threads": null, + "ms_per_iter": 337.4410200002603, + "final_ll": -3.2847793027686554 + }, + { + "config": "m1", + "channels": 48, + "backend": "mlx-f32", + "threads": null, + "ms_per_iter": 35.69712499855086, + "final_ll": -3.208386182785034 + }, + { + "config": "m1", + "channels": 48, + "backend": "torch-cpu-f64", + "threads": null, + "ms_per_iter": 148.65664332173765, + "final_ll": -3.2083835269014345 + }, + { + "config": "m1", + "channels": 48, + "backend": "torch-mps-f32", + "threads": null, + "ms_per_iter": 375.34432664047927, + "final_ll": -3.208386182785034 + }, + { + "config": "m1", + "channels": 48, + "backend": "numpy-cpu-f64", + "threads": null, + "ms_per_iter": 499.61313331965357, + "final_ll": -3.2089337357203798 + }, + { + "config": "m1", + "channels": 70, + "backend": "mlx-f32", + "threads": null, + "ms_per_iter": 38.29424163792282, + "final_ll": -3.216428518295288 + }, + { + "config": "m1", + "channels": 70, + "backend": "torch-cpu-f64", + "threads": null, + "ms_per_iter": 195.01211500028148, + "final_ll": -3.217461695658916 + }, + { + "config": "m1", + "channels": 70, + "backend": "torch-mps-f32", + "threads": null, + "ms_per_iter": 404.7074583196081, + "final_ll": -3.2163822650909424 + }, + { + "config": "m1", + "channels": 70, + "backend": "numpy-cpu-f64", + "threads": null, + "ms_per_iter": 743.3544716797769, + "final_ll": -3.220537041706068 + } + ] +} \ No newline at end of file diff --git a/.context/issue-351/raw/dimsweep/dimsweep_mac_ll.log b/.context/issue-351/raw/dimsweep/dimsweep_mac_ll.log new file mode 100644 index 00000000..1e341781 --- /dev/null +++ b/.context/issue-351/raw/dimsweep/dimsweep_mac_ll.log @@ -0,0 +1,15 @@ +platform: {'machine': 'arm64', 'system': 'Darwin', 'nproc': 14, 'torch': '2.12.1', 'loadavg': [10.3, 15.3, 12.1]} +data (70, 60000) | config m1 | channels [32, 48, 70] | samples 30000 | iters 25 x 1 | ['mlx-f32', 'torch-cpu-f64', 'torch-mps-f32', 'numpy-cpu-f64'] + [m1] 32ch mlx-f32 35.30 ms/it LL=-3.28488 + [m1] 32ch torch-cpu-f64 141.09 ms/it LL=-3.28489 + [m1] 32ch torch-mps-f32 302.25 ms/it LL=-3.28488 + [m1] 32ch numpy-cpu-f64 337.44 ms/it LL=-3.28478 + [m1] 48ch mlx-f32 35.70 ms/it LL=-3.20839 + [m1] 48ch torch-cpu-f64 148.66 ms/it LL=-3.20838 + [m1] 48ch torch-mps-f32 375.34 ms/it LL=-3.20839 + [m1] 48ch numpy-cpu-f64 499.61 ms/it LL=-3.20893 + [m1] 70ch mlx-f32 38.29 ms/it LL=-3.21643 + [m1] 70ch torch-cpu-f64 195.01 ms/it LL=-3.21746 + [m1] 70ch torch-mps-f32 404.71 ms/it LL=-3.21638 + [m1] 70ch numpy-cpu-f64 743.35 ms/it LL=-3.22054 +wrote .context/issue-351/raw/dimsweep/dimsweep_mac_ll.json diff --git a/.context/issue-351/raw/dimsweep/timing70_head.json b/.context/issue-351/raw/dimsweep/timing70_head.json new file mode 100644 index 00000000..b60d7a24 --- /dev/null +++ b/.context/issue-351/raw/dimsweep/timing70_head.json @@ -0,0 +1,55 @@ +{ + "platform": { + "machine": "arm64", + "system": "Darwin", + "nproc": 14, + "torch": "2.12.1", + "loadavg": [ + 3.2, + 3.9, + 4.8 + ] + }, + "config": { + "data": "/Users/yahya/Documents/git/mlx-fc-phase15-final-parity/benchmarks/data/ds002718_sub-002_eeg70.npy", + "channels": "70", + "montage": null, + "samples": 30000, + "iters": 25, + "repeats": 3, + "backends": "mlx-f32,torch-cpu-f64,torch-mps-f32", + "out": "/private/tmp/claude-501/-Users-yahya-Documents-git-pamica/3b0e67f1-eae1-4864-8013-454e2363dda4/scratchpad/p15gates/dim_mlx-fc-phase15-final-parity.json", + "report": null, + "n_models": 1, + "share": false, + "fortran_bin": "/Users/yahya/Documents/git/mlx-fc-phase15-final-parity/benchmarks/fortran/amica15", + "fortran_threads": null, + "threads": null + }, + "rows": [ + { + "config": "m1", + "channels": 70, + "backend": "mlx-f32", + "threads": null, + "ms_per_iter": 33.12467999989167, + "final_ll": -3.216428518295288 + }, + { + "config": "m1", + "channels": 70, + "backend": "torch-cpu-f64", + "threads": null, + "ms_per_iter": 189.41867836052552, + "final_ll": -3.217461695658916 + }, + { + "config": "m1", + "channels": 70, + "backend": "torch-mps-f32", + "threads": null, + "ms_per_iter": 166.6829549986869, + "final_ll": -3.2163822650909424 + } + ] +} \ No newline at end of file diff --git a/.context/issue-351/raw/dimsweep/timing70_pre_epic_e38aa11.json b/.context/issue-351/raw/dimsweep/timing70_pre_epic_e38aa11.json new file mode 100644 index 00000000..b63b486b --- /dev/null +++ b/.context/issue-351/raw/dimsweep/timing70_pre_epic_e38aa11.json @@ -0,0 +1,55 @@ +{ + "platform": { + "machine": "arm64", + "system": "Darwin", + "nproc": 14, + "torch": "2.12.1", + "loadavg": [ + 3.2, + 4.0, + 4.8 + ] + }, + "config": { + "data": "/Users/yahya/Documents/git/mlx-fc-phase15-final-parity/benchmarks/data/ds002718_sub-002_eeg70.npy", + "channels": "70", + "montage": null, + "samples": 30000, + "iters": 25, + "repeats": 3, + "backends": "mlx-f32,torch-cpu-f64,torch-mps-f32", + "out": "/private/tmp/claude-501/-Users-yahya-Documents-git-pamica/3b0e67f1-eae1-4864-8013-454e2363dda4/scratchpad/p15gates/dim_p15_pre2.json", + "report": null, + "n_models": 1, + "share": false, + "fortran_bin": "/private/tmp/claude-501/-Users-yahya-Documents-git-pamica/3b0e67f1-eae1-4864-8013-454e2363dda4/scratchpad/p15_pre2/benchmarks/fortran/amica15", + "fortran_threads": null, + "threads": null + }, + "rows": [ + { + "config": "m1", + "channels": 70, + "backend": "mlx-f32", + "threads": null, + "ms_per_iter": 33.264005000237375, + "final_ll": -3.216428518295288 + }, + { + "config": "m1", + "channels": 70, + "backend": "torch-cpu-f64", + "threads": null, + "ms_per_iter": 190.75368164107203, + "final_ll": -3.217461695658916 + }, + { + "config": "m1", + "channels": 70, + "backend": "torch-mps-f32", + "threads": null, + "ms_per_iter": 165.05729664117098, + "final_ll": -3.2163822650909424 + } + ] +} \ No newline at end of file diff --git a/.context/issue-351/raw/fixture_parity_head.txt b/.context/issue-351/raw/fixture_parity_head.txt new file mode 100644 index 00000000..34d61184 --- /dev/null +++ b/.context/issue-351/raw/fixture_parity_head.txt @@ -0,0 +1,3 @@ +torch K=200: LL -3.401731 fixture -3.401873 |dLL|=1.42e-04 corr=0.998321 min=0.9827 amari=4.757e-03 stop=max_iter iters=200 (36s) +numpy K=200: LL -3.401741 fixture -3.401873 |dLL|=1.32e-04 corr=0.998317 min=0.9826 amari=4.753e-03 stop=None iters=200 (65s) +mlx K=200: LL -3.401729 fixture -3.401873 |dLL|=1.44e-04 corr=0.998291 min=0.9823 amari=4.787e-03 stop=max_iter iters=200 (6s) diff --git a/.context/issue-351/raw/gated_tests.log b/.context/issue-351/raw/gated_tests.log new file mode 100644 index 00000000..420ed7e6 --- /dev/null +++ b/.context/issue-351/raw/gated_tests.log @@ -0,0 +1,2 @@ +pamica/tests/test_component_rows.py after the first scan: gm2 reference 4.222e-03, torch 4.222e-03 +======================== 37 passed in 121.82s (0:02:01) ======================== diff --git a/.context/issue-351/raw/gated_tests_post_phase17.log b/.context/issue-351/raw/gated_tests_post_phase17.log new file mode 100644 index 00000000..9c9aea80 --- /dev/null +++ b/.context/issue-351/raw/gated_tests_post_phase17.log @@ -0,0 +1,109 @@ +============================= test session starts ============================== +platform darwin -- Python 3.12.14, pytest-9.1.1, pluggy-1.6.0 +rootdir: /Users/yahya/Documents/git/mlx-fc-phase15-final-parity +configfile: pytest.ini +plugins: cov-7.1.0, xdist-3.8.0 +collected 37 items + +pamica/tests/test_fortran_param_forwarding.py .....................Running PyTorch AMICA (natural-gradient EM backend)... +WARNING: params.json settings with no AMICATorchNG equivalent are ignored (NG uses its own behavior): ['do_history', 'histstep', 'writestep']. The Fortran run may honor them, so a parity comparison can differ. +Fitting AMICA with 32 channels, 4096 samples +Models: 1, Mixture components: 3 + AMICA-NG: 0%| | 0/2 [00:000.9) + + Mixing Matrix Relative Error: 0.0475 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== + +Report saved to: /Users/yahya/Documents/git/mlx-fc-phase15-final-parity/.context/issue-351/raw/harness_pre_epic_e38aa11/validation_report.txt +Running NumPy AMICA (legacy reference backend)... + +AMICA: 0%| | 0/100 [00:000.9) + + Mixing Matrix Relative Error: 0.0476 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== + +Report saved to: /Users/yahya/Documents/git/mlx-fc-phase15-final-parity/.context/issue-351/raw/harness_pre_epic_e38aa11/validation_report_numpy.txt +Running MLX AMICA (Apple-GPU float32 backend)... +WARNING: params.json settings with no AMICAMLXNG equivalent are ignored (MLX uses its own behavior): ['do_history', 'histstep', 'writestep']. The Fortran run may honor them, so a parity comparison can differ. +Fitting AMICA with 32 channels, 30504 samples +Models: 1, Mixture components: 3 + + +====================================================================== +VALIDATION REPORT: Fortran vs MLX AMICA +====================================================================== + +1. LOG-LIKELIHOOD COMPARISON: +---------------------------------------- + Fortran Final LL: -3.4112742644 + MLX Final LL: -3.411242 + LL Ratio (MLX/Fortran): 1.0000 + LL Absolute Difference: 0.000032 + +2. CONVERGENCE COMPARISON: +---------------------------------------- + Fortran Iterations: 100 + MLX Iterations: 100 + +3. COMPONENT CORRELATION: +---------------------------------------- + Mean Correlation: 0.9992 + Min Correlation: 0.9935 + Max Correlation: 1.0000 + Std Correlation: 0.0014 + ✓ Components are highly correlated (>0.9) + + Mixing Matrix Relative Error: 0.0474 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== + +Report saved to: /Users/yahya/Documents/git/mlx-fc-phase15-final-parity/.context/issue-351/raw/harness_pre_epic_e38aa11/validation_report_mlx.txt + +PARITY SUMMARY (seed=42, max_iter=100): +| backend | precision | iterations | final LL | abs. LL difference | mean matched corr | min matched corr | Amari distance | runtime (s) | +|---|---|---:|---:|---:|---:|---:|---:|---:| +| Fortran (reference) | float64 | 100 | -3.411274 | | | | | 10.1 | +| PyTorch | float64 | 100 | -3.411245 | 0.000029 | 0.9992 | 0.9935 | 0.0037 | 17.9 | +| NumPy | float64 | 100 | -3.411246 | 0.000029 | 0.9992 | 0.9934 | 0.0037 | 32.5 | +| MLX | float32 | 100 | -3.411242 | 0.000032 | 0.9992 | 0.9935 | 0.0037 | 3.2 | + +Summary saved to: /Users/yahya/Documents/git/mlx-fc-phase15-final-parity/.context/issue-351/raw/harness_pre_epic_e38aa11/parity_summary.md +uv run --project $W python validate_implementations.py --backend all $R 68.02s user 27.79s system 147% cpu 1:04.93 total diff --git a/.context/issue-351/raw/harness_pre_epic_e38aa11/validation_report.txt b/.context/issue-351/raw/harness_pre_epic_e38aa11/validation_report.txt new file mode 100644 index 00000000..cad7189b --- /dev/null +++ b/.context/issue-351/raw/harness_pre_epic_e38aa11/validation_report.txt @@ -0,0 +1,33 @@ + +====================================================================== +VALIDATION REPORT: Fortran vs PyTorch AMICA +====================================================================== + +1. LOG-LIKELIHOOD COMPARISON: +---------------------------------------- + Fortran Final LL: -3.4112742644 + PyTorch Final LL: -3.411245 + LL Ratio (PyTorch/Fortran): 1.0000 + LL Absolute Difference: 0.000029 + +2. CONVERGENCE COMPARISON: +---------------------------------------- + Fortran Iterations: 100 + PyTorch Iterations: 100 + +3. COMPONENT CORRELATION: +---------------------------------------- + Mean Correlation: 0.9992 + Min Correlation: 0.9935 + Max Correlation: 1.0000 + Std Correlation: 0.0014 + ✓ Components are highly correlated (>0.9) + + Mixing Matrix Relative Error: 0.0475 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== diff --git a/.context/issue-351/raw/harness_pre_epic_e38aa11/validation_report_mlx.txt b/.context/issue-351/raw/harness_pre_epic_e38aa11/validation_report_mlx.txt new file mode 100644 index 00000000..4c24409e --- /dev/null +++ b/.context/issue-351/raw/harness_pre_epic_e38aa11/validation_report_mlx.txt @@ -0,0 +1,33 @@ + +====================================================================== +VALIDATION REPORT: Fortran vs MLX AMICA +====================================================================== + +1. LOG-LIKELIHOOD COMPARISON: +---------------------------------------- + Fortran Final LL: -3.4112742644 + MLX Final LL: -3.411242 + LL Ratio (MLX/Fortran): 1.0000 + LL Absolute Difference: 0.000032 + +2. CONVERGENCE COMPARISON: +---------------------------------------- + Fortran Iterations: 100 + MLX Iterations: 100 + +3. COMPONENT CORRELATION: +---------------------------------------- + Mean Correlation: 0.9992 + Min Correlation: 0.9935 + Max Correlation: 1.0000 + Std Correlation: 0.0014 + ✓ Components are highly correlated (>0.9) + + Mixing Matrix Relative Error: 0.0474 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== diff --git a/.context/issue-351/raw/harness_pre_epic_e38aa11/validation_report_numpy.txt b/.context/issue-351/raw/harness_pre_epic_e38aa11/validation_report_numpy.txt new file mode 100644 index 00000000..1514abb1 --- /dev/null +++ b/.context/issue-351/raw/harness_pre_epic_e38aa11/validation_report_numpy.txt @@ -0,0 +1,33 @@ + +====================================================================== +VALIDATION REPORT: Fortran vs NumPy AMICA +====================================================================== + +1. LOG-LIKELIHOOD COMPARISON: +---------------------------------------- + Fortran Final LL: -3.4112742644 + NumPy Final LL: -3.411246 + LL Ratio (NumPy/Fortran): 1.0000 + LL Absolute Difference: 0.000029 + +2. CONVERGENCE COMPARISON: +---------------------------------------- + Fortran Iterations: 100 + NumPy Iterations: 100 + +3. COMPONENT CORRELATION: +---------------------------------------- + Mean Correlation: 0.9992 + Min Correlation: 0.9934 + Max Correlation: 1.0000 + Std Correlation: 0.0014 + ✓ Components are highly correlated (>0.9) + + Mixing Matrix Relative Error: 0.0476 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== diff --git a/.context/issue-351/raw/harness_run1/fortran_run/fortran_output/out.txt b/.context/issue-351/raw/harness_run1/fortran_run/fortran_output/out.txt new file mode 100644 index 00000000..e3298da9 --- /dev/null +++ b/.context/issue-351/raw/harness_run1/fortran_run/fortran_output/out.txt @@ -0,0 +1,108 @@ + minimum eigenvalues = 4.8799005132501518 6.9201197127079643 7.6562147928880719 + maximum eigenvalues = 9711.1430838537053 3039.6850435125002 1244.4129447052055 + num eigs kept = 32 + minimum eigenvalues = 4.8799005132501518 6.9201197127079643 7.6562147928880719 + maximum eigenvalues = 9711.1430838537053 3039.6850435125002 1244.4129447052055 + num eigs kept = 32 + iter 1 lrate = 0.0500000000 LL = -3.5138404119 nd = 0.0582472378, D = 0.42970E-02 0.42970E-02 ( 0.10 s, 0.0 h) + iter 2 lrate = 0.0500000000 LL = -3.4686647974 nd = 0.0257873373, D = 0.37613E-02 0.37613E-02 ( 0.10 s, 0.0 h) + iter 3 lrate = 0.0500000000 LL = -3.4616920323 nd = 0.0243745621, D = 0.37866E-02 0.37866E-02 ( 0.10 s, 0.0 h) + iter 4 lrate = 0.0500000000 LL = -3.4590483434 nd = 0.0268337143, D = 0.57743E-02 0.57743E-02 ( 0.10 s, 0.0 h) + iter 5 lrate = 0.0500000000 LL = -3.4570950991 nd = 0.0275204167, D = 0.10300E-01 0.10300E-01 ( 0.10 s, 0.0 h) + iter 6 lrate = 0.0500000000 LL = -3.4554259776 nd = 0.0271554390, D = 0.17193E-01 0.17193E-01 ( 0.10 s, 0.0 h) + iter 7 lrate = 0.0500000000 LL = -3.4539448816 nd = 0.0263078732, D = 0.25963E-01 0.25963E-01 ( 0.10 s, 0.0 h) + iter 8 lrate = 0.0500000000 LL = -3.4526061773 nd = 0.0252305695, D = 0.36058E-01 0.36058E-01 ( 0.10 s, 0.0 h) + iter 9 lrate = 0.0500000000 LL = -3.4514049324 nd = 0.0239589135, D = 0.46948E-01 0.46948E-01 ( 0.10 s, 0.0 h) + iter 10 lrate = 0.0500000000 LL = -3.4503446967 nd = 0.0224833972, D = 0.58114E-01 0.58114E-01 ( 0.10 s, 0.0 h) + iter 11 lrate = 0.0500000000 LL = -3.4494273388 nd = 0.0208405539, D = 0.69043E-01 0.69043E-01 ( 0.10 s, 0.0 h) + iter 12 lrate = 0.0500000000 LL = -3.4486503827 nd = 0.0191031940, D = 0.79272E-01 0.79272E-01 ( 0.10 s, 0.0 h) + iter 13 lrate = 0.0500000000 LL = -3.4480028416 nd = 0.0173800933, D = 0.88429E-01 0.88429E-01 ( 0.10 s, 0.0 h) + iter 14 lrate = 0.0500000000 LL = -3.4474749603 nd = 0.0156968335, D = 0.96270E-01 0.96270E-01 ( 0.10 s, 0.0 h) + iter 15 lrate = 0.0500000000 LL = -3.4470391911 nd = 0.0143341558, D = 0.10260E+00 0.10260E+00 ( 0.10 s, 0.0 h) + iter 16 lrate = 0.0500000000 LL = -3.4466732866 nd = 0.0132110732, D = 0.10754E+00 0.10754E+00 ( 0.10 s, 0.0 h) + iter 17 lrate = 0.0500000000 LL = -3.4463623229 nd = 0.0123285260, D = 0.11120E+00 0.11120E+00 ( 0.10 s, 0.0 h) + iter 18 lrate = 0.0500000000 LL = -3.4460914899 nd = 0.0116493533, D = 0.11371E+00 0.11371E+00 ( 0.10 s, 0.0 h) + iter 19 lrate = 0.0500000000 LL = -3.4458502064 nd = 0.0111482807, D = 0.11524E+00 0.11524E+00 ( 0.10 s, 0.0 h) + iter 20 lrate = 0.0500000000 LL = -3.4456268009 nd = 0.0107728174, D = 0.11596E+00 0.11596E+00 ( 0.10 s, 0.0 h) + iter 21 lrate = 0.0500000000 LL = -3.4454181035 nd = 0.0104894729, D = 0.11604E+00 0.11604E+00 ( 0.10 s, 0.0 h) + iter 22 lrate = 0.0500000000 LL = -3.4452217620 nd = 0.0102683247, D = 0.11562E+00 0.11562E+00 ( 0.10 s, 0.0 h) + iter 23 lrate = 0.0500000000 LL = -3.4450342435 nd = 0.0100942550, D = 0.11481E+00 0.11481E+00 ( 0.10 s, 0.0 h) + iter 24 lrate = 0.0500000000 LL = -3.4448536484 nd = 0.0099474996, D = 0.11371E+00 0.11371E+00 ( 0.10 s, 0.0 h) + iter 25 lrate = 0.0500000000 LL = -3.4446793315 nd = 0.0098193230, D = 0.11240E+00 0.11240E+00 ( 0.10 s, 0.0 h) + iter 26 lrate = 0.0500000000 LL = -3.4445112148 nd = 0.0097106364, D = 0.11093E+00 0.11093E+00 ( 0.10 s, 0.0 h) + iter 27 lrate = 0.0500000000 LL = -3.4443458120 nd = 0.0096101155, D = 0.10935E+00 0.10935E+00 ( 0.10 s, 0.0 h) + iter 28 lrate = 0.0500000000 LL = -3.4441841429 nd = 0.0095174602, D = 0.10770E+00 0.10770E+00 ( 0.10 s, 0.0 h) + iter 29 lrate = 0.0500000000 LL = -3.4440258521 nd = 0.0094318094, D = 0.10601E+00 0.10601E+00 ( 0.10 s, 0.0 h) + iter 30 lrate = 0.0500000000 LL = -3.4438715045 nd = 0.0093537182, D = 0.10429E+00 0.10429E+00 ( 0.10 s, 0.0 h) + iter 31 lrate = 0.0500000000 LL = -3.4437197766 nd = 0.0092762513, D = 0.10256E+00 0.10256E+00 ( 0.10 s, 0.0 h) + iter 32 lrate = 0.0500000000 LL = -3.4435705718 nd = 0.0092096201, D = 0.10084E+00 0.10084E+00 ( 0.10 s, 0.0 h) + iter 33 lrate = 0.0500000000 LL = -3.4434240077 nd = 0.0091470859, D = 0.99122E-01 0.99122E-01 ( 0.10 s, 0.0 h) + iter 34 lrate = 0.0500000000 LL = -3.4432801434 nd = 0.0090904652, D = 0.97425E-01 0.97425E-01 ( 0.10 s, 0.0 h) + iter 35 lrate = 0.0500000000 LL = -3.4431381670 nd = 0.0090375639, D = 0.95754E-01 0.95754E-01 ( 0.10 s, 0.0 h) + iter 36 lrate = 0.0500000000 LL = -3.4429990901 nd = 0.0089824829, D = 0.94111E-01 0.94111E-01 ( 0.10 s, 0.0 h) + iter 37 lrate = 0.0500000000 LL = -3.4428609571 nd = 0.0089354414, D = 0.92496E-01 0.92496E-01 ( 0.10 s, 0.0 h) + iter 38 lrate = 0.0500000000 LL = -3.4427244769 nd = 0.0088906811, D = 0.90910E-01 0.90910E-01 ( 0.10 s, 0.0 h) + iter 39 lrate = 0.0500000000 LL = -3.4425891313 nd = 0.0088492082, D = 0.89359E-01 0.89359E-01 ( 0.10 s, 0.0 h) + iter 40 lrate = 0.0500000000 LL = -3.4424553915 nd = 0.0088070259, D = 0.87837E-01 0.87837E-01 ( 0.10 s, 0.0 h) + iter 41 lrate = 0.0500000000 LL = -3.4423226720 nd = 0.0087682129, D = 0.86349E-01 0.86349E-01 ( 0.10 s, 0.0 h) + iter 42 lrate = 0.0500000000 LL = -3.4421911989 nd = 0.0087309078, D = 0.84898E-01 0.84898E-01 ( 0.10 s, 0.0 h) + iter 43 lrate = 0.0500000000 LL = -3.4420611786 nd = 0.0086945363, D = 0.83480E-01 0.83480E-01 ( 0.10 s, 0.0 h) + iter 44 lrate = 0.0500000000 LL = -3.4419321185 nd = 0.0086603649, D = 0.82095E-01 0.82095E-01 ( 0.10 s, 0.0 h) + iter 45 lrate = 0.0500000000 LL = -3.4418042727 nd = 0.0086283889, D = 0.80742E-01 0.80742E-01 ( 0.10 s, 0.0 h) + iter 46 lrate = 0.0500000000 LL = -3.4416768672 nd = 0.0085974373, D = 0.79422E-01 0.79422E-01 ( 0.10 s, 0.0 h) + iter 47 lrate = 0.0500000000 LL = -3.4415505599 nd = 0.0085658375, D = 0.78132E-01 0.78132E-01 ( 0.10 s, 0.0 h) + iter 48 lrate = 0.0500000000 LL = -3.4414252826 nd = 0.0085355070, D = 0.76874E-01 0.76874E-01 ( 0.10 s, 0.0 h) + iter 49 lrate = 0.0500000000 LL = -3.4413008778 nd = 0.0085045367, D = 0.75648E-01 0.75648E-01 ( 0.10 s, 0.0 h) + iter 50 lrate = 0.0500000000 LL = -3.4411775002 nd = 0.0073499988, D = 0.74448E-01 0.74448E-01 ( 0.10 s, 0.0 h) + iter 51 lrate = 0.1000000000 LL = -3.4409782229 nd = 0.0073279306, D = 0.73141E-01 0.73141E-01 ( 0.10 s, 0.0 h) + iter 52 lrate = 0.2000000000 LL = -3.4405919551 nd = 0.0072717685, D = 0.70617E-01 0.70617E-01 ( 0.10 s, 0.0 h) + iter 53 lrate = 0.3000000000 LL = -3.4400260592 nd = 0.0071975653, D = 0.67051E-01 0.67051E-01 ( 0.10 s, 0.0 h) + iter 54 lrate = 0.4000000000 LL = -3.4392902421 nd = 0.0071025916, D = 0.62693E-01 0.62693E-01 ( 0.10 s, 0.0 h) + iter 55 lrate = 0.5000000000 LL = -3.4383944792 nd = 0.0069838003, D = 0.57833E-01 0.57833E-01 ( 0.10 s, 0.0 h) + iter 56 lrate = 0.6000000000 LL = -3.4373514586 nd = 0.0068564048, D = 0.52779E-01 0.52779E-01 ( 0.10 s, 0.0 h) + iter 57 lrate = 0.7000000000 LL = -3.4361733721 nd = 0.0067176434, D = 0.47812E-01 0.47812E-01 ( 0.10 s, 0.0 h) + iter 58 lrate = 0.8000000000 LL = -3.4348773500 nd = 0.0065306037, D = 0.43152E-01 0.43152E-01 ( 0.10 s, 0.0 h) + iter 59 lrate = 0.9000000000 LL = -3.4334862033 nd = 0.0063144473, D = 0.38858E-01 0.38858E-01 ( 0.10 s, 0.0 h) + iter 60 lrate = 1.0000000000 LL = -3.4320290718 nd = 0.0060694050, D = 0.35007E-01 0.35007E-01 ( 0.10 s, 0.0 h) + iter 61 lrate = 1.0000000000 LL = -3.4306539336 nd = 0.0058165467, D = 0.31848E-01 0.31848E-01 ( 0.10 s, 0.0 h) + iter 62 lrate = 1.0000000000 LL = -3.4293727997 nd = 0.0055406777, D = 0.29288E-01 0.29288E-01 ( 0.10 s, 0.0 h) + iter 63 lrate = 1.0000000000 LL = -3.4281948781 nd = 0.0052692527, D = 0.27250E-01 0.27250E-01 ( 0.10 s, 0.0 h) + iter 64 lrate = 1.0000000000 LL = -3.4271211832 nd = 0.0050272799, D = 0.25745E-01 0.25745E-01 ( 0.10 s, 0.0 h) + iter 65 lrate = 1.0000000000 LL = -3.4261411718 nd = 0.0048073579, D = 0.24812E-01 0.24812E-01 ( 0.10 s, 0.0 h) + iter 66 lrate = 1.0000000000 LL = -3.4252466155 nd = 0.0046179247, D = 0.24345E-01 0.24345E-01 ( 0.10 s, 0.0 h) + iter 67 lrate = 1.0000000000 LL = -3.4244237228 nd = 0.0044367521, D = 0.24187E-01 0.24187E-01 ( 0.10 s, 0.0 h) + iter 68 lrate = 1.0000000000 LL = -3.4236656099 nd = 0.0042766153, D = 0.24212E-01 0.24212E-01 ( 0.10 s, 0.0 h) + iter 69 lrate = 1.0000000000 LL = -3.4229613447 nd = 0.0041429572, D = 0.24322E-01 0.24322E-01 ( 0.10 s, 0.0 h) + iter 70 lrate = 1.0000000000 LL = -3.4223013447 nd = 0.0040280167, D = 0.24478E-01 0.24478E-01 ( 0.10 s, 0.0 h) + iter 71 lrate = 1.0000000000 LL = -3.4216799403 nd = 0.0039220625, D = 0.24654E-01 0.24654E-01 ( 0.10 s, 0.0 h) + iter 72 lrate = 1.0000000000 LL = -3.4210881340 nd = 0.0038434466, D = 0.24854E-01 0.24854E-01 ( 0.10 s, 0.0 h) + iter 73 lrate = 1.0000000000 LL = -3.4205200068 nd = 0.0037706961, D = 0.25099E-01 0.25099E-01 ( 0.10 s, 0.0 h) + iter 74 lrate = 1.0000000000 LL = -3.4199694700 nd = 0.0037100029, D = 0.25387E-01 0.25387E-01 ( 0.10 s, 0.0 h) + iter 75 lrate = 1.0000000000 LL = -3.4194343331 nd = 0.0036479584, D = 0.25686E-01 0.25686E-01 ( 0.10 s, 0.0 h) + iter 76 lrate = 1.0000000000 LL = -3.4189134918 nd = 0.0035920222, D = 0.26006E-01 0.26006E-01 ( 0.10 s, 0.0 h) + iter 77 lrate = 1.0000000000 LL = -3.4184066190 nd = 0.0035339680, D = 0.26356E-01 0.26356E-01 ( 0.10 s, 0.0 h) + iter 78 lrate = 1.0000000000 LL = -3.4179137938 nd = 0.0034747481, D = 0.26715E-01 0.26715E-01 ( 0.10 s, 0.0 h) + iter 79 lrate = 1.0000000000 LL = -3.4174367980 nd = 0.0034084627, D = 0.27097E-01 0.27097E-01 ( 0.10 s, 0.0 h) + iter 80 lrate = 1.0000000000 LL = -3.4169767547 nd = 0.0033321218, D = 0.27486E-01 0.27486E-01 ( 0.10 s, 0.0 h) + iter 81 lrate = 1.0000000000 LL = -3.4165384642 nd = 0.0032441994, D = 0.27901E-01 0.27901E-01 ( 0.10 s, 0.0 h) + iter 82 lrate = 1.0000000000 LL = -3.4161213948 nd = 0.0031523169, D = 0.28348E-01 0.28348E-01 ( 0.10 s, 0.0 h) + iter 83 lrate = 1.0000000000 LL = -3.4157282834 nd = 0.0030511007, D = 0.28801E-01 0.28801E-01 ( 0.10 s, 0.0 h) + iter 84 lrate = 1.0000000000 LL = -3.4153589168 nd = 0.0029613584, D = 0.29281E-01 0.29281E-01 ( 0.10 s, 0.0 h) + iter 85 lrate = 1.0000000000 LL = -3.4150109305 nd = 0.0028842884, D = 0.29777E-01 0.29777E-01 ( 0.10 s, 0.0 h) + iter 86 lrate = 1.0000000000 LL = -3.4146806129 nd = 0.0028133041, D = 0.30284E-01 0.30284E-01 ( 0.10 s, 0.0 h) + iter 87 lrate = 1.0000000000 LL = -3.4143657476 nd = 0.0027405093, D = 0.30773E-01 0.30773E-01 ( 0.10 s, 0.0 h) + iter 88 lrate = 1.0000000000 LL = -3.4140664829 nd = 0.0026759716, D = 0.31255E-01 0.31255E-01 ( 0.10 s, 0.0 h) + iter 89 lrate = 1.0000000000 LL = -3.4137827396 nd = 0.0026172247, D = 0.31754E-01 0.31754E-01 ( 0.10 s, 0.0 h) + iter 90 lrate = 1.0000000000 LL = -3.4135099614 nd = 0.0025723970, D = 0.32216E-01 0.32216E-01 ( 0.10 s, 0.0 h) + iter 91 lrate = 1.0000000000 LL = -3.4132471327 nd = 0.0025251906, D = 0.32652E-01 0.32652E-01 ( 0.10 s, 0.0 h) + iter 92 lrate = 1.0000000000 LL = -3.4129956272 nd = 0.0024806124, D = 0.33075E-01 0.33075E-01 ( 0.10 s, 0.0 h) + iter 93 lrate = 1.0000000000 LL = -3.4127524864 nd = 0.0024369464, D = 0.33513E-01 0.33513E-01 ( 0.10 s, 0.0 h) + iter 94 lrate = 1.0000000000 LL = -3.4125183782 nd = 0.0023791588, D = 0.33946E-01 0.33946E-01 ( 0.10 s, 0.0 h) + iter 95 lrate = 1.0000000000 LL = -3.4122952276 nd = 0.0023308726, D = 0.34370E-01 0.34370E-01 ( 0.10 s, 0.0 h) + iter 96 lrate = 1.0000000000 LL = -3.4120794495 nd = 0.0022935368, D = 0.34803E-01 0.34803E-01 ( 0.10 s, 0.0 h) + iter 97 lrate = 1.0000000000 LL = -3.4118716676 nd = 0.0022663411, D = 0.35211E-01 0.35211E-01 ( 0.10 s, 0.0 h) + iter 98 lrate = 1.0000000000 LL = -3.4116682686 nd = 0.0022430245, D = 0.35585E-01 0.35585E-01 ( 0.10 s, 0.0 h) + iter 99 lrate = 1.0000000000 LL = -3.4114690851 nd = 0.0022236502, D = 0.35953E-01 0.35953E-01 ( 0.10 s, 0.0 h) + iter 100 lrate = 1.0000000000 LL = -3.4112742644 nd = 0.0022054248, D = 0.36296E-01 0.36296E-01 ( 0.10 s, 0.0 h) +... done. Execution time: 0.00 h +output directory = ./fortran_output/ diff --git a/.context/issue-351/raw/harness_run1/fortran_run/input.param b/.context/issue-351/raw/harness_run1/fortran_run/input.param new file mode 100644 index 00000000..c8f97ce2 --- /dev/null +++ b/.context/issue-351/raw/harness_run1/fortran_run/input.param @@ -0,0 +1,82 @@ +files ./eeglab_data.fdt +outdir ./fortran_output/ +block_size 512 +do_opt_block 0 +blk_min 256 +blk_step 256 +blk_max 1024 +num_models 1 +max_threads 1 +use_min_dll 1 +min_dll 1e-09 +use_grad_norm 1 +min_grad_norm 1e-07 +num_mix_comps 3 +pdftype 0 +max_iter 100 +num_samples 1 +data_dim 32 +field_dim 30504 +field_blocksize 1 +do_history 0 +histstep 10 +share_comps 0 +share_start 100 +comp_thresh 0.99 +share_iter 100 +lrate 0.05 +minlrate 1e-08 +mineig 1.000000e-12 +lratefact 0.5 +rholrate 0.05 +rho0 1.5 +minrho 1.0 +maxrho 2.0 +rholratefact 0.5 +kurt_start 3 +num_kurt 5 +kurt_int 1 +do_newton 1 +newt_start 50 +newt_ramp 10 +newtrate 1.0 +do_reject 0 +numrej 3 +rejsig 3.0 +rejstart 2 +rejint 3 +writestep 20 +write_nd 0 +write_LLt 1 +decwindow 1 +max_decs 3 +fix_init 0 +update_A 1 +update_c 1 +update_gm 1 +update_alpha 1 +update_mu 1 +update_beta 1 +invsigmax 100.0 +invsigmin 0.0 +do_rho 1 +load_rej 0 +load_W 0 +load_c 0 +load_gm 0 +load_alpha 0 +load_mu 0 +load_beta 0 +load_rho 0 +load_comp_list 0 +do_mean 1 +do_sphere 1 +doPCA 1 +pcakeep 32 +pcadb 30.0 +byte_size 4 +doscaling 1 +scalestep 1 +num_comps 32 +do_approx_sphere 1 +seed 42 diff --git a/.context/issue-351/raw/harness_run1/parity_summary.md b/.context/issue-351/raw/harness_run1/parity_summary.md new file mode 100644 index 00000000..f9cb8f00 --- /dev/null +++ b/.context/issue-351/raw/harness_run1/parity_summary.md @@ -0,0 +1,6 @@ +| backend | precision | iterations | final LL | abs. LL difference | mean matched corr | min matched corr | Amari distance | runtime (s) | +|---|---|---:|---:|---:|---:|---:|---:|---:| +| Fortran (reference) | float64 | 100 | -3.411274 | | | | | 10.0 | +| PyTorch | float64 | 100 | -3.411003 | 0.000271 | 0.9991 | 0.9918 | 0.0038 | 18.1 | +| NumPy | float64 | 100 | -3.411003 | 0.000272 | 0.9991 | 0.9917 | 0.0038 | 32.6 | +| MLX | float32 | 100 | -3.410998 | 0.000276 | 0.9991 | 0.9917 | 0.0038 | 3.0 | diff --git a/.context/issue-351/raw/harness_run1/stdout.log b/.context/issue-351/raw/harness_run1/stdout.log new file mode 100644 index 00000000..17e297cf --- /dev/null +++ b/.context/issue-351/raw/harness_run1/stdout.log @@ -0,0 +1,240 @@ +AMICA uses float64 for Fortran parity; MPS lacks float64 support, so falling back to CPU. Pass dtype=torch.float32 with device='mps' to run on MPS. +Validation with seed=42, max_iter=100 +Output directory: .context/issue-351/raw/harness_run1 +Loaded data: (32, 30504) +Running Fortran AMICA... +Running PyTorch AMICA (natural-gradient EM backend)... +WARNING: params.json settings with no AMICATorchNG equivalent are ignored (NG uses its own behavior): ['do_history', 'histstep', 'writestep']. The Fortran run may honor them, so a parity comparison can differ. +Fitting AMICA with 32 channels, 30504 samples +Models: 1, Mixture components: 3 +AMICA uses float64 for Fortran parity; MPS lacks float64 support, so falling back to CPU. Pass dtype=torch.float32 with device='mps' to run on MPS. + +Starting AMICA fitting... +Starting optimization... + +====================================================================== +VALIDATION REPORT: Fortran vs PyTorch AMICA +====================================================================== + +1. LOG-LIKELIHOOD COMPARISON: +---------------------------------------- + Fortran Final LL: -3.4112742644 + PyTorch Final LL: -3.411003 + LL Ratio (PyTorch/Fortran): 0.9999 + LL Absolute Difference: 0.000271 + +2. CONVERGENCE COMPARISON: +---------------------------------------- + Fortran Iterations: 100 + PyTorch Iterations: 100 + +3. COMPONENT CORRELATION: +---------------------------------------- + Mean Correlation: 0.9991 + Min Correlation: 0.9918 + Max Correlation: 1.0000 + Std Correlation: 0.0017 + ✓ Components are highly correlated (>0.9) + + Mixing Matrix Relative Error: 0.0432 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== + +Report saved to: .context/issue-351/raw/harness_run1/validation_report.txt +Running NumPy AMICA (legacy reference backend)... + +AMICA: 0%| | 0/100 [00:000.9) + + Mixing Matrix Relative Error: 0.0432 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== + +Report saved to: .context/issue-351/raw/harness_run1/validation_report_numpy.txt +Running MLX AMICA (Apple-GPU float32 backend)... +WARNING: params.json settings with no AMICAMLXNG equivalent are ignored (MLX uses its own behavior): ['do_history', 'histstep', 'writestep']. The Fortran run may honor them, so a parity comparison can differ. +Fitting AMICA with 32 channels, 30504 samples +Models: 1, Mixture components: 3 + + +====================================================================== +VALIDATION REPORT: Fortran vs MLX AMICA +====================================================================== + +1. LOG-LIKELIHOOD COMPARISON: +---------------------------------------- + Fortran Final LL: -3.4112742644 + MLX Final LL: -3.410998 + LL Ratio (MLX/Fortran): 0.9999 + LL Absolute Difference: 0.000276 + +2. CONVERGENCE COMPARISON: +---------------------------------------- + Fortran Iterations: 100 + MLX Iterations: 100 + +3. COMPONENT CORRELATION: +---------------------------------------- + Mean Correlation: 0.9991 + Min Correlation: 0.9917 + Max Correlation: 1.0000 + Std Correlation: 0.0017 + ✓ Components are highly correlated (>0.9) + + Mixing Matrix Relative Error: 0.0433 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== + +Report saved to: .context/issue-351/raw/harness_run1/validation_report_mlx.txt + +PARITY SUMMARY (seed=42, max_iter=100): +| backend | precision | iterations | final LL | abs. LL difference | mean matched corr | min matched corr | Amari distance | runtime (s) | +|---|---|---:|---:|---:|---:|---:|---:|---:| +| Fortran (reference) | float64 | 100 | -3.411274 | | | | | 10.0 | +| PyTorch | float64 | 100 | -3.411003 | 0.000271 | 0.9991 | 0.9918 | 0.0038 | 18.1 | +| NumPy | float64 | 100 | -3.411003 | 0.000272 | 0.9991 | 0.9917 | 0.0038 | 32.6 | +| MLX | float32 | 100 | -3.410998 | 0.000276 | 0.9991 | 0.9917 | 0.0038 | 3.0 | + +Summary saved to: .context/issue-351/raw/harness_run1/parity_summary.md +uv run python validate_implementations.py --backend all --fortran-binary $R 68.25s user 28.52s system 148% cpu 1:05.38 total diff --git a/.context/issue-351/raw/harness_run1/validation_report.txt b/.context/issue-351/raw/harness_run1/validation_report.txt new file mode 100644 index 00000000..30026a13 --- /dev/null +++ b/.context/issue-351/raw/harness_run1/validation_report.txt @@ -0,0 +1,33 @@ + +====================================================================== +VALIDATION REPORT: Fortran vs PyTorch AMICA +====================================================================== + +1. LOG-LIKELIHOOD COMPARISON: +---------------------------------------- + Fortran Final LL: -3.4112742644 + PyTorch Final LL: -3.411003 + LL Ratio (PyTorch/Fortran): 0.9999 + LL Absolute Difference: 0.000271 + +2. CONVERGENCE COMPARISON: +---------------------------------------- + Fortran Iterations: 100 + PyTorch Iterations: 100 + +3. COMPONENT CORRELATION: +---------------------------------------- + Mean Correlation: 0.9991 + Min Correlation: 0.9918 + Max Correlation: 1.0000 + Std Correlation: 0.0017 + ✓ Components are highly correlated (>0.9) + + Mixing Matrix Relative Error: 0.0432 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== diff --git a/.context/issue-351/raw/harness_run1/validation_report_mlx.txt b/.context/issue-351/raw/harness_run1/validation_report_mlx.txt new file mode 100644 index 00000000..4f464504 --- /dev/null +++ b/.context/issue-351/raw/harness_run1/validation_report_mlx.txt @@ -0,0 +1,33 @@ + +====================================================================== +VALIDATION REPORT: Fortran vs MLX AMICA +====================================================================== + +1. LOG-LIKELIHOOD COMPARISON: +---------------------------------------- + Fortran Final LL: -3.4112742644 + MLX Final LL: -3.410998 + LL Ratio (MLX/Fortran): 0.9999 + LL Absolute Difference: 0.000276 + +2. CONVERGENCE COMPARISON: +---------------------------------------- + Fortran Iterations: 100 + MLX Iterations: 100 + +3. COMPONENT CORRELATION: +---------------------------------------- + Mean Correlation: 0.9991 + Min Correlation: 0.9917 + Max Correlation: 1.0000 + Std Correlation: 0.0017 + ✓ Components are highly correlated (>0.9) + + Mixing Matrix Relative Error: 0.0433 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== diff --git a/.context/issue-351/raw/harness_run1/validation_report_numpy.txt b/.context/issue-351/raw/harness_run1/validation_report_numpy.txt new file mode 100644 index 00000000..f6d394c7 --- /dev/null +++ b/.context/issue-351/raw/harness_run1/validation_report_numpy.txt @@ -0,0 +1,33 @@ + +====================================================================== +VALIDATION REPORT: Fortran vs NumPy AMICA +====================================================================== + +1. LOG-LIKELIHOOD COMPARISON: +---------------------------------------- + Fortran Final LL: -3.4112742644 + NumPy Final LL: -3.411003 + LL Ratio (NumPy/Fortran): 0.9999 + LL Absolute Difference: 0.000272 + +2. CONVERGENCE COMPARISON: +---------------------------------------- + Fortran Iterations: 100 + NumPy Iterations: 100 + +3. COMPONENT CORRELATION: +---------------------------------------- + Mean Correlation: 0.9991 + Min Correlation: 0.9917 + Max Correlation: 1.0000 + Std Correlation: 0.0017 + ✓ Components are highly correlated (>0.9) + + Mixing Matrix Relative Error: 0.0432 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== diff --git a/.context/issue-351/raw/harness_run2/fortran_run/fortran_output/out.txt b/.context/issue-351/raw/harness_run2/fortran_run/fortran_output/out.txt new file mode 100644 index 00000000..e029b665 --- /dev/null +++ b/.context/issue-351/raw/harness_run2/fortran_run/fortran_output/out.txt @@ -0,0 +1,108 @@ + minimum eigenvalues = 4.8799005132501518 6.9201197127079643 7.6562147928880719 + maximum eigenvalues = 9711.1430838537053 3039.6850435125002 1244.4129447052055 + num eigs kept = 32 + minimum eigenvalues = 4.8799005132501518 6.9201197127079643 7.6562147928880719 + maximum eigenvalues = 9711.1430838537053 3039.6850435125002 1244.4129447052055 + num eigs kept = 32 + iter 1 lrate = 0.0500000000 LL = -3.5138404119 nd = 0.0582472378, D = 0.42970E-02 0.42970E-02 ( 0.10 s, 0.0 h) + iter 2 lrate = 0.0500000000 LL = -3.4686647974 nd = 0.0257873373, D = 0.37613E-02 0.37613E-02 ( 0.10 s, 0.0 h) + iter 3 lrate = 0.0500000000 LL = -3.4616920323 nd = 0.0243745621, D = 0.37866E-02 0.37866E-02 ( 0.10 s, 0.0 h) + iter 4 lrate = 0.0500000000 LL = -3.4590483434 nd = 0.0268337143, D = 0.57743E-02 0.57743E-02 ( 0.10 s, 0.0 h) + iter 5 lrate = 0.0500000000 LL = -3.4570950991 nd = 0.0275204167, D = 0.10300E-01 0.10300E-01 ( 0.10 s, 0.0 h) + iter 6 lrate = 0.0500000000 LL = -3.4554259776 nd = 0.0271554390, D = 0.17193E-01 0.17193E-01 ( 0.10 s, 0.0 h) + iter 7 lrate = 0.0500000000 LL = -3.4539448816 nd = 0.0263078732, D = 0.25963E-01 0.25963E-01 ( 0.10 s, 0.0 h) + iter 8 lrate = 0.0500000000 LL = -3.4526061773 nd = 0.0252305695, D = 0.36058E-01 0.36058E-01 ( 0.10 s, 0.0 h) + iter 9 lrate = 0.0500000000 LL = -3.4514049324 nd = 0.0239589135, D = 0.46948E-01 0.46948E-01 ( 0.10 s, 0.0 h) + iter 10 lrate = 0.0500000000 LL = -3.4503446967 nd = 0.0224833972, D = 0.58114E-01 0.58114E-01 ( 0.10 s, 0.0 h) + iter 11 lrate = 0.0500000000 LL = -3.4494273388 nd = 0.0208405539, D = 0.69043E-01 0.69043E-01 ( 0.10 s, 0.0 h) + iter 12 lrate = 0.0500000000 LL = -3.4486503827 nd = 0.0191031940, D = 0.79272E-01 0.79272E-01 ( 0.10 s, 0.0 h) + iter 13 lrate = 0.0500000000 LL = -3.4480028416 nd = 0.0173800933, D = 0.88429E-01 0.88429E-01 ( 0.10 s, 0.0 h) + iter 14 lrate = 0.0500000000 LL = -3.4474749603 nd = 0.0156968335, D = 0.96270E-01 0.96270E-01 ( 0.10 s, 0.0 h) + iter 15 lrate = 0.0500000000 LL = -3.4470391911 nd = 0.0143341558, D = 0.10260E+00 0.10260E+00 ( 0.10 s, 0.0 h) + iter 16 lrate = 0.0500000000 LL = -3.4466732866 nd = 0.0132110732, D = 0.10754E+00 0.10754E+00 ( 0.10 s, 0.0 h) + iter 17 lrate = 0.0500000000 LL = -3.4463623229 nd = 0.0123285260, D = 0.11120E+00 0.11120E+00 ( 0.10 s, 0.0 h) + iter 18 lrate = 0.0500000000 LL = -3.4460914899 nd = 0.0116493533, D = 0.11371E+00 0.11371E+00 ( 0.10 s, 0.0 h) + iter 19 lrate = 0.0500000000 LL = -3.4458502064 nd = 0.0111482807, D = 0.11524E+00 0.11524E+00 ( 0.10 s, 0.0 h) + iter 20 lrate = 0.0500000000 LL = -3.4456268009 nd = 0.0107728174, D = 0.11596E+00 0.11596E+00 ( 0.10 s, 0.0 h) + iter 21 lrate = 0.0500000000 LL = -3.4454181035 nd = 0.0104894729, D = 0.11604E+00 0.11604E+00 ( 0.10 s, 0.0 h) + iter 22 lrate = 0.0500000000 LL = -3.4452217620 nd = 0.0102683247, D = 0.11562E+00 0.11562E+00 ( 0.10 s, 0.0 h) + iter 23 lrate = 0.0500000000 LL = -3.4450342435 nd = 0.0100942550, D = 0.11481E+00 0.11481E+00 ( 0.10 s, 0.0 h) + iter 24 lrate = 0.0500000000 LL = -3.4448536484 nd = 0.0099474996, D = 0.11371E+00 0.11371E+00 ( 0.10 s, 0.0 h) + iter 25 lrate = 0.0500000000 LL = -3.4446793315 nd = 0.0098193230, D = 0.11240E+00 0.11240E+00 ( 0.10 s, 0.0 h) + iter 26 lrate = 0.0500000000 LL = -3.4445112148 nd = 0.0097106364, D = 0.11093E+00 0.11093E+00 ( 0.10 s, 0.0 h) + iter 27 lrate = 0.0500000000 LL = -3.4443458120 nd = 0.0096101155, D = 0.10935E+00 0.10935E+00 ( 0.10 s, 0.0 h) + iter 28 lrate = 0.0500000000 LL = -3.4441841429 nd = 0.0095174602, D = 0.10770E+00 0.10770E+00 ( 0.10 s, 0.0 h) + iter 29 lrate = 0.0500000000 LL = -3.4440258521 nd = 0.0094318094, D = 0.10601E+00 0.10601E+00 ( 0.10 s, 0.0 h) + iter 30 lrate = 0.0500000000 LL = -3.4438715045 nd = 0.0093537182, D = 0.10429E+00 0.10429E+00 ( 0.10 s, 0.0 h) + iter 31 lrate = 0.0500000000 LL = -3.4437197766 nd = 0.0092762513, D = 0.10256E+00 0.10256E+00 ( 0.10 s, 0.0 h) + iter 32 lrate = 0.0500000000 LL = -3.4435705718 nd = 0.0092096201, D = 0.10084E+00 0.10084E+00 ( 0.10 s, 0.0 h) + iter 33 lrate = 0.0500000000 LL = -3.4434240077 nd = 0.0091470859, D = 0.99122E-01 0.99122E-01 ( 0.10 s, 0.0 h) + iter 34 lrate = 0.0500000000 LL = -3.4432801434 nd = 0.0090904652, D = 0.97425E-01 0.97425E-01 ( 0.10 s, 0.0 h) + iter 35 lrate = 0.0500000000 LL = -3.4431381670 nd = 0.0090375639, D = 0.95754E-01 0.95754E-01 ( 0.10 s, 0.0 h) + iter 36 lrate = 0.0500000000 LL = -3.4429990901 nd = 0.0089824829, D = 0.94111E-01 0.94111E-01 ( 0.10 s, 0.0 h) + iter 37 lrate = 0.0500000000 LL = -3.4428609571 nd = 0.0089354414, D = 0.92496E-01 0.92496E-01 ( 0.10 s, 0.0 h) + iter 38 lrate = 0.0500000000 LL = -3.4427244769 nd = 0.0088906811, D = 0.90910E-01 0.90910E-01 ( 0.10 s, 0.0 h) + iter 39 lrate = 0.0500000000 LL = -3.4425891313 nd = 0.0088492082, D = 0.89359E-01 0.89359E-01 ( 0.10 s, 0.0 h) + iter 40 lrate = 0.0500000000 LL = -3.4424553915 nd = 0.0088070259, D = 0.87837E-01 0.87837E-01 ( 0.10 s, 0.0 h) + iter 41 lrate = 0.0500000000 LL = -3.4423226720 nd = 0.0087682129, D = 0.86349E-01 0.86349E-01 ( 0.10 s, 0.0 h) + iter 42 lrate = 0.0500000000 LL = -3.4421911989 nd = 0.0087309078, D = 0.84898E-01 0.84898E-01 ( 0.10 s, 0.0 h) + iter 43 lrate = 0.0500000000 LL = -3.4420611786 nd = 0.0086945363, D = 0.83480E-01 0.83480E-01 ( 0.10 s, 0.0 h) + iter 44 lrate = 0.0500000000 LL = -3.4419321185 nd = 0.0086603649, D = 0.82095E-01 0.82095E-01 ( 0.10 s, 0.0 h) + iter 45 lrate = 0.0500000000 LL = -3.4418042727 nd = 0.0086283889, D = 0.80742E-01 0.80742E-01 ( 0.10 s, 0.0 h) + iter 46 lrate = 0.0500000000 LL = -3.4416768672 nd = 0.0085974373, D = 0.79422E-01 0.79422E-01 ( 0.10 s, 0.0 h) + iter 47 lrate = 0.0500000000 LL = -3.4415505599 nd = 0.0085658375, D = 0.78132E-01 0.78132E-01 ( 0.10 s, 0.0 h) + iter 48 lrate = 0.0500000000 LL = -3.4414252826 nd = 0.0085355070, D = 0.76874E-01 0.76874E-01 ( 0.10 s, 0.0 h) + iter 49 lrate = 0.0500000000 LL = -3.4413008778 nd = 0.0085045367, D = 0.75648E-01 0.75648E-01 ( 0.10 s, 0.0 h) + iter 50 lrate = 0.0500000000 LL = -3.4411775002 nd = 0.0073499988, D = 0.74448E-01 0.74448E-01 ( 0.10 s, 0.0 h) + iter 51 lrate = 0.1000000000 LL = -3.4409782229 nd = 0.0073279306, D = 0.73141E-01 0.73141E-01 ( 0.10 s, 0.0 h) + iter 52 lrate = 0.2000000000 LL = -3.4405919551 nd = 0.0072717685, D = 0.70617E-01 0.70617E-01 ( 0.10 s, 0.0 h) + iter 53 lrate = 0.3000000000 LL = -3.4400260592 nd = 0.0071975653, D = 0.67051E-01 0.67051E-01 ( 0.10 s, 0.0 h) + iter 54 lrate = 0.4000000000 LL = -3.4392902421 nd = 0.0071025916, D = 0.62693E-01 0.62693E-01 ( 0.10 s, 0.0 h) + iter 55 lrate = 0.5000000000 LL = -3.4383944792 nd = 0.0069838003, D = 0.57833E-01 0.57833E-01 ( 0.10 s, 0.0 h) + iter 56 lrate = 0.6000000000 LL = -3.4373514586 nd = 0.0068564048, D = 0.52779E-01 0.52779E-01 ( 0.10 s, 0.0 h) + iter 57 lrate = 0.7000000000 LL = -3.4361733721 nd = 0.0067176434, D = 0.47812E-01 0.47812E-01 ( 0.11 s, 0.0 h) + iter 58 lrate = 0.8000000000 LL = -3.4348773500 nd = 0.0065306037, D = 0.43152E-01 0.43152E-01 ( 0.10 s, 0.0 h) + iter 59 lrate = 0.9000000000 LL = -3.4334862033 nd = 0.0063144473, D = 0.38858E-01 0.38858E-01 ( 0.10 s, 0.0 h) + iter 60 lrate = 1.0000000000 LL = -3.4320290718 nd = 0.0060694050, D = 0.35007E-01 0.35007E-01 ( 0.10 s, 0.0 h) + iter 61 lrate = 1.0000000000 LL = -3.4306539336 nd = 0.0058165467, D = 0.31848E-01 0.31848E-01 ( 0.11 s, 0.0 h) + iter 62 lrate = 1.0000000000 LL = -3.4293727997 nd = 0.0055406777, D = 0.29288E-01 0.29288E-01 ( 0.10 s, 0.0 h) + iter 63 lrate = 1.0000000000 LL = -3.4281948781 nd = 0.0052692527, D = 0.27250E-01 0.27250E-01 ( 0.10 s, 0.0 h) + iter 64 lrate = 1.0000000000 LL = -3.4271211832 nd = 0.0050272799, D = 0.25745E-01 0.25745E-01 ( 0.10 s, 0.0 h) + iter 65 lrate = 1.0000000000 LL = -3.4261411718 nd = 0.0048073579, D = 0.24812E-01 0.24812E-01 ( 0.10 s, 0.0 h) + iter 66 lrate = 1.0000000000 LL = -3.4252466155 nd = 0.0046179247, D = 0.24345E-01 0.24345E-01 ( 0.10 s, 0.0 h) + iter 67 lrate = 1.0000000000 LL = -3.4244237228 nd = 0.0044367521, D = 0.24187E-01 0.24187E-01 ( 0.10 s, 0.0 h) + iter 68 lrate = 1.0000000000 LL = -3.4236656099 nd = 0.0042766153, D = 0.24212E-01 0.24212E-01 ( 0.10 s, 0.0 h) + iter 69 lrate = 1.0000000000 LL = -3.4229613447 nd = 0.0041429572, D = 0.24322E-01 0.24322E-01 ( 0.10 s, 0.0 h) + iter 70 lrate = 1.0000000000 LL = -3.4223013447 nd = 0.0040280167, D = 0.24478E-01 0.24478E-01 ( 0.10 s, 0.0 h) + iter 71 lrate = 1.0000000000 LL = -3.4216799403 nd = 0.0039220625, D = 0.24654E-01 0.24654E-01 ( 0.10 s, 0.0 h) + iter 72 lrate = 1.0000000000 LL = -3.4210881340 nd = 0.0038434466, D = 0.24854E-01 0.24854E-01 ( 0.10 s, 0.0 h) + iter 73 lrate = 1.0000000000 LL = -3.4205200068 nd = 0.0037706961, D = 0.25099E-01 0.25099E-01 ( 0.10 s, 0.0 h) + iter 74 lrate = 1.0000000000 LL = -3.4199694700 nd = 0.0037100029, D = 0.25387E-01 0.25387E-01 ( 0.10 s, 0.0 h) + iter 75 lrate = 1.0000000000 LL = -3.4194343331 nd = 0.0036479584, D = 0.25686E-01 0.25686E-01 ( 0.10 s, 0.0 h) + iter 76 lrate = 1.0000000000 LL = -3.4189134918 nd = 0.0035920222, D = 0.26006E-01 0.26006E-01 ( 0.10 s, 0.0 h) + iter 77 lrate = 1.0000000000 LL = -3.4184066190 nd = 0.0035339680, D = 0.26356E-01 0.26356E-01 ( 0.10 s, 0.0 h) + iter 78 lrate = 1.0000000000 LL = -3.4179137938 nd = 0.0034747481, D = 0.26715E-01 0.26715E-01 ( 0.10 s, 0.0 h) + iter 79 lrate = 1.0000000000 LL = -3.4174367980 nd = 0.0034084627, D = 0.27097E-01 0.27097E-01 ( 0.10 s, 0.0 h) + iter 80 lrate = 1.0000000000 LL = -3.4169767547 nd = 0.0033321218, D = 0.27486E-01 0.27486E-01 ( 0.10 s, 0.0 h) + iter 81 lrate = 1.0000000000 LL = -3.4165384642 nd = 0.0032441994, D = 0.27901E-01 0.27901E-01 ( 0.11 s, 0.0 h) + iter 82 lrate = 1.0000000000 LL = -3.4161213948 nd = 0.0031523169, D = 0.28348E-01 0.28348E-01 ( 0.10 s, 0.0 h) + iter 83 lrate = 1.0000000000 LL = -3.4157282834 nd = 0.0030511007, D = 0.28801E-01 0.28801E-01 ( 0.10 s, 0.0 h) + iter 84 lrate = 1.0000000000 LL = -3.4153589168 nd = 0.0029613584, D = 0.29281E-01 0.29281E-01 ( 0.10 s, 0.0 h) + iter 85 lrate = 1.0000000000 LL = -3.4150109305 nd = 0.0028842884, D = 0.29777E-01 0.29777E-01 ( 0.10 s, 0.0 h) + iter 86 lrate = 1.0000000000 LL = -3.4146806129 nd = 0.0028133041, D = 0.30284E-01 0.30284E-01 ( 0.10 s, 0.0 h) + iter 87 lrate = 1.0000000000 LL = -3.4143657476 nd = 0.0027405093, D = 0.30773E-01 0.30773E-01 ( 0.10 s, 0.0 h) + iter 88 lrate = 1.0000000000 LL = -3.4140664829 nd = 0.0026759716, D = 0.31255E-01 0.31255E-01 ( 0.10 s, 0.0 h) + iter 89 lrate = 1.0000000000 LL = -3.4137827396 nd = 0.0026172247, D = 0.31754E-01 0.31754E-01 ( 0.10 s, 0.0 h) + iter 90 lrate = 1.0000000000 LL = -3.4135099614 nd = 0.0025723970, D = 0.32216E-01 0.32216E-01 ( 0.10 s, 0.0 h) + iter 91 lrate = 1.0000000000 LL = -3.4132471327 nd = 0.0025251906, D = 0.32652E-01 0.32652E-01 ( 0.10 s, 0.0 h) + iter 92 lrate = 1.0000000000 LL = -3.4129956272 nd = 0.0024806124, D = 0.33075E-01 0.33075E-01 ( 0.10 s, 0.0 h) + iter 93 lrate = 1.0000000000 LL = -3.4127524864 nd = 0.0024369464, D = 0.33513E-01 0.33513E-01 ( 0.10 s, 0.0 h) + iter 94 lrate = 1.0000000000 LL = -3.4125183782 nd = 0.0023791588, D = 0.33946E-01 0.33946E-01 ( 0.10 s, 0.0 h) + iter 95 lrate = 1.0000000000 LL = -3.4122952276 nd = 0.0023308726, D = 0.34370E-01 0.34370E-01 ( 0.10 s, 0.0 h) + iter 96 lrate = 1.0000000000 LL = -3.4120794495 nd = 0.0022935368, D = 0.34803E-01 0.34803E-01 ( 0.10 s, 0.0 h) + iter 97 lrate = 1.0000000000 LL = -3.4118716676 nd = 0.0022663411, D = 0.35211E-01 0.35211E-01 ( 0.10 s, 0.0 h) + iter 98 lrate = 1.0000000000 LL = -3.4116682686 nd = 0.0022430245, D = 0.35585E-01 0.35585E-01 ( 0.10 s, 0.0 h) + iter 99 lrate = 1.0000000000 LL = -3.4114690851 nd = 0.0022236502, D = 0.35953E-01 0.35953E-01 ( 0.10 s, 0.0 h) + iter 100 lrate = 1.0000000000 LL = -3.4112742644 nd = 0.0022054248, D = 0.36296E-01 0.36296E-01 ( 0.10 s, 0.0 h) +... done. Execution time: 0.00 h +output directory = ./fortran_output/ diff --git a/.context/issue-351/raw/harness_run2/fortran_run/input.param b/.context/issue-351/raw/harness_run2/fortran_run/input.param new file mode 100644 index 00000000..c8f97ce2 --- /dev/null +++ b/.context/issue-351/raw/harness_run2/fortran_run/input.param @@ -0,0 +1,82 @@ +files ./eeglab_data.fdt +outdir ./fortran_output/ +block_size 512 +do_opt_block 0 +blk_min 256 +blk_step 256 +blk_max 1024 +num_models 1 +max_threads 1 +use_min_dll 1 +min_dll 1e-09 +use_grad_norm 1 +min_grad_norm 1e-07 +num_mix_comps 3 +pdftype 0 +max_iter 100 +num_samples 1 +data_dim 32 +field_dim 30504 +field_blocksize 1 +do_history 0 +histstep 10 +share_comps 0 +share_start 100 +comp_thresh 0.99 +share_iter 100 +lrate 0.05 +minlrate 1e-08 +mineig 1.000000e-12 +lratefact 0.5 +rholrate 0.05 +rho0 1.5 +minrho 1.0 +maxrho 2.0 +rholratefact 0.5 +kurt_start 3 +num_kurt 5 +kurt_int 1 +do_newton 1 +newt_start 50 +newt_ramp 10 +newtrate 1.0 +do_reject 0 +numrej 3 +rejsig 3.0 +rejstart 2 +rejint 3 +writestep 20 +write_nd 0 +write_LLt 1 +decwindow 1 +max_decs 3 +fix_init 0 +update_A 1 +update_c 1 +update_gm 1 +update_alpha 1 +update_mu 1 +update_beta 1 +invsigmax 100.0 +invsigmin 0.0 +do_rho 1 +load_rej 0 +load_W 0 +load_c 0 +load_gm 0 +load_alpha 0 +load_mu 0 +load_beta 0 +load_rho 0 +load_comp_list 0 +do_mean 1 +do_sphere 1 +doPCA 1 +pcakeep 32 +pcadb 30.0 +byte_size 4 +doscaling 1 +scalestep 1 +num_comps 32 +do_approx_sphere 1 +seed 42 diff --git a/.context/issue-351/raw/harness_run2/parity_summary.md b/.context/issue-351/raw/harness_run2/parity_summary.md new file mode 100644 index 00000000..2681d7c9 --- /dev/null +++ b/.context/issue-351/raw/harness_run2/parity_summary.md @@ -0,0 +1,6 @@ +| backend | precision | iterations | final LL | abs. LL difference | mean matched corr | min matched corr | Amari distance | runtime (s) | +|---|---|---:|---:|---:|---:|---:|---:|---:| +| Fortran (reference) | float64 | 100 | -3.411274 | | | | | 10.2 | +| PyTorch | float64 | 100 | -3.411003 | 0.000271 | 0.9991 | 0.9918 | 0.0038 | 18.1 | +| NumPy | float64 | 100 | -3.411003 | 0.000272 | 0.9991 | 0.9917 | 0.0038 | 32.5 | +| MLX | float32 | 100 | -3.410998 | 0.000276 | 0.9991 | 0.9917 | 0.0038 | 3.0 | diff --git a/.context/issue-351/raw/harness_run2/stdout.log b/.context/issue-351/raw/harness_run2/stdout.log new file mode 100644 index 00000000..4f3d8534 --- /dev/null +++ b/.context/issue-351/raw/harness_run2/stdout.log @@ -0,0 +1,240 @@ +AMICA uses float64 for Fortran parity; MPS lacks float64 support, so falling back to CPU. Pass dtype=torch.float32 with device='mps' to run on MPS. +Validation with seed=42, max_iter=100 +Output directory: .context/issue-351/raw/harness_run2 +Loaded data: (32, 30504) +Running Fortran AMICA... +Running PyTorch AMICA (natural-gradient EM backend)... +WARNING: params.json settings with no AMICATorchNG equivalent are ignored (NG uses its own behavior): ['do_history', 'histstep', 'writestep']. The Fortran run may honor them, so a parity comparison can differ. +Fitting AMICA with 32 channels, 30504 samples +Models: 1, Mixture components: 3 +AMICA uses float64 for Fortran parity; MPS lacks float64 support, so falling back to CPU. Pass dtype=torch.float32 with device='mps' to run on MPS. + +Starting AMICA fitting... +Starting optimization... + +====================================================================== +VALIDATION REPORT: Fortran vs PyTorch AMICA +====================================================================== + +1. LOG-LIKELIHOOD COMPARISON: +---------------------------------------- + Fortran Final LL: -3.4112742644 + PyTorch Final LL: -3.411003 + LL Ratio (PyTorch/Fortran): 0.9999 + LL Absolute Difference: 0.000271 + +2. CONVERGENCE COMPARISON: +---------------------------------------- + Fortran Iterations: 100 + PyTorch Iterations: 100 + +3. COMPONENT CORRELATION: +---------------------------------------- + Mean Correlation: 0.9991 + Min Correlation: 0.9918 + Max Correlation: 1.0000 + Std Correlation: 0.0017 + ✓ Components are highly correlated (>0.9) + + Mixing Matrix Relative Error: 0.0432 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== + +Report saved to: .context/issue-351/raw/harness_run2/validation_report.txt +Running NumPy AMICA (legacy reference backend)... + +AMICA: 0%| | 0/100 [00:000.9) + + Mixing Matrix Relative Error: 0.0432 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== + +Report saved to: .context/issue-351/raw/harness_run2/validation_report_numpy.txt +Running MLX AMICA (Apple-GPU float32 backend)... +WARNING: params.json settings with no AMICAMLXNG equivalent are ignored (MLX uses its own behavior): ['do_history', 'histstep', 'writestep']. The Fortran run may honor them, so a parity comparison can differ. +Fitting AMICA with 32 channels, 30504 samples +Models: 1, Mixture components: 3 + + +====================================================================== +VALIDATION REPORT: Fortran vs MLX AMICA +====================================================================== + +1. LOG-LIKELIHOOD COMPARISON: +---------------------------------------- + Fortran Final LL: -3.4112742644 + MLX Final LL: -3.410998 + LL Ratio (MLX/Fortran): 0.9999 + LL Absolute Difference: 0.000276 + +2. CONVERGENCE COMPARISON: +---------------------------------------- + Fortran Iterations: 100 + MLX Iterations: 100 + +3. COMPONENT CORRELATION: +---------------------------------------- + Mean Correlation: 0.9991 + Min Correlation: 0.9917 + Max Correlation: 1.0000 + Std Correlation: 0.0017 + ✓ Components are highly correlated (>0.9) + + Mixing Matrix Relative Error: 0.0433 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== + +Report saved to: .context/issue-351/raw/harness_run2/validation_report_mlx.txt + +PARITY SUMMARY (seed=42, max_iter=100): +| backend | precision | iterations | final LL | abs. LL difference | mean matched corr | min matched corr | Amari distance | runtime (s) | +|---|---|---:|---:|---:|---:|---:|---:|---:| +| Fortran (reference) | float64 | 100 | -3.411274 | | | | | 10.2 | +| PyTorch | float64 | 100 | -3.411003 | 0.000271 | 0.9991 | 0.9918 | 0.0038 | 18.1 | +| NumPy | float64 | 100 | -3.411003 | 0.000272 | 0.9991 | 0.9917 | 0.0038 | 32.5 | +| MLX | float32 | 100 | -3.410998 | 0.000276 | 0.9991 | 0.9917 | 0.0038 | 3.0 | + +Summary saved to: .context/issue-351/raw/harness_run2/parity_summary.md +uv run python validate_implementations.py --backend all --fortran-binary $R 68.21s user 30.74s system 152% cpu 1:05.08 total diff --git a/.context/issue-351/raw/harness_run2/validation_report.txt b/.context/issue-351/raw/harness_run2/validation_report.txt new file mode 100644 index 00000000..30026a13 --- /dev/null +++ b/.context/issue-351/raw/harness_run2/validation_report.txt @@ -0,0 +1,33 @@ + +====================================================================== +VALIDATION REPORT: Fortran vs PyTorch AMICA +====================================================================== + +1. LOG-LIKELIHOOD COMPARISON: +---------------------------------------- + Fortran Final LL: -3.4112742644 + PyTorch Final LL: -3.411003 + LL Ratio (PyTorch/Fortran): 0.9999 + LL Absolute Difference: 0.000271 + +2. CONVERGENCE COMPARISON: +---------------------------------------- + Fortran Iterations: 100 + PyTorch Iterations: 100 + +3. COMPONENT CORRELATION: +---------------------------------------- + Mean Correlation: 0.9991 + Min Correlation: 0.9918 + Max Correlation: 1.0000 + Std Correlation: 0.0017 + ✓ Components are highly correlated (>0.9) + + Mixing Matrix Relative Error: 0.0432 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== diff --git a/.context/issue-351/raw/harness_run2/validation_report_mlx.txt b/.context/issue-351/raw/harness_run2/validation_report_mlx.txt new file mode 100644 index 00000000..4f464504 --- /dev/null +++ b/.context/issue-351/raw/harness_run2/validation_report_mlx.txt @@ -0,0 +1,33 @@ + +====================================================================== +VALIDATION REPORT: Fortran vs MLX AMICA +====================================================================== + +1. LOG-LIKELIHOOD COMPARISON: +---------------------------------------- + Fortran Final LL: -3.4112742644 + MLX Final LL: -3.410998 + LL Ratio (MLX/Fortran): 0.9999 + LL Absolute Difference: 0.000276 + +2. CONVERGENCE COMPARISON: +---------------------------------------- + Fortran Iterations: 100 + MLX Iterations: 100 + +3. COMPONENT CORRELATION: +---------------------------------------- + Mean Correlation: 0.9991 + Min Correlation: 0.9917 + Max Correlation: 1.0000 + Std Correlation: 0.0017 + ✓ Components are highly correlated (>0.9) + + Mixing Matrix Relative Error: 0.0433 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== diff --git a/.context/issue-351/raw/harness_run2/validation_report_numpy.txt b/.context/issue-351/raw/harness_run2/validation_report_numpy.txt new file mode 100644 index 00000000..f6d394c7 --- /dev/null +++ b/.context/issue-351/raw/harness_run2/validation_report_numpy.txt @@ -0,0 +1,33 @@ + +====================================================================== +VALIDATION REPORT: Fortran vs NumPy AMICA +====================================================================== + +1. LOG-LIKELIHOOD COMPARISON: +---------------------------------------- + Fortran Final LL: -3.4112742644 + NumPy Final LL: -3.411003 + LL Ratio (NumPy/Fortran): 0.9999 + LL Absolute Difference: 0.000272 + +2. CONVERGENCE COMPARISON: +---------------------------------------- + Fortran Iterations: 100 + NumPy Iterations: 100 + +3. COMPONENT CORRELATION: +---------------------------------------- + Mean Correlation: 0.9991 + Min Correlation: 0.9917 + Max Correlation: 1.0000 + Std Correlation: 0.0017 + ✓ Components are highly correlated (>0.9) + + Mixing Matrix Relative Error: 0.0432 + ✓ Mixing matrices are very similar (<10% error) + +4. OVERALL ASSESSMENT: +---------------------------------------- + ✓ Implementations produce comparable results + +====================================================================== diff --git a/.context/issue-351/raw/keep_best/ensemble.json b/.context/issue-351/raw/keep_best/ensemble.json new file mode 100644 index 00000000..bd635eab --- /dev/null +++ b/.context/issue-351/raw/keep_best/ensemble.json @@ -0,0 +1,71 @@ +{ + "n": 20, + "keep_best_tol": 1e-09, + "budgets": { + "100": { + "fortran_mean": -3.35503721537913, + "fortran_sd": 0.002902508714644331, + "return_last_mean": -3.3542282783344946, + "return_last_sd": 0.002895896178028667, + "keep_best_mean": -3.3542282783344946, + "keep_best_sd": 0.002895896178028667, + "sd_ratio_return_last": 0.9977217857840354, + "sd_ratio_keep_best": 0.9977217857840354, + "gap_return_last": 0.0008089370446353605, + "gap_keep_best": 0.0008089370446353605, + "ks_p_keep_best": 0.8319696107963263, + "ks_p_return_last": 0.8319696107963263, + "tost_p_keep_best": 7.482078444204207e-23, + "n_restored": 0, + "restored_seeds": [], + "max_restore_gain": 0.0 + }, + "200": { + "fortran_mean": -3.3418448453873153, + "fortran_sd": 0.002960879790217216, + "return_last_mean": -3.3416384843303377, + "return_last_sd": 0.002220142292228306, + "keep_best_mean": -3.3416384843303377, + "keep_best_sd": 0.002220142292228306, + "sd_ratio_return_last": 0.74982520383424, + "sd_ratio_keep_best": 0.74982520383424, + "gap_return_last": 0.00020636105697757756, + "gap_keep_best": 0.00020636105697757756, + "ks_p_keep_best": 0.571336004933722, + "ks_p_return_last": 0.571336004933722, + "tost_p_keep_best": 4.382251121317329e-31, + "n_restored": 0, + "restored_seeds": [], + "max_restore_gain": 0.0 + }, + "300": { + "fortran_mean": -3.3392473308579143, + "fortran_sd": 0.0026565681804202193, + "return_last_mean": -3.3391461730474417, + "return_last_sd": 0.001984933047167868, + "keep_best_mean": -3.339145964447732, + "keep_best_sd": 0.0019848062301341036, + "sd_ratio_return_last": 0.7471794105634016, + "sd_ratio_keep_best": 0.7471316733983256, + "gap_return_last": 0.00010115781047259631, + "gap_keep_best": 0.00010136641018210213, + "ks_p_keep_best": 0.9831368772656193, + "ks_p_return_last": 0.9831368772656193, + "tost_p_keep_best": 5.280723043692209e-39, + "n_restored": 1, + "restored_seeds": [ + 3 + ], + "max_restore_gain": 4.1719941843432196e-06 + } + }, + "keep_best_gap_to_fortran_at_100": { + "100": 0.0008089370446353605, + "200": 0.01339873104879219, + "300": 0.01589125093139776 + }, + "ng_stop_reasons": [ + "max_iter" + ], + "ng_newton_fallbacks_total": 0 +} \ No newline at end of file diff --git a/.context/issue-351/raw/keep_best/ensemble.npz b/.context/issue-351/raw/keep_best/ensemble.npz new file mode 100644 index 0000000000000000000000000000000000000000..606368333f41b2e5b542edbe980a2c135a40ccb3 GIT binary patch literal 98750 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 #HPps~JFUADrQ-|KS=q;gE zInCG6dl=vjv+f4od@(HQe$>`jS`K>q(jB>1$_?8(54wA_Z^QI8S8^DpR*=wSa&2U3 zH)ye%qs`J`fl}m#FA-+K|32A*IOBQr_q4>Ew_F?YW0X#?XrjX0`<$aq zyR%Vwghso^g90GQc;L1ZZx3Qjk$K^+&JP{`lr9)umV{HKX+h;4WRLnnR_~puI5uNc zum~dgfSVKXU5#wQaPnEjiwe^3R{Ppbv!C!;zC>nn-6Q_fS$UOR4_LFosBg(`H?>$0 z3_F(Zo|MLSj+EFO;%CDq*UGvVcve8FTEj}r;$Lw0Y8Iyy@q5Y#oR_S5O2PkK@BU{P zSr3dd&+`~CaKZYG7}~T`G?42o&-!)3^=M5n=h(qINPN|#AaRlrA4|6hxopb>PwxD* z_?DXuHa_QVNs(UzF?Ds-Lwaj~WUziaGEI>L*QvwEtyGJyV{H^x( z$o)@iH__EC>cz!tbkIRq^w-&B3Jjb45^85cdd*MXhPyG7dE4RNli$}kVQ5Z?0(C+v zNUZv`?=QayjHEQnO1;sBZ&ck5J6+w58M9vYDiFTq%6Dgm+E=>ukmMRJG_pMp=7Cv<4EF0e zh{hrfK7|Kz!=th3N-PlrNNeH8@@+2SpHKU(yeTJuB_n7bkDcfTK8hFrlPa%8uM$@- zS>2{6 zk7*F{BgsC1a7L12W5!40xbduR$P3P8MQ9bBG~mch1@Fs0kg4BE4}-t7EnG>Wg{|u% zsQ=X}s&x03GYyi1`4?m6=)w@XGCbql<}`=ElN$?vi9bG-E>nT1MK=+Rx@Vvgp2&3(9kDhj6#KXghA8%|4V=?JrW0vAIM13zymWIqh z-es0g&6(0+9z8o#sK|tU(&`6Tk21rf%zyIZE4d(N&foq!{L1j@MVZ1f_a4Oh!`Y(L zn+mRuF#g_(<-;%SPnT@}wg~1bRhyKRDER*0wNJm*=LlbUEV%w|DOgFUmF<7h3BuH6 zb38-`5$n7Wg_@fW3p47TVveT3IF7&*C&l<--}dU?^JG7i{Z*u{n~M!PD`Y)VyUzzb zBBl8bQxBpS=l6c<8Kx;;@}s5S0ON+$63K#mus7%9f&A1ybewr5&Hp7ges6r)j{Z4D?}F@e z^9b)(hT6_G!*(C;oY?+-=J_5dF=x?s^$9QLHBoi*nHo1lrFrmGJ7LRXamO&w1$?p}jz5qy)K#Gv-^L*wHf$#_F|sYvz|h=abOH>$aZ(kKj??^Ep$1(e-dS zkYa^LW}O}g{YQ^=51%?V)HjCo;%;1hNxO*j3QPM{CCdOiP5#|;vLrWqv7_^X&IVY^ zw@Ut^$pASR^e1Q*&+E%_g0L_5;{!&A0VKZvN0RlO6>#d9?cvko zEod;DirwHH@nfh`j_(~BLC$HG(|-u3+}rUqB~yh29+$u8!{3d*Ix?(EnQ&odHZ!UO1~C}ZADj?TOZ+l=mz-8}IB~A#Wfv4Vg~GaP z7@v=?f(g}WjsO#C=*P{d)2O-xT%82WpY+hf!_sF))Gkkga4uelFm@hj#Me5fNB9$3 zt>11wn(GCT=Z^Jc%5H)8$LGAYG2z6CSg!{}i^43%d+gfE>*$v$wQ4{d^kz5dTN*#?S{$ zOrPv`*ee20qox7h2fTQ@U%JuQ5Hk$guQPHzk>rIVP>Hqx7fcYlzWuP|Bru8}31KkK z0#BDIFR4jx{e->!WmnTdG~kz&U6a#?%DBvo(`rc$i;m&zJ;w#)|2h9$=n@|UFAj4U zWU;}F3WG~b-e&a+E;UZH7=eZrH#^!UUzKKeZ@83cw zVj^CJox*48q61b@cT$;-5@j0HWZ%0ZYbFf?Mr!udX7k}NN^@T1;~A8@?56lDZXBGQ z&FFCqq{Ua8Pjo!|M1h%uW;yFd0#K2i+slJz3aIe4zgi+(rmvD-s!@i^KtnuQK7sUF z&R+1b7V0B=GO7NTH-B$}C$$ov_K+Nf(IfN3H)G6rR9A*iXIC4ddR^Qry0;WaP^#5@ z@ig!kcy~X;T^K)K7GiM9Sx3X&VH~B-&wvLCZ%|>UfmgGJEMrNZD!%F5nUIoMR5Ypi ztqQDyo5Q_Ik6F23h>~aJwK6&!w=BM)sHgx#t+Ni?p{B zXRlI{WF=UHA|`%9#X!+-r1sFjBG{9*WB16@Ce#}eV6sb65QCbQ?vsEDTD0um&snm9 zs7fbAn#prsVe@J7dWebgR`NMNjV8<7OqE;q&;$AI{yxTldYOU5t^q&Df z;vw`u;3A-&=WL6AI|V3Q3`Yzs=x`eMA-+~oA?&s<$Frkh1ZC_#y_VX@1a;H@=wTZ;LbJdqMiaKYzDb3&0|< zE!pHuJKEspt&V0Agr_rbE{u zmm24$Z4V0(;lLFAq6 znM{rWB9x<9@Ecct(? zI{13-)h0^dGZ;K-M&?h4c+EeQGr+d6yQar2mH|tYZeYFS6c~N+gU7CW5%e8;!zjVd ziEn;CZ`tQejpsQ$%o7MFHMb#5@T?M5v{}gf6`;wbWjx?~4*_S_qc?+eQXj(q^CfuC| z4i=xp#qlm3Z`T_~Xkgdvo-u8mEky0x>suo^gqj*Sv!=-XBkaZBOZplcXvQ~rfGuJW z^$Yk&S6DK^Ki(mpl`qH8@$qvOCG#6dT|BsjzHAOj_{aM=lDS3L(`%Zydh@`>e`bG-$Jy2$r}UN<|A%*`G67PZ|lFO*MipxeHS2VZ8p(@sU` z+vsR5>?sTlHCoQATXSNcf-hHv$o%O(i?fY=(iSQSsTZMXT0ts9YyMNs;S%-Z>OflZv&n0OFL4a-q%+$Opg{2LztR{lGg?peeaV|Bq9c{Sado|Z9N6g zN7`;jsMFv@EyEjcj*y2lmbmbYhX5!;JZ+%xio(UTKx5UDjMu+_h z`Sn=!*N{Dr+ab-?aj@lIlVh**|#jfocEN(ud6O;G^qy zTWX6ywL$$?@c17j)2g8DGC}Uu3|Fs)KKlixMYNi^$+>g1M@la2D=!}S>wo6kMOkRP z%}ng+J}THL8sR1t%85(Xx~2R?nc=NVFETie&4G7bZ3enR>u8GO#`eb^1E9pCEOuO( zf+O#y?7vq^4O#KE>%T5@VNSZwpO3I|Vnx2$X-_SFXk+5U8Ykb03cd)PFa5a)PEcK- zRHrPX_#|y++c6fLIcVU>Ju-*x{_9en3ZaAjKMELVNl)$dX4vy$?vB~q;WSHOI0+Cet`KEg9lx7bNOe_Hk#u{9Ak zNMrv;ShRs2Z=Pa4cTbuM+xbQ{NIabZlX0&OiuX^0ecW{F++l7&xSRFRI}_rMut=082)K`$j$*{RhJ6?E)VPcWTbrem6+p@?Wr-#y;cbLp2X8`Ku9&v5u!@CQF z+Iq?JLx8?X_Nnm@VwPX|DoFM+kES;t3hrA*{WEVmY?KJ!hS9`c(}^1)+-y z5Bv^3ii{i>0?D$nN91=?@S*5R!=IX;k%7>gJHsBt`&W5w(sA25Qf5<|Z=m{!)S|)< z6w_3IJ8lKGwvxX9uc-d%2-5%DOjj`|nC%3aHEFLe8ZRL6vjIao|CsPQNlTM2uI2^rxZ?Yl=H750Xg+^!4 z#dALoUleZxj|B8`M0_WJLB#P+9gRtl{`|>zOJ33oEqRvV>`ILfG`q}56H~2V;E9Z} zLMrmUDgS=(BH?bHyWk*0^7fDBYKI32zmG3optX&S3%_4;F1!^E@k zU~4H&?;kL6mO}zfvq0u+R%YiBdg%W54)3+E%V4XlIYlJsH(EWOnq}-j4{yyaDOLsx z;H;|rG~L1#u&_^!>&*NTnm2E|^zn5E^1566X@z?LTn&11orgtTF8l*NCKbDyCPopaUHx5{T>$73 z8dnn!(Be>WTIL+N1;iYx#pE?K11KV=7rsf7e!<5yC+q2XU|chLtazCQ=RbWb=UL1G z4KGdvr>Qbv_k-#tJ(l8-!&)i&UgRn`@>K5MW#X|-PHVi#BSnozB!-+m3J;<$HlCm7 zA1(TqeMq$IXVm&mgMez~k;$n?c}7!%s@dqlh^z zLd4LO^bs6fZn=dmpyvBcWgU9-aI}Ovf1H^Kj-={)Q%G;^>qHGBHmAnUe-!dJnn+$f zc%AlAp)}P09z7i~F^fj%f+i~WutM(z>2PLC;<0tAe<$m@OuWO*4q{yXVEcbAy9~NF zP{M%NQz3GWnMTtpM{&_&=f0j@8*v?g>5Jz0m&5_Io_aZ+roRtJs}<)SkXk{u`$D4{ z9k`?<&C(~)m}*i&6@|Uk~s-~ zNco+K@DcQ`($oe&z<~O@k7>9!4b07yeSKY<8nQLbT9_$O!QLL-susyQB;R?Valp9; z?S52tZ<{we_7B|_nRIjz>CA^mDAabN>z1rW13rWsNqfPW_h&Dvt-r`ZMa_f1tjUZXk z@5i0*Rp2^|~UKnPxWy+HVJ$D*2s zHxqxLHn*3X5@b(87v;3|^y*J?YPcmG-aikp*}rUSE&ifXS6EryU(Z|{DYpZDBu6eHMpI)?`<;P)bNg5pAsIAS)(1)Z? zR{dSOzl@qHpv@^MZbs!rl0+LLOVI{vX4*;M2_(4QUyZ#gXw*z{C@07Vnq6>^_?>-0yfeOCwK zc%6TKip*&WRati*sQwA|u_Sr#bX-9vj>f;L;iJND&x^9lgcHxw2OGn9u}1LazfxLu zl3zc?6Iim#bsexOxx4gUrzX69$AKz&CJct^Ms@Q3fE1mh=UR!kXOeTZMD1|{c(U~6 zsbR_xit`9Hw_IKUOuXD4$J9tJ{mP5GsY^q^ULfzFP$3;;7?<4MOmfSg{JKkmwdgSL zEbp+onvZN1ZoP{!Vt^&7FWYzTX24$c2>sak5UssuS~+Dkj&|q-+%Z-VhXsPVk6%<# z!vX6c=9-;V;EUYO8@El_uveg>-iPlCVAp%Wr-#q~Ly3dJxgP>%K>f1(i&~QV*2g9B zN9vowQ$L~RfXWqc=jWUAg2(B|Ui-fg*Ahm&;!+-~DOiu-HUaKxG8c*-c;dT%V+F0f z?n#t+vkpwh&Ubnp8$+pcv^BP_Bf#{$vHE|)oygr7s9ZT(4?fKWTg|#PpfcGG?&G&s z(OusoUy>OKKWAX9;~HE8$pYD@`4(n?anY6NRhvGN)9lQXEFrlR7H|D8+zfbt$!Ixf zhz{S9o#5kXT1NjgSRPVR7C~Hk&=X-6dbpwA{qO5M$s=aqeV@ra=S)HF-xlH}`7Y`2 z7sNgY61?)a%z`L5eB%7y@@l`vo5g$bE6`Jw7F%Z*v^v6LK8XTURs3sSq1*J6i_HWy^3f^5wNeT7f zgRL}|y$5fu1H-uwWqVY&LHv8YRsWd|L_RDaJyimJV|Q|+B#T15lX)+forE!+ws`WB zDB?e%zwr(g@#8lp+o`ljzTh+$gWGZ_GdwGFEs04*7&?mY+4G+4XEyny4&-@=!$frB zQFu{1D0w>Z%0rSGn@WFs)^05d=^Ustewj+ZeYZvaaBHyRBlW)a=j4}BF+TKrKl%QQ zMmruK-%y7AO}o70-x06Cqc+tzy+&kJQ>Pf9C<(jMynzk5Z~hg|TX-435AR!4y}A2E z3LaJq{E;*|f)YH>Jbk;D8BRAFZYgw0!1wYhWw$c6;pI&hE1j1eD1UDly(4)~JD$aR znX<-<{f?e(S|F{IQnL?SvG-Z=c;Zr{1j%jhKCk#UJVya~eZBqicQzL+;(Qkt`b8Al zXfr&h_TEC3KFuQa>OA!X-XT4-d;i57w#ab674vNA zB$D5aJ4ss+b!HAFMoitFVPJ-oLpwR$qPVdhRewDl_dL4H$nj{7a42ImwfIGeNBeP+ zQ@Up~E560`V&CK;R_rSN$GV<*8x%9wV?9s2tRHn0a!;_$pb*}xZ}Xn;;aU6bp9=R= z!H4$Ko@cp+f!MmoU0qFa$dsqFV|B9^S^doNs<|T$jkc|n9hv9DhB&NYp`Q`gnQr@@ zUm$@q+_kFLh#z5BtNClj9lgkqGX4<^t^)x91A{c;-;4PD;O{Ue7c`Asv|k*g!=ri8 zw_+aWp!@pg`7iF&hD-tXUkoFsWjr~`du6VLTZ+Q-@NEt3z zrMmd3V80mjXhV7F+G}6BQ!)+iL(FpCP^NOW!ct${spw#8GgZxt$3=FsmHnBzx#Q+Yn7t zdh-6H(oT6BPB`bP(-|tf6zIO-JA7+%i~JpWmnPb@;8c!{(++Vy=-Y5=w@^R_lKpyK zQSc!R6s9cwjq_p0-T&NvFfU9a37Kt|C4)JjqXM6&T6r7lNB8(F&T_(0;Y}mn#lq93^+*?C$bkD3!71>yUR6a2$Q^vJyl5fN>n{yDxF!$F9nCcRV?<8QWCC z?*b`kHyYQre=k4u3hX&Vr9uZUpNebJ{Kbh&Ymc6jC;h5~sc7C?X_II%{B06l*b3OZ z$54OrH#g3F88&D~daax4g69niYd|BC$vw2lgT=fq*i%2^#Gm6fFR6SU0l&}Ksb<)% zqoW?JB#0{tk)eJ?@Ovtp%G-1qjc`;!q=fr3!0JODfX)Hm7hDa;s6Gg!hesO0)vN)uO;~zh$(v@PsyV zj0q!(I z-@iqbM{4U4(A$H02A+@$ zkEH&QpXL?7eM!Pp(({YR)bn{(lw3awR_nRQp3MQb_x&_cYaaoUCK{YlbL(g{x%PC4 zH66^U8rR9rDUeEc#8qbh5SY2NzgJm`2g>z* zba}!=_R!YmzRfzMPpW)-t1glTZnOHp9AiTEiXIFqOwz2-*h^IVvSSIFnJRpzPWIxZ z`TRGjPPYL&cZp%Uzl6^anBB3>PXk`sS`>a&Ab?|i*RDM9Wyh?g3(5gbt6=(On5yrf zAZ$2$a@xjf5vc|JkuiA82&baD89WzgVeDLIAm30EI^1%8+KqVEU(nq|U(ETSMC>Pp z--OqfAN6zIT#608QU0BAU2hNE`W|*k{=g(Ud93)rfecox5;CNwlf8w;j&Wt(Chxb$ z^m9vLG%XyHX|l+M{E)R;;3i`=@usZygmBYL5uw z0z=!a!rq_Ah(p}%?pH>5m|~v9-NA@Ef7UMVE*b?7&Dakl^KGEODwgMUezf?C&!NsM zMp*!RrImd!XTUyhm*v=|IkB4I-K(;M?;7POKzGHO0lLXoo4W9b<6oAiTXPq=;T^{c z>$~5%pr0yjLOJ0&?HRu!<#AyLWJ$BxlCP!0)k*JCf0FmayxUjm@`5RNUqUQ>{HI=| z9vtpzX2FDwst!bd(jfkE-9EOI&_$qgZA{xvp6u(DO_XHb(7>>%fcF;#S+FbE+WRbq z4R^7>ePg)uFZh0iHfy|84t_YJ#YJ!52CjC=N#qADp~F@#pPRpR1EuMMBYNHR_^HA4 zPw$%>AoxJX^^;%`wN5kCQq;uZlMD`_ZFB;VLJRm63I|B=BMJlTX=LdzA$(W=?+sf#&j-Q8n*%cr zB^vNFHtJQyEYNNFj>?IhL#R1X6_w16fdnSq4NA)l_*OD;DR5*9*#Ejo_hOy_+YWZU znwzD?e1lARk~P(6J&jNMTSh86&dG1MUO@-_kFi>h3=l4P${J77$~tNkv7=XsWx{=* z3N?Dvexe`#s{tu?)NnYC0p0JWfgM}{%qO@O5%5Uyo64U9R!4-mEq`nvojcJVJgS(m zWZL?h^foTYc=^s7ryIMmRs32ER~jSSp<{eQw0i*vckY{_i{ONQ{4M&^yQyJ_rqQXeC@)<%8{pSED??4TEcA{xpZ4G$F0kf!$vI z6j)f^vMNIS;EOO#dTwwDrS1KDW0Ul~V9Y)ZP3c8o{L_2S{meYT$7{vMTT6piHj4aM z4Y%X{Uf!Y1V=^#`JLdXobse?nOJGkKE%G%b!e_U|jt)T4L=y?>9qKa!?F~FonVXKd% zS03pRX_=c7iG=GT)PsGAM?*z8sL-(sIo9w>zG&x$Wj&|(&iHcS)PzqvmaO<7%{~9G zVKG~%Q0UebH7!1PsZIDdmm4+A3NNy4Nt!`61`eUd;d8+JTe8Nr#Z~b2T&FbWTWPH4 zX`=W%krp4M7l_-r!UDynVR*+I37jHUBDL!XJFfoxYumc8Ag(nrEm_>L00s^ybu8j; zbd93$tl}2~EKdngn$;Tsk8C4{0(x1o&F1`$GlVA)ajbgD=$|b9nB~L%mfVWvmK7i+L1GWD4K?PLSb7-!a)t0E zJMydR+tG_Z-#Ay!vcdj@?2W@uc0&GQmtVeLc;L-vVpNBo@<5UAVmIQ1DEL9xDVdQA zy~xa|B{Sgy*~@KSeyOd=j8%-tg8X?VX-%SbeEAK_o-I;*cBvE zCALuG+mD=mGj~)yCw%rRQR|QB_@D->m18f3^cNM1-GBU|!rwzw4maT?#GDZIMlYEQ z-``-57uF;DwAbg?6%{MN*;j{E{(u%#v~f+`^}Y~(HPXG~-yJS^+dGZZtza3=xH{iC z)bkhp_m0|5te2c;F7+p} z+Z;dsdj^b90x9D@y!d=s(yir}GEl0nY#`f-7t5JdijJKsM6gf8M?j1dYBP6?$G%?z zU$Q6pUyylJvxoXyPU7dfyiM13_hoii zDr-pc4kHRXxpGrCfIxW4OBhrORHXgCOx^p13W_Almx+JM^M1R!5$Q{t2p?9gDqjMb zsre_S9Y_zTQ0!B2jv}1CH*IsNY6|)2`&6|@ap2Kof2XB#{-V|Oga2ZhZh}{9HeGR1 zP3YRYeZ8|OG?>|apZk1uDR};z@m0`?Q4r9Wi`BKr?=$6O%`8B{+7=P3ZiON+jIRN# z`)$KX0pr5e-&Vl|$Z8m%UI9YR20jmKAotr+*(F zp*Fo0dL<@u&#n2soV$Sd9>&OS%NYd3v$|i7h6C;sc8|Fr#{pd=2Fo_(dEsTbJ5d*; z{E=V2QMOiSGuR6}9ar8FAIsx;J>5)8Y<&9AU2-7E?z^b(aMT#nyq@479u$Q~}{ zf=_)Q6W%+oU+cEIgd8-tzYd>S0gNYwOzuKv*eDhzcvzhQRt+Cl>EvL*)7?g+kvYFX z-X0boUl}UA&7k^97r75~`VA>Bo)W|ocEb+?6}cd{H%pY40|l=!b>0scAl~eafy<4S zbkNT6r(5<*T0A#A$uQP42fl5BRmVbZ+{k+-^x20G=%RJ|gK0h%X#U8mfj;9Om?;#G z<_O&cOL-Zb{vHFU(?i_V&V=M;G*9Y?}E_}Aid?{C))W*;7<9u zZcm08RC~QuB6~OwnUrh!hZBBVN0$2EUM4nNZ;fMLwzI*JLHk>Onz-=svZB6e(xZ%( z+z9L;y!rve3I3~PAWwuZK>5=+s9@DKyQxzH zW@DUZc!wGBVWEZfdt8~w({9F<#3cG0HXT1QUq8`<_Mqv*^%8WHB?X*6YkUG(x~;Naa)%*OTO;0U*;qL(iQ?o;$_ z7s5x;=aYlANqG$LKZ=RKBa(MieeRukr=}h3{g4`{%smA%syM6JVg}KtTZV=8B~|FH zukrON4?g_vhQFKm_%hlPw^v!ZY8t(Cd|O~#(~tV+{+S>8t_#(g2IX&bYT`hy<@_om zCTNz=aN?o$4|MX-_%p+HPH0*DUIX0a!VwQGr8Tdx!nSiisVY8lLVlYlyMtf}S%mFzg zdn?7}MWD>BsHkrB1vPgycIkR{g7Wt#4jv>wFXL^&<0!U_ZiI$zmoFyV_8-gLJO6G0 z?}$XccCs(idv`f-^vOS9tL?)u#>ary3R{YnM}8tOvXsDlwi8@9bLmR$>^l0rvOSNU zaQ<)S_FOo|I|j15=gz&`!;E>#`lY3m>7lpaCxtydgXsRC&XUUWU!dc+-c>m!YFJHK zzRZ1g8$N2R@?7ZGIGQ$I{1b-+e9Ek+~3D>W5IA)OUNEVsv@((d*+GDLf0jALM_L9$H2}R6_LBNZ)~WfA;UugmH9S)$sRO z0deTxFa4GBY83=It3lN!;%8~0J*wijj-LDES!}2iPv+w*_Zrz}fvMSyF%x-CQU1tU zq;oT3h4Jz&2hwA0Hf>pYd3zS!0~ReOKb9bB)n>`>XR6Rlr%{+m&OA_4{pNDvA`O21 z>TgH>dnV|8@`DlQAtp$%`gu^lSqN*4(I?8(tsxFAch3Gr3hua+p4#_%1dP`@ihoU7 zLsZAulK%Bm!>L1`8O!V^f%=0W`(MdSP=n)soB8KOun=>p@tkop_;CGhY@)3c}i4S`@el~o$2$EALwoS>=HV5sFW zrKDrB@M-^M@u6}F=%snh>A*%QGD&}*Ek%0OBQwzsk%Sw-n-eil!@`Ptj-5Q&SHlUd zMGs8mJ!}UOQ{IBU;awm-R@)`!^9l;#40ux#Lhj2oWwBh;Yap*tGbr&K1N<7BE$_d) zh9)M@^~XSx=YIF5-Jf`d9t>AFC-LT>V{U%}TPnwpk5dAdY&R2pFXqpttFegMzGxcm zr0+r1Os)AdUgY@{mgsXp>o;KX5^K(WQ37leUU2Ps#|Bv=^1a0ai?t9KY6_2bkRG+jeT zX^YuyY@Hr&Gl@~lE}leX*^Z`74>}3&Nxk|5oq5Mj?&^lYn_=gEEEq8@HxkStTEwLmYi*m&PI;%m`bZDHR}j zO_kAL*Pq0b-pa+!_?>X6g&r=QboqxI--P-Z1g!$IDH*9el7rPYRo}JoTNG})3I&(* z7SPhIfdBR;vEWypA$~3*y=c&;rSyXL2wFYG<}y;lhLs*Ds2(JJ`_5pRPq8#p$V7Iu zo9;v@$d7NK^)+n(H-n69AEs3!w$}a0Z4M*gPxpRWZwWf+oTv2tN8vKk6IRJ|x1oZX zYMV`|A2yLF-J{jT)o~U7m1(!;I!LLEzT(5~*S>`4YaDDR@U5sNS@>^$&^ZtB|#VCyQ*OW>G7*W1I? z9?ot7;}bXTl{RzZhI5kDubwOe@$t5)!Z~J$_ISm7^y@?96=v#3TLz$rSEigjH;UMJ z_SR_1Zz1J}*L@1kQ{(Rq)FDT^$a9V_!z5Uqd`@cf&k|$TLGtVI>;}eOaLLi&>qyZY zQcR=bFe7{ZlLK>2{x3VgrTxSFk_w~f)K7Z;bSo;D|M5g@(*6x__=})gd&E~{@#w~j zwWqAuzVyE)AUqHHDi36hbWr18w+&~81ct$r^hsChI4Rgn@A_w}Z3#R*dj8h|i z72uH}2R%$8Dqfc3D-T7L9U=8$9gzhG?m? zMe_u*pcq!FlkcUl-heHh|lh2NV^;@YoX=$e z3TsCp?B02Al>VSc&0E$q=hjj0&KCm}CG+TL?$(1v<#Evdm$man;S%EGnbN!PeF3SM z57XGLlRjXI^RsJ8RM>cLUAh$W6!>^UhbfMC5G~#goz{~gWH#!z1_J^#xDam@DXIJf zRL6C{#)WR8PohhQdI;Zyv4Z1~59xi%pP@5iBlG1oWkIG|-#K)1xK=)>u5GWE$LiE9n7c`p~%Q$S$cbePH&1gLPIC9jv8KF@2defpjl654=pE$GCcq zLr|m?3Dntb##Bv!&-ce~{_5&Nd;b8j2W1pkJZLfDNqk{Hnhw3IE}TFLS(14k2S-8p zC%NM;{Sz4v+E@7K%X-GDeeN4v8oXo~~uR`b$N zcnab3hW|0Ce`1GbG;dTclH7cK_Woq;YwPIw*q)6iFDS8Dy2F!t`c`CrYmT`xh8~NG zoQNzl6opP1_Zu`%vEh}=JNnP6Pl7S=uuN5-Npxd_Ai@~75W}Bhi6ewNAjmwHX}nmA zRP_V1r{Diy_Cy9AkggS$|?nnkCq&-(X9sYE@jVehjgLV7b3d7Vk=}@7yPmN z6BAbU-%q8OTn+fFq)#OecY!DGmI;@QKt{``a^s#Y5{je2@gCM15Ue3LhlL5}^R4!f zu+%I#+uo7Jr9A;EsoLwj>nY&hE-E&;@Cn2^B3iWT`WiYHNF&{Qo&}zn;2Nc&%}0|p z39<2ytAS*m>)UT$#4G3bc-}5K2UP1+j@uKibNlY24|ftSUS{5~brxn7frYLny@|#1~Lej+lYv(e62HNdj_bQ%Z-@i(L(b}YU{IEt;p{c9U!GN?Y?)LfX*f( z_}v-8Q?@lX@Ow=B&*!rRkHu{vpFEb@(=65Khv_lb;`5z|L+>Mt;jbC=jqO7+y>|-o z3E_N`#MXt_j`0sCoPG&HT@o$gZubqm``~r8Z~p zzv0t^p3Ut!w&h9q7Y{F&d69Qc-QuWxXVy5Vx@r5@l6n)lFE4P!kB$H-pS_+-lOj0b zavEoQLqA%(JacYiRtVO9{k9&$qXzxMMWx*H#o(58WwC1kHPpMe^Kc*=C6v0C^<|%& zCQSA@BLAd>6Xq`NOwv%5gs1d)OsvgC@b1$FJ5v8j!IcQByu6F6fLlDsUzYR}H`C>4 zO0{?6(&N4%pVufM^^xOFSClnjXq^>LV>=7n<#x~IvmNPAyo!9-zP149j*CdwNzvgv zFHLpE2wJ%KZaZ}!nngd(?rFJxZV=sO^2u`4k$_*8_jBalro~}=pOplO$H2&C<6ub( zH8heox$%cd3d&S#ttzIf5e^+Q56c|McSk+t9v!8IW$lq$N3v+3!ZRc3fLadt$v^v# zm^>?HeC+>+>yQAZIe8@hCCNMfK(q#1y!`OlVZkc`=9Fa5R0SA%$hqQVQRJN{hA-{f z|M;5u9C#lMlZ+R#}I9-5$?Cx4(9?=0EPU__f6jkGsB&3Y$dn#x$wTIprclMg&^C^i_^kO z-C%#%0)6@pE}Y%JFvsNm2UWc6IcYyY{P3A8)yc$5I1*8vY($WlADBx$%cW|-|0W%9`-RvlaAeujc+r!~gfJU&1S?L&(yOULuTesrZ#8oPBM2)%2wXw$LXIp==4aAwM~xgyLB&wQ-MXcRm2~0rDQ$F z#Elm-M?|b!moq^AD0lt=N+}q(`tM@h0}1Hb{`)uAg7#ENY92_!&i86Pm&o@KyZdB?FfRwJw7oMt8MOoUyg0QQF}VU{ z`IPyV*$9t&Zh4paQBE9iZC831DS^3^9~|;}D28Wrs2Q!^FQKyRNz(wUb|BnURVjH> z6pL{5exPsVg=hMj=_m&1aH)KBRLpBm?6IfGbw?`6nQPuZ&J<1sT|2U#o+Mtx`zN}4 zBE479?^WH-N+VgU;9@!v!o-CmnHZz18leNJQeHGzHTKQhq_`3Ex8~&M&Xk>av`I(dv>*?p9hdK6*y4i9%h_9x%lgpfY5!T; zQ@r?sI=6M=T~?gl?Ou76YXyw7Pp|xLBKw$|FAP@)3P5CRXr!lM6G{{1>n(n<8%lk6 z#T)d67cx5t<8vv}c*<*8Pa&`y<(5A7=cC_5#T?5)b;?{=Z-Y&Ci;)BFyqukAzeV0> z>q9cP4+|3R#gSotW*%rCAkZmKw*#`ONtnx!UVTx%Bk!IF4(zMD-u5<=85SD}#Dr_E zfrsW{0o-#7z>zF!b(H78VcjbQX0RR97(IzTOnNtwA7wfIoh0YMjp5_h<{9wPnR|)x z*J<$MX*}K1Ne6j#%hns=AYkECrg(B$3NW+0N>d8A%#l@$Oiui2|#+VX=)Ft7& z@~5$V6he?+$NN`CtT=pae(Ju{9%_8`6VH{W$7jLbx5b8+;^&ZDMHZDbn-HFUEzS0+ zRs{2;70mmR9I0Kr<9gN|GOuke_u>A@4BvWhDQN9sgz^8fis?;;!Hi%37e2!88rfGJ z$9I$5YbFMpY@bdbx0c^p)xk4JgeBTg+lm=hpN;sn#q04xxg(*86MDYLoYv zlP>qJPzKDLR(Igc>z}A}vW8-vm7J5DDXZ$^B#)>u+UF_5jH%?GvM0oBqqE%Syd`#b z0G+cPG4Bn8aS-Q6R=Q8hkn>WuN;N$vel1w$)VehQXjtj98ogN|PvO0@nrvI3?`4d! zu?0V-)&H5XUcrdeD=jss9_c6(=+fN$5PU_bke?_ z^Jh6^_vxE81FB7i-Tk4fpvF0B{p7n15TbWaWsz$cZJ&>=Ud$jq?p$Yw%9nJQ;!car zExBE==a5gMzqAT$JY!LK(5VBh`7~9MRU}&Q4r8<^ngL5226}#Na&X^kCjKW688E8_ z^{)ND2tVyepWLbbesKDk(wSoxRPa`p=E*1Y)Hs;lzpn4UW$?=LYw-&4QW(?m-(@8I z(RYt0C@yuCqF7Oe3;XDozy-G}E2SS^~87VraP%cT)7+o6MkIcuOZ>Ac{*@4|3&-)Xi7ghzAmVJXV&CYH+rwg)N|H@1?S{eWG@k3;mlX| ztm%1ftgtvFB_vAbW2#HG0(zg()j@w6&%z0GM_lgA!4e9bb~@$v_On4ioqc;8OtpgZ zg^hQtAuU{vz9J~T)q%Ph59vJFH3ORDwslJ?hSBH2t?&2c81X^{y-oAh46=`@yC*^? z0C#43Rei0|f*KExA(Lm~&|oCIp6a70zOJqxdbpCDBd&}?@!G$^u%0WG);TtOnT0)F zn#{XG*}M96x#%I60@YkH;Z1(HS3spo_9V@XYF(^WH1MsfYwXloE}GJ&{bx+_5i$3v zPRH)wfg3F;sWRvm(2Jf6f+nlO=x6=ew)3&#Sj+m!<)1Z+sE%&Cvb$pt6=d(RDh+M{ zrZ-QYR5&z()E`$a*5}P4TjQ+S;cQ;Gvc4&wQZ)v;1HX(F02xfVmf(1ng|wT_gBRh#`h1qlfHJ+MKijYgT-iBk zmqQ_hyE0mvf+d-8>BdU7Fr^Savd*LND~%SSn3-GbgyY`FjgT<=1YSTPSf; zhehMPC{!5VI`Fzw8Sb?-DmnLW1DG05?KmUa2PE!}rcAK7+3*O^v7IvJ`$TvoBBAB4%Bf(P@Ni^dc{&o-JTNsOG7L7CuT)+! zZbEu)CK{*MW&kd5`%_7{obN@lJBlV)@#~S{@UavM9CWWEdvO;XeqsdugPiEG!^PY} zio4aIxN0rL<2ox|$l&7ECOr*p-#^!$rnBPJx=TAl{!F9rS(hAJlDof1LHPg??()$q zccm)yek1;@`%jktZlE1pw%c{S4dANaRo7pSB;i9V8DDi-Rmf=5CzC!gfml+IT$}*m zng`W>HtP|EoX=G@;)v(lg7-kv4Xibkk{wAiwmWLgKbqn8CgGDr(QoNtaMi6(ue|q%ge`LPDpW{rzTUDrR5%JR7gcjz; zj!KtUt^osme&+cnTc~;RkC4R2anPlr>06?+f@~P^JN?2b)V=?GqJaH82tJ)ECK5ye zZM-El7}B=TExAVL!hMI^2}cVjaEIXihRC=hB~g$1lmGN9k8Si<>LCp!64(96>gEjECzx=zmD(aYM0rK?w4ks5W0EAw{>l9T_-cBE5}U*3L9Z0kbAKQ#V*XrGi~B}lnYaM$|^ zGYqUhJo3RX7kKlihEH7~xu%Sf!>X3EKsMG4Qn|K)!%=O=_GGLAUR{yuMiw^McaSnO zdYcoP8Jr)oh~a?ta*4O3KXOB17gycMSz0Wc=;QEpirfRmEdRW&p9hcXN>Ut%pGRy&d4Mm*m0yk{700Tcu-wXpaPJ5bdovcWO4>QWMogw+BW&I?5y_!uh z^r=Cofb`oV&Gi=Lq$!{yw@0h_kpblYl|SGh(*Wpww*0u#fDSgaM#U+Sd4Ij;m}i3~ z@${Ln)t%YJfOUNNpWYnV218*vG{2cwfJaFHL)$-E96c{2GH{OpzPkHt<(Cc>JUOxW zYO#tE3w|(oWKlrglOHq^V0CL=O(C%x-hgubOAkyrA=Pqk-#4mR&93B zQNbtWQYAXV3#f-d^4QXa4D|BwhTqd0q%U*%(cWjBY&dISU`EQF4u6|GR-XD-8g`o@ z=j5;^@X0}f^5*MWkQ+fAC$YGSrX9i$Y0C2BR0S5Uo*7DL>8WNI)VKhii#EO*P!0u; zH&R>R9|p+v-?P5wJG+p4pTFVTqx*bOmhdXse?+7)tFd6mY<|&EL6R>CKjO|oa!HWo&ikjX z<%nvPfn$sG9ZqbUzdEMY2pruX`pjif;}{u#)?@bDAmVX&q(5H|V(n0PNq>mEBX!Rz z=9^2y@|n3B>LCWqo5gU+x0fHLCSz20O8_#P1@7mLBt1Y~iUXtFqIh8M%O0ikTVS^F z@kn1xFRHa&c~mSxjhSxqw?3?{MA->f_|LSgqrGQT1a}nmfg2-V{>Bo&XiL6!W4$dE zG~uCaePeY3l;?^49`x@=sv&P(`d_V~sshb_YY`*|6yY({YC=5pnyVMGI|ZQEhlW>s ziLb7fx$V=V6U|8DMr_i^uCGWri~1pTBk2pUySqA8)u0ms#~BnI1#zvM2BTa_Ay`qm z%n%w%1ucZ*q~~Qg;4+iI!4oUgaI}nryELo;b*~%DYkD=JT}<_#)~-(=-j49gq&^Eu z>VvmEy@^lbX7=yl`bF@RZn7Z`l3d-l+BW;g{fK2eU3HtxQz=e{J-g&uk9f8xx_6wS z$EWtF|C_x@dVCzyb}opK%sCg`e~fd&Bey47O?xr)U926*UqJ8bLuTBB*jF0xBHS3U=T&`L4Vpb#Sfolcex{@iK&ioJE)3tEF z6S)a|T2xx)POTvazR))GsS~l}Ww>dmk0RQy5u6Htm~h4opFS_t)SlMYj3(Yf7SDrY*g;_ItLM*!~Q5~gP*2l;%V7Kfw%O2)g5cxV>PyH1g z99Ek+6S6fAicUNf?jw7zUkSH^OJ6Rd_My41W#Y-wO~~;KoR)=ucCJ^d%rIetxzk_# zIym4Wt;8_X?SJU`>6YXkG2(GM&z>RC_#WIpxTaGoFpui=82g~-c56E6%=OM0wgVK(pi*N#@c=Ic%FU$blv+o{8+*<@Uzbjr`N#Mp=Zc4Vd zxhdgu6*GOwoij)x``W1Q;2_Xy@wvS9j0JboUg!GhGYNi9O|W_fEAzi*xOsyu?6~9qmS3$odp)mXMR{@vcs%f zOVx##8b62cZYpFl!d9K{?BaLU5WPZQ>Z5lH=zU==-{O^d;4F8i(eBkO=-Nf&deC_R zjSDA=ye8bFw;3X0zm3d|Kuh%hF?JK&-NPJto=2uf!(dwlKX7alHu#LaHHG6sOxUljsD$}$YOZ6=(Cb7 zz|&Y|bI_gi!?P|khi*)xqc_yE&wI85k;3r*R2LZ_RY#Wm&p8f!=;-V~#KUc{NWH91 z;jjWqOb-Xh(ULsMjPKN_X9ao@s>EnMH47FS>yGNbt_SS-7dP@x4xk;g{jT{2Y;Ykr zS#FJ=1KV~_$seaB_u?mizu1Xq0wqvgU_<&}A_JPvVf!D~?u>&Q-ya?C(WZf1y8AUGIR63T_>o#wy>TRy@h%Q+YyvL6IwzAZAE4q+@5q}m zi~b50EiW+TpyX_K5ap}-r%r|c8a)cSZMOYF&{hgRagDYzkvAStnifCJSLR1Q<@LT2pFL%Oy2pqVo{eFl8I?nnyKe)4s zLJQserel`Tds@Y}<2v(bz_547W_<oqUF*a%?juMCTYG^VzlU?^avS>iXL3q6p71lmKKsk=X274WnZ0hdokN2;*UumB zD?=KqT8Gzmegkj*ym(lVH-Ji(qHoHQocXN^t4D7QsbHk`?jJE_lSpOmxv&}iHdxh< z5~tewi;Cc`5{(1&Fyi*U$ZmROyrR>bU#Gl|dJ}-!gc>W>3Zb*ibeuu+I<(sxa`oVI z&x)l_ARYAlaXw4ZJrZr+HvV#cwHIjJeoC`jcL{v8_!z6I%Z6Xs_S!_OPav&VrztLO zHv!d+7vZg@vq(60;h`iaB|MdZwxiB5;L2z^fu2{?I5LH4%Z+UYO+9{~5p;A6xn{4> zi`w!*cEc;N|FN&5VcfyTq0az!&@rfmF-{;L^36W=BR!Uz)?AV!bGeUb=i`)5!)Q8u zLi|=PD}LyzPj4ws4=>uCx?{9^0=fJP&7)u+N4q{rDK~2ouXpAXFYeo|NICc46}wyX zSpAoSe}L%#D2%(v%_NV}A-bWM7Wo-qer$)1Stku=cb0a9}s$`7WKm&4WxJv)y(+_m^N=c%1OFMX2(AU?*Qj3lre6QE{ZvdJ0Jonur zM2(}OCwzjF7$HrSL%j{zKOFiK9PR2c1;+OG@7}Si5z!mp3HmKKfPVRFw-x1bVvS2F zbp=ktfZ{t}n@jZu=#980sN}bfyzlp4k^R>Np3=m{dAM=l&l77%G_xCs`!4?4%gh2R zR#?yc{M-+6MBLQ{%)R?aob* z^t4&gzL89 zbDmwz=$tki!;gSmB($ELH_g3_H02W;cJE$8_If1^v1aw4gp+ph+|!3aCszqTq9DeKoa9}bKOO#}9aH_!1# zjG!Ygx?#WdE_lM%IIw$(c*a;G1f3u|RBM;8-$^)kA60#&lzlm1=`uSbIIabs*&9~; z3*>_j+G-9Z18MlWF?ZE>LlFCl)^DtyXaqq|?!Kh?$%?g`oFn$xEFqaM(rLAXqkFMk zb|5Z>5Ay6P-XEn(iA4`?Z8`)B!E9Tj1HW_UL3#cT#eMPYxQo(Sz;aLkI`!0Dc=AI6 zE=D``3!SCGEOH;$e(5iv>E*9{+k6~QL->uBK#wAfwXFTrQJ;&>o}H;S*!}^!{`tBb zf|78o!iUbMga&3l2|e&yoeSFAb_F%=5rvK>l61pTR57~Czj@%Cie8!NQ%(5W`i@1D z$ri}53ICO~NqY4m+gsxM1>l(Er33AGW8fR_ANu)I0&uX3`jHO}3%0FPyEj-S1wFIR z#oZ%4=!4y!x!Hu`*K1V{T6}kk227x{kH#^heCLJe*ov? zI~MHp`SNEwe|5` zZ;`$z1I>ojT-}n&0kb@J$Et`N{H*uNWP@cmludZ-1bOK{nr+FupHkyM*433 zj1t`&?)@kwi;lG%6VFQdLtB+DMr_@R#Ph7kJ9qD%L|JV*{NP#vt8xV`q}w|!ix=ehz`*UTO zal)HLW~h5kM*4dV3x0q6$}Awf~dYiq{LA{#;Hi@1moj!lqeb<{)P zv;?%8I%hRQ_UJ3(KIP%`^jJa2UgV!48{X0~Ji{(V4X-SZd{+M;17H63lA2$ngX#MY z83qWGo{(>)R=Wc)c4aHSK-W#)naBK_q7<0mfNR{rT`7B^ESK-6=m9CLlhoFIGK2}o z3I1WqU>XEEni?w!ZUR{8h>tGm{J{^h-*a`wrC`a!2+!-ngxfivwWjGX2mF%ls$U3k zVxtiAR`(7DeBk~YjtfWjz@S@7yS2Yk!d>$WjofbrhbsaO z9hZ}VPvfh^)qKW4XI|dg{nHDGuE=fIy?YXP(aE((~uRWItITvS+|8o zg`j$c>^iW1;C-#ZRSd7ZUbtYtj~coZKBLqi{XkiN|0VOvIiSMelboi-i*JWjY@)jq zc-3CKqG5d+ZSVS7}UTbYEZ!!@)w?j{T%~B_t_MFWD|Z!SUI(g<_4l#w7QBnr_lZP-BxmU z^3l%&H=TPeNsq2jb6iJk7&vM8pIfGDLj_MKzT66?ge&}(ViUwa(H)WTS#O~QCFKN` zP^}QYs{MH6h}IHv>r&F5w&jGYTXC{}UW?$iPRr+2@{YMQ;Lr8tCnt$`7ah z{IaDEi9>@E9u|3ln@F|s@rlx>(m36m%KrxAPOSSjYN#=a7ROc}4-R(Wgo|88eb?-F zVw=R+vW&MJxO1W?=mN(A3f;Z`#-Cswd}y0#Yj5v5aKCz->*51?tk3yQEAS)(9{u7_^pS(dp?pLvWlf;X!bPn$;>@!zqSZ; z2HT!hQA)#z9j^NAj}+nA$1ge+u5jSrS0erX??1Qy`s-<42M7L9ZSvTR@Q#c%YBkgz zjRBggDBJNGQ4=2=pquoefoCnhcqQ^M!l(aqWyX?)aF}x0$a6LcZ2U+w$~1!TyU%*w ze{p02$QLNJZO@SWU5+wK{$6H0Ydz8%y@v}I&y1!Ck)C2ifa2BXwarL+f4=d05Hoak zmc2LrVjfKUo%?#*oD~MlOdU!LoIzaIX2sIjCGiVwmOpeS#o_tN5bF>D8rZtN>dPoL z4@^GxmtVX&f+lztN4yOPzacZ>!6CB^n`W-UmPSx=!fM44_#)re&iA9<1k4TGBvrpzWeuk}oNQVff6O`BTQi zFicDA?#KW$9!S)cJN{rFq!I^vf zH8Y6EQ;Grp2x#SkF4aY*Z-V%sj>FyalA?c6U}_5WzT8pNnK{Dufs=6Fd1_;)^qYa9 zEN7>^1nC();QEryHjR8&imcwgpn|nIz|1OWPJl<5QfnWSMu2X*vv<1&2fn_uuHn<=dVm)Gr8?$P!x5BDyGn9K zEX8*^m|U5m#O0p}3OqFUxst+3X2K8mIK8vP@x(glZL}y~kQIX}j{XIkHY7){Q^&4B z`s>+UCT#vBPqOl`Tl;(FEO@5;;GcE|H-70Xqv{ey1E(0mUOE1thbG4xuF2W6;#(pr zNgsyzvDZ}>lvg4RKLiSCUA>j5c;-Ei|EWoWJ)FV5R_R410M7+9TEO0#8eLQcDa3=so8{%v9NyOMhfm_D+^ zmDWQbAm+SSejhE~X?9^NKZp)evgRGzrlo?)GlO>Dsy4vPPpjB(8GXnG;ig zlmC(vNsTM-gy|N&CApMuU$ngZs3C87aQx?sJMd7(z5{H8t9#(2%s~gaAr#Q@`is8F zAZR+r(E2iw9-2zI`YeXg;wbIC2VVK`;0KR$%~>`%uSV1fymbrA#k`n*U zqu5*cl^XUXf4gK%?#W5|j~+wfgLz{YCGYr8T$6=wq2S4`Gd-!ZFU}c{nW5E_}Gs$wMW(t zzu|__N52V~wQr*{?`4=X2kCH|B2(ejz!em`@7`Ru4kgANhqF4OBH0DLwuhBH-3I*#wfq1T8%BOu&^G@kW`T){h_ppCYVqTe>*bsl{5 z>d89Uztg?+r{#dwq0w94saC+5*1s{0*_(*6OF~0|^xRH7+3-kDl*H+$lzMOFF9C-w zn;XW*FcQAn@l1+vZS_?TwYIrQ!lk!I;=|QBV5_uF9p|xCFy^-WRqS9tz~-qEd96}V zr!LU>U&{bkt7aMOcr%71CZ`!ZzA$06|NMBf48^c_xj)+;_dX=MtKZq>I|B^cwV&%{ zNF%CcPyb_BONHmxI{f?oF+#|YHIv9U20nWGmI>UbL9CmL{IAHKP^f#`_VPXACEz`O z`{Xxjc;|XdisD)UI4C62EN`+08h4EQUzg{Ef>&SW*EO!A|5|rW{218a?lgK=p z@3So~B7E#0aq=#qj&%5AShv!TpQ+&c4|aw-a@>&JXhY`i05fcIJdwM_#Ekc>*2D-n zF+j;TnxRsDgFyaS6nDB&YRt}C^Yz|E9=zaOzZKHTi4C_N>>G-z1RW>4 zuQ#A_AYMwjd!D>oPy0__er`$l{q>BTqAf=9^HXzAZf*ej){ayEnG-)w zRPiL-O^18l<_)jBTSS&f@*q~=#ce^JGRyA=!Coa*t-CX;VBpa20mX!Al-C@Q`md18 zk&-nY-|yprNz`K3lec?7%;}ZgG|m)Q%27tOww%mWNAAZ--LD02TmO56PMyY16e9~N}$?V`2#2X z6RR}2n{OU8DHz?;*J?l)zPu5hC;77N;MHq)TB#(Ab7DS}O;EUTeA<^+#0rj@9afs2vaPr|)vN9pZ&9G4ZLZ9NhSbqU3$Q zL{7NxX`bNNIcBK!S7d`h?Y~LT$W|Etk@UFt zeph()tZ5!e^NmUTI<|s*U%mKMLgv7$=?1^gSX3ic-OxXrg=DS}cq8Fk5(R$t;%4A; z4kqj)c}AerkqWXncpvdnmcgA(IvxIXGVt2a*WU|)8%SyHlH&5QWmKOwB)gVO`t!Fh zX!7U{1Az#k>~i9nY54C*+^W+EFfl5;toV-`J4GvPiT!K>yPa8Ne5F;P`n|<+S87VA z-b9uF*XL2n^`@grgj1>NXm;YeB^3d9$eWZffkG6ttyH zc&V3oI*l0r_JZCUhn%0Mte`)IJ9&Pv6`?1gtlh39%OJ~C#K4AvaIF6PWLUk-326>Z zy*jBcjdT1y{AjgQfGby8hq)ZK5u@}Kv44)d_$u#fsf2L`tf}VA%=l~^9d>+DF|wBv zCXN>Wj7%Ozyes3;Zkm0->xNhKzYkKdT-xrmr8_Tv%Ps44;K2Ymc~@^YZ6yu9cl|`s zcU>~?b1rO7?Og#0W&AYJJ%nqd>3m5impuPE!VxTyf=^CT!-7yZ0fi9KFW(Ny-cs%Z-#Q%y8rJEcmc#8wOs6}*_IA<@rzx_J zmwRiV^JD-?DmImUy}N~KOtv|1j*~pvQ;GkqNBQuFs+{q*AzFOu!dER7F23rkl3r&h~z&FYcW9$QKpo(2`ew_5@E?xff z@2!F$u5P%v)x+3>_^4@8jxuv#)|ZTL+ob=2Bm4^|e|+c0HTHUGQaOYtsxJC1`)(7u z`{GFLknt$GLnq%)A;AvIX0Ph!2a@}F{U!epP4dn@r}wcfatj&SC%bEH^S~b2UGCd8 zOxR&+Z`MKgc`#=@PhowE^e)Z(FUKu6pwf|diiXi#xUV(aE0OR=j+``q6l;%BwuFmI ztUfC~mCgD451@kUPHCSOH0h!AS1q71{TA{32~|@HV}<@}piNeT9=}S-Z*5T|{c9zw zJegK%n9;T{{icitulfhEb-2xeGYM|7=Y`S$ZRQ-)wa_WxP#_i2>BRvX-BII|-%e<~ zs@tf@0C7s4=_RsVT3nQkuj)+N5BsIgU^!3 zhsZnO`>}MR5ftz;a16DRdv9sMKw+O0xnFF@Q0*K8)6KP|D-!hZa~2QolNErym4}A! zByJ(hURM5}H$A*}?vk5P`4%Yb_tZNOK>SrJb#c?pbeO*HAlKtiO2|_aE_#OW=gvP)uifP|{v*+H$0rlkRH||L;p%0IJJ^V<{2VbF#5j(bt5-2?a_WBNkv--t+lR?|a zMW;F-LZ0L=y>$CcDwa?f-LZOqnsv~eb9r(X@w`8dKjwR=(h+3GIhTL8T0!0I+Z@Jj zd5BKy7_aTtB9aPoKlz+x07XY_*Q#>RLcQ}zg|A3Yjd#85v02ltE(^7z<82eKkiG7c z|K8F}45I%Y`-<2~_M`j0&)>{t(nE{KUdK9kIiY9IQ^}Ppov23LtoAnH57XXSu$E2O zLagHN4w~9@qxomdgKTNS_+8&|qEd!0te@}N+swZ7aipY z-5W+6cfBTQNNpCe*LJO}m@?tqiN{9S#+1-s>^aLJ%1Ly({9Wj=tG|Gm1LLn6cM7ar zb0Z`8J1vY^^7gWvrNg=BtxoUXO>!10eu--*2f+OaE|c-LW)K#&x8nN1JTN??q5_j> zaZm^6?kMtoYQ19qH!X*7RJff*I(o`Mw}JDdsY@$({V3(ZGvlfl4s@+ED%cu~nq7YA;qC7{Tzv6LMGbgm zWpMSDVJ(nStc9u^8|WPGK9$~3YMlP;gtmb#D;&>1_xMc1|M@s4HkUSNv1YNSS7&TD z8Vz{s$^C=$n|JyN?X#xDBlU({O3KTiUiP1$+f@qqs&!8ibJ`4W*)H8_*~|o^isd}0 zYzWt`v-8p~HhP#s$6ICKvxNq|Q`#!W`+?X!Me}-HT6|=$h|-W{2e@kKV-q~S0z9vs ztV`tVN9Kwabb$g?(DGOB_+5E&-q4Hf%O*Zl_AX7sQ$D1(Z6BriNoE7=mpL$j70bYI zu)quTAa-~v#Xjy;P9G?8^i8>wSp_oE*G0rLS@G|fGI^OTap<6Ue^XL{9<$_V37p}W z0#g;9x7(g}gTFg3fZrWWXfU;GPWId|lDuYQ{@)AY%RZIO9TGi*oU+74t2P<%&(Zr~ zK^N(99mj^m#L)pDs6g-Zw3Y@g?C+MFIYs(6Mav$h{A=h^Qi)8F9Wy?e-f(>ptf2j+ ziR{m6rQqYE43V*$qu|L7W~Ug^V>n&HCO&J?i|$_gX727wde~Es_;jQ<5NAF2<3j=u zP~4P2%cQ^vu*`_dJLfus?j4AGD05{ET?jh&@jFXBV3S`A@CvK~r#MeJo~WgTE!mtA z19VJKwCj+Y-pVYxuxSIXy`Be!kMf^JYt4cx?e~VXGp%47nclg_?BLDhq6;Ssj0;7fWxwIZ3P@%6};pEn&x ztu#5oLPxj3%EHtjgZet+jPqE{Gu%KH6Elli{1o_QQsIY!XB1fH_L1D-tVY1FCdt3o zZv!Q9b;QSo&jFer*;l=9&Y(=yw{!R_71TeFnltI#hx~^XhjvZ~!j>m0t7ADCV0$yR za)O%lG#esqvWbUEpW1R(^U)Ua?cFVGu(%0QnRk0V;wN)r$qdD zzbMy??LoQ{@{j+k8$lF70hGhm3&;x2jv4=)1B#<}`i-WU2v_ud-k1g}w9Kkg=s!cw z*L^$N((P7}>AfObuF*np+NdGHBAxiPMP^(~m#5H4e`St6XGhW53qI>s{Y=mH<3jK%MAlsI(+%*gU!s$1z<$G z^WpceJ>Wcta#uwM9j@gUt<${Oi5PAsO2o8Gp+eI=f5ODq(9S77Ax@IpyIB6Q`in?A z2wV4v4jvi;C%7flHY;YpHI6Y3rD-OpdNJu$@eR>rPVC(V*RQKP%iOI(=D|0=l=yc6v69whn&u_&{QJS$v&n?N zusi?gNl1Z{Enf4}KAA^nux(E%Zz=lC-)m&r(uP7R;^~gdEP(P4aT(_`HVF4kK}Y55 z3OKM=;AU}R8d^F0y?IYCJ%0K52L;d?K>We{PUm8pK|@vNLYZR+YH-lnwV(8IoiB2Q zb$txb5uTWzQMVc2nT!ZLWFrSehpCgwQlXiRKqM$s8gsSU{mriV}0% z@L!viT>w)@syd#KzWp03Yq=sNW_Y>ePk;-_J-v2H$&!~|LNl(r0`j*P(bA&_#aF`y zP~J7(^Vj3%kY=*AGS7ZmsQ={Mq=SDqV2oR)Nh1Amsk7{*43c#~^?+btyy`mIe4*HC zzTJu9C@E-wCR7zSX3@)t3ULRg9Pl7v-*$NM7+4m0l(W~q3oV5z zRBO?7A-!@hPWQj0cilg0H}-oSyz9PsdUIkP{Ip(BolN7JXku zFC-ezJrJ4!_1$)}{3m8ny8NyXS=(yhSe)#2pYY!{eilg{iR%SX4Qd|G=&7KO#~zu+ zj~#&0`JGSb(m2r6J(lq6=n!C2;~Ts~T&VeC$Ei;>=-ue;Uy_}bs4d}mdjW47@RY2pzoAx# zsNvDAO`dk3BDQi%k$8m|Ov^%!s!+n489!7_OSaGo+rMU;v`tXJ@L45ZeiU7vd7(YH zhYA-jos(>Oy$sGbhz>8gzDG)h?sbvf%jnI_Pl4FyqbM)FW6L>f2-NR0WR6kkMHlNF zZm!FYgToPbT+8-VprvA)eV-GSfrQ=eQprcxD#W9yC6&&@+a#oQgV%A1s3@8R3vfhc@7B#{5nz-<|xt zU^%L93nbt5@opE@DhjAeGj)3L6Hl0bX8=1I(=yIQWv-fw1y! zM6EI>llh(SH2aMH3+JCjgQj>RDue9fepG(c2X*&sBrbqM0mAXWI}X65$XK;C z1r2=rPrA0UKm$rGVD<4}rH3r8Yfig)NKaj&;>y0HG4SifgzP7$C@ zR4p-8?M1!C46>&pFc`dl(BMnqBJeH}SW0@m4W{Rq1HyS00KMLAC6=Zxzk6nYY20avNUarc^wD5h zdQCB)N;K_v>z)H6IW?D+zOAFO9{G@cM~H9a<-VEQ-qJAqk4RBdjRs5^xh*yNCJDsw zcs}DWBb+9OaD@az0a)Vk^!xuPIuEy;zc-Gz_ugB3YKRijIVGeN$ycdFOHtAwR4QpN zl&p+|3Te?I&e778N>h99z31=z{s7mN>v=xU=bZa~zhAE>rgYHk@W(9^H7;E8%Kobe z;fGB>h-M*N_^Xe<-@g zmH0q%|J=O!T!Ri~pAWt{eu)wDIeFN%>8V4rkyoP>?t*Zji^2Kb4q3c$-;sA_q!wHos{00`#DZ>rA z@^j@tG^3rBUyd6mKQ4Wi?#zS-r3z13o*_K67l$72k>J1$hxH?A*ZE;~Nzg$1I30Y) zC9^1ENWABLO2TK3FhL(6Fn)yOl6N#_27h_SfhmkSDwyk+5Zi?t3?Y%=o}u;LEx5;&467li;w3U+DHf zjrT&Vn6B%oG6mU-h}hYGw`iq-hh!D!kFp6ssl*i-)_gHI?^GxGknkHyB41URUFXC* zHTQIAd=SFP3DIo=9_kR6YH0im62PF;b1dYzEOciOqOF!9aV6m|@}|x4RgzXBe79E3 zzE}a6<*g~y{e=O~$c*S1&eCHsir7A?s%=o^5sjChZXhYfeqI%`0d%i!-BatC5DZi? zJwN-11FzkFwOH=53tKaI^9Wc=<9{p4{cxfg*ttgW^NMI7wrVU`J+H1-m_HcvkK_s4$K^+rNSa7UhKT>$qYXzpL)K#iFkces`Q6% zE5b{2p7Z}|c;FAl16RX?De-&WWr~wMj5xk1;NX`OC47J3ahBqBdOZL30WCL;1lE6g z>BTJJoa|#>T<~`u06S##|G2YDVTyC%(Cr!VXKPc=74Pl>FY{Xt7WpzjFJqDXHq!5Z z(O&u`;|wK?mbN;1B~}7+9I@PGBu|ZBz2f|CoxTWSzb73%n7@MjZ%^1Ct`)}SXY6nA z(r{r8JH-s0cV%GiBQnt#oCoK_Z`S2|t|L*-#SeDE)96-&by3$%QEas0XA@FNgXu?xQC`6(%VgcW+(Cp&6qG=ic_8Icq(#bEI4!;xf}Ip9{@Qq>*5 z4lK*X?P+8>z=>gz&`QjSzc)0;@7O^LEm{;S6<8KgSbtx|e{vH@`I{MsgUkYYt@5TQ zZH|&8#@uE$y6JI+ux;QUQ*ur}L9OKn7ExxMrJlrf9xRo0#kt)s9X$TQxh*NS2`=$h zguVZd9$);@74pP}9y6ss7|^52ME}zKG8V}DGeFN-S}c_nPcJ9ZKI|2R6=$;JnTEI^ za~YF_?u;OGs;8TOc8(oqM=LedZE#~&6=4(KW>LI%XZ_O~~2 z`u^pHW4DgFZR?2ROa9uHToQ|b<(RpDc`P>`EoE7<(&2@adkh@YP1&(@?n(Df8wTh! ztMINQa1reZ;*{|YVTN%Od|Sr zSD8G|3wD>4c(=gh%d>mt=0!0q=^Rw_lfc7GX#+m5Nw3qnI-ypC2FATCGk1_d8=19EI6_}(wCFciX;;&_q8Z;<5U(6=YX?+(77Lh?0(Y+pvT)IrKP=8xW0y=K)S_SO9 zSatf_RlhVA9JIP+(tDN%?raqhy30fOHGwXRNSF=B|61m&;TMIAb{;KTKiTkDwpD-4 zDmSD!`$OmYomG_m#C-o#$00y@BlX(dMg@3%s7#8CV?drqf_azdtmwN z!tUco$b9hZLtP5(HtXQ02W_Co?nN_t<$ zdW^6y!>@8Ly*Rw)r_zC-1jEBR9xS{d5oUx$pbazk)58a8p@cY-o{Qo*3IVsG z%`FM{A+I8-MuG)Tw#*a+9-IJmmmE)LpIiWqF5Hw>*H!S-dx=@qU8L7vz!7awOowGt zlGc^v#W7d&Ruo4V3+Y2AEX5db6@PvLx zc$PCY^cOJOTe-*u^Oml98Gq-*J=zU9oD2Ne?vJnd`?Xob|EIid<=r^?_TuJK8>9h) z)i#;Tn`Pj)7QtTMUcv#rHNWCY^71_vk{6XCn4w_y!E0ltRQQUycK+V?J8;9#8KZiY zf9R=OCZAvl4~$7HVy`$z16O+YN3F0?;;YXuxE2Kt0sV|fD%k)o*gYC?=z!2BQm*_g z5)n0vwBKv@KMG}q(`ITDbcbgV>$igvqCE6iD!h(q=jU2LH9fU<@iyU`dPooIF7ZMQ zy*&Bdw!P?+>p*|Z#}%Mv-h6)CcLl_pihXz1eGtV(b4)6cd}3g)P5PNa3HVp0`THm> z3-(UU40^VB42hfYF zDr%`F%gEOusN}lhEZTFKXWz%rZIqXIXt;KW4e#Gt@s}s_t8Yg)&z)iIM^B#hf4arn z17fBgXz%;P1=$WSU1M0*iV-qyX~6s zAG*YY8@`_iy>^5dZW{aMTs+Bw@5^vxQpHfii~$R7rdV1Wx-2i4NAe7myZVfMs_C)1 zOt;oxq!e7+l6t-`j}oi7-qdaLA$vq|?TH#;dR)HO#pkdY3!MI%Qg#OPfe&_RcCTyJ zh=00)o(>7ZV%`)_i5`i%iIkkFbc(r|c*~QYEiG1LCp&)%IKDqn{`iY@FmU+*|~YpRrnt%+NzU z&3mJa+lwINUPZY%1wAfS*wd{)AOu%G=}wE8u7Ke6&`AEcQKVQRM3A;apvw57+H1oq zV%RxB=LQ9X?-^}ZX5faq%zNk`6SwvES*VS!W|n)8`8qy zW7C(YKW&0tEFxF@-KNkVy^@Y-Hae*Bn<>vpj0e8ARNZf`v;gu>_~^%+;eisDa~oSu z(c#z&s$UaU7to@>C!-^TKd`QzciQ5`Cv-Pl0e)y^#tnx4vYJ;r5Zm3bYWlyuggacN zd+`tzWc|u<<{Ih!w@Xk{mJkl+F1EuGe2LQ_!nwfJGMw3>ri7QLno+`8@1t6?BS89brEAVBE^N#^ZtZk+6CGm_W)$1O ziNCn((pFV(BP}4dc=_-eNRbd8+m_(QpN8uW9&X(PnPHg|yl?qnuGb>lz)%nPe5+mm zuo)v}y!__jCT9!cc^z)IO?Z75qN=~oN(n;t#0|#YtRAp?8I*B|31U~-`yQH1>{#=J zq1j5U6yz>=Xc~H(8Or%Z8;bKyqKcR6ML8so`~3NWeg((#t4em1eCuUTSA>~ ze_X4FHUao~a_tM%G;l92tV(KAhXYwYJZwF`kwU8hPZV_z&=j!Vc_@{5g#N5M#Zr|5 znQYAWQg$ori5Kk9HHDgz+=^kC*09_PBR| z$@DnN|Egf|W40SDq}1QJ;KBflP8?1&j$8o2HkFyPWpr>b3D-18BFKefrm$F`#G?^z7sf;>Gd_I&+ug_1^Ngx=&XwqxJdp-Ookm zf$r7WsSD#B$mpy5y#sGofN9_LFPce&M=``W_q9C*Y4;1hvbs$;MyADlnmfl4TL0_J z_KF7A>pZi5-@5>~e-CwD;hRQ5;!BrA?Y6<=n71z14^YAq^+OM@at)wMaq>rP9OeAl&&kFtVUF zsUv;c*Y~rZoeC!WL8}BS^GCzLg#w=b8##s=EnAY?)JFh^=&Y^|7K7pS#vcq{6V6|( zi)98s@sg@%?ExyKfUz*%x`E8ETb0`mb7r@adD@j}k=a%tVK}f#X+%MKwY!cw>Er>0 zhy+I$axa7`;^{)`)>!?7jzu#p5&bdsv>_A zx2#0*heEBfr#4Jjdal;J^UWJ2+nd;wL@?kEUg%pAXb8|h(#mwTrG&0S9{%B{IH68=i@dVn8nQl-SC1)LfG6Aiy49g^!Kb+}?wsK_BYXpV`K*nc3KPB(^q+#BH8VU?T&@)qPkgMCb|#NAXkgQ!(}G*0 z^GNPgrt)_NCTvc_a$fak3kZ*_Gg9rM!Hw;T>c1YYAnEqAw4bgwBWKSX`9JJ4z`P;w z{rzB0s9xF;o99n_27$Zh^tFiJ^rLMeZ`KgX$*6IDCqkZc&AC5a87t_F*>%-9Wl=cE zJ6QgEng%nx+$}=;h#r0ve-k~E#DynQo)71Up)L78$e*K8`q2YU z{C+khWzBvYgq@1vo{wAtH?`U7U-^ncZ^4sx8X9wm!rx&nhjAD%=4oAEzcGgLa!)JH z{bIw%gU-r}na-kjU!E%t<#XeNCw3hb41^~Tx%-g2I5iY@tQX4s%>_>#&nllSXN9cv zf4=ir(m>JmujXPTNB25i*t}Sy2|c6_IT6!51ym@n8E6r&LNevajBMjxR48TMx6VC- zaIfav7U^xyQMa$el~17)e0QDhu#ADm6v=h zn-lI`{vq#pcmUnL@z$$O!==zh)IOn6xl$zECus~{k zcTC9qW#a}unoBqn$U(|Kf(81fdNw~7>jGC4x;ccU-Xe#1?i|(rHPR1?cYGU61@Ath z&@emq7xkQt=<$0H0WQxc^?fK900|~^<8O%1w14EDWBv#oE-&RDX7OagL5Uu8o6qU7 zQk(yauvluSdpzKPZ8YH#=P#ZvZ)JhU-dNEkMM%Mux3eZLssBZ(hmKPGw{sR5Z)r*z z9c)HSGy4zRs$+w?@$z1E+0)3CV((7rOB`7G72|~@l8g6#cT2$b)K7G(CEWV?6OxC$ zlY9NX%_>UuqEfmOJc9(DUcWJ~$%D7fglKKwm51Uj_n#FvRD<9DY3=&#RSA-k|MOMx zC!C*j*T|RT^B1$q&FA&x#(KRo;!5dHwu}?p*&hxTw4WzJ}k4 zp{t)ks%gwtUd@Dyo5<}DGffF;>#5fEj4@#MTHza9FW11Pcyw`O8XXkz?la}^X#%?j zXX$R4Z6njV{THSDiopf#1oKRB8MuqyxIgM6HSGSLxhTCrayqfh7E3`N!6OTA4 z7xB&ysDXqpNfY4^M)GVX36^wuE&RCOLHyV)u|G&T)$z{FryS6%6DnU`S^*OdUQzc{ z27vjtp7G1K6Nt_JfsF0*0gxw@6T8{Of}z&CmA?8plo)nu;w5!E;Pi9ekx(`Wjz=2Y z*)uVUX1;Ik)0G`Vd~JsQf4?!nbKdg?0<*0suR;geAMQprkkSdYOat|&Hk&SFpE&J# zXQiyvN!iD1N3|APCf zqUf>x>vSc*J41l_d(X{MVbWg~q6x~a7J$||B_1Djt3gY~aN+myH8B39AbHmVYPf09 zLSs$%VppyI9;t}$MikIw(c)kea=3QFRyMg0HHUY<&R3oRDL}rkaCR1~MYTPdvYr6H zWrUGa+b^(niw+AjB-V<1Y2iQ-P#CZb*O7#Z@J z0fU;3kM*|K!5%y2J#6YLD6aZHbJw0G6egx!ZJ^l*TYxT<*zy7_<`OTSsKw3PIM{{UBDd0R6`va7s*N9Oa~ixev3Gey~Wfa6ZX7dIeN<&hD=O;6y~N zH_2tDd&LO%9UKEn6cnO==m)_E(ZKX46RW8CgRQ)JSw8xc9b;4HFoCvN1M@fbFkqXA z&__l`5c)47qBh|)FJ!$iGs%)Vhj78`CF}4-M8}#KFR{G|8n%!1IJA=Q7xkGQ0mz7B z*M>QCKUJb+kNvLKrAB~rjbQiR@)h)nW~b?B^Dw!u^xqb$=m&4J1eorrHGnUB_M^XP?%%%au51=N@wdi<|)KRWy#Wb!;)0AT`5`hE+Jf(_}FOP=*;;LP25CdMuBMQ&@EqL3cy4CJO%I3ca{195`QRE9=W5c5VeltGn zng#oEO|@!f34bd8MEv6I36R@Z22a;5Bb!Xd#*TYK=*h!ZX`Qz@aZxe#RU-j9;+cG} zZbY7AYiBm0tf3YJ@0Xb{QwTyOSv7tAx@r`B zcekt!8^Jyf)7lp^lPE!3yXX1RHqgCj_h;`$ACT|4OeG#e@@Ng0Zimc@8+zq4FpzHUY2Y0MF0+zf!(V2D`iZzRqU@6(^j5H}-ieQZQdvIVhUD!|PJQF# z*{TN?+E*^szI_E`->4Qf)%Kx-1BV+QU7G@WIhRUb9HYb?jO?`!6_!z_GiB*x>tXPr z;d*|`%rAMg{547GIq12i0f{?l8nh__tDvWC77y{8i6vHY}(-df*^Wj#3q zLPamIxp>V1zTWdE{BEy;%kpBHDRQYZ?YS9n_lnPKPu*W6m%Lb(g_cpScCOYen5LH`wWe`7|k;nN7{l)F5>@rmpq z*_pI+M!uln5XJ_Dr2%xVD5EOUY90tpnGaX&r@)s!mXvBX&I8w%6Yu8!&7%sRc2Rlp z9;C~8?qf*(7^1kF&msPx2EF*oyf?Ui9fa)JE2y3^f|yUO9PEl&N5`~6s(wFQMmss; zZ!)^ffw7e0US-!6^e^f~z4%lIs1P-t_*eQ1Fu$`m@(vvb4bs=x#P~-M(^+>l!HF*@ zJ=D1D3iT?=Jdcj_Jm>>5C-f?^{*8k&Rj;_2dTO?Hk#AI+Skm2Pus-^|ClquH?Yy&4zV- zVkWJ2O^nc(ZGCP1`Y&+ushG~31U+;s;=Sla<~mX8kN@qP5{LMj5W}(EQcygYe_yH$ zCpNG6lGDW3iC*xAa(frZVl9O`eATro(7{|S$)>Is`Kc*Ym~7JErs0Kpx*Qrvt)!oWi%?_a5|vtojE@rcV6O?wMtU+f7R7bl5`6&0rI)c&IH3yKMkvldjIx z4l$_6(Q$ISdI7y?FnI8%%4+hf+ zy_sF-!VS&FrU$~xfVn13>WyMnY@E(xqe4pyg)?RQ42Ty+zyaI)da$NM8L-CIG-V!Bs#pO9Xh_c_I} zqbVTZt!kW2%nIVZ!50?)Y#3Ou-o7k*cog`?@Rfw*RRX`O)6Jr!Hy5i`dgIi}8WQd) zn5YxrhA&m}*E0`N;lt(=$?=C+@ay8!i3Q7Cc$<^S@Bw8DaJC4%ShqNjXm^aiWh@*; zXDb?PX(;M}(W3hmC2MwA?{LDoll02!qZc^s9<77a;CJYR0+|B{WGgScqlW)}*wERG z?S&0AC%<-PbHgK@x*{s1Z`)%lZSM5@C-|oC!4RJ=0G$j~=~N=5q1GJj_7MFHs-=^s z{>Q?K*@ME4cmL$Wk7X^AZtnSt{_LMqrTsyHp>xRDv|qF^^sJNBcnTGktzj0F<7UQ- zkM{l(rs2acQ>iLX5DsGQddL^5jwV!+*u!i6c@pS{KMi=;$cLZtT=KF!$BmBm9@_{nqoBpXuR2d>K*tk{ zAI@qmU|+XI|E*YVn8$iMc3g`eV(pD`HcDz}tFV?gG|-H)yz-u=iBiC{d>*qP^1N4s za{P1IrvcMsIQdzJ7l2rZg^cG07gkNqDTqtpf!t)G)3B`|CS)C?`=t zCxh{_!E;++$1Pi}|NBVu4w=1r&Pnz-PkqE|ig}@k6Q4o$I0w`yliC|xvIaafgk#HE zXrY0rM$6zwMkvl*bg`;t483u`5nn{+X|ho))u%+Fr4wW?Y8yBB{OL&u>ygUon_2&V<-ID^dTwx-v zh~rskT0_&LY8GZAg793-y~d3F!zj^HZahs(05(%Sut~hrh)O@o?@Y7m1nWArG8f42 z5|aIrVl%o5jDlQ`onWDY!;R|ZDXkPR=wgS%Pbvz`N?~BDNpNxA%B1gLQx2NtxC@ z5c}YhTGiMxFfsOEy&o|Gj`+Ks5kDmkV|ZS^KQ_w@FZ||H9Uor=$Ipm(K6>#>` zf$tPpqQ`Cd5893?3Rv?ksQWL?PG)=Z?os!sg`U?s8t$NmvDm%wgg6}KlkB* zqZ)oZzwT_IBU&6Z=chD*8<~a!~!zU&LfH^ChT_76;V}>I_tp-gHG1yMHv@geu09U0Dr z8(xUd^tthj9==VtbgUu#JwzAB6t~U-0r)hoSjB?7CJf(7EYo4of5KpHFD(vVO6HpH zT|>=!5`JzMIH1Fy2@R7wTrel9pQn9m35W{2zPhi~56)e!YBM3ZuJ%1)bKTLL@JROd zVtXhf)(wjPt>)d1*sd*FI+4%y9GAMzA1Y4xuy^I=w$=RmP-uwa{2EielCe&!V~H6F0z=oQQ5f>9jbGH(Z0 zf}zG)59~@jftJ)S=_9*AO60ZsO!8t_L;3~x@EKmrem#{o{oMfa==6Nsb)6nsx|dcy zentsz|CKimxG9Jq3bE^s9$yB@9(x_8HS>VFsMFr!Qwu=QaQ>H{H8-{qdS4PuLjiT) zSSr5G;KpyxHysQ8(M096{dZSU zLzsvuWgrEP@pSxsxQhmAKRK!AbePbY!d>FT$9g{O$LcheeG(&ci=b}=YC4+qnWG7DCZI)a_uVxM( zjeNlXEgu@@Vd8soxgQ)s>pOtl7t5cvz9e~5GvD!jH>u#&|DqMmD^%bULoI!SY+=|g zX>RNo(hHaeXjl}5JHbyo?7*-2%aC&)<1D{5;TEQ>au%?^TtEyfbKidNq{2pzx1`4j zmmp9j{d^tgcc3RgOMlO22HEo;oJd^hK@E#;v8TMJz$N{;^~>X1;OJY^V>LSSfVN69GbXwHD}O`n@+jcT zP}3b7K0m-ub2YNR+B!RM`Xmzt{BE%=ag%T_(u)7|-m_f;F#l?vm?ZJ|cps$eXkG@oaaKWt zf(*D^?{M9!FC89xxJA2tM-HCn5tTj=CJrmV>-#)O>jDR^`V3nhn*&$^)Eao;4Y0nLmrv5;zyU^naqOh2FJ#p%nP3D1B4bks;KRwp(c@ z^&HqiyO8MT+=ZC!hqEfl?|tB3NuE1tJaGZP|#MuUf2{yy&VUq+I^l%E3BZaNi9=Vx4OV% zR+<7O(m(Xg^7xio&4+_RqmHW<68~HV4a1WtAzWH7k65YbA^P2V%5-xEnEHq~e)OV; z!!H_W`ABbylD$?jd_T$8T@1Uo9GZmIR20r(gGzK$RL5oWHQC!1xAR%BQb6@fS9Zsm z%>cXBt4t{@)R^+*(4@m@TDFrd9LBNh!WnW3Aqav0Ph5ovqK}S*j(2tQDBlATmAe+XZ33x2wzc%&a>n*|l>zxL4yH-ffS$=3aGQz)hN+9jo*zd(eEScN`4E4Ei{Xu9R|284)xxNAUi z&&!7IRL5kfaC@GaSPaDv^zi%9@f6qxIPEKUuTL>S{}HQ)UVG(Wj$+@`BH6=pTO$z~ z&URGW%)b7Np9}W~2b{mbvVy|K#O8NDq{ndyE6)$#?*%i7M#6j-xv-4ko#w!IeJE-` zyZ0g$1AJ*_pmS211J=Yi|FM)S0u?@XE(0&;fz)`w-C4r*i+_CQ-}Q?NKsa5G&#H13 z1v@_}F4AO#FSfFu6+C3am4O2EYQ$3;`niQsa)Hc^==zlo&Cesr{L$L%lnu}rvnZYJ z&4o|uG5&Tb>H(u+h3*j_OHijGhjD#*6F9ua_R!6N8ke1q{hGrf1?QRG?u^_ugC|Qae(F*G*U!p=eKPLG?^GZjtX)sIHEBjb@2l5pyXp}Nq%qFiWw{PY zn4V1^n(YNAG{4Lpx=s8zPyXHvbHiYT&e2bdcM;WBtghtsP-E`(cfbBU+eD1fChW3g z-@e1Kx@_M&!lx?Q-)Zuz2iy(dpMGS(3H4*tK4s_kpt-a9w}-AiK#XrTKT`hLK2kQKKgr41%%X`j=4{}V0DqFMVXWkjgM$K}#Y-HqqqVoN7f?z;K)%*9R$mU?Vvr6)i@iSkiM95IaMkL2}((BLly` zd}#$s!g5)kAO|+yVWYr(m2hubhGYW_6<3**w_5F_hpA6) zY^e0nU}yV}7q!~!k)y=-v8Rf8;M-2d3v4Xk5z`N6IU^1Z9M|7}VCQ$jQCl2AW~868 z>xzVQ;RDhex8gd52tWAE!J!RN-CuxhbL#7VsZ{vqzRx8EpGhCp&+(MBG3igU=_kgV zX2SPgT=Tv5jr93koiw(8QN!M(FpHXD~L+AS79{g&HzHTu=Jm2TE#ZQ_EhG-29Q) z=q|AZkl_2tJ6fa>IqeoyIKWB?&8-;zI}YgK!h^c;x#>Y5lX=GQef=z=-AA>pB*Fzn zZF15lS+|jqL3Og;?tkEoqCWL}#5iJE?eG+GF_cO%xl8NX z7VzRcy`a}c4X;($=S@Vnpv!){4}APi@_zx5T0bVqd~Cz0NkwiDM6tQLI#^u*X$!5U z-KC3&>fia(?jsFo$!?-oK79dcUfzGN^~?qmY0o{~mpG4npKWHIn|cJko{ai}#ur41$3D}>ppPC&m)gO z$Ic&THqq?clJ*4B^K*VdyLsDp1leQ=9Q^!b3B>N&lM z%oaKCe{y1i^oLKq>H1NM{zT3<94`6|9!I||{JEktrlA_<9 z1oN}bC;Q0WUw(a`Z*%|!F3)x8Zc?Se^6!#A?=cxdl%6J+j;gegKK{8tVP<;F+?;98 z^r#C!zh+ZE3mMqKqiuWOzyn~TzS}oqfpEtwP96FBxDrTicU~Rt9R}GcpCf(~o}_4M zEUhmmFHSzsFx`A{3b2N1CE9U~0)H!3U*8jdL2HW7Auql;5a`P8B*8xc9u7#?ODy~a zNvA^xy6I@JO3mm;H*#-J`?q1j%P|AW-)DFi@{+mj*e(XjN@mQsphtR2RIuVH>t1EA zUKG2(cy)qr9jQcl#Bq(SAochYYgxp9S^4yo&e?19u&{2=+R-1(@ZQd^38m@mF!eU2 zb#Z?yV7|~DEL-ynb&a1llsWborMKNT{d#c?1sZwGUGt=cxi+KsQo5SaQZ$ojI#nCG zz4zzwoJZ^EhiZ)#7jr#Ye15p%r9}aXURPcken`!<-H@D!1I0Sy;&Goq* zo%!^fvQ(7t?j&}7{(~rCbGcUYX=X{-e@MfH)>oT=X-U4Hmu-7}zh*O?F!JbsMz7UlQ|^4!zJCR@M^X{^;8`C@4C%_ld;4|2Dm?44;e!(#J*YDUP5zNH(1 zGQkydN-`&KtSkPI&V)alGGT~%{12S}6vcY}CojJIdLuqkv=yv8buHu)nE>3{W-HW5 zO-RnihbJd@7TnFYu74pziT}ONmHpsDxcEb6ktcMQQPYoXrY!F^Wb^7m%+?KR$h22# zBeZ!ANXtJ=Q=MeQgW`0Kk6qcJ)ZV4_>!Gv2Z@ovnsYDt+(n59)kxWppddaEvP(PTJ zQ;tjvTS80E&xbz{r^MG{FQ+Nr9Y*5rw`PhbdJquc*Ck|vKih1bS|yj62g_p)&AHQ*WFuaciBaop>S$wMJ={tujU~5CQ)^i^C~k;@5wY% z{Kk(D9aGhC-)IK68%lFsr3ok6qy93_K?=Oru!cnv*52+ z-+s!&o8X52(f?#tXt40xiqB`_R#3g(8_5?oD~K}8ayEeQg4Tbxy`5CZg@ z{~cSX|3HcMxnv4>i}APgTt78D;*;B~=QT{;?*exB8EGM_>}K_IH)_nckz;)9D?4OT z7QAZkcN2L%zAfN*m*gl#e$<=#Q$U}lvWbMJ*(mwP%e@z?*>V2Vm-iP1SHN#m0q-46 z9Qfp*y7T6a2>?=){FwSW(dTM6`}f7vQ1y0j#(Tnj<86@9cI)Z`qpZW%sEZcS1>?2- z3_Iq~z!#Rz(UYS9QJu{@aD4$KO4hYYzRO3<0{Z!DumKzy9B_WeO9LPH)weg2bNU~_ zpI1H*&f0FiCr(bpJ3;9;cWRfsG!!{}nu8{$9cW*0EC2X=9IPDnNtWjQh1w>+Ncx0R z!5*cGbE4v#;1=c{u{5tjH(dr5uaWbhO8=Of@s$FwslDQxN_?K&wm+?p6W&OUSZ#@h z{|NHm`qlUK_BLAnb}l+*EEMHY+<36@st4$Cm&b;mC7vz~sc=n_ThGu$Lf7wQfhx)p ziulWY=w6B(>m9psP@k-DLO_R-?EOahRwXu(?fGOxJHn2s=Z?C*n4*WFicj_nj%}lN z&pUEt%mks)*-ty)8LT5)wa}?rlH;OtR|zn7o{AQ((IIB#kIEi`3;h`vi#} zypxyp>{?_m@JfxL>hb%Bj#L~#$6u_Y))-YlZg@5pS^l`q8L%x^?vExo`F& z+X6^Be2Nq>>%mKZnu04Qsc@u&gO!k2JBViWvM)Whj?#=@j5zEf+~Mc{89doJ2;vTm zYTEWN!l~PxjVbjyP+X|ZHTjDMzFwWskY8@fakkt4NWSuT_3!H|pvc`V z;YNNv;<`0aT>;1;y% z&)shTC&BfmefOBpcL0aATY~jxvrr88nITz0a<9pBF|imW_oI>0$t&Y?K-Do*?WWF_U4rV}ST(DJ6Y`{FPh^$tbXE_k2v37+!@|a~gasd(@H?LUV-n>!6iHpb+6^p9euT`Z5N^!zr#j z1YUz}C$(?r=a+4HEj?;X)&8KShH!+J{yv)h%(#T!wUjGm7*oOqWgFKzL~OC6P zVzg#a+`mX|uA8;M{#Bpn{>53)R~h_Gn#|=~20UcsNshOAXWZo$O|qBgkm9LR=>=~H zGRmBW7H?0bA1_NNMWbwK_a^^Pzz<1vIue&!K!sy3hX-6moFSDvi;sT?xzlV}RncR> z?sWNdSL6tyxy{%fR7?q9IoykFtQ`bKM^_Jo-y8vIonQRVixH0Q$o1~YlzEVz5TAIm zvJi;so2nMjY!Dx&*P&?APmuYLgl}AdQgzB{P50yC0~1 zDk{YbyRbPyac`n`7o}_lB?yNB8u|Z>(1TIT$R=P`9WU_5%-+-)fdimfEzhG zwr=cZgG%>OGvV-iR4ge!*g7N#zuj1E_HX!(X1~e2{!f+>)*e~#+&n4@sl-_-#5*`} zlYY^o8GR=B>dIwLYlA9asa`nf;X?cr;b-RP;--Ov&f9Of{p;uePq5~E%{;hqKews% zIU~&O>K!~SPy@cuv%g#I;e;kebe8!=%fMN%^xCT$Zs_yhh1(Q=`EhJtWVEw1CA``0 z>iEuS9n40`iR6$y+?Uy!t}7*5#D8dTy_a|bEa+wB{56))f2J7c`|#q9rNzIayVUp$Wh#u9QDFU3Bp_Zcg20(BpzK`msAFGCdeeo zeec2d4bUmAJhq}FfmbhR1a%Zj;y?o+$X)siQ9YN;O;J;WTc#)W=aBr}w*#X|R`&Bi zeEmMfz94lN-MQzy&D1Sr^%>ZGxRli7puzFTR9?cHT)0Fhze#%fZtR!i()4Evk^aWcykZivZ4%{crxHt|0zun0U2~Nhfn-((8WxuJa zV}$*c=Ut}5d2qBSY1bd4i}_SOKOWHG~LY9nOzPqh4GO{RrAzO!tgwU z$luf6BG}0Glir93B~-fWcV5&>9J}4fx?fpJkCSYoTz3kv!$)S^zeBtSzG*TjBi ztokyfXrX8eTwjTfTRN}@GJ@B73Qhl`=se@G{{A@3ZSTGJ-m)50&S_~;gi=UC3Xzsn zc9NBfhHU+cNJa`3=R_!bMY8wad;ib>aZkMNzW4V&=ks~LuZse8R~}O0x5#_9s4)8R zrCMOlTK>ExLlTG8v_}*>P~&QDYPteRA!xAmpI&y%8p_$xr5xwZkGrIpR1Ck%!XJiW zdrHKF;O&n+vKcXin>M?=P8lsh^#k6nplkz(Z?D*|%QFVfO4zRc(Cb6FFYnIS)<{CR z=ql+z!dv~;hoWo*sjyv{EX`bI3#dyFcy{_c7na>yc0ku!1bd0hN>`9Q+m!*C38NJX zW5$3t_(8}$gg$r6zhv~bO z@rc9Cfwwc&;v?_5vRZc9~|D56k!API45Pw~xz^{;oMC~c`=)QWepZKJbB-QtY`i$UYtNX$AwF2n zW)rC6Mu#Qb;XY6$0i%x?+_Yid1;hSzyboyLhDK*-(}rI05uWo*tKf1s5V`2<7@Ii< zOhsxe(xqp>*)tWvJ3q4E+>OpKe||aGLNUoU@u^ zFp$L8S10s&?$Y6<)|F=AP?GD}e3)viNe^qb1^xG7i1Z;ixZWB?Nx>E)X(K&9MSL@V zgvp=q)|~ZP4xd@!!B5zoPI;;FLcs&s4N3i?@JyI_SY?kqlnOs)DX%dC1e7Uiuf3#j zP-J5SswRE+T-vtS9}_|7@qd$fr7 zvT|xJ>9NAP$`5qu@5sGa#rn$)8c7`S-GEy(@}ime=zfbZOze z)w-sYhs3Y*`Rse~Bg&Bei`)G?!r|sk=;ko3qJwR_U$~puX~R1Y!!E9$`H7^yrX{h! z|9`J+I(M$KW2w}^?wsc`Smcq^i=}C4=tw_WM*Wo+?%}n|74BRGqZexPO3!G(jYJ=( zziTwGzbfjPmhc4nov8RK`9JcW*X3X?qaH+O)E338NRQ@ZmBi>$M>oijqnbTfN%9$| z-dj7Iq=(071&mdBM?t{8X0t8kUbIym?dfX$5qvc2dRhC04rjZ79XtLmBCn>cyFI*I z*zXkknlt+v&|Jx!e^b>69yt%Y7Wq=cXT6Pdm$WAVjlScXyV2c$}ow{#XdL#->}jM#ct+2O=5v)~GUzAs0c5Wef^_e42*6

    h6Io85 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

    b>vq4M;7uN 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�Ed0}vWi}|V2P9Lbel>APFP)-AiJLc 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⪥QM8Ns@rbOm}>Ovb>N*EI9D+IZ+dq; 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

    U9%yL+E^MK3e!k}WhRAj@VAv* zG+ZjEh=5|X(xEV}B>xJun3na0zwQrTgoAUoZct+HkJHexjs#h?RcHQuSgvBBg+aNvWLJp8P@c|W|izgYtMPhJ&)87K62!U9nyK4{$d zlM^0P=-C1T9_4LYo2!HO~KSxL^O@tI%c>Qv0V0T2h7ENt-h+wr>Bw7O`qo>`%$d*$4GBDOL-w`M;YP zWO_D^CdaK>g@fu}FFf$4z@rh$`OlOM5@)^sdrB`kiNSwM!IlYsH$@;DA~4{@|GN28 zJd~jLj|~+5^Xaf$uoQ66{gX)ar}%P?;@=j}e`ewm{gX-iZ<)nz6#wr9)6=9{i#Qdz Mvyj3)&!hVP0kF)l@&Et; literal 0 HcmV?d00001 diff --git a/.context/issue-351/raw/newton/fortran_seed1.npz b/.context/issue-351/raw/newton/fortran_seed1.npz new file mode 100644 index 0000000000000000000000000000000000000000..5990a075b27732bf68c1254aea8132003d560a12 GIT binary patch literal 95652 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 CD1kp?-dORvIb#pi!`t4` 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 Cs#v4| literal 0 HcmV?d00001 diff --git a/.context/issue-351/raw/newton/fortran_seed2.npz b/.context/issue-351/raw/newton/fortran_seed2.npz new file mode 100644 index 0000000000000000000000000000000000000000..60e3e5f71c24d834255ceb3470db7135a20d56f3 GIT binary patch literal 95652 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 literal 0 HcmV?d00001 diff --git a/.context/issue-351/raw/newton/gpu_queue2.log b/.context/issue-351/raw/newton/gpu_queue2.log new file mode 100644 index 00000000..f280858a --- /dev/null +++ b/.context/issue-351/raw/newton/gpu_queue2.log @@ -0,0 +1,2 @@ +pamica seed13: final_ll=-3.697816 iters=2000 stop_reason=max_iter n_newton_fallbacks=0 end_newtrate=0.5 time=6239s +pamica fixinit: final_ll=-3.697812 iters=2000 stop_reason=max_iter n_newton_fallbacks=0 end_newtrate=0.5 time=4323s diff --git a/.context/issue-351/raw/newton/newton_seeds_report.json b/.context/issue-351/raw/newton/newton_seeds_report.json new file mode 100644 index 00000000..afce2622 --- /dev/null +++ b/.context/issue-351/raw/newton/newton_seeds_report.json @@ -0,0 +1,238 @@ +{ + "runs": { + "fortran_fixinit": { + "final_ll": -3.697811900183683, + "seconds": 3393.1054351329803 + }, + "fortran_seed1": { + "final_ll": -3.6978038496639263, + "seconds": 3529.269710302353 + }, + "fortran_seed2": { + "final_ll": -3.6977085058389902, + "seconds": 2824.826691389084 + }, + "pamica_fixinit": { + "final_ll": -3.697811975919964, + "stop_reason": "max_iter", + "n_newton_fallbacks": 0, + "end_newtrate": 0.5, + "seconds": 4322.5316915512085 + }, + "pamica_seed13": { + "final_ll": -3.6978163881752835, + "stop_reason": "max_iter", + "n_newton_fallbacks": 0, + "end_newtrate": 0.5, + "seconds": 6239.20730638504 + }, + "pamica_seed42": { + "final_ll": -3.6975690998613517, + "stop_reason": "max_iter", + "n_newton_fallbacks": 0, + "end_newtrate": 0.5, + "seconds": 5206.991969823837 + }, + "pamica_seed7": { + "final_ll": -3.6978123737664954, + "stop_reason": "max_iter", + "n_newton_fallbacks": 0, + "end_newtrate": 0.5, + "seconds": 5979.722954750061 + } + }, + "pairs": { + "fortran_seed1 vs fortran_seed2": { + "mean": 0.9845232160684182, + "min": 0.8413590217123816, + "n_below_0.9": 2, + "worst5": [ + 0.841, + 0.851, + 0.932, + 0.937, + 0.94 + ] + }, + "pamica_seed13 vs fortran_seed1": { + "mean": 0.9956494072840422, + "min": 0.9456874180196005, + "n_below_0.9": 0, + "worst5": [ + 0.946, + 0.966, + 0.986, + 0.989, + 0.989 + ] + }, + "pamica_seed13 vs fortran_seed2": { + "mean": 0.9814609218531232, + "min": 0.7726103993365133, + "n_below_0.9": 3, + "worst5": [ + 0.773, + 0.838, + 0.897, + 0.918, + 0.938 + ] + }, + "pamica_seed42 vs fortran_seed1": { + "mean": 0.9632013836844461, + "min": 0.6669135684644986, + "n_below_0.9": 8, + "worst5": [ + 0.667, + 0.757, + 0.8, + 0.822, + 0.832 + ] + }, + "pamica_seed42 vs fortran_seed2": { + "mean": 0.9864819095469834, + "min": 0.8583950552591829, + "n_below_0.9": 3, + "worst5": [ + 0.858, + 0.863, + 0.896, + 0.908, + 0.964 + ] + }, + "pamica_seed7 vs fortran_seed1": { + "mean": 0.9949488656298753, + "min": 0.9431185504188633, + "n_below_0.9": 0, + "worst5": [ + 0.943, + 0.978, + 0.984, + 0.984, + 0.988 + ] + }, + "pamica_seed7 vs fortran_seed2": { + "mean": 0.9818638960989596, + "min": 0.785890714613924, + "n_below_0.9": 3, + "worst5": [ + 0.786, + 0.825, + 0.897, + 0.925, + 0.935 + ] + }, + "pamica_seed13 vs pamica_seed42": { + "mean": 0.9623214361648936, + "min": 0.657888742144425, + "n_below_0.9": 7, + "worst5": [ + 0.658, + 0.675, + 0.809, + 0.846, + 0.861 + ] + }, + "pamica_seed13 vs pamica_seed7": { + "mean": 0.9991797816142324, + "min": 0.9906842414174939, + "n_below_0.9": 0, + "worst5": [ + 0.991, + 0.995, + 0.997, + 0.997, + 0.998 + ] + }, + "pamica_seed42 vs pamica_seed7": { + "mean": 0.9643885413233992, + "min": 0.6694651529069076, + "n_below_0.9": 7, + "worst5": [ + 0.669, + 0.679, + 0.806, + 0.848, + 0.868 + ] + }, + "pamica_fixinit vs fortran_fixinit": { + "mean": 0.9999997990780136, + "min": 0.9999964129902119, + "n_below_0.9": 0, + "worst5": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + }, + "fortran_fixinit vs fortran_seed1": { + "mean": 0.9960136346706421, + "min": 0.9551047201618098, + "n_below_0.9": 0, + "worst5": [ + 0.955, + 0.977, + 0.988, + 0.988, + 0.99 + ] + }, + "fortran_fixinit vs fortran_seed2": { + "mean": 0.9822761943412508, + "min": 0.7852272438593741, + "n_below_0.9": 2, + "worst5": [ + 0.785, + 0.829, + 0.902, + 0.926, + 0.938 + ] + }, + "pamica_fixinit vs pamica_seed13": { + "mean": 0.9993247579335198, + "min": 0.9917649489800673, + "n_below_0.9": 0, + "worst5": [ + 0.992, + 0.997, + 0.997, + 0.998, + 0.998 + ] + }, + "pamica_fixinit vs pamica_seed42": { + "mean": 0.9635912741837158, + "min": 0.6674551834143085, + "n_below_0.9": 7, + "worst5": [ + 0.667, + 0.677, + 0.805, + 0.847, + 0.848 + ] + }, + "pamica_fixinit vs pamica_seed7": { + "mean": 0.9998199920891492, + "min": 0.9980012380369315, + "n_below_0.9": 0, + "worst5": [ + 0.998, + 0.998, + 0.999, + 0.999, + 1.0 + ] + } + } +} \ No newline at end of file diff --git a/.context/issue-351/raw/newton/pamica_fixinit.npz b/.context/issue-351/raw/newton/pamica_fixinit.npz new file mode 100644 index 0000000000000000000000000000000000000000..ca56cfd067ce4059bfa2d9aadd09c721a080da26 GIT binary patch literal 96478 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≻N(d9!T@qTT#}U3o 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 z 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 X`+U>USVfI?8uHUZ#%-E3Q&9Xrn@s#@ literal 0 HcmV?d00001 diff --git a/.context/issue-351/raw/newton/pamica_seed42.npz b/.context/issue-351/raw/newton/pamica_seed42.npz new file mode 100644 index 0000000000000000000000000000000000000000..2216433e9eaf6aa35baef69f331317aca8293748 GIT binary patch literal 96478 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 =)LzusHz!s8c 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 KeIchQDEM6Sz3xV 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%0AC?uKzov4xR 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 literal 0 HcmV?d00001 diff --git a/.context/issue-351/raw/newton_mlx/pamica_seed42_mlx.npz b/.context/issue-351/raw/newton_mlx/pamica_seed42_mlx.npz new file mode 100644 index 0000000000000000000000000000000000000000..d84294a00ca8e162741fc4e899a27d2158e79bc2 GIT binary patch literal 68314 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 N_`U6G`sbJTzX0Ej|CRs% literal 0 HcmV?d00001 diff --git a/.context/issue-351/raw/newton_mlx/stdout.log b/.context/issue-351/raw/newton_mlx/stdout.log new file mode 100644 index 00000000..fa07d8a2 --- /dev/null +++ b/.context/issue-351/raw/newton_mlx/stdout.log @@ -0,0 +1,4 @@ +lrate floor (1e-12) reached at iter 951; stopping. +pamica seed7_mlx: final_ll=-3.698091 iters=952 stop_reason=lrate_floor n_newton_fallbacks=0 end_newtrate=9.094947017729282e-13 time=1050s +lrate floor (1e-12) reached at iter 927; stopping. +pamica seed42_mlx: final_ll=-3.698110 iters=928 stop_reason=lrate_floor n_newton_fallbacks=0 end_newtrate=9.094947017729282e-13 time=1073s diff --git a/.context/issue-351/raw/orientation_check.txt b/.context/issue-351/raw/orientation_check.txt new file mode 100644 index 00000000..168d9de1 --- /dev/null +++ b/.context/issue-351/raw/orientation_check.txt @@ -0,0 +1,10 @@ +root /Users/yahya/Documents/git/mlx-fc-phase15-final-parity +max |A - A.T| of the initial A: 9.821e-03 +pamica iteration-1 LL: -3.513155323725571 +binary iteration-1 LL, seed_from_torch: -3.513155323725570 (diff +8.882e-16) +binary iteration-1 LL, stored A untransposed: -3.513168802764010 (diff -1.348e-05) +root /private/tmp/claude-501/-Users-yahya-Documents-git-pamica/3b0e67f1-eae1-4864-8013-454e2363dda4/scratchpad/p15_pre +max |A - A.T| of the initial A: 9.822e-03 +pamica iteration-1 LL: -3.513128081184544 +binary iteration-1 LL, seed_from_torch: -3.513128081184544 (diff +8.882e-16) +binary iteration-1 LL, stored A untransposed: -3.513141342844886 (diff -1.326e-05) diff --git a/.context/issue-351/raw/pin_check.json b/.context/issue-351/raw/pin_check.json new file mode 100644 index 00000000..1bad30c9 --- /dev/null +++ b/.context/issue-351/raw/pin_check.json @@ -0,0 +1,34 @@ +{ + "reproduce_table1 single-model sweep": { + "identical": true, + "lines": 79 + }, + "reproduce_table1 multi-model ensemble": { + "identical": true, + "lines": 79 + }, + "keep_best_ensemble reference": { + "identical": true, + "lines": 80 + }, + "bundled_single_basins reference": { + "identical": true, + "lines": 80 + }, + "newton_seeds reference": { + "identical": true, + "lines": 80 + }, + "newton_seeds same-start reference": { + "identical": true, + "lines": 83 + }, + "orientation_check": { + "identical": true, + "lines": 83 + }, + "share_comps_counts part c": { + "identical": true, + "lines": 83 + } +} \ No newline at end of file diff --git a/.context/issue-351/raw/pin_check_pinned_params_pre_phase17.json b/.context/issue-351/raw/pin_check_pinned_params_pre_phase17.json new file mode 100644 index 00000000..54dd392f --- /dev/null +++ b/.context/issue-351/raw/pin_check_pinned_params_pre_phase17.json @@ -0,0 +1,10 @@ +{ + "reproduce_table1 single-model sweep": "files ./data.fdt\noutdir ./amicaout/\nblock_size 512\ndo_opt_block 0\nblk_min 256\nblk_step 256\nblk_max 1024\nnum_models 1\nmax_threads 4\nuse_min_dll 0\nmin_dll 1.000000e-09\nuse_grad_norm 0\nmin_grad_norm 1.000000e-07\nnum_mix_comps 3\npdftype 0\nmax_iter 2000\nnum_samples 1\nfield_blocksize 1\ndo_history 0\nhiststep 10\nshare_comps 0\nshare_start 100\ncomp_thresh 0.990000\nshare_iter 100\nlrate 0.050000\nminlrate 1.000000e-08\nmineig 1.000000e-12\nlratefact 0.500000\nrholrate 0.050000\nrho0 1.500000\nminrho 1.000000\nmaxrho 2.000000\nrholratefact 0.500000\nkurt_start 3\nnum_kurt 5\nkurt_int 1\ndo_newton 0\nnewt_start 50\nnewt_ramp 10\nnewtrate 1.000000\ndo_reject 0\nnumrej 3\nrejsig 3.000000\nrejstart 2\nrejint 3\nwritestep 20\nwrite_nd 0\nwrite_LLt 1\ndecwindow 1\nmax_decs 3\nfix_init 0\nupdate_A 1\nupdate_c 1\nupdate_gm 1\nupdate_alpha 1\nupdate_mu 1\nupdate_beta 1\ninvsigmax 100.000000\ninvsigmin 0.000000\ndo_rho 1\nload_rej 0\nload_W 0\nload_c 0\nload_gm 0\nload_alpha 0\nload_mu 0\nload_beta 0\nload_rho 0\nload_comp_list 0\ndo_mean 1\ndo_sphere 1\ndoPCA 1\npcakeep 32\npcadb 30.000000\nbyte_size 4\ndoscaling 1\nscalestep 1\ndata_dim 32\nfield_dim 64\n", + "reproduce_table1 multi-model ensemble": "files ./data.fdt\noutdir ./amicaout/\nblock_size 512\ndo_opt_block 0\nblk_min 256\nblk_step 256\nblk_max 1024\nnum_models 2\nmax_threads 4\nuse_min_dll 1\nmin_dll 1.000000e-09\nuse_grad_norm 1\nmin_grad_norm 1.000000e-07\nnum_mix_comps 3\npdftype 0\nmax_iter 100\nnum_samples 1\nfield_blocksize 1\ndo_history 0\nhiststep 10\nshare_comps 0\nshare_start 100\ncomp_thresh 0.990000\nshare_iter 100\nlrate 0.050000\nminlrate 1.000000e-08\nmineig 1.000000e-12\nlratefact 0.500000\nrholrate 0.050000\nrho0 1.500000\nminrho 1.000000\nmaxrho 2.000000\nrholratefact 0.500000\nkurt_start 3\nnum_kurt 5\nkurt_int 1\ndo_newton 1\nnewt_start 50\nnewt_ramp 10\nnewtrate 1.000000\ndo_reject 0\nnumrej 3\nrejsig 3.000000\nrejstart 2\nrejint 3\nwritestep 20\nwrite_nd 0\nwrite_LLt 1\ndecwindow 1\nmax_decs 3\nfix_init 0\nupdate_A 1\nupdate_c 1\nupdate_gm 1\nupdate_alpha 1\nupdate_mu 1\nupdate_beta 1\ninvsigmax 100.000000\ninvsigmin 0.000000\ndo_rho 1\nload_rej 0\nload_W 0\nload_c 0\nload_gm 0\nload_alpha 0\nload_mu 0\nload_beta 0\nload_rho 0\nload_comp_list 0\ndo_mean 1\ndo_sphere 1\ndoPCA 1\npcakeep 32\npcadb 30.000000\nbyte_size 4\ndoscaling 1\nscalestep 1\ndata_dim 32\nfield_dim 64\n", + "keep_best_ensemble reference": "files ./data.fdt\noutdir ./amicaout/\nblock_size 512\ndo_opt_block 0\nblk_min 256\nblk_step 256\nblk_max 1024\nnum_models 2\nmax_threads 1\nuse_min_dll 1\nmin_dll 1.000000e-09\nuse_grad_norm 1\nmin_grad_norm 1.000000e-07\nnum_mix_comps 3\npdftype 0\nmax_iter 300\nnum_samples 1\nfield_blocksize 1\ndo_history 0\nhiststep 10\nshare_comps 0\nshare_start 100\ncomp_thresh 0.990000\nshare_iter 100\nlrate 0.050000\nminlrate 1.000000e-08\nmineig 1.000000e-12\nlratefact 0.500000\nrholrate 0.050000\nrho0 1.500000\nminrho 1.000000\nmaxrho 2.000000\nrholratefact 0.500000\nkurt_start 3\nnum_kurt 5\nkurt_int 1\ndo_newton 1\nnewt_start 50\nnewt_ramp 10\nnewtrate 1.000000\ndo_reject 0\nnumrej 3\nrejsig 3.000000\nrejstart 2\nrejint 3\nwritestep 20\nwrite_nd 0\nwrite_LLt 1\ndecwindow 1\nmax_decs 3\nfix_init 0\nupdate_A 1\nupdate_c 1\nupdate_gm 1\nupdate_alpha 1\nupdate_mu 1\nupdate_beta 1\ninvsigmax 100.000000\ninvsigmin 0.000000\ndo_rho 1\nload_rej 0\nload_W 0\nload_c 0\nload_gm 0\nload_alpha 0\nload_mu 0\nload_beta 0\nload_rho 0\nload_comp_list 0\ndo_mean 1\ndo_sphere 1\ndoPCA 1\npcakeep 32\npcadb 30.000000\nbyte_size 4\ndoscaling 1\nscalestep 1\nseed 3\ndata_dim 32\nfield_dim 64\n", + "bundled_single_basins reference": "files ./data.fdt\noutdir ./amicaout/\nblock_size 512\ndo_opt_block 0\nblk_min 256\nblk_step 256\nblk_max 1024\nnum_models 1\nmax_threads 3\nuse_min_dll 0\nmin_dll 1.000000e-09\nuse_grad_norm 0\nmin_grad_norm 1.000000e-07\nnum_mix_comps 3\npdftype 0\nmax_iter 2000\nnum_samples 1\nfield_blocksize 1\ndo_history 0\nhiststep 10\nshare_comps 0\nshare_start 100\ncomp_thresh 0.990000\nshare_iter 100\nlrate 0.050000\nminlrate 1.000000e-08\nmineig 1.000000e-12\nlratefact 0.500000\nrholrate 0.050000\nrho0 1.500000\nminrho 1.000000\nmaxrho 2.000000\nrholratefact 0.500000\nkurt_start 3\nnum_kurt 5\nkurt_int 1\ndo_newton 0\nnewt_start 50\nnewt_ramp 10\nnewtrate 1.000000\ndo_reject 0\nnumrej 3\nrejsig 3.000000\nrejstart 2\nrejint 3\nwritestep 20\nwrite_nd 0\nwrite_LLt 1\ndecwindow 1\nmax_decs 3\nfix_init 0\nupdate_A 1\nupdate_c 1\nupdate_gm 1\nupdate_alpha 1\nupdate_mu 1\nupdate_beta 1\ninvsigmax 100.000000\ninvsigmin 0.000000\ndo_rho 1\nload_rej 0\nload_W 0\nload_c 0\nload_gm 0\nload_alpha 0\nload_mu 0\nload_beta 0\nload_rho 0\nload_comp_list 0\ndo_mean 1\ndo_sphere 1\ndoPCA 1\npcakeep 32\npcadb 30.000000\nbyte_size 4\ndoscaling 1\nscalestep 1\nseed 4\ndata_dim 32\nfield_dim 64\n", + "newton_seeds reference": "files ./data.fdt\noutdir ./amicaout/\nblock_size 512\ndo_opt_block 0\nblk_min 256\nblk_step 256\nblk_max 1024\nnum_models 1\nmax_threads 10\nuse_min_dll 0\nmin_dll 1.000000e-09\nuse_grad_norm 0\nmin_grad_norm 1.000000e-07\nnum_mix_comps 3\npdftype 0\nmax_iter 2000\nnum_samples 1\nfield_blocksize 1\ndo_history 0\nhiststep 10\nshare_comps 0\nshare_start 100\ncomp_thresh 0.990000\nshare_iter 100\nlrate 0.050000\nminlrate 1.000000e-08\nmineig 1.000000e-12\nlratefact 0.500000\nrholrate 0.050000\nrho0 1.500000\nminrho 1.000000\nmaxrho 2.000000\nrholratefact 0.500000\nkurt_start 3\nnum_kurt 5\nkurt_int 1\ndo_newton 1\nnewt_start 50\nnewt_ramp 10\nnewtrate 1.000000\ndo_reject 0\nnumrej 3\nrejsig 3.000000\nrejstart 2\nrejint 3\nwritestep 20\nwrite_nd 0\nwrite_LLt 1\ndecwindow 1\nmax_decs 3\nfix_init 0\nupdate_A 1\nupdate_c 1\nupdate_gm 1\nupdate_alpha 1\nupdate_mu 1\nupdate_beta 1\ninvsigmax 100.000000\ninvsigmin 0.000000\ndo_rho 1\nload_rej 0\nload_W 0\nload_c 0\nload_gm 0\nload_alpha 0\nload_mu 0\nload_beta 0\nload_rho 0\nload_comp_list 0\ndo_mean 1\ndo_sphere 1\ndoPCA 1\npcakeep 32\npcadb 30.000000\nbyte_size 4\ndoscaling 1\nscalestep 1\nseed 1\ndata_dim 32\nfield_dim 64\n", + "newton_seeds same-start reference": "files ./data.fdt\noutdir ./out/\nblock_size 512\ndo_opt_block 0\nblk_min 256\nblk_step 256\nblk_max 1024\nnum_models 1\nmax_threads 1\nuse_min_dll 0\nmin_dll 1.000000e-09\nuse_grad_norm 0\nmin_grad_norm 1.000000e-07\nnum_mix_comps 3\npdftype 0\nmax_iter 2000\nnum_samples 1\nfield_blocksize 1\ndo_history 0\nhiststep 10\nshare_comps 0\nshare_start 100\ncomp_thresh 0.990000\nshare_iter 100\nlrate 0.050000\nminlrate 1.000000e-08\nmineig 1.000000e-12\nlratefact 0.500000\nrholrate 0.050000\nrho0 1.500000\nminrho 1.000000\nmaxrho 2.000000\nrholratefact 0.500000\nkurt_start 3\nnum_kurt 5\nkurt_int 1\ndo_newton 1\nnewt_start 50\nnewt_ramp 10\nnewtrate 1.000000\ndo_reject 0\nnumrej 3\nrejsig 3.000000\nrejstart 2\nrejint 3\nwritestep 2001\nwrite_nd 0\nwrite_LLt 0\ndecwindow 1\nmax_decs 3\nfix_init 0\nupdate_A 1\nupdate_c 1\nupdate_gm 1\nupdate_alpha 1\nupdate_mu 1\nupdate_beta 1\ninvsigmax 100.000000\ninvsigmin 0.000000\ndo_rho 1\nload_rej 0\nload_W 0\nload_c 1\nload_gm 1\nload_alpha 1\nload_mu 1\nload_beta 1\nload_rho 1\nload_comp_list 0\ndo_mean 1\ndo_sphere 1\ndoPCA 1\npcakeep 32\npcadb 30.000000\nbyte_size 4\ndoscaling 1\nscalestep 1\nindir ./init\ndata_dim 32\nfield_dim 64\nload_mean 1\nload_sphere 0\nload_A 1\n", + "orientation_check": "files ./data.fdt\noutdir ./out/\nblock_size 512\ndo_opt_block 0\nblk_min 256\nblk_step 256\nblk_max 1024\nnum_models 1\nmax_threads 1\nuse_min_dll 0\nmin_dll 1.000000e-09\nuse_grad_norm 0\nmin_grad_norm 1.000000e-07\nnum_mix_comps 3\npdftype 0\nmax_iter 1\nnum_samples 1\nfield_blocksize 1\ndo_history 0\nhiststep 10\nshare_comps 0\nshare_start 100\ncomp_thresh 0.990000\nshare_iter 100\nlrate 0.050000\nminlrate 1.000000e-08\nmineig 1.000000e-12\nlratefact 0.500000\nrholrate 0.050000\nrho0 1.500000\nminrho 1.000000\nmaxrho 2.000000\nrholratefact 0.500000\nkurt_start 3\nnum_kurt 5\nkurt_int 1\ndo_newton 1\nnewt_start 50\nnewt_ramp 10\nnewtrate 1.000000\ndo_reject 0\nnumrej 3\nrejsig 3.000000\nrejstart 2\nrejint 3\nwritestep 2\nwrite_nd 0\nwrite_LLt 0\ndecwindow 1\nmax_decs 3\nfix_init 0\nupdate_A 1\nupdate_c 1\nupdate_gm 1\nupdate_alpha 1\nupdate_mu 1\nupdate_beta 1\ninvsigmax 100.000000\ninvsigmin 0.000000\ndo_rho 1\nload_rej 0\nload_W 0\nload_c 1\nload_gm 1\nload_alpha 1\nload_mu 1\nload_beta 1\nload_rho 1\nload_comp_list 0\ndo_mean 1\ndo_sphere 1\ndoPCA 1\npcakeep 32\npcadb 30.000000\nbyte_size 4\ndoscaling 1\nscalestep 1\nindir ./init\ndata_dim 32\nfield_dim 64\nload_mean 1\nload_sphere 0\nload_A 1\n", + "share_comps_counts part c": "files ./data.fdt\noutdir ./out/\nblock_size 512\ndo_opt_block 0\nblk_min 256\nblk_step 256\nblk_max 1024\nnum_models 2\nmax_threads 1\nuse_min_dll 1\nmin_dll 1.000000e-09\nuse_grad_norm 1\nmin_grad_norm 1.000000e-07\nnum_mix_comps 3\npdftype 0\nmax_iter 8\nnum_samples 1\nfield_blocksize 1\ndo_history 0\nhiststep 10\nshare_comps 0\nshare_start 8\ncomp_thresh 0.990000\nshare_iter 100\nlrate 0.050000\nminlrate 1.000000e-08\nmineig 1.000000e-12\nlratefact 0.500000\nrholrate 0.050000\nrho0 1.500000\nminrho 1.000000\nmaxrho 2.000000\nrholratefact 0.500000\nkurt_start 3\nnum_kurt 5\nkurt_int 1\ndo_newton 0\nnewt_start 50\nnewt_ramp 10\nnewtrate 1.000000\ndo_reject 0\nnumrej 3\nrejsig 3.000000\nrejstart 2\nrejint 3\nwritestep 9\nwrite_nd 0\nwrite_LLt 0\ndecwindow 1\nmax_decs 3\nfix_init 0\nupdate_A 1\nupdate_c 1\nupdate_gm 1\nupdate_alpha 1\nupdate_mu 1\nupdate_beta 1\ninvsigmax 100.000000\ninvsigmin 0.000000\ndo_rho 1\nload_rej 0\nload_W 0\nload_c 1\nload_gm 1\nload_alpha 1\nload_mu 1\nload_beta 1\nload_rho 1\nload_comp_list 0\ndo_mean 1\ndo_sphere 1\ndoPCA 1\npcakeep 32\npcadb 30.000000\nbyte_size 4\ndoscaling 1\nscalestep 1\nindir ./init\ndata_dim 32\nfield_dim 64\nload_mean 1\nload_sphere 0\nload_A 1\n" +} \ No newline at end of file diff --git a/.context/issue-351/raw/pin_check_post_phase17.json b/.context/issue-351/raw/pin_check_post_phase17.json new file mode 100644 index 00000000..dae97b59 --- /dev/null +++ b/.context/issue-351/raw/pin_check_post_phase17.json @@ -0,0 +1,82 @@ +{ + "reproduce_table1 single-model sweep": { + "pinned_identical_to_saved": false, + "same_key_order": false, + "added": { + "do_approx_sphere": "1" + }, + "removed": {}, + "changed": {}, + "unpinned_identical_to_pinned": false + }, + "reproduce_table1 multi-model ensemble": { + "pinned_identical_to_saved": false, + "same_key_order": false, + "added": { + "do_approx_sphere": "1" + }, + "removed": {}, + "changed": {}, + "unpinned_identical_to_pinned": false + }, + "keep_best_ensemble reference": { + "pinned_identical_to_saved": false, + "same_key_order": false, + "added": { + "do_approx_sphere": "1" + }, + "removed": {}, + "changed": {}, + "unpinned_identical_to_pinned": false + }, + "bundled_single_basins reference": { + "pinned_identical_to_saved": false, + "same_key_order": false, + "added": { + "do_approx_sphere": "1" + }, + "removed": {}, + "changed": {}, + "unpinned_identical_to_pinned": false + }, + "newton_seeds reference": { + "pinned_identical_to_saved": false, + "same_key_order": false, + "added": { + "do_approx_sphere": "1" + }, + "removed": {}, + "changed": {}, + "unpinned_identical_to_pinned": false + }, + "newton_seeds same-start reference": { + "pinned_identical_to_saved": false, + "same_key_order": false, + "added": { + "do_approx_sphere": "1" + }, + "removed": {}, + "changed": {}, + "unpinned_identical_to_pinned": false + }, + "orientation_check": { + "pinned_identical_to_saved": false, + "same_key_order": false, + "added": { + "do_approx_sphere": "1" + }, + "removed": {}, + "changed": {}, + "unpinned_identical_to_pinned": false + }, + "share_comps_counts part c": { + "pinned_identical_to_saved": false, + "same_key_order": false, + "added": { + "do_approx_sphere": "1" + }, + "removed": {}, + "changed": {}, + "unpinned_identical_to_pinned": false + } +} \ No newline at end of file diff --git a/.context/issue-351/raw/pin_run_check_post_phase17.json b/.context/issue-351/raw/pin_run_check_post_phase17.json new file mode 100644 index 00000000..60b0ddb4 --- /dev/null +++ b/.context/issue-351/raw/pin_run_check_post_phase17.json @@ -0,0 +1,128 @@ +{ + "reproduce_table1 single-model sweep": { + "new_has_approx": true, + "files": [ + "A", + "LL", + "LLt", + "S", + "W", + "alpha", + "c", + "comp_list", + "gm", + "mean", + "mu", + "out.txt", + "rho", + "sbeta" + ], + "identical": [ + "A", + "LL", + "LLt", + "S", + "W", + "alpha", + "c", + "comp_list", + "gm", + "mean", + "mu", + "rho", + "sbeta", + "out.txt (times removed)" + ], + "different": [], + "missing": [], + "returncodes": [ + 0, + 0 + ], + "all_identical": true + }, + "reproduce_table1 multi-model ensemble": { + "new_has_approx": true, + "files": [ + "A", + "LL", + "LLt", + "S", + "W", + "alpha", + "c", + "comp_list", + "gm", + "mean", + "mu", + "out.txt", + "rho", + "sbeta" + ], + "identical": [ + "A", + "LL", + "LLt", + "S", + "W", + "alpha", + "c", + "comp_list", + "gm", + "mean", + "mu", + "rho", + "sbeta", + "out.txt (times removed)" + ], + "different": [], + "missing": [], + "returncodes": [ + 0, + 0 + ], + "all_identical": true + }, + "newton_seeds reference": { + "new_has_approx": true, + "files": [ + "A", + "LL", + "LLt", + "S", + "W", + "alpha", + "c", + "comp_list", + "gm", + "mean", + "mu", + "out.txt", + "rho", + "sbeta" + ], + "identical": [ + "A", + "LL", + "LLt", + "S", + "W", + "alpha", + "c", + "comp_list", + "gm", + "mean", + "mu", + "rho", + "sbeta", + "out.txt (times removed)" + ], + "different": [], + "missing": [], + "returncodes": [ + 0, + 0 + ], + "all_identical": true + } +} \ No newline at end of file diff --git a/.context/issue-351/raw/precision_agreement.json b/.context/issue-351/raw/precision_agreement.json new file mode 100644 index 00000000..aae206d2 --- /dev/null +++ b/.context/issue-351/raw/precision_agreement.json @@ -0,0 +1,20 @@ +{ + "100": { + "torch_final_ll": -3.4110031143196786, + "mlx_final_ll": -3.4109981060028076, + "abs_ll_difference": 5.0083168710024495e-06, + "mean_matched_corr": 0.9999999096496164, + "min_matched_corr": 0.9999993779271434, + "amari": 4.975071791940432e-05, + "max_abs_W_diff": 0.00044353275198191766 + }, + "200": { + "torch_final_ll": -3.401731290101124, + "mlx_final_ll": -3.401728868484497, + "abs_ll_difference": 2.4216166267798656e-06, + "mean_matched_corr": 0.9999997770650518, + "min_matched_corr": 0.9999982846224417, + "amari": 6.270096656839582e-05, + "max_abs_W_diff": 0.0006998187339538331 + } +} \ No newline at end of file diff --git a/.context/issue-351/raw/pytest_full.log b/.context/issue-351/raw/pytest_full.log new file mode 100644 index 00000000..c9236c32 --- /dev/null +++ b/.context/issue-351/raw/pytest_full.log @@ -0,0 +1,52 @@ +============================= test session starts ============================== +platform darwin -- Python 3.12.14, pytest-9.1.1, pluggy-1.6.0 +rootdir: /Users/yahya/Documents/git/mlx-fc-phase15-final-parity +configfile: pytest.ini +testpaths: pamica/tests +plugins: cov-7.1.0, xdist-3.8.0 +created: 8/8 workers +8 workers [1998 items] + +........................................................................ [ 3%] +........................................................................ [ 7%] +........................................................................ [ 10%] +........................................................................ [ 14%] +......................................................................ss [ 18%] +.......sss.............................................................. [ 21%] +........................................................................ [ 25%] +...ssssss............................................................... [ 28%] +........................................................................ [ 32%] +........................................................................ [ 36%] +.......................................................................s [ 39%] +ss...................................................................... [ 43%] +......................ssss...........................s.................. [ 46%] +........................................................................ [ 50%] +........................................................................ [ 54%] +..........................................................ssss.......... [ 57%] +........................................................................ [ 61%] +........................................sss............................. [ 64%] +.........................sssss....................s..................... [ 68%] +...........................................................sss.......... [ 72%] +........................................................................ [ 75%] +........................................................................ [ 79%] +........................................................................ [ 82%] +........................................................................ [ 86%] +........................................................................ [ 90%] +........................................................................ [ 93%] +.......sss.ss........................................................... [ 97%] +.......................................xxx............ [100%] +=============================== warnings summary =============================== +pamica/tests/test_sample_data.py::test_loadmodout_reproduces_fortran_reported_ll + /Users/yahya/Documents/git/mlx-fc-phase15-final-parity/.venv/lib/python3.12/site-packages/numpy/linalg/_linalg.py:2349: RuntimeWarning: divide by zero encountered in slogdet + sign, logdet = _umath_linalg.slogdet(a, signature=signature) + +pamica/tests/test_sample_data.py::test_loadmodout_reproduces_fortran_reported_ll + /Users/yahya/Documents/git/mlx-fc-phase15-final-parity/.venv/lib/python3.12/site-packages/numpy/linalg/_linalg.py:2349: RuntimeWarning: overflow encountered in slogdet + sign, logdet = _umath_linalg.slogdet(a, signature=signature) + +pamica/tests/test_sample_data.py::test_loadmodout_reproduces_fortran_reported_ll + /Users/yahya/Documents/git/mlx-fc-phase15-final-parity/.venv/lib/python3.12/site-packages/numpy/linalg/_linalg.py:2349: RuntimeWarning: invalid value encountered in slogdet + sign, logdet = _umath_linalg.slogdet(a, signature=signature) + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +===== 1955 passed, 40 skipped, 3 xfailed, 3 warnings in 1404.05s (0:23:24) ===== diff --git a/.context/issue-351/raw/pytest_full_post_phase17.log b/.context/issue-351/raw/pytest_full_post_phase17.log new file mode 100644 index 00000000..a26d362a --- /dev/null +++ b/.context/issue-351/raw/pytest_full_post_phase17.log @@ -0,0 +1,4063 @@ +============================= test session starts ============================== +platform darwin -- Python 3.12.14, pytest-9.1.1, pluggy-1.6.0 -- /Users/yahya/Documents/git/mlx-fc-phase15-final-parity/.venv/bin/python +cachedir: .pytest_cache +rootdir: /Users/yahya/Documents/git/mlx-fc-phase15-final-parity +configfile: pytest.ini +testpaths: pamica/tests +plugins: cov-7.1.0, xdist-3.8.0 +created: 8/8 workers +8 workers [2017 items] + +scheduling tests via LoadScheduling + +pamica/tests/mlx_tests/test_mlx_backend.py::test_rejects_unsupported_config +pamica/tests/mlx_tests/test_mlx_reject.py::test_state_dict_round_trip_persists_numrej_and_good_idx +pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_restored_params_match_a_truncated_refit_bit_identically +pamica/tests/mlx_tests/test_mlx_pdf.py::test_auto_switch_noop_when_num_kurt_zero +pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_reject_invalid_model_idx[get_mixing_matrix] +pamica/tests/mne_tests/test_mne_backends.py::test_mlx_fit_builds_the_mlx_backend +pamica/tests/test_backend_guards.py::test_guarded_accessors_work_on_a_healthy_fit[torch] +pamica/tests/mne_tests/test_mne_wrapper.py::test_mir_pmi_nbins_passthrough +[gw0] [ 0%] PASSED pamica/tests/mlx_tests/test_mlx_backend.py::test_rejects_unsupported_config +pamica/tests/mlx_tests/test_mlx_backend.py::test_sufficient_stats_match_numpy_reference +[gw0] [ 0%] PASSED pamica/tests/mlx_tests/test_mlx_backend.py::test_sufficient_stats_match_numpy_reference +pamica/tests/mlx_tests/test_mlx_backend.py::test_newton_stats_match_numpy_reference +[gw0] [ 0%] PASSED pamica/tests/mlx_tests/test_mlx_backend.py::test_newton_stats_match_numpy_reference +pamica/tests/mlx_tests/test_mlx_backend.py::test_backend_stable_on_full_data +[gw4] [ 0%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_reject_invalid_model_idx[get_mixing_matrix] +pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_reject_invalid_model_idx[get_unmixing_matrix] +[gw7] [ 0%] PASSED pamica/tests/test_backend_guards.py::test_guarded_accessors_work_on_a_healthy_fit[torch] +pamica/tests/test_backend_guards.py::test_guarded_accessors_work_on_a_healthy_fit[mlx] +[gw4] [ 0%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_reject_invalid_model_idx[get_unmixing_matrix] +pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_reject_invalid_model_idx[get_sensor_mixing_matrix] +[gw3] [ 0%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_state_dict_round_trip_persists_numrej_and_good_idx +pamica/tests/mlx_tests/test_mlx_reject.py::test_save_load_round_trip_persists_numrej_and_good_idx +[gw4] [ 0%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_reject_invalid_model_idx[get_sensor_mixing_matrix] +pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_reject_invalid_model_idx[get_rho] +[gw2] [ 0%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_auto_switch_noop_when_num_kurt_zero +pamica/tests/mlx_tests/test_mlx_pdf.py::test_pdtype_from_kurtosis_decision +[gw2] [ 0%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_pdtype_from_kurtosis_decision +pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_with_newton[2-3] +[gw5] [ 0%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_mlx_fit_builds_the_mlx_backend +pamica/tests/mne_tests/test_mne_backends.py::test_get_sources_matches_the_mlx_transform[mlx_keep] +[gw5] [ 0%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_get_sources_matches_the_mlx_transform[mlx_keep] +pamica/tests/mne_tests/test_mne_backends.py::test_get_sources_matches_the_mlx_transform[mlx_full] +[gw7] [ 0%] PASSED pamica/tests/test_backend_guards.py::test_guarded_accessors_work_on_a_healthy_fit[mlx] +pamica/tests/test_backend_guards.py::test_guarded_accessors_work_on_a_healthy_fit[numpy] +[gw4] [ 0%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_reject_invalid_model_idx[get_rho] +pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_reject_invalid_model_idx[variance_order] +[gw3] [ 0%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_save_load_round_trip_persists_numrej_and_good_idx +pamica/tests/mlx_tests/test_mlx_reject.py::test_phase2_era_payload_loads_with_no_rejection_state +[gw7] [ 0%] PASSED pamica/tests/test_backend_guards.py::test_guarded_accessors_work_on_a_healthy_fit[numpy] +pamica/tests/test_backend_guards.py::test_guarded_accessors_refuse_degenerate_stop_reason[torch] +[gw3] [ 0%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_phase2_era_payload_loads_with_no_rejection_state +pamica/tests/mlx_tests/test_mlx_reject.py::test_missing_core_extra_key_still_raises +[gw7] [ 0%] PASSED pamica/tests/test_backend_guards.py::test_guarded_accessors_refuse_degenerate_stop_reason[torch] +pamica/tests/test_backend_guards.py::test_guarded_accessors_refuse_degenerate_stop_reason[mlx] +[gw4] [ 0%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_reject_invalid_model_idx[variance_order] +pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_rejects_non_int_model_idx +[gw3] [ 0%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_missing_core_extra_key_still_raises +pamica/tests/mlx_tests/test_mlx_reject.py::test_malformed_numrej_raises_named_error +[gw7] [ 1%] PASSED pamica/tests/test_backend_guards.py::test_guarded_accessors_refuse_degenerate_stop_reason[mlx] +pamica/tests/test_backend_guards.py::test_guarded_accessors_refuse_degenerate_stop_reason[numpy] +[gw4] [ 1%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_rejects_non_int_model_idx +pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_output_shape_and_dtype +[gw7] [ 1%] PASSED pamica/tests/test_backend_guards.py::test_guarded_accessors_refuse_degenerate_stop_reason[numpy] +pamica/tests/test_backend_guards.py::test_guarded_accessors_refuse_nonfinite_param[torch] +[gw7] [ 1%] PASSED pamica/tests/test_backend_guards.py::test_guarded_accessors_refuse_nonfinite_param[torch] +pamica/tests/test_backend_guards.py::test_guarded_accessors_refuse_nonfinite_param[mlx] +[gw4] [ 1%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_output_shape_and_dtype +pamica/tests/mlx_tests/test_mlx_transform.py::test_accessor_shapes +[gw5] [ 1%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_get_sources_matches_the_mlx_transform[mlx_full] +pamica/tests/mne_tests/test_mne_backends.py::test_apply_reproduces_the_input[mlx_keep] +[gw3] [ 1%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_malformed_numrej_raises_named_error +pamica/tests/mlx_tests/test_mlx_reject.py::test_malformed_good_idx_raises_named_error +[gw5] [ 1%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_apply_reproduces_the_input[mlx_keep] +pamica/tests/mne_tests/test_mne_backends.py::test_apply_reproduces_the_input[mlx_full] +[gw5] [ 1%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_apply_reproduces_the_input[mlx_full] +pamica/tests/mne_tests/test_mne_backends.py::test_exclude_removes_only_that_component +[gw7] [ 1%] PASSED pamica/tests/test_backend_guards.py::test_guarded_accessors_refuse_nonfinite_param[mlx] +pamica/tests/test_backend_guards.py::test_guarded_accessors_refuse_nonfinite_param[numpy] +[gw5] [ 1%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_exclude_removes_only_that_component +pamica/tests/mne_tests/test_mne_backends.py::test_saved_ica_applies_identically +[gw5] [ 1%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_saved_ica_applies_identically +pamica/tests/mne_tests/test_mne_backends.py::test_mlx_and_torch_export_the_same_basis +[gw7] [ 1%] PASSED pamica/tests/test_backend_guards.py::test_guarded_accessors_refuse_nonfinite_param[numpy] +pamica/tests/test_backend_guards.py::test_data_accessors_reject_1d_and_wrong_channel_count[torch] +[gw4] [ 1%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_accessor_shapes +pamica/tests/mlx_tests/test_mlx_transform.py::test_variance_order_is_a_permutation_with_matching_svar +[gw2] [ 1%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_with_newton[2-3] +pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_with_newton[3-3] +[gw3] [ 1%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_malformed_good_idx_raises_named_error +pamica/tests/mlx_tests/test_mlx_reject.py::test_reject_outliers_nonfinite_ll_raises_instability_error +[gw7] [ 1%] PASSED pamica/tests/test_backend_guards.py::test_data_accessors_reject_1d_and_wrong_channel_count[torch] +pamica/tests/test_backend_guards.py::test_data_accessors_reject_1d_and_wrong_channel_count[mlx] +[gw3] [ 1%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_reject_outliers_nonfinite_ll_raises_instability_error +pamica/tests/mlx_tests/test_mlx_reject.py::test_reject_outliers_all_finite_never_drops_everything +[gw3] [ 1%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_reject_outliers_all_finite_never_drops_everything +pamica/tests/mlx_tests/test_mlx_reject.py::test_reject_outliers_zeroes_the_llt_stash_for_dropped_samples +[gw3] [ 1%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_reject_outliers_zeroes_the_llt_stash_for_dropped_samples +pamica/tests/mlx_tests/test_mlx_restarts.py::test_single_restart_is_bit_identical_to_a_direct_fit +[gw1] [ 2%] PASSED pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_restored_params_match_a_truncated_refit_bit_identically +pamica/tests/mlx_tests/test_mlx_keepbest.py::test_pdftype1_forced_restore_rolls_back_n_kurt_done_with_pdtype +[gw7] [ 2%] PASSED pamica/tests/test_backend_guards.py::test_data_accessors_reject_1d_and_wrong_channel_count[mlx] +pamica/tests/test_backend_guards.py::test_data_accessors_reject_1d_and_wrong_channel_count[numpy] +[gw5] [ 2%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_mlx_and_torch_export_the_same_basis +pamica/tests/mne_tests/test_mne_backends.py::test_degenerate_fit_builds_no_basis_and_refuses_export[torch] +[gw4] [ 2%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_variance_order_is_a_permutation_with_matching_svar +pamica/tests/mlx_tests/test_mlx_transform.py::test_get_mixing_and_unmixing_are_inverses +[gw5] [ 2%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_degenerate_fit_builds_no_basis_and_refuses_export[torch] +pamica/tests/mne_tests/test_mne_backends.py::test_degenerate_fit_builds_no_basis_and_refuses_export[mlx] +[gw3] [ 2%] PASSED pamica/tests/mlx_tests/test_mlx_restarts.py::test_single_restart_is_bit_identical_to_a_direct_fit +pamica/tests/mlx_tests/test_mlx_restarts.py::test_single_restart_records_the_one_fit_it_ran +[gw7] [ 2%] PASSED pamica/tests/test_backend_guards.py::test_data_accessors_reject_1d_and_wrong_channel_count[numpy] +pamica/tests/test_backend_guards.py::test_channel_count_follows_n_channels_in_after_pca[torch] +[gw5] [ 2%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_degenerate_fit_builds_no_basis_and_refuses_export[mlx] +pamica/tests/mne_tests/test_mne_backends.py::test_n_iter_counts_the_e_steps_that_ran[torch-max_iter] +[gw4] [ 2%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_get_mixing_and_unmixing_are_inverses +pamica/tests/mlx_tests/test_mlx_transform.py::test_n_channels_in_full_rank_equals_n_channels +[gw3] [ 2%] PASSED pamica/tests/mlx_tests/test_mlx_restarts.py::test_single_restart_records_the_one_fit_it_ran +pamica/tests/mlx_tests/test_mlx_restarts.py::test_an_explicit_single_seed_overrides_the_constructor_seed +[gw7] [ 2%] PASSED pamica/tests/test_backend_guards.py::test_channel_count_follows_n_channels_in_after_pca[torch] +pamica/tests/test_backend_guards.py::test_channel_count_follows_n_channels_in_after_pca[mlx] +[gw4] [ 2%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_n_channels_in_full_rank_equals_n_channels +pamica/tests/mlx_tests/test_mlx_transform.py::test_n_channels_in_reports_original_width_on_rank_reduced_fit +[gw3] [ 2%] PASSED pamica/tests/mlx_tests/test_mlx_restarts.py::test_an_explicit_single_seed_overrides_the_constructor_seed +pamica/tests/mlx_tests/test_mlx_restarts.py::test_best_of_n_returns_the_argmax_of_the_independent_fits +[gw7] [ 2%] PASSED pamica/tests/test_backend_guards.py::test_channel_count_follows_n_channels_in_after_pca[mlx] +pamica/tests/test_backend_guards.py::test_channel_count_follows_n_channels_in_after_pca[numpy] +[gw4] [ 2%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_n_channels_in_reports_original_width_on_rank_reduced_fit +pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_matches_forward_activations_on_training_data +[gw7] [ 2%] PASSED pamica/tests/test_backend_guards.py::test_channel_count_follows_n_channels_in_after_pca[numpy] +pamica/tests/test_backend_guards.py::test_torch_from_state_dict_unexpected_key_raises_value_error +[gw7] [ 2%] PASSED pamica/tests/test_backend_guards.py::test_torch_from_state_dict_unexpected_key_raises_value_error +pamica/tests/test_backend_guards.py::test_torch_from_state_dict_missing_key_raises_value_error +[gw2] [ 2%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_with_newton[3-3] +pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_with_newton[4-1] +[gw4] [ 2%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_matches_forward_activations_on_training_data +pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_multimodel_routes_by_model_idx +[gw7] [ 2%] PASSED pamica/tests/test_backend_guards.py::test_torch_from_state_dict_missing_key_raises_value_error +pamica/tests/test_backend_guards.py::test_mlx_from_state_dict_unexpected_key_raises_value_error +[gw7] [ 3%] PASSED pamica/tests/test_backend_guards.py::test_mlx_from_state_dict_unexpected_key_raises_value_error +pamica/tests/test_backend_guards.py::test_mlx_from_state_dict_missing_key_raises_value_error +[gw4] [ 3%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_multimodel_routes_by_model_idx +pamica/tests/mlx_tests/test_mlx_transform.py::test_get_rho_raises_on_force_set_nan_rho +[gw7] [ 3%] PASSED pamica/tests/test_backend_guards.py::test_mlx_from_state_dict_missing_key_raises_value_error +pamica/tests/test_blocktune.py::test_validate_rejects_unusable_sweeps[0-1024-128-blk_min must be >= 1] +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_validate_rejects_unusable_sweeps[0-1024-128-blk_min must be >= 1] +pamica/tests/test_blocktune.py::test_validate_rejects_unusable_sweeps[-4096-1024-128-blk_min must be >= 1] +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_validate_rejects_unusable_sweeps[-4096-1024-128-blk_min must be >= 1] +pamica/tests/test_blocktune.py::test_validate_rejects_unusable_sweeps[128-1024-0-blk_step must be >= 1] +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_validate_rejects_unusable_sweeps[128-1024-0-blk_step must be >= 1] +pamica/tests/test_blocktune.py::test_validate_rejects_unusable_sweeps[128-1024--8-blk_step must be >= 1] +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_validate_rejects_unusable_sweeps[128-1024--8-blk_step must be >= 1] +pamica/tests/test_blocktune.py::test_validate_rejects_unusable_sweeps[2048-1024-128-blk_max must be >= blk_min] +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_validate_rejects_unusable_sweeps[2048-1024-128-blk_max must be >= blk_min] +pamica/tests/test_blocktune.py::test_validate_accepts_fortran_and_pamica_defaults +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_validate_accepts_fortran_and_pamica_defaults +pamica/tests/test_blocktune.py::test_candidates_use_fortran_arithmetic_stepping +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_candidates_use_fortran_arithmetic_stepping +pamica/tests/test_blocktune.py::test_default_sweep_brackets_the_measured_optimum +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_default_sweep_brackets_the_measured_optimum +pamica/tests/test_blocktune.py::test_candidates_are_clamped_to_n_samples +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_candidates_are_clamped_to_n_samples +pamica/tests/test_blocktune.py::test_candidates_clamped_when_floor_exceeds_data +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_candidates_clamped_when_floor_exceeds_data +pamica/tests/test_blocktune.py::test_candidates_are_capped_by_the_memory_estimate +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_candidates_are_capped_by_the_memory_estimate +pamica/tests/test_blocktune.py::test_candidates_reject_empty_data +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_candidates_reject_empty_data +pamica/tests/test_blocktune.py::test_memory_cap_shrinks_as_channels_and_models_rise +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_memory_cap_shrinks_as_channels_and_models_rise +pamica/tests/test_blocktune.py::test_memory_cap_is_none_when_memory_is_unknown +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_memory_cap_is_none_when_memory_is_unknown +pamica/tests/test_blocktune.py::test_host_memory_is_positive_or_unavailable +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_host_memory_is_positive_or_unavailable +pamica/tests/test_blocktune.py::test_recognizes_every_backend_allocation_failure[exc0] +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_recognizes_every_backend_allocation_failure[exc0] +pamica/tests/test_blocktune.py::test_recognizes_every_backend_allocation_failure[exc1] +[gw7] [ 3%] PASSED pamica/tests/test_blocktune.py::test_recognizes_every_backend_allocation_failure[exc1] +pamica/tests/test_blocktune.py::test_recognizes_every_backend_allocation_failure[exc2] +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_recognizes_every_backend_allocation_failure[exc2] +pamica/tests/test_blocktune.py::test_recognizes_every_backend_allocation_failure[exc3] +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_recognizes_every_backend_allocation_failure[exc3] +pamica/tests/test_blocktune.py::test_recognizes_every_backend_allocation_failure[exc4] +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_recognizes_every_backend_allocation_failure[exc4] +pamica/tests/test_blocktune.py::test_recognizes_every_backend_allocation_failure[exc5] +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_recognizes_every_backend_allocation_failure[exc5] +pamica/tests/test_blocktune.py::test_recognizes_every_backend_allocation_failure[exc6] +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_recognizes_every_backend_allocation_failure[exc6] +pamica/tests/test_blocktune.py::test_does_not_mistake_a_real_bug_for_memory_pressure[exc0] +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_does_not_mistake_a_real_bug_for_memory_pressure[exc0] +pamica/tests/test_blocktune.py::test_does_not_mistake_a_real_bug_for_memory_pressure[exc1] +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_does_not_mistake_a_real_bug_for_memory_pressure[exc1] +pamica/tests/test_blocktune.py::test_does_not_mistake_a_real_bug_for_memory_pressure[exc2] +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_does_not_mistake_a_real_bug_for_memory_pressure[exc2] +pamica/tests/test_blocktune.py::test_does_not_mistake_a_real_bug_for_memory_pressure[exc3] +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_does_not_mistake_a_real_bug_for_memory_pressure[exc3] +pamica/tests/test_blocktune.py::test_does_not_mistake_a_real_bug_for_memory_pressure[exc4] +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_does_not_mistake_a_real_bug_for_memory_pressure[exc4] +pamica/tests/test_blocktune.py::test_does_not_mistake_a_real_bug_for_memory_pressure[exc5] +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_does_not_mistake_a_real_bug_for_memory_pressure[exc5] +pamica/tests/test_blocktune.py::test_does_not_mistake_a_real_bug_for_memory_pressure[exc6] +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_does_not_mistake_a_real_bug_for_memory_pressure[exc6] +pamica/tests/test_blocktune.py::test_picks_the_fastest_candidate +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_picks_the_fastest_candidate +pamica/tests/test_blocktune.py::test_logs_the_choice_and_every_timing_at_info +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_logs_the_choice_and_every_timing_at_info +pamica/tests/test_blocktune.py::test_scores_each_candidate_on_its_fastest_pass +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_scores_each_candidate_on_its_fastest_pass +pamica/tests/test_blocktune.py::test_repeats_must_be_at_least_one +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_repeats_must_be_at_least_one +pamica/tests/test_blocktune.py::test_falls_back_to_last_working_size_on_allocation_failure +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_falls_back_to_last_working_size_on_allocation_failure +pamica/tests/test_blocktune.py::test_does_not_try_larger_candidates_after_a_failure +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_does_not_try_larger_candidates_after_a_failure +pamica/tests/test_blocktune.py::test_keeps_the_static_default_when_nothing_can_be_timed +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_keeps_the_static_default_when_nothing_can_be_timed +pamica/tests/test_blocktune.py::test_first_candidate_failure_does_not_claim_a_size_ran +[gw7] [ 4%] PASSED pamica/tests/test_blocktune.py::test_first_candidate_failure_does_not_claim_a_size_ran +pamica/tests/test_blocktune.py::test_a_non_allocation_error_propagates +[gw7] [ 5%] PASSED pamica/tests/test_blocktune.py::test_a_non_allocation_error_propagates +pamica/tests/test_blocktune.py::test_a_non_allocation_error_stops_on_the_very_first_pass +[gw7] [ 5%] PASSED pamica/tests/test_blocktune.py::test_a_non_allocation_error_stops_on_the_very_first_pass +pamica/tests/test_blocktune.py::test_empty_candidate_list_is_a_programming_error +[gw7] [ 5%] PASSED pamica/tests/test_blocktune.py::test_empty_candidate_list_is_a_programming_error +pamica/tests/test_blocktune.py::test_search_composes_the_cap_and_the_sweep +[gw7] [ 5%] PASSED pamica/tests/test_blocktune.py::test_search_composes_the_cap_and_the_sweep +pamica/tests/test_blocktune.py::test_every_backend_resolves_the_same_tuner_defaults[torch] +[gw7] [ 5%] PASSED pamica/tests/test_blocktune.py::test_every_backend_resolves_the_same_tuner_defaults[torch] +pamica/tests/test_blocktune.py::test_every_backend_resolves_the_same_tuner_defaults[numpy] +[gw7] [ 5%] PASSED pamica/tests/test_blocktune.py::test_every_backend_resolves_the_same_tuner_defaults[numpy] +pamica/tests/test_blocktune.py::test_every_backend_resolves_the_same_tuner_defaults[mlx] +[gw7] [ 5%] PASSED pamica/tests/test_blocktune.py::test_every_backend_resolves_the_same_tuner_defaults[mlx] +pamica/tests/test_channel_selection.py::test_read_electrode_positions +[gw7] [ 5%] PASSED pamica/tests/test_channel_selection.py::test_read_electrode_positions +pamica/tests/test_channel_selection.py::test_positions_for_channels_alignment +[gw7] [ 5%] PASSED pamica/tests/test_channel_selection.py::test_positions_for_channels_alignment +pamica/tests/test_channel_selection.py::test_selection_deterministic_unique_sorted +[gw7] [ 5%] PASSED pamica/tests/test_channel_selection.py::test_selection_deterministic_unique_sorted +pamica/tests/test_channel_selection.py::test_distributed_more_spread_than_first_n +[gw4] [ 5%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_get_rho_raises_on_force_set_nan_rho +pamica/tests/mlx_tests/test_mlx_transform.py::test_fit_path_is_unchanged_by_phase1 +[gw7] [ 5%] PASSED pamica/tests/test_channel_selection.py::test_distributed_more_spread_than_first_n +pamica/tests/test_channel_selection.py::test_n_ge_localized_returns_all_localized +[gw7] [ 5%] PASSED pamica/tests/test_channel_selection.py::test_n_ge_localized_returns_all_localized +pamica/tests/test_channel_selection.py::test_unlocalized_rows_excluded +[gw7] [ 5%] PASSED pamica/tests/test_channel_selection.py::test_unlocalized_rows_excluded +pamica/tests/test_channel_selection.py::test_n_zero_returns_empty +[gw7] [ 5%] PASSED pamica/tests/test_channel_selection.py::test_n_zero_returns_empty +pamica/tests/test_channel_selection.py::test_seed_is_centroid_nearest +[gw7] [ 5%] PASSED pamica/tests/test_channel_selection.py::test_seed_is_centroid_nearest +pamica/tests/test_channel_selection.py::test_coincident_coordinates_stay_unique +[gw7] [ 5%] PASSED pamica/tests/test_channel_selection.py::test_coincident_coordinates_stay_unique +pamica/tests/test_component_layout.py::test_an_unmerged_payload_converts_block_for_block +[gw7] [ 5%] PASSED pamica/tests/test_component_layout.py::test_an_unmerged_payload_converts_block_for_block +pamica/tests/test_component_layout.py::test_a_one_dimensional_comp_list_is_refused +[gw7] [ 5%] PASSED pamica/tests/test_component_layout.py::test_a_one_dimensional_comp_list_is_refused +pamica/tests/test_component_layout.py::test_a_non_integer_comp_list_is_refused[float-ids] +[gw7] [ 5%] PASSED pamica/tests/test_component_layout.py::test_a_non_integer_comp_list_is_refused[float-ids] +pamica/tests/test_component_layout.py::test_a_non_integer_comp_list_is_refused[nan-id] +[gw7] [ 5%] PASSED pamica/tests/test_component_layout.py::test_a_non_integer_comp_list_is_refused[nan-id] +pamica/tests/test_component_layout.py::test_a_mismatched_A_shape_is_refused[transposed] +[gw7] [ 6%] PASSED pamica/tests/test_component_layout.py::test_a_mismatched_A_shape_is_refused[transposed] +pamica/tests/test_component_layout.py::test_a_mismatched_A_shape_is_refused[one-column-short] +[gw7] [ 6%] PASSED pamica/tests/test_component_layout.py::test_a_mismatched_A_shape_is_refused[one-column-short] +pamica/tests/test_component_layout.py::test_a_mismatched_A_shape_is_refused[three-dimensional] +[gw7] [ 6%] PASSED pamica/tests/test_component_layout.py::test_a_mismatched_A_shape_is_refused[three-dimensional] +pamica/tests/test_component_layout.py::test_an_out_of_range_id_is_refused[negative] +[gw7] [ 6%] PASSED pamica/tests/test_component_layout.py::test_an_out_of_range_id_is_refused[negative] +pamica/tests/test_component_layout.py::test_an_out_of_range_id_is_refused[past-the-end] +[gw7] [ 6%] PASSED pamica/tests/test_component_layout.py::test_an_out_of_range_id_is_refused[past-the-end] +pamica/tests/test_component_layout.py::test_an_id_repeated_within_a_model_is_refused +[gw7] [ 6%] PASSED pamica/tests/test_component_layout.py::test_an_id_repeated_within_a_model_is_refused +pamica/tests/test_component_layout.py::test_an_id_shared_across_models_asks_for_a_refit +[gw7] [ 6%] PASSED pamica/tests/test_component_layout.py::test_an_id_shared_across_models_asks_for_a_refit +pamica/tests/test_component_rows.py::test_numpy_rejects_other_pdftypes +[gw7] [ 6%] PASSED pamica/tests/test_component_rows.py::test_numpy_rejects_other_pdftypes +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-scale-ng-pdf0] +[gw3] [ 6%] PASSED pamica/tests/mlx_tests/test_mlx_restarts.py::test_best_of_n_returns_the_argmax_of_the_independent_fits +pamica/tests/mlx_tests/test_mlx_restarts.py::test_records_are_aligned_with_the_independent_fits +[gw4] [ 6%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_fit_path_is_unchanged_by_phase1 +[gw2] [ 6%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_with_newton[4-1] +pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_pcakeep_applies_with_annotation_rejection +pamica/tests/mlx_tests/test_mlx_pdf.py::test_logcosh_stability[88.0] +[gw2] [ 6%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_logcosh_stability[88.0] +pamica/tests/mlx_tests/test_mlx_pdf.py::test_logcosh_stability[90.0] +[gw2] [ 6%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_logcosh_stability[90.0] +pamica/tests/mlx_tests/test_mlx_pdf.py::test_logcosh_stability[500.0] +[gw2] [ 6%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_logcosh_stability[500.0] +pamica/tests/mlx_tests/test_mlx_pdf.py::test_logcosh_stability[1000.0] +[gw2] [ 6%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_logcosh_stability[1000.0] +pamica/tests/mlx_tests/test_mlx_pdf.py::test_sharing_with_adaptive_switcher_smoke +[gw3] [ 6%] PASSED pamica/tests/mlx_tests/test_mlx_restarts.py::test_records_are_aligned_with_the_independent_fits +pamica/tests/mlx_tests/test_mlx_restarts.py::test_derived_seeds_run_the_documented_sequence +[gw3] [ 6%] PASSED pamica/tests/mlx_tests/test_mlx_restarts.py::test_derived_seeds_run_the_documented_sequence +pamica/tests/mlx_tests/test_mlx_restarts.py::test_a_refit_repeats_the_same_search +[gw5] [ 6%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_n_iter_counts_the_e_steps_that_ran[torch-max_iter] +pamica/tests/mne_tests/test_mne_backends.py::test_n_iter_counts_the_e_steps_that_ran[torch-min_dll] +[gw7] [ 6%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-scale-ng-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m1-scale-ng-pdf0] +[gw3] [ 6%] PASSED pamica/tests/mlx_tests/test_mlx_restarts.py::test_a_refit_repeats_the_same_search +pamica/tests/mlx_tests/test_mlx_restarts.py::test_a_degenerate_restart_is_recorded_but_never_selected +[gw1] [ 7%] PASSED pamica/tests/mlx_tests/test_mlx_keepbest.py::test_pdftype1_forced_restore_rolls_back_n_kurt_done_with_pdtype +pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_inactive_under_share_comps_logs_and_never_restores +[gw3] [ 7%] PASSED pamica/tests/mlx_tests/test_mlx_restarts.py::test_a_degenerate_restart_is_recorded_but_never_selected +pamica/tests/mlx_tests/test_mlx_restarts.py::test_all_degenerate_restarts_keep_the_last_one +[gw3] [ 7%] PASSED pamica/tests/mlx_tests/test_mlx_restarts.py::test_all_degenerate_restarts_keep_the_last_one +pamica/tests/mlx_tests/test_mlx_restarts.py::test_a_crashing_restart_does_not_kill_the_search +[gw3] [ 7%] PASSED pamica/tests/mlx_tests/test_mlx_restarts.py::test_a_crashing_restart_does_not_kill_the_search +pamica/tests/mlx_tests/test_mlx_restarts.py::test_a_single_restart_still_raises +[gw3] [ 7%] PASSED pamica/tests/mlx_tests/test_mlx_restarts.py::test_a_single_restart_still_raises +pamica/tests/mlx_tests/test_mlx_restarts.py::test_all_restarts_crashing_keeps_the_degenerate_contract +[gw3] [ 7%] PASSED pamica/tests/mlx_tests/test_mlx_restarts.py::test_all_restarts_crashing_keeps_the_degenerate_contract +pamica/tests/mlx_tests/test_mlx_restarts.py::test_the_winners_tuned_block_size_is_restored +[gw7] [ 7%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m1-scale-ng-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-scale-ng-pdf0] +[gw4] [ 7%] PASSED pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_pcakeep_applies_with_annotation_rejection +pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_pcakeep_sources_are_timeline_shaped_and_rank_sized +[gw4] [ 7%] PASSED pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_pcakeep_sources_are_timeline_shaped_and_rank_sized +pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_pcakeep_scoring_is_timeline_aligned +[gw4] [ 7%] PASSED pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_pcakeep_scoring_is_timeline_aligned +pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_pcakeep_pmi_scores_good_samples_only +[gw3] [ 7%] PASSED pamica/tests/mlx_tests/test_mlx_restarts.py::test_the_winners_tuned_block_size_is_restored +pamica/tests/mlx_tests/test_mlx_scoring.py::test_model_loglik_reproduces_the_estep_forward_pass +[gw0] [ 7%] PASSED pamica/tests/mlx_tests/test_mlx_backend.py::test_backend_stable_on_full_data +pamica/tests/mlx_tests/test_mlx_backend.py::test_converged_ll_matches_torch_float32 +[gw7] [ 7%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-scale-ng-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-scale-ng-pdf1] +[gw2] [ 7%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_sharing_with_adaptive_switcher_smoke +pamica/tests/mlx_tests/test_mlx_pdf.py::test_get_pdftype_rejects_bad_model_idx +[gw3] [ 7%] PASSED pamica/tests/mlx_tests/test_mlx_scoring.py::test_model_loglik_reproduces_the_estep_forward_pass +pamica/tests/mlx_tests/test_mlx_scoring.py::test_model_loglik_is_the_estep_before_the_last_m_step +[gw2] [ 7%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_get_pdftype_rejects_bad_model_idx +pamica/tests/mlx_tests/test_mlx_pdf.py::test_get_pdftype_requires_fit +[gw2] [ 7%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_get_pdftype_requires_fit +pamica/tests/mlx_tests/test_mlx_pdf.py::test_sharing_newton_and_fixed_family_fit_completes +[gw5] [ 7%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_n_iter_counts_the_e_steps_that_ran[torch-min_dll] +pamica/tests/mne_tests/test_mne_backends.py::test_n_iter_counts_the_e_steps_that_ran[mlx-max_iter] +[gw1] [ 7%] PASSED pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_inactive_under_share_comps_logs_and_never_restores +pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_round_trips_through_state_dict +[gw7] [ 7%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-scale-ng-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-scale-ng-pdf1] +[gw1] [ 8%] PASSED pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_round_trips_through_state_dict +pamica/tests/mlx_tests/test_mlx_keepbest.py::test_missing_keep_best_key_loads_with_the_default +[gw4] [ 8%] PASSED pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_pcakeep_pmi_scores_good_samples_only +pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_pcakeep_mir_still_refuses_pca_reduction +[gw1] [ 8%] PASSED pamica/tests/mlx_tests/test_mlx_keepbest.py::test_missing_keep_best_key_loads_with_the_default +pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_does_not_rescue_a_diverged_fit_that_peaked_earlier +[gw4] [ 8%] PASSED pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_pcakeep_mir_still_refuses_pca_reduction +pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_pcakeep_export_is_rank_sized_and_orthonormal +[gw4] [ 8%] PASSED pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_pcakeep_export_is_rank_sized_and_orthonormal +pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_auto_rank_detection_applies_with_annotation_rejection +[gw7] [ 8%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-scale-ng-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-scale-newton-pdf0] +[gw5] [ 8%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_n_iter_counts_the_e_steps_that_ran[mlx-max_iter] +pamica/tests/mne_tests/test_mne_backends.py::test_n_iter_counts_the_e_steps_that_ran[mlx-min_dll] +[gw1] [ 8%] PASSED pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_does_not_rescue_a_diverged_fit_that_peaked_earlier +pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_restart_record_reports_the_restored_iterate +[gw3] [ 8%] PASSED pamica/tests/mlx_tests/test_mlx_scoring.py::test_model_loglik_is_the_estep_before_the_last_m_step +pamica/tests/mlx_tests/test_mlx_scoring.py::test_model_probability_is_normalized +[gw7] [ 8%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-scale-newton-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m1-scale-newton-pdf0] +[gw3] [ 8%] PASSED pamica/tests/mlx_tests/test_mlx_scoring.py::test_model_probability_is_normalized +pamica/tests/mlx_tests/test_mlx_scoring.py::test_single_model_probability_is_all_ones +[gw3] [ 8%] PASSED pamica/tests/mlx_tests/test_mlx_scoring.py::test_single_model_probability_is_all_ones +pamica/tests/mlx_tests/test_mlx_scoring.py::test_model_loglik_uses_stored_sphere_not_reprocess +[gw5] [ 8%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_n_iter_counts_the_e_steps_that_ran[mlx-min_dll] +pamica/tests/mne_tests/test_mne_meg.py::test_pre_whitener_is_one_scale_per_channel_type +[gw3] [ 8%] PASSED pamica/tests/mlx_tests/test_mlx_scoring.py::test_model_loglik_uses_stored_sphere_not_reprocess +pamica/tests/mlx_tests/test_mlx_scoring.py::test_model_loglik_requires_fit +[gw3] [ 8%] PASSED pamica/tests/mlx_tests/test_mlx_scoring.py::test_model_loglik_requires_fit +pamica/tests/mlx_tests/test_mlx_scoring.py::test_model_loglik_rejects_non_finite_input +[gw4] [ 8%] PASSED pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_auto_rank_detection_applies_with_annotation_rejection +pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_auto_rank_sources_are_timeline_shaped_and_rank_sized +[gw4] [ 8%] PASSED pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_auto_rank_sources_are_timeline_shaped_and_rank_sized +pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_auto_rank_scoring_is_timeline_aligned +[gw4] [ 8%] PASSED pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_auto_rank_scoring_is_timeline_aligned +pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_auto_rank_pmi_scores_good_samples_only +[gw7] [ 8%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m1-scale-newton-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-scale-newton-pdf0] +[gw3] [ 8%] PASSED pamica/tests/mlx_tests/test_mlx_scoring.py::test_model_loglik_rejects_non_finite_input +pamica/tests/mlx_tests/test_mlx_sharing.py::test_unscheduled_sharing_is_bit_identical[1] +[gw7] [ 9%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-scale-newton-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-scale-newton-pdf1] +[gw6] [ 9%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_mir_pmi_nbins_passthrough +pamica/tests/mne_tests/test_mne_wrapper.py::test_mir_pmi_respect_model_idx_bounds +[gw6] [ 9%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_mir_pmi_respect_model_idx_bounds +pamica/tests/mne_tests/test_mne_wrapper.py::test_mir_pmi_require_fit +[gw6] [ 9%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_mir_pmi_require_fit +pamica/tests/mne_tests/test_mne_wrapper.py::test_mir_pmi_refuse_degenerate +[gw3] [ 9%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_unscheduled_sharing_is_bit_identical[1] +pamica/tests/mlx_tests/test_mlx_sharing.py::test_unscheduled_sharing_is_bit_identical[2] +[gw2] [ 9%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_sharing_newton_and_fixed_family_fit_completes +pamica/tests/mlx_tests/test_mlx_pdf.py::test_sharing_newton_and_adaptive_switcher_fit_completes +[gw1] [ 9%] PASSED pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_restart_record_reports_the_restored_iterate +pamica/tests/mlx_tests/test_mlx_kitchen_sink.py::test_kitchen_sink_lifecycle +[gw4] [ 9%] PASSED pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_auto_rank_pmi_scores_good_samples_only +pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_auto_rank_mir_raises_pca_reduction_error +[gw4] [ 9%] PASSED pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_auto_rank_mir_raises_pca_reduction_error +pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_full_rank_mir_succeeds +[gw5] [ 9%] PASSED pamica/tests/mne_tests/test_mne_meg.py::test_pre_whitener_is_one_scale_per_channel_type +pamica/tests/mne_tests/test_mne_meg.py::test_scaling_equalizes_channel_type_variance +[gw3] [ 9%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_unscheduled_sharing_is_bit_identical[2] +pamica/tests/mlx_tests/test_mlx_sharing.py::test_default_multimodel_fit_leaves_every_component_distinct +[gw7] [ 9%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-scale-newton-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-scale-newton-pdf1] +[gw3] [ 9%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_default_multimodel_fit_leaves_every_component_distinct +pamica/tests/mlx_tests/test_mlx_sharing.py::test_two_model_share_fit_completes_and_merges +[gw7] [ 9%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-scale-newton-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-noscale-ng-pdf0] +[gw7] [ 9%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-noscale-ng-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m1-noscale-ng-pdf0] +[gw3] [ 9%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_two_model_share_fit_completes_and_merges +pamica/tests/mlx_tests/test_mlx_sharing.py::test_merge_on_the_final_iteration_completes +[gw3] [ 9%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_merge_on_the_final_iteration_completes +pamica/tests/mlx_tests/test_mlx_sharing.py::test_convergence_stop_can_fire_while_A_is_frozen +[gw3] [ 9%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_convergence_stop_can_fire_while_A_is_frozen +pamica/tests/mlx_tests/test_mlx_sharing.py::test_three_model_share_fit_completes +[gw5] [ 9%] PASSED pamica/tests/mne_tests/test_mne_meg.py::test_scaling_equalizes_channel_type_variance +pamica/tests/mne_tests/test_mne_meg.py::test_roundtrip_holds_for_mixed_channel_types +[gw2] [ 9%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_sharing_newton_and_adaptive_switcher_fit_completes +pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_round_trip_params_are_bit_identical +[gw7] [ 10%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m1-noscale-ng-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-noscale-ng-pdf0] +[gw2] [ 10%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_round_trip_params_are_bit_identical +pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_round_trip_config_and_extra_are_equal +[gw7] [ 10%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-noscale-ng-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-noscale-ng-pdf1] +[gw2] [ 10%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_round_trip_config_and_extra_are_equal +pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_round_trip_transform_is_bit_identical +[gw2] [ 10%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_round_trip_transform_is_bit_identical +pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_round_trip_accessors_are_bit_identical +[gw1] [ 10%] PASSED pamica/tests/mlx_tests/test_mlx_kitchen_sink.py::test_kitchen_sink_lifecycle +pamica/tests/mlx_tests/test_mlx_llt_stash.py::test_llt_invariant_holds_for_a_default_fit +[gw2] [ 10%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_round_trip_accessors_are_bit_identical +pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_round_trip_sensor_mixing_matrix_is_close +[gw4] [ 10%] PASSED pamica/tests/mne_tests/test_annotation_rank_reduction.py::test_full_rank_mir_succeeds +pamica/tests/mne_tests/test_bad_annotation.py::test_fit_omits_bad_annotated_samples +[gw2] [ 10%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_round_trip_sensor_mixing_matrix_is_close +pamica/tests/mlx_tests/test_mlx_persistence.py::test_save_load_round_trip_bit_identical +[gw1] [ 10%] PASSED pamica/tests/mlx_tests/test_mlx_llt_stash.py::test_llt_invariant_holds_for_a_default_fit +pamica/tests/mlx_tests/test_mlx_llt_stash.py::test_llt_is_the_logsumexp_of_lht +[gw3] [ 10%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_three_model_share_fit_completes +pamica/tests/mlx_tests/test_mlx_sharing.py::test_rank_reduced_share_fit_completes +[gw2] [ 10%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_save_load_round_trip_bit_identical +pamica/tests/mlx_tests/test_mlx_persistence.py::test_save_load_npz_layout +[gw1] [ 10%] PASSED pamica/tests/mlx_tests/test_mlx_llt_stash.py::test_llt_is_the_logsumexp_of_lht +pamica/tests/mlx_tests/test_mlx_llt_stash.py::test_llt_stashed_at_one_model_too +[gw2] [ 10%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_save_load_npz_layout +pamica/tests/mlx_tests/test_mlx_persistence.py::test_round_trip_bit_identical_on_rank_reduced_fit +[gw1] [ 10%] PASSED pamica/tests/mlx_tests/test_mlx_llt_stash.py::test_llt_stashed_at_one_model_too +pamica/tests/mlx_tests/test_mlx_llt_stash.py::test_stashed_llt_is_one_m_step_behind_the_written_parameters +[gw7] [ 10%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-noscale-ng-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-noscale-ng-pdf1] +[gw2] [ 10%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_round_trip_bit_identical_on_rank_reduced_fit +pamica/tests/mlx_tests/test_mlx_persistence.py::test_round_trip_preserves_forced_merge_state +[gw2] [ 10%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_round_trip_preserves_forced_merge_state +pamica/tests/mlx_tests/test_mlx_persistence.py::test_round_trip_preserves_adaptive_switcher_state +[gw7] [ 10%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-noscale-ng-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-noscale-newton-pdf0] +[gw5] [ 10%] PASSED pamica/tests/mne_tests/test_mne_meg.py::test_roundtrip_holds_for_mixed_channel_types +pamica/tests/mne_tests/test_mne_meg.py::test_eeg_only_scaling_is_absorbed_by_the_sphere +[gw1] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_llt_stash.py::test_stashed_llt_is_one_m_step_behind_the_written_parameters +pamica/tests/mlx_tests/test_mlx_llt_stash.py::test_llt_stash_is_none_before_any_estep_ran +[gw1] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_llt_stash.py::test_llt_stash_is_none_before_any_estep_ran +pamica/tests/mlx_tests/test_mlx_llt_stash.py::test_keep_best_restore_rolls_the_llt_stash_back +[gw3] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_rank_reduced_share_fit_completes +pamica/tests/mlx_tests/test_mlx_sharing.py::test_second_identify_call_does_not_resurrect_merged_columns +[gw3] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_second_identify_call_does_not_resurrect_merged_columns +pamica/tests/mlx_tests/test_mlx_sharing.py::test_merged_away_columns_keep_their_last_finite_value +[gw7] [ 11%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-noscale-newton-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m1-noscale-newton-pdf0] +[gw3] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_merged_away_columns_keep_their_last_finite_value +pamica/tests/mlx_tests/test_mlx_sharing.py::test_a_frozen_window_matches_the_torch_schedule[True-2] +[gw3] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_a_frozen_window_matches_the_torch_schedule[True-2] +pamica/tests/mlx_tests/test_mlx_sharing.py::test_a_frozen_window_matches_the_torch_schedule[False-2] +[gw3] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_a_frozen_window_matches_the_torch_schedule[False-2] +pamica/tests/mlx_tests/test_mlx_sharing.py::test_a_frozen_window_matches_the_torch_schedule[False-1] +[gw3] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_a_frozen_window_matches_the_torch_schedule[False-1] +pamica/tests/mlx_tests/test_mlx_sharing.py::test_a_frozen_applies_to_a_single_model_without_sharing +[gw3] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_a_frozen_applies_to_a_single_model_without_sharing +pamica/tests/mlx_tests/test_mlx_sharing.py::test_freeze_holds_A_while_the_mixture_keeps_moving +[gw2] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_round_trip_preserves_adaptive_switcher_state +pamica/tests/mlx_tests/test_mlx_persistence.py::test_round_trip_preserves_newton_fallback_count +[gw1] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_llt_stash.py::test_keep_best_restore_rolls_the_llt_stash_back +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_composes_unmixing_the_documented_way +[gw2] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_round_trip_preserves_newton_fallback_count +pamica/tests/mlx_tests/test_mlx_persistence.py::test_round_trip_preserves_multi_restart_records +[gw1] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_composes_unmixing_the_documented_way +pamica/tests/mlx_tests/test_mlx_mir.py::test_pmi_matches_pairwise_mi_on_transform +[gw6] [ 11%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_mir_pmi_refuse_degenerate +pamica/tests/mne_tests/test_pca_residual.py::test_full_rank_export_is_unchanged +[gw3] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_freeze_holds_A_while_the_mixture_keeps_moving +pamica/tests/mlx_tests/test_mlx_sharing.py::test_share_constructor_validation[kwargs0-share_start] +[gw3] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_share_constructor_validation[kwargs0-share_start] +pamica/tests/mlx_tests/test_mlx_sharing.py::test_share_constructor_validation[kwargs1-share_iter] +[gw3] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_share_constructor_validation[kwargs1-share_iter] +pamica/tests/mlx_tests/test_mlx_sharing.py::test_share_constructor_validation[kwargs2-share_iter] +[gw2] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_round_trip_preserves_multi_restart_records +[gw3] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_share_constructor_validation[kwargs2-share_iter] +pamica/tests/mlx_tests/test_mlx_sharing.py::test_share_constructor_validation[kwargs3-comp_thresh] +[gw3] [ 11%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_share_constructor_validation[kwargs3-comp_thresh] +pamica/tests/mlx_tests/test_mlx_sharing.py::test_share_constructor_validation[kwargs4-comp_thresh] +[gw3] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_share_constructor_validation[kwargs4-comp_thresh] +pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_refuses_unfitted_model +pamica/tests/mlx_tests/test_mlx_sharing.py::test_share_settings_are_not_validated_when_sharing_is_off +[gw2] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_refuses_unfitted_model +pamica/tests/mlx_tests/test_mlx_persistence.py::test_save_refuses_unfitted_model +[gw3] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_share_settings_are_not_validated_when_sharing_is_off +[gw2] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_save_refuses_unfitted_model +pamica/tests/mlx_tests/test_mlx_sharing.py::test_single_model_sharing_is_accepted_and_inert +pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_refuses_degenerate_fit +[gw2] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_refuses_degenerate_fit +pamica/tests/mlx_tests/test_mlx_persistence.py::test_save_refuses_degenerate_fit +[gw2] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_save_refuses_degenerate_fit +pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_refuses_force_set_nonfinite_param_naming_it +[gw2] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_state_dict_refuses_force_set_nonfinite_param_naming_it +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_wrong_format_version +[gw3] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_single_model_sharing_is_accepted_and_inert +pamica/tests/mlx_tests/test_mlx_sharing.py::test_non_finite_sphere_fails_loudly +[gw2] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_wrong_format_version +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_missing_section[config] +[gw3] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_non_finite_sphere_fails_loudly +pamica/tests/mlx_tests/test_mlx_sharing.py::test_pinv_sphere_before_fit_raises +[gw3] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_sharing.py::test_pinv_sphere_before_fit_raises +pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_requires_fitted_model +[gw3] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_requires_fitted_model +pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_require_fitted_model[get_mixing_matrix] +[gw3] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_require_fitted_model[get_mixing_matrix] +pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_require_fitted_model[get_unmixing_matrix] +[gw3] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_require_fitted_model[get_unmixing_matrix] +pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_require_fitted_model[get_sensor_mixing_matrix] +[gw3] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_require_fitted_model[get_sensor_mixing_matrix] +pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_require_fitted_model[get_rho] +[gw3] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_require_fitted_model[get_rho] +pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_require_fitted_model[variance_order] +[gw3] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_accessors_require_fitted_model[variance_order] +pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_rejects_invalid_model_idx[-1] +[gw2] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_missing_section[config] +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_missing_section[params] +[gw1] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_pmi_matches_pairwise_mi_on_transform +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_and_pmi_honour_model_idx +[gw3] [ 12%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_rejects_invalid_model_idx[-1] +pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_rejects_invalid_model_idx[1] +[gw7] [ 13%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m1-noscale-newton-pdf0] +[gw2] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_missing_section[params] +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_missing_section[extra] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-noscale-newton-pdf0] +[gw2] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_missing_section[extra] +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_missing_param +[gw3] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_rejects_invalid_model_idx[1] +pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_rejects_invalid_model_idx[2] +[gw2] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_missing_param +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_shape_drift +[gw2] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_shape_drift +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_comp_list_shape_drift +[gw3] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_rejects_invalid_model_idx[2] +pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_rejects_invalid_model_idx[100] +[gw7] [ 13%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-noscale-newton-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-noscale-newton-pdf1] +[gw2] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_comp_list_shape_drift +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_shape_drift_for_every_param[mean] +[gw3] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_transform.py::test_transform_rejects_invalid_model_idx[100] +pamica/tests/test_component_rows.py::test_the_metric_compares_the_sensor_maps[mlx] +[gw2] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_shape_drift_for_every_param[mean] +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_shape_drift_for_every_param[W] +[gw4] [ 13%] PASSED pamica/tests/mne_tests/test_bad_annotation.py::test_fit_omits_bad_annotated_samples +pamica/tests/mne_tests/test_bad_annotation.py::test_mask_indexes_exactly_the_fitted_columns +[gw4] [ 13%] PASSED pamica/tests/mne_tests/test_bad_annotation.py::test_mask_indexes_exactly_the_fitted_columns +pamica/tests/mne_tests/test_bad_annotation.py::test_fit_equals_manual_column_drop +[gw2] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_shape_drift_for_every_param[W] +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_shape_drift_for_every_param[mu] +[gw1] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_and_pmi_honour_model_idx +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_and_pmi_honour_nbins +[gw2] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_shape_drift_for_every_param[mu] +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_shape_drift_for_every_param[gm] +[gw2] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_shape_drift_for_every_param[gm] +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_nan_comp_list +[gw2] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_nan_comp_list +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_non_integer_valued_comp_list +[gw2] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_non_integer_valued_comp_list +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_accepts_whole_valued_float_comp_list +[gw2] [ 13%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_accepts_whole_valued_float_comp_list +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_nonfinite_mu +[gw2] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_nonfinite_mu +pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_singular_restored_W +[gw1] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_and_pmi_honour_nbins +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_real_fitted_unmixing_is_large_and_positive +[gw2] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_from_state_dict_rejects_singular_restored_W +pamica/tests/mlx_tests/test_mlx_persistence.py::test_load_rejects_wrong_format_version +[gw2] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_load_rejects_wrong_format_version +pamica/tests/mlx_tests/test_mlx_persistence.py::test_load_rejects_truncated_file +[gw7] [ 14%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-noscale-newton-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-noscale-newton-pdf1] +[gw2] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_load_rejects_truncated_file +pamica/tests/mlx_tests/test_mlx_persistence.py::test_load_rejects_byte_truncated_file +[gw1] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_real_fitted_unmixing_is_large_and_positive +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_on_a_share_comps_merged_model +[gw2] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_load_rejects_byte_truncated_file +pamica/tests/mlx_tests/test_mlx_persistence.py::test_load_rejects_missing_param_array +[gw2] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_persistence.py::test_load_rejects_missing_param_array +pamica/tests/mlx_tests/test_mlx_reject.py::test_reject_param_validation +[gw2] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_reject_param_validation +pamica/tests/mlx_tests/test_mlx_reject.py::test_reject_defaults_match_torch +[gw2] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_reject_defaults_match_torch +pamica/tests/mlx_tests/test_mlx_reject.py::test_do_reject_false_leaves_good_idx_unset +[gw7] [ 14%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-noscale-newton-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-scale-ng-pdf0] +[gw2] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_do_reject_false_leaves_good_idx_unset +pamica/tests/mlx_tests/test_mlx_reject.py::test_rejection_shrinks_good_sample_set_on_the_expected_schedule +[gw6] [ 14%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_full_rank_export_is_unchanged +pamica/tests/mne_tests/test_pca_residual.py::test_full_rank_two_model_export_is_unchanged +[gw2] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_rejection_shrinks_good_sample_set_on_the_expected_schedule +pamica/tests/mlx_tests/test_mlx_reject.py::test_maxrej_zero_disables_rejection +[gw5] [ 14%] PASSED pamica/tests/mne_tests/test_mne_meg.py::test_eeg_only_scaling_is_absorbed_by_the_sphere +pamica/tests/mne_tests/test_mne_meg.py::test_rank_deficient_fit_exports_and_roundtrips +[gw2] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_maxrej_zero_disables_rejection +pamica/tests/mlx_tests/test_mlx_reject.py::test_none_rejected_in_a_pass_still_counts_as_a_pass +[gw2] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_none_rejected_in_a_pass_still_counts_as_a_pass +pamica/tests/mlx_tests/test_mlx_reject.py::test_multimodel_rejection_keeps_gm_and_c_finite +[gw1] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_on_a_share_comps_merged_model +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_requires_a_fitted_model +[gw1] [ 14%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_requires_a_fitted_model +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_step_populates_history_at_right_iterations +[gw1] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_step_populates_history_at_right_iterations +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_step_zero_leaves_history_empty +[gw2] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_multimodel_rejection_keeps_gm_and_c_finite +pamica/tests/mlx_tests/test_mlx_reject.py::test_keep_best_inactive_under_do_reject_warns +[gw7] [ 15%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-scale-ng-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m2-scale-ng-pdf0] +[gw1] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_step_zero_leaves_history_empty +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_step_negative_raises +[gw1] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_step_negative_raises +pamica/tests/mlx_tests/test_mlx_mir.py::test_max_iter_zero_raises +[gw1] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_max_iter_zero_raises +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_step_zero_matches_omitted_argument +[gw2] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_keep_best_inactive_under_do_reject_warns +pamica/tests/mlx_tests/test_mlx_reject.py::test_keep_best_inactive_reason_prefers_do_reject_when_both_are_on +[gw1] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_step_zero_matches_omitted_argument +pamica/tests/mlx_tests/test_mlx_mir.py::test_failing_mir_waypoint_does_not_kill_the_fit +[gw1] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_failing_mir_waypoint_does_not_kill_the_fit +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_raises_under_auto_detected_rank_reduction +[gw2] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_keep_best_inactive_reason_prefers_do_reject_when_both_are_on +[gw1] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_raises_under_auto_detected_rank_reduction +pamica/tests/mlx_tests/test_mlx_reject.py::test_keep_best_restore_never_fires_under_do_reject +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_full_rank_succeeds +[gw4] [ 15%] PASSED pamica/tests/mne_tests/test_bad_annotation.py::test_fit_equals_manual_column_drop +pamica/tests/mne_tests/test_bad_annotation.py::test_reject_false_keeps_all_samples +[gw1] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_full_rank_succeeds +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_step_on_rank_reduced_data_completes_with_one_warned_nan_waypoint +[gw1] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_step_on_rank_reduced_data_completes_with_one_warned_nan_waypoint +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_history_survives_keep_best_restore +[gw5] [ 15%] PASSED pamica/tests/mne_tests/test_mne_meg.py::test_rank_deficient_fit_exports_and_roundtrips +pamica/tests/mne_tests/test_mne_meg.py::test_rank_deficient_components_are_sensor_shaped +[gw1] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_history_survives_keep_best_restore +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_history_empty_after_state_dict_round_trip +[gw1] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_history_empty_after_state_dict_round_trip +pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_history_survives_best_of_n_restarts_winner_not_last +[gw2] [ 15%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_keep_best_restore_never_fires_under_do_reject +pamica/tests/mlx_tests/test_mlx_reject.py::test_do_reject_survives_best_of_n_restarts_winner_not_last +[gw4] [ 15%] PASSED pamica/tests/mne_tests/test_bad_annotation.py::test_reject_false_keeps_all_samples +pamica/tests/mne_tests/test_bad_annotation.py::test_non_bad_annotations_are_inert +[gw7] [ 15%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m2-scale-ng-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-scale-ng-pdf0] +[gw2] [ 16%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_do_reject_survives_best_of_n_restarts_winner_not_last +pamica/tests/mlx_tests/test_mlx_reject.py::test_do_reject_with_a_genuine_share_comps_merge +[gw7] [ 16%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-scale-ng-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-scale-ng-pdf1] +[gw5] [ 16%] PASSED pamica/tests/mne_tests/test_mne_meg.py::test_rank_deficient_components_are_sensor_shaped +pamica/tests/mne_tests/test_mne_meg.py::test_rank_deficient_apply_reconstructs +[gw1] [ 16%] PASSED pamica/tests/mlx_tests/test_mlx_mir.py::test_mir_history_survives_best_of_n_restarts_winner_not_last +pamica/tests/mlx_tests/test_mlx_newton.py::test_newton_fit_is_stable_on_full_data[1] +[gw4] [ 16%] PASSED pamica/tests/mne_tests/test_bad_annotation.py::test_non_bad_annotations_are_inert +pamica/tests/mne_tests/test_bad_annotation.py::test_start_stop_composes_with_rejection +[gw2] [ 16%] PASSED pamica/tests/mlx_tests/test_mlx_reject.py::test_do_reject_with_a_genuine_share_comps_merge +pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[torch--1] +[gw2] [ 16%] PASSED pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[torch--1] +pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[torch-2.5] +[gw2] [ 16%] PASSED pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[torch-2.5] +pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[torch-True] +[gw2] [ 16%] PASSED pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[torch-True] +pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[numpy-0] +[gw2] [ 16%] PASSED pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[numpy-0] +pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[numpy--1] +[gw2] [ 16%] PASSED pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[numpy--1] +pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[numpy-2.5] +[gw2] [ 16%] PASSED pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[numpy-2.5] +pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[numpy-True] +[gw2] [ 16%] PASSED pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[numpy-True] +pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[mlx-0] +[gw2] [ 16%] PASSED pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[mlx-0] +pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[mlx--1] +[gw2] [ 16%] PASSED pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[mlx--1] +pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[mlx-2.5] +[gw2] [ 16%] PASSED pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[mlx-2.5] +pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[mlx-True] +[gw2] [ 16%] PASSED pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[mlx-True] +pamica/tests/test_doscaling_rows.py::test_doscaling_matches_the_seeded_reference[1] +[gw2] [ 16%] SKIPPED pamica/tests/test_doscaling_rows.py::test_doscaling_matches_the_seeded_reference[1] +pamica/tests/test_doscaling_rows.py::test_doscaling_matches_the_seeded_reference[2] +[gw2] [ 16%] SKIPPED pamica/tests/test_doscaling_rows.py::test_doscaling_matches_the_seeded_reference[2] +pamica/tests/test_fortran_adapter.py::test_write_fdt_roundtrip +[gw2] [ 16%] PASSED pamica/tests/test_fortran_adapter.py::test_write_fdt_roundtrip +pamica/tests/test_fortran_adapter.py::test_write_fdt_channel_subset +[gw2] [ 17%] PASSED pamica/tests/test_fortran_adapter.py::test_write_fdt_channel_subset +pamica/tests/test_fortran_adapter.py::test_parse_fortran_out +[gw2] [ 17%] PASSED pamica/tests/test_fortran_adapter.py::test_parse_fortran_out +pamica/tests/test_fortran_adapter.py::test_write_fortran_param +[gw2] [ 17%] PASSED pamica/tests/test_fortran_adapter.py::test_write_fortran_param +pamica/tests/test_fortran_adapter.py::test_write_fortran_param_multimodel +[gw2] [ 17%] PASSED pamica/tests/test_fortran_adapter.py::test_write_fortran_param_multimodel +pamica/tests/test_fortran_adapter.py::test_fortran_availability_and_dispatch +[gw2] [ 17%] PASSED pamica/tests/test_fortran_adapter.py::test_fortran_availability_and_dispatch +pamica/tests/test_fortran_adapter.py::test_run_fortran_raises_on_nonzero_exit +[gw2] [ 17%] PASSED pamica/tests/test_fortran_adapter.py::test_run_fortran_raises_on_nonzero_exit +pamica/tests/test_fortran_adapter.py::test_run_fortran_smoke +[gw2] [ 17%] SKIPPED pamica/tests/test_fortran_adapter.py::test_run_fortran_smoke +pamica/tests/test_fortran_adapter.py::test_run_fortran_never_reports_a_bogus_zero +[gw2] [ 17%] SKIPPED pamica/tests/test_fortran_adapter.py::test_run_fortran_never_reports_a_bogus_zero +pamica/tests/test_fortran_adapter.py::test_run_fortran_requires_two_timed_iters +[gw2] [ 17%] SKIPPED pamica/tests/test_fortran_adapter.py::test_run_fortran_requires_two_timed_iters +pamica/tests/test_fortran_param_forwarding.py::test_accepted_keys_come_from_the_reference_source +[gw2] [ 17%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_accepted_keys_come_from_the_reference_source +pamica/tests/test_fortran_param_forwarding.py::test_missing_source_disables_filtering +[gw2] [ 17%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_missing_source_disables_filtering +pamica/tests/test_fortran_param_forwarding.py::test_do_newton_is_forwarded +[gw2] [ 17%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_do_newton_is_forwarded +pamica/tests/test_fortran_param_forwarding.py::test_block_size_is_forwarded +[gw2] [ 17%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_block_size_is_forwarded +pamica/tests/test_fortran_param_forwarding.py::test_aliased_names_are_translated +[gw2] [ 17%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_aliased_names_are_translated +pamica/tests/test_fortran_param_forwarding.py::test_canonical_names_are_also_translated +[gw2] [ 17%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_canonical_names_are_also_translated +pamica/tests/test_fortran_param_forwarding.py::test_load_sample_data_writes_fortran_spellings_end_to_end +[gw2] [ 17%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_load_sample_data_writes_fortran_spellings_end_to_end +pamica/tests/test_fortran_param_forwarding.py::test_keys_absent_from_the_template_are_appended +[gw2] [ 17%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_keys_absent_from_the_template_are_appended +pamica/tests/test_fortran_param_forwarding.py::test_overrides_beat_params +[gw2] [ 17%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_overrides_beat_params +pamica/tests/test_fortran_param_forwarding.py::test_untouched_template_values_survive +[gw2] [ 17%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_untouched_template_values_survive +pamica/tests/test_fortran_param_forwarding.py::test_unsupported_key_is_reported +[gw2] [ 17%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_unsupported_key_is_reported +pamica/tests/test_fortran_param_forwarding.py::test_python_only_keys_are_not_reported +[gw2] [ 17%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_python_only_keys_are_not_reported +pamica/tests/test_fortran_param_forwarding.py::test_real_params_json_produces_a_parseable_file +[gw2] [ 18%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_real_params_json_produces_a_parseable_file +pamica/tests/test_fortran_param_forwarding.py::test_list_values_are_space_separated +[gw2] [ 18%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_list_values_are_space_separated +pamica/tests/test_fortran_param_forwarding.py::test_default_reference_binary_falls_back_loudly +[gw2] [ 18%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_default_reference_binary_falls_back_loudly +pamica/tests/test_fortran_param_forwarding.py::test_env_override_selects_the_native_engine +[gw2] [ 18%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_env_override_selects_the_native_engine +pamica/tests/test_fortran_param_forwarding.py::test_backend_flag_parses_names_lists_and_all +[gw2] [ 18%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_backend_flag_parses_names_lists_and_all +pamica/tests/test_fortran_param_forwarding.py::test_backend_flag_rejects_unknown_names[cuda] +[gw2] [ 18%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_backend_flag_rejects_unknown_names[cuda] +pamica/tests/test_fortran_param_forwarding.py::test_backend_flag_rejects_unknown_names[torch,] +[gw2] [ 18%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_backend_flag_rejects_unknown_names[torch,] +pamica/tests/test_fortran_param_forwarding.py::test_backend_flag_rejects_unknown_names[] +[gw2] [ 18%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_backend_flag_rejects_unknown_names[] +pamica/tests/test_fortran_params.py::TestZeroRecognizedKeysGuard::test_genuinely_empty_file_raises +[gw2] [ 18%] PASSED pamica/tests/test_fortran_params.py::TestZeroRecognizedKeysGuard::test_genuinely_empty_file_raises +pamica/tests/test_fortran_params.py::TestZeroRecognizedKeysGuard::test_comment_only_file_raises +[gw2] [ 18%] PASSED pamica/tests/test_fortran_params.py::TestZeroRecognizedKeysGuard::test_comment_only_file_raises +pamica/tests/test_fortran_params.py::TestFromParamsFileIntegration::test_param_extension_is_detected +[gw2] [ 18%] PASSED pamica/tests/test_fortran_params.py::TestFromParamsFileIntegration::test_param_extension_is_detected +pamica/tests/test_fortran_params.py::TestFromParamsFileIntegration::test_kwargs_still_override_the_param_path +[gw2] [ 18%] PASSED pamica/tests/test_fortran_params.py::TestFromParamsFileIntegration::test_kwargs_still_override_the_param_path +pamica/tests/test_fortran_params.py::TestFromParamsFileIntegration::test_param_and_json_agree_on_model_construction +[gw2] [ 18%] PASSED pamica/tests/test_fortran_params.py::TestFromParamsFileIntegration::test_param_and_json_agree_on_model_construction +pamica/tests/test_fortran_params.py::TestFromParamsFileIntegration::test_param_extension_with_json_content_is_sniffed_as_json +[gw2] [ 18%] PASSED pamica/tests/test_fortran_params.py::TestFromParamsFileIntegration::test_param_extension_with_json_content_is_sniffed_as_json +pamica/tests/test_fortran_params.py::TestFromParamsFileIntegration::test_compact_json_named_param_does_not_silently_default +[gw2] [ 18%] PASSED pamica/tests/test_fortran_params.py::TestFromParamsFileIntegration::test_compact_json_named_param_does_not_silently_default +pamica/tests/test_fortran_params.py::TestFitAppliesFileDefaults::test_file_defaults_reach_the_fitted_backend +[gw4] [ 18%] PASSED pamica/tests/mne_tests/test_bad_annotation.py::test_start_stop_composes_with_rejection +pamica/tests/mne_tests/test_bad_annotation.py::test_model_probability_is_timeline_aligned +[gw6] [ 18%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_full_rank_two_model_export_is_unchanged +pamica/tests/mne_tests/test_pca_residual.py::test_full_rank_apply_is_unchanged[exclude0] +[gw6] [ 18%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_full_rank_apply_is_unchanged[exclude0] +pamica/tests/mne_tests/test_pca_residual.py::test_full_rank_apply_is_unchanged[exclude1] +[gw6] [ 18%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_full_rank_apply_is_unchanged[exclude1] +pamica/tests/mne_tests/test_pca_residual.py::test_fitted_basis_is_full_and_exported_unaltered +[gw4] [ 18%] PASSED pamica/tests/mne_tests/test_bad_annotation.py::test_model_probability_is_timeline_aligned +pamica/tests/mne_tests/test_bad_annotation.py::test_scoring_follows_the_passed_instances_annotations +[gw4] [ 19%] PASSED pamica/tests/mne_tests/test_bad_annotation.py::test_scoring_follows_the_passed_instances_annotations +pamica/tests/mne_tests/test_bad_annotation.py::test_mir_scores_good_samples_only +[gw4] [ 19%] PASSED pamica/tests/mne_tests/test_bad_annotation.py::test_mir_scores_good_samples_only +pamica/tests/mne_tests/test_bad_annotation.py::test_epochs_fit_is_unaffected +[gw5] [ 19%] PASSED pamica/tests/mne_tests/test_mne_meg.py::test_rank_deficient_apply_reconstructs +pamica/tests/mne_tests/test_mne_wrapper.py::test_get_sources_matches_amica_transform +[gw2] [ 19%] PASSED pamica/tests/test_fortran_params.py::TestFitAppliesFileDefaults::test_file_defaults_reach_the_fitted_backend +pamica/tests/test_fortran_params.py::TestFitAppliesFileDefaults::test_explicit_kwargs_win_over_file_defaults +[gw2] [ 19%] PASSED pamica/tests/test_fortran_params.py::TestFitAppliesFileDefaults::test_explicit_kwargs_win_over_file_defaults +pamica/tests/test_fortran_params.py::TestFitAppliesFileDefaults::test_unhandled_file_settings_warn_once +[gw7] [ 19%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-scale-ng-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-scale-ng-pdf1] +[gw2] [ 19%] PASSED pamica/tests/test_fortran_params.py::TestFitAppliesFileDefaults::test_unhandled_file_settings_warn_once +pamica/tests/test_fortran_params.py::TestFitAppliesFileDefaults::test_checkpoint_settings_warn_not_applied +[gw7] [ 19%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-scale-ng-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-scale-newton-pdf0] +[gw2] [ 19%] PASSED pamica/tests/test_fortran_params.py::TestFitAppliesFileDefaults::test_checkpoint_settings_warn_not_applied +pamica/tests/test_fortran_params.py::TestFitAppliesFileDefaults::test_json_alias_settings_now_reach_the_backend +[gw4] [ 19%] PASSED pamica/tests/mne_tests/test_bad_annotation.py::test_epochs_fit_is_unaffected +pamica/tests/mne_tests/test_bad_annotation.py::test_nan_inside_bad_segment_is_tolerated +[gw6] [ 19%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_fitted_basis_is_full_and_exported_unaltered +pamica/tests/mne_tests/test_pca_residual.py::test_exported_basis_is_orthonormal +[gw6] [ 19%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_exported_basis_is_orthonormal +pamica/tests/mne_tests/test_pca_residual.py::test_residual_variances_are_the_discarded_eigenvalues +[gw6] [ 19%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_residual_variances_are_the_discarded_eigenvalues +pamica/tests/mne_tests/test_pca_residual.py::test_apply_restores_the_residual +[gw6] [ 19%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_apply_restores_the_residual +pamica/tests/mne_tests/test_pca_residual.py::test_n_pca_components_restores_the_reference_reconstruction +[gw6] [ 19%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_n_pca_components_restores_the_reference_reconstruction +pamica/tests/mne_tests/test_pca_residual.py::test_exclude_leaves_the_residual_untouched[0] +[gw6] [ 19%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_exclude_leaves_the_residual_untouched[0] +pamica/tests/mne_tests/test_pca_residual.py::test_exclude_leaves_the_residual_untouched[19] +[gw6] [ 19%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_exclude_leaves_the_residual_untouched[19] +pamica/tests/mne_tests/test_pca_residual.py::test_get_sources_matches_amica_transform +[gw6] [ 19%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_get_sources_matches_amica_transform +pamica/tests/mne_tests/test_pca_residual.py::test_residual_export_is_logged +[gw2] [ 19%] PASSED pamica/tests/test_fortran_params.py::TestFitAppliesFileDefaults::test_json_alias_settings_now_reach_the_backend +pamica/tests/test_fortran_params.py::TestFitAppliesFileDefaults::test_fit_without_from_params_file_is_unaffected +[gw2] [ 19%] PASSED pamica/tests/test_fortran_params.py::TestFitAppliesFileDefaults::test_fit_without_from_params_file_is_unaffected +pamica/tests/test_fortran_params.py::TestReadParamsFile::test_param_and_json_agree_on_overlapping_settings +[gw2] [ 20%] PASSED pamica/tests/test_fortran_params.py::TestReadParamsFile::test_param_and_json_agree_on_overlapping_settings +pamica/tests/test_fortran_params.py::TestReadParamsFile::test_json_top_level_must_be_an_object +[gw2] [ 20%] PASSED pamica/tests/test_fortran_params.py::TestReadParamsFile::test_json_top_level_must_be_an_object +pamica/tests/test_fortran_params.py::TestReadParamsFile::test_json_alias_and_canonical_key_conflict_raises +[gw2] [ 20%] PASSED pamica/tests/test_fortran_params.py::TestReadParamsFile::test_json_alias_and_canonical_key_conflict_raises +pamica/tests/test_fortran_params.py::TestReadParamsFile::test_json_alias_is_translated_to_canonical +[gw2] [ 20%] PASSED pamica/tests/test_fortran_params.py::TestReadParamsFile::test_json_alias_is_translated_to_canonical +pamica/tests/test_fortran_params.py::TestReadParamsFile::test_json_non_aliased_keys_pass_through_unchanged +[gw2] [ 20%] PASSED pamica/tests/test_fortran_params.py::TestReadParamsFile::test_json_non_aliased_keys_pass_through_unchanged +pamica/tests/test_fortran_params.py::TestReadParamsFile::test_unparsable_non_json_text_raises +[gw2] [ 20%] PASSED pamica/tests/test_fortran_params.py::TestReadParamsFile::test_unparsable_non_json_text_raises +pamica/tests/test_fortran_params.py::TestReadParamsFile::test_fortran_text_still_dispatches_to_the_fortran_reader +[gw2] [ 20%] PASSED pamica/tests/test_fortran_params.py::TestReadParamsFile::test_fortran_text_still_dispatches_to_the_fortran_reader +pamica/tests/test_fortran_params.py::TestReadParamsFile::test_alias_table_derived_from_fortran_renames_plus_share_int +[gw2] [ 20%] PASSED pamica/tests/test_fortran_params.py::TestReadParamsFile::test_alias_table_derived_from_fortran_renames_plus_share_int +pamica/tests/test_initial_mixing.py::test_the_shared_draw_is_the_reference_recipe[1] +[gw2] [ 20%] PASSED pamica/tests/test_initial_mixing.py::test_the_shared_draw_is_the_reference_recipe[1] +pamica/tests/test_initial_mixing.py::test_the_shared_draw_is_the_reference_recipe[2] +[gw2] [ 20%] PASSED pamica/tests/test_initial_mixing.py::test_the_shared_draw_is_the_reference_recipe[2] +pamica/tests/test_initial_mixing.py::test_the_shared_draw_is_the_reference_recipe[3] +[gw2] [ 20%] PASSED pamica/tests/test_initial_mixing.py::test_the_shared_draw_is_the_reference_recipe[3] +pamica/tests/test_initial_mixing.py::test_normalizing_a_zero_or_nan_component_raises[zero] +[gw2] [ 20%] PASSED pamica/tests/test_initial_mixing.py::test_normalizing_a_zero_or_nan_component_raises[zero] +pamica/tests/test_initial_mixing.py::test_normalizing_a_zero_or_nan_component_raises[nan] +[gw2] [ 20%] PASSED pamica/tests/test_initial_mixing.py::test_normalizing_a_zero_or_nan_component_raises[nan] +pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[torch-1-1] +[gw7] [ 20%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-scale-newton-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m2-scale-newton-pdf0] +[gw6] [ 20%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_residual_export_is_logged +pamica/tests/mne_tests/test_pca_residual.py::test_every_model_restores_the_residual +[gw2] [ 20%] PASSED pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[torch-1-1] +pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[torch-1-2] +[gw2] [ 20%] PASSED pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[torch-1-2] +pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[torch-2-1] +[gw2] [ 20%] PASSED pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[torch-2-1] +pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[torch-2-2] +[gw4] [ 20%] PASSED pamica/tests/mne_tests/test_bad_annotation.py::test_nan_inside_bad_segment_is_tolerated +pamica/tests/mne_tests/test_bad_annotation.py::test_fully_annotated_range_raises +[gw4] [ 20%] PASSED pamica/tests/mne_tests/test_bad_annotation.py::test_fully_annotated_range_raises +pamica/tests/mne_tests/test_bad_annotation.py::test_empty_start_stop_range_blames_the_range +[gw4] [ 21%] PASSED pamica/tests/mne_tests/test_bad_annotation.py::test_empty_start_stop_range_blames_the_range +pamica/tests/mne_tests/test_bad_annotation.py::test_fully_annotated_scoring_raises +[gw4] [ 21%] PASSED pamica/tests/mne_tests/test_bad_annotation.py::test_fully_annotated_scoring_raises +pamica/tests/mne_tests/test_end_to_end_workflow.py::test_average_reference_leaves_rank_n_minus_one +[gw2] [ 21%] PASSED pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[torch-2-2] +pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[numpy-1-1] +[gw4] [ 21%] PASSED pamica/tests/mne_tests/test_end_to_end_workflow.py::test_average_reference_leaves_rank_n_minus_one +pamica/tests/mne_tests/test_end_to_end_workflow.py::test_fit_keeps_n_minus_one_components[torch] +[gw2] [ 21%] PASSED pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[numpy-1-1] +pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[numpy-1-2] +[gw1] [ 21%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_newton_fit_is_stable_on_full_data[1] +pamica/tests/mlx_tests/test_mlx_newton.py::test_newton_fit_is_stable_on_full_data[3] +[gw5] [ 21%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_get_sources_matches_amica_transform +pamica/tests/mne_tests/test_mne_wrapper.py::test_get_components_are_channel_space_maps +[gw5] [ 21%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_get_components_are_channel_space_maps +pamica/tests/mne_tests/test_mne_wrapper.py::test_apply_without_exclude_reconstructs +[gw5] [ 21%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_apply_without_exclude_reconstructs +pamica/tests/mne_tests/test_mne_wrapper.py::test_apply_excludes_component +[gw5] [ 21%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_apply_excludes_component +pamica/tests/mne_tests/test_mne_wrapper.py::test_to_mne_ica_returns_valid_fitted_ica +[gw5] [ 21%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_to_mne_ica_returns_valid_fitted_ica +pamica/tests/mne_tests/test_mne_wrapper.py::test_export_is_cached_and_invalidated_on_refit +[gw2] [ 21%] PASSED pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[numpy-1-2] +pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[numpy-2-1] +[gw2] [ 21%] PASSED pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[numpy-2-1] +pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[numpy-2-2] +[gw2] [ 21%] PASSED pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[numpy-2-2] +pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[mlx-1-1] +[gw7] [ 21%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m2-scale-newton-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-scale-newton-pdf0] +[gw2] [ 21%] PASSED pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[mlx-1-1] +pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[mlx-1-2] +[gw2] [ 21%] PASSED pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[mlx-1-2] +pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[mlx-2-1] +[gw2] [ 21%] PASSED pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[mlx-2-1] +pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[mlx-2-2] +[gw2] [ 21%] PASSED pamica/tests/test_initial_mixing.py::test_every_fit_starts_from_unit_norm_components[mlx-2-2] +pamica/tests/test_initial_mixing.py::test_the_pre_change_code_fails_the_start_checks[torch-1] +[gw7] [ 21%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-scale-newton-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-scale-newton-pdf1] +[gw4] [ 22%] PASSED pamica/tests/mne_tests/test_end_to_end_workflow.py::test_fit_keeps_n_minus_one_components[torch] +pamica/tests/mne_tests/test_end_to_end_workflow.py::test_fit_keeps_n_minus_one_components[mlx] +[gw2] [ 22%] PASSED pamica/tests/test_initial_mixing.py::test_the_pre_change_code_fails_the_start_checks[torch-1] +pamica/tests/test_initial_mixing.py::test_the_pre_change_code_fails_the_start_checks[torch-2] +[gw2] [ 22%] PASSED pamica/tests/test_initial_mixing.py::test_the_pre_change_code_fails_the_start_checks[torch-2] +pamica/tests/test_initial_mixing.py::test_the_pre_change_code_fails_the_start_checks[numpy-1] +[gw4] [ 22%] PASSED pamica/tests/mne_tests/test_end_to_end_workflow.py::test_fit_keeps_n_minus_one_components[mlx] +pamica/tests/mne_tests/test_end_to_end_workflow.py::test_excluding_a_component_removes_only_its_back_projection[torch] +[gw4] [ 22%] PASSED pamica/tests/mne_tests/test_end_to_end_workflow.py::test_excluding_a_component_removes_only_its_back_projection[torch] +pamica/tests/mne_tests/test_end_to_end_workflow.py::test_excluding_a_component_removes_only_its_back_projection[mlx] +[gw2] [ 22%] PASSED pamica/tests/test_initial_mixing.py::test_the_pre_change_code_fails_the_start_checks[numpy-1] +pamica/tests/test_initial_mixing.py::test_the_pre_change_code_fails_the_start_checks[numpy-2] +[gw4] [ 22%] PASSED pamica/tests/mne_tests/test_end_to_end_workflow.py::test_excluding_a_component_removes_only_its_back_projection[mlx] +pamica/tests/mne_tests/test_end_to_end_workflow.py::test_eeglab_export_reloads_with_the_reduced_rank[torch] +[gw4] [ 22%] PASSED pamica/tests/mne_tests/test_end_to_end_workflow.py::test_eeglab_export_reloads_with_the_reduced_rank[torch] +pamica/tests/mne_tests/test_end_to_end_workflow.py::test_eeglab_export_reloads_with_the_reduced_rank[mlx] +[gw4] [ 22%] PASSED pamica/tests/mne_tests/test_end_to_end_workflow.py::test_eeglab_export_reloads_with_the_reduced_rank[mlx] +pamica/tests/mne_tests/test_end_to_end_workflow.py::test_save_load_gives_identical_sources[torch] +[gw4] [ 22%] PASSED pamica/tests/mne_tests/test_end_to_end_workflow.py::test_save_load_gives_identical_sources[torch] +pamica/tests/mne_tests/test_end_to_end_workflow.py::test_save_load_gives_identical_sources[mlx] +[gw4] [ 22%] PASSED pamica/tests/mne_tests/test_end_to_end_workflow.py::test_save_load_gives_identical_sources[mlx] +pamica/tests/mne_tests/test_end_to_end_workflow.py::test_input_param_drives_the_fit[torch] +[gw6] [ 22%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_every_model_restores_the_residual +pamica/tests/mne_tests/test_pca_residual.py::test_epochs_apply_restores_the_residual +[gw7] [ 22%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-scale-newton-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-scale-newton-pdf1] +[gw2] [ 22%] PASSED pamica/tests/test_initial_mixing.py::test_the_pre_change_code_fails_the_start_checks[numpy-2] +pamica/tests/test_initial_mixing.py::test_the_pre_change_code_fails_the_start_checks[mlx-1] +[gw2] [ 22%] PASSED pamica/tests/test_initial_mixing.py::test_the_pre_change_code_fails_the_start_checks[mlx-1] +pamica/tests/test_initial_mixing.py::test_the_pre_change_code_fails_the_start_checks[mlx-2] +[gw2] [ 22%] PASSED pamica/tests/test_initial_mixing.py::test_the_pre_change_code_fails_the_start_checks[mlx-2] +pamica/tests/test_initial_mixing.py::test_numpy_uses_an_A_supplied_before_fit_as_is +[gw1] [ 22%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_newton_fit_is_stable_on_full_data[3] +pamica/tests/mlx_tests/test_mlx_newton.py::test_newton_fit_is_stable_on_full_data[42] +[gw7] [ 22%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-scale-newton-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-noscale-ng-pdf0] +[gw2] [ 22%] PASSED pamica/tests/test_initial_mixing.py::test_numpy_uses_an_A_supplied_before_fit_as_is +pamica/tests/test_initial_mixing.py::test_numpy_refit_continues_from_its_fitted_A_as_is +[gw0] [ 22%] PASSED pamica/tests/mlx_tests/test_mlx_backend.py::test_converged_ll_matches_torch_float32 +pamica/tests/mlx_tests/test_mlx_backend.py::test_multimodel_matches_torch_float32 +[gw7] [ 23%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-noscale-ng-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m2-noscale-ng-pdf0] +[gw6] [ 23%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_epochs_apply_restores_the_residual +pamica/tests/mne_tests/test_pca_residual.py::test_saved_ica_restores_the_residual +[gw6] [ 23%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_saved_ica_restores_the_residual +pamica/tests/mne_tests/test_pca_residual.py::test_float_n_pca_components_selects_by_explained_variance +[gw6] [ 23%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_float_n_pca_components_selects_by_explained_variance +pamica/tests/mne_tests/test_pca_residual.py::test_mixed_channel_types_restore_the_residual_in_original_units +[gw2] [ 23%] PASSED pamica/tests/test_initial_mixing.py::test_numpy_refit_continues_from_its_fitted_A_as_is +pamica/tests/test_initial_mixing.py::test_a_torch_state_dict_restores_A_as_is +[gw5] [ 23%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_export_is_cached_and_invalidated_on_refit +pamica/tests/mne_tests/test_mne_wrapper.py::test_fit_with_channel_subset +[gw2] [ 23%] PASSED pamica/tests/test_initial_mixing.py::test_a_torch_state_dict_restores_A_as_is +pamica/tests/test_initial_mixing.py::test_an_mlx_save_restores_A_as_is +[gw2] [ 23%] PASSED pamica/tests/test_initial_mixing.py::test_an_mlx_save_restores_A_as_is +pamica/tests/test_initial_mixing.py::test_a_wrapper_save_restores_A_as_is +[gw2] [ 23%] PASSED pamica/tests/test_initial_mixing.py::test_a_wrapper_save_restores_A_as_is +pamica/tests/test_initial_mixing.py::test_numpy_fix_init_starts_from_the_identity +[gw2] [ 23%] PASSED pamica/tests/test_initial_mixing.py::test_numpy_fix_init_starts_from_the_identity +pamica/tests/test_initial_mixing.py::test_numpy_refuses_a_supplied_A_no_fit_can_start_from[columns] +[gw2] [ 23%] PASSED pamica/tests/test_initial_mixing.py::test_numpy_refuses_a_supplied_A_no_fit_can_start_from[columns] +pamica/tests/test_initial_mixing.py::test_numpy_refuses_a_supplied_A_no_fit_can_start_from[nan] +[gw2] [ 23%] PASSED pamica/tests/test_initial_mixing.py::test_numpy_refuses_a_supplied_A_no_fit_can_start_from[nan] +pamica/tests/test_initial_mixing.py::test_numpy_refuses_a_supplied_A_no_fit_can_start_from[zero-row] +[gw2] [ 23%] PASSED pamica/tests/test_initial_mixing.py::test_numpy_refuses_a_supplied_A_no_fit_can_start_from[zero-row] +pamica/tests/test_initial_mixing.py::test_the_numpy_restart_redraws_unit_norm_components[1] +[gw2] [ 23%] PASSED pamica/tests/test_initial_mixing.py::test_the_numpy_restart_redraws_unit_norm_components[1] +pamica/tests/test_initial_mixing.py::test_the_numpy_restart_redraws_unit_norm_components[2] +[gw6] [ 23%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_mixed_channel_types_restore_the_residual_in_original_units +pamica/tests/mne_tests/test_pca_residual.py::test_average_reference_rank_reduction_reconstructs +[gw4] [ 23%] PASSED pamica/tests/mne_tests/test_end_to_end_workflow.py::test_input_param_drives_the_fit[torch] +pamica/tests/mne_tests/test_end_to_end_workflow.py::test_input_param_drives_the_fit[mlx] +[gw7] [ 23%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m2-noscale-ng-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-noscale-ng-pdf0] +[gw4] [ 23%] PASSED pamica/tests/mne_tests/test_end_to_end_workflow.py::test_input_param_drives_the_fit[mlx] +pamica/tests/mne_tests/test_end_to_end_workflow.py::test_torch_and_mlx_find_the_same_components +[gw5] [ 23%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_fit_with_channel_subset +pamica/tests/mne_tests/test_mne_wrapper.py::test_fit_from_epochs +[gw7] [ 23%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-noscale-ng-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-noscale-ng-pdf1] +[gw4] [ 23%] PASSED pamica/tests/mne_tests/test_end_to_end_workflow.py::test_torch_and_mlx_find_the_same_components +pamica/tests/mne_tests/test_mne_backends.py::test_backend_is_validated_like_amica +[gw4] [ 24%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_backend_is_validated_like_amica +pamica/tests/mne_tests/test_mne_backends.py::test_unfitted_repr_names_the_backend[torch] +[gw4] [ 24%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_unfitted_repr_names_the_backend[torch] +pamica/tests/mne_tests/test_mne_backends.py::test_unfitted_repr_names_the_backend[mlx] +[gw4] [ 24%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_unfitted_repr_names_the_backend[mlx] +pamica/tests/mne_tests/test_mne_backends.py::test_fitted_repr_names_the_backend +[gw2] [ 24%] PASSED pamica/tests/test_initial_mixing.py::test_the_numpy_restart_redraws_unit_norm_components[2] +pamica/tests/test_initial_mixing.py::test_the_pre_change_numpy_restart_redrew_unnormalized_components +[gw1] [ 24%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_newton_fit_is_stable_on_full_data[42] +pamica/tests/mlx_tests/test_mlx_newton.py::test_newton_inactive_is_bit_identical +[gw4] [ 24%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_fitted_repr_names_the_backend +pamica/tests/mne_tests/test_mne_backends.py::test_mlx_without_mlx_raises_import_error +[gw4] [ 24%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_mlx_without_mlx_raises_import_error +pamica/tests/mne_tests/test_mne_backends.py::test_unknown_fit_keyword_propagates_from_amica +[gw4] [ 24%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_unknown_fit_keyword_propagates_from_amica +pamica/tests/mne_tests/test_mne_backends.py::test_mlx_rejects_a_dtype +[gw4] [ 24%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_mlx_rejects_a_dtype +pamica/tests/mne_tests/test_mne_backends.py::test_fit_without_lrate_uses_the_backend_default[torch_keep] +[gw4] [ 24%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_fit_without_lrate_uses_the_backend_default[torch_keep] +pamica/tests/mne_tests/test_mne_backends.py::test_fit_without_lrate_uses_the_backend_default[mlx_keep] +[gw4] [ 24%] PASSED pamica/tests/mne_tests/test_mne_backends.py::test_fit_without_lrate_uses_the_backend_default[mlx_keep] +pamica/tests/test_default_settings.py::test_native_block_is_pamicas_block_split_over_the_threads +[gw4] [ 24%] PASSED pamica/tests/test_default_settings.py::test_native_block_is_pamicas_block_split_over_the_threads +pamica/tests/test_default_settings.py::test_native_input_param_writes_every_setting_it_can +[gw2] [ 24%] PASSED pamica/tests/test_initial_mixing.py::test_the_pre_change_numpy_restart_redrew_unnormalized_components +pamica/tests/test_initial_mixing.py::test_the_reference_draws_unit_norm_components[1-1] +[gw2] [ 24%] SKIPPED pamica/tests/test_initial_mixing.py::test_the_reference_draws_unit_norm_components[1-1] +pamica/tests/test_initial_mixing.py::test_the_reference_draws_unit_norm_components[1-42] +[gw2] [ 24%] SKIPPED pamica/tests/test_initial_mixing.py::test_the_reference_draws_unit_norm_components[1-42] +pamica/tests/test_initial_mixing.py::test_the_reference_draws_unit_norm_components[2-1] +[gw2] [ 24%] SKIPPED pamica/tests/test_initial_mixing.py::test_the_reference_draws_unit_norm_components[2-1] +pamica/tests/test_initial_mixing.py::test_the_reference_draws_unit_norm_components[2-42] +[gw2] [ 24%] SKIPPED pamica/tests/test_initial_mixing.py::test_the_reference_draws_unit_norm_components[2-42] +pamica/tests/test_initial_mixing.py::test_the_reference_normalizes_pamicas_draw_as_pamica_does[1] +[gw4] [ 24%] PASSED pamica/tests/test_default_settings.py::test_native_input_param_writes_every_setting_it_can +[gw2] [ 24%] SKIPPED pamica/tests/test_initial_mixing.py::test_the_reference_normalizes_pamicas_draw_as_pamica_does[1] +pamica/tests/test_initial_mixing.py::test_the_reference_normalizes_pamicas_draw_as_pamica_does[2] +[gw2] [ 24%] SKIPPED pamica/tests/test_initial_mixing.py::test_the_reference_normalizes_pamicas_draw_as_pamica_does[2] +pamica/tests/test_dimsweep_threads.py::test_is_cpu_classifies_backends +pamica/tests/test_iteration_order.py::test_a_is_held_on_exactly_the_reference_iterations_without_sharing[torch] +[gw4] [ 25%] PASSED pamica/tests/test_dimsweep_threads.py::test_is_cpu_classifies_backends +pamica/tests/test_dimsweep_threads.py::test_is_cpu_covers_every_backend +[gw4] [ 25%] PASSED pamica/tests/test_dimsweep_threads.py::test_is_cpu_covers_every_backend +pamica/tests/test_dimsweep_threads.py::test_report_surfaces_all_failed_backend +[gw4] [ 25%] PASSED pamica/tests/test_dimsweep_threads.py::test_report_surfaces_all_failed_backend +pamica/tests/test_dimsweep_threads.py::test_threads_recorded_end_to_end +[gw1] [ 25%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_newton_inactive_is_bit_identical +pamica/tests/mlx_tests/test_mlx_newton.py::test_newton_direction_matches_formula +[gw1] [ 25%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_newton_direction_matches_formula +pamica/tests/mlx_tests/test_mlx_newton.py::test_lrate_ramps_toward_newtrate_while_posdef +[gw4] [ 25%] PASSED pamica/tests/test_dimsweep_threads.py::test_threads_recorded_end_to_end +pamica/tests/test_dimsweep_threads.py::test_run_torch_restores_global_thread_count +[gw4] [ 25%] PASSED pamica/tests/test_dimsweep_threads.py::test_run_torch_restores_global_thread_count +pamica/tests/test_doscaling_rows.py::test_torch_rescale_is_an_exact_change_of_scale[1-ng] +[gw6] [ 25%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_average_reference_rank_reduction_reconstructs +pamica/tests/mne_tests/test_pca_residual.py::test_pcadb_reduction_restores_the_residual +[gw2] [ 25%] PASSED pamica/tests/test_iteration_order.py::test_a_is_held_on_exactly_the_reference_iterations_without_sharing[torch] +pamica/tests/test_iteration_order.py::test_a_is_held_on_exactly_the_reference_iterations_without_sharing[numpy] +[gw1] [ 25%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_lrate_ramps_toward_newtrate_while_posdef +pamica/tests/mlx_tests/test_mlx_newton.py::test_fallback_ramps_toward_lrate_cap_and_counts +[gw4] [ 25%] PASSED pamica/tests/test_doscaling_rows.py::test_torch_rescale_is_an_exact_change_of_scale[1-ng] +pamica/tests/test_doscaling_rows.py::test_torch_rescale_is_an_exact_change_of_scale[1-newton] +[gw7] [ 25%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-noscale-ng-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-noscale-ng-pdf1] +[gw1] [ 25%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_fallback_ramps_toward_lrate_cap_and_counts +pamica/tests/mlx_tests/test_mlx_newton.py::test_newtrate_ratchet_is_suppressed_before_newt_start +[gw4] [ 25%] PASSED pamica/tests/test_doscaling_rows.py::test_torch_rescale_is_an_exact_change_of_scale[1-newton] +pamica/tests/test_doscaling_rows.py::test_torch_rescale_is_an_exact_change_of_scale[2-ng] +[gw7] [ 25%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-noscale-ng-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-noscale-newton-pdf0] +[gw4] [ 25%] PASSED pamica/tests/test_doscaling_rows.py::test_torch_rescale_is_an_exact_change_of_scale[2-ng] +pamica/tests/test_doscaling_rows.py::test_torch_rescale_is_an_exact_change_of_scale[2-newton] +[gw5] [ 25%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_fit_from_epochs +pamica/tests/mne_tests/test_mne_wrapper.py::test_plot_components_returns_figure +[gw2] [ 25%] PASSED pamica/tests/test_iteration_order.py::test_a_is_held_on_exactly_the_reference_iterations_without_sharing[numpy] +pamica/tests/test_iteration_order.py::test_a_is_held_on_exactly_the_reference_iterations_without_sharing[mlx] +[gw5] [ 25%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_plot_components_returns_figure +pamica/tests/mne_tests/test_mne_wrapper.py::test_pca_components_orthonormal_and_variance_ordered +[gw5] [ 25%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_pca_components_orthonormal_and_variance_ordered +pamica/tests/mne_tests/test_mne_wrapper.py::test_fit_with_start_stop_subranges_raw +[gw4] [ 26%] PASSED pamica/tests/test_doscaling_rows.py::test_torch_rescale_is_an_exact_change_of_scale[2-newton] +pamica/tests/test_doscaling_rows.py::test_numpy_rescale_is_an_exact_change_of_scale[1-ng] +[gw2] [ 26%] PASSED pamica/tests/test_iteration_order.py::test_a_is_held_on_exactly_the_reference_iterations_without_sharing[mlx] +pamica/tests/test_iteration_order.py::test_a_held_iteration_only_rescales_with_doscaling_on[torch] +[gw7] [ 26%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-noscale-newton-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m2-noscale-newton-pdf0] +[gw6] [ 26%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_pcadb_reduction_restores_the_residual +pamica/tests/mne_tests/test_pca_residual.py::test_float32_fit_restores_the_residual +[gw2] [ 26%] PASSED pamica/tests/test_iteration_order.py::test_a_held_iteration_only_rescales_with_doscaling_on[torch] +pamica/tests/test_iteration_order.py::test_a_held_iteration_only_rescales_with_doscaling_on[numpy] +[gw4] [ 26%] PASSED pamica/tests/test_doscaling_rows.py::test_numpy_rescale_is_an_exact_change_of_scale[1-ng] +pamica/tests/test_doscaling_rows.py::test_numpy_rescale_is_an_exact_change_of_scale[1-newton] +[gw6] [ 26%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_float32_fit_restores_the_residual +pamica/tests/mne_tests/test_pca_residual.py::test_float32_retained_basis_matches_the_float32_sphere +[gw6] [ 26%] PASSED pamica/tests/mne_tests/test_pca_residual.py::test_float32_retained_basis_matches_the_float32_sphere +pamica/tests/test_amari_distance.py::test_amari_distance_self_is_zero +[gw6] [ 26%] PASSED pamica/tests/test_amari_distance.py::test_amari_distance_self_is_zero +pamica/tests/test_amari_distance.py::test_amari_distance_invariant_to_permutation_and_scale +[gw6] [ 26%] PASSED pamica/tests/test_amari_distance.py::test_amari_distance_invariant_to_permutation_and_scale +pamica/tests/test_amari_distance.py::test_amari_distance_nonzero_for_unrelated +[gw6] [ 26%] PASSED pamica/tests/test_amari_distance.py::test_amari_distance_nonzero_for_unrelated +pamica/tests/test_amari_distance.py::test_amari_distance_nonnegative +[gw6] [ 26%] PASSED pamica/tests/test_amari_distance.py::test_amari_distance_nonnegative +pamica/tests/test_amari_distance.py::test_amari_distance_symmetric +[gw6] [ 26%] PASSED pamica/tests/test_amari_distance.py::test_amari_distance_symmetric +pamica/tests/test_amari_distance.py::test_amari_distance_rejects_degenerate_input +[gw6] [ 26%] PASSED pamica/tests/test_amari_distance.py::test_amari_distance_rejects_degenerate_input +pamica/tests/test_amari_distance.py::test_amari_distance_rejects_scalar_input +[gw6] [ 26%] PASSED pamica/tests/test_amari_distance.py::test_amari_distance_rejects_scalar_input +pamica/tests/test_amari_ensemble_script.py::test_model_amari_finds_swapped_pairing +[gw6] [ 26%] PASSED pamica/tests/test_amari_ensemble_script.py::test_model_amari_finds_swapped_pairing +pamica/tests/test_amari_ensemble_script.py::test_model_amari_matches_best_of_naive_pairings +[gw6] [ 26%] PASSED pamica/tests/test_amari_ensemble_script.py::test_model_amari_matches_best_of_naive_pairings +pamica/tests/test_amari_ensemble_script.py::test_perm_test_distance_metric_detects_worse_between +[gw6] [ 26%] PASSED pamica/tests/test_amari_ensemble_script.py::test_perm_test_distance_metric_detects_worse_between +pamica/tests/test_amari_ensemble_script.py::test_perm_test_similarity_metric_detects_worse_between +[gw6] [ 26%] PASSED pamica/tests/test_amari_ensemble_script.py::test_perm_test_similarity_metric_detects_worse_between +pamica/tests/test_amari_ensemble_script.py::test_perm_test_no_evidence_when_groups_are_equivalent +[gw6] [ 26%] PASSED pamica/tests/test_amari_ensemble_script.py::test_perm_test_no_evidence_when_groups_are_equivalent +pamica/tests/test_amica.py::TestAMICA::test_data_loading +[gw6] [ 27%] PASSED pamica/tests/test_amica.py::TestAMICA::test_data_loading +pamica/tests/test_amica.py::TestAMICA::test_pdf_computation +[gw6] [ 27%] PASSED pamica/tests/test_amica.py::TestAMICA::test_pdf_computation +pamica/tests/test_amica.py::TestAMICA::test_preprocessing +[gw6] [ 27%] PASSED pamica/tests/test_amica.py::TestAMICA::test_preprocessing +pamica/tests/test_amicaout_reduced.py::test_sphere_is_padded_to_the_fortran_record_shape +[gw4] [ 27%] PASSED pamica/tests/test_doscaling_rows.py::test_numpy_rescale_is_an_exact_change_of_scale[1-newton] +pamica/tests/test_doscaling_rows.py::test_numpy_rescale_is_an_exact_change_of_scale[2-ng] +[gw2] [ 27%] PASSED pamica/tests/test_iteration_order.py::test_a_held_iteration_only_rescales_with_doscaling_on[numpy] +pamica/tests/test_iteration_order.py::test_a_held_iteration_only_rescales_with_doscaling_on[mlx] +[gw7] [ 27%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m2-noscale-newton-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-noscale-newton-pdf0] +[gw2] [ 27%] PASSED pamica/tests/test_iteration_order.py::test_a_held_iteration_only_rescales_with_doscaling_on[mlx] +pamica/tests/test_iteration_order.py::test_default_fits_hold_a_on_iterations_100_to_105[torch] +[gw7] [ 27%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-noscale-newton-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-noscale-newton-pdf1] +[gw5] [ 27%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_fit_with_start_stop_subranges_raw +pamica/tests/mne_tests/test_mne_wrapper.py::test_apply_on_epochs +[gw6] [ 27%] PASSED pamica/tests/test_amicaout_reduced.py::test_sphere_is_padded_to_the_fortran_record_shape +pamica/tests/test_amicaout_reduced.py::test_loadmodout_reads_a_reduced_fit +[gw6] [ 27%] PASSED pamica/tests/test_amicaout_reduced.py::test_loadmodout_reads_a_reduced_fit +pamica/tests/test_amicaout_reduced.py::test_sources_roundtrip_under_reduced_rank +[gw6] [ 27%] PASSED pamica/tests/test_amicaout_reduced.py::test_sources_roundtrip_under_reduced_rank +pamica/tests/test_amicaout_reduced.py::test_sphere_is_written_and_read_column_major[True] +[gw4] [ 27%] PASSED pamica/tests/test_doscaling_rows.py::test_numpy_rescale_is_an_exact_change_of_scale[2-ng] +pamica/tests/test_doscaling_rows.py::test_numpy_rescale_is_an_exact_change_of_scale[2-newton] +[gw6] [ 27%] PASSED pamica/tests/test_amicaout_reduced.py::test_sphere_is_written_and_read_column_major[True] +pamica/tests/test_amicaout_reduced.py::test_sphere_is_written_and_read_column_major[False] +[gw7] [ 27%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-noscale-newton-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-noscale-newton-pdf1] +[gw6] [ 27%] PASSED pamica/tests/test_amicaout_reduced.py::test_sphere_is_written_and_read_column_major[False] +pamica/tests/test_backend_accessors.py::test_unfitted_accessors_raise[torch] +[gw6] [ 27%] PASSED pamica/tests/test_backend_accessors.py::test_unfitted_accessors_raise[torch] +pamica/tests/test_backend_accessors.py::test_unfitted_accessors_raise[mlx] +[gw6] [ 27%] PASSED pamica/tests/test_backend_accessors.py::test_unfitted_accessors_raise[mlx] +pamica/tests/test_backend_accessors.py::test_accessors_return_the_fitted_state_as_float64[full-torch] +[gw6] [ 27%] PASSED pamica/tests/test_backend_accessors.py::test_accessors_return_the_fitted_state_as_float64[full-torch] +pamica/tests/test_backend_accessors.py::test_accessors_return_the_fitted_state_as_float64[full-mlx] +[gw7] [ 27%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-noscale-newton-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-reject-newton] +[gw6] [ 28%] PASSED pamica/tests/test_backend_accessors.py::test_accessors_return_the_fitted_state_as_float64[full-mlx] +pamica/tests/test_backend_accessors.py::test_accessors_return_the_fitted_state_as_float64[reduced-torch] +[gw5] [ 28%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_apply_on_epochs +pamica/tests/mne_tests/test_mne_wrapper.py::test_exported_ica_saves_and_reloads +[gw6] [ 28%] PASSED pamica/tests/test_backend_accessors.py::test_accessors_return_the_fitted_state_as_float64[reduced-torch] +pamica/tests/test_backend_accessors.py::test_accessors_return_the_fitted_state_as_float64[reduced-mlx] +[gw5] [ 28%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_exported_ica_saves_and_reloads +pamica/tests/mne_tests/test_mne_wrapper.py::test_plot_sources_returns_figure +[gw6] [ 28%] PASSED pamica/tests/test_backend_accessors.py::test_accessors_return_the_fitted_state_as_float64[reduced-mlx] +pamica/tests/test_backend_accessors.py::test_accessors_return_the_fitted_state_as_float64[two_models-torch] +[gw6] [ 28%] PASSED pamica/tests/test_backend_accessors.py::test_accessors_return_the_fitted_state_as_float64[two_models-torch] +pamica/tests/test_backend_accessors.py::test_accessors_return_the_fitted_state_as_float64[two_models-mlx] +[gw6] [ 28%] PASSED pamica/tests/test_backend_accessors.py::test_accessors_return_the_fitted_state_as_float64[two_models-mlx] +pamica/tests/test_backend_accessors.py::test_accessors_return_independent_copies[torch] +[gw6] [ 28%] PASSED pamica/tests/test_backend_accessors.py::test_accessors_return_independent_copies[torch] +pamica/tests/test_backend_accessors.py::test_accessors_return_independent_copies[mlx] +[gw6] [ 28%] PASSED pamica/tests/test_backend_accessors.py::test_accessors_return_independent_copies[mlx] +pamica/tests/test_backend_accessors.py::test_model_center_validates_the_index[torch] +[gw6] [ 28%] PASSED pamica/tests/test_backend_accessors.py::test_model_center_validates_the_index[torch] +pamica/tests/test_backend_accessors.py::test_model_center_validates_the_index[mlx] +[gw6] [ 28%] PASSED pamica/tests/test_backend_accessors.py::test_model_center_validates_the_index[mlx] +pamica/tests/test_backend_accessors.py::test_single_model_center_is_zero +[gw6] [ 28%] PASSED pamica/tests/test_backend_accessors.py::test_single_model_center_is_zero +pamica/tests/test_backend_accessors.py::test_float32_torch_fit_still_returns_float64 +[gw6] [ 28%] PASSED pamica/tests/test_backend_accessors.py::test_float32_torch_fit_still_returns_float64 +pamica/tests/test_backend_accessors.py::test_mlx_accessors_agree_with_torch[full] +[gw6] [ 28%] PASSED pamica/tests/test_backend_accessors.py::test_mlx_accessors_agree_with_torch[full] +pamica/tests/test_backend_accessors.py::test_mlx_accessors_agree_with_torch[reduced] +[gw6] [ 28%] PASSED pamica/tests/test_backend_accessors.py::test_mlx_accessors_agree_with_torch[reduced] +pamica/tests/test_backend_accessors.py::test_mlx_accessors_agree_with_torch[two_models] +[gw6] [ 28%] PASSED pamica/tests/test_backend_accessors.py::test_mlx_accessors_agree_with_torch[two_models] +pamica/tests/test_llt_stash.py::test_numpy_stashed_llt_is_the_estep_behind_reported_ll +[gw5] [ 28%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_plot_sources_returns_figure +pamica/tests/mne_tests/test_mne_wrapper.py::test_seed_determinism +[gw2] [ 28%] PASSED pamica/tests/test_iteration_order.py::test_default_fits_hold_a_on_iterations_100_to_105[torch] +pamica/tests/test_iteration_order.py::test_default_fits_hold_a_on_iterations_100_to_105[numpy] +[gw4] [ 28%] PASSED pamica/tests/test_doscaling_rows.py::test_numpy_rescale_is_an_exact_change_of_scale[2-newton] +pamica/tests/test_doscaling_rows.py::test_mlx_rescale_is_an_exact_change_of_scale[1-ng] +[gw7] [ 28%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-reject-newton] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m2-reject-newton] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_mlx_rescale_is_an_exact_change_of_scale[1-ng] +pamica/tests/test_doscaling_rows.py::test_mlx_rescale_is_an_exact_change_of_scale[1-newton] +[gw1] [ 29%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_newtrate_ratchet_is_suppressed_before_newt_start +pamica/tests/mlx_tests/test_mlx_newton.py::test_newtrate_ratchets_at_maxdecs_once_newton_runs +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_mlx_rescale_is_an_exact_change_of_scale[1-newton] +pamica/tests/test_doscaling_rows.py::test_mlx_rescale_is_an_exact_change_of_scale[2-ng] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_mlx_rescale_is_an_exact_change_of_scale[2-ng] +pamica/tests/test_doscaling_rows.py::test_mlx_rescale_is_an_exact_change_of_scale[2-newton] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_mlx_rescale_is_an_exact_change_of_scale[2-newton] +pamica/tests/test_doscaling_rows.py::test_numpy_rescales_like_torch[disjoint] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_numpy_rescales_like_torch[disjoint] +pamica/tests/test_doscaling_rows.py::test_mlx_rescales_like_torch[disjoint] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_mlx_rescales_like_torch[disjoint] +pamica/tests/test_doscaling_rows.py::test_numpy_rescales_like_torch[permuted] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_numpy_rescales_like_torch[permuted] +pamica/tests/test_doscaling_rows.py::test_mlx_rescales_like_torch[permuted] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_mlx_rescales_like_torch[permuted] +pamica/tests/test_doscaling_rows.py::test_numpy_rescales_like_torch[merged] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_numpy_rescales_like_torch[merged] +pamica/tests/test_doscaling_rows.py::test_mlx_rescales_like_torch[merged] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_mlx_rescales_like_torch[merged] +pamica/tests/test_doscaling_rows.py::test_a_zero_or_nan_norm_row_is_left_untouched[torch-zero] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_a_zero_or_nan_norm_row_is_left_untouched[torch-zero] +pamica/tests/test_doscaling_rows.py::test_a_zero_or_nan_norm_row_is_left_untouched[torch-nan] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_a_zero_or_nan_norm_row_is_left_untouched[torch-nan] +pamica/tests/test_doscaling_rows.py::test_a_zero_or_nan_norm_row_is_left_untouched[numpy-zero] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_a_zero_or_nan_norm_row_is_left_untouched[numpy-zero] +pamica/tests/test_doscaling_rows.py::test_a_zero_or_nan_norm_row_is_left_untouched[numpy-nan] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_a_zero_or_nan_norm_row_is_left_untouched[numpy-nan] +pamica/tests/test_doscaling_rows.py::test_a_zero_or_nan_norm_row_is_left_untouched[mlx-zero] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_a_zero_or_nan_norm_row_is_left_untouched[mlx-zero] +pamica/tests/test_doscaling_rows.py::test_a_zero_or_nan_norm_row_is_left_untouched[mlx-nan] +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_a_zero_or_nan_norm_row_is_left_untouched[mlx-nan] +pamica/tests/test_doscaling_rows.py::test_doscaling_off_is_byte_identical_to_the_pre_fix_code[torch-1] +[gw6] [ 29%] PASSED pamica/tests/test_llt_stash.py::test_numpy_stashed_llt_is_the_estep_behind_reported_ll +pamica/tests/test_llt_stash.py::test_stashed_llt_is_one_m_step_behind_the_written_parameters +[gw4] [ 29%] PASSED pamica/tests/test_doscaling_rows.py::test_doscaling_off_is_byte_identical_to_the_pre_fix_code[torch-1] +pamica/tests/test_doscaling_rows.py::test_doscaling_off_is_byte_identical_to_the_pre_fix_code[torch-2] +[gw7] [ 29%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m2-reject-newton] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-reject-newton] +[gw7] [ 29%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-reject-newton] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m3-scale-newton-pdf0] +[gw6] [ 30%] PASSED pamica/tests/test_llt_stash.py::test_stashed_llt_is_one_m_step_behind_the_written_parameters +pamica/tests/test_llt_stash.py::test_torch_and_numpy_stash_the_same_llt_on_matched_state +[gw4] [ 30%] PASSED pamica/tests/test_doscaling_rows.py::test_doscaling_off_is_byte_identical_to_the_pre_fix_code[torch-2] +pamica/tests/test_doscaling_rows.py::test_doscaling_off_is_byte_identical_to_the_pre_fix_code[numpy-1] +[gw2] [ 30%] PASSED pamica/tests/test_iteration_order.py::test_default_fits_hold_a_on_iterations_100_to_105[numpy] +pamica/tests/test_iteration_order.py::test_default_fits_hold_a_on_iterations_100_to_105[mlx] +[gw6] [ 30%] PASSED pamica/tests/test_llt_stash.py::test_torch_and_numpy_stash_the_same_llt_on_matched_state +pamica/tests/test_llt_stash.py::test_llt_invariant_breaks_when_rejection_fires_on_the_last_iteration +[gw7] [ 30%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m3-scale-newton-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m3-scale-newton-pdf0] +[gw1] [ 30%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_newtrate_ratchets_at_maxdecs_once_newton_runs +pamica/tests/mlx_tests/test_mlx_newton.py::test_newtrate_never_ratchets_without_newton +[gw6] [ 30%] PASSED pamica/tests/test_llt_stash.py::test_llt_invariant_breaks_when_rejection_fires_on_the_last_iteration +pamica/tests/test_llt_stash.py::test_llt_invariant_returns_one_iteration_after_a_rejection +[gw4] [ 30%] PASSED pamica/tests/test_doscaling_rows.py::test_doscaling_off_is_byte_identical_to_the_pre_fix_code[numpy-1] +pamica/tests/test_doscaling_rows.py::test_doscaling_off_is_byte_identical_to_the_pre_fix_code[numpy-2] +[gw2] [ 30%] PASSED pamica/tests/test_iteration_order.py::test_default_fits_hold_a_on_iterations_100_to_105[mlx] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-6-False] +[gw2] [ 30%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-6-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-6-True] +[gw2] [ 30%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-6-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-1-False] +[gw2] [ 30%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-1-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-1-True] +[gw2] [ 30%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-1-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-0-False] +[gw2] [ 30%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-0-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-0-True] +[gw2] [ 30%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-0-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch--3-False] +[gw2] [ 30%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch--3-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch--3-True] +[gw2] [ 30%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch--3-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-2.5-False] +[gw2] [ 30%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-2.5-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-2.5-True] +[gw2] [ 30%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-2.5-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-True-False] +[gw2] [ 30%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-True-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-True-True] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[torch-True-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-6-False] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-6-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-6-True] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-6-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-1-False] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-1-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-1-True] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-1-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-0-False] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-0-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-0-True] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-0-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy--3-False] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy--3-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy--3-True] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy--3-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-2.5-False] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-2.5-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-2.5-True] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-2.5-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-True-False] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-True-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-True-True] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[numpy-True-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-6-False] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-6-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-6-True] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-6-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-1-False] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-1-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-1-True] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-1-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-0-False] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-0-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-0-True] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-0-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx--3-False] +[gw2] [ 31%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx--3-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx--3-True] +[gw2] [ 32%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx--3-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-2.5-False] +[gw2] [ 32%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-2.5-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-2.5-True] +[gw2] [ 32%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-2.5-True] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-True-False] +[gw2] [ 32%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-True-False] +pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-True-True] +[gw2] [ 32%] PASSED pamica/tests/test_iteration_order.py::test_every_backend_rejects_a_share_iter_below_7[mlx-True-True] +pamica/tests/test_iteration_order.py::test_a_params_file_reaches_the_share_validation[torch-share_iter-6-must be an integer >= 7, got 6: below 7] +[gw2] [ 32%] PASSED pamica/tests/test_iteration_order.py::test_a_params_file_reaches_the_share_validation[torch-share_iter-6-must be an integer >= 7, got 6: below 7] +pamica/tests/test_iteration_order.py::test_a_params_file_reaches_the_share_validation[torch-share_start-0-share_start must be an integer >= 1, got 0] +[gw2] [ 32%] PASSED pamica/tests/test_iteration_order.py::test_a_params_file_reaches_the_share_validation[torch-share_start-0-share_start must be an integer >= 1, got 0] +pamica/tests/test_iteration_order.py::test_a_params_file_reaches_the_share_validation[numpy-share_iter-6-must be an integer >= 7, got 6: below 7] +[gw2] [ 32%] PASSED pamica/tests/test_iteration_order.py::test_a_params_file_reaches_the_share_validation[numpy-share_iter-6-must be an integer >= 7, got 6: below 7] +pamica/tests/test_iteration_order.py::test_a_params_file_reaches_the_share_validation[numpy-share_start-0-share_start must be an integer >= 1, got 0] +[gw2] [ 32%] PASSED pamica/tests/test_iteration_order.py::test_a_params_file_reaches_the_share_validation[numpy-share_start-0-share_start must be an integer >= 1, got 0] +pamica/tests/test_iteration_order.py::test_a_params_file_reaches_the_share_validation[mlx-share_iter-6-must be an integer >= 7, got 6: below 7] +[gw2] [ 32%] PASSED pamica/tests/test_iteration_order.py::test_a_params_file_reaches_the_share_validation[mlx-share_iter-6-must be an integer >= 7, got 6: below 7] +pamica/tests/test_iteration_order.py::test_a_params_file_reaches_the_share_validation[mlx-share_start-0-share_start must be an integer >= 1, got 0] +[gw2] [ 32%] PASSED pamica/tests/test_iteration_order.py::test_a_params_file_reaches_the_share_validation[mlx-share_start-0-share_start must be an integer >= 1, got 0] +pamica/tests/test_iteration_order.py::test_the_step_after_a_decrease_already_uses_the_halved_rate[torch] +[gw2] [ 32%] PASSED pamica/tests/test_iteration_order.py::test_the_step_after_a_decrease_already_uses_the_halved_rate[torch] +pamica/tests/test_iteration_order.py::test_the_step_after_a_decrease_already_uses_the_halved_rate[numpy] +[gw2] [ 32%] PASSED pamica/tests/test_iteration_order.py::test_the_step_after_a_decrease_already_uses_the_halved_rate[numpy] +pamica/tests/test_iteration_order.py::test_the_step_after_a_decrease_already_uses_the_halved_rate[mlx] +[gw6] [ 32%] PASSED pamica/tests/test_llt_stash.py::test_llt_invariant_returns_one_iteration_after_a_rejection +pamica/tests/test_llt_stash.py::test_keep_best_restore_rolls_the_llt_stash_back +[gw5] [ 32%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_seed_determinism +pamica/tests/mne_tests/test_mne_wrapper.py::test_unfitted_calls_raise +[gw5] [ 32%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_unfitted_calls_raise +pamica/tests/mne_tests/test_mne_wrapper.py::test_fit_rejects_non_mne_input +[gw5] [ 32%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_fit_rejects_non_mne_input +pamica/tests/mne_tests/test_mne_wrapper.py::test_fit_rejects_start_stop_on_epochs +[gw2] [ 32%] PASSED pamica/tests/test_iteration_order.py::test_the_step_after_a_decrease_already_uses_the_halved_rate[mlx] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_one_mstep_matches_torch_twin[4-1] +[gw5] [ 32%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_fit_rejects_start_stop_on_epochs +pamica/tests/mne_tests/test_mne_wrapper.py::test_fit_rejects_out_of_range_stop +[gw5] [ 32%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_fit_rejects_out_of_range_stop +pamica/tests/mne_tests/test_mne_wrapper.py::test_fit_rejects_non_finite_data +[gw5] [ 33%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_fit_rejects_non_finite_data +pamica/tests/mne_tests/test_mne_wrapper.py::test_pca_reduction_is_supported +[gw2] [ 33%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_one_mstep_matches_torch_twin[4-1] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_one_mstep_matches_torch_twin[1-1] +[gw2] [ 33%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_one_mstep_matches_torch_twin[1-1] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_full_fit_ll_matches_torch_float64[2-3] +[gw7] [ 33%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m3-scale-newton-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m3-scale-newton-pdf0] +[gw1] [ 33%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_newtrate_never_ratchets_without_newton +pamica/tests/mlx_tests/test_mlx_newton.py::test_grad_norm_floor_fires_under_newton +[gw5] [ 33%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_pca_reduction_is_supported +pamica/tests/mne_tests/test_mne_wrapper.py::test_degenerate_fit_is_refused_and_labeled +[gw7] [ 33%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m3-scale-newton-pdf0] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m3-noscale-ng-pdf1] +[gw1] [ 33%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_grad_norm_floor_fires_under_newton +pamica/tests/mlx_tests/test_mlx_newton.py::test_newton_schedule_state_resets_at_fit_start +[gw1] [ 33%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_newton_schedule_state_resets_at_fit_start +pamica/tests/mlx_tests/test_mlx_newton.py::test_newton_finalize_uses_preupdate_mu +[gw1] [ 33%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_newton_finalize_uses_preupdate_mu +pamica/tests/mlx_tests/test_mlx_newton.py::test_multimodel_newton_mstep_is_finite[1] +[gw1] [ 33%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_multimodel_newton_mstep_is_finite[1] +pamica/tests/mlx_tests/test_mlx_newton.py::test_multimodel_newton_mstep_is_finite[2] +[gw1] [ 33%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_multimodel_newton_mstep_is_finite[2] +pamica/tests/mlx_tests/test_mlx_newton.py::test_multimodel_newton_fit_completes +[gw1] [ 33%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_multimodel_newton_fit_completes +pamica/tests/mlx_tests/test_mlx_newton.py::test_three_model_newton_mstep_and_fit_are_finite +[gw4] [ 33%] PASSED pamica/tests/test_doscaling_rows.py::test_doscaling_off_is_byte_identical_to_the_pre_fix_code[numpy-2] +pamica/tests/test_doscaling_rows.py::test_doscaling_off_is_byte_identical_to_the_pre_fix_code[mlx-1] +[gw4] [ 33%] PASSED pamica/tests/test_doscaling_rows.py::test_doscaling_off_is_byte_identical_to_the_pre_fix_code[mlx-1] +pamica/tests/test_doscaling_rows.py::test_doscaling_off_is_byte_identical_to_the_pre_fix_code[mlx-2] +[gw5] [ 33%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_degenerate_fit_is_refused_and_labeled +pamica/tests/mne_tests/test_mne_wrapper.py::test_failed_refit_leaves_prior_state_intact +[gw7] [ 33%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m3-noscale-ng-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m3-noscale-ng-pdf1] +[gw4] [ 33%] PASSED pamica/tests/test_doscaling_rows.py::test_doscaling_off_is_byte_identical_to_the_pre_fix_code[mlx-2] +pamica/tests/test_doscaling_rows.py::test_scalestep_counts_iterations_from_one[1-torch] +[gw7] [ 33%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m3-noscale-ng-pdf1] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-ng-pdf2] +[gw6] [ 33%] PASSED pamica/tests/test_llt_stash.py::test_keep_best_restore_rolls_the_llt_stash_back +pamica/tests/test_llt_stash.py::test_torch_write_runs_no_forward_pass +[gw7] [ 34%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-ng-pdf2] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-ng-pdf2] +[gw1] [ 34%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_three_model_newton_mstep_and_fit_are_finite +pamica/tests/mlx_tests/test_mlx_newton.py::test_frozen_iterations_do_not_count_newton_fallbacks +[gw1] [ 34%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_frozen_iterations_do_not_count_newton_fallbacks +pamica/tests/mlx_tests/test_mlx_newton.py::test_sharing_and_newton_fit_completes +[gw7] [ 34%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-ng-pdf2] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-ng-pdf3] +[gw6] [ 34%] PASSED pamica/tests/test_llt_stash.py::test_torch_write_runs_no_forward_pass +pamica/tests/test_llt_stash.py::test_numpy_checkpoint_runs_no_forward_pass +[gw4] [ 34%] PASSED pamica/tests/test_doscaling_rows.py::test_scalestep_counts_iterations_from_one[1-torch] +pamica/tests/test_doscaling_rows.py::test_scalestep_counts_iterations_from_one[1-numpy] +[gw7] [ 34%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-ng-pdf3] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-ng-pdf3] +[gw7] [ 34%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-ng-pdf3] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-ng-pdf4] +[gw5] [ 34%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_failed_refit_leaves_prior_state_intact +pamica/tests/mne_tests/test_mne_wrapper.py::test_per_model_export_roundtrips_with_nonzero_c +[gw7] [ 34%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m1-ng-pdf4] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-ng-pdf4] +[gw7] [ 34%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m1-ng-pdf4] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-newton-4blocks] +[gw0] [ 34%] PASSED pamica/tests/mlx_tests/test_mlx_backend.py::test_multimodel_matches_torch_float32 +pamica/tests/mlx_tests/test_mlx_backend.py::test_degenerate_fit_stops_and_reports_nan +[gw0] [ 34%] PASSED pamica/tests/mlx_tests/test_mlx_backend.py::test_degenerate_fit_stops_and_reports_nan +pamica/tests/mlx_tests/test_mlx_backend.py::test_init_matches_torch_seed +[gw0] [ 34%] PASSED pamica/tests/mlx_tests/test_mlx_backend.py::test_init_matches_torch_seed +pamica/tests/mlx_tests/test_mlx_backend.py::test_multimodel_dead_model_keeps_prior_c +[gw6] [ 34%] PASSED pamica/tests/test_llt_stash.py::test_numpy_checkpoint_runs_no_forward_pass +pamica/tests/test_llt_stash.py::test_llt_stash_is_none_before_any_estep_ran +[gw6] [ 34%] PASSED pamica/tests/test_llt_stash.py::test_llt_stash_is_none_before_any_estep_ran +pamica/tests/test_metrics_mir.py::test_marginal_entropies_default_nbins_matches_formula +[gw0] [ 34%] PASSED pamica/tests/mlx_tests/test_mlx_backend.py::test_multimodel_dead_model_keeps_prior_c +pamica/tests/mlx_tests/test_mlx_backend.py::test_rholrate_ratchets_at_maxdecs_not_per_decrease +[gw6] [ 34%] PASSED pamica/tests/test_metrics_mir.py::test_marginal_entropies_default_nbins_matches_formula +pamica/tests/test_metrics_mir.py::test_marginal_entropies_matches_histogram_rederivation +[gw6] [ 34%] PASSED pamica/tests/test_metrics_mir.py::test_marginal_entropies_matches_histogram_rederivation +pamica/tests/test_metrics_mir.py::test_mir_identity_unmixing_is_approximately_zero +[gw1] [ 34%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_sharing_and_newton_fit_completes +pamica/tests/mlx_tests/test_mlx_newton.py::test_rank_reduced_newton_fit_completes +[gw6] [ 35%] PASSED pamica/tests/test_metrics_mir.py::test_mir_identity_unmixing_is_approximately_zero +pamica/tests/test_metrics_mir.py::test_mir_scaled_identity_matches_closed_form +[gw6] [ 35%] PASSED pamica/tests/test_metrics_mir.py::test_mir_scaled_identity_matches_closed_form +pamica/tests/test_metrics_mir.py::test_mir_returns_finite_scalars_for_full_channel_data +[gw6] [ 35%] PASSED pamica/tests/test_metrics_mir.py::test_mir_returns_finite_scalars_for_full_channel_data +pamica/tests/test_metrics_mir.py::test_mir_real_fitted_unmixing_is_large_and_positive +[gw6] [ 35%] PASSED pamica/tests/test_metrics_mir.py::test_mir_real_fitted_unmixing_is_large_and_positive +pamica/tests/test_metrics_mir.py::test_mir_nbins_passthrough_matches_manual_computation +[gw6] [ 35%] PASSED pamica/tests/test_metrics_mir.py::test_mir_nbins_passthrough_matches_manual_computation +pamica/tests/test_metrics_mir.py::test_mir_raises_on_singular_unmixing +[gw6] [ 35%] PASSED pamica/tests/test_metrics_mir.py::test_mir_raises_on_singular_unmixing +pamica/tests/test_metrics_mir.py::test_mir_raises_on_non_square_unmixing +[gw6] [ 35%] PASSED pamica/tests/test_metrics_mir.py::test_mir_raises_on_non_square_unmixing +pamica/tests/test_metrics_mir.py::test_mir_raises_on_non_finite_data +[gw6] [ 35%] PASSED pamica/tests/test_metrics_mir.py::test_mir_raises_on_non_finite_data +pamica/tests/test_metrics_mir.py::test_marginal_entropies_raises_on_constant_channel +[gw6] [ 35%] PASSED pamica/tests/test_metrics_mir.py::test_marginal_entropies_raises_on_constant_channel +pamica/tests/test_metrics_pmi.py::test_pairwise_mi_is_exactly_symmetric +[gw6] [ 35%] PASSED pamica/tests/test_metrics_pmi.py::test_pairwise_mi_is_exactly_symmetric +pamica/tests/test_metrics_pmi.py::test_pairwise_mi_diagonal_matches_marginal_entropy +[gw6] [ 35%] PASSED pamica/tests/test_metrics_pmi.py::test_pairwise_mi_diagonal_matches_marginal_entropy +pamica/tests/test_metrics_pmi.py::test_ica_sources_have_lower_pairwise_mi_than_raw_channels +[gw6] [ 35%] PASSED pamica/tests/test_metrics_pmi.py::test_ica_sources_have_lower_pairwise_mi_than_raw_channels +pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_returns_valid_permutation +[gw6] [ 35%] PASSED pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_returns_valid_permutation +pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_increases_adjacent_weight +[gw6] [ 35%] PASSED pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_increases_adjacent_weight +pamica/tests/test_metrics_pmi.py::test_pairwise_mi_raises_on_non_finite_data +[gw6] [ 35%] PASSED pamica/tests/test_metrics_pmi.py::test_pairwise_mi_raises_on_non_finite_data +pamica/tests/test_metrics_pmi.py::test_pairwise_mi_raises_on_constant_channel +[gw6] [ 35%] PASSED pamica/tests/test_metrics_pmi.py::test_pairwise_mi_raises_on_constant_channel +pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_trivial_cases +[gw6] [ 35%] PASSED pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_trivial_cases +pamica/tests/test_metrics_pmi.py::test_pairwise_mi_nbins_passthrough_matches_manual_computation +[gw6] [ 35%] PASSED pamica/tests/test_metrics_pmi.py::test_pairwise_mi_nbins_passthrough_matches_manual_computation +pamica/tests/test_metrics_pmi.py::test_pairwise_mi_raises_on_invalid_nbins +[gw6] [ 35%] PASSED pamica/tests/test_metrics_pmi.py::test_pairwise_mi_raises_on_invalid_nbins +pamica/tests/test_metrics_pmi.py::test_pairwise_mi_raises_on_nbins_too_sparse +[gw6] [ 35%] PASSED pamica/tests/test_metrics_pmi.py::test_pairwise_mi_raises_on_nbins_too_sparse +pamica/tests/test_metrics_pmi.py::test_pairwise_mi_single_component +[gw6] [ 35%] PASSED pamica/tests/test_metrics_pmi.py::test_pairwise_mi_single_component +pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_two_components +[gw6] [ 36%] PASSED pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_two_components +pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_raises_on_non_square +[gw6] [ 36%] PASSED pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_raises_on_non_square +pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_raises_on_non_finite +[gw6] [ 36%] PASSED pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_raises_on_non_finite +pamica/tests/test_numpy_block_tuning.py::test_allocation_failure_falls_back_to_the_last_working_size +[gw1] [ 36%] PASSED pamica/tests/mlx_tests/test_mlx_newton.py::test_rank_reduced_newton_fit_completes +pamica/tests/mlx_tests/test_mlx_pdf.py::test_pdftype_validation +[gw1] [ 36%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_pdftype_validation +pamica/tests/mlx_tests/test_mlx_pdf.py::test_kurt_schedule_validation +[gw1] [ 36%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_kurt_schedule_validation +pamica/tests/mlx_tests/test_mlx_pdf.py::test_pdtype_h_is_none_only_for_gg +[gw1] [ 36%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_pdtype_h_is_none_only_for_gg +pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_finite_and_monotone[0-3] +[gw6] [ 36%] PASSED pamica/tests/test_numpy_block_tuning.py::test_allocation_failure_falls_back_to_the_last_working_size +pamica/tests/test_numpy_block_tuning.py::test_a_real_error_in_the_probe_is_not_swallowed +[gw6] [ 36%] PASSED pamica/tests/test_numpy_block_tuning.py::test_a_real_error_in_the_probe_is_not_swallowed +pamica/tests/test_numpy_cli.py::test_cli_accepts_fortran_text_params_file +[gw1] [ 36%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_finite_and_monotone[0-3] +pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_finite_and_monotone[2-3] +[gw4] [ 36%] PASSED pamica/tests/test_doscaling_rows.py::test_scalestep_counts_iterations_from_one[1-numpy] +pamica/tests/test_doscaling_rows.py::test_scalestep_counts_iterations_from_one[1-mlx] +[gw1] [ 36%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_finite_and_monotone[2-3] +pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_finite_and_monotone[3-3] +[gw7] [ 36%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-newton-4blocks] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m2-newton-4blocks] +[gw4] [ 36%] PASSED pamica/tests/test_doscaling_rows.py::test_scalestep_counts_iterations_from_one[1-mlx] +pamica/tests/test_doscaling_rows.py::test_scalestep_counts_iterations_from_one[3-torch] +[gw1] [ 36%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_finite_and_monotone[3-3] +pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_finite_and_monotone[4-1] +[gw1] [ 36%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_finite_and_monotone[4-1] +pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_finite_and_monotone[1-1] +[gw6] [ 36%] PASSED pamica/tests/test_numpy_cli.py::test_cli_accepts_fortran_text_params_file +pamica/tests/test_numpy_cli.py::test_cli_still_accepts_json_params_file +[gw1] [ 36%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_family_fit_finite_and_monotone[1-1] +pamica/tests/mlx_tests/test_mlx_pdf.py::test_multimodel_fixed_family +[gw5] [ 36%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_per_model_export_roundtrips_with_nonzero_c +pamica/tests/mne_tests/test_mne_wrapper.py::test_per_model_export_is_distinct_and_cached +[gw5] [ 36%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_per_model_export_is_distinct_and_cached +pamica/tests/mne_tests/test_mne_wrapper.py::test_per_model_apply_reconstructs +[gw5] [ 37%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_per_model_apply_reconstructs +pamica/tests/mne_tests/test_mne_wrapper.py::test_per_model_apply_exclude_differs_by_model +[gw5] [ 37%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_per_model_apply_exclude_differs_by_model +pamica/tests/mne_tests/test_mne_wrapper.py::test_per_model_get_components_and_plot +[gw5] [ 37%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_per_model_get_components_and_plot +pamica/tests/mne_tests/test_mne_wrapper.py::test_model_probability_is_normalized +[gw1] [ 37%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_multimodel_fixed_family +pamica/tests/mlx_tests/test_mlx_pdf.py::test_auto_switcher_runs_and_is_stable +[gw5] [ 37%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_model_probability_is_normalized +pamica/tests/mne_tests/test_mne_wrapper.py::test_single_model_probability_is_all_ones +[gw5] [ 37%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_single_model_probability_is_all_ones +pamica/tests/mne_tests/test_mne_wrapper.py::test_model_probability_and_plot_on_epochs +[gw4] [ 37%] PASSED pamica/tests/test_doscaling_rows.py::test_scalestep_counts_iterations_from_one[3-torch] +pamica/tests/test_doscaling_rows.py::test_scalestep_counts_iterations_from_one[3-numpy] +[gw1] [ 37%] PASSED pamica/tests/mlx_tests/test_mlx_pdf.py::test_auto_switcher_runs_and_is_stable +pamica/tests/test_fortran_param_forwarding.py::test_backend_flag_rejects_unknown_names[ALL] +[gw1] [ 37%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_backend_flag_rejects_unknown_names[ALL] +pamica/tests/test_fortran_param_forwarding.py::test_every_backend_returns_the_comparison_contract[torch] +[gw7] [ 37%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m2-newton-4blocks] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-newton-4blocks] +[gw5] [ 37%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_model_probability_and_plot_on_epochs +pamica/tests/mne_tests/test_mne_wrapper.py::test_plot_model_probability_default_srate_gives_seconds +[gw6] [ 37%] PASSED pamica/tests/test_numpy_cli.py::test_cli_still_accepts_json_params_file +pamica/tests/test_numpy_cli.py::test_load_params_maps_canonical_keys_for_fortran_text +[gw6] [ 37%] PASSED pamica/tests/test_numpy_cli.py::test_load_params_maps_canonical_keys_for_fortran_text +pamica/tests/test_numpy_cli.py::test_cli_without_outdir_still_writes_to_output +[gw5] [ 37%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_plot_model_probability_default_srate_gives_seconds +pamica/tests/mne_tests/test_mne_wrapper.py::test_out_of_range_model_idx_raises +[gw5] [ 37%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_out_of_range_model_idx_raises +pamica/tests/mne_tests/test_mne_wrapper.py::test_get_model_probability_rejects_non_mne_input +[gw5] [ 37%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_get_model_probability_rejects_non_mne_input +pamica/tests/mne_tests/test_mne_wrapper.py::test_degenerate_fit_refused_on_dominance_methods +[gw1] [ 37%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_every_backend_returns_the_comparison_contract[torch] +pamica/tests/test_fortran_param_forwarding.py::test_every_backend_returns_the_comparison_contract[numpy] +[gw1] [ 37%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_every_backend_returns_the_comparison_contract[numpy] +pamica/tests/test_fortran_param_forwarding.py::test_every_backend_returns_the_comparison_contract[mlx] +[gw1] [ 37%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_every_backend_returns_the_comparison_contract[mlx] +pamica/tests/test_fortran_param_forwarding.py::test_torch_and_numpy_runners_apply_the_same_params +[gw1] [ 37%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_torch_and_numpy_runners_apply_the_same_params +pamica/tests/test_fortran_param_forwarding.py::test_mlx_runner_applies_the_same_params_as_torch +[gw1] [ 38%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_mlx_runner_applies_the_same_params_as_torch +pamica/tests/test_fortran_param_forwarding.py::test_numpy_runner_writes_under_the_output_dir +[gw1] [ 38%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_numpy_runner_writes_under_the_output_dir +pamica/tests/test_fortran_param_forwarding.py::test_main_writes_one_report_per_backend_and_a_summary +[gw7] [ 38%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-newton-4blocks] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-keepbest-restores] +[gw7] [ 38%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-keepbest-restores] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-keepbest-restores] +[gw6] [ 38%] PASSED pamica/tests/test_numpy_cli.py::test_cli_without_outdir_still_writes_to_output +pamica/tests/test_numpy_grad_norm.py::test_grad_norm_is_on_fortran_scale +[gw7] [ 38%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-keepbest-restores] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-restarts] +[gw4] [ 38%] PASSED pamica/tests/test_doscaling_rows.py::test_scalestep_counts_iterations_from_one[3-numpy] +pamica/tests/test_doscaling_rows.py::test_scalestep_counts_iterations_from_one[3-mlx] +[gw6] [ 38%] PASSED pamica/tests/test_numpy_grad_norm.py::test_grad_norm_is_on_fortran_scale +pamica/tests/test_numpy_grad_norm.py::test_grad_norm_tracks_convergence +[gw1] [ 38%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_main_writes_one_report_per_backend_and_a_summary +pamica/tests/test_fortran_param_forwarding.py::test_default_run_is_the_historical_torch_only_run +[gw4] [ 38%] PASSED pamica/tests/test_doscaling_rows.py::test_scalestep_counts_iterations_from_one[3-mlx] +pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[torch-0] +[gw4] [ 38%] PASSED pamica/tests/test_doscaling_rows.py::test_every_backend_rejects_an_invalid_scalestep[torch-0] +pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_raises_on_asymmetric +[gw4] [ 38%] PASSED pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_raises_on_asymmetric +pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_handles_ties +[gw4] [ 38%] PASSED pamica/tests/test_metrics_pmi.py::test_block_diagonal_order_handles_ties +pamica/tests/test_metrics_posterior.py::test_healthy_lht_columns_sum_to_one_and_match_model_probability +[gw7] [ 38%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[torch-m2-restarts] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m2-restarts] +[gw4] [ 38%] PASSED pamica/tests/test_metrics_posterior.py::test_healthy_lht_columns_sum_to_one_and_match_model_probability +pamica/tests/test_metrics_posterior.py::test_all_models_neg_inf_at_a_sample_raises_distinct_message +[gw4] [ 38%] PASSED pamica/tests/test_metrics_posterior.py::test_all_models_neg_inf_at_a_sample_raises_distinct_message +pamica/tests/test_metrics_posterior.py::test_nan_loglik_raises_a_different_message_than_neg_inf +[gw4] [ 38%] PASSED pamica/tests/test_metrics_posterior.py::test_nan_loglik_raises_a_different_message_than_neg_inf +pamica/tests/test_metrics_posterior.py::test_caller_prefix_appears_in_both_error_messages +[gw4] [ 38%] PASSED pamica/tests/test_metrics_posterior.py::test_caller_prefix_appears_in_both_error_messages +pamica/tests/test_mlx_convergence_stops.py::test_negative_maxincs_is_rejected +[gw4] [ 38%] PASSED pamica/tests/test_mlx_convergence_stops.py::test_negative_maxincs_is_rejected +pamica/tests/test_mlx_convergence_stops.py::test_min_dll_stops_at_the_same_iteration_as_torch +[gw1] [ 38%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_default_run_is_the_historical_torch_only_run +pamica/tests/test_fortran_param_forwarding.py::test_parity_summary_renders_every_numeric_column +[gw1] [ 39%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_parity_summary_renders_every_numeric_column +pamica/tests/test_fortran_param_forwarding.py::test_mlx_backend_without_mlx_exits_with_the_install_hint[torch,mlx] +[gw1] [ 39%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_mlx_backend_without_mlx_exits_with_the_install_hint[torch,mlx] +pamica/tests/test_fortran_param_forwarding.py::test_mlx_backend_without_mlx_exits_with_the_install_hint[all] +[gw1] [ 39%] PASSED pamica/tests/test_fortran_param_forwarding.py::test_mlx_backend_without_mlx_exits_with_the_install_hint[all] +pamica/tests/test_fortran_param_forwarding.py::test_backend_meets_its_parity_bar_against_fortran[torch] +[gw1] [ 39%] SKIPPED pamica/tests/test_fortran_param_forwarding.py::test_backend_meets_its_parity_bar_against_fortran[torch] +pamica/tests/test_fortran_param_forwarding.py::test_backend_meets_its_parity_bar_against_fortran[numpy] +[gw1] [ 39%] SKIPPED pamica/tests/test_fortran_param_forwarding.py::test_backend_meets_its_parity_bar_against_fortran[numpy] +pamica/tests/test_fortran_param_forwarding.py::test_backend_meets_its_parity_bar_against_fortran[mlx] +[gw1] [ 39%] SKIPPED pamica/tests/test_fortran_param_forwarding.py::test_backend_meets_its_parity_bar_against_fortran[mlx] +pamica/tests/test_fortran_params.py::test_known_scalar_mappings_land +[gw1] [ 39%] PASSED pamica/tests/test_fortran_params.py::test_known_scalar_mappings_land +pamica/tests/test_fortran_params.py::test_renamed_keys_translate +[gw1] [ 39%] PASSED pamica/tests/test_fortran_params.py::test_renamed_keys_translate +pamica/tests/test_fortran_params.py::test_boolean_flags_are_python_bool +[gw1] [ 39%] PASSED pamica/tests/test_fortran_params.py::test_boolean_flags_are_python_bool +pamica/tests/test_fortran_params.py::test_list_valued_keys +[gw1] [ 39%] PASSED pamica/tests/test_fortran_params.py::test_list_valued_keys +pamica/tests/test_fortran_params.py::test_unrecognized_keys_warn_and_are_dropped +[gw1] [ 39%] PASSED pamica/tests/test_fortran_params.py::test_unrecognized_keys_warn_and_are_dropped +pamica/tests/test_fortran_params.py::test_unsupported_keys_warn_and_are_dropped +[gw1] [ 39%] PASSED pamica/tests/test_fortran_params.py::test_unsupported_keys_warn_and_are_dropped +pamica/tests/test_fortran_params.py::test_block_size_search_keys_translate +[gw1] [ 39%] PASSED pamica/tests/test_fortran_params.py::test_block_size_search_keys_translate +pamica/tests/test_fortran_params.py::test_checkpoint_keys_translate +[gw1] [ 39%] PASSED pamica/tests/test_fortran_params.py::test_checkpoint_keys_translate +pamica/tests/test_fortran_params.py::test_block_size_search_keys_reach_the_backend +[gw1] [ 39%] PASSED pamica/tests/test_fortran_params.py::test_block_size_search_keys_reach_the_backend +pamica/tests/test_fortran_params.py::test_mapping_tables_partition_every_known_fortran_key +[gw1] [ 39%] PASSED pamica/tests/test_fortran_params.py::test_mapping_tables_partition_every_known_fortran_key +pamica/tests/test_fortran_params.py::test_table_sizes_match_the_documented_counts +[gw1] [ 39%] PASSED pamica/tests/test_fortran_params.py::test_table_sizes_match_the_documented_counts +pamica/tests/test_fortran_params.py::test_missing_source_disables_unrecognized_filtering +[gw1] [ 39%] PASSED pamica/tests/test_fortran_params.py::test_missing_source_disables_unrecognized_filtering +pamica/tests/test_fortran_params.py::test_repeated_key_last_line_wins +[gw1] [ 39%] PASSED pamica/tests/test_fortran_params.py::test_repeated_key_last_line_wins +pamica/tests/test_fortran_params.py::test_comments_and_blank_lines_are_skipped +[gw1] [ 39%] PASSED pamica/tests/test_fortran_params.py::test_comments_and_blank_lines_are_skipped +pamica/tests/test_fortran_params.py::test_out_of_range_flag_matches_fortran_semantics +[gw1] [ 40%] PASSED pamica/tests/test_fortran_params.py::test_out_of_range_flag_matches_fortran_semantics +pamica/tests/test_fortran_params.py::TestInlineCommentsAndExponents::test_inline_comment_is_stripped_from_scalar_value +[gw1] [ 40%] PASSED pamica/tests/test_fortran_params.py::TestInlineCommentsAndExponents::test_inline_comment_is_stripped_from_scalar_value +pamica/tests/test_fortran_params.py::TestInlineCommentsAndExponents::test_inline_comment_is_stripped_from_list_value +[gw1] [ 40%] PASSED pamica/tests/test_fortran_params.py::TestInlineCommentsAndExponents::test_inline_comment_is_stripped_from_list_value +pamica/tests/test_fortran_params.py::TestInlineCommentsAndExponents::test_hash_without_leading_space_is_not_treated_as_a_comment +[gw1] [ 40%] PASSED pamica/tests/test_fortran_params.py::TestInlineCommentsAndExponents::test_hash_without_leading_space_is_not_treated_as_a_comment +pamica/tests/test_fortran_params.py::TestInlineCommentsAndExponents::test_fortran_double_precision_exponent_uppercase +[gw1] [ 40%] PASSED pamica/tests/test_fortran_params.py::TestInlineCommentsAndExponents::test_fortran_double_precision_exponent_uppercase +pamica/tests/test_fortran_params.py::TestInlineCommentsAndExponents::test_fortran_double_precision_exponent_lowercase +[gw1] [ 40%] PASSED pamica/tests/test_fortran_params.py::TestInlineCommentsAndExponents::test_fortran_double_precision_exponent_lowercase +pamica/tests/test_fortran_params.py::TestMalformedLines::test_key_with_no_value_raises +[gw1] [ 40%] PASSED pamica/tests/test_fortran_params.py::TestMalformedLines::test_key_with_no_value_raises +pamica/tests/test_fortran_params.py::TestMalformedLines::test_unparsable_int_value_raises +[gw1] [ 40%] PASSED pamica/tests/test_fortran_params.py::TestMalformedLines::test_unparsable_int_value_raises +pamica/tests/test_fortran_params.py::TestMalformedLines::test_unparsable_float_value_raises +[gw1] [ 40%] PASSED pamica/tests/test_fortran_params.py::TestMalformedLines::test_unparsable_float_value_raises +pamica/tests/test_fortran_params.py::TestMalformedLines::test_value_that_is_only_an_inline_comment_raises +[gw1] [ 40%] PASSED pamica/tests/test_fortran_params.py::TestMalformedLines::test_value_that_is_only_an_inline_comment_raises +pamica/tests/test_fortran_params.py::TestZeroRecognizedKeysGuard::test_all_unrecognized_content_raises +[gw1] [ 40%] PASSED pamica/tests/test_fortran_params.py::TestZeroRecognizedKeysGuard::test_all_unrecognized_content_raises +pamica/tests/test_fortran_params.py::TestZeroRecognizedKeysGuard::test_all_unsupported_content_does_not_raise +[gw1] [ 40%] PASSED pamica/tests/test_fortran_params.py::TestZeroRecognizedKeysGuard::test_all_unsupported_content_does_not_raise +pamica/tests/test_numpy_kwargs.py::TestAlternateSpellingAndNChannels::test_alternate_spelling_names_the_correct_key[n_mix-num_mix] +[gw1] [ 40%] PASSED pamica/tests/test_numpy_kwargs.py::TestAlternateSpellingAndNChannels::test_alternate_spelling_names_the_correct_key[n_mix-num_mix] +pamica/tests/test_numpy_kwargs.py::TestAlternateSpellingAndNChannels::test_n_models_and_n_mix_are_not_torch_only +[gw1] [ 40%] PASSED pamica/tests/test_numpy_kwargs.py::TestAlternateSpellingAndNChannels::test_n_models_and_n_mix_are_not_torch_only +pamica/tests/test_numpy_kwargs.py::TestAlternateSpellingAndNChannels::test_n_channels_gets_its_own_message +[gw1] [ 40%] PASSED pamica/tests/test_numpy_kwargs.py::TestAlternateSpellingAndNChannels::test_n_channels_gets_its_own_message +pamica/tests/test_numpy_kwargs.py::TestAlternateSpellingAndNChannels::test_n_channels_is_not_torch_only_or_alternate_spelling +[gw1] [ 40%] PASSED pamica/tests/test_numpy_kwargs.py::TestAlternateSpellingAndNChannels::test_n_channels_is_not_torch_only_or_alternate_spelling +pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_every_offending_key_is_named[kwargs0-must_contain0] +[gw1] [ 40%] PASSED pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_every_offending_key_is_named[kwargs0-must_contain0] +pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_every_offending_key_is_named[kwargs1-must_contain1] +[gw1] [ 40%] PASSED pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_every_offending_key_is_named[kwargs1-must_contain1] +pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_every_offending_key_is_named[kwargs2-must_contain2] +[gw1] [ 40%] PASSED pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_every_offending_key_is_named[kwargs2-must_contain2] +pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_every_offending_key_is_named[kwargs3-must_contain3] +[gw5] [ 40%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_degenerate_fit_refused_on_dominance_methods +[gw1] [ 41%] PASSED pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_every_offending_key_is_named[kwargs3-must_contain3] +pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_every_offending_key_is_named[kwargs4-must_contain4] +[gw1] [ 41%] PASSED pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_every_offending_key_is_named[kwargs4-must_contain4] +pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_every_offending_key_is_named[kwargs5-must_contain5] +pamica/tests/mne_tests/test_mne_wrapper.py::test_get_pdftype_and_rho_per_model +[gw1] [ 41%] PASSED pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_every_offending_key_is_named[kwargs5-must_contain5] +pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_every_offending_key_is_named[kwargs6-must_contain6] +[gw1] [ 41%] PASSED pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_every_offending_key_is_named[kwargs6-must_contain6] +[gw5] [ 41%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_get_pdftype_and_rho_per_model +pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_single_category_messages_are_unchanged_by_the_refactor +pamica/tests/mne_tests/test_mne_wrapper.py::test_shared_components_empty_by_default +[gw1] [ 41%] PASSED pamica/tests/test_numpy_kwargs.py::TestAllCategoriesNamedTogether::test_single_category_messages_are_unchanged_by_the_refactor +pamica/tests/test_numpy_kwargs.py::TestCanonicalAliasKwargs::test_canonical_spelling_takes_effect[min_nd-min_grad_norm-5e-06] +[gw5] [ 41%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_shared_components_empty_by_default +pamica/tests/mne_tests/test_mne_wrapper.py::test_metadata_respects_model_idx_bounds +[gw5] [ 41%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_metadata_respects_model_idx_bounds +[gw1] [ 41%] PASSED pamica/tests/test_numpy_kwargs.py::TestCanonicalAliasKwargs::test_canonical_spelling_takes_effect[min_nd-min_grad_norm-5e-06] +pamica/tests/mne_tests/test_mne_wrapper.py::test_metadata_requires_fit +pamica/tests/test_numpy_kwargs.py::TestCanonicalAliasKwargs::test_canonical_spelling_takes_effect[maxdecs-max_decs-7] +[gw5] [ 41%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_metadata_requires_fit +pamica/tests/mne_tests/test_mne_wrapper.py::test_mir_matches_amica_per_model +[gw1] [ 41%] PASSED pamica/tests/test_numpy_kwargs.py::TestCanonicalAliasKwargs::test_canonical_spelling_takes_effect[maxdecs-max_decs-7] +pamica/tests/test_numpy_kwargs.py::TestCanonicalAliasKwargs::test_canonical_spelling_takes_effect[share_iter-share_int-42] +[gw1] [ 41%] PASSED pamica/tests/test_numpy_kwargs.py::TestCanonicalAliasKwargs::test_canonical_spelling_takes_effect[share_iter-share_int-42] +pamica/tests/test_numpy_kwargs.py::TestCanonicalAliasKwargs::test_both_spellings_at_once_raises[min_nd-min_grad_norm] +[gw1] [ 41%] PASSED pamica/tests/test_numpy_kwargs.py::TestCanonicalAliasKwargs::test_both_spellings_at_once_raises[min_nd-min_grad_norm] +pamica/tests/test_numpy_kwargs.py::TestCanonicalAliasKwargs::test_both_spellings_at_once_raises[maxdecs-max_decs] +[gw1] [ 41%] PASSED pamica/tests/test_numpy_kwargs.py::TestCanonicalAliasKwargs::test_both_spellings_at_once_raises[maxdecs-max_decs] +pamica/tests/test_numpy_kwargs.py::TestCanonicalAliasKwargs::test_both_spellings_at_once_raises[share_iter-share_int] +[gw1] [ 41%] PASSED pamica/tests/test_numpy_kwargs.py::TestCanonicalAliasKwargs::test_both_spellings_at_once_raises[share_iter-share_int] +pamica/tests/test_numpy_kwargs.py::TestCanonicalAliasKwargs::test_canonical_to_numpy_key_matches_alias_set +[gw1] [ 41%] PASSED pamica/tests/test_numpy_kwargs.py::TestCanonicalAliasKwargs::test_canonical_to_numpy_key_matches_alias_set +pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_files_kwarg_loads_real_data_and_fits +[gw5] [ 41%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_mir_matches_amica_per_model +pamica/tests/mne_tests/test_mne_wrapper.py::test_pmi_matches_amica_per_model +[gw1] [ 41%] PASSED pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_files_kwarg_loads_real_data_and_fits +pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_files_kwarg_matches_params_file_bit_for_bit +[gw6] [ 41%] PASSED pamica/tests/test_numpy_grad_norm.py::test_grad_norm_tracks_convergence +pamica/tests/test_numpy_grad_norm.py::test_grad_norm_uses_the_pre_step_mixing_matrix +[gw4] [ 41%] PASSED pamica/tests/test_mlx_convergence_stops.py::test_min_dll_stops_at_the_same_iteration_as_torch +pamica/tests/test_mlx_convergence_stops.py::test_min_dll_never_fires_before_two_likelihoods +[gw4] [ 41%] PASSED pamica/tests/test_mlx_convergence_stops.py::test_min_dll_never_fires_before_two_likelihoods +pamica/tests/test_mlx_convergence_stops.py::test_grad_norm_stops_at_the_same_iteration_as_torch +[gw6] [ 42%] PASSED pamica/tests/test_numpy_grad_norm.py::test_grad_norm_uses_the_pre_step_mixing_matrix +pamica/tests/test_numpy_grad_norm.py::test_nd_uses_pre_update_model_weights +[gw7] [ 42%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[numpy-m2-restarts] +pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-restarts] +[gw6] [ 42%] PASSED pamica/tests/test_numpy_grad_norm.py::test_nd_uses_pre_update_model_weights +pamica/tests/test_numpy_grad_norm.py::test_nd_ignores_the_post_update_model_weights +[gw7] [ 42%] PASSED pamica/tests/test_component_rows.py::test_unshared_fits_are_byte_identical_to_the_column_layout[mlx-m2-restarts] +pamica/tests/test_component_rows.py::test_sources_grouped_by_a_merge_share_one_component_map[torch] +[gw6] [ 42%] PASSED pamica/tests/test_numpy_grad_norm.py::test_nd_ignores_the_post_update_model_weights +pamica/tests/test_numpy_grad_norm.py::test_shared_column_keeps_grad_norm_finite +[gw6] [ 42%] PASSED pamica/tests/test_numpy_grad_norm.py::test_shared_column_keeps_grad_norm_finite +pamica/tests/test_numpy_kwargs.py::TestKeySetMatchesReadSites::test_every_consumed_key_is_read_and_vice_versa +[gw6] [ 42%] PASSED pamica/tests/test_numpy_kwargs.py::TestKeySetMatchesReadSites::test_every_consumed_key_is_read_and_vice_versa +pamica/tests/test_numpy_kwargs.py::TestKeySetMatchesReadSites::test_no_bracket_or_non_literal_reads_are_hiding_from_the_scan +[gw6] [ 42%] PASSED pamica/tests/test_numpy_kwargs.py::TestKeySetMatchesReadSites::test_no_bracket_or_non_literal_reads_are_hiding_from_the_scan +pamica/tests/test_numpy_kwargs.py::TestKeySetMatchesReadSites::test_accepted_kwargs_is_the_union_of_its_declared_sources +[gw6] [ 42%] PASSED pamica/tests/test_numpy_kwargs.py::TestKeySetMatchesReadSites::test_accepted_kwargs_is_the_union_of_its_declared_sources +pamica/tests/test_numpy_kwargs.py::TestKeySetMatchesReadSites::test_torch_only_options_derived_from_torch_signature +[gw6] [ 42%] PASSED pamica/tests/test_numpy_kwargs.py::TestKeySetMatchesReadSites::test_torch_only_options_derived_from_torch_signature +pamica/tests/test_numpy_kwargs.py::TestKeySetMatchesReadSites::test_numpy_impl_core_has_no_module_level_torch_import +[gw6] [ 42%] PASSED pamica/tests/test_numpy_kwargs.py::TestKeySetMatchesReadSites::test_numpy_impl_core_has_no_module_level_torch_import +pamica/tests/test_numpy_kwargs.py::TestKeySetMatchesReadSites::test_every_torch_only_option_has_a_reason_and_no_numpy_equivalent +[gw6] [ 42%] PASSED pamica/tests/test_numpy_kwargs.py::TestKeySetMatchesReadSites::test_every_torch_only_option_has_a_reason_and_no_numpy_equivalent +pamica/tests/test_numpy_kwargs.py::TestTypoRaises::test_typo_raises_type_error_with_suggestion +[gw6] [ 42%] PASSED pamica/tests/test_numpy_kwargs.py::TestTypoRaises::test_typo_raises_type_error_with_suggestion +pamica/tests/test_numpy_kwargs.py::TestTypoRaises::test_typo_of_a_named_constructor_parameter_is_suggested[verbos-verbose] +[gw6] [ 42%] PASSED pamica/tests/test_numpy_kwargs.py::TestTypoRaises::test_typo_of_a_named_constructor_parameter_is_suggested[verbos-verbose] +pamica/tests/test_numpy_kwargs.py::TestTypoRaises::test_typo_of_a_named_constructor_parameter_is_suggested[param_file-params_file] +[gw6] [ 42%] PASSED pamica/tests/test_numpy_kwargs.py::TestTypoRaises::test_typo_of_a_named_constructor_parameter_is_suggested[param_file-params_file] +pamica/tests/test_numpy_kwargs.py::TestTypoRaises::test_typo_of_a_named_constructor_parameter_is_suggested[use_tqdmm-use_tqdm] +[gw6] [ 42%] PASSED pamica/tests/test_numpy_kwargs.py::TestTypoRaises::test_typo_of_a_named_constructor_parameter_is_suggested[use_tqdmm-use_tqdm] +pamica/tests/test_numpy_kwargs.py::TestTypoRaises::test_unknown_keyword_with_no_close_match_still_raises +[gw6] [ 42%] PASSED pamica/tests/test_numpy_kwargs.py::TestTypoRaises::test_unknown_keyword_with_no_close_match_still_raises +pamica/tests/test_numpy_kwargs.py::TestTypoRaises::test_multiple_unknown_keywords_all_named +[gw6] [ 42%] PASSED pamica/tests/test_numpy_kwargs.py::TestTypoRaises::test_multiple_unknown_keywords_all_named +pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_torch_only_option_raises_specific_message[device] +[gw6] [ 42%] PASSED pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_torch_only_option_raises_specific_message[device] +pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_torch_only_option_raises_specific_message[dtype] +[gw6] [ 42%] PASSED pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_torch_only_option_raises_specific_message[dtype] +pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_torch_only_option_raises_specific_message[keep_best] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_torch_only_option_raises_specific_message[keep_best] +pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_torch_only_option_raises_specific_message[kurt_int] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_torch_only_option_raises_specific_message[kurt_int] +pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_torch_only_option_raises_specific_message[kurt_start] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_torch_only_option_raises_specific_message[kurt_start] +pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_torch_only_option_raises_specific_message[num_kurt] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_torch_only_option_raises_specific_message[num_kurt] +pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_keep_best_specifically +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_keep_best_specifically +pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_device_and_dtype_specifically +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestUnsupportedCrossBackendOption::test_device_and_dtype_specifically +pamica/tests/test_numpy_kwargs.py::TestAlternateSpellingAndNChannels::test_alternate_spelling_names_the_correct_key[n_models-num_models] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestAlternateSpellingAndNChannels::test_alternate_spelling_names_the_correct_key[n_models-num_models] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[minrho] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[minrho] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[n_restarts] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[n_restarts] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[newt_ramp] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[newt_ramp] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[newt_start] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[newt_start] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[newtrate] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[newtrate] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[num_comps] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[num_comps] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[num_mix] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[num_mix] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[num_models] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[num_models] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[pcadb] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[pcadb] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[pcakeep] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[pcakeep] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[pdftype] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[pdftype] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[rejint] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[rejint] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[rejsig] +[gw6] [ 43%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[rejsig] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[rejstart] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[rejstart] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[restart_seeds] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[restart_seeds] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[restartiter] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[restartiter] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[rho0] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[rho0] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[rholrate] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[rholrate] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[rholratefact] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[rholratefact] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[scalestep] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[scalestep] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[seed] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[seed] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[share_comps] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[share_comps] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[share_int] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[share_int] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[share_iter] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[share_iter] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[share_start] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[share_start] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[use_grad_norm] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[use_grad_norm] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[use_min_dll] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[use_min_dll] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[writestep] +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[writestep] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_outdir_constructs_and_takes_effect +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_outdir_constructs_and_takes_effect +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_all_options_together_construct +[gw6] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_all_options_together_construct +pamica/tests/test_numpy_newton_multimodel.py::test_multimodel_newton_fit_finalizes_curvature[1] +[gw1] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_files_kwarg_matches_params_file_bit_for_bit +pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_conflicting_value_with_params_file_raises[files-kwarg_value0] +[gw1] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_conflicting_value_with_params_file_raises[files-kwarg_value0] +pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_conflicting_value_with_params_file_raises[data_dim-33] +[gw1] [ 44%] PASSED pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_conflicting_value_with_params_file_raises[data_dim-33] +pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_conflicting_value_with_params_file_raises[field_dim-kwarg_value2] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_conflicting_value_with_params_file_raises[field_dim-kwarg_value2] +pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_same_value_from_both_sources_is_not_a_conflict +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_same_value_from_both_sources_is_not_a_conflict +pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_no_files_error_message_mentions_keyword_argument +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_no_files_error_message_mentions_keyword_argument +pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_no_data_dim_error_message_mentions_keyword_argument +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestDataLocationKwargs::test_no_data_dim_error_message_mentions_keyword_argument +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_values_cover_every_consumed_key +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_values_cover_every_consumed_key +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[blk_max] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[blk_max] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[blk_min] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[blk_min] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[blk_step] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[blk_step] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[block_size] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[block_size] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[comp_thresh] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[comp_thresh] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[do_approx_sphere] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[do_approx_sphere] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[do_history] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[do_history] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[do_mean] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[do_mean] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[do_newton] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[do_newton] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[do_opt_block] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[do_opt_block] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[do_reject] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[do_reject] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[do_sphere] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[do_sphere] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[doscaling] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[doscaling] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[histstep] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[histstep] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[invsigmax] +[gw1] [ 45%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[invsigmax] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[invsigmin] +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[invsigmin] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[lrate] +[gw4] [ 46%] PASSED pamica/tests/test_mlx_convergence_stops.py::test_grad_norm_stops_at_the_same_iteration_as_torch +pamica/tests/test_mlx_convergence_stops.py::test_grad_norm_never_fires_before_two_likelihoods +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[lrate] +[gw6] [ 46%] PASSED pamica/tests/test_numpy_newton_multimodel.py::test_multimodel_newton_fit_finalizes_curvature[1] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[lratefact] +pamica/tests/test_numpy_newton_multimodel.py::test_multimodel_newton_fit_finalizes_curvature[2] +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[lratefact] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[max_decs] +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[max_decs] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[max_iter] +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[max_iter] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[maxdecs] +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[maxdecs] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[maxincs] +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[maxincs] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[maxrej] +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[maxrej] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[maxrestarts] +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[maxrestarts] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[maxrho] +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[maxrho] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[min_dll] +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[min_dll] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[min_grad_norm] +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[min_grad_norm] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[min_nd] +[gw4] [ 46%] PASSED pamica/tests/test_mlx_convergence_stops.py::test_grad_norm_never_fires_before_two_likelihoods +pamica/tests/test_mlx_convergence_stops.py::test_grad_norm_is_the_norm_of_the_step_actually_taken +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[min_nd] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[mineig] +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[mineig] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[mineig_rel] +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[mineig_rel] +pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[minlrate] +[gw1] [ 46%] PASSED pamica/tests/test_numpy_kwargs.py::TestEveryDocumentedOptionConstructs::test_option_constructs_and_takes_effect[minlrate] +pamica/tests/test_numpy_restarts.py::test_records_are_aligned_with_the_independent_fits +[gw4] [ 46%] PASSED pamica/tests/test_mlx_convergence_stops.py::test_grad_norm_is_the_norm_of_the_step_actually_taken +pamica/tests/test_mlx_convergence_stops.py::test_grad_norm_floor_fires_on_a_likelihood_decrease +[gw7] [ 47%] PASSED pamica/tests/test_component_rows.py::test_sources_grouped_by_a_merge_share_one_component_map[torch] +pamica/tests/test_component_rows.py::test_sources_grouped_by_a_merge_share_one_component_map[numpy] +[gw4] [ 47%] PASSED pamica/tests/test_mlx_convergence_stops.py::test_grad_norm_floor_fires_on_a_likelihood_decrease +pamica/tests/test_mlx_convergence_stops.py::test_lrate_floor_still_fires_when_grad_norm_cannot +[gw4] [ 47%] PASSED pamica/tests/test_mlx_convergence_stops.py::test_lrate_floor_still_fires_when_grad_norm_cannot +pamica/tests/test_mlx_convergence_stops.py::test_grad_norm_shadows_grad_norm_floor_under_shipped_defaults +[gw4] [ 47%] PASSED pamica/tests/test_mlx_convergence_stops.py::test_grad_norm_shadows_grad_norm_floor_under_shipped_defaults +pamica/tests/test_mlx_convergence_stops.py::test_shipped_defaults_are_inert_on_a_100_iteration_fit +[gw6] [ 47%] PASSED pamica/tests/test_numpy_newton_multimodel.py::test_multimodel_newton_fit_finalizes_curvature[2] +pamica/tests/test_numpy_newton_multimodel.py::test_multimodel_newton_fit_finalizes_curvature[3] +[gw6] [ 47%] PASSED pamica/tests/test_numpy_newton_multimodel.py::test_multimodel_newton_fit_finalizes_curvature[3] +pamica/tests/test_numpy_newton_multimodel.py::test_rank_reduced_newton_fit_completes +[gw5] [ 47%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_pmi_matches_amica_per_model +pamica/tests/mne_tests/test_mne_wrapper.py::test_mir_pmi_on_epochs +[gw1] [ 47%] PASSED pamica/tests/test_numpy_restarts.py::test_records_are_aligned_with_the_independent_fits +pamica/tests/test_numpy_restarts.py::test_derived_seeds_run_the_documented_sequence +[gw1] [ 47%] PASSED pamica/tests/test_numpy_restarts.py::test_derived_seeds_run_the_documented_sequence +pamica/tests/test_numpy_restarts.py::test_a_refit_repeats_the_same_search +[gw7] [ 47%] PASSED pamica/tests/test_component_rows.py::test_sources_grouped_by_a_merge_share_one_component_map[numpy] +pamica/tests/test_component_rows.py::test_sources_grouped_by_a_merge_share_one_component_map[mlx] +[gw7] [ 47%] PASSED pamica/tests/test_component_rows.py::test_sources_grouped_by_a_merge_share_one_component_map[mlx] +pamica/tests/test_component_rows.py::test_a_planted_duplicate_is_merged_and_the_model_is_unchanged[torch-default] +[gw6] [ 47%] PASSED pamica/tests/test_numpy_newton_multimodel.py::test_rank_reduced_newton_fit_completes +pamica/tests/test_numpy_outdir.py::test_default_writes_nothing +[gw6] [ 47%] PASSED pamica/tests/test_numpy_outdir.py::test_default_writes_nothing +pamica/tests/test_numpy_outdir.py::test_explicit_outdir_writes_as_before +[gw6] [ 47%] PASSED pamica/tests/test_numpy_outdir.py::test_explicit_outdir_writes_as_before +pamica/tests/test_numpy_outdir.py::test_params_file_outdir_is_explicit +[gw6] [ 47%] PASSED pamica/tests/test_numpy_outdir.py::test_params_file_outdir_is_explicit +pamica/tests/test_numpy_params_file.py::TestNumpyFromBothParamsFormats::test_shared_hyperparameters_agree +[gw6] [ 47%] PASSED pamica/tests/test_numpy_params_file.py::TestNumpyFromBothParamsFormats::test_shared_hyperparameters_agree +pamica/tests/test_numpy_params_file.py::TestNumpyFromBothParamsFormats::test_fortran_text_fit_runs_with_no_explicit_data +[gw1] [ 47%] PASSED pamica/tests/test_numpy_restarts.py::test_a_refit_repeats_the_same_search +pamica/tests/test_numpy_restarts.py::test_a_restart_does_not_inherit_the_previous_restart_schedule +[gw5] [ 47%] PASSED pamica/tests/mne_tests/test_mne_wrapper.py::test_mir_pmi_on_epochs +pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[torch-min_dll] +[gw4] [ 47%] PASSED pamica/tests/test_mlx_convergence_stops.py::test_shipped_defaults_are_inert_on_a_100_iteration_fit +pamica/tests/test_mlx_keepbest_cross_backend.py::test_torch_keep_best_satisfies_the_max_contract +[gw1] [ 47%] PASSED pamica/tests/test_numpy_restarts.py::test_a_restart_does_not_inherit_the_previous_restart_schedule +pamica/tests/test_numpy_restarts.py::test_a_degenerate_restart_is_recorded_but_never_selected +[gw6] [ 47%] PASSED pamica/tests/test_numpy_params_file.py::TestNumpyFromBothParamsFormats::test_fortran_text_fit_runs_with_no_explicit_data +pamica/tests/test_numpy_params_file.py::TestNumpyKwargsOverrideFile::test_explicit_kwargs_win_over_file_defaults +[gw6] [ 48%] PASSED pamica/tests/test_numpy_params_file.py::TestNumpyKwargsOverrideFile::test_explicit_kwargs_win_over_file_defaults +pamica/tests/test_numpy_params_file.py::TestNumpyPdftypeGuard::test_non_gg_pdftype_raises[1] +[gw6] [ 48%] PASSED pamica/tests/test_numpy_params_file.py::TestNumpyPdftypeGuard::test_non_gg_pdftype_raises[1] +pamica/tests/test_numpy_params_file.py::TestNumpyPdftypeGuard::test_non_gg_pdftype_raises[2] +[gw6] [ 48%] PASSED pamica/tests/test_numpy_params_file.py::TestNumpyPdftypeGuard::test_non_gg_pdftype_raises[2] +pamica/tests/test_numpy_params_file.py::TestNumpyPdftypeGuard::test_non_gg_pdftype_raises[3] +[gw6] [ 48%] PASSED pamica/tests/test_numpy_params_file.py::TestNumpyPdftypeGuard::test_non_gg_pdftype_raises[3] +pamica/tests/test_numpy_params_file.py::TestNumpyPdftypeGuard::test_non_gg_pdftype_raises[4] +[gw6] [ 48%] PASSED pamica/tests/test_numpy_params_file.py::TestNumpyPdftypeGuard::test_non_gg_pdftype_raises[4] +pamica/tests/test_numpy_params_file.py::TestNumpyPdftypeGuard::test_gg_pdftype_constructs +[gw6] [ 48%] PASSED pamica/tests/test_numpy_params_file.py::TestNumpyPdftypeGuard::test_gg_pdftype_constructs +pamica/tests/test_numpy_params_file.py::TestNumpyPdftypeGuard::test_default_pdftype_is_gg +[gw6] [ 48%] PASSED pamica/tests/test_numpy_params_file.py::TestNumpyPdftypeGuard::test_default_pdftype_is_gg +pamica/tests/test_numpy_params_file.py::TestNumpyUnconsumedKeyWarning::test_input_param_unconsumed_keys_are_named +[gw6] [ 48%] PASSED pamica/tests/test_numpy_params_file.py::TestNumpyUnconsumedKeyWarning::test_input_param_unconsumed_keys_are_named +pamica/tests/test_numpy_params_file.py::TestNumpyUnconsumedKeyWarning::test_sample_params_json_has_no_unconsumed_keys +[gw6] [ 48%] PASSED pamica/tests/test_numpy_params_file.py::TestNumpyUnconsumedKeyWarning::test_sample_params_json_has_no_unconsumed_keys +pamica/tests/test_numpy_params_file.py::TestNumpyUnconsumedKeyWarning::test_bundled_default_has_no_unconsumed_keys +[gw6] [ 48%] PASSED pamica/tests/test_numpy_params_file.py::TestNumpyUnconsumedKeyWarning::test_bundled_default_has_no_unconsumed_keys +pamica/tests/test_numpy_params_file.py::TestNumpyUnconsumedKeyWarning::test_checkpoint_keys_reach_numpy_attributes +[gw6] [ 48%] PASSED pamica/tests/test_numpy_params_file.py::TestNumpyUnconsumedKeyWarning::test_checkpoint_keys_reach_numpy_attributes +pamica/tests/test_numpy_reject.py::test_reject_param_validation +[gw6] [ 48%] PASSED pamica/tests/test_numpy_reject.py::test_reject_param_validation +pamica/tests/test_numpy_reject.py::test_do_reject_false_leaves_good_idx_unset +[gw1] [ 48%] PASSED pamica/tests/test_numpy_restarts.py::test_a_degenerate_restart_is_recorded_but_never_selected +pamica/tests/test_numpy_restarts.py::test_all_degenerate_restarts_keep_the_degenerate_contract +[gw6] [ 48%] PASSED pamica/tests/test_numpy_reject.py::test_do_reject_false_leaves_good_idx_unset +pamica/tests/test_numpy_reject.py::test_rejection_shrinks_good_sample_set +[gw1] [ 48%] PASSED pamica/tests/test_numpy_restarts.py::test_all_degenerate_restarts_keep_the_degenerate_contract +pamica/tests/test_numpy_restarts.py::test_a_crashing_restart_does_not_kill_the_search +[gw5] [ 48%] PASSED pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[torch-min_dll] +pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[torch-grad_norm] +[gw6] [ 48%] PASSED pamica/tests/test_numpy_reject.py::test_rejection_shrinks_good_sample_set +pamica/tests/test_numpy_reject.py::test_rejection_does_not_spuriously_stop_fit +[gw1] [ 48%] PASSED pamica/tests/test_numpy_restarts.py::test_a_crashing_restart_does_not_kill_the_search +pamica/tests/test_numpy_restarts.py::test_a_single_restart_still_raises +[gw1] [ 48%] PASSED pamica/tests/test_numpy_restarts.py::test_a_single_restart_still_raises +pamica/tests/test_numpy_restarts.py::test_all_restarts_crashing_keeps_the_degenerate_contract +[gw1] [ 48%] PASSED pamica/tests/test_numpy_restarts.py::test_all_restarts_crashing_keeps_the_degenerate_contract +pamica/tests/test_numpy_restarts.py::test_the_written_output_is_the_winners_not_the_last_restarts +[gw6] [ 49%] PASSED pamica/tests/test_numpy_reject.py::test_rejection_does_not_spuriously_stop_fit +pamica/tests/test_numpy_reject.py::test_multimodel_rejection_keeps_gm_and_c_finite +[gw5] [ 49%] PASSED pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[torch-grad_norm] +pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[torch-grad_norm_floor] +[gw1] [ 49%] PASSED pamica/tests/test_numpy_restarts.py::test_the_written_output_is_the_winners_not_the_last_restarts +pamica/tests/test_numpy_restarts.py::test_the_winners_tuned_block_size_is_restored +[gw6] [ 49%] PASSED pamica/tests/test_numpy_reject.py::test_multimodel_rejection_keeps_gm_and_c_finite +pamica/tests/test_numpy_reject.py::test_rejection_nonfinite_ll_raises_instability_error +[gw6] [ 49%] PASSED pamica/tests/test_numpy_reject.py::test_rejection_nonfinite_ll_raises_instability_error +pamica/tests/test_numpy_restarts.py::test_single_restart_is_bit_identical_to_a_direct_fit +[gw5] [ 49%] PASSED pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[torch-grad_norm_floor] +pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[torch-lrate_floor] +[gw4] [ 49%] PASSED pamica/tests/test_mlx_keepbest_cross_backend.py::test_torch_keep_best_satisfies_the_max_contract +pamica/tests/test_mlx_keepbest_cross_backend.py::test_mlx_keep_best_satisfies_the_max_contract +[gw4] [ 49%] PASSED pamica/tests/test_mlx_keepbest_cross_backend.py::test_mlx_keep_best_satisfies_the_max_contract +pamica/tests/test_mlx_keepbest_cross_backend.py::test_both_backends_disable_the_safeguard_under_share_comps +[gw5] [ 49%] PASSED pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[torch-lrate_floor] +pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[numpy-min_dll] +[gw6] [ 49%] PASSED pamica/tests/test_numpy_restarts.py::test_single_restart_is_bit_identical_to_a_direct_fit +pamica/tests/test_numpy_restarts.py::test_single_restart_records_the_one_fit_it_ran +[gw1] [ 49%] PASSED pamica/tests/test_numpy_restarts.py::test_the_winners_tuned_block_size_is_restored +pamica/tests/test_numpy_share_comps.py::test_share_constructor_validation[kwargs0-share_start] +[gw1] [ 49%] PASSED pamica/tests/test_numpy_share_comps.py::test_share_constructor_validation[kwargs0-share_start] +pamica/tests/test_numpy_share_comps.py::test_share_constructor_validation[kwargs1-share_int] +[gw1] [ 49%] PASSED pamica/tests/test_numpy_share_comps.py::test_share_constructor_validation[kwargs1-share_int] +pamica/tests/test_numpy_share_comps.py::test_share_constructor_validation[kwargs2-share_int] +[gw1] [ 49%] PASSED pamica/tests/test_numpy_share_comps.py::test_share_constructor_validation[kwargs2-share_int] +pamica/tests/test_numpy_share_comps.py::test_share_constructor_validation[kwargs3-comp_thresh] +[gw1] [ 49%] PASSED pamica/tests/test_numpy_share_comps.py::test_share_constructor_validation[kwargs3-comp_thresh] +pamica/tests/test_numpy_share_comps.py::test_share_constructor_validation[kwargs4-comp_thresh] +[gw1] [ 49%] PASSED pamica/tests/test_numpy_share_comps.py::test_share_constructor_validation[kwargs4-comp_thresh] +pamica/tests/test_numpy_share_comps.py::test_comp_used_survives_a_second_identify_call +[gw6] [ 49%] PASSED pamica/tests/test_numpy_restarts.py::test_single_restart_records_the_one_fit_it_ran +pamica/tests/test_numpy_restarts.py::test_an_explicit_single_seed_overrides_the_constructor_seed +[gw1] [ 49%] PASSED pamica/tests/test_numpy_share_comps.py::test_comp_used_survives_a_second_identify_call +pamica/tests/test_numpy_share_comps.py::test_comp_used_matches_the_columns_comp_list_references +[gw6] [ 49%] PASSED pamica/tests/test_numpy_restarts.py::test_an_explicit_single_seed_overrides_the_constructor_seed +pamica/tests/test_numpy_restarts.py::test_best_of_n_returns_the_argmax_of_the_independent_fits +[gw1] [ 49%] PASSED pamica/tests/test_numpy_share_comps.py::test_comp_used_matches_the_columns_comp_list_references +pamica/tests/test_numpy_share_comps.py::test_sharing_leaves_finite_mixture_parameters +[gw6] [ 50%] PASSED pamica/tests/test_numpy_restarts.py::test_best_of_n_returns_the_argmax_of_the_independent_fits +pamica/tests/test_numpy_share_comps.py::test_a_frozen_window_matches_the_torch_schedule[False-2] +[gw6] [ 50%] PASSED pamica/tests/test_numpy_share_comps.py::test_a_frozen_window_matches_the_torch_schedule[False-2] +pamica/tests/test_numpy_share_comps.py::test_a_frozen_window_matches_the_torch_schedule[False-1] +[gw6] [ 50%] PASSED pamica/tests/test_numpy_share_comps.py::test_a_frozen_window_matches_the_torch_schedule[False-1] +pamica/tests/test_numpy_share_comps.py::test_a_frozen_applies_to_a_single_model_without_sharing +[gw6] [ 50%] PASSED pamica/tests/test_numpy_share_comps.py::test_a_frozen_applies_to_a_single_model_without_sharing +pamica/tests/test_numpy_share_comps.py::test_freeze_holds_A_but_still_measures_the_gradient +[gw4] [ 50%] PASSED pamica/tests/test_mlx_keepbest_cross_backend.py::test_both_backends_disable_the_safeguard_under_share_comps +pamica/tests/test_mlx_newton_cross_backend.py::test_newton_curvature_matches_float64_twin[1] +[gw1] [ 50%] PASSED pamica/tests/test_numpy_share_comps.py::test_sharing_leaves_finite_mixture_parameters +pamica/tests/test_numpy_share_comps.py::test_unused_columns_keep_their_last_finite_value +[gw6] [ 50%] PASSED pamica/tests/test_numpy_share_comps.py::test_freeze_holds_A_but_still_measures_the_gradient +pamica/tests/test_numpy_share_comps.py::test_shared_column_update_matches_the_torch_backend +[gw4] [ 50%] PASSED pamica/tests/test_mlx_newton_cross_backend.py::test_newton_curvature_matches_float64_twin[1] +pamica/tests/test_mlx_newton_cross_backend.py::test_newton_curvature_matches_float64_twin[55] +[gw5] [ 50%] PASSED pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[numpy-min_dll] +pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[numpy-grad_norm] +[gw2] [ 50%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_full_fit_ll_matches_torch_float64[2-3] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_full_fit_ll_matches_torch_float64[3-3] +[gw3] [ 50%] PASSED pamica/tests/test_component_rows.py::test_the_metric_compares_the_sensor_maps[mlx] +pamica/tests/test_component_rows.py::test_the_exported_A_is_the_reference_layout[torch] +[gw1] [ 50%] PASSED pamica/tests/test_numpy_share_comps.py::test_unused_columns_keep_their_last_finite_value +pamica/tests/test_numpy_share_comps.py::test_default_comp_list_is_unaffected +[gw4] [ 50%] PASSED pamica/tests/test_mlx_newton_cross_backend.py::test_newton_curvature_matches_float64_twin[55] +pamica/tests/test_mlx_newton_cross_backend.py::test_warmed_newton_mstep_matches_float64_twin +[gw6] [ 50%] PASSED pamica/tests/test_numpy_share_comps.py::test_shared_column_update_matches_the_torch_backend +pamica/tests/test_numpy_share_comps.py::test_forced_merge_fit_is_finite_in_both_backends +[gw3] [ 50%] PASSED pamica/tests/test_component_rows.py::test_the_exported_A_is_the_reference_layout[torch] +pamica/tests/test_component_rows.py::test_the_exported_A_is_the_reference_layout[numpy] +[gw1] [ 50%] PASSED pamica/tests/test_numpy_share_comps.py::test_default_comp_list_is_unaffected +pamica/tests/test_numpy_share_comps.py::test_non_finite_parameters_are_reported_as_failure +[gw4] [ 50%] PASSED pamica/tests/test_mlx_newton_cross_backend.py::test_warmed_newton_mstep_matches_float64_twin +pamica/tests/test_mlx_newton_cross_backend.py::test_early_block_newton_falls_back_like_the_float64_twin +[gw4] [ 50%] PASSED pamica/tests/test_mlx_newton_cross_backend.py::test_early_block_newton_falls_back_like_the_float64_twin +pamica/tests/test_mlx_newton_cross_backend.py::test_multimodel_newton_mstep_matches_float64_twin[False] +[gw1] [ 50%] PASSED pamica/tests/test_numpy_share_comps.py::test_non_finite_parameters_are_reported_as_failure +pamica/tests/test_numpy_share_comps.py::test_checkpoints_never_persist_non_finite_parameters +[gw1] [ 50%] PASSED pamica/tests/test_numpy_share_comps.py::test_checkpoints_never_persist_non_finite_parameters +pamica/tests/test_numpy_share_comps.py::test_shared_column_takes_one_gm_weighted_step +[gw4] [ 51%] PASSED pamica/tests/test_mlx_newton_cross_backend.py::test_multimodel_newton_mstep_matches_float64_twin[False] +pamica/tests/test_mlx_newton_cross_backend.py::test_multimodel_newton_mstep_matches_float64_twin[True] +[gw4] [ 51%] PASSED pamica/tests/test_mlx_newton_cross_backend.py::test_multimodel_newton_mstep_matches_float64_twin[True] +pamica/tests/test_mlx_newton_cross_backend.py::test_three_model_newton_curvature_matches_float64_twin +[gw5] [ 51%] PASSED pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[numpy-grad_norm] +pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[numpy-grad_norm_floor] +[gw1] [ 51%] PASSED pamica/tests/test_numpy_share_comps.py::test_shared_column_takes_one_gm_weighted_step +pamica/tests/test_numpy_share_comps.py::test_merged_away_column_does_not_move +[gw3] [ 51%] PASSED pamica/tests/test_component_rows.py::test_the_exported_A_is_the_reference_layout[numpy] +pamica/tests/test_component_rows.py::test_the_exported_A_is_the_reference_layout[mlx] +[gw4] [ 51%] PASSED pamica/tests/test_mlx_newton_cross_backend.py::test_three_model_newton_curvature_matches_float64_twin +pamica/tests/test_mlx_newton_cross_backend.py::test_newton_fit_ll_matches_float64_torch +[gw3] [ 51%] PASSED pamica/tests/test_component_rows.py::test_the_exported_A_is_the_reference_layout[mlx] +pamica/tests/test_component_rows.py::test_single_model_export_is_byte_identical_to_the_pre_change_code[torch] +[gw5] [ 51%] PASSED pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[numpy-grad_norm_floor] +pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[numpy-lrate_floor] +[gw1] [ 51%] PASSED pamica/tests/test_numpy_share_comps.py::test_merged_away_column_does_not_move +pamica/tests/test_numpy_share_comps.py::test_a_frozen_window_matches_the_torch_schedule[True-2] +[gw1] [ 51%] PASSED pamica/tests/test_numpy_share_comps.py::test_a_frozen_window_matches_the_torch_schedule[True-2] +pamica/tests/test_pamica.py::test_pdf_general_branch_is_continuous_with_the_special_cases +[gw1] [ 51%] PASSED pamica/tests/test_pamica.py::test_pdf_general_branch_is_continuous_with_the_special_cases +pamica/tests/test_pamica.py::test_data_loading +[gw1] [ 51%] PASSED pamica/tests/test_pamica.py::test_data_loading +pamica/tests/test_pamica.py::test_error_handling +[gw1] [ 51%] PASSED pamica/tests/test_pamica.py::test_error_handling +pamica/tests/test_pca_reduction_cross_backend.py::test_torch_and_numpy_agree_on_the_reduced_sphere[pcakeep] +[gw3] [ 51%] PASSED pamica/tests/test_component_rows.py::test_single_model_export_is_byte_identical_to_the_pre_change_code[torch] +pamica/tests/test_component_rows.py::test_single_model_export_is_byte_identical_to_the_pre_change_code[numpy] +[gw5] [ 51%] PASSED pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[numpy-lrate_floor] +pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[mlx-min_dll] +[gw1] [ 51%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_torch_and_numpy_agree_on_the_reduced_sphere[pcakeep] +pamica/tests/test_pca_reduction_cross_backend.py::test_torch_and_numpy_agree_on_the_reduced_sphere[pcadb] +[gw6] [ 51%] PASSED pamica/tests/test_numpy_share_comps.py::test_forced_merge_fit_is_finite_in_both_backends +pamica/tests/test_numpy_share_comps.py::test_merge_on_the_final_iteration_completes +[gw3] [ 51%] PASSED pamica/tests/test_component_rows.py::test_single_model_export_is_byte_identical_to_the_pre_change_code[numpy] +pamica/tests/test_component_rows.py::test_single_model_export_is_byte_identical_to_the_pre_change_code[mlx] +[gw3] [ 51%] PASSED pamica/tests/test_component_rows.py::test_single_model_export_is_byte_identical_to_the_pre_change_code[mlx] +pamica/tests/test_component_rows.py::test_load_results_refuses_a_pre_change_multi_model_A +[gw1] [ 51%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_torch_and_numpy_agree_on_the_reduced_sphere[pcadb] +pamica/tests/test_pca_reduction_cross_backend.py::test_mlx_agrees_with_torch_on_the_reduced_sphere[pcakeep] +[gw5] [ 52%] PASSED pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[mlx-min_dll] +pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[mlx-grad_norm] +[gw1] [ 52%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mlx_agrees_with_torch_on_the_reduced_sphere[pcakeep] +pamica/tests/test_pca_reduction_cross_backend.py::test_mlx_agrees_with_torch_on_the_reduced_sphere[pcadb] +[gw1] [ 52%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mlx_agrees_with_torch_on_the_reduced_sphere[pcadb] +pamica/tests/test_pca_reduction_cross_backend.py::test_pcakeep_and_pcadb_together_equal_pcakeep_alone[torch] +[gw5] [ 52%] PASSED pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[mlx-grad_norm] +pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[mlx-grad_norm_floor] +[gw1] [ 52%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_pcakeep_and_pcadb_together_equal_pcakeep_alone[torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_pcakeep_and_pcadb_together_equal_pcakeep_alone[numpy] +[gw3] [ 52%] PASSED pamica/tests/test_component_rows.py::test_load_results_refuses_a_pre_change_multi_model_A +pamica/tests/test_component_rows.py::test_load_results_refuses_a_truncated_A[one-value-short] +[gw5] [ 52%] PASSED pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[mlx-grad_norm_floor] +pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[mlx-lrate_floor] +[gw5] [ 52%] PASSED pamica/tests/test_iteration_order.py::test_a_stopped_fit_returns_the_parameters_its_final_ll_describes[mlx-lrate_floor] +pamica/tests/test_iteration_order.py::test_a_keep_best_restore_returns_the_parameters_of_its_ll[torch] +[gw1] [ 52%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_pcakeep_and_pcadb_together_equal_pcakeep_alone[numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_pcakeep_and_pcadb_together_equal_pcakeep_alone[mlx] +[gw3] [ 52%] PASSED pamica/tests/test_component_rows.py::test_load_results_refuses_a_truncated_A[one-value-short] +pamica/tests/test_component_rows.py::test_load_results_refuses_a_truncated_A[one-component-short] +[gw1] [ 52%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_pcakeep_and_pcadb_together_equal_pcakeep_alone[mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_precedence_is_logged_once_per_model[torch] +[gw1] [ 52%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_precedence_is_logged_once_per_model[torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_precedence_is_logged_once_per_model[numpy] +[gw1] [ 52%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_precedence_is_logged_once_per_model[numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_precedence_is_logged_once_per_model[mlx] +[gw1] [ 52%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_precedence_is_logged_once_per_model[mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_do_sphere_false_ignores_the_request_with_one_warning[torch] +[gw1] [ 52%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_do_sphere_false_ignores_the_request_with_one_warning[torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_do_sphere_false_ignores_the_request_with_one_warning[numpy] +[gw1] [ 52%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_do_sphere_false_ignores_the_request_with_one_warning[numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_do_sphere_false_ignores_the_request_with_one_warning[mlx] +[gw5] [ 52%] PASSED pamica/tests/test_iteration_order.py::test_a_keep_best_restore_returns_the_parameters_of_its_ll[torch] +pamica/tests/test_iteration_order.py::test_a_keep_best_restore_returns_the_parameters_of_its_ll[mlx] +[gw1] [ 52%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_do_sphere_false_ignores_the_request_with_one_warning[mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs0-pcakeep-torch] +[gw1] [ 52%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs0-pcakeep-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs0-pcakeep-numpy] +[gw1] [ 52%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs0-pcakeep-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs0-pcakeep-mlx] +[gw1] [ 52%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs0-pcakeep-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs1-pcakeep-torch] +[gw1] [ 53%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs1-pcakeep-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs1-pcakeep-numpy] +[gw1] [ 53%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs1-pcakeep-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs1-pcakeep-mlx] +[gw1] [ 53%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs1-pcakeep-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs2-pcakeep-torch] +[gw1] [ 53%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs2-pcakeep-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs2-pcakeep-numpy] +[gw1] [ 53%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs2-pcakeep-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs2-pcakeep-mlx] +[gw1] [ 53%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs2-pcakeep-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs3-pcakeep-torch] +[gw1] [ 53%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs3-pcakeep-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs3-pcakeep-numpy] +[gw1] [ 53%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs3-pcakeep-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs3-pcakeep-mlx] +[gw1] [ 53%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs3-pcakeep-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs2-mlx] +[gw1] [ 53%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs2-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs3-torch] +[gw1] [ 53%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs3-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs3-mlx] +[gw1] [ 53%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs3-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs0-torch] +[gw6] [ 53%] PASSED pamica/tests/test_numpy_share_comps.py::test_merge_on_the_final_iteration_completes +pamica/tests/test_numpy_share_comps.py::test_numpy_merge_decision_matches_torch_backend +[gw5] [ 53%] PASSED pamica/tests/test_iteration_order.py::test_a_keep_best_restore_returns_the_parameters_of_its_ll[mlx] +pamica/tests/test_iteration_order.py::test_the_rho_rate_ceiling_survives_a_save[torch] +[gw1] [ 53%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs0-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs0-mlx] +[gw3] [ 53%] PASSED pamica/tests/test_component_rows.py::test_load_results_refuses_a_truncated_A[one-component-short] +pamica/tests/test_component_rows.py::test_updates_from_a_merged_state_match_the_seeded_reference[scale] +[gw3] [ 53%] SKIPPED pamica/tests/test_component_rows.py::test_updates_from_a_merged_state_match_the_seeded_reference[scale] +pamica/tests/test_component_rows.py::test_updates_from_a_merged_state_match_the_seeded_reference[noscale] +[gw3] [ 53%] SKIPPED pamica/tests/test_component_rows.py::test_updates_from_a_merged_state_match_the_seeded_reference[noscale] +pamica/tests/test_component_rows.py::test_the_reference_scan_never_merges +[gw3] [ 53%] SKIPPED pamica/tests/test_component_rows.py::test_the_reference_scan_never_merges +pamica/tests/test_component_rows.py::test_an_early_mass_merge_collapses_the_reference_too +[gw3] [ 53%] SKIPPED pamica/tests/test_component_rows.py::test_an_early_mass_merge_collapses_the_reference_too +pamica/tests/test_component_rows_persistence.py::test_torch_state_dict_round_trip[single] +[gw1] [ 54%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs0-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs1-torch] +[gw1] [ 54%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs1-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs1-mlx] +[gw1] [ 54%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs1-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs2-torch] +[gw1] [ 54%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs2-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs2-mlx] +[gw5] [ 54%] PASSED pamica/tests/test_iteration_order.py::test_the_rho_rate_ceiling_survives_a_save[torch] +pamica/tests/test_iteration_order.py::test_the_rho_rate_ceiling_survives_a_save[mlx] +[gw1] [ 54%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs2-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs3-torch] +[gw3] [ 54%] PASSED pamica/tests/test_component_rows_persistence.py::test_torch_state_dict_round_trip[single] +pamica/tests/test_component_rows_persistence.py::test_torch_state_dict_round_trip[unmerged] +[gw5] [ 54%] PASSED pamica/tests/test_iteration_order.py::test_the_rho_rate_ceiling_survives_a_save[mlx] +pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[torch-lrate] +[gw1] [ 54%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs3-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs3-mlx] +[gw6] [ 54%] PASSED pamica/tests/test_numpy_share_comps.py::test_numpy_merge_decision_matches_torch_backend +pamica/tests/test_numpy_share_comps.py::test_rank_reduced_numpy_sharing_completes +[gw5] [ 54%] PASSED pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[torch-lrate] +pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[torch-lrate_cap] +[gw1] [ 54%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs3-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs4-torch] +[gw5] [ 54%] PASSED pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[torch-lrate_cap] +pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[torch-newtrate] +[gw1] [ 54%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs4-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs4-mlx] +[gw5] [ 54%] PASSED pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[torch-newtrate] +pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[torch-rholrate] +[gw1] [ 54%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_accepts_a_request_that_reduces_nothing[kwargs4-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_torch_and_mlx_reduced_fits_find_the_same_components +[gw5] [ 54%] PASSED pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[torch-rholrate] +pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[torch-rholrate_cap] +[gw5] [ 54%] PASSED pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[torch-rholrate_cap] +pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[mlx-lrate] +[gw5] [ 54%] PASSED pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[mlx-lrate] +pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[mlx-lrate_cap] +[gw5] [ 54%] PASSED pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[mlx-lrate_cap] +pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[mlx-newtrate] +[gw5] [ 55%] PASSED pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[mlx-newtrate] +pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[mlx-rholrate] +[gw5] [ 55%] PASSED pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[mlx-rholrate] +pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[mlx-rholrate_cap] +[gw5] [ 55%] PASSED pamica/tests/test_iteration_order.py::test_a_save_with_a_nonfinite_rate_is_refused_by_name[mlx-rholrate_cap] +pamica/tests/test_iteration_order.py::test_a_save_without_its_rho_rate_is_refused_by_name[torch] +[gw3] [ 55%] PASSED pamica/tests/test_component_rows_persistence.py::test_torch_state_dict_round_trip[unmerged] +pamica/tests/test_component_rows_persistence.py::test_torch_state_dict_round_trip[merged] +[gw5] [ 55%] PASSED pamica/tests/test_iteration_order.py::test_a_save_without_its_rho_rate_is_refused_by_name[torch] +pamica/tests/test_iteration_order.py::test_a_save_without_its_rho_rate_is_refused_by_name[mlx] +[gw5] [ 55%] PASSED pamica/tests/test_iteration_order.py::test_a_save_without_its_rho_rate_is_refused_by_name[mlx] +pamica/tests/test_iteration_order_native_oracle.py::test_decrease_response_matches_the_reference[decreases] +[gw5] [ 55%] SKIPPED pamica/tests/test_iteration_order_native_oracle.py::test_decrease_response_matches_the_reference[decreases] +pamica/tests/test_iteration_order_native_oracle.py::test_decrease_response_matches_the_reference[ratchet] +[gw5] [ 55%] SKIPPED pamica/tests/test_iteration_order_native_oracle.py::test_decrease_response_matches_the_reference[ratchet] +pamica/tests/test_iteration_order_native_oracle.py::test_a_freeze_without_sharing_matches_the_reference[freeze] +[gw5] [ 55%] SKIPPED pamica/tests/test_iteration_order_native_oracle.py::test_a_freeze_without_sharing_matches_the_reference[freeze] +pamica/tests/test_iteration_order_native_oracle.py::test_a_freeze_without_sharing_matches_the_reference[freeze-noscaling] +[gw5] [ 55%] SKIPPED pamica/tests/test_iteration_order_native_oracle.py::test_a_freeze_without_sharing_matches_the_reference[freeze-noscaling] +pamica/tests/test_llt_stash.py::test_fortran_reference_llt_is_the_last_estep +[gw5] [ 55%] PASSED pamica/tests/test_llt_stash.py::test_fortran_reference_llt_is_the_last_estep +pamica/tests/test_llt_stash.py::test_written_llt_matches_the_fortran_write_convention[1] +[gw3] [ 55%] PASSED pamica/tests/test_component_rows_persistence.py::test_torch_state_dict_round_trip[merged] +pamica/tests/test_component_rows_persistence.py::test_mlx_save_round_trip[single] +[gw3] [ 55%] PASSED pamica/tests/test_component_rows_persistence.py::test_mlx_save_round_trip[single] +pamica/tests/test_component_rows_persistence.py::test_mlx_save_round_trip[unmerged] +[gw3] [ 55%] PASSED pamica/tests/test_component_rows_persistence.py::test_mlx_save_round_trip[unmerged] +pamica/tests/test_component_rows_persistence.py::test_mlx_save_round_trip[merged] +[gw1] [ 55%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_torch_and_mlx_reduced_fits_find_the_same_components +pamica/tests/test_pca_reduction_cross_backend.py::test_mlx_save_load_round_trips_the_reduction +[gw1] [ 55%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mlx_save_load_round_trips_the_reduction +pamica/tests/test_pca_reduction_cross_backend.py::test_mlx_payload_without_the_keys_loads_with_defaults +[gw3] [ 55%] PASSED pamica/tests/test_component_rows_persistence.py::test_mlx_save_round_trip[merged] +pamica/tests/test_component_rows_persistence.py::test_wrapper_save_round_trip[torch-single] +[gw1] [ 55%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mlx_payload_without_the_keys_loads_with_defaults +pamica/tests/test_pca_reduction_cross_backend.py::test_torch_payload_without_the_keys_loads_with_defaults +[gw5] [ 55%] PASSED pamica/tests/test_llt_stash.py::test_written_llt_matches_the_fortran_write_convention[1] +pamica/tests/test_llt_stash.py::test_written_llt_matches_the_fortran_write_convention[2] +[gw3] [ 55%] PASSED pamica/tests/test_component_rows_persistence.py::test_wrapper_save_round_trip[torch-single] +pamica/tests/test_component_rows_persistence.py::test_wrapper_save_round_trip[torch-unmerged] +[gw6] [ 56%] PASSED pamica/tests/test_numpy_share_comps.py::test_rank_reduced_numpy_sharing_completes +pamica/tests/test_numpy_share_comps.py::test_zero_norm_column_is_not_merged_and_raises_no_warning +[gw1] [ 56%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_torch_payload_without_the_keys_loads_with_defaults +pamica/tests/test_pca_reduction_cross_backend.py::test_torch_save_load_round_trips_numpy_scalar_request +[gw6] [ 56%] PASSED pamica/tests/test_numpy_share_comps.py::test_zero_norm_column_is_not_merged_and_raises_no_warning +pamica/tests/test_numpy_share_comps.py::test_non_finite_sphere_fails_loudly +[gw3] [ 56%] PASSED pamica/tests/test_component_rows_persistence.py::test_wrapper_save_round_trip[torch-unmerged] +pamica/tests/test_component_rows_persistence.py::test_wrapper_save_round_trip[torch-merged] +[gw6] [ 56%] PASSED pamica/tests/test_numpy_share_comps.py::test_non_finite_sphere_fails_loudly +pamica/tests/test_numpy_viz.py::test_maps_and_sources_match_the_accessors[full] +[gw1] [ 56%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_torch_save_load_round_trips_numpy_scalar_request +pamica/tests/test_pca_reduction_cross_backend.py::test_refit_after_a_rank_reduction[pcakeep-torch] +[gw6] [ 56%] PASSED pamica/tests/test_numpy_viz.py::test_maps_and_sources_match_the_accessors[full] +pamica/tests/test_numpy_viz.py::test_load_results_reads_a_reduced_sphere[full] +[gw6] [ 56%] PASSED pamica/tests/test_numpy_viz.py::test_load_results_reads_a_reduced_sphere[full] +pamica/tests/test_numpy_viz.py::test_plot_components_draws_the_maps_and_the_sources[full] +[gw3] [ 56%] PASSED pamica/tests/test_component_rows_persistence.py::test_wrapper_save_round_trip[torch-merged] +pamica/tests/test_component_rows_persistence.py::test_wrapper_save_round_trip[mlx-single] +[gw6] [ 56%] PASSED pamica/tests/test_numpy_viz.py::test_plot_components_draws_the_maps_and_the_sources[full] +pamica/tests/test_numpy_viz.py::test_plot_pdf_fits_histograms_the_sources[full] +[gw3] [ 56%] PASSED pamica/tests/test_component_rows_persistence.py::test_wrapper_save_round_trip[mlx-single] +pamica/tests/test_component_rows_persistence.py::test_wrapper_save_round_trip[mlx-unmerged] +[gw6] [ 56%] PASSED pamica/tests/test_numpy_viz.py::test_plot_pdf_fits_histograms_the_sources[full] +pamica/tests/test_numpy_viz.py::test_plot_model_comparison_draws_the_reconstruction[full] +[gw6] [ 56%] PASSED pamica/tests/test_numpy_viz.py::test_plot_model_comparison_draws_the_reconstruction[full] +pamica/tests/test_numpy_viz.py::test_maps_and_sources_match_the_accessors[reduced] +[gw1] [ 56%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_refit_after_a_rank_reduction[pcakeep-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_refit_after_a_rank_reduction[pcakeep-numpy] +[gw3] [ 56%] PASSED pamica/tests/test_component_rows_persistence.py::test_wrapper_save_round_trip[mlx-unmerged] +pamica/tests/test_component_rows_persistence.py::test_wrapper_save_round_trip[mlx-merged] +[gw6] [ 56%] PASSED pamica/tests/test_numpy_viz.py::test_maps_and_sources_match_the_accessors[reduced] +pamica/tests/test_numpy_viz.py::test_load_results_reads_a_reduced_sphere[reduced] +[gw6] [ 56%] PASSED pamica/tests/test_numpy_viz.py::test_load_results_reads_a_reduced_sphere[reduced] +pamica/tests/test_numpy_viz.py::test_plot_components_draws_the_maps_and_the_sources[reduced] +[gw3] [ 56%] PASSED pamica/tests/test_component_rows_persistence.py::test_wrapper_save_round_trip[mlx-merged] +pamica/tests/test_component_rows_persistence.py::test_old_torch_state_dict_converts_losslessly[single] +[gw6] [ 56%] PASSED pamica/tests/test_numpy_viz.py::test_plot_components_draws_the_maps_and_the_sources[reduced] +pamica/tests/test_numpy_viz.py::test_plot_pdf_fits_histograms_the_sources[reduced] +[gw3] [ 56%] PASSED pamica/tests/test_component_rows_persistence.py::test_old_torch_state_dict_converts_losslessly[single] +pamica/tests/test_component_rows_persistence.py::test_old_torch_state_dict_converts_losslessly[unmerged] +[gw6] [ 57%] PASSED pamica/tests/test_numpy_viz.py::test_plot_pdf_fits_histograms_the_sources[reduced] +pamica/tests/test_numpy_viz.py::test_plot_model_comparison_draws_the_reconstruction[reduced] +[gw6] [ 57%] PASSED pamica/tests/test_numpy_viz.py::test_plot_model_comparison_draws_the_reconstruction[reduced] +pamica/tests/test_pamica.py::test_amica_initialization +[gw6] [ 57%] PASSED pamica/tests/test_pamica.py::test_amica_initialization +pamica/tests/test_pamica.py::test_data_preprocessing +[gw6] [ 57%] PASSED pamica/tests/test_pamica.py::test_data_preprocessing +pamica/tests/test_pamica.py::test_pdf_computation +[gw6] [ 57%] PASSED pamica/tests/test_pamica.py::test_pdf_computation +pamica/tests/test_pamica.py::test_pdf_is_a_normalized_density_for_general_rho +[gw6] [ 57%] PASSED pamica/tests/test_pamica.py::test_pdf_is_a_normalized_density_for_general_rho +pamica/tests/test_rank_policy.py::test_validator_accepts[None-pcadb6] +[gw6] [ 57%] PASSED pamica/tests/test_rank_policy.py::test_validator_accepts[None-pcadb6] +pamica/tests/test_rank_policy.py::test_validator_accepts[None-30.0] +[gw6] [ 57%] PASSED pamica/tests/test_rank_policy.py::test_validator_accepts[None-30.0] +pamica/tests/test_rank_policy.py::test_validator_accepts[32-30.0] +[gw6] [ 57%] PASSED pamica/tests/test_rank_policy.py::test_validator_accepts[32-30.0] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[True] +[gw6] [ 57%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[True] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[False] +[gw6] [ 57%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[False] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[pcakeep2] +[gw6] [ 57%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[pcakeep2] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[pcakeep3] +[gw6] [ 57%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[pcakeep3] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[0] +[gw6] [ 57%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[0] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[-3] +[gw6] [ 57%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[-3] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[2.7] +[gw6] [ 57%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[2.7] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[20] +[gw6] [ 57%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcakeep[20] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[0] +[gw6] [ 57%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[0] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[-5] +[gw6] [ 57%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[-5] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[nan] +[gw6] [ 57%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[nan] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[inf] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[inf] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[True] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[True] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[False] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[False] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[pcadb6] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[pcadb6] +pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[pcadb7] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_pcadb[pcadb7] +pamica/tests/test_rank_policy.py::test_validator_rejects_either_parameter_when_both_are_set +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_validator_rejects_either_parameter_when_both_are_set +pamica/tests/test_rank_policy.py::test_validator_never_logs +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_validator_never_logs +pamica/tests/test_rank_policy.py::test_ignored_request_logs_the_precedence_once +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_ignored_request_logs_the_precedence_once +pamica/tests/test_rank_policy.py::test_ignored_request_warns_once_without_sphering[20-None] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_ignored_request_warns_once_without_sphering[20-None] +pamica/tests/test_rank_policy.py::test_ignored_request_warns_once_without_sphering[None-30.0] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_ignored_request_warns_once_without_sphering[None-30.0] +pamica/tests/test_rank_policy.py::test_ignored_request_warns_once_without_sphering[20-30.0] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_ignored_request_warns_once_without_sphering[20-30.0] +pamica/tests/test_rank_policy.py::test_ignored_request_warns_once_without_sphering[pcakeep3-None] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_ignored_request_warns_once_without_sphering[pcakeep3-None] +pamica/tests/test_rank_policy.py::test_nothing_to_ignore_without_a_request +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_nothing_to_ignore_without_a_request +pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[None-None-32-True-False] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[None-None-32-True-False] +pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[20-None-32-True-True] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[20-None-32-True-True] +pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[31-None-32-True-True] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[31-None-32-True-True] +pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[32-None-32-True-False] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[32-None-32-True-False] +pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[64-None-32-True-False] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[64-None-32-True-False] +pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[None-30.0-32-True-True] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[None-30.0-32-True-True] +pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[32-30.0-32-True-False] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[32-30.0-32-True-False] +pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[20-30.0-32-True-True] +[gw6] [ 58%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[20-30.0-32-True-True] +pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[20-None-32-False-False] +[gw6] [ 59%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[20-None-32-False-False] +pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[None-30.0-32-False-False] +[gw6] [ 59%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[None-30.0-32-False-False] +pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[20-30.0-32-False-False] +[gw6] [ 59%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[20-30.0-32-False-False] +pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[None-None-32-False-False] +[gw6] [ 59%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_truth_table[None-None-32-False-False] +pamica/tests/test_rank_policy.py::test_reduction_predicate_validates_first[-3-None-pcakeep] +[gw6] [ 59%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_validates_first[-3-None-pcakeep] +pamica/tests/test_rank_policy.py::test_reduction_predicate_validates_first[20-None-pcakeep] +[gw6] [ 59%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_validates_first[20-None-pcakeep] +pamica/tests/test_rank_policy.py::test_reduction_predicate_validates_first[None--5.0-pcadb] +[gw6] [ 59%] PASSED pamica/tests/test_rank_policy.py::test_reduction_predicate_validates_first[None--5.0-pcadb] +pamica/tests/test_rank_policy.py::test_numerical_rank_rejects_invalid_reduction[-3-None] +[gw6] [ 59%] PASSED pamica/tests/test_rank_policy.py::test_numerical_rank_rejects_invalid_reduction[-3-None] +pamica/tests/test_rank_policy.py::test_numerical_rank_rejects_invalid_reduction[2.7-None] +[gw6] [ 59%] PASSED pamica/tests/test_rank_policy.py::test_numerical_rank_rejects_invalid_reduction[2.7-None] +pamica/tests/test_rank_policy.py::test_numerical_rank_rejects_invalid_reduction[0-None] +[gw6] [ 59%] PASSED pamica/tests/test_rank_policy.py::test_numerical_rank_rejects_invalid_reduction[0-None] +pamica/tests/test_rank_policy.py::test_numerical_rank_rejects_invalid_reduction[True-None] +[gw6] [ 59%] PASSED pamica/tests/test_rank_policy.py::test_numerical_rank_rejects_invalid_reduction[True-None] +pamica/tests/test_rank_policy.py::test_numerical_rank_rejects_invalid_reduction[None-0.0] +[gw6] [ 59%] PASSED pamica/tests/test_rank_policy.py::test_numerical_rank_rejects_invalid_reduction[None-0.0] +pamica/tests/test_rank_policy.py::test_numerical_rank_rejects_invalid_reduction[None--5.0] +[gw6] [ 59%] PASSED pamica/tests/test_rank_policy.py::test_numerical_rank_rejects_invalid_reduction[None--5.0] +pamica/tests/test_rank_policy.py::test_numerical_rank_pcakeep_wins_over_pcadb +[gw6] [ 59%] PASSED pamica/tests/test_rank_policy.py::test_numerical_rank_pcakeep_wins_over_pcadb +pamica/tests/test_rank_policy.py::test_all_backends_agree_on_the_rank +[gw1] [ 59%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_refit_after_a_rank_reduction[pcakeep-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_refit_after_a_rank_reduction[pcakeep-mlx] +[gw5] [ 59%] PASSED pamica/tests/test_llt_stash.py::test_written_llt_matches_the_fortran_write_convention[2] +pamica/tests/test_llt_stash.py::test_native_binary_llt_is_the_last_estep +[gw5] [ 59%] SKIPPED pamica/tests/test_llt_stash.py::test_native_binary_llt_is_the_last_estep +pamica/tests/test_llt_stash.py::test_torch_stashed_llt_is_the_estep_behind_final_ll +[gw1] [ 59%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_refit_after_a_rank_reduction[pcakeep-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_refit_after_a_rank_reduction[automatic-torch] +[gw3] [ 59%] PASSED pamica/tests/test_component_rows_persistence.py::test_old_torch_state_dict_converts_losslessly[unmerged] +pamica/tests/test_component_rows_persistence.py::test_old_mlx_save_converts_losslessly[single] +[gw3] [ 59%] PASSED pamica/tests/test_component_rows_persistence.py::test_old_mlx_save_converts_losslessly[single] +pamica/tests/test_component_rows_persistence.py::test_old_mlx_save_converts_losslessly[unmerged] +[gw3] [ 60%] PASSED pamica/tests/test_component_rows_persistence.py::test_old_mlx_save_converts_losslessly[unmerged] +pamica/tests/test_component_rows_persistence.py::test_old_wrapper_save_converts_losslessly[torch-single] +[gw1] [ 60%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_refit_after_a_rank_reduction[automatic-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_refit_after_a_rank_reduction[automatic-numpy] +[gw3] [ 60%] PASSED pamica/tests/test_component_rows_persistence.py::test_old_wrapper_save_converts_losslessly[torch-single] +pamica/tests/test_component_rows_persistence.py::test_old_wrapper_save_converts_losslessly[torch-unmerged] +[gw6] [ 60%] PASSED pamica/tests/test_rank_policy.py::test_all_backends_agree_on_the_rank +pamica/tests/test_reference_constants.py::test_log_normalizer_is_the_reference_literal_in_single_precision[LOG_SQRT_PI] +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_log_normalizer_is_the_reference_literal_in_single_precision[LOG_SQRT_PI] +pamica/tests/test_reference_constants.py::test_log_normalizer_is_the_reference_literal_in_single_precision[LOG_SQRT_2PI] +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_log_normalizer_is_the_reference_literal_in_single_precision[LOG_SQRT_2PI] +pamica/tests/test_reference_constants.py::test_log_normalizer_is_the_reference_literal_in_single_precision[LOG_NORM_COSH_SUB] +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_log_normalizer_is_the_reference_literal_in_single_precision[LOG_NORM_COSH_SUB] +pamica/tests/test_reference_constants.py::test_log_normalizer_is_the_reference_literal_in_single_precision[LOG_NORM_COSH_SUP] +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_log_normalizer_is_the_reference_literal_in_single_precision[LOG_NORM_COSH_SUP] +pamica/tests/test_reference_constants.py::test_epsdble_is_the_reference_literal_in_single_precision +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_epsdble_is_the_reference_literal_in_single_precision +pamica/tests/test_reference_constants.py::test_dyadic_normalizers_are_exact[LOG2-1308-log(dble(2.0))-2.0] +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_dyadic_normalizers_are_exact[LOG2-1308-log(dble(2.0))-2.0] +pamica/tests/test_reference_constants.py::test_dyadic_normalizers_are_exact[LOG2-1324-log(dble(2.0))-2.0] +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_dyadic_normalizers_are_exact[LOG2-1324-log(dble(2.0))-2.0] +pamica/tests/test_reference_constants.py::test_dyadic_normalizers_are_exact[LOG4-1346-log(dble(4.0))-4.0] +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_dyadic_normalizers_are_exact[LOG4-1346-log(dble(4.0))-4.0] +pamica/tests/test_reference_constants.py::test_the_helper_does_not_round_twice[1.772453851] +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_the_helper_does_not_round_twice[1.772453851] +pamica/tests/test_reference_constants.py::test_the_helper_does_not_round_twice[2.506628274] +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_the_helper_does_not_round_twice[2.506628274] +pamica/tests/test_reference_constants.py::test_the_helper_does_not_round_twice[4.132731354] +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_the_helper_does_not_round_twice[4.132731354] +pamica/tests/test_reference_constants.py::test_the_helper_does_not_round_twice[1.858073988] +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_the_helper_does_not_round_twice[1.858073988] +pamica/tests/test_reference_constants.py::test_the_helper_does_not_round_twice[1.0e-16] +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_the_helper_does_not_round_twice[1.0e-16] +pamica/tests/test_reference_constants.py::test_every_inexact_single_precision_literal_in_the_reference_is_accounted_for +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_every_inexact_single_precision_literal_in_the_reference_is_accounted_for +pamica/tests/test_reference_constants.py::test_every_inexact_default_in_the_reference_header_is_accounted_for +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_every_inexact_default_in_the_reference_header_is_accounted_for +pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[_HALF_LOG_PI = 0.5 * math.log(math.pi)] +[gw6] [ 60%] PASSED pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[_HALF_LOG_PI = 0.5 * math.log(math.pi)] +pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[_LOG_SQRT_2PI = math.log(2.506628274)] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[_LOG_SQRT_2PI = math.log(2.506628274)] +pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[_LOG_NORM_COSH_SUB = math.log(4.132731354)] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[_LOG_NORM_COSH_SUB = math.log(4.132731354)] +pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[_LOG_NORM_COSH_SUP = math.log(1.858073988)] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[_LOG_NORM_COSH_SUP = math.log(1.858073988)] +pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[_EPSDBLE = 1e-16] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[_EPSDBLE = 1e-16] +pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[_LOG2 = math.log(2.0)] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[_LOG2 = math.log(2.0)] +pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[_LOG4 = math.log(4.0)] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[_LOG4 = math.log(4.0)] +pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[log_pdf = -np.abs(y) - np.log(2.0)] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[log_pdf = -np.abs(y) - np.log(2.0)] +pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[log_pdf = -y * y - 0.5 * np.log(np.pi)] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[log_pdf = -y * y - 0.5 * np.log(np.pi)] +pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[logab = np.where(ayrho < 1e-16, 0.0, logab)] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[logab = np.where(ayrho < 1e-16, 0.0, logab)] +pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[pdf = np.exp(-y * y) / np.sqrt(np.pi)] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[pdf = np.exp(-y * y) / np.sqrt(np.pi)] +pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[pdf1 = np.exp(-0.5 * y * y) / np.sqrt(2 * np.pi)] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[pdf1 = np.exp(-0.5 * y * y) / np.sqrt(2 * np.pi)] +pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[pdf2 = np.exp(-0.5 * y * y / rho) / np.sqrt(2 * np.pi * rho)] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_the_copy_scanner_flags_the_removed_definitions[pdf2 = np.exp(-0.5 * y * y / rho) / np.sqrt(2 * np.pi * rho)] +pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[mlx_impl/__init__.py] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[mlx_impl/__init__.py] +pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[mlx_impl/core.py] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[mlx_impl/core.py] +pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/__init__.py] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/__init__.py] +pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/cli.py] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/cli.py] +pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/core.py] +[gw3] [ 61%] PASSED pamica/tests/test_component_rows_persistence.py::test_old_wrapper_save_converts_losslessly[torch-unmerged] +pamica/tests/test_component_rows_persistence.py::test_old_wrapper_save_converts_losslessly[mlx-single] +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/core.py] +[gw5] [ 61%] PASSED pamica/tests/test_llt_stash.py::test_torch_stashed_llt_is_the_estep_behind_final_ll +pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/data.py] +pamica/tests/test_pre_change_loader.py::test_the_guard_catches_a_fetch_without_running_it +[gw6] [ 61%] PASSED pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/data.py] +pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/load.py] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/load.py] +pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/pdf.py] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/pdf.py] +pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/utils.py] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/utils.py] +pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/viz.py] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[numpy_impl/viz.py] +pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[torch_impl/__init__.py] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[torch_impl/__init__.py] +pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[torch_impl/core.py] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[torch_impl/core.py] +pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[torch_impl/utils.py] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_no_backend_keeps_a_copy_of_the_constants[torch_impl/utils.py] +pamica/tests/test_reference_constants.py::test_every_backend_imports_the_shared_constants[torch_impl] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_every_backend_imports_the_shared_constants[torch_impl] +pamica/tests/test_reference_constants.py::test_every_backend_imports_the_shared_constants[numpy_impl] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_every_backend_imports_the_shared_constants[numpy_impl] +pamica/tests/test_reference_constants.py::test_every_backend_imports_the_shared_constants[mlx_impl] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_every_backend_imports_the_shared_constants[mlx_impl] +pamica/tests/test_reference_constants.py::test_torch_and_numpy_rho2_log_density_is_the_references +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_torch_and_numpy_rho2_log_density_is_the_references +pamica/tests/test_reference_constants.py::test_the_plotted_density_is_the_fits[1.0] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_the_plotted_density_is_the_fits[1.0] +pamica/tests/test_reference_constants.py::test_the_plotted_density_is_the_fits[1.5] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_the_plotted_density_is_the_fits[1.5] +pamica/tests/test_reference_constants.py::test_the_plotted_density_is_the_fits[2.0] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_the_plotted_density_is_the_fits[2.0] +pamica/tests/test_reference_constants.py::test_mlx_rho2_normalizer_is_the_references +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_mlx_rho2_normalizer_is_the_references +pamica/tests/test_reference_constants.py::test_family_matches_the_seeded_reference[pdftype2] +[gw6] [ 62%] SKIPPED pamica/tests/test_reference_constants.py::test_family_matches_the_seeded_reference[pdftype2] +pamica/tests/test_reference_constants.py::test_family_matches_the_seeded_reference[pdftype4] +[gw6] [ 62%] SKIPPED pamica/tests/test_reference_constants.py::test_family_matches_the_seeded_reference[pdftype4] +pamica/tests/test_reference_constants.py::test_family_matches_the_seeded_reference[pdftype1] +[gw6] [ 62%] SKIPPED pamica/tests/test_reference_constants.py::test_family_matches_the_seeded_reference[pdftype1] +pamica/tests/test_reference_constants.py::test_the_pre_344_substitution_changes_every_constant_it_patches[torch] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_the_pre_344_substitution_changes_every_constant_it_patches[torch] +pamica/tests/test_reference_constants.py::test_the_pre_344_substitution_changes_every_constant_it_patches[numpy] +[gw6] [ 62%] PASSED pamica/tests/test_reference_constants.py::test_the_pre_344_substitution_changes_every_constant_it_patches[numpy] +pamica/tests/test_reference_constants.py::test_the_pre_344_substitution_changes_every_constant_it_patches[mlx] +[gw6] [ 63%] PASSED pamica/tests/test_reference_constants.py::test_the_pre_344_substitution_changes_every_constant_it_patches[mlx] +pamica/tests/test_restart_policy.py::test_single_restart_passes_the_base_seed_through_untouched +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_single_restart_passes_the_base_seed_through_untouched +pamica/tests/test_restart_policy.py::test_seeds_derive_consecutively_from_the_base_seed +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_seeds_derive_consecutively_from_the_base_seed +pamica/tests/test_restart_policy.py::test_explicit_seeds_win_over_derivation +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_explicit_seeds_win_over_derivation +pamica/tests/test_restart_policy.py::test_explicit_seeds_must_match_n_restarts +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_explicit_seeds_must_match_n_restarts +pamica/tests/test_restart_policy.py::test_n_restarts_must_be_at_least_one +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_n_restarts_must_be_at_least_one +pamica/tests/test_restart_policy.py::test_n_restarts_must_be_an_int +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_n_restarts_must_be_an_int +pamica/tests/test_restart_policy.py::test_seeds_must_be_ints +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_seeds_must_be_ints +pamica/tests/test_restart_policy.py::test_multi_restart_without_a_seed_is_refused +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_multi_restart_without_a_seed_is_refused +pamica/tests/test_restart_policy.py::test_highest_likelihood_wins +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_highest_likelihood_wins +pamica/tests/test_restart_policy.py::test_ties_keep_the_earlier_restart +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_ties_keep_the_earlier_restart +pamica/tests/test_restart_policy.py::test_degenerate_restarts_are_excluded_even_when_they_score_higher +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_degenerate_restarts_are_excluded_even_when_they_score_higher +pamica/tests/test_restart_policy.py::test_non_finite_likelihood_is_excluded_even_if_not_flagged +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_non_finite_likelihood_is_excluded_even_if_not_flagged +pamica/tests/test_restart_policy.py::test_all_degenerate_selects_nothing +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_all_degenerate_selects_nothing +pamica/tests/test_restart_policy.py::test_records_must_stay_aligned +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_records_must_stay_aligned +pamica/tests/test_restart_policy.py::test_copies_are_independent_of_the_live_value +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_copies_are_independent_of_the_live_value +pamica/tests/test_restart_policy.py::test_unknown_types_raise_rather_than_alias +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_unknown_types_raise_rather_than_alias +pamica/tests/test_restart_policy.py::test_every_backend_exposes_the_same_restart_surface[torch] +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_every_backend_exposes_the_same_restart_surface[torch] +pamica/tests/test_restart_policy.py::test_every_backend_exposes_the_same_restart_surface[numpy] +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_every_backend_exposes_the_same_restart_surface[numpy] +pamica/tests/test_restart_policy.py::test_every_backend_exposes_the_same_restart_surface[mlx] +[gw6] [ 63%] PASSED pamica/tests/test_restart_policy.py::test_every_backend_exposes_the_same_restart_surface[mlx] +pamica/tests/test_restart_policy.py::test_every_backend_validates_the_configuration_at_construction[torch] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_every_backend_validates_the_configuration_at_construction[torch] +pamica/tests/test_restart_policy.py::test_every_backend_validates_the_configuration_at_construction[numpy] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_every_backend_validates_the_configuration_at_construction[numpy] +pamica/tests/test_restart_policy.py::test_every_backend_validates_the_configuration_at_construction[mlx] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_every_backend_validates_the_configuration_at_construction[mlx] +pamica/tests/test_restart_policy.py::test_a_crashed_restart_is_degenerate_on_every_backend[torch] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_a_crashed_restart_is_degenerate_on_every_backend[torch] +pamica/tests/test_restart_policy.py::test_a_crashed_restart_is_degenerate_on_every_backend[numpy] +[gw6] [ 64%] SKIPPED pamica/tests/test_restart_policy.py::test_a_crashed_restart_is_degenerate_on_every_backend[numpy] +pamica/tests/test_restart_policy.py::test_a_crashed_restart_is_degenerate_on_every_backend[mlx] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_a_crashed_restart_is_degenerate_on_every_backend[mlx] +pamica/tests/test_restart_policy.py::test_the_error_stop_reason_is_distinct_from_the_likelihood_ones +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_the_error_stop_reason_is_distinct_from_the_likelihood_ones +pamica/tests/test_restart_policy.py::test_restart_state_lists_account_for_everything_a_fit_writes[torch] +[gw1] [ 64%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_refit_after_a_rank_reduction[automatic-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_refit_after_a_rank_reduction[automatic-mlx] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_restart_state_lists_account_for_everything_a_fit_writes[torch] +pamica/tests/test_restart_policy.py::test_restart_state_lists_account_for_everything_a_fit_writes[numpy] +[gw3] [ 64%] PASSED pamica/tests/test_component_rows_persistence.py::test_old_wrapper_save_converts_losslessly[mlx-single] +pamica/tests/test_component_rows_persistence.py::test_old_wrapper_save_converts_losslessly[mlx-unmerged] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_restart_state_lists_account_for_everything_a_fit_writes[numpy] +pamica/tests/test_restart_policy.py::test_restart_state_lists_account_for_everything_a_fit_writes[mlx] +[gw1] [ 64%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_refit_after_a_rank_reduction[automatic-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_restarts_survive_a_rank_reduction[pcakeep-torch] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_restart_state_lists_account_for_everything_a_fit_writes[mlx] +pamica/tests/test_restart_policy.py::test_state_and_invariant_lists_do_not_overlap[torch] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_state_and_invariant_lists_do_not_overlap[torch] +pamica/tests/test_restart_policy.py::test_state_and_invariant_lists_do_not_overlap[numpy] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_state_and_invariant_lists_do_not_overlap[numpy] +pamica/tests/test_restart_policy.py::test_state_and_invariant_lists_do_not_overlap[mlx] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_state_and_invariant_lists_do_not_overlap[mlx] +pamica/tests/test_restart_policy.py::test_the_fitted_parameters_are_all_restart_state[torch] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_the_fitted_parameters_are_all_restart_state[torch] +pamica/tests/test_restart_policy.py::test_the_fitted_parameters_are_all_restart_state[numpy] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_the_fitted_parameters_are_all_restart_state[numpy] +pamica/tests/test_restart_policy.py::test_the_fitted_parameters_are_all_restart_state[mlx] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_the_fitted_parameters_are_all_restart_state[mlx] +pamica/tests/test_restart_policy.py::test_derived_seeds_are_identical_across_backends[torch] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_derived_seeds_are_identical_across_backends[torch] +pamica/tests/test_restart_policy.py::test_derived_seeds_are_identical_across_backends[numpy] +[gw6] [ 64%] PASSED pamica/tests/test_restart_policy.py::test_derived_seeds_are_identical_across_backends[numpy] +pamica/tests/test_restart_policy.py::test_derived_seeds_are_identical_across_backends[mlx] +[gw6] [ 65%] PASSED pamica/tests/test_restart_policy.py::test_derived_seeds_are_identical_across_backends[mlx] +pamica/tests/test_sample_data.py::test_loadmodout_llt_reshape_order +[gw6] [ 65%] PASSED pamica/tests/test_sample_data.py::test_loadmodout_llt_reshape_order +pamica/tests/test_sample_data.py::test_loadmodout_reproduces_fortran_reported_ll +[gw6] [ 65%] PASSED pamica/tests/test_sample_data.py::test_loadmodout_reproduces_fortran_reported_ll +pamica/tests/test_sample_data.py::test_amicaoutput_sources_contract_and_c_subtraction +[gw5] [ 65%] PASSED pamica/tests/test_pre_change_loader.py::test_the_guard_catches_a_fetch_without_running_it +[gw6] [ 65%] PASSED pamica/tests/test_sample_data.py::test_amicaoutput_sources_contract_and_c_subtraction +pamica/tests/test_pre_change_loader.py::test_missing_git_skips_locally +pamica/tests/test_sample_data.py::test_sample_data_scikit +[gw5] [ 65%] PASSED pamica/tests/test_pre_change_loader.py::test_missing_git_skips_locally +pamica/tests/test_pre_change_loader.py::test_missing_git_fails_under_ci +[gw5] [ 65%] PASSED pamica/tests/test_pre_change_loader.py::test_missing_git_fails_under_ci +pamica/tests/test_pre_change_loader.py::test_outside_a_checkout_skips_without_fetch_advice +[gw3] [ 65%] PASSED pamica/tests/test_component_rows_persistence.py::test_old_wrapper_save_converts_losslessly[mlx-unmerged] +pamica/tests/test_component_rows_persistence.py::test_old_merged_save_is_refused[torch-state_dict] +[gw5] [ 65%] PASSED pamica/tests/test_pre_change_loader.py::test_outside_a_checkout_skips_without_fetch_advice +pamica/tests/test_pre_change_loader.py::test_outside_a_checkout_fails_under_ci +[gw5] [ 65%] PASSED pamica/tests/test_pre_change_loader.py::test_outside_a_checkout_fails_under_ci +pamica/tests/test_pre_change_loader.py::test_an_alias_is_never_reused_for_another_commit +[gw5] [ 65%] PASSED pamica/tests/test_pre_change_loader.py::test_an_alias_is_never_reused_for_another_commit +pamica/tests/test_rank_policy.py::test_full_rank_data_keeps_every_dimension +[gw5] [ 65%] PASSED pamica/tests/test_rank_policy.py::test_full_rank_data_keeps_every_dimension +pamica/tests/test_rank_policy.py::test_relative_floor_finds_the_true_rank +[gw1] [ 65%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_restarts_survive_a_rank_reduction[pcakeep-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_restarts_survive_a_rank_reduction[pcakeep-numpy] +[gw5] [ 65%] PASSED pamica/tests/test_rank_policy.py::test_relative_floor_finds_the_true_rank +pamica/tests/test_rank_policy.py::test_absolute_floor_over_retains +[gw5] [ 65%] PASSED pamica/tests/test_rank_policy.py::test_absolute_floor_over_retains +pamica/tests/test_rank_policy.py::test_average_reference_costs_exactly_one_dimension +[gw5] [ 65%] PASSED pamica/tests/test_rank_policy.py::test_average_reference_costs_exactly_one_dimension +pamica/tests/test_rank_policy.py::test_scale_invariance_of_the_relative_floor +[gw5] [ 65%] PASSED pamica/tests/test_rank_policy.py::test_scale_invariance_of_the_relative_floor +pamica/tests/test_rank_policy.py::test_absolute_floor_is_not_scale_invariant +[gw5] [ 65%] PASSED pamica/tests/test_rank_policy.py::test_absolute_floor_is_not_scale_invariant +pamica/tests/test_rank_policy.py::test_pcakeep_is_capped_by_the_detected_rank +[gw5] [ 65%] PASSED pamica/tests/test_rank_policy.py::test_pcakeep_is_capped_by_the_detected_rank +pamica/tests/test_rank_policy.py::test_non_finite_eigenvalues_keep_full_dimension +[gw5] [ 65%] PASSED pamica/tests/test_rank_policy.py::test_non_finite_eigenvalues_keep_full_dimension +pamica/tests/test_rank_policy.py::test_defaults_match_the_documented_constants +[gw5] [ 66%] PASSED pamica/tests/test_rank_policy.py::test_defaults_match_the_documented_constants +pamica/tests/test_rank_policy.py::test_validator_accepts[None-None] +[gw5] [ 66%] PASSED pamica/tests/test_rank_policy.py::test_validator_accepts[None-None] +pamica/tests/test_rank_policy.py::test_validator_accepts[1-None] +[gw5] [ 66%] PASSED pamica/tests/test_rank_policy.py::test_validator_accepts[1-None] +pamica/tests/test_rank_policy.py::test_validator_accepts[20-None] +[gw5] [ 66%] PASSED pamica/tests/test_rank_policy.py::test_validator_accepts[20-None] +pamica/tests/test_rank_policy.py::test_validator_accepts[pcakeep3-None] +[gw5] [ 66%] PASSED pamica/tests/test_rank_policy.py::test_validator_accepts[pcakeep3-None] +pamica/tests/test_rank_policy.py::test_validator_accepts[None-3.0] +[gw5] [ 66%] PASSED pamica/tests/test_rank_policy.py::test_validator_accepts[None-3.0] +pamica/tests/test_rank_policy.py::test_validator_accepts[None-30] +[gw5] [ 66%] PASSED pamica/tests/test_rank_policy.py::test_validator_accepts[None-30] +pamica/tests/test_schedule_gates.py::test_newton_takes_its_first_step_on_iteration_newt_start[mlx-1] +[gw3] [ 66%] PASSED pamica/tests/test_component_rows_persistence.py::test_old_merged_save_is_refused[torch-state_dict] +pamica/tests/test_component_rows_persistence.py::test_old_merged_save_is_refused[torch-wrapper] +[gw5] [ 66%] PASSED pamica/tests/test_schedule_gates.py::test_newton_takes_its_first_step_on_iteration_newt_start[mlx-1] +pamica/tests/test_schedule_gates.py::test_newton_takes_its_first_step_on_iteration_newt_start[mlx-5] +[gw5] [ 66%] PASSED pamica/tests/test_schedule_gates.py::test_newton_takes_its_first_step_on_iteration_newt_start[mlx-5] +pamica/tests/test_schedule_gates.py::test_newt_start_zero_fits_exactly_as_one[torch] +[gw3] [ 66%] PASSED pamica/tests/test_component_rows_persistence.py::test_old_merged_save_is_refused[torch-wrapper] +pamica/tests/test_component_rows_persistence.py::test_old_merged_save_is_refused[mlx-state_dict] +[gw3] [ 66%] PASSED pamica/tests/test_component_rows_persistence.py::test_old_merged_save_is_refused[mlx-state_dict] +pamica/tests/test_component_rows_persistence.py::test_old_merged_save_is_refused[mlx-wrapper] +[gw1] [ 66%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_restarts_survive_a_rank_reduction[pcakeep-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_restarts_survive_a_rank_reduction[pcakeep-mlx] +[gw1] [ 66%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_restarts_survive_a_rank_reduction[pcakeep-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_restarts_survive_a_rank_reduction[automatic-torch] +[gw3] [ 66%] PASSED pamica/tests/test_component_rows_persistence.py::test_old_merged_save_is_refused[mlx-wrapper] +pamica/tests/test_decompose_equiv.py::test_match_identical_is_one +[gw3] [ 66%] PASSED pamica/tests/test_decompose_equiv.py::test_match_identical_is_one +pamica/tests/test_decompose_equiv.py::test_match_invariant_to_permutation_and_sign +[gw3] [ 66%] PASSED pamica/tests/test_decompose_equiv.py::test_match_invariant_to_permutation_and_sign +pamica/tests/test_decompose_equiv.py::test_match_low_for_unrelated +[gw3] [ 66%] PASSED pamica/tests/test_decompose_equiv.py::test_match_low_for_unrelated +pamica/tests/test_default_settings.py::test_numpy_shares_every_torch_default +[gw3] [ 66%] PASSED pamica/tests/test_default_settings.py::test_numpy_shares_every_torch_default +pamica/tests/test_default_settings.py::test_numpy_params_json_carries_the_shared_defaults +[gw3] [ 66%] PASSED pamica/tests/test_default_settings.py::test_numpy_params_json_carries_the_shared_defaults +pamica/tests/test_default_settings.py::test_mlx_shares_every_torch_default +[gw3] [ 67%] PASSED pamica/tests/test_default_settings.py::test_mlx_shares_every_torch_default +pamica/tests/test_default_settings.py::test_native_input_param_shares_every_torch_default +[gw3] [ 67%] PASSED pamica/tests/test_default_settings.py::test_native_input_param_shares_every_torch_default +pamica/tests/test_schedule_gates.py::test_rejection_first_fires_on_iteration_rejstart[mlx] +[gw3] [ 67%] PASSED pamica/tests/test_schedule_gates.py::test_rejection_first_fires_on_iteration_rejstart[mlx] +pamica/tests/test_schedule_gates.py::test_numpy_restart_window_is_the_first_restartiter_iterations[3-2-1] +[gw1] [ 67%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_restarts_survive_a_rank_reduction[automatic-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_restarts_survive_a_rank_reduction[automatic-numpy] +[gw3] [ 67%] PASSED pamica/tests/test_schedule_gates.py::test_numpy_restart_window_is_the_first_restartiter_iterations[3-2-1] +pamica/tests/test_schedule_gates.py::test_numpy_restart_window_is_the_first_restartiter_iterations[3-3-0] +[gw1] [ 67%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_restarts_survive_a_rank_reduction[automatic-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_restarts_survive_a_rank_reduction[automatic-mlx] +[gw1] [ 67%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_restarts_survive_a_rank_reduction[automatic-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_channel_check_names_the_input_count[torch] +[gw1] [ 67%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_channel_check_names_the_input_count[torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_channel_check_names_the_input_count[mlx] +[gw3] [ 67%] PASSED pamica/tests/test_schedule_gates.py::test_numpy_restart_window_is_the_first_restartiter_iterations[3-3-0] +pamica/tests/test_schedule_gates.py::test_numpy_restart_window_is_the_first_restartiter_iterations[0-0-0] +[gw1] [ 67%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_channel_check_names_the_input_count[mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_reloaded_reduced_model_transforms_and_refits[torch] +[gw3] [ 67%] PASSED pamica/tests/test_schedule_gates.py::test_numpy_restart_window_is_the_first_restartiter_iterations[0-0-0] +pamica/tests/test_schedule_gates.py::test_numpy_restart_clears_the_small_gain_count_as_the_reference_does +[gw1] [ 67%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_reloaded_reduced_model_transforms_and_refits[torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_reloaded_reduced_model_transforms_and_refits[mlx] +[gw1] [ 67%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_reloaded_reduced_model_transforms_and_refits[mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_at_fit_time[pcakeep--3-torch] +[gw1] [ 67%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_at_fit_time[pcakeep--3-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_at_fit_time[pcakeep--3-numpy] +[gw1] [ 67%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_at_fit_time[pcakeep--3-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_at_fit_time[pcakeep--3-mlx] +[gw1] [ 67%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_at_fit_time[pcakeep--3-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_at_fit_time[pcadb--5.0-torch] +[gw1] [ 67%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_at_fit_time[pcadb--5.0-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_at_fit_time[pcadb--5.0-numpy] +[gw1] [ 67%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_at_fit_time[pcadb--5.0-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_at_fit_time[pcadb--5.0-mlx] +[gw1] [ 67%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_at_fit_time[pcadb--5.0-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_by_the_mir_step_gate[pcakeep-20-torch] +[gw1] [ 67%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_by_the_mir_step_gate[pcakeep-20-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_by_the_mir_step_gate[pcakeep-20-mlx] +[gw1] [ 68%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_by_the_mir_step_gate[pcakeep-20-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_by_the_mir_step_gate[pcadb--5.0-torch] +[gw1] [ 68%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_by_the_mir_step_gate[pcadb--5.0-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_by_the_mir_step_gate[pcadb--5.0-mlx] +[gw1] [ 68%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_reassigned_request_is_caught_by_the_mir_step_gate[pcadb--5.0-mlx] +pamica/tests/test_pre_change_loader.py::test_loading_a_present_commit_only_reads +[gw3] [ 68%] PASSED pamica/tests/test_schedule_gates.py::test_numpy_restart_clears_the_small_gain_count_as_the_reference_does +pamica/tests/test_schedule_gates.py::test_numpy_restart_iteration_applies_no_update +[gw1] [ 68%] PASSED pamica/tests/test_pre_change_loader.py::test_loading_a_present_commit_only_reads +pamica/tests/test_pre_change_loader.py::test_a_missing_commit_skips_locally_with_the_fetch_command +[gw1] [ 68%] PASSED pamica/tests/test_pre_change_loader.py::test_a_missing_commit_skips_locally_with_the_fetch_command +pamica/tests/test_pre_change_loader.py::test_a_missing_commit_fails_under_ci +[gw1] [ 68%] PASSED pamica/tests/test_pre_change_loader.py::test_a_missing_commit_fails_under_ci +pamica/tests/test_pre_change_loader.py::test_an_abbreviated_pin_is_rejected +[gw1] [ 68%] PASSED pamica/tests/test_pre_change_loader.py::test_an_abbreviated_pin_is_rejected +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-rejstart-0-1-extra4] +[gw1] [ 68%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-rejstart-0-1-extra4] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-rejstart--2-1-extra5] +[gw1] [ 68%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-rejstart--2-1-extra5] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-rejstart-2.0-1-extra6] +[gw1] [ 68%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-rejstart-2.0-1-extra6] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-share_start-0-1-extra7] +[gw1] [ 68%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-share_start-0-1-extra7] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-share_start--5-1-extra8] +[gw1] [ 68%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-share_start--5-1-extra8] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-share_start-2.5-1-extra9] +[gw1] [ 68%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-share_start-2.5-1-extra9] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-share_start-True-1-extra10] +[gw1] [ 68%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-share_start-True-1-extra10] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-newt_start--1-0-extra0] +[gw1] [ 68%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-newt_start--1-0-extra0] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-newt_start--1-0-extra1] +[gw1] [ 68%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-newt_start--1-0-extra1] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-newt_start-2.5-0-extra2] +[gw1] [ 68%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-newt_start-2.5-0-extra2] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-newt_start-True-0-extra3] +[gw1] [ 68%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-newt_start-True-0-extra3] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-rejstart-0-1-extra4] +[gw1] [ 68%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-rejstart-0-1-extra4] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-rejstart--2-1-extra5] +[gw1] [ 69%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-rejstart--2-1-extra5] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-rejstart-2.0-1-extra6] +[gw1] [ 69%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-rejstart-2.0-1-extra6] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-share_start-0-1-extra7] +[gw1] [ 69%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-share_start-0-1-extra7] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-share_start--5-1-extra8] +[gw1] [ 69%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-share_start--5-1-extra8] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-share_start-2.5-1-extra9] +[gw1] [ 69%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-share_start-2.5-1-extra9] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-share_start-True-1-extra10] +[gw1] [ 69%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[mlx-share_start-True-1-extra10] +pamica/tests/test_schedule_gates.py::test_boundary_schedule_settings_are_accepted[torch] +[gw1] [ 69%] PASSED pamica/tests/test_schedule_gates.py::test_boundary_schedule_settings_are_accepted[torch] +pamica/tests/test_schedule_gates.py::test_boundary_schedule_settings_are_accepted[numpy] +[gw1] [ 69%] PASSED pamica/tests/test_schedule_gates.py::test_boundary_schedule_settings_are_accepted[numpy] +pamica/tests/test_schedule_gates.py::test_boundary_schedule_settings_are_accepted[mlx] +[gw1] [ 69%] PASSED pamica/tests/test_schedule_gates.py::test_boundary_schedule_settings_are_accepted[mlx] +pamica/tests/test_schedule_gates.py::test_numpy_only_schedule_settings_are_validated[kwargs0-histstep must be an integer >= 1, got 0] +[gw1] [ 69%] PASSED pamica/tests/test_schedule_gates.py::test_numpy_only_schedule_settings_are_validated[kwargs0-histstep must be an integer >= 1, got 0] +pamica/tests/test_schedule_gates.py::test_numpy_only_schedule_settings_are_validated[kwargs1-restartiter must be an integer >= 0, got -1] +[gw1] [ 69%] PASSED pamica/tests/test_schedule_gates.py::test_numpy_only_schedule_settings_are_validated[kwargs1-restartiter must be an integer >= 0, got -1] +pamica/tests/test_schedule_gates.py::test_numpy_only_schedule_settings_are_validated[kwargs2-maxrestarts must be an integer >= 0, got -1] +[gw1] [ 69%] PASSED pamica/tests/test_schedule_gates.py::test_numpy_only_schedule_settings_are_validated[kwargs2-maxrestarts must be an integer >= 0, got -1] +pamica/tests/test_schedule_gates.py::test_numpy_only_schedule_settings_are_validated[kwargs3-restartiter must be an integer >= 0, got 2.5] +[gw1] [ 69%] PASSED pamica/tests/test_schedule_gates.py::test_numpy_only_schedule_settings_are_validated[kwargs3-restartiter must be an integer >= 0, got 2.5] +pamica/tests/test_schedule_native_oracle.py::test_torch_first_newton_iterations_match_the_reference +[gw1] [ 69%] SKIPPED pamica/tests/test_schedule_native_oracle.py::test_torch_first_newton_iterations_match_the_reference +pamica/tests/test_schedule_native_oracle.py::test_numpy_first_newton_iterations_match_the_reference +[gw1] [ 69%] SKIPPED pamica/tests/test_schedule_native_oracle.py::test_numpy_first_newton_iterations_match_the_reference +pamica/tests/test_schedule_native_oracle.py::test_the_window_compared_is_newton_sensitive +[gw1] [ 69%] SKIPPED pamica/tests/test_schedule_native_oracle.py::test_the_window_compared_is_newton_sensitive +pamica/tests/test_sensor_mixing_cross_backend.py::test_sensor_mixing_inverts_the_spatial_filter[torch-full] +[gw1] [ 69%] PASSED pamica/tests/test_sensor_mixing_cross_backend.py::test_sensor_mixing_inverts_the_spatial_filter[torch-full] +pamica/tests/test_sensor_mixing_cross_backend.py::test_sensor_mixing_inverts_the_spatial_filter[torch-reduced] +[gw1] [ 69%] PASSED pamica/tests/test_sensor_mixing_cross_backend.py::test_sensor_mixing_inverts_the_spatial_filter[torch-reduced] +pamica/tests/test_sensor_mixing_cross_backend.py::test_sensor_mixing_inverts_the_spatial_filter[numpy-full] +[gw1] [ 69%] PASSED pamica/tests/test_sensor_mixing_cross_backend.py::test_sensor_mixing_inverts_the_spatial_filter[numpy-full] +pamica/tests/test_sensor_mixing_cross_backend.py::test_sensor_mixing_inverts_the_spatial_filter[numpy-reduced] +[gw1] [ 69%] PASSED pamica/tests/test_sensor_mixing_cross_backend.py::test_sensor_mixing_inverts_the_spatial_filter[numpy-reduced] +pamica/tests/test_sensor_mixing_cross_backend.py::test_sensor_mixing_inverts_the_spatial_filter[mlx-full] +[gw1] [ 70%] PASSED pamica/tests/test_sensor_mixing_cross_backend.py::test_sensor_mixing_inverts_the_spatial_filter[mlx-full] +pamica/tests/test_sensor_mixing_cross_backend.py::test_sensor_mixing_inverts_the_spatial_filter[mlx-reduced] +[gw1] [ 70%] PASSED pamica/tests/test_sensor_mixing_cross_backend.py::test_sensor_mixing_inverts_the_spatial_filter[mlx-reduced] +pamica/tests/test_sensor_mixing_cross_backend.py::test_numpy_sensor_mixing_matches_torch[full] +[gw1] [ 70%] PASSED pamica/tests/test_sensor_mixing_cross_backend.py::test_numpy_sensor_mixing_matches_torch[full] +pamica/tests/test_sensor_mixing_cross_backend.py::test_numpy_sensor_mixing_matches_torch[reduced] +[gw1] [ 70%] PASSED pamica/tests/test_sensor_mixing_cross_backend.py::test_numpy_sensor_mixing_matches_torch[reduced] +pamica/tests/test_sensor_mixing_cross_backend.py::test_mlx_sensor_mixing_matches_torch[full] +[gw1] [ 70%] PASSED pamica/tests/test_sensor_mixing_cross_backend.py::test_mlx_sensor_mixing_matches_torch[full] +pamica/tests/test_sensor_mixing_cross_backend.py::test_mlx_sensor_mixing_matches_torch[reduced] +[gw1] [ 70%] PASSED pamica/tests/test_sensor_mixing_cross_backend.py::test_mlx_sensor_mixing_matches_torch[reduced] +pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[torch-True-full] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_numpy_restart_iteration_applies_no_update +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-newt_start--1-0-extra0] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-newt_start--1-0-extra0] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-newt_start--1-0-extra1] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-newt_start--1-0-extra1] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-newt_start-2.5-0-extra2] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-newt_start-2.5-0-extra2] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-newt_start-True-0-extra3] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-newt_start-True-0-extra3] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-rejstart-0-1-extra4] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-rejstart-0-1-extra4] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-rejstart--2-1-extra5] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-rejstart--2-1-extra5] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-rejstart-2.0-1-extra6] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-rejstart-2.0-1-extra6] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-share_start-0-1-extra7] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-share_start-0-1-extra7] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-share_start--5-1-extra8] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-share_start--5-1-extra8] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-share_start-2.5-1-extra9] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-share_start-2.5-1-extra9] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-share_start-True-1-extra10] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[torch-share_start-True-1-extra10] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-newt_start--1-0-extra0] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-newt_start--1-0-extra0] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-newt_start--1-0-extra1] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-newt_start--1-0-extra1] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-newt_start-2.5-0-extra2] +[gw3] [ 70%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-newt_start-2.5-0-extra2] +pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-newt_start-True-0-extra3] +[gw3] [ 71%] PASSED pamica/tests/test_schedule_gates.py::test_schedule_settings_are_rejected_with_one_message[numpy-newt_start-True-0-extra3] +pamica/tests/test_viz.py::test_plot_pmi_heatmap_pixels_are_actually_reordered[False] +[gw3] [ 71%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_pixels_are_actually_reordered[False] +pamica/tests/test_viz.py::test_plot_pmi_heatmap_pixels_are_actually_reordered[True] +[gw3] [ 71%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_pixels_are_actually_reordered[True] +pamica/tests/test_viz.py::test_plot_pmi_heatmap_labels_param_indexed_by_order +[gw3] [ 71%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_labels_param_indexed_by_order +pamica/tests/test_viz.py::test_plot_pmi_heatmap_model_title +[gw3] [ 71%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_model_title +pamica/tests/test_viz.py::test_plot_pmi_heatmap_raises_on_non_square +[gw3] [ 71%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_raises_on_non_square +pamica/tests/test_viz.py::test_plot_pmi_heatmap_raises_on_non_finite[False-nan] +[gw3] [ 71%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_raises_on_non_finite[False-nan] +pamica/tests/test_viz.py::test_plot_pmi_heatmap_raises_on_non_finite[False-inf] +[gw3] [ 71%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_raises_on_non_finite[False-inf] +pamica/tests/test_viz.py::test_plot_pmi_heatmap_raises_on_non_finite[True-nan] +[gw3] [ 71%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_raises_on_non_finite[True-nan] +pamica/tests/test_viz.py::test_plot_pmi_heatmap_raises_on_non_finite[True-inf] +[gw3] [ 71%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_raises_on_non_finite[True-inf] +pamica/tests/test_viz.py::test_plot_pmi_heatmap_accepts_existing_ax +[gw1] [ 71%] PASSED pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[torch-True-full] +pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[torch-True-reduced] +[gw3] [ 71%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_accepts_existing_ax +pamica/tests/test_viz.py::test_plot_model_probability_returns_two_axes +[gw1] [ 71%] PASSED pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[torch-True-reduced] +pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[torch-False-full] +[gw1] [ 71%] PASSED pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[torch-False-full] +pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[torch-False-reduced] +[gw1] [ 71%] PASSED pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[torch-False-reduced] +pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[numpy-True-full] +[gw1] [ 71%] PASSED pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[numpy-True-full] +pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[numpy-True-reduced] +[gw1] [ 71%] PASSED pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[numpy-True-reduced] +pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[numpy-False-full] +[gw1] [ 71%] PASSED pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[numpy-False-full] +pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[numpy-False-reduced] +[gw1] [ 71%] PASSED pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[numpy-False-reduced] +pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[mlx-True-full] +[gw1] [ 71%] PASSED pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[mlx-True-full] +pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[mlx-True-reduced] +[gw1] [ 72%] PASSED pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[mlx-True-reduced] +pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[mlx-False-full] +[gw1] [ 72%] PASSED pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[mlx-False-full] +pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[mlx-False-reduced] +[gw3] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_returns_two_axes +pamica/tests/test_viz.py::test_plot_model_probability_one_line_per_model +[gw1] [ 72%] PASSED pamica/tests/test_sphere_export_order.py::test_loadmodout_sphere_matches_backend[mlx-False-reduced] +pamica/tests/test_viz.py::test_read_eeglab_set_metadata_tolerates_unlocalized_channels +[gw3] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_one_line_per_model +pamica/tests/test_viz.py::test_plot_model_probability_probabilities_sum_to_one +[gw3] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_probabilities_sum_to_one +pamica/tests/test_viz.py::test_plot_model_probability_bottom_panel_is_best_model_ll +[gw1] [ 72%] PASSED pamica/tests/test_viz.py::test_read_eeglab_set_metadata_tolerates_unlocalized_channels +pamica/tests/test_viz.py::test_plot_pmi_heatmap_returns_figure +[gw3] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_bottom_panel_is_best_model_ll +pamica/tests/test_viz.py::test_plot_model_probability_xaxis_samples_without_srate +[gw1] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_returns_figure +pamica/tests/test_viz.py::test_plot_pmi_heatmap_masks_diagonal_by_default +[gw1] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_masks_diagonal_by_default +pamica/tests/test_viz.py::test_plot_pmi_heatmap_mask_diagonal_false_leaves_diagonal_visible +[gw1] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_mask_diagonal_false_leaves_diagonal_visible +pamica/tests/test_viz.py::test_plot_pmi_heatmap_default_order_matches_block_diagonal_order +[gw3] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_xaxis_samples_without_srate +pamica/tests/test_viz.py::test_plot_model_probability_xaxis_seconds_with_srate +[gw1] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_default_order_matches_block_diagonal_order +pamica/tests/test_viz.py::test_plot_pmi_heatmap_custom_order_is_honored +[gw3] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_xaxis_seconds_with_srate +pamica/tests/test_viz.py::test_plot_model_probability_smooth_sec_requires_srate +[gw3] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_smooth_sec_requires_srate +pamica/tests/test_viz.py::test_plot_model_probability_window_sec_requires_srate +[gw3] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_window_sec_requires_srate +pamica/tests/test_viz.py::test_plot_model_probability_smoothing_preserves_normalization +[gw3] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_smoothing_preserves_normalization +pamica/tests/test_viz.py::test_plot_model_probability_smoothing_edge_corrected_not_dragged_to_zero +[gw1] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_pmi_heatmap_custom_order_is_honored +pamica/tests/test_viz.py::test_plot_model_probability_accepts_existing_axes +[gw3] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_smoothing_edge_corrected_not_dragged_to_zero +pamica/tests/test_viz.py::test_plot_model_probability_smooth_sec_too_small_raises +[gw3] [ 72%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_smooth_sec_too_small_raises +pamica/tests/test_viz.py::test_plot_model_probability_smooth_sec_too_large_raises +[gw3] [ 73%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_smooth_sec_too_large_raises +pamica/tests/test_viz.py::test_plot_model_probability_window_sec_sets_xlim +[gw3] [ 73%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_window_sec_sets_xlim +pamica/tests/test_viz.py::test_plot_model_probability_legend_labels +[gw3] [ 73%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_legend_labels +pamica/tests/test_wrapper_backends.py::test_dtype_with_the_mlx_backend_raises_before_fitting +[gw3] [ 73%] PASSED pamica/tests/test_wrapper_backends.py::test_dtype_with_the_mlx_backend_raises_before_fitting +pamica/tests/test_wrapper_backends.py::test_unknown_fit_keyword_names_the_backend[torch] +[gw3] [ 73%] PASSED pamica/tests/test_wrapper_backends.py::test_unknown_fit_keyword_names_the_backend[torch] +pamica/tests/test_wrapper_backends.py::test_unknown_fit_keyword_names_the_backend[mlx] +[gw3] [ 73%] PASSED pamica/tests/test_wrapper_backends.py::test_unknown_fit_keyword_names_the_backend[mlx] +pamica/tests/test_wrapper_backends.py::test_torch_only_and_unknown_together_name_both +[gw3] [ 73%] PASSED pamica/tests/test_wrapper_backends.py::test_torch_only_and_unknown_together_name_both +pamica/tests/test_wrapper_backends.py::test_backend_attribute_is_rechecked_at_fit +[gw3] [ 73%] PASSED pamica/tests/test_wrapper_backends.py::test_backend_attribute_is_rechecked_at_fit +pamica/tests/test_wrapper_backends.py::test_fit_publishes_the_backend_record[torch] +[gw3] [ 73%] PASSED pamica/tests/test_wrapper_backends.py::test_fit_publishes_the_backend_record[torch] +pamica/tests/test_wrapper_backends.py::test_fit_publishes_the_backend_record[mlx] +[gw3] [ 73%] PASSED pamica/tests/test_wrapper_backends.py::test_fit_publishes_the_backend_record[mlx] +pamica/tests/test_wrapper_backends.py::test_every_delegating_method_works[torch] +[gw1] [ 73%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_accepts_existing_axes +pamica/tests/test_viz.py::test_plot_model_probability_raises_without_lht +[gw1] [ 73%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_raises_without_lht +pamica/tests/test_viz.py::test_plot_model_probability_lht_array_matches_out +[gw1] [ 73%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_lht_array_matches_out +pamica/tests/test_viz.py::test_plot_model_probability_requires_exactly_one_source +[gw1] [ 73%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_requires_exactly_one_source +pamica/tests/test_viz.py::test_plot_model_probability_rejects_non_2d_lht +[gw1] [ 73%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_rejects_non_2d_lht +pamica/tests/test_viz.py::test_plot_model_probability_lht_with_smoothing +[gw1] [ 73%] PASSED pamica/tests/test_viz.py::test_plot_model_probability_lht_with_smoothing +pamica/tests/test_wrapper_backends.py::test_v1_payload_still_loads_and_transforms_identically +[gw3] [ 73%] PASSED pamica/tests/test_wrapper_backends.py::test_every_delegating_method_works[torch] +pamica/tests/test_wrapper_backends.py::test_every_delegating_method_works[mlx] +[gw1] [ 73%] PASSED pamica/tests/test_wrapper_backends.py::test_v1_payload_still_loads_and_transforms_identically +pamica/tests/test_wrapper_backends.py::test_unknown_backend_name_lists_the_valid_ones +[gw1] [ 73%] PASSED pamica/tests/test_wrapper_backends.py::test_unknown_backend_name_lists_the_valid_ones +pamica/tests/test_wrapper_backends.py::test_device_with_the_mlx_backend_raises_at_construction +[gw1] [ 73%] PASSED pamica/tests/test_wrapper_backends.py::test_device_with_the_mlx_backend_raises_at_construction +pamica/tests/test_wrapper_backends.py::test_mlx_backend_without_mlx_raises_import_error +[gw1] [ 74%] PASSED pamica/tests/test_wrapper_backends.py::test_mlx_backend_without_mlx_raises_import_error +pamica/tests/test_wrapper_backends.py::test_import_pamica_never_imports_mlx +[gw4] [ 74%] PASSED pamica/tests/test_mlx_newton_cross_backend.py::test_newton_fit_ll_matches_float64_torch +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_log_pdf_matches_fortran[2] +[gw4] [ 74%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_log_pdf_matches_fortran[2] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_log_pdf_matches_fortran[3] +[gw4] [ 74%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_log_pdf_matches_fortran[3] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_log_pdf_matches_fortran[4] +[gw4] [ 74%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_log_pdf_matches_fortran[4] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_log_pdf_matches_fortran[1] +[gw4] [ 74%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_log_pdf_matches_fortran[1] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_score_matches_fortran[2] +[gw4] [ 74%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_score_matches_fortran[2] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_score_matches_fortran[3] +[gw4] [ 74%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_score_matches_fortran[3] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_score_matches_fortran[4] +[gw4] [ 74%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_score_matches_fortran[4] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_score_matches_fortran[1] +[gw4] [ 74%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_family_score_matches_fortran[1] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_gg_path_bit_identical_none_vs_code0 +[gw4] [ 74%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_gg_path_bit_identical_none_vs_code0 +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_one_mstep_matches_torch_twin[2-3] +[gw4] [ 74%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_one_mstep_matches_torch_twin[2-3] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_one_mstep_matches_torch_twin[3-3] +[gw3] [ 74%] PASSED pamica/tests/test_wrapper_backends.py::test_every_delegating_method_works[mlx] +pamica/tests/test_wrapper_backends.py::test_unfitted_accessors_raise_not_fitted[torch] +[gw3] [ 74%] PASSED pamica/tests/test_wrapper_backends.py::test_unfitted_accessors_raise_not_fitted[torch] +pamica/tests/test_wrapper_backends.py::test_unfitted_accessors_raise_not_fitted[mlx] +[gw3] [ 74%] PASSED pamica/tests/test_wrapper_backends.py::test_unfitted_accessors_raise_not_fitted[mlx] +pamica/tests/test_wrapper_backends.py::test_degenerate_fit_is_refused_on_both_backends[torch] +[gw3] [ 74%] PASSED pamica/tests/test_wrapper_backends.py::test_degenerate_fit_is_refused_on_both_backends[torch] +pamica/tests/test_wrapper_backends.py::test_degenerate_fit_is_refused_on_both_backends[mlx] +[gw3] [ 74%] PASSED pamica/tests/test_wrapper_backends.py::test_degenerate_fit_is_refused_on_both_backends[mlx] +pamica/tests/test_wrapper_backends.py::test_repr_names_the_backend_and_state[torch] +[gw3] [ 74%] PASSED pamica/tests/test_wrapper_backends.py::test_repr_names_the_backend_and_state[torch] +pamica/tests/test_wrapper_backends.py::test_repr_names_the_backend_and_state[mlx] +[gw4] [ 74%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_one_mstep_matches_torch_twin[3-3] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs4-pcakeep-torch] +[gw4] [ 74%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs4-pcakeep-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs4-pcakeep-numpy] +[gw3] [ 75%] PASSED pamica/tests/test_wrapper_backends.py::test_repr_names_the_backend_and_state[mlx] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs4-pcakeep-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs4-pcakeep-mlx] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs4-pcakeep-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs5-pcadb-torch] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs5-pcadb-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs5-pcadb-numpy] +pamica/tests/test_wrapper_backends.py::test_params_file_drives_the_mlx_backend +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs5-pcadb-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs5-pcadb-mlx] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs5-pcadb-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs6-pcadb-torch] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs6-pcadb-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs6-pcadb-numpy] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs6-pcadb-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs6-pcadb-mlx] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs6-pcadb-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs7-pcadb-torch] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs7-pcadb-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs7-pcadb-numpy] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs7-pcadb-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs7-pcadb-mlx] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs7-pcadb-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs8-pcadb-torch] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs8-pcadb-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs8-pcadb-numpy] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs8-pcadb-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs8-pcadb-mlx] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs8-pcadb-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs9-pcadb-torch] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs9-pcadb-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs9-pcadb-numpy] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs9-pcadb-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs9-pcadb-mlx] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs9-pcadb-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs10-pcadb-torch] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs10-pcadb-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs10-pcadb-numpy] +[gw4] [ 75%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs10-pcadb-numpy] +pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs10-pcadb-mlx] +[gw4] [ 76%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_invalid_request_raises_at_construction[kwargs10-pcadb-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs0-torch] +[gw4] [ 76%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs0-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs0-mlx] +[gw4] [ 76%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs0-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs1-torch] +[gw4] [ 76%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs1-torch] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs1-mlx] +[gw4] [ 76%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs1-mlx] +pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs2-torch] +[gw4] [ 76%] PASSED pamica/tests/test_pca_reduction_cross_backend.py::test_mir_step_rejects_a_reduction_request_up_front[kwargs2-torch] +pamica/tests/test_wrapper_backends.py::test_torch_and_mlx_wrappers_find_the_same_components[20] +[gw3] [ 76%] PASSED pamica/tests/test_wrapper_backends.py::test_params_file_drives_the_mlx_backend +pamica/tests/test_wrapper_backends.py::test_json_params_file_drives_the_mlx_backend +[gw3] [ 76%] PASSED pamica/tests/test_wrapper_backends.py::test_json_params_file_drives_the_mlx_backend +pamica/tests/test_wrapper_backends.py::test_params_file_warning_names_the_torch_backend +[gw3] [ 76%] PASSED pamica/tests/test_wrapper_backends.py::test_params_file_warning_names_the_torch_backend +pamica/tests/test_wrapper_backends.py::test_wrapper_resolves_the_backends_own_defaults[torch] +[gw3] [ 76%] PASSED pamica/tests/test_wrapper_backends.py::test_wrapper_resolves_the_backends_own_defaults[torch] +pamica/tests/test_wrapper_backends.py::test_wrapper_resolves_the_backends_own_defaults[mlx] +[gw3] [ 76%] PASSED pamica/tests/test_wrapper_backends.py::test_wrapper_resolves_the_backends_own_defaults[mlx] +pamica/tests/test_wrapper_backends.py::test_default_wrapper_fit_is_the_default_backend_fit[torch] +[gw4] [ 76%] PASSED pamica/tests/test_wrapper_backends.py::test_torch_and_mlx_wrappers_find_the_same_components[20] +pamica/tests/test_wrapper_backends.py::test_wrapper_accessors_agree_across_backends +[gw4] [ 76%] PASSED pamica/tests/test_wrapper_backends.py::test_wrapper_accessors_agree_across_backends +pamica/tests/test_wrapper_backends.py::test_malformed_mlx_payload_raises_a_named_error[missing_params] +[gw4] [ 76%] PASSED pamica/tests/test_wrapper_backends.py::test_malformed_mlx_payload_raises_a_named_error[missing_params] +pamica/tests/test_wrapper_backends.py::test_malformed_mlx_payload_raises_a_named_error[non_tensor_param] +[gw4] [ 76%] PASSED pamica/tests/test_wrapper_backends.py::test_malformed_mlx_payload_raises_a_named_error[non_tensor_param] +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_ng_save_requires_fit +[gw4] [ 76%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_ng_save_requires_fit +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_ng_save_load_roundtrip +[gw4] [ 76%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_ng_save_load_roundtrip +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_ng_load_rejects_unknown_version +[gw4] [ 76%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_ng_load_rejects_unknown_version +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_ng_default_device_avoids_mps_float64 +[gw4] [ 76%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_ng_default_device_avoids_mps_float64 +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_raw_backend_default_construction_uses_cpu_on_mps +[gw4] [ 76%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_raw_backend_default_construction_uses_cpu_on_mps +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_raw_backend_default_fit_runs_on_cpu_on_mps +[gw4] [ 76%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_raw_backend_default_fit_runs_on_cpu_on_mps +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_raw_backend_float32_auto_selection_keeps_mps +[gw4] [ 77%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_raw_backend_float32_auto_selection_keeps_mps +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_raw_backend_explicit_mps_float64_raises +[gw4] [ 77%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_raw_backend_explicit_mps_float64_raises +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_load_without_a_device_uses_the_backend_choice +[gw4] [ 77%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_load_without_a_device_uses_the_backend_choice +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_ng_explicit_mps_float64_raises +[gw4] [ 77%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_ng_explicit_mps_float64_raises +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_ng_mps_float32_escape_hatch +[gw1] [ 77%] PASSED pamica/tests/test_wrapper_backends.py::test_import_pamica_never_imports_mlx +pamica/tests/test_wrapper_backends.py::test_explicit_lrate_overrides_the_backend_default[torch] +[gw1] [ 77%] PASSED pamica/tests/test_wrapper_backends.py::test_explicit_lrate_overrides_the_backend_default[torch] +pamica/tests/test_wrapper_backends.py::test_explicit_lrate_overrides_the_backend_default[mlx] +[gw1] [ 77%] PASSED pamica/tests/test_wrapper_backends.py::test_explicit_lrate_overrides_the_backend_default[mlx] +pamica/tests/test_wrapper_backends.py::test_pcakeep_through_the_mlx_wrapper +[gw1] [ 77%] PASSED pamica/tests/test_wrapper_backends.py::test_pcakeep_through_the_mlx_wrapper +pamica/tests/test_wrapper_backends.py::test_mir_step_gate_through_the_mlx_wrapper +[gw1] [ 77%] PASSED pamica/tests/test_wrapper_backends.py::test_mir_step_gate_through_the_mlx_wrapper +pamica/tests/test_wrapper_backends.py::test_save_load_round_trip[torch] +[gw1] [ 77%] PASSED pamica/tests/test_wrapper_backends.py::test_save_load_round_trip[torch] +pamica/tests/test_wrapper_backends.py::test_save_load_round_trip[mlx] +[gw1] [ 77%] PASSED pamica/tests/test_wrapper_backends.py::test_save_load_round_trip[mlx] +pamica/tests/test_wrapper_backends.py::test_mlx_params_round_trip_with_their_dtypes +[gw1] [ 77%] PASSED pamica/tests/test_wrapper_backends.py::test_mlx_params_round_trip_with_their_dtypes +pamica/tests/test_wrapper_backends.py::test_mlx_bool_array_survives_the_payload_conversion +[gw1] [ 77%] PASSED pamica/tests/test_wrapper_backends.py::test_mlx_bool_array_survives_the_payload_conversion +pamica/tests/test_wrapper_backends.py::test_unsavable_state_value_names_its_key_path[config-restart_seeds-value0] +[gw1] [ 77%] PASSED pamica/tests/test_wrapper_backends.py::test_unsavable_state_value_names_its_key_path[config-restart_seeds-value0] +pamica/tests/test_wrapper_backends.py::test_unsavable_state_value_names_its_key_path[extra-good_idx-value1] +[gw1] [ 77%] PASSED pamica/tests/test_wrapper_backends.py::test_unsavable_state_value_names_its_key_path[extra-good_idx-value1] +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_llt_partial_final_block +[gw4] [ 77%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_ng_mps_float32_escape_hatch +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_requires_fit +[gw4] [ 77%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_requires_fit +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_variance_order_requires_fit +[gw4] [ 77%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_variance_order_requires_fit +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_bytes +[gw4] [ 77%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_bytes +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_loadmodout_readable +[gw4] [ 77%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_loadmodout_readable +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_variance_order +[gw4] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_variance_order +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_ll_matches_kept_iterate +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_llt_partial_final_block +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_from_state_dict_write_amica_output_omits_llt +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_from_state_dict_write_amica_output_omits_llt +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_ng_wrapper_fit_transform_real_data +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_ng_wrapper_fit_transform_real_data +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_fit_exposes_converged_and_stop_reason +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_fit_exposes_converged_and_stop_reason +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_unfitted_output_raises_not_fitted +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_unfitted_output_raises_not_fitted +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_degenerate_fit_refuses_output +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_degenerate_fit_refuses_output +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_composes_unmixing_the_documented_way +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_composes_unmixing_the_documented_way +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_pmi_matches_pairwise_mi_on_transform +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_pmi_matches_pairwise_mi_on_transform +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_and_pmi_honour_model_idx +[gw2] [ 78%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_full_fit_ll_matches_torch_float64[3-3] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_full_fit_ll_matches_torch_float64[4-1] +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_and_pmi_honour_model_idx +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_and_pmi_honour_nbins +[gw5] [ 78%] PASSED pamica/tests/test_schedule_gates.py::test_newt_start_zero_fits_exactly_as_one[torch] +pamica/tests/test_schedule_gates.py::test_newt_start_zero_fits_exactly_as_one[numpy] +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_and_pmi_honour_nbins +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_real_fitted_unmixing_is_large_and_positive +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_real_fitted_unmixing_is_large_and_positive +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_step_populates_history_at_right_iterations +[gw4] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_ll_matches_kept_iterate +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_llt_roundtrip +[gw4] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_llt_roundtrip +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_llt_multimodel +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_step_populates_history_at_right_iterations +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_step_zero_leaves_history_empty +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_step_zero_leaves_history_empty +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_step_negative_raises +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_step_negative_raises +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_max_iter_zero_raises +[gw1] [ 78%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_max_iter_zero_raises +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_failing_mir_waypoint_does_not_kill_the_fit +[gw4] [ 79%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_llt_multimodel +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_loadmodout_sources_reproduce_live_transform_multimodel +[gw1] [ 79%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_failing_mir_waypoint_does_not_kill_the_fit +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_step_zero_matches_omitted_argument +[gw3] [ 79%] PASSED pamica/tests/test_wrapper_backends.py::test_default_wrapper_fit_is_the_default_backend_fit[torch] +pamica/tests/test_wrapper_backends.py::test_default_wrapper_fit_is_the_default_backend_fit[mlx] +[gw1] [ 79%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_step_zero_matches_omitted_argument +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_raises_under_pca_reduction +[gw1] [ 79%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_raises_under_pca_reduction +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_step_raises_under_pca_reduction_up_front +[gw1] [ 79%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_step_raises_under_pca_reduction_up_front +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_bundled_input_param_passes_the_mir_step_gate +[gw4] [ 79%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_loadmodout_sources_reproduce_live_transform_multimodel +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_written_w_bytes_are_genuine_fortran_layout +[gw4] [ 79%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_written_w_bytes_are_genuine_fortran_layout +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_load_results_returns_internal_w_multimodel +[gw4] [ 79%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_load_results_returns_internal_w_multimodel +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_loadmodout_sources_roundtrip_with_share_comps +[gw3] [ 79%] PASSED pamica/tests/test_wrapper_backends.py::test_default_wrapper_fit_is_the_default_backend_fit[mlx] +pamica/tests/test_wrapper_backends.py::test_weights_only_safe_converts_scalars_and_names_nested_paths +[gw3] [ 79%] PASSED pamica/tests/test_wrapper_backends.py::test_weights_only_safe_converts_scalars_and_names_nested_paths +pamica/tests/test_wrapper_backends.py::test_numpy_scalar_settings_save_and_load[torch] +[gw3] [ 79%] PASSED pamica/tests/test_wrapper_backends.py::test_numpy_scalar_settings_save_and_load[torch] +pamica/tests/test_wrapper_backends.py::test_numpy_scalar_settings_save_and_load[mlx] +[gw3] [ 79%] PASSED pamica/tests/test_wrapper_backends.py::test_numpy_scalar_settings_save_and_load[mlx] +pamica/tests/test_wrapper_backends.py::test_v2_payload_without_a_backend_is_malformed +[gw3] [ 79%] PASSED pamica/tests/test_wrapper_backends.py::test_v2_payload_without_a_backend_is_malformed +pamica/tests/test_wrapper_backends.py::test_unknown_format_version_names_the_readable_ones +[gw3] [ 79%] PASSED pamica/tests/test_wrapper_backends.py::test_unknown_format_version_names_the_readable_ones +pamica/tests/test_wrapper_backends.py::test_mlx_payload_with_a_device_raises +[gw3] [ 79%] PASSED pamica/tests/test_wrapper_backends.py::test_mlx_payload_with_a_device_raises +pamica/tests/test_wrapper_backends.py::test_mlx_payload_without_mlx_names_the_backend +[gw3] [ 79%] PASSED pamica/tests/test_wrapper_backends.py::test_mlx_payload_without_mlx_names_the_backend +pamica/tests/test_wrapper_backends.py::test_mlx_eeglab_export_reloads_through_loadmodout[None] +[gw3] [ 79%] PASSED pamica/tests/test_wrapper_backends.py::test_mlx_eeglab_export_reloads_through_loadmodout[None] +pamica/tests/test_wrapper_backends.py::test_mlx_eeglab_export_reloads_through_loadmodout[20] +[gw3] [ 79%] PASSED pamica/tests/test_wrapper_backends.py::test_mlx_eeglab_export_reloads_through_loadmodout[20] +pamica/tests/test_wrapper_backends.py::test_torch_and_mlx_wrappers_find_the_same_components[None] +[gw1] [ 79%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_bundled_input_param_passes_the_mir_step_gate +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_raises_under_auto_detected_rank_reduction +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_raises_under_auto_detected_rank_reduction +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_full_rank_succeeds +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_mir_full_rank_succeeds +pamica/tests/torch_tests/test_edge_cases.py::test_fit_rejects_non_2d_input +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_edge_cases.py::test_fit_rejects_non_2d_input +pamica/tests/torch_tests/test_edge_cases.py::test_single_channel_fit_is_graceful +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_edge_cases.py::test_single_channel_fit_is_graceful +pamica/tests/torch_tests/test_edge_cases.py::test_single_sample_is_graceful +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_edge_cases.py::test_single_sample_is_graceful +pamica/tests/torch_tests/test_edge_cases.py::test_from_params_file_reads_and_overrides +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_edge_cases.py::test_from_params_file_reads_and_overrides +pamica/tests/torch_tests/test_edge_cases.py::test_load_rejects_missing_backend_key +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_edge_cases.py::test_load_rejects_missing_backend_key +pamica/tests/torch_tests/test_ng_backend.py::test_sufficient_stats_match_numpy_reference +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_sufficient_stats_match_numpy_reference +pamica/tests/torch_tests/test_ng_backend.py::test_newton_direction_matches_formula +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_newton_direction_matches_formula +pamica/tests/torch_tests/test_ng_backend.py::test_newton_posdef_mstep_composition +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_newton_posdef_mstep_composition +pamica/tests/torch_tests/test_ng_backend.py::test_newton_stats_match_at_rho_boundaries[1.0] +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_newton_stats_match_at_rho_boundaries[1.0] +pamica/tests/torch_tests/test_ng_backend.py::test_newton_stats_match_at_rho_boundaries[2.0] +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_newton_stats_match_at_rho_boundaries[2.0] +pamica/tests/torch_tests/test_ng_backend.py::test_multimodel_sufficient_stats_match_numpy_reference +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_multimodel_sufficient_stats_match_numpy_reference +pamica/tests/torch_tests/test_ng_backend.py::test_single_model_bias_c_stays_zero +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_single_model_bias_c_stays_zero +pamica/tests/torch_tests/test_ng_backend.py::test_multimodel_bias_c_is_responsibility_weighted_data_mean +[gw1] [ 80%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_multimodel_bias_c_is_responsibility_weighted_data_mean +pamica/tests/torch_tests/test_ng_backend.py::test_newton_multimodel_finite_and_shaped +[gw3] [ 80%] PASSED pamica/tests/test_wrapper_backends.py::test_torch_and_mlx_wrappers_find_the_same_components[None] +pamica/tests/torch_tests/test_ng_backend.py::test_blocking_invariance +[gw4] [ 80%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_loadmodout_sources_roundtrip_with_share_comps +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_loadmodout_llt_gm_reorder_alignment +[gw3] [ 80%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_blocking_invariance +pamica/tests/torch_tests/test_ng_backend.py::test_blocking_invariance_multimodel_bias +[gw3] [ 80%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_blocking_invariance_multimodel_bias +pamica/tests/torch_tests/test_ng_backend.py::test_reported_ll_includes_jacobian_near_fortran_at_init +[gw4] [ 80%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_loadmodout_llt_gm_reorder_alignment +pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_llt_reject_zeroes_rejected_samples +[gw3] [ 81%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_reported_ll_includes_jacobian_near_fortran_at_init +pamica/tests/torch_tests/test_ng_backend.py::test_forward_ll_per_source_factorization_order +[gw3] [ 81%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_forward_ll_per_source_factorization_order +pamica/tests/torch_tests/test_ng_backend.py::test_fit_produces_finite_unmixing_on_real_data +[gw4] [ 81%] PASSED pamica/tests/torch_tests/test_amica_ng_wrapper.py::test_write_amica_output_llt_reject_zeroes_rejected_samples +pamica/tests/torch_tests/test_ng_backend.py::test_multimodel_rejection_keeps_bias_c_finite +[gw1] [ 81%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_newton_multimodel_finite_and_shaped +pamica/tests/torch_tests/test_ng_backend.py::test_newton_three_model_finite_and_shaped +[gw3] [ 81%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_fit_produces_finite_unmixing_on_real_data +pamica/tests/torch_tests/test_ng_backend.py::test_newton_stats_match_numpy_reference +[gw5] [ 81%] PASSED pamica/tests/test_schedule_gates.py::test_newt_start_zero_fits_exactly_as_one[numpy] +pamica/tests/test_schedule_gates.py::test_newt_start_zero_fits_exactly_as_one[mlx] +[gw3] [ 81%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_newton_stats_match_numpy_reference +pamica/tests/torch_tests/test_ng_backend.py::test_newton_mstep_matches_numpy_reference +[gw3] [ 81%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_newton_mstep_matches_numpy_reference +pamica/tests/torch_tests/test_ng_backend.py::test_newton_finalize_uses_preupdate_mu +[gw3] [ 81%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_newton_finalize_uses_preupdate_mu +pamica/tests/torch_tests/test_ng_backend.py::test_keep_best_single_model_is_bit_exact +[gw7] [ 81%] PASSED pamica/tests/test_component_rows.py::test_a_planted_duplicate_is_merged_and_the_model_is_unchanged[torch-default] +pamica/tests/test_component_rows.py::test_a_planted_duplicate_is_merged_and_the_model_is_unchanged[torch-strict] +[gw7] [ 81%] PASSED pamica/tests/test_component_rows.py::test_a_planted_duplicate_is_merged_and_the_model_is_unchanged[torch-strict] +pamica/tests/test_component_rows.py::test_a_planted_duplicate_is_merged_and_the_model_is_unchanged[numpy-default] +[gw5] [ 81%] PASSED pamica/tests/test_schedule_gates.py::test_newt_start_zero_fits_exactly_as_one[mlx] +pamica/tests/test_schedule_gates.py::test_rho_rate_ratchet_opens_only_after_iteration_newt_start[torch] +[gw2] [ 81%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_full_fit_ll_matches_torch_float64[4-1] +pamica/tests/test_mlx_pdf_families_cross_backend.py::test_full_fit_ll_matches_torch_float64[1-1] +[gw7] [ 81%] PASSED pamica/tests/test_component_rows.py::test_a_planted_duplicate_is_merged_and_the_model_is_unchanged[numpy-default] +pamica/tests/test_component_rows.py::test_a_planted_duplicate_is_merged_and_the_model_is_unchanged[numpy-strict] +[gw7] [ 81%] PASSED pamica/tests/test_component_rows.py::test_a_planted_duplicate_is_merged_and_the_model_is_unchanged[numpy-strict] +pamica/tests/test_component_rows.py::test_a_planted_duplicate_is_merged_and_the_model_is_unchanged[mlx-default] +[gw7] [ 81%] PASSED pamica/tests/test_component_rows.py::test_a_planted_duplicate_is_merged_and_the_model_is_unchanged[mlx-default] +pamica/tests/test_component_rows.py::test_a_planted_duplicate_is_merged_and_the_model_is_unchanged[mlx-strict] +[gw7] [ 81%] PASSED pamica/tests/test_component_rows.py::test_a_planted_duplicate_is_merged_and_the_model_is_unchanged[mlx-strict] +pamica/tests/test_component_rows.py::test_the_column_semantics_misses_and_breaks_a_planted_duplicate +[gw7] [ 81%] PASSED pamica/tests/test_component_rows.py::test_the_column_semantics_misses_and_breaks_a_planted_duplicate +pamica/tests/test_component_rows.py::test_the_metric_compares_the_sensor_maps[torch] +[gw7] [ 81%] PASSED pamica/tests/test_component_rows.py::test_the_metric_compares_the_sensor_maps[torch] +pamica/tests/test_component_rows.py::test_the_metric_compares_the_sensor_maps[numpy] +[gw7] [ 81%] PASSED pamica/tests/test_component_rows.py::test_the_metric_compares_the_sensor_maps[numpy] +pamica/tests/test_native_engine.py::test_platform_tag_unsupported_raises +[gw7] [ 82%] PASSED pamica/tests/test_native_engine.py::test_platform_tag_unsupported_raises +pamica/tests/test_native_engine.py::test_asset_name_alias_and_extension +[gw7] [ 82%] PASSED pamica/tests/test_native_engine.py::test_asset_name_alias_and_extension +pamica/tests/test_native_engine.py::test_verify_checksum +[gw7] [ 82%] PASSED pamica/tests/test_native_engine.py::test_verify_checksum +pamica/tests/test_native_engine.py::test_resolve_env_override +[gw7] [ 82%] PASSED pamica/tests/test_native_engine.py::test_resolve_env_override +pamica/tests/test_native_engine.py::test_resolve_no_download_without_cache +[gw7] [ 82%] PASSED pamica/tests/test_native_engine.py::test_resolve_no_download_without_cache +pamica/tests/test_native_engine.py::test_resolve_download_verifies_and_caches +[gw7] [ 82%] PASSED pamica/tests/test_native_engine.py::test_resolve_download_verifies_and_caches +pamica/tests/test_native_engine.py::test_resolve_bad_checksum_does_not_poison_cache +[gw7] [ 82%] PASSED pamica/tests/test_native_engine.py::test_resolve_bad_checksum_does_not_poison_cache +pamica/tests/test_native_engine.py::test_native_engine_runs_and_produces_amica_output +[gw7] [ 82%] SKIPPED pamica/tests/test_native_engine.py::test_native_engine_runs_and_produces_amica_output +pamica/tests/test_native_engine.py::test_native_engine_asymmetric_sphere_reads_the_right_orientation +[gw7] [ 82%] SKIPPED pamica/tests/test_native_engine.py::test_native_engine_asymmetric_sphere_reads_the_right_orientation +pamica/tests/test_native_engine.py::test_native_engine_param_aliases_and_multimodel +[gw7] [ 82%] SKIPPED pamica/tests/test_native_engine.py::test_native_engine_param_aliases_and_multimodel +pamica/tests/test_native_engine.py::test_native_engine_degenerate_fit_raises_clearly +[gw7] [ 82%] SKIPPED pamica/tests/test_native_engine.py::test_native_engine_degenerate_fit_raises_clearly +pamica/tests/test_native_engine.py::test_native_engine_rejects_non_2d +[gw7] [ 82%] SKIPPED pamica/tests/test_native_engine.py::test_native_engine_rejects_non_2d +pamica/tests/test_native_engine.py::test_native_engine_missing_binary_errors +[gw7] [ 82%] PASSED pamica/tests/test_native_engine.py::test_native_engine_missing_binary_errors +pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_likelihood_leaves_the_same_finite_history[torch] +[gw7] [ 82%] PASSED pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_likelihood_leaves_the_same_finite_history[torch] +pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_likelihood_leaves_the_same_finite_history[numpy] +[gw7] [ 82%] PASSED pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_likelihood_leaves_the_same_finite_history[numpy] +pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_likelihood_leaves_the_same_finite_history[mlx] +[gw7] [ 82%] PASSED pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_likelihood_leaves_the_same_finite_history[mlx] +pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_direction_stops_before_the_update[torch-dAk] +[gw7] [ 82%] PASSED pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_direction_stops_before_the_update[torch-dAk] +pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_direction_stops_before_the_update[torch-ndtmpsum] +[gw7] [ 82%] PASSED pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_direction_stops_before_the_update[torch-ndtmpsum] +pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_direction_stops_before_the_update[numpy-dAk] +[gw7] [ 82%] PASSED pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_direction_stops_before_the_update[numpy-dAk] +pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_direction_stops_before_the_update[numpy-ndtmpsum] +[gw7] [ 82%] PASSED pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_direction_stops_before_the_update[numpy-ndtmpsum] +pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_direction_stops_before_the_update[mlx-dAk] +[gw7] [ 82%] PASSED pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_direction_stops_before_the_update[mlx-dAk] +pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_direction_stops_before_the_update[mlx-ndtmpsum] +[gw5] [ 83%] PASSED pamica/tests/test_schedule_gates.py::test_rho_rate_ratchet_opens_only_after_iteration_newt_start[torch] +pamica/tests/test_schedule_gates.py::test_rho_rate_ratchet_opens_only_after_iteration_newt_start[numpy] +[gw7] [ 83%] PASSED pamica/tests/test_nonfinite_stops.py::test_a_nonfinite_direction_stops_before_the_update[mlx-ndtmpsum] +pamica/tests/test_nonfinite_stops.py::test_nonfinite_parameters_after_the_last_update_stop_the_fit[torch] +[gw7] [ 83%] PASSED pamica/tests/test_nonfinite_stops.py::test_nonfinite_parameters_after_the_last_update_stop_the_fit[torch] +pamica/tests/test_nonfinite_stops.py::test_nonfinite_parameters_after_the_last_update_stop_the_fit[numpy] +[gw7] [ 83%] PASSED pamica/tests/test_nonfinite_stops.py::test_nonfinite_parameters_after_the_last_update_stop_the_fit[numpy] +pamica/tests/test_nonfinite_stops.py::test_nonfinite_parameters_after_the_last_update_stop_the_fit[mlx] +[gw7] [ 83%] PASSED pamica/tests/test_nonfinite_stops.py::test_nonfinite_parameters_after_the_last_update_stop_the_fit[mlx] +pamica/tests/test_nonfinite_stops.py::test_numpy_restart_window_takes_only_what_a_restart_repairs[params_A-True] +[gw7] [ 83%] PASSED pamica/tests/test_nonfinite_stops.py::test_numpy_restart_window_takes_only_what_a_restart_repairs[params_A-True] +pamica/tests/test_nonfinite_stops.py::test_numpy_restart_window_takes_only_what_a_restart_repairs[params-False] +[gw7] [ 83%] PASSED pamica/tests/test_nonfinite_stops.py::test_numpy_restart_window_takes_only_what_a_restart_repairs[params-False] +pamica/tests/test_numpy_block_tuning.py::test_search_is_now_off_by_default +[gw7] [ 83%] PASSED pamica/tests/test_numpy_block_tuning.py::test_search_is_now_off_by_default +pamica/tests/test_numpy_block_tuning.py::test_bare_default_fit_runs_at_the_shipped_block_size +[gw7] [ 83%] PASSED pamica/tests/test_numpy_block_tuning.py::test_bare_default_fit_runs_at_the_shipped_block_size +pamica/tests/test_numpy_block_tuning.py::test_tuner_runs_on_the_reduced_channel_count +[gw1] [ 83%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_newton_three_model_finite_and_shaped +pamica/tests/torch_tests/test_ng_backend.py::test_multimodel_bias_c_matches_numpy_reference +[gw1] [ 83%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_multimodel_bias_c_matches_numpy_reference +pamica/tests/torch_tests/test_ng_backend.py::test_numpy_multimodel_bias_c_updates +[gw7] [ 83%] PASSED pamica/tests/test_numpy_block_tuning.py::test_tuner_runs_on_the_reduced_channel_count +pamica/tests/test_numpy_block_tuning.py::test_default_sweep_bounds_match_the_other_backends +[gw7] [ 83%] PASSED pamica/tests/test_numpy_block_tuning.py::test_default_sweep_bounds_match_the_other_backends +pamica/tests/test_numpy_block_tuning.py::test_block_size_untouched_when_search_is_off +[gw1] [ 83%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_numpy_multimodel_bias_c_updates +pamica/tests/torch_tests/test_ng_backend.py::test_multimodel_transform_applies_bias_c +[gw7] [ 83%] PASSED pamica/tests/test_numpy_block_tuning.py::test_block_size_untouched_when_search_is_off +pamica/tests/test_numpy_block_tuning.py::test_sweep_bounds_are_not_validated_when_search_is_off +[gw7] [ 83%] PASSED pamica/tests/test_numpy_block_tuning.py::test_sweep_bounds_are_not_validated_when_search_is_off +pamica/tests/test_numpy_block_tuning.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs0-blk_min must be >= 1] +[gw7] [ 83%] PASSED pamica/tests/test_numpy_block_tuning.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs0-blk_min must be >= 1] +pamica/tests/test_numpy_block_tuning.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs1-blk_step must be >= 1] +[gw7] [ 83%] PASSED pamica/tests/test_numpy_block_tuning.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs1-blk_step must be >= 1] +pamica/tests/test_numpy_block_tuning.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs2-blk_max must be >= blk_min] +[gw7] [ 83%] PASSED pamica/tests/test_numpy_block_tuning.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs2-blk_max must be >= blk_min] +pamica/tests/test_numpy_block_tuning.py::test_naive_helper_is_gone_not_deprecated +[gw7] [ 83%] PASSED pamica/tests/test_numpy_block_tuning.py::test_naive_helper_is_gone_not_deprecated +pamica/tests/test_numpy_block_tuning.py::test_chosen_block_size_is_a_valid_candidate_and_is_logged +[gw1] [ 84%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_multimodel_transform_applies_bias_c +pamica/tests/torch_tests/test_ng_backend.py::test_multimodel_dead_model_keeps_prior_c +[gw1] [ 84%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_multimodel_dead_model_keeps_prior_c +pamica/tests/torch_tests/test_ng_backend.py::test_rejection_degenerate_ll_raises_clear_error +[gw1] [ 84%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_rejection_degenerate_ll_raises_clear_error +pamica/tests/torch_tests/test_ng_backend.py::test_reject_param_validation +[gw1] [ 84%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_reject_param_validation +pamica/tests/torch_tests/test_ng_backend.py::test_rejection_shrinks_good_sample_set +[gw0] [ 84%] PASSED pamica/tests/mlx_tests/test_mlx_backend.py::test_rholrate_ratchets_at_maxdecs_not_per_decrease +pamica/tests/mlx_tests/test_mlx_backend.py::test_rholrate_ceiling_ratchet_gated_on_newt_start +[gw7] [ 84%] PASSED pamica/tests/test_numpy_block_tuning.py::test_chosen_block_size_is_a_valid_candidate_and_is_logged +pamica/tests/test_numpy_block_tuning.py::test_candidates_never_exceed_the_sample_count +[gw7] [ 84%] PASSED pamica/tests/test_numpy_block_tuning.py::test_candidates_never_exceed_the_sample_count +pamica/tests/test_numpy_block_tuning.py::test_post_tune_fit_is_bit_identical_to_a_direct_fit +[gw4] [ 84%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_multimodel_rejection_keeps_bias_c_finite +pamica/tests/torch_tests/test_ng_backend.py::test_state_dict_requires_fit +[gw4] [ 84%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_state_dict_requires_fit +pamica/tests/torch_tests/test_ng_backend.py::test_state_dict_roundtrip_all_fields +[gw7] [ 84%] PASSED pamica/tests/test_numpy_block_tuning.py::test_post_tune_fit_is_bit_identical_to_a_direct_fit +pamica/tests/test_numpy_block_tuning.py::test_post_tune_fit_is_bit_identical_under_do_reject +[gw7] [ 84%] PASSED pamica/tests/test_numpy_block_tuning.py::test_post_tune_fit_is_bit_identical_under_do_reject +pamica/tests/torch_tests/test_ng_block_tuning.py::test_defaults_are_the_shared_re_derived_bounds +[gw7] [ 84%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_defaults_are_the_shared_re_derived_bounds +pamica/tests/torch_tests/test_ng_block_tuning.py::test_block_size_untouched_when_search_is_off +[gw7] [ 84%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_block_size_untouched_when_search_is_off +pamica/tests/torch_tests/test_ng_block_tuning.py::test_sweep_bounds_are_not_validated_when_search_is_off +[gw7] [ 84%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_sweep_bounds_are_not_validated_when_search_is_off +pamica/tests/torch_tests/test_ng_block_tuning.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs0-blk_min must be >= 1] +[gw7] [ 84%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs0-blk_min must be >= 1] +pamica/tests/torch_tests/test_ng_block_tuning.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs1-blk_step must be >= 1] +[gw7] [ 84%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs1-blk_step must be >= 1] +pamica/tests/torch_tests/test_ng_block_tuning.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs2-blk_max must be >= blk_min] +[gw7] [ 84%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs2-blk_max must be >= blk_min] +pamica/tests/torch_tests/test_ng_block_tuning.py::test_chosen_block_size_is_a_valid_candidate_and_is_logged +[gw7] [ 84%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_chosen_block_size_is_a_valid_candidate_and_is_logged +pamica/tests/torch_tests/test_ng_block_tuning.py::test_probes_leave_block_size_and_parameters_untouched +[gw2] [ 84%] PASSED pamica/tests/test_mlx_pdf_families_cross_backend.py::test_full_fit_ll_matches_torch_float64[1-1] +pamica/tests/test_mlx_reject_cross_backend.py::test_reject_decisions_agree_up_to_float32_rounding[1-7] +[gw7] [ 84%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_probes_leave_block_size_and_parameters_untouched +pamica/tests/torch_tests/test_ng_block_tuning.py::test_allocation_failure_falls_back_to_the_last_working_size +[gw5] [ 85%] PASSED pamica/tests/test_schedule_gates.py::test_rho_rate_ratchet_opens_only_after_iteration_newt_start[numpy] +pamica/tests/test_schedule_gates.py::test_rho_rate_ratchet_opens_only_after_iteration_newt_start[mlx] +[gw2] [ 85%] PASSED pamica/tests/test_mlx_reject_cross_backend.py::test_reject_decisions_agree_up_to_float32_rounding[1-7] +pamica/tests/test_mlx_reject_cross_backend.py::test_reject_decisions_agree_up_to_float32_rounding[1-8] +[gw7] [ 85%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_allocation_failure_falls_back_to_the_last_working_size +pamica/tests/torch_tests/test_ng_block_tuning.py::test_a_real_error_in_the_probe_is_not_swallowed +[gw7] [ 85%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_a_real_error_in_the_probe_is_not_swallowed +pamica/tests/torch_tests/test_ng_block_tuning.py::test_wrapper_forwards_the_search_kwargs +[gw2] [ 85%] PASSED pamica/tests/test_mlx_reject_cross_backend.py::test_reject_decisions_agree_up_to_float32_rounding[1-8] +pamica/tests/test_mlx_reject_cross_backend.py::test_reject_decisions_agree_up_to_float32_rounding[2-7] +[gw5] [ 85%] PASSED pamica/tests/test_schedule_gates.py::test_rho_rate_ratchet_opens_only_after_iteration_newt_start[mlx] +pamica/tests/test_schedule_gates.py::test_rho_rate_ratchet_gate_at_the_shipped_default_newt_start[torch] +[gw7] [ 85%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_wrapper_forwards_the_search_kwargs +pamica/tests/torch_tests/test_ng_block_tuning.py::test_state_dict_round_trips_the_tuned_block_size +[gw2] [ 85%] PASSED pamica/tests/test_mlx_reject_cross_backend.py::test_reject_decisions_agree_up_to_float32_rounding[2-7] +pamica/tests/test_mlx_reject_cross_backend.py::test_reject_decisions_agree_up_to_float32_rounding[2-8] +[gw7] [ 85%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_state_dict_round_trips_the_tuned_block_size +pamica/tests/torch_tests/test_ng_convergence.py::test_min_dll_stop_matches_independent_reference +[gw1] [ 85%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_rejection_shrinks_good_sample_set +pamica/tests/torch_tests/test_ng_block_tuning.py::test_candidates_never_exceed_the_sample_count +[gw1] [ 85%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_candidates_never_exceed_the_sample_count +pamica/tests/torch_tests/test_ng_block_tuning.py::test_tuner_runs_on_the_reduced_channel_count +[gw5] [ 85%] PASSED pamica/tests/test_schedule_gates.py::test_rho_rate_ratchet_gate_at_the_shipped_default_newt_start[torch] +pamica/tests/test_schedule_gates.py::test_rho_rate_ratchet_gate_at_the_shipped_default_newt_start[numpy] +[gw2] [ 85%] PASSED pamica/tests/test_mlx_reject_cross_backend.py::test_reject_decisions_agree_up_to_float32_rounding[2-8] +pamica/tests/test_mlx_reject_cross_backend.py::test_reject_agreement_check_catches_a_divergence[threshold] +[gw1] [ 85%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_tuner_runs_on_the_reduced_channel_count +pamica/tests/torch_tests/test_ng_block_tuning.py::test_tuner_runs_for_multiple_models +[gw2] [ 85%] PASSED pamica/tests/test_mlx_reject_cross_backend.py::test_reject_agreement_check_catches_a_divergence[threshold] +pamica/tests/test_mlx_reject_cross_backend.py::test_reject_agreement_check_catches_a_divergence[trajectory] +[gw1] [ 85%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_tuner_runs_for_multiple_models +pamica/tests/torch_tests/test_ng_block_tuning.py::test_small_data_degrades_to_a_single_clamped_candidate +[gw1] [ 85%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_small_data_degrades_to_a_single_clamped_candidate +pamica/tests/torch_tests/test_ng_block_tuning.py::test_available_memory_falls_back_to_host_ram_on_cpu +[gw1] [ 85%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_available_memory_falls_back_to_host_ram_on_cpu +pamica/tests/torch_tests/test_ng_block_tuning.py::test_search_runs_under_do_reject +[gw5] [ 85%] PASSED pamica/tests/test_schedule_gates.py::test_rho_rate_ratchet_gate_at_the_shipped_default_newt_start[numpy] +pamica/tests/test_schedule_gates.py::test_rho_rate_ratchet_gate_at_the_shipped_default_newt_start[mlx] +[gw2] [ 85%] PASSED pamica/tests/test_mlx_reject_cross_backend.py::test_reject_agreement_check_catches_a_divergence[trajectory] +pamica/tests/test_mlx_reject_cross_backend.py::test_reject_x_pdftype1_kurtosis_switch_matches_across_backends +[gw1] [ 86%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_search_runs_under_do_reject +pamica/tests/torch_tests/test_ng_block_tuning.py::test_post_tune_fit_is_bit_identical_to_a_direct_fit +[gw5] [ 86%] PASSED pamica/tests/test_schedule_gates.py::test_rho_rate_ratchet_gate_at_the_shipped_default_newt_start[mlx] +pamica/tests/test_schedule_gates.py::test_newton_switch_on_clears_the_decrease_counter[torch] +[gw5] [ 86%] PASSED pamica/tests/test_schedule_gates.py::test_newton_switch_on_clears_the_decrease_counter[torch] +pamica/tests/test_schedule_gates.py::test_newton_switch_on_clears_the_decrease_counter[numpy] +[gw5] [ 86%] PASSED pamica/tests/test_schedule_gates.py::test_newton_switch_on_clears_the_decrease_counter[numpy] +pamica/tests/test_schedule_gates.py::test_newton_switch_on_clears_the_decrease_counter[mlx] +[gw5] [ 86%] PASSED pamica/tests/test_schedule_gates.py::test_newton_switch_on_clears_the_decrease_counter[mlx] +pamica/tests/test_schedule_gates.py::test_rejection_first_fires_on_iteration_rejstart[torch] +[gw4] [ 86%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_state_dict_roundtrip_all_fields +pamica/tests/torch_tests/test_ng_backend.py::test_state_dict_refuses_degenerate_model +[gw2] [ 86%] PASSED pamica/tests/test_mlx_reject_cross_backend.py::test_reject_x_pdftype1_kurtosis_switch_matches_across_backends +pamica/tests/test_mlx_reject_cross_backend.py::test_mir_matches_torch_on_identical_parameters +[gw4] [ 86%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_state_dict_refuses_degenerate_model +pamica/tests/torch_tests/test_ng_backend.py::test_state_dict_refuses_nonfinite_params +[gw4] [ 86%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_state_dict_refuses_nonfinite_params +pamica/tests/torch_tests/test_ng_backend.py::test_write_amica_output_refuses_degenerate_model +[gw2] [ 86%] PASSED pamica/tests/test_mlx_reject_cross_backend.py::test_mir_matches_torch_on_identical_parameters +pamica/tests/test_mlx_sharing_cross_backend.py::test_merge_decision_matches_the_torch_backend +[gw4] [ 86%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_write_amica_output_refuses_degenerate_model +pamica/tests/torch_tests/test_ng_backend.py::test_write_amica_output_refuses_nonfinite_params +[gw2] [ 86%] PASSED pamica/tests/test_mlx_sharing_cross_backend.py::test_merge_decision_matches_the_torch_backend +pamica/tests/test_mlx_sharing_cross_backend.py::test_gm_prev_weighting_matches_torch[False] +[gw4] [ 86%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_write_amica_output_refuses_nonfinite_params +pamica/tests/torch_tests/test_ng_backend.py::test_state_dict_snapshots_not_aliases +[gw4] [ 86%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_state_dict_snapshots_not_aliases +pamica/tests/torch_tests/test_ng_backend.py::test_end_to_end_correlation_vs_fortran +[gw2] [ 86%] PASSED pamica/tests/test_mlx_sharing_cross_backend.py::test_gm_prev_weighting_matches_torch[False] +pamica/tests/test_mlx_sharing_cross_backend.py::test_gm_prev_weighting_matches_torch[True] +[gw2] [ 86%] PASSED pamica/tests/test_mlx_sharing_cross_backend.py::test_gm_prev_weighting_matches_torch[True] +pamica/tests/test_mlx_sharing_cross_backend.py::test_comp_used_before_fit_raises_in_both_backends +[gw2] [ 86%] PASSED pamica/tests/test_mlx_sharing_cross_backend.py::test_comp_used_before_fit_raises_in_both_backends +pamica/tests/test_mlx_transform_cross_backend.py::test_transform_matches_float64_torch_twin +[gw5] [ 86%] PASSED pamica/tests/test_schedule_gates.py::test_rejection_first_fires_on_iteration_rejstart[torch] +pamica/tests/test_schedule_gates.py::test_rejection_first_fires_on_iteration_rejstart[numpy] +[gw2] [ 86%] PASSED pamica/tests/test_mlx_transform_cross_backend.py::test_transform_matches_float64_torch_twin +pamica/tests/test_mlx_transform_cross_backend.py::test_multimodel_transform_matches_float64_twin_with_c_centering +[gw1] [ 86%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_post_tune_fit_is_bit_identical_to_a_direct_fit +pamica/tests/torch_tests/test_ng_convergence.py::test_new_stop_reasons_are_not_degenerate +[gw1] [ 87%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_new_stop_reasons_are_not_degenerate +pamica/tests/torch_tests/test_ng_convergence.py::test_min_dll_stop_leaves_backend_usable +[gw2] [ 87%] PASSED pamica/tests/test_mlx_transform_cross_backend.py::test_multimodel_transform_matches_float64_twin_with_c_centering +pamica/tests/test_mlx_transform_cross_backend.py::test_accessors_match_float64_torch_twin +[gw2] [ 87%] PASSED pamica/tests/test_mlx_transform_cross_backend.py::test_accessors_match_float64_torch_twin +pamica/tests/test_mlx_transform_cross_backend.py::test_variance_order_matches_float64_twin +[gw2] [ 87%] PASSED pamica/tests/test_mlx_transform_cross_backend.py::test_variance_order_matches_float64_twin +pamica/tests/test_mlx_transform_cross_backend.py::test_variance_order_check_catches_a_divergence[reversed-outside the float32 band] +[gw2] [ 87%] PASSED pamica/tests/test_mlx_transform_cross_backend.py::test_variance_order_check_catches_a_divergence[reversed-outside the float32 band] +pamica/tests/test_mlx_transform_cross_backend.py::test_variance_order_check_catches_a_divergence[shuffled-outside the float32 band] +[gw2] [ 87%] PASSED pamica/tests/test_mlx_transform_cross_backend.py::test_variance_order_check_catches_a_divergence[shuffled-outside the float32 band] +pamica/tests/test_mlx_transform_cross_backend.py::test_variance_order_check_catches_a_divergence[closest_pair_swap-outside the float32 band] +[gw2] [ 87%] PASSED pamica/tests/test_mlx_transform_cross_backend.py::test_variance_order_check_catches_a_divergence[closest_pair_swap-outside the float32 band] +pamica/tests/test_mlx_transform_cross_backend.py::test_variance_order_check_catches_a_divergence[inflated_variance-float32 cap] +[gw2] [ 87%] PASSED pamica/tests/test_mlx_transform_cross_backend.py::test_variance_order_check_catches_a_divergence[inflated_variance-float32 cap] +pamica/tests/test_mlx_transform_cross_backend.py::test_sensor_mixing_matrix_matches_twin_on_rank_reduced_fit +[gw2] [ 87%] PASSED pamica/tests/test_mlx_transform_cross_backend.py::test_sensor_mixing_matrix_matches_twin_on_rank_reduced_fit +pamica/tests/test_mlx_transform_cross_backend.py::test_transform_matches_forward_activations_via_twin_composition +[gw2] [ 87%] PASSED pamica/tests/test_mlx_transform_cross_backend.py::test_transform_matches_forward_activations_via_twin_composition +pamica/tests/test_native_engine.py::test_platform_tag[Darwin-arm64-macos-arm64] +[gw2] [ 87%] PASSED pamica/tests/test_native_engine.py::test_platform_tag[Darwin-arm64-macos-arm64] +pamica/tests/test_native_engine.py::test_platform_tag[Linux-x86_64-linux-x64] +[gw2] [ 87%] PASSED pamica/tests/test_native_engine.py::test_platform_tag[Linux-x86_64-linux-x64] +pamica/tests/test_native_engine.py::test_platform_tag[Linux-aarch64-linux-arm64] +[gw2] [ 87%] PASSED pamica/tests/test_native_engine.py::test_platform_tag[Linux-aarch64-linux-arm64] +pamica/tests/test_native_engine.py::test_platform_tag[Windows-AMD64-windows-x64] +[gw2] [ 87%] PASSED pamica/tests/test_native_engine.py::test_platform_tag[Windows-AMD64-windows-x64] +pamica/tests/test_native_engine.py::test_platform_tag[Windows-ARM64-windows-arm64] +[gw2] [ 87%] PASSED pamica/tests/test_native_engine.py::test_platform_tag[Windows-ARM64-windows-arm64] +pamica/tests/torch_tests/test_ng_convergence.py::test_mir_history_empty_after_save_load +[gw5] [ 87%] PASSED pamica/tests/test_schedule_gates.py::test_rejection_first_fires_on_iteration_rejstart[numpy] +pamica/tests/torch_tests/test_ng_convergence.py::test_do_reject_interaction_grad_norm_stop_leaves_good_idx_usable +[gw2] [ 87%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_mir_history_empty_after_save_load +pamica/tests/torch_tests/test_ng_exception_consistency.py::test_inv_failure_leaves_the_instance_degenerate +[gw2] [ 87%] PASSED pamica/tests/torch_tests/test_ng_exception_consistency.py::test_inv_failure_leaves_the_instance_degenerate +pamica/tests/torch_tests/test_ng_exception_consistency.py::test_inv_failure_under_multi_restart_also_leaves_it_degenerate +[gw2] [ 87%] PASSED pamica/tests/torch_tests/test_ng_exception_consistency.py::test_inv_failure_under_multi_restart_also_leaves_it_degenerate +pamica/tests/torch_tests/test_ng_exception_consistency.py::test_pinv_sphere_nonfinite_raise_sets_degenerate_stop_reason +[gw2] [ 87%] PASSED pamica/tests/torch_tests/test_ng_exception_consistency.py::test_pinv_sphere_nonfinite_raise_sets_degenerate_stop_reason +pamica/tests/torch_tests/test_ng_float32_stability.py::test_score_is_zero_at_zero_activation +[gw2] [ 88%] PASSED pamica/tests/torch_tests/test_ng_float32_stability.py::test_score_is_zero_at_zero_activation +pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_stable_on_full_data[1-True] +[gw5] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_do_reject_interaction_grad_norm_stop_leaves_good_idx_usable +pamica/tests/torch_tests/test_ng_convergence.py::test_both_stops_disabled_never_produce_new_stop_reasons +[gw5] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_both_stops_disabled_never_produce_new_stop_reasons +pamica/tests/torch_tests/test_ng_convergence.py::test_disabled_stops_default_path_matches_max_iter_on_short_run +[gw1] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_min_dll_stop_leaves_backend_usable +pamica/tests/torch_tests/test_ng_convergence.py::test_grad_norm_floor_stop_leaves_wrapper_usable +[gw1] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_grad_norm_floor_stop_leaves_wrapper_usable +pamica/tests/torch_tests/test_ng_convergence.py::test_keep_best_restores_genuine_overshoot_under_min_dll_stop +[gw1] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_keep_best_restores_genuine_overshoot_under_min_dll_stop +pamica/tests/torch_tests/test_ng_convergence.py::test_do_reject_interaction_grad_norm_floor_stop_leaves_good_idx_usable +[gw5] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_disabled_stops_default_path_matches_max_iter_on_short_run +pamica/tests/torch_tests/test_ng_convergence.py::test_min_dll_stop_reachable_at_shipped_default_threshold +[gw7] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_min_dll_stop_matches_independent_reference +pamica/tests/torch_tests/test_ng_convergence.py::test_min_dll_never_fires_before_two_ll_values +[gw7] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_min_dll_never_fires_before_two_ll_values +pamica/tests/torch_tests/test_ng_convergence.py::test_grad_norm_stop_matches_independent_reference +[gw1] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_do_reject_interaction_grad_norm_floor_stop_leaves_good_idx_usable +pamica/tests/torch_tests/test_ng_convergence.py::test_do_reject_interaction_min_dll_stop_leaves_good_idx_usable +[gw7] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_grad_norm_stop_matches_independent_reference +pamica/tests/torch_tests/test_ng_convergence.py::test_grad_norm_never_fires_on_first_iteration +[gw7] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_grad_norm_never_fires_on_first_iteration +pamica/tests/torch_tests/test_ng_convergence.py::test_grad_norm_floor_fires_on_likelihood_decrease +[gw2] [ 88%] PASSED pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_stable_on_full_data[1-True] +pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_stable_on_full_data[2-False] +[gw7] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_grad_norm_floor_fires_on_likelihood_decrease +pamica/tests/torch_tests/test_ng_convergence.py::test_lrate_floor_still_reachable_without_grad_norm +[gw1] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_do_reject_interaction_min_dll_stop_leaves_good_idx_usable +pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_ll_matches_float64[False] +[gw7] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_lrate_floor_still_reachable_without_grad_norm +pamica/tests/torch_tests/test_ng_convergence.py::test_grad_norm_shadows_grad_norm_floor_under_shipped_defaults +[gw7] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_grad_norm_shadows_grad_norm_floor_under_shipped_defaults +pamica/tests/torch_tests/test_ng_convergence.py::test_a_frozen_window_still_computes_fresh_grad_norm +[gw7] [ 88%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_a_frozen_window_still_computes_fresh_grad_norm +pamica/tests/torch_tests/test_ng_metadata.py::test_pdftype_names_cover_all_codes +[gw7] [ 88%] PASSED pamica/tests/torch_tests/test_ng_metadata.py::test_pdftype_names_cover_all_codes +pamica/tests/torch_tests/test_ng_metadata.py::test_get_pdftype_default_is_generalized_gaussian +[gw7] [ 88%] PASSED pamica/tests/torch_tests/test_ng_metadata.py::test_get_pdftype_default_is_generalized_gaussian +pamica/tests/torch_tests/test_ng_metadata.py::test_get_pdftype_reflects_fixed_family +[gw7] [ 88%] PASSED pamica/tests/torch_tests/test_ng_metadata.py::test_get_pdftype_reflects_fixed_family +pamica/tests/torch_tests/test_ng_metadata.py::test_get_rho_shape_and_bounds +[gw7] [ 89%] PASSED pamica/tests/torch_tests/test_ng_metadata.py::test_get_rho_shape_and_bounds +pamica/tests/torch_tests/test_ng_metadata.py::test_shared_components_empty_without_sharing +[gw7] [ 89%] PASSED pamica/tests/torch_tests/test_ng_metadata.py::test_shared_components_empty_without_sharing +pamica/tests/torch_tests/test_ng_metadata.py::test_shared_components_reports_partial_merges +[gw2] [ 89%] PASSED pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_stable_on_full_data[2-False] +pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_stable_on_full_data[3-True] +[gw7] [ 89%] PASSED pamica/tests/torch_tests/test_ng_metadata.py::test_shared_components_reports_partial_merges +pamica/tests/torch_tests/test_ng_metadata.py::test_get_rho_differs_per_model +[gw7] [ 89%] PASSED pamica/tests/torch_tests/test_ng_metadata.py::test_get_rho_differs_per_model +pamica/tests/torch_tests/test_ng_metadata.py::test_get_pdftype_adaptive_switcher_is_mixed +[gw7] [ 89%] PASSED pamica/tests/torch_tests/test_ng_metadata.py::test_get_pdftype_adaptive_switcher_is_mixed +pamica/tests/torch_tests/test_ng_metadata.py::test_metadata_accessors_reject_bad_model_idx +[gw7] [ 89%] PASSED pamica/tests/torch_tests/test_ng_metadata.py::test_metadata_accessors_reject_bad_model_idx +pamica/tests/torch_tests/test_ng_metadata.py::test_metadata_accessors_require_fit +[gw7] [ 89%] PASSED pamica/tests/torch_tests/test_ng_metadata.py::test_metadata_accessors_require_fit +pamica/tests/torch_tests/test_ng_metadata.py::test_amica_wrapper_delegates_metadata +[gw5] [ 89%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_min_dll_stop_reachable_at_shipped_default_threshold +pamica/tests/torch_tests/test_ng_convergence.py::test_convergence_config_round_trips_through_state_dict +[gw5] [ 89%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_convergence_config_round_trips_through_state_dict +pamica/tests/torch_tests/test_ng_convergence.py::test_missing_convergence_keys_fall_back_to_fortran_defaults +[gw5] [ 89%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_missing_convergence_keys_fall_back_to_fortran_defaults +pamica/tests/torch_tests/test_ng_convergence.py::test_mir_history_survives_keep_best_restore +[gw7] [ 89%] PASSED pamica/tests/torch_tests/test_ng_metadata.py::test_amica_wrapper_delegates_metadata +pamica/tests/torch_tests/test_ng_model_posterior.py::test_model_loglik_matches_internal_lht +[gw5] [ 89%] PASSED pamica/tests/torch_tests/test_ng_convergence.py::test_mir_history_survives_keep_best_restore +pamica/tests/torch_tests/test_ng_model_posterior.py::test_amica_wrapper_delegates +[gw1] [ 89%] PASSED pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_ll_matches_float64[False] +pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_ll_matches_float64[True] +[gw0] [ 89%] PASSED pamica/tests/mlx_tests/test_mlx_backend.py::test_rholrate_ceiling_ratchet_gated_on_newt_start +pamica/tests/mlx_tests/test_mlx_backend.py::test_rholrate_ceiling_resets_at_fit_start +[gw0] [ 89%] PASSED pamica/tests/mlx_tests/test_mlx_backend.py::test_rholrate_ceiling_resets_at_fit_start +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_search_is_off_by_default_with_the_shared_bounds +[gw0] [ 89%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_search_is_off_by_default_with_the_shared_bounds +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_block_size_untouched_when_search_is_off +[gw0] [ 89%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_block_size_untouched_when_search_is_off +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_sweep_bounds_are_not_validated_when_search_is_off +[gw0] [ 89%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_sweep_bounds_are_not_validated_when_search_is_off +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs0-blk_min must be >= 1] +[gw0] [ 89%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs0-blk_min must be >= 1] +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs1-blk_step must be >= 1] +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs1-blk_step must be >= 1] +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs2-blk_max must be >= blk_min] +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_sweep_bounds_are_validated_when_search_is_on[kwargs2-blk_max must be >= blk_min] +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_chosen_block_size_is_a_valid_candidate_and_is_logged +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_chosen_block_size_is_a_valid_candidate_and_is_logged +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_candidates_never_exceed_the_sample_count +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_candidates_never_exceed_the_sample_count +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_memory_cap_uses_the_recommended_working_set +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_memory_cap_uses_the_recommended_working_set +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_memory_cap_falls_back_to_memory_size +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_memory_cap_falls_back_to_memory_size +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_memory_cap_is_none_when_mlx_reports_nothing +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_memory_cap_is_none_when_mlx_reports_nothing +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_tuner_runs_on_the_reduced_channel_count +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_tuner_runs_on_the_reduced_channel_count +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_post_tune_fit_is_bit_identical_to_a_direct_fit +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_post_tune_fit_is_bit_identical_to_a_direct_fit +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_probe_evaluates_the_whole_lazy_graph +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_probe_evaluates_the_whole_lazy_graph +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_metal_allocation_failure_is_a_catchable_exception +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_metal_allocation_failure_is_a_catchable_exception +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_allocation_failure_falls_back_to_the_last_working_size +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_allocation_failure_falls_back_to_the_last_working_size +pamica/tests/mlx_tests/test_mlx_blocktune.py::test_a_real_error_in_the_probe_is_not_swallowed +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_blocktune.py::test_a_real_error_in_the_probe_is_not_swallowed +pamica/tests/mlx_tests/test_mlx_exception_consistency.py::test_274_guard_raise_leaves_the_instance_degenerate +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_exception_consistency.py::test_274_guard_raise_leaves_the_instance_degenerate +pamica/tests/mlx_tests/test_mlx_exception_consistency.py::test_274_guard_raise_under_multi_restart_also_leaves_it_degenerate +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_exception_consistency.py::test_274_guard_raise_under_multi_restart_also_leaves_it_degenerate +pamica/tests/mlx_tests/test_mlx_exception_consistency.py::test_choose_pdfs_invariant_raise_sets_degenerate_stop_reason +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_exception_consistency.py::test_choose_pdfs_invariant_raise_sets_degenerate_stop_reason +pamica/tests/mlx_tests/test_mlx_exception_consistency.py::test_pinv_sphere_nonfinite_raise_sets_degenerate_stop_reason +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_exception_consistency.py::test_pinv_sphere_nonfinite_raise_sets_degenerate_stop_reason +pamica/tests/mlx_tests/test_mlx_export.py::test_write_amica_output_round_trips_through_loadmodout[1] +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_export.py::test_write_amica_output_round_trips_through_loadmodout[1] +pamica/tests/mlx_tests/test_mlx_export.py::test_write_amica_output_round_trips_through_loadmodout[2] +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_export.py::test_write_amica_output_round_trips_through_loadmodout[2] +pamica/tests/mlx_tests/test_mlx_export.py::test_share_comps_export_round_trips_through_loadmodout +[gw0] [ 90%] PASSED pamica/tests/mlx_tests/test_mlx_export.py::test_share_comps_export_round_trips_through_loadmodout +pamica/tests/mlx_tests/test_mlx_export.py::test_written_ll_ends_at_the_keep_best_restored_iterate +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_model_posterior.py::test_amica_wrapper_delegates +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_log_pdf_matches_fortran[2] +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_log_pdf_matches_fortran[2] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_log_pdf_matches_fortran[3] +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_log_pdf_matches_fortran[3] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_log_pdf_matches_fortran[4] +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_log_pdf_matches_fortran[4] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_log_pdf_matches_fortran[1] +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_log_pdf_matches_fortran[1] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_score_matches_fortran[2] +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_score_matches_fortran[2] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_score_matches_fortran[3] +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_score_matches_fortran[3] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_score_matches_fortran[4] +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_score_matches_fortran[4] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_score_matches_fortran[1] +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_score_matches_fortran[1] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_dpdf_is_minus_fp_times_pdf +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_dpdf_is_minus_fp_times_pdf +pamica/tests/torch_tests/test_ng_pdf_families.py::test_gg_path_bit_identical_none_vs_code0 +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_gg_path_bit_identical_none_vs_code0 +pamica/tests/torch_tests/test_ng_pdf_families.py::test_log_pdf_only_matches_log_pdf_and_deriv[None] +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_log_pdf_only_matches_log_pdf_and_deriv[None] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_log_pdf_only_matches_log_pdf_and_deriv[0] +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_log_pdf_only_matches_log_pdf_and_deriv[0] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_log_pdf_only_matches_log_pdf_and_deriv[1] +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_log_pdf_only_matches_log_pdf_and_deriv[1] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_log_pdf_only_matches_log_pdf_and_deriv[2] +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_log_pdf_only_matches_log_pdf_and_deriv[2] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_log_pdf_only_matches_log_pdf_and_deriv[3] +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_log_pdf_only_matches_log_pdf_and_deriv[3] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_log_pdf_only_matches_log_pdf_and_deriv[4] +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_log_pdf_only_matches_log_pdf_and_deriv[4] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_block_size_default_is_8192 +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_block_size_default_is_8192 +pamica/tests/torch_tests/test_ng_pdf_families.py::test_pdtype_h_is_none_only_for_gg +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_pdtype_h_is_none_only_for_gg +pamica/tests/torch_tests/test_ng_pdf_families.py::test_pdftype_validation +[gw5] [ 91%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_pdftype_validation +pamica/tests/torch_tests/test_ng_pdf_families.py::test_kurt_schedule_validation +[gw5] [ 92%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_kurt_schedule_validation +pamica/tests/torch_tests/test_ng_pdf_families.py::test_pdtype_from_kurtosis_decision +[gw5] [ 92%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_pdtype_from_kurtosis_decision +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_finite_and_monotone[0-3] +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_export.py::test_written_ll_ends_at_the_keep_best_restored_iterate +pamica/tests/mlx_tests/test_mlx_export.py::test_do_reject_zero_sentinel_is_written +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_export.py::test_do_reject_zero_sentinel_is_written +pamica/tests/mlx_tests/test_mlx_export.py::test_from_state_dict_model_warns_and_omits_llt +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_export.py::test_from_state_dict_model_warns_and_omits_llt +pamica/tests/mlx_tests/test_mlx_export.py::test_write_amica_output_requires_a_fitted_model +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_export.py::test_write_amica_output_requires_a_fitted_model +pamica/tests/mlx_tests/test_mlx_export.py::test_write_amica_output_refuses_a_degenerate_fit +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_export.py::test_write_amica_output_refuses_a_degenerate_fit +pamica/tests/mlx_tests/test_mlx_export.py::test_write_amica_output_refuses_force_set_nonfinite_param +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_export.py::test_write_amica_output_refuses_force_set_nonfinite_param +pamica/tests/mlx_tests/test_mlx_export.py::test_write_amica_output_makes_no_extra_forward_pass +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_export.py::test_write_amica_output_makes_no_extra_forward_pass +pamica/tests/mlx_tests/test_mlx_export.py::test_export_matches_a_torch_twin_to_float32_precision[1] +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_export.py::test_export_matches_a_torch_twin_to_float32_precision[1] +pamica/tests/mlx_tests/test_mlx_export.py::test_export_matches_a_torch_twin_to_float32_precision[2] +[gw7] [ 92%] PASSED pamica/tests/torch_tests/test_ng_model_posterior.py::test_model_loglik_matches_internal_lht +pamica/tests/torch_tests/test_ng_model_posterior.py::test_model_probability_is_normalized +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_export.py::test_export_matches_a_torch_twin_to_float32_precision[2] +pamica/tests/mlx_tests/test_mlx_fit_noop.py::test_unscaled_fit_is_bit_identical_to_the_pre_phase3_epic_tip[1] +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_fit_noop.py::test_unscaled_fit_is_bit_identical_to_the_pre_phase3_epic_tip[1] +pamica/tests/mlx_tests/test_mlx_fit_noop.py::test_unscaled_fit_is_bit_identical_to_the_pre_phase3_epic_tip[2] +[gw5] [ 92%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_finite_and_monotone[0-3] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_finite_and_monotone[2-3] +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_fit_noop.py::test_unscaled_fit_is_bit_identical_to_the_pre_phase3_epic_tip[2] +pamica/tests/mlx_tests/test_mlx_fit_noop.py::test_unscaled_fit_with_keep_best_off_is_also_bit_identical +[gw2] [ 92%] PASSED pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_stable_on_full_data[3-True] +pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_stable_on_full_data[4-False] +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_fit_noop.py::test_unscaled_fit_with_keep_best_off_is_also_bit_identical +pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_duplicated_column_raises_runtime_error_not_abort +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_duplicated_column_raises_runtime_error_not_abort +pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_duplicated_column_error_names_iteration_and_condition +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_duplicated_column_error_names_iteration_and_condition +pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_near_singular_column_raises +[gw0] [ 92%] PASSED pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_near_singular_column_raises +pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_killer_combination_duplicate_and_nan_raises_not_abort +[gw0] [ 93%] PASSED pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_killer_combination_duplicate_and_nan_raises_not_abort +pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_purely_nonfinite_a_flows_to_nan_params_not_abort +[gw0] [ 93%] PASSED pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_purely_nonfinite_a_flows_to_nan_params_not_abort +pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_second_model_singular_names_model_one +[gw0] [ 93%] PASSED pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_second_model_singular_names_model_one +pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_guard_is_bit_identical_on_healthy_state +[gw0] [ 93%] PASSED pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_guard_is_bit_identical_on_healthy_state +pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_healthy_full_fit_completes_unaffected +[gw0] [ 93%] PASSED pamica/tests/mlx_tests/test_mlx_inv_guard.py::test_healthy_full_fit_completes_unaffected +pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_defaults_to_true +[gw0] [ 93%] PASSED pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_defaults_to_true +pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_off_is_accepted +[gw0] [ 93%] PASSED pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_off_is_accepted +pamica/tests/mlx_tests/test_mlx_keepbest.py::test_monotone_default_fit_is_bit_identical_with_keep_best_on_or_off +[gw7] [ 93%] PASSED pamica/tests/torch_tests/test_ng_model_posterior.py::test_model_probability_is_normalized +pamica/tests/torch_tests/test_ng_model_posterior.py::test_single_model_probability_is_all_ones +[gw0] [ 93%] PASSED pamica/tests/mlx_tests/test_mlx_keepbest.py::test_monotone_default_fit_is_bit_identical_with_keep_best_on_or_off +pamica/tests/mlx_tests/test_mlx_keepbest.py::test_snapshot_is_a_copy_not_an_alias +[gw0] [ 93%] PASSED pamica/tests/mlx_tests/test_mlx_keepbest.py::test_snapshot_is_a_copy_not_an_alias +pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_restores_a_genuine_overshoot +[gw0] [ 93%] PASSED pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_restores_a_genuine_overshoot +pamica/tests/mlx_tests/test_mlx_keepbest.py::test_forced_restore_model_loglik_matches_state_dict_round_trip +[gw7] [ 93%] PASSED pamica/tests/torch_tests/test_ng_model_posterior.py::test_single_model_probability_is_all_ones +pamica/tests/torch_tests/test_ng_model_posterior.py::test_model_loglik_uses_stored_sphere_not_reprocess +[gw0] [ 93%] PASSED pamica/tests/mlx_tests/test_mlx_keepbest.py::test_forced_restore_model_loglik_matches_state_dict_round_trip +pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_restore_does_not_rewrite_ll_history +[gw5] [ 93%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_finite_and_monotone[2-3] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_finite_and_monotone[3-3] +[gw0] [ 93%] PASSED pamica/tests/mlx_tests/test_mlx_keepbest.py::test_keep_best_restore_does_not_rewrite_ll_history +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_finite_and_monotone[4-1] +[gw7] [ 93%] PASSED pamica/tests/torch_tests/test_ng_model_posterior.py::test_model_loglik_uses_stored_sphere_not_reprocess +pamica/tests/torch_tests/test_ng_model_posterior.py::test_model_loglik_requires_fit +[gw7] [ 93%] PASSED pamica/tests/torch_tests/test_ng_model_posterior.py::test_model_loglik_requires_fit +pamica/tests/torch_tests/test_ng_model_posterior.py::test_model_loglik_rejects_non_finite_input +[gw0] [ 93%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_finite_and_monotone[4-1] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_finite_and_monotone[1-1] +[gw7] [ 93%] PASSED pamica/tests/torch_tests/test_ng_model_posterior.py::test_model_loglik_rejects_non_finite_input +pamica/tests/torch_tests/test_ng_pdf_families.py::test_adaptive_switch_with_rejection +[gw5] [ 93%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_finite_and_monotone[3-3] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_state_dict_roundtrips_pdftype_state +[gw0] [ 94%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_finite_and_monotone[1-1] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_auto_switcher_runs_and_is_stable +[gw7] [ 94%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_adaptive_switch_with_rejection +pamica/tests/torch_tests/test_ng_pdf_families.py::test_multimodel_fixed_family +[gw5] [ 94%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_state_dict_roundtrips_pdftype_state +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_with_newton[2-3] +[gw0] [ 94%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_auto_switcher_runs_and_is_stable +pamica/tests/torch_tests/test_ng_pdf_families.py::test_auto_switch_noop_when_num_kurt_zero +[gw0] [ 94%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_auto_switch_noop_when_num_kurt_zero +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_converged_ll_matches_fortran[2-3] +[gw0] [ 94%] SKIPPED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_converged_ll_matches_fortran[2-3] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_converged_ll_matches_fortran[3-3] +[gw0] [ 94%] SKIPPED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_converged_ll_matches_fortran[3-3] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_converged_ll_matches_fortran[4-1] +[gw0] [ 94%] SKIPPED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_converged_ll_matches_fortran[4-1] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_converged_ll_matches_fortran[1-1] +[gw0] [ 94%] SKIPPED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_converged_ll_matches_fortran[1-1] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_sharing_newton_and_fixed_family_fit_completes +[gw7] [ 94%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_multimodel_fixed_family +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_converged_ll_matches_fortran[0-3] +[gw7] [ 94%] SKIPPED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_converged_ll_matches_fortran[0-3] +pamica/tests/torch_tests/test_ng_rank_deficient.py::test_full_rank_model_dimension_unchanged +[gw7] [ 94%] PASSED pamica/tests/torch_tests/test_ng_rank_deficient.py::test_full_rank_model_dimension_unchanged +pamica/tests/torch_tests/test_ng_rank_deficient.py::test_rank_deficient_fits_instead_of_going_degenerate +[gw5] [ 94%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_with_newton[2-3] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_with_newton[3-3] +[gw7] [ 94%] PASSED pamica/tests/torch_tests/test_ng_rank_deficient.py::test_rank_deficient_fits_instead_of_going_degenerate +pamica/tests/torch_tests/test_ng_rank_deficient.py::test_default_relative_threshold_recovers_the_exact_rank +[gw7] [ 94%] PASSED pamica/tests/torch_tests/test_ng_rank_deficient.py::test_default_relative_threshold_recovers_the_exact_rank +pamica/tests/torch_tests/test_ng_rank_deficient.py::test_fortran_absolute_floor_over_retains +[gw2] [ 94%] PASSED pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_stable_on_full_data[4-False] +pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_stable_on_full_data[5-True] +[gw7] [ 94%] PASSED pamica/tests/torch_tests/test_ng_rank_deficient.py::test_fortran_absolute_floor_over_retains +pamica/tests/torch_tests/test_ng_rank_deficient.py::test_sensor_mixing_matrix_reconstructs_the_input +[gw5] [ 94%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_with_newton[3-3] +pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_with_newton[4-1] +[gw7] [ 94%] PASSED pamica/tests/torch_tests/test_ng_rank_deficient.py::test_sensor_mixing_matrix_reconstructs_the_input +pamica/tests/torch_tests/test_ng_rank_deficient.py::test_pcakeep_with_pca_whitening_no_longer_crashes +[gw7] [ 94%] PASSED pamica/tests/torch_tests/test_ng_rank_deficient.py::test_pcakeep_with_pca_whitening_no_longer_crashes +pamica/tests/torch_tests/test_ng_restarts.py::test_single_restart_is_bit_identical_to_a_direct_fit +[gw7] [ 94%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_single_restart_is_bit_identical_to_a_direct_fit +pamica/tests/torch_tests/test_ng_restarts.py::test_single_restart_records_the_one_fit_it_ran +[gw7] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_single_restart_records_the_one_fit_it_ran +pamica/tests/torch_tests/test_ng_restarts.py::test_an_explicit_single_seed_overrides_the_constructor_seed +[gw5] [ 95%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_family_fit_with_newton[4-1] +pamica/tests/torch_tests/test_ng_rank_deficient.py::test_rank_reduced_newton_fit_completes +[gw7] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_an_explicit_single_seed_overrides_the_constructor_seed +pamica/tests/torch_tests/test_ng_restarts.py::test_best_of_n_returns_the_argmax_of_the_independent_fits +[gw7] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_best_of_n_returns_the_argmax_of_the_independent_fits +pamica/tests/torch_tests/test_ng_restarts.py::test_records_are_aligned_with_the_independent_fits +[gw7] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_records_are_aligned_with_the_independent_fits +pamica/tests/torch_tests/test_ng_restarts.py::test_derived_seeds_run_the_documented_sequence +[gw7] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_derived_seeds_run_the_documented_sequence +pamica/tests/torch_tests/test_ng_restarts.py::test_a_refit_repeats_the_same_search +[gw5] [ 95%] PASSED pamica/tests/torch_tests/test_ng_rank_deficient.py::test_rank_reduced_newton_fit_completes +pamica/tests/torch_tests/test_ng_rank_deficient.py::test_nan_data_still_reaches_the_degenerate_fit_contract +[gw5] [ 95%] PASSED pamica/tests/torch_tests/test_ng_rank_deficient.py::test_nan_data_still_reaches_the_degenerate_fit_contract +pamica/tests/torch_tests/test_ng_restarts.py::test_a_crashing_restart_does_not_kill_the_search +[gw1] [ 95%] PASSED pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_ll_matches_float64[True] +pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_mps_smoke +[gw7] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_a_refit_repeats_the_same_search +pamica/tests/torch_tests/test_ng_restarts.py::test_a_degenerate_restart_is_recorded_but_never_selected +[gw5] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_a_crashing_restart_does_not_kill_the_search +pamica/tests/torch_tests/test_ng_restarts.py::test_a_single_restart_still_raises +[gw5] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_a_single_restart_still_raises +pamica/tests/torch_tests/test_ng_restarts.py::test_all_degenerate_restarts_keep_the_degenerate_contract +[gw5] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_all_degenerate_restarts_keep_the_degenerate_contract +pamica/tests/torch_tests/test_ng_restarts.py::test_the_winners_tuned_block_size_is_restored +[gw7] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_a_degenerate_restart_is_recorded_but_never_selected +pamica/tests/torch_tests/test_ng_restarts.py::test_all_restarts_crashing_keeps_the_degenerate_contract +[gw7] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_all_restarts_crashing_keeps_the_degenerate_contract +pamica/tests/torch_tests/test_ng_restarts.py::test_a_payload_without_restart_records_still_loads +[gw7] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_a_payload_without_restart_records_still_loads +pamica/tests/torch_tests/test_ng_restarts.py::test_wrapper_forwards_n_restarts_and_mirrors_the_records +[gw5] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_the_winners_tuned_block_size_is_restored +pamica/tests/torch_tests/test_ng_restarts.py::test_state_dict_round_trips_the_configuration_and_the_records +[gw7] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_wrapper_forwards_n_restarts_and_mirrors_the_records +pamica/tests/torch_tests/test_ng_restarts.py::test_wrapper_records_the_single_restart_of_a_default_fit +[gw5] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_state_dict_round_trips_the_configuration_and_the_records +pamica/tests/torch_tests/test_ng_restarts.py::test_wrapper_refuses_a_best_of_n_fit_whose_restarts_all_degenerate +[gw7] [ 95%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_wrapper_records_the_single_restart_of_a_default_fit +pamica/tests/torch_tests/test_ng_sharing.py::test_single_model_byte_identical_with_share_toggled +[gw5] [ 96%] PASSED pamica/tests/torch_tests/test_ng_restarts.py::test_wrapper_refuses_a_best_of_n_fit_whose_restarts_all_degenerate +pamica/tests/torch_tests/test_ng_sharing.py::test_default_multimodel_leaves_comps_unshared +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_mps_smoke +pamica/tests/torch_tests/test_ng_gm_ordering.py::test_ndtmpsum_uses_pre_update_model_weights +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_gm_ordering.py::test_ndtmpsum_uses_pre_update_model_weights +pamica/tests/torch_tests/test_ng_gm_ordering.py::test_a_update_uses_pre_update_model_weights +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_gm_ordering.py::test_a_update_uses_pre_update_model_weights +pamica/tests/torch_tests/test_ng_gm_ordering.py::test_same_pre_update_gm_is_deterministic +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_gm_ordering.py::test_same_pre_update_gm_is_deterministic +pamica/tests/torch_tests/test_ng_gm_ordering.py::test_single_model_is_unaffected +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_gm_ordering.py::test_single_model_is_unaffected +pamica/tests/torch_tests/test_ng_sharing.py::test_three_model_guard_and_merge +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_three_model_guard_and_merge +pamica/tests/torch_tests/test_ng_sharing.py::test_comp_thresh_one_merges_only_exact_duplicates +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_comp_thresh_one_merges_only_exact_duplicates +pamica/tests/torch_tests/test_ng_sharing.py::test_a_frozen_window[True-2] +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_a_frozen_window[True-2] +pamica/tests/torch_tests/test_ng_sharing.py::test_a_frozen_window[False-2] +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_a_frozen_window[False-2] +pamica/tests/torch_tests/test_ng_sharing.py::test_a_frozen_window[False-1] +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_a_frozen_window[False-1] +pamica/tests/torch_tests/test_ng_sharing.py::test_a_frozen_window_starting_inside_a_cycle +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_a_frozen_window_starting_inside_a_cycle +pamica/tests/torch_tests/test_ng_sharing.py::test_share_constructor_validation[kwargs0-share_start] +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_share_constructor_validation[kwargs0-share_start] +pamica/tests/torch_tests/test_ng_sharing.py::test_share_constructor_validation[kwargs1-share_iter] +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_share_constructor_validation[kwargs1-share_iter] +pamica/tests/torch_tests/test_ng_sharing.py::test_share_constructor_validation[kwargs2-share_iter] +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_share_constructor_validation[kwargs2-share_iter] +pamica/tests/torch_tests/test_ng_sharing.py::test_share_constructor_validation[kwargs3-comp_thresh] +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_share_constructor_validation[kwargs3-comp_thresh] +pamica/tests/torch_tests/test_ng_sharing.py::test_share_constructor_validation[kwargs4-comp_thresh] +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_share_constructor_validation[kwargs4-comp_thresh] +pamica/tests/torch_tests/test_ng_sharing.py::test_pca_reduction_no_longer_rejected_at_construction +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_pca_reduction_no_longer_rejected_at_construction +pamica/tests/torch_tests/test_ng_sharing.py::test_non_finite_sphere_still_fails_loudly +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_non_finite_sphere_still_fails_loudly +pamica/tests/torch_tests/test_ng_sharing.py::test_get_sensor_mixing_matrix_non_finite_sphere_fails_loudly +[gw1] [ 96%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_get_sensor_mixing_matrix_non_finite_sphere_fails_loudly +pamica/tests/torch_tests/test_ng_sharing.py::test_two_model_share_fit_survives_merge +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_single_model_byte_identical_with_share_toggled +pamica/tests/torch_tests/test_ng_sharing.py::test_identify_merges_one_collinear_pair +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_identify_merges_one_collinear_pair +pamica/tests/torch_tests/test_ng_sharing.py::test_merge_on_the_final_iteration_completes +[gw5] [ 97%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_default_multimodel_leaves_comps_unshared +pamica/tests/torch_tests/test_ng_sharing.py::test_guard_prevents_within_model_collapse +[gw5] [ 97%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_guard_prevents_within_model_collapse +pamica/tests/torch_tests/test_ng_sharing.py::test_low_rank_projected_data_share_fit_completes +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_merge_on_the_final_iteration_completes +pamica/tests/torch_tests/test_ng_sharing.py::test_rank_reduced_share_fit_completes +[gw0] [ 97%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_sharing_newton_and_fixed_family_fit_completes +pamica/tests/torch_tests/test_ng_pdf_families.py::test_sharing_newton_and_adaptive_switcher_fit_completes +[gw5] [ 97%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_low_rank_projected_data_share_fit_completes +pamica/tests/torch_tests/test_ng_sharing.py::test_full_rank_merge_decisions_unchanged_by_pinv +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_rank_reduced_share_fit_completes +pamica/tests/torch_tests/test_ng_sharing.py::test_pinv_matches_inv_on_a_fitted_full_rank_sphere +[gw1] [ 97%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_two_model_share_fit_survives_merge +pamica/tests/torch_tests/test_ng_sharing.py::test_sharing_reduces_unique_count_without_degrading_ll +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_pinv_matches_inv_on_a_fitted_full_rank_sphere +pamica/tests/torch_tests/test_ng_utils.py::test_setup_device_auto_returns_valid_device +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_utils.py::test_setup_device_auto_returns_valid_device +pamica/tests/torch_tests/test_ng_utils.py::test_setup_device_mps_falls_back_when_unavailable +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_utils.py::test_setup_device_mps_falls_back_when_unavailable +pamica/tests/torch_tests/test_ng_utils.py::test_check_numerical_stability_clean +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_utils.py::test_check_numerical_stability_clean +pamica/tests/torch_tests/test_ng_utils.py::test_check_numerical_stability_flags_nan +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_utils.py::test_check_numerical_stability_flags_nan +pamica/tests/torch_tests/test_ng_utils.py::test_check_numerical_stability_flags_inf +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_utils.py::test_check_numerical_stability_flags_inf +pamica/tests/torch_tests/test_ng_utils.py::test_check_numerical_stability_raises_when_requested +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_utils.py::test_check_numerical_stability_raises_when_requested +pamica/tests/torch_tests/test_ng_utils.py::test_stabilize_log_probabilities_clamps_below_floor +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_utils.py::test_stabilize_log_probabilities_clamps_below_floor +pamica/tests/torch_tests/test_ng_utils.py::test_stabilize_log_probabilities_custom_floor +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_utils.py::test_stabilize_log_probabilities_custom_floor +pamica/tests/torch_tests/test_ng_utils.py::test_compute_correlation_matrix_identity_on_self +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_utils.py::test_compute_correlation_matrix_identity_on_self +pamica/tests/torch_tests/test_ng_utils.py::test_find_best_permutation_recovers_shuffle_and_sign +[gw7] [ 97%] PASSED pamica/tests/torch_tests/test_ng_utils.py::test_find_best_permutation_recovers_shuffle_and_sign +pamica/tests/torch_tests/test_ng_utils.py::test_load_eeglab_data_shape +[gw7] [ 98%] PASSED pamica/tests/torch_tests/test_ng_utils.py::test_load_eeglab_data_shape +[gw0] [ 98%] PASSED pamica/tests/torch_tests/test_ng_pdf_families.py::test_sharing_newton_and_adaptive_switcher_fit_completes +pamica/tests/torch_tests/test_ng_sharing.py::test_share_config_and_comp_list_roundtrip +[gw5] [ 98%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_full_rank_merge_decisions_unchanged_by_pinv +pamica/tests/torch_tests/test_ng_utils.py::test_setup_device_cpu_explicit +[gw5] [ 98%] PASSED pamica/tests/torch_tests/test_ng_utils.py::test_setup_device_cpu_explicit +[gw2] [ 98%] PASSED pamica/tests/torch_tests/test_ng_float32_stability.py::test_float32_stable_on_full_data[5-True] +pamica/tests/torch_tests/test_ng_rank_deficient.py::test_tesla_scale_data_fits_by_default +[gw2] [ 98%] PASSED pamica/tests/torch_tests/test_ng_rank_deficient.py::test_tesla_scale_data_fits_by_default +pamica/tests/torch_tests/test_ng_rank_deficient.py::test_absolute_floor_rejects_tesla_scale_data +[gw2] [ 98%] PASSED pamica/tests/torch_tests/test_ng_rank_deficient.py::test_absolute_floor_rejects_tesla_scale_data +[gw0] [ 98%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_share_config_and_comp_list_roundtrip +[gw1] [ 98%] PASSED pamica/tests/torch_tests/test_ng_sharing.py::test_sharing_reduces_unique_count_without_degrading_ll +pamica/tests/torch_tests/test_ng_utils.py::test_setup_device_cuda_falls_back_when_unavailable +[gw1] [ 98%] PASSED pamica/tests/torch_tests/test_ng_utils.py::test_setup_device_cuda_falls_back_when_unavailable +[gw4] [ 98%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_end_to_end_correlation_vs_fortran +pamica/tests/torch_tests/test_ng_backend.py::test_end_to_end_correlation_vs_fortran_from_sample_params_json +[gw3] [ 98%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_keep_best_single_model_is_bit_exact +pamica/tests/torch_tests/test_ng_backend.py::test_keep_best_snapshot_restore_roundtrip +[gw3] [ 98%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_keep_best_snapshot_restore_roundtrip +pamica/tests/torch_tests/test_ng_backend.py::test_keep_best_returns_within_tol_of_peak +[gw3] [ 98%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_keep_best_returns_within_tol_of_peak +pamica/tests/torch_tests/test_ng_backend.py::test_keep_best_inactive_under_reject +[gw3] [ 98%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_keep_best_inactive_under_reject +pamica/tests/torch_tests/test_ng_backend.py::test_keep_best_does_not_rescue_a_diverged_fit_that_peaked_earlier +[gw3] [ 98%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_keep_best_does_not_rescue_a_diverged_fit_that_peaked_earlier +pamica/tests/torch_tests/test_ng_backend.py::test_rholrate_ratchets_at_maxdecs_not_per_decrease +[gw3] [ 98%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_rholrate_ratchets_at_maxdecs_not_per_decrease +pamica/tests/torch_tests/test_ng_block_tuning.py::test_search_is_off_by_default +[gw3] [ 98%] PASSED pamica/tests/torch_tests/test_ng_block_tuning.py::test_search_is_off_by_default +[gw4] [ 98%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_end_to_end_correlation_vs_fortran_from_sample_params_json +pamica/tests/torch_tests/test_ng_backend.py::test_full_fit_parity_numpy_vs_ng +[gw4] [ 98%] PASSED pamica/tests/torch_tests/test_ng_backend.py::test_full_fit_parity_numpy_vs_ng +[gw6] [ 99%] XFAIL pamica/tests/test_sample_data.py::test_sample_data_scikit +pamica/tests/test_sample_data.py::test_sample_data_cli +[gw6] [ 99%] XFAIL pamica/tests/test_sample_data.py::test_sample_data_cli +pamica/tests/test_sample_data.py::test_sample_data_light +[gw6] [ 99%] XFAIL pamica/tests/test_sample_data.py::test_sample_data_light +pamica/tests/test_sample_data.py::test_sample_data_numpy_vs_fortran +[gw6] [ 99%] PASSED pamica/tests/test_sample_data.py::test_sample_data_numpy_vs_fortran +pamica/tests/test_sample_data.py::test_cli_output_format_roundtrip +[gw6] [ 99%] PASSED pamica/tests/test_sample_data.py::test_cli_output_format_roundtrip +pamica/tests/test_sample_data.py::test_numpy_llt_roundtrip +[gw6] [ 99%] PASSED pamica/tests/test_sample_data.py::test_numpy_llt_roundtrip +pamica/tests/test_sample_data.py::test_numpy_llt_reject_zeroes_rejected_samples +[gw6] [ 99%] PASSED pamica/tests/test_sample_data.py::test_numpy_llt_reject_zeroes_rejected_samples +pamica/tests/test_sample_data.py::test_numpy_llt_multimodel_roundtrip +[gw6] [ 99%] PASSED pamica/tests/test_sample_data.py::test_numpy_llt_multimodel_roundtrip +pamica/tests/test_sample_data.py::test_numpy_llt_partial_final_block +[gw6] [ 99%] PASSED pamica/tests/test_sample_data.py::test_numpy_llt_partial_final_block +pamica/tests/test_sample_data.py::test_restart_on_early_nan_recovers +[gw6] [ 99%] PASSED pamica/tests/test_sample_data.py::test_restart_on_early_nan_recovers +pamica/tests/test_sample_data.py::test_restart_gives_up_after_maxrestarts +[gw6] [ 99%] PASSED pamica/tests/test_sample_data.py::test_restart_gives_up_after_maxrestarts +pamica/tests/test_sample_data.py::test_check_convergence_ratchets_lrate_on_decrease +[gw6] [ 99%] PASSED pamica/tests/test_sample_data.py::test_check_convergence_ratchets_lrate_on_decrease +pamica/tests/test_sample_data.py::test_reinitialize_for_restart_resets_rho_ceiling +[gw6] [ 99%] PASSED pamica/tests/test_sample_data.py::test_reinitialize_for_restart_resets_rho_ceiling +pamica/tests/test_sample_data.py::test_cli_subprocess_output_loadable +[gw6] [ 99%] PASSED pamica/tests/test_sample_data.py::test_cli_subprocess_output_loadable +pamica/tests/test_schedule_gates.py::test_every_gate_fires_on_the_reference_iteration +[gw6] [ 99%] PASSED pamica/tests/test_schedule_gates.py::test_every_gate_fires_on_the_reference_iteration +pamica/tests/test_schedule_gates.py::test_a_non_positive_interval_is_a_value_error_not_a_zero_division[0] +[gw6] [ 99%] PASSED pamica/tests/test_schedule_gates.py::test_a_non_positive_interval_is_a_value_error_not_a_zero_division[0] +pamica/tests/test_schedule_gates.py::test_a_non_positive_interval_is_a_value_error_not_a_zero_division[-2] +[gw6] [ 99%] PASSED pamica/tests/test_schedule_gates.py::test_a_non_positive_interval_is_a_value_error_not_a_zero_division[-2] +pamica/tests/test_schedule_gates.py::test_newton_takes_its_first_step_on_iteration_newt_start[torch-1] +[gw6] [ 99%] PASSED pamica/tests/test_schedule_gates.py::test_newton_takes_its_first_step_on_iteration_newt_start[torch-1] +pamica/tests/test_schedule_gates.py::test_newton_takes_its_first_step_on_iteration_newt_start[torch-5] +[gw6] [ 99%] PASSED pamica/tests/test_schedule_gates.py::test_newton_takes_its_first_step_on_iteration_newt_start[torch-5] +pamica/tests/test_schedule_gates.py::test_newton_takes_its_first_step_on_iteration_newt_start[numpy-1] +[gw6] [ 99%] PASSED pamica/tests/test_schedule_gates.py::test_newton_takes_its_first_step_on_iteration_newt_start[numpy-1] +pamica/tests/test_schedule_gates.py::test_newton_takes_its_first_step_on_iteration_newt_start[numpy-5] +[gw6] [100%] PASSED pamica/tests/test_schedule_gates.py::test_newton_takes_its_first_step_on_iteration_newt_start[numpy-5] + +=============================== warnings summary =============================== +pamica/tests/test_sample_data.py::test_loadmodout_reproduces_fortran_reported_ll + /Users/yahya/Documents/git/mlx-fc-phase15-final-parity/.venv/lib/python3.12/site-packages/numpy/linalg/_linalg.py:2349: RuntimeWarning: divide by zero encountered in slogdet + sign, logdet = _umath_linalg.slogdet(a, signature=signature) + +pamica/tests/test_sample_data.py::test_loadmodout_reproduces_fortran_reported_ll + /Users/yahya/Documents/git/mlx-fc-phase15-final-parity/.venv/lib/python3.12/site-packages/numpy/linalg/_linalg.py:2349: RuntimeWarning: overflow encountered in slogdet + sign, logdet = _umath_linalg.slogdet(a, signature=signature) + +pamica/tests/test_sample_data.py::test_loadmodout_reproduces_fortran_reported_ll + /Users/yahya/Documents/git/mlx-fc-phase15-final-parity/.venv/lib/python3.12/site-packages/numpy/linalg/_linalg.py:2349: RuntimeWarning: invalid value encountered in slogdet + sign, logdet = _umath_linalg.slogdet(a, signature=signature) + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +===== 1974 passed, 40 skipped, 3 xfailed, 3 warnings in 1417.81s (0:23:37) ===== +EXIT=0 diff --git a/.context/issue-351/raw/seedcheck_issue145_binary/run1.log b/.context/issue-351/raw/seedcheck_issue145_binary/run1.log new file mode 100644 index 00000000..0fef42b0 --- /dev/null +++ b/.context/issue-351/raw/seedcheck_issue145_binary/run1.log @@ -0,0 +1,127 @@ +Authorization required, but no authorization protocol specified + +Authorization required, but no authorization protocol specified + +Authorization required, but no authorization protocol specified + +Authorization required, but no authorization protocol specified + + 1 processor name = hallucinating + 1 host_num = 302850659 + This is MPI process 1 of 1 ; I am process 1 of 1 on node: hallucinating + 1 : node root process 1 of 1 +Processing arguments ... + num_files = 1 + FILES: + ./eeglab_data.fdt + num_dir_files = 1 + initial matrix block_size = 512 + do_opt_block = 0 + blk_min = 256 + blk_step = 256 + blk_max = 1024 + number of models = 1 + max_thrds = 1 + use_min_dll = 1 + min dll = 1.0000000000000001E-009 + use_grad_norm = 1 + min grad norm = 9.9999999999999995E-008 + number of density mixture components = 3 + pdf type = 0 + max_iter = 1 + num_samples = 1 + data_dim = 32 + field_dim = 30504 + do_history = 0 + histstep = 10 + share_comps = 0 + share_start = 100 + comp_thresh = 0.98999999999999999 + share_int = 100 + initial lrate = 5.0000000000000003E-002 + minimum lrate = 1.0000000000000000E-008 + minimum data covariance eigenvalue = 9.9999999999999998E-013 + lrate factor = 0.50000000000000000 + initial rholrate = 5.0000000000000003E-002 + rho0 = 1.5000000000000000 + min rho = 1.0000000000000000 + max rho = 2.0000000000000000 + rho lrate factor = 0.50000000000000000 + kurt_start = 3 + num kurt = 5 + kurt interval = 1 + do_newton = 1 + newt_start = 50 + newt_ramp = 10 + initial newton lrate = 1.0000000000000000 + do_reject = 0 + num reject = 3 + reject sigma = 3.0000000000000000 + reject start = 2 + reject interval = 3 + write step = 100 + write_nd = 0 + write_LLt = 1 + dec window = 1 + max_decs = 3 + fix_init = 0 + update_A = 1 + update_c = 1 + update_gm = 1 + update_alpha = 1 + update_mu = 1 + update_beta = 1 + invsigmax = 100.00000000000000 + invsigmin = 0.0000000000000000 + do_rho = 1 + load_rej = 0 + load_c = 0 + load_gm = 0 + load_alpha = 0 + load_mu = 0 + load_beta = 0 + load_rho = 0 + load_comp_list = 0 + do_mean = 1 + do_sphere = 1 + pcakeep = 32 + pcadb = 30.000000000000000 + byte_size = 4 + doscaling = 1 + scalestep = 1 +mkdir: cannot create directory ‘./out/’: File exists + output directory = ./out/ + 1 : setting num_thrds to 1 ... + 1 : using 1 threads. + 1 : node_thrds = 1 + bytes in real = 4 + 1 : REAL nbyte = 4 + getting segment list ... + blocks in sample = 30504 + total blocks = 30504 + node blocks = 30504 + node 1 start: file 1 sample 1 index 1 + node 1 stop : file 1 sample 1 index 30504 + 1 : data = -35.797485351562500 2.3078439235687256 + getting the mean ... + mean = -3.7090141912586323 -6.7516788952437894 2.5880450259870105 + subtracting the mean ... + getting the covariance matrix ... + cnt = 30504 + doing eig nx = 32 lwork = 10240 + minimum eigenvalues = 4.8799005132501803 6.9201197127079803 7.6562147928880702 + maximum eigenvalues = 9711.1430838537090 3039.6850435125002 1244.4129447052057 + num eigs kept = 32 + getting the sphering matrix ... + minimum eigenvalues = 4.8799005132501803 6.9201197127079803 7.6562147928880702 + maximum eigenvalues = 9711.1430838537090 3039.6850435125002 1244.4129447052057 + num eigs kept = 32 + sphering the data ... + numeigs = 32 + 1 : Allocating variables ... + 1 : Initializing variables ... + 1 : block size = 512 + 1 : entering the main loop ... + iter 1 lrate = 0.0500000000 LL = -3.5114441940 nd = 0.0556873701, D = 0.42075E-02 0.42075E-02 ( 0.10 s, 0.0 h) +... done. Execution time: 0.00 h + output directory = ./out/ diff --git a/.context/issue-351/raw/seedcheck_issue145_binary/run2.log b/.context/issue-351/raw/seedcheck_issue145_binary/run2.log new file mode 100644 index 00000000..2bb904bd --- /dev/null +++ b/.context/issue-351/raw/seedcheck_issue145_binary/run2.log @@ -0,0 +1,127 @@ +Authorization required, but no authorization protocol specified + +Authorization required, but no authorization protocol specified + +Authorization required, but no authorization protocol specified + +Authorization required, but no authorization protocol specified + + 1 processor name = hallucinating + 1 host_num = 302850659 + This is MPI process 1 of 1 ; I am process 1 of 1 on node: hallucinating + 1 : node root process 1 of 1 +Processing arguments ... + num_files = 1 + FILES: + ./eeglab_data.fdt + num_dir_files = 1 + initial matrix block_size = 512 + do_opt_block = 0 + blk_min = 256 + blk_step = 256 + blk_max = 1024 + number of models = 1 + max_thrds = 1 + use_min_dll = 1 + min dll = 1.0000000000000001E-009 + use_grad_norm = 1 + min grad norm = 9.9999999999999995E-008 + number of density mixture components = 3 + pdf type = 0 + max_iter = 1 + num_samples = 1 + data_dim = 32 + field_dim = 30504 + do_history = 0 + histstep = 10 + share_comps = 0 + share_start = 100 + comp_thresh = 0.98999999999999999 + share_int = 100 + initial lrate = 5.0000000000000003E-002 + minimum lrate = 1.0000000000000000E-008 + minimum data covariance eigenvalue = 9.9999999999999998E-013 + lrate factor = 0.50000000000000000 + initial rholrate = 5.0000000000000003E-002 + rho0 = 1.5000000000000000 + min rho = 1.0000000000000000 + max rho = 2.0000000000000000 + rho lrate factor = 0.50000000000000000 + kurt_start = 3 + num kurt = 5 + kurt interval = 1 + do_newton = 1 + newt_start = 50 + newt_ramp = 10 + initial newton lrate = 1.0000000000000000 + do_reject = 0 + num reject = 3 + reject sigma = 3.0000000000000000 + reject start = 2 + reject interval = 3 + write step = 100 + write_nd = 0 + write_LLt = 1 + dec window = 1 + max_decs = 3 + fix_init = 0 + update_A = 1 + update_c = 1 + update_gm = 1 + update_alpha = 1 + update_mu = 1 + update_beta = 1 + invsigmax = 100.00000000000000 + invsigmin = 0.0000000000000000 + do_rho = 1 + load_rej = 0 + load_c = 0 + load_gm = 0 + load_alpha = 0 + load_mu = 0 + load_beta = 0 + load_rho = 0 + load_comp_list = 0 + do_mean = 1 + do_sphere = 1 + pcakeep = 32 + pcadb = 30.000000000000000 + byte_size = 4 + doscaling = 1 + scalestep = 1 +mkdir: cannot create directory ‘./out/’: File exists + output directory = ./out/ + 1 : setting num_thrds to 1 ... + 1 : using 1 threads. + 1 : node_thrds = 1 + bytes in real = 4 + 1 : REAL nbyte = 4 + getting segment list ... + blocks in sample = 30504 + total blocks = 30504 + node blocks = 30504 + node 1 start: file 1 sample 1 index 1 + node 1 stop : file 1 sample 1 index 30504 + 1 : data = -35.797485351562500 2.3078439235687256 + getting the mean ... + mean = -3.7090141912586323 -6.7516788952437894 2.5880450259870105 + subtracting the mean ... + getting the covariance matrix ... + cnt = 30504 + doing eig nx = 32 lwork = 10240 + minimum eigenvalues = 4.8799005132501803 6.9201197127079803 7.6562147928880702 + maximum eigenvalues = 9711.1430838537090 3039.6850435125002 1244.4129447052057 + num eigs kept = 32 + getting the sphering matrix ... + minimum eigenvalues = 4.8799005132501803 6.9201197127079803 7.6562147928880702 + maximum eigenvalues = 9711.1430838537090 3039.6850435125002 1244.4129447052057 + num eigs kept = 32 + sphering the data ... + numeigs = 32 + 1 : Allocating variables ... + 1 : Initializing variables ... + 1 : block size = 512 + 1 : entering the main loop ... + iter 1 lrate = 0.0500000000 LL = -3.5132320906 nd = 0.0576198453, D = 0.39757E-02 0.39757E-02 ( 0.09 s, 0.0 h) +... done. Execution time: 0.00 h + output directory = ./out/ diff --git a/.context/issue-351/raw/seedcheck_issue145_binary/seedcheck.txt b/.context/issue-351/raw/seedcheck_issue145_binary/seedcheck.txt new file mode 100644 index 00000000..1233c9c1 --- /dev/null +++ b/.context/issue-351/raw/seedcheck_issue145_binary/seedcheck.txt @@ -0,0 +1,6 @@ +43b9f0e3dfd7b1e05754fdd5c86e5abe0b47b784ac8019adb995d20ae2c53b59 /home/yahya/pyAMICA-issue144/pyAMICA/sample_data/amica15_linux +run1 + iter 1 lrate = 0.0500000000 LL = -3.5114441940 nd = 0.0556873701, D = 0.42075E-02 0.42075E-02 ( 0.10 s, 0.0 h) +run2 + iter 1 lrate = 0.0500000000 LL = -3.5132320906 nd = 0.0576198453, D = 0.39757E-02 0.39757E-02 ( 0.09 s, 0.0 h) +max|A1-A2| after 1 iteration: 0.009557581902372853 diff --git a/.context/issue-351/raw/share_comps/share_comps_counts.json b/.context/issue-351/raw/share_comps/share_comps_counts.json new file mode 100644 index 00000000..227cb694 --- /dev/null +++ b/.context/issue-351/raw/share_comps/share_comps_counts.json @@ -0,0 +1,117 @@ +{ + "a": { + "share_off_final_ll": -3.3393496568371823, + "share_off_stop": "max_iter", + "share_off_newton_fallbacks": 0, + "share_on_final_ll": -3.3409951996200453, + "share_on_stop": "max_iter", + "share_on_newton_fallbacks": 0, + "merges": [ + { + "iter": 100, + "model": 0, + "source": 11, + "into_model": 1, + "into_source": 3, + "map_abs_cos": 0.9699489398459946 + } + ], + "unique_components": 63, + "groups": [ + { + "group": [ + [ + 1, + 3 + ], + [ + 0, + 11 + ] + ], + "identical": true + } + ] + }, + "b": { + "scan_at_8_thr_0.9": { + "merged": 31, + "final_ll": -3.453995960732006, + "stop": "max_iter" + }, + "scan_at_8_thr_0.95": { + "merged": 30, + "final_ll": -3.4531792987461793, + "stop": "max_iter" + }, + "scan_at_8_thr_0.99": { + "merged": 17, + "final_ll": -3.4340012191597338, + "stop": "max_iter" + }, + "scan_at_20_thr_0.9": { + "merged": 30, + "final_ll": -3.4702865291798437, + "stop": "max_iter" + }, + "scan_at_20_thr_0.95": { + "merged": 22, + "final_ll": -3.4485215414735295, + "stop": "max_iter" + }, + "scan_at_20_thr_0.99": { + "merged": 5, + "final_ll": -3.4105196773183413, + "stop": "max_iter" + } + }, + "c": { + "state_A_diff_torch_vs_ref": 5.2365930183172205e-06, + "state_LL_diff_torch_vs_ref": 1.550818593543113e-07, + "thr_0.9": { + "reference_state": 32, + "torch_state": 32, + "numpy_state": 32, + "torch_fit_scan": 32, + "same_comp_list_ref_torch_numpy_fit": [ + true, + true, + true + ] + }, + "thr_0.95": { + "reference_state": 32, + "torch_state": 32, + "numpy_state": 32, + "torch_fit_scan": 32, + "same_comp_list_ref_torch_numpy_fit": [ + true, + true, + true + ] + }, + "thr_0.99": { + "reference_state": 29, + "torch_state": 29, + "numpy_state": 29, + "torch_fit_scan": 29, + "same_comp_list_ref_torch_numpy_fit": [ + true, + true, + true + ] + } + }, + "d": { + "first_scan_merges": 28, + "gm2_after_first_scan": 0.5720240822678972, + "gm2_1_after_scan": 0.0021825430273267536, + "gm2_2_after_scan": 0.0009015369020092886, + "second_scan_stop": "max_iter", + "merges_after_second_scan": 28, + "gm2_after_second_scan": 0.000244140625, + "long_fit_stop": "nan_params", + "long_fit_iterations_recorded": 23, + "long_fit_last_iteration_index": 22 + } +} \ No newline at end of file diff --git a/.context/issue-351/raw/share_comps/stdout.log b/.context/issue-351/raw/share_comps/stdout.log new file mode 100644 index 00000000..d70f10d2 --- /dev/null +++ b/.context/issue-351/raw/share_comps/stdout.log @@ -0,0 +1,140 @@ +keep_best is inactive under share_comps: best-iterate selection by LL is not well-defined (a merge changes the parameter count and reverting to an earlier snapshot would undo the merge), so fit() returns the last iterate. +keep_best is inactive under share_comps: best-iterate selection by LL is not well-defined (a merge changes the parameter count and reverting to an earlier snapshot would undo the merge), so fit() returns the last iterate. +keep_best is inactive under share_comps: best-iterate selection by LL is not well-defined (a merge changes the parameter count and reverting to an earlier snapshot would undo the merge), so fit() returns the last iterate. +keep_best is inactive under share_comps: best-iterate selection by LL is not well-defined (a merge changes the parameter count and reverting to an earlier snapshot would undo the merge), so fit() returns the last iterate. +keep_best is inactive under share_comps: best-iterate selection by LL is not well-defined (a merge changes the parameter count and reverting to an earlier snapshot would undo the merge), so fit() returns the last iterate. +keep_best is inactive under share_comps: best-iterate selection by LL is not well-defined (a merge changes the parameter count and reverting to an earlier snapshot would undo the merge), so fit() returns the last iterate. +keep_best is inactive under share_comps: best-iterate selection by LL is not well-defined (a merge changes the parameter count and reverting to an earlier snapshot would undo the merge), so fit() returns the last iterate. +Starting AMICA fitting... +Starting optimization... + iter 2 lrate = 0.0500000000 LL = -3.4682954058 nd = 0.0278618498, D = 4.41736e-02 4.41736e-02 ( 1.51 s, 0.0 h) + iter 3 lrate = 0.0500000000 LL = -3.4603659507 nd = 0.0301293222, D = 7.92946e-03 7.92946e-03 ( 2.29 s, 0.0 h) + iter 4 lrate = 0.0500000000 LL = -3.4563302679 nd = 0.0361807797, D = 4.03568e-03 4.03568e-03 ( 3.08 s, 0.0 h) + iter 5 lrate = 0.0500000000 LL = -3.4524903836 nd = 0.0403887082, D = 3.83988e-03 3.83988e-03 ( 3.86 s, 0.0 h) + iter 6 lrate = 0.0500000000 LL = -3.4487996014 nd = 0.0425780782, D = 3.69078e-03 3.69078e-03 ( 4.67 s, 0.0 h) + iter 7 lrate = 0.0500000000 LL = -3.4451593881 nd = 0.0432309476, D = 3.64021e-03 3.64021e-03 ( 5.45 s, 0.0 h) + iter 8 lrate = 0.0500000000 LL = -3.4416417346 nd = 0.0430271621, D = 3.51765e-03 3.51765e-03 ( 6.26 s, 0.0 h) +Optimization finished after 8 iterations +keep_best is inactive under share_comps: best-iterate selection by LL is not well-defined (a merge changes the parameter count and reverting to an earlier snapshot would undo the merge), so fit() returns the last iterate. +keep_best is inactive under share_comps: best-iterate selection by LL is not well-defined (a merge changes the parameter count and reverting to an earlier snapshot would undo the merge), so fit() returns the last iterate. +keep_best is inactive under share_comps: best-iterate selection by LL is not well-defined (a merge changes the parameter count and reverting to an earlier snapshot would undo the merge), so fit() returns the last iterate. +keep_best is inactive under share_comps: best-iterate selection by LL is not well-defined (a merge changes the parameter count and reverting to an earlier snapshot would undo the merge), so fit() returns the last iterate. +keep_best is inactive under share_comps: best-iterate selection by LL is not well-defined (a merge changes the parameter count and reverting to an earlier snapshot would undo the merge), so fit() returns the last iterate. +Non-finite mu at iter 22 (a mixture component likely collapsed); stopping. +{ + "a": { + "share_off_final_ll": -3.3393496568371823, + "share_off_stop": "max_iter", + "share_off_newton_fallbacks": 0, + "share_on_final_ll": -3.3409951996200453, + "share_on_stop": "max_iter", + "share_on_newton_fallbacks": 0, + "merges": [ + { + "iter": 100, + "model": 0, + "source": 11, + "into_model": 1, + "into_source": 3, + "map_abs_cos": 0.9699489398459946 + } + ], + "unique_components": 63, + "groups": [ + { + "group": [ + [ + 1, + 3 + ], + [ + 0, + 11 + ] + ], + "identical": true + } + ] + }, + "b": { + "scan_at_8_thr_0.9": { + "merged": 31, + "final_ll": -3.453995960732006, + "stop": "max_iter" + }, + "scan_at_8_thr_0.95": { + "merged": 30, + "final_ll": -3.4531792987461793, + "stop": "max_iter" + }, + "scan_at_8_thr_0.99": { + "merged": 17, + "final_ll": -3.4340012191597338, + "stop": "max_iter" + }, + "scan_at_20_thr_0.9": { + "merged": 30, + "final_ll": -3.4702865291798437, + "stop": "max_iter" + }, + "scan_at_20_thr_0.95": { + "merged": 22, + "final_ll": -3.4485215414735295, + "stop": "max_iter" + }, + "scan_at_20_thr_0.99": { + "merged": 5, + "final_ll": -3.4105196773183413, + "stop": "max_iter" + } + }, + "c": { + "state_A_diff_torch_vs_ref": 5.2365930183172205e-06, + "state_LL_diff_torch_vs_ref": 1.550818593543113e-07, + "thr_0.9": { + "reference_state": 32, + "torch_state": 32, + "numpy_state": 32, + "torch_fit_scan": 32, + "same_comp_list_ref_torch_numpy_fit": [ + true, + true, + true + ] + }, + "thr_0.95": { + "reference_state": 32, + "torch_state": 32, + "numpy_state": 32, + "torch_fit_scan": 32, + "same_comp_list_ref_torch_numpy_fit": [ + true, + true, + true + ] + }, + "thr_0.99": { + "reference_state": 29, + "torch_state": 29, + "numpy_state": 29, + "torch_fit_scan": 29, + "same_comp_list_ref_torch_numpy_fit": [ + true, + true, + true + ] + } + }, + "d": { + "first_scan_merges": 28, + "gm2_after_first_scan": 0.5720240822678972, + "gm2_1_after_scan": 0.0021825430273267536, + "gm2_2_after_scan": 0.0009015369020092886, + "second_scan_stop": "max_iter", + "merges_after_second_scan": 28, + "gm2_after_second_scan": 0.000244140625, + "long_fit_stop": "nan_params", + "long_fit_iterations_recorded": 23, + "long_fit_last_iteration_index": 22 + } +} diff --git a/.context/issue-351/raw/table1_bundled/bundled_multimodel_ensemble.npz b/.context/issue-351/raw/table1_bundled/bundled_multimodel_ensemble.npz new file mode 100644 index 0000000000000000000000000000000000000000..7063d70883399935297e8aa38994f42032e84c53 GIT binary patch literal 656654 zcmbSy_cxbs{J)p+viFXRB9xJlRa_TI6d745WzUQ#$|@so(KJFtDrH1=lw22*N@SDl zz4xBq*XKL_f$#OpeeV0*=UnGL=f0lL$9fu`r=XM~`#(#V447SE-2cCeNX9{??IZ2s zb%#L4LUxag>VLoV|E{qYO!W;f5y*VW0>tbceC)i%RE5M&IVy`u3W+&-df)W6@wn#c zZSU|u&$Vr?`#6xE`#9TpIgswf<&H}VDJn<`$;(Md3Iz%Me=NH5&7o~0`e&fOR!&Cj zq8tvPTabOyM+uKci(Vq0pn?|`w9ltpC&F*Xzsw5@cYvR79mS36d0>fT-2*{S0z7-G z_a|g!hRrd12jvfO!7^4+Kk@zzl=Zx+rt}yQuANo>m-4s{-1;UguqHJR)DCWaU*g&X z{hKR`$HZ5GirA&-51p+@vnthqF?#_tnud94KORJ%w|U~u4)+6>nhMh(wl>gC?YdiA z$PDc%(yyT6b&zpw%<*CzGhB1>bo0Et49wVPpK?oeqDz_AQvF~PsAldmi@G%gB&8m+ z_S{=WE+g6RSOzxG`|06--_%z?xJN>T&Y4w^ZY*;pz)lEf>4;ck9Uk1z-Z*uRRREf| z-q>Gu=m#Flj@!mkqL9s0oAr>y8hZI}ea-yiAs874dW0I+Q0+rs{ilg4aOop6sy)R3 z6Iu+E&Oc+sM~dH+%Chs|yL;v{=4#vE(a+2cCu26K@wg%Ur7kTVXRaYvN?JwlI2vD` z`algg>n0X1+?Rz+!7OC`8Vpd+NA!Dw$^mGR-TY;2L;%-!zhS8RJqy0|6*pI%Q-EAt ze^_fR55WhomuTFgK%AGA>6|xe_ zD9?`dcu;xFQDGQ&yY4Y_#5i~qA)mYBO9l^+^X~Yw?0`x_9w5FR27Uu)%_=2m@r8+n zahLYrD1r7xi_+n7a9^HmIj49D^&Tn}>@rwI3pD0CG%Ea%a8xz<=u2UhEHj1`lsNZvZv3_EH85;W9Nj411yil3n|pV<5h7E) z$HF&@oJKF`{2N;X0{*@;76*w~+UMeTwh<1z7Zv>Av_2~|qWyJ+ri~1~=iHi#W#+;O zMGP8e39IOv^r;KC90jmdP27PaQZz6%@|OB!9RYhd4(wa^vcm_A1*t3YX^53Q`n3!V z9h85HGw*E;0u9yqYWC7il(QcDsq8f`lrF4}$fxJQ!PBeh!P;9Wq|R@6_O=kD$)?cm zUQ&d`N)Ea28ad(J^`J!a_p7LFdnj1aRTz$|#LW+|+ zLOYb$UO@#_FU-nhHh|=_-S5*$oX}1|lxsz?6-;#N`_OaJVe5?L)LMITIQ>bPP|iIH zgbRh3?)kFece;$x|A^eswm$y7+#g!ZR?~D?N@)!NmGD!cBNM2sVq{NdWDc-OrhBm) zHz9w=oPwYRW_ZApO-@^e4j#`+t>mpGht$_+gS+|3F}200fTY7;z+9#H;=UCR>}-@o z8}BHfoG5PGjHbY@aug1^suWN$B>aXn^A=iveyc%0coPII2UcFRUqJQuQwN2+C}GRp z^nikbeIPCCbA$QEDmuJh%1uAI1;nksiqD>7fg$6|YRH>_53>%WSifS0PL>Y_+$tF1 z6+?Y`vTN)xo&RCyt0vNZ;k((>uDmdgI@XLB$cwq_R!&X$aYFl--QvA3s4%&j2>XXj zA)IiRk2Zgw8>T%>U3~LF3ck#`#@IPefcbrg=4k$Gf&&r0nW;jDA@{XN)7jJo6c_X5 zzc9)%v=?(dvS^qYO3V~Cik%rk-#BuBgGVj!=Q>f{bz&S~8bV2|IXf)*Z=T1;kp+id zr}({^LBv;xkGvMp5Kw&08RQwrg&#kFug%!?76BSqfNrSx;?d2z#l%Z$I_Z7)HR`^XV zSl-4-7v{;TK7zL8eDQ* zIe!L|WA%x1FGq=F5PXbl<@Tq*Az@C{zn)Eihc+*+U6Eygqk)VDDaW{An4^908yZqQ ztST0eB;P`fOlL08aIc|P(*qT@T@xsd@3uf`KO>BK{O;=uyIFKrYkn|plpk-nbg68j zLooLy^Wyevc3d5DRlp^U1HU{hR;}9i2K~uhU)11af-FDR1$DYlK)PmI=DU`(uzY02 zbCqllMQ(7~xw~{DvVkvi<2ux^@9Bd{V&?+z{UyD!rCJ9B$qA|7Z~a6{kC}G+^yzSd zR-BIYJ4)<#U)kl#!v$oa_4(iPpR|yIf$FypW`VLF>Q27@M8t~pdkT-38o=XLrk%hl zG92;mrj}_XGh9vC{-Jw^2<5EpVpFGj&{y3r3!`i7m?ccqcz}~1ziVa8G4?(LZ-9ZI zf9FoXq1{nxiAtSjN#TUc>;b}g;T-TVt?X^ie_W86>wC^K z1rGe{W`H)06&DPm;heQ8S_k*OYhG(5u)wRJI%42HB_tTP#9tLZh_&8$@}@msL7+UT zY`A(9{WE(mFU7bHLRwwVhBeZ`LLL0gBW)1`jAgq`-rN9QIMrgrGL?7Y6+RqY<;X6E<%M}81)@XZml9Z>y}L4hr7tc&L%EjdJx=`i#YOrY752c zc#f$3szAR$$k7A99F-BB=Og|FNQaxbM0U(!*1;<6QLE?TB93h~_e)5v6;) zle+=JVxzvaSFNK;vO&ufw{`S=(OyVLhylK4r}#yAi4&?ZDM(8PQA6ehVgtd212RsI zj0bU3!Ij457Va))+&Uree7}AJ41FulYU4c)W8>mlGjo1{#Bq=4do!)*_Xo{z)8}OH zR+$OU1I=H+ls(0)U6~8A3oLmYO(fv)t955DKh6Myi;D2!=Y3?hz&B?kP6jvVj}5zv z&|!Q4DAC7KLhXUN&XRv*(6?O4K<0-utau}SbTXF`o_s%fU~H0rOHKKj?Oszu;U=wg z5JHCk1*Wf=^h~1@OnLL-E427mk0$MZo^7agP58HfJ3ai7(U&_h!-$PCey+)Un*rp; z@BXZRrNiVp*ED4$_d(_d1Iby^c`4X%Y@|u826JCMZ7S6m@W+11d=r~VpzqxD^!r~9 zd{I-RM5}-vUJhX7B^YzUb5&xGpUSME@_UrSE;O9*<+&>TLc=N0GZUL`OG6JmEKBe0 z(z3vNhdX(1FYW-Y$p=vvQV6(bDoCaLAq$R9`m!=gJ%`Ho4N;b^6rAlmDKSyOgUQ#d zUZStEx$@cg)4?JCkv zq|Pw^OAC1)+nH~)a>Ltb_fU8UH4GE*D_97ifIrGuB2~U0z>0~_%#0UmD>J4#@slEXHWQ3lwUdYooo( z0^@dOWelzM0K*|;{{uCgn9)}$pq!}~L_3GFs?x8axhS$7-Y4U>M0-OjHbHZlS00R^nvLh z=Sp$KZ1nnB(;P5k#Kq@mrO4AJ!Dsed%L?Z^=)f#Rpsh1CJh@et#d?Jv(m%IGjF*^j zjxX~{ygVIrJ?btI_hbNh_Xp`FuSvm!hWDs_&XMEuR$nrRU)Ug%V0;f<`ZBOgX&=t| z`UU+}auJwyA4KnU;}?01I?!vglqZgA)HruBfkK#d6zsccZ<*M#!h#UX_)n@^$gfUg z-BF$npIxQ8-28SE^|o!C2};>SG6kYPM|zh*WI3bC7vX6{B)`YGNI{ErYSM0?{M)C~;YU*p;xmi-?Kod8B~Pdz4!>X!dih5UlVg z%8CqZ10})I%QB_=U`XGJW;f|L^fxv&yZw_Ij|!ftk=~=j{POx=c_g?X3z_WKr$?Fa z+i(dGK|O`CTxnjm)31Sp*-I3vC#bLyx2W!z>OLsry}QY1%!5VekC8>(p@8eMzfW?= zv0!m8{JcT-IkK^N)#D*81D|>~&dVjV0U3tGylh(%Pnk2{Ui9b&nY8|Qs-)Pk@>=}X zjS&L8^y%yf{KyBNx{ut|j2b}IbMoZfHz{Gziyo>ezkR@PLcO3+U=eUs)71rCBjFyK z%9+D#TgdLeO2H067EE)LqqFkz1bUiaYw;>`7i86zJ7h3T0ENqfzm7bl!D)T^2S;i* zfbF}ncP8!(7*?we&6SmajmA1BX%%YdFsNXx?!y7)$e3?`;2T0oJm!WB1tea}U_==D z(1^$$fjP>uMRfAvh4t^zV_>G<{@63JeH8uqB|YId5BC3eWgwQF7VCX_gKr9Nq0jo+ zx4S+1&^p^RA0|@Zca;|WhN&OWhK>G@C5lxbcJQX!R@)!Maoh9dI5!g<{-#hm{DKa9 zeA)iXXg!F0dGb?^99lv{(kt)E9!SDHMg{(~XPb!f$+X`A9x8Y$MQXl@rXNgw{o#|c zzyishgnzD*riGXCBf6a2myjZZ$f$JqU-UKKVVRzN5@}!kNwHZ+k4s);v&@Z8AU!6H z{EH49z!A}W8$U9Rax+`RN?vWFvro!TEM-lC3aR)=tx12>an^PVGN^NSt2Cn< zajjnz^czrd#bs+%d1_2^m{s%H2}b-^-sIv-w`SyVrr3p0`XAbJVddgg-$b(lDoULW zOi<2j&7z-Y4aJ75t(uuK;~z1%es9jN0-cg;@5o5?c7A?T;y}YTaCX`~_duBzUzz?O zoEp;(9>ZWP8NESJ% zZ{!pNGi-qyAI@ZmbxnbIqQNv23q#6v7bd;zexcnl*7oK4f53u4SyGCb7F&7-xLGHC zM{&lgp;BAvK&V;c4U5S>5Y+Stxzw=+SlrleC-x12QxaB^r!Np-x#qZY!?6R{(dOln z(sw#|x!>4&?&~PTaXHgtVIuDrAt0C5m@OpA!B%s`~bE88yDQ za#}c0p%3L+S-$qaGJ~GgF%e!8cah7VqQlKD%OL4yrTuEu7;?=taPLps1>VTw+r*Ov z#HY?zrhQ=ml{ulO8CCSSJVMN`_QpIYQHpd8cN{{$M_eRIeoMem`Px%kR;wV1#n5P; zFo6Q+dmdYTC&DLtzdQWiPxDjn7^Z5L2rjYZGSc=V#(&= z-`h5OD72O0<>vuvYJSS13 z6NA`NE*aEMCab#dI||eTv~@G%*x_ZC5(l;Rzu?O!qq$(A4N%Vg@$GGkEws@T|SWrMI7ie{vt3vsT{)cQi)+x~BIc^*F+B!ABjyh~@*$nCKQLRcrjL zpHG8Raux5SQEnqPwS=zzrClI?6WyQ{>;y$cxe1XvQ=oBwE#iP9BP`=?I<)e>8`WRF zqIYJI09WT-XhjVc&<`q(qp7DWP;lanM6bqPK*L{aw|uJ%#KZGF9@j@vVzl9lk(v$k zDZ5>}sWtBLS{tu&@t$tfDhOtvRbWnkhKnf3Kr=^6_v7(Wupz{vF5Qnm7>IV%oVe)LRy@4eqcpkb_W?AJ73~H zw~l5GByb)a>;rw|0$(}5P5=hZuSP0&OVAB!6VV;jQb6NK<4WG!i(V}#N}_Y~2>BOi zQRmS@5`$jR-QGvyl&1eg7uJB|f8WlO%rj##nRccW`5y2YhU!RpagC1^5|IOVbhw9%ZA9r*!VMYy}C0^%wq}!xb zd_LeO`u19oyOoOuvQ7SrGFGRC`8q*8!I2AK{`-9~ZpJ~7jCBXgJ!BEp%_)jJ@&&GbDbxG__?hiTu}==ZO*rpf^*>u^3rW z|6AF$UebJuPB(UIydo?j%jX465^>ZxL__3soZ$r6{V3vIVa9;N^>0r0j#6V^<7k5m zB%E(ZN5SE66$(nM%AjL2aBcz!<;uXT*({J;uchTm)h^II zxN^NAe;61lRyTgO+d-{`-xSP;>G1`X367S{1;nXiRwN@eiUzh9tlE3WXaErh|&ZITA6ohb9Ygj$k=Dgh)MKn<_A{xqJTqopzq@WMp!MXe+12U zB1!L_M;#XyP~*oi9V*#DRQ9*VK4p9k*i>@bDO?)>Iwy@EiE!_u#mjVVN~WWL{$_z{ zZ`?LmnAytRiXw;k!wQd;vst0xne3a#z%t01bp$k|egB((TAv_Uh^)zTE;NQLqU^>D znPj$lko_w`$E9xq*{AkwzbWE_GVJ0>Q{?+7`Ozb9xiM;7uKc$(y?hSAv#gpj(_C27 z`-xgWe=88yOFofSunOuz21~AAS^`#qw8|!DUm>nYDa!>(8py|SFm8#O0UJ`>;0()M zK#aLIb4docE39_|Ia>cXA8=;0wjEhINt%t#J{jT2YHeJBksD}Bf5g8WX zye&+7WDkum2mCau+(-1{X5gGTMr*uo^lPI-K>aDOn6H@!Z!8A9s<|0)`kf{Jjl>?H z@{ru;PSiLMDmZK@WZi*g?~~W)tIngReH@l%>^vUT=&W<|x!0mgZUpPjWb07%q7gXO^U>htW@P_)mIkNZgr8>*Crl+9jv!U4BFh@1&K+#va~5 z!_ln?B)uMs{Wg>ySeyU@`}-bzB!5Jc;gTL_>^922Hh=2cOI!LhncUVwdZ%hkXtnNkhFhhM9x01&oXdJQIoi%I|a1EdvygF%7JF1 z?pTC;Cdi2V-u>n3IJz$CWPB`!8t*GmE+~B*1S0{rDm~B4g9J;ZHz7(3NY^#v?Vmh8 z81MCQ*kel z1|Qb-oQ`pC2T!kaF>#jiL3JlPMZt?yuw?WG#ro-4R2xruMUJN#t&MfiXFga4ZF4E# zRQ09+$7IN6z;|YRCY&+xYRDp(7C%;&oKK6x7ux7t*Z5)Tkux7B=Q5FARyTu6z#^y! zwK?B$dL5K~tJ|o0Lxs~!nO3U1$zWaFp6j2AEl?!*=KOxn4rq5L-l`g11pL(ogR}JA z=<&G5>kW5uJb&hdQP}emr10;cz`jl;@@snVo7RvDyT{$Gj@w=W3qj@|MQEwvjy$9h^cZTH+pdxGtijhh(!jMS8(mO9OfP_@k~g?;?XwGo=s0Tfs5BWgb613HSysNxUrI21jUe`hd?U zc;0rvi%D(}Se|_;FL8LGCF9b?xXGGFs;>EW@z>L zvCwYfGNR5Av!kzFLqlX7Q^m6F==LzVW&+(Dpkvre6Ff48%&wk1Y;trQC`9G@Ja!pF zWntNhr^hI;#2L5admb|=bMYbB&5Cv;?9l!2^_%-Z!j-1X#AO*(9}kaM4`YX}9{%?q zs_dYYKnIo*-dZr2^~BVTouo&re3idlxeUD9;%~(~96)11*WBJ;*#z>Mq2DhQuAv8F z@lOIp`w*?sxYxx}D*S4SM~lngEs~lloRJ|PLN{eQ_=OHJL6H_2!N*+{;N4(J-yg|& z)YxF)9e;EYu`g-u8r4pqWD1X~QLfB5;3J`2&3Os!)nuezDPBP*rD|EHrOH9_9mQkT z(G7@5$Qi%3{{s|D6@H^=83)`J>8ugYsUQi05O0o?ma_N6c zv53}W!)#p6jQ}qOyV*<R+4;V1TQ>y3hm&tI04W;|V1~%Ae&iZGYQV1%W zc11?8OaXy=WVOxJ9B|@vfvfgBD^7Q)GwnLOjgDR%R=RpI6zSGaJkzU~MUSbRq=r$a9Ks~O!Vv;s2o{07h)5FYFiZ6``)|ARby)0zU2@UI<(~}^%aCr5eG!ewEm$Xe$<(EXb%f6~ZRXzmVgILeeP10i~Z8jF@h?lY1sT39l68fX;BS43pnlaUc+w!{Uy!;ml%lq+9a7st6xF;I_Z><3 z1gqaftAPC*_I+XaZzk^4N0N`3&!xb1Mr2xq)39{$=bcG*w}rXIRJH(bvIce0X~Q9Iw;}Ib2!S|1J?rhxcK@}EN|PJHf_0!dhGZ-X$4o%ygPHq+5kBg zpF8vL)AMDtf(m~!Q4WGse>*uAJqjFdDhIRC4S(C!@WBb8 zkk`$hw*dK@6E=y1mB`oW+_y~$O87My`SHoa_Z_j41I`4%o}C@X_|rDhRxK_inKy5ocNGa|BXR zLh6AACkFT9u+*hy>8&C?W{7Mm^*7;x@n?@a8^-d$6J@MjBatNjUL#k-yRwd?N+#|S zOLq`M0XI{$YCTvRDSsp%$A~Sd*Z!(~BI(@cpLhSwWP_H5Odm~jCO~_Bz|)}@q`X*t zM9$m(I&|+tT-hONl7Cv}V)oUD3g7!IdXw0}f@9=s-}FVSqfh7ON7c@CqBl=dvU^hK zasJgOXlsNOr-nRDOdVszyxY@`U9YEr$g=g#_qO$5?@{fBFCRHhGD)z{yL7Tu-3h3kF{F^4E<|D98!cLF=26iH3aNEU zR=hp*?_X@qFGN4#r@df9z|PtyUrZN~`STJI! zJ;;#>HdV@u798S-=Z3GYOD)qw-akG27H2lW>ymEbc)oU?3q8kd&L3C zs;9W_|6zfV@ljFn7r5~qq3NyKP6m9GuA)z8nFG>?Tn4xVF1^dMlJn)4-QN&%49X^pg>As)92~GcY9V#B-jjoNj zPPowG8yb`$Jx=U!Q~Gz*zb6j&61fLoecI+n@u)W>_zcuHDmh5Uz)wSkPaJ*dM&GJ#&3smqb z<30A~{tBd-d`v+xf*l&ZN?-M#;(;n6TePEWOwg`%_vJ%gO8nS4{p-D9HcXZ&AEBW& z4j!8x@s0>%hPt(>6Xn{J*ta5YpUs*QZq5HZ`Dl3!9Y6SqXl+5jU2+{$m4AnU$tq># zr-$tL2kjLzh2J|U#-rrM6=Qk${8R(?4kIa7$CoK}wP6`C95Fd%Ww{Qxa{|I@k8PlN z2fd2e>~YYT?4q}I{%n3R|LD*o-=f++zPDkNWvR9XZLn)#Ox zb?*V^k1OxT{7L@bRSo-zbvpRHAuQDaF+o?g<98x#rje%i4Yv$KRvaAaQwjCxux0CA zJwjPOP!tK05_>R5!fj_I+ak!Yak_uui6&-vs_Rd_PdWws_Fd(|z6lMEMVwNsPII8# zj9Bx#S^`h**Jvqqh{Nj32D%oL6nL(}hwaX*Meu-)Zy~jml>bTJGVVD@jt$9)6ET#H zXv3q1FSUGNip?Mz=(SrZ6d$d!Ht7X>`W9?@Wqqf0%_rWz#m4V((Azl z2QuhbZe8KP{mfT7SYl`)mxEnSuQLlaEbA2(y0D4t3t0F5wS56^JCqZD-&KXj8R}Rj zPAlQ1RhqZzRHS{?cFH%t^q|mk6ROlX4)~*oi9D{Fq}Q((Mos)3L*c%4gtMY-FebB* zLHYS8id(KGGo<^A2oN!GFkic|KtP5 zyH0djjiBQ^4rdPhm_XKdpF~JfECK4ZmP-yz%Yav6v>P&#WA54;G=#NfB*2z%$HHa| zairC5EOgDGla5|7)}LrGKh5r~P4zzDjCXxrX8l2*;@{UK{u~EhFO_dane3sPlcOv2 z&lbQ3x-vem=d&nvwAS}{4ioW3Opup!1qlO2@fTn9Uc5}4&_hHW=1t}~vI`Xn>=uc4pg@=>0_|&sk>f*!g z;6on6#S4BlK(|y|_whq=e5L7{>S8J#P8QR&QT#fOs59s!yoyF{UdG6fq zCz8*;Pa*S7Gn^iGrSkpEym1^xL=5HGOaDQ6^{b(OS_xQ-c|hOA6j^xarn|Sd z-3Otm;)3Y)eP*2b@Qy^65*vJCp|q`%vyL>1(mCH|9mFQZ3B;SL)R;f-gGaPFIllck zIfb7W1HHpFt3z?CAha@8kJV%z)!Uxp<f z0vRKFOb6)tfep=Zi);aQ%xlEdq~|^ke#iwj+f}6?xiIlP>!wu@F66k~o?Hz!JAa!s zzZ?X`V}1XkjrcGN!PHBG#W!@>Gp6xp-syv@T3`4*&$ zw&>TkDSsO|xF2gSOBXmDN58KKzQ+;F<)8n?gfIN;l&{rA(k4N3E#4oD@V#B{ZO-(L%n zV=uvj{;Jdj*i0s^&l0+fL=V+h{eDLcbz&^_4!)=WJ#C?3G@@kKj`$(sro##fIi7f- zwSx;v{<3DB$YOL z<*(b8s+P={$ZNMvcak0k)yj+AaTkH*2R-H)8dxEXQ*+%8A2)nhEW<2o$b~soHZQj{ z3BbQiu~MIpvEvfT?`+YXv6;W$4hCC%XgFH4EV5WDY zq?0!hx>_X-5m?FKiNzFAQRyW>bNVmsu^+o=uJ=o%p86_UE7%z78(IexT~yCr3Gafz zW7CUmOtiQluKy(WEGs6vXe-}Ivk#tk+$y3xH3XKYx*GNd7Qo2G+k)~JN%Is+OiZis zEYS0P0$%K#1t;?5%~QCSL84Ho4%q2L{xm#pTU}gud{=nu^x6(k*1gGBk+6(T8Hnc{ z)FX#Qukuyma;CvVviBqH&uc)MKf^}%%n%q@JAiJv&x79v@>1G^qi9vC{-KXC9ahX2 z3Ayk<020hKljA=Qp=W%J&m;Glu-m>G`|~$b;0OC5LCX$u$UK!VRVYY?xsuvtKff3Q z8>LE4+b+pqY(1mqOcOc&b9BAguXhTGtiO=CF}ndGw(j!zf9wQ;PPPly5`RHe%4^@~ zXp$dse&%vw>mnEsap91b`Hb`;Rf11ykwc4wGv?Q=mJ#drZEDKiDrCdDO!aAF9#tB! zpXX0w!}6y}lK9hD@MQyKRt1F(U=#Fyn2o0gm6%g6m}u<)zA1|*#*akdZ12sXyGN+; z1Fbga+dn&ia{AjiT3#j`8&g+&@W2oni8~ZipRfcj6t8Rvw6vfs_EQ&|)Y!3Gi;-fm zEDI!>x`=Q{a$uX@3}ih&0iqTzh*J^BVEk)SLw-#TxZNNVxJpI^U&%_Tsg}(Fx1lmW zN>?&G{-1zyOcMqCZ^s}%{Qxh#^Hh^;#d!_=KGQ2uWWNg-mG&JC0`|ciecivqt{hld zX0zn+*>%*lc#?hQ?>b8SRw@>tNty?_@}(iWod9D#-{5IKPKo7BYP5d|tRjg(h6(?^ zH9)v*HF4!;H^>q>Po1<)z$G6yeEwr6$Fr>3D*AR*_-Ky)M}$X#QC}FInruN6)BSNh z&)0yQ#f^>&s$4K8nj<;PfE`j0E>|XLFQa7j=&7H3Euibs8$YG%Tgb`Pv~let8(tYy z?eyo_LNTWiWf8*)V0HDRTcTP9#5wu?P`PC=^*vPa@lhrywAFrDPw_Ck;&o^q6BkiJ zX6>mH;qn`>qjHYqbKDHuCeBW5 z0z#^0pj`Mm3LkNMq0u^m0@eR3ey&d9Uk?B7HNI>DKj#aAl9S3%Z~fBJds3cVIQK)C zX2K#E5qr?bIlGH~HQArcoFe6Tiv%v08ukGNh7|e6+CA{6+BA6HmJBC;pKQJQ@ex8vT6jJcC=e7ykG+)MQkfu#E^K?ot$Guv)nkZ_@&H9 zXba-JWv;flI0j<*q=1&&7`iG(_nhg`KB(P%&H0JQ4g>KQk(#;<#6MW;7_%(`2~HW& z?e`h*@!w~6FI-v&Ugv||%&KS6a~}$u6YeB`SM=lRe8VtGt6!KBshCGMyOK5Zf3f4b zYLTQ^t9H~}XCJ70YZA~bY)6(m%z-?mZJ)fbF=SP+{*@xK57-5h61I`;i1yY4%?z44 zP(XjX-EM{w9vI%wQz{z)Guz7>L@|=iJEP0mv&{hC*EK2nyj(&`yp6@^4*_NlDy>l@ zETLDWnKt!^8FS{F3v2k1!%7$ahp#fmkt6MIbN)+HsCmy`^bqSVdNFkW*U04tpz$la zlxu?t?@{UU)86X@-X@W_6D@<-8A%7~dLdYM?$Sd&^?Y7!k@ zQTrmHK!wA7V$Tpi?|}C%u{p1(7-6sEsdTqyQeR0k4|Dpk51Pk2dk@iUqRUe_zHp}= zSTtXu_#4^}h@oNn8t!E9BH@8y#8p!M%jD%L{+=}?e=+3JHrY5Z&i+BG0hAab z_+`s$(R}`Q;#*55>~8OE&wZjFNLcGmxPGL?|5b_IeaglI|Jvti1!=D%OOdMk??Rfu z&8yv;S{c+Biv>6Ss9?lOK_~94B~5@8@!OG&GJW1~@ zJugh?&j8D?`Y6NJHhOdYB)PX+7dXb0{hrgl8@Mxt`%T;uf@JoGYx2L1AYnn~oWBKJ zII7v2!j6Xpa!pC|EYveY4#wV_kIhz5knqQt@H|GyUh8Z2*LMdH4?5J!br7&^{LB*@ z!$lMjOgY?AxB$L{=f<`AjRMZM6H8{sbnwmT1|`Y09z?y6cEi0n4{ahip}E_krF1hYoG8eDyO_HYOvLUV z`4F&)JZBaM6wK)GUDIOGfU#fTSHaXOsa8DBpqlJR5 z`7|su)#!!n;0yW2O<-q|#&%CD14)2wRvyw^0EMfyi9^5yu!^u1JzFt{&VIP0^ZnQg zir-53kb7n)qsN;opa>o7wkXCm}W9^s%LhW_3M5P^+K>w@% zMco=QC_VG5M{)r5aRi5R^%Wyc_Xa|#RS!yjIZOFay$6-+J|G)U`HM!Z6i$jXO`y6M z>g@W~4dfGk>znt|0O0()S7IBwkCgaTkKf!_2hi1X1= z!5=ly;|m)L?)z&XyUe<0>>^wQPemT1@3-eb|kKb&nY}Yt);nEv3GF$-XzU^_Xn6IO%j9K2- zcdC%~8TzjhzqnvP)4Tr;ODrJ4_PHTR^zooyg|MP0PDw*xg)9Tbl`(vOsl zlp{@(cF@3ByR`6=6|jFS^7D1SQPezpmaSWS5tLb;9%wzU0e|=MQ4|Cz;T{>cB(H8F zR0xYBb5Gs_v5c!c38GxErhp~-c*qoDyig;x&rb)ik8 zWPusz8};z&66p9}6rG1(h+P}U+k5Z5my|YzoRfw$H6)=W73Dz@X`*d5G>A}}_rkbEi?pY)gfBBiP@G&ni1J29_-{y7Cs2(RAutuhDtvtt4=%C{zo-nzw)u9pSKfqP5b6!3WIdxd9`OwdDuY5TG9Br?5o z#bz9{LwD781NOu@bWgcg#Y6aBaxL8^?Jg7GS9AX#y-IFK&8)YkZAy(b&OO57Iim2r z-JS=NIt`vI3S5j27sWITz2QqGBJf3RYV_#73XD?K2oU3Og664IO9QJePdMD_^JrB)T@ZUN9eU77FH9lGg9E;QGOuRD}jXXj`8lNK!C_gx5zmy@p6}qSRHt5ycsBQOb*4 zl!OxBRtY>{mAncD0&?rENlD>$Mi{jmFAEG1w2S0?%nP46+bDcxk-!eFhZH_9iNj%w zFE%tK2k^EGS(4gmR$RtpG*|y`7pR}G(LZ5M0pI7{y036w2CIg2GIJ?2LcJ4Lt3kH_ zoH0CPGJCE>tj*Dqpd8^__uk(tOW(G7`m** zq{&0%X8(OmucpDrPh9eFpk;@TCQI>F1UvjJ)eDX4m~g42lSOX~7mRr$wbM!H+_ell zN!?wN*!hC_PuMC3pXBNLgjupe#@Q1CCj8xi=b69e$`lbGdtxOqm>QQn_zK$x<7f!OcOvQh~_P3p}8mm+d(Y+1&2Yg!+lYkT;?@;3)}YQ9I6_bCDDNTrKbW$hwAp z`&JI6)Ud)#t6zakbAM6wr~%I~SOA)uPBg6wgnl<^dvIHl0wNx`yZW3I3Y5|*8JRO+ z^?=uh?N_PcONl$H&pI|h@SR8p8!KwO$M}`vZv`p*&?D+aS;_>D7Nu9kT;4-hH+z0B zzuf{&FFc0DyjDQi$Kih`iC#u?dgyx(nGvuOaQFSwizC2h$p<#3EdYU%A7*^i)8Nh$ z_%W77jSt>lKiO9}1O^3G$Gz4X!GglQxWn_4sB9~7yts}O$LFkEG<`4+Ol2;;i%s7K zW-m^9_CF)ReS1Zg$_B%TMSIAe;pIHgQwyY8(jbTLZrx`HN@l<{Z_W#zJ2r>NGxlYp zly;H1K24rL71BL?s4H@h48%EOaS7&uVl5omr0eENQW3iaI%4Y{n8gt9c`B{$!RN#d7;AImr(c5(2Dt45eUeow*RPiC^V6mh!<70Y+5AMnEpYUoa@~S-$e7KhZz7Z7Y@T{YS`PXWG8fDRA z*kSSb)G>AlTem9vi#Cz!r_*$R=n?2PoDyt#!+@9BdB0UXrG0c^@-$g*=^83}bn@%PfhN#1@U-f_#u#!sXDz3qM)Zi7v!f2m z4}teJ{QYt)M_}<Fs2Iqy3OoN)Y-eujY)HkMxnNR%8aG_Bu{mZHNa zR2L0TR*R^L!bOX^t_$s`Y@j~7F;oO^u^)IK zglQzdiG5;e1@Y$%4}GrPLkqc=zsl&aLN8b>=<6-CXLUW)z(2GI8U@>eEJKUx6 z-sv1tfok58J$+}$;QgjO$C~G}D6*kv$u?yVY`v`z{b9-myQ7tb{`<0xc#B^OF&q%a z&#Rse^sKFbkJ0C9X}R{1>6Zh&p)RC2^%jmSA7lzdck97s5gbxhnM!79^x(hNG(n9?)jbFC2<^pw+MaL%Q`iVDSb zL3KOmNx{vnKQ9*uw#={C=5xE~_wvCf6n%_%Pc?qy$08*x3FkiRE6;@!ulpU?H5)*a zN=2qTDKo%<>VARs{%=HF+U_3wRp8VJi`yBSO~4-6YWtCO6urNxqPHkRhPyJzRPWjn z{@>1zeb)FL^ykA9_Xn~nkX^(@xPDU_TEFs$FWp)|aTKK4W05Q{M0@Rvd?zb*S0UYV zFkpg#N#6O+6_ofscd%QW6&Vcll|N0hNd?odUH(h8PX?XpxR<3CyAhcPN=nrueh)&z zmxMbu(Au83nc6ZX)GXUzUm$*tOsBx?;Iu__qk!W84P^z;i+IFnw>E)HGvQzVPm3T< z@R{4i+Ae_3YgdF2xwT4;)0QGHSuq^@Jot;V3~>#Ivgs&|q7U!+H#DkOQMyo+oR#i4 zN{(yt?GUAcpW37O9V1z=bN@{t^93g8<}vJZmhj=r|D*RivoeKzHzHchbl9O&W&%Y^ z2RRhy-kaUq?gnXtGF9AaLtuX+HBF*l8h*lJRS)Y`pt_^D@WK-+9MW`iF8<;wF)x=+ zYee$F=qfLR>NnkhTw2@8B5)fO@rm5<6&J-Xs$Z$e*Hhv5qfG}CKI|ZmPq!@e4+!A7 zA=kqOM2}A0=n0=K)ijcx?SFw zj*@tyC4Uh4#QGpr3+5gM+z_ZR=Y;8D#6Z*g$RuU>(dWu7BjR3TXkDBe2_V5kK&P`=#w*E$@Ie`aXsXg;H zbe!OhBn!x0)M12)e%b93Vw=cs+|De^Q3z7B{gD^BMh+(hv*%n#S>V&-w|k(gLZg4(oD)}H&b0iVOo2x#q@)dZ z8Q~|fE}A#QJ8?Hx`CI0?4AdBq%+NV64EcDgsZ2e|pk!gf7ewg%m-?PtovIbUr#pGy zM0RF@o`xIu=55IFrki%n7rG&&<|bs&89I(mb8Hu-{$2%1A=M^{>iakE}_L|$D~?M3Xx#Ja~U*@bgn?{zClUQG&!!5wX2<=Wy8N;d#W2Mu;TU4 zAIuyyN73Bp0w3MGw2-Rg`oTUTCpO-q+F{wSjLy%JiqZ5@V5wg(B?<)=&>o4x_lqYO zu(*VauwBRs(h9m`lQ-OtXuN)1|3Tf1Dtog=jL#DsomxO>(kglz|EF6tLFZ zbfCoYx-Wu5-S&~wu7ByxUnYslJK2pF9 zR=00N57>I9@cyr*VRU@qjY|HDIS{r!m;d1$39L5z?Q=?d7G#Q_9M%}vMJ)et+uF-r zWJ`5dfLtsA)K!Y!>c52087nGxP4y+zUCHvP{MjsMjiB`+zew~&e3rm zCM&@B!qG6ln?#RBE!3`niwZmZF(25sVZ^LS(wF%UY#@KK+Dxqz^w5Ft%=4ny1!TZo zEhrhuhKEKQ{7gN0aCV%}0ikPyNc%jsaHvN!;#dthI!Ev=G;_1ZK4{GloCLlMPYdFF zFc|OFLS&ZX)4yzK4BN3^>^KXp7Q=IlyF*{K@#;D2h87-xyLU3;%6W-vgB71kbAA zy_7gDwhu@x*EZZk|Ck$iAG41lksqPX9IqF^`diVrKMWb*U-y2i2ODfSX{+$#sVe4CpCWb_3{~E z%?42DbWY(=OATsj517{oC31s~FEwMC_5qAqc4j2_Xf_>G`^o1h@UCzVg&WbE*=X-B zw!Fjvg}eUSRz1RmH*B*y!)5Eig$U&{9yzQyL^ipv)O8=}>$Aj`m{G%6&iMp-ZE{@k zsbKa@cLcLuf@iQ|FxTjHK__9OL83Wcbjn;8w}!Q^3u< zzU5ID@%N)N@%vUx0Y&$&hQ&^8g3g#f$!(V>5m~j7=2ts1NIPeh!sWt@57uRdf0N5b zvM1Y$H3=PX-$v`#oXQ&TRI0b8)nUTv#$-o7FiF6@Wo=4E)}pe(9_^M2{)`;|^XHWP^0OH`{fp2>omDeES~nDkyfDJd;6BAUBld=NgphF!P<$ z5xj@F@cSsEJD-9maP#J|l_aK3)ZSa$>Z-Pje1oo1jFc^+%4Q#>{}xH%rj}ms)BSRk zW!L5H^mqm&G8I7Amr9DwQ76s1gC8_1PXJjtVf z8~s$SNNvogf+4zwmKSyZ0=>TP52(32knzxceUD8>*z(L~c#3r&QU7_BuFFD!yQYm~ zKNC6JwC-5ar$k?b-IKaow|oz=U$m*OB?CZ>U;54A<9nb; zuvjsXaR`z9YF)J;azb%ua-%yGI#FT}ZHNkS|NEv=+wPp-L)9%I6YJ3f=(n^%)0RCQ z{#D8_ru0V~vMt9x))k_HAC0NqrC(BGLy56Ji62>@|A|{*yMO{KNG*OFN0GrQ!j@TygGfpV{^Mi z&OP?*Suvxveh{H9%k|!#6w0OWguw5fqB&F- zY*lxc<^vj5jyfX>YC%m)@Zr3)LeO5Ypml3~5wIo+D#vMUA?_2AXYXXw!DD@Lmv0a~ zf=_20Tf)kF0mpo*F=;~y%4(jg99$*%fHWQMvQ(?+lT*YCv2s!KZ)JxO9iQp zCSMXvDe&)`W?KQoyDEaLRl}Z#5*u!}2O|a!xDt3K?2j4Ie+d5@BWSRSt{)nDI(V@U zB;Jo~V*R-Vw7%Pt`7JZxLI0P|;T7Kz+icmD{vt~3P5NN7TC@?!-x)R7ZTp6DLi~g) zBKCnxNR6w_7zLc~X8A&@@(=KT3mn69Wx$nNG0<;m8C1WMK2}&w3wPg!Q!FfOq8_UU zj8?c1jHFV~rouk&%={*3xt;VXL0}ZnyjWE7_Nql6F2#QRo3IRC zxkU0Fs!s%(;`NsseA|(R>u5{vjbT80)T_&OY@Nujo^%f*@-7X%cixb`C&k`~AcaU! zU=Kc@jX%|6;MX--C-PgPs4l?q0f}WMNY9D?+?ucrXifz(9=JAwCQB_|WXlu00S$$t zD(6=bR^;J$^qUt;zAffvt=a)U->8dZK3qhsb&KHVK*L_6R>b6P z(B@6#tiP-Nepb3QhlKC_H(SQI3t~!ltRf_SgQ+&^$;!Z0U@JeW#{5~rrHAke*4@B*jhrXRC>}963d9A!8hyk12IBh+e^Za zhXGyXf_dEQWnkfCAHJBfSYY6xEN`p* z;6f|Ne=xV+b8!a^>a|;EUtK|!ZI6vl9hySqtl6K)KT_iUm*&rP30{qVS9TAj91H&W z;>h)w7w(A7m0RCbW(9a`_bR>=C;EF!QtxFBZ==LYX8#U60l43qMO(V|qUbQ|_m+;N z_?5s>|6j|CU_DE*M(6`2v{K*Y)X*e{)*Z229k)9`UNnhUDKQ6#_FS^#;?^|EvOv%4;HpoGG`X43B6l#BJF)H2qX;_E(?UwGBM zMSTN|*6=5%aIoUiW5=zXp8NrEZePfY^ojZQN&yJX;ebN^`I||~gwF8xLF&CgYFO`4 z@_bW&85pgc`C?1@4YZHG3vl?=0K}}sqrc5>A~4yVZt#c$WN~>eUtGy(k@S_=*%}jVCx=e_soDPN&TMteHUaob}oi$D6_W$`XqENdn() zL)hHLjpN)T*=U*vfQ`lljjNU9khIa?-n?c7C9$kd*)$M&%v4T6Qk!Ko!7tKwRe=Ok z=$tR#_Z$GHFI*ao$RoiEORZ$sY7^)xY;ZJDuA?X4+qwr?H$ZYFSDi=0JR*zMF^kmU zg$a6!UxXrd!0u-I_bYWnKv$|UbK&6%B2D;laq2x8_NN>SuZXw`q^4xd%BV=Of@;vb z6bm`la6NQ<*JJ}w1n7b?(Ngej>GK#6SwKx?Bit0y(}4OS8s5LT1E!Qm&;E4XM4WQd z8J8BfK~BX%4jPMfbjtrU7$wdFqfSHGJK{V3?Ol-B+k5+HdBaMMoBj{dT6fd@IYZ1T z?~2fm2wF_SJoA=MbPK5Tm^cIs?E`L>q-KX6QTSTgKcHEgxSyTvN)x8m&}|yB-UM$t z4AY+cZZ<9jnJPVZ4jXkNqi(BE67dNjLHE7E|N0mxmWy0*m|%t*hLRLKmL!;*Bjbe0 zxiMsJBk`+*bQ6^SbIleJXb1Z7Tf(=>2>sq!N`xnV6SQTz`6}6Vg3@`8Oyg%g=z{W5 zt(l2Bz~X=P`ccYlaH83Unoed1z1uw@_$_`K=_YZ;Jo(%M&_^QNN^ox`mR0He&8cwb z21&Nw;u7jCDxM3+3#eXfr#2{Q4K*M2&MdrF0?xL5J)zyuhG^W)Qd|h$#>}^e`QpUi zahED3BUhd{Cq8T@k=JP8Moy3W!A>F%VQ!XDaG4+SMRG>@w=@AiS=$et=T^bAmebnX zd!wLM+mK7yfZ&JqGrVeA`bg+Fs!YG9n~`<;@IZ~wHcA>2h3e_K=%i0mZKuU7($_v- zIQxtgdNk)5om?hzP4{)=6N-m{ZHnD{ub63|k{hR3RoD%(<&yr&q;4VIXbxAY))656 zbZGOb{|2I4nW1A4Swxyykx^l#L%>#a@$D!Jp`!*lzr6;=(J|KMyW7W_0cY0YQ|7uY z@Rc__KhCZD_uBVBh;vTBxI#MUA@-c{KBj=u!B+f_VJ zGTQ?M`b^36ZA9)~h&fodrAojroVk=YX_#*P9sb29TEV`K6Gi7{LuS zTxS$)N7biZb2%8(<8ec_Np?qKF8?^dcD-Z`ZNoyoR^=|FlMxgpyu5=#Nt$w$Bd1aD zv+{{P%@X8&{5sc8+XSc^pxe=Zy8~{P=UvLoodIi|zoIf4ml1EK@la~WG&mJioFOvb zh@#@URWS1=uqiq3FvmC#>gIy_wWHgBea*9T?81Fu-NlV-BYDL#l2`x`%rRl{4hFwSXO4GoRZ+mvENoD_{3Zt_Am2>euEfO^Glx1o@g|? zWIg`TFQ9+9a_ySTFuKE8o@zwoMZ=ltn82NFuw?RGS=f6KMM-Hj^@`EL3=PgXFja@z z>qFH14{acOg(Q>1TD#zlabF+9%_X#a=6dbtM# zEu>IeN=DPXVg-vpdXT+?Fpwh!MP@B>JHFv$*?;7RIQDw!_`av89+Y(+UnSSx2U|Athib|npcm0*jHe}c(W%YT zqU7J$U}LO6);|h9IG%`W^FEV7w}9ch0zs6x;gztoDbcqVJSQVbTSf&d3Y0$m3*^He z7&YmCM-ctiHD}qNRwj6(Bt7B7VZtvvThKYLw}_7R6&}tY?wz+G)6svTx{&KF(r-3% ztsq;t<02Edje7X000$DrHW*+q;_}&(5w+`@e!|m5QSNhTq!U2k6 zZ*4|CIOtLHVR>dBq%JeF&;u@PJ*9sji;EVwt5)|uPGo|!HV=qmqiAD7c0SuzM#{bSb}$+wY5Z}g*%1ME;;W0T>L z!a7nyS(gguC82FOBa7XygHXVRe(s<*GrXuG#gl1Y0Y;Y=&VCk^hhKs-G}{9X;*pht zlyg38xXSCTasOQosG%8MI$p>ORXGRX7kZ*M@OFlGT2Tt`HyP3$?-zx3A^N#l1ZUgk znF|Lu-7Yv)SoL|dRvdr75%f`)TMkA`{MWPCO%5xh5(g{nMd4{<%RdGLzu^T_xAuWK z8W@m}_=%Fpi&RBD$@jm^h*c>$y~z5>p>Ofu-Va*?AeZKr<(KmiN*GCSINu||D_Xj| z+=Nax`OHY=TA(asUQA>Bbz2%2r`l|v5j_CET=w1Aupoi5NnY3{+4@d_cn8`flBd!#(Bbp1?4QSMlS0RL63?Otj;knXB$16^ zz<1-em>#>2pdX7=-g!G5Pqb|*j9ga}5B({n|-W--1eS-l|KFnLK z9ZPUbl0x2fI1qkrA-^Ekd3s#@oOkNK26o6_ap*}0WQ8w8KRo+2(*e>Yr(GT&VuxwY zzhW2VB(asK?b~D4#D0Xwk=;jL3d6iycRkffqGv$oohr4r3eKOzLN0S6aOQ_@LHIDy z142@oiRaib%DUpgokS0>>YQ`Y?V^L~Cb80|UeUmzOEnC-S19mS6E>(~d<3ktWY*3v zvcgx=`ue1o7g6@&k+69IPRx-;;tRggLVX5ZJKk-AAAQmF+KIO*ppqwIPK}-%+f={R z|M`j@%dRt=ruxN#FY!IEHt-$l+DVHWpG-XJ)|p@`{#Z z6H`FTcyU(cf;fy8dJ~wZQ3-Ap1~5`Ag)EQTLvEcC!ty1ZjNuXE;Hm=CJ1ZGhtS%NL%ecq^hjGXYo#HLzA>)+%$7cua zdW{sD5butd@p(<_@r zy!I~?r=|Q@@d%e3+u93$eCwKT<<>tztWgweXBtff`~J$244BQMogI0!bCwy_E9Ue_ zbu56mEskDo(h-y<8@@<;j1}8_NtJ*2f*$*E+c%t@oI`yzH%qpq$nZn%W%1!-17MUs zlAGcYi}M% z!8`h3^QtW7zw`d+SG55UB@&u9-ok*{8PY8%Noa7fpYNZzgWTAkSL(mQGQ)B)|A$e8f2+OYNAmueIPUgKGhlJ&gZx}GL1{wcDD2kb z>-I;+QAN=m-4JJ9I2;_O@#dN|q<*`uGvvevZ;KvoIYaExxOQ>b_}fqpdf&I3I3BWx z@|Q1INmi)Ae!IuQM<4G47C5luRmh1Qn9tBV)v>_3r@euB{>=FG@e+oD{}w>8B_m?n9;`@0ncZDRH@6JLizkBC1i#y0m?Y_&yvF zmz3vN0{>pEpVRGOfIF8$*>c5+{`}Zmk$9q4eXZ>9`R^?(c3&bmBm-W5vI00TeWu-hd5Q#1+VJchm6pUUJ1G$>Xcat-5uxGP+eg(tXY1F* zmQjp?ZS|`a2F%favMytV;JT>DZFE8g_#=}1v)D&z_)_&kUMe>kUi*|R@Zkg-E;yiC zQ$)&!vFFV}w@1WW(D|1n>m-3b6_&?jCl0~+zPq9t1FSIn zfWKw0kOUT!eHWX;x&{36w?vkdIADn(8a`9Tg2(@TJi@;D2fVrRN-I8p5Zv|&PZV;Y z#OfzAQoj;hm&!u7S(7zdtSdGs#-rVe=JwAvTfJ^XGb3g#ky0DL?a7xC(SUDA_>^{G zRt-A_`WprdO;lLk`KzU=>nJh{xXIZZoCc)&T`S#Uc`((JkxfpsMWFubA-VZ4Dh%>| zX7>>}_G8nBzYG$6Psvq}h06mXI7Q&rl?xk!P(QWSOgD)J-|9P18J)%gBd=1{KC4xus*#=hl&7IUw(4OLb_#K%LkixqXIqlUvl(@Eo5{l+by5ah zClYv%yH)Ov{62C$mMc%iMg|X%M%8?&R)QgkALnD;wvo3&k>~aWVsDDPNB7H_1!Q|8 zxMSb5A4QgJ!j~LG-=1M*@Xak|{3dkug6gmeY&y)zFkT!7z|40js@aNe&oomP$g<-7 z`*=>zmmfFlaOKVC?4c!HLCbpyW1z=SNv(Z$40PUpo)k&&0fHJCwDd1d1Ii!bB9R0~ ziBdk%bKv73D8I2y{$Z6AGwg+j7~WvQ`p(O2NSFm5zSREr`6?F{w>hK{{g(x+Q*N(Q z`V!~jq|4l)Yb%Ji>RGg#7a9E48LllpPw)!|ZY7ovP{94Nw4KTc64=1K>THR3(MsS+ zS`Q^sy!xLt`IW#OKzi2nDDT~M^q_6#ajfP5N~w9L6_>G!UePwcxEQnz+&@i@*q>p? zS1-2m-%_K&fp6%0N&J~%S(~scM=mw~NAu5e;10o$x$olmq<|helPBAa+RX!pXbFuU zgM#pC;P#6Hv|E5DMetC&=OobLP~S+l)tf{&vb|#aaK=zNmZ33P0`$7_Yb!KCaV6Wlv)!JfwNsp(aoqvdSKNGp0-n zDLrCz5;xb-*?IQYM+iRPiF0BSzFTYHv9gkqVRSbr=ln^cyu6EU&67obdanvO_5Y~p zU1z}}2^24$1q=bf>1ZX2U>>-(SWJ@B#|T$T>C-r+%fPZ+nCkgaJ{V?VIH-J?1{$2N zAn7iVzy{WIRl{Lx;5g|wX{K>Td{aW?{>MYv0JU~rqg*4w5BxC3ObY6Gz`8Uj=s>yO$EbhCcpLkWrB|E7QMcg{-OsQ(zhF~N#G`mpgZ-I z^zh|jns_lkGOQYA=Bz?+wy#3_-kEdTC}E|k_W5xcD0wKZ5^gR6A%^*XCPFV8O>ng*+k=PkK4_?`#bVu39-F6$Ff2$2A@$dQYo4f{4B+sssb%CXn)1^MsO%I z4p^}KT1`uZM~lv}{=6)TCzT5>lok{H%~z&%WKk=~?rO^N*$F;q6mOnrG&2IOxkzr9 zT%m-sJL(J@YGhb$EPOO%nV1*Z?%v)SV#Hj31B4gz+2GkE3&#_eiF@X_y}!VH9_;YA z#o&GqHFSSkm%G5m3w>ioh1UZGaPB0_qf^T~u=-ENM~7W*!r$q4^EKzk)%X2Zo-we& zf&zZ$dty}BeCl%u3Be&b*pXtAU&06fM1Jv(Bm5wj4`BiJsyxu=^)t}oosY;Y>c85% zGvkln?zFGBk>mKwT~^M-J{_0T%Ss>2+K`accFeuNgM^9c7M?>X<_e4d#jm4_tlwYpT%^Jl)HTs9N9&NW zgCH&a9zEPx&nE^90jPH~Tt{__7CVyjht6H4gx9z;^RSskc-h#}_Y5!aagE z|DI>(kXb`g&hf`bFRmlq!!tMK*``oo@smDtf;T|srSH>h#0u|S&h}Px<$xi5lg|^L zuA@($|GDxwH-q!%PRgoFv%t?MqP>TF+3+u~+bVOa&4{jJ>7EGr7I?oEP1#z#f~<>= z*WEJDMXK))wV9sf$8(l8nVcjdkTWJp)Q8xAVfp#2$FdFyR^oP;dK@`~e62S(COznt4w35wgUzCw`T(VKqLxAZntIl9k7PK-_>0GgwThKPT=d zt!tZ?;zKx~O@m5*qj5hH3L^95n`VLkY=V-E-mIXQ>w=O$rMRGW!;h}oU zi#5O}bBazfjvO*O_fDila$=*=pxo+11Sf%QCi}Hh;Bz4X zuRGZvfgcXTK9NMe{d(Br`xFA>Xn`d`I*{NiXi8r>zLGkRI-88lj3Kvo+arlHp(m_S&3`XqQ&(?yAw~M|NpJa#3mL!!= z8i>4YY{Orc<8-*Tu=9vX02O9+Le~um4uK*)d4xbX8%#`lYX5@g5L{e{m-i&Nc#5X; zL1|sfKwsX?@uU%cEQppJT z@IrL-{o@=k^fXB|bABJyI``)eFmHjkPGmwUgim~Ujn~EDj|lD$x$@9CnGAAy2Hrj0 zK8C`qj7padQDK><*H)9F$gp0D^e3^+A@ESx{2Y+r!WPduJfs{4fWRHkrlJv|$F6F?cYWMLwuS98sJ)HQsgknl6(&&j%Y$?^GsOPF*WW2$>JfV9R5-`OLSi3Q zIRAQ9{XR*3JHR7>mM)svI#BW8fSwxnJkkrB z^dAi#MLm|3!mEcx;GFzT6-|;sWKcQHBJ-OWudgT5U7{{RLQ@I%)xE}n@cTytDjD4P zJHz&)bBTP_1~>Yv*>Ms)D)$gImI2{~TmeQBuAwhk7{%Sz)u zZ-V2Rhu_*LQb7+k>wlbtpV{b4YW;|G2w20Qw%aF{!L&Y&2nE46x1$IUaB8gr43kn! zudQp4k7jtH?sbCu(Tc5#=_iQ%&F`_M^eI#ybzsT#+%_2eLF?F*E)VatvzfW4Od-0S z+p~)djQE^)#R~`b31mTOnO-Bfg+7{%CnIK&$7`OX4M*ZVa*XynDY2JllrQQ8wFWbmdm3VNfMgmhEo;W! zI5mM@h2PuJsAYj?IVx|w&zS)m!q-|X|MBBX=^5P0=pCVBX;$X5en79+_LJN4DInb) zO5O~@$CRZAPRlw;g1wA0rq~Wvpmd&Tvj&1bFD$qQf{A{g2@j7|b(09Bm?0WIL=PdW zqQ>hf!F4F_iXU0d+CWyt8aeKlf1@`qrp681H_>V%MfGihBX1|Ar+k%>75_S{%C~)H z87#K-rNq$_=V{U+>R2f=ehw5*?^F}}ZIZqJDG3H!&MM33;66^+Z^U2yGr{9?849BQJs zQ@vJ2f?pSyuU+dQ#}8x0*Z(*Z{Y|}Z>NQzRSg)5(py1ss!j9ieoOejEv-hbCn(}R= zCmJwAE5HJ?I+^@pC3y(VO{q69y&q7MKG)i}{{%L)a|(KQsd3JqrOXFKLt}rc9-d3yOOv#1?e0*cjZY6C&5oBOVLt>BdfrXVm3LWzYqAF&Xn6N zC&wLiC&wbQmrUP9c&+a75l`DO=zDj8sc)Yha;@gI zQKgKdfU}ECDoX5dy*&OMrdvTA0XhATcF19hZX}iIIvMscqIA%CyaXsOQ$I^5dOnX; z31-*KCRlA`Z$6D_uo&46kD(?Ph@!pgEsDrTgzu6nr&b zn$nUJv+|9A%1b2&XWE%@a@0+lKZI`>nDwMjV2&DZryEoXb8G{}Xwx*RHDV7% zNlkZbEF0|9u^-DZCOCbUq@p{je}C+TKBKRkKja9mlfT?x*Q1NG z$j;E0&@td;u(+C_)r2 zgXrBso{5-*DIi#MHRN^bB2pCovK<$_3f5ec$C$1&;upURK9X!Mp)2(IE_V6^cU}I{ zb;i)IsN-gQ?^0qfy^|_hj~7s}pvsy8dkOm0!khgRFA(z) zPeK8aNAL0B4l^gX;(N+r)2yz$sN2s?$y;d?Bo|56sSx?lG#-!YfFq2MK85elIYz=K z_nKA(4?EBg>m=?&ysS9=OFNA@QzHuXGr(s{IN|L#8Wa9TD@ab=(?w%@5Zuzud^1+C z1&ZGPdjB8w3K%57Oy|bPu=vMxg^~C<#DAy6+2{lbKDc~cZZc&RvC8WS>Cg|L%H_Ab z;=JQ1u;fIrcnHyJnwRbzF{(hF#}my-JNtlPjzrLMHnAUzOvCVC{}d2-axvbLqy^<< zFnAv(=6HUi^JZEU6u9LVPa~DaB2c5dI#f>RS88XDRZR;t04B`@g`>GU$T(n^Vq1#H zi)6ZdWwc&Jo)NclMYgtpeE9z4{s=eZlgEkj!XohT!4E1$Wdu(?j+gV7{T}ja-pn>^ zBYdx)hfO{`A%ng28|sAJ0Ny^RvrR2%M^W?bPw9%dVOp9Q>&(k_bVl-;*NF&feB9(% z*M4Cgu<<5&t*f$(E-cd?O-~vD+rEyP7fC7*xP5o2;o%zKR>_prVI+aiYhd}MMKYWm zdwNZ|VFi7)`dzJgej4Hdk2dAb)4*r{NZD6D5;_{s1JRwP zF*G$56OLpykfmMT&CISY@PH+&u#Kq;)k+jhKhWL-jd@pT?(EMaO=Di)uU(TsSAW&~ z&j<-FTJ`+l&hQ-xY$iS`&t3)h5jdI-)_^V7{YN#0V_;NLPPZy<2su_wX&h4C0H#B( z`V_Br2)!)LgnVQT-6IJUm3=t?!mZ~!?zFUnb8`{jf%xtS^W3vHX1q4@m~%LGu9Ev5mifH2Cn_d8s{8Vk+cm1(}dv|NEj--Y+g+Uz2e=6 z&Od1d-L@kxWdci(Q6y9MB-NMMNOR?#ZW;4jY&Xduk zHG^KM=+kAfN><7#y?idsU$bYe3HZj2$m3wic&H>W_2omfRu49=+dPxW9!9O)Pj@yu^3lMAqQxESc|>`3 zV9!ot7Hwbr@`I&e0Ii)=dR4%&gX%t|xtucHL~lG#tCJ5CJ%TauXY#DN5N3=B+#giJ zZ=X4~qAz5SCMN84lk6T)QHgf2xJL#rQ;;T4z8VJUe|esKl%a*j-!}SUe$v6;_dJ{A zEZJeA6u-#yJ7MTp$zgwk;K-;x;qzo6^w}@;dLOlt`v5uXyT00q0d(x`zcbVmGa$E& zo6YGREevS(d7?(lyKZ3yMeTnHo^^1mz!UOeL?Nk2{;gmSQ0GbSJQ`XWu93ga>PGu-~H0fI|$V%Zg4oSu)-UgT4W)1#2%vcw?Uy7worZ3M)U(a zdWdYkYpQGj80YVjberJC2F$ODHWGV)m^{yT%FRkcO(mszHYqCTd^GF)Kdpl>ZMy%9 zn@cG;RT5G_-*pf&nO&0-mz08|!>x0RccxHc*T=#`?Nk^EB%JDHoJLoKgL&p=^8NUx7gLJWj0T-Co5v8Svee{7!^qO9njnZ@r zW&7&4C>)lCE-{>660In3*)5S1>lGaMQKMB&039d10^END8Ue<}^_-|7l z)*=6vfXbRJ{~n>|sSEh1aNXVpGrL~vf6~T~fA-?U&;x21R(#{XZ)+ToTrq|asp;{R zWEqD<7IOIgp^g=%D?<|*3lnc46-;VznH4h)2mhn!yyLO_-Z*Z1?7cTdWGf?mJ0~=Z zrj%r-RFsjeq#{B{qNs@M>`GG3AuCzgd+)vX_}#xh{pa<(p69;LxvtOW{r3EqP^n4u zw0BlbJEN{HgK4=Jb(fq+5V24_WyQ{jed)6=KOP!Ia(CW;J-x{fWhI;>0(L}TrgMJHrk~8J}@@5?r?LzFGQefE$K#Uk*;$;DoM8cCL`{oqwM< z@A%az3rku*nUj5H#kJX2Sxb|+VERVwmmj^QU^0M~IYvwcrg`%nN{>E>m3bJtRh)z| z{+&CJaCr+D9xU#xHl71bHuhv4G7ErpS5`1}i3!VXRn)SD6Mj-EHr==o9&BG)>9tK7 zMbF>fCGFc<29Ij{S}ytULwiiy_j&&~*pAPvbu(T@wav0A+qU#LrLfqXZJM}Gu->M! z<`Kt)Ako0YdKa*ovwC@qY=hVLZVRMT@I!aeGUG2+1gAD|?8%LX?D(jcw6885k?S*V zdvtE12UJfbrw*I?~WDZ$MK0CCE zI`7p<881@f@X?^64{ALiu;*5FxHKg`c10p6%w7Nop5pZkS+4_4uXR%z$Cl8alb<4N zs>rb3&u5>& z-q5?0bC3c@(<_9ox^AM^uMKplyZB*d%?3LEVZebKeMUv7wS z;#{&)=9S7dZP#*YjvJnk zdc#tmvH|KAOt186&Z7?pq>YQ-Gr-A|B<$eE2~Kg(W{LzCV*68c6J4vp%+8r6l|SzaAV9NcCSt3}iNUgou3eFL4gn zKZ?XWV2+wGf_eh5KIdrvaGwrW+`d=p_<<6x+`ne7Fu;I2x9CI@6b13kycT-0Qz(|L*si2c2%Bn0;*>T~%*5_)vA8;Jx3yDWqNqgq~zQ%5oEh!nCH(nk2c29N>yV zt)d)cN#3sAxx9g@%g&|y^Ahuci{&O>T~rw5bOm0i7RA<9jTx5={-U1ClwW+Cd9lMQ z76rTLX=KT6ENQ(vh_;LD$nI3J;Vi!s@8lFIVe^Xru>-`sjhW}J^}X(SP$NwFBKYZv|R-^saHhN%FatLW1ws zXmDBP;=nkFXRK%U*j0ueLn3yWgx8ky-PSY7zzuXE>4f=-DiIhWdiiAG_CLyoBvvov~BQ2djlu_ZT31jn`I2JI#CGkh%({3 z)fYe3)8J}JyxC>6oKW$0!@bSlL%h)*)C^>v||K|8A@S59u{$YgRLGsi7 zH~6{>8UDGre!Pno#_`0E;thH5bQ*8Pz5;4|gYT*O{GAEZ&puM*>_v@F)+G1{g|Xpl zA;Ko}%j?MWI?Ic-ax(ndXjRN_iLk>w(+POWz=KC;x;_e8QQ?gRvVk&jVlK$(>)b`Z z43@Gjd}xEXV0Mi7O^udjaE1BqoHtlNxwK2n3pFd~s_fliks>CX5*0oT+$I2pX@-LE z+zNU%zVJst{?io%yQ(~z4-oseWn&wfWl>zO)99pnpB*P@@MT=PSPcGKy}UANL~v^l zEXt;l6Z5L#IW=1~LD+7|%QzWC179Uu$w%H_M2TlpN=hIncI=mWPnER|<_y01EylNl zNXicu*FKSBzSk~hJp@NwDlB4s(2EJK7y20{^K)SJm;a^PX^_LjWD4=OnQXXbsVzu> zMBLNjN&;y8mr&(>N~tN|uYmr{h=ax_I!I|58B!EI1ooWS*D}O7VC7)KSFO!)U?fSt z_wiUeYE9!DIbux0vSr~V|CR~A+kuKFZ?{C9l zQT1vnnRu=nrPNFW@L)=o?>9{#`Rs_H z^Lv7iPdT0f4~fB+hkQ*QZwEnWdg_-yDKkj5KJ(js??qtfx-uxL#)E?=?n|mH^W)`< zeN81S9Qbcf$S%XTRdjlW#9Du)5}6t1PP8)d5;@zBnI^yq!yP`Iy+ZIEm&FnUC5V1L z`y?k{0>Pys^^FUV80U#x!ozf04|Z6fcQ?Yrh6|;FfySNb^8gQ2ApEYJZlv?@>I@Hg4 z2e7h13z121r-<;rCNSO*_!FOk_|4QNfS8+K+L1Y&L50T^(?v}lu;BU2Va=rx60oyh zC7|gZ!7J%H@VUvW7ZDo1wHHlmp!NK)?|eHm?A-giaekZ~{$OX>_#n%S8^2ZQu`STT zQ=Q-2bh+CB|K~3;$-4_6bc+3?g4PHMKC)b;* z%+s>Kw`$bVG4z{=cQ=*XGVCA>txa~0;h==|lZv;MkI><=t+qPS<4v@wR83C#mlEe~ zR2x_Ea$@HV`q>A>p1o<_;!C*47BFtBd^D^xf%ae1Z50rs#zU~v`kOAn<&~oS7QV28 z_A}~4lKkqyS<>K^Bv<$0re@;`cbrOfov{)$R8no;59&(ejI4>^$*Ju&K?+6t5!1m-9+qb{{~$PBl7G6omS^&I+6a1tFOgNz3+ZaJD!;47j`|K0w)38>DCTIh>0!l>|PVvP=5Fha_VLGd#g_BvI5dP|FV zPKMC>E-kEr|BiFc-`((u4?)4;)pR-TxZQ>lt**jz8tT}-kVEz7! ziQG6QcfOmUlmYwcP$!bBjv@c|K@^GOL&(R-*39oI7kp}Z&=@c^09f$6o+Q~sco1C1 zduGY-1bO(Yj9{X7-2~r#h$ZrezXkGhUo@cbHzMMZUg5w_C-Z1J!I$mH-qp`KP4Eo_ zCQ9D1v*DL=zyHwja>1NotJ+@_Yryb(K8s6pkyd`xs2eM}O2aOO7!R{W^;)sLKnU-m_-WzLasY&jdv zbqR9#8bO|uRSv3oq`>Ey(QQl3})^G3ieN?8>DavG5*S-d_oJC2@q zztRmA8v_AL*QlyP*MQbT(Ti0Ce`1m~Hcozi0!hD8E@%>%0rKk>YE~VMNG3Cms2z0C%KSb`F@i01wEK9 zi#^k|2RH`1SU*~SMiah)RVD9cK`Mh@c3d494$u%2FbkOlF{#f2+U0og$CmCeYN7{3 zOD4DUl<4v6eODO&$@mXhm^-P8j1t_sr^jqRvUh@2LoNf6>jWQBzG2_mr7iGG!&gp& zYysV?yn5$?(hj)d{CM%WHL?^h4L=*##zEdT~bstVh*E4LRZMCvf&p2wp zru<64hkKNm21|C7M$y5C4E}3SiVPP}yfURsTm=&hX_Zj~k7dLt?8^SsDPY~ir!jSK z5afR<3r<}nxUZ`3zrH4N-v`q#-OSyYL5%Eq7s__WfO%b!&}G8!POBIzX{00pqX%r( z*OY0XqTPFb+W0Oab5gPLZG02e*}bJ;C?vx-msdsIc_{IY+Z(}!j5_qy@0@gNV;R_R zU$SgIN$in9*}SA3H57L>sPrWA9@^3~`NJ}sKxaFBrQkj_)NxymGLfG^W{byT3##W( z>u8M1fyH6;qX?O{v6Ev(gWP&9hjw)0TCV%O)K#!7%0(~LGYPD6lvN!DBXYW$T~TQKI{2FWuc~&=cKv$|HRZT|Mc| z!n;Y0ADTW`7@sG{lkLTFw;D^q&#|BV71K+|;r)rQ|LlqPD_p7D6x~6!RPN`lShK_O zJ7Fj7-faP@I=ze6#ofRyN1xqVaVj{Jq($p-{tsZYxS`WSMS+i)ra1`(4g#}<^?g6# z8u*vV(Tql#02WOSnN(qd(whl=&xm{Fg%moS?}p_lm_%E7=JX(%|7oex;8KhP{3ZT* z9o_)Zm)-1dXH#Ks;};%}o7O?U@}RWa(Ok4BzhiZh$eB}|RBC?YF#+=b%2>soq=f#m z|2EaoD?DM7aBS1z~{^`y3oE}+kRja z<%o)C%SG&>U+bLpe%}Yc79*we62XsptoGTMNs0#+dr3c04qic8q$_%jCs|>yivFAG zSOy#=o~`R)Mh2yyZUofCtRsD@FQ@YcR?wl7;1YZK3|hRb{otJ4F6i?cyn&;rF(ZZg z`G!Fjxak+&8p*PYHXc;jUSG>bZN1mW3lA|R43 z1qIG5-<(7#qM`%pM1IG?5~*iBjz_PRWM6FlSw`oQlk2Y8t%FOEGJigs*C4S;lFs$F z>!367fvJ6GFY2{6YYY=x2E*?*>Ek|7!vv!Ik*CCf-xyBJ#_CO?7UJ7H_oxLe{5dwh z_qG21$dL~Ku(riBuG8M*l>%+R%Et@m%9x>S$!F8;Q#^RsJa>_-wGCYDdSHGl zb_=EE>9l!BcA~2$C3f>;OTg%JnH3w;7ZlQP}Onr#H}%V?3H*^PIiXAUx8{nb_0=V60D z$^B=neg80`N!qS*E$;*ovrThe*+a+)k?x1ZO@afvIobRFZKB&B9~-H6eF7`Bf1Ud$i{R7=*fO7ChTG4|mXwdt{Z@O!(Aj{Cn zQLnxgEep`fWr=aa%%cOTjDW~-KW`SfwcQ8;uNPZWv~42cB2u4!`WkqzIzCY_OL#Jh zY*nX-dB&Zk1Pb}1n;?9e?pAB|G{}gYX6Ce^$E%XTC6|aC71ipo1LMUF=xLdH#tWfk z6PX}Ple+>|CPJ_A?{Byy`d>NlH+o;a_@SnaU_G?bXEu~*&%aucm$O3w@;L9@m~u(1ee9J(5#CQpgW zU$8RX_vuH+wcLMc5Z>&?{(km3c4FQ%p48N7zYm@fDZi_?H;)|DOkeLIO4v}|qbAPT zh3YL{@=o9`6wZ|OhhlvOM4obYdr$16mH&NJprsr`HI?URFZkA=eO1>??bFF%na%vO zHq|Y_pvL(9_3sAoW0T#C`RhMWCi#V;dYh5pc>lNNb!!;7eg0lUa2t?D zKiQe%t!wDwAZua4Ob2=xpwun5pAEZ=>Meggl7NEKEmBqp9~w>2ZzWS=kK0A|$S41O z6)GO?y=!rF7!8*&|5SDv1>YSjbA>c^!3*oB`JX7(&};zs?L0sMMXzsp+RDzLxu#0N z7}7l9{gLM_JhBYFv4;$`W=^4A{WbgJF4Q=R;JYPz%mTh3{#qr1Ga+^8rM9_U4#?bJ z!Z`D34s6N&IYpzA0F?GF&~`*Df;-bW)!#M=j!x(c@}QDgjqFn#W`zDt0hES;}}oxl%SZ{oloht3&5al&ER^#3}W53p$@&gjO5-g z$+X1xgSL)vj>Y*^;0NpulFH{ z`-koR+YzLm@T_K|egKUaB=u_&--Ejo$0`3?+Xh1)6F#1y-2f>{jD3rvGr+?0ZIoLt z84iCR)`!*mfmY(h*}36rFv~d{`zejs_tIL$lPVLyzTAVnr@EWadX-b()FoOx^g6$4 zRB;U53qJqntQZ$Gy)wd}I7g0=hY}6)UqvRgVRv&I)k5b2K=~T zfa`QT%MlWGpNcO|X;}m@$GC;)h#pbJ7l9o68F5IxbCmJ0HqmqTQJ$6C5`Zo~o-sTN z;*je%_rFpuf_s>4O=e#~gA1Lsj0bNwqS8i2l2ZX2^#884|J++noW7|>(jOFpwnrFy zlJ}N@{?N~LHiD~?{_^=e+X^34OFg0)b)OG@{#EdyM`|AV(%<~>=BF5D+J>+6{8mvy zeAAsmN`j|95oT)p0ey9Youh+pkbRlJ5OQD?r zG^f2JFQP#3TeVli9>w!QK6{yPbgUA6Kj?im>mv`euhYKEb&(Iw>Bw|Z`HZ5SvTF}B z895<|VUtT)V-BR`r;u4o%fp_JISdcQ2yahT+|9OMitsBXbp=%sm4xtaS0;f1Vv@Vtf_>c_YFB^>3zMrjJ&u?Y;&W`s_5{t^@XQ^-EjKCA%S zH5FzZ_z&Ryite8K1ldYdk>(dsqiD zJ&9cR)&us7;-PdfzQrXoQj`nx=|((qdn5t_3pRC#%n1~wf1W&Ba1bUfXVKpIM9hy^ z1BboTI?%nlcMm8Y6oa?bIKl;@8o}cuyw7%)iC(krytyZllj>m$dQ4~EfP^os|Dw7% z4rDH!)G}HWz{5?R(_E5#@KtS|$^%YX=u?1n=g#qB-k_-3N19@ItH|qDtu`m*I`(O- ze)%xZ#SJa^#(U;8W>b|KJ8YzENKB$y=oq z9ZWmmI(SeBduqu0H>WG&I5~j0btXS=g7Dn6M>anW5P6JS@lY+sem;Ky$=;$+P zj?e<+(%lrVS<(&cbD!5pOHx3Fm4X1R!?gHN#W{oMyDX69{0R%cN?IK3{`}?G(OJOz z?`c8=@f`5tR-9l}A-H|D$M(HHy@|G+Lflp<55vEk*D?~FIB@3|r9qCwJkO{SLL-0wmAumpz%udi`F3?)XmA@HA@AnL>>W=e4SsC_0XMg)u4XpO{y6UY=p!mv z5+ZXtg_h`*Zq9nj5qr^0KfWsJV-z^F{T|kJK7t|j9_a7o`A&AeX{#hS%&v$9tyVBAZSpCZvh`0pz*0~9-fu2Zd; zCJ{Z|r46k7_i8GruWi)YWEO>k6W`j%dX?ZL?*zF>$2_dkK=*&64y_N4X{D8c~6bQ z`FlWUTyt9Zfe3VXc2Kp{R1k(aX>~~jal^;^C%CM?OF*@}|6V}t63{wHdp_hrCkmgC#Q%WA5AoM~K1jlOxA~Q@I!^ptWtI1G`5{S9h@SmjR`Vs!{~TX3<+B}!QIUOZcCqax@fx0afV3Ehdq5k_B>q*f9d(BF zKf){SA=H1*+JqX4@_%fLA~>ErgZ|Qvy$vWXTmQVscMeQ`LG#CYCmoC`vzz~xO$%9f zeKU8&dqLJBS0SBR19-x2VPmcL8#EYiFjO`5fZ5k9e1BOdfpC1#PloAAaK+cHd;dFP z|MmTxL#z-9>Nqa`*zgz!=U>@2&8*Ww_JNe!7UOp?0oht?Jvkp7P~U54 zX-bAKtq**=@s0)$t}rEwvrGdS1J&GfK6_whaKJ3mixTfd``- z)~`e2T2RV4Zmnxgi|7_B`2zR2IP{n34Q-qez>DtvHBK%}xYg2j;N}f(c!uqXXTYs3 zL>7+Ne+$sUuCKRBqX=(D=ab`#dyISN5TBt_(R(iVgd^Rvn|TPyO6)n_@}R($7B&5F zK?(M(dO!X}%+netrMd!$+=IrK!{+APM6SfRe$tkBu6f)-Sg?`-ONnk-7G0r(zika3 z$EOl|;DAG{T6`QBW_HY&6I?38pJI34caDJeXs+U4dAmq7R+V??FD*7*<13wEm$7>}pjPTKMdr|I#3=WgdpgvX3@_?DbF3d4rKtO4y# zGQH{+K3I`GsQQz%PVk%-i;uq{!@rVF?JkTgfgjE9FihS95keA8GU_4Or-PsXxXsYg3LDqjFP3bL>~^(OkI=s>6F76PG8w!W4U z-6)4X-``n<@aow=5so-Hj)sMpSN$Ow&PaW+slPA<0)x3cxD?s&Q*GzWac>UXA$M8x zsvY4kObvhNaGengS<|h(&mn^spQPR3A-pM%9u>=3GVg-ut4;Cm*SAn)^Q3Ky91qmD z-z_p?*aL5WT>E#wWDe!keS0`Z^cmx9DjbcPSs>F%M2gc~2ERznA!(=h@FjzLE7RYW z(5AVnbhU0jI``g_;y{iv#8*B&JaCB|x7{r)lAMhI2YzY|+CI2YNXAaOsa+-~^C2r*ewA+K<@8 zQ~HY^*Mmd1W(_Wa2DnNOoa^ z9d{h*-^wuJ$yDJUUjGTegFk#_)gZ?&1M97YyGd|rs52&5h6@)FfsI?OQ{d(cC6V;! z3*a&B=J&>QRyf?~K%ZR4hd(4PiP52n%jV2b%Gj>#yEh9qEwUKgT^EF8@;sc`{|Ueik!z{0<6G!?9z(N&Aq)0a zwu#wZBtbWIQf)=q3gUjEE}9A{Vcawajg~qe4l;2TYIhhzFEusqgw9Z5RXzEIZxLhY zDxKLcWBv;CAS(6%qV{kq3-^p5CHaJja zFZW8B0h@G4)YjQ=0Pf8aw}?AU#QV-bZS8bL73N3opShPgZ65J*o+vMI-X_;u&z}huL&P*B+#OZY20d5E&d} zB)xP!H3-u0dHo7@putSEGZ#ke3C>g6oXFI1a_p{Qt$U1z92a&xtq*mfBfJAQtecCu zu}j7U=l*FnI4#xMU+`oNw8&Q(wdu5jhh(~Ou73!_)nmG8H*+dtpIU7xHcNvy+>fUS z`n03Usbdds5nep@YiD{73v2-TmYVxjgr9{j>h50iQFhqkL^@IZlMYJzjM9F&!iH~o z3Z&5z`HrTQBs;3V#2!uUq+%ZLE;?rMYGE*d98-6^Jo09D4e-l3DXf$JuA?=$;>Vtzk_tlgb`cPDAG#Q zSBY2SfZwBqtfb>d(F!T<`B5(t9Qk3#9(__0<_!;d6htt%e^A4MiZ|EZXe$^zbI~hg(}swx_{k9B~}OB!%MjF zxwY>+#@;0SFDBt!r_LH++HLIOw_Zg9PggvmQYkU($L+q=qtsY{xj>?u_a&%bzKLXz%|HJQT#h|8ImYr`L@&2AN60>ydK_|_kS|f%x!RA)RKil&=NOtN^ zPuL3a{8m0L(NsAJwB#>)eGQ?*jCV5!Z~80(jTiT`c?Rcz{DIo`CUFk@b|J&|xB}r@ zvi9z$c|wg}-fR@`CFXXoj-J3|favAN+>(Fiz zeu+o`lP@IifrZ1K=~_h2#5nh7e&zf&SP>RJ$8dliN*l$5#eLcU4W@zX*IL@az69s` z+B>tT)aHS{uJAhI%D#TY@#_-ky(ePs;X#GF$Za_9C5!>#FN?tgLCdJ*NR)A@!5X+| z?yA4fiugPgT-e=CZGZmf(<_vb*U0obZo|^{U-nm!^asJ)g=$O9+qmgPbb$1}c1)tG?pk;{^~Np4)TS zh!S?0lSupGIiN@++!Dx|0;5t29_;bs;MINwX|S^ddNTZfYs$=ldaht!+uj`%@@Y2r z&i6{>6JIN1EL@M6-u6!vIt+uHnpeUKyVQ73B(X*}tQ^E9xjUMD=>T37x}NCu27s}@ z@2Y*FfNTG5UpZ^jjaH_q3qoG5ApIa?6X%T$#9~)+cdStYQk`JdQz&i)eRAb99#&I8 zcxS*jcCG`6GH0(Ic)*FPx2#m`iG0DA6LD>t6zQl%rv0ASAu^n6Gk(&1c@?P+P=`$U zt$^keTx)l3?*d>fth8oVheqm-oH~%$0bEE+x|MaUXu+cB#xBvHbx<7W6C4@?-PtCm z78Gb<5!Ji*&bC|VRQ2zWq_!M1DCCE3vW_6kVR$}+@XgnMs`aa7Cp;Le{u|GmTae!> z8`YZmZ=@D9bnk=%Ih^g0@7iMA0w%`crQ#pPfPRu+UZ`vxuyEjBdhJ97jc)NCiCf~p ziOB`lXU?Re{U&yS+LQgjUgyH_!i{2ZRDxMO{m(kucBZRR>14q<6i3Wd6f3~&l)`y~ zZ3^fetlj$L)h-%%almcHVp#KA0EhI*DOD2dFr&+`BkCb@iRNRE_(lh4l{{G&I3l+*UTV+~jV z+g;8odAfvey^q78VwUJJiSfQRT^U5$zf0bqFj+(uf(zfcU$&zphQA^DeHXiEw$u_W0UT+2^k&fQgYerCYiJokhri#v z^7L3NJ$`uQv7eq?BI5qGXfDgKjts-jmLBON;Ujj9g+a&GL086w6YHmleTilqD^mv1 zJC6_0Y`e#Z`L1j0w-i%i&Z6V<6Fh_`Q?BN3wbcMhlJ-4fu-$;jg*8@>bqHX0`%mMA zg`;49Tn0DpTL3ZvIkagaoxu5nC|4VAFEBGP@8Kc*@ur8|jN>j5b2R&iLMb*9e0Apy z)hoyk{q_#AJUc-S3w}k_S9dJ}Vav3Qe=Gzy#I^Q99pPzKLp~(~G|afAmEo-MV|pUb zLB$d1_ZQgqh7{k38bS_-0=#Q@s=#<^Wt+Ao8AiT?mOSzONILHQ>YENKSP^lKd7kix z+a8Ufy_(Jrvz{bf>>zwT3X(3ruVl_64H?>ZrAhO^lOuuPxe*Sf#{t&>EIUFBTEC3_I zN;xeq(BV;5i*fQhL_X(<`j?^KYsk8iF*L1z8RRC-+4A=cqPD&FX(|(sfmUwv_YUe+ zr07{|sVz(a@sr6>A)-h6Ei>`8OVAHA8f@Ij$2AF5_#W)d%&el|QcHR>-g&?n)V{^C zJp|r4G_bSkOTY^A^Y6r-^WqaZ;P@iJqxU}pGj~;XL9MqVkMNZ&Bz`_dL3xtt*y$J+9K98;q3TNT zLV0A?1Z6rwxXUvA#UHDnuF>L@^wl0v(RYRlQ#u&O z4IChcoRr*_tm6ZSiFY;mmt86Hs7`P+Jv#yp2j2J|kurz=HMgz-Vz6u*4_D+Ax?S)fvH;{g!{?RriDlGCf{%q~>In>0*Al)ec z8+;5ncIbd_9r#lU_l*^-fK7@7rG0^)mqiCBPhOzJ*8L-?I;P}Mj$H3k4Z*t$;GhcTy*&zS z{2hY5H1`n24##1?`$S$jiF5aPJO$R$uyl#c6M_;&EWUqFuw#vps7U34ZNO&rZt>{T zO%T-eUMj4g3CqlDF0&^!g25B#`FwPV&*fb23Rip+7~_z;d!}L*DT%iADt%f5+n+1N zvajre2RORW(WMQvubbm~C$T^ON`5qomB`hC)?6Ltz-}-_F4(xXP2`IC_E)ezB{+;h z_xE+RE&z}GFDwoelc?_A#<%DDDBw>ks2Q{`5nNqS+6_xw24bWHCPs2fD5uMxSR?q9`wm*+s@m7iRN zmMpMgQbwLc_;fG7IQyYwiyYr7G9JD_Fmvk2_u=Q#9NJ*c)jFg{LFE2)o81Xcw4mft3HAzFBDoerX{h{si~DXH$QJ%7z@?_HGgy@E!&)7+YOr53|CIA8GVwe5i>C zHCIZ?c}lpf&7>>rHIF)dPj@%Z4Iwyuv~59_1>aa4SQ;hrT-4QEcCC5yD8VAMUR!e= zT)T1Dc%ys~jR?>!?q}kI%YN5|wSBfxb*}QPe>Nq47Wvde@GX(Q8GO!QQ9g{0!t3ob zO*Gi2>zTl|+5#{Q@OxOi(+`+5e(nw}zeJ6n$-f3_(_p1L1t%7j#(-y}hs-hNY{a)w zK9VNN312xjXKQkh%Rj)?`Tg(;~qe%u66S_;y)d;cNolC(ehcNY+EtnQ0? z!q2v;(I9l`F&X4E8q~M(97T~0(c~L12T*6-=x3*r5kz%G=APkbGjfVi@6uFjK&)TC z-+8)Bj!749tAAmq!?QB9Z%?TbJYPoXwUf7KG1t6zYY0CZ9N(Q*Y|H8gk(|xfGkX@0 zeVeOKH=igxLMdU%KzR99eGdL+xKRP-#mQ;rN5?>%o#Q1sLn0?NOt<@1XA%_UUeS}B zC1L$6nsCaIdZff3vHkM-3TSnwx!y#E!O%+5EzQ3@V6gNZ^JB?Yba!set}p-peeF|B zdl%=CTSDypZ6z|Sc=L0)TH6Nt9+2)GL)@1cF5&_GbIZUdsQRn^0teJ0cQZCPJOJEG zF7?rOFu;_}!?boc%R$6j;Wc9_YAgdM=pAyGft4z6qu`wxq+Iwc@gW*U3MK+BWU*DC zF~LGup_RzJDn67Lv>@gQXL4r(;Ut>Z^1aw)M&wKHar`s%rh=LKe(=R>%%UJR)iZ0? z69H{MS$fB1!iRjmH?5(o1u1jsYt(BtBS%zHe%5~)bzWrg&t+LdeJeof-s>%JpwQ?u z-=$AroxgC!w__HReZ3(2S(Ob3`$;?(jjINl6yJ`Z>&s|Nha}*9jt08!iZ)%v1Rp=? zNTplh2GFtoNgrsxg&fWw8|O}*M!~ysp}>I}4@=$1f8#`rMdMC-C(iT3nYMhn1HY2c zHyCfjMapPP*5vL4_q&gJr%FR3Sw7qYDkW;~F~JAQ#pSbuTVUO~ z!!4km=pplN^0%%*sH2*}Hsd{yioVOe>mqg*O z;7N|-m-un1=b&rI@eMSSJRvyG(TPS~dlkpz`QhQ_`K|KcJ)|amer;=R9Z1|EeLcU) z3xB`zlOFwA4(xxH53MeVG1u!uqeG*?_U(M8{kBj-D*Adgnyf%wLGB&Sxgct?`(0L%@VGF|0|b$RZU zjRz=TywdG_i`6O6Y#mBgj5wgDXs>5K;e9o(*|n4OJpvmZz8Df$6@$%D3Af|Vv%+zG zH#!I6++1-b_`K_FYW(VIhSKi11k7;z5Ta2-oG%re;{`JQ0M3oxkwY31u&q4cP{3Uk z{QL2?x*t0`%vKEi7GXj>kIN~&f7DRoESLC?!Ww)ypw6(l(@Y31o|=--6DUKY)U8+g zwYlN%($E0CYI*qfa@qF|qQ_E9V0M@L7@_fB|Kimpby$4!KkiqDXdz0ixbVq^1mmr< zFZi}_U~|LO5+9=tL?dQ+{pT4$c*kR^Lqod;aQhawg($GYFMNT_)kf@)zg1stsg(*A z7YXmPt`Nt!#{@H%!nv_ux6)thP~yJ+Uj&719tW07_+mpabA{s9BjF_f)=jE*(OjjVOx*drV1KS_@iCH^;DP;v}7C} z__!d5_1^lq`{s$mx)O3$X?+@~^PAdT)KMJXnCGp$Va*KhW-j(P(2ByAbS=erULH6> zTbDs^^;TJbG-T)d>g0;qCWb>*OMbVa{NFG_yB1 z{&S389Ul|KZ_D?cDAOH6-s+T0KW5j!(B2Qp6Q{XwNMepKgD$~G*z}}1&Pxxumt#}? z`}v{KXD6SdCy05hwaaF48AsO2t4?21H@5l z>(&wPmE6(S{?U|+@3qHCv==v!eJv4=kZ%DB`^1EW zXb-@iyk`yd#*%RC#Kdpe2!dmX_o4^fsPWGXT9vjiH7HbNl~9mJ2Iap*)L-mifqgB8 z^5ya$k)Tf>$$-euD;}a5FMQCCg!ojWN{Zw*`(G9di*` z?B0zgwoJ5Lr77IG*C)H5x&YQMZ-#E zm6cNH95X9quk5||9^d(^zv`{L@9#b5dG7nVa{8n!X2H|LdY50wtpelNqRekD#Fu{F zmQ81r54*VY@=sVWL1wR+y+Ow%@jVS1#+Sb+VN2VU!y_epFkd0pN#pe#P+d$e{&jN- zymxHR`TMUEcn#4R<_7-;NtIljYDtpV(onrM^N=unvwW#DVvcvPVHVxX3HbDxH{RRD=P0%OcedO3EJ3OHB zG-2t=JmQr-UG3wwjTmhG<+r8D=Rf4RN2e1jOg2st^N*B-SIuWW48Nkl-ok0eIK}1> zrOmg7&2~CyG(ESnpe+CuEuK||JXD1iyQB7&kbD8`nDwi@$%55p)r09?%0Qow*Fshh zKTfFh^};)W0D1UfQ$N}we;%=|rnh~5!3|?p4o_H;9Kv|rhq(EtoVbngLODMhCl(a^ zt)uXg8e07}@Op$rG2glFhk+MYfzc|%*xI>45ZF=VCnd0g@>7et1ecibFDBIkEHsNK zX`C@HF^wKd@~!1?A0?mft0z|elnCQ}A#RojL$|=@%Cn_*{vmLfqoG_VWC|s96zp_l z*bN)~vtoY;J_RDCAI6K9x$!5hwo~TqBj}Znn!!8bv*0wLkXaC=#HmluJ7n!=g&)mC ze$`tELBBuZuiVWD&(N3kS+>S9S{+`yVo_HN^vaxnzgrna|2@wAFGockp1dBElh)XS z(BIRszA;o7+OfCk-Q>U_(n;^)O)0Q_jzf;G_dKv)?OxblHwC&qw^A09TG6-Sqd^yS zxbRQqP`9Q)zZftXv^^lub6JYMVeF(`? zLg%s1dBS%{-|5=A=&S%6RF!d5W8kNTzh)=1u>vnN4(xY(vcZol7e)%zcU2+DV^%(n zt;Bb86gv76j*r^UhnY1tjPP=4MoiZDED9e}b$;GJd}Qpqf1K(d-i#djN4CU|Sf7-| z$n!K3Fg>54+kUW)N_h$=JV`F{V4=2?xd1(s-EFbeO7?mI{VyNn{3TrQZ^Z#^C)-gz z*NcniG@F4@u~vvIOFEjKl?>h^wE=8XtfbXs%fT|&qvPJG6i^duU9yxbMyeaff*4iD zk*{C<@q?Zu-+9Pjs0g*AoeBa|;ftbpa6?y2Uw{MRQttCR{ONJZ*MKXPGoj$~jsRdHzI7v_b)fHFQ6*(oS>M{5wcJCnY4~yZRUFC&_ zc9BeXu2Nw;6H(+~-U7lRuL_+ZybAFCVg$7!9TpdA8p^wxjks&iEk1g;4DQC0$#ED9 z2Ip+f(-|~_N4(J=c47+b;T%z=8#)ML_RoEC`rHo&k1Ng_5TB&e8JS% zR|2r|nL8ZbPtFflJnr5hJzp;e$Lxn=8)!a3@ug4mBAB4Bsl08*2$iTq=6dtUzACaW z$ZPK+`uZmWSw?N6u+ijx)xNi25C!J-J->w>%#TJ!2y~%aM5CRW*@M)?V)iS~vc>z-K*xEcYBXeEYoxo%W>55sFy@RbB2=8)*w5)TRBzekGEl z6JEcgD98bSJhe{R=~{qx|6#k=+|32cSLLqP=o6ks@j2`1CnLykkFl(Otsvk`V7Wy6EugiFv1t;FmH`8hxK>hZ`Q)9RZbyo~t zX3k@U*FvJjpJXt@en|0!y^sSYm2J3NG!oz9zO&L(N*m~#eD|ePx;d2o;u&S2V=pi< zbgp~(gd3mDWteHvWQE1SE13@@>0tDP-uRUl6TsIHl)V)sJjU^qH;2D0f!4=)KNeq4 zp~kRYd#5WiWZw4QIi>Sdutd0&)6i=cT)y?YX6N29Fv5^$usfj^95#&3DOxWDdOs(* zO5FJIb;Dx!KPJ>LnYEhdPRcN_lXa$T-a|M^cbE();xKv@a!`B_mIIZp*x|fsCOnx| ziLP^zJ;A$lIyX5QEKDCI&Xg$!^}|K-p6wQcu90k}<(Luoxrg&f36i&VjuOO!K3IvuLexj{*OEcF1@=N}>JI6ll^M`fhe* z0Wp0p=MVh84R)-)JlXr54d0|rxLxpd0}$Q#NXW|xAm@1cvFG|KSn0#nCr;l4T{q}GFqZWZ%r7Ro~f2NQdFRVK(=ur1H!O%Ko0RPy6uG7mZJ>rW~70ZD!{ z;<*ahvHtVhefBS!P>|T6tm;8J{ObkH$2iGBhVg6k9)wQ^@>d zo2B=u=Xt`#k$!Ece3~70P^J4l+rNyYd{b@SqL6+ zbf|7#V}`CYK3r!yiC0$YplH3Xn70Tp+H^(_Cj;)oTm$vCBRgM+5+7;x)lYaNBTPMVm$qAlfuli|51E~XbN;}u7;NEy& zYNf>uBq{kvKap_46=gd!@azck5Rd#EDBO%Jyp5}B#p=MSqj}mE=D{w{%fecZt|Gai zG#f3IK2+3LxUI=Nf)uv6ldkG?f?V*$_Ij@pEDkzzu|};CXxveIZAtPj$~g^H210}j zr1w2BC1nAGEe>2h?ad57K1}hPd9(`VG@|K_)25*w@Vc67wGYI66n4GN8;3d)pL8br zG2+3GBQL^vT0qm1q}V4wd|=j(*@TT+!JLvzmSyKQ;#XIf;IXEHTRbLH$6|VbyI<|* z6xruWZ0{9#Mm3G>!gD`SS#*KR{%_tEN|3ohJ2!REyIv4AcM-9W{E$&}l4S(mLV~iL zfhj9}=)h%x9Sv7GU_|h-+<-a`92B;jvtEP(%F$^ve|bWU-|gStIr51T?qquGGEJT% zG6#Mx6+hsB!k3um>YmJ^vFYy?N0d7e@50*C8%kw}a#CI5;Lsxar+LN7Qe+p*9=ko; zPd$o+Y_xg@Vt6q>sLWu!#()V;;IPhjdYD0H6)`PJ`eICJikO!Hb3Yu(p%I)0NH{P_ z+;$F}8q7T%`06XN&9&M0y>J$t7h1LNBEF7CiCB+IixuByZu#dBRsWX%jpEDb|tlrR^_yJH;S0N|K(IgJy2xo<1Wk@CcW0leeJ(WQT?#M`N%p-_)XUMPB8I3M!UNC zl#Po4wxTReJ&HNx=jarIXQUB3afr{S?1Cj_9? zLBj-YS1zc3GUovoIhQVeu5}!w=Y{rl4<+r5=g}!D?mz9$oH#Q&;*Ux&GZvmWtV9Z5 z_;|Mk6?@Sl;4j}d%%8jl3Rs-od!jRe+|!+xtjnjsz2=lO$D`|D`{WB9C#n_{?-e;y zZ8L%{pQOG1Ipr5?3b&k23S6;n)?^UK1$~D* z(kz{q(AAcYPl}JPgZthA=OW{mk&k-y=Z{u0aQt)ie`#c1G!=LCzh{5GB40>xUx9G8 z6ibhMXW`|AR1^CCug-A6=YD!P;_V{Xzjb}-s0YcD*p~bmp=HK&;y>nV9p+F$U+B(A zEjIj>_H%T$;1bF|XIQ_anGOC^kU8?8JrwOAb{S@N3MklhUH01|nSWgF*7->KN}g&T z9rK^l;yi9nE?dtkq@rM=Ao+#x)@lmhavWrYqNXWzd}iawIA)I4f%Mib6%L3k&56Of zh-$a-ziYtlxb&8+)jVo+b!>b}_9)U0JaaX*G>}Sy%SUgC4!dk|=^R^MK?Qoj!)=0F zAfaVMkJ+*sLFy z)OdDv0+Y~#$BEDNFRy58^ESBTcInJ5$`xQY^1_x?h`jD_U!dk% zJb0n%E@@&mhxAL^Y7*9{v1zk)9iPD>;y6Bed9-vH9X+XR8HP~8yF%kIm zkk2gexoTy*#k-1*awK@{RbYb>w>niW^lTzOx_^}KKm|yZOR}z<9RX~HJiGGQ2Dmx#8r~=wLg&Y*cIaLs=N~5yKvG@s`#w|Q(rz(mJ$LQIE!#Ep^!$MC+C>`N zkTWV7)lVQ+%IQu}1{;KfTP#Z!8j#0^$#?zokwX(2pxB>O-VKWR5-b60o zW>WqT*~8h6RGq3OeaVdKKa?p{*x>t(+DgM=usvkKtK++XIC%c0m5Iqf#mX0l+LlNj zA?oOgD5k}0haPQOU7iEjs7}eMfES-zD&Fm1umKGH6#ooX^Z;P3|EkB586I|eU3d2* zna@0Hx@0dz^6Y??D(3zWhzi_&`OXVz*zj;_JcZ=`7mZ$r#EfkL7&6jet-OI6d^L35 z_0nKxbDMYkhyDS|5sSdSOJ#_D`;M=Z#s+eb?3b?F0 zbKe_nS=WK)3zQL#q(8*}?@_{Mk0!(!5pn(8HA-yr4gJzc8b&8HQ%C#mjUa!;)1jSr zD6uE|V202nElyr6p*q6$7il8n0~$`m2Y%Hr^zp7%Bw`nx?aaXoqjM~SSh$$5&Aabj z{HO(umzo@Mo-x>m1)JJwlX7wY3<9vU)`Dx&Uo-xxj?IIf9XaCb@lmTDQ ziG0U6G=q*5)fy!o{R+%`8<;1hmcd%feSPbJF)%(wA^68g0?uhpXtdVl18IBqxXZ!B z&svw7B{#YZx}P-;himquD;w=r39DnEl$KKTOVtvZxKVre8cjcH>pz7I-mIa+u4lhK zD;q>9yJp>lESJISrIxpQgvcH!E1}EOfE`we-dZ1@Cmsj4$bAl9=wYLff7p)(78o+= zJwIQwf__|ajEo2xLkXSdTy)93E}5Ld6+biLBoFWNiGHKNw>rYo24fAyjG z`hOF&*14eY=reX!vad)n_4!J*wS9fU6B*kggATB(Dg!aUgOZ*{o__$WT z#ECIfunC%~sJ4*`w=Vxqya;BqOg|XA%z@UIF``VFH1N;Xvq48mYIyUX!-rjE1K^^% z>s@eY8%M%-F?_J|vh*aXgFsJ&y-dyV<3*BAc&1>jl;|AeXcC zVZCl5@Z`NUd1jLEBwf`zHpH3m@#vLyQ{vySRL23e7b$V8N>cJ6vNu+K{iySOIq@_k zt>%Zh_oKr-5ijH%3IF`f=*LSNGhl%4$Iz$cIxxsJy-2S%2;%eBzG{2U0XaH7*NDAc zNO$F9ZI162Fwo}?av5j?;_~I)pVgXBt;ZV%%5lQs&$~13KD&nWzNx=4Y>|Mue?EW9 zbtK+5j=`_bwa$af$HH9pQIPYBFs)$qLz2H@dYU=CLO32tsdTrUhJjx6!Twcya!;SV z_C%U^u)Mr?I8nS=1G<`x4srq8=s{B3r=zDuVPlz32)p%Ppl6rkvFX?hcHZY)3=8>+ z`0w7dNI;~|)>`t=&~FxX){k4~Ua14S>5d(zZCpmLCr&3SlRb+1$iKbOtrXbR&68Im zjtRb;eM@P0fb`iLWfN;&)4*F>N;KZYdlcNIk^lH?sufZt8$uJuRl{d2Ja)_lD#He*cSW{sNNyIHI=+ zhidEd?Z3ar7QwFhKi~1raYXUtI%TO56Rrp+RwUz15WVYJyauF&VY4f;j70;eSTqg((=%o&O5oXe;BZ?^K>&c+D1PNs#J=)3Q^kT`0g07IaKkuy73L+upA~h z0h6Q^)I4&+svuz=wR`vT`!=P$rk#rBp|+lkpbr19YG&>WbnkEOez&K3i$bmMxP(y60kPiz3P|v4?KEcwlU{5 zk1oSA>Px@6!OH#@Ar7+wFil=QQH`bO1Sb*S z4X4!5&r{(1pQsDR5?aCfpUnfdCIevXd_mOrh-Fl>ku}M9{3Gb-5nqv*{Dr)X)6i*_ zY?8CS9PLeVx@;~7PqQ?ZAzzb;_jVC|NHqPM&0@`8VCA85aOaOjgqN0>#lBPE8~ZNX z9%J1AR`dr`E;f`QyRV~`eH}|^z{d8{L%s@Nt3>D4A3l%1xs5)zBDt}NBd@R7p4xNO{6`81F>Tlg30!ech zpY8(rTWvx=LnhI^5TBV}-?vb}&a7LG7Z{+Kx%caAX&RhfIugf0dUrWuhS%4J^x^Wi z9y8O_J=kJ+T9LDl+)sN-d@^-cKpOL(Q?l=Q;IdoQ(vw?U*z>#g-kK{cu+#6Eb?l4x zh%JK7L;4seWC?rEB~&2|t2-R&Zv97#mG?Hd$seSJD(@y5%{GX4#da#R`&1AJb=JBn zqC^MJ`sxPPRrZ3_2U%V@%0I#AdP%9v0V;R^N&7kV(BhV#{JkfOCIRb-_&;o?>w%MX zI($sC0s=+%F~}1B^(6}X!a~~#T|4-UZ zE*xKU<=(mfc=1P%sOtwpnW1icVn?CN9_(H@F|kDMofU`0a;YeV@a);5vExx9*!}M6 zXAAFsB&~bbcvmhLE|xO)lvbjG2}T{N)y(anY5#E5P{xIuNCm z%TNqf9>4U^E|P#h&Yu?F;lhstH6#0Vw0SVvSA6WO{2Z{}cH@ET6qr9!Wv$kZ9VYMb z==7=NgZ+6e$+9o^Le^C0WKLy4$k+1cafG5cj()h#_v0rYjzj8L6igsBgbtro#0xAaHQUyD~FwXw$j{w(I!d z-2l0|(}e4)nti=-mE^vu8G_4w^4an3pzAvKl~z%PhSG0aDMgU@GYYM@H8O|p`7BqM^?&}r1n4RzupGR_HHV@X{ z9#stDnEmESywAapu7!XSuuFs!Sg#$EE{wavu*3z zzXRTIZ+v{lLk$L{9k5@73qVTTi}R125H!%IIapyVgu`f>T+a)yAhm`V_Pi^+xV7z$ zn(u!C*y}&4Z?6hxkW2pCv(JM?p)iNby-#PUF^k6g-HY6$7k5Ek_U&wMk;_}px_4$`Wf`?)%?Us zwPuvN)WuyvI|rTw8kqiwlY`QzhwUTbiI+RvJ*DT#hco`J>4s7)JGt{~nGc^Wle7j8p0dge@p!p4W@hM)_?cpG*~lnW_P2Nfiv^XJ6tmapn{@?jQmLfc<6C<*>K_>Ty;y% z&%j#}ODG0^XkZb7Al&Qu=vz??l;x!aqvYUE#q*9Q#w4Nh1h=gEM+&_1mX|q-Z!dIu zafvEkOAHPfu`PKs?!arg1|^(+Qy^87?Phr4JTMJ=AZQ}X1&_v>TzUxw;r*Q2OB8v5jLN7u3L-VITh<`{+~8+q}hF702PCKl+v{ib1Lrv%;?s&IgA zksDiaU-UljM2Y)Y^9sYgg>XXgrp^9(!t?xVa3&y#4Q3ubeYhcd6L2}5()asM0hVeV zRr!?9g$G0jcHT%9hxY0FlWyr(BK_e@=`Or{(5Eqtlgxo2&)CkpLWEx*65M^zsfKu) zw`079^n_s9uQwbN1zX?(tABc5lN8)PJ2HB&g&7A`NABN5Yv`+Jv+nte68Mwd=M z|7Qz7OcUnv-6y&Z`c->_)p?3QCeKQvcOVt^{8lVJ$Fhue{fO|N(3wCslrm1am=iZ1 z;XC<)XGK@?+B}L~AiMjs`DpH9N4ZAcYInTMSQH!GBj(W**8( z;q8>+4Ce|i+|76|u4uU*9P+wla)$Vu{kQVnUYPJeaf!G-2h56VP1wfwxTOMza{*(& zeVOoE(_P+~hzW;2nriIUFnIL1jT(2qNx7v+8A5W?+0ymzI z3TU(n!X)McF)k<7;P*T0BL7C{@b6bIEX(6oL4tuAwUxpoi2q9mIX!nlRIg~JNkN_i zU(6`IBgvjX?QopNHPTnVF2lv+wF|lflc*OWbg=!y<+;QA#!(7Q*w?^vR-C&(bcXYn z3~atve~{mT8mpAttXr1Yft^NlE-saD!cBU}l@&Y(l&0%@RjFpsr;@?>U-I zodL>N9xc1f&H&@y>^nLs^th%m_Z-7v4y+w_na_gwYIJ|R|C$Ab@GF_Q2a#P=I5F-< zVK(u5@FWO6p0}rmlO9f@QJL)U<$&7MHUkfS$uO)x?WYFcHLsVHi7{eV-GR{CU8^Xu zI9=@LYfec2g)4E+p%skmxVrf=Qx*oi%R1abvkvZ3W_Oxh8v#{SNk!lLs9;u!i)ee$ zDiAptXw_6F1%G{!yc^WkfyS6Cg4v&NLy5FcN_Hox;eomJ^|+_Y2uG{CJ1=nHnRQ$H zvo8sUc3SWdQw1A7`poY0b1Tx9lxX^QY`eY;T(kf_rOOt(+3~{Dpz!_%AMxL4 z%Ko<3V#Lq%Tr8cT94xjT_ver!Jw{&N_CyyJm{PE_m@=;cr61Nje(sGlv|5VolAGX&-H?r)CxnCM_Z7D zUD)7;_Xf&P9ktD$uSaekCI`l!63&v$3ZQ?)2>I*;X!(UX;JeL_9lyWP;JMIT!O|E8 zoa201p)r9C+b|q>!s`V{KW4}4%;+1%R{iGE3%>QNLSxyV9*lE0V zP1&H>P{t?MXaP7Cm>1u(Qy7YF<^M!vPB$Pr+qt=Svy!KG)|# zp2ON-I#C|jelDHyy~7krTRH3MpTGtO&hficaX=i~JSuT_0P#|lFCU9=`-gl!gIN88yK;5axo$#PJ|LJ%r8)UPTBQxh+krCb8rHu1em)Wwe$fqUoV z?WJ2+QCVXnt1RI>@K)@VyYiXzle)w=efYY7$baeFX(Pnj_$=p!=dpf}CLa-PlrDgk z_X%$Jb!Hr*kD6QC8l&zl9Rh#cne zSzQGjQYTKd$V?+^m`vqNct0mX0t{^TGQhmU(XOBG|3=A74;n^{2p3eSfnDOvASyrA zdY@8|66!0w&MEZdg4O)y_F=wscqrgSTBbJzzE06+akj7@-2Ne6lSJ;xs?xGWtYjE(F_o`?``a5V606kMV5IbW3E`RVyk~X(AmN!6O*tItbRfOOn{Tw#CwdX3 zmUZMJ!x);XExFHIL=WFq%p_GbFha+!e7)RcVYt_Gv-J5BYDm8xE-i3}71v92Y0_`= zW8c|dizmJ?;JB*21rM_6aKx#+4|6#aKvp~G4)L}FK87}JcSTOj8lf$u7}Wz-cjOn# zIMxB5iMb1<*9L&bw&_;xJqoOF$te=Jbt7woL$3k?8Sv&hGqYP{7b=ltb}L~iLr)%L zh8MOFZ_A0KHZ~c;SyU_&dLBiGGtd4fw?H^C^8I8LJEuY%Xn2d3+{Cr3pI*ROoqm-Glm@S3x% zMmxH%q^}j2Tm)S9ti;||od!DMUYjF>l6aR(@k6msfKT=vc<#?LgTTbc)>>!soV`Z> z_Dlf{o}kcs-cVEz-p;gN5l`p93O`!*ORIC?w>(slY!8VC#3o6Bn)o2EC1>Xg$IT(B z@MNv^@H(LIYmK&ukst2=@pp~4j0saaK2M^&!32-&kw|~;)eH(K>h`kF#sjg}oiDao z_;G8D%w1bM3OrFiZBKQT4<6+y_qgE9hGQjbUwad;Z@s?ML~2PZDBekbwbF(g8|c5{ zd*Zc3_H8d_%Vk+`{a-l>i}Pe}pHHdwjd((2Z*aukZ)3+(H7Bed%*+Gkte?fTPdNyG z&c8rGi3(rgKa+9$0v8sE7MR+g-viqv9f#HE27t;y#@BCY3+Oc6&OcJ-thm9Q`;yUK zPFU@@W%#&}NktP3Ao-pRJRh{{{Hh`Pww17Pfh>b6^n!gjBqV z`pJyD#UdZtk^IX!-So;zy$(=N6qncfm=f0P&wS%=)c`nU7qq{=3r3zA4il!i2volw z_<2p22N!PaThTK6i!82Rx1VSt9FN)Qk7lYAP-gbsmp2E6@x+~)b=`kraEH<7$%%vi zP^qp%>9F?Zp4?x%7_nXdg&k*RSz++|dX5gS1vJmnWzS6B$77uA z%R7$o!Sc`RNvb!6F=IjVNy!Xa{2=SoYIhJ740A&b6NGDWV9KkNhldZR*!r|;16kPp z%wOo!W))b&N-53zVp|DDCuNg$|Em=k8{w{DVdol)~hUnIt91^okg<>gCW&*FFeflPlTDHU}or z%%@8^!^5jUm@~XZymS_%C|=k)>Opcb@S{@6CYn%{z$fBDum?wUR*NFq{An`nx`5EH~!kt4EMDC1#J2IUq{_< z0{oA?`ZU+*pm(@~o$MRJN3XDh4@LXX#~b>dhlCh$w2|9`nmQWz{TJ2s!#OiZ<$~u? zU(XRFF<&4l{CfdC6L(g;N%Cu}oC4NvF@m^f!Xex>fdW=!=syj;)QAjU!sv|`qae-u zP|mhaFS=d%Q8|+IXv}urj|lhp2f~8=wyuV?fr9jCIoqAgxa0-%{z%eG3)PLh+*Lw} zzb|`eOcrf|%$F=-o#y`#^XQeC+@HPZZ1URK85NT2opSEk>A3*z)AW{}-ll<9TNic* zr!Jt3X_59bA}iqZpNJjIw^(tOn%79+00n$6(;+{5bs5!ON^*SDJOvc;T^}6seTRDF zPO2}x8wX~SA9gLo6%)>&WD%PD3Q_`X>3sP{kekWpck@DI|Egx3lsDCnUPX)7^;C@m z^^TMsMu#Ziv>nBm?j_>0>wFZ3>l(FHLr8(%`Fd-goLAQR9=|mL6Zr zmcUG1!)74yi1XJqh1XNl;D2tXcn+5nU!49B-6ZjR#)dw*boT+_spZp7p4%k`uW$ys z+YNUBLyE3+;f@i|FzxU%)q)%Qt*=*ANxwsxk&zd?!b(wbewhH|-$MM=8sSq$8^~B( zGSs%063Q(f(R;zSfVenz^cimv?sa?foOovspy2%Z@`x@iY_S%+z1%R0_FVq7Zlbb+ z60p-(mlJ(J^r%5m%*8D*+x<(VpY)P)sOx}NMJMnLm`;v)R|hiAQ!9>WbppkDIpLV* z1vH}D{(D$$1JszGkXn$)M6-WdX=J8d(BJjz1s1#MV9n!BX)l^p6!lDvHG;h#4NF{h zpKqD~jXx}`mE+}LKeK!2fn@Uhbsmi&o@Jb_dMi9Hf(^E|)LVYc8$|7oS%q5%hEVQa zm()D+y*aw{Bfc(}0w-Hb^}8Dqzq>7#sO+J{6mr(fM%o;Z?ug+}r63x7>L*>Y$Tl4m z4R4qI6;25aN6xb`XRQG%qjuw&?cd;Z-093w9y+`&sB~d|d>r+kw8$$Y+_<~HySnU> zhXCu{jAemERxILi;Id}eKP20<5~Ojt2lYJA`|I{=4He$M-7qcEhjc5h{COJA0Mp}- z86+^%!jO~6B|@ge&&;crtsu7rK5WMSJ5d!4nh#IdZ_se!$^H9hW!H#D=KxDN*Uv)q zJoHa34;3R6Ia*j0A3TPt&fe~fS|r@{#~u0I6n}xjADhXorDgER`e;?@w_B^}3^=loJj5wkH#_6ztI!n#gelA!U zruT=LlLCvqG#~IKJ?(?z1Hy|7+kiL0sZ#653}9pII>tUSgJ?5WUQa8L{@Ci>eTMdQ zuwlsdzxK5V(A96SSAp;=^fL_AJBn!GBaK_HBwag^Pn1I5Ui&TN&^etgVzmYYz7Gm7 z?Vd&LN&625ziT7D%WDCJc@!|>l=*T1*(>dbW$LWKOCZEY>Jtw;Exzz|N_ARo1I5Ed zpU6;FENIH^m&(fnpX%%|X$Ygj+recP^i12}o!neplD!z@CcuBPXT%}nsl&->2_RbO z6VGLhCJ?*!dPlFr4?um;;XG_0{;Skj57*oV5I3GY@TGwYK3;!|)NWDY{fCbnX_F&d zVRLco|K>Sx=9{6Wh~oud`WB2>Jw<%R(ywmYAESW}3Yado+7TnUvR3V=Hbs`l-pqEujf4PgZfdCKuC@{xrZJcyq3EW-V5~h z?Ec#PY8>!!eJO-_%}7eQ%{h6o9>lipGsA!t(|S(3bZsu96s9Mx@~x!bC(a%nNwp5> zBv;C4ce7#2+ajZEwhdsAWnm#9C2Iw(LIOywp;IfIY)RKj?ST*RBPxg4HwnSjZ8Fsp>X^A zEb;m&IM039ON-wh@21(yLIuNHF35^iFQLON)SZjm#7Ea@yiIXy8-yK-+hn)s1AF|b z&dFvh0=GJMUK1PQoAErMw36Hn^fgZ&d3n48X|3~Gvbb#nJEz0rRy$Tv`tcCshEo+_ zbk^G5>-IksYu?;+?!YYil{oeGM`Rm#z8z7pSE3wjiARJriIpR|+dtpRnhzqcfwyxh zPD9ATzVIf;uK{rK(VL8w_zWa3d5UBE^azOc+ns%7XBBd;e#dXU)dk!}#aQ`&Q{yyw zMP#ioh-`fiZy&PhMbr3R+O4!ML_5`zvG45)c-D8|@<2QB96A15lkc7f+>#4sxl;pR zcrR5|-R2_tHGT4S%vB0}X!z0|ed->d@Y`Ca!FU#NH;ov&kes-H^6H`8Z@D4oM~f@D zHzrXsuf=y!4Kf!T*L^(kbqYm%qUm^G5CY16$@f?Vbb&nKl&36-q<4Hww^?wZ49VY~ zNaLgY0$$~dy%-cPC;ZzdI)Q|*lBBzst;x0riVg*GC6|+XfmrV$)(_fn7xJ#3Aw8`l zzF$2Cktp<+5NBFL#uR_Hw$y|LeR#VR6=Y&a>siA=vxv=3pcS6%G1?YED?$Y!Fd1!MYTJy2M zEc#|^t#x;i4{IAX8U>DX!q_bT{G;c2q4+nxuw8zv&}g?*DW2K}jy@jp+I$qSe`Xz2 z-osQ0^Dd=Cs{&Gh$p#sw3vq!nKsUk8s2 zs-HQTw!?E00@I<26NCWJ!9)bGe&-M@fIG`K;U%^~O2y z!@bw<1_K9{w(Be63+x4JEVY^GKm=2&$22Mvp1IdsJyUhF4It?BU*iDj**T2N%f987 z#Mf9q)(#v{go`=V5v9E>(AS*Cdowixg>T+CWX#wBL>ufqZ?^X$xnn7EMhknPq(RIr z5XXh1ZCg7}+3-Ov`nfMm=6y)0St$4EL0)Jx5~cw8Md2N;>o->j5As@s?{Yt6!d%A@ zFa1vP+=^GK2wUZZdFeXvnlC4ee*a7QZ1W2GCV8kkl=vSW{`9X_qnE(6@eAuMtql03 zEB%R=JmmhL(zJG`PXfA|{?KW;&xK=UUS#oypN(J@LhS{7Zyhx_y51|(}1Yuzg z_mL1SPAJo*#V!4k1qaOhg}817q}Vu)EWXx)@XqZ&J3T&uLvB(Nou^yS$r0V(^9$Ut zIQ6v1<rSa$KAWM*7bz93X# zBMiUPOS~zi;K8Om`!`;-)5HF$b*)VQPH?xQ`9WwdKU{849EtswkA8B;epyVCgRQ0i z$uG^*z_U=(EOSB-%Jt6XE8ddAPt!wflIUfzCeP1cuBQutzWvM8-jH=b^`VYV)`uD1 z-Z>xE{D^qN8cL0|2`>lIi=9aMwSo%$Ta6lOC(whVeNF$ksNrmN3iBtkb-;CUV|7)B z%vou*jQmtqfqB_krJF{axZt?u>MqC%qjvnVy^_EWX8BYIN+BG zNePcjbBOWzR@N9lD~#>p>WDEFh3_cQdRwF>4E_9re)TXd&JkkHMGfcLJ`PyuQ`1#T%>w_%vo5Jduwk7zqxQ77GiX1rQOBWDcGB}vKUlOV z02y!a4pW&x`=Z{_RBz~j#2<7|hT6|E!>v!FsellMVTy#>RSw>ASL@bZ~S_3^i z$Fxg7Od*d1#uSe>l3Uc?ars6oEzW2gvx!<71lOh-E~Z`R1pxyNo~c&#piI8Y8gj4;N*|kK2bB5h{pJ;8O)X|Ig8kp$eCSCbksURvWy-}VUM70OD6kYs! zpUfR=7Gi6z<r8MbI#KmdR}5C!@(SAk78ti<$3^=Ob13ul z9^!Rk$1f&TQzb(y5d8zHC{EH}6o2)4+y5;)d@Dhb*WoP;qgAUvME3O&?_lk|N95;n zz-04NCi(uckezz26)1uQkhhJ%MRqdx`MI}gjUM_butr_;qa^&(TF6x^2@98HD&8{6 zV!zuOLA`36@bA~ri4$-R)klW!j3x8A`*p3dhuIr}*#n=fZ*M07Of7;A9d4)0N+CF`BC!?kGzzZHiRu-T-pRY8!O>~ObWotmlotBWqI>B_ zmd58kqTt-n59G@PCf42&+Cez=&p8BDjZU*-A)VezZW&7AO|kDd$O{@9vZci^Qp|U>oS<| zIqlEwP5aG=1J-3|ByaM-8_zxE_0JJbjfH@*GRf_gYA#XrC^w_6?f<4+ojO5(p*6>6 z?tbD4{aEOG`tEJ=?>swl3S@XQVnH!?s&E=aKiMH*{X&(8;1!b*1gy`!M zZj{xmOn8$rHj_?Q&tf{-?Mzc#=6rG1Zmfsu4?Y;NN-XobQ>l`wZR4SwK zZIDPMsi+i@9U`l!tTG#v70$7hy+iiid#~U9`wMtIujje%bFS<2dB2Nu<>pZ1i$$m=QNSEPhRXP6;2h>(9^1E)T_~w@42(2P zpy6X~&J^Fqf$^!SRPBc}Fx`bjo#pcwpj=Ow`&~|WK>MTPPL&K09KDUv{+cz^csumi z)ZrE|6DFlr^>`SW$Eodfq%hz~u7?RR;v|sg1$I|r+d--ykI+#Rlp!0#Z{Vg02Y#XK z*7{4I7rqr>E8xnbf(5Ual4FY3k-^qg32ucwWQM8&KmHp-AC#DHDh(+@lCsp8D1t{> z5%T^($$1uRI34y%(RvHXJ*eMJe98`M|6Z6bmiZp&Z6anRl(CYV+ihm*V+~Dw^|!Oc;Y;e6d4A*u=vi2B{~?9 zB>C%VA3av&O_FyqA@+bXgZ0h~Aj>ZqSXd3&8Ap*?D;>cDzmfA?m7q z0_ynsgZEMk;lGo;Aan0lGq`>__O47L8Kl{Lruy&XDkxQWVjt(X08)AOr1xlu{6Mr` zFq8Ty+RFSQ(qlx1chxPvWdCgia$CpQzlCfen+M5Z?^QYw3w{3e-R2#{z2KS8LgfCp z7F-Rv%xLk?hCCPU%xysW;qrH<5doqnvDdfsc@TiJU3=XRNwH@(+sE{U5u|sdy4l;i z5xtzSd1+`(^qvFRm^l=Z!22UFnC5 zzh4+|yZX<-nx8W8q5_A2ZplCJ-id=+j<}~Bci=B?&|}8QGuziQ8<`-jnOG5wXNC>C ztd&wK`^etrj#2^5F4#X~isMe2swSvM+Y_4v{O@c!w%+LE& z(ZjOclA9-9tfEXsp>zxBa-jM6J_F7T1F@#0O&-MD8C{+7$lxZ!AGJb~bnR9EUEEDM zSG65P()uA$Q-dCED?FJUBi{e7I#0_M$L@i*G1<**R4n+Y?$DQQb~5~m;rwU*ul?v2 ze_Ahfz8qvccBL=scL)0U+Ld?i`7)>vmz9z@*a<{WoV#$ed=0!*(EBDBu?P(G&i|M- z8boe?XPo`BhCtQhw6-O0g6l2WS=3ue0eKlR#@ecD0GpY=-9v&~K-=e9@i}4@xa3-g zSMRbxub_DzstQIdDMs13e|i(8_!Z|bu`DBy{QJFL3JsPe?Ji7ckc5*eUjnWiSOYhi z&pwpWB6x)1SvKFV5S+QAAO0#4T>K;(UyhWt9rUkrQ`DA@@L(EW&hWZL>~lMEZTUoN z5OcE52T_(m^uv1loCXz<*E%h@IXSoq#`I#ArCAq|SiZ`;Bb5s%MAnkOI{O~jjel4A z&2JeUtEvq2h-^Y{_N)xeKe58(?*WP9i=6Ps=Fy6l)K(1KDbc^u7x}5tt)ZkT>aKc$3r71CfC3&x?mYxO7VZn z_=pP149eY*CLKUFQzrG*nImA2MM(dKVhgC#H5-?CM$+6w|Nev`G0*%iY zDV?RJ!olC;OSUbVbI1-ksqJ(0)1=V3X%Sihg#~Qy%GPpEZ9jUx9gg(gLOnBZh z4oE33~BNfya{Ec#s~jAGp3Pg1Kaebo)YpDF!X@I?T%ASL{ISX&@Fc{xD;zy z#z5r9%DLUh;XaYG3gRtlYpp>xGz(2k-&c_jEoY3cMhoiHj$E?r>_-R6i~s3*=Aa6X z4G~EdMohu;RiB@o16p6aX>sE!fM$zX>{nP-vG~Vc*Vt_!a^boQ4{tBX`AVzj zV!n!$FB;U!9Nt7}J3qIS_4vOvMt8QUOC&O>PD!#=24X1=I&coux!m@BF+9|pAU z@&zZ>H^3Fu7m9}L^&t76n{dl5!ZXx!L$>DIE;zLnqkCKJI~ZIx9y&Eu2|lHG^Sx(k z0}f@8y+TDh=%Q!7dMB}8_*5)yT6=B*{Bl3~?Yrv`Y9V=L!A2^CM<7fxKECA&9gU52>c>uW69F=0I# zif4w8sNshO>A4OMf_pn6^w&9(1xo2(`y|sz2A99^u4Mk{1F9!_{ga8_qV&JdaZ7^K zIC+!dR}(KAMk6JqXM-rPQl8^!vKI@Wm`+#ojUeH(aiKd!vbY8Ugbr9`mFDa*sYT?`Om$e``7AOYb?W;b&JG*sqOT`EriQjw)`1*U4>>c26tBp73pVYdhNI*fGNNT*EOh##!zBuQBj%W12wp)-DG!@I zdJF;~wh(uD$bjSiyo-wJnL-iO=4@^z8%Sj$E%8Jo32a!UNg>rHg_e4>YPlOUn7)5N zQGm$r6vRWSriwi<7ia&6_Y_70hh8RDThhRrwYTn5@z6pZ#pdYJID!vs$3^>>cN^L6 zu5Df4nL%|=CbWFsH6ae)w%itLN_=YE(%a!u1G4E;wzTLP0vC84Zm?R7qA4{QvX1*A zkj9IuF~xlrb?ftn^V~~BGa0NO#?*+pnzUG&&}ACjf6esv)8E8BQl4D$j4~&rNqfS@ zIk$w~uH?VfJjQ_8yo(<7*b^MwvkIZw;nYz2Kp<!zVQ?|;3wC_Pn_45?hC!ts+>c|d`zBn&{9E*quHaqgjZ+y*+DaB^9sbGDQrA# zLW-+g#iypuGyt78?$5O=)2JpbY1H;9HLh?MH7E@2Md5wqM^bW2fqJ}vdcVp)Fh(;d zJ=Z`Axn9wfeK%?Z$G$8G%qG*o55r5jnOjWoim%b=t_nTQVOPm1JxhFkhdP|pduNe) z5?A=?Jqk>JGHb;mnH)z=?_CU$_d^fpA3O5@>_$B)-!BjT$Aro5?61DqSOhUr|5n=x z?wibAvqPla0?=|e#lKOJ6tj!j>%|W4f{|1n_2F1-Kvb*U0_;~eR>k#nk%~1S7+KPG_=EHikhfs5xm)}-5 zBYf_+88xEFf>jK;d?^Ti*2yK|&b!%SFy8OLx)&$WSIbDcHRrX8@-=(PnB$i~=v{5o zbmHo5_I$0fopT!ue0h-O_-Pw(d8UL+u`eQYGQ0Sb8700VA29Uv^)8YO?#@kBUqz0b zM?6U&2|nO5S+pIr3XXocp3bC93%%+hUiZ$3K_`-+_x;9$fRtbR65F3qv__|O&6C(S zSbVzVTS{;;=_AhnhST&ogHe88$8r)Jef=rCS%lyM^*)NtBYGGzOs3#;KXFVE&-!_C zJQJy2j8&(8-v9&yo{CExSwdepGJ}unEu*}&CTCWw8RYo;$O+d-8t7-Ok*X=Rfpnrj zn#~hAs{xuK#{j}FZ?bJ2EIgM3IIg#lI1_wI0qJD@&fgr^^h%e2`&=*TRI^@x`eXxS zgsH4@_7fbi1{nu)G7`L2@_zUS!8i5Z_LO-aNP%_!m1~u5)&USCUFQ-^fiIkXd}m>D z8_YO(Nv73oBO0qD@;V=4Zui)Z<22bUvhX7JmW<$r!GfOo$?=`Q{LT8U6Qm$`(Slrx>Gog5^Efmw7*Ks>9xOI(eF?Z!Ov|HS58T> z!{E#shFQ5abb?LGQmD5Z*eAWd@6fmbj)JsFA1-?MXo7`VNLvUxEA353K5Ivry)Ole z7n$+?ET>=^JsT#N?RtYQ9VjF$=olU27m&^sYg1fFcv~yIkr3Y;a+r2`%}sbL(yZ^_ zD11(hN9#L>FMcBOA1YUprB{~GKjAXl^*nlLDZusVSQRC7OEfxt^`$ogqdMfgCEviC zFV&ZS&G#U)h>;BX^Q}O*s6J{hlgQ`Pn7{b+-!Rz9Ey=$d)(KXvW099%7qD4q&QEUZ z2R^*_1X$d5ft&Xfm4tH{8VcXmy`(?_MMd?u)cwYhrEaOrF9#BMTyblAa)c7fO~hOh zub)H;7c;60uT$XpPW-OdkrkRt3{TrGO(9`7CytSw4Uk2mR$E8#^rX~R8h*b@1tD+9 zN>Zlg!Khue^?kA5h+Ty~)Pwggus%df_e6FbMR{#;@tTjLCnRn6a`T5lIh+2OZMO;# zdtTM(fF>E{+o>}vS|fo4c2&xvg+oBObMVy0lXavStX~=OmKRb*o18BdXhb=c3d+Go z#QwG7WL-iw(XVr`&Au?d1*|M?DKsk6;*R@va|#VJs8&68pUYz&bX~JlSZb$%&DV0e z&q#M5Cjo`x|74fY75xh|figsIb)AoKqMHMXJq}HG;vE1F^(P|k6Q0BOnIY3L^gF~o zbeluRhnRCUaXcy`rG&Cyf({92lELFn6?#{+7SQ8JivMbqhJikln1vfJCH`S|CiI*J z!8eOGcDrvu1M5qzr=2vLLAp+d+)CmqYT^GDdP9E@)RgRstuvOPmDqi%?_T71$iO)fxYivCCU;-$)=F3-pU>eIMB2RFN}Kh|)V1_uo; zlm5Io0h*_S%dR?5!kM`IzntP5=+;%E6>-AfHRGHbacp!8g*rsvq?jYY=2ov_7S12QH{#>OnJx-^=Js@kK>jfLJ=1){r@aPfJPh`Y`q2o^Z+iug6MO8FB3rX$4Zu z4_{C>|2GEuv)*KiAR*#O&nCu2cmh-d4db}xyipR}OUF44a%>$PBX*kTNs9T`zH5BG z2}-oJKi}~h2j+j&0utXYfkSl{{S^~1I&(!dKp;8?@Q_EF@lt1?#O8Uh_SH6MEHDYUW;6YE@1I49GFoeG^!*^Nx4H2U;ghO#%h<|$_XC-3 zJ_)#gi;eJ1ol#XK`cv+nrAfm)NvMy_J+>`~6-p`CxV+G0hb%uclDF#j!H~qP!P+B& ze|n4JrOkmYkb5IvSZHt&(ap?X@E2VLwY;1UI}Q_m$+C^iPTL8zx%X~F^3@jdJhGYi zC}tT*^!_T_bsq$7%Ji~yvma6Mb6+W+fi|Ff*zfWkB3CA>w`UVZ@EpzsE(&R&W~A{* zklX!G3G%rXS?82W4gGWP@x0L+0;kX4xFshw2^{pIqK=qOf&s(D6_11a;3+MW+7t;T z{(RAT$e!pcX=grTD=HwxZ*Qh7gfy=MCf=3ZG`$gEJK@@`C^m!s)@N*stB(Vjb2jVq z;|VC!iv9dXMi)BUE9UX3d>;h{y*Jr>vk2r)q)PML7)IwZpU`F!Ub+v*p7tdRtb*!E zSANM*qE9iT#7AE?1O$)X4HgsMM904Bx9pwXK#qE2PiocH!S}gBPudy=-2ATp)~BI4 zU~`CrQ%;cJqfS;ypLb~mh6Z0UTG>mLG3`M+)t2!8~Yk0=TzuPh^UYEwpmdJ805(3AQSJcrq+FJ9q+tH7&7 zRkud64UnJLb`w}@N8)Yll$^Zh`dpNXoD8H+r1iRc zn#eCxKg>EkegNL$%L^+c<_G5THM=i~y__d|fSp zd=criP%8WIIGa2jcHDmw44Qziegq+(ZR`>B1-3m(la?5~;5DRM?S7-gZk3AU_U;7j8D4=)bRoRETpH~ zbB-P^`kiC8+uZ;y4a5aT|*KP`-2^j7ozS%B!ABenW6PF0CA$I~-4fDfU<5E)ssH zwnX9V06!VRljcg3bwUV6b#T{SaG`)Bg>~Xaam2m*Wm>#PCoSe_rhD)Eg9Tsa*q|D4 z+5r2h5!osoBG}T<(th*lKFAWwuo6&Kg=_~SPDbCRfQuGoyrLAND6gFLimkpp47f>e zSW8QsOTA2PA>tlf?yuD+5Ke=?WYuPfNU&pulKluhfj%Ie*Q9j*9S7!5`iLqy*U{%E z!x97ZyGXB}Th+Pg513LZ6?R(RK~u>j&&Ih(Fja_U-oBv}+;^UBt7B7wqJ=CQk_1<1 zI&k$_?%)U@h}~Mv&q?89It%fyewy&0yjz;nMFzM-R+%Jus0nCrIQn}p^5R=j)Ye+D zdnoN>`K!f6f=7^iD5s8-2_BrSrVi|3!FfefqL=(xAosb^#IDGr@Zu|ylS16|_{7dZ zi$wET0DSnI6C~GwVyNN|c6)IwO-;@@aajTe_B0Q6yHG*ud(}_X0=9sOX+L+dD?fyt zikG|U|sKPM&A@#?0uRc!Z^vsyQ!Oh8qMKD!c z7p_BN7jv!)5%Y^vBLa`=-8iv2t8c^rm zy$`P!55gA1-28@v2jH68qG1x$29BD&|M#!Ay}9qJTn>13_GF9&%{WRmTY6!ITsbo(NAK(IV$y|uZ15b1wa2llP(G4g>?RVX-JE$9_#Wg3d>)cpL{6+mP!MVpUQm6M%CCdEV#x!wo5>1M2-HLx8 z4eiw)8|HHnJYpS%xy<7-@KQ0kr@W{zWSF5bP~T*S0ViUvktnVrS_|ckRTU{H(cG2T zP4F+&gYJu;DVD_kYL9OZ7<0ov`|*F2D#-9z&!be){`BzMRIpJ4E#W(_nf(|~^t_^K zLs_mJ6vhpnzYfr76a7a;X|Z<&?MUUL@%Mro**YaFfdfr|r6N&PnT z`XhvE#9Mu;V(H-0Oz?-w57hYb2xnuWw;CL@gp&K3D}YtK;dblGW->IRpz_ypGgagm zUjC8juCW71BFop4XXWtEM+TgOEL4~+j_+gjWo9V2(0!{inFUs-Pd$06x(}M#uT#cT z5%=Ox*VJYXO2F0jc4m@9N_h6TRP%;5AAEOT-Ab`!7;toS2%YWV#oB}J-=zuOS4gmx zMP9oIT)i}ZAyr8X_R>-9{HMSSgB?ub1Cu(CppOQnX5I>TaAx>%mBe54O;0)m5`42i zG%Z)vY1p8wP4@iTo-NQTlZag|P{Tt3xf{z;toTNP*4;!3MrcAgBH|vwg#UGws49n3 zK(U8P5qDaI@%p1H<|ZFI(U+C=6*|JxQMD7^apn0SQV}dwU1FIB-$xi3FA+V)_a2eE z*>BpAZsGG&UkPqb-T>{IQA;z55TZ)*n;`tXZBHJN&xyesma!LW8TZkH14olTB#7Xa zo1*XBcloh-vT}xstq7#nw|$=dObsq)35Ezc?}BB^n`cIZi2cF6V6MX(tgvQfY~3e~ z$dz$d7h4@=f&K?aW$tCOVA>&jX*uE^V>`_F&{~}EV$j4?FYauEi!9vk&QG}^d2)JE znA|8ZW1D=g#z_f#J#wf{9VL4FZxkc%REc0$iBPtg)NK?-eUz#5C^t4bJX1XPn-+6V z0>LnX^DyzeENht~5eWZ2pQ-S^L0WqLY7;g&v1`Jl*CP<$?m*x$c&(y!hC!ER!W? zMaW7$>22W0h)+46f5YI&2sM6fl86)e`k3ogfjW^=_;sAlf@2lo!wve^NA1Uqy?s7L z-HPf&ViUG5wuQT>manLB`s zzHXl?CUTiP(y6R;CRA9RIc?PPIxYUyS0MI;@TJS0xJh;O8au9dEqHF5j}>2Di(rXL z=BM7J{VSMvfXfq2a`tLF`B27BlgxCNq67(0ogmHxy=L*QKH^7 zso?`1JR_vTZuw#mypL2XXAvZU)ginWt`g6yyaz$Gi~fqRwD*_UjeH4sqD0k~LR<_V ze8qLTD3BW_Z^p!H5*(A#bNM@e#zo*@_G|HP7gy1`A7zFqpn|3@H^vmy+3|4nAyPgO zO1Sgyf#&WkA0}Tb>GbTP$IrE*DgMy1V$st@L!9Q^c&+floEen_d^cv2{EUkW4$(>3 zFn1n+3*u~dSC$XK?*bwPIrllS62HNvlcPi6BQC9&)}g@0^s~c>dE{`A=Qvj$v1h+d zUm0)ZwglW$jL`0v1;Apv=(kHjhHc*0KTo>C2>Gc3zc&d|!Y<1@Oq9=P;1NWvK8VVVxSih%)c0&x$S5o-kfVcz4)dzf-+nPJlLva8_acZ{fUR?%j#{GPC zp)B|bP=4u6RS#a=TlglH!3`NiZD!nWtO2*JYfR@_{{V}h^&Jw5)R4B>Ir*$C1$H@E zdX4jYE+VTj|6ar)1KslfJ6Wx`8gT^ggT_%L z_JuHH*81~QoQwfOgLBfsb=zQTGLq#yvESs#Q{I;*xZr9&eWKHXRJgx!wJ^hE0mb-V zPw^0-#{UV4MkSx*!DnuM@@4&%50)52FFOvfV;L)B)e~AxfWhf`l%g;z^gCO3%F=`z znudq}_iBX+2316|$~>C~Co7x_ANkVYfHA|&{w7+?m)ia7ulq2{Dpy<^{BI0(gh{1- zRpEuVWh~r;%l6Uq^^QI|f~!;Msb{5-B!FGk3}ZAb_+d%Fxz+CjwD9nlNxT6s4IGgO zSj6ACaa{U6X#;0sPNGwEW0Haq$F?duj49G#fqPT#!5wR0O=(5(wBtAsE&4B)k;vt| z;WyNML>z#;4 z>iKzvi#+%s!x~v{0XHU1ETC@Drom^Y4E|=xxylTpx$d#C{7178^@<4`Rov+h`N6GF*On<5qB}C=@zNSMgc#GnW zqf9wJ4zvzbCW#b>b#UbGQE3XeyTLFg&PMEW?%UKm@3COf;cLccRrugu!f`sUscLZV z+n2b&jBg-sbI&M_lnyJmvlbTo?MBn=ls|IX*zxZ_4uu9~M8Da0?AI`-F#cd1`O)bU z1D^2__IfDP~WQ8u5RC-0~gyptMq*8 zL0$(1vl?9wh;S34*mp<+PI6XsIlDW^ueyl!nGiXy<^1eRa&iM)^ttQxM3EgMQ;Hx3 zCtBROc~&E`Tn;kHiE|c;u|rNSj~VM%`{=eGs@;7)4HgXkat&q^-uTly>2jVNclZdt%cUuBAxC)V%UWXIBw}&1p@R-*F;z!@OW#AyltRb5W(Se^ z+7( z;E?n&-ipKxR8r9UJyDt!FMkzreMWHd%M2f!3!G+vDVS%){?{H@DVycXcjSf=S}7Wa z>{ICR7lV}w`%%u@mcrz6}-xW>hjv!qNa+;|#jM)0%aED_9BaE*|pkIEyhF%Eh zpNRcA0OE%nbTl)!k`K40T-911T`KP6?wOtEl{0rOmH~syI@zI8W#upw9tmFq6=TtRqKfR6D#Qn)^}7CQ1Z(Qk2wQ6$nlS#UhEq+ z%;ArqUVF?7n+D<(7mFs4qTWi?y^=*_ngR_XY#HE_e9RFYz(DkM4{~@U@MFfvucyxV z&7<(f~)xNGy%*j{$;wc>e0rltEPwq}^A==8B zf+RaqxOY>k9WhBh#(lPcPKgvje}8}57?Zv~bS)|Oe?$JI|hE%Vml z#u+E-PcN_Rg7H_QRJ%kzY7)MkIxQCf#;Fy*7Y`EMB*q|fr% zW-Osk@&g~0>trF9g9FXRnQm}sV5yR=aS^m2(QLNMw6KE3(d&=a5bzRw-Uf}z|iiZ&vW) zpD*T*)C5vPweN9HlVhg=n~kj&+NOXx`$XrVXaN){4vvI4k-_J!iA_bsTx{dqVm?Vf z@qgXea_wOHhyH8S`8{jCjq+df3-u?Ez@vwv)uw3IQSw4s0b@%qF!A{H#x{!BTi&?S zv-pnzk4b-Z=pdBwf|YE1YJ>+Rlkxb88o@RcD!8ssRq_vorEmzb?3bZ1m8y4ps-)Pl zLC?GQEF-3SqdWdmh~UV6-Ft$LlVJA$D8p7A76A8nyyNCQBB$`4+WkAx8~o|eQ#IyC z4cl)PeXMsGMLym~MOx!%@lsIwiNyjIXzRZ?Do@jf-ujf^sm@*mM%_1~TlCrSzLs}( zIdcVgdh*TgJ#)f8@x@!aXuk~_(Qx8_SI0q}cnAxxN7B z1~9+fpZe@V0GfE~tM}J!6VWP^?7jO;@RfJ&8UL!@1X~?a8HLZs!SBq-=i}3apZDiW z`NdDesB$r{rd4|%X`G`uX|}VB$QM(4bmaz7kiGHTXa&LL;^N5O43&WQAAkCG<1-WP z>kth27CnvL{uvt97oxygW^?vmNwz?Ew3`2TDm^ZJ*%@gVe*pefwBLU>OO5-2YYR+R zT2PoYr3Yij60o%XeK$H{95q{*(#6!zfb6)yRdKfsbk66}I!y}|4mg$DaP0;KZ20DW z_Pf|7$d77pJ$9ZQZ*PNTNn1Dve*`n5ALA5%%N?@I-_pP!de z`s=eHvcw+q;pH3ed4KevEnDOCu?T9IIC@A|#NiKmMDide{0IsBv^kj&8oPt2m`y#s z$QMw<63N3|su@7@^>XZ^nk~Selsd@GH4YNYZwr2q-9WARZJe!|)Yw3=^sQp*HYhuD zmnOu#2_3!lG%84l6P};FuR27$kC&a0Scudjy6r5NGrc^EzOJ#}Nmg(^pKT-0?cfPuvFp}gCaID0=NSUYu!$X|_FDU6U{t%?8M`t!3wC;7bTgV*|j zPHWlfPVXns$bFfzh{y+4`M$QYOIZYp%T+JTe(WPHYU)G6GA!7MJpaI-3k1*Oa|%au z3Xu=swkB(Ef}w?BQ24s= zqH>`$Jlj9YY0pgy>+C)6k`)mgaPbP=uhM^!RFe;1UlA*AXr|(MbZQcm9n9F`&EbG) z^xkF;WmTy7_w-EWGY(kv?&HwS-vr+_u|Mmh-x8u{KQFONY~Mys|9$n%aT_S*rAuvR zlH!WDb?+v0SzuCmc~+hFAeg-?ep$3^6S-O*pLQJBL|3`iI%9>qQ2sQF`5)eO#4{NB zqn2?L{NC%3>kVeW&ZUv0O493yJ2_T&t||h}o_CsKAny5V7u*ur#wNjIb0Zb0c6#XM zMKfO(+=hbx&a^&CSU^ver$QyqjiI0H2WmW<=a7Hq>GSH{+33{o=nNa@Envuf_|{c^ zg5yt4)$nk49?+KYus&a@6*}|9IrSGbi!9TA(ATah(R?j^$}bR=QE+xfkM_7L{mP zDBc`SYy-N7K9p%+TTqp|&3wW5Uv#-e^`t$qKXP^bdR^Tx55>%Kzx$fBg-qBSCOM}j z0e9g+kCtN;xIB6L2hDwI_(yNZkQ<^eR(=iuVv1nXju=_b#HhYYsa5fCiHU9X;^Wcnq9~ zl-s)5PKIYYFeTt!Pwr_q7hJfU7sUgF%({w#f^1<7-9drtmfgsz_jEqA}q zp+7vH9f3q|+S`CB=G@LcI{j?&-n%Db;O$74&K{BfuaWoxFie4&^tN;V`h((Psen3cd8Ofd|oJ=_Mze$eCD6IUfN z?FNy}jrv-gJBDa&x95+Y-$L46x~5IT#Qj>3+1PoB9}1D$9e3>SMe2_-KV1nQKxenI zyc*^w0G;qV<@?iQkeXdb#=({bUoqxA)m+zy!ZvE#jxG?~E59cP%=dRuu!VynHyiQZ zvR_nR6ese>obW!$*KgoZaS%Bt(O0TcS9iRhe*@XSW}@);w~U(ZJ~}SnxdnC%8f>oI zX2HQm94j8~RY*LF<(*T52y8U9nWwcMM2%0U^De3kgJs8;L8^u`NT82X^s>Pwu(=xA zaV2gV?PR59^Z#f^s}_=2>isr|UR*&{gd^(!_@d(yoI*6;4nD( z`A4@J(aUllVRLVZ9RiwR$e%Br68~MM6zXf)1x}lR*KKLHQAZ_f6UX~yqCcJK5V6sS zMmlTrVjnMpVw&1kB9e)|m)_%iSWW{c>M8h+*Nr1XhjY3EnQZ`Aop*kPN0Gw${<04e zok(+s;pFgqE%+9+xh8C}20o0ucYnf60VDEy0}m5icM95*gYK>L1cz%YtuUe&T;e+a zj_nuG_mkbYIqg+|I{G?HD{qVf#_N(7&y_C$vtf3{mtIu(>MhS}A|{)N!c62?4&nDL6LU`yblPm5$Evf;fZ+~Ro{6YdN4iyRi*b{~Ax5-sJE>56pnzoPM zE>iV!Calg~)9?&n9d=gY`!n$a;rd=?e6uvtg^nnWG*wP^~WWH8IHc|(MO9%kEY z1kOE7LYzLgeA@P>(7BvvKOXt7AV1H(^;ftUcu2nu>RoC@cRzg>^**+WRz^;Va}aZg zoJ%>JJ*NpzH)f*Zw_HK_K^iY^OOB!(y`8gmz1yI~DBenLhYIez3x6hCO@a?P#%am^ zS_Ct4P4w0hd@xR~%caI*4$WR-leKRk!L}!rzyD<31)Leb!fl?8gCIGv>GnrNFVT}+ zB~nnIU{_yiJmEA1+%~zd<+(ot(aUx;&R-W$tlgJlCHol=zuU5E*h@ojl2cxDudail zPj)S8#P=-v2q>Ql{s#aJy#jT`CTg1$A(WQ;h|89_RaU+R`EufeJa?Cn7n4!Yaqn3a z8Qe{kE;I<19C{cy_BW8TgVmca2s1v+nR}tMy5vhg3VUV}%JN1P8FQC*9bADwvifn?;F03`_PnP0mLsWdA_U}Lxw%vT+;*xxlnV_Gog1DICA-T{%KkRl5a;+(g zu)u6b34dOBbeQY`KNPvLdQN&<6F%kZllHm5iF4EFMXIe7@#mtREdZ>p}@ngW>3U7cw|^&Q8nlJ3TD2YCQhv{VHJm&PmS2$^c2?X8Vdt z$zk$8E8EWJ9FXo%=BOaSvu~wxvswd`nC~%J`|)}jsJncsU}A|3+HG8(Io7lSs`^Ol zqwZqB*IC`FvBd~=a=z;RscHlaRjLnWq~}1(<7J~8BfDs3h^aDhtPe!Fi+%K{O#`G~ z{HC%J8K7?bxwTEA{~~R?))mM`aOf^(3AU5Wf$P^vi(;+~0m}f5c!gJE;EEp0nSvf7 zCus)09=BUS3#va?gk>Aia*9V}2Qm74NJYAU3MUXtzHyE}ZzqvUWm95sWWfw)m`px@ zBse*Lt(3|NXmPD~KxtPGBXpL2S;=;v6;n?;{2`k^0F4!kl}ib~nch9&ppbMS+?C=P zFn*dEX5Y3<(b6D;r#Y_-ixd8vus@*{?}pf6>fZ+WTanYCQ+IVd#zYnV?LBKB`GgaG zueIFCb>PDPj>ptxmx)8?J8kjDeo4cRsyCXLesRN|sOMG!yAqIZz_L5?840|ma%QDo zh5;VPFwMV~N(W7@Z}P=Ql(v-jMron5{^`gA02rRxlmVY{i5?&=M%louS1JSmgsol61T=$lzOZYyE1R}MisTpR` zz9e6Fw}%uo_st)gyG;gXJDshr%4x&e>`Kz4h@C|?-qzY!~JAyqikl_Sur*FN<0q0JIzf|a`bR2(fPB=L3(&} zz$(2NA+QXXZx@`Q)1y9sV#qZl7%QsQZ;*JE*Yy+EOSreKAX5-ug} zZ|Wx!pVRb&+F|1Pb=UvV6}KBqu%yWS)-`_~_#ygjdBq`dDDc&G7){dQlNBj^>gR!Bi@L5p!ZYsOv5PM{NHbw+T!8srF2 zka1hEL+Q24ulMfnA&u&pBE=CwNL9N3Y(jv8m_IZzv_}j8HacqiN6VbBlG5(t;z3VuSbg2rV|{9ke_Aeg{;+#-P&HSrk{_Yn^Gr ziYcfsMD$YZX6>n)j|JdqD~F4|og?UsL%>V3&y(P_QKMzw5e__7 z^xHn_HNhLdsv)CN#f1-yMhR;FWr0eiWNOxV4?#ngLt_cy0p+-KTB?6;5G8+!?2^zS zeBu{4oSRaXk;&7S-1jLrQQn2LUY$yE{3XzDjOI2czULg!uLcFNOi{U>ii;b#<61fY zkvLDYI$PxO+mT{2s>d1w!}R!#lAhP4Z8m5T&+z#C5qcc%dr@;)kquHp`VSA=nV|fP z|HI^ZHc0hUSLGPNlOI<-Tqr>JL=H|V2dT44z$LEt-+RxhKs(K9QY}wz_(CT%=ZfeK z@>LeC!iP!WS00-6WrAxGC6y|kvPln*ys|1wBO~~43Z3S5E0oxy;8{_Q%M#e2&EOtZ zAaca_oD=q&Cs1>D>JKd^7RWL=XqbC)1$`FR<0>v>!V1!XZ4yLp+dJ#U;LRF#d@AKk zwsgZJ*dktDo|o7mv+9}J*mUCkFHwc$KaoIML+xrB`CSxz3=nHL2S$awZ zb?v`WgzwVAF;(qQXLAnt;%mv<4-Z74WbqyL)q|_Ze`4a*b+JE$Rhw0?`Yr=J%N%=$ z>?IY>x-@Rvudo5ONg1E`I@1vS>cC){NDio-@>*UchUimxwI6w>O^(TH^A1fKub`E8 zH^mzAcG1XTnaE9p6(pYa=0}hN9lX0#EOLj05mW869{Y==;ZDHBe(Q%JQ0n_z;dS^1 zSTHP=KiJO#_Zq@?bK9w~4fW#+62gl{r7?Ky%lU4Qw|6tzN0Z>==JJHDXN@9LTMu-f zVgwjvjMt>3&_Fp}H#0NaO(dn)QvbPO0Svip%7~h+0D8fh!T#BHB>cn!h+fb%xGDEHB*&36e<9oe!G7nmk`0@dB3f5P?(t;QoQ}I?55WQ_-~NY!j`x%hYB48tf6rU)(nX8^&J%3bwZz?z?wTZE$bTcycFiASlWYH&lcBfT-pO%*K1Wdn^^Ff;@sxq<6Yny z9Sj|4W`vA+W<_%53h_(4-%-q)Wz zFjBsiI*#DI&fZO5YARku0TvTCdbJjiz0^;wyu?YQ;(5>6nz$eE-fa9`$2Y{zPFMy15SDH=L}P4ZMNKRb-LU!w8Bej6AhKeDRQ ztVh3$g(bWUmXMR-7LjnD2QB($60`CIC%ATFrG|m%kK{bQbCk$`CrYdQT-hVRsu>}R zs}T$^$j>6TS+^SSUfI&;%-=+c?z4*Q`y`l?=b!$W@2lYH;^fnO4^k{m)E~|Z_kzk3 zwbnN&i@|+6Iu7pn6?BD(_k{j2BIhJ0{yU3!KDP&__(w$$e#=eg#1mGOn1cU`a8ej0 z_NDHRKS9|7qB?ARH#^2q)7%|3M&<<&{r-=y@8unIFE_!boU#Y-UY=NaPxzES8kDSX zJ2GKUL66rA`J{L?;83Jl+#x9oUxloh**AR%i=#AbLx)KZ17&?!>|!!y{n36KLY?+njBS;AZr#%#QEeQ2pzSI z1Xsjx^t{YVc5+Cbc4DT`?>_z9=6Pa>&3BE~d!Eg4;0WiJ`c^q07!IqPo zutf7BT1in<_-RvzbYka_g<=b+4%5G_G(d(^((`U$!uzai(Go|>-wf23oLrgZHV|vS zC(Yf=MzB9EG^Ay+1E#)x=HKq^1b1J)nSOkZlj!Yf@aD7ALSdu&9yMbybfM z*4~?q`jWtiht(LIp7^YxSedJetweA3NdNCYlt2;=S}^%IQ!gUz*iYj{rgU&OlC%Fw z;5s^%@3H(=x(QvJ(x+r3V5x>M)={)4a_5CCqVhL-}K4e17PSWa(Qr>$deSc zyeX{QMb!fd17K+c04kyn`Y2u5$^EAzXpJx z!V9_+FIRz;)nN-e5;9!eBU$9oIe_5)(anq_GeAqZVNz*x64jH-i7FTn9_0LEn_h(f zJmSo~ntP2opf7-{%=r^J*8I{FIF&|$O(bqEfNKL_*wi(+@O?ATmflb~_}>Bm6)xEq zEy!S()@PCmf?Ghp3Ua&N?EpcQzJMOW7f@<#ykQu!grc|`XPeU*A?d%S6TVI(fM+LQ zRakTY1=#h494aR`rwuNrcxoomJQK1R%O>WDHVm(`H|7xq%j%yRBQ99nugd9?8#sF zBk?;H7*iJ5CFs{6Yj*92r;cnP{p0`%o=`gYCA*F39K$}knYpOFJUfBR)22A&ZqA@? z>K};@zmj1!LunVAvR*_>ch>*=2{DL|m;KevXNOyU|1>MScEQ~wkkgvH3a;)I_kY&v z1F@={u4PBLFuh>-*Y{FGz_KLzr@G$&P?aNpwr^aG>_75fEFk7NW@~@;YD}52Dn)FW zv&%jrUyh~kw4=bv&-hh#YQ}+#hEZLr4-0mto#~2enE+7?EdP%3@1YyhUv+JNbHeea zmrECO>G1ant9L8^vB5au%lhy$8w~yXef_#cA!y|q>#T2`MH4S2c!LNYas7{YZ!kfJ zZ7n$?uJcTwPj{!3HGWTn$}f_nB6Hq9S9cGweVbQ5J$VxdXs@ROG-* z+fop-l_989zlwhQkfhW8q=xw?U0!oMVt^chq8CKsMo@)kUC{ogGQhH3mtT2m14U#S zUNNQGNA!OK2C7zw`EtzCi*x^xz=1SYj!0o*-d*JUD58js=>6DS(Wl%3HgeqSD&_>w z{o%@f|6`&z{?T0&=Wl}s&YIBS8G;LQi_~nrmEc_K9~kicMZ9dAu?p=3QJo*P3uDS{k)fOB324c;Z%r6>lqn z_uk8nRl-Z{E_^{fXO$FRuOI$A>qJfD_KkT|0?N=xQ?EZ$?;2vcV;8f2w-+$Z=INHN ztRd@77UQc{!tlz0Nk3@T(<_(l{VA-!vZ!GccfsW_F zJl{VI1Kvyn>SudIZ>&tk^rlcVNI5aQ<3~>iAK$xXdCZ>&u%3Mb>yaisdujug5bHhff&n-yhPH~}h^ge32GG6LcKMAf?(I4y> zTm%zrqEpv?t%J6Jp{QF|DWTMnpecFD1#m@wj#6NF4yBh33Dq>NAWvI~ztj0ssOWH{ z`TOl1)KzjV`+~&=xE87Xw3y&GsZx{M6>BV@ZlgzICA+hT@14IdwO2P-`tR<_s^=&e z<`eB738%x#M~v4k;)(gli>|Z7aTDmBVc;w3fhk~mH#L1`S&Z1Lt;Z$i4ugjx%iZLw zT_`c@0nZOJ7PyL{Xf4vZQ84pb2uIckIBL5lZbQBYUL0x_sHK@eu6zH?-QSZzzqG~1 zG?WL{?@t^Gq$Yu$k#*;C&TkW3TP&kQ@Ph}BfX6az4EO`R&kV~yPJEm9nEUYA5yaQ8 zujjR~4dh(^qwP0m!1>PQ_wUFPd{emqTCW*`P@LN;q%%1R7K>Hni~MPzjeW6pp2s?( zxsxN7-b8T5A6Q*IOYB+2-nAUKJU)*CivL=j`ZWy1&;7i_pu7OSo0WYIYFb3njTemy zpHktwPwd1bUeUv}o(ZYEerl}gHu#d1ff90r{Yr};Sp?OalH=_DGl1zRW81At;{Vj< z(>q&{4WwT_WqgXO2gS|T{c|(!MY7Lax4i7eL7&TK?s$Tu&++&~VWGnmVtH{^9N+5& z!okmSb#4%I;nu9|r!1S`k-V1;!y*Z^kYlxaci`;(a%>vash( zaA4_#?+l_7L}s1F@^UWIK4i7m4kx#y-I#F zo1A9EL+c$M&s z3zRYC`PBX1K)3n)tPgrGfa3|TKYh)z2DHN!RbNgmgRNJ^neLqy88-KNKi(Br4VUfpAD$h{Btn6RY%FI)sO8JLpNyjW0%OPjWQfo%5 zG^(Bt$VP$9waG2Rx0~p0=%tLme+m8|pb1i(A;oU`V=u}}yU}n*?IjC3!vFJKR9>lj z50sp+Sc;DuLbHJaR4>VAP&n7qoyyP4;J;d)%ukZ!m@(~4-rD;eP*8fwr7L<6g^0#f zTXfOFh0m|mQ$qWI;jf6x2dk%$vdquQ8$YKJ3wO$vdFTvaH#=H;wS^9!c#IMTdg-uA zE_3_EiXLP&t@`zG{WjQ8V-PS|C&j%3ZfsPCTG8Rzf?A_GG59B!!(+#34?XMKX}?56 zh815#T3Wv8MQ^Roh-k^@07=QRg4VHB)aP%D+IQ*z64r$#9_Q zVUx|Y2Jqv9l)%x8guk?F^2!#GD}Cy>bqy`NmUfR={ScrNEV>D`TY z6K}TB!qe}y3vdy1-XT9F(li39{u4%t1Tzy=Q}EpY(BJtuwc{YPLOTsr=oQj3fX=ESdu8~5yhPiEQO zN+tuyQBx+7x+n`=@><=?zPW`+!kG`&gplFA4AJ-gX$$DckG!7kR${+7WwkmRw2PKx zPP0f6?_s*=&!4z9rjWo*d4Er~F627ob>@cu271D%2E#eXFf)r{JeJr6mG@T-W%7Fg zv#@JT^ymaIzO(U1{bDx?@LV?kmA?kgdOEkNzhT2ZZnJ#u%)*fER6JAY%mzxB6mGol zzYlzu{8(lQ4yEc@kvXekqW8?VRd?&h9}wnlGDfyD3ywAYis3dGM&h~yYEEyvkiJ82 zWk?;tfjXHe$Q8ATdmmWh(W+x97b`owN(c{^z$m~VUCzU@r_^cv_ zTUwM?Qpqr5^|42VgJihf*=x^7vI=za&OZ(hC&7{FF3O9}ztG{dZoPvx{YW$;r^59k z;hVL4FTDSC8?=6zAKRf^Lnngjt-BZbp<_wxZI2I2fKz1pgMP^Z$e|CH6j!MLj2l_r zLH#6{-1GgL6m4qUxOu^6@!cq@p)m~|Ke7PyErrS!Y)J8JOT*EE;#r_|o7T|YbQpco zvw5X^X%#f*GAWLHoCHG&N0}wE#=!H+u|J*1$zWgvS8R_-9+LPEw(gMRp%E9+<>sfW zs9v(GbI+aNv-E#@?)Y#I1>LCNH#@Whks)Np}QDlD3t@G7CV(#u3 zeR^bs66a)Tp4>>NM(a0ozO&yL1WqUI_1ZX=ki&l*KXj@HuA92W&tytY7@WcQu{LB8 z#l6wxy`E0+Px^JsXS^xlm&DqG30W&dE-M0Xsm!6D^$GMowgaHSK<{k|;nP$NeURYb z-H1dt{bRCIh+e|#R{f8iCUh?S-UBY{1@vHFJXRu!1==C!}b*74QZrqW?# zzi_%oF$SY4qln==g*l+cT10nGgcPo?+T3sT+(EN;BsUk10rp)3+Lg9n!1EhP4_FwsP^?HamruqxDoyYF*z~3raJ!{cjKvM0WF5~> za>U*{c_re#B+(;Xedc0)_wYJ!=3!Mlk~0U2tX6LyVebSYnB1T>cMP;jJdsK!<|%0v zRdfUT5K7jM#3X+`ieIT&UJ10G1~S+4$fHkqqn<*BC|V66$W2lH*lL*+FCNIBX&`d* zX#dlV7li-JxGifn>i#6kf1mbQIhq_UD_N0rD-46}ch)?xSpZX`_unw@jUb`NXQ>OR>QPE_s$Wg>CQ!VPx86Kgf$nb~ zeyM3k`2F%v1$^n925k}Iat=>>z_HqbFL%!~;Y-rhV-xDcJ(jexc$?_Wuf((3N>ZDm zx}#ABBYMLiUaYLG@WCQdesJtH<>M8wkr2?}_7Rt_u7QUK zyH;Kg5_zCgRgO}1xnsBi1E!ZQae>$9u#g~-S*K;frQ5^8A!j*ZsdbT;=ZFY? z{*)`0L0$|l9FGeP3!#B(|5C3r{}qQhGv1EwPZkjqht{2sR#TvT?qZju?=~vT-I$#t zd~8up)vfZnWcY!*-HhL^3XJqQ#~bTMi+^LfYi7wJ@TP63^OrCw_%tmrS<`zTFk2=D zs|OCEuIPK_wF&I_GJ6nb)=lER`$f$pzjP09Gh5erlZipw%!9RcK1wjEeA8<-hXLO0 zzA0f+MuPuqdn-5AAPG$~^ItwaE`sGv&(A(gss>jQd>6eFS5b!3cdplbYH+Ek;^k{T zQFuOQ(TMSu2y{S$ve(`ngvVlDhP)&Ez+D2-E1^#XpwC;%d><+?C}C3UpKK?EYd%H~ zUw*&?FI9#f{AVJE>9eTcPwn#J+xlHMR_9pZghXfRfe1FpS7|G?NWTdD@`T)mhs2;& z>n|&3`Y$E7GQyF>=S}8G{`5k`@>Oab!}^W zTWS}CYGoPm5_^Buf>-X5uiC-lL;jbn#J=F9apk9{4@KdL>M?rJ3j%P$%O_v(?le-m zrD}_Rb3w)UsBK9}5^R_jLFw~m2f1BVpBQiyz#@JOy4}QFcz8;F>X#KW(W`zE$k!tb z|1#V?RpmqQ9p$p=m|ghs%^-fGY&B_YXdNV{4uxT^fdJ$*V89!V_t0bmH!e=w^f;ou z3m%IlpAuJNgoAsOQy=(OP#UOk%Y4s*3v5ESR%d8&(m9{Txdjevmmn^jlgAD_pZ`@? zqp1gvZ!m;~9VCaY0_rC|ld{6?yksRu8fsX5fIY__^q}wLN1}HgAHbzqs?xUl^6)wO!{K63M|HxnFP8_aD>)o;WT(QR{Vz|&B0i`xLh3t8 zoCB5YVp8c(zCeyS#aE-ICkd`=K+epe0r2p#EgAhdAC}0&lcz%m(UfuioU-*A+EA?3 zE@|h)KoOF)vnY7a`%Y} z3z@o2-RVqYz=tl}dACjEi=S|WJAtKxxdxZEnrbJCp>9@Joa zIlx(mcJ9)39Off(rUj$h+l?f!ljgJIXTLq9eC6_k+8QPpe3ER^_rfG_r4dq;&LM%% z!tYNCMG^j(3m-D22<|4=0=FDHi2yD)<@>T7ss^R_dVeo*%0eFg?{_{m(P3||r!4%D z%fMS_QyEXK0@ok8<9E~*U=wqZ$|Way{K~VVOp}!j*2E{-H~N!c;eikEj4&Cro4Ngp zjZ_)BVXaH^rxs98l+KA@4h~qxt6>%8M}s@OqZ2FN^S};{jUO`^{4hYFx_D+x61&M; zH2iU9!$R?ooGi3xAVbi(zQSmBOio`--)JcUFLYaK>Ws3$H^1^-*Q&W;>2Y!5^OP2^P4b3nF&1#a}{4t=vGhrH}uD&C9- zpnMu7F_^3Jd^tcRSdAG=8$01 zJ_UZ5^D_L(co|^VNqEXicvjDg>$_Z|*hGVGR!?^nQD8N3%YlgyTD*E|hpt?d5pHwR z+l4X+;LpNo2~S)mkPmBjZ!Qx#l)Eo_E<}=_5g$#U%x3>slq)s>%)Cy zY*28*pr2EUHhr-Zd43*I zR3P?Ej!`;!q8T(e^J576*e)ZCSGq8;^fw>LD`ky-_{LT!m~Vbmwt+PPS|ot6jBB9;uM~VuvAhRH$Ikw`f6+p^d)Ipb!g;cBIt|m?VdtuW)N|EG@p8v|TG2!U;8Y28u(K z77@vxEn&F2h{`xqViPykfKczHgo~Ry=(mUM&esDHxH)hlxuuTa0tIVkHpC30L~VGm zwsQ&@Ik{KA3=@U}$My6p3;u!VFDEYquCT%D);wO9Rcetv`{Fh4U1HuO5nwA$!2&ZH z7~ZEbj{*7qzmG^p7LbhR0ro~CRy?T|Zp3Ax3X_Y>vvNc>L0^~UP}-3tP*3}t8lRwo zY#Zv%-BE++sNS(0eFYv^#rT2MAc`7y`UZpvkZl9rs)G0GDx{F1&?xIz5#g2ay^!OY z!iGP+4l4-nB=RW|u`)cbiRaLB{WDxP6p+n@$#Z^|1@}JF1#3j!+vC>v5G}$>OGn-# zc>Osi)Yj6dzHpNjzDs?md83*e4;2oym$A@db}QE?)_GREVyNBs>{*Yey4R1At6I4(4eR{V2&2WgD9oB&)IP z5F_3Rn!LXLpdcSZItz~qLOZ!(Y;o6@Z(s&Z9DOgR`D!2NU2e3R{YMY0(Xr(N)a!`P zy^|;D))Zjto2^~l7s6)i8lPI`C}7J4CI4O_Hhkfu;n2HBNWRdq>%U$}EDZsQHbFOcB06kYj61k0RDtrc`* z!0{8Uul$hkFiH~z08$7OjBk@}*8wSEpj$iE~gU=70 zbTj;prUYT=X#5D~rvh{>7UGc`6BwgYfXp*B@9N!?rabcD6jPh+DG~`xhL+=!*`140N zsCEareDzpne7u8VPo`Yh7$t$ZV!sVL^vL0)TYcf~?j*YAqjvM(#6HNn86kQ~i3DqZ ze<6ODeiroSo!9m=6v07aUmTgX)DZ`upJ^GZc0EFTdVo z3xz$KJ1^q51rUqhvEsW7Q0DiqM74NU7~;xUzxiPmtc}Z%9h9Yp=^b;DO6+Us;*#Z& zTN?COTmHMfpBy=E8jGBIW50@aDQvx+`bhC(t+{i5{?I_O0khEMH+`UeQib*R?myt= zo}0ZQzku|Uq*zmnNC@tXM%>O>8my(9QR?JMio*=FqxE;VU|!;wY}hoxInyn_ZOXZf z=HIi`U9{Ro>2$KPiD_~WpPNp(_1`8q^x5I;FU>`;Kt`f)@dG7Ru)jS~FGA#fzvrGc z|4R=0s^-gtSGK^W$qt)c9x1kph`y>2LvRWTc)A+`hXH@&TmC2kPOR@L+g7PKjm&;e zkHnVI9FBhQ8Z1Uj`BXRd2yRDv z5tE?6^DQK=rhHUJX9s+KZuI-zWukvrpYb(Ff(lRO=#1CDyrm>KF#nVZ3)=nG z$PAuF)K#aA(wRiz-_xw(hc7c=Qs$nP5IqL0YyPimGJP0~-#--VHAsoSJR^VS&_D&d zTYW3NiTe;gFExMAiFL4J#8Ko*aDhwG4m}K|<-}K+%4sy7je)^y!<>hFmXV|NZY_rZ zku#w4F(7%!@GB^pUj#XBUfQSJNm$UBsAO$|ItPwJ;-Gkb;lP0sZHc{rUnqQ>1m(i9--;3CGQmAKjLSt%y z39}F0d~vXD5w!j>RXP?$iA_{`|CT4y9I9~oMtwKqVCL~%{ng2SFdP^`E!i5KW!6|+eljMAo2(k!>?G5QnTT{o@?G8?RfAq zX?FaO^BUNGd|TC<=tI^o21=gEqJeos!{bG66j-O5ed6D@4Rmz!>*wF1Ge9tW#P<6( zGq$Uq?fk1Sjq0l^F8ud(5s}3OEu}Is;gmS4Dqj8$WZF&9j|x^m^O#lvz4dx~v|HxcAiNWt zwI%Uz)1Oz0m}G9fzt-m?n{NHng%nF^TGW zoXH-X>ja9|zuAS~9|ok}@9!m#i~!Q2+PtmhVZdqowDGzr4L1B4!O5OY0=uudkX9Y* z1h3sW9pe?p(e|(mt6V(`9D8|EasurjBU|P#8`M-d-jjT!esB(qzgwD~3)(`zH1y8> z2%v;>WeR6BHz;8RTeJ<==>hO_qyLBg4>C+bLHVdfoRP?fa*fb^Cb)u6#qWvUo~${OB|MF+`fZFq-?PEgDn5MSnK<+>@R~oAEC@eI>Hkb6@)z!( zRU%kq$*_!aWTqu=5vVr%Vg4eL96R%ycx0TOLy>&;9>XsvV8OM%b3<2%oJeW$utfeQ z@J_pws!?4JkUC8;7tt$_-6S2W?_37i(yR5qbr(Rww?h{_c^rX3z|T0J@O`lTCdT9H zBo}-^txeg%nT7VPr^UMGsbIAVzqn`bJbJq+xxG|Chb7ZKE~s(WqPI5T8cakkac-1U zURq=tVQ#Fyg@2{RmZe{$^kJQI|*N_>|0w;ZH>7nBNe5rg4Z)EY}0{-B+j$35qMvqOEo z0GVKNa-5R7ZjqYIh1HyXBwBHj!TxVrs+&7Ruhiqi0KI-IvWTH`BkNm5cgU0OY^jc* z(o*MFJA}W){^T$D-uWRElf6E&qs<5}Up*(*xxRr6xeE_h#`b|gJ)?V7Wdnfw^i<)s zfeCaZuFQ-qcNWY#y!Sj=8wtjS-nKCpFk?^K|H3qe7Eo55L`xiTj1s;MtmxrJP`(^DJ-_E0_--{r-MV&EFQyha|khOQ(jRMQp_ zTp@FxFD<)Mps4yj#r2DAAZv1SfJPt}q0gxr^ip2|L+{T^o(*g00YwhONmGLBSx}&G z;&KP5ja9OkzqEyFA{+|>87Z+K+5W;G8(R2UJhuIE);20>c%))xJcHo!z?|GaTCDLS zQka8u7NO>UXB<9lqbuL?hBA58;v2yXVE*o|Q6 z#zthZd6&|&X9OHPmvZE`H7WiaUzYj7ng;LP=g<%#Cx>*gtjAQX$C2+%XBE|38(>Vv zB-!QtE_kRRCc`|s54uR&1TKoTgQ0N&PqU}v=&L5jX#2$$G~xCUzK&l2Jy*P6WHixY zR47-uaAOIvajYCVRkexYH9_|8~EUpE|)G5TLDal7;xeiX3WD3vqlfge8qug?7 zyXf+tsQ#M~8%U(ptm&r+;knLEJ|scx18!vrMcg(ayaAlI9{Lj;DXM$D*NigPz-u2} zj+O>e{Fh|Ahb)SW$c;{&S=ie{;#>q}=4-zvVC9xJWBJhk}O$3Ewe0a z*SI&3MU-r)^`}uZc_e@~PmAz(wi|259qtF;d*(Ug*9uVew$$aNcp+$;I?Ghh-ieOS zy(@jZ!U8vS3R^zRtf1%d3Qgv?8yx;9p?`$PQQdj;*jKH5932SuwpO;CMxcS`7ncDI zq-S-}Z0{z#_qng9PpmS-wW$n7{Uk>0B*rD>?>z`Y$9!y{brsqm4Y!hkV}NIV4i+_c zgIf_dHRveEL6(~Jm+qD#kh3ecd*uZ+%sd>QZSAm)bSif8)Y>_qL_Pap=H?2rRjddy zI=BWZsMy=4{!Ab~ty}!}9ETCnhyHewImQzBZy&`HiWp7L zopEQxqiNTjgLn4P<_u?j1z1D}_AOJL9Tq`M=2%n{1M$7ciZhiF^A{e?TXPo~$>2it zvSps+B-k`J4vrTk$6h^e6M1ZR!H~Rtk@XW+eEhD($*8N;@K=V#^TeY}&~a-qIjoBe zkKU3wXv;bWu2T+p$=&G$2L-O1_x+)O>gGJsW}90`@8l%x?HdJ=xz`OlL{|XK(Tm;o zAC}PR+x3fI-Yg=cWe+Z1UrLCp=eW!_37=kqkk(tCZE(^UP0QRCgxIB<`CAMnR2Gi2 z(u|u!`a<`3#q8+tjq^%hOfNP74QK8Is*4kVOTMY-?|Gu1;zA!Iy*dF7hsN961`Z-y+_YZsj`vdoXnzkv0ulKxwu))iAz<8JRjV* zjV0a(+8wjp7W*Jmh^e4#as&k^L9GX;=rP&NMNb(wPFN*dFYrPq44rj|j`~6POG8{D zU45BoaHv+yw3YulidDK}RjNCOKFwY3@jOW6K}DTjh7p{;Ns}45@~#Ecu_(Pil0o=? zx=i1mHrzpEr`A#v$M=wWih0O3>nK=BthL`}>IN+Bt@l4lZ=#4EImHJ7J4oSPmk9mf zII_@Ed3TGa5B&bpDeicY0Cb3w#HD{@MTu@{l6~bO1)bw(kq_N?GId=l9KCYf#f;!wx$XPZ{MVg@ z419hJXos_59fMbw|P>`A6P*n>3#HqFNQ#<+4Cj+!Ud%IiXokbPY6CVZ{KCqriK4C<~_*^ z8U^h)?X`~|n+K6um-pP7SmCH4jZx~OX>{V$#C8WUSLdoO5iu-h!+ds3S}$lKz*=Kp zLSq~w9AjfOMII!0BWUsPPaaX6#}G7C+(Yof?!2C0;bp*%b3(3PiQHv;y2q*hYuqrY z1|MJGVSsdp6FJJm=7FLFP43obYDmiB>>JKog|;8Wa;CG8LiQic?5)h4FauYg-bk=E*N~mB!T=M39KG+k0SGsgX}zn zMmUBSI+0qT+2B2({I6J#W|S8X&Y3s~wu?jNHSSmEbcHbOY?#`v%U_i0*PK6M#|QiF zCA4}-Z-ZY#zN9rh3~*wucVh4ZH-3J(BQmXV5s{XkUh(xKyvzxvUsUfOgfp#K0!ajK z;h+D(8}DCH!NsW(yVX-P*huzvOBFrQqlrHLhl)vt;9q7ZQIXKW-U2dh_>dNUdGl|n z@*XQr_PK6l??(#R!W@SEAM62|uxrEJQf#oK@hA;dsVEj6zr5_NLFB4J1_xD%dxFuM z%F(;8>cO9(yTWEIl(>@dq3~|a3UZu!LLnQu3iSN<@3>@8!wnlIUVdV~M|YWZAmsis ziWip<+x*A@ZRP5VH@}cW`j4w(qT!^N3fTRWvL?sc&r55#36E-=>NC3}ybB6qzdAHm zG2`zHV+xNpD6j^l_LJrQ0l*vZ{lU9qt6m2w+V*ACG2sM5g~lwU(@Iwhfzl=mSGbHZa9M>j_3@pB$~2e& z(Do+1rNuNkdexbA98j05ZYf%c8oxZolUp87g5e6S)ICjt^YKB+Nj_*9)vv7`Y}#hQ zoU-rZ3vUwr|G+1tr{6Hct+A-B9w>l=LbpXP_s$@WWtRoj54*^1^~}O@`2@(38OpXM zJf=HS>~yINw6M=jRFYk83AIj}j>P%V5p-(`%!Vib7p^AzinXYOFrKdM~>~nCz-zMZ&lbtt# zZz+B3@x@JK++T7wZbAUwe&`x~%y|SzK6+V-W+-r2%u}BDKnX~4*onk{m=oK+HISTG zSAiRp8aGr3AKmb0@85=r>tJrnjB@;>2$U7s(D9Yz!fshiiyoX5P?7~U#!8UEyz_fwd;Qq&1MaJc(P;VLG#x)g_mc)&+4x|Q$ryD5V1?4sO-z%H1fS-_KmTy5EFd&E zoNi1$g5pV@6?2|$L0JR0Z$@lW!(tJdYeIxC#`viFsUTratdl!eJZw1#dQPV~S~ar6 z=#m5{`ZYPYK%e$n#Dxp0QeNQ{jjKl^@mZOs1}n(gfYfiXj}<;%7rh)*Kn*4NxSuRG z(?iBE9xt@Bg#?}V8X7p+;Ouxog;K2u3>WgtLk3jv0{ck(N6RS`EZ1|GB9#u`@^IVr zzpe&ToF(qzr+7xrg4?^<^PE;D37e+X^bXN9n?PVgzsx*{}V+wSaI}VY}h%4&vqA zaPDH?2VW~J^Yes=9JS$(jPnvS_@zqi_;F)Sh{x}I_2FR0x=N`hO|Q)h^a-z9 z++QO-)_{Fr?W+1ahm7E6XIv1Y8J$Ps?nys2D@gHK6-(Sg@NwwLB`Ol)`F-pjJxM+Btm(M zE?dkvBvqIf(@h~mQ=aN=qBG|p!CxB4*I#l~qv3ig`FFibsDN+c7vrZ9Wci0NRV0rLbBP}Rb~22e zc)qkP=}#_$WzKrVUl+>Iuix?&F0o5!@ohFm7AY~O86bOcpp6MGco=6NKQM-(^2)`I zG*jb!t>HU0zO>kg$$Ra(2?gw9{m(|KS=oTj{l&$;O#Bs#9I8e@(LAlTCs&ZVMzYD7E`kF-=d^8COXLh6$!nQX?IBP5g{`3aK4Ok2>Apt1k8cN8L>ism zLIw428UqQ=kND|!h05(6@G3{P%Y>T*uIKh`pZiS$HEokFyJ2V zXw67={=@wx5Ouit*xWJ$eDg_FC5~Jf<_9Uy&JlZphF^K2_Y%1AMzN9w&kZJ6z58&( zkvk0CCEY5qA^16oqE4^vB>zKoL4Ef))(PLFK?3%$_=E0cM>3n#k-&N%>GB1{2FLGA z_ev@L19m!Vz5c|Wcga76uA1<7Wd|5fjNcstWPM&@ercOvSM|1R@fT8P&uRT}+L#3Q zc`yAiJ;aBfzk0N2VZnvFX;oX^zL$gajbR#*Lo6^Xxyj>0E*GBtvk@UJMvo8lJds>> zC;VQjx=b%t$e?}&Z6doQ2YzT|dikZ|6iQwk`{e$T6mxaN3<&m9!Z=>t%X`99(Dx0$ z*D&$ijd5T%8Gf{ahI!3pr#a~1rz%eK1+)QZ#;9MDIgsIjxAA9pWhc?M)06GHiZmFE z9-_NU@JkgaMuk3#2;f4md+Axqtk`OX;?{5wan3Je72o#W0rFY!ba*4-Y2@r54HBb< z&-AA+7sqdd=1j%oJnBPWYo>_C;VTQ~&9+K)7-7KdOGU#0R^r$mcB{U#W`QzMM(-(m z$YECLt%n=;XF*Xu<#{{3RwN`ozP@X^gesbTye;w}gDL9Axo#Eafk)13!R?BixQ2{G z(KeVJ>&t11A73Vg)P8I)OI~qd@WFFtk7I-2dU6*WzPf}oegu&}=NJHUGk5GS>-+=d zfA8G?N1RV@#1$CD$Pb{ggVQexAJ+kg$4awJAIHJ6x5QTsKWfN5_Gzh!P64V4bx1!H zXMrb#ThAs;Zz1YO)LP2>2XMlIt(u%!7kF+{*<(0Ci{S_RW+}BUbTg(?S%7l@X|%}1 z)h&|X+wA(kL%b%CMH^$n$E6N*J70HE@}druG2Gzq=st?&PNya>da}U#LN=P=XS%@K zNolvGcEVT2!(XDQxd7M;vmKf_SmEB&&9;^z23RW2_=d_>Nzs~L0S#GgE?kNKscos}H@q~0oEdA_kBM)b6pV)8z{(-px6LX<|T zUUMMNaSQembC$zz%htGl4Fcy?J>je_;C|q$ z1lS-v8oDb9=UjSt3>S!bXOUt!aerJ$ zB9)OQ_}bDBQux+t7ePnFWXfkLLPa`LT+`fzA+4dIzd7DtVrQ7K2~HTy4o zFCPT89fkY&&e6f6Q?#`d?DANJ%CyCg>@jrxnHb7TCy??5v59`q@8D#Qu}{-eYUuy& z%zoN!A^c&FqF+utEgp7w6T`hKh(%Q&`a>!j_?!Lj<6V!bAl&aOrfd6` zaN%R<);8qgsqn1u$Zy3$UO1Lg9#a}j=HOvNMkD4GlvA%vcWRv*|JRyxAw-4>kN51- zIhV!=Z`s_WJ#m&Azkd^uWNW>I2ABJ^)b}#r>150KnrLn;etJ>YG>Z$TsCqE2-Pr+u z+&;r=xr6Lm-;G^MoFm+3?&4-G5i*y3G=@K#D6n0?*_c~AtLVw+)u<5TY5;F1TdY)2 z;SyiLa+8#PpdUK^Qle@FPNU+s@MA%yO6R0>T;IetcoOWS)=r44|A(>7zGe+}uGiApKGQGn8aWDfiy=gu7$gOo*fZKBbA znv8cgIH2T0hLhKM@_xrPCix_v6LZMvNlC;Sy2Pf(@=TW>3hNIrcoXi6@|QTD52VML zK~&TDCKvHHj<&08?4pI^V-E9AFRYWiNU!|~nGST>ZRX`0re4zb&E2QF)eN3}Rtzq! z8A6R*J?^xm_u9gYyX47zdYBQl^>!ce>3)@=vE7kD2d_OGx9bj~$K`K^2dYKI;NAue zf9?=EEOU-q$m-D?IKUzbDMR7LxFP*P4Z&5o-MQfT8)JrbQ6Gq?Gifpbpf-w~C9Q-@&z4y27n%LQptgn^m!R7TkI> zY0Ed;1x|JRSJPQpgld>?-FscMWdaIHG_SzgzQ!}N8 z$VXU1Ww#LNi#$~*pD6}eV?;Kes`jB#pBJ@1o7rLf-D}+A&4epL8JA^B^va5_iNQS8 zc}Rpz;D<>b@uT?PUxSjv;9vnycmx~aiBI!e*|N7G?u}R90_S?cY0t%@*Do)i7j^yL z?vwrv?m<7#*pIZBT5Y-2mU^7%Qz<6uSuMyf^H{(7b2_;1UBtPG&?&%u{89g}&UtWl zYHkSdtpYdjbk_#U0;v78KT0T<4y#C~yu12`2SzAV6&@pfH@SwpIj17nA+wf%?bMwW z;23fT`35YZ{(ALy`F~e|3swtz@p~El7xt?3lX9l_Zfa--@$|vZ17h>-==s~yEQ@=y;H$sl zU9l`ic(H&*E`glGycB#*R<1UJ4TZ+Ev;+!xbot%8aIc@>ywcQ@JGGl=R-jMni@*lB zuvY4D@#Hq*=TASSrZoeq*yW$47R>=W8-X=Z%2{-c)TSSloCTJ)fv(km*)YHIN0$FC zHiJTgvwJ_heh-qUtt)!|&LY{3TSZ4-)8O#$T|4tv#bH;e7KawsI%52Jy-3G_3ZF8t zb9imPL3D}3HQzlMa8lVN9c2d&C^vo~#o_u4stx!>Hjb3|vBC?Zl@k(B&uICYzBv2dOQwoX{WRB;Luw~z|K1+I{_gCBVlD~IYp5-r@pGSBu_NX%^ z1Nj|K*hMzx5zCDb&2B3S{CKs@^k&2u(g`&k!?hJaAd5(ZDtqr9F#bf6+0Yz`SRoYv}Bu#+0y?Ww7_> zk6#scH-YrMk&4dvA+%2=F!F^2HP$zA5K9YQ1@pf+lY)(BkWhZgSC)!=)z=*f-OJXVo*8cAI&-l#}qLn)G zRbRXd4RK%UJ5KVj$JUv?0ixp+wWI|&b}S+1MLqh`n*w1 zfv0lxPn|GpApYh}6DOA;6yZW{o5Xdb#i{U|opKm7BzXI8TR&%_Pha)^tNzLbUEAniJl1E#YocOzF5V>fu~zJul??@^Dm-xU(+f$sFlYbN z_T2(#-gj(zk|AnmGu`n=Ap2(D+rGPDa@N?%q8yreVaezVP<6#|n@= zK_OgO(+kRcA5hj8j3YyP{mk>EuTgxlY=7{^9BTP>E$sN;Wt20?|LWQEI%It6(2%g1 zB$Uq8dbz2?j8j}$_UneWf}jAsZk5SNR9~$8+R|hVoc;LJj4GuNDYA)bb`CN_Y_I>(EbuXEesEy@!FIAFc-~q z2hQK{TR;KbBAYUCo53Jg4{}==IRiE6VCaJPhWT+S`2N%}-c1E& zIK;A@-9h;Je8B~0uT`+)+N2vj+tM?H%``Kw`i}}q%B3$A*DaxifFmy3Jin2S?@=qJ znQhc!I_>zAH600^?^>coP)J#4PekuK~!&e9kh2U=}v#@0e9Wi?3D6nFi1KVNSiU7+l`e9dJQ zz3Q-4oZGj6xw)V~^Yj36X@MWVL{Fo(CGKE#-4)b6eZym6a{&#Wy2^Is zNgm3}u=J^2?*f)bKQ=@@okW=~C)z^ZZGpnphkJ?yFp$Vzy&VuVh2X7QnU4?UANfxoNs75 z=Wm1G8)_IC(Tk>XsbL4B(*x(aWndK7Z{!xx0@T3UL-FVOP+9LpmaXjqa;k{pA0-?a zQ{I}NPnlCt`OTYq8lUxnGm;#MzVDY&hT69Vcu^1rPt*@&{2)5SjCE;#;S!Sgw`)12 zd;##YJn+p^+(7Ct%z$Qk1YO;D@F#=^5hS=*-jjHUVgpY$~{jZuW3O$u)6q7X5qu zIQ4?`2BNq(+i^Ux5zMn?2cH-%K@K-h%a$~cp&Ky`ANH>;0-AI!z2EU`;L9(^rF5!j z;B+Y9Q>OVGYRqbh`;T=4>=b`xeB%$vZOARL7B*7DgAx*-FOdGKR+F8orXu5rb{}I$ z>8*L>Y3+VNhV(yHD@su6M-2&4) z!u{k=HG&Z;hXI{U(&PT{r1XN}Byf;^GheGvg&f}{=^f_S2|!J9{(tJ7ptZ?DnL?5a zyZ@@>3rbo>BX-uQ+P`{{T*tn#zWxeS_KG*qNm3c*L|O#=I^O{fvW+Xg8)*iQTYgcV zaUp$lF*1j>1`#Gxj)@uyhSEl_h zkC)7iH~gd4!F5 zoCzl^&b5(OUmZZBobR_5w$>4sTwBDUH$y0p8n}z@$6(dPEu_Hctt!>D36k@@U2nZNfCp9t3QtU_=rnR@%z(I-^_mywcLr<oNBkTqpdCBe`NCca;h5hMBOpoU#Kg-rE;cJD`)Cax{yB_sQ1)?^0`x&)?0-H5NI z$wU0$^$pOXcre6pg#q?``C1t1N`qTgeSR`t<$&6$+S{!j+il#U zq~f*U5!zZ#CL1~&EmE|sPV~g+y&*;ae-0=0-dlU|f)_qGbXmMDNeFs4P?x#sk#piJ zRo5&h4)|HZZCaM>2U#3T^xXLX62!b^^`WZDyf|j{hlYbYE8bDOyE(&01NP=0)4KF{8_>7C#A~$F zFlJx<%J~E>m{!Phvv`FY@996Neo~bh+BQDFC~Ulhq6X6$f_F>et6Ex|_z;;>eJ?tR z81UdX4LNF3o1Cy{RY0KNmju>QWonSm7r`xsM)Y>!5|CQZmU?o}ZhYx;bz%s!7#!7+ zrY$0UxT9CEf4RiW0bQC}LrVJ9q2f$dyypI2;A;A*<9?*y*DH%Jr2KjV;V)|PnGue2 zT(qBwVe}Td)2?s!Cvz`+ekF66<9r?R*1e)b`IR3w2`E0w5|MyidTEvIajW1$nu(#8 zs|41uLkf!v^WgHQCoP&iocOqI$Jr|u6j1)|BMYnjj4=GrZ8{I4EC02l5W1bR4JJA* z+H=yyp-a`TL+^RTaGb{3CW<8yn7TgE+#JUV)20q(2{(wt>z5=hbF*?|;e6WX_2)-` z?q{RMIimMbA9(H7%u7e|s_Coy6F0#Jf%(2G$28zS$M#BQHCnvl$MEK|D<5`u-StQ^ zL4x#8lsapZT;z>gX%#}w{CKNr`zLR$1oS=fCGp$UevtXt1Roz_#btMQ$34C$3t0jV z{)rwJhey<;n0(%oUa$zvqjg~kZ1ei(DSp@lrjImUYXohfVacytDK?C7JiWTEyIK;C zCWnSNcN2ab#CyGWa$wt?4}Zygn+3OTxqq_{6v0Dd$)3f`yqKXPPe_x87shB*?WBHG z2K4PeT9us?fK(uJSCo$+ta8(MF?DboF^TMjM@@OL)zVdPv4|HRd}1TaFjI^YExiQV zIN2bp$Hf)tMPY1?Q(eFF3cxdmvzqh?H`bw%8i~J>h1*Utej9xd3UUp_&DboXPhE>~ z0u6E`U&O6=V3Qs4oxQI`M??HER#oiYa}4mm4cM9_prPP{ODAe}LCc!bXk*%C zFm(J+jW{a>c1U+tO9`%1+c9$BvCEPVll@f=~?|nbE z`BVlQ9ixBxwL}aS$mJ+3RW3-UYTz=~%l<-uy=B}fHDmZ+nXMvupG)xg)Z*n%`f^XU)D(+EoVQSy_Q?*-bC~L0C z{^OmUFh2excL>EgGAk7SXZna-&lI*$XEN_<+njzndS@Mc$%$RN`g94k^RWEUf3Zw@ zJ4KI=j|f7s@hC^5ZW&l(V{0v~APlp1RDbTbT10|bRf09ke6UboNJhm{0GrY!WNQ1Y zl75|(Q|?0Y*s0&rgfW~2uR0mo0xdfHDLuAVMV1eq7txn4Vi16ly8i7N@ngDA(qC0=+2M^u=A@OKWM4i4Y-bJ# zLvTh>{!L0eU}?=wM^P|jA){6ic!zBE&jLGh6E zNxS5qk5(3j?DtZI1^k8ZxrpD7xYbl~q{xG%_>*%$z(&sY1>tM?WJa+s+?B=(0?JuW z)V4sx-Tx92zevLz|CHk=qs8&g`Ot`ko>kB-_eSG{^ape_i6fi1FVS`KUhilNj7IRJ);bya7@%q7ZbEXjAK5=#Op%B_JA^vRR>R+Qslre%+x++Av>2*wYCP#7zB4)5 zPr1K%uwe>L<8mQ7zm#~Yl`aqQEehOSOi+L)s)r@{G-BOol^Jp?q{KbiDUEmU&$>P1+J+b^ll zpg8VX$Y!Z{#|i0$P%d+|G%UMt>C^Y6V&u7ba>A7K3(99Ddu}9)!{MgO5ruq=_(h1& z@Cp|lblj#`n7ktj*BAGz+8yA4!+V4T2Rzttrq9d}B)9(0Gl;^{dNxLa6FNj0x8Iv1^Uy8}cikddxc_0%K%UAL zXlnKSs?JG={|%I<3*2f({7T36Sz43l%K7T5H`f_qBR91t^CHnNW)gja<39t%yb>PU z^jW00XW^xg1T7BfyYbriizE)7VbhS>ro#GK!lyJi1n_yiQpKpFO!z7P(tk0;FQvOE zGk8^V1zqg*$?uO~$FG&^f1D_xh6a09wNguH;0LtTb~<(oMfhafS_d|x(s$35?ibO) zf;Y4^N`AsPLd}vR@Zv1ue(ZcETWb@YKNp!et+@`IvgWAEp9Oe2TsC zmIBYr1RL+XM+5Um=S0$pFUDw1doPErGPaluGhg6Cd@q0SLMgCtN-WxmeaqfkLJVvv-v{4aXl!Y&+O9nxgGet zV1trpJ{MM=9Bs!WKQgVFmtOmu4cE+u*v=|SLksG^0mcQK@SXiDvgx9NpJTZ0@7UnS z3>si5v6U8{ydHaRm7f-l4z8Ndb`dU=&RdnM? zIw5Ee3r)x67C;?m%AVvNvY!!b($5f^MLcCsdscQCo6F*iPZ^^#mwC6^Q=9NxzU{)FF{F*qB>w1qsS8!Q&@(85@? zlxTi|Ily&T{)Ly{Br4&4OHuut65FO)y^|cC0e^A_TJLZ$K=t>(m(R*^;ev)!^pl(< z?|zXX#n5pMbsCAy>Ykeb!z&j|tHTKojXgm(LuUjm)$1h3q%c8o=Z}pd54rGP&4-;5 z#0S~ZE6O^2ei!_&&adi7GRcovda&i*5P(tl0?%Fupu^INr<_m9OJPoRNn7t9Y|!L< z(ugYIfNorB{i$aph~2XtY$G;Tz|I?WZwtzoz|GIi2{V5+;WGbw=}6~t;AQu$tbLC- zw6e&0?OsL=%Xk}OCa&@0FL{r@e`%XXd{*Ohc<~Q#`U5)dsr*D8RK4F-*Z07Zm;AxI zN$+{PdbpceG!;(Dk+BzM9Y?SKraLsB=7w3mwrBP&alj>+oFYfUeHlBrUG&hp1;sOR zd>S(&zDcjkT+AWEpgE9V<58Iq?(p~EtZ@>+lt<|g(ClG>=jNCdwO(zYyYehfbw<-@ z=iRX&E6;k6sufshc8wjr;lrQo=?I6#nI}$Srx32Aay=^YR2eqNw+_y`?!eX-6M4~@ zTi{us+&=wvKAcR?Uve{%3Px`)8hOzY4zsGriCi-lOwY=AIEV0`CI;Sy(|J~)df{fp zOc@GjtQ@c$!&VIb8jhqG@opj|dy4FdmPMXfG2{G(;UhUxgrjt$wq-(*5lgii zcyafM;(>qv{%|NT;*4jH?omGA#ohuI!{+oWpgb%!ZGlP%lb68O=SM8q@Z#LsK@Jwo z`otu7*_#6AZJRI#zTt*e$t=BXyXkQnwAb_4LxmH)4P?63MREJr;rEkg)!~kk7PgiaY)&wOPm?A{WsB;&A1MX!@@!j$kO0|Fu_UBlHVv&We-(YaN~lnR?XRsq<`DI${FvYg;~i1wi`Ls`3P6}<>{9kOQ_8!Uck1Q8apSI-YqU11wb`s zeTpUv7s82Xe-zx?CV;OQ1iZAFm51M4>P&N$ z0w2r17u(Uvi;ejYy;KBhF#3+8-jO5q;8ip?%}1j(@H6)g`%P*-e3oxbCg=Mqu;0wP z{}#|54yf z-$knXDdf4B+`xH{z6vP#dN=yd(qb=Z-b^nt*Y7mke}q4_59E9p&UzKWh0j;JXUjr1 z%(!&issAcBd?xAs=^bA_3N&`oocV7N-9D*t{flWG(&y20NxeM}LKt~mFKYARpK5m3 z@)mFI_*8~4~GS%!h>-1j#d z>YOm_R;f7^wz!}I9PZhfKG+srttDQB|ttqAP6GTc`qx`7PY9qD2()5EmM z40LA~9p+A_Wd1wP4O^dedw9QW2AK;#tTn`DflmF~PR)KAEa7`eP~!Cl9RDFwM2dx>J2*E`GRl?*#5fAwIr7YIGX#=BokGghgR!Q_-dL zeQX^ypLox3fQlJ28*fqhZx#dpm**B#R_U;JQDDLet7PkbZ z63uTpUlD>+3bOXWQ_NVxj@I?d>sjP6>oxXPobcY&%ifp@N#NMSzN1vfzM=EaGmYu@ zbOW8i6IAC&j`Nhyj*l6Qgx}ZTZqrHfpV4}Pmz{Jc!GrP4$Mmx$fOE7Xh9|5CnSM+c ztq7O}Kk5oz}A?aE`omR{W9qJp$IGOy3}dI8yI#=}WURZ2{KXgr_e#T@XL_;7j8xl!Qz^oQ)? z{aLVjxw-E*7c(~dVH{;P%nS{s#IJHxw1BUR z9`+ld$2X)X#DBk{!%@-?oI2mi!Q=Js3Yjhmz?b|!C$=ay(J)i-iRX%hFQ@m4OZ6E) zUTQVf=qM0{{}q`WH}zt`wHsIVT{}GmKvJx+Z`BO?s3o;tNHvUZd{3b9_xV8jb_F;; zJ!HgH0=E;LpRmE_6M>veFZzL=Z#28uEGus2TwLb1XMz^aT~v;a-1w=fpiwvdB>0>E z%~zQ4;{&h9*ZlFIhj$}YoDx+9aGjRnjvGZA;B0irh08<4w_D}!FKkW&1vYki!wwF7 zkRy_7KAjbY%Iei9-JpT$F-;6#h(GAp*Ye$a3MAlze6c92M_e$d^U+BaqF-t?)KZy; z_M#DE21YlQb)ebeViX#`fb#x!zU|Ur#;f^`d+4}G-m#?1JVSF4`M)(v?Hl01S0>&S z>Q60zCp6k7W|t`8J#ad&Sfw3&eI)0+O>}2&TgO#bJO(bR_sf;K(_@p&IO`4yX1vBm z!Q+|P1LV$JPJG3^4*rQ4pO84l1=~y9_bICqU*QRkOY!t0X!%_v*T!4oAG@P=>(fqp zEL7rOd6RPs>1xo$$?xI8_J$u$L>0Dx?f`YcdXP3&4`7)R!}>(@)ex4_u9-Y%64R!qOZ`F(ib8u(9V>H;mzlCVNQHb zHonn=wi~5i{a3c*01Kpl{4pxhmma=8FC=CDdj@ph5&s*R`WxIYn7*C@$^5E}3bSxk~vX=B`NQ}e>B`G!{FtHW8PfGyq4ymY`C*SLh(%yi1 zqSq-*kI0*c4V5^7*rwrBteJ!sngjs zI6qFMWKiiXz(yv8HYVd>V#D%VNAEgtkKJ?E;4=s82pRt7b-fqGRz3`S=_>>$I`9XM zm%ZS()B3Z|IvapB?|4~@X9_U@smoc@N3)v&QDLm2eq^4 z!nz4Z&7)D|xv-zs>(D$efKY-@RJei(<=m4LsrtiN+ZBvd!0*) zX9-x|TT|WbxPdfRPi?Utm;*x7rxgRQ%z(Y?6GPuhDR9-4mYh(^64DC}u;=5RM|UJ% z(Pu@>fR*f(FBAo%Krr+w$IDR)*uSC!<60)r<@-O<41dwUg2!*pRTWKuJ31X=&$Ze> z<&TW{=X|xO@`G2g#lCj%Hi`ePkp`L94{ETBwN#^DCl<{p{FagPgv4H*pMywwMAIjA zWD|T=uynRiZ3TCaO9=U-MWP0NCK*|UDHPT_edOiSO|}DddG#kQLOd#{<3&#Sff-d7;4RgyBK2zW;Fi-cG2O87?(lRFcTT#_ihU{ z2Sz`+`JokPEd2>B(OpNql#52TV+-K0mvf5V0wwI8RCHS|rNxew~d|AT&m zd2FMi4;UH+Y5AFxXin2cK#-dgzN!*^ zDAy7LRwXp3>&AzH@zms^-xmRR{>p)Fcf#`uHhS!)AvgyyqC|#0Sv2*(No~>`c*G5M;&L*Ma&sd4ifmdfV=-(pXMPD5x zE>mGB`obbM=^L65T-EKiqQ)n>S56&TC*0IH*=sc^6QIlf+uAjA2^g}%zO#~t8?q== z$tLTpp||rRX&i(<&?bN^r$U&pn`K>z(tq{fgwe~4drO4NhW2@7EpXzavVDf7((9l( zHvj7AZ^9+?zOYL}s|;l_EOIsm(qPsq&1hzgawOC;@kH+_3tqCW?0Q~J1&3SdVnSbR zptK`wntp^QlMzvyY9zD(rsOU!7|aeMmU4ReXu?5Gy*wSn$%uj2jO|945+!t}zSR2o z1o3x0vNa47p@PP)zlVPZO`zEniI+58B***t^(oJQNYKQ3QdL}l0>TsjvI{jP0PDD3 z;%VDS;!mtR3NMtQ>N8rQjhBYN)8uDfxh%85uKLN+5SbrE25`}IYzvrqlG&S2QtT{J zV*Z|NjCiltgLPYxw;=8^6`k7rI9i&&pxY}Z4*y=fuqe5fo<5Z!HmCz_!|N6q^ z+X2GqyuG^MW73TtHuT%*Q{({(X;ahIz&SKw!E)P8s0OKG81kHZ8K||rrz4rssZ~Icx;$Q>0f1ADz{YWnbD(+^*T}3U(Dc0_AqVWzWX*B)(r^Yh4 zUiz$)@)iRgt>YCOq&t9IdoMV%duYeM*>!PmGvVRp=K^ps1*tb3SK*U_Kyi6{Rec;R%I$97_JA8H!u5FeLf zz+rKxXE}-oP~p1Y(5>^_c;r^*pRjxLU^OwS)xS|1w_j$|DJrAEr4biACYrLJi}@$6-aop z4rAAwemBryc9Ug&AqgocF8{z{LWK`ra|6fR4h%1wx1A9)bJ>%Xri9F&Md*lYUk$Jt7 znDwC+lt0qP80oeJ(#;r~9m95G&xU?ClV7CANOup@Q zal%fEKNJbduxqvcz4TpvJj1g8Ox>V7WMuDL(R3{VTmqgQ$6|Lt+-%fw!h!)(>1woS^`(c)SaX!-2V$q8e#9&X|V5*9G&+9@lQ0K z6Uvd3gfG>~SxubKZ>jI#}uQW=$Ybf3#8K%h`J24jcsrcI6UMWf}6Q2*VImz=T z06WnPv%&bIYFsk!d7x6WjEU6xI>{AE9`N}``k=QZir*9e`)<{szv0rfaP{;ruHqQN zZ^rtPVdq;=+H!x@^A#o>C42LwrPU773*61CMaKhMR$py=$)-!HQX6gSz1T9N-;-6sue*aqdpKe=-Gg;Bl#E z*)J6q_;)^u-ki{9PRE79yUV=LPHBnu814t-t5?VSeHftP=E&#AtCZN(bSBokjTz>O zXEz9o5}I7NCaS#apj-9;VOc(IgEzQl*m z^&p1zq|Yw7Ipq3zxs!VjCA2CS+box1h57$dkBdETMym0G^vdLJHF#V$;;cu7so(eJ zcgFlfbDrw!->ih8RIEJL^v!ngEJO5p_YgBqT)AZJ+$;}wu{?;7He$kNx$DgnvJa3= zTyU+H3GuhR`mN$YJ6dU!@vf+l{9@JEk+AFnK8gbzHPs!nL| z!@g)!F`p<3sMZ~D6`wu?-JLq)EmAZwbx^Tuz&I}qd}Ji^<7p3gW04p4D31qTsG=Nh5<+0;7EU>bw z=JwB?eiZAFSE1H91^gWk7CeY4Lpzggonr?mVeF$va=O;DDE^cieE z(IFpug~n-co${==)?w0f`}*=T10!x|=s`W$+mwUKx8}Z=rp^MH(}(ZtEKx&cnFhl~ za~W(|5AVC!GD7-)A|AE=z347QmiW9wBf>R#mS;|gLt}ZR>b|d&=&qj0VRf%zFniPp-mdXj&<``SJBI>{w!mY(piB{_-DEWw)k zTzFiZqL9^^?0s|p$T(8Ap;d*MFMivkk4P}+hq+={cG%D!}kccMn#g| z?id3UhbN9b@LK^cJ@Xh5la8**5kZcG-TZt<9mhpq*KenTj)2@W={9>_v-a1(p;sU{w)m@{{4O* z!x2{KH(+^XeVPM~^a|d)aGwhYmG5U^B)%#3yg+%qFb1eU`Bt^NZyV^_NgbLaIm#U4 znE8DaqA=;$XX}*8w_yE{dPCyl8N|{)CpPeO1oSNk+W7vVhg4>({Kb2}A>I@zul{OY zT&g^H+&z!v37^wkIsb+9dc1fWv~;Z-W$yDC_I=NTfA6<&bL0?)^)pV+e-HD)@Lyvx zd(E1`e{4ZR{Ga5Y;cokL@z;dm?_rxVy?>RcbNpN@$8BMJb@;B@LOdlr=Xp7VUv~lx zNgQ{|D|>?8H>F0_QEmeG_-D-rDr!twW#ZB6F$}!#@ZOW(L-OUKOz%3cGr_2F>h?_pq6lcruI$C z)6w8Qi3I*}dKTzQsqS_B!7M1fvgzsVNDYr~y-$8Z{A}Y-j-`=YHR6gZk*)eBiL-U} z1hu#4(a)45DerIz_#=_5T+s}cQa8;qFAy;kT}87quhbqIE~6NKVdJi_3ZBsT0)nD!#N z5yg%6M&}D8Po}W6J72s3c-?k5`_*m;eGFf$YtP|>6yJi=oONb^x6JDu3M9u-GdChw zvor~~b!n_PO}U^hOTPKd40gQhUrD=^K??|S-|aru!wcISTCP3x;)Y?5>>U3xWFu-@ z`8}0ZODKD9yaN>cYz7omVY(>83Zm<=AhbOvbo@a2tL&EKG`8<^P z`vZf34Npd#W4Luqr)drJt}7&ojgo!T(>l9Xbba8osA*MPJL!i_Y&*U@!HId|WxU)b zX`#h|h>Hg~Ho%SC1A4-U4aYY9OG`OM0n=M=Ge0AFT;&MrqqDo05o^#XT|SbdHvkU4 zv&%eqMB@??*f)(F9m2<6mvw`$T3Y{AUTg(=?8%;&PW}KMKkif?DkSH7Bfl<9${rB> z@W5?{dJa5r=a!FX-T>k{duOcdz7VV*GrIxI7Ql@&ZO+5-jIe^@hm6n%3Osz4qU267 z@pI3g3{XF@0Ip^S7dFqSKrfdaS&sfRI7vZQPW^l@Qn>Q=DfNp9#9DSEq4MG;`X_y) zdjGzE;9pwvV13^@@R%&D_;!;Q%T(;-@GNJ5;&MOA%?M9F{e=x? z?H=tec{zk`eU-W$m_&=aB{hu~h(AU|`2bU|j0(KGc~ov`7a!dFZ%Ad7lMW*HzFSw# z2+!JbJNH}y752WLb~Bdv(&K+`ygK@D1SG+|oo_lx4_$7<_spzfp!&_Bs-c4t>x6|1 zi7B-q%U#rP<@5*;-Y~Kh(4d9ev`5yCt_=Y<#}4-+q_^C8s+0G{L%N?IPo(yMJb%uI zUovU22F<5u-UgKLqvOY1tDmiC=IUiP!S!OqQFvCgx?LJxeDtPNCXx}SnTD=>yCMly zR9=67rcD7a;BM9T33F)IWA%u^`94%nGwEi`Lkpvo#o15#kD~IB_n+7Wo}B{F_{hR`94GPfKu8)d8;J%_-owm&(bW~quSYAr6voVZbLTTm$X>#nC-PA`<=n4 zxAhi(^Z<%Zu2Gu3NRQPNm8cYE{s8Y8%i}u<&*bF!G=aCR^jLfGmXUttcSN~oxmuFw z{YDjUy4P7+ktA9U4l8R%ZoCi0+(g*mHNLH~aQ|tL7x!`g9qFYI?aTNj#yS8lD_{RS z{HhNaUQ|^)cz_bCpAeX+r5r%BA0&1<7I5SBolBk<3rO!&p43k7$D=^js<-+~KpkSZ z4R#ZQ9z1*gX6fQA(Lv4aJ}wzc;XY{tx9N;+bRan6LxTM}a?Ctc^tX5zaXx8FXFafq z0!E7}E~pP6-k6w2aY7TQFtb*WDurBml+2(4CQld%%Gq4?-%8xXF#y)+o^#K8t8EAu9EXE z9?X+vB#`XIjeqPoy8S4A9R0l{ctm!P2Jcd_|6C-6koGh5pVA{9JZyg3`$uTz-ETw)fsMdxkO!Dd$iqWfX}-Xdo#>QQ1+}mxh$GA`+?WGS0DADO>j5dvCwz z_YZg(ALlvG`+eWnRcu4}YIFMfWjdI!!J*7=)a(@Szb`s6ngz2c_N|=|1u77qFg;ojwwCaI{mKf0WBZjh|H5~U{ZoPYvw$`IVP#Xox~Xt>&as-w8nyg zg~~+B5FO0o{p(eFhYH$9tz0s&WrN2gUc(kQgszo8a7q*FLV*nK-vy+0md4swosDo6NimQS@2h2T6fH?t7r>#~1hq&P0Sq_> zvw4_TkUE9=pzg#Z@~w!hD8w^BIZ=jt_JR~tde=blPlY(2Z>9dCytoMFUB`!&8S=m% z{~L@SNVb64iN8*6yK6``;b_U-tZX!N_vpK#$Z23uH(EVZO62Isw0y4-d!TNYL)}TG zi|Es|D#^R6#C_E)VU|NsV_U9-5fM*{B zvLD55gYYT(%j16sz|rFhXRC)s&>T1YYue2obb#jT_;rU7z`OI7vfHKv+*0}4>GgUa zumpT%wVP&zM|I_1-%Qv>6T>&DcJ5W9UCT_z**n~@H?qg!a|;*NQuUXWy0?i!X74}bA6)@wtoR0+47X9L^lxWn2@FD_4~GjGZGk&{wqtQTWU!<;W#TQz0HP6N zw|+4akABf|>XDB(p>z>z*;t~-UoLLvE~ohuWqXNkOUh3I@$wT!MX{97{H#*1RtzO> zJ(K$@pO`;g=GeOIFf@tkmM>KtKP?G4Pi*_o9x4Z}b1c)`ga_I7arxw_;WAL{aOIXg z?<`_WTa7p-LGY@Nb>6W5y#l$GlqouE~?v`VlWckb#1jEK-i|ETE zgLmi0cwU&#AmLYDH5XKw;DslaqXK?o;2+tk&z23#s5m25-X(7oMQVD-ZxNj8J9zl? zwfm%4Xu`m3iFyQ3UOYwmxr-GCqh+U$Pey?LS0w|dYI4{dKKYExm&}ospWXzpJ@$IQs9KTN> z!N1{EuN!vJJ&&3f!>_&Ge7XQU&ueT??+=5|UVnbxFrk`81)_Z3Tp6t2bYMFbd=|v>ueu{zA+= zm0a0PQ)pFVwYG`iPxOu%-pPJUjhp@3L_>PX@jlluUp~PJp4yT!rLrS>ST71n89R1> zir5LN)%j&0*&WNSPTK`0jEB(;E@2IBj~qnq!4&T6 zT-5#PU5y=J`>ny_JOdTp>^jc%GL!`B|L65=CBFx->&L{d|NVpHo1H@$Bw4Vz+MB08 zoJY`MnUlRyL@rfM@4T1i_AlhFGIvnBpU5x8WhJ=;vO?3$+tVgH11Nj;+S9LvnaJg)O|Db{CL75Ht5S+(YbYWVh6kHh~0t zz8(3GZFJvvs=#r&5kcOB^4qi|*iZ7*IKPxM9Q*3d%17)0+f|-ay!)~MD$ZTgR|#AJ z0~O?FCS8dhW&iIK-i3Tr()-11|2Z)Sam_jKL@ys~PG364@VgWol&kCwjC)1&k>5T3 zTtf6hw#2hje3sFsaG(tL@)V$M+PCvd_>SaC9Gi@zD}cM1-BTUQAtZ7$++&mQg?Y5p zju#To3zmrme|dsSK)$#SN881l9BJAZ$k8P69)#-wm76N%Or)Q zMk^b)df2d=7FhsmY(Khn?(f65#NJ<4WBc%Z72D%HTd$yN(vvhd$-b z)8TECa{q+sF2tST|F2YR194DZav1j|ho0r7B^BKJsP)jSCY3J@T*5O{a~%ulx1V4B zIML54?%>zew5msL^)pHZ#M%5W~p4k%jqSLjRl z$;&-9dAK;p;jFyvJAI|!$gh&K@Q*M9+)~teNaD}~cuUwghHZBEt&PKvXaZhP>%eopM@R40IC2IzWUGa&syYUkQW&V8H6Fr4ALzpZ-)-b}Y(pRSP zYD51RzXvNLH+^1Xs-;XzoS$jE; znnga5C-hU{OfN0-%6EQX$VGI&fl2~qHzsMmez*l*X|7H^^j|?yr#5(3XGYPLKT>B6 zn(Bbxb$MGlzGc82ok*KA%76=J=0BUN_5gvCP5z&xCP1XbrM-2U0btHTT|%Qffn16o z_7yf!L17I^y}C2&$n#R9Su4X1qPbeN@as1tuD_w&b}VKSMW|g$-oL&G!tV-GRcOv4 z@aW)&kq%mX{(uSjQK|*x6wXgdnnsB^cusE$d5~e2s;km zCBxaHPPAC z<=to}H(`2uw-@Ln#hmkT7lY;BuYbzQqlU(OM%>zy8;GiQb^Lo+6POeJnaGu~fJC3r zcHKQm3jf>uS9xWA81=cHjpm?YhBIEHb8Q4~lv^hvnj@ePxF2a>HZi2a1B}Y_)Tyl~ z>j0O=9N`7FpQE>J4yA^Z?RQd`6~<9To_!T;Y(>RaGtZ=G^TLSUy{n5^8(`RjcW0H@ z3-^qR6j>9!`y|S>~Z^ZsaBDHhNkCz^cE~l}W zm`woo&h4nUadOCiH2yUw9}SMyNvL7*p}9&f2h?-Z^Ag-@=_Xm1Xq zpuzr>Z-ifkjWd6=g%N>+c_*`STi9^nLB@qEBZS||RKsR8gYZ8~v#2QOH6k(Pr*E`e zTYxdj_}z476&zDvZd;-6L>iHCe6#mi;Dbdni^vN^pImvnI#`Jc-a7R8>5A+ExDa~>qs3`t zZ5nWDQ)(3$%||*sJ3NE#@L%sxkte*X%3-F*1$bQGt7jbl2X3`J zby94sK|eM}H?5u#y@Jbjeia{fz>+bKx>e2sqHE;*HCeTb*g7b#l$~w@ww4-kEU*RD z`e!6ON!>(&qwyv;S~%c4SA)mVmzdz=rRp?;qLaur{EbX(>?WeypS)vpUk=?Tk;RQ{c0%_sNJ?e?f1@;^BqM(og(1T3SKl6y;H_&_XOtPq@oGU)aIDz|xECXS>eCkzJheY&zd^YKD4U<3PIVar z^Y`|SRVOwgz;kr{v1dCv|Cc27I+3URwt36{<%wOe7IrNz(VWPc4RNzJ{9OVZJ#JgF z4$RQVKtIg+6u~FB-XbjC#|PtL(er!VO0b5nAo8e61Ik`>{jZCU$US;`#x87$;3r!w znS=hqkej}<^?eRCY`Yuv<$4Dhz8=xKA5FYR{7TYzNIoM3AAUPIs4a5{`*p4ThlZsEGh~WGUT%x(00myCxmmuR=D!W1`#7@mJ7>XdW+oZ0U!&-zC&NkygCu_E zYRvxgEy0;BIWH%#B~vdU8d806$VNhPh})kH zHXS^D3(g5blz(H~?ASIK)Rl7R6ySukTZgDm_z?N8IM;T@|XQ(dn<%j)gWkzx`V({{%S7uj;d1;-5jOUjoE_}0E<=D48Cb-j3V)(C+$QL=@ z%Q$gW0=niVjF{|-!TUe+t^L&r)|iVDqn_m|Xuq#>hpuQFSlqUY8FG`xm&8;@Kgsan z{D>2QNxVY%5NW}U-A!sZ^hSq1bbS#hvJ_RizZQhjj%~d4uZ3VY_nQxP1q$$_N<+-x z2=Vuq=YH&$#D)o$K36Bls_dDF! z`jC`V=adM}JK%n?@h&aY`Mpkica`uOQ-x5gpIii}W&8NGU1EP>rFNP11{Izg{o7DX z+)F6yuD0@rEP&KXqd^mIVSKUtl&VQE(Vts0w=$#UfG5BHq4Cfpatp>$hV>(}K(hUj z>NRdwc**0jlj+lSbV#?Ye_4kUy7=oi_UG(@D}9GQ3;sv&*Xa~Q9}n6HX)FF$N$_5qdO zZ+999!lz-k+pfS-G^+PH(=SF8Qs|xcdr!=lx(XxIevxwFMLJ1iQWFjM@!66{W7+`H zi&Gw~XJWuUlKtv@CIruA|M~I(1y&rv$;+^OLkz|`?%prn<%5^m=^uTqro{SrnM=u) zE#QbMRYq4q7T{8M*%H4ciwF5e_ImaRE>+N#)bs!Ma@m|xIb}xpkB4n3Y00^vFom0} z!Y1JXKIgp`2Snhy%Bpo^OcRjosjj=J$cE3HyiFBkNrkVbeba8|BJNvEH)}7~&jEcv z7vM6(3V+aA78Fg2!!$p~q9;uRZ>_{=N+XdTN6H>NOJR@=w7hLMP7r=<0V%gP7uM-v zU2{SBdCf-DL^nyNc~%ajsLeH82Uav5met&-|d<;G-Mb#o1iOQ82!H58;2 zfL!(6zRaEr$nsD1olkctpn;;WegFCph>a+JW;0TOnx~zWs0Y-c{|ODHvvu3x;psau z+cYHbcxT8uSJW*-Ajj$2iK634Ed{SBDZ62 z`4PiU)^uwbv+s<*h4l+6%J$D2y ze;8Z+Kt>5m-?+)JE9{`3_K_AWs-jrRpQgO~8XL^_^lfSy-9}yK>e6nivf%a-*7pig zQaIhkUfR~a9koV8KDGb8iq77s3SkZsg=`N^UvSW~!N~F_FwB_n{?08poaiBg*Kcdj z9Q?Ts1gXw>=lk2C?+Yy{^aOvRtHH?KityA{7S_L&GGWIG??0AH+O42Jb5zbB2|mf74y&abEh8qoQ!=~PO^4goIN1)^u|qL0{y>(?yJ({Q zz0aK;7QAq|-{+j<2#8W-KiN7!h8gTHBp1w3W9wS?*nVF&IA_yc`{5`%ykZ-EfXkK@ zHoR0QPJdU*w~J{aXIZ znio0oTKMT>xzC8cOq($u&AnB`lo>4;QM`l}K5E~3b!8oyDUqvMipfD;XJ*RNt&7O) z{P40%9v{^2`Y7;;R}!A3xb{=ij{#Gp9Qqegv4PD0D}Hn36f6F$ZgT&SC=>1(R0(U2 ze8hw!SA zCQ+P)$c<>!CmiB|-u1AXu;+pi#rD_PE|EHnzRSb+}0V(lc0hJDJsRJJnPY9 zSzdhf#eH<6L5nTPe;e&biG~#yGGQh9Wg(4kEU<^OZTS(=zuS9k1=UNq@M(pUwplx@ z*ifjpwfZ0jj*WkNbr4YEv$;_u0b*qMeNVbX;gwCq$ZuCr(cBA!_CIg`Sf+-FsPb;# zSvDLjBaoj?y^3DbR0ReQ{p^#8cU-KAzK@81y;vgaGDw-rzjeQ22P|x!9G+g;L@&M+ zx*o1R3X>T=3BG0GfL9#CoOM-~k>ZN%3pc{s@o!fnZ_tny<|ZemEm?1Y%8Q5m>L2xh zvbUD{4+xL0r5gq7wW~a^4xc$xr?(7VbA_I4(j1Qka6BO9g z;nQ8J$IO^4&co`^Nph%?6ty1tg8{0gb2tW%6aB0bht?v3D_1rZ`stV?Ew&}Yq#Wx5 zsCT$rbp9SSp1K<~e`-Ki9$gKYHlpz*4frfv5kp4;+DF)Nu=!Yo-O3)gcVCj3mEZ-+ z=4jR$hBTq^50986k5j|x6YN>sqI+PrUiWy~>lM)66tZkdO$sGW-r30^@*$By?+)|4 zV#mT>>V?63zd&;-i*qEw(OHt-vgg=5fDN34-2ymA!4fZ}{lDlX^mCra%8;7y7-T(Q zy1mnalFBw~#!AR=zwtZLt@BLyP)<3&IHVyw8y32ETzK#(St;#?G2zcU=;4vVMTNhg zPNDbsI7GZ>$UKZD_JN1w8j8fXNnz*NykPin8}$p;-c>k7hIOsU+jO{DG1c+?trjCu z%y0a}yt7;ewk)ezn62=@nw$TgDvXn3YhR8Bm0i1_T|Lnc?-6?hmSMBt7%>QE-@>~PZIb#$dHnUynug4o+@OZYsXz@OIy=U*L|K=FE_ z1p{hLKkHsU!{)eG|%L@zZNm*ypW}0kkB;+%jiX*Vuc}xxbVtfAx3K%}zs-d;h9I#Q8-D zOV>3dKE%-BU9|-Ic#kKW=xu;TG~J8;%S{Y#gPbK> zV`n1w>Q+arqiq;*61KXN^m!Egj?!p-PeF>!dW^1SvK)q8+s7Lom9L_f$xVH$mOfB6 zo>$f%#*2TbvTg1?m;@}k69ToG5uhjKb{y(wgdtm!456eu;DW&SQ0@pS*uPdJ78Ff9 zFNJ=H=Iqa)^nul%vMFn*QK*Egpm_{^kwuh4JoNZK=J@>ji&WSGd~IJNyxZFQ!L8RJ zIrdDL`={>Cf=idw1&o+Up|_;kz*$y=tlnQ|$|BxBl^F|9U#4sUYx!BtW0zRqaDQR= zzcfn7`?2~KGMr)ev9xw7C+MaBs~ghn9pcbE%RePJJw_Ur*>oqjx%vvSSpFnoOQfz%-u93^1F zE%TBD3zV&ybk5V_8YXK$ZYx%t-{wsjYPbV<^TaL9v}kbJ=w#}=;{-Uwvwhk$Zi&dN zcsu`=B*%8~&Hrh+tRTtH(C0yhD7=ta0=|6MLZ|wMmN{;cVt4m9rb)Ngfv8fRmZm%f zzPDxITB<{W*RzdC8N|4t-4rKd3pEFhCy6LU_DrzhhV)pO^C((6e!usXv3I$YyGX9RfegP72mGa>!ky)&HXPwg=(pHFf{oh{ zkj^p<$~-{>g&rMg9+v1uWyfVk9DZz}{_VUIBgJws_*j19PfGNul%9Vuw7}VkcCrX^ZCKQ)M$_ANPMmhEdDdBxe z&*xG^?nM2kCz4=ZMoQ9FWpP4#=))b_lhI;ikhxk@#*vf;Gu=i{y11I~yZxz~Sv31t17>RT9V2G=8hv`V|n^d+Vr-e(EEgHGA1pK3Zr zx+x*TwZQ}6`pJ8@bA&UQr`aaK^c?Tya@JlzH}c)0aJU!P#I|-A1`nW2$RM(Sw$Y~@ z5yZIjbSN@!3w$|sWskw95825|w|A9qgL8_<5|TTJJ-|LqSav@Tp8p&4n(8^>x6Gh= zl`~8Z{V$Du&?ERlLC2q{hFu5+q>V*3#^P+af7e5&otS$+x3N8G;mrY=tZLkK9i*XU zsy&q(Qv)cJ;TEX(pv8kN180lv2rsAFz)cFLL1cY4R6{Nj492L>KKgF~$=_Bn`K-GDiZU6zetaZ{W0X6bn#3OJ&$1A!n+XH- zWuf_|TS5=T>uLQYNhtA)gU5%ss7P^B>1>LNBq{crwxc%SP5~Ads;62H?}3rL0~_J0 z-_XlyoBo??R4{LoHeXkR6l0FzsGn!(aKIe%gL=Xi$CX&k9E^96A4@3wFH1)|1pQnYP;K zXB!24f9-AQ%SZd*@xUfu_@5czV;P?PmB0hbmtN$S{ zrE`Zf{2NfU`gv87n{6nG)%R@G$|6#rnftQ2NrTC174APLvN*pE42L^bi4g4kbd3@9G-t6m1*-#2==i&g*~{eTwOn}!5T(zbgr;hl2o zsxGu9!8?yeKFxKW_^!wStqf# zGD#AUCVG^mE7M&hJ(N((S(+~HD#3TDBiL8WMp9Cr3K`hxSBF7Hv~*y`I=tf z+CogBp7TKu8DPAFkm?VyHPClQo8gi?9oEU?TFwSr;IN^aMS|BhVDsRh_lnv;rMC($ zd7qsE4-+z2^e;C8S9#ORaf%dh{z6gs+Vv4oe9lmLPJaZ1K5*C$vZ2JUFOlwkdAf=C zbtY<$iA*77aVy2B+eM&K>G3%O;`>O`|B@~;M22OL>`z5HE+dDMYU7dS4lvBTDF5`s z4l1WT|wVkdh-W}y`~Jw38jrZ2Dnc1(ej@BE}(uFwEfm_8YOnq{*F1*f&Tk>e(}nS zY2=kOA@%JtEuIcSG2GYYQR@fpJ)!(%L}M`^r4Y!7x#LRbTDbaZNPF=!S` zt64|R)E_r*><*#1&jame4e6ntbG}IAgAFj}w6ZuraJ&??Sv5a|v?4K^&ds!(DiAr) zA8d&>(HXbAp~ZcK9113s*T-{!$G=nOjtZnA;WpFY4-eN-dR>&yW9dRLVIVqVF~5Xd zJWT2CsjULx>K`GgB%9!A-#M;{pL-xtH*6uEZU8BHsA%fpJ@8FlTjLbjG+H>ZZ={w( zgDK87LP}zfs;GGlhIg)mLqgiZVVuN0q|WbfMgt92GPf=l+rlVxkl{i`JS)DW&k^3i z7l+ouXUXF4)qwBGCErLrIN?DVyB8BwBWQ<>B{3_X6&7T?AFP4uF(%s&mdj{ipA*RFWfi&4U?q;8KJW;B?mYD(?8&Mrv0CBOIDiWUb* zQ$2rY)rYDt`)stGt3^)YTJ{Cmd^qRNlJu?dpCFX;8C^i=5cv9>^<}wOIEwrAcSgBt z9;n|*i_p?zg|^J|67Fnd*irgAx%uc4I4gJZdUl5(tR~Zb7cRaA92{G1T7NAf-3!*2 z7miV5$)|?DT0ZZi-SkQ>wj^?>{^Xb27V|E0vXb6^>AMahyXo@97e0Yc{W*g?Wyxsy ziBsm%IbvR+Y1JY2k`A_-j?$|+t26}i`eR+=4m5&K>QdgP zuFrt|an2tM1sh0?&S!^)tPEAuTZc%D3BYf)Cg*K77E$-$PU?gDE)?N?{mRzc z&Cf{o@xi$h*>gzmZ4JjhF9oFBN;h0t zBE!7DqM07;EP?y?cE_KGk-#f4530hXs4xrl&VdSPQoPVNR_SXo1PVE2saaLm(Q^`V z)w#TNpcr+^x!_=F!EUp`$~y-UQ}(W{JCgk?+Ti= z`1*X{#3-07=*%C?Uj=h^2j(7}Swj(EoZhdY4P7`#U)STa4Xh>PbSsH{nB7U;Ti%CO z03FwkwGTDnNeoX9s#~7`{<$VPMf_Zt-0VClpPW3@N_=dS$18&WbZyOb4V-}SDg-@? zo$yU0tX$5tS^x$O_C}jTUoPnDELo&3abNhs?sjXM@XVhRQq8(53VB=RtGp8!AxFxZ z?aNvPm}R>{@l%8yntUnCvF&AnRgA3?R>b_@C7&hbnGh$Y?=!#FElCb%>U5H?I4pw3 zFg}Sx#JT98JFB#PQw-X_Xp!e9^78q45#`tGxS~c`a9fBf7)=ufbcOzS;B9br|c5BSwifa_$g}aQwBjzqPNdG zGQwA8ZRvW{6T;HO{#|4&fVsmgFJ4gPhXO}04(apv0i1kABx+3rW?r+pdVEz8CI$>8 z`*X0s?7l9?BV^1lUa#@Fa6A__^Y1L9s~3kkZ*QqR`nQSfTs&*j8W?b3F6ONv`j?H< zt|~R(8R0J*evg7Fb}Uqq^5Si(2n@`-bH%=A9jsmO=G-9is0O2Ur5OZwJtm`SYmbQm zqUR&|Zpm{%rM@TevJ&(VCwgbL9!lR47cY*m$7h$J#7C5J9#^X-xTWqd-q5=zE$e*C~_+iBXeEW~3 zg95?9OXBDVv?RDS;*av6Fv%eK#TRssB1WrJFInGeseL^)m-Bf){4 z72(Q8OgJQ{fWfDi9$r*1(|>Vk5wP$om@eGLbgf9c>=@jPewoW!M|h*ABI)THyTPhLPOn$~I-*J{ zmzvaG2O_nv_pR=VL4%|t+-hx`$a$K_eC{DDtX5<*lTj7HNzRWMDc=u(y0x%RQ!5Pc zovEGl1;Q_SFuAi%cAOH<3#srDt?SbN_6qx4F#kAYp zqA-v9{E0g*d^j=he*MF3DjYUqmaa%ki64BYyXZhdN2UwjZ5S`0k0qB!CP)RK_s@;mMq4JR`JeHLgc%l? z1sh#Lip21Qm2(!pMgmhZ`1&IwjS`PlsJ11b+55n+Wtbg0b$wt- zBv*iLkr5Lje)O1jKw&sUcOPU6z5CFbMC7!yhiu|QC7^ghcD~CrIgGOk?J9HRgw~C3 zTexm<2Q>IHikHQ(Mskf$;awhVqm#TPqeBV%Wvk7NgO^c4#^^5732I0aFwEh!xr``a z%-AS31Aa(aDRfqz1@D{r{6=&ez;RSYKbu+@tAA(xH(t1hGRAK-_)apx3XVUKr=v*; zzP?!P^7=Hee<9{kpV)^Or#J#?>lkovsk(;DmWLP!!P=K|+uO-h(C6_5Mb8zY@1?J^#$_>qkg0MA(*r74H6%lm zqM;7Ct@=v)nTYq&r>_+IM61a*?_q!zmf(d_jQP`5p<1L_WvOy`g zGK88FQp-*81X982G@dlX&v$O;V3Xe@P*c)teiz6C%NNE!#bvYNaZ#_B!-W6FexuGp z=sOLJ2r_lxhQyrvL*m^re?}N<^D8R-8Xc~i^BZ`c!Gn7RjMT5*tOI6qMh=S3;xMb} zi~wVlMzv0cn`zmNF+ktJB@dXmB2n>@Z-pM+qr(eo3F`G_U7b+!esoQ)AuTs+6_E4K z{{9och$5I@ew8V01)uM_^e{|ypbNhvfbh{Ku*e{9Z!g9ME5EUrYle_u{wmuTjYfhy z-@E*Wf|3dQ=UuO!lUN6z|E8KFpvGQ53?4p7A45Brgy}#2IgTgPrbY%n%0bZ#wZ?u{VK{9@RhZPc3|xOY zRmQwy!6_53xg*!eag?zponJ2{M&E`cC(g-0iybZ7*TGXLhTT{=kNAA_1(|vu-r^*@ z%#0Hyv4cSUzyNbuAOlQFQ+_Q(@YZg>tUSv7Zwd{l#;4nC&7qZ-W8n^B^l)oob1qtd z1aItZY{xVYBHh%dQ^hA)pzu?HByEdzP?6Zi;03vG%tS+vxhyHnvCGTwU15bJ1zJ>f zYkOc()^3hVehvM2dx%;gjs%}#KIOq|*NL9ov5b9}zXGaG$UE)eRTPkvP?RM$0{+fA z?XTnsz_@}hbK_Bxkh>&pUCnDAppUnP<_fpKw{yH1Du@xJcR7z7YpxGD8Qo+6F%J> z;l+&YBcG;UlEbh}rk{zj-%!q#m9sXVnn7Aus3V*94(N_0-*P2#8^8Nwla7?`0`3@< zx7W^-;%I6LEzLtspoU68@7>d%z&`Hq(Cr#QD3bP_AvG%($@JYiUpL7KIb~Hdw=YxU zSamMes!=wa6cuEd( z;(>4pzcVw$yoH_Tp8h&FJf5UbH;7lj&BT)qyF`zuo$KvZ!`U6sp!EI|6zu_q8~q!e zx0ew6WS(NK2RkhDRe5>%TOY`#&+|zn?%Ndy6Det}$>3$}=QQs1M)9!il%k~q^}eI>IC8E zIk)`ix+&2&v;N%n;=~eq&33=!{U2K5eb#A_$~_r~DjzpbvnKX?>e*sLg#Re9(W?5- z>=3x^^1WAaV*{AI36vKX|OX83tbh?b@x~sqs@XtChQ*643WYQR=_j1V@47 zy~sl%w||_wB17Q&5IWfqM$yCE1bh$XbTAoBgWb588A^U)pF!b!@bI}A6vh$g*wIYP zIbH2f%Fa{(+R3w~h3`4w_a}aRayNzX`Gp*Bw`dL+(2;gnt%L)=3sMu6rC1~K-0WUK zrc)^W@K=qE#bvPPFmy~_jTJw1e^s&X6^G0QALbd((ZKH2fa7+Z>quC_{OG+8y=YC0}RX@KhK;(hI^9qRV*K~LLBqa!0@XQWM*aQVkY)17mLpGtCsGgjRz0P_K2K- z>f6-JE#p1(({RG-YuFxm#P&C>sDuhvK5OZ&dC&+tO2?0F^8N#NPpQ`%QVxRA+~%tK z+a%ap_o;uzA|=+iCDWDqi2+(1e@1fg-5P55*NDA9@C@0GMf-+bSt0nGYXV-jg!lA; ziwkYfEHZv|U~N>i8I`H^Q$M@Jfp3e3KG&H24)ipJ;{5bh5RbH8ONJXQ&Njwr_ry6c z(@W{tvmZs^V$uCGGI2~W)I4a$?ED+l{Z?oD{0A&bRJ>4fIL}Hcgar?1R74NFrG)=30fSovZW`q=JKpoB!28HKTac=R)4u?>q(skB)Tc=xu`rmf?9`(^eq$ zwR-ud+&rlB^?Y)(o*n-qd!obYB@TmhoL2mQv*AQDkDE50v{2PsG$Qm9IebF4tN|w& zpx#z+-tF#1G>ctr#rHV~j@0P2c@|8Pq{EPk;`$Py8J|G{o za?*7h#j)pHmHxF3Vmd6wrx$8u;Wejniu0u#Na=RFnL-XN zoTZPwyz`z6CR@=S`Rh#!Gim5lWPek@xid4$KT3!>P83+Te8PfDZi>>?QxW+VuX;Z9 z=RH7>U$L0<)CO`nJ>gcKO#Hvi&hZKTq~$GmDX4I}!mnDbCTg?G&lC)m9dJh6?It_li2eZiN zh06EXcfY_NN>ciLh7}Z3LK3`D&xT#&bd|EW=YisEiTquuMKpT0sv^p78_aW3O^t|9 z!iTG2)p?f}(M=m;hKQX3pmoKz^kjlCCizmyL>tEiGq-&zFA@8rQ%i-0KCkHU<+B#1 zG$*NGb|BT{w&MGkdbf~wmW1T9nmph;+mm3fvqmc7+BHmDcL^CMk^ z$di27wt8hm0r9b(sP;Q^=m_8Bxe1zO#IIhG_b+7%9T>m+*7=VV?6ZCtbCZh>?<5<0 zypUZ%zHj%|j?fT3ECvgYoI_iP`RBD)E}~W7dE9ZY^n>HbT~0>l@CzWN?Z#?27bjO3bq4IZPEv zfxX(wIu`!!ASMZ`$f*PSsQj8uy3vsr$oBB#k~W70z%xkQm~yZV&1DUVcDZ~5>cdVp zy22|6jT}y@6kvobrmJ3x1c$6(J;N3k)T3hTo32%{h61ZHyA6D5P)%}TpnK&$IOKNG z=>8G~epH=utrJY3y9Y0iEx+DFzeykE`N?{UCIm>1ZJ_KapIDyWV!W3r+a=M0o2m;pku&cR}GnkT~TX z?deAiOHQP(c!W%#=$~4J`Hs;`!pGT^Syya zCzb+hw+TMe=O@Q!gnrTaHO_-K_EnOO&z4YB?y88;^Bur)KOmOm{5tsL)bj(0||G5R|e&oC_LFE5Z zFW&%*#N0ZUQjVOP;A?MI(s*rNZ3h3HN@zUkv;|z```>#E5I*>0`NFrFhftbZqTD-& zF0?w_9Z?v)44%rE-C4gwgDVaO&uIK74&(IyN6~rsbN#hp+}?YS>`g|ZQt2G2C@tfs zC@C6NDP^ldV*XbIyHV*ZcjVD(`DIuzB|R z9d$B2zCK^R|EVMkET&`Qaj<8?_jgHYsBkmEM0HcM$LB{7+goP^c$xwq6Wu~yHLOa)<q$AL0MRW;fb}wQqtL1OJnmUfUqO+N-sy zsv4EPH|eQ7y#ij=XvruPFu~LJ#$6&5IPjlck`}CY7r?~}r#^&UoFLCzb-8C7wD`jC z)AjS3sX)rwUSWiW8M3@s82(gWi7Ys48kiM@;T6M&@9d)prP+qxK*nSZgbyX~XAqyp zu|GvLRq3Qh9esDCjHM5h@7ORA8=M8Ks5)T7pB78pTY41RHHmWQl@zm#m~rhZI(^~D zPINr3a#QFg1(vW*#;$e~;G3oQ`00ZyK*=`DB!zlim>rVsX z9gsEj#`k~B0?PJ&29v{7u;0%quur!a9nj?8X{5*o%e*uFx>*UIj#K^U0TO|@xH4Y_x(CI(3Ns`7bkA=y~|>Lb9Lu1aOOW1e2{paSx@(*u+=Oe zDus!(q;-C{L8Y83I4}qf&ni>Uwp1b>%jWDunQiDRws=uXcoeT_^d`2QxbUp(@Bg~) z(BkW5%){2Q7X8maWz{5Z)c_v*+p-df_zc^evP0y!UQMwSS`|K8)(p+lwQpfxaQ}SSJO3 zf1mOplm9BPF_i!Isl6MRB(KoCYL7!1vA1b>s%AjTvF|HU@_i`pTH*B&?^WP-qbOoL zkMMyy9O>OQ+5pu?V(L-fSwJP_aHnHq3w?Sw+b62M4`y&l{}uJvg|~fn$X151LW}!% zb*QB`KvKou-JibDLDL=DmmW?L{y05XljW0kG$eWMZ)+_td@RK9GRmG3+PlWgKY6lMkSgk0!DNK)5JxdzGLwmuLkJMf3>^g8G!$WcT zC>@+%%MBbACZCVprC?NJ0d(xcYMjrC(KD5Wh`(2BQ0sFF@d zx^Xb#S?)6JU4l8N_-4JD*-=XT*|$jdj_eQ`Q65ivbej(5xm5dWx-O&hZ)>GYV(B5O zd|!57unskUPBoV7qJljFujEf2?L|BOTroNoGKFr}y<%3-9|jj*N0)jXV}UOvT?}6; z2;f#J&zncNR>48>w{Nfhss-nIMtIdqU!XhvtNst=sIc9;zAgP7WbTYPM8}M0(b?qF z`KAnify|}9|3VIJqrwk-zPN$>{Db0;|9U%#uCe;B5xxu%p1rE?^05yX+l3p@sEELM zCYv`&D{J7B|LV>ow{n0woo5MFs705=hrxTzP2{>Y(6+9>j-oF0+jYEOMmSgd>(O3v zkDiqC3x8JxhOUZc#?v*TCq@cLn;vfA|qegkRt=kokq% z<_yL`qoP=PkYzbyPSWLk;Z28Q%EC^Kh;$u0`#-v*j0HUPZ?iotG*}e(ly|ONEz4E7%x}K1xdzfrG6k4Mo>Cfseb(eM-t2 z^mgdn*g48Ilt{6D`lC46r>g!if9p+wk9WF0&TCmgPxT%=zV0{=h`!*0WNR}r8F}y= zX)s~|HtUg&7&@5D;I~jG(2h6{y}GSQzP}0RqWY=QB{XU?`?SeF4>Z5Zysc+TfeWsz z@KV4=#6ovvL8L+)>fDj^L)x=QJo@^(uB=tmEfZ&xI?#)x#_~t+Thil$?I|A{gt}0!EPQEjocLu2%Y1{nYk{X7mz#+> zJB$B=0{E&;Jt8O?of^);?DT+zRIZqRCfK6 zD=%ImbL1^}cliaHNlB)hFZ>ZIXj!I*}xm$+!q4uWMv& zR8*oYmZAR+jZ7gA{yN-GM-5r6E?i18Ye31T-sycRk;%XySoDTcPEY?)nI@|H@Txd;zZ%~>R-<1n7QywyT0z|O(rN(Rc(8i zUMhHHfdiRWYibIYVt%Q2cf-h6H)ts44fIC{~-8q2`^t$}8 zC36MIi%DDBhVETNBEzp^?hzh`-HL5j^JqQNY}Ti32yn?`49+IG z7U>qj3szq!;J|Gew=!BX&(|tFd)cB4$P2%6d;dxZrmBP$D`9a6tOGl9vdD9ecKymN z11%UvyCU6Rt_%l{heYu)3*lF>Pc&yV#*m~^hT;<80jyvC^5*q-CY)JjY5vAc9Ugn_ zVz1-88-I9%-8*(LLi^uCg6TO^NQZ^`n|2x(q>S!1bR~VoW~%yAj=Kcl*M>7*-!s%O zn{w2R2Y?Y8K#6B2AH^ZQL~r(LRvZ@i2V7f|riPj)S*T4vaKZ>FJ~1;pS!mYP)MoV` z4-~n6gqqKq3J1#lV?6SY3SP(w;k-Q}iGQ(bDSQ?c!mHO_Wf)z)Sv-^Zo7|MzW80o058dRt_`IrU@jZ(!#Nw;RmlpY=YFzIS17&R;VOs zpIt0Z4`2PI%1~|7f_Jqyo?p%000k;W{j=L@u!MF}ROxOt+Vf`b^m5@kxK@(LyPH!U z=CIc{8_JZT*tW>nKa`3vT^1R{7Ksy7%HQ-PYGHVC?~RjU&ZM6{D)j5y-T^QtOC92& z$qrriw^`2YQ^nBxacR30FV3K?ml}O41FdyNv};B?k;bl~@f#9M@M6sI3a1Dj%w761 zb-ZB!Bss4;-VmXIk}T;vIZ1EyNu}C!>IYh^_VMxPpMShCeJ^i5D|NEE_fVD zT|z^-8O9=o0`QrcncK;TPQczOmalt~1B<3`X~={TAM~k>j{n3Yp!O%KmtWdgaNK2I zgNJ7*@!>yXtbTLM@MHR9%I6m}Fg`%eJ?7#PGK%;+YVt!6>OD&8K#e@m`Ws(Rn;J8u z_$-oNc90PsA5mmvM*{f2?MbT<#Dhg0^Q~38Ngv*(OrDx>C0bkp@=8e`k+TYyaK$a7_0)cNQ*XtbQE&@ID_nXMn=cMGSDo+1 z`^eyqey3*LHCp&_Ww+B_0d8FAzqkAl*)NNqKH2;sARp<_{k|JIC5T1o7MW}`(6QY3(Hs zt%~&poe5u*`Mj^DD&bT$Q|$=<8$|`BDgLYmmTV%vzDOyKCBn=0){;0fB!wg2^!i?$ zAsz$%>}y%<>`>0K#nhUQ9~!!L_}%beh33)A8lR~I;hCeZz( zfnQ0$%R9te?-r}T`z~+F8NM~5PNKftRmKBH&f43hZIFES{AKP>y=0Fzcf3SjgC2i* zCKzOVmKU#-DAEqmFQBN)-1R*BY2hhH>Zunen6QAx{n|;wcb0NK-{_P`_}YiWl=&V| zL!ZKE7dxi`aQ8p{FKNNE$Op*1FfCjHkF2j-O#Wto&U?X&>gG=L@@34M-{JE>=gJH> zM>*kd%Bx0FuB@V41o358Ck5lijySwam_SbjTDii1^5Bij-8KU`+)%gK;Mc99Az=Al z)PB!(3Os6c^5j!~4xG{DMiH(^a>R~$*}s2~efEc^)^nCwKsClBibbgm$$Xvpn`Af$ zs&j(uFOj@y@rYTNcOCK5vsV?_>C3_q;d#+Nkt<*+V9V4>q8+qpx4Wj6uAmI66FYVp zQ(&Eh^=Fk#Gsv+)`jli;5vpA15UAji#Gj0J&({#nirCR+5hZU~c+LBcQuWy+WZIk* z`&X$IOnsjzmCloaN%^JyVQf@braCL^yh{)0OWoCBx*`Ufc^f89udu;VLGFF+SLyKn zsxI2J$vI#yz;Kt30VO(|J2?ydceEXJspH9A!`pNu<6)Wl+TV7mc zfz)@T_RrD_z{_Eatcr*k!f*7kkWLmpQ!N`6No0fG*^m96>PkRe8IuI&8GiUPYwu1M z$#uYOQ=aL4b`+W8C)|`q6gcLKcDy^`x1Zp;YqMOr2(G_;qtrn1aE%W|-m<4`pvxEa z($!*{!Df{76|FiNOfMgA$H2)ACHB)hajj6o{%;g#`K&47OinO&@Wnaw^`z)mrpXz= zCi`dq4qGWq(Umc-oQR}v0- z(n0#TUG+SNl;MM*`7Pn&w0P;e=CN#YP6*~o*|I6%f^D@`J0p$T(UoSi(u;m_xP17( zHMyJIut-?$t^$CwOX*uSLK8u}NNh>R4t)f ze{_3Wg6JUY5s#;whz?hb9!wt#9RwqP>aTk6kD;LRrOy)C=wQ&V__Xlfm57h6Cn!LE z3r+i^N{tH9V{@2rsyLo-rUlRJY!8pZpu5JG$ z{p%6bbnilVz#SSKhtCV<=}mz<6~7;^&kTd}Xm9V|-|z`x*bAXU-S#1Wk^bGe zR0dkw0aO&v6MFW2E3)T1w6HbC0M#FARfv``<8f}fHh1$yr1X2gR#ejzU_F^%=E^LF#IR!r76I$3@-1xym`c>2>d!ebtnueprts2 z-+ty?*uJzi*3DoM$tdk&YfBV`Yz~*>YYz%T%H@j^XJ{#Ltfq?ex#$s~&ge>$n7V-^ zFPh5QQ16178#PN4Bq#8lhUv%@$$5`)bvoY`KiU?i)um^))yk!xk@dbVVC5%3!8Tm}G%r z=!EdoL|*t?>#}54G7SvBrF%;J9v3EcwBAI*ExMFcwCANU8@}Bho%FGo64o0i`zSv3g4EgTN$ymK%3o5snIbh-?)sH#{6d>L9Os`$_Hsa~~ zu=#6^3TjeGE1V~Dw>z^8Te$B#^7=ioBb|B}l4f^V`#D{!8@uPqJgcd3$BJBjC~cKGIY7 z<*}xE$i$1G_THTLHZ1s;|26f;-*|E67~79n2^vU6b$-i^ZxIzno~Ax$!hiv5QbqWF zQFzMJ;(5#ug#0*PeVRVS22Z3){QdEW8ZJEA-cHjcUXM@g6rl_Ch{GnSH09J5aGtyH zBzTG)FPt>LtKXlDsyIfk)VPv!Nl)g)Wz`|HW-@~I5Do zM~@f7Kc=jR*B}`u7us2p8@SBQl(yu65-PZ8Z2s2*Uj!ty^xd}F@yn}?EQDWZmZHEiQYs`6mH`Y6`1on*I3Ddn# zaul~C#hPciv5zcg@55|1{FPL-jU!_*r{Nlhg)U&>AnS=|o zX{}u9$N>Gf74x4Fj!1HhP0LYQc^J{XhxeT9EV?bLR~&zw7HSoQWM|x`$NwIz-G5mv zgg3%+nM)qgL5rLt)i)kgqOhTbnv?9`k@lcGMoyz;xNklayf8{>mAc$F+)#lxHYh#2H zHyv-9Te3rkivm|o2WQYI+wL_pGEXxSaDD%PVg|fVIp8kwg517p$>FuKp0{pl3(Zo6f4 zQWL+Ng3HhPM!gTHA?%$_syzq3cG~!8=pY4r$L3p@bdBVn&td7aSE-?3?Tf%W6?CxU z%#zR_%|7s&Q#WAb0_nwf$oF2fp@6MbIZ6w0T?lB}Jr4W8i0iS@^wI_cEIRwJ#i1k| zb+eDk-{30;@;g_TFZNMEf92%C8Gbe_^DMzrbGiu$4T&ff1rC5Vc}h+?WeRA|TvF#L zS_=fKT{ww~7riaR9V>ht=t=L8ByYM#YHck4|V(zKFIsL*g=uGTWZsJ?nVf^6{ zi$EU|zDn)MeKiv>|J3|s`<))wE_%cmu`i=@w_B_BHSywD=2+Vm#%5G@PsFx`h7%5c zO5VIj?uFZq{4o@`9x&uq(KvXvp+V*tyT1=+(X^%RfrUjTEdO&s_kHOs=tW)k@JbuF z>PY+G9P1ExjqNs43)heZ-Qt=_SRHUEkKFJ~=>$JltEb)lbHIbsR^OOn*s$F_Qb$T$ zM!Ww~wCykD#LNyg>kB7%;j8bImRj@!NYZ82ua)?K6-7$-szy|TmB{Xowp#1JKJo0n zUx~!q>rOGM{&ybq#NR1bsVdNH=<%(%a}~*N z+zTkITLk?qLp#MA*AaIZ^^Ic=8(=`2X3wC=45-c37?)up+}S{J=Vyew^WPbP9cErq z(D1CGMDXz*WPb3E=SBz}4z_ips5!?3t!-rk(~^ltZ0A>ZfAaSn_^F&D*@6zHvmiTh z;$2v8P`D2z7_r@mI)RI=BCjCFo#H>nK*tRa+pxFWXvbhx#fbVa+MaLk-m#Y+?mnsR zeCZS|%%Y@xb2p^{j4b=J2J$taS=P$J^I9$7llS`TwTD~iT3Tau=N&fq$*fYbM}+W( z{S;L5tf{dU`k}$D7K1M6>)HR9CFjaJzLyquP-DwTydw689&VY39E{hyw_7%#Z*JEJce+?nw z9CO8H>H>6#CJW|K>glz3v3=f%~j++0wi+Jwa*I>e*2)M zVD+~#z*#7t{~ z?u~)2w5}{W;-`Q7BPejOuLh}ff2@oNT?5_MMTEc7eL;Wxdyiaz8^FwO2vx+4fqCKe zs&sl)_||KmOY~JX{Jo{6ub`a{haS$X<*p*W#0<9G@Y@*L{&zBY2r=U+@y7i1nm+JF z?bVQLXD6Dc71%F!fC8Jy=jI*gA>8@Bi_b&$FyXT`xrA&`g?2=Ko)q~rgr#_4z|utf%{CMXqrU?GB~aaI#{ANkWf2r+680rC6j}>jV?*zj#7&_4P2?v7A&d zbe0;PE{qkhOaFypWMlfeuT#Q7X3yngAJW5E0|hf@!J&jvjYwbu3*4xxQr3e`rBcf$3JvBSl^6maR@&PAiR z71YQ*?g4Q5I|EB&O$RvR|NFP*N*_A^tKiBr!hdr7@3OYu*E$f#XKQNSR0IB%p-0rS z3#gk*)g^?Q3NM?o7&kwo$D@AfNes00h{rJduAognvhJ*YQDZ-d*wrsk+O09e&<;JZ znXN6bdo9jw=WI7%ud=nPa;PKy(_f6Pf!knmXJYXeDSEugSex>scmefj+e=S3^?=ki ziO3MaJWzB%zi)S4A3DM?@Zae68nWE6$xK0f0|9%kJ85dpfOvf^i>FVBH!QT1)?2?1 zFkkX~?3PXDke9ndPfBUgSWClz%F~s3o`Ue}jXCE$(hGi}$nF;^l#VK+ zJ_qfM+C)Ct7IBYlIPsyVB5D@@HuOke=1+W23vkt=w!WZ0i-cg2^jS9=xVgAtm+HI% z9z1f<-g#{X*xwKKS{Pdbe$_M!nKI0{LG1x=#1cFHP=H$sI~`!iJj9a=&unOaImPBFvN3~w3k=S+j`e+sqd(k6fsiPJ`zv8<468=N^Ji&xa4UE4Q5o5OuKq-9YNHRLwtsK z@64vbwHPkg{Xl+#igO0tH`6Jmq7;Rri^Z(32ybIsJ3F+{YzEvE4O2T)G>lg1bXVL= z;z|Gb(B0G3)G&OWTex^fGq|Y|t)2Rd1%CD96P0|yiW5n+kzTGAJ>8%vNayPXtY4Da zl-Niw{lik#o0AOqV3}#!S}Gr0dbMkXWu5dUJ0HHM&R7MP&hJcR%O*KihQ&Jx5#kGEwJTUYMe>}TMLMnN13)KN;I2&k78>6O>U-f&hu{0;U+31H0e@s;;HQ~# zAo8{|KDmVoUR06yix8;>TKl-80!hE(gPqnt+JPl>J^$y;X9nbXWqICg#l8sGt$s98 zCVd+^*-ZL5vcEDOcaK(X;>Mhrjs5ARgCOUT*qx1ZcVHpHy}$6kUSPAaMx}af1vFYE z9tj8>0&ghIy|_9!VPC02JooGb(BTXW(%UhP7zMqW{xUA17fH74N$(fXwI05Ux+Gs9 zdCT4`>Tf@~FzE2bT6qFZFFY?w$r(k%>^hn5zKvknWJ-9#aTI<3YU-F}HHzLla=fKh zq{F2{!J5k9J?M(BL4=1|J37Qc$rA6t06Bl=82viW3oD;z^_r*70`2YbH)Ve*aL<`n zx2_6Rf?0i~R}EQgSYW;fs(Wpr*~^CnHuV32&2iJ?b0h62f^q(~C=E9h{dF(H^a3Z2 zPVOvzzGoIVD7+NRJ++0p0#Y;Q-Ime53-dBZOjzLCd*8DD+e42Ze_FrFzPbqP!yRJ2 zsLp`%Bk{9I>SJKY9XwIR{OV@|=GY3Gncq;p2 zyG|grzp(Dj{#=w@#qJ#X(0Icb_-?KsTqWK7+9fP3&->p3GlBr|d^MwvW!R_Cek zLI0Bm(^+$#@9D?N9NJMJ&&I|=9P|s3!hEoh6HDjQhUo%Z5Rz+9Ociw7C4MttuGdaS9E|m zjX#gAO{uZQgv6ScKr?8Hy{Vl?`rHv2mDSg}*|FZ`tC5iOM;9F=-Y=b(fNYN`S~4t{ zVe!)8=HX8QaC|HBCAA+p@9YtoK57qV3G?K{Sr_YDd-QkfG^`#=Xy3;CPAi`q?lJID8j8HoQB49M%4sGx98fsfo~R zUlRtH5T@vTUxx~EtCn($-lM}I^)erB1u;W;O5eckwqbBTwf{b2F$Mh6O4+^ZgfRY| z*QhTSM+;jMm@P$OcpyuSC%=r{AfRC^lH1+R1v435ms6|GBDQtKri%e|uwr4~nd?C# zNQkS^k;P9EmJWytw{*+{v8B2KcQq-v$?l4fGs3K26Mwcsnc?Ww`ALp=Ui>XWI==o93w-{pF`1s^a`e~n zXxR-W_|Rx_^uYo%w!2*W?{hs9JoKN?mAu#%u&c5xP^L}<%bIdpM2K<1`nuPrhI)AL z`HyrREW~5h!h6O&;2kYY_YLS-8Q_G+5>m4bPSL=#?Qcf)QwUL3%X&HE>H<)2OXlp! zq{pW5p(3Xe4#0$`OfKn9y1?S&^Il&?S>Q(rfgcx5xpBfs{h7=>W-OwKSY@TH(_#P? zRk%$(m@NUq`gJr*K>|?RLFE}s;yU2`nj5ieO#m)C-K$%s(S#+r|73c%g<#l3VD85Z zKB#{+&~u|h2rrhV5b+H+1b=jG?`xBJPZhV%N6Rr#6;=9q>+=dISo7Bs-XiCOvd0xu za@^SCWeJ6LfCT(C>J@yotpUsj-@004y$IZt^ zNNDxr4Sx=(dd8Ez`VAkvGJouYn9c~$Pl<~)Q*A-xzOvR8tHi%1pZ40BO$Kkz*wA>I z65e&CnAT29H4`L1?rjjf9%AD3i7Esw!Ao+lD41+4}c5ro;jaAM29g2 zi+hpS8p=Ntyyr)=5Ef{7{n###3paKj_~c)`5;SG)`#*_-c7^>Z6S zn%|g24zNmKcFnei36gvNm44}Zpm7tD&~{x6G~a>ueUP}WmNkv!q64ma*Gz)*4JuCdvg^=CrCivM>ZEAsFBBCJ(rj&`7sfsr3o{bEq6#rleuC^bId z8D1n-J^-Sh3mOy|5g*;NHP841dg@oRZptt;;N}Shw!RPefos4~a zIA8&p9)7fpel>(OClUTf?be8Sp+p-}|DKW+{@)tv%X`(eKP_ltVdxYa@f14#YJKMz+5-id2C2+Bjy=n5)9BQyU+}ZW87acTyzNFT*jK2Jq`tR`-2JBi~ioR4*;HBBb z2W4bVI=8rX^`qoGnmrXbJgP|Y_mLrG58q8R4{NBjsp9R$alZj0(g9DoEczAN(zfBZ9byk-Qzd%aq(tQhaIow8Q zx0$BYFiI@xCh_|&dQ@>SXuuYR<~ zC!q7re%Eq)x6qy5K#8*p3t&|JWJcIJGv@v0(!qX^B$_Uc~5#76`qF5=-ea_I3J;If13)j4DM}FJirWjSGZ~uevP7@h>4)mjb)%A zYGJ8Pcq3QT?L)MQXXA*p%N|7vvUkaGwqqD%#}yd{6fC^-(7i#iFer8z1h_ut7*JS8 zc}l5?Zy+VU*5*2Bu1$q|JL_KqLw@*QwEtU+|5g#_u-Ee$S4!wqZ?Ir9q7HYT&{^MK zRfqg1`~#1+vf#`6J(L{m8-cb$f2&slCC>b;Dj4I<3NP6G{q$?42N}`6z7RM^52Z4q zGKn_idPKO^6r&G>}dhk{tps zgyYrvW&a`N^;hvnIY!X@??COs^abGdCNsH`e-?3;W*+vln*k1G(Ot@fo0iZ?$4k4l zj(9_E(2W<9{+U|va&3tktP?RfZ~K^dj6_qD+!9ECVemG)j@BSZPC6>R*RURKs8~m> z)665~6Va#bj`P9nf#t-X7Su53M_aV_9`f95Sv_P_M+1Mpwx+LiW5e%~T6rmMsltuS zmWGo`n;<&w^(z;uHE`{%nBGtoncEv)+Nb!A1HUVLenXMWxx1Q16cfE?!G*_L*D_XW zQO3II!{#26TUOeY+P=Js#2Ff2JnQ9vhPy@kOeR^OM$kpBp{)(_{P62E^b&xkR2;WV zp3z{A0S5WZxEl2S_{*2*&I<6k^@%NzW(Hh78MkoDt`8h}_S5O&Zw|Oa#*F#Et9JBI zy6zaW8$j)A2PpH={ zG7h%jK1tr2<1=a-x^LOxN!CZVSFDRs|NMt?p9~7P{bg3PY>XYMsOCAfP%WV9Pw#vE zpV8s4`q*z&A>7#R7%hW~HxK+iSLCN6vlCK+{Cv9HQeYp!eKtUz6K>o+cOvEP09ci@ z@5^s*0y3><3_oNHp?Q}*sU`!>;DKlSsB`%eaJWGy^ZLgyxVG-iXSv0Rk29RIv1Flx zG_)hrJRX~J#CIV=sry3ltcJZ};ZN{{$a~DG!g@r68NVOc4*ls3GN2Q&dj! z3<}o1%P;k{0Fm-}Cp8-@7QVvRHTr@ZhfO~V{6^3zdoq|Vv0fE`FP7rtZZpmSe|_^& z8!LVoe4?LP^d)(p>)+Wdv04EPN7?Rhgmoag(9uE$R~8s&EMn8q#fo1D1|5EVkpkZK z&p09<%M5cZ=#{n!pCfEsr@W5%_q6VH%IDz?E+R^N{`05Zk{-*T z;O^IvqUUUo!s8odi)0^AK#q*RxH~~61?@maZv_e#M;Qh;xUmV2KR50s13$|>HIgCR zgl7ZaV$bYgfbtx(rMYZeP<{H=-(=NQRL8sW$Y)Im7K2kJT5*gposw>Wp7>OiG$a*DHW*eO;SQz?zh8kwX(Y7W0r~|iCe?N@f=7bjY1!A4NocO&GWlJZCM3|qu zS{#>12_r@IZ*$Sm*T;7f&mr2B}TgV~gLM0D2TE6YBBcCKR5h$KA2 z;wU=miXmiuS)y>oWC73(q!zNingic^s+zRthmrJEM!lj?CF0+zJ98wR7D^57lZ%z1 z#`z(7AAa$#qVTI)!aoyg(ZOaO!T%a)F;#I+vdkk!tfaoVk1uKkz+Rri9+$=u1E)j! zp43F-)gzrvX}JVK^sLU$0d>5-7219{cMwGVj2G;GwFP1(3hzh$S_1-t1)c30%<%Q6 z7fsbS#zE=A?Mc>oML4snR?AY_1gPHRO~^E`VWER1H@^9D<1CkLQMC)Pu9bj@146CUq=#+wQ1D;IIN|PfvY7;1 zGQgqKf_)vwTfjS{UgR6PfNqx1Q-J^q%;9Ml9TGB(Bu8)2NLx{3iM{onZRM0u#c1y; z)#P84`85AgXcpN^jz%o68R{Xv&U@z_G>BJ+Ui3Pv9N7c!*UAr`BEIv3l0RM~<^pA7 z@krf$QgF{;Y~h0iIoMcAbxALS6*3<_m1Ym>QTSvCL*M8;I6~2?^bm358Ooe5D(5L6 ztuj@~jA<(hN>JD9bKrr0*6Kg6oMpkrt=Bs$U-to?=!HKI_l*NRW0?UIR}GGsI366l zwuX8fLnD|;A7;Xrr!ep#@tz$iN@yEk#16M*{%*vQ9JNHv@6R{bp(hC)0Z4+$|bhkXJCRDbE9U6f`|OVc3fTlqSm#sgk{5 zmqG%!Bo|Ejo2P3NxddcOf6SE(G2+`*aX)`1w*amRHQqpWDtxKGRZ%TX1p235x+8Xk z3Hz_}B<&%+ue9s6Y3shEXVX;OJN%^ss4kpW&U(2FDh__vI9%R_K8W$(Jo0e^t-btG zJu_(v(#K|gcg7C__9rja-kXjiCR^uC+rJ-x?ax`=jA0eneQ@*fBPYV+JnN^NZQF(J zr^rbc$}q#b=7Dwkp2UB%mx^P3tsk-SD{}gF_5d#pf4cdEW)Lq{d3A;40a&h`h>aK1izIX22mEAqSE@|r7(^Yz$I6k}? z6Was>2u)Z>hWdvK5p67-AP`VJzAq0WDcRSf5m6#e14z zQ1rJR!1Qqahs~WjF!F6{v@vuJ0O4ypS@$6zVLo?xzaJ}n6F-rDjBx7sZ(p_0SLMWk z_U8-I#AZ;g%tddVwRyDn`0b84w+*s4JwMFY+ycZ-XVM+(lZFnR4Ag5RZzEInKW&oX(?PJly}Tfy(NJqe;RfA8#9A)ShP{otEa&eLe zvGhE5?#CF}ti?!(q&^*~nax9{lN zSmWQwk=l&)GvNYiyH<7gaWw#iI4vW)FEhw87}_6MCfxJ`GsA1Si(u5q-fNfHESg}4 zVf!wU@A>(n4Kr<8==39h*?Gnr4W}@l`X;i8#zcKfOYsc4n0xs4Paisn<}(E7C24Td z$G_j>1zCs(+nz>^uMd1OPrG^l+%&2az4?*0mH2f1g8eVrFp~E{-`kek(_r*zp!8$G zSp--QztK12gCiQJixjoku+yTY~u2xWpMG5`KB$)tOcloLV`@zheV<{`wZ8X-)FD9@luj6-vP&K5oY; z`#7{q+%?uZg$bW|tMDYrn+o10LED3&9JtDzCl03&PW(o1XsBx^a9*;Y-hRLj2Y;w* z_WmY)s?!3Om`c`>!$0lUtg~zA_9c0-8%@ioVdqNihu}rzVRmd}l$LN0!71~r-S{@e|bTE~W`d_=nOh6U5nXt^v2Rpwm>a6HG?7Ejnltu6obFyEEK1 zAoh#K!r&!FI5wR89uOYuvzW*s7;3 z6)f7~F+cgG1$fEsY&f1(3eMHiipZW>Aig;r-P2bIZz=TMSj4vuB$X1w{a$ViBy8oV z@{4YR7pLsYb|*}NI009kqLeYvohAPwUnddVu^K=8l}QWIezO^t`neB#6a`pEHL=68 zlEY;@_xk_edx<&fPxck*TDtKnJowYC^z@bwB-glSOr1l72gcoc`O5UtIQZ?}S{L<( z1y*0~fAYnu7t}<29Uc;=C0wn#=h=7uq1Z90r-A0{NW$XX^E>(Dz(BC%nNk?}9-atr zkz=QZ4Q$Pis$AF66Ycb?Ymu|yM@~#kr$!GNWr^mz_IeV1V{6}Zdiw)hG195XKhlgs zzfDbd6tLo5?%koP&uRgOG-u$6W3*Ua_eShy;S_TI^XsG9ff?{+fNe)L#TMFgg{z^S zoQEa7WY$<67E!;fSz3793XsT}`J;S=4I3zaQu>oZ1@m5-4JiqBBD#f@ME=@EBu4L| za4?(+e?OMtze{WZ2uP)szUc2l;TgT<4$QMaleT12^mGoG(sZ_YZ!!eBeA|;%`bcim zs!BHSNEdRL(`#l}{s>%@9$Nokk6w!7QDg)N74bMKPv@vdGbbJ zZ%Pwbd+{ITN1bvs@zv)GdjYw>^KpHLgS9|=f0VBNT?QN!t@QcV`A$@QPQRu|xeH~i z?XQc~X+fU{MGX!`Zh@c6Yu`8AS5djl_J1*iUpk+0A$<27D|ru7N41&@*8Lbd6Z)-$QkSpPdtQgrR?o%6)D3RZz1G zCg&K&L5=-F@R6u@s9A>gYtr}(x@VLc=1iWSY`?;e-1>hW+3nG65w-t)Ql{eHck&!<8-T>9_AP?6Ep_at=O1&@N#N_cIaM|jxuqibW zsvL6JJhGXM)*p|yI;s)<(WyITg@`z}C%wg&V(GC@uvM!t2Q|E@I#IkmvX072DP$}H z_mP;C^$7<9GHCo?uJf;a+(8%L16^DmjhihQkead zYRa|_?jK#zeTn`OeT|BoM7uR`+(kyvVRHerP|{tP`OyiUTV*VpTMh%qPuFfwC9NU# z@ONbNI#e)5M>zcNA98}fkbVxePlMZUze&|P)Bv|ObH9^^DDbh7E1mrGEubM|^of)a z9bEm;<$keZ6TLk=lX8Gj2y#!k@9^5BfLDo%42)YVz$BmRZ=J>zYV_v#_OqEBYdn^W z$iaE2_SNA}@93&QiS=5!%r|nJHvCajLuLs{a&Y*Du9D;9!@!F!e2my*Q5-sdn*{%i zydjYpNBB|_lYO|f+X3Hp(SolA4g6tucPVXmALy=VJR9nt0#Xalv&BnD@Q-V<**~95 zf^X=LT52N)j{j`aI$kyl)@-O)pPU>3$^Tv+nt4QugWoEvtR&OGml8cue7k~}=K|e* z`#nM^ zgEtI;jw$K5yf-k$!sD|SoOU9 zY^cKsN}V)6fLkc>S4Fxriu9exHfhVYs9Oq7F|PXxo$W@d>B2M@-?V_GOXt{6YAhie zzLmh&1M^5*`Gwm`dJ|Ievtm0WNs3+G_US3EcY=u8OHFE)gW%T{9<$v{5?m>Kg)W!y zE^TI~Yu~lrL|*A)&O+d7skHUx*&_EN!tmOjM5p}gVawu_`Jx3p^9chMEIpf|HVB9QJ< zkCtQ9CbE7XeeeYFoaR-QNJ-Z)1NhADrFi}x0zUNZt+8~Ih=fX?v5fc}F~|Ptm{%>K zaF$QgAFqs~Q;YML#oJcUO7llYC!!|=Q?@sJhc}StCMn?v39Vj`RZe=QO=J^L zg>H;eFKwZ%X+6K;Bdf?@{6@gQA_+AAni!O|*8})o6_@lx_o7?ccfLN);DN&7EJ_** z6R7P!@!}m8V$bN)nWh>{aC;>+NSQ?Yk-G1R&HwuqDO1}nPp6XO%+--?*X>nsIsf#| z3tDn0-tGTECVvSOT(+MT{xuHB$3#VE@G7V{r&V&Rv=dDGN4@+y&=06?__;s(+zZ^( zyv&EA{vclCFy^BlhtbWkng!OzcCh1p#c-di43!)ItX2%%BRJ-ZP7e8V2yHLQ9U~h- z!7`q?#m|OOu5ICj7Ln&$;hQ};Yq$bd{*3OX`p=@nN4R5eOizH|*-|!EF;Yl?eZT(U z>jMU#Qpsg%I*|TI{)gjAbBKe9clhGHU2ygE2=C*j3Dnno>GvR!Uwmk+?nlSQfHNsV zzpW1~As2@l@9{cMXLotp|GC6svA=f)vwY{YgMBYI@LcchUo{4f; zoV&dTlFz;jN+USy5s9f9L2}gCH(#-%)~XAg+}m-P;Y17TH*Q@jc?3D3FeCn*}9;!C`)cpFyL@Hi34s3QW;?OU?(b5$s(3tIcf$ z4?opwcJu29=nQ#Hlj5F$7*r{uDmJk(h`5I7^dcvJ4Ky-76Cy@-Q#*-YpvDlEbHN3=IKqmr&-Z*YnQ?7m$;+ zyA~q)|BcUj{%YKA2I5MESM}8A!3F0KvLUnxRBj4U#*8na9n})4^toA7F_fcE?huP+ z3+kutfFZ=qyr*!PvI1oA1k4ok&jM?Ye_6LH>Oi1ZTE?f#M7}B~r+YT46I`k$2{XAm z4dxg|jcNl3j{w8j0PQ=Jc$xC|o>9{jFw7?RetxSPF;XQO(yk34+!J>1D*ZfSk9Jm5 zyuk{Kug!*6S?{CR4SQB9t}UR)Rr@xWQ4iix;G-Qqq=HFoTZ4w2`@nfkGQZ!KNTEEl zU(%`10cDL*0KJ!55`{)DN zCJK1)evr(JemBx%ctKK>MGogQg|3`>HHn5RO8p;})}sZTiGxGp0{F2E^NcppPy2D- z1ecD|!}%-w9oy&k(Ieh>>ZYSCuq+_=>6)#toPT5MzZd@#(11i7O7Y_=BpI!9jwzzIRKi+`heXv$%s;8q$ zgZnfM)dG%lVBc^Jz4*2%#I-o~d*S04pceb5^SPA;UN$1T2o2mT&bueQshelW5P)XR?=y+-C20y%& zsIy_uAOwwOzb;rbi@}ahB2Gp@OpwuJ?x#=)161>E^gB{Vhx4!TvUG2=!^3Tg{H&4( z;S&{$WCsOVtmtg@*H)Po)5Zi5R2mlO_FGSAu2mG*=H71f80Lbv#1lUDPq5sx-dM#;H`(v#~`wD_5D#Q9Hsb>m#{vd^VDxxziWh(9mNtDIka2H;c8)k{Q_5erpj#=Up0MO#!kK8^>(@pj|V$y^m~`01sR+ajMR zw7GWX-Ynq*FqIKg5{&Uv0D#Dn)A@Ad4 z1Sh_{@i_ASS9a{3>{X#$$BwH_j~`(ANbrvmBg`V(sG)+~$??Ki!jsIJM$TrX05@$R zcUU4F(FYROoA>5~YDy-J&$p=|rQ6xDkPsgDkg>utKXMm+9*!^%J}v^^ia%rdQAzy1 z#j0gg|MJ83&&;yCFNvIJ(lOz$eT|6|6f_Qi)B|IVRUe_cg=77=XG{L!+% zz7KuBEhPJtjuWQrdOWiZWy4?YoPJ8LE&$&~w14Yg;)51%jQ&nI@#3*jF^gw0lZf^p z$EC5SypYY_*d@A>3bNk{@2k+-KtrvRMPKCz?oy2sZ9%ZGm`=4h^h zuj5e;7e4c2Q#;u%xpg6E?3dW^_na{9`0%=PG*1-oW09Y$LA$`kSZhB1F|MavU z{S5k6J^SRG5DUz`8gXOhGb{d6;Bh7th`}Ug`T)T|f?ppl5FtT$ro*q7zPb=j4<%yQ zuH7VhHa(VxOLE`0fT)*6%h)tCe5;r$^>RxVtI)cgaby&LJ{bm{g{`b`u;)rbGr^rs zs;Yga)5Z-G0{qv+hG&4Ly3tJkLs2;VP7?Uv;DINmB!ff;zXJaKJC~O35WQi2)yReo zZoGA$$4`h&9ER2;rugW}LmD-u6YeU@;E+b-zx>Mruy2J~aBh?zJLsK0XI;jE*#&h} zA9<2v*L1;yY|Si~X*VtO=oebtX)Ar~{WJ#@Ns`cD`Ob>{ZkzT$o8*Q~kBYPu&ah*R z-0Q(N2rmp@qtL+1@+GuVW}jOeR}KDDjO}2fc{Hy?cQXAFVG4WFHoI%S4~~v~Z{8RA zjJjH<8G{M_LDovMIyJ!)k+v|aGm1R`y9UX7>pyYf=_uuG_6;sf5=!y*k|r;tko}tX zVPOohwm)iOjiAEHYu^SmZzO|L%6>daMJtHhv!&GS9UBfErJRh-PDdAYA%f&qF)h}|1~^h2Ziv(A7p8n1bf;%SASWs<3BlOiB9{>kSqS&hVC13*!w+aFC~f> zpZv6IIy^fIRx1YzY>0oiFtsj^{WJ{@tTH!octLQTpLkweT4#m3jvJ*a@9ClF-WfeD zjZ#!35Ok*gA2YT}f9ZWhV8mx2&B5@B-C2L4VU&F^_+=b1CRch(FxXYKt_#{yLCAf@XZT?YR@GIXKSKd7zKWT zpRWpz^PXA+RlFRBQdL&LvS~)Ca5V=G`k?v!Y#`CoOgcLssm+4TuG>p(8 zUloXbgw}LRM$HXA%R5yyJdH0otOA~U!#Q@APat} z5_~n{GAmR}y&Bm??9nZ6y7Za!Qo=3w6Vq{%3{bLN#pkDj5cZRzldC4Yc8gusf9KDv zA@4sH9RrO7w>8J}1IH=w;+ygpJMXyRD{=3p zqBUmtvc6d_hKmjVxiK(#%8ecJeO{U|&aMCluRVS(aHj|ru$(wM`cMLY&9sz`>m|d- zsuWxd%^E>!9wocUG!1k(TxY+F35TAz-qkKTc376S+~91nfsQ;*jqZ^D4VHtfj$9*e zL~>DvFJAA=qK|ymM@Cwy@K#etMI5c) zrND2UYT-~5A50wk^(~4|1XgyqGd7-Jg;NPnKe-Vc;GCt}ef=$Y_^F=3NJ?u2y_)~Q z@K=Nv(pV3gkiMM&8+&%)-qX9ta4wpxW1I<^lcn<>7uZIOA)o#8#SXwh_nP=q1bijq zX@tv`JqwQgV>}vP&kBWhy@$+H1hA~@oPKCJFHH6AoXm`%!4W|fQmW&$xL?DdYr=pR znhtdBY#Q-l^y50`{ipo!-yXNs!ZTLf-Ku5t$&ngHK5l&;eWnpqWa%7U`z3&@+|R#$ zTDga+S-)6wpQXeVb*t44N<1+9hxC`547~W^LU=^u4hJ?d`lhT9z>E`~uh9P)Cf?81 zS*?8hMX)`*`y!=@@N3Z52DcFV6Ti=9W`wE>US6OwOCwuGE%|)=hdYOmfRuUm)>seX z?u>bRu9+F0w`mG!j%9&V1wPk|4{+k?dkv49HPXOS9wirb+EKz|_pLunbKQ3*y7=pZ(KWAisH6jW_JSA38enh#Zbph!VHfhCXS zHTI!v-?cU0r_jUPj}|sJ4CtW6c76WO`F&t(d+UNa7Z0?Z=eGPwKLCuI9Owi_sW9y# zVOX4@h8c_7bpAdx(6Q)vaX6_g&iP1@n4PeP#3f9<5qkHVQ|BDTy#o#_@ zj#YJSeNKw~MvrspmJz&(wBMbi`r^=q^cOgjw}^a;Lr)$(CW^PDv+@mihk&>Cdm83e z!Xq}DX>B0Lj}0h_BI%#=!d|Mxo1?^>SAF=A)$t~F++ncr(`lRsrwiT4@F((*4CBJd ze63YLRh&^{G@T0DY&Ygo$Fo3}$PtQq+YO*EbUn_tR|KjaSARD9aTl1@iGIc z_xbvD*zq-`U%}4m#ChKRNMz;rA~LBaWfT@iL_c8zo<6&bquSvSRf;Bc;a@gf+eXY(wEEG_;(4z}OZ@na(6w)`?dL!OsTawN zJRfc`qabApYy$mWqDEx%gXp~JQq1u=4ty`?j^az374UPeu9rfK3+rm$CV6*|1b5hr zP;3lxtTX- zSa5Rp&f+*IQG7iS6Tt@^o0C891(V_Z(JRTxoBP1B&e6u`;S8ee|Mj06EhEf6e!AzN zjW~?DuGAGrlOzGa{mbPo-3)*ckbGJCc8hY8;n z)z^u8w89gJE_1SNhL}V0mOU?!>Jh+NlD{<4oR<+#ZTeCl(ckhVmCDw+#{)Hf6#=am zEg-36$uach0sO(0d;I$m0chYV8j*3B5*OY&XT$ev2DH>Iat^^0xjOo`41XMHBzEBDsL`_B%@ zGQM%OK6x2TL@}N1B>0nO;|tF4{3dv?POsdwbs6xlf{T-8nL`8@#_h_F`9GkA*>O2Z zhv0~v*071s+eSA2t46Yc1fR#WSKYpi4VuIC*9G+Xh{M{C<5MOzJY~{nqJDq|i=MT5 z6LnMquHLw}F;_wKF5l56{h_DC3D4w5Kc?@XI-PyRMXepQ=lzXXjWXdnFDAE#(KK+u zC!qi7QWcV|r>*n(Hw-+YheK(D>2L?tdHQ6-5kM-yJuEg%imA<%ypQTd1GYP5ZL36X zvp4>V!}22*+%R-r_5(35SZ}4^VbmriykpJW6f{#HCRxD1y~7UO5FvRbxK0J5?3qb2 zXp7Ognw_j47URg!p1zpbmIUg_Ci5N%{|N%IS>{Hh8xSR{Y@wx`=3 zdhJ)C^zqazI2rqF&}wW9=$4mv09_hfociaDR4g^-orwG37eMe@XHcK*n^x4s_k*lN z`zMHHWO{l_cm_oF6x!OI$OXn%x9Z$vHj&{csjs9?eO!y-tSv zE*Zz?(JDb#j~evIjuF1{q+rew-9m{a7T>mQdx?E735$9P2@KrMU0<@H!6s%jX(j1OTMSaTZS3o>(!qb{%U?}AUq_nccbwE@CW-vBDTU;J9Yl|| zw2it)0LpZHzgHe4(Et-F*H6#_!cMZ`2a89ILQI~#Me^T26xfiBr^z6U#tdj@+YqjtN_61|XUOO4#aj`!4?@66kl^qj=zY zF)+@1UdGrU0nOx@)9#vPzRzYNM2S{5aA#!zI z0==r__;a8_!EtFZ_?j-x)-rz{g`}Iczi;XT{;c-Xn%($VBn z6rjq}pTs^=y>P;aia4j7*N62Brol*^>$1dAVy=BeG@SLq5TNjzAL6}FhHcY#S}o;= z0ndu@@#y;^Tz!50cgI17VaewfN1`u6Q(mTCv<+^*YiAHMBZr$_ z44SB81}Jm-{Bm%cL1Q7~(NfbCcu&69nEO^2GW(C-;Y%?wueI6HZ_`}`o5HC9dda5lRM`eD>L>!+`rndKM7vn6Wg&N-otPf7NSpP zg{h2W0Ml(D`;TuvsqAc{;J{EdRf<)V+nuw0-kY}zMhe69R`V~YPP;d&|%j~W#I~f3qN02dET#%5#CIA z74OBq0W?b#CY$Fs(B~A*ZO5OBAm9f5G~>l#;QWYu@QX4%j`KXXQnfluaMJ$ViYNRE zG{a}~y-rS|!%ttGF0mwqE?(BDKgRcfr%^2XU70y>*8S7B3W8JcpSD*)_GuxgQ<%RR zM)3K@`O_PMBj&)TuIJ1yY?*+cg2lI{j~6rOoFok;TLxRc3AQFD=F!3ZV~Jv(-H1E- z$F0GTeH1w+&7IHF4!9-T=RDZ2qm|6=%1hoYK*9HcS5{Rcx@p7`m8O~t?$X~8?q*&> z2|w>LB%uv3(-#`_mx}=-<+}wkf^1k+B$fAw!aA}Ny4!V-h8gdBpUjbJB7vrghp)4+ zEP?nGVb4zDypD9vzBPM(4T%|Ey38FY35B&BMl+m9puJ$M_f9ecHo~(sV#OSos;VOF zs2e?Yu?xR#II)eolVZ#8{RyC-RDCIil>>7mu!p#B&x0)gVPoskR&+MRH*vvp2DwFV z+&B_d21aggbRuz1XsgyG=l6LOQNOY5?vrT*JL8hd_j7l_`nw`teTo@C_ob)7(uc^E z&lUU{vDiT;`4gk4!m04dhn+(H8GXoQq$@jopB`JM>zr0m+D0DICCyKUJ5i&Nx_bLJ zF6=O5f3fgAB|LmL=e$`jDKy@aj*!+{2Yf9lraf0!;Np=%)3OR)812Wgv`)2weC%Zc z{#7i3?0YqWbR2s?u&TPL)Pe9mlvrhS=ue^FrQ)xQTj+4JY)s03eZ62Tz4GmzJs*C; z-xes}&ki@0>1W>s+yQi^m*ebnza1( zBAb-a4SgnBk2fWx0VzbFWvzB z3pvJL-fn>jxnJTMnd_)$W-jSH(YLJo<1;C`$c9NGIt}T!)w6VLU3MllyT`C!O!*<9U(OzK^Mi5RGI!Vh$OqirOng?re72k`Vf2n za%$1fHzhaG?Y#!qw;=S;9I}WeI7;q!{*yj~!N~8* zw=?|QSSoYh;nfE++)W+WTIX1UF3$Z(Q&gxy-1|HPijlZ~SE+ff4^E->IWgxx)k);n zePN5KWgjfpI2||nQHs9lQXkHZuSDg={#DImWY|l*u*5x_4tEb!e={9s!F3Kf-{dOW)Vdm86~8&i~r%FhBcb1zMteWn?Cgq?n* zx7bb0p?kRw|JRP_pHy5fSttiDuf|I-IZ@&G4{Jx3$nwBQWlnYB`DGxaLO+?F-3omD z-=^6TexuVRqoPMMs4ytL@=#%t;B4Hacuq^#2+R-dH91qzLEbsGr)=*z;M-#TW9BWS zaPO}7dKu9ZYbZSKvAnf{qAa(sos%WQ)kg(BI;jwR`E8QiTB7G8XBjtB`|2Nf`O3LS z?KT^0DSyXV`_@imSVG2o(Rdo2{R9rx-XX!Vwbu;qE0E(i76H|I)1By8Ui(^~ z!FNzMOOpvc&7n*0oXl3%DX~t@j-`>%4j>VERIh_Jks0P?*HVTY3QWJsM@wtm42WdIqG;-J9U-{rAnvy2SUl(*X57_BCYp z>Dm|JQNllG%yede;Jv7FI5f?ZvSIh8vYX686CimxXy2O_G179h;X>nPoUn zNK^yh3vVqsp3Do|F}A_a%fU#v+a;zG3uwTIXFis)0bMxe#NsX0iXKoKzt1gS1Zm)g zzI4?xN_xDbMQ2P3Gp}BJd-DDO=v1iQ6LZ-^=JaKC+LSv)|7n>!&u<#BjI68k*AaO~ zugv&tjTms`!2Gmm!4gPqPE_WO?*JtV@^3igd%#|*&M_L54RA*6!E_gqR}{VWx95Gp zERw(cTv7G<9QaqG^In(etu0;m|K=h#fr1_g&39Z~L;XKAll_l(Bcalb*0>*Yz?=F- zTlIz>%sargd_m|CR;C;O`sybsG>d#|w@&caaAE6#AyzgxCy>{zE4>b0Z3MxBG$!~| z^-0tiB8BS0)Vl{L2tHu!wu#?47WkX_$mH*{E6Dfxo=`y$4g4CM$hodT592&}rM9XE z0ENsy8}oopbaO_fg@14tc=)Myja^?b5n03W(W zR;}Cx5NJ?Lt=qN)E@rLG0^51ODRl{Pwl{&46e8zMS%VIg*i&7s6u_q*H=d9yrNqHE zhYdrSM4&fc&D*$SVZ13NY$)|!3c65|w3gYjW8=0L1I@(eP7rOsw{O7-Z!M*ra8f=3 zABMBKhYqwIYIcHxkmd>IWbq(W^z$f zUjpLwaMScqEg}gE7ZRBx=Ajh}=QGbuBT!GxGj)<5iXJibw=|kTQSP#Z4+*~OeGyp( z&JuCB6TW`&42LK*3$rQsyz@{Z=pex3$HQsR3gSzIuy>-$M>q7PYe zy=Cz-k#{p8aT@-;NqmnvrqejBNaD&9AuPqxk}!evN}8a)2HZI(_Q;Ir%WU=gNcw*# z{70|Ju%ieSjF3L#z4V3$GR+BuM-#oq=Jcx4jR-Eh)6YWjo#^W{^2hw7l;nYtKYg}l zG8nPMzlq~kCwQUocjxjrR(@=rwZD1dBQ?C7x*c?eZUkv?4caXUOdy@eQRh!%)c**<-X5ReN4A_egOWnUT#gNTUHDLDV$FmdtgasPzsmqcKIqpS9V0y0 zyyh2;5fijrnPWYYOM_e4AD_ykTSc>9FGO+G@WCUY`fK|?W#MJvaH@L}!cer-=ukD$ z)0=)yX+%TxhUFh7oqTE{2ycg-;@3H?1-~yHIvPF30vFtjJU_%RV;#DGB4J_l@Z8SL zV?ADrAo%Vhc4k&q9IXTeZKl?N%y0hIIl|jRKCKi=#@2+Mh{v!!A-GB!?TE^%js@GM zEEaz6VSudcdwi3^#6F(0=b38|8GKB?EBrWw6DEFMfO6VPNcEwiM~V(BCcR&?P7=+6 z8_PN8i^}=n7&DujUF9f}_;QgVgysPDlRiu_ti%mXp1F%Pe&&UVL2H|@TC|~>cr=4Q z;akmD_$)JTMhU~V#$3lwbK>8iLt=*Lfu__9%6)ji4$oE{y2AcV5FTdlqhhAv!sVyp zPStPhqprK)$O+K}P~HDtgzP#8b`s0dsg9gQQK0xh#*7%8Bd$tsUP@?k(A%Yf@WHvQ zI{JvtbHNfQ#Hv7W45YK0&d~1&;oB!HZ93G`hB2*peeT;djtB_|!4Ku6~3K z&g`*=`>PSX!^J~p&#eTZof_FtC6PxU-92+DvX%|rb|)D*dwU((PG6vU7IOfW@Flm+ z=ZoMMH_zmk`EkLO>^YwDLp$hTp5ONUQR05=acBJUm;;8zy2xiXD8cN?&kFP#yfD;G zu|O^ME-`ofXD1sg1*!ZhBky=lA}PPBYzYrC{7mYt=XZjK9ydxK8uEDzg-5|QH0ooB>J4?GW7z1Tu-D&(eB+!Wa0)|mU^1SkF*-M43S zj2(J~gsjg53&Dr!5C2F$qJgTk64m8&gm0t1>b~FH6xg&0*vnL*yxeDb`2m*{ymI{YcK37(*i6%7 zR8ZbT?hg*3`M!M+=$uVkLF75lz4HJgc1PeEL8rw6 z?{;fEb&Op_A1*~d@z*;56Ai2X_{?9w&ek`y`ZR9g2A1hq3NSQx!nh*CZJng*cOMI>& z%{&vQ`lZgHJ!?nK7OYeyJ zTU>XM7O^J{AALHyt3!OJJb(G!g1CMLIdpPIAEqGnHTjgy#T{ZU>HCD9r4QZxy{+rD z%76uFO3pWYo&s`I+fOWs{Xvf0{NlRaGDzS)7k`AZ2gOo9>#kp5#plz`Tw<@Fflp~e z0t`D>P=$(OQo@lLbh6xmnfrbd`u=|3s<@j5rfOd=3@F@0f`SxR0#h2ny%X1NN(HV1 z=>(6%w`pl$|G|V%Za4@Gpttmqh!k)Ls4c(9ECOUX-R=5!6k)3BT~hvFap)m)-jDv+ zAlN(gji_i+VMCq!WCLQ{m`u_5!+-3Q*vwSI!S2C2Sk}Lv?wTS9&o;8Zab4xa2Gv)> zE_1DepqcY8Cis}4jpt0?c3cf&;hq2UpBoqMuykR^Ct0CDv!n~d z%exZjR!^TJ| z*8?cw?L=O3Qh`qJZWU=B@MeQ?H+#aGz1oQ9?yE=dE3G06nS@)}HNcF~Y3}(3sUXClX;7-{eg1Y3KP);N8^n9iY+({ksxh~IwA04l3cp10> zRGM_(XhOonq;4QJs7CNX&@;P94l?LB=9@uD#tzdDK8`tglMIs^dW6^8F=OGe=FjSF z8|aa&!`V6C9`xJlmTFk6IF!BFRgf%2@QB6at{sZx#ug5ztaMZwP;dU#o+&sDv|WFh z9~An7zVsURScX$Te-g**0)4{(n`QWDV|x71KW**>;TylhuSz=KP7QlWeM&~!CeSr$ zCTYPYHq0#}|1pq`*#GPtF32oqgdN~!e|rZB>}31UNfAYb$tgnK-B)WsVxI#W)a%)Z zpNF)Aj2U3sSqsR~=7Cf7Z@KrLtf1TF$zfkImeF|OJFzGoL8#!`S-h6t1zJhgbc-Ld z;=jidrS|&RVN4wBc#k(J{_{p>Y3Z{nd@9Q6s~x$5B!}`-zNfW;^}6Xg^+U~$)^q%MqTz)0%^n4%i-uV90g2;J9@t6g-rSzl0_>TkaTIBGT%z-TjCsKUd zJfE{FixtP4Q-tOorNo9QVtzuu2Z4~Mkab1iF6z|?o$7L@fVWT2YgS@{*JMrl)L3H- zO+GtRYB|i0yUs^s$5Qd&@`($qthFrgyz_i{1i`<|JY*-3PF9K9BUhx@W-E|}hkSSI zwJ~(}po2^@!MWNvb&bYrwgCMzul#2z-H6<){V)C-qJj>$WrG722%i}Lly)2IA|hws zKbaLU2^=f(V-z_yKpV|Ut#UPS-`)(K?IJj7X4I*Y>5tg3Nq(`-A5UTEf*k()!d`~v z`8hq#5q+Jfz0FV8YIecPD>MsrWusK2M)gSWy29VcO|J<+sqi8HE=3nwn{x2E%)9{tdA_IxmN3ItO0PWQ)<;q1 zklOi|b^~_bld-)px?1gzv;G|D6N%%o1k88ZZC- zjC5T@hTHL79sh_NP~Hyjonk&HJ9(?MwJB9B_I)T%R)J?_XY*;g}HrP5(5T3cz`1#N1 zD){rXML~I{A9%*xEW*DiprNRx_%v}}2K!}*PHM7XIi>irphp8p)LGy9;_(p_^7c{9 z()R`Q3fwRwgFN{1!Y~i%G%5DfHe8B3yNfc(NXwT5Nuf-;;(E=80YoP^;J&9#a7pi9 zd!~`M3}lY?<@Gqs11YoB*<1HkKt%G@$a|dAAjHbcy{>^7A59ZpMMZzn#mmVY%Z;mu zMbHCPy4E2x0ZzuSy$xVt&UcNiv;l-u`RHGuqQ-VqOOQ~ zzzZuH9oKaC{-Ny9^$(C{6e*VbEKLmmpL2Tnl~6}Fx>I;>V25l2wLS5;_l#`F1y4KviW*}g5YLkdM6hE`VSal#PUE*afT4t)QMqRda%EznKhITHDi z9A0J*7SD}Z1qG4|5fwKl!9`&^2jQdSSY;uH)U%EWtLxZURlMiL33p!j@5a+%@m4zH zoGVQDlEJl)6z4Vp$96_SzV!|$Tgxk;BKmTyobJy^?h#zUMh_7=PDPOiJ+=&0Jo2LiYq^$fp>z7Q_8QD zgPi-q7X?po;~Bc{p>xK|Xd^2%Wd2SQ5O#8|6mR8+YnS_6>?fCiVavOJU)gBj&DmVZ zCoBU%YU~n4{dXxC#$9yVXlD%6KKbRxSZ4~b2JJ=~DA zpTXdeBpEh;^FzN=U>l^glU%Uc+C_u!E-BPMWrn#-XM;^dmQd5!2hQ{Zh2YGyn{ltg z+d)`G`nL9`B}6kM=Eqb&fg0m?bMCrNAgfDm<307n&)f56RY7R3K|Ql0cV{UUlIpqkhH;xD*h=6R}mVisJ_Zb*9N zL4u!U%wCY`5`o%(scx)`te{kA_|>P996z?)cI$n#59r_Pk$)yKd%nu9PuazopwZNp zeb(?6aLNfxdRp2BLeJ;NJGqiT9>*BH0IFRi*!6Cn;KYK*oNE6?zpX~aiu2$723G(h zokP%-D~$M!mvX8!-9c#39@g}V=oJJc|B!Jg*#eU?r862tsgV|SBSI7CL&?F$ zOK)b8?bJP+p7nhcGbwaZkDCl0I8|OVzCTKMv3S#NGu47KYyIwzUd1Ero*2n*EZ>mq z>uwh0J%G&S%^5TjSm4VVj$@CCsPW0BHV&1#VW6D1HyG@&hH}R386J5~q2k?b;pU%9 z=&Od!MD#B*?4d8(^hkmfK2Wx+GZCeL`}Y&pd>j{mfD@On-TFKziYawmBK$rUB*TY4 zj5A?lw%{kyH|d~TjV4Ltc{Y4IFFoL|>J~U-LFYOVO##;|C%XmZN5NkmBaS|Yd9*yQ ze9?)F;Ncz4zVKXS8>Je+@*BgGz`t3}yXo#L5J$~W(ilO3DIa(=xdq$;KRQ3jW-+Xz zf|~O3c-R zs`pHjya@kqj!*N-gWqUsMBHX1YMb!Ixys4HQgEXBRN&6TL9m&=Nnvne6)8l09^j6j z1)ZzU?oJT?A`gx(@8rJ}xckhTSi&a{KRS>NDxM;Nnhye)uMU@^s-%^(PV%()H@E%X z0poR~AY|@t)42&^2H0;FYA>TktMjd%4TC`IR$`igBr}AR+ecC^t%4f;6{}5x)2(W> zE8$7-mBw2)*&gc-g4*1UA+-@|{Mp2qg-0<5D83>);wSJIm1OlENcbuNWpoyndNl~I zbwuK{dDax#w(psiem?=$zw|Eo{-VGtdu1=QHH(3CSkkL!w&ZvsAhY788X;e6W}oqm zn*|~lmWJ$&I*`Z&XI9IbQLyd#KuOn?$Oj9gZJ9={q1OToyQHDn=yiGXwKkp#bc!|K zoRNm$5C6EFHMF*jhJLl#2(c^zS_yYo<#UrjF?6RvhpHRgkT^U_okInUZ#Z2lYg`8m zlCrA4gx{-x_wBf{)*|?JrguMwdJ^?Lq^?oFSOTVh9=zbX+K6}*FI#`RH;Mjk8DG>~ z`iEq_$gF97$3VW)rWKuF1NhTZ*u-;i4%{MF}81grNC-)<#AKa(-#3daxgj2N_c(in2f%>A{k-iA!i!QVz&o9ROB0 z-&!UN{RUnG|GXbxT}F{U6#L=Y1Yg|Ux#-AVBl53Xtp0Li1n3T=sz0XbLw%%^(kfdc z=-fwBlKw}_r~=HSxO!1yBf*V=KwdH&=yqglIB*Ua#G|`|#9UP~{d2T>_$<=;!Smjg zmJ%-1eV;7XmdcqxUT<4(RsLI{k?JAeC)kN_6ix1 z@vY7&L{d>vNM)4>N zX`jl=Ilh+yX|~H6-|r3}<2R8n;zT<^?iY4y`TZQw#{YnfwcbBOS#h{_K(-$!alY8s zAJ~T6b~Jjn-6DIAla}AIAQk?6{fnYS>OXLhx7hmY9y(|;_+3&bXb1)S1?-HUT>ujw zjEp+m;=efU38z6RJIQ-U?8ua^fips^^ZHTo+aJ+wd3FtO+shkNS zKD$PR;^eKLz=yR&LRNGhIA_O=Hxu8zHYFh;>hUs&b{8)dr|JS+dVRMFGTK2vK5H+O z*hIx=bJK_F)&YI-kn#Eixo73Q;8@97Lc>xTp>_kQAdl1bK)=r)B&sFdZ)g&Nta)R< zEpYTAhufdDes8UyS8dCdEA9*6blIQC1Bab=sUsGmv_aFt9 zYI5(J%r8X?l$~sOnLp6!#NUA(pZ*~Op8BIYX<6XLjbV2`#MLob+#bf~SVTR|W74fCe{`wf;A zXb!n`lY38-b z+Y5+uCZn%w5WM;D`S$(*G05FS&zyd)7Sv}3@$Ly(2G>58aFxl=pko%9XX(j2l1cHo z?^&)nl;kLQ@1>#+=-K)4+opX#5Q-ME46w5kK8 zT}-*eux}Ju+VTqBk!eS6-D)>(_4K07w-urub$#gNkC;Et2Ah%Lz2x_IZq1yB&htkB~W+;{3~Cx+(B3&e(s5@PM5k{JQLZe+g)vaXvLXLW8%n#hF+Ld<9q& z7UgPpqH_bY+84LYp#C9g8@9qV(ahg$P{EGv%I96ZpmOis;D5ixpy4m?;r8iS z@ZX-+YdOybz=hhd?d*9Zw{xl`ag6W-nUaL8Ly0M@5G`2!KJ|leUeD1=bc~}H`{z=r zC3?|2zZAcj?R}_Jh$y7_mRxgR zmJ5>Z=ABY$F(`8Qo_our2rRO^?tR9Q2Y%r^_v^j4GQ662_S@^fJ-~gH=b+I&((|+o z__dw%!;NjXo=Ja{giBm?dv_mWgkM!;lBM46fEA~W-Y%7`qpurdH<59~cD+uhK<%Nsu#~w(y2*LSumlVcODXif9l*6u21iO!X z`n5^8DZOQtJ2LNc!t^Nn+;^wQo-5vOUk~vG_P>uX54g&WA3gJG6eKx-$B_>YyVx}% zNS{7@XqwoC;OggsVj3859*V6KPQRf}#J8z2L8x(Q(F^Glp5TEi%TYyTV9V`$l>DX^ z%yf1z;Pj${5r?PFe`ghh91Y0>25=2sD(XK;lb+3FE1%-wquf3~|s>s(@b4?fy&p%<#?AB_=E4X<3WREL6x_1`780{3hAk4G5_x zHNE1;(gqs2i+pr=>bm0b-iRGo;zi%>3vUUJS0s@ #HPps~JFUADrQ-|KS=q;gE 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

    h6Io85 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

    b>vq4M;7uN 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.999) + measured: correlation diff=+0.006 (perm p=0.884) + measured: Amari diff=+0.005 (perm p=0.051) +------------------------------------------------------------------------------ +Row: Multi | Ensemble log-likelihood: Fortran; pamica + paper: -3.3539; -3.3629 (KS p=6e-5) + measured: Fortran=-3.3543 (sd 0.002); pamica=-3.3541 (sd 0.003); KS p=8.32e-01 (n=20/20 Fortran, 20/20 pamica runs ok) +------------------------------------------------------------------------------ +bundled tier wall-clock: 2842s + +wrote .context/issue-351/raw/table1_bundled/report.json diff --git a/.context/issue-351/raw/table1_external/report.json b/.context/issue-351/raw/table1_external/report.json new file mode 100644 index 00000000..3520b5e0 --- /dev/null +++ b/.context/issue-351/raw/table1_external/report.json @@ -0,0 +1,28 @@ +{ + "external": { + "tier": "external", + "n_channels": 70, + "n_frames": 747750, + "k": 152.60204081632654, + "wall_clock_s": 26185.370152069023, + "single_model": { + "n_channels": 70, + "n_seeds_requested": 5, + "n_seeds_ok": 5, + "n_fortran_ok": 5, + "ng_vs_fortran_corr_mean": 0.9996495858496243, + "ng_vs_fortran_corr_min": 0.9602403962666957, + "ng_vs_fortran_corr_sd": 0.000479692302794331, + "ng_vs_fortran_amari_mean": 0.001258754660040508, + "ng_vs_fortran_amari_sd": 0.0006199672303702011, + "fortran_vs_fortran_corr_mean": 0.998267073292794, + "fortran_vs_fortran_corr_min": 0.9167018463145121, + "fortran_vs_fortran_amari_mean": 0.002489315961253494, + "fortran_ll_mean": -3.699346392049425, + "fortran_ll_sd": 6.525058589552108e-05, + "ng_ll_mean": -3.6993407627685135, + "ng_ll_sd": 4.121894736588951e-05, + "ll_gap_of_means": 5.629280911634282e-06 + } + } +} \ No newline at end of file diff --git a/.context/issue-351/raw/table1_external/stdout.log b/.context/issue-351/raw/table1_external/stdout.log new file mode 100644 index 00000000..47746fce --- /dev/null +++ b/.context/issue-351/raw/table1_external/stdout.log @@ -0,0 +1,42 @@ +reproduce_table1: tier=external, threads=16, out_dir=/home/yahya/epic324-runs/external +device: CUDA (NVIDIA GeForce RTX 4090) +Fortran reference binary: /home/yahya/.cache/pamica/bin/v0.3.3/amica15-linux-x64 (native/v0.3.3) + +### external tier ### +data: benchmarks/data/ds002718_sub-002_eeg70_full.npy (70ch x 747750fr, k=152.6) +[external] seed 201: Fortran (16 threads, do_newton=0)... +[external] seed 201: Fortran done in 2246s, LL=-3.6993 +[external] seed 201: pamica (device=cuda, do_newton=0)... +[external] seed 201: pamica done in 1941s, LL=-3.6993, mean_corr=0.9999, min_corr=0.9996, amari=0.0008 +[external] seed 202: Fortran (16 threads, do_newton=0)... +[external] seed 202: Fortran done in 2559s, LL=-3.6994 +[external] seed 202: pamica (device=cuda, do_newton=0)... +[external] seed 202: pamica done in 5013s, LL=-3.6993, mean_corr=0.9987, min_corr=0.9602, amari=0.0024 +[external] seed 203: Fortran (16 threads, do_newton=0)... +[external] seed 203: Fortran done in 2363s, LL=-3.6993 +[external] seed 203: pamica (device=cuda, do_newton=0)... +[external] seed 203: pamica done in 4320s, LL=-3.6993, mean_corr=0.9999, min_corr=0.9985, amari=0.0012 +[external] seed 204: Fortran (16 threads, do_newton=0)... +[external] seed 204: Fortran done in 1972s, LL=-3.6993 +[external] seed 204: pamica (device=cuda, do_newton=0)... +[external] seed 204: pamica done in 1893s, LL=-3.6993, mean_corr=1.0000, min_corr=0.9998, amari=0.0007 +[external] seed 205: Fortran (16 threads, do_newton=0)... +[external] seed 205: Fortran done in 1992s, LL=-3.6995 +[external] seed 205: pamica (device=cuda, do_newton=0)... +[external] seed 205: pamica done in 1886s, LL=-3.6994, mean_corr=0.9998, min_corr=0.9955, amari=0.0012 +============================================================================== +EXTERNAL tier: 70ch x 747750fr, k=152.6 +============================================================================== +------------------------------------------------------------------------------ +Row: Single | Log-likelihood gap (on002718) + paper: within ~0.0005 of -3.6993 + measured: Fortran mean=-3.6993 (sd 0.0001); pamica mean=-3.6993 (sd 0.0000); gap=0.0000 (n=5/5 seeds ok) +------------------------------------------------------------------------------ +Row: Single | Component correlation (on002718) + paper: 0.998 + measured: pamica vs Fortran: mean=0.9996 (min=0.9602, sd 0.0005) + measured: Fortran vs Fortran self-consistency: mean=0.9983 (min=0.9167) +------------------------------------------------------------------------------ +external tier wall-clock: 26185s + +wrote /home/yahya/epic324-runs/external/report.json diff --git a/.context/issue-351/run_newton_hallu.sh b/.context/issue-351/run_newton_hallu.sh new file mode 100644 index 00000000..fbc3ddfa --- /dev/null +++ b/.context/issue-351/run_newton_hallu.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Driver for newton_seeds.py on the CUDA workstation (issue #351 item 4a). +# Two queues run side by side: the pamica fits on the GPU, the reference fits +# on FORTRAN_THREADS CPU threads. Launch detached: +# nohup bash .context/issue-351/run_newton_hallu.sh OUT_DIR > OUT_DIR.log 2>&1 < /dev/null & +set -u +OUT=${1:?usage: run_newton_hallu.sh OUT_DIR} +DATA=${DATA:-benchmarks/data/ds002718_sub-002_eeg70_full.npy} +FORTRAN_THREADS=${FORTRAN_THREADS:-10} +UV=${UV:-uv} +SCRIPT=.context/issue-351/newton_seeds.py +mkdir -p "$OUT" + +gpu_queue() { + for s in 42 7 13; do + [ -f "$OUT/pamica_seed$s.npz" ] || "$UV" run python "$SCRIPT" pamica --seed "$s" --data "$DATA" --out-dir "$OUT" + done + [ -f "$OUT/pamica_fixinit.npz" ] || "$UV" run python "$SCRIPT" pamica --fixinit --data "$DATA" --out-dir "$OUT" +} + +cpu_queue() { + for s in 1 2; do + [ -f "$OUT/fortran_seed$s.npz" ] || "$UV" run python "$SCRIPT" fortran --seed "$s" --threads "$FORTRAN_THREADS" --data "$DATA" --out-dir "$OUT" + done + [ -f "$OUT/fortran_fixinit.npz" ] || "$UV" run python "$SCRIPT" fortran --fixinit --threads "$FORTRAN_THREADS" --data "$DATA" --out-dir "$OUT" +} + +date +gpu_queue > "$OUT/gpu_queue.log" 2>&1 & +cpu_queue > "$OUT/cpu_queue.log" 2>&1 & +wait +date +"$UV" run python "$SCRIPT" compare --out-dir "$OUT" +echo DONE diff --git a/.context/issue-351/share_comps_counts.py b/.context/issue-351/share_comps_counts.py new file mode 100644 index 00000000..3f624245 --- /dev/null +++ b/.context/issue-351/share_comps_counts.py @@ -0,0 +1,229 @@ +"""The ``share_comps`` figures of ``docs/guides/amica-differences.md`` +("Component sharing compares and ties components"), re-measured under epic +#324 (issue #351 item 6). Protocols as the page and the Phase 8 run records +(PR 342) describe them, on the bundled sample EEG: + +a. end to end: 2 models, seed 42, Newton on, 300 iterations, ``share_start`` + 100, ``share_iter`` 100, ``comp_thresh`` 0.95, PyTorch defaults otherwise; + every merge with its iteration and the |cos| of the two scalp maps, and the + final log-likelihood with sharing on and off; +b. early scans: 2 models, seed 42, PyTorch defaults, one scan at iteration 8 + or 20 (``share_iter`` 100), fit to ``share_start + 2``; merges per threshold; +c. the reference's similarity on its own early state: the binary runs 8 + iterations from pamica's seed-42 initialization (the test suite's ``_OPT`` + optimizer settings), and the reference's formula (``Spinv2 = Spinv^T Spinv``, + i.e. the scalp maps ``pinv(S) @ A``) on that state is compared with + PyTorch's and NumPy's scans of their own 8-iteration states; +d. the collapse recipe of ``test_component_rows.py`` (4096 samples, seed 23, + ``share_start`` = ``share_iter`` = 11, ``comp_thresh`` 0.9): merges at the + first scan, the second model's ``gm`` over the next iterations, and the + iteration where the fit goes non-finite. + + uv run python .context/issue-351/share_comps_counts.py OUT.json +""" + +from __future__ import annotations + +import dataclasses +import json +import sys +import tempfile +from pathlib import Path + +import numpy as np +import torch + +from _reference_settings import REFERENCE_SETTINGS +from pamica import AMICA_NumPy +from pamica.numpy_impl.utils import identify_shared_components +from pamica.tests.native_oracle import run_seeded_reference, seed_from_torch +from pamica.tests.test_component_rows import _COLLAPSE, _OPT, _REF_OPT, DATA_FILE +from pamica.torch_impl.core import AMICATorchNG +from pamica.torch_impl.utils import load_eeglab_data + +NW, FIELD = 32, 30504 +X = load_eeglab_data(str(DATA_FILE), data_dim=NW, field_dim=FIELD).astype(np.float64) + + +def logged(base): + """``base`` with every merge recorded: (1-based iteration, model, source, + merged-into model, source, |cos| of the two scalp maps before the scan).""" + + class Logged(base): + merge_log: list + + def _identify_shared_comps(self): + before = self.comp_list.cpu().numpy().copy() + maps = [self.get_sensor_mixing_matrix(h) for h in range(self.n_models)] + maps = [q / np.linalg.norm(q, axis=0) for q in maps] + super()._identify_shared_comps() + after = self.comp_list.cpu().numpy() + for hh in range(self.n_models): + for ii in range(self.n_channels): + if after[ii, hh] != before[ii, hh]: + tgt = after[ii, hh] + h, i = next( + (h, i) + for h in range(self.n_models) + for i in range(self.n_channels) + if before[i, h] == tgt + ) + cos = float(abs(maps[h][:, i] @ maps[hh][:, ii])) + self.merge_log.append((self.iteration + 1, h, i, hh, ii, cos)) + + return Logged + + +def part_a() -> dict: + kw = dict(n_channels=NW, n_models=2, n_mix=3, device="cpu", seed=42, do_newton=True) + share = dict(share_comps=True, share_start=100, share_iter=100, comp_thresh=0.95) + off = AMICATorchNG(**kw).fit(X, max_iter=300, verbose=False) + m = logged(AMICATorchNG)(**kw, **share) + m.merge_log = [] + m.fit(X, max_iter=300, verbose=False) + groups = [] + for g in m.shared_components(): + v = [m.get_mixing_matrix(h)[:, i] for h, i in g] + groups.append( + {"group": g, "identical": all(np.array_equal(v[0], x) for x in v[1:])} + ) + return { + "share_off_final_ll": float(off.final_ll_), + "share_off_stop": str(off.stop_reason), + "share_off_newton_fallbacks": int(off.n_newton_fallbacks), + "share_on_final_ll": float(m.final_ll_), + "share_on_stop": str(m.stop_reason), + "share_on_newton_fallbacks": int(m.n_newton_fallbacks), + "merges": [ + { + "iter": r[0], + "model": r[1], + "source": r[2], + "into_model": r[3], + "into_source": r[4], + "map_abs_cos": r[5], + } + for r in m.merge_log + ], # fmt: skip + "unique_components": int(m.comp_used.sum()), + "groups": groups, + } + + +def part_b() -> dict: + out = {} + for start in (8, 20): + for thr in (0.9, 0.95, 0.99): + m = AMICATorchNG( + NW, n_models=2, seed=42, share_comps=True, share_start=start, + share_iter=100, comp_thresh=thr, device="cpu", + ) # fmt: skip + m.fit(X, max_iter=start + 2, verbose=False) + merged = 2 * NW - len(np.unique(m.comp_list.cpu().numpy())) + out[f"scan_at_{start}_thr_{thr}"] = { + "merged": int(merged), + "final_ll": float(m.final_ll_), + "stop": str(m.stop_reason), + } + return out + + +def part_c(work: Path) -> dict: + pre = 8 + + def torch_model(**kw): + return AMICATorchNG( + n_channels=NW, n_models=2, n_mix=3, seed=42, device="cpu", + dtype=torch.float64, keep_best=False, **{**_OPT, **kw}, + ) # fmt: skip + + init = torch_model() + init._preprocess(X) + init._initialize_parameters() + default = init.comp_list.numpy().copy() + seed = dataclasses.replace(seed_from_torch(init), comp_list=default) + ref8 = run_seeded_reference( + seed, DATA_FILE, work / "c", n_samples=FIELD, max_iter=pre, num_models=2, + **{ + **REFERENCE_SETTINGS, **_REF_OPT, + "share_start": pre, "share_iter": 100, "share_comps": 0, + }, + ) # fmt: skip + t8 = torch_model(share_comps=False) + t8.fit(X, max_iter=pre, verbose=False) + with tempfile.TemporaryDirectory() as td: + n8 = AMICA_NumPy( + num_models=2, num_mix=3, max_iter=pre, seed=42, use_tqdm=False, + outdir=str(td), do_opt_block=False, writestep=10**6, **_OPT, + ) # fmt: skip + n8.fit(X) + ref_maps = np.linalg.pinv(ref8.S) @ ref8.A + out = { + "state_A_diff_torch_vs_ref": float(np.abs(t8.A.numpy().T - ref8.A).max()), + "state_LL_diff_torch_vs_ref": float( + np.abs(np.asarray(t8.ll_history) - ref8.LL).max() + ), + } + for thr in (0.9, 0.95, 0.99): + res = {} + lists = {} + for name, maps in ( + ("reference_state", ref_maps), + ("torch_state", t8._component_sensor_maps()), + ("numpy_state", n8._component_sensor_maps()), + ): + new, _ = identify_shared_components(maps, default.copy(), thr) + lists[name] = new + res[name] = int(2 * NW - len(np.unique(new))) + ts = torch_model( + share_comps=True, share_start=pre, share_iter=100, comp_thresh=thr + ) + ts.fit(X, max_iter=pre, verbose=False) + res["torch_fit_scan"] = int(2 * NW - len(np.unique(ts.comp_list.numpy()))) + res["same_comp_list_ref_torch_numpy_fit"] = [ + bool(np.array_equal(lists["reference_state"], lists["torch_state"])), + bool(np.array_equal(lists["reference_state"], lists["numpy_state"])), + bool(np.array_equal(lists["reference_state"], ts.comp_list.numpy())), + ] + out[f"thr_{thr}"] = res + return out + + +def part_d() -> dict: + Xs = X[:, :4096] + out: dict = {} + first = AMICATorchNG(**_COLLAPSE) + first.fit(Xs, max_iter=_COLLAPSE["share_start"], verbose=False) + out["first_scan_merges"] = int(2 * NW - len(np.unique(first.comp_list.numpy()))) + out["gm2_after_first_scan"] = float(first.gm.numpy()[1]) + for k in (1, 2): + m = AMICATorchNG(**_COLLAPSE) + m.fit(Xs, max_iter=_COLLAPSE["share_start"] + k, verbose=False) + out[f"gm2_{k}_after_scan"] = float(m.gm.numpy()[1]) + second = AMICATorchNG(**_COLLAPSE) + second.fit( + Xs, max_iter=_COLLAPSE["share_start"] + _COLLAPSE["share_iter"], verbose=False + ) + out["second_scan_stop"] = str(second.stop_reason) + out["merges_after_second_scan"] = int( + 2 * NW - len(np.unique(second.comp_list.numpy())) + ) + out["gm2_after_second_scan"] = float(second.gm.numpy()[1]) + long = AMICATorchNG(**_COLLAPSE) + long.fit(Xs, max_iter=40, verbose=False) + out["long_fit_stop"] = str(long.stop_reason) + out["long_fit_iterations_recorded"] = len(long.ll_history) + out["long_fit_last_iteration_index"] = int(long.iteration) + return out + + +def main() -> None: + out = Path(sys.argv[1]) + with tempfile.TemporaryDirectory() as td: + rep = {"a": part_a(), "b": part_b(), "c": part_c(Path(td)), "d": part_d()} + out.write_text(json.dumps(rep, indent=2)) + print(json.dumps(rep, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/.context/issue-51/best_iterate_safeguard.md b/.context/issue-51/best_iterate_safeguard.md index f2a3fef5..906b5998 100644 --- a/.context/issue-51/best_iterate_safeguard.md +++ b/.context/issue-51/best_iterate_safeguard.md @@ -1,5 +1,11 @@ # Multi-model NG log-likelihood: best-iterate safeguard (issue #51) +> **Historical record.** These figures predate epic #324, which changed every +> backend's default trajectory. The re-measured `keep_best` ensemble (seeded, +> against the pinned v0.3.3 native binary) is in `.context/issue-351/findings.md` +> (script `.context/issue-351/keep_best_ensemble.py`) and in ADR 0003's +> "Re-measured after epic #324" section. + **Bottom line.** The multi-model NG log-likelihood was ~0.02 lower and ~13x more variable than Fortran because `AMICATorchNG.fit` returned the *last* EM iterate under a deliberately non-monotone learning-rate schedule. Returning the *best* diff --git a/.context/progress_summary.md b/.context/progress_summary.md index 817e1d3e..440c8852 100644 --- a/.context/progress_summary.md +++ b/.context/progress_summary.md @@ -16,7 +16,8 @@ what remains, as of epic #324 (after v0.3.3). User-facing detail is in `docs/cha - Newton is ported from the NumPy reference and stays positive-definite (0 fallbacks on the sample data). - **Metrics:** single-model LL ~ -3.40 (Fortran -3.4018); Hungarian-matched component correlation - ~0.997, clearing the >0.95 gate. + ~0.997, clearing the >0.95 gate. Re-measured under epic #324 (#351) against the bundled + 200-iteration fixture: LL within 1.4e-4, correlation 0.998, Amari 4.8e-3, on all three backends. ### Adaptive PDF selection (issue #26) - All five `amica15.f90` `pdftype` density families ported to `AMICATorchNG`: 0 generalized @@ -28,18 +29,20 @@ what remains, as of epic #324 (after v0.3.3). User-facing detail is in `docs/cha ### Multi-model AMICA (issue #27) - Validated by **distributional equivalence**: multi-model AMICA is not partition-identifiable, so - the NG-vs-Fortran partition cross-correlation distribution is statistically indistinguishable - from Fortran's own run-to-run spread (Mann-Whitney p=0.97, TOST within +/-0.05). Per-block - sufficient stats are bit-exact vs Fortran. + the NG-vs-Fortran partition cross-correlation distribution is compared with Fortran's own + run-to-run spread. Re-measured under epic #324 (#351): between minus within-Fortran +0.006 + (run-level permutation p=0.88; Amari +0.005, p=0.051), final LL -3.3541 vs -3.3543 (Kolmogorov-Smirnov (KS) p=0.83). + Per-block sufficient stats agree with Fortran to round-off. - Per-model bias `c` update (`update_c`) ported to both backends, guarded to a no-op for `n_models=1` so single-model parity stays bit-exact. See `.context/issue-27/`. ### Best-iterate safeguard (issue #51) - `AMICATorchNG.fit` returns the highest-LL iterate (`keep_best`, default on; `final_ll_` reports the returned iterate's LL, `ll_history` keeps the true trajectory), not the last iterate under - the non-monotone lrate schedule. Cuts multi-model LL sd from 12.7x to 2.0x Fortran's at a - matched 100-iter budget. Single-model #24 parity stays bit-exact (monotone => no restore). See - ADR 0003. + the non-monotone lrate schedule. It cut multi-model LL sd from 12.7x to 2.0x Fortran's at a + matched 100-iter budget; re-measured under epic #324 (#351) the sd ratio is 1.0x with or without + it, and a restore fired in one of 20 seeded fits (300-iteration budget only). Single-model #24 parity stays bit-exact + (monotone => no restore). See ADR 0003. ### Degenerate-fit contract (issues #50, #306, #339) - The `AMICA` wrapper no longer treats a degenerate fit (`stop_reason` nan_ll / singular_ll / @@ -88,8 +91,10 @@ what remains, as of epic #324 (after v0.3.3). User-facing detail is in `docs/cha - Validation harness (`validate_implementations.py --backend {torch,numpy,mlx}`, a comma-separated list, or `all`; default `torch`, whose report is unchanged) runs each backend against one Fortran reference run with the same settings and matches components via the Hungarian algorithm, on real - sample EEG (#315). All three meet the Fortran bar on the bundled sample (LL within 3.2e-5, - correlation 0.9992, Amari 0.004; rows and bars in `docs/guides/validation.md`), pinned by the + sample EEG (#315). All three meet the Fortran bar on the bundled sample (re-measured under epic + #324 in #351: LL within 2.8e-4, correlation 0.9991, Amari 0.004 from independent starts, inside the + reference's own seed-to-seed LL sd of 2.6e-4; from a shared start LL within 1.6e-6; rows and bars + in `docs/guides/validation.md`, run records in `.context/issue-351/`), pinned by the `AMICA_RUN_FORTRAN`-gated test in `test_fortran_param_forwarding.py`. ## Remaining diff --git a/AGENTS.md b/AGENTS.md index be7c1577..864f635b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -113,8 +113,9 @@ remaining MLX gap vs the PyTorch backend is float32-only precision (Apple GPUs h ## Current Status - PyTorch backend with GPU/MPS/CPU support; the `AMICATorchNG` natural-gradient EM backend now - matches the Fortran reference (LL ~ -3.40, component correlation ~0.997) with Newton enabled - and positive-definite (issue #24). + matches the Fortran reference with Newton enabled and positive-definite (issue #24): against the + bundled 200-iteration `amicaout` fixture, LL within 1.4e-4 and component correlation 0.998 on + all three backends (re-measured under epic #324 in #351; ~0.997 when #24 closed). - `AMICA(backend="mlx")` and `AMICAICA(backend="mlx")` (epic #324 Phase 4, #313) run the MLX backend end to end: fit, `from_params_file`, `pcakeep`, the #50 degenerate-fit contract, `.pt` save/load (wrapper `format_version` 2 records the backend; version 1 still loads as torch), EEGLAB export and MNE `apply`. @@ -122,9 +123,10 @@ remaining MLX gap vs the PyTorch backend is float32-only precision (Apple GPUs h - `validate_implementations.py --backend {torch,numpy,mlx}` (a comma-separated list, or `all`; default `torch`, whose report is unchanged) runs each backend against one Fortran reference run with the same settings and matches components via the Hungarian algorithm (#315). All three meet the Fortran bar on - the bundled sample (LL within 3.2e-5, correlation 0.9992, Amari 0.004; rows and bars in - `docs/guides/validation.md`), pinned by the `AMICA_RUN_FORTRAN`-gated test in - `test_fortran_param_forwarding.py`. + the bundled sample (re-measured under epic #324 in #351: LL within 2.8e-4, correlation 0.9991, Amari + 0.004 from independent starts, where the reference's own seed-to-seed LL sd is 2.6e-4; from a shared + start LL within 1.6e-6, correlation 0.99999993; rows and bars in `docs/guides/validation.md`), pinned + by the `AMICA_RUN_FORTRAN`-gated test in `test_fortran_param_forwarding.py`. - Epic #324 aligned every backend's default fit with the reference, so default trajectories differ from 0.3.3 (changelog warning): the reference's per-iteration order, with the exit before the update and the A-freeze on every fit (ADR 0008), `doscaling` of component rows (ADR 0006), component-row @@ -139,7 +141,8 @@ remaining MLX gap vs the PyTorch backend is float32-only precision (Apple GPUs h **Single-model parity: DONE (#24).** The natural-gradient A-update transpose fix (plus exact-EM mixture updates, digamma rho update, symmetric-ZCA sphere, Jacobian LL) brought both `AMICATorchNG` and the legacy NumPy `numpy_impl/core.py` to Fortran's solution (LL ~ -3.40, Hungarian-matched component -correlation ~0.997, > 0.95 gate cleared; root cause in `.context/issue-24/`). Also resolved: Newton +correlation ~0.997 at the time and 0.998 re-measured under epic #324, > 0.95 gate cleared; root cause +in `.context/issue-24/`). Also resolved: Newton stability (posdef, 0 fallbacks), backend consolidation (#32/#31), NumPy CLI save/load format (#30), NG save/load persistence (#36), and the degenerate-fit contract (#50: the `AMICA` wrapper marks a degenerate fit unusable via `converged_`/`stop_reason_` and refuses `transform`/`get_*`/`save`, @@ -178,12 +181,14 @@ update from a merged state seeded through `load_comp_list` matches it to float64 - **Multi-model (#27): VALIDATED by distributional equivalence.** Multi-model AMICA is not partition-identifiable, so exact partition parity with Fortran is the wrong acceptance bar (the `>0.95` cross-corr in #27's title asks the algorithm to be more identifiable than it is). The right - test is whether the two implementations sample the same distribution over solutions. On an - N=20-each ensemble (real sample EEG, `n_models=2`), the NG-vs-Fortran partition cross-corr - distribution is **statistically equivalent to Fortran's own run-to-run distribution** (Mann-Whitney - p=0.97, TOST equivalent within ±0.05; within-Fortran/within-NG/between all ~0.63-0.64). The - single-run ~0.64 cross-corr is intrinsic estimator spread, not a defect -- Fortran agrees with - *itself* at 0.63. See `.context/issue-27/multimodel_distributional_equivalence.md` (+ figure). + test is whether the two implementations sample the same distribution over solutions. Re-measured + under epic #324 (#351, N=20 each, real sample EEG, `n_models=2`, pinned v0.3.3 binary): mean + pairwise cross-corr within-Fortran 0.626, within-pamica 0.638, between 0.632; between minus + within-Fortran +0.006 (inside the ±0.05 margin; run-level permutation p=0.88; by Amari distance + +0.005, p=0.051, pamica's ensemble spreading slightly more than Fortran's); final LL -3.3541 vs + -3.3543 (Kolmogorov-Smirnov (KS) p=0.83). The single-run ~0.63 cross-corr matches Fortran's agreement with itself. + See `docs/guides/validation.md`, `.context/issue-351/` and, for the pre-epic record, + `.context/issue-27/multimodel_distributional_equivalence.md`. Supporting: per-block sufficient stats are bit-exact vs Fortran; the per-model bias `c` update (Fortran `update_c`: `c[i,h] = sum_t v_h*x / sum_t v_h`) is ported to both backends, guarded to a no-op for `n_models=1` (single-model parity stays bit-exact), see @@ -193,11 +198,15 @@ update from a merged state seeded through `load_comp_list` matches it to float64 variance was driven by late Newton-fallback overshoots (one seed peaked at -3.357 then crashed to -3.545 in its final iterations). `AMICATorchNG.fit` now returns the highest-LL iterate (`keep_best`, default on; `final_ll_` reports the returned iterate's LL, `ll_history` stays the true trajectory). - At matched 100-iter budget this cuts the LL sd from 12.7x to 2.0x Fortran's; the residual ~0.009 - mean gap is convergence speed, not a worse optimum (at 200 iters NG reaches Fortran's exact mean - -3.3541, at 300 it exceeds it -- the M-step is bit-exact vs Fortran). Single-model #24 parity stays - byte-for-byte (monotone => no restore). Inactive under `do_reject`. See ADR 0003 and - `.context/issue-51/`. + At a matched 100-iter budget it cut the LL sd from 12.7x to 2.0x Fortran's (the residual ~0.009 mean + gap was then read as convergence speed). Re-measured under epic #324 (#351, seeded 20-run ensemble + against the pinned binary): the late overshoots are gone (largest LL dip 1.2e-5 in 20 trajectories, + no Newton fallbacks), the sd ratio is 1.0x at 100 iterations with or without `keep_best`, a restore + fired in one of 20 seeded fits, at the 300-iteration budget only (gain 4.2e-6), and the mean gap is + +8.1e-4 at 100 iterations, +2.1e-4 at 200 and +1.0e-4 at 300. The pre-epic code on the same seeds + reproduces the old gap; 7 of its 20 fits stopped early on a `min_dll` check that counted LL dips as + small gains (fixed in #339), and its full-length fits trail by 0.006. `keep_best` stays on. Single-model #24 parity stays byte-for-byte (monotone => no + restore). Inactive under `do_reject`. See ADR 0003 and `.context/issue-351/`. ## Development Workflow 1. **Check context:** `.context/plan.md` for current tasks and priorities. diff --git a/README.md b/README.md index a722e54a..bb525b1d 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,9 @@ EEG/EMG blind source separation and is a drop-in replacement for EEGLAB's AMICA: single-model output is written in exactly the reference's on-disk format and loads directly in EEGLAB. -Single-model results match the Fortran reference (Hungarian-matched component correlation ~ 0.998 -on well-determined data, Newton disabled); see the -[documentation](https://eeglab.org/pAMICA/) for validation details and the -backend-selection guide. +Single-model results match the Fortran reference (Hungarian-matched component correlation 0.9996 +on well-determined data, Newton disabled, with the reference agreeing with itself at 0.998); see the +[documentation](https://eeglab.org/pAMICA/) for validation details and the backend-selection guide. ## Overview diff --git a/benchmarks/README_dimsweep.md b/benchmarks/README_dimsweep.md index 2ef1d66d..b5445777 100644 --- a/benchmarks/README_dimsweep.md +++ b/benchmarks/README_dimsweep.md @@ -95,15 +95,18 @@ sample. Read this section before running the external tier -- it is not a quick **Bundled tier** (`--tier bundled`, default): uses `pamica/sample_data/eeglab_data.fdt` (32 channels, 30,504 frames, already committed -- no download). Default budget is 5 single-model seeds at 2000 iterations plus a 20-run multi-model ensemble at 100 iterations (matching -`docs/guides/validation.md`'s documented protocol). Measured end to end on a 2026 Apple Silicon -laptop (10 cores, no CUDA, MPS unusable here because these runs are float64 -- see below): +`docs/guides/validation.md`'s documented protocol). Measured end to end on two Apple Silicon hosts +(no CUDA; MPS unusable here because these runs are float64 -- see below): -| phase | measured | -|---|---| -| single-model sweep (5 seeds x 2000 iter, Fortran + pamica) | ~24 min | -| multi-model ensemble (20 runs x 100 iter, Fortran + pamica) | ~9 min | -| score-function / sufficient-statistics check | <1 s | -| **total** | **~33 min** | +| phase | 2026 laptop, 10 cores | Apple M4 Pro, 14 cores, 2026-09-23 (issue #351) | +|---|---|---| +| single-model sweep (5 seeds x 2000 iter, Fortran + pamica) | ~24 min | ~32 min | +| multi-model ensemble (20 runs x 100 iter, Fortran + pamica) | ~9 min | ~15 min | +| score-function / sufficient-statistics check | <1 s | <1 s | +| **total** | **~33 min** | **~47 min** | + +The M4 Pro run (`--threads 4`, `OMP_NUM_THREADS=4`, the code of epic #324) shared the machine with other +benchmark jobs (load average 10-24), so its times are an upper bound for that host. A `--n-seeds 2 --max-iter 100 --multimodel-runs 2 --multimodel-max-iter 20` smoke test (not the paper's protocol -- noisier numbers, just a pipeline check) completes in well under a minute. @@ -145,6 +148,12 @@ Single-fit wall-clock at this exact configuration (70ch, 747,750 frames, 2000 it | native-fortran-f64 | 1303 s (~22 min) | | pamica, CUDA float64 | 1856 s (~31 min) | +The full external tier was re-run on 2026-09-23 with the code of epic #324 (issue #351) on the same +kind of host (32 cores, RTX 4090), shared with other users' jobs, with the v0.3.3 reference at +`--threads 16`: the Fortran fits took 1972-2559 s each, the pamica CUDA float64 fits 1886-5013 s +(the slowest while the GPU was shared), and the whole tier, run sequentially, 26185 s (~7.3 hours). +On an unshared host expect the pipelined estimates below. + Scaling those to the paper's 5-seed protocol (Fortran phases can run back to back while each seed's GPU phase overlaps the next seed's Fortran phase, the way the original workstation script pipelined them): diff --git a/benchmarks/reproduce_table1.py b/benchmarks/reproduce_table1.py index 9d1ffa83..cef2ae91 100644 --- a/benchmarks/reproduce_table1.py +++ b/benchmarks/reproduce_table1.py @@ -72,7 +72,7 @@ import sys import time from pathlib import Path -from typing import Callable +from typing import Any, Callable import numpy as np import torch @@ -106,6 +106,55 @@ MULTIMODEL_SEED_START = 1 +# The reference's settings for every Table 1 run: the values of the bundled +# pamica/sample_data/input.param, which AMICANative's defaults mirrored when +# the table was measured, and do_approx_sphere 1, the binary's compiled value +# for a key that file leaves unset (AMICANative writes it since issue #354). +# Spelled out, so that a change of AMICANative's own defaults cannot change +# this protocol; each call adds its model count, iteration budget and the +# settings it overrides. +REFERENCE_SETTINGS: dict[str, Any] = { + "block_size": 512, "do_opt_block": 0, "blk_min": 256, "blk_step": 256, + "blk_max": 1024, "use_min_dll": 1, "min_dll": 1e-09, "use_grad_norm": 1, + "min_grad_norm": 1e-07, "pdftype": 0, "share_comps": 0, "share_start": 100, + "comp_thresh": 0.99, "share_iter": 100, "lrate": 0.05, "minlrate": 1e-08, + "mineig": 1e-12, "lratefact": 0.5, "rholrate": 0.05, "rho0": 1.5, + "minrho": 1.0, "maxrho": 2.0, "rholratefact": 0.5, "kurt_start": 3, + "num_kurt": 5, "kurt_int": 1, "do_newton": 1, "newt_start": 50, + "newt_ramp": 10, "newtrate": 1.0, "do_reject": 0, "numrej": 3, + "rejsig": 3.0, "rejstart": 2, "rejint": 3, "decwindow": 1, "max_decs": 3, + "fix_init": 0, "update_A": 1, "update_c": 1, "update_gm": 1, + "update_alpha": 1, "update_mu": 1, "update_beta": 1, "invsigmax": 100.0, + "invsigmin": 0.0, "do_rho": 1, "do_mean": 1, "do_sphere": 1, + "do_approx_sphere": 1, "doPCA": 1, "pcadb": 30.0, "byte_size": 4, + "doscaling": 1, "scalestep": 1, +} # fmt: skip + + +def single_model_reference_kwargs(max_iter: int) -> dict[str, Any]: + """``AMICANative`` settings of the single-model sweep: Newton and the + reference's early stops off, so both sides run the full budget.""" + return { + **REFERENCE_SETTINGS, + "num_models": 1, + "num_mix_comps": 3, + "max_iter": max_iter, + "do_newton": 0, + "use_min_dll": 0, + "use_grad_norm": 0, + } + + +def multimodel_reference_kwargs(max_iter: int) -> dict[str, Any]: + """``AMICANative`` settings of the multi-model ensemble (the #27 protocol).""" + return { + **REFERENCE_SETTINGS, + "num_models": 2, + "num_mix_comps": 3, + "max_iter": max_iter, + } + + # --------------------------------------------------------------------------- # Parity metrics (Amari, Cichocki & Yang 1996; Hungarian-matched correlation), # reimplemented here rather than imported from validate_implementations.py -- @@ -334,13 +383,7 @@ def run_single_model_sweep( threads=threads, max_threads=threads, timeout=3600, - n_models=1, - n_mix=3, - max_iter=max_iter, - do_newton=0, - use_min_dll=0, - use_grad_norm=0, - block_size=512, + **single_model_reference_kwargs(max_iter), ) eng.fit(data) except (RuntimeError, TimeoutError) as exc: @@ -481,9 +524,7 @@ def run_multimodel_ensemble( threads=threads, max_threads=threads, timeout=1800, - n_models=2, - n_mix=3, - max_iter=max_iter, + **multimodel_reference_kwargs(max_iter), ) eng.fit(data) except RuntimeError as exc: @@ -499,6 +540,9 @@ def run_multimodel_ensemble( model.fit( data, max_iter=max_iter, + # The #27 protocol's rate (input.param), pinned so the ensemble does + # not follow a change of the wrapper's default. + lrate=0.05, do_newton=True, seed=MULTIMODEL_SEED_START + i, block_size=512, diff --git a/docs/assets/figures/multimodel-ensemble.png b/docs/assets/figures/multimodel-ensemble.png index d309965be1d74de6b4c14894478379373a1db38c..7527aa45efecf8c3fe2ee6bba8500d5a5e871375 100644 GIT binary patch literal 159225 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 %O6 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

    91>u719W9|Bmvb!g^En-<}(e?4ZZB>JsS?-_5 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

    MTZjD^=nktfcuN>0d5ME~wnn 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

  • Ys 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 literal 147783 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 diff --git a/docs/changelog.md b/docs/changelog.md index aba5f488..f465d77e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -633,6 +633,15 @@ and every backend's fitting follows the Fortran reference more closely. ### Documentation +- **Parity figures re-measured with the finished epic** (issue #351, epic #324 Phase 15). + The validation guide, the differences guide, ADR 0003 and the paper quote measurements of this release's code against the pinned v0.3.3 native binary; + the run records are in `.context/issue-351/`. + - The harness's independent-start log-likelihood difference is now 2.7e-4 (it was 2.9e-5), + within the reference's own seed-to-seed spread at 100 iterations (standard deviation 2.6e-4 over eight seeds); + from a shared start the difference is 1.6e-6 (2.4e-4 with the code before the epic). + - The multi-model ensemble's log-likelihood matches the reference's (-3.3541 against -3.3543, Kolmogorov-Smirnov p = 0.83), where it trailed by 0.009; + `keep_best` restored one of 20 seeded fits, at the 300-iteration budget only. + - The `share_comps` example fit merges one pair (it merged three), and the early scans merge 30 and 17 components at iteration 8 (they merged 32 and 24). - **The documentation describes the finished epic** (issue #352, epic #324 Phase 16). The concept pages walk one iteration in the reference's order (E-step, the likelihood-decrease response and the stopping checks, the exit before any update, then the update, diff --git a/docs/guides/amica-differences.md b/docs/guides/amica-differences.md index c894403d..ff76e084 100644 --- a/docs/guides/amica-differences.md +++ b/docs/guides/amica-differences.md @@ -14,7 +14,7 @@ that is not listed, that is a bug worth |---|---|---|---|---|---| | 1 | Rank threshold | absolute floor `mineig=1e-15` | relative floor `mineig_rel=1e-12` | the absolute floor is unit-dependent: MEG in Tesla yields rank 0, and average-referenced EEG is detected by luck | `mineig_rel=None` | | 2 | Zero numerical rank | `numeigs = 0`, continues | `ValueError` naming cause and fix | fitting a zero-dimensional model is not a recoverable state | none (no reason to want it) | -| 3 | Returned iterate | last EM iterate | highest-likelihood iterate (`keep_best`) | the lrate schedule is non-monotone; late Newton overshoots cut LL variance 12.7x → 2.0x | `keep_best=False` | +| 3 | Returned iterate | last EM iterate | highest-likelihood iterate (`keep_best`) | the lrate schedule is non-monotone, so a fit can end below its peak (before epic #324, late Newton overshoots made the multi-model LL sd 12.7x Fortran's, 2.0x with `keep_best`; with the epic's code a restore fired in one of 20 seeded fits, at the 300-iteration budget only; ADR 0003) | `keep_best=False` | | 4 | Newton | off in the compiled defaults (amica15_header.f90:19); the bundled `input.param` turns it on (`do_newton 1`) | off | the compiled default; Newton-off fits isolate the algorithm from initialization for parity work | `do_newton=True`, as the bundled `input.param` sets | | 5 | Degenerate fits | returns NaN sources, and writes them out on its `writestep` cadence | the `AMICA` wrapper, on either backend, refuses `transform`/`get_*`/`save`; the raw `AMICATorchNG`, `AMICAMLXNG` and NumPy `AMICA` backends refuse their own output accessors on a degenerate fit too (issue #306); NumPy additionally reports `converged=False` with a `stop_reason`, refuses the final write, and skips each periodic checkpoint with a logged reason (leaving the last valid one on disk). Every backend stops before a non-finite value is applied or returned: a non-finite likelihood (`nan_ll`, or `singular_ll` for an infinite one; never recorded in the history), update direction (`nan_direction`) or parameter after an update (`nan_params`), where the reference applies a NaN step and exits on the next likelihood | NaN sources silently poison downstream analysis, and `loadmodout` reads a NaN checkpoint back without complaint | none (see issues #50, #240, #306 and #339) | | 6 | Precision | float64 | float64 (float32 on Apple GPUs) | Apple GPUs have no float64; float32 agrees to ~7 significant digits, not bit-parity | `dtype=torch.float64` | @@ -577,19 +577,21 @@ and indexed its COLUMNS by component id, but a stored column is one sphered chan Seeded from a merged state through the reference's `load_comp_list`, the PyTorch and NumPy updates match the native binary to float64 round-off after one and three iterations, with `doscaling` on and off (`pamica/tests/test_component_rows.py`, opt-in with `AMICA_RUN_FORTRAN=1`). -On the bundled sample (2 models, 300 iterations, `share_start=100`, `comp_thresh=0.95`) -the scan merges three pairs whose maps agree (|cos| 0.956 to 0.971), -where the old metric merged three whose maps did not (|cos| 0.06, 0.35 and 0.55). +On the bundled sample (2 models, seed 42, Newton on, 300 iterations, `share_start=100`, `share_iter=100`, `comp_thresh=0.95`) +the scan at iteration 100 merges one pair whose maps agree (|cos| 0.970) and the scans at 200 and 300 merge nothing, +so the fit ends with 63 of 64 components at log-likelihood -3.3410 (-3.3393 with sharing off; measured with the finished epic #324, issue #351). +Right after this change, before the reference's iteration order (issue #339) and the later changes of epic #324, the same fit merged three such pairs (|cos| 0.956 to 0.971), +and the old metric three whose maps did not (|cos| 0.06, 0.35 and 0.55). Two consequences to know: - A scan early in a fit merges most components, and the reference's formula does the same. Both models start near the identity, so their components stay near-collinear for the first iterations - (on the sample with 2 models and seed 42, a scan at iteration 8 merges all 32 at `comp_thresh=0.95` and 24 at 0.99; - one at iteration 20 merges 24 and 5). + (on the sample with 2 models, seed 42 and PyTorch's defaults, a scan at iteration 8 merges 30 of 32 at `comp_thresh=0.95` and 17 at 0.99; + one at iteration 20 merges 22 and 5). The reference's own scan cannot show this (it never merges, above), but its similarity with `Spinv2 = Spinv^T Spinv`, applied to the binary's own state after 8 iterations from pamica's initialization, - merges exactly the pairs the PyTorch and NumPy scans merge: 32, 32 and 30 at `comp_thresh` 0.9, 0.95 and 0.99. + merges exactly the pairs the PyTorch and NumPy scans merge: 32, 32 and 29 at `comp_thresh` 0.9, 0.95 and 0.99. - A model left with few components of its own then loses its responsibility, and can end in a non-finite fit. In a short recipe (4096 samples, seed 23, `share_start=11`, `share_iter=11`, `comp_thresh=0.9`), 28 merges at iteration 11 dropped the second model's `gm` from 0.57 to 9.0e-4 within two iterations diff --git a/docs/guides/backends.md b/docs/guides/backends.md index ca292801..e1b9c099 100644 --- a/docs/guides/backends.md +++ b/docs/guides/backends.md @@ -159,7 +159,9 @@ Measured on real 70-channel EEG (see the project benchmarks and - On **CPU**, intra-op threads are workload-limited; around 4 threads was the sweet spot in the measured laptop sweep, with 8+ regressing. -All backends agree on the log-likelihood to ~3 significant digits on real data. +With the same settings and the same start, the backends' log-likelihoods after 25 iterations of the throughput sweep agree to 1e-5 at 32 and 48 channels; +at 70 channels on its 30000 frames ($k\approx6$) they differ by up to 1e-3, about the effect of perturbing one sample by 1e-9 µV +([details](validation.md#cross-backend-log-likelihood-agreement-single-model)). !!! note "Cross-backend equivalence and data adequacy" Whether two backends recover the *same* independent components depends on how diff --git a/docs/guides/validation.md b/docs/guides/validation.md index ac46302f..fb0853a4 100644 --- a/docs/guides/validation.md +++ b/docs/guides/validation.md @@ -20,12 +20,12 @@ Throughout, IC abbreviates independent component and LL log-likelihood. |---|---|---| | Source-density score and log-density (non-GG families) | vs the literal `amica15.f90` expressions | bit-exact ($<10^{-12}$) | | Per-block sufficient statistics and one M-step | vs Fortran | bit-exact ($\sim\!10^{-15}$) | -| Single-model solution (`do_newton=0`, $k\approx153$) | log-likelihood, component correlation vs Fortran | LL within ~0.0005 of $-3.6993$; correlation 0.998 | -| Single-model solution (`do_newton=0`, bundled, $k\approx30$) | Amari distance vs Fortran | 0.006 | -| Every backend against the reference (harness defaults, bundled) | `validate_implementations.py --backend all`: PyTorch, NumPy and MLX each vs Fortran | LL within 3.2e-5; correlation 0.9992; Amari distance 0.004, for all three (measured before issue #333's `doscaling` change; see [the per-backend rows](#parity-rows-per-backend)) | -| Multi-model solution | distributional similarity over 20-run ensembles | indistinguishable from Fortran's own run-to-run spread ($p = 0.96$) | +| Single-model solution (`do_newton=0`, $k\approx153$) | log-likelihood, component correlation vs Fortran | both at LL $-3.6993$ (gap of the means $6\times10^{-6}$); correlation 0.9996 (Fortran vs Fortran 0.998) | +| Single-model solution (`do_newton=0`, bundled, $k\approx30$) | Amari distance vs Fortran | 0.011 over the protocol's 5 run pairs, one of whose reference runs ended in another basin; 0.004 over 50 pairs (Fortran vs Fortran 0.005) | +| Every backend against the reference (harness defaults, bundled) | `validate_implementations.py --backend all`: PyTorch, NumPy and MLX each vs Fortran | from independent starts: LL within 2.8e-4 (the reference's own seed-to-seed standard deviation is 2.6e-4), correlation 0.9991, Amari distance 0.004, for all three; from a shared start: LL within 1.6e-6, correlation 0.99999993 ([per-backend rows](#parity-rows-per-backend)) | +| Multi-model solution | distributional similarity over 20-run ensembles | between-implementation correlation within 0.006 of Fortran's own run-to-run agreement (one-sided permutation $p = 0.88$; Amari distance $p = 0.051$); final log-likelihood $-3.3541$ against $-3.3543$ (KS $p = 0.83$) | | Device and precision invariance | same independent components across CPU/CUDA/MPS/MLX, float32/float64, Linux/macOS | identical (1.000) across all eight torch/MLX combinations | -| Cross-backend log-likelihood | converged LL across every backend | agree to ~3 significant digits (max pairwise ~0.003) | +| Cross-backend log-likelihood | same settings and start, 25 iterations, every backend | agree to 1e-5 at 32 and 48 channels; at 70 channels ($k\approx6$) within 1.1e-3, the size of a round-off perturbation's effect ([details](#cross-backend-log-likelihood-agreement-single-model)) | | EEGLAB output | `write_amica_output` round-trip through `loadmodout15` | single-model bytes are an exact serialization; loads with correct layout | | Degenerate fits | non-finite log-likelihood, update direction or parameters | refused, never returned as NaN sources | @@ -70,11 +70,7 @@ falling back, with a warning, to the bundled macOS x86_64 `amica15mac`, which ca ### Parity rows per backend -These rows predate epic #324 Phase 7 (issue #333), -which changed `doscaling` from normalizing stored columns to normalizing components, as the reference does; -they will be re-measured before the epic merges. - -Measured on 2026-09-22 on an Apple M4 Pro (14 cores, 64 GB, macOS 27; MLX 0.32.0, PyTorch 2.12.1, NumPy 2.5.0) +Measured on 2026-09-23 with the code of epic #324 (issue #351) on an Apple M4 Pro (14 cores, 64 GB, macOS 27; MLX 0.32.0, PyTorch 2.12.1, NumPy 2.5.0) against the v0.3.3 release native engine (`amica15-macos-arm64`, SHA-256 `c8b2ac7f...`), with the harness defaults: ```bash @@ -90,9 +86,9 @@ so on a machine that fetched an earlier release the default can run an older bin | Backend | Precision | Final LL | LL difference from Fortran | Mean matched correlation | Min matched correlation | Amari distance | Runtime (s) | Expected bar | |---|---|---:|---:|---:|---:|---:|---:|---| | Fortran (reference) | float64 | -3.411274 | | | | | 10.0 | the reference | -| PyTorch (`AMICA`) | float64 | -3.411245 | 0.000029 | 0.9992 | 0.9935 | 0.0037 | 17.7 | correlation > 0.95, Amari < 0.05, LL difference < 0.005 | -| NumPy (`AMICA_NumPy`) | float64 | -3.411246 | 0.000029 | 0.9992 | 0.9934 | 0.0037 | 32.8 | the PyTorch bar, and final LL within 1e-5 of PyTorch's | -| MLX (`AMICA(backend="mlx")`) | float32 | -3.411242 | 0.000032 | 0.9992 | 0.9935 | 0.0037 | 3.4 | the PyTorch bar, and final LL within 1e-4 of PyTorch's | +| PyTorch (`AMICA`) | float64 | -3.411003 | 0.000271 | 0.9991 | 0.9918 | 0.0038 | 18.1 | correlation > 0.95, Amari < 0.05, LL difference < 0.005 | +| NumPy (`AMICA_NumPy`) | float64 | -3.411003 | 0.000272 | 0.9991 | 0.9917 | 0.0038 | 32.6 | the PyTorch bar, and final LL within 1e-5 of PyTorch's | +| MLX (`AMICA(backend="mlx")`) | float32 | -3.410998 | 0.000276 | 0.9991 | 0.9917 | 0.0038 | 3.0 | the PyTorch bar, and final LL within 1e-4 of PyTorch's | Every backend meets its bar. All four runs stop at the 100-iteration budget, so the rows compare matched trajectories rather than converged optima; @@ -102,6 +98,35 @@ MLX computes in float32 (about seven significant digits per operation), so its b it lands within about five significant digits of the float64 likelihood, which is float32 consistency, not float64 parity. Runtime is one run's wall-clock time for the fit alone and varies by about a third between runs on the same host; the reference's includes process start-up and is single-threaded. +Epic #324 left the runtimes unchanged: the code before its changes to the fit (e38aa11), run in the same session, took 10.1, 17.9, 32.5 and 3.2 seconds. + +**The log-likelihood difference in these rows mostly reflects the two starting points.** +The harness starts each side from its own draw: numpy's `RandomState(42)` for pamica, gfortran's generator seeded with 42 for the reference. +After 100 iterations the final log-likelihood still depends on where a fit started. +Over eight seeds, the reference's own final log-likelihood has a standard deviation of 2.6e-4 and a range of 8.0e-4 (pamica's: 2.7e-4 and 8.0e-4), +and over the 64 pairs of a pamica start and a reference start the median difference is 3.5e-4 (range 1.7e-5 to 8.2e-4). +The 2.7e-4 in the table lies within that spread. +Before those changes the same run showed 2.9e-5, which reflected this particular pair of starts: +from a shared start, that code trailed the reference by 2.4e-4 after 100 iterations, which offset most of the difference between the two starts. + +The update rule itself is compared from a shared start: +pamica's seed-42 initialization is written into the reference's `load_*` files, and both sides run the harness settings for 100 iterations, the reference on one thread +(`.context/issue-351/harness_gap.py`). + +| Code | LL difference | Mean matched correlation | Amari distance | +|---|---:|---:|---:| +| before epic #324's changes to the fit (e38aa11) | 2.4e-4 | 0.99999 | 5.8e-4 | +| with them | 1.6e-6 | 0.99999993 | 3.9e-5 | + +The earlier row starts from that code's own initialization, which predates the component-row layout (issue #334) and the normalized initial mixing matrix (issue #341); +from each code's seeded state, the reference's first-iteration log-likelihood matches pamica's to 9e-16, so the state was written in the orientation the reference reads. +Against the bundled `amicaout` output (200 reference iterations with the `input.param` settings, from its own unseeded start), +the three backends are within 1.3e-4 to 1.4e-4 in log-likelihood, with mean matched correlation 0.9983 (minimum 0.982 to 0.983) and Amari distance 4.8e-3. + +The float32 MLX backend and the float64 PyTorch backend draw their start from the same generator, so they can be compared from the same start directly. +With the harness settings, after 100 iterations they differ by 5.0e-6 in log-likelihood, with mean matched correlation 0.99999991 (minimum 0.9999994) and Amari distance 5.0e-5; +after 200 iterations by 2.4e-6, 0.9999998 and 6.3e-5 (`.context/issue-351/precision_agreement.py`). + The same bars are pinned by `test_backend_meets_its_parity_bar_against_fortran` in `pamica/tests/test_fortran_param_forwarding.py`, which runs when `AMICA_RUN_FORTRAN=1` is set (as the weekly macOS job does). @@ -114,11 +139,48 @@ it is not sensitive to whether a particular small dataset happens to be well-con 32-channel sample ($k\approx30$, at the project's own data-adequacy boundary) gives a consistent Amari distance: -- Log-likelihood ~ -3.6993 ($k\approx153$; Fortran ~ -3.6993, gap ~0.0003). -- Hungarian-matched component correlation ~0.998 ($k\approx153$; Fortran-vs-Fortran self-consistency - over the same 5 seeds: ~0.999), clearing the >0.95 gate. On the bundled sample ($k\approx30$) both - numbers are consistent: ~0.998 pamica-vs-Fortran, ~0.998 Fortran-vs-Fortran. -- Amari distance ~0.006 (bundled sample; Fortran-vs-Fortran: ~0.005). +- Log-likelihood $-3.6993$ on both sides ($k\approx153$): Fortran $-3.699346$ (sd $6.5\times10^{-5}$), pamica $-3.699341$ (sd $4.1\times10^{-5}$), + a gap of the means of $5.6\times10^{-6}$. +- Hungarian-matched component correlation 0.9996 ($k\approx153$; sd 0.0005, lowest single component 0.960), + against the reference's own agreement over the same five runs of 0.998 (lowest component 0.917), clearing the >0.95 gate; + Amari distance 0.0013 (Fortran against Fortran 0.0025). +- Amari distance on the bundled sample: 0.011 over the protocol's five run pairs, 0.004 over 50 pairs (next subsection). + +Per seed on the external recording (re-measured on 2026-09-23 with the code of epic #324; `benchmarks/reproduce_table1.py --tier external` on the RTX 4090 host, +pamica on CUDA in float64, the pinned v0.3.3 reference at 16 threads, clock-seeded; `.context/issue-351/raw/table1_external/`): + +| seed | mean matched correlation | min matched correlation | Amari distance | +|---:|---:|---:|---:| +| 201 | 0.9999 | 0.9996 | 0.0008 | +| 202 | 0.9987 | 0.9602 | 0.0024 | +| 203 | 0.9999 | 0.9985 | 0.0012 | +| 204 | 1.0000 | 0.9998 | 0.0007 | +| 205 | 0.9998 | 0.9955 | 0.0012 | + +Before epic #324's changes to the fit, the same protocol gave a log-likelihood gap of ~0.0003, a correlation of ~0.998 and a Fortran-vs-Fortran agreement of ~0.999. + +### The bundled sample + +On the bundled 32-channel sample ($k\approx30$) the reproduction tier fits five pamica seeds (301-305) and five reference runs, one pair per seed, +Newton off, 2000 iterations (re-measured on 2026-09-23 with the code of epic #324; `.context/issue-351/raw/table1_bundled/`): + +| seed | reference final LL | pamica final LL | mean matched correlation | min matched correlation | Amari distance | +|---:|---:|---:|---:|---:|---:| +| 301 | -3.3995 | -3.3997 | 0.9995 | 0.9960 | 0.0031 | +| 302 | -3.3996 | -3.3996 | 0.9993 | 0.9952 | 0.0037 | +| 303 | -3.4006 | -3.3997 | 0.9267 | 0.4528 | 0.0380 | +| 304 | -3.3995 | -3.3995 | 0.9997 | 0.9981 | 0.0029 | +| 305 | -3.3996 | -3.3994 | 0.9982 | 0.9893 | 0.0069 | + +The five pairs average a correlation of 0.985 and an Amari distance of 0.011; +the ten pairs among the five reference runs average 0.971 and 0.019. +The reference run of seed 303 ended in a lower-likelihood basin (-3.4006, against -3.3995 to -3.3996 for the other four), and that pair dominates the five-pair means. +The tier does not seed its reference runs (`AMICANative` draws a clock-based seed per run), so a rerun draws new starts, and an event like this one may or may not recur. +With the unmixing matrices kept (`.context/issue-351/bundled_single_basins.py`), +ten seeded reference runs (seeds 1-10, final LL -3.39984 to -3.39945) against the same five pamica fits give, over all 50 pairs, +a mean Amari distance of 0.0044 (largest 0.0083) and a mean correlation of 0.9988; +the reference against itself, over 45 pairs, 0.0054 (largest 0.0101) and 0.9985; pamica against itself, over 10 pairs, 0.0039 and 0.9991. +Before epic #324 the five-pair figures were an Amari distance of ~0.006 (Fortran against Fortran ~0.005) and a correlation of ~0.998. The fixed source-density families are bit-exact against the literal Fortran score/derivative expressions (~1e-15), and the backend converges to the binary's solution within ~0.005 log-likelihood on either dataset. @@ -126,13 +188,42 @@ and the backend converges to the binary's solution within ~0.005 log-likelihood ### Newton-enabled runs and the initialization basin The comparison above disables Newton (`do_newton=0`) to isolate the algorithm from its starting point. -With Newton enabled (`do_newton=1`, as in the bundled parameter files and EEGLAB's `runamica15.m`; pamica's own default is off), agreement at the full 2000-iteration budget depends on the initialization, not on any dynamics difference between the backends. -From an *identical* initialization (the same starting mixing matrix and densities fed to both), `pamica` and Fortran converge to the same solution: -mean Hungarian-matched correlation ~0.997 with no collapsed components on the full 70-channel recording, the residual being floating-point summation-order noise between two implementations rather than an algorithmic gap. -From *independent* random initializations the picture differs, because the two backends' random number generators do not share a state, so a fixed seed does not map to a matched start. -At the long Newton budget this occasionally settles a few of the weakest, under-determined components into a different but equally likely (equal- or higher-likelihood) optimum. -Fortran is more robust to its own random inits (run-to-run self-consistency ~0.9997) than `pamica` is, so the effect appears as a `pamica`-specific spread on those components, not a divergence from the reference. -It is therefore an initialization-basin property (like the non-identifiable multi-model case below), not a parity defect; see issue #145 and the optional init-robustness follow-up #198. +With Newton enabled (`do_newton=1`, as in the bundled parameter files and EEGLAB's `runamica15.m`; pamica's own default is off), agreement at the full 2000-iteration budget depends on where a fit starts. +Re-measured on 2026-09-23 with the code of epic #324 on the full 70-channel recording +(pamica on CUDA in float64, seeds 42, 13 and 7; the pinned v0.3.3 reference seeded 1 and 2; the `input.param` settings with the early stops off; +`.context/issue-351/newton_seeds.py`), Hungarian-matched correlation of the 70 unmixing rows: + +| Pair | Mean | Min | Components below 0.9 | +|---|---:|---:|---:| +| pamica seed 42 vs reference seed 1 | 0.963 | 0.667 | 8 | +| pamica seed 42 vs reference seed 2 | 0.986 | 0.858 | 3 | +| pamica seed 13 vs reference seed 1 | 0.996 | 0.946 | 0 | +| pamica seed 13 vs reference seed 2 | 0.981 | 0.773 | 3 | +| pamica seed 7 vs reference seed 1 | 0.995 | 0.943 | 0 | +| pamica seed 7 vs reference seed 2 | 0.982 | 0.786 | 3 | +| reference seed 1 vs reference seed 2 | 0.985 | 0.841 | 2 | +| pamica seed 42 vs pamica seed 13 | 0.962 | 0.658 | 7 | +| pamica vs reference, both from the same start | 0.9999998 | 0.9999964 | 0 | +| reference from that start vs reference seed 1 | 0.996 | 0.955 | 0 | + +The weakest, under-determined components settle into different basins from different starts, in the reference's runs as in pamica's: +pamica's seed 42 differs from the reference's seed 1 on 8 components, and the reference's seed 2 differs from its own seed 1 on 2 and from each pamica seed on 3. +The alternative basins have equal or higher likelihood +(final log-likelihood: reference seeds 1 and 2, -3.697804 and -3.697709; pamica seeds 42, 13 and 7, -3.697569, -3.697816 and -3.697812). +The same start is the deterministic one of issue #145 ($A = I$, `mu` at -1, 0 and 1, `sbeta` = 1, `rho` = 1.5), fed to the reference through its `load_*` files; +from it, pamica and the reference end with the same components after 2000 Newton iterations +(mean correlation 0.9999998, Amari distance $3.8\times10^{-5}$, final log-likelihoods $7.6\times10^{-8}$ apart). +In issue #145, measured before epic #324's changes to the fit, two clock-seeded reference runs agreed at 0.9997 (minimum 0.998), +pamica's seeds 42, 13 and 7 reached 0.942, 0.994 and 0.996 against them, and the same start gave 0.997. +The seeded pair above (0.985) is a second measurement of the reference's own seed-to-seed spread, on one more pair of starts. +See issue #145 and the optional init-robustness follow-up #198. + +A single-seed supplementary run gives the float32 side at this data size: +the MLX backend (float32), with the same keywords and the same start as the CUDA seed-7 fit, stops on its learning-rate floor after 952 iterations, +after about 120 likelihood decreases below float32 resolution, at log-likelihood -3.69809 (the float64 fit was at -3.69791 at that iteration). +Its components match the float64 seed-7 fit at a mean correlation of 0.985 (minimum 0.907) and the reference's seed 1 at 0.983 (minimum 0.905); +with seed 42, float32 and float64 differ on 10 of the 70 components (mean 0.955). +At this budget the weak components' float32 basin varies the way a change of start does (`.context/issue-351/raw/newton_mlx/`). ### Source-density families are bit-exact @@ -167,106 +258,127 @@ See `pamica/tests/torch_tests/test_ng_pdf_families.py` and ADR 0002. ## Multi-model distributional similarity -Multi-model AMICA is not partition-identifiable, so exact partition parity with Fortran is the wrong acceptance bar. -The right test is whether the two implementations sample a similar distribution over solutions. Running an ensemble of `N = 20` fits per implementation on the bundled sample EEG (`n_models = 2`, 3 mixture components, 100 iterations, matched schedule), -the pamica-vs-Fortran partition cross-correlation distribution overlaps Fortran's own run-to-run distribution: +Multi-model AMICA is not partition-identifiable: fits from different starts reach different partitions of nearly the same likelihood, +so a single-run partition comparison with Fortran cannot serve as the acceptance bar. +The comparison is between the distributions of solutions the two implementations sample. +An ensemble of `N = 20` fits per implementation on the bundled sample EEG (`n_models = 2`, 3 mixture components, 100 iterations, matched schedule) +gives the distributions of pairwise agreement below +(`benchmarks/reproduce_table1.py --tier bundled`, re-measured on 2026-09-23 with the code of epic #324 against the pinned v0.3.3 native binary, +whose runs here draw their own clock-based seeds; the 40 fits are saved in `.context/issue-351/raw/table1_bundled/bundled_multimodel_ensemble.npz`). +The schedule matches except for one setting: the reference runs with `invsigmin` 0.0, the value EEGLAB's `runamica15.m` writes, and pamica with 1e-8. | Distribution (pairwise Hungarian-matched \|corr\|) | Mean | SD | Range | |---|---:|---:|---| -| within-Fortran (Fortran vs Fortran) | 0.638 | 0.040 | [0.572, 0.797] | -| within-pamica (pamica vs pamica) | 0.661 | 0.045 | [0.583, 0.820] | -| between (pamica vs Fortran) | 0.649 | 0.045 | [0.582, 0.886] | +| within-Fortran (Fortran vs Fortran) | 0.626 | 0.036 | [0.568, 0.868] | +| within-pamica (pamica vs pamica) | 0.638 | 0.042 | [0.578, 0.805] | +| between (pamica vs Fortran) | 0.632 | 0.042 | [0.564, 0.845] | -![Multi-model solution-ensemble cross-correlation distributions for pamica and Fortran.](../assets/figures/multimodel-ensemble.png){ width=640 } +![Multi-model solution-ensemble cross-correlation and log-likelihood distributions for pamica and Fortran.](../assets/figures/multimodel-ensemble.png){ width=640 } /// caption -Pairwise Hungarian-matched component correlation for 20 pamica and 20 Fortran multi-model fits of the sample EEG. -The within-Fortran, within-pamica, and between-implementation distributions overlap: the estimators sample the same solution space. +Pairwise Hungarian-matched component correlation (A) and final log-likelihood (B) for 20 pamica and 20 Fortran multi-model fits of the sample EEG. +The three agreement distributions overlap, and so do the two likelihood distributions. /// -The three distribution means lie within 0.011 of each other (between 0.649, within-Fortran 0.638), well inside a $\pm 0.05$ margin. To test this at the correct unit of analysis, we use a **run-level permutation test**: -the 190/400 pairwise correlations are *not* independent (each of the 40 runs appears in ~39 pairs), +The three means lie within 0.012 of each other. +The between-minus-within-Fortran difference is +0.006, inside the $\pm 0.05$ margin the original study set +(run-level bootstrap 90% interval -0.002 to 0.014, `.context/issue-351/equivalence_check.py`). +The pairwise values (190 within-Fortran and 190 within-pamica, plus 400 cross-implementation pairs) are not independent (each of the 40 runs appears in 39 pairs), so a Mann-Whitney or TOST applied to the pairwise values is pseudoreplicated and its p-value is invalid. -Permuting the 40 runs as intact units instead (20000 permutations, statistic = within-Fortran minus between-implementation mean correlation) respects that dependence and finds **no evidence that cross-implementation agreement is worse than Fortran's own run-to-run agreement ($p = 0.96$)**. - -The single-run cross-correlation of ~0.65 is therefore intrinsic estimator spread, not a shortfall: Fortran agrees with *itself* at 0.64. -The per-block sufficient statistics and one M-step are bit-exact against Fortran (~$10^{-15}$), -so the update equations are correct; -a small residual in the log-likelihood *distribution* (pamica $-3.363 \pm 0.006$ vs Fortran $-3.354 \pm 0.003$; -Kolmogorov-Smirnov $p \approx 6\times10^{-5}$) is an optimizer-quality effect, -not a model-correctness defect (pamica reaches Fortran's mean with about twice as many iterations). +The significance test permutes the 40 runs as intact units instead (20000 permutations, statistic = within-Fortran minus between-implementation mean correlation). +For the one-sided hypothesis that cross-implementation agreement is worse than Fortran's own run-to-run agreement, it gives $p = 0.88$. + +The single-run cross-correlation of ~0.63 matches Fortran's agreement with itself (0.63), so it measures the estimator's run-to-run spread. +The per-block sufficient statistics and one M-step agree with the reference to round-off (~$3\times10^{-16}$ relative). +The final log-likelihoods agree too: pamica $-3.3541 \pm 0.003$, Fortran $-3.3543 \pm 0.002$ (Kolmogorov-Smirnov $p = 0.83$). +The ensembles of this study measured before epic #324's changes to the fit differed on this one metric +(pamica $-3.363 \pm 0.006$ against Fortran $-3.354 \pm 0.003$, $p \approx 6\times10^{-5}$), a gap attributed then to convergence speed. +Refitting the pamica half with that code (e38aa11) against the same 20 reference fits gives $-3.3627 \pm 0.006$ ($p = 1\times10^{-5}$) +(`.context/issue-351/multimodel_pamica_fits.py`). +Seven of those 20 fits stop early on `min_dll`, whose check counted likelihood dips as small gains until issue #339 (mean $-3.3679$), +and the 13 that run the full 100 iterations average $-3.3600$, so the old gap came partly from the early stops and partly from the update rule of that code. +A seeded ensemble with the same settings (the reference seeded 0-19 and single-threaded) puts pamica's mean $8.1\times10^{-4}$ above the reference's at 100 iterations, $2.1\times10^{-4}$ at 200 and $1.0\times10^{-4}$ at 300 +([ADR 0003](https://github.com/sccn/pAMICA/blob/main/.context/decisions/0003-best-iterate-safeguard.md)). ### Amari distance: a second, assignment-free metric The correlation above needs a Hungarian assignment step to resolve component permutation before it can be computed. The Amari distance does not: it is permutation- and scale-invariant by construction, -so it is a genuinely independent check on the same 20-run ensembles (`.context/issue-27/ensemble.npz`; -recomputed by `.context/issue-27/amari_distance.py` with no re-fitting, since the raw unmixing matrices from the original 40 fits are already saved). +so it is an independent check on the same 20-run ensembles, computed from the saved unmixing matrices with no refitting +(`.context/issue-351/multimodel_ensemble.py`, which reuses `.context/issue-27/amari_distance.py`). Each stacked 2-model matrix is split into its per-model 32x32 blocks; since which Fortran model corresponds to which pamica model is not identified, both label pairings are tried and the lower-distance pairing is kept, per run pair. -This pairing correction is not free: on this ensemble it lowers the reported distance by ~0.02-0.03 versus always keeping the naive (unswapped) pairing, a similar order of magnitude to the within-Fortran/within-pamica gap below, so part of that gap plausibly reflects how often each group happens to need the swap, not just genuine agreement differences. +This pairing correction lowers the mean distance by 0.0185 on this ensemble (333 of the 780 run pairs take the swapped pairing), +the same order as the gaps between the groups below, so part of those gaps may reflect how often each group needs the swap. | Distribution (Amari distance, lower is better) | Mean | SD | |---|---:|---:| -| within-Fortran (Fortran vs Fortran) | 0.174 | 0.023 | -| within-pamica (pamica vs pamica) | 0.154 | 0.019 | -| between (pamica vs Fortran) | 0.163 | 0.022 | +| within-Fortran (Fortran vs Fortran) | 0.166 | 0.017 | +| within-pamica (pamica vs pamica) | 0.176 | 0.025 | +| between (pamica vs Fortran) | 0.172 | 0.022 | -The same run-level permutation test (20000 permutations, intact 40-run units) finds no evidence that between-implementation agreement is worse than Fortran's own run-to-run agreement ($p > 0.999$), agreeing with the correlation-based conclusion above. +By this metric pamica's ensemble spreads slightly more than the reference's (0.176 against 0.166), +and the between-implementation distance lies between the two: +0.005 from within-Fortran (bootstrap 90% interval 0.002 to 0.009). +The same one-sided run-level permutation test gives $p = 0.051$. +Before epic #324's changes to the fit, pamica's ensemble was the tighter one: +refit with that code against the same reference fits, within-pamica 0.151 and between 0.160 ($p = 0.998$), +and the original ensembles of this study measured 0.154 and 0.163 against the bundled `amica15mac` binary's 0.174 ($p > 0.999$). ??? note "Per-run detail (all 40 runs, both metrics)" Table 1 in the paper and the group summaries above report distribution means; the table below gives each of the 40 runs' own mean agreement to its own group's other 19 runs (`within`) and to all 20 opposite-implementation runs (`between`), for both metrics. - Regenerate with `uv run python .context/issue-27/amari_distance.py`, which writes `.context/issue-27/per_run_detail.csv`. + Regenerate with `uv run python .context/issue-351/multimodel_ensemble.py --from-npz .context/issue-351/raw/table1_bundled/bundled_multimodel_ensemble.npz`, + which writes `.context/issue-351/per_run_detail.csv` (with each run's final log-likelihood) and the figure above. | implementation | run | corr within | corr between | Amari within | Amari between | |---|---:|---:|---:|---:|---:| - | Fortran | 0 | 0.6164 | 0.6274 | 0.1880 | 0.1745 | - | Fortran | 1 | 0.6292 | 0.6568 | 0.1784 | 0.1592 | - | Fortran | 2 | 0.6465 | 0.6396 | 0.1694 | 0.1654 | - | Fortran | 3 | 0.6593 | 0.6740 | 0.1627 | 0.1508 | - | Fortran | 4 | 0.6323 | 0.6522 | 0.1797 | 0.1629 | - | Fortran | 5 | 0.6692 | 0.6773 | 0.1590 | 0.1488 | - | Fortran | 6 | 0.6660 | 0.6808 | 0.1628 | 0.1502 | - | Fortran | 7 | 0.6341 | 0.6400 | 0.1786 | 0.1678 | - | Fortran | 8 | 0.6285 | 0.6491 | 0.1779 | 0.1663 | - | Fortran | 9 | 0.6212 | 0.6341 | 0.1835 | 0.1702 | - | Fortran | 10 | 0.6308 | 0.6414 | 0.1794 | 0.1663 | - | Fortran | 11 | 0.6297 | 0.6444 | 0.1736 | 0.1614 | - | Fortran | 12 | 0.6334 | 0.6416 | 0.1782 | 0.1690 | - | Fortran | 13 | 0.6513 | 0.6630 | 0.1693 | 0.1574 | - | Fortran | 14 | 0.6666 | 0.6689 | 0.1584 | 0.1540 | - | Fortran | 15 | 0.6231 | 0.6254 | 0.1827 | 0.1750 | - | Fortran | 16 | 0.6147 | 0.6258 | 0.1903 | 0.1755 | - | Fortran | 17 | 0.6280 | 0.6483 | 0.1749 | 0.1632 | - | Fortran | 18 | 0.6441 | 0.6371 | 0.1691 | 0.1665 | - | Fortran | 19 | 0.6389 | 0.6505 | 0.1733 | 0.1637 | - | pamica | 0 | 0.6317 | 0.6149 | 0.1690 | 0.1806 | - | pamica | 1 | 0.6935 | 0.6777 | 0.1440 | 0.1529 | - | pamica | 2 | 0.6624 | 0.6583 | 0.1545 | 0.1606 | - | pamica | 3 | 0.6406 | 0.6459 | 0.1527 | 0.1561 | - | pamica | 4 | 0.6493 | 0.6384 | 0.1506 | 0.1569 | - | pamica | 5 | 0.6755 | 0.6591 | 0.1548 | 0.1622 | - | pamica | 6 | 0.6339 | 0.6236 | 0.1677 | 0.1807 | - | pamica | 7 | 0.6809 | 0.6788 | 0.1417 | 0.1479 | - | pamica | 8 | 0.6780 | 0.6552 | 0.1508 | 0.1631 | - | pamica | 9 | 0.6270 | 0.6206 | 0.1703 | 0.1823 | - | pamica | 10 | 0.6855 | 0.6665 | 0.1474 | 0.1612 | - | pamica | 11 | 0.6739 | 0.6556 | 0.1506 | 0.1612 | - | pamica | 12 | 0.6321 | 0.6190 | 0.1617 | 0.1753 | - | pamica | 13 | 0.6639 | 0.6610 | 0.1519 | 0.1597 | - | pamica | 14 | 0.6866 | 0.6849 | 0.1460 | 0.1520 | - | pamica | 15 | 0.6944 | 0.6672 | 0.1417 | 0.1559 | - | pamica | 16 | 0.6576 | 0.6533 | 0.1551 | 0.1633 | - | pamica | 17 | 0.6435 | 0.6249 | 0.1615 | 0.1756 | - | pamica | 18 | 0.6753 | 0.6547 | 0.1442 | 0.1549 | - | pamica | 19 | 0.6302 | 0.6180 | 0.1562 | 0.1655 | + | Fortran | 0 | 0.6169 | 0.6061 | 0.1710 | 0.1853 | + | Fortran | 1 | 0.6348 | 0.6508 | 0.1615 | 0.1593 | + | Fortran | 2 | 0.6145 | 0.6064 | 0.1756 | 0.1876 | + | Fortran | 3 | 0.6250 | 0.6356 | 0.1655 | 0.1684 | + | Fortran | 4 | 0.6290 | 0.6424 | 0.1654 | 0.1663 | + | Fortran | 5 | 0.6118 | 0.6047 | 0.1707 | 0.1853 | + | Fortran | 6 | 0.6268 | 0.6334 | 0.1610 | 0.1648 | + | Fortran | 7 | 0.6238 | 0.6247 | 0.1700 | 0.1765 | + | Fortran | 8 | 0.6332 | 0.6471 | 0.1643 | 0.1696 | + | Fortran | 9 | 0.6344 | 0.6458 | 0.1606 | 0.1648 | + | Fortran | 10 | 0.6057 | 0.6019 | 0.1782 | 0.1876 | + | Fortran | 11 | 0.6353 | 0.6534 | 0.1627 | 0.1631 | + | Fortran | 12 | 0.6182 | 0.6147 | 0.1680 | 0.1796 | + | Fortran | 13 | 0.6497 | 0.6503 | 0.1590 | 0.1642 | + | Fortran | 14 | 0.6270 | 0.6343 | 0.1637 | 0.1679 | + | Fortran | 15 | 0.6315 | 0.6322 | 0.1653 | 0.1715 | + | Fortran | 16 | 0.6101 | 0.6119 | 0.1701 | 0.1772 | + | Fortran | 17 | 0.6181 | 0.6287 | 0.1698 | 0.1718 | + | Fortran | 18 | 0.6428 | 0.6764 | 0.1596 | 0.1537 | + | Fortran | 19 | 0.6222 | 0.6371 | 0.1638 | 0.1664 | + | pamica | 0 | 0.6507 | 0.6421 | 0.1703 | 0.1671 | + | pamica | 1 | 0.6491 | 0.6338 | 0.1687 | 0.1687 | + | pamica | 2 | 0.6264 | 0.6206 | 0.1829 | 0.1785 | + | pamica | 3 | 0.6277 | 0.6324 | 0.1860 | 0.1730 | + | pamica | 4 | 0.6390 | 0.6358 | 0.1804 | 0.1706 | + | pamica | 5 | 0.5984 | 0.6025 | 0.1983 | 0.1880 | + | pamica | 6 | 0.6608 | 0.6492 | 0.1605 | 0.1615 | + | pamica | 7 | 0.6266 | 0.6175 | 0.1823 | 0.1786 | + | pamica | 8 | 0.6177 | 0.6205 | 0.1859 | 0.1751 | + | pamica | 9 | 0.6587 | 0.6442 | 0.1633 | 0.1644 | + | pamica | 10 | 0.6385 | 0.6269 | 0.1747 | 0.1749 | + | pamica | 11 | 0.6291 | 0.6259 | 0.1800 | 0.1752 | + | pamica | 12 | 0.6608 | 0.6499 | 0.1642 | 0.1631 | + | pamica | 13 | 0.6591 | 0.6456 | 0.1638 | 0.1665 | + | pamica | 14 | 0.6237 | 0.6252 | 0.1823 | 0.1733 | + | pamica | 15 | 0.6357 | 0.6361 | 0.1795 | 0.1704 | + | pamica | 16 | 0.6256 | 0.6219 | 0.1789 | 0.1720 | + | pamica | 17 | 0.6408 | 0.6326 | 0.1784 | 0.1742 | + | pamica | 18 | 0.6173 | 0.6221 | 0.1868 | 0.1751 | + | pamica | 19 | 0.6663 | 0.6528 | 0.1574 | 0.1609 | ## Cross-platform device and precision invariance The strongest reassurance that pamica is a single, well-defined implementation is that it recovers the *same* independent components no matter where or how it runs. Fitting the same real EEG (ds002718 sub-002, 147,000 frames, 70 channels, 2000 iterations) -on every backend and Hungarian-matching the unmixing components across them: +on every backend and Hungarian-matching the unmixing components across them +(measured before epic #324's changes to the fit and not re-measured since; issue #351 re-measured the figures elsewhere on this page): ![Cross-backend IC-equivalence matrix at 70 channels.](../assets/figures/cross-backend-equivalence-matrix.png){ width=680 } /// caption @@ -295,6 +407,8 @@ Whether backends recover the *same* components depends on how well-determined th $$k = \frac{\text{frames}}{\text{channels}^2}$$ +The two sweeps below were measured before epic #324's changes to the fit and have not been re-measured since. + As `k` grows, cross-backend component equivalence rises toward 1.0; at the rule-of-thumb minimum (`k` around 20-30) only the strongest components are backend-reproducible, while the rest are under-determined and settle into different but equally valid local optima (AMICA is non-convex). @@ -389,6 +503,9 @@ Full usage is in the [EEGLAB interoperability guide](eeglab.md); tests are in `p Throughput on real EEG (OpenNeuro ds002718 sub-002; `n_mix=3`, `pdftype=0`, `block_size=512`, warmed, min-of-repeats). CPU, MPS, and MLX were measured on Apple Silicon; CUDA on a separate NVIDIA RTX 4090 host, so MLX-versus-CUDA reads as "best Apple-GPU path versus a strong NVIDIA GPU", not a same-box comparison. +These tables predate epic #324. +A same-session check on the Apple M4 Pro at 70 channels (`benchmark_dimsweep.py`, 30000 frames, 25 iterations, three repeats; issue #351) +found per-iteration cost unchanged by the epic: 33.3 and 33.1 ms for MLX, 190.8 and 189.4 ms for torch-CPU float64, 165.1 and 166.7 ms for torch-MPS float32, before and after. ### Single-model, ms/iteration @@ -490,14 +607,27 @@ narrows here too is untested. ### Cross-backend log-likelihood agreement (single-model) -Every backend converges to the same log-likelihood to ~3 significant digits on real EEG, -across device and precision, confirming the whole backend family end-to-end: - -| channels | MLX f32 | CUDA f64 | torch-CPU f64 | torch-MPS f32 | NumPy f64 | -|---:|---:|---:|---:|---:|---:| -| 32 | -3.28634 | -3.28635 | -3.28636 | -3.28635 | -3.28620 | -| 48 | -3.20951 | -3.20952 | -3.20953 | -3.20951 | -3.21019 | -| 70 | -3.21579 | -3.21562 | -3.21560 | -3.21570 | -3.21315 | +The log-likelihood each backend reaches with the same settings and the same start, +from the throughput sweep's own runs (`benchmark_dimsweep.py`: the first 30000 frames of ds002718 sub-002, +the first 32, 48 or 70 channels, 25 iterations, `block_size=512`, no Newton, seed 42; +re-measured on 2026-09-23 with the code of epic #324, issue #351, on the Apple M4 Pro and, for CUDA, the RTX 4090 host): + +| channels | MLX f32 | CUDA f64 | CUDA f32 | torch-CPU f64 | torch-MPS f32 | NumPy f64 | +|---:|---:|---:|---:|---:|---:|---:| +| 32 | -3.28488 | -3.28489 | -3.28488 | -3.28489 | -3.28488 | -3.28478 | +| 48 | -3.20839 | -3.20838 | -3.20839 | -3.20838 | -3.20839 | -3.20893 | +| 70 | -3.21643 | -3.21723 | -3.21689 | -3.21746 | -3.21638 | -3.22054 | + +At 32 and 48 channels every backend that starts from the shared draw agrees to 1e-5, so float32 matches float64 to five significant digits. +At 70 channels the sweep's 30000 frames give $k\approx6$, and 25 iterations at `lrate=0.1` amplify round-off: +two float64 runs of the same code on CPU and CUDA differ by 2.3e-4, the backends by up to 1.1e-3, +and adding 1e-9 µV to one sample changes the torch-CPU value by 1.3e-15 after one iteration and 4.4e-4 after 25 +(the trajectory has a likelihood decrease on its 25th iteration). +The 70-channel differences are of the same size as that sensitivity, +and the code before epic #324's changes to the fit (e38aa11) shows the same growth. +NumPy is called without a seed here, so it starts from its own draw, which is why its column sits apart (by up to 4.2e-3 at 70 channels). +The component-level float32 comparison is in [the per-backend rows](#parity-rows-per-backend): +from a shared start, MLX float32 and PyTorch float64 agree to a mean matched correlation of 0.99999991 after 100 iterations. ## Other validated behaviors @@ -508,7 +638,7 @@ guarded to a no-op so the parity results above stay byte-for-byte unchanged. | Behavior | Status | Validation | |---|---|---| -| Best-iterate safeguard (`keep_best`, #51) | on by default | returns the highest-LL iterate; cuts multi-model LL sd from 12.7x to 2.0x Fortran's. Single-model parity stays bit-exact (monotone, no restore). ADR 0003 | +| Best-iterate safeguard (`keep_best`, #51) | on by default | returns the highest-LL iterate. It cut the multi-model LL sd from 12.7x to 2.0x Fortran's before epic #324; re-measured with the epic's code, the sd ratio is 1.0x with or without it and a restore fired in one of 20 seeded fits, at the 300-iteration budget only (gain 4.2e-6). Single-model parity stays bit-exact (monotone, no restore). ADR 0003 | | Per-model bias `c` update (#27) | on for `n_models>1` | Fortran `update_c`; per-block stats bit-exact; no-op for `n_models=1` | | Component sharing (`share_comps`, #60, #334) | off by default | Fortran `identify_shared_comps` ported; the scan itself has no bit-exact oracle (`Spinv2` is never allocated, so the reference's scan computes NaN similarities and never merges), but the update from a merged state seeded through the reference's `load_comp_list` matches the native binary to float64 round-off on PyTorch and NumPy (`test_component_rows.py`, opt-in with `AMICA_RUN_FORTRAN=1`); byte-identical when unshared | | Outlier rejection (`do_reject`, #123) | off by default | `good_idx` mechanism on all three backends (NumPy, PyTorch, MLX -- the last landed epic #278 Phase 3, #289); MLX/NumPy ports validated vs the PyTorch backend | @@ -624,7 +754,7 @@ uv run pytest # the full parity/beha ``` The multi-model ensemble and Amari detail regenerate from saved fits (no re-fitting) with -`uv run python .context/issue-27/amari_distance.py`. The cross-platform benchmark and equivalence +`uv run python .context/issue-351/multimodel_ensemble.py --from-npz .context/issue-351/raw/table1_bundled/bundled_multimodel_ensemble.npz`. The cross-platform benchmark and equivalence figures are produced by `benchmarks/benchmark_decompose.py` (and the sweep scripts alongside it); the underlying findings are in `.context/issue-84/` and `.context/issue-90/`. diff --git a/paper.md b/paper.md index 83a7341d..0d10b8aa 100644 --- a/paper.md +++ b/paper.md @@ -30,13 +30,15 @@ bibliography: paper.bib # Summary Independent Component Analysis (ICA) separates electroencephalographic and magnetoencephalographic (EEG/MEG) recordings into maximally independent sources that isolate brain, muscle, and artifact activities for downstream analysis [@makeig1995independent; @vigario1997independent; @iversen2019megeeg]. -Adaptive Mixture ICA (AMICA) [@palmer2006super; @palmer2007modeling; @palmer2012amica] models each source with a flexible, self-adjusting probability density and lets several ICA models coexist, one per segment of a recording. +Adaptive Mixture ICA (AMICA) [@palmer2006super; @palmer2007modeling; @palmer2012amica] models each source with a flexible, self-adjusting probability density and lets several ICA models coexist, +one per segment of a recording. Among the algorithms benchmarked by @delorme2012independent it returned the least dependent and most dipolar decompositions, -properties that study links to physiological interpretability. +properties that the study links to physiological interpretability. Its reference implementation, by Jason Palmer, is a Fortran program distributed as a compiled binary and usually run from MATLAB/EEGLAB; it runs only on the central processing unit (CPU) and has no Python interface. -`pamica` reproduces the reference Fortran results within numerical tolerance while running on the CPU, NVIDIA graphics processing units (GPUs, via CUDA), and Apple GPUs (through Apple's MLX array framework [@mlx2023]). +`pamica` reproduces the reference Fortran's single-model results within numerical tolerance, and its multi-model solutions spread like the reference's own, +while running on the CPU, NVIDIA graphics processing units (GPUs, via CUDA), and Apple GPUs (through Apple's MLX array framework [@mlx2023]). It reimplements the algorithm on PyTorch [@paszke2019pytorch], NumPy [@harris2020array], and SciPy [@virtanen2020scipy], and exposes a scikit-learn-style estimator under a BSD-3-Clause license. It writes the format EEGLAB's AMICA loader reads, so established MATLAB tooling consumes its results unchanged. @@ -52,10 +54,10 @@ General-purpose Python ICA implementations do not fill this gap. `scikit-learn` and `MNE-Python` provide FastICA [@hyvarinen2000independent] and Infomax [@bell1995information; @lee1999independent], while Picard [@ablin2018faster] offers faster-converging maximum-likelihood ICA; none implement AMICA's mixture of models, adaptive generalized-Gaussian densities, or Newton updates, so none can reproduce its decompositions. -`pamica` is for analysts who want AMICA-quality decompositions in Python, for anyone with a GPU who wants faster runs than the CPU-only binary, -and for methodologists who need a transparent reference to build on. +`pamica` serves analysts who want AMICA-quality decompositions in Python and anyone with a GPU who wants faster runs than the CPU-only binary; +it is also a transparent reference for methodologists to build on. The parity measurements reported here use EEG. -The algorithm itself is modality-agnostic, and an external user has fit rank-reduced, Maxwell-filtered MEG with it end to end, +The algorithm itself is modality-agnostic, and an external user has fit rank-reduced, Maxwell-filtered MEG with it end to end (see the research impact statement), but parity with the reference has not yet been measured on MEG. # State of the field @@ -90,46 +92,54 @@ Double precision is the default because the parity comparisons need it; single p Parity is measured two ways: by Hungarian-matched component correlation, and by the Amari distance [@amari1996new], a relabeling- and scale-invariant metric that needs no assignment step. -Both implementations ran AMICA's default 2000 iterations with Newton disabled (`pamica`'s own default), to isolate the algorithm from initialization. -With Newton enabled and independent seeds, some of the weakest components settle into a different basin of equal or higher likelihood: -on one seed of three this affected ten of seventy components, while the other two matched the reference at ~0.99. -A matched initialization restores ~0.997, so this is a property of the initialization, not a parity defect. -The single-model comparison uses a well-determined external recording ([NEMAR on002718](https://doi.org/10.82901/nemar.on002718), $k\approx153$, where $k$ = frames over squared channel count [@frank2025sufficient]) alongside the bundled 32-channel sample ($k\approx30$). -A mixture of ICA models is not partition-identifiable, so exact partition parity is the wrong bar there; -it is judged instead by whether the implementations sample a similar distribution of solutions, over ensembles of 20 runs each (\autoref{fig:ensemble}). -A permutation test finds no evidence that cross-implementation agreement is worse than Fortran's own run-to-run agreement. +Both implementations ran EEGLAB's default of 2000 iterations with Newton disabled (`pamica`'s own default), to isolate the algorithm from initialization. +With Newton enabled, some of the weakest components settle into different basins of equal or higher likelihood from different starts, in the reference's runs as in `pamica`'s: +against one reference run, one `pamica` seed of three differed on eight of seventy components and the other two matched at 0.995 and 0.996, +and two reference runs from different seeds differed on two. +From a shared start the two implementations end with the same components (correlation 0.9999998). +The single-model comparison uses a well-determined external recording (OpenNeuro ds002718, mirrored in the NEMAR archive [@delorme2022nemar] as [on002718](https://doi.org/10.82901/nemar.on002718), +$k\approx153$, where $k$ = frames over squared channel count [@frank2025sufficient]) alongside the bundled 32-channel sample ($k\approx30$). +On the bundled sample one of the five reference runs ended in a lower-likelihood basin and sets the five-pair Amari mean; +over 50 run pairs the distance between the implementations (0.004) is close to the reference's own run-to-run distance (0.005). +A mixture of ICA models is not partition-identifiable, +so the implementations are compared by the distributions of solutions they sample, over ensembles of 20 runs each (\autoref{fig:ensemble}). +One-sided run-level permutation tests of whether cross-implementation agreement is worse than the reference's own run-to-run agreement +give $p=0.88$ by correlation and $p=0.051$ by Amari distance, on which `pamica`'s ensemble spreads slightly more than the reference's. | Regime | Metric (dataset) | Result (mean) | |---|---|---| -| Single | Log-likelihood gap (on002718) | within ~0.0005 of $-3.6993$ | -| Single | Component correlation (on002718) | 0.998 | -| Single | Amari distance (bundled) | 0.006 | -| Single | Score functions, sufficient statistics | exact, $\sim\!10^{-15}$ | -| Multi | Correlation, one run: cross; within-Fortran | 0.65; 0.64 (sd 0.05) | -| Multi | Amari, one run: cross; within-Fortran | 0.163; 0.174 (sd 0.02) | -| Multi | Ensemble agreement, cross $-$ within-Fortran | correlation $+0.011$ ($p=0.96$); Amari $-0.011$ ($p>0.999$) | -| Multi | Ensemble log-likelihood: Fortran; `pamica` | $-3.3539$; $-3.3629$ (Kolmogorov-Smirnov $p=6\times10^{-5}$) | - -: Parity of `pamica` with the Fortran reference. Multi-model rows are over 20-run ensembles (190 within-, 400 cross-implementation pairs); sd is the standard deviation, given where computed. -The final row is the one metric on which the ensembles differ significantly, and it is convergence speed rather than optimum quality: `pamica` reaches Fortran's mean by 200 iterations and passes it by 300. - -![Multi-model ensemble partition-correlation (A) and log-likelihood (B) distributions, 20 `pamica` and 20 Fortran fits of the sample EEG; dashed lines mark each mean. -A's three distributions overlap, so the single-run correlation reflects intrinsic run-to-run spread, not a gap to the reference. -B's separation is Table 1's 0.009 gap on a ~0.035 axis.\label{fig:ensemble}](docs/assets/figures/multimodel-ensemble.png){ width=100% } - -All backends converge to the same single-model log-likelihood on real EEG (maximum pairwise difference ~0.003), -and single precision matches double to four or five significant digits on that log-likelihood. -Component-level float32 agreement is not yet characterized at a matched iteration budget, so float64 remains the default for parity work, -and double-precision CUDA is the reproducible NVIDIA path. +| Single | Log-likelihood gap (on002718) | $6\times10^{-6}$, both at $-3.6993$ | +| Single | Component correlation (on002718) | 0.9996 (Fortran vs Fortran 0.998) | +| Single | Amari distance (bundled) | 0.011 (5 run pairs); 0.004 (50 pairs) | +| Single | Score functions; sufficient statistics | $\le 2\times10^{-15}$ absolute; $3\times10^{-16}$ relative | +| Multi | Correlation, one run: cross; within-Fortran | 0.632; 0.626 (sd 0.04) | +| Multi | Amari, one run: cross; within-Fortran | 0.172; 0.166 (sd 0.02) | +| Multi | Ensemble agreement, cross $-$ within-Fortran | correlation $+0.006$ ($p=0.88$); Amari $+0.005$ ($p=0.051$) | +| Multi | Ensemble log-likelihood: Fortran; `pamica` | $-3.3543$; $-3.3541$ (Kolmogorov-Smirnov $p=0.83$) | + +: Parity of `pamica` with the Fortran reference. Multi-model rows are over 20-run ensembles (190 within-Fortran and 190 within-`pamica`, plus 400 cross-implementation pairs); sd is the standard deviation, given where computed; +$p$ values are one-sided run-level permutation tests. + +![Multi-model ensemble partition-correlation (A) and log-likelihood (B) distributions, 20 `pamica` and 20 Fortran fits of the sample EEG; vertical lines mark each mean. +A's three distributions overlap: the single-run correlation of ~0.63 matches the reference's agreement with itself. +B's two means lie 0.0002 apart on a ~0.02 axis.\label{fig:ensemble}](docs/assets/figures/multimodel-ensemble.png){ width=100% } + +From a shared start with the harness settings, the single-precision MLX backend and the double-precision PyTorch backend differ by $5\times10^{-6}$ in log-likelihood after 100 iterations, +with mean matched component correlation 0.9999999 and Amari distance $5\times10^{-5}$. +In the throughput benchmark's runs on a 30,000-frame excerpt of the external recording, the backends that share a start agree after 25 iterations to $10^{-5}$ in log-likelihood on 32 and 48 channels; +with 70 channels ($k\approx6$) they differ by up to $10^{-3}$, about the effect of perturbing one sample by $10^{-9}$ µV. +Double precision remains the default for parity work, and double-precision CUDA is the reproducible NVIDIA path. On real 70-channel EEG at `block_size=512`, per-iteration cost is 25 ms for MLX on an Apple GPU, 39 ms for double-precision CUDA on an RTX 4090, and 30 ms for native Fortran on a 24-core i9-13900K, against 193 ms for PyTorch on an Apple-Silicon CPU and 255 ms for PyTorch-Metal. -That comparison is block-size-confounded: PyTorch-Metal is far more block-size-sensitive than the CPU it runs beside, falling from 431 to 30.5 ms/iteration on the bundled 32-channel sample -between `block_size=512` and `pamica`'s current 8192 default, still behind the CPU's 21.7 ms/iteration there, and to 13.5 ms/iteration, below the CPU's 15.8 ms, -at a further-tuned single-block setting that is memory-limited rather than a free win, since peak block memory scales with `block_size`, which is why 8192 stays the shipped default. +That comparison is block-size-confounded: PyTorch-Metal is far more block-size-sensitive than the CPU it runs beside, +falling from 431 to 30.5 ms/iteration on the bundled 32-channel sample between `block_size=512` and `pamica`'s current 8192 default, +still behind the CPU's 21.7 ms/iteration there, +and to 13.5 ms/iteration, below the CPU's 15.8 ms, at a further-tuned single-block setting. +Peak block memory grows with `block_size`, which is why 8192 stays the shipped default. MLX stays the fastest Apple backend throughout the sweep. Full tables, the data-size sweep, and reproduction commands are in the [documentation](https://eeglab.org/pAMICA/guides/validation/); the correctness harness never uses synthetic data. -The harness and sample data ship with the package, and every row of Table 1 but the two on002718 entries re-runs from the bundled sample alone. +The harness and sample data are in the source repository, and every row of Table 1 but the two on002718 entries re-runs from the bundled sample alone. # Research impact statement @@ -140,10 +150,10 @@ where an MNE core developer and the author of a competing port lean toward `pami since this Center develops AMICA and maintains the reference Fortran and EEGLAB. `sccn/pAMICA` has been public since 2021, with public issues, pull requests, nine releases (six on PyPI), and a Zenodo archive. -Three researchers outside the author group have filed issues, two mid-analysis on their own data: +Three researchers outside the author group have filed issues, two of them mid-analysis on their own data: a 236-channel, 8.3-million-sample EEG decomposition (issue 207) and Maxwell-filtered 306-channel MEG (issue 221); the third is adopting AMICA for a multiverse analysis in MNE-Python (issue 206). -Integration into this Center's Python preprocessing and into NEMAR [@delorme2022nemar], the public archive we operate (800 datasets, ~40,000 participants, 55 TB), is in progress. +Integration into this Center's Python preprocessing and into NEMAR, the public archive we operate (800 datasets, ~40,000 participants, 55 TB), is in progress. # AI usage disclosure diff --git a/paper.pdf b/paper.pdf index a0750bebd72b4f59447ca5ef880e0b9c44efa7c6..0bfcd83295a7edbc43f8e0a5b009b0bd0958a644 100644 GIT binary patch delta 188344 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 z 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ⅅJ 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 From 410b71bb3ca8dbd6869dbd922f6e11c305944461 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 24 Sep 2026 06:53:33 +0000 Subject: [PATCH 21/26] Rebuild paper.pdf --- paper.pdf | Bin 385666 -> 385666 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/paper.pdf b/paper.pdf index 0bfcd83295a7edbc43f8e0a5b009b0bd0958a644..d62edb8e1ce3ff8913d24767796f6d19db4fd8d0 100644 GIT binary patch delta 211 zcmZoVD&BNdyrG4$g{g&k3(E=-R#RguBcttWMOa=lLfF&iim{kOnK#8)ETJmI#aSd6 zRV-4>O)X6=lXT6JQ%!VD%#uxX6O&EMbj{O@k_=PK6D?8s57p^1sBk+G|pp`n|lo12BXxs$W0k+ZpxrKO3Tf(>CM Pv6Hyvw|CxW{mBFXm8v)o delta 211 zcmZoVD&BNdyrG4$g{g&k3(E=-Rzm|TL-XxxMOa=lLfF&iim{kOnK#8)ETJmI#aSd6 zRSb Date: Wed, 23 Sep 2026 23:56:29 -0700 Subject: [PATCH 22/26] Credit the Research Skills plugins in the paper's AI disclosure --- paper.bib | 9 +++++++++ paper.md | 7 +++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/paper.bib b/paper.bib index 0b903bdc..6ed0d312 100644 --- a/paper.bib +++ b/paper.bib @@ -278,3 +278,12 @@ @inproceedings{frank2025sufficient publisher = {IEEE}, doi = {10.1109/NER61569.2025.11588993} } + +@misc{shirazi2026researchskills, + title = {Research Skills: {Claude Code} plugins for academic research and development}, + author = {Shirazi, Seyed Yahya}, + year = {2026}, + note = {Version 0.16.4}, + doi = {10.5281/zenodo.20696515}, + howpublished = {\url{https://github.com/neuromechanist/research-skills}} +} diff --git a/paper.md b/paper.md index 0d10b8aa..a968f1a3 100644 --- a/paper.md +++ b/paper.md @@ -159,8 +159,11 @@ Integration into this Center's Python preprocessing and into NEMAR, the public a Generative AI was used in this project, disclosed here under the journal's policy. -**Tools.** Anthropic's Claude models (Sonnet and Opus families), through the Claude Code command-line assistant. -The instructions given to them are public in the repository (`AGENTS.md`, `CLAUDE.md`, `.rules/`). +**Tools.** Anthropic's Claude models (Sonnet and Opus families), through the Claude Code command-line assistant, +with the Research Skills plugin collection [@shirazi2026researchskills] as the working harness, which Claude Code does not provide on its own. +Its project plugin supplies the repository instructions (`AGENTS.md`, `CLAUDE.md`, `.rules/`), the phased, issue-tracked development workflow and the pull-request review, +and its manuscript plugin the manuscript review. +The instructions given to the models are public in the repository. **Scope.** The source code (translating the reference Fortran into Python, refactoring, scaffolding tests), the documentation, and the drafting and copy-editing of this manuscript. From 4d684dd51dc7fd5fb58835e1254e9cfea0e859c7 Mon Sep 17 00:00:00 2001 From: Seyed Yahya Shirazi Date: Wed, 23 Sep 2026 23:56:29 -0700 Subject: [PATCH 23/26] Skip the Python CI jobs when no code changes Filter CI on code paths (docs, paper, paper.pdf, metadata and non-Python .context records skip; .context Python files and the tested ensemble.npz still run), and commit the rebuilt paper.pdf back only on dev and main. Checked with actionlint. --- .github/workflows/ci.yml | 57 +++++++++++++++++++++------------ .github/workflows/draft-pdf.yml | 13 +++++--- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7de0407d..e1440dbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,15 @@ name: CI -# Python jobs (lint/test/build) skip when a change touches only prose or -# metadata -- Markdown, the docs site, the JOSS paper (paper.md/paper.bib), and -# citation/archive metadata (CITATION.cff, .zenodo.json, LICENSE) -- none of -# which affect the package. Spell-checking (typos.yml) and the paper PDF build -# (draft-pdf.yml) have their own triggers, so prose is still checked. +# Python jobs (lint/test/build) skip when a change touches no code: only +# Markdown, the docs site, the JOSS paper (paper.md/paper.bib and the rebuilt +# paper.pdf), citation/archive metadata (CITATION.cff, .zenodo.json, LICENSE), +# or the non-Python records under .context/ (notes, raw run outputs, figures). +# Python files under .context/ still run CI: ruff lints them, and the test +# suite loads .context/issue-27/amari_distance.py with its ensemble.npz, which +# is re-included for that reason. The filter is `paths` with negations, where a +# later positive pattern re-includes what an earlier negation dropped. +# Spell-checking (typos.yml) and the paper PDF build (draft-pdf.yml) have their +# own triggers, so prose is still checked. # # push runs on both `main` (release branch) and `dev` (integration branch, the # default branch): post-merge pushes to dev are what catch drift that only @@ -42,23 +47,33 @@ name: CI on: push: branches: [main, dev] - paths-ignore: - - "**.md" - - "**.bib" - - "docs/**" - - "mkdocs.yml" - - "CITATION.cff" - - ".zenodo.json" - - "LICENSE" + paths: + - "**" + - "!**.md" + - "!**.bib" + - "!docs/**" + - "!mkdocs.yml" + - "!CITATION.cff" + - "!.zenodo.json" + - "!LICENSE" + - "!paper.pdf" + - "!.context/**" + - ".context/**.py" + - ".context/issue-27/ensemble.npz" pull_request: - paths-ignore: - - "**.md" - - "**.bib" - - "docs/**" - - "mkdocs.yml" - - "CITATION.cff" - - ".zenodo.json" - - "LICENSE" + paths: + - "**" + - "!**.md" + - "!**.bib" + - "!docs/**" + - "!mkdocs.yml" + - "!CITATION.cff" + - "!.zenodo.json" + - "!LICENSE" + - "!paper.pdf" + - "!.context/**" + - ".context/**.py" + - ".context/issue-27/ensemble.npz" concurrency: # push: per-SHA group (each push to main/dev gets its own run, never diff --git a/.github/workflows/draft-pdf.yml b/.github/workflows/draft-pdf.yml index a999935f..3701bcfc 100644 --- a/.github/workflows/draft-pdf.yml +++ b/.github/workflows/draft-pdf.yml @@ -3,10 +3,13 @@ name: Draft PDF # Builds paper.md into a JOSS-formatted PDF (Pandoc + inara, via the official # openjournals action). Not a JOSS submission step -- this just keeps the # committed paper.pdf in the repo root in sync with paper.md/paper.bib for -# local review. On a push to a branch it commits the rebuilt PDF back. Skipped -# for pull_request runs (a PR checkout is a detached merge ref, not the head -# branch) and for tag pushes (also detached, e.g. the v* tag a release creates), -# where pushing would go to the wrong place or fail outright. +# local review. On a push to `dev` or `main` it commits the rebuilt PDF back. +# Feature branches only build it (the PDF is uploaded as an artifact): a bot +# commit at a PR's head leaves that PR's CI runs waiting for approval, and the +# PDF is rebuilt on `dev` once the PR merges. Skipped for pull_request runs (a +# PR checkout is a detached merge ref, not the head branch) and for tag pushes +# (also detached, e.g. the v* tag a release creates), where pushing would go to +# the wrong place or fail outright. on: push: paths: @@ -42,7 +45,7 @@ jobs: # The action writes the compiled PDF next to the paper source. path: paper.pdf - name: Commit and push rebuilt PDF - if: github.event_name == 'push' && github.ref_type == 'branch' + if: github.event_name == 'push' && github.ref_type == 'branch' && (github.ref_name == 'dev' || github.ref_name == 'main') run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" From f8eb0ed4ef0f0e1911a8d7e9555d2a2aeafff725 Mon Sep 17 00:00:00 2001 From: Seyed Yahya Shirazi Date: Wed, 23 Sep 2026 23:56:30 -0700 Subject: [PATCH 24/26] Add changelog entries for the paper credit and CI filters --- docs/changelog.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index f465d77e..663e7dc1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -653,6 +653,17 @@ and every backend's fitting follows the Fortran reference more closely. [Unmapped Fortran keywords](guides/amica-differences.md#unmapped-fortran-keywords) section, which names three keywords that are dead in the reference itself (`filter_length`/`dft_length`/`decwindow`) and records the `do_rho`-vs-`pdftype` divergence. +- **The paper credits the Research Skills plugins** as the development harness, + in its AI usage disclosure. + +### Continuous integration + +- **The Python jobs skip changes that touch no code.** + A change limited to Markdown, the docs site, the paper (including the rebuilt `paper.pdf`), citation metadata, + or the non-Python records under `.context/` runs only spell-checking and, for the paper, the PDF build. + Python files under `.context/` still run the full CI. +- **The rebuilt `paper.pdf` is committed back on `dev` and `main` only.** + Feature branches build it as an artifact, so a PR's head is never a bot commit whose checks wait for approval. ## 0.3.3 From 15b53be72070840ad25055b8fb5c78bf7c1228bd Mon Sep 17 00:00:00 2001 From: Seyed Yahya Shirazi Date: Wed, 23 Sep 2026 23:56:55 -0700 Subject: [PATCH 25/26] Name the project plugin's design workflow and link Research Skills --- paper.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/paper.md b/paper.md index a968f1a3..4afef1b5 100644 --- a/paper.md +++ b/paper.md @@ -160,9 +160,10 @@ Integration into this Center's Python preprocessing and into NEMAR, the public a Generative AI was used in this project, disclosed here under the journal's policy. **Tools.** Anthropic's Claude models (Sonnet and Opus families), through the Claude Code command-line assistant, -with the Research Skills plugin collection [@shirazi2026researchskills] as the working harness, which Claude Code does not provide on its own. -Its project plugin supplies the repository instructions (`AGENTS.md`, `CLAUDE.md`, `.rules/`), the phased, issue-tracked development workflow and the pull-request review, -and its manuscript plugin the manuscript review. +with the Research Skills plugin collection () [@shirazi2026researchskills] as the working harness, which Claude Code does not provide on its own. +Its project plugin supplies the project's design and planning workflow, the repository instructions (`AGENTS.md`, `CLAUDE.md`, `.rules/`), +the phased, issue-tracked development and the pull-request review; +its manuscript plugin supplies the manuscript review. The instructions given to the models are public in the repository. **Scope.** The source code (translating the reference Fortran into Python, refactoring, scaffolding tests), the documentation, and the drafting and copy-editing of this manuscript. From 0cf978f169fa6cfdea7fba8ce3d60161b3f46d6a Mon Sep 17 00:00:00 2001 From: Seyed Yahya Shirazi Date: Wed, 23 Sep 2026 23:57:12 -0700 Subject: [PATCH 26/26] Match the Research Skills citation to its CITATION.cff --- paper.bib | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paper.bib b/paper.bib index 6ed0d312..bbf3dfc6 100644 --- a/paper.bib +++ b/paper.bib @@ -280,10 +280,10 @@ @inproceedings{frank2025sufficient } @misc{shirazi2026researchskills, - title = {Research Skills: {Claude Code} plugins for academic research and development}, + title = {Research Skills}, author = {Shirazi, Seyed Yahya}, year = {2026}, - note = {Version 0.16.4}, + note = {Version 0.16.4, released 2026-09-20; BSD-3-Clause}, doi = {10.5281/zenodo.20696515}, howpublished = {\url{https://github.com/neuromechanist/research-skills}} }