Skip to content

Commit 2519b25

Browse files
committed
add: per-layer timing benchmark (Table VII verification)
1 parent a9bcf4e commit 2519b25

1 file changed

Lines changed: 220 additions & 0 deletions

File tree

validation/time_layers.py

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
"""
2+
CANSYD — Per-layer inference timing benchmark for Table VII.
3+
4+
Measures the per-sample inference time of each CANSYD layer on synthetic
5+
vibration data (L=2048, single CPU core). Reports mean ± std over 1000
6+
iterations in a format ready for the paper.
7+
8+
Usage:
9+
cd _cansyd_repo
10+
python -m validation.time_layers
11+
12+
Hardware should be reported in the paper caption alongside these results.
13+
"""
14+
15+
import time
16+
import platform
17+
import numpy as np
18+
19+
# ── Configuration ───────────────────────────────────────────────────────────
20+
L = 2048 # window length (matches Table VII)
21+
N_ITER = 1000 # iterations per layer
22+
FS = 12000 # Hz (CWRU sampling rate)
23+
RPM = 1797 # CWRU drive-end shaft speed
24+
N_WARMUP = 50 # warmup iterations (excluded from timing)
25+
26+
np.random.seed(42)
27+
28+
# ── Generate synthetic vibration data ───────────────────────────────────────
29+
# Realistic: outer-race fault signal + noise (matches the paper's scenario)
30+
fr = RPM / 60.0
31+
f_bpfo = 4.5 * fr * (1 - 0.2033) # approximate BPFO for 6205
32+
t = np.arange(L) / FS
33+
signal = np.sin(2 * np.pi * f_bpfo * t) * 0.5 + np.random.randn(L) * 0.3
34+
signal = signal.astype(np.float32)
35+
36+
# Pre-generate batch for causal layer (needs multiple samples)
37+
N_BATCH = 200
38+
signals_batch = np.random.randn(N_BATCH, L).astype(np.float32)
39+
labels_batch = (np.random.rand(N_BATCH) > 0.5).astype(int)
40+
conditions_batch = np.random.choice([0, 1, 2, 3], size=N_BATCH)
41+
42+
43+
def time_function(func, n_iter=N_ITER, n_warmup=N_WARMUP):
44+
"""Time a function over n_iter calls, return per-call time in ms."""
45+
# Warmup
46+
for _ in range(n_warmup):
47+
func()
48+
49+
times = []
50+
for _ in range(n_iter):
51+
t0 = time.perf_counter()
52+
func()
53+
t1 = time.perf_counter()
54+
times.append((t1 - t0) * 1000) # ms
55+
56+
times = np.array(times)
57+
return float(times.mean()), float(times.std())
58+
59+
60+
# ============================================================================
61+
# Layer 1: CNN (1D-Conv + softmax)
62+
# ============================================================================
63+
def time_cnn():
64+
try:
65+
import os
66+
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
67+
import tensorflow as tf
68+
tf.config.set_visible_devices([], 'GPU') # force CPU
69+
70+
from cansyd.perception.cnn import build_cnn
71+
model = build_cnn(input_shape=(L, 1), num_classes=10)
72+
x = signal.reshape(1, L, 1)
73+
74+
# Warmup
75+
for _ in range(N_WARMUP):
76+
model.predict(x, verbose=0)
77+
78+
times = []
79+
for _ in range(N_ITER):
80+
t0 = time.perf_counter()
81+
model.predict(x, verbose=0)
82+
t1 = time.perf_counter()
83+
times.append((t1 - t0) * 1000)
84+
85+
times = np.array(times)
86+
return float(times.mean()), float(times.std())
87+
except ImportError:
88+
print(" [SKIP] TensorFlow not installed — CNN timing unavailable")
89+
return None, None
90+
except Exception as e:
91+
print(f" [SKIP] CNN timing failed: {e}")
92+
return None, None
93+
94+
95+
# ============================================================================
96+
# Layer 2: Physics (FFT + envelope spectrum + prominence)
97+
# ============================================================================
98+
def time_physics():
99+
from cansyd.physics.bearing import fault_frequency_evidence
100+
101+
def run():
102+
fault_frequency_evidence(signal, RPM, fs=FS)
103+
104+
return time_function(run)
105+
106+
107+
# ============================================================================
108+
# Layer 3a: Causal (ATE + permutation placebo)
109+
# ============================================================================
110+
def time_causal():
111+
from cansyd.causal.estimators import analyze_causal, signal_kurtosis
112+
113+
treatment = signal_kurtosis(signals_batch)
114+
115+
def run():
116+
analyze_causal(
117+
treatment, labels_batch, conditions_batch,
118+
domain='timing_benchmark'
119+
)
120+
121+
# Causal layer uses 1000 permutations internally — time for 1 call
122+
# and report as per-batch (divide by N_BATCH for per-sample)
123+
times = []
124+
for _ in range(N_WARMUP):
125+
run()
126+
for _ in range(min(N_ITER, 100)): # fewer iters (each call is expensive)
127+
t0 = time.perf_counter()
128+
run()
129+
t1 = time.perf_counter()
130+
times.append((t1 - t0) * 1000 / N_BATCH) # per-sample
131+
132+
times = np.array(times)
133+
return float(times.mean()), float(times.std())
134+
135+
136+
# ============================================================================
137+
# Layer 3b: Counterfactual (local sensitivity fallback)
138+
# ============================================================================
139+
def time_counterfactual():
140+
from cansyd.counterfactual.sensitivity import local_sensitivity
141+
142+
def run():
143+
local_sensitivity(signal, condition_actual=0, condition_perturbed=3)
144+
145+
return time_function(run)
146+
147+
148+
# ============================================================================
149+
# Layer 4: Consensus Router
150+
# ============================================================================
151+
def time_consensus():
152+
from cansyd.consensus.fusion import fuse
153+
154+
def run():
155+
fuse('CONFIRMED', 0.95)
156+
157+
return time_function(run)
158+
159+
160+
# ============================================================================
161+
# Main
162+
# ============================================================================
163+
if __name__ == '__main__':
164+
print("=" * 65)
165+
print("CANSYD Per-Layer Inference Timing Benchmark")
166+
print("=" * 65)
167+
print(f" Window length (L): {L}")
168+
print(f" Iterations: {N_ITER}")
169+
print(f" Warmup: {N_WARMUP}")
170+
print(f" Platform: {platform.processor()}")
171+
print(f" Python: {platform.python_version()}")
172+
print(f" CPU: {platform.machine()}")
173+
print()
174+
175+
results = {}
176+
177+
print("[1/5] CNN (1D-Conv + softmax)...")
178+
m, s = time_cnn()
179+
if m is not None:
180+
results['1. CNN (1D-Conv + softmax)'] = (m, s)
181+
print(f" {m:.2f} ± {s:.2f} ms")
182+
183+
print("[2/5] Physics (FFT + envelope)...")
184+
m, s = time_physics()
185+
results['2. Physics (FFT + envelope)'] = (m, s)
186+
print(f" {m:.2f} ± {s:.2f} ms")
187+
188+
print("[3/5] Causal (ATE + permutation)...")
189+
m, s = time_causal()
190+
results['3a. Causal (ATE + permutation)'] = (m, s)
191+
print(f" {m:.2f} ± {s:.2f} ms/sample")
192+
193+
print("[4/5] Counterfactual (sensitivity)...")
194+
m, s = time_counterfactual()
195+
results['3b. Counterfactual (abduction)'] = (m, s)
196+
print(f" {m:.2f} ± {s:.2f} ms")
197+
198+
print("[5/5] Consensus router...")
199+
m, s = time_consensus()
200+
results['4. Consensus router'] = (m, s)
201+
print(f" {m:.4f} ± {s:.4f} ms")
202+
203+
# ── Summary Table ───────────────────────────────────────────────────────
204+
print()
205+
print("=" * 65)
206+
print("RESULTS (for Table VII)")
207+
print("=" * 65)
208+
print(f"{'Layer':<35} {'Time (ms)':>12} {'± std':>10}")
209+
print("-" * 60)
210+
211+
total_mean = 0.0
212+
for layer, (m, s) in results.items():
213+
print(f" {layer:<33} {m:>10.2f} ±{s:>7.2f}")
214+
total_mean += m
215+
216+
print("-" * 60)
217+
print(f" {'TOTAL':<33} {total_mean:>10.2f}")
218+
print()
219+
print("Copy these values into Table VII of main.tex.")
220+
print("Report hardware in the table caption.")

0 commit comments

Comments
 (0)