-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest.py
More file actions
1635 lines (1370 loc) · 76.3 KB
/
Copy pathtest.py
File metadata and controls
1635 lines (1370 loc) · 76.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
Enhanced BCG Classifier Testing Script
This script evaluates trained BCG classifiers with:
1. Uncertainty quantification and probabilistic outputs
2. Detection threshold analysis
3. Enhanced visualizations with probability information
"""
import os
# Fix threading issues for HPC systems - set before any numpy/sklearn imports
os.environ['NUMEXPR_MAX_THREADS'] = '128'
os.environ['OMP_NUM_THREADS'] = '1' # Prevent numpy threading conflicts
os.environ['MKL_NUM_THREADS'] = '1' # Intel MKL threading limit
import argparse
import torch
import torch.nn as nn
import numpy as np
import pandas as pd
import joblib
import matplotlib.pyplot as plt
from datetime import datetime
from scipy.ndimage import maximum_filter, zoom
from data.data_read import prepare_dataframe, BCGDataset
from ml_models.candidate_classifier import BCGCandidateClassifier
from utils.candidate_based_bcg import extract_patch_features, extract_context_features
from utils.viz_bcg import show_failures
# NEW: BCG dataset support
from data.data_read_bcgs import create_bcg_datasets, BCGDataset as NewBCGDataset
from data.candidate_dataset_bcgs import (create_bcg_candidate_dataset_from_loader,
create_desprior_candidate_dataset_from_files,
collate_bcg_candidate_samples)
from ml_models.uq_classifier import BCGProbabilisticClassifier
from utils.reproducibility import set_global_seed, make_deterministic
# ============================================================================
# Use BCGProbabilisticClassifier from ml_models.uq_classifier
# ============================================================================
# ============================================================================
# ENHANCED PREDICTION FUNCTIONS
# ============================================================================
def calculate_bcg_rank(true_bcg, all_candidates, scores, distance_threshold=10.0):
"""Calculate the rank of the true BCG among all candidates.
Args:
true_bcg: True BCG coordinates (x, y)
all_candidates: Array of candidate coordinates
scores: Scores/probabilities for each candidate
distance_threshold: Distance threshold to consider a candidate as matching the true BCG
Returns:
rank: 1-indexed rank of true BCG (1 = best, 2 = second best, etc.)
Returns None if true BCG not found within threshold
"""
if len(all_candidates) == 0 or len(scores) == 0:
return None
# Calculate distances from each candidate to true BCG
distances_to_true = np.sqrt(np.sum((all_candidates - np.array(true_bcg))**2, axis=1))
# Find candidates within distance threshold of true BCG
matching_candidates = distances_to_true <= distance_threshold
if not np.any(matching_candidates):
return None # True BCG not found among candidates
# Get the best matching candidate (closest to true BCG)
best_match_idx = np.argmin(distances_to_true)
# Sort candidates by score (descending order)
sorted_indices = np.argsort(scores)[::-1]
# Find rank of the best matching candidate
rank = np.where(sorted_indices == best_match_idx)[0][0] + 1 # 1-indexed
return rank
def predict_bcg_with_probabilities(image, model, feature_scaler=None,
detection_threshold=0.1, return_all_candidates=False, additional_features=None,
use_desprior_candidates=False, filename=None, dataset_type=None,
use_color_features=False, color_extractor=None, desprior_csv_path=None, **candidate_kwargs):
"""Predict BCG candidates with calibrated probabilities and uncertainty.
Args:
additional_features: Additional features to append to visual features (e.g., redshift, delta_mstar_z)
use_desprior_candidates: Whether to use DESprior candidates instead of automatic detection
filename: Image filename (required if use_desprior_candidates=True)
dataset_type: Dataset type (required if use_desprior_candidates=True)
"""
# Find candidates using appropriate method
if use_desprior_candidates:
# Use DESprior candidates from BCG dataset
if desprior_csv_path is None:
raise ValueError("desprior_csv_path must be provided when use_desprior_candidates=True")
try:
candidates_df = pd.read_csv(desprior_csv_path)
file_candidates = candidates_df[candidates_df['filename'] == filename]
if len(file_candidates) == 0:
all_candidates = np.array([])
else:
# Extract coordinates and candidate features
all_candidates = file_candidates[['x', 'y']].values
# Extract all auxiliary features: delta_mstar, starflag, rz, cluster_redshift
required_cols = ['delta_mstar', 'starflag', 'rz', 'cluster_redshift']
for col in required_cols:
if col not in file_candidates.columns:
print(f"ERROR: Required column '{col}' not found in DESprior candidates CSV")
print(f"Available columns: {list(file_candidates.columns)}")
import sys
sys.exit(1)
candidate_specific_features = file_candidates[required_cols].values
# Extract visual features and combine with candidate features
from utils.candidate_based_bcg import extract_candidate_features
visual_features, _ = extract_candidate_features(
image, all_candidates, patch_size=candidate_kwargs.get('patch_size', 64),
include_context=True, include_color=use_color_features,
color_extractor=color_extractor
)
# Combine visual features with candidate-specific features
features = np.hstack([visual_features, candidate_specific_features])
# NOTE: DESprior candidates already include all necessary features including additional features
# Do not add additional features again as they are already included in the feature extraction
except Exception as e:
print(f"Warning: Failed to load DESprior candidates for {filename}: {e}")
all_candidates = np.array([])
else:
# Use automatic candidate detection
from utils.candidate_based_bcg import find_bcg_candidates, extract_candidate_features
all_candidates, intensities = find_bcg_candidates(image, **candidate_kwargs)
if len(all_candidates) > 0:
features, _ = extract_candidate_features(
image, all_candidates, patch_size=candidate_kwargs.get('patch_size', 64),
include_context=True, include_color=use_color_features,
color_extractor=color_extractor
)
# Append additional features if provided (e.g., from BCG dataset)
if additional_features is not None and len(features) > 0:
# Replicate additional features for each candidate
additional_features_repeated = np.tile(additional_features, (len(features), 1))
features = np.concatenate([features, additional_features_repeated], axis=1)
if len(all_candidates) == 0:
return {
'best_bcg': None,
'all_candidates': np.array([]),
'probabilities': np.array([]),
'uncertainties': np.array([]),
'detections': np.array([]),
'detection_probabilities': np.array([]),
'best_features': None # No features when no candidates
}
# Scale features
if feature_scaler is not None:
scaled_features = feature_scaler.transform(features)
features_tensor = torch.FloatTensor(scaled_features)
else:
scaled_features = features
features_tensor = torch.FloatTensor(features)
# Get probabilities and uncertainties
model.eval()
with torch.no_grad():
if hasattr(model, 'predict_with_uncertainty') and hasattr(model, 'temperature'):
# Probabilistic model with UQ trained with ranking loss
raw_logits = model(features_tensor).squeeze(-1)
probabilities, uncertainties = model.predict_with_uncertainty(features_tensor)
probabilities = probabilities.numpy()
uncertainties = uncertainties.numpy()
# Ensure arrays are at least 1D
probabilities = np.atleast_1d(probabilities)
uncertainties = np.atleast_1d(uncertainties)
# Fallback if probabilities near zero
if np.max(probabilities) < 1e-6:
probabilities = np.atleast_1d(torch.sigmoid(raw_logits).numpy())
uncertainties = np.zeros_like(probabilities)
elif hasattr(model, 'temperature'):
# This is a probabilistic model without MC dropout - trained with ranking loss
logits = model.forward_with_temperature(features_tensor).squeeze(-1)
probabilities = np.atleast_1d(torch.sigmoid(logits).numpy())
uncertainties = np.zeros_like(probabilities) # No uncertainty available
else:
# This is a traditional classifier - use raw scores for ranking, convert to probs for display
scores = model(features_tensor).squeeze(-1)
probabilities = np.atleast_1d(torch.sigmoid(scores).numpy())
uncertainties = np.zeros_like(probabilities) # No uncertainty available
# Find detections above threshold
detection_mask = probabilities >= detection_threshold
detections = all_candidates[detection_mask]
detection_probabilities = probabilities[detection_mask]
# Find best BCG (highest probability)
if len(probabilities) > 0:
best_idx = np.argmax(probabilities)
best_bcg = tuple(all_candidates[best_idx])
# FEATURE ANALYSIS: Get best candidate's features
best_features = scaled_features[best_idx]
else:
best_bcg = None
best_features = None
results = {
'best_bcg': best_bcg,
'all_candidates': all_candidates,
'probabilities': probabilities,
'uncertainties': uncertainties,
'detections': detections,
'detection_probabilities': detection_probabilities,
'best_features': best_features # Add features to return
}
return results
# ============================================================================
# ENHANCED VISUALIZATION FUNCTIONS
# ============================================================================
def show_enhanced_predictions(images, targets, predictions, all_candidates_list,
all_scores_list, all_probabilities_list=None,
indices=None, save_dir=None, phase=None, use_uq=False,
metadata_list=None, detection_threshold=0.5, dataset_type="bcg_2p2arcmin"):
"""Enhanced visualization with probability information, adaptive candidate display, and probability labels."""
from utils.viz_bcg import show_predictions_with_candidates, show_predictions_with_candidates_enhanced
# Use both original and enhanced visualization functions
# First create original plots
show_predictions_with_candidates(
images=images,
targets=targets,
predictions=predictions,
all_candidates_list=all_candidates_list,
candidate_scores_list=all_scores_list,
indices=indices,
save_dir=save_dir,
phase=phase,
probabilities_list=all_probabilities_list,
detection_threshold=detection_threshold,
use_uq=use_uq,
metadata_list=metadata_list
)
# Then create enhanced plots in physical_images subdirectory
show_predictions_with_candidates_enhanced(
images=images,
targets=targets,
predictions=predictions,
all_candidates_list=all_candidates_list,
candidate_scores_list=all_scores_list,
indices=indices,
save_dir=save_dir,
phase=phase,
probabilities_list=all_probabilities_list,
detection_threshold=detection_threshold,
use_uq=use_uq,
metadata_list=metadata_list,
dataset_type=dataset_type
)
def plot_probability_analysis(all_probabilities_list, all_uncertainties_list,
distances, save_dir=None):
"""Plot probability and uncertainty analysis."""
if not all_probabilities_list or not any(len(p) > 0 for p in all_probabilities_list):
return
# Set style consistent with plot_physical_results.py
plt.rcParams.update({"text.usetex":False,"font.family":"serif","mathtext.fontset":"cm","axes.linewidth":1.2})
# Collect all probabilities and uncertainties
all_probs = []
all_uncs = []
best_probs = [] # Probability of best candidate
best_uncs = [] # Uncertainty of best candidate
for i, (probs, uncs) in enumerate(zip(all_probabilities_list, all_uncertainties_list)):
if len(probs) > 0:
all_probs.extend(probs)
best_probs.append(np.max(probs))
if len(uncs) > 0:
all_uncs.extend(uncs)
best_idx = np.argmax(probs)
best_uncs.append(uncs[best_idx])
if not all_probs:
return
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# Probability distribution
axes[0, 0].hist(all_probs, bins=30, alpha=0.7, edgecolor='black')
axes[0, 0].set_xlabel('BCG Probability', fontsize=18)
axes[0, 0].set_ylabel('Count', fontsize=18)
axes[0, 0].set_title('Distribution of All Candidate Probabilities', fontsize=18)
axes[0, 0].tick_params(axis='both', labelsize=18)
axes[0, 0].grid(True, alpha=0.3)
# Best candidate probabilities
axes[0, 1].hist(best_probs, bins=20, alpha=0.7, color='orange', edgecolor='black')
axes[0, 1].set_xlabel('Best Candidate Probability', fontsize=18)
axes[0, 1].set_ylabel('Count', fontsize=18)
axes[0, 1].set_title('Distribution of Best Candidate Probabilities', fontsize=18)
axes[0, 1].tick_params(axis='both', labelsize=18)
axes[0, 1].grid(True, alpha=0.3)
# Uncertainty analysis
if all_uncs:
axes[1, 0].hist(all_uncs, bins=30, alpha=0.7, color='red', edgecolor='black')
axes[1, 0].set_xlabel('Uncertainty', fontsize=18)
axes[1, 0].set_ylabel('Count', fontsize=18)
axes[1, 0].set_title('Distribution of All Candidate Uncertainties', fontsize=18)
axes[1, 0].tick_params(axis='both', labelsize=18)
axes[1, 0].grid(True, alpha=0.3)
# Probability vs Uncertainty scatter
if len(best_probs) == len(best_uncs):
scatter = axes[1, 1].scatter(best_probs, best_uncs, c=distances[:len(best_probs)],
cmap='viridis', alpha=0.6)
axes[1, 1].set_xlabel('Best Candidate Probability', fontsize=18)
axes[1, 1].set_ylabel('Best Candidate Uncertainty', fontsize=18)
axes[1, 1].set_title('Probability vs Uncertainty (colored by distance error)', fontsize=18)
axes[1, 1].tick_params(axis='both', labelsize=18)
axes[1, 1].grid(True, alpha=0.3)
plt.colorbar(scatter, ax=axes[1, 1], label='Distance Error (pixels)')
else:
# If no uncertainties, just show probability vs distance
if len(best_probs) <= len(distances):
axes[1, 0].scatter(best_probs, distances[:len(best_probs)], alpha=0.6)
axes[1, 0].set_xlabel('Best Candidate Probability', fontsize=18)
axes[1, 0].set_ylabel('Distance Error (pixels)', fontsize=18)
axes[1, 0].set_title('Probability vs Distance Error', fontsize=18)
axes[1, 0].tick_params(axis='both', labelsize=18)
axes[1, 0].grid(True, alpha=0.3)
axes[1, 1].text(0.5, 0.5, 'No uncertainty\ninformation available',
ha='center', va='center', transform=axes[1, 1].transAxes,
fontsize=18)
axes[1, 1].set_title('Uncertainty Analysis', fontsize=18)
plt.tight_layout()
if save_dir:
os.makedirs(save_dir, exist_ok=True)
save_path = os.path.join(save_dir, 'probability_analysis.png')
plt.savefig(save_path, dpi=150, bbox_inches='tight')
print(f"Probability analysis saved to: {save_path}")
plt.show()
plt.close()
# ============================================================================
# MAIN TESTING FUNCTIONS
# ============================================================================
def split_dataset(dataset, train_ratio=0.7, val_ratio=0.2, random_seed=42):
"""Split dataset into train/validation/test sets (same as training)."""
torch.manual_seed(random_seed)
np.random.seed(random_seed)
n_samples = len(dataset)
indices = torch.randperm(n_samples).tolist()
n_train = int(train_ratio * n_samples)
n_val = int(val_ratio * n_samples)
train_indices = indices[:n_train]
val_indices = indices[n_train:n_train + n_val]
test_indices = indices[n_train + n_val:]
train_subset = torch.utils.data.Subset(dataset, train_indices)
val_subset = torch.utils.data.Subset(dataset, val_indices)
test_subset = torch.utils.data.Subset(dataset, test_indices)
return train_subset, val_subset, test_subset
def load_trained_model(model_path, scaler_path, feature_dim, use_uq=False, use_color_features=False):
"""Load trained model, feature scaler, and optional color extractor."""
# Load appropriate model type
if use_uq:
model = BCGProbabilisticClassifier(feature_dim, hidden_dims=[128, 64, 32], dropout_rate=0.2)
else:
model = BCGCandidateClassifier(feature_dim)
model.load_state_dict(torch.load(model_path, map_location='cpu'))
model.eval()
# Load scaler
feature_scaler = joblib.load(scaler_path)
# Load color extractor if available (lazy import to avoid NUMEXPR issues)
color_extractor = None
if use_color_features:
try:
from utils.color_features import ColorFeatureExtractor
# Try to load color extractor from the same directory as the model
model_dir = os.path.dirname(model_path)
model_name = os.path.splitext(os.path.basename(model_path))[0]
color_extractor_path = os.path.join(model_dir, f"{model_name}_color_extractor.pkl")
if os.path.exists(color_extractor_path):
try:
color_extractor = joblib.load(color_extractor_path)
print(f"Loaded color extractor from: {color_extractor_path}")
except Exception as e:
raise RuntimeError(
f"Failed to load required color extractor from {color_extractor_path}: {e}. "
f"Color features cannot be used without the properly trained color extractor. "
f"Either re-run without --use_color_features or ensure the color extractor file exists."
)
else:
raise FileNotFoundError(
f"Color extractor not found at: {color_extractor_path}. "
f"Color features require the trained color extractor file. "
f"Either re-run without --use_color_features or ensure the color extractor was saved during training."
)
except ImportError as e:
raise ImportError(
f"Failed to import ColorFeatureExtractor: {e}. "
f"Color features cannot be used without the color feature module. "
f"Either re-run without --use_color_features or install the required dependencies."
)
return model, feature_scaler, color_extractor
def evaluate_enhanced_model(model, scaler, test_dataset, candidate_params,
original_dataframe=None, dataset_type='SPT3G_1500d',
use_uq=False, detection_threshold=0.1,
use_desprior_candidates=False, use_color_features=False,
color_extractor=None, desprior_csv_path=None):
"""Evaluate enhanced model with UQ capabilities."""
print(f"Evaluating {'probabilistic' if use_uq else 'deterministic'} model on {len(test_dataset)} test images...")
print("Using rank-based evaluation (top-k candidate success tracking)...")
predictions = []
targets = []
distances = []
candidate_counts = []
failed_predictions = []
all_candidates_list = []
all_scores_list = []
test_images = []
sample_metadata = []
# UQ-specific tracking
all_probabilities_list = []
all_uncertainties_list = []
detection_counts = []
# Rank-based evaluation tracking
bcg_ranks = []
# FEATURE ANALYSIS: Collect features for post-analysis
all_features_list = [] # Store all features for analysis
sample_labels = [] # Store labels for analysis (rank-based success)
for i in range(len(test_dataset)):
sample = test_dataset[i]
image = sample['image']
true_bcg = sample['BCG']
filename = sample.get('filename', f'sample_{i}')
# Store image for visualization
test_images.append(image)
# Extract metadata from original dataframe if available
metadata = {'filename': filename}
# For BCG data, extract redshift directly from sample
if args.use_bcg_data and 'cluster_z' in sample:
cluster_z = sample['cluster_z']
if hasattr(cluster_z, 'numpy'):
cluster_z = cluster_z.numpy()
elif torch.is_tensor(cluster_z):
cluster_z = cluster_z.numpy()
metadata['z'] = float(cluster_z)
if original_dataframe is not None:
cluster_name = filename.replace('.tif', '').split('_')[0]
metadata['cluster_name'] = cluster_name
cluster_col = 'Cluster name' if 'Cluster name' in original_dataframe.columns else 'cluster_name'
if cluster_col in original_dataframe.columns:
matching_rows = original_dataframe[original_dataframe[cluster_col] == cluster_name]
if not matching_rows.empty:
row = matching_rows.iloc[0]
# Try multiple redshift column names
for z_col in ['z', 'Cluster z', 'redshift']:
if z_col in row and not pd.isna(row[z_col]):
metadata['z'] = row[z_col]
break
prob_cols = [col for col in row.index if 'prob' in col.lower()]
if prob_cols:
metadata['bcg_prob'] = row[prob_cols[0]]
# Extract RA/Dec for coordinate system
if 'BCG RA' in row:
metadata['bcg_ra'] = row['BCG RA']
if 'BCG Dec' in row:
metadata['bcg_dec'] = row['BCG Dec']
# Extract ALL BCG candidates for this cluster (for multiple RedMapper candidates)
all_bcg_candidates = []
for _, bcg_row in matching_rows.iterrows():
bcg_info = {}
# Get coordinates
if 'BCG RA' in bcg_row and 'BCG Dec' in bcg_row:
bcg_info['ra'] = bcg_row['BCG RA']
bcg_info['dec'] = bcg_row['BCG Dec']
# Get probability
prob_cols = [col for col in bcg_row.index if 'prob' in col.lower()]
if prob_cols and not pd.isna(bcg_row[prob_cols[0]]):
bcg_info['prob'] = bcg_row[prob_cols[0]]
# Get pixel coordinates (x, y)
if 'x' in bcg_row and 'y' in bcg_row:
bcg_info['x'] = bcg_row['x']
bcg_info['y'] = bcg_row['y']
# Only add if we have essential information
if bcg_info:
all_bcg_candidates.append(bcg_info)
if len(all_bcg_candidates) > 0:
metadata['all_bcg_candidates'] = all_bcg_candidates
# Extract additional features if using BCG data
additional_features = None
if args.use_bcg_data and args.use_additional_features:
if 'additional_features' in sample:
additional_features = sample['additional_features']
if hasattr(additional_features, 'numpy'):
additional_features = additional_features.numpy()
elif torch.is_tensor(additional_features):
additional_features = additional_features.numpy()
else:
# Fallback: extract additional features directly from sample
if 'cluster_z' in sample and 'delta_mstar_z' in sample:
cluster_z = sample['cluster_z']
delta_mstar_z = sample['delta_mstar_z']
if hasattr(cluster_z, 'numpy'):
cluster_z = cluster_z.numpy()
elif torch.is_tensor(cluster_z):
cluster_z = cluster_z.numpy()
if hasattr(delta_mstar_z, 'numpy'):
delta_mstar_z = delta_mstar_z.numpy()
elif torch.is_tensor(delta_mstar_z):
delta_mstar_z = delta_mstar_z.numpy()
additional_features = np.array([cluster_z, delta_mstar_z])
# Check if additional features are available for BCG data
# Make prediction with appropriate method
if use_uq:
results = predict_bcg_with_probabilities(
image, model, scaler,
detection_threshold=detection_threshold,
additional_features=additional_features,
use_desprior_candidates=use_desprior_candidates,
filename=filename,
dataset_type=dataset_type,
use_color_features=use_color_features,
color_extractor=color_extractor,
desprior_csv_path=desprior_csv_path,
**candidate_params
)
predicted_bcg = results['best_bcg']
all_candidates = results['all_candidates']
scores = results['probabilities'] # These are probabilities, not raw scores
probabilities = results['probabilities']
uncertainties = results['uncertainties']
detections = results['detections']
best_features = results['best_features'] # Get the best candidate's features
# FEATURE ANALYSIS: Store features for UQ case
all_features_list.append(best_features)
# Track UQ metrics
all_probabilities_list.append(probabilities)
all_uncertainties_list.append(uncertainties)
detection_counts.append(len(detections))
else:
# Use traditional method
if use_desprior_candidates:
# Use DESprior candidates from BCG dataset
from data.candidate_dataset_bcgs import create_desprior_candidate_dataset_from_files
# For testing, we need to extract DESprior candidates for this specific image
# This is a simplified approach - in practice, you'd want to cache this
# filename is already available from the loop variable
# Import required modules
from data.data_read_bcgs import BCGDataset
# Load DESprior candidates for this specific image/cluster
if desprior_csv_path is None:
raise ValueError("desprior_csv_path must be provided when use_desprior_candidates=True")
try:
candidates_df = pd.read_csv(desprior_csv_path)
file_candidates = candidates_df[candidates_df['filename'] == filename]
if len(file_candidates) == 0:
predicted_bcg = None
scores = np.array([])
all_candidates = np.array([])
else:
# Extract coordinates and candidate features
all_candidates = file_candidates[['x', 'y']].values
# Extract all auxiliary features: delta_mstar, starflag, rz, cluster_redshift
required_cols = ['delta_mstar', 'starflag', 'rz', 'cluster_redshift']
for col in required_cols:
if col not in file_candidates.columns:
print(f"ERROR: Required column '{col}' not found in DESprior candidates CSV")
print(f"Available columns: {list(file_candidates.columns)}")
import sys
sys.exit(1)
candidate_specific_features = file_candidates[required_cols].values
# Extract visual features and combine with candidate features
from utils.candidate_based_bcg import extract_candidate_features
visual_features, _ = extract_candidate_features(
image, all_candidates, patch_size=candidate_params.get('patch_size', 64),
include_context=True, include_color=use_color_features,
color_extractor=color_extractor
)
# Combine visual features with candidate-specific features
combined_features = np.hstack([visual_features, candidate_specific_features])
# NOTE: DESprior candidates use their own feature set and don't include additional BCG features
if scaler is not None:
scaled_features = scaler.transform(combined_features)
features_tensor = torch.FloatTensor(scaled_features)
else:
scaled_features = combined_features
features_tensor = torch.FloatTensor(combined_features)
with torch.no_grad():
scores = model(features_tensor).squeeze(-1).numpy()
best_idx = np.argmax(scores)
predicted_bcg = tuple(all_candidates[best_idx])
# FEATURE ANALYSIS: Store the best candidate's features
all_features_list.append(scaled_features[best_idx])
except Exception as e:
print(f"Warning: Failed to load DESprior candidates for {filename}: {e}")
predicted_bcg = None
scores = np.array([])
all_candidates = np.array([])
# FEATURE ANALYSIS: Add placeholder for failed cases
all_features_list.append(None)
else:
from utils.candidate_based_bcg import find_bcg_candidates, extract_candidate_features
all_candidates, intensities = find_bcg_candidates(image, **candidate_params)
if len(all_candidates) == 0:
predicted_bcg = None
scores = np.array([])
# FEATURE ANALYSIS: Add placeholder for no candidates case
all_features_list.append(None)
else:
features, _ = extract_candidate_features(
image, all_candidates, patch_size=candidate_params.get('patch_size', 64),
include_context=True, include_color=use_color_features,
color_extractor=color_extractor
)
# Append additional features if provided (e.g., from BCG dataset)
if additional_features is not None and len(features) > 0:
# Replicate additional features for each candidate
additional_features_repeated = np.tile(additional_features, (len(features), 1))
features = np.concatenate([features, additional_features_repeated], axis=1)
if scaler is not None:
scaled_features = scaler.transform(features)
features_tensor = torch.FloatTensor(scaled_features)
else:
scaled_features = features
features_tensor = torch.FloatTensor(features)
with torch.no_grad():
scores = model(features_tensor).squeeze(-1).numpy()
best_idx = np.argmax(scores)
predicted_bcg = tuple(all_candidates[best_idx])
# FEATURE ANALYSIS: Store the best candidate's features
all_features_list.append(scaled_features[best_idx])
# No UQ information available
probabilities = np.array([])
uncertainties = np.array([])
all_probabilities_list.append(probabilities)
all_uncertainties_list.append(uncertainties)
detection_counts.append(len(all_candidates) if len(all_candidates) > 0 else 0)
if predicted_bcg is None:
# No candidates found
failed_predictions.append({
'index': i,
'filename': filename,
'reason': 'no_candidates',
'true_bcg': true_bcg
})
# Add placeholder entries to maintain list consistency
predictions.append((np.nan, np.nan)) # Placeholder prediction
targets.append(true_bcg) # Still add the true target
distances.append(np.inf) # Infinite distance for failed predictions
candidate_counts.append(0) # No candidates found
all_candidates_list.append(np.array([]).reshape(0, 2))
all_scores_list.append(np.array([]))
sample_metadata.append(metadata)
bcg_ranks.append(None) # No rank when no candidates found
# FEATURE ANALYSIS: Add label for failed case (failure = 0)
sample_labels.append(0)
continue
# Compute distance error
distance = np.sqrt(np.sum((np.array(predicted_bcg) - true_bcg)**2))
distances.append(distance)
candidate_counts.append(len(all_candidates))
# Store results
predictions.append(predicted_bcg)
targets.append(true_bcg)
all_candidates_list.append(all_candidates)
all_scores_list.append(scores)
sample_metadata.append(metadata)
# Calculate rank of true BCG among all candidates
bcg_rank = calculate_bcg_rank(true_bcg, all_candidates, scores, distance_threshold=10.0)
bcg_ranks.append(bcg_rank)
# FEATURE ANALYSIS: Generate label based on rank and distance
# Success criteria: rank 1 (best candidate) OR distance <= 20 pixels
label = 1 if (bcg_rank == 1 or distance <= 20.0) else 0
sample_labels.append(label)
# Check for potential failure cases - only consider it a failure if:
# 1. Distance is large AND true BCG is not in top-3 candidates
# 2. Or if true BCG is not found among candidates at all
is_failure = False
failure_reason = None
if bcg_rank is None: # True BCG not found among candidates
is_failure = True
failure_reason = 'bcg_not_detected'
elif distance > 100 and bcg_rank > 3: # Large distance error AND not in top-3
is_failure = True
failure_reason = 'large_error_low_rank'
elif distance > 100: # Large distance error but in top-3 (could be acceptable)
failure_reason = 'large_error_good_rank' # Log but don't treat as failure
if is_failure:
failed_predictions.append({
'index': i,
'filename': filename,
'reason': failure_reason,
'predicted': predicted_bcg,
'true_bcg': true_bcg,
'distance': distance,
'rank': bcg_rank,
'candidates': all_candidates,
'scores': scores
})
# Compute metrics
distances = np.array(distances)
success_rates = {}
for threshold in [10, 20, 30, 50]:
success_rate = np.mean(distances <= threshold) if len(distances) > 0 else 0
success_rates[f'success_rate_{threshold}px'] = success_rate
# Calculate rank-based success metrics
valid_ranks = [rank for rank in bcg_ranks if rank is not None]
rank_metrics = {}
if len(valid_ranks) > 0:
# Count successes by rank (top-k accuracy)
rank_metrics['rank_1_success'] = len([r for r in valid_ranks if r == 1]) / len(predictions) if len(predictions) > 0 else 0
rank_metrics['rank_2_success'] = len([r for r in valid_ranks if r <= 2]) / len(predictions) if len(predictions) > 0 else 0
rank_metrics['rank_3_success'] = len([r for r in valid_ranks if r <= 3]) / len(predictions) if len(predictions) > 0 else 0
rank_metrics['rank_5_success'] = len([r for r in valid_ranks if r <= 5]) / len(predictions) if len(predictions) > 0 else 0
rank_metrics['mean_rank'] = np.mean(valid_ranks)
rank_metrics['median_rank'] = np.median(valid_ranks)
else:
rank_metrics = {
'rank_1_success': 0.0,
'rank_2_success': 0.0,
'rank_3_success': 0.0,
'rank_5_success': 0.0,
'mean_rank': float('inf'),
'median_rank': float('inf')
}
metrics = {
'n_predictions': len(predictions),
'n_failed': len(failed_predictions),
'mean_distance': np.mean(distances) if len(distances) > 0 else float('inf'),
'median_distance': np.median(distances) if len(distances) > 0 else float('inf'),
'std_distance': np.std(distances) if len(distances) > 0 else 0,
'min_distance': np.min(distances) if len(distances) > 0 else float('inf'),
'max_distance': np.max(distances) if len(distances) > 0 else 0,
'mean_candidates': np.mean(candidate_counts) if len(candidate_counts) > 0 else 0,
**success_rates,
**rank_metrics
}
# Add UQ-specific metrics
if use_uq:
metrics.update({
'mean_detections': np.mean(detection_counts) if len(detection_counts) > 0 else 0,
'detection_threshold': detection_threshold,
'mean_probability': np.mean([np.mean(p) for p in all_probabilities_list if len(p) > 0]),
'mean_uncertainty': np.mean([np.mean(u) for u in all_uncertainties_list if len(u) > 0])
})
return (predictions, targets, distances, failed_predictions, metrics,
all_candidates_list, all_scores_list, test_images, sample_metadata,
all_probabilities_list, all_uncertainties_list, bcg_ranks,
all_features_list, sample_labels)
def print_enhanced_evaluation_report(metrics, failed_predictions, use_uq=False):
"""Print detailed evaluation report with UQ information."""
print("\n" + "="*60)
print("ENHANCED EVALUATION RESULTS")
print("="*60)
print(f"Total predictions: {metrics['n_predictions']}")
print(f"Failed predictions: {metrics['n_failed']}")
print(f"Average candidates per image: {metrics['mean_candidates']:.1f}")
if use_uq:
print(f"Average detections per image: {metrics['mean_detections']:.1f}")
print(f"Detection threshold: {metrics['detection_threshold']:.3f}")
print(f"Average probability: {metrics.get('mean_probability', 0):.3f}")
print(f"Average uncertainty: {metrics.get('mean_uncertainty', 0):.3f}")
print()
if metrics['n_predictions'] > 0:
print("Distance Metrics:")
print(f" Mean error: {metrics['mean_distance']:.2f} pixels")
print(f" Median error: {metrics['median_distance']:.2f} pixels")
print(f" Std deviation: {metrics['std_distance']:.2f} pixels")
print(f" Min error: {metrics['min_distance']:.2f} pixels")
print(f" Max error: {metrics['max_distance']:.2f} pixels")
print()
print("Distance-based Success Rates:")
for key, value in metrics.items():
if 'success_rate' in key:
threshold = key.split('_')[-1]
print(f" Within {threshold}: {value*100:.1f}%")
print()
# Add rank-based success rates
if 'rank_1_success' in metrics:
print("Rank-based Success Rates:")
print(f" Best candidate (Rank 1): {metrics['rank_1_success']*100:.1f}%")
print(f" Top-2 candidates (Rank ≤2): {metrics['rank_2_success']*100:.1f}%")
print(f" Top-3 candidates (Rank ≤3): {metrics['rank_3_success']*100:.1f}%")
print(f" Top-5 candidates (Rank ≤5): {metrics['rank_5_success']*100:.1f}%")
if metrics['mean_rank'] != float('inf'):
print(f" Mean rank: {metrics['mean_rank']:.2f}")
print(f" Median rank: {metrics['median_rank']:.1f}")
print()
if failed_predictions:
print("Failed Prediction Analysis:")
failure_reasons = {}
for failure in failed_predictions:
reason = failure['reason']
failure_reasons[reason] = failure_reasons.get(reason, 0) + 1
for reason, count in failure_reasons.items():
print(f" {reason}: {count} cases")
def main(args):
"""Main evaluation function."""
# REPRODUCIBILITY: Set global seed and enable deterministic mode
set_global_seed(42)
make_deterministic(warn=True)
print("=" * 60)
print("ENHANCED BCG CLASSIFIER EVALUATION")
print("=" * 60)
print(f"Model: {args.model_path}")
print(f"Scaler: {args.scaler_path}")
print(f"Dataset: {args.dataset_type}")
print(f"Images: {args.image_dir}")
if args.use_uq:
print(f"Uncertainty quantification: threshold={args.detection_threshold}")
print()
# Load original truth table for metadata
print("Loading original truth table...")
original_df = pd.read_csv(args.truth_table)
if args.use_bcg_data:
# Use new BCG dataset
print("Loading new BCG dataset...")
print(f"Dataset type: {args.bcg_arcmin_type}")
if args.z_range:
print(f"Redshift filter: {args.z_range}")
if args.delta_mstar_z_range:
print(f"Delta M* z filter: {args.delta_mstar_z_range}")
# Create train and test datasets using the new BCG data reader
# During testing, we never use RedMapper probabilities as input features
# (that would be cheating - we want to predict without knowing the answer)
train_dataset, test_dataset = create_bcg_datasets(
dataset_type=args.bcg_arcmin_type,
split_ratio=0.8, # 80% train, 20% test
z_range=args.z_range,
delta_mstar_z_range=args.delta_mstar_z_range,
include_additional_features=args.use_additional_features,
include_redmapper_probs=False, # Never use RedMapper probs during testing
image_dir=args.image_dir, # Pass the image directory from command line
csv_path=args.bcg_csv_path # Pass custom BCG CSV path if provided
)
# Use test split for evaluation
test_subset = test_dataset
dataset = test_dataset # Set dataset for feature dimension analysis
print(f"Found {len(test_dataset)} samples in test split")
else:
# Load processed dataset (original approach)
print("Loading processed dataset...")
dataframe = prepare_dataframe(args.image_dir, args.truth_table, args.dataset_type)
print(f"Found {len(dataframe)} samples in dataset")
# Create BCG dataset
dataset = BCGDataset(args.image_dir, dataframe)
# Split dataset (use same random seed as training)
train_subset, val_subset, test_subset = split_dataset(dataset, train_ratio=0.7, val_ratio=0.2)
print(f"Using test split: {len(test_subset)} samples")
# Determine feature dimension by analyzing a sample
# This ensures we get the correct dimension regardless of options
print("Determining feature dimension from a sample...")
sample_image = dataset[0]['image']
if hasattr(sample_image, 'numpy'):
sample_image = sample_image.numpy()
elif torch.is_tensor(sample_image):
sample_image = sample_image.numpy()
# Get candidate parameters for feature extraction
candidate_params_sample = {
'min_distance': args.min_distance,
'threshold_rel': args.threshold_rel,
'exclude_border': args.exclude_border,
'max_candidates': args.max_candidates
}
# Load color extractor first if needed (before dimension calculation)
temp_color_extractor = None
if args.use_color_features:
print("Loading color extractor for feature dimension calculation...")