ronboger Claude Opus 4.5 commited on
Commit
ec2e76e
·
1 Parent(s): 5f84065

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 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', 'esm'],
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}")