Spaces:
Runtime error
Runtime error
feat: add Docker/Apptainer support and FDR investigation tools
Browse files- Add Dockerfile for containerized deployment
- Add apptainer.def for HPC environments
- Add scripts to investigate FDR calibration data discrepancy
- Add precomputed probability verification test
- Remove ESM embedding (separate heavy dependency)
The FDR investigation checks for data leakage between calibration sets.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- apptainer.def +68 -0
- protein_conformal/cli.py +2 -51
- scripts/investigate_fdr.py +105 -0
- scripts/slurm_investigate.sh +36 -0
- scripts/test_precomputed_probs.py +80 -0
apptainer.def
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Bootstrap: docker
|
| 2 |
+
From: pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime
|
| 3 |
+
|
| 4 |
+
%labels
|
| 5 |
+
Author Ron Boger <ronboger@berkeley.edu>
|
| 6 |
+
Version 1.0
|
| 7 |
+
Description Conformal Protein Retrieval - Functional protein mining with statistical guarantees
|
| 8 |
+
|
| 9 |
+
%post
|
| 10 |
+
# Update and install system dependencies
|
| 11 |
+
apt-get update && apt-get install -y \
|
| 12 |
+
git \
|
| 13 |
+
wget \
|
| 14 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 15 |
+
|
| 16 |
+
# Install Python dependencies
|
| 17 |
+
pip install --no-cache-dir \
|
| 18 |
+
numpy \
|
| 19 |
+
pandas \
|
| 20 |
+
scipy \
|
| 21 |
+
scikit-learn \
|
| 22 |
+
matplotlib \
|
| 23 |
+
seaborn \
|
| 24 |
+
tqdm \
|
| 25 |
+
faiss-gpu \
|
| 26 |
+
biopython \
|
| 27 |
+
pytorch-lightning \
|
| 28 |
+
h5py \
|
| 29 |
+
transformers \
|
| 30 |
+
sentencepiece \
|
| 31 |
+
gradio>=4.0.0
|
| 32 |
+
|
| 33 |
+
# Create workspace
|
| 34 |
+
mkdir -p /workspace/data /workspace/results /workspace/protein_vec_models
|
| 35 |
+
|
| 36 |
+
%environment
|
| 37 |
+
export PYTHONPATH=/workspace
|
| 38 |
+
export GRADIO_SERVER_NAME=0.0.0.0
|
| 39 |
+
export GRADIO_SERVER_PORT=7860
|
| 40 |
+
|
| 41 |
+
%runscript
|
| 42 |
+
echo "Conformal Protein Retrieval (CPR)"
|
| 43 |
+
echo "Usage:"
|
| 44 |
+
echo " apptainer run cpr.sif cpr --help"
|
| 45 |
+
echo " apptainer run cpr.sif python -m protein_conformal.gradio_app"
|
| 46 |
+
exec "$@"
|
| 47 |
+
|
| 48 |
+
%help
|
| 49 |
+
Conformal Protein Retrieval (CPR)
|
| 50 |
+
|
| 51 |
+
This container provides tools for functional protein mining with
|
| 52 |
+
conformal guarantees, as described in:
|
| 53 |
+
"Functional protein mining with conformal guarantees"
|
| 54 |
+
Nature Communications (2025) 16:85
|
| 55 |
+
|
| 56 |
+
Usage:
|
| 57 |
+
# Run CLI
|
| 58 |
+
apptainer exec cpr.sif cpr embed --input seqs.fasta --output emb.npy
|
| 59 |
+
apptainer exec cpr.sif cpr search --query q.npy --database db.npy -o results.csv
|
| 60 |
+
|
| 61 |
+
# Run Gradio UI
|
| 62 |
+
apptainer exec cpr.sif python -m protein_conformal.gradio_app
|
| 63 |
+
|
| 64 |
+
# Interactive shell
|
| 65 |
+
apptainer shell cpr.sif
|
| 66 |
+
|
| 67 |
+
Build:
|
| 68 |
+
apptainer build cpr.sif apptainer.def
|
protein_conformal/cli.py
CHANGED
|
@@ -46,10 +46,9 @@ def cmd_embed(args):
|
|
| 46 |
embeddings = _embed_protein_vec(sequences, device, args)
|
| 47 |
elif args.model == 'clean':
|
| 48 |
embeddings = _embed_clean(sequences, device, args)
|
| 49 |
-
elif args.model == 'esm':
|
| 50 |
-
embeddings = _embed_esm(sequences, device, args)
|
| 51 |
else:
|
| 52 |
print(f"Unknown model: {args.model}")
|
|
|
|
| 53 |
sys.exit(1)
|
| 54 |
|
| 55 |
print(f"Embeddings shape: {embeddings.shape}")
|
|
@@ -157,50 +156,6 @@ def _embed_clean(sequences, device, args):
|
|
| 157 |
return clean_embeddings
|
| 158 |
|
| 159 |
|
| 160 |
-
def _embed_esm(sequences, device, args):
|
| 161 |
-
"""Embed using ESM-1b or ESM2 model."""
|
| 162 |
-
import numpy as np
|
| 163 |
-
import torch
|
| 164 |
-
|
| 165 |
-
try:
|
| 166 |
-
import esm
|
| 167 |
-
except ImportError:
|
| 168 |
-
print("Error: ESM package not installed.")
|
| 169 |
-
print("Install with: pip install fair-esm")
|
| 170 |
-
sys.exit(1)
|
| 171 |
-
|
| 172 |
-
esm_version = getattr(args, 'esm_version', '1b')
|
| 173 |
-
print(f"Loading ESM-{esm_version} model...")
|
| 174 |
-
|
| 175 |
-
if esm_version == '2':
|
| 176 |
-
model, alphabet = esm.pretrained.esm2_t33_650M_UR50D()
|
| 177 |
-
else:
|
| 178 |
-
model, alphabet = esm.pretrained.esm1b_t33_650M_UR50S()
|
| 179 |
-
|
| 180 |
-
batch_converter = alphabet.get_batch_converter()
|
| 181 |
-
model = model.to(device).eval()
|
| 182 |
-
|
| 183 |
-
# Process in batches
|
| 184 |
-
batch_size = getattr(args, 'batch_size', 4)
|
| 185 |
-
embeddings = []
|
| 186 |
-
|
| 187 |
-
for i in range(0, len(sequences), batch_size):
|
| 188 |
-
batch_seqs = sequences[i:i+batch_size]
|
| 189 |
-
batch_data = [(f"seq_{j}", seq) for j, seq in enumerate(batch_seqs)]
|
| 190 |
-
|
| 191 |
-
_, _, batch_tokens = batch_converter(batch_data)
|
| 192 |
-
batch_tokens = batch_tokens.to(device)
|
| 193 |
-
|
| 194 |
-
with torch.no_grad():
|
| 195 |
-
results = model(batch_tokens, repr_layers=[33], return_contacts=False)
|
| 196 |
-
# Mean pooling over sequence length
|
| 197 |
-
batch_emb = results["representations"][33].mean(dim=1).cpu().numpy()
|
| 198 |
-
embeddings.append(batch_emb)
|
| 199 |
-
|
| 200 |
-
if (i + batch_size) % 20 == 0 or i + batch_size >= len(sequences):
|
| 201 |
-
print(f" Processed {min(i + batch_size, len(sequences))}/{len(sequences)}")
|
| 202 |
-
|
| 203 |
-
return np.concatenate(embeddings)
|
| 204 |
|
| 205 |
|
| 206 |
def cmd_search(args):
|
|
@@ -431,15 +386,11 @@ def main():
|
|
| 431 |
p_embed.add_argument('--input', '-i', required=True, help='Input FASTA file')
|
| 432 |
p_embed.add_argument('--output', '-o', required=True, help='Output .npy file for embeddings')
|
| 433 |
p_embed.add_argument('--model', '-m', default='protein-vec',
|
| 434 |
-
choices=['protein-vec', 'clean'
|
| 435 |
help='Embedding model (default: protein-vec)')
|
| 436 |
p_embed.add_argument('--cpu', action='store_true', help='Force CPU even if GPU available')
|
| 437 |
p_embed.add_argument('--clean-model', default='split100',
|
| 438 |
help='CLEAN model variant (default: split100)')
|
| 439 |
-
p_embed.add_argument('--esm-version', default='1b', choices=['1b', '2'],
|
| 440 |
-
help='ESM version (default: 1b)')
|
| 441 |
-
p_embed.add_argument('--batch-size', type=int, default=4,
|
| 442 |
-
help='Batch size for ESM (default: 4)')
|
| 443 |
p_embed.set_defaults(func=cmd_embed)
|
| 444 |
|
| 445 |
# search command
|
|
|
|
| 46 |
embeddings = _embed_protein_vec(sequences, device, args)
|
| 47 |
elif args.model == 'clean':
|
| 48 |
embeddings = _embed_clean(sequences, device, args)
|
|
|
|
|
|
|
| 49 |
else:
|
| 50 |
print(f"Unknown model: {args.model}")
|
| 51 |
+
print("Available models: protein-vec, clean")
|
| 52 |
sys.exit(1)
|
| 53 |
|
| 54 |
print(f"Embeddings shape: {embeddings.shape}")
|
|
|
|
| 156 |
return clean_embeddings
|
| 157 |
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
|
| 161 |
def cmd_search(args):
|
|
|
|
| 386 |
p_embed.add_argument('--input', '-i', required=True, help='Input FASTA file')
|
| 387 |
p_embed.add_argument('--output', '-o', required=True, help='Output .npy file for embeddings')
|
| 388 |
p_embed.add_argument('--model', '-m', default='protein-vec',
|
| 389 |
+
choices=['protein-vec', 'clean'],
|
| 390 |
help='Embedding model (default: protein-vec)')
|
| 391 |
p_embed.add_argument('--cpu', action='store_true', help='Force CPU even if GPU available')
|
| 392 |
p_embed.add_argument('--clean-model', default='split100',
|
| 393 |
help='CLEAN model variant (default: split100)')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
p_embed.set_defaults(func=cmd_embed)
|
| 395 |
|
| 396 |
# search command
|
scripts/investigate_fdr.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""
|
| 3 |
+
Investigate FDR calibration discrepancy between datasets.
|
| 4 |
+
Checks for data leakage and compares calibration results.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import sys
|
| 9 |
+
sys.path.insert(0, '.')
|
| 10 |
+
from protein_conformal.util import get_sims_labels, get_thresh_FDR
|
| 11 |
+
|
| 12 |
+
print("=" * 60)
|
| 13 |
+
print("FDR Calibration Dataset Investigation")
|
| 14 |
+
print("=" * 60)
|
| 15 |
+
print()
|
| 16 |
+
|
| 17 |
+
# Load both calibration datasets
|
| 18 |
+
print("Loading datasets...")
|
| 19 |
+
pfam_new = np.load('data/pfam_new_proteins.npy', allow_pickle=True)
|
| 20 |
+
backup_data = np.load('/groups/doudna/projects/ronb/conformal_backup/protein-conformal/data/conformal_pfam_with_lookup_dataset.npy', allow_pickle=True)
|
| 21 |
+
|
| 22 |
+
print(f'pfam_new_proteins.npy: {len(pfam_new)} samples')
|
| 23 |
+
print(f'backup dataset: {len(backup_data)} samples')
|
| 24 |
+
print()
|
| 25 |
+
|
| 26 |
+
# Check for overlap (potential leakage)
|
| 27 |
+
print("Checking for overlap between datasets...")
|
| 28 |
+
pfam_metas = set(d['meta'] for d in pfam_new)
|
| 29 |
+
backup_metas = set(d['meta'] for d in backup_data)
|
| 30 |
+
overlap = pfam_metas & backup_metas
|
| 31 |
+
print(f" Unique in pfam_new: {len(pfam_metas)}")
|
| 32 |
+
print(f" Unique in backup: {len(backup_metas)}")
|
| 33 |
+
print(f" Overlap: {len(overlap)} ({len(overlap)/len(pfam_metas)*100:.1f}% of pfam_new)")
|
| 34 |
+
print()
|
| 35 |
+
|
| 36 |
+
# Compare similarity distributions
|
| 37 |
+
print("Similarity score distributions:")
|
| 38 |
+
sims_new, labels_new = get_sims_labels(pfam_new[:500], partial=False)
|
| 39 |
+
sims_backup, labels_backup = get_sims_labels(backup_data[:500], partial=False)
|
| 40 |
+
|
| 41 |
+
print(f" pfam_new (500 samples):")
|
| 42 |
+
print(f" Similarity: min={sims_new.min():.6f}, max={sims_new.max():.6f}, mean={sims_new.mean():.6f}")
|
| 43 |
+
print(f" Labels: {labels_new.sum()}/{labels_new.size} positive ({labels_new.mean()*100:.1f}%)")
|
| 44 |
+
print()
|
| 45 |
+
print(f" backup (500 samples):")
|
| 46 |
+
print(f" Similarity: min={sims_backup.min():.6f}, max={sims_backup.max():.6f}, mean={sims_backup.mean():.6f}")
|
| 47 |
+
print(f" Labels: {labels_backup.sum()}/{labels_backup.size} positive ({labels_backup.mean()*100:.1f}%)")
|
| 48 |
+
print()
|
| 49 |
+
|
| 50 |
+
# Run FDR calibration on both with same parameters
|
| 51 |
+
print("Running FDR calibration (alpha=0.1, n_calib=1000, 10 trials)...")
|
| 52 |
+
print()
|
| 53 |
+
|
| 54 |
+
def run_fdr_trials(data, name, n_trials=10, n_calib=1000):
|
| 55 |
+
lhats = []
|
| 56 |
+
risks = []
|
| 57 |
+
tprs = []
|
| 58 |
+
|
| 59 |
+
for trial in range(n_trials):
|
| 60 |
+
np.random.seed(42 + trial)
|
| 61 |
+
np.random.shuffle(data)
|
| 62 |
+
cal_data = data[:n_calib]
|
| 63 |
+
test_data = data[n_calib:n_calib+500]
|
| 64 |
+
|
| 65 |
+
X_cal, y_cal = get_sims_labels(cal_data, partial=False)
|
| 66 |
+
X_test, y_test = get_sims_labels(test_data, partial=False)
|
| 67 |
+
|
| 68 |
+
lhat, fdr_cal = get_thresh_FDR(y_cal, X_cal, alpha=0.1, delta=0.5, N=100)
|
| 69 |
+
lhats.append(lhat)
|
| 70 |
+
|
| 71 |
+
# Calculate test risk and TPR
|
| 72 |
+
preds = (X_test >= lhat).astype(int)
|
| 73 |
+
tp = np.sum((preds == 1) & (y_test == 1))
|
| 74 |
+
fp = np.sum((preds == 1) & (y_test == 0))
|
| 75 |
+
fn = np.sum((preds == 0) & (y_test == 1))
|
| 76 |
+
|
| 77 |
+
tpr = tp / (tp + fn) if (tp + fn) > 0 else 0
|
| 78 |
+
risk = fp / (fp + tp) if (fp + tp) > 0 else 0
|
| 79 |
+
|
| 80 |
+
tprs.append(tpr)
|
| 81 |
+
risks.append(risk)
|
| 82 |
+
|
| 83 |
+
print(f"{name}:")
|
| 84 |
+
print(f" λ (threshold): {np.mean(lhats):.10f} ± {np.std(lhats):.10f}")
|
| 85 |
+
print(f" Risk (FDR): {np.mean(risks):.4f} ± {np.std(risks):.4f}")
|
| 86 |
+
print(f" TPR: {np.mean(tprs)*100:.1f}% ± {np.std(tprs)*100:.1f}%")
|
| 87 |
+
print()
|
| 88 |
+
return lhats, risks, tprs
|
| 89 |
+
|
| 90 |
+
lhats_new, risks_new, tprs_new = run_fdr_trials(pfam_new.copy(), "pfam_new_proteins")
|
| 91 |
+
lhats_backup, risks_backup, tprs_backup = run_fdr_trials(backup_data.copy(), "backup_dataset")
|
| 92 |
+
|
| 93 |
+
print("=" * 60)
|
| 94 |
+
print("CONCLUSION")
|
| 95 |
+
print("=" * 60)
|
| 96 |
+
if abs(np.mean(lhats_new) - np.mean(lhats_backup)) < 0.00001:
|
| 97 |
+
print("✓ Thresholds are similar - datasets likely compatible")
|
| 98 |
+
else:
|
| 99 |
+
print("⚠ Thresholds differ significantly!")
|
| 100 |
+
print(f" Difference: {abs(np.mean(lhats_new) - np.mean(lhats_backup)):.10f}")
|
| 101 |
+
|
| 102 |
+
if len(overlap) > len(pfam_metas) * 0.5:
|
| 103 |
+
print("⚠ High overlap between datasets - potential data source")
|
| 104 |
+
else:
|
| 105 |
+
print("✓ Low overlap - datasets appear independent")
|
scripts/slurm_investigate.sh
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#SBATCH --job-name=cpr-investigate
|
| 3 |
+
#SBATCH --output=logs/cpr-investigate-%j.out
|
| 4 |
+
#SBATCH --error=logs/cpr-investigate-%j.err
|
| 5 |
+
#SBATCH --time=1:00:00
|
| 6 |
+
#SBATCH --mem=32G
|
| 7 |
+
#SBATCH --cpus-per-task=4
|
| 8 |
+
|
| 9 |
+
# CPR Investigation - FDR calibration and precomputed probability verification
|
| 10 |
+
set -e
|
| 11 |
+
mkdir -p logs data
|
| 12 |
+
|
| 13 |
+
source ~/.bashrc
|
| 14 |
+
eval "$(conda shell.bash hook)"
|
| 15 |
+
conda activate conformal-s
|
| 16 |
+
|
| 17 |
+
cd /groups/doudna/projects/ronb/conformal-protein-retrieval
|
| 18 |
+
|
| 19 |
+
echo "========================================"
|
| 20 |
+
echo "CPR Investigation"
|
| 21 |
+
echo "Date: $(date)"
|
| 22 |
+
echo "Node: $(hostname)"
|
| 23 |
+
echo "========================================"
|
| 24 |
+
echo ""
|
| 25 |
+
|
| 26 |
+
echo "=== Part 1: FDR Calibration Investigation ==="
|
| 27 |
+
python scripts/investigate_fdr.py
|
| 28 |
+
echo ""
|
| 29 |
+
|
| 30 |
+
echo "=== Part 2: Precomputed Probability Verification ==="
|
| 31 |
+
python scripts/test_precomputed_probs.py
|
| 32 |
+
echo ""
|
| 33 |
+
|
| 34 |
+
echo "========================================"
|
| 35 |
+
echo "Completed: $(date)"
|
| 36 |
+
echo "========================================"
|
scripts/test_precomputed_probs.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""
|
| 3 |
+
Test that precomputed probability lookup gives same results as computing from scratch.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import sys
|
| 9 |
+
sys.path.insert(0, '.')
|
| 10 |
+
from protein_conformal.util import simplifed_venn_abers_prediction, get_sims_labels
|
| 11 |
+
|
| 12 |
+
print("=" * 60)
|
| 13 |
+
print("Precomputed Probability Verification")
|
| 14 |
+
print("=" * 60)
|
| 15 |
+
print()
|
| 16 |
+
|
| 17 |
+
# Load calibration data
|
| 18 |
+
print("Loading calibration data...")
|
| 19 |
+
cal_data = np.load('data/pfam_new_proteins.npy', allow_pickle=True)
|
| 20 |
+
np.random.seed(42)
|
| 21 |
+
np.random.shuffle(cal_data)
|
| 22 |
+
cal_subset = cal_data[:100]
|
| 23 |
+
|
| 24 |
+
X_cal, y_cal = get_sims_labels(cal_subset, partial=False)
|
| 25 |
+
X_cal = X_cal.flatten()
|
| 26 |
+
y_cal = y_cal.flatten()
|
| 27 |
+
print(f" Calibration pairs: {len(X_cal)}")
|
| 28 |
+
print(f" Similarity range: [{X_cal.min():.6f}, {X_cal.max():.6f}]")
|
| 29 |
+
print()
|
| 30 |
+
|
| 31 |
+
# Create precomputed lookup table
|
| 32 |
+
print("Creating precomputed lookup table (100 bins)...")
|
| 33 |
+
min_sim, max_sim = X_cal.min(), X_cal.max()
|
| 34 |
+
bins = np.linspace(min_sim, max_sim, 100)
|
| 35 |
+
|
| 36 |
+
lookup = []
|
| 37 |
+
for sim in bins:
|
| 38 |
+
p0, p1 = simplifed_venn_abers_prediction(X_cal, y_cal, sim)
|
| 39 |
+
lookup.append({'similarity': sim, 'p0': p0, 'p1': p1, 'prob': (p0+p1)/2})
|
| 40 |
+
|
| 41 |
+
lookup_df = pd.DataFrame(lookup)
|
| 42 |
+
print(f" Lookup table: {len(lookup_df)} entries")
|
| 43 |
+
print()
|
| 44 |
+
|
| 45 |
+
# Test on random similarity values
|
| 46 |
+
print("Testing lookup vs direct computation on 20 random values...")
|
| 47 |
+
test_sims = np.random.uniform(min_sim, max_sim, 20)
|
| 48 |
+
|
| 49 |
+
print(f"{'Similarity':>12} | {'Direct':>8} | {'Lookup':>8} | {'Diff':>8}")
|
| 50 |
+
print("-" * 50)
|
| 51 |
+
|
| 52 |
+
max_diff = 0
|
| 53 |
+
for sim in test_sims:
|
| 54 |
+
# Direct computation
|
| 55 |
+
p0, p1 = simplifed_venn_abers_prediction(X_cal, y_cal, sim)
|
| 56 |
+
prob_direct = (p0 + p1) / 2
|
| 57 |
+
|
| 58 |
+
# Lookup with interpolation
|
| 59 |
+
lower = lookup_df[lookup_df['similarity'] <= sim].iloc[-1] if len(lookup_df[lookup_df['similarity'] <= sim]) > 0 else lookup_df.iloc[0]
|
| 60 |
+
upper = lookup_df[lookup_df['similarity'] >= sim].iloc[0] if len(lookup_df[lookup_df['similarity'] >= sim]) > 0 else lookup_df.iloc[-1]
|
| 61 |
+
prob_lookup = (lower['prob'] + upper['prob']) / 2
|
| 62 |
+
|
| 63 |
+
diff = abs(prob_direct - prob_lookup)
|
| 64 |
+
max_diff = max(max_diff, diff)
|
| 65 |
+
print(f"{sim:12.8f} | {prob_direct:8.4f} | {prob_lookup:8.4f} | {diff:8.4f}")
|
| 66 |
+
|
| 67 |
+
print()
|
| 68 |
+
print("=" * 60)
|
| 69 |
+
if max_diff < 0.01:
|
| 70 |
+
print(f"✓ VERIFICATION PASSED (max diff: {max_diff:.4f})")
|
| 71 |
+
print(" Precomputed lookup matches direct computation")
|
| 72 |
+
else:
|
| 73 |
+
print(f"⚠ VERIFICATION WARNING (max diff: {max_diff:.4f})")
|
| 74 |
+
print(" Consider using more bins for better accuracy")
|
| 75 |
+
print("=" * 60)
|
| 76 |
+
|
| 77 |
+
# Save the lookup table
|
| 78 |
+
output_path = 'data/sim2prob_lookup.csv'
|
| 79 |
+
lookup_df.to_csv(output_path, index=False)
|
| 80 |
+
print(f"\nSaved lookup table to: {output_path}")
|