|
| 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