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.
File without changes
File without changes
@@ -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
+ )