Repository navigation
Refuse degenerate fits in the AMICA wrapper (#50) - #54
Conversation
AMICA.fit() marked is_fitted_ = True unconditionally, so transform()/ get_mixing_matrix()/get_unmixing_matrix() ran on a degenerate fit (stop_reason nan_ll/singular_ll) and returned NaN sources with no error, while state_dict()/save() already refused such a model. Make the wrapper's contract consistent: - fit() sets is_fitted_ only when the fit converged, and exposes converged_ (bool) and stop_reason_ (str) for inspection; a degenerate fit logs a wrapper-level warning. - A new _check_usable() guard raises a clear degenerate error (naming the stop_reason) from transform/get_mixing/get_unmixing/save, mirroring state_dict()'s refusal. An unfitted model still raises a distinct "must be fitted" error. - load() carries stop_reason_/converged_ through (a saved model is always converged, since state_dict refuses degenerate ones). Tested: 3 new wrapper tests (converged/stop_reason exposed on a normal fit; unfitted vs degenerate error messages; transform/get_*/save all refuse a degenerate model) following the established real-fit-then-force-stop_reason pattern; 10 wrapper tests pass. ruff clean; ty no new diagnostics.
PR review (3 Sonnet reviewers): - CRITICAL (regression I introduced): _check_usable keyed on model_ is None, but fit() assigned self.model_ before training, so a mid-fit exception left a half-built backend that the guard let through (and, on refit, stale is_fitted_=True). fit() now trains a LOCAL backend and only publishes it to self on success -- a first-fit crash keeps model_ None (clean "not fitted"), a failed refit keeps the last good model. - Strengthened the degenerate test to a REAL divergence: a single NaN injected into the real EEG forces an actual nan_ll stop (error-path robustness test, not a parity oracle), so it exercises fit()'s real is_fitted_=False/warning bookkeeping instead of a forced marker, and now also covers fit_transform. - Added load() converged_/stop_reason_ round-trip assertions. 10 wrapper tests pass; ruff clean; ty no new diagnostics.
Member
Author
Review response (3 Sonnet reviewers: code, silent-failure, tests)code-reviewer: no ≥80-confidence issues (ran the wrapper suite 10/10, verified Fixed (commit 7bdf38a)
Not changed (with rationale)
|
neuromechanist
deleted the branch
51-multi-model-ng-log-likelihood-is-002-lower-and-more-variable-than-fortran
July 7, 2026 05:17
This was referenced Jul 19, 2026
Merged
This was referenced Aug 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #50. Stacked on #53 (issue #51) — review the last commit; the base retargets to
mainautomatically when #53 merges.Problem
AMICA.fit()setis_fitted_ = Trueunconditionally, sotransform()/get_mixing_matrix()/get_unmixing_matrix()ran on a degenerate fit (stop_reasoninnan_ll/singular_ll) and returned NaN sources with no exception, whilestate_dict()/save()already refused such a model. Then_models>1work made this more reachable.Fix — consistent, fail-loud contract (mirrors
state_dict's refusal)fit()setsis_fitted_only when the fit converged, and exposesconverged_(bool) andstop_reason_(str) for inspection; a degenerate fit logs a wrapper-level warning._check_usable()guard raises a clear degenerateRuntimeError(naming thestop_reason) fromtransform/get_mixing_matrix/get_unmixing_matrix/save. An unfitted model still raises a distinct"must be fitted"ValueError.load()carriesstop_reason_/converged_through (a saved model is always converged, sincestate_dictrefuses degenerate ones).Contract chosen per maintainer decision: expose attributes + refuse output (not a hard error at
fit()), so a diverged run stays inspectable viastop_reason_/ll_history_.Tests
3 new wrapper tests:
converged_/stop_reason_exposed on a normal fit; unfitted vs degenerate error messages are distinct and diagnosable;transform/get_*/saveall refuse a degenerate model. The degenerate marker is forced after a real fit (the backend does not diverge on the clean sample EEG), the same patterntest_state_dict_refuses_degenerate_modelalready uses — real data, no mocks. 10 wrapper tests pass; ruff clean;tyno new diagnostics.