telemetry-anomdet 0.1.0__tar.gz → 0.3.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.
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/LICENSE +1 -1
- telemetry_anomdet-0.3.0/PKG-INFO +226 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/README.md +61 -9
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/pyproject.toml +46 -24
- telemetry_anomdet-0.3.0/src/telemetry_anomdet/evaluation.py +388 -0
- telemetry_anomdet-0.3.0/src/telemetry_anomdet/ingest/__init__.py +41 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/ingest/csv_loader.py +13 -5
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/ingest/dataset.py +3 -6
- telemetry_anomdet-0.3.0/src/telemetry_anomdet/ingest/esa.py +347 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/ingest/smap.py +29 -3
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/models/__init__.py +3 -2
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/models/base.py +7 -7
- telemetry_anomdet-0.3.0/src/telemetry_anomdet/models/deep/__init__.py +18 -0
- telemetry_anomdet-0.3.0/src/telemetry_anomdet/models/deep/_ar.py +186 -0
- telemetry_anomdet-0.3.0/src/telemetry_anomdet/models/deep/_kan.py +353 -0
- telemetry_anomdet-0.3.0/src/telemetry_anomdet/models/deep/_net.py +241 -0
- telemetry_anomdet-0.3.0/src/telemetry_anomdet/models/deep/codegen.py +1034 -0
- telemetry_anomdet-0.3.0/src/telemetry_anomdet/models/deep/distill.py +484 -0
- telemetry_anomdet-0.3.0/src/telemetry_anomdet/models/deep/gdn.py +651 -0
- telemetry_anomdet-0.3.0/src/telemetry_anomdet/models/deep/kan_gdn.py +164 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/models/ensemble.py +1 -1
- telemetry_anomdet-0.3.0/src/telemetry_anomdet/thresholding.py +392 -0
- telemetry_anomdet-0.3.0/src/telemetry_anomdet/version.py +12 -0
- telemetry_anomdet-0.3.0/src/telemetry_anomdet/visualization.py +425 -0
- telemetry_anomdet-0.3.0/src/telemetry_anomdet.egg-info/PKG-INFO +226 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet.egg-info/SOURCES.txt +27 -1
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet.egg-info/requires.txt +9 -2
- telemetry_anomdet-0.3.0/tests/test_csv_loader.py +263 -0
- telemetry_anomdet-0.3.0/tests/test_deep_ar.py +429 -0
- telemetry_anomdet-0.3.0/tests/test_deep_attribution.py +246 -0
- telemetry_anomdet-0.3.0/tests/test_deep_codegen.py +733 -0
- telemetry_anomdet-0.3.0/tests/test_deep_distill.py +218 -0
- telemetry_anomdet-0.3.0/tests/test_deep_gdn.py +268 -0
- telemetry_anomdet-0.3.0/tests/test_deep_kan.py +85 -0
- telemetry_anomdet-0.3.0/tests/test_deep_kangdn.py +110 -0
- telemetry_anomdet-0.3.0/tests/test_deep_smoothing.py +103 -0
- telemetry_anomdet-0.3.0/tests/test_evaluation_prauc.py +87 -0
- telemetry_anomdet-0.3.0/tests/test_evaluation_sequences.py +122 -0
- telemetry_anomdet-0.3.0/tests/test_ingest_esa.py +349 -0
- telemetry_anomdet-0.3.0/tests/test_score_channels.py +167 -0
- telemetry_anomdet-0.3.0/tests/test_threshold_budget.py +124 -0
- telemetry_anomdet-0.3.0/tests/test_thresholding.py +281 -0
- telemetry_anomdet-0.3.0/tests/test_visualization.py +257 -0
- telemetry_anomdet-0.1.0/PKG-INFO +0 -364
- telemetry_anomdet-0.1.0/src/telemetry_anomdet/evaluation.py +0 -167
- telemetry_anomdet-0.1.0/src/telemetry_anomdet/ingest/__init__.py +0 -21
- telemetry_anomdet-0.1.0/src/telemetry_anomdet/version.py +0 -15
- telemetry_anomdet-0.1.0/src/telemetry_anomdet/visualization.py +0 -13
- telemetry_anomdet-0.1.0/src/telemetry_anomdet.egg-info/PKG-INFO +0 -364
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/setup.cfg +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/__init__.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/feature_extraction/features.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/ingest/ccsds_loader.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/models/supervised/__init__.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/models/unsupervised/__init__.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/models/unsupervised/gaussian_nb.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/models/unsupervised/isolation_forest.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/models/unsupervised/kmeans.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/models/unsupervised/pca.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/preprocessing/__init__.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/preprocessing/preprocessing.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet/py.typed +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet.egg-info/dependency_links.txt +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/src/telemetry_anomdet.egg-info/top_level.txt +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/tests/test_base_detector.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/tests/test_ensemble.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/tests/test_evaluation.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/tests/test_import.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/tests/test_preprocessing.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/tests/test_smap.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/tests/test_unsup_kmeans.py +0 -0
- {telemetry_anomdet-0.1.0 → telemetry_anomdet-0.3.0}/tests/test_unsup_pca.py +0 -0
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
same "printed page" as the copyright notice for easier
|
|
187
187
|
identification within third-party archives.
|
|
188
188
|
|
|
189
|
-
Copyright 2026
|
|
189
|
+
Copyright 2025-2026 Julio Anzaldo
|
|
190
190
|
|
|
191
191
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
192
|
you may not use this file except in compliance with the License.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: telemetry-anomdet
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Spacecraft telemetry anomaly detection: classical and graph deep detectors, label-free thresholding, and distillation to C for flight hardware.
|
|
5
|
+
Author-email: Julio Anzaldo <anzaldojuliojesus@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/JulioAnzaldo/telemetry-anomdet
|
|
8
|
+
Project-URL: Repository, https://github.com/JulioAnzaldo/telemetry-anomdet
|
|
9
|
+
Keywords: telemetry,anomaly-detection,machine-learning,python
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: C
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
26
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
27
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
28
|
+
Classifier: Topic :: System :: Monitoring
|
|
29
|
+
Classifier: Typing :: Typed
|
|
30
|
+
Requires-Python: >=3.11
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Requires-Dist: pandas<3.0.0,>=2.2.2
|
|
34
|
+
Requires-Dist: numpy<3.0,>=2.1
|
|
35
|
+
Requires-Dist: scikit-learn<2.0,>=1.3
|
|
36
|
+
Provides-Extra: ccsds
|
|
37
|
+
Requires-Dist: ccsdspy<3.0,>=2.0.0; extra == "ccsds"
|
|
38
|
+
Provides-Extra: deep
|
|
39
|
+
Requires-Dist: torch>=2.0; extra == "deep"
|
|
40
|
+
Provides-Extra: viz
|
|
41
|
+
Requires-Dist: matplotlib>=3.7; extra == "viz"
|
|
42
|
+
Provides-Extra: track
|
|
43
|
+
Requires-Dist: wandb>=0.17; extra == "track"
|
|
44
|
+
Dynamic: license-file
|
|
45
|
+
|
|
46
|
+
<h2 align="center">telemetry-anomdet</h2>
|
|
47
|
+
|
|
48
|
+
<p align="center">
|
|
49
|
+
<a href="https://julioanzaldo.github.io/telemetry-anomdet/"><img alt="Documentation" src="https://img.shields.io/badge/Docs-Online-blue"></a>
|
|
50
|
+
<a href="https://test.pypi.org/project/telemetry-anomdet/"><img alt="Test PyPI" src="https://img.shields.io/badge/Test-PyPI-yellow"></a>
|
|
51
|
+
<a href="https://pypi.org/project/telemetry-anomdet/"><img alt="PyPI" src="https://img.shields.io/pypi/v/telemetry-anomdet?label=PyPi"></a>
|
|
52
|
+
<a href="https://github.com/JulioAnzaldo/telemetry-anomdet/actions/workflows/ci.yml"><img alt="Testing" src="https://img.shields.io/github/actions/workflow/status/JulioAnzaldo/telemetry-anomdet/ci.yml?branch=main&label=Testing&logo=github&logoColor=white"></a>
|
|
53
|
+
<a href="https://github.com/JulioAnzaldo/telemetry-anomdet/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/License-Apache%202.0-blue"></a>
|
|
54
|
+
</p>
|
|
55
|
+
|
|
56
|
+
**telemetry-anomdet** is an open-source anomaly detection toolkit for spacecraft telemetry. It runs classical and graph-based deep detectors behind one interface, selects alarm thresholds without labels, points each alarm at the channels that raised it, and distills a trained detector down to Power of Ten conformant C that runs on flight hardware. SHAP attribution and LLM-generated diagnostic reports are on the roadmap.
|
|
57
|
+
|
|
58
|
+
Benchmarked on SMAP and MSL (NASA) and ESA-ADB (ESA).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
<table>
|
|
63
|
+
<tr>
|
|
64
|
+
<td align="center"><a href="https://julioanzaldo.github.io/telemetry-anomdet/getting_started.html"><b>Getting Started</b></a><br>Installation and setup</td>
|
|
65
|
+
<td align="center"><a href="https://julioanzaldo.github.io/telemetry-anomdet/api/models/base.html"><b>API Reference</b></a><br>BaseDetector, models, ensemble</td>
|
|
66
|
+
<td align="center"><a href="https://julioanzaldo.github.io/telemetry-anomdet/tutorials/real_time_example.html"><b>Tutorial</b></a><br>End-to-end detection example</td>
|
|
67
|
+
<td align="center"><a href="https://julioanzaldo.github.io/telemetry-anomdet/user_guide/pipeline_overview.html"><b>Pipeline Overview</b></a><br>Ingest, detect, explain</td>
|
|
68
|
+
<td align="center"><a href="https://github.com/JulioAnzaldo/telemetry-anomdet/discussions"><b>Discussions</b></a><br>Questions and feedback</td>
|
|
69
|
+
</tr>
|
|
70
|
+
</table>
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Quick Install
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# recommended
|
|
78
|
+
uv add telemetry-anomdet
|
|
79
|
+
|
|
80
|
+
# or with pip
|
|
81
|
+
pip install telemetry-anomdet
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The base install is torch-free. The deep detectors (`GDN`, `KANGDN`) need the
|
|
85
|
+
`deep` extra:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv add "telemetry-anomdet[deep]"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Plotting, including the learned sensor graph, needs the `viz` extra.
|
|
92
|
+
|
|
93
|
+
## Example Usage
|
|
94
|
+
|
|
95
|
+
### Fit the Ensemble on Nominal Telemetry
|
|
96
|
+
|
|
97
|
+
All detectors are trained on **nominal data only**, no anomaly labels used during training. Labels are used exclusively for evaluation.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import numpy as np
|
|
101
|
+
from telemetry_anomdet.models.unsupervised.pca import PCAAnomaly
|
|
102
|
+
from telemetry_anomdet.models.unsupervised.kmeans import KMeansAnomaly
|
|
103
|
+
from telemetry_anomdet.models.ensemble import AnomalyEnsemble
|
|
104
|
+
|
|
105
|
+
# X_train: (n_windows, window_size, n_features), produced by windowify()
|
|
106
|
+
X_train = ...
|
|
107
|
+
|
|
108
|
+
ensemble = AnomalyEnsemble(
|
|
109
|
+
models = {
|
|
110
|
+
"pca": PCAAnomaly(n_components = 10),
|
|
111
|
+
"kmeans": KMeansAnomaly(n_clusters = 5, scale = True),
|
|
112
|
+
},
|
|
113
|
+
combine = "mean",
|
|
114
|
+
normalize = "robust",
|
|
115
|
+
percentile = 95.0,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
ensemble.fit(X_train)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Inference and Anomaly Flags
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
scores = ensemble.decision_function(X_test) # higher = more anomalous
|
|
125
|
+
flags = ensemble.is_anomaly(X_test) # boolean mask at training threshold
|
|
126
|
+
|
|
127
|
+
# Runtime sensitivity override, no retraining needed
|
|
128
|
+
flags_strict = ensemble.is_anomaly(X_test, percentile = 98.0)
|
|
129
|
+
flags_custom = ensemble.is_anomaly(X_test, threshold = 0.75)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Per-Model Score Decomposition
|
|
133
|
+
|
|
134
|
+
`score_components()` returns raw per-model scores before normalization or combination. This is the SHAP hook: perturb input channels, measure how each sensor drives each model's score independently.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
components = ensemble.score_components(X_test)
|
|
138
|
+
# {"pca": array(100,), "kmeans": array(100,)}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Getting the SMAP dataset
|
|
142
|
+
|
|
143
|
+
The benchmarks and examples need a local copy of the NASA SMAP/MSL data, which
|
|
144
|
+
is not redistributed with the toolkit. From the root of the repo:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pip install kaggle
|
|
148
|
+
|
|
149
|
+
# Requires a Kaggle API key in ~/.kaggle/kaggle.json
|
|
150
|
+
kaggle datasets download -d patrickfleith/nasa-anomaly-detection-dataset-smap-msl \
|
|
151
|
+
&& mv nasa-anomaly-detection-dataset-smap-msl.zip data.zip \
|
|
152
|
+
&& unzip -o data.zip \
|
|
153
|
+
&& rm data.zip \
|
|
154
|
+
&& mv data/data tmp && rm -r data && mv tmp data
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
That leaves `data/train/`, `data/test/` and `labeled_anomalies.csv`. Point the
|
|
158
|
+
examples at it:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
export TAD_SMAP_DIR=data # Windows: $env:TAD_SMAP_DIR = "data"
|
|
162
|
+
python examples/smap_demo.py
|
|
163
|
+
python examples/smap_benchmark.py
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Datasets are never committed; `data/` is gitignored.
|
|
167
|
+
|
|
168
|
+
### Coming in Phase 3+
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
# Per-channel SHAP attribution (Phase 3)
|
|
172
|
+
# shap_values = explainer.explain(ensemble, X_anomalous)
|
|
173
|
+
# {"sensor_01": 0.42, "sensor_02": 0.18, ...}
|
|
174
|
+
|
|
175
|
+
# LLM diagnostic report (Phase 4)
|
|
176
|
+
# report = llm_engine.explain(shap_values, context, channel_names)
|
|
177
|
+
# DiagnosticReport(
|
|
178
|
+
# anomaly_type = "power subsystem fault",
|
|
179
|
+
# severity = "high",
|
|
180
|
+
# primary_channels = ["sensor_01", "sensor_07"],
|
|
181
|
+
# explanation = "...",
|
|
182
|
+
# recommended_action = "...",
|
|
183
|
+
# confidence = 0.87,
|
|
184
|
+
# )
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Features
|
|
188
|
+
|
|
189
|
+
**Available now**
|
|
190
|
+
|
|
191
|
+
- **One interface for every detector.** Classical and deep detectors share the same `fit` / `decision_function` / `predict` / `is_anomaly` API, so stacking or swapping models needs no per-model glue.
|
|
192
|
+
- **Graph deviation detectors.** `GDN` and `KANGDN` forecast each channel from its learned top-k neighbours and score the deviation, catching readings that are individually plausible but wrong in relation to each other. `feat_mode` selects how `KANGDN` encodes each channel's history (`linear`, `kan`, `ar_kan`, `ar_kan_residual`).
|
|
193
|
+
- **Which sensor, not just when.** `channel_deviations` and `dominant_channels` attribute each alarm to the channels driving it, and `plot_sensor_graph` draws the graph the detector learned.
|
|
194
|
+
- **Alarms without labels.** `threshold_for_budget` and `dynamic_threshold` pick an operating point from the score distribution alone. Operations can state an alarm rate; they cannot state a recall they have no way to observe.
|
|
195
|
+
- **Honest evaluation.** Event-level scoring alongside the conventional point-adjusted F1, plus a random baseline row in the benchmark. ESA-ADB is scored by its own protocol, with F0.5 as the headline metric.
|
|
196
|
+
- **Runs on flight hardware.** A fitted `KANGDN` distills to a torch-free NumPy evaluator, then to Power of Ten conformant C with golden vectors, for every `feat_mode`. Host and ESP32-S3 targets are in [`targets/`](targets/).
|
|
197
|
+
- **Stacking ensemble** with robust (median + IQR) score normalization, built for anomaly scores that are extreme by definition.
|
|
198
|
+
- **Per-model score decomposition** via `score_components()`, the hook that enables per-channel attribution without retraining.
|
|
199
|
+
- **Spacecraft-native ingestion.** SMAP, MSL, ESA-ADB and CSV load straight into a long-form `TelemetryDataset`, no schema wrangling. The ESA-ADB loader reads the official archive directly, with no preprocessing step; its payloads are pickled, so load only that archive, never untrusted files.
|
|
200
|
+
- **Runtime sensitivity control.** `is_anomaly()` accepts a percentile or threshold override, so operators re-tune without retraining.
|
|
201
|
+
|
|
202
|
+
**On the roadmap**
|
|
203
|
+
|
|
204
|
+
- `TranAD`: transformer-based sequence reconstruction
|
|
205
|
+
- `SHAPExplainer`: per-channel attribution over `score_components()` (Phase 3)
|
|
206
|
+
- LLM diagnostic reports, SHAP chart supplied as an image (Phase 4)
|
|
207
|
+
- Human-in-the-loop threshold feedback (Phase 5)
|
|
208
|
+
|
|
209
|
+
## AI Transparency
|
|
210
|
+
|
|
211
|
+
AI assistants are used while writing and maintaining this project's documentation.
|
|
212
|
+
|
|
213
|
+
What that does not change:
|
|
214
|
+
|
|
215
|
+
- **Numbers come from runs.** Benchmark figures are produced by [`examples/smap_benchmark.py`](examples/smap_benchmark.py) and [`examples/esa_benchmark.py`](examples/esa_benchmark.py), and the hardware results in the deployment docs are measured on an ESP32-S3 by the conformance harness in [`targets/`](targets/). Nothing quoted in these docs is estimated or inferred.
|
|
216
|
+
- **Behavior claims are backed by tests.** The suite gates CI, and the documented API is covered by it.
|
|
217
|
+
|
|
218
|
+
Everything published here is reviewed before it lands.
|
|
219
|
+
|
|
220
|
+
## Getting Help
|
|
221
|
+
|
|
222
|
+
### Discussions
|
|
223
|
+
To suggest improvements, or ask for help, please see [GitHub Discussions](https://github.com/JulioAnzaldo/telemetry-anomdet/discussions)
|
|
224
|
+
|
|
225
|
+
### Bug reports
|
|
226
|
+
To submit a report on any bugs or issues, [open an issue here](https://github.com/JulioAnzaldo/telemetry-anomdet/issues)
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
<h2 align="center">telemetry-anomdet</h2>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<a href="https://julioanzaldo.github.io/telemetry-anomdet/"><img alt="Documentation" src="https://img.shields.io/badge/
|
|
4
|
+
<a href="https://julioanzaldo.github.io/telemetry-anomdet/"><img alt="Documentation" src="https://img.shields.io/badge/Docs-Online-blue"></a>
|
|
5
5
|
<a href="https://test.pypi.org/project/telemetry-anomdet/"><img alt="Test PyPI" src="https://img.shields.io/badge/Test-PyPI-yellow"></a>
|
|
6
|
-
<a href="https://pypi.org/project/telemetry-anomdet/"><img alt="PyPI" src="https://img.shields.io/pypi/v/telemetry-anomdet"></a>
|
|
7
|
-
<a href="https://github.com/JulioAnzaldo/telemetry-anomdet/actions/workflows/ci.yml"><img alt="
|
|
8
|
-
<a href="https://github.com/JulioAnzaldo/telemetry-anomdet/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/
|
|
6
|
+
<a href="https://pypi.org/project/telemetry-anomdet/"><img alt="PyPI" src="https://img.shields.io/pypi/v/telemetry-anomdet?label=PyPi"></a>
|
|
7
|
+
<a href="https://github.com/JulioAnzaldo/telemetry-anomdet/actions/workflows/ci.yml"><img alt="Testing" src="https://img.shields.io/github/actions/workflow/status/JulioAnzaldo/telemetry-anomdet/ci.yml?branch=main&label=Testing&logo=github&logoColor=white"></a>
|
|
8
|
+
<a href="https://github.com/JulioAnzaldo/telemetry-anomdet/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/License-Apache%202.0-blue"></a>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
**telemetry-anomdet** is an open-source anomaly detection toolkit for spacecraft telemetry. It runs
|
|
11
|
+
**telemetry-anomdet** is an open-source anomaly detection toolkit for spacecraft telemetry. It runs classical and graph-based deep detectors behind one interface, selects alarm thresholds without labels, points each alarm at the channels that raised it, and distills a trained detector down to Power of Ten conformant C that runs on flight hardware. SHAP attribution and LLM-generated diagnostic reports are on the roadmap.
|
|
12
|
+
|
|
13
|
+
Benchmarked on SMAP and MSL (NASA) and ESA-ADB (ESA).
|
|
12
14
|
|
|
13
15
|
---
|
|
14
16
|
|
|
@@ -34,6 +36,15 @@ uv add telemetry-anomdet
|
|
|
34
36
|
pip install telemetry-anomdet
|
|
35
37
|
```
|
|
36
38
|
|
|
39
|
+
The base install is torch-free. The deep detectors (`GDN`, `KANGDN`) need the
|
|
40
|
+
`deep` extra:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv add "telemetry-anomdet[deep]"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Plotting, including the learned sensor graph, needs the `viz` extra.
|
|
47
|
+
|
|
37
48
|
## Example Usage
|
|
38
49
|
|
|
39
50
|
### Fit the Ensemble on Nominal Telemetry
|
|
@@ -82,6 +93,33 @@ components = ensemble.score_components(X_test)
|
|
|
82
93
|
# {"pca": array(100,), "kmeans": array(100,)}
|
|
83
94
|
```
|
|
84
95
|
|
|
96
|
+
### Getting the SMAP dataset
|
|
97
|
+
|
|
98
|
+
The benchmarks and examples need a local copy of the NASA SMAP/MSL data, which
|
|
99
|
+
is not redistributed with the toolkit. From the root of the repo:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install kaggle
|
|
103
|
+
|
|
104
|
+
# Requires a Kaggle API key in ~/.kaggle/kaggle.json
|
|
105
|
+
kaggle datasets download -d patrickfleith/nasa-anomaly-detection-dataset-smap-msl \
|
|
106
|
+
&& mv nasa-anomaly-detection-dataset-smap-msl.zip data.zip \
|
|
107
|
+
&& unzip -o data.zip \
|
|
108
|
+
&& rm data.zip \
|
|
109
|
+
&& mv data/data tmp && rm -r data && mv tmp data
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
That leaves `data/train/`, `data/test/` and `labeled_anomalies.csv`. Point the
|
|
113
|
+
examples at it:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
export TAD_SMAP_DIR=data # Windows: $env:TAD_SMAP_DIR = "data"
|
|
117
|
+
python examples/smap_demo.py
|
|
118
|
+
python examples/smap_benchmark.py
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Datasets are never committed; `data/` is gitignored.
|
|
122
|
+
|
|
85
123
|
### Coming in Phase 3+
|
|
86
124
|
|
|
87
125
|
```python
|
|
@@ -106,19 +144,33 @@ components = ensemble.score_components(X_test)
|
|
|
106
144
|
**Available now**
|
|
107
145
|
|
|
108
146
|
- **One interface for every detector.** Classical and deep detectors share the same `fit` / `decision_function` / `predict` / `is_anomaly` API, so stacking or swapping models needs no per-model glue.
|
|
147
|
+
- **Graph deviation detectors.** `GDN` and `KANGDN` forecast each channel from its learned top-k neighbours and score the deviation, catching readings that are individually plausible but wrong in relation to each other. `feat_mode` selects how `KANGDN` encodes each channel's history (`linear`, `kan`, `ar_kan`, `ar_kan_residual`).
|
|
148
|
+
- **Which sensor, not just when.** `channel_deviations` and `dominant_channels` attribute each alarm to the channels driving it, and `plot_sensor_graph` draws the graph the detector learned.
|
|
149
|
+
- **Alarms without labels.** `threshold_for_budget` and `dynamic_threshold` pick an operating point from the score distribution alone. Operations can state an alarm rate; they cannot state a recall they have no way to observe.
|
|
150
|
+
- **Honest evaluation.** Event-level scoring alongside the conventional point-adjusted F1, plus a random baseline row in the benchmark. ESA-ADB is scored by its own protocol, with F0.5 as the headline metric.
|
|
151
|
+
- **Runs on flight hardware.** A fitted `KANGDN` distills to a torch-free NumPy evaluator, then to Power of Ten conformant C with golden vectors, for every `feat_mode`. Host and ESP32-S3 targets are in [`targets/`](targets/).
|
|
109
152
|
- **Stacking ensemble** with robust (median + IQR) score normalization, built for anomaly scores that are extreme by definition.
|
|
110
153
|
- **Per-model score decomposition** via `score_components()`, the hook that enables per-channel attribution without retraining.
|
|
111
|
-
- **Spacecraft-native ingestion.** SMAP and CSV load straight into a long-form `TelemetryDataset`, no schema wrangling.
|
|
154
|
+
- **Spacecraft-native ingestion.** SMAP, MSL, ESA-ADB and CSV load straight into a long-form `TelemetryDataset`, no schema wrangling. The ESA-ADB loader reads the official archive directly, with no preprocessing step; its payloads are pickled, so load only that archive, never untrusted files.
|
|
112
155
|
- **Runtime sensitivity control.** `is_anomaly()` accepts a percentile or threshold override, so operators re-tune without retraining.
|
|
113
156
|
|
|
114
157
|
**On the roadmap**
|
|
115
158
|
|
|
116
|
-
- `
|
|
159
|
+
- `TranAD`: transformer-based sequence reconstruction
|
|
117
160
|
- `SHAPExplainer`: per-channel attribution over `score_components()` (Phase 3)
|
|
118
161
|
- LLM diagnostic reports, SHAP chart supplied as an image (Phase 4)
|
|
119
162
|
- Human-in-the-loop threshold feedback (Phase 5)
|
|
120
|
-
|
|
121
|
-
|
|
163
|
+
|
|
164
|
+
## AI Transparency
|
|
165
|
+
|
|
166
|
+
AI assistants are used while writing and maintaining this project's documentation.
|
|
167
|
+
|
|
168
|
+
What that does not change:
|
|
169
|
+
|
|
170
|
+
- **Numbers come from runs.** Benchmark figures are produced by [`examples/smap_benchmark.py`](examples/smap_benchmark.py) and [`examples/esa_benchmark.py`](examples/esa_benchmark.py), and the hardware results in the deployment docs are measured on an ESP32-S3 by the conformance harness in [`targets/`](targets/). Nothing quoted in these docs is estimated or inferred.
|
|
171
|
+
- **Behavior claims are backed by tests.** The suite gates CI, and the documented API is covered by it.
|
|
172
|
+
|
|
173
|
+
Everything published here is reviewed before it lands.
|
|
122
174
|
|
|
123
175
|
## Getting Help
|
|
124
176
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "telemetry-anomdet"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Spacecraft telemetry anomaly detection: classical and graph deep detectors, label-free thresholding, and distillation to C for flight hardware."
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
license =
|
|
7
|
-
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.11"
|
|
8
9
|
authors = [
|
|
9
10
|
{ name = "Julio Anzaldo", email = "anzaldojuliojesus@gmail.com" }
|
|
10
11
|
]
|
|
@@ -12,50 +13,71 @@ dependencies = [
|
|
|
12
13
|
"pandas>=2.2.2,<3.0.0",
|
|
13
14
|
"numpy>=2.1,<3.0",
|
|
14
15
|
"scikit-learn>=1.3,<2.0",
|
|
15
|
-
"click>=8.4.2,<9.0",
|
|
16
|
-
"typing-extensions>=4.16.0",
|
|
17
16
|
]
|
|
18
17
|
keywords = ["telemetry", "anomaly-detection", "machine-learning", "python"]
|
|
18
|
+
# License is declared as an SPDX expression above, not a classifier: license
|
|
19
|
+
# classifiers are deprecated and setuptools warns on them.
|
|
19
20
|
classifiers = [
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Intended Audience :: Science/Research",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Natural Language :: English",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
20
26
|
"Programming Language :: Python :: 3",
|
|
21
|
-
"Programming Language :: Python :: 3
|
|
27
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
28
|
"Programming Language :: Python :: 3.11",
|
|
23
29
|
"Programming Language :: Python :: 3.12",
|
|
24
30
|
"Programming Language :: Python :: 3.13",
|
|
25
|
-
"
|
|
26
|
-
|
|
31
|
+
"Programming Language :: Python :: 3.14",
|
|
32
|
+
# The toolkit is pure Python; the artifact it generates for flight targets is C.
|
|
33
|
+
"Programming Language :: C",
|
|
27
34
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
35
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
36
|
+
"Topic :: Scientific/Engineering :: Astronomy",
|
|
37
|
+
"Topic :: Software Development :: Libraries",
|
|
38
|
+
"Topic :: Software Development :: Embedded Systems",
|
|
39
|
+
"Topic :: Software Development :: Code Generators",
|
|
40
|
+
"Topic :: System :: Monitoring",
|
|
41
|
+
"Typing :: Typed",
|
|
33
42
|
]
|
|
34
43
|
|
|
35
44
|
[project.optional-dependencies]
|
|
36
45
|
ccsds = [
|
|
37
46
|
"ccsdspy>=2.0.0,<3.0",
|
|
38
47
|
]
|
|
48
|
+
# Phase 2 deep detector (GDN). Optional so the base install stays
|
|
49
|
+
# torch-free; torch is imported lazily at fit time.
|
|
50
|
+
deep = [
|
|
51
|
+
"torch>=2.0",
|
|
52
|
+
]
|
|
53
|
+
# Plotting. Optional so the base install stays lean; matplotlib is imported
|
|
54
|
+
# lazily inside each figure function.
|
|
55
|
+
viz = [
|
|
56
|
+
"matplotlib>=3.7",
|
|
57
|
+
]
|
|
58
|
+
# Experiment tracking for the benchmark scripts. Nothing in the library imports
|
|
59
|
+
# it; examples/smap_benchmark.py logs to it only when TAD_WANDB_PROJECT is set.
|
|
60
|
+
track = [
|
|
61
|
+
"wandb>=0.17",
|
|
62
|
+
]
|
|
39
63
|
|
|
40
64
|
[dependency-groups]
|
|
41
65
|
dev = [
|
|
42
66
|
"pytest>=9.1.1,<10.0",
|
|
43
67
|
"pytest-cov>=7.1.0,<8.0",
|
|
44
|
-
"pre-commit>=4.6.
|
|
68
|
+
"pre-commit>=4.6.2,<5.0",
|
|
45
69
|
"black>=26.5.1,<27.0",
|
|
46
|
-
"ruff>=0.16.
|
|
70
|
+
"ruff>=0.16.2,<1.0",
|
|
47
71
|
]
|
|
48
72
|
docs = [
|
|
49
73
|
"sphinx>=7.0",
|
|
50
|
-
"sphinx-
|
|
74
|
+
"pydata-sphinx-theme>=0.15",
|
|
51
75
|
"sphinx-autodoc-typehints>=2.0",
|
|
52
76
|
"numpydoc>=1.7",
|
|
77
|
+
"sphinx-sitemap>=2.6",
|
|
78
|
+
"sphinxext-opengraph>=0.9",
|
|
79
|
+
"sphinx-notfound-page>=1.0",
|
|
53
80
|
]
|
|
54
|
-
# Phase 2 -- uncomment when GDN/TranAD training starts
|
|
55
|
-
# deep = [
|
|
56
|
-
# "torch>=2.0",
|
|
57
|
-
# "torch-geometric",
|
|
58
|
-
# ]
|
|
59
81
|
# Phase 3
|
|
60
82
|
# xai = [
|
|
61
83
|
# "shap",
|
|
@@ -72,12 +94,12 @@ Homepage = "https://github.com/JulioAnzaldo/telemetry-anomdet"
|
|
|
72
94
|
Repository = "https://github.com/JulioAnzaldo/telemetry-anomdet"
|
|
73
95
|
|
|
74
96
|
[build-system]
|
|
75
|
-
requires = ["setuptools>=
|
|
97
|
+
requires = ["setuptools>=84.0.0", "wheel"]
|
|
76
98
|
build-backend = "setuptools.build_meta"
|
|
77
99
|
|
|
78
100
|
[tool.ruff]
|
|
79
101
|
line-length = 100
|
|
80
|
-
target-version = "
|
|
102
|
+
target-version = "py311"
|
|
81
103
|
src = ["src", "tests"]
|
|
82
104
|
|
|
83
105
|
[tool.ruff.lint]
|