omnipulse 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnipulse-0.1.0/.gitignore +0 -0
- omnipulse-0.1.0/.python-version +0 -0
- omnipulse-0.1.0/PKG-INFO +31 -0
- omnipulse-0.1.0/README.md +0 -0
- omnipulse-0.1.0/pyproject.toml +76 -0
- omnipulse-0.1.0/scripts/generate_sim_data.py +86 -0
- omnipulse-0.1.0/src/transient_wst/__init__.py +41 -0
- omnipulse-0.1.0/src/transient_wst/core.py +287 -0
- omnipulse-0.1.0/src/transient_wst/foundation.py +175 -0
- omnipulse-0.1.0/src/transient_wst/io.py +108 -0
- omnipulse-0.1.0/src/transient_wst/mcp_server.py +161 -0
- omnipulse-0.1.0/src/transient_wst/reduction.py +156 -0
- omnipulse-0.1.0/src/transient_wst/utils.py +112 -0
- omnipulse-0.1.0/tests/__init__.py +0 -0
- omnipulse-0.1.0/tests/conftest.py +0 -0
- omnipulse-0.1.0/tests/test_foundation.py +106 -0
- omnipulse-0.1.0/tests/test_io.py +0 -0
- omnipulse-0.1.0/tests/test_reduction.py +0 -0
- omnipulse-0.1.0/tests/test_scattering.py +280 -0
|
File without changes
|
|
File without changes
|
omnipulse-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: omnipulse
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Enterprise-grade Wavelet Scattering Transform engine for non-stationary time-series analysis. Designed as an MCP-compatible scientific processing backend.
|
|
5
|
+
Author: Samvardhan Singh
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Keywords: anomaly-detection,mcp,scattering-transform,signal-processing,time-series,wavelet
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: kymatio>=0.3.0
|
|
19
|
+
Requires-Dist: mcp>=1.0.0
|
|
20
|
+
Requires-Dist: numpy<3,>=1.26
|
|
21
|
+
Requires-Dist: scikit-learn<2,>=1.5
|
|
22
|
+
Requires-Dist: scipy<1.15,>=1.13
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
28
|
+
Provides-Extra: foundation
|
|
29
|
+
Requires-Dist: torch>=2.2; extra == 'foundation'
|
|
30
|
+
Provides-Extra: gpu
|
|
31
|
+
Requires-Dist: torch>=2.2; extra == 'gpu'
|
|
File without changes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
# ── PEP 621 project metadata ───────────────────────────────────────────────────
|
|
6
|
+
[project]
|
|
7
|
+
name = "omnipulse"
|
|
8
|
+
version = "0.1.0"
|
|
9
|
+
description = "Enterprise-grade Wavelet Scattering Transform engine for non-stationary time-series analysis. Designed as an MCP-compatible scientific processing backend."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Samvardhan Singh" }]
|
|
14
|
+
keywords = ["wavelet", "scattering-transform", "time-series", "signal-processing", "mcp", "anomaly-detection"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# ── Runtime dependencies ───────────────────────────────────────────────────────
|
|
28
|
+
dependencies = [
|
|
29
|
+
"numpy>=1.26,<3",
|
|
30
|
+
"scipy>=1.13,<1.15",
|
|
31
|
+
"kymatio>=0.3.0",
|
|
32
|
+
"scikit-learn>=1.5,<2",
|
|
33
|
+
"mcp>=1.0.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# ── Optional / development extras ─────────────────────────────────────────────
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=8.0",
|
|
40
|
+
"pytest-cov>=5.0",
|
|
41
|
+
"ruff>=0.4",
|
|
42
|
+
"mypy>=1.10",
|
|
43
|
+
]
|
|
44
|
+
gpu = [
|
|
45
|
+
"torch>=2.2",
|
|
46
|
+
]
|
|
47
|
+
foundation = [
|
|
48
|
+
"torch>=2.2",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
# ── Entry points ───────────────────────────────────────────────────────────────
|
|
52
|
+
[project.scripts]
|
|
53
|
+
omnipulse-server = "transient_wst.mcp_server:main"
|
|
54
|
+
|
|
55
|
+
# ── Hatchling build — src layout ──────────────────────────────────────────────
|
|
56
|
+
[tool.hatch.build.targets.wheel]
|
|
57
|
+
packages = ["src/transient_wst"]
|
|
58
|
+
|
|
59
|
+
# ── Ruff linter config ────────────────────────────────────────────────────────
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 100
|
|
62
|
+
target-version = "py310"
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
66
|
+
|
|
67
|
+
# ── Mypy strict type checking ─────────────────────────────────────────────────
|
|
68
|
+
[tool.mypy]
|
|
69
|
+
python_version = "3.10"
|
|
70
|
+
strict = true
|
|
71
|
+
ignore_missing_imports = true
|
|
72
|
+
|
|
73
|
+
# ── Pytest ─────────────────────────────────────────────────────────────────────
|
|
74
|
+
[tool.pytest.ini_options]
|
|
75
|
+
testpaths = ["tests"]
|
|
76
|
+
addopts = "--tb=short -v"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
scripts/generate_sim_data.py
|
|
3
|
+
----------------------------
|
|
4
|
+
Generates synthetic 1-D time-series signals saved as .npy arrays.
|
|
5
|
+
Each file mimics 2 seconds of data at 1000 Hz.
|
|
6
|
+
|
|
7
|
+
Most files contain Gaussian white noise, but specifically:
|
|
8
|
+
- 10 files contain a high-frequency chirp transient.
|
|
9
|
+
- 2 files contain extreme mathematical anomalies (massive flatline, inf spikes).
|
|
10
|
+
|
|
11
|
+
This validates the Agentic QueryEngine's variance-based outlier
|
|
12
|
+
rejection tools.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import argparse
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
def generate_signals(output_dir: str, n_files: int = 50) -> None:
|
|
20
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
21
|
+
fs = 1000
|
|
22
|
+
n_samples = 2 * fs # 2 seconds
|
|
23
|
+
t = np.arange(n_samples) / fs
|
|
24
|
+
|
|
25
|
+
# Deterministic randomness for repeatability
|
|
26
|
+
rng = np.random.default_rng(42)
|
|
27
|
+
|
|
28
|
+
indices = rng.permutation(n_files)
|
|
29
|
+
transient_idx = set(indices[:10])
|
|
30
|
+
anomaly_idx = set(indices[10:12])
|
|
31
|
+
|
|
32
|
+
print(f"Generating {n_files} total files in '{output_dir}'")
|
|
33
|
+
print(f"Injecting transients at indices: {sorted(list(transient_idx))}")
|
|
34
|
+
print(f"Injecting anomalies at indices: {sorted(list(anomaly_idx))}")
|
|
35
|
+
|
|
36
|
+
transient_count = 0
|
|
37
|
+
anomaly_count = 0
|
|
38
|
+
|
|
39
|
+
for i in range(n_files):
|
|
40
|
+
# Base signal: random normal distribution
|
|
41
|
+
signal = rng.standard_normal(n_samples)
|
|
42
|
+
|
|
43
|
+
if i in transient_idx:
|
|
44
|
+
# Inject non-stationary transient (high frequency chirp)
|
|
45
|
+
f0, f1 = 50, 200
|
|
46
|
+
start_i, end_i = 600, 1400
|
|
47
|
+
t_chirp = t[start_i:end_i]
|
|
48
|
+
|
|
49
|
+
# Simple chirp
|
|
50
|
+
chirp = np.sin(2 * np.pi * (f0 + (f1 - f0) * t_chirp / (2 * t_chirp[-1])) * t_chirp)
|
|
51
|
+
window = np.hanning(len(chirp))
|
|
52
|
+
|
|
53
|
+
# Amplified and injected
|
|
54
|
+
signal[start_i:end_i] += chirp * window * 10.0
|
|
55
|
+
transient_count += 1
|
|
56
|
+
|
|
57
|
+
elif i in anomaly_idx:
|
|
58
|
+
# Massive artifacts to trigger artifact rejection
|
|
59
|
+
if anomaly_count % 2 == 0:
|
|
60
|
+
# Extreme infinity spikes simulating disconnected electrode
|
|
61
|
+
signal[800:810] = np.inf
|
|
62
|
+
signal[1500:1510] = -np.inf
|
|
63
|
+
else:
|
|
64
|
+
# Flatline with massive DC offset
|
|
65
|
+
signal[500:1800] = 1_000_000.0
|
|
66
|
+
anomaly_count += 1
|
|
67
|
+
|
|
68
|
+
# Reshape to 2-D (Batch=1, Time=n_samples) and cast to float32
|
|
69
|
+
# Kymatio expects [B, T] inputs
|
|
70
|
+
final_array = signal.reshape(1, -1).astype(np.float32)
|
|
71
|
+
|
|
72
|
+
filename = os.path.join(output_dir, f"signal_{i:03d}.npy")
|
|
73
|
+
np.save(filename, final_array)
|
|
74
|
+
|
|
75
|
+
print("Data generation complete.")
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
parser = argparse.ArgumentParser(description="Generate synthetic .npy data")
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--output",
|
|
81
|
+
type=str,
|
|
82
|
+
default=os.path.join(os.path.dirname(__file__), "..", "data", "raw_sim"),
|
|
83
|
+
help="Output directory to save the arrays"
|
|
84
|
+
)
|
|
85
|
+
args = parser.parse_args()
|
|
86
|
+
generate_signals(args.output)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
transient_wst
|
|
3
|
+
=============
|
|
4
|
+
Enterprise-grade Wavelet Scattering Transform (WST) engine for non-stationary
|
|
5
|
+
time-series data. Designed as a self-contained, MCP-compatible scientific
|
|
6
|
+
processing backend for the Agentic-Wavelet Foundation Pipeline.
|
|
7
|
+
|
|
8
|
+
This package is **domain-agnostic** — it processes any 1-D time-series
|
|
9
|
+
(EEG, FRB, seismic, financial) through a cascaded Kymatio scattering
|
|
10
|
+
transform with optional PCA dimensionality reduction.
|
|
11
|
+
|
|
12
|
+
Public API
|
|
13
|
+
----------
|
|
14
|
+
>>> from transient_wst import WaveletScatteringExtractor, ScatteringResult
|
|
15
|
+
>>> extractor = WaveletScatteringExtractor(J=8, Q=(8, 1))
|
|
16
|
+
>>> result = extractor.transform(signal_array)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
__version__: str = version("transient-wst")
|
|
23
|
+
except PackageNotFoundError: # running from source without install
|
|
24
|
+
__version__ = "0.0.0.dev"
|
|
25
|
+
|
|
26
|
+
# ── Public surface ─────────────────────────────────────────────────────────────
|
|
27
|
+
from transient_wst.core import WaveletScatteringExtractor, ScatteringResult
|
|
28
|
+
from transient_wst.reduction import PCAReducer
|
|
29
|
+
from transient_wst.io import load_npy_directory, save_arrays
|
|
30
|
+
from transient_wst.utils import compute_snr_db, detect_outlier_paths
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"WaveletScatteringExtractor",
|
|
34
|
+
"ScatteringResult",
|
|
35
|
+
"PCAReducer",
|
|
36
|
+
"load_npy_directory",
|
|
37
|
+
"save_arrays",
|
|
38
|
+
"compute_snr_db",
|
|
39
|
+
"detect_outlier_paths",
|
|
40
|
+
"__version__",
|
|
41
|
+
]
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
transient_wst.core
|
|
3
|
+
==================
|
|
4
|
+
Primary WaveletScatteringExtractor class and associated dataclasses.
|
|
5
|
+
|
|
6
|
+
This module wraps Kymatio's ``Scattering1D`` implementation and exposes a
|
|
7
|
+
clean, typed interface consumed by the MCP server (mcp_server.py) and
|
|
8
|
+
directly by downstream Python clients.
|
|
9
|
+
|
|
10
|
+
Mathematical foundation
|
|
11
|
+
-----------------------
|
|
12
|
+
The Wavelet Scattering Transform (WST) constructs a robust signal
|
|
13
|
+
representation through an iterative cascade:
|
|
14
|
+
|
|
15
|
+
* **S₀x(t)** = x * φ(t) — local average
|
|
16
|
+
* **S₁x(t, λ₁)** = |x * ψ_λ₁| * φ(t) — energy per band
|
|
17
|
+
* **S₂x(t, λ₁, λ₂)** = ||x * ψ_λ₁| * ψ_λ₂| * φ(t) — transient energy
|
|
18
|
+
|
|
19
|
+
The representation is translation-invariant and Lipschitz-continuous to
|
|
20
|
+
small time-warping deformations, making it ideal for non-stationary
|
|
21
|
+
transient detection in any high-noise time-series domain.
|
|
22
|
+
|
|
23
|
+
Design contract
|
|
24
|
+
---------------
|
|
25
|
+
* ``WaveletScatteringExtractor`` is intentionally stateless after construction.
|
|
26
|
+
Feed any raw time-series array and receive a ``ScatteringResult``.
|
|
27
|
+
* All numpy dtypes are float32 to maintain GPU memory efficiency.
|
|
28
|
+
* Raises ``ValueError`` on invalid hyperparameter combinations rather than
|
|
29
|
+
silently producing malformed tensors.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import logging
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from typing import Optional
|
|
37
|
+
|
|
38
|
+
import numpy as np
|
|
39
|
+
import numpy.typing as npt
|
|
40
|
+
|
|
41
|
+
from transient_wst.utils import (
|
|
42
|
+
compute_snr_db,
|
|
43
|
+
compute_variance_per_path,
|
|
44
|
+
count_anomalous_values,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Result container
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class ScatteringResult:
|
|
56
|
+
"""Immutable container returned by :meth:`WaveletScatteringExtractor.transform`.
|
|
57
|
+
|
|
58
|
+
Attributes
|
|
59
|
+
----------
|
|
60
|
+
coefficients:
|
|
61
|
+
Raw scattering output tensor of shape ``(B, P, T/2^J)``, where
|
|
62
|
+
``B`` = batch size, ``P`` = total scattering paths,
|
|
63
|
+
``T/2^J`` = temporally down-sampled length.
|
|
64
|
+
snr_db:
|
|
65
|
+
Signal-to-Noise Ratio of the output coefficients in decibels.
|
|
66
|
+
Used by the QueryEngine's autonomous evaluation logic.
|
|
67
|
+
null_count:
|
|
68
|
+
Number of NaN or Inf values detected in the output tensor.
|
|
69
|
+
variance:
|
|
70
|
+
Per-path variance vector of shape ``(P,)``. An abrupt spike in
|
|
71
|
+
any element signals a likely disconnected electrode or artifact.
|
|
72
|
+
meta:
|
|
73
|
+
Arbitrary key/value metadata forwarded from the caller (e.g.,
|
|
74
|
+
subject ID, electrode label).
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
coefficients: npt.NDArray[np.float32]
|
|
78
|
+
snr_db: float
|
|
79
|
+
null_count: int
|
|
80
|
+
variance: npt.NDArray[np.float32]
|
|
81
|
+
meta: dict[str, object] = field(default_factory=dict)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Core extractor
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class WaveletScatteringExtractor:
|
|
90
|
+
"""Cascaded Wavelet Scattering Transform (WST) extractor.
|
|
91
|
+
|
|
92
|
+
Wraps ``kymatio.numpy.Scattering1D`` and exposes a minimal, typed API
|
|
93
|
+
designed for integration with the Agentic MCP pipeline.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
J : int
|
|
98
|
+
Maximum scale of the scattering transform as a power of two.
|
|
99
|
+
Defines the temporal integration window (``2^J`` samples).
|
|
100
|
+
Valid range: ``1 ≤ J ≤ 16``.
|
|
101
|
+
Q : tuple[int, int]
|
|
102
|
+
``(Q1, Q2)`` — number of wavelets per octave for the first- and
|
|
103
|
+
second-order filter banks respectively.
|
|
104
|
+
Recommended for EEG: ``Q=(8, 1)`` to ``Q=(16, 2)``.
|
|
105
|
+
T : int | None
|
|
106
|
+
Explicit averaging scale for the low-pass filter. Defaults to
|
|
107
|
+
``2^J`` when ``None``.
|
|
108
|
+
sampling_rate : float
|
|
109
|
+
Sampling frequency of the input signal in Hz. Used for SNR
|
|
110
|
+
computation and diagnostic logging; not passed to Kymatio directly.
|
|
111
|
+
|
|
112
|
+
Raises
|
|
113
|
+
------
|
|
114
|
+
ValueError
|
|
115
|
+
If ``J`` is outside ``[1, 16]`` or either Q component is < 1.
|
|
116
|
+
|
|
117
|
+
Examples
|
|
118
|
+
--------
|
|
119
|
+
>>> import numpy as np
|
|
120
|
+
>>> from transient_wst import WaveletScatteringExtractor
|
|
121
|
+
>>> rng = np.random.default_rng(0)
|
|
122
|
+
>>> signal = rng.standard_normal((4, 1024)).astype(np.float32) # (B, T)
|
|
123
|
+
>>> extractor = WaveletScatteringExtractor(J=6, Q=(8, 1), sampling_rate=256.0)
|
|
124
|
+
>>> result = extractor.transform(signal)
|
|
125
|
+
>>> result.coefficients.shape # (4, P, T/2^J)
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
J: int,
|
|
131
|
+
Q: tuple[int, int],
|
|
132
|
+
T: Optional[int] = None,
|
|
133
|
+
sampling_rate: float = 256.0,
|
|
134
|
+
) -> None:
|
|
135
|
+
self._validate_hyperparameters(J, Q)
|
|
136
|
+
|
|
137
|
+
self.J = J
|
|
138
|
+
self.Q = Q
|
|
139
|
+
self.T = T if T is not None else 2**J
|
|
140
|
+
self.sampling_rate = sampling_rate
|
|
141
|
+
|
|
142
|
+
# Lazy-initialise Kymatio to avoid import cost at module load time.
|
|
143
|
+
self._scattering = None
|
|
144
|
+
self._n_samples: int | None = None
|
|
145
|
+
logger.info(
|
|
146
|
+
"WaveletScatteringExtractor initialised | J=%d Q=%s T=%d fs=%.1f Hz",
|
|
147
|
+
self.J,
|
|
148
|
+
self.Q,
|
|
149
|
+
self.T,
|
|
150
|
+
self.sampling_rate,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# ------------------------------------------------------------------
|
|
154
|
+
# Public API
|
|
155
|
+
# ------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def transform(
|
|
158
|
+
self,
|
|
159
|
+
signal: npt.NDArray[np.float32],
|
|
160
|
+
meta: Optional[dict[str, object]] = None,
|
|
161
|
+
) -> ScatteringResult:
|
|
162
|
+
"""Apply the cascaded Wavelet Scattering Transform to *signal*.
|
|
163
|
+
|
|
164
|
+
Implements the full S₀, S₁, S₂ cascade:
|
|
165
|
+
S₂x(t, λ₁, λ₂) = ||x * ψ_λ₁| * ψ_λ₂| * φ(t)
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
signal:
|
|
170
|
+
Input array of shape ``(B, T)`` — batch of 1-D time series.
|
|
171
|
+
Dtype is cast to ``float32`` internally if required.
|
|
172
|
+
meta:
|
|
173
|
+
Optional metadata forwarded verbatim into :class:`ScatteringResult`.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
ScatteringResult
|
|
178
|
+
Structured result including coefficients, SNR, null count, and
|
|
179
|
+
per-path variance — exactly the payload the QueryEngine parses.
|
|
180
|
+
|
|
181
|
+
Raises
|
|
182
|
+
------
|
|
183
|
+
ValueError
|
|
184
|
+
If *signal* is not 2-D or contains fewer samples than ``2^J``.
|
|
185
|
+
RuntimeError
|
|
186
|
+
If the underlying Kymatio execution fails.
|
|
187
|
+
"""
|
|
188
|
+
# ── 1. Validate and cast ────────────────────────────────────────
|
|
189
|
+
if signal.ndim != 2:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"Signal must be 2-D (B, T); got {signal.ndim}-D with shape {signal.shape}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
n_samples = signal.shape[1]
|
|
195
|
+
min_samples = 2 ** self.J
|
|
196
|
+
if n_samples < min_samples:
|
|
197
|
+
raise ValueError(
|
|
198
|
+
f"Signal length T={n_samples} is less than 2^J={min_samples}. "
|
|
199
|
+
f"Increase T or decrease J."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
signal = signal.astype(np.float32, copy=False)
|
|
203
|
+
|
|
204
|
+
# ── 2. Lazy-init the Kymatio operator ───────────────────────────
|
|
205
|
+
if self._scattering is None or self._n_samples != n_samples:
|
|
206
|
+
self._build_scattering_operator(n_samples)
|
|
207
|
+
|
|
208
|
+
# ── 3. Execute Kymatio forward pass ─────────────────────────────
|
|
209
|
+
try:
|
|
210
|
+
coefficients = self._scattering(signal)
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
raise RuntimeError(
|
|
213
|
+
f"Kymatio forward pass failed: {exc}"
|
|
214
|
+
) from exc
|
|
215
|
+
|
|
216
|
+
coefficients = np.asarray(coefficients, dtype=np.float32)
|
|
217
|
+
|
|
218
|
+
logger.info(
|
|
219
|
+
"Scattering complete | input=%s → output=%s",
|
|
220
|
+
signal.shape,
|
|
221
|
+
coefficients.shape,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# ── 4. Compute diagnostics ──────────────────────────────────────
|
|
225
|
+
snr_db = compute_snr_db(signal, coefficients)
|
|
226
|
+
null_count = count_anomalous_values(coefficients)
|
|
227
|
+
variance = compute_variance_per_path(coefficients)
|
|
228
|
+
|
|
229
|
+
# ── 5. Return structured result ─────────────────────────────────
|
|
230
|
+
return ScatteringResult(
|
|
231
|
+
coefficients=coefficients,
|
|
232
|
+
snr_db=snr_db,
|
|
233
|
+
null_count=null_count,
|
|
234
|
+
variance=variance,
|
|
235
|
+
meta=meta or {},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# ------------------------------------------------------------------
|
|
239
|
+
# Private helpers
|
|
240
|
+
# ------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
def _build_scattering_operator(self, n_samples: int) -> None:
|
|
243
|
+
"""Lazily instantiate the Kymatio Scattering1D operator.
|
|
244
|
+
|
|
245
|
+
Uses ``kymatio.numpy.Scattering1D`` (CPU backend) for Phase 2.
|
|
246
|
+
GPU acceleration via ``kymatio.torch`` is deferred to Phase 3.
|
|
247
|
+
|
|
248
|
+
Parameters
|
|
249
|
+
----------
|
|
250
|
+
n_samples : int
|
|
251
|
+
Number of time samples ``T`` in the input batch.
|
|
252
|
+
"""
|
|
253
|
+
from kymatio.numpy import Scattering1D
|
|
254
|
+
|
|
255
|
+
self._scattering = Scattering1D(
|
|
256
|
+
J=self.J,
|
|
257
|
+
shape=(n_samples,),
|
|
258
|
+
Q=self.Q,
|
|
259
|
+
T=self.T,
|
|
260
|
+
)
|
|
261
|
+
self._n_samples = n_samples
|
|
262
|
+
|
|
263
|
+
logger.info(
|
|
264
|
+
"Built Scattering1D operator | J=%d Q=%s T=%d shape=(%d,)",
|
|
265
|
+
self.J,
|
|
266
|
+
self.Q,
|
|
267
|
+
self.T,
|
|
268
|
+
n_samples,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def _validate_hyperparameters(J: int, Q: tuple[int, int]) -> None:
|
|
273
|
+
"""Raise ``ValueError`` for out-of-range hyperparameters."""
|
|
274
|
+
if not (1 <= J <= 16):
|
|
275
|
+
raise ValueError(f"J must be in [1, 16]; got {J}.")
|
|
276
|
+
if len(Q) != 2:
|
|
277
|
+
raise ValueError(f"Q must be a 2-tuple (Q1, Q2); got {Q!r}.")
|
|
278
|
+
q1, q2 = Q
|
|
279
|
+
if q1 < 1 or q2 < 1:
|
|
280
|
+
raise ValueError(f"Both Q components must be ≥ 1; got Q={Q}.")
|
|
281
|
+
|
|
282
|
+
def __repr__(self) -> str:
|
|
283
|
+
return (
|
|
284
|
+
f"{self.__class__.__name__}("
|
|
285
|
+
f"J={self.J}, Q={self.Q}, T={self.T}, "
|
|
286
|
+
f"sampling_rate={self.sampling_rate})"
|
|
287
|
+
)
|