11import sys
22import traceback
3+ import os
4+ import numpy as np
5+ import scipy .stats as stats
36
47try :
8+ os .environ ['TF_CPP_MIN_LOG_LEVEL' ] = '2'
59 import os
10+ import site
11+ import sys
12+
13+ # Inject pip-installed NVIDIA DLLs into Windows PATH so TF 2.10 can use the GPU without a system CUDA install
14+ try :
15+ site_packages = site .getsitepackages ()[1 ] if len (site .getsitepackages ()) > 1 else site .getsitepackages ()[0 ]
16+ nvidia_base = os .path .join (site_packages , "nvidia" )
17+ dll_paths = [
18+ os .path .join (nvidia_base , "cudnn" , "bin" ),
19+ os .path .join (nvidia_base , "cublas" , "bin" ),
20+ os .path .join (nvidia_base , "cuda_runtime" , "bin" ),
21+ ]
22+ os .environ ["PATH" ] = os .pathsep .join (dll_paths ) + os .pathsep + os .environ .get ("PATH" , "" )
23+ except Exception :
24+ pass
625
7- os .environ ['TF_CPP_MIN_LOG_LEVEL' ] = '2'
8- import numpy as np
9- import scipy .stats as stats
1026 import tensorflow as tf
1127
1228 from cnsd import Dataset
1329 from cnsd .diagnosis .system import CNSD
14- from cnsd .perception .cnn import _train_cnn # <--- WE IMPORT JUST THE CNN TRAINER
30+ from cnsd .perception .cnn import _train_cnn
1531 from cnsd .physics import PhysicsConfig
1632 from validate_pu import load_pu_domain_split
1733
1834 print ('Loading Authentic PU dataset (Cross-Domain RPM Split)...' )
19- (X_train , y_train , cond_train ), (X_target , y_target , cond_target ) = load_pu_domain_split ()
35+ (X_train_full , y_train_full , cond_train_full ), (X_target , y_target , cond_target ) = load_pu_domain_split ()
2036
21- unique_rpm = set (cond_train ).union (set (cond_target ))
37+ unique_rpm = set (cond_train_full ).union (set (cond_target ))
2238 rpm_map = {float (r ): float (r ) for r in unique_rpm }
2339 pu_physics = PhysicsConfig (
2440 bearing = {'n_balls' : 8 , 'd_ball' : 6.75 , 'd_pitch' : 28.5 , 'contact_angle' : 0.0 },
3248 2 : ('Inner Race' , 'High' ),
3349 }
3450
35- # Shuffle train data exactly like validate_pu.py to ensure parity
36- train_indices = np .arange (len (y_train ))
37- np .random .seed (42 )
38- np .random .shuffle (train_indices )
39- X_train , y_train , cond_train = (
40- X_train [train_indices ],
41- y_train [train_indices ],
42- cond_train [train_indices ],
43- )
44-
45- train_ds = Dataset .from_arrays (
46- X_train ,
47- y_train ,
48- cond_train ,
49- fs = 64000 ,
50- physics = pu_physics ,
51- taxonomy = pu_taxonomy ,
52- name = 'PU_Train' ,
53- )
54- test_ds = Dataset .from_arrays (
55- X_target ,
56- y_target ,
57- cond_target ,
58- fs = 64000 ,
59- physics = pu_physics ,
60- taxonomy = pu_taxonomy ,
61- name = 'PU_Test' ,
62- )
63-
64- # Train Primary Model
65- print ('\n [A] Training Primary CNSD Model (Seed=42)...' )
66- np .random .seed (42 )
67- tf .random .set_seed (42 )
68- model = CNSD ()
69- model .fit (train_ds , epochs = 20 )
70-
71- # Train Ensemble Models
72- # FIX: We only need the CNN part for the ensemble, not the full Physics+DoWhy SCM pipeline!
73- # Training the full CNSD() 4 times was causing the joblib multiprocessing memory leak.
74- ens = []
75- nc = int (train_ds .y .max ()) + 1
76- for s in [100 , 101 , 102 ]:
77- print (f'\n [B] Training Ensemble Member CNN only (Seed={ s } )...' )
78- np .random .seed (s )
79- tf .random .set_seed (s )
80- # Train just the Keras CNN directly, skipping the heavy memory SCM
81- m_cnn = _train_cnn (train_ds .X , train_ds .y , num_classes = nc , epochs = 20 , seed = s )
82- ens .append (m_cnn )
83-
84- # ---- shared: test signals, labels, condition ----
85- sig = np .stack ([test_ds .X [i ].reshape (- 1 ) for i in range (len (test_ds .X ))]).astype (np .float32 )
86- yte = test_ds .y
87- cond_te = test_ds .cond
88- Xin = sig [..., None ] # (n, L, 1) for the conv nets
89-
90- def gap_by_score (score , correct , thr ):
91- hi = score >= thr
92- lo = ~ hi
93- ah = correct [hi ].mean () if hi .any () else float ('nan' )
94- al = correct [lo ].mean () if lo .any () else float ('nan' )
95- return ah - al , ah , al , int (hi .sum ()), int (lo .sum ())
96-
97- print ('=' * 60 , '\n 1. PHYSICS gap across tau\n ' , '=' * 60 )
98- probs = model .cnn .predict (Xin , batch_size = 128 , verbose = 0 )
99- pred = probs .argmax (1 )
100- correct = pred == yte
101- print (f'baseline CNN acc on test: { correct .mean ():.3f} ' )
102- for tau in [1.0 , 2.0 , 3.0 ]:
103- model .symbolic .tau = float (tau )
104- verds = np .array (
105- [
106- model .symbolic .diagnose (sig [i ], pred [i ], cond_te [i ])['verdict' ]
107- for i in range (len (sig ))
108- ]
109- )
110- conf = verds == 'CONFIRMED'
111- cnfl = verds == 'CONFLICT'
112- ca = correct [conf ].mean () if conf .any () else float ('nan' )
113- fa = correct [cnfl ].mean () if cnfl .any () else float ('nan' )
114- print (
115- f' tau={ tau :.1f} GAP={ ca - fa :+.3f} CONF={ ca :.3f} (n={ conf .sum ()} ) CNFL={ fa :.3f} (n={ cnfl .sum ()} )'
116- )
117-
118- print ('\n ' , '=' * 60 , '\n 2. SOFTMAX confidence\n ' , '=' * 60 )
119- softmax_conf = probs .max (1 )
120- for thr in [0.5 , 0.7 , 0.9 , 0.95 , 0.99 ]:
121- g , ah , al , nh , nl = gap_by_score (softmax_conf , correct , thr )
122- print (f' conf>={ thr :.2f} GAP={ g :+.3f} HI={ ah :.3f} (n={ nh } ) LO={ al :.3f} (n={ nl } )' )
123-
124- print ('\n ' , '=' * 60 , '\n 3. MC-DROPOUT (30 passes)\n ' , '=' * 60 )
51+ def get_matched_coverage_gap (score , correct , target_n ):
52+ if target_n == 0 :
53+ return float ('nan' )
54+ # Score is higher for MORE confident
55+ # Sort descending by score
56+ sorted_indices = np .argsort (score )[::- 1 ]
57+ hi_indices = sorted_indices [:target_n ]
58+ lo_indices = sorted_indices [target_n :]
59+
60+ hi_mask = np .zeros (len (score ), dtype = bool )
61+ hi_mask [hi_indices ] = True
62+ lo_mask = ~ hi_mask
63+
64+ ah = correct [hi_mask ].mean () if hi_mask .any () else float ('nan' )
65+ al = correct [lo_mask ].mean () if lo_mask .any () else float ('nan' )
66+ return ah - al
12567
12668 class AlwaysDropout (tf .keras .layers .Dropout ):
12769 def call (self , inputs , training = None ):
@@ -132,69 +74,175 @@ def clone_for_mc(layer):
13274 return AlwaysDropout (layer .rate )
13375 return layer .__class__ .from_config (layer .get_config ())
13476
135- mc_model = tf .keras .models .clone_model (model .cnn , clone_function = clone_for_mc )
136- mc_model .set_weights (model .cnn .get_weights ())
137-
138- T = 30
139- mc = []
140- for _ in range (T ):
141- mc .append (mc_model .predict (Xin , batch_size = 128 , verbose = 0 ))
142- mc = np .stack (mc ) # (T,n,classes)
143- mc_mean = mc .mean (0 )
144- mc_pred = mc_mean .argmax (1 )
145- mc_correct = mc_pred == yte
146- eps = 1e-12
147- certainty = (mc_mean * np .log (mc_mean + eps )).sum (1 ) # = -entropy; higher = more certain
148- print (f'MC mean acc: { mc_correct .mean ():.3f} ' )
149- for q in [50 , 30 , 20 , 10 ]: # keep top (100-q)% most certain as 'reliable'
150- thr = np .percentile (certainty , q )
151- g , ah , al , nh , nl = gap_by_score (certainty , mc_correct , thr )
152- print (f' top { 100 - q } % certain GAP={ g :+.3f} HI={ ah :.3f} (n={ nh } ) LO={ al :.3f} (n={ nl } )' )
153-
154- print ('\n ' , '=' * 60 , '\n 4. ENSEMBLE disagreement\n ' , '=' * 60 )
155- ens_preds = np .stack ([m .predict (Xin , batch_size = 128 , verbose = 0 ).argmax (1 ) for m in ens ])
156- vote = stats .mode (ens_preds , axis = 0 , keepdims = False ).mode
157- unanimous = (ens_preds == vote ).all (0 )
158- ens_correct = vote == yte
159- print (f'ensemble vote acc: { ens_correct .mean ():.3f} | unanimous: { unanimous .mean ():.1%} ' )
160- g , ah , al , nh , nl = gap_by_score (
161- unanimous .astype (float ), ens_correct , 1.0
162- ) # unanimous=reliable
163- print (f' unanimous=reliable GAP={ g :+.3f} HI={ ah :.3f} (n={ nh } ) LO={ al :.3f} (n={ nl } )' )
164-
165- print ('\n ' , '=' * 60 , '\n 5. NOISE TEST: physics CONFLICT on unanimous-but-wrong\n ' , '=' * 60 )
166- rng = np .random .RandomState (42 )
167- sig_power = (sig ** 2 ).mean ()
168- model .symbolic .tau = 3.0
169- print (
170- f'{ "noise" :>6} | { "ens_acc" :>7} | { "unanim-wrong" :>12} | { "phys catches" :>12} | { "base rate" :>9} '
77+ seeds = [42 , 43 , 44 , 45 , 46 ]
78+
79+ results = {
80+ 'phys_gap' : [],
81+ 'soft_gap' : [],
82+ 'mc_gap' : [],
83+ 'ens_gap' : [],
84+ 'noise_catch' : {db : [] for db in [np .inf , 20 , 10 , 5 , 0 ]}
85+ }
86+
87+ # Test data processing
88+ test_ds = Dataset .from_arrays (
89+ X_target , y_target , cond_target , fs = 64000 , physics = pu_physics , taxonomy = pu_taxonomy , name = 'PU_Test'
17190 )
172- print ('-' * 60 )
91+ sig_te = np .stack ([test_ds .X [i ].reshape (- 1 ) for i in range (len (test_ds .X ))]).astype (np .float32 )
92+ yte = test_ds .y
93+ cond_te = test_ds .cond
94+ Xin_te = sig_te [..., None ]
95+
96+ for seed in seeds :
97+ print (f"\n { '=' * 80 } \n === RUNNING SEED { seed } ===\n { '=' * 80 } " )
98+ tf .keras .backend .clear_session ()
99+ np .random .seed (seed )
100+ tf .random .set_seed (seed )
101+
102+ # 1. 80/20 Split for Calibration
103+ indices = np .arange (len (y_train_full ))
104+ np .random .shuffle (indices )
105+
106+ split_idx = int (0.8 * len (indices ))
107+ train_idx = indices [:split_idx ]
108+ calib_idx = indices [split_idx :]
109+
110+ X_tr , y_tr , cond_tr = X_train_full [train_idx ], y_train_full [train_idx ], cond_train_full [train_idx ]
111+ X_ca , y_ca , cond_ca = X_train_full [calib_idx ], y_train_full [calib_idx ], cond_train_full [calib_idx ]
112+
113+ train_ds = Dataset .from_arrays (X_tr , y_tr , cond_tr , fs = 64000 , physics = pu_physics , taxonomy = pu_taxonomy , name = 'PU_Train' )
114+ calib_ds = Dataset .from_arrays (X_ca , y_ca , cond_ca , fs = 64000 , physics = pu_physics , taxonomy = pu_taxonomy , name = 'PU_Calib' )
115+
116+ # 2. Train Primary Model (Bypass SCM to prevent multiprocess deadlocks)
117+ model = CNSD ()
118+ nc = int (train_ds .y .max ()) + 1
119+ model .cnn = _train_cnn (train_ds .X , train_ds .y , num_classes = nc , epochs = 20 , seed = seed )
120+ model .symbolic = model ._build_symbolic (train_ds )
121+ model ._fitted = True
122+
123+ # 3. Train Ensemble Models
124+ ens = []
125+ nc = int (train_ds .y .max ()) + 1
126+ for s in [seed * 10 , seed * 10 + 1 , seed * 10 + 2 ]:
127+ np .random .seed (s )
128+ tf .random .set_seed (s )
129+ m_cnn = _train_cnn (train_ds .X , train_ds .y , num_classes = nc , epochs = 20 , seed = s )
130+ ens .append (m_cnn )
131+
132+ # 4. Calibrate Tau
133+ sig_ca = np .stack ([calib_ds .X [i ].reshape (- 1 ) for i in range (len (calib_ds .X ))]).astype (np .float32 )
134+ Xin_ca = sig_ca [..., None ]
135+ probs_ca = model .cnn .predict (Xin_ca , batch_size = 128 , verbose = 0 )
136+ pred_ca = probs_ca .argmax (1 )
137+ correct_ca = (pred_ca == calib_ds .y )
138+
139+ best_tau = 1.0
140+ best_gap = - 100.0
141+ for tau in [1.0 , 1.5 , 2.0 , 2.5 , 3.0 ]:
142+ model .symbolic .tau = float (tau )
143+ verds = np .array ([model .symbolic .diagnose (sig_ca [i ], pred_ca [i ], calib_ds .cond [i ])['verdict' ] for i in range (len (sig_ca ))])
144+ conf = (verds == 'CONFIRMED' )
145+ cnfl = (verds == 'CONFLICT' )
146+ ca = correct_ca [conf ].mean () if conf .any () else 0.0
147+ fa = correct_ca [cnfl ].mean () if cnfl .any () else 1.0
148+ gap = ca - fa
149+ if gap > best_gap :
150+ best_gap = gap
151+ best_tau = tau
152+
153+ print (f"Calibrated best tau = { best_tau } (Calib GAP = { best_gap :+.3f} )" )
154+ model .symbolic .tau = float (best_tau )
155+
156+ # 5. Evaluate on Test Set
157+ probs_te = model .cnn .predict (Xin_te , batch_size = 128 , verbose = 0 )
158+ pred_te = probs_te .argmax (1 )
159+ correct_te = (pred_te == yte )
160+
161+ # Physics evaluation
162+ verds = np .array ([model .symbolic .diagnose (sig_te [i ], pred_te [i ], cond_te [i ])['verdict' ] for i in range (len (sig_te ))])
163+ conf = (verds == 'CONFIRMED' )
164+ cnfl = (verds == 'CONFLICT' )
165+ ca = correct_te [conf ].mean () if conf .any () else float ('nan' )
166+ fa = correct_te [cnfl ].mean () if cnfl .any () else float ('nan' )
167+ phys_gap = ca - fa
168+ target_n = int (conf .sum ())
169+ results ['phys_gap' ].append (phys_gap )
170+ print (f"Physics GAP={ phys_gap :+.3f} (Coverage N={ target_n } )" )
171+
172+ # Softmax evaluation at matched coverage
173+ softmax_score = probs_te .max (1 )
174+ soft_gap = get_matched_coverage_gap (softmax_score , correct_te , target_n )
175+ results ['soft_gap' ].append (soft_gap )
176+
177+ # MC-Dropout at matched coverage
178+ mc_model = tf .keras .models .clone_model (model .cnn , clone_function = clone_for_mc )
179+ mc_model .set_weights (model .cnn .get_weights ())
180+ T = 30
181+ mc_preds = []
182+ for _ in range (T ):
183+ mc_preds .append (mc_model .predict (Xin_te , batch_size = 128 , verbose = 0 ))
184+ mc_preds = np .stack (mc_preds )
185+ mc_mean = mc_preds .mean (0 )
186+ mc_pred_class = mc_mean .argmax (1 )
187+ mc_correct = (mc_pred_class == yte )
188+ eps = 1e-12
189+ mc_score = (mc_mean * np .log (mc_mean + eps )).sum (1 ) # Certainty (negative entropy)
190+ mc_gap = get_matched_coverage_gap (mc_score , mc_correct , target_n )
191+ results ['mc_gap' ].append (mc_gap )
192+
193+ # Ensemble at matched coverage
194+ ens_preds_probs = np .stack ([m .predict (Xin_te , batch_size = 128 , verbose = 0 ) for m in ens ]) # (3, n, c)
195+ ens_mean = ens_preds_probs .mean (0 ) # (n, c)
196+ ens_pred_class = ens_mean .argmax (1 )
197+ ens_correct = (ens_pred_class == yte )
198+ # Score = negative entropy of ensemble mean
199+ ens_score = (ens_mean * np .log (ens_mean + eps )).sum (1 )
200+ ens_gap = get_matched_coverage_gap (ens_score , ens_correct , target_n )
201+ results ['ens_gap' ].append (ens_gap )
202+
203+ print (f"Matched Coverage GAPs -> Softmax:{ soft_gap :+.3f} | MC-Drop:{ mc_gap :+.3f} | Ens:{ ens_gap :+.3f} " )
204+
205+ # 6. Noise Test
206+ rng = np .random .RandomState (seed )
207+ sig_power = (sig_te ** 2 ).mean ()
208+ for snr_db in [np .inf , 20 , 10 , 5 , 0 ]:
209+ if np .isinf (snr_db ):
210+ sig_n = sig_te
211+ else :
212+ npow = sig_power / (10 ** (snr_db / 10 ))
213+ sig_n = sig_te + rng .randn (* sig_te .shape ).astype (np .float32 ) * np .sqrt (npow )
214+
215+ Xin_n = sig_n [..., None ]
216+ # Use ensemble mode vote to define 'unanimous' exactly like Abhi's template
217+ ep_class = np .stack ([m .predict (Xin_n , batch_size = 128 , verbose = 0 ).argmax (1 ) for m in ens ])
218+ v = stats .mode (ep_class , axis = 0 , keepdims = False ).mode
219+ unan = (ep_class == v ).all (0 )
220+ ok = (v == yte )
221+
222+ pv = np .array ([model .symbolic .diagnose (sig_n [i ], v [i ], cond_te [i ])['verdict' ] for i in range (len (sig_n ))])
223+ pc = (pv == 'CONFLICT' )
224+ uw = unan & (~ ok )
225+ catch = pc [uw ].mean () if uw .sum () > 0 else float ('nan' )
226+ results ['noise_catch' ][snr_db ].append (catch )
227+ s = 'clean' if np .isinf (snr_db ) else f'{ snr_db } dB'
228+ print (f" Noise={ s :>5} | catch_rate={ catch :.3f} " )
229+
230+ print ("\n " + "=" * 60 + "\n FINAL AGGREGATED RESULTS (5 Seeds)\n " + "=" * 60 )
231+ print (f"Physics GAP: { np .nanmean (results ['phys_gap' ]):+.3f} ± { np .nanstd (results ['phys_gap' ]):.3f} " )
232+ print (f"Softmax GAP: { np .nanmean (results ['soft_gap' ]):+.3f} ± { np .nanstd (results ['soft_gap' ]):.3f} " )
233+ print (f"MC-Drop GAP: { np .nanmean (results ['mc_gap' ]):+.3f} ± { np .nanstd (results ['mc_gap' ]):.3f} " )
234+ print (f"Ensemble GAP: { np .nanmean (results ['ens_gap' ]):+.3f} ± { np .nanstd (results ['ens_gap' ]):.3f} " )
235+
236+ print ("\n Noise Test Catch Rate (Physics catches Ensemble's confident errors):" )
173237 for snr_db in [np .inf , 20 , 10 , 5 , 0 ]:
174- if np .isinf (snr_db ):
175- sig_n = sig
176- else :
177- npow = sig_power / (10 ** (snr_db / 10 ))
178- sig_n = sig + rng .randn (* sig .shape ).astype (np .float32 ) * np .sqrt (npow )
179- Xin_n = sig_n [..., None ]
180- ep = np .stack ([m .predict (Xin_n , batch_size = 128 , verbose = 0 ).argmax (1 ) for m in ens ])
181- v = stats .mode (ep , axis = 0 , keepdims = False ).mode
182- unan = (ep == v ).all (0 )
183- ok = v == yte
184- pv = np .array (
185- [
186- model .symbolic .diagnose (sig_n [i ], v [i ], cond_te [i ])['verdict' ]
187- for i in range (len (sig_n ))
188- ]
189- )
190- pc = pv == 'CONFLICT'
191- uw = unan & (~ ok )
192- catch = pc [uw ].mean () if uw .sum () > 0 else float ('nan' )
193238 s = 'clean' if np .isinf (snr_db ) else f'{ snr_db } dB'
194- print (f'{ s :>6} | { ok .mean ():>7.3f} | { uw .sum ():>12} | { catch :>12.3f} | { pc .mean ():>9.3f} ' )
195-
196- print ('================ DONE ================' )
197-
239+ vals = [v for v in results ['noise_catch' ][snr_db ] if not np .isnan (v )]
240+ m = np .nanmean (vals ) if len (vals ) > 0 else float ('nan' )
241+ std = np .nanstd (vals ) if len (vals ) > 0 else float ('nan' )
242+ print (f" { s :>6} : { m :.3f} ± { std :.3f} " )
243+
244+ print ("================ DONE ================" )
245+
198246except Exception :
199247 with open ('crash_traceback.txt' , 'w' ) as f :
200248 traceback .print_exc (file = f )
0 commit comments