MSI_Julia_CNN/scripts_python/guided_vascular.py

196 lines
7.5 KiB
Python

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.")