Causal Neuro-Symbolic Diagnosis - a five-layer fault-diagnosis framework
Project description
A five-layer framework that verifies bearing-fault diagnoses against physics and causal structure — instead of trusting a neural network's confidence alone.
Quickstart · How it works · Results · API · Reproduce · Citation
What is CANSYD?
CANSYD (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.
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. CANSYD 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?
from cansyd import CANSYD, Dataset
data = Dataset.from_arrays(signals, labels, condition, fs=12000)
report = CANSYD().fit(data).diagnose(data)
print(report.summary())
# HIGH_CONFIDENCE: 61% RELIABLE: 22% UNCERTAIN: 9% MANUAL_REVIEW: 8%
# physics verification rate: CONFIRMED 71% · CONFLICT 18% · INCONCLUSIVE 11%
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.
Background & provenance
CANSYD is the consolidated, tested implementation of a line of prior work by the authors: an earlier CNSD research prototype and a set of exploratory notebooks developing the physics-verification and causal layers. Those materials are being prepared for public release and will be linked here. This repository is the release-grade framework built from that research.
Table of Contents
- Quickstart
- How it works
- Results
- Features
- Installation
- Usage
- Configuration
- Repository structure
- Reproducing the experiments
- Extending CANSYD
- Project status & roadmap
- Contributing
- Citation
- License
🚀 Quickstart
Install (core is dependency-light — no TensorFlow required to use the physics and causal tools):
pip install cansyd # core
pip install "cansyd[all]" # + perception (TensorFlow) and counterfactual (DoWhy)
Diagnose a dataset in five lines. Bring your own arrays — signals, labels, the operating condition per window, and the sampling rate:
import numpy as np
from cansyd import CANSYD, Dataset
X = np.random.randn(200, 1024) # 200 windows of 1024 samples
y = np.random.randint(0, 10, 200) # fault labels
cond = np.random.choice([0, 1, 2, 3], 200) # operating condition per window
data = Dataset.from_arrays(X, y, cond, fs=12000)
report = CANSYD().fit(data).diagnose(data)
print(report.summary())
for statement in report.root_causes()[:5]:
print(statement) # e.g. "outer-race comb at 107 Hz (2 harmonics) supports predicted Outer fault"
print(f"{len(report.conflicts())} units flagged for review (physics disagrees with the network).")
Runnable versions live in examples/: quickstart.py, public_api_demo.py, and unseen_data.py.
🧠 How it works
CANSYD is a propose → verify → decide pipeline. The network only proposes; the diagnosis is not trusted until physics and causal reasoning have checked it.
| Layer | Role | What it produces |
|---|---|---|
| 1 · Perception | A 1-D CNN (or self-supervised S-JEPA backbone) classifies the vibration window. | predicted fault ŷ, confidence c |
| 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 |
| 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 |
| 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 |
| 4 · Consensus | Fuses the network confidence with the physics verdict. A physical conflict can veto a confident network. | HIGH_CONFIDENCE / RELIABLE / UNCERTAIN / MANUAL_REVIEW |
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. CANSYD reads it directly and uses it to verify, and if necessary overrule, the network. See docs/architecture.md for the full design.
📊 Results
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.
| Dataset | Regime | Physics Δ | Ensemble Δ | Physics vs. Ensemble | Verdict |
|---|---|---|---|---|---|
| PU (900→1500 rpm) | shifted | +0.538 | +0.386 | p = 0.0032 | ✅ significant win |
| XJTU-SY (cross-condition) | shifted | +0.460 | +0.381 | p = 0.0028 | ✅ significant win |
| CWRU (cross-load) | saturated | +0.189 | +0.074 | p = 0.072 | ⚪ null (reported as control) |
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.
On the CWRU null. We report it at equal prominence because it is evidence, not a weakness. CANSYD'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.
✨ Features
| Feature | Detail | |
|---|---|---|
| 🔬 | Physics verification | Envelope-spectrum check against BPFO/BPFI/BSF/FTF with harmonic aggregation and FFT-resolution-adaptive tolerance. |
| ⚖️ | Three-valued verdict | CONFIRMED / CONFLICT / INCONCLUSIVE — the engine abstains when the physics can't adjudicate, instead of guessing. |
| 🧾 | Explainable output | Every diagnosis carries a named component, characteristic frequencies, a root-cause statement, and a maintenance action. |
| 🎯 | Runtime causal reasoning | Pearl Rung-2 do(Z) intervention on a corrected SCM, with CATE, cross-load invariance, and a DoWhy refutation suite. |
| 🔮 | Counterfactuals | Pearl Rung-3 on an invertible SCM with a model-independent severity outcome; graceful sensitivity-analysis fallback. |
| 🛡️ | Physics veto | A confident-but-unsupported prediction is escalated to human review — the failure a confidence-only pipeline can't catch. |
| 🔌 | Pluggable physics | bearing, gear, and a zero-knowledge spectral provider behind one registry; add a mechanism with a single class. |
| 📦 | Deployable | Dependency-light core (no TensorFlow needed), one-call API, universal dataset contract, tested and packaged. |
📥 Installation
Requirements: Python ≥ 3.11.
# from PyPI (core only — numpy / scipy / scikit-learn / pyyaml)
pip install cansyd
# with optional extras
pip install "cansyd[perception]" # 1-D CNN / S-JEPA backbone (TensorFlow, Keras)
pip install "cansyd[counterfactual]" # Rung-3 counterfactuals (DoWhy, pandas, networkx)
pip install "cansyd[all]" # everything
From source (for development or exact reproduction):
git clone https://github.com/shodhx/cansyd.git
cd CANSYD
pip install -e ".[all]"
# or pin the exact validated environment:
pip install -r requirements.txt
The core install deliberately avoids heavy dependencies:
from cansyd.causal import intervention_effect_of_conditionand the physics engine work without TensorFlow. Only training the CNN perception layer needs theperceptionextra.
🛠️ Usage
CANSYD exposes one clean object with four verbs.
Diagnose
from cansyd import CANSYD, Dataset
model = CANSYD(config="cansyd_config.yaml") # or CANSYD() for defaults
report = model.fit(data).diagnose(data)
report.summary() # decision + verification-rate breakdown
report.root_causes() # human-readable root-cause statements
report.conflicts() # units where physics disagrees with the network
report.verification_rate() # CONFIRMED / CONFLICT / INCONCLUSIVE fractions
report.accuracy_by_verdict() # accuracy within each verdict bucket
Explain — the causal warrant (Rung 2)
effect = model.explain(data) # interventional effect of the operating condition do(Z)
What-if — a counterfactual (Rung 3)
cf = model.what_if(data, intervention={"load": 0.8}, unit_index=0)
# "what severity would this unit have shown at load 0.8?"
Inspect the structural causal model
scm = model.scm_analysis(data) # the fitted SCM behind the causal layers
⚙️ Configuration
CANSYD 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.
# cansyd_config.yaml
dataset:
name: "cwru"
sampling_rate_hz: 12000
domain:
type: "bearing" # -> selects the bearing physics provider
physics:
parameters:
bearing_type: "6205-2RS"
motor_load_rpm: { 0: 1797, 1: 1772, 2: 1750, 3: 1730 }
taxonomy:
classes:
0: ["Normal", null]
7: ["Outer", 0.007]
# ... (fault family, defect size in inches)
🗂️ Repository structure
CANSYD/
├── README.md # you are here
├── LICENSE # MIT
├── cansyd_config.yaml # example configuration
├── requirements.txt # exact validated environment
├── setup.py / pyproject.toml # packaging
├── cansyd/ # the framework (installable package)
│ ├── api.py # public CANSYD class: diagnose / explain / what_if / scm_analysis
│ ├── builder.py # assembles the five-layer pipeline from config
│ ├── config.py # YAML config loader
│ ├── perception/ # Layer 1 — 1-D CNN + S-JEPA backbone
│ │ └── cnn.py
│ ├── symbolic/ # Layer 2 — physics verification engine
│ │ └── engine.py
│ ├── physics/ # characteristic-frequency kinematics
│ │ ├── bearing.py # envelope analysis, harmonic prominence, adaptive tolerance
│ │ ├── gear.py
│ │ └── providers/ # pluggable domain registry: bearing / gear / spectral
│ ├── scm/ # the corrected structural causal model
│ │ └── graph.py
│ ├── causal/ # Layer 3 — Rung-2 intervention, CATE, invariance
│ │ ├── estimators.py
│ │ └── refutation.py # DoWhy refutation suite (+ permutation fallback)
│ ├── counterfactual/ # Layer 3B — Rung-3 counterfactual
│ │ ├── rung3.py
│ │ └── sensitivity.py # sensitivity-analysis fallback
│ ├── consensus/ # Layer 4 — verdict + confidence -> decision
│ │ └── fusion.py
│ ├── datasets/ # universal dataset contract + loaders
│ │ └── contract.py
│ └── diagnosis/ # orchestration + the DiagnosisReport object
│ ├── system.py
│ └── report.py
├── validation/ # reproducible cross-domain benchmarks
│ ├── multi_seed_benchmark.py # 20-seed physics-vs-ensemble benchmark
│ ├── validate_cwru.py
│ ├── validate_pu.py
│ └── validate_xjtusy.py
├── examples/ # runnable usage demos
├── test/ # test suite (runs without heavy deps)
├── docs/ # documentation + README assets
│ ├── architecture.md # full design document
│ └── assets/ # README images (hero, architecture, results)
├── EXPERIMENTS.md # the validation log (every run, incl. nulls)
├── CONTRIBUTING.md · CODE_OF_CONDUCT.md · SECURITY.md · MAINTAINERS.md
└── CHANGELOG.md
🔁 Reproducing the experiments
Every headline number in EXPERIMENTS.md comes from the validation/ scripts. With a dataset prepared under data/ (see per-script docstrings) and the pinned environment installed:
# 20-seed, matched-coverage physics-vs-ensemble benchmark
python -m validation.multi_seed_benchmark --dataset pu
python -m validation.multi_seed_benchmark --dataset xjtusy
python -m validation.multi_seed_benchmark --dataset cwru
Seeds are fixed (42–61); numbers should match within run-to-run noise. Data paths are overridable via the CANSYD_DATA_CWRU, CANSYD_DATA_PU, and CANSYD_DATA_XJTU-SY environment variables.
🔧 Extending CANSYD to new machinery
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:
from cansyd.physics.providers import register_provider, BaseProvider
class MyProvider(BaseProvider):
def characteristic_frequencies(self, rpm, geometry): ...
def dominant_family(self, envelope_spectrum): ...
register_provider("my_machine", MyProvider)
# now: domain.type = "my_machine" in cansyd_config.yaml
Scope note. CANSYD is validated on bearings, where the characteristic-frequency physics is exact. See
EXPERIMENTS.mdfor all logs.
📌 Project status & roadmap
CANSYD is v1.0.0 and one step from public release. The framework, physics/causal/counterfactual layers, and cross-domain benchmarks are complete and tested.
- Five-layer pipeline with physics veto
- Rung-2 intervention + refutation suite; Rung-3 counterfactual
- Three-dataset cross-domain validation (PU, XJTU-SY, CWRU)
- Pluggable provider registry; dependency-light core
- Gear-mesh provider (mechanism-agnostic verification)
See ROADMAP.md for where CANSYD is headed and how to help, and CHANGELOG.md for version history.
🤝 Contributing
Contributions are welcome. The workflow, integrity norms (honest nulls, no fabricated results, auto-generated artifacts), and review process are in CONTRIBUTING.md. In short:
- Fork → branch → make your change.
- Run
ruff check . && ruff format .andpytest test/before submitting. - Open a PR describing the change; CI must be green.
Please also read the Code of Conduct and, for vulnerabilities, the Security Policy.
📝 Citation
If you use CANSYD, please cite:
@software{cansyd2026,
title = {CANSYD: Causal Neuro-Symbolic Diagnosis},
author = {Prasad, Abhimanyu and Mahmud, Kazi Tasfin},
year = {2026},
url = {https://github.com/shodhx/cansyd},
version = {1.0.0},
}
📄 License
Released under the MIT License. © 2026 the CANSYD authors.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cansyd-1.0.0.tar.gz.
File metadata
- Download URL: cansyd-1.0.0.tar.gz
- Upload date:
- Size: 1.3 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0d0977c8e8af1d338d135f7d6ea04a41bfb099c2d98ffbfd318d1fbc6f677f5c
|
|
| MD5 |
d5f67d51460d3a79e813b0284d57f61b
|
|
| BLAKE2b-256 |
504a8fd22390bd0766e77de57036477ce9c5ddf7451378885d3eae02b5f658be
|
File details
Details for the file cansyd-1.0.0-py3-none-any.whl.
File metadata
- Download URL: cansyd-1.0.0-py3-none-any.whl
- Upload date:
- Size: 45.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7a241dbc62fc555351aa68c49a8fe2127147097fdf297c10775f06ae147d59d2
|
|
| MD5 |
1a6cc2ebec7dfb9615113d4081eccf10
|
|
| BLAKE2b-256 |
81255f55d3f2c8a046a39a6fde66bf354b1b1901e92e30b2597f9a28e3b81ac1
|