Skip to content

Modernize PyRCN for scikit-learn 1.6+ (Python 3.10+)#62

Merged
renierts merged 13 commits into
devfrom
modernize-sklearn
Jul 18, 2026
Merged

Modernize PyRCN for scikit-learn 1.6+ (Python 3.10+)#62
renierts merged 13 commits into
devfrom
modernize-sklearn

Conversation

@renierts

Copy link
Copy Markdown
Collaborator

Summary

Modernize PyRCN to run on the current scientific-Python stack
(scikit-learn 1.9 / NumPy 2.x / SciPy 1.18 / Python 3.10–3.13) and to
clean up the codebase, without changing the public API. The full test suite
passes (85 passed / 2 skipped); flake8 and mypy are clean.

The PyTorch nn subpackage (an early draft) is removed and torch is dropped
as a dependency — a redesigned backend for sequential data is planned
separately.

Phase 1 — scikit-learn 1.6+ compatibility (no behavioral change)

  • Remove the nn subpackage and the torch/torchvision/torchaudio deps.
  • Replace private/deprecated sklearn/scipy imports with public APIs
    (own ACTIVATIONS table, RegressorMixin type hints, scipy.sparse,
    drop _deprecate_positional_args, plain typing).
  • Migrate estimators to the 1.6+ developer API: validate_data,
    mixins-before-BaseEstimator MRO (restores is_regressor/is_classifier),
    is_clusterer in Coates.
  • Refactor the sequence-aware metrics to the public sklearn API
    (drop private _check_targets / _check_reg_targets; route
    mean_squared_error(squared=False) to root_mean_squared_error).
  • Packaging/CI: requires-python >=3.10, scikit-learn>=1.6, dynamic version
    (0.0.18), fix the BSD license classifier, drop setup.cfg/pytest.ini,
    CI matrix 3.10–3.13, update repo URLs to the PlasmaControl org.

Phase 2 — code unification + bug fixes

  • Mechanical (behavior-preserving): PEP 604 / PEP 585 type hints,
    f-strings, sorted imports + from __future__ import annotations,
    BaseExceptionTypeError, dead-code removal, strict mypy (now enforced
    in CI), and the same modernization applied to tests/.
  • Intentional bug fixes (behavior-changing):
    • _make_sparse now uses the passed random_state instead of the global
      NumPy RNG, so sparse weights are reproducible from a seed (the exact mask
      for a given seed changes).
    • Coates follows sklearn constructor conventions: it stores the raw
      random_state (resolved in fit) and no longer uses a shared mutable
      KMeans() default, so clone/get_params round-trip correctly. Verified
      numerically identical for a single fit with an explicit clusterer; the
      differing cases (clone/get_params, default clusterer across multiple
      instances, repeated fit with an int seed) are all where the previous
      behavior was a bug.

🤖 Generated with Claude Code

renierts and others added 13 commits July 17, 2026 15:56
The nn/ PyTorch subpackage was an early draft and is out of scope for the
scikit-learn compatibility modernization. Remove it along with the torch,
torchvision and torchaudio dependencies so that `import pyrcn` no longer
requires PyTorch.

- Delete src/pyrcn/nn/
- Drop nn from the top-level package imports and __all__
- Strip torch from util.seed_everything (keep random/numpy seeding)
- Remove torch/torchvision/torchaudio from pyproject dependencies

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prepare for scikit-learn >= 1.6 by removing reliance on private or deprecated
symbols. No behavioral change.

- base/_activations.py: define the inplace activation table locally instead of
  importing and mutating the private sklearn.neural_network._base.ACTIVATIONS
  global.
- Drop the deprecated @_deprecate_positional_args decorator throughout;
  __init__/metric signatures are already keyword-only via `*`.
- echo_state_network, extreme_learning_machine: use public
  sklearn.base.RegressorMixin in type hints instead of the private
  sklearn.linear_model._base.LinearModel.
- node_to_node: import csr_matrix from scipy.sparse, not the removed
  scipy.sparse.csr submodule.
- Replace the typing_extensions / sys.version_info shims with plain typing
  (Python >= 3.10) and drop now-dead `import sys`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Custom estimators relied on APIs removed or deprecated in scikit-learn 1.6:

- BaseEstimator._validate_data / _check_n_features were removed. Replace them
  with the public sklearn.utils.validation.validate_data function.
- The estimator type is now derived from tags and _estimator_type is
  deprecated. Per the official developer guide, mixins must precede
  BaseEstimator in the MRO so __sklearn_tags__ resolves the estimator type
  correctly; reorder the bases of every estimator accordingly. This restores
  is_regressor / is_classifier detection.
- Coates: replace the getattr(clusterer, "_estimator_type") check with the
  public sklearn.base.is_clusterer helper.

No behavioral change intended. The suite goes from 50 to 21 failing; the
remaining failures are all in the sequence-aware metrics wrappers and are
addressed separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sequence-aware metric wrappers called scikit-learn's private
_check_targets / _check_reg_targets helpers, whose signatures changed in 1.6
(sample_weight became positional), causing unpacking and validation errors.
Replace those calls: flatten the per-sequence arrays with the public
check_consistent_length + np.concatenate and derive the target type via the
public sklearn.utils.multiclass.type_of_target. The public metric performs its
own target validation, so the private helpers are no longer needed.

Also route mean_squared_error(squared=False) to the public
root_mean_squared_error, since the `squared` parameter was removed from
sklearn.metrics.mean_squared_error in 1.6. PyRCN's own `squared` argument is
preserved.

No behavioral change intended.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The repository moved from TUD-STKS/PyRCN to PlasmaControl/PyRCN. Update all
GitHub and mybinder links in the README, docs and example notebook. Links to
other TUD-STKS repositories (Automatic-Music-Transcription, gci_estimation)
are left unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Bump requires-python to >=3.10 (the floor for scikit-learn >= 1.6) and add
  a scikit-learn>=1.6 dependency floor; list numpy/scipy/joblib/pandas
  explicitly.
- Fix the license classifier (was MIT; the LICENSE file and all source
  headers are BSD-3-Clause).
- Single source of truth for the version: pyproject reads it dynamically from
  pyrcn._version, and _version.py is set to 0.0.18 (was 0.0.17post1).
- Add test and examples optional-dependency extras.
- Remove the redundant setup.cfg (pyproject is now canonical).
- Fix requirements.txt: the malformed `requests[...]` lines were meant to be
  matplotlib/seaborn/ipywidgets/ipympl/tqdm; drop obsolete typing-extensions.
- mypy: ignore_missing_imports for third-party packages without type stubs.
- Read the Docs: build with Python 3.12 (>=3.10 is now required).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CI matrix: Python 3.10-3.13 (drop 3.9); bump actions/checkout to v4 and
  actions/setup-python to v5 in both workflows.
- Install the package via the `test` extra instead of an ad-hoc plugin list
  plus requirements.txt; run flake8 (blocking) and mypy (informational, the
  remaining strict-typing findings are deferred to the code-unification pass).
- Remove pytest.ini, whose addopts required cov/mypy plugins just to collect
  tests; pyproject's [tool.pytest.ini_options] keeps a bare `pytest` working,
  and coverage is requested explicitly in CI.
- Fix a flake8 E231 in tests so linting can cover the test suite too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply modern Python 3.10 idioms (the new requires-python floor) consistently:

- Type hints: PEP 604 unions (Union[A, B] -> A | B, Optional[A] -> A | None)
  and PEP 585 builtin generics (Dict/List/Tuple -> dict/list/tuple); move
  Callable/Iterable to collections.abc. Applied with pyupgrade --py310-plus.
- Replace str.format(...) calls with f-strings.
- Sort and group imports consistently (isort) and add
  `from __future__ import annotations` to every module.
- Reflow the multi-line signatures whose alignment shifted.

Purely mechanical; no behavioral change. flake8 clean, suite unchanged
(85 passed / 2 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Remove the dead, commented-out NonlinearVectorAutoregression class that had
  been left as a module-level string literal in blocks/_input_to_node.py.
- Replace `raise BaseException(...)` with `raise TypeError(...)` in the ESN and
  ELM partial_fit guards; BaseException should never be raised directly.
- Fix a stray `%s` left in a NodeToNode hidden_layer_size error message.

No behavioral change (aside from the more specific exception type on an
invalid-regressor error path). flake8 clean, suite unchanged (85 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_make_sparse takes a random_state argument but drew the retained-weight
indices from the global numpy RNG (np.random.choice), so the sparsity mask
was not reproducible from a fixed seed. Use the passed random_state.

This makes the sparse weight masks deterministic w.r.t. random_state (the
exact mask for a given seed changes); the test suite is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Coates no longer calls check_random_state in __init__; it stores the raw
  random_state and resolves it in _extract_random_patches, so get_params /
  clone round-trip correctly. Numerically equivalent for a single fit.
- Replace the mutable default arguments Coates(clusterer=KMeans()) and
  lorenz(x_0=[1.0, 1.0, 1.0]) with None sentinels, constructing the default
  inside the method/function to avoid a shared mutable instance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve the remaining strict-mypy findings so `mypy src/pyrcn` is clean, and
drop the informational `continue-on-error` from the CI type-check step.

- util.batched: annotate the generator return as Iterator[tuple].
- util.value_to_tuple: correct the signature (value may be a tuple; size is an
  int) and always return.
- BatchIntrinsicPlasticity: initialize self._m/_c as floats (they are assigned
  float distribution parameters).
- Coates: type the pooling axis as `int | None`, and assert self.clusterer is
  not None at the post-fit use sites (it is resolved to KMeans() in fit).

No behavioral change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply the same modernization used for the package to tests/: PEP 604 / PEP 585
type syntax and f-strings (pyupgrade --py310-plus), consistent import sorting
with `from __future__ import annotations` (isort), and removal of unused
imports (autoflake). No test logic changed; suite unchanged (85 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@renierts
renierts merged commit e22a799 into dev Jul 18, 2026
4 checks passed
@renierts
renierts deleted the modernize-sklearn branch July 18, 2026 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant