import os import torch import torch.nn as nn from torchvision import models, transforms from PIL import Image import numpy as np import pandas as pd import torch.nn.functional as F import matplotlib.pyplot as plt # 1. CONFIGURATION BASE_DIR = os.path.dirname(os.path.abspath(__file__)) IMG_DIR = os.path.join(BASE_DIR, "..", "datos_para_ai") ENCODER_PATH = os.path.join(BASE_DIR, "..", "models", "msi_encoder_trained.pth") OUTPUT_DIR = os.path.join(BASE_DIR, "..", "figures", "guided_validation") os.makedirs(OUTPUT_DIR, exist_ok=True) DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") # FIX: Restore the full list of verified vascular landmarks to build a robust structural vector ANCHOR_MZS = [155.75, 156.07, 156.40, 273.77, 274.09, 275.07, 286.12, 304.00, 304.33] SIMI_THRESHOLD = 0.85 # Calibrated to capture true negative space co-exclusions down to 0.85 class MSI_Structural_Encoder(nn.Module): """ The exact high-fidelity custom architecture used during your successful zero-padded 256x256 contrastive training run. """ def __init__(self): super(MSI_Structural_Encoder, self).__init__() self.features = nn.Sequential( # Layer 1: Input 256x256x1 (Standardized size) nn.Conv2d(1, 32, kernel_size=5, stride=2, padding=2, bias=False), nn.GroupNorm(4, 32), nn.ReLU(), # Layer 2 nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1, bias=False), nn.GroupNorm(8, 64), nn.ReLU(), # Layer 3 nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1, bias=False), nn.GroupNorm(16, 128), nn.ReLU(), # Layer 4 nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1, bias=False), nn.GroupNorm(32, 256), nn.ReLU(), nn.AdaptiveAvgPool2d((1, 1)) ) def forward(self, x): h = self.features(x).squeeze(-1).squeeze(-1) return h def load_trained_encoder(path): if not os.path.exists(path): raise FileNotFoundError(f"Missing model checkpoint: {path}") model = MSI_Structural_Encoder() # Load only the feature extractor weights to match the save format in CNN_proof_1.py model.features.load_state_dict(torch.load(path, map_location=DEVICE)) print(f"Successfully synchronized weights with custom encoder architecture: {path}") model.to(DEVICE) model.eval() return model if __name__ == "__main__": encoder = load_trained_encoder(ENCODER_PATH) transform = transforms.Compose([ transforms.Resize((256, 256)), # Standardized size matching training transforms.ToTensor() ]) # 3. SCAN ALL IMAGE MAPS all_files = sorted([f for f in os.listdir(IMG_DIR) if f.endswith('.png')]) print(f"Discovered {len(all_files)} total ion slices in data pool.") ion_registry = [] for f in all_files: try: mz_extracted = float(f.replace("ion_", "").replace(".png", "")) ion_registry.append({"filename": f, "mz": mz_extracted}) except ValueError: continue df_ions = pd.DataFrame(ion_registry) # 4. EXTRACT VECTORS print("Extracting L2-normalized latent fingerprints...") latent_vectors = {} with torch.no_grad(): for _, row in df_ions.iterrows(): fpath = os.path.join(IMG_DIR, row['filename']) img_tensor = transform(Image.open(fpath).convert("L")).unsqueeze(0).to(DEVICE) feat = encoder(img_tensor) feat_norm = F.normalize(feat, p=2, dim=1) # Project onto unit hypersphere latent_vectors[row['filename']] = feat_norm.squeeze(0).cpu().numpy() # 5. CONSTRUCT SEED anchor_features = [] found_anchors = [] for anchor in ANCHOR_MZS: match = df_ions[np.isclose(df_ions['mz'], anchor, atol=0.05)] if not match.empty: fname = match.iloc[0]['filename'] anchor_features.append(latent_vectors[fname]) found_anchors.append(fname) if len(anchor_features) == 0: print("Error: None of your specified anchor vein images were located.") exit() vascular_profile_centroid = np.mean(anchor_features, axis=0) vascular_profile_centroid /= np.linalg.norm(vascular_profile_centroid) print(f"Generated unified vascular profile from {len(found_anchors)} anchor ions.") # 6. EVALUATE SIMILARITY guided_matches = [] for fname, vector in latent_vectors.items(): similarity = np.dot(vector, vascular_profile_centroid) if similarity >= SIMI_THRESHOLD: mz_val = df_ions[df_ions['filename'] == fname]['mz'].values[0] guided_matches.append({"filename": fname, "mz": mz_val, "similarity": similarity}) df_guided = pd.DataFrame(guided_matches).sort_values(by="similarity", ascending=False) print(f"\nFound {len(df_guided)} ions displaying vascular alignment (Cosine Sim >= {SIMI_THRESHOLD})") csv_out = os.path.join(OUTPUT_DIR, "guided_vascular_ions.csv") df_guided.to_csv(csv_out, index=False) # 7. GENERATE GALLERIES max_per_page = 12 chunks = [df_guided[i:i + max_per_page] for i in range(0, len(df_guided), max_per_page)] for page_idx, chunk in enumerate(chunks): num_plots = len(chunk) rows = int(np.ceil(num_plots / 4)) cols = min(num_plots, 4) fig, axes = plt.subplots(rows, cols, figsize=(16, rows * 4)) fig.suptitle(f"Guided Vascular Affinity Grouping - Tier {page_idx + 1}", fontsize=16, y=0.98) flat_axes = axes.flatten() if isinstance(axes, np.ndarray) else [axes] for idx, (_, row) in enumerate(chunk.iterrows()): ax = flat_axes[idx] img_path = os.path.join(IMG_DIR, row['filename']) img = Image.open(img_path) # Use 'viridis' for improved structural visibility ax.imshow(img, cmap='viridis') ax.set_title(f"m/z {row['mz']:.2f}\nSim: {row['similarity']:.4f}", fontsize=10) ax.axis('off') for empty_idx in range(num_plots, len(flat_axes)): flat_axes[empty_idx].axis('off') plt.tight_layout() gallery_out = os.path.join(OUTPUT_DIR, f"Guided_Vascular_Gallery_Page_{page_idx + 1}.png") plt.savefig(gallery_out, dpi=200, bbox_inches='tight') plt.close() print("\nGuided extraction validation cycle complete.") print("\n--- RUNNING ISOTOPIC FUNCTIONAL VALIDATION ---") verified_biological_count = 0 for idx, row in df_guided.iterrows(): target_mz = row['mz'] isotope_mz = target_mz + 1.0033 # Check if the expected isotope exists within your parsed data pool isotope_match = df_ions[np.isclose(df_ions['mz'], isotope_mz, atol=0.02)] if not isotope_match.empty: iso_fname = isotope_match.iloc[0]['filename'] iso_vector = latent_vectors[iso_fname] # Calculate similarity of the isotope to your core vascular vector iso_sim = np.dot(iso_vector, vascular_profile_centroid) if iso_sim >= 0.80: print(f"Verified Structural Pair: Base m/z {target_mz:.2f} (Sim: {row['similarity']:.4f}) -> Isotope m/z {isotope_mz:.2f} (Sim: {iso_sim:.4f})") verified_biological_count += 1 print(f"Validation complete. Confirmed {verified_biological_count} true isotopic structural pairs.")