|
| 1 | +<div align="center"> |
| 2 | + |
| 3 | +<img src="docs/assets/hero.png" alt="CNSD — Causal Neuro-Symbolic Diagnosis" width="100%"> |
| 4 | + |
| 5 | +<br> |
| 6 | + |
| 7 | +[](LICENSE) |
| 8 | +[](https://www.python.org/downloads/) |
| 9 | +[](CHANGELOG.md) |
| 10 | +[](.github/workflows) |
| 11 | +[](https://github.com/astral-sh/ruff) |
| 12 | +[](#citation) |
| 13 | + |
| 14 | +**A five-layer framework that _verifies_ bearing-fault diagnoses against physics and causal structure — instead of trusting a neural network's confidence alone.** |
| 15 | + |
| 16 | +[Quickstart](#-quickstart) · [How it works](#-how-it-works) · [Results](#-results) · [API](#-usage) · [Reproduce](#-reproducing-the-experiments) · [Citation](#-citation) |
| 17 | + |
| 18 | +</div> |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## What is CNSD? |
| 23 | + |
| 24 | +**CNSD (Causal Neuro-Symbolic Diagnosis)** is a Python framework for diagnosing rolling-element **bearing faults** from vibration signals. A neural network proposes a diagnosis; then three independent layers check that proposal against the **physics of the signal** and the **causal structure of the machine**, and a consensus layer turns the result into an actionable maintenance decision. |
| 25 | + |
| 26 | +The problem it solves is **operating-condition shift**: a model trained at one load or speed silently loses accuracy at another, and its confidence — the usual signal for "can I trust this prediction?" — is the first thing to break. CNSD answers a question a plain classifier cannot: *is this predicted fault physically present in the signal, and would it survive a change of operating condition?* |
| 27 | + |
| 28 | +```python |
| 29 | +from cnsd import CNSD, Dataset |
| 30 | + |
| 31 | +data = Dataset.from_arrays(signals, labels, condition, fs=12000) |
| 32 | +report = CNSD().fit(data).diagnose(data) |
| 33 | + |
| 34 | +print(report.summary()) |
| 35 | +# HIGH_CONFIDENCE: 61% RELIABLE: 22% UNCERTAIN: 9% MANUAL_REVIEW: 8% |
| 36 | +# physics verification rate: CONFIRMED 71% · CONFLICT 18% · INCONCLUSIVE 11% |
| 37 | +``` |
| 38 | + |
| 39 | +> **Why it matters.** On two cross-domain benchmarks the physics layer is a **significantly better reliability signal than deep ensembles** (p < 0.005), and under heavy noise it catches **up to 100%** of the ensemble's confident mistakes. Where the task is easy, it honestly reports **no advantage** — see [Results](#-results). |
| 40 | +
|
| 41 | +--- |
| 42 | + |
| 43 | +## Table of Contents |
| 44 | + |
| 45 | +- [Quickstart](#-quickstart) |
| 46 | +- [How it works](#-how-it-works) |
| 47 | +- [Results](#-results) |
| 48 | +- [Features](#-features) |
| 49 | +- [Installation](#-installation) |
| 50 | +- [Usage](#-usage) |
| 51 | +- [Configuration](#-configuration) |
| 52 | +- [Repository structure](#-repository-structure) |
| 53 | +- [Reproducing the experiments](#-reproducing-the-experiments) |
| 54 | +- [Extending CNSD](#-extending-cnsd-to-new-machinery) |
| 55 | +- [Project status & roadmap](#-project-status--roadmap) |
| 56 | +- [Contributing](#-contributing) |
| 57 | +- [Citation](#-citation) |
| 58 | +- [License](#-license) |
| 59 | + |
| 60 | +--- |
| 61 | + |
| 62 | +## 🚀 Quickstart |
| 63 | + |
| 64 | +**Install** (core is dependency-light — no TensorFlow required to use the physics and causal tools): |
| 65 | + |
| 66 | +```bash |
| 67 | +pip install cnsd # core |
| 68 | +pip install "cnsd[all]" # + perception (TensorFlow) and counterfactual (DoWhy) |
| 69 | +``` |
| 70 | + |
| 71 | +**Diagnose a dataset in five lines.** Bring your own arrays — signals, labels, the operating condition per window, and the sampling rate: |
| 72 | + |
| 73 | +```python |
| 74 | +import numpy as np |
| 75 | +from cnsd import CNSD, Dataset |
| 76 | + |
| 77 | +X = np.random.randn(200, 1024) # 200 windows of 1024 samples |
| 78 | +y = np.random.randint(0, 10, 200) # fault labels |
| 79 | +cond = np.random.choice([0, 1, 2, 3], 200) # operating condition per window |
| 80 | +data = Dataset.from_arrays(X, y, cond, fs=12000) |
| 81 | + |
| 82 | +report = CNSD().fit(data).diagnose(data) |
| 83 | + |
| 84 | +print(report.summary()) |
| 85 | +for statement in report.root_causes()[:5]: |
| 86 | + print(statement) # e.g. "outer-race comb at 107 Hz (2 harmonics) supports predicted Outer fault" |
| 87 | + |
| 88 | +print(f"{len(report.conflicts())} units flagged for review (physics disagrees with the network).") |
| 89 | +``` |
| 90 | + |
| 91 | +Runnable versions live in [`examples/`](examples/): [`quickstart.py`](examples/quickstart.py), [`public_api_demo.py`](examples/public_api_demo.py), and [`unseen_data.py`](examples/unseen_data.py). |
| 92 | + |
| 93 | +--- |
| 94 | + |
| 95 | +## 🧠 How it works |
| 96 | + |
| 97 | +CNSD is a **propose → verify → decide** pipeline. The network only *proposes*; the diagnosis is not trusted until physics and causal reasoning have checked it. |
| 98 | + |
| 99 | +<div align="center"> |
| 100 | +<img src="docs/assets/architecture.png" alt="CNSD five-layer architecture" width="95%"> |
| 101 | +</div> |
| 102 | + |
| 103 | +| Layer | Role | What it produces | |
| 104 | +|------|------|------------------| |
| 105 | +| **1 · Perception** | A 1-D CNN (or self-supervised **S-JEPA** backbone) classifies the vibration window. | predicted fault `ŷ`, confidence `c` | |
| 106 | +| **2 · Symbolic** | Checks the prediction against the bearing's **characteristic frequencies** in the envelope spectrum. Independently names the fault family the *signal* supports. | verdict `CONFIRMED` / `CONFLICT` / `INCONCLUSIVE`, root cause, maintenance action | |
| 107 | +| **3 · Causal (Rung 2)** | Estimates the interventional effect of the **operating condition** `do(Z)` on a corrected structural causal model (vibration does **not** cause the fault), with a refutation suite. | interventional warrant, CATE, invariance test | |
| 108 | +| **3B · Counterfactual (Rung 3)** | On an invertible SCM, asks how the diagnosis would move under a different condition, using a continuous, model-independent severity outcome (RMS). | per-unit counterfactual stability | |
| 109 | +| **4 · Consensus** | Fuses the network confidence with the physics verdict. **A physical conflict can veto a confident network.** | `HIGH_CONFIDENCE` / `RELIABLE` / `UNCERTAIN` / `MANUAL_REVIEW` | |
| 110 | + |
| 111 | +**The core idea in one sentence:** a bearing defect strikes at a *characteristic frequency* fixed by geometry and shaft speed — a signature that is invariant to load by mechanical law, computable at any operating condition, and never consulted by a plain classifier. CNSD reads it directly and uses it to verify, and if necessary overrule, the network. See [`docs/architecture.md`](docs/architecture.md) for the full design. |
| 112 | + |
| 113 | +--- |
| 114 | + |
| 115 | +## 📊 Results |
| 116 | + |
| 117 | +Across three public bearing datasets, we compare reliability estimators at **matched coverage** (every estimator is allowed to trust the same number of predictions; the reported **gap Δ** is `accuracy(trusted) − accuracy(rest)`). Full log: [`EXPERIMENTS.md`](EXPERIMENTS.md). |
| 118 | + |
| 119 | +<div align="center"> |
| 120 | +<img src="docs/assets/results.png" alt="Physics verification vs. deep ensemble across datasets" width="72%"> |
| 121 | +</div> |
| 122 | + |
| 123 | +| Dataset | Regime | Physics Δ | Ensemble Δ | Physics vs. Ensemble | Verdict | |
| 124 | +|---------|--------|-----------|------------|----------------------|---------| |
| 125 | +| **PU** (900→1500 rpm) | shifted | **+0.538** | +0.386 | **p = 0.0032** | ✅ significant win | |
| 126 | +| **XJTU-SY** (cross-condition) | shifted | **+0.460** | +0.381 | **p = 0.0028** | ✅ significant win | |
| 127 | +| **CWRU** (cross-load) | saturated | +0.189 | +0.074 | p = 0.072 | ⚪ null (reported as control) | |
| 128 | + |
| 129 | +**Noise robustness.** As additive noise increases, the physics layer catches the ensemble's *confident* errors: **100% at 0 dB on XJTU-SY**, ~40% on the saturated CWRU regime. |
| 130 | + |
| 131 | +> **On the CWRU null.** We report it at equal prominence because it is *evidence*, not a weakness. CNSD's thesis is that mechanistic verification helps where the signal is degraded and the network is unsure — and adds nothing where predictions are already reliable. CWRU's cross-load task is comparatively saturated, so the absence of an advantage there is exactly what the mechanism predicts. |
| 132 | +
|
| 133 | +--- |
| 134 | + |
| 135 | +## ✨ Features |
| 136 | + |
| 137 | +| | Feature | Detail | |
| 138 | +|---|---------|--------| |
| 139 | +| 🔬 | **Physics verification** | Envelope-spectrum check against BPFO/BPFI/BSF/FTF with harmonic aggregation and FFT-resolution-adaptive tolerance. | |
| 140 | +| ⚖️ | **Three-valued verdict** | `CONFIRMED` / `CONFLICT` / `INCONCLUSIVE` — the engine *abstains* when the physics can't adjudicate, instead of guessing. | |
| 141 | +| 🧾 | **Explainable output** | Every diagnosis carries a named component, characteristic frequencies, a root-cause statement, and a maintenance action. | |
| 142 | +| 🎯 | **Runtime causal reasoning** | Pearl Rung-2 `do(Z)` intervention on a corrected SCM, with CATE, cross-load invariance, and a DoWhy refutation suite. | |
| 143 | +| 🔮 | **Counterfactuals** | Pearl Rung-3 on an invertible SCM with a model-independent severity outcome; graceful sensitivity-analysis fallback. | |
| 144 | +| 🛡️ | **Physics veto** | A confident-but-unsupported prediction is escalated to human review — the failure a confidence-only pipeline can't catch. | |
| 145 | +| 🔌 | **Pluggable physics** | `bearing`, `gear`, and a zero-knowledge `spectral` provider behind one registry; add a mechanism with a single class. | |
| 146 | +| 📦 | **Deployable** | Dependency-light core (no TensorFlow needed), one-call API, universal dataset contract, tested and packaged. | |
| 147 | + |
| 148 | +--- |
| 149 | + |
| 150 | +## 📥 Installation |
| 151 | + |
| 152 | +**Requirements:** Python ≥ 3.11. |
| 153 | + |
| 154 | +```bash |
| 155 | +# from PyPI (core only — numpy / scipy / scikit-learn / pyyaml) |
| 156 | +pip install cnsd |
| 157 | + |
| 158 | +# with optional extras |
| 159 | +pip install "cnsd[perception]" # 1-D CNN / S-JEPA backbone (TensorFlow, Keras) |
| 160 | +pip install "cnsd[counterfactual]" # Rung-3 counterfactuals (DoWhy, pandas, networkx) |
| 161 | +pip install "cnsd[all]" # everything |
| 162 | +``` |
| 163 | + |
| 164 | +**From source** (for development or exact reproduction): |
| 165 | + |
| 166 | +```bash |
| 167 | +git clone https://github.com/abhiprd2000/CNSD.git |
| 168 | +cd CNSD |
| 169 | +pip install -e ".[all]" |
| 170 | +# or pin the exact validated environment: |
| 171 | +pip install -r requirements.txt |
| 172 | +``` |
| 173 | + |
| 174 | +> The core install deliberately avoids heavy dependencies: `from cnsd.causal import intervention_effect_of_condition` and the physics engine work **without TensorFlow**. Only training the CNN perception layer needs the `perception` extra. |
| 175 | +
|
| 176 | +--- |
| 177 | + |
| 178 | +## 🛠️ Usage |
| 179 | + |
| 180 | +CNSD exposes one clean object with four verbs. |
| 181 | + |
| 182 | +### Diagnose |
| 183 | + |
| 184 | +```python |
| 185 | +from cnsd import CNSD, Dataset |
| 186 | + |
| 187 | +model = CNSD(config="cnsd_config.yaml") # or CNSD() for defaults |
| 188 | +report = model.fit(data).diagnose(data) |
| 189 | + |
| 190 | +report.summary() # decision + verification-rate breakdown |
| 191 | +report.root_causes() # human-readable root-cause statements |
| 192 | +report.conflicts() # units where physics disagrees with the network |
| 193 | +report.verification_rate() # CONFIRMED / CONFLICT / INCONCLUSIVE fractions |
| 194 | +report.accuracy_by_verdict() # accuracy within each verdict bucket |
| 195 | +``` |
| 196 | + |
| 197 | +### Explain — the causal warrant (Rung 2) |
| 198 | + |
| 199 | +```python |
| 200 | +effect = model.explain(data) # interventional effect of the operating condition do(Z) |
| 201 | +``` |
| 202 | + |
| 203 | +### What-if — a counterfactual (Rung 3) |
| 204 | + |
| 205 | +```python |
| 206 | +cf = model.what_if(data, intervention={"load": 0.8}, unit_index=0) |
| 207 | +# "what severity would this unit have shown at load 0.8?" |
| 208 | +``` |
| 209 | + |
| 210 | +### Inspect the structural causal model |
| 211 | + |
| 212 | +```python |
| 213 | +scm = model.scm_analysis(data) # the fitted SCM behind the causal layers |
| 214 | +``` |
| 215 | + |
| 216 | +--- |
| 217 | + |
| 218 | +## ⚙️ Configuration |
| 219 | + |
| 220 | +CNSD is driven by a single YAML file. The taxonomy, sampling rate, and bearing geometry live in one place, so a new machine is onboarded by editing config — not code. |
| 221 | + |
| 222 | +```yaml |
| 223 | +# cnsd_config.yaml |
| 224 | +dataset: |
| 225 | + name: "cwru" |
| 226 | + sampling_rate_hz: 12000 |
| 227 | +domain: |
| 228 | + type: "bearing" # -> selects the bearing physics provider |
| 229 | +physics: |
| 230 | + parameters: |
| 231 | + bearing_type: "6205-2RS" |
| 232 | + motor_load_rpm: { 0: 1797, 1: 1772, 2: 1750, 3: 1730 } |
| 233 | +taxonomy: |
| 234 | + classes: |
| 235 | + 0: ["Normal", null] |
| 236 | + 7: ["Outer", 0.007] |
| 237 | + # ... (fault family, defect size in inches) |
| 238 | +``` |
| 239 | + |
| 240 | +--- |
| 241 | + |
| 242 | +## 🗂️ Repository structure |
| 243 | + |
| 244 | +``` |
| 245 | +CNSD/ |
| 246 | +├── README.md # you are here |
| 247 | +├── LICENSE # MIT |
| 248 | +├── cnsd_config.yaml # example configuration |
| 249 | +├── requirements.txt # exact validated environment |
| 250 | +├── setup.py / pyproject.toml # packaging |
| 251 | +├── cnsd/ # the framework (installable package) |
| 252 | +│ ├── api.py # public CNSD class: diagnose / explain / what_if / scm_analysis |
| 253 | +│ ├── builder.py # assembles the five-layer pipeline from config |
| 254 | +│ ├── config.py # YAML config loader |
| 255 | +│ ├── perception/ # Layer 1 — 1-D CNN + S-JEPA backbone |
| 256 | +│ │ └── cnn.py |
| 257 | +│ ├── symbolic/ # Layer 2 — physics verification engine |
| 258 | +│ │ └── engine.py |
| 259 | +│ ├── physics/ # characteristic-frequency kinematics |
| 260 | +│ │ ├── bearing.py # envelope analysis, harmonic prominence, adaptive tolerance |
| 261 | +│ │ ├── gear.py |
| 262 | +│ │ └── providers/ # pluggable domain registry: bearing / gear / spectral |
| 263 | +│ ├── scm/ # the corrected structural causal model |
| 264 | +│ │ └── graph.py |
| 265 | +│ ├── causal/ # Layer 3 — Rung-2 intervention, CATE, invariance |
| 266 | +│ │ ├── estimators.py |
| 267 | +│ │ └── refutation.py # DoWhy refutation suite (+ permutation fallback) |
| 268 | +│ ├── counterfactual/ # Layer 3B — Rung-3 counterfactual |
| 269 | +│ │ ├── rung3.py |
| 270 | +│ │ └── sensitivity.py # sensitivity-analysis fallback |
| 271 | +│ ├── consensus/ # Layer 4 — verdict + confidence -> decision |
| 272 | +│ │ └── fusion.py |
| 273 | +│ ├── datasets/ # universal dataset contract + loaders |
| 274 | +│ │ └── contract.py |
| 275 | +│ └── diagnosis/ # orchestration + the DiagnosisReport object |
| 276 | +│ ├── system.py |
| 277 | +│ └── report.py |
| 278 | +├── validation/ # reproducible cross-domain benchmarks |
| 279 | +│ ├── multi_seed_benchmark.py # 20-seed physics-vs-ensemble benchmark |
| 280 | +│ ├── validate_cwru.py |
| 281 | +│ ├── validate_pu.py |
| 282 | +│ └── validate_xjtusy.py |
| 283 | +├── examples/ # runnable usage demos |
| 284 | +├── test/ # test suite (runs without heavy deps) |
| 285 | +├── docs/ # documentation + README assets |
| 286 | +│ ├── architecture.md # full design document |
| 287 | +│ └── assets/ # README images (hero, architecture, results) |
| 288 | +├── EXPERIMENTS.md # the validation log (every run, incl. nulls) |
| 289 | +├── CONTRIBUTING.md · CODE_OF_CONDUCT.md · SECURITY.md · MAINTAINERS.md |
| 290 | +└── CHANGELOG.md |
| 291 | +``` |
| 292 | + |
| 293 | +--- |
| 294 | + |
| 295 | +## 🔁 Reproducing the experiments |
| 296 | + |
| 297 | +Every headline number in [`EXPERIMENTS.md`](EXPERIMENTS.md) comes from the `validation/` scripts. With a dataset prepared under `data/` (see per-script docstrings) and the pinned environment installed: |
| 298 | + |
| 299 | +```bash |
| 300 | +# 20-seed, matched-coverage physics-vs-ensemble benchmark |
| 301 | +python -m validation.multi_seed_benchmark --dataset pu |
| 302 | +python -m validation.multi_seed_benchmark --dataset xjtusy |
| 303 | +python -m validation.multi_seed_benchmark --dataset cwru |
| 304 | +``` |
| 305 | + |
| 306 | +Seeds are fixed (42–61); numbers should match within run-to-run noise. Data paths are overridable via the `CNSD_DATA_CWRU`, `CNSD_DATA_PU`, and `CNSD_DATA_SEU` environment variables. |
| 307 | + |
| 308 | +--- |
| 309 | + |
| 310 | +## 🔧 Extending CNSD to new machinery |
| 311 | + |
| 312 | +The physics layer is a registry of **providers**. `bearing` is validated here; `gear` and a zero-knowledge `spectral` fallback ship alongside it. A new mechanism is one class: |
| 313 | + |
| 314 | +```python |
| 315 | +from cnsd.physics.providers import register_provider, BaseProvider |
| 316 | + |
| 317 | +class MyProvider(BaseProvider): |
| 318 | + def characteristic_frequencies(self, rpm, geometry): ... |
| 319 | + def dominant_family(self, envelope_spectrum): ... |
| 320 | + |
| 321 | +register_provider("my_machine", MyProvider) |
| 322 | +# now: domain.type = "my_machine" in cnsd_config.yaml |
| 323 | +``` |
| 324 | + |
| 325 | +> **Scope note.** CNSD is validated on **bearings**, where the characteristic-frequency physics is exact. See [`EXPERIMENTS.md`](EXPERIMENTS.md) for all logs. |
| 326 | +
|
| 327 | +--- |
| 328 | + |
| 329 | +## 📌 Project status & roadmap |
| 330 | + |
| 331 | +CNSD is **v1.0.0** and one step from public release. The framework, physics/causal/counterfactual layers, and cross-domain benchmarks are complete and tested. |
| 332 | + |
| 333 | +- [x] Five-layer pipeline with physics veto |
| 334 | +- [x] Rung-2 intervention + refutation suite; Rung-3 counterfactual |
| 335 | +- [x] Three-dataset cross-domain validation (PU, XJTU-SY, CWRU) |
| 336 | +- [x] Pluggable provider registry; dependency-light core |
| 337 | +- [ ] Gear-mesh provider (mechanism-agnostic verification) |
| 338 | +......and continued |
| 339 | +See [`CHANGELOG.md`](CHANGELOG.md) for version history. |
| 340 | + |
| 341 | +--- |
| 342 | + |
| 343 | +## 🤝 Contributing |
| 344 | + |
| 345 | +Contributions are welcome. The workflow, integrity norms (honest nulls, no fabricated results, auto-generated artifacts), and review process are in [`CONTRIBUTING.md`](CONTRIBUTING.md). In short: |
| 346 | + |
| 347 | +1. Fork → branch → make your change. |
| 348 | +2. Run `ruff check . && ruff format .` and `pytest test/` before submitting. |
| 349 | +3. Open a PR describing the change; CI must be green. |
| 350 | + |
| 351 | +Please also read the [Code of Conduct](CODE_OF_CONDUCT.md) and, for vulnerabilities, the [Security Policy](SECURITY.md). |
| 352 | + |
| 353 | +--- |
| 354 | + |
| 355 | +## 📝 Citation |
| 356 | + |
| 357 | +If you use CNSD, please cite: |
| 358 | + |
| 359 | +```bibtex |
| 360 | +@software{cnsd2026, |
| 361 | + title = {CNSD: Causal Neuro-Symbolic Diagnosis for Bearing Fault Diagnosis}, |
| 362 | + author = {Prasad, Abhimanyu and Mahmud, Kazi Tasfin}, |
| 363 | + year = {2026}, |
| 364 | + url = {https://github.com/abhiprd2000/CNSD}, |
| 365 | + version = {1.0.0}, |
| 366 | +} |
| 367 | +``` |
| 368 | + |
| 369 | + |
| 370 | +--- |
| 371 | + |
| 372 | +## 📄 License |
| 373 | + |
| 374 | +Released under the [MIT License](LICENSE). © 2026 the CNSD authors. |
| 375 | + |
| 376 | +--- |
| 377 | + |
| 378 | +<div align="center"> |
| 379 | +<sub>Built for machines that shouldn't fail silently.</sub> |
| 380 | +</div> |
0 commit comments