|
| 1 | +import matplotlib.pyplot as plt |
| 2 | +import networkx as nx |
| 3 | +import numpy as np |
| 4 | + |
| 5 | +from cnsd import Dataset |
| 6 | +from cnsd.causal import analyze_causal, signal_kurtosis |
| 7 | +from cnsd.diagnosis.system import CNSD |
| 8 | + |
| 9 | +# --------------------------------------------------------- |
| 10 | +# SETUP |
| 11 | +# --------------------------------------------------------- |
| 12 | +np.random.seed(42) |
| 13 | + |
| 14 | +print('Loading datasets...') |
| 15 | +# CWRU |
| 16 | +from validate_run import load_cwru # noqa: E402 |
| 17 | + |
| 18 | +X_cwru, y_cwru, cond_cwru = load_cwru() |
| 19 | +data_cwru = Dataset.from_arrays(X_cwru, y_cwru, cond_cwru, fs=12000, name='CWRU') |
| 20 | + |
| 21 | +# PU |
| 22 | +from cnsd.physics import PhysicsConfig # noqa: E402 |
| 23 | +from validate_pu import load_pu_domain_split # noqa: E402 |
| 24 | + |
| 25 | +train_pu, target_pu = load_pu_domain_split() |
| 26 | +unique_rpm = set(train_pu[2]).union(set(target_pu[2])) |
| 27 | +rpm_map = {float(r): float(r) for r in unique_rpm} |
| 28 | +physics_pu = PhysicsConfig( |
| 29 | + bearing={'n_balls': 8, 'd_ball': 6.75, 'd_pitch': 28.5, 'contact_angle': 0.0}, |
| 30 | + cond_to_rpm=rpm_map, |
| 31 | + fs=64000, |
| 32 | + name='PU-6203', |
| 33 | +) |
| 34 | +data_pu = Dataset.from_arrays( |
| 35 | + target_pu[0], target_pu[1], target_pu[2], fs=64000, physics=physics_pu |
| 36 | +) |
| 37 | + |
| 38 | +# XJTU-SY |
| 39 | +from cnsd.datasets.xjtusy import load_xjtusy_domain_split # noqa: E402 |
| 40 | + |
| 41 | +train_xjtu, target_xjtu = load_xjtusy_domain_split(window_size=4096) |
| 42 | +data_xjtu = target_xjtu |
| 43 | + |
| 44 | +# --------------------------------------------------------- |
| 45 | +# A. LAYER 3 VALIDATION (CAUSAL LAYER) |
| 46 | +# --------------------------------------------------------- |
| 47 | +print('\n--- SECTION A: LAYER 3A (CAUSAL LAYER) ---') |
| 48 | +print('Note: Faults in benchmark datasets are experimentally assigned (compositional).') |
| 49 | +print( |
| 50 | + 'This section serves as a mathematical demonstration of the SCM capability, not as discovered causal effects.' |
| 51 | +) |
| 52 | +datasets_dict = {'CWRU': data_cwru, 'PU': data_pu, 'XJTU': data_xjtu} |
| 53 | + |
| 54 | +causal_results = [] |
| 55 | +for name, data in datasets_dict.items(): |
| 56 | + kurt = signal_kurtosis(data.X) |
| 57 | + res = analyze_causal(kurt, data.y, data.cond, domain=name) |
| 58 | + causal_results.append( |
| 59 | + { |
| 60 | + 'Dataset': name, |
| 61 | + 'ATE (Demo)': f'{res["ate"]:.4f}', |
| 62 | + 'CI': f'[{res["ci"][0]:.4f}, {res["ci"][1]:.4f}]', |
| 63 | + 'Significance': f'p={res["p_value"]:.4f}', |
| 64 | + } |
| 65 | + ) |
| 66 | + |
| 67 | +print(f'{"Dataset":<10} | {"ATE (Demo)":<15} | {"95% CI":<20} | {"Significance"}') |
| 68 | +print('-' * 65) |
| 69 | +for r in causal_results: |
| 70 | + print(f'{r["Dataset"]:<10} | {r["ATE (Demo)"]:<15} | {r["CI"]:<20} | {r["Significance"]}') |
| 71 | + |
| 72 | +# Generate Visual Causal Graph |
| 73 | +G = nx.DiGraph() |
| 74 | +G.add_node('Z\n(Operating\nCondition)', pos=(0, 1)) |
| 75 | +G.add_node('X\n(Signal\nFeatures)', pos=(1, 0)) |
| 76 | +G.add_node('Y\n(Fault\nOutcome)', pos=(2, 1)) |
| 77 | + |
| 78 | +G.add_edges_from( |
| 79 | + [ |
| 80 | + ('Z\n(Operating\nCondition)', 'X\n(Signal\nFeatures)'), |
| 81 | + ('X\n(Signal\nFeatures)', 'Y\n(Fault\nOutcome)'), |
| 82 | + ('Z\n(Operating\nCondition)', 'Y\n(Fault\nOutcome)'), |
| 83 | + ] |
| 84 | +) |
| 85 | + |
| 86 | +pos = nx.get_node_attributes(G, 'pos') |
| 87 | +plt.figure(figsize=(6, 4)) |
| 88 | +nx.draw( |
| 89 | + G, |
| 90 | + pos, |
| 91 | + with_labels=True, |
| 92 | + node_size=6000, |
| 93 | + node_color='lightblue', |
| 94 | + font_size=10, |
| 95 | + font_weight='bold', |
| 96 | + arrowsize=20, |
| 97 | +) |
| 98 | +ax = plt.gca() |
| 99 | +ax.margins(0.20) |
| 100 | +plt.title('Visual Causal Graph (Pearl Rung-2 SCM)') |
| 101 | +plt.savefig('paper_causal_graph.png', dpi=300, bbox_inches='tight') |
| 102 | +plt.close() |
| 103 | +print('Saved paper_causal_graph.png') |
| 104 | + |
| 105 | +# --------------------------------------------------------- |
| 106 | +# B. LAYER 4 VALIDATION (COUNTERFACTUAL LAYER) |
| 107 | +# --------------------------------------------------------- |
| 108 | +print('\n--- SECTION B: COUNTERFACTUAL LAYER ---') |
| 109 | +import tensorflow as tf # noqa: E402 |
| 110 | + |
| 111 | +tf.random.set_seed(42) |
| 112 | + |
| 113 | +# Train CNSD on XJTU |
| 114 | +print('Training CNSD on XJTU-SY for Counterfactual & Decision Layers...') |
| 115 | +model_xjtu = CNSD(conf_thresh=0.90) |
| 116 | +model_xjtu.fit(train_xjtu, epochs=10) # 10 epochs is enough for demonstration |
| 117 | + |
| 118 | +# Pick an example: A severe fault in XJTU target data |
| 119 | +fault_idx_severe = np.where(data_xjtu.y > 0)[0][10] |
| 120 | +actual_cond = data_xjtu.cond[fault_idx_severe] |
| 121 | +cf_cond = 15.0 # Intervene with a genuinely different speed (900 RPM) to see real risk delta |
| 122 | + |
| 123 | +cf_res_severe = model_xjtu.what_if(data_xjtu, fault_idx_severe, cf_cond) |
| 124 | +actual_rms_1 = cf_res_severe['factual'].get('Y', 0.0) |
| 125 | +cf_rms_1 = cf_res_severe['counterfactual'].get('Y', 0.0) |
| 126 | + |
| 127 | +# S2: Marginal fault (e.g., index 1163) |
| 128 | +fault_idx_marginal = np.where(data_xjtu.y > 0)[0][1163] |
| 129 | +cf_res_marginal = model_xjtu.what_if(data_xjtu, fault_idx_marginal, cf_cond) |
| 130 | +actual_rms_2 = cf_res_marginal['factual'].get('Y', 0.0) |
| 131 | +cf_rms_2 = cf_res_marginal['counterfactual'].get('Y', 0.0) |
| 132 | + |
| 133 | +print('Table: Counterfactual Analysis') |
| 134 | +print(f'Scenario | Actual RMS (Z={actual_cond}Hz) | Counterfactual RMS (Z={cf_cond}Hz)') |
| 135 | +print(f'S1 (Severe) | {float(actual_rms_1):.4f} | {float(cf_rms_1):.4f}') |
| 136 | +print(f'S2 (Marginal)| {float(actual_rms_2):.4f} | {float(cf_rms_2):.4f}') |
| 137 | + |
| 138 | +# Plot Grouped Bar Chart |
| 139 | +labels = ['Actual RMS', f'Counterfactual RMS\n(do(Z={cf_cond}))'] |
| 140 | +x = np.arange(len(labels)) |
| 141 | +width = 0.35 |
| 142 | + |
| 143 | +fig, ax = plt.subplots(figsize=(7, 5)) |
| 144 | +rects1 = ax.bar( |
| 145 | + x - width / 2, |
| 146 | + [float(actual_rms_1), float(cf_rms_1)], |
| 147 | + width, |
| 148 | + label='S1 (Severe Fault)', |
| 149 | + color=['darkred', 'darkred'], |
| 150 | +) |
| 151 | +rects2 = ax.bar( |
| 152 | + x + width / 2, |
| 153 | + [float(actual_rms_2), float(cf_rms_2)], |
| 154 | + width, |
| 155 | + label='S2 (Marginal Fault)', |
| 156 | + color=['salmon', 'salmon'], |
| 157 | +) |
| 158 | + |
| 159 | +ax.set_ylabel('Vibration RMS (Continuous)') |
| 160 | +ax.set_title(f'Counterfactual Intervention (do(Z={cf_cond}))') |
| 161 | +ax.set_xticks(x) |
| 162 | +ax.set_xticklabels(labels) |
| 163 | +ax.legend() |
| 164 | + |
| 165 | +# Dynamic ylim based on the max value |
| 166 | +max_val = max(actual_rms_1, cf_rms_1, actual_rms_2, cf_rms_2) |
| 167 | +ax.set_ylim(0, max_val * 1.3) |
| 168 | + |
| 169 | +plt.savefig('paper_counterfactual_risk.png', dpi=300, bbox_inches='tight') |
| 170 | +plt.close() |
| 171 | +print('Saved paper_counterfactual_risk.png') |
| 172 | + |
| 173 | +# --------------------------------------------------------- |
| 174 | +# C. LAYER 5 VALIDATION (DECISION LAYER) |
| 175 | +# --------------------------------------------------------- |
| 176 | +print('\n--- SECTION C: DECISION LAYER ---') |
| 177 | +# Diagnose a balanced subset to save time |
| 178 | +normal_idxs = np.where(data_xjtu.y == 0)[0][:25] |
| 179 | +fault_idxs = np.where(data_xjtu.y > 0)[0][:25] |
| 180 | +idxs = np.concatenate([normal_idxs, fault_idxs]) |
| 181 | +np.random.shuffle(idxs) |
| 182 | + |
| 183 | +subset_data = Dataset.from_arrays( |
| 184 | + data_xjtu.X[idxs], |
| 185 | + data_xjtu.y[idxs], |
| 186 | + data_xjtu.cond[idxs], |
| 187 | + fs=data_xjtu.fs, |
| 188 | + physics=data_xjtu.physics, |
| 189 | + taxonomy=data_xjtu.taxonomy, |
| 190 | +) |
| 191 | +report = model_xjtu.diagnose(subset_data) |
| 192 | + |
| 193 | +print(f'{"Machine":<10} | {"CNSD Recommended Action":<40}') |
| 194 | +print('-' * 55) |
| 195 | + |
| 196 | +maintenance_priority = [] |
| 197 | + |
| 198 | +for i, rec in enumerate(report.records[:10]): |
| 199 | + machine_id = f'M-{i + 1:03d}' |
| 200 | + cnsd_dec = rec['action'] |
| 201 | + |
| 202 | + print(f'{machine_id:<10} | {cnsd_dec:<40}') |
| 203 | + |
| 204 | + # Priority ranking score = CNN confidence + severity if confirmed |
| 205 | + score = rec['cnn_confidence'] * (1 if rec['status'] == 'CONFIRMED_FAULT' else 0) |
| 206 | + maintenance_priority.append((machine_id, score, rec['physics_verdict'])) |
| 207 | + |
| 208 | +print('\nOptional: Maintenance-Priority Ranking') |
| 209 | +maintenance_priority.sort(key=lambda x: x[1], reverse=True) |
| 210 | +for rank, (m_id, score, verdict) in enumerate(maintenance_priority[:5]): |
| 211 | + print(f'Rank {rank + 1}: {m_id} (Priority Score: {score:.2f}, Verdict: {verdict})') |
| 212 | + |
| 213 | +# SECTION F OPERATIONAL UTILITY REMOVED (As requested by Abhi, performance was identical to baseline and weakened the paper) |
0 commit comments