|
| 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: CAUSAL LAYER ---') |
| 48 | +datasets_dict = {'CWRU': data_cwru, 'PU': data_pu, 'XJTU': data_xjtu} |
| 49 | + |
| 50 | +causal_results = [] |
| 51 | +for name, data in datasets_dict.items(): |
| 52 | + kurt = signal_kurtosis(data.X) |
| 53 | + res = analyze_causal(kurt, data.y, data.cond, domain=name) |
| 54 | + causal_results.append( |
| 55 | + { |
| 56 | + 'Dataset': name, |
| 57 | + 'Causal Effect': f'{res["ate"]:.4f}', |
| 58 | + 'CI': f'[{res["ci"][0]:.4f}, {res["ci"][1]:.4f}]', |
| 59 | + 'Significance': f'p={res["p_value"]:.4f}', |
| 60 | + } |
| 61 | + ) |
| 62 | + |
| 63 | +print(f'{"Dataset":<10} | {"Causal Effect":<15} | {"95% CI":<20} | {"Significance"}') |
| 64 | +print('-' * 65) |
| 65 | +for r in causal_results: |
| 66 | + print(f'{r["Dataset"]:<10} | {r["Causal Effect"]:<15} | {r["CI"]:<20} | {r["Significance"]}') |
| 67 | + |
| 68 | +# Generate Visual Causal Graph |
| 69 | +G = nx.DiGraph() |
| 70 | +G.add_node('Z\n(Operating\nCondition)', pos=(0, 1)) |
| 71 | +G.add_node('X\n(Signal\nFeatures)', pos=(1, 0)) |
| 72 | +G.add_node('Y\n(Fault\nOutcome)', pos=(2, 1)) |
| 73 | + |
| 74 | +G.add_edges_from( |
| 75 | + [ |
| 76 | + ('Z\n(Operating\nCondition)', 'X\n(Signal\nFeatures)'), |
| 77 | + ('X\n(Signal\nFeatures)', 'Y\n(Fault\nOutcome)'), |
| 78 | + ('Z\n(Operating\nCondition)', 'Y\n(Fault\nOutcome)'), |
| 79 | + ] |
| 80 | +) |
| 81 | + |
| 82 | +pos = nx.get_node_attributes(G, 'pos') |
| 83 | +plt.figure(figsize=(6, 4)) |
| 84 | +nx.draw( |
| 85 | + G, |
| 86 | + pos, |
| 87 | + with_labels=True, |
| 88 | + node_size=6000, |
| 89 | + node_color='lightblue', |
| 90 | + font_size=10, |
| 91 | + font_weight='bold', |
| 92 | + arrowsize=20, |
| 93 | +) |
| 94 | +ax = plt.gca() |
| 95 | +ax.margins(0.20) |
| 96 | +plt.title('Visual Causal Graph (Pearl Rung-2 SCM)') |
| 97 | +plt.savefig('paper_causal_graph.png', dpi=300, bbox_inches='tight') |
| 98 | +plt.close() |
| 99 | +print('Saved paper_causal_graph.png') |
| 100 | + |
| 101 | +# --------------------------------------------------------- |
| 102 | +# B. LAYER 4 VALIDATION (COUNTERFACTUAL LAYER) |
| 103 | +# --------------------------------------------------------- |
| 104 | +print('\n--- SECTION B: COUNTERFACTUAL LAYER ---') |
| 105 | +import tensorflow as tf # noqa: E402 |
| 106 | + |
| 107 | +tf.random.set_seed(42) |
| 108 | + |
| 109 | +# Train CNSD on XJTU |
| 110 | +print('Training CNSD on XJTU-SY for Counterfactual & Decision Layers...') |
| 111 | +model_xjtu = CNSD(conf_thresh=0.90) |
| 112 | +model_xjtu.fit(train_xjtu, epochs=10) # 10 epochs is enough for demonstration |
| 113 | + |
| 114 | +# Pick an example: A severe fault in XJTU target data |
| 115 | +fault_idx = np.where(data_xjtu.y > 0)[0][10] |
| 116 | +actual_cond = data_xjtu.cond[fault_idx] |
| 117 | +cf_cond = 35.0 if actual_cond == 37.5 else 37.5 |
| 118 | + |
| 119 | +cf_res = model_xjtu.what_if(data_xjtu, fault_idx, cf_cond) |
| 120 | + |
| 121 | +actual_risk = cf_res['factual'].get( |
| 122 | + 'Y', model_xjtu.cnn.predict(data_xjtu.X[fault_idx : fault_idx + 1], verbose=0).max() |
| 123 | +) |
| 124 | +counterfactual_risk = cf_res['counterfactual'].get( |
| 125 | + 'Y', actual_risk + cf_res['counterfactual'].get('prob_change', 0) |
| 126 | +) |
| 127 | + |
| 128 | +print('Table: Counterfactual Analysis') |
| 129 | +print(f'Scenario | Actual Risk (Z={actual_cond}Hz) | Counterfactual Risk (Z={cf_cond}Hz)') |
| 130 | +print(f'S1 | {float(actual_risk):.4f} | {float(counterfactual_risk):.4f}') |
| 131 | + |
| 132 | +# Plot Bar Chart |
| 133 | +labels = ['Actual Risk', 'Counterfactual Risk'] |
| 134 | +values = [float(actual_risk), float(counterfactual_risk)] |
| 135 | +plt.figure(figsize=(5, 4)) |
| 136 | +plt.bar(labels, values, color=['red', 'green']) |
| 137 | +plt.title(f'Counterfactual Intervention (do(Z={cf_cond}))') |
| 138 | +plt.ylabel('Failure Risk') |
| 139 | +plt.ylim(0, 1.1) |
| 140 | +plt.savefig('paper_counterfactual_risk.png', dpi=300, bbox_inches='tight') |
| 141 | +plt.close() |
| 142 | +print('Saved paper_counterfactual_risk.png') |
| 143 | + |
| 144 | +# --------------------------------------------------------- |
| 145 | +# C. LAYER 5 VALIDATION (DECISION LAYER) |
| 146 | +# --------------------------------------------------------- |
| 147 | +print('\n--- SECTION C: DECISION LAYER ---') |
| 148 | +# Diagnose a balanced subset to save time |
| 149 | +normal_idxs = np.where(data_xjtu.y == 0)[0][:25] |
| 150 | +fault_idxs = np.where(data_xjtu.y > 0)[0][:25] |
| 151 | +idxs = np.concatenate([normal_idxs, fault_idxs]) |
| 152 | +np.random.shuffle(idxs) |
| 153 | + |
| 154 | +subset_data = Dataset.from_arrays( |
| 155 | + data_xjtu.X[idxs], |
| 156 | + data_xjtu.y[idxs], |
| 157 | + data_xjtu.cond[idxs], |
| 158 | + fs=data_xjtu.fs, |
| 159 | + physics=data_xjtu.physics, |
| 160 | + taxonomy=data_xjtu.taxonomy, |
| 161 | +) |
| 162 | +report = model_xjtu.diagnose(subset_data) |
| 163 | + |
| 164 | +print(f'{"Machine":<10} | {"CNSD Decision":<20} | {"Expert Decision":<15} | {"Match"}') |
| 165 | +print('-' * 60) |
| 166 | + |
| 167 | +maintenance_priority = [] |
| 168 | + |
| 169 | +for i, rec in enumerate(report.records[:10]): |
| 170 | + machine_id = f'M-{i + 1:03d}' |
| 171 | + cnsd_dec = rec['action'] |
| 172 | + expert_dec = 'Immediate Shutdown' if subset_data.y[i] > 0 else 'Normal Operation' |
| 173 | + |
| 174 | + # Simple semantic match logic |
| 175 | + match = ( |
| 176 | + 'YES' |
| 177 | + if ( |
| 178 | + subset_data.y[i] > 0 |
| 179 | + and ( |
| 180 | + 'Shutdown' in cnsd_dec or 'Maintenance' in cnsd_dec or 'inspect' in cnsd_dec.lower() |
| 181 | + ) |
| 182 | + ) |
| 183 | + or (subset_data.y[i] == 0 and 'monitor' in cnsd_dec.lower()) |
| 184 | + else 'NO' |
| 185 | + ) |
| 186 | + |
| 187 | + print(f'{machine_id:<10} | {cnsd_dec[:18]:<20} | {expert_dec:<15} | {match}') |
| 188 | + |
| 189 | + # Priority ranking score = CNN confidence + severity if confirmed |
| 190 | + score = rec['cnn_confidence'] * (1 if rec['status'] == 'CONFIRMED_FAULT' else 0) |
| 191 | + maintenance_priority.append((machine_id, score, rec['physics_verdict'])) |
| 192 | + |
| 193 | +print('\nOptional: Maintenance-Priority Ranking') |
| 194 | +maintenance_priority.sort(key=lambda x: x[1], reverse=True) |
| 195 | +for rank, (m_id, score, verdict) in enumerate(maintenance_priority[:5]): |
| 196 | + print(f'Rank {rank + 1}: {m_id} (Priority Score: {score:.2f}, Verdict: {verdict})') |
| 197 | + |
| 198 | +# --------------------------------------------------------- |
| 199 | +# F. OPERATIONAL UTILITY STUDY |
| 200 | +# --------------------------------------------------------- |
| 201 | +print('\n--- SECTION F: OPERATIONAL UTILITY ---') |
| 202 | +# Costs |
| 203 | +COST_FALSE_ALARM = -500 |
| 204 | +COST_MISSED_FAULT = -5000 |
| 205 | +COST_HIT = 2000 |
| 206 | + |
| 207 | +# Evaluate on the 50 subset |
| 208 | +baseline_preds = model_xjtu.cnn.predict(subset_data.X, verbose=0).argmax(1) |
| 209 | +cnsd_statuses = [r['status'] for r in report.records] |
| 210 | +gt = subset_data.y |
| 211 | + |
| 212 | + |
| 213 | +def calc_utility(preds, is_cnsd=False): |
| 214 | + tp = fp = fn = tn = 0 |
| 215 | + for i in range(len(gt)): |
| 216 | + actual_fault = gt[i] > 0 |
| 217 | + if is_cnsd: |
| 218 | + cnsd_dec = report.records[i]['action'] |
| 219 | + pred_fault = ( |
| 220 | + 'Shutdown' in cnsd_dec or 'Maintenance' in cnsd_dec or 'inspect' in cnsd_dec.lower() |
| 221 | + ) |
| 222 | + else: |
| 223 | + pred_fault = preds[i] > 0 |
| 224 | + |
| 225 | + if pred_fault and actual_fault: |
| 226 | + tp += 1 |
| 227 | + elif pred_fault and not actual_fault: |
| 228 | + fp += 1 |
| 229 | + elif not pred_fault and actual_fault: |
| 230 | + fn += 1 |
| 231 | + else: |
| 232 | + tn += 1 |
| 233 | + |
| 234 | + cost = (tp * COST_HIT) + (fp * COST_FALSE_ALARM) + (fn * COST_MISSED_FAULT) |
| 235 | + return tp, fp, cost |
| 236 | + |
| 237 | + |
| 238 | +base_tp, base_fp, base_cost = calc_utility(baseline_preds, False) |
| 239 | +cnsd_tp, cnsd_fp, cnsd_cost = calc_utility(None, True) |
| 240 | + |
| 241 | +print(f'{"Method":<10} | {"Early Warning (TP)":<20} | {"False Alarms":<15} | {"Maintenance Value"}') |
| 242 | +print('-' * 75) |
| 243 | +print(f'{"Baseline":<10} | {base_tp:<20} | {base_fp:<15} | ${base_cost:,}') |
| 244 | +print(f'{"CNSD":<10} | {cnsd_tp:<20} | {cnsd_fp:<15} | ${cnsd_cost:,}') |
0 commit comments