Skip to content

[ENH] Add the Torsk anomaly detector - #3758

Open
lazizbekravshanov wants to merge 4 commits into
aeon-toolkit:mainfrom
lazizbekravshanov:torsk-1636
Open

[ENH] Add the Torsk anomaly detector#3758
lazizbekravshanov wants to merge 4 commits into
aeon-toolkit:mainfrom
lazizbekravshanov:torsk-1636

Conversation

@lazizbekravshanov

Copy link
Copy Markdown

Closes #1636

Why

Torsk (Heim and Avery 2019) is an echo state network detector that TimeEval includes in its benchmark set, and #1636 asks for it in aeon. The placement and fidelity questions were settled in the issue thread: it lives under series/deep_learning following the VLDB TSAD taxonomy, and it follows the TimeEval configuration rather than the full spatial input map stack of the paper.

Design and deviations from the paper

The detector keeps a fixed random sparse reservoir and a random input map drawn once in _fit. _predict slides over the series, refits a linear readout at every position, runs it freely for prediction_window_size frames, and turns the prediction errors into a Gaussian tail normality score that is mapped back onto the time points each prediction covers.

Every deviation from the paper is listed in the class docstring Notes section, in the same style as DWT_MLEAD. The ones worth a reviewer's attention:

  • Readout solver. The reference implementations offer a Tikhonov solver but implement it incorrectly, adding the scalar constant to every element of the Gram matrix. Solving the normal equations at all squares the condition number of an already underdetermined system (39 rows against 111 columns at defaults). This implementation solves by truncated SVD of the design matrix, which is the reference's own default pinv_svd path. The difference is not cosmetic: on the two Exathlon series with 165 and 239 channels, ridge normal equations scored 0.55 and 0.52 ROC AUC and took 62 and 130 seconds; truncated SVD scores 0.63 and 0.64, which is reference parity, in 8 and 12 seconds.
  • Sign of the normality score. Equation 3 in the paper has the difference reversed and would flag drops in error rather than spikes. Both reference codebases use the opposite sign. This follows the code.
  • Input scaling. Like TimeEval, the series is min max scaled to the range minus one to one using the global minimum and maximum of the series being scored, which includes any anomaly. The default input_scale depends on that range, so it is kept, and the leak is stated in Notes rather than hidden.
  • Single input map, no IMED. The paper's spatially aware map stack is for two dimensional fields and does not apply to ordinary series. IMED whitening is applied asymmetrically upstream and is left out.

The method is strongly seed sensitive. The docstring says so and recommends setting random_state.

What

  • aeon/anomaly_detection/series/deep_learning/_torsk.py, the Torsk class
  • aeon/anomaly_detection/series/deep_learning/__init__.py and tests/__init__.py, new subpackage
  • aeon/anomaly_detection/series/deep_learning/tests/test_torsk.py, output, multivariate, determinism, input validation and channel mismatch tests
  • docs/api_reference/anomaly_detection.rst, new Deep learning section

No new dependencies. The core is numpy plus scipy.sparse, scipy.sparse.linalg.eigs and scipy.special.erf, all already core. check_estimator(Torsk) passes all 22 checks, the doctest runs, and the full anomaly_detection suite is green.

One determinism detail worth knowing: scipy.sparse.linalg.eigs draws its ARPACK start vector from the global RNG unless v0 is passed, which silently breaks random_state reproducibility. The reservoir construction passes a seeded v0, and test_torsk_determinism guards it.

Evidence

As asked in the thread, the implementation was evaluated against the TimeEval reference on real benchmark datasets rather than only on synthetic series. Both implementations ran on identical data, 27 datasets from 19 TimeEval collections loaded through aeon.datasets.load_anomaly_detection and the curated loaders, 7 seeds each, single threaded.

Summary: all 27 datasets agree within the pooled seed standard deviation. A Wilcoxon signed rank test on the per dataset differences gives p = 0.229, so no significant difference between the two implementations. Mean absolute difference in ROC AUC is 0.041. Mean runtime is 1.65 s for this implementation against 4.72 s for the reference, 2.9 times faster, mostly from the solver.

The reference was run from the vendored TimeEval code with marshmallow pinned below 3.13, since that code predates the default field rename. The evaluation script is self contained and I am happy to attach it or turn it into a benchmarking example if that is useful.

Per dataset ROC AUC, mean ± sd over 7 seeds
dataset length channels aeon ROC AUC TimeEval ROC AUC diff
CalIt2 / CalIt2-traffic 5040 2 0.581 ± 0.033 0.558 ± 0.031 +0.022
Daphnet / S05R01E3 9600 9 0.539 ± 0.037 0.549 ± 0.022 -0.010
Daphnet / S09R01E0 9600 9 0.874 ± 0.083 0.848 ± 0.066 +0.026
Dodgers / 101-freeway-traffic 50400 1 0.537 ± 0.034 0.546 ± 0.034 -0.008
Exathlon / 10 2 1000000 67 10250 165 0.605 ± 0.039 0.630 ± 0.053 -0.026
Exathlon / 9 2 1000000 66 7481 239 0.643 ± 0.038 0.619 ± 0.036 +0.024
Genesis / genesis-anomalies 16220 18 0.652 ± 0.186 0.653 ± 0.099 -0.001
KDD-TSAD / 053 UCR Anomaly DISTORTEDWalkingAceleration1 6684 1 0.514 ± 0.163 0.651 ± 0.145 -0.138
KDD-TSAD / 054 UCR Anomaly DISTORTEDWalkingAceleration5 6684 1 0.757 ± 0.217 0.798 ± 0.124 -0.041
Metro / metro-traffic-volume 48204 5 0.500 ± 0.032 0.492 ± 0.037 +0.008
NAB / art daily flatmiddle 4032 1 0.548 ± 0.201 0.614 ± 0.205 -0.066
NAB / art daily jumpsdown 4032 1 0.428 ± 0.130 0.533 ± 0.237 -0.105
NASA-MSL / F-4 3422 1 0.390 ± 0.168 0.417 ± 0.154 -0.027
NASA-MSL / P-11 3535 1 0.689 ± 0.163 0.742 ± 0.197 -0.053
NASA-SMAP / A-5 4693 1 0.968 ± 0.017 0.975 ± 0.017 -0.006
NASA-SMAP / A-6 4453 1 0.977 ± 0.011 0.974 ± 0.013 +0.003
NormA / Discords marotta valve tek 14 5000 1 0.711 ± 0.138 0.602 ± 0.227 +0.109
NormA / Discords patient respiration2 4000 1 0.683 ± 0.146 0.756 ± 0.125 -0.073
Occupancy / room-occupancy-1 9752 5 0.618 ± 0.047 0.634 ± 0.051 -0.016
SMD / machine-1-6 23689 38 0.501 ± 0.035 0.494 ± 0.022 +0.007
SMD / machine-3-4 23687 38 0.719 ± 0.048 0.689 ± 0.091 +0.030
TSB-UAD-artificial / -104 2 0.01 5 3030 1 0.561 ± 0.202 0.581 ± 0.129 -0.019
TSB-UAD-artificial / -20 2 0.02 3 4692 1 0.589 ± 0.118 0.686 ± 0.129 -0.097
TSB-UAD-synthetic / SMAP add point outlier 0.02-SMAPF-4 data 3422 1 0.511 ± 0.100 0.455 ± 0.074 +0.056
TSB-UAD-synthetic / SMAP add point outlier 0.05-SMAPF-4 data 3422 1 0.501 ± 0.069 0.475 ± 0.047 +0.026
ecg diff count 3 10000 1 0.834 ± 0.106 0.798 ± 0.103 +0.036
kdd tsad 135 7501 1 0.446 ± 0.255 0.533 ± 0.227 -0.087
mean 0.625 0.641 -0.016

Open questions

  • Short series. On a 20 point series (the length the general test suite uses) the score is all zeros, because the normality score needs a baseline of tens of windows. It passes check_estimator but by being degenerate rather than by working. Would you rather it return zeros there or raise for series below some minimum length?
  • spectral_radius default. The paper and every nmheim experiment use 1.5; nmheim's default_params.json and the TimeEval manifest use 2.0. Benchmarking could not separate them. This ships 2.0 for consistency with the TimeEval configuration, and switching is one line.
  • Global scaling. Whether scaling by the statistics of the series being scored is acceptable as a default, given it leaks the anomaly into preprocessing. Kept for fidelity to TimeEval.

AI disclosure

AI assistance was used for navigating the codebase, drafting the docstring and test scaffolding, and running the benchmark comparison. The algorithm reading, parameter choices, the deviations list, the solver investigation and every line of the final code were reviewed and run by me. Numbers in the Evidence section are from local runs on this branch.

This pull request includes code written with the assistance of AI.
The code has not yet been reviewed by a human.

Copilot AI lite review requested due to automatic review settings August 24, 2026 17:31
@aeon-actions-bot aeon-actions-bot Bot added anomaly detection Anomaly detection package enhancement New feature, improvement request or other non-bug code enhancement labels Aug 24, 2026
@aeon-actions-bot

Copy link
Copy Markdown
Contributor

Thank you for contributing to aeon

I have added the following labels to this PR based on the title: [ enhancement ].
I have added the following labels to this PR based on the changes made: [ anomaly detection ]. Feel free to change these if they do not properly represent the PR.

The Checks tab will show the status of our automated tests. You can click on individual test runs in the tab or "Details" in the panel below to see more information if there is a failure.

If our pre-commit code quality check fails, please run pre-commit locally and push the fixes to your PR branch.

Don't hesitate to ask questions on the aeon Discord channel if you have any.

PR CI actions

These checkboxes will add labels to enable or disable CI functionality for this PR. This may not take effect immediately, and a new commit may be required to run the new configuration.

  • Run pre-commit checks for all files
  • Run mypy typecheck tests
  • Run all pytest tests and configurations
  • Run all notebook example tests
  • Run numba-disabled codecov tests
  • Disable numba cache loading
  • Regenerate expected results for testing
  • Push an empty commit to re-run CI checks

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds the Torsk echo-state-network (ESN) anomaly detector to aeon.anomaly_detection.series under the new deep_learning subpackage, aligning the implementation and defaults with the TimeEval configuration and exposing it in the anomaly detection API docs.

Changes:

  • Introduces Torsk detector implementation (_torsk.py) with reservoir construction, sliding readout fitting, and normality-based scoring.
  • Adds a new deep_learning anomaly detection subpackage with tests (output shape/localisation, multivariate support, determinism, and validation).
  • Updates the anomaly detection API reference docs to include a new Deep-learning section.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
aeon/anomaly_detection/series/deep_learning/_torsk.py Implements the Torsk detector and its scoring logic.
aeon/anomaly_detection/series/deep_learning/__init__.py Exposes Torsk at the deep-learning subpackage level.
aeon/anomaly_detection/series/deep_learning/tests/test_torsk.py Adds unit tests for output, multivariate behavior, determinism, and validation.
aeon/anomaly_detection/series/deep_learning/tests/__init__.py Initializes the deep learning test package.
docs/api_reference/anomaly_detection.rst Documents the new Deep-learning section and Torsk API entry.

This pull request includes code written with the assistance of AI.
The code has not yet been reviewed by a human.


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +223 to +233
if self.spectral_radius <= 0.0:
raise ValueError("spectral_radius must be positive")
if self.transient_window_size >= self.train_window_size:
raise ValueError(
"transient_window_size must be smaller than train_window_size"
)
if self.normality_small_window < 1 or self.normality_large_window < 1:
raise ValueError("normality window sizes must be at least 1")
if not 0.0 <= self.rcond < 1.0:
raise ValueError("rcond must be in [0, 1)")

Comment on lines +156 to +160
normality_small_window: int = 10,
normality_large_window: int = 100,
rcond: float = 1e-4,
random_state: int | None = None,
):
lazizbekravshanov added a commit to lazizbekravshanov/aeon that referenced this pull request Aug 24, 2026
Responds to the two automated review comments on aeon-toolkit#3758. prediction_window_size=0
previously reached a division by zero and transient_window_size=train_window_size-1
an IndexError inside the SVD; both now fail fast with a ValueError, as does a
negative transient_window_size, which was silently accepted. random_state is
annotated int | np.random.RandomState | None to match the docstring and the
other detectors.
@lazizbekravshanov

Copy link
Copy Markdown
Author

Both points were valid, addressed in a9bece9.

  1. Parameter validation. Confirmed all three failure modes before fixing: prediction_window_size=0 reached a float division by zero, transient_window_size=train_window_size-1 an IndexError inside the SVD, and a negative transient_window_size was silently accepted and produced scores. _check_params now requires train_window_size >= 2, prediction_window_size >= 1 and transient_window_size in [0, train_window_size - 2], with one test per new ValueError.

  2. random_state annotation. Now int | np.random.RandomState | None, matching the docstring and the annotations in _cblof.py and _iforest.py.

Implements Torsk (Heim & Avery 2019, arXiv:1909.01709) as a series anomaly
detector following the TimeEval configuration: a fixed random sparse ESN
reservoir, a ridge readout refitted at every sliding position, autoregressive
prediction, and a Gaussian tail normality score over prediction errors.

Deviations from the paper and both reference implementations are documented
in the class docstring Notes section. Pure numpy and scipy, no new
dependencies. File is unregistered pending the placement decision in aeon-toolkit#1636.
…t#1636)

Places the detector under deep_learning per the VLDB TSAD taxonomy discussed in
the issue, adds the subpackage, registers Torsk in the API reference, and sets
the module maintainer.
Replaces the ridge normal equations solver with a truncated SVD of the design
matrix, matching the reference implementation's default pinv_svd path. Forming
the Gram matrix squares the condition number of an already underdetermined
system, which measurably degrades accuracy and runtime once
window_size * n_channels grows large: on Exathlon series with 165 and 239
channels the SVD solver improves ROC AUC from 0.55/0.52 to 0.63/0.64 (reference
parity) and is 7 to 11 times faster. Renames tikhonov_beta to rcond, the
relative singular value cutoff.
Responds to the two automated review comments on aeon-toolkit#3758. prediction_window_size=0
previously reached a division by zero and transient_window_size=train_window_size-1
an IndexError inside the SVD; both now fail fast with a ValueError, as does a
negative transient_window_size, which was silently accepted. random_state is
annotated int | np.random.RandomState | None to match the docstring and the
other detectors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

anomaly detection Anomaly detection package enhancement New feature, improvement request or other non-bug code enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ENH] Add DL anomaly detection algorithms: Torsk

2 participants