ronboger Claude Opus 4.5 commited on
Commit
89b7db8
·
1 Parent(s): f49c4e2

feat: add FNR threshold computation and partial match support

Browse files

- Create scripts/compute_fnr_table.py for FNR threshold computation
- Add --partial flag to FDR and FNR scripts for partial Pfam matches
- Create SLURM script for FNR computation
- Update CLAUDE.md with session progress and CLEAN test results

CLEAN embeddings tested successfully on GPU (H200):
- Requires fair-esm>=2.0.0
- Output: 128-dimensional embeddings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

CLAUDE.md CHANGED
@@ -192,6 +192,50 @@ The correct dataset (`pfam_new_proteins.npy`) has diverse families and matches p
192
 
193
  ---
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  ### Session Notes Template
196
 
197
  ```
 
192
 
193
  ---
194
 
195
+ ### 2026-02-03 ~09:30 PST - Threshold Computation & CLEAN Integration
196
+
197
+ **Completed:**
198
+ - [x] Fixed Apptainer mount point issue (`%setup` section creates dirs before container init)
199
+ - [x] Submitted FDR threshold job (100 trials × 8 alpha levels) - Job 1012489
200
+ - [x] Created `scripts/compute_fnr_table.py` for FNR threshold computation
201
+ - [x] Added `--partial` flag to both FDR and FNR scripts for partial match support
202
+ - [x] Submitted FNR threshold job - Job 1012530
203
+ - [x] Tested CLEAN embeddings on GPU - **WORKING**
204
+ - [x] Committed and pushed Apptainer fixes to origin
205
+
206
+ **CLEAN Embedding Test Results:**
207
+ ```
208
+ GPU: NVIDIA H200
209
+ Embeddings shape: (2, 128) # CLEAN uses 128-dim, not 1024 like Protein-Vec
210
+ Min: -2.7802, Max: 2.5827, Mean: 0.0498
211
+ ```
212
+ - Requires: `pip install fair-esm>=2.0.0`
213
+ - CLEAN model weights: `CLEAN_repo/app/data/pretrained/CLEAN_pretrained/`
214
+
215
+ **Blocked:**
216
+ - **Apptainer build**: glibc 2.33/2.34 mismatch - PyTorch 2.1.0 has older glibc than cluster's fakeroot
217
+ - **Fix**: Update to `pytorch/pytorch:2.4.0-cuda12.1-cudnn9-runtime` base image
218
+
219
+ **Running Jobs:**
220
+ - Job 1012489: FDR thresholds (exact match) - ~50 min, still on α=0.001
221
+ - Job 1012530: FNR thresholds (exact + partial) - just started
222
+
223
+ **Files Created/Modified:**
224
+ - `scripts/compute_fnr_table.py` - NEW: FNR threshold computation
225
+ - `scripts/slurm_compute_fnr_thresholds.sh` - NEW: SLURM job for FNR
226
+ - `scripts/compute_fdr_table.py` - Added `--partial` flag
227
+ - `scripts/slurm_compute_fdr_thresholds.sh` - Increased time/memory
228
+ - `apptainer.def` - Added `%setup` section for mount points
229
+
230
+ **Next Steps:**
231
+ 1. Wait for FDR job to complete, verify α=0.1 ≈ 0.999980225
232
+ 2. Submit partial match FDR job once exact matches verified
233
+ 3. Update README with CLEAN embedding instructions
234
+ 4. Update Apptainer base image to PyTorch 2.4+
235
+ 5. Update GETTING_STARTED.md with computed thresholds
236
+
237
+ ---
238
+
239
  ### Session Notes Template
240
 
241
  ```
scripts/compute_fdr_table.py CHANGED
@@ -34,7 +34,8 @@ from protein_conformal.util import get_thresh_FDR, get_sims_labels
34
 
35
 
36
  def compute_fdr_threshold(cal_data, alpha: float, n_trials: int = 100,
37
- n_calib: int = 1000, seed: int = None) -> dict:
 
38
  """
39
  Compute FDR threshold at a given alpha level.
40
 
@@ -56,7 +57,7 @@ def compute_fdr_threshold(cal_data, alpha: float, n_trials: int = 100,
56
  trial_data = cal_data[:n_calib]
57
 
58
  # Get similarity scores and labels
59
- X_cal, y_cal = get_sims_labels(trial_data, partial=False)
60
 
61
  # Compute threshold
62
  l_hat, risk = get_thresh_FDR(X_cal, y_cal, alpha=alpha)
@@ -108,12 +109,23 @@ def main():
108
  default=42,
109
  help='Random seed for reproducibility (default: 42)'
110
  )
 
 
 
 
 
111
 
112
  args = parser.parse_args()
113
 
 
 
 
 
114
  # Standard alpha levels that users commonly need
115
  alpha_levels = [0.001, 0.005, 0.01, 0.02, 0.05, 0.1, 0.15, 0.2]
116
 
 
 
117
  print(f"Loading calibration data from {args.calibration}...")
118
  cal_data = np.load(args.calibration, allow_pickle=True)
119
  print(f" Loaded {len(cal_data)} calibration samples")
@@ -122,6 +134,7 @@ def main():
122
  print(f" Trials per alpha: {args.n_trials}")
123
  print(f" Calibration samples per trial: {args.n_calib}")
124
  print(f" Random seed: {args.seed}")
 
125
  print()
126
 
127
  results = []
@@ -136,7 +149,8 @@ def main():
136
  alpha=alpha,
137
  n_trials=args.n_trials,
138
  n_calib=args.n_calib,
139
- seed=trial_seed
 
140
  )
141
 
142
  results.append({
@@ -170,7 +184,8 @@ def main():
170
  print(f"\nSaved to {args.output}")
171
 
172
  # Also save a simple version for easy lookup
173
- simple_output = args.output.parent / 'fdr_thresholds_simple.csv'
 
174
  df[['alpha', 'threshold_mean']].rename(
175
  columns={'threshold_mean': 'lambda_threshold'}
176
  ).to_csv(simple_output, index=False)
 
34
 
35
 
36
  def compute_fdr_threshold(cal_data, alpha: float, n_trials: int = 100,
37
+ n_calib: int = 1000, seed: int = None,
38
+ partial: bool = False) -> dict:
39
  """
40
  Compute FDR threshold at a given alpha level.
41
 
 
57
  trial_data = cal_data[:n_calib]
58
 
59
  # Get similarity scores and labels
60
+ X_cal, y_cal = get_sims_labels(trial_data, partial=partial)
61
 
62
  # Compute threshold
63
  l_hat, risk = get_thresh_FDR(X_cal, y_cal, alpha=alpha)
 
109
  default=42,
110
  help='Random seed for reproducibility (default: 42)'
111
  )
112
+ parser.add_argument(
113
+ '--partial',
114
+ action='store_true',
115
+ help='Use partial matches (at least one Pfam domain matches)'
116
+ )
117
 
118
  args = parser.parse_args()
119
 
120
+ # Update output path if partial and using default
121
+ if args.partial and args.output == Path('results/fdr_thresholds.csv'):
122
+ args.output = Path('results/fdr_thresholds_partial.csv')
123
+
124
  # Standard alpha levels that users commonly need
125
  alpha_levels = [0.001, 0.005, 0.01, 0.02, 0.05, 0.1, 0.15, 0.2]
126
 
127
+ match_type = "partial" if args.partial else "exact"
128
+ print(f"Computing FDR thresholds ({match_type} matches)")
129
  print(f"Loading calibration data from {args.calibration}...")
130
  cal_data = np.load(args.calibration, allow_pickle=True)
131
  print(f" Loaded {len(cal_data)} calibration samples")
 
134
  print(f" Trials per alpha: {args.n_trials}")
135
  print(f" Calibration samples per trial: {args.n_calib}")
136
  print(f" Random seed: {args.seed}")
137
+ print(f" Match type: {match_type}")
138
  print()
139
 
140
  results = []
 
149
  alpha=alpha,
150
  n_trials=args.n_trials,
151
  n_calib=args.n_calib,
152
+ seed=trial_seed,
153
+ partial=args.partial
154
  )
155
 
156
  results.append({
 
184
  print(f"\nSaved to {args.output}")
185
 
186
  # Also save a simple version for easy lookup
187
+ suffix = '_partial' if args.partial else ''
188
+ simple_output = args.output.parent / f'fdr_thresholds{suffix}_simple.csv'
189
  df[['alpha', 'threshold_mean']].rename(
190
  columns={'threshold_mean': 'lambda_threshold'}
191
  ).to_csv(simple_output, index=False)
scripts/compute_fnr_table.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ Compute FNR thresholds at standard alpha levels for the lookup table.
4
+
5
+ This script computes False Negative Rate (FNR) controlling thresholds using
6
+ conformal risk control. FNR thresholds ensure that the fraction of true
7
+ positives missed is controlled at level alpha.
8
+
9
+ The thresholds are computed by:
10
+ 1. Sampling calibration data multiple times (n_trials)
11
+ 2. Computing the FNR threshold for each trial
12
+ 3. Averaging across trials to get a stable estimate
13
+
14
+ Note on reproducibility:
15
+ - Due to random sampling of calibration data, results may vary slightly between runs
16
+ - The standard deviation across trials indicates the expected variability
17
+ - For exact reproduction, use the same random seed
18
+
19
+ Usage:
20
+ python scripts/compute_fnr_table.py --calibration data/pfam_new_proteins.npy
21
+ python scripts/compute_fnr_table.py --calibration data/pfam_new_proteins.npy --partial
22
+ """
23
+
24
+ import argparse
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ import numpy as np
29
+ import pandas as pd
30
+
31
+ # Add parent directory to path
32
+ sys.path.insert(0, str(Path(__file__).parent.parent))
33
+
34
+ from protein_conformal.util import get_thresh_new, get_sims_labels
35
+
36
+
37
+ def compute_fnr_threshold(cal_data, alpha: float, n_trials: int = 100,
38
+ n_calib: int = 1000, seed: int = None,
39
+ partial: bool = False) -> dict:
40
+ """
41
+ Compute FNR threshold at a given alpha level.
42
+
43
+ Parameters:
44
+ cal_data: Calibration data array
45
+ alpha: Target FNR level (e.g., 0.1 means at most 10% false negatives)
46
+ n_trials: Number of trials for averaging
47
+ n_calib: Number of calibration samples per trial
48
+ seed: Random seed for reproducibility
49
+ partial: If True, use partial matches (at least one Pfam domain matches)
50
+
51
+ Returns dict with:
52
+ - mean_threshold: Average threshold across trials
53
+ - std_threshold: Standard deviation across trials
54
+ """
55
+ if seed is not None:
56
+ np.random.seed(seed)
57
+
58
+ thresholds = []
59
+
60
+ for trial in range(n_trials):
61
+ # Shuffle and sample calibration data
62
+ np.random.shuffle(cal_data)
63
+ trial_data = cal_data[:n_calib]
64
+
65
+ # Get similarity scores and labels
66
+ X_cal, y_cal = get_sims_labels(trial_data, partial=partial)
67
+
68
+ # Compute FNR threshold
69
+ l_hat = get_thresh_new(X_cal, y_cal, alpha)
70
+
71
+ thresholds.append(l_hat)
72
+
73
+ return {
74
+ 'mean_threshold': np.mean(thresholds),
75
+ 'std_threshold': np.std(thresholds),
76
+ 'min_threshold': np.min(thresholds),
77
+ 'max_threshold': np.max(thresholds),
78
+ }
79
+
80
+
81
+ def main():
82
+ parser = argparse.ArgumentParser(
83
+ description='Compute FNR thresholds at standard alpha levels'
84
+ )
85
+ parser.add_argument(
86
+ '--calibration', '-c',
87
+ type=Path,
88
+ required=True,
89
+ help='Path to calibration data (.npy file)'
90
+ )
91
+ parser.add_argument(
92
+ '--output', '-o',
93
+ type=Path,
94
+ default=None,
95
+ help='Output CSV file (default: results/fnr_thresholds.csv or results/fnr_thresholds_partial.csv)'
96
+ )
97
+ parser.add_argument(
98
+ '--n-trials',
99
+ type=int,
100
+ default=100,
101
+ help='Number of calibration trials (default: 100)'
102
+ )
103
+ parser.add_argument(
104
+ '--n-calib',
105
+ type=int,
106
+ default=1000,
107
+ help='Number of calibration samples per trial (default: 1000)'
108
+ )
109
+ parser.add_argument(
110
+ '--seed',
111
+ type=int,
112
+ default=42,
113
+ help='Random seed for reproducibility (default: 42)'
114
+ )
115
+ parser.add_argument(
116
+ '--partial',
117
+ action='store_true',
118
+ help='Use partial matches (at least one Pfam domain matches)'
119
+ )
120
+
121
+ args = parser.parse_args()
122
+
123
+ # Set default output path based on partial flag
124
+ if args.output is None:
125
+ suffix = '_partial' if args.partial else ''
126
+ args.output = Path(f'results/fnr_thresholds{suffix}.csv')
127
+
128
+ # Standard alpha levels that users commonly need
129
+ alpha_levels = [0.001, 0.005, 0.01, 0.02, 0.05, 0.1, 0.15, 0.2]
130
+
131
+ match_type = "partial" if args.partial else "exact"
132
+ print(f"Computing FNR thresholds ({match_type} matches)")
133
+ print(f"Loading calibration data from {args.calibration}...")
134
+ cal_data = np.load(args.calibration, allow_pickle=True)
135
+ print(f" Loaded {len(cal_data)} calibration samples")
136
+
137
+ print(f"\nComputing thresholds at {len(alpha_levels)} alpha levels...")
138
+ print(f" Trials per alpha: {args.n_trials}")
139
+ print(f" Calibration samples per trial: {args.n_calib}")
140
+ print(f" Random seed: {args.seed}")
141
+ print(f" Match type: {match_type}")
142
+ print()
143
+
144
+ results = []
145
+ for alpha in alpha_levels:
146
+ print(f" α = {alpha:.3f}...", end=" ", flush=True)
147
+
148
+ # Use different seed offset for each alpha to ensure independence
149
+ trial_seed = args.seed + int(alpha * 10000)
150
+
151
+ stats = compute_fnr_threshold(
152
+ cal_data.copy(), # Copy to avoid mutation
153
+ alpha=alpha,
154
+ n_trials=args.n_trials,
155
+ n_calib=args.n_calib,
156
+ seed=trial_seed,
157
+ partial=args.partial
158
+ )
159
+
160
+ results.append({
161
+ 'alpha': alpha,
162
+ 'threshold_mean': stats['mean_threshold'],
163
+ 'threshold_std': stats['std_threshold'],
164
+ 'threshold_min': stats['min_threshold'],
165
+ 'threshold_max': stats['max_threshold'],
166
+ 'match_type': match_type,
167
+ })
168
+
169
+ print(f"λ = {stats['mean_threshold']:.10f} ± {stats['std_threshold']:.2e}")
170
+
171
+ # Create DataFrame and save
172
+ df = pd.DataFrame(results)
173
+
174
+ # Add human-readable notes
175
+ print(f"\n{'='*70}")
176
+ print(f"FNR Threshold Lookup Table ({match_type} matches)")
177
+ print(f"{'='*70}")
178
+ print(f"{'Alpha':<8} {'Threshold (λ)':<20} {'Std Dev':<12}")
179
+ print("-" * 70)
180
+ for _, row in df.iterrows():
181
+ print(f"{row['alpha']:<8.3f} {row['threshold_mean']:<20.12f} {row['threshold_std']:<12.2e}")
182
+ print(f"{'='*70}")
183
+
184
+ # Save to CSV
185
+ args.output.parent.mkdir(parents=True, exist_ok=True)
186
+ df.to_csv(args.output, index=False)
187
+ print(f"\nSaved to {args.output}")
188
+
189
+ # Also save a simple version for easy lookup
190
+ simple_output = args.output.parent / f'fnr_thresholds{"_partial" if args.partial else ""}_simple.csv'
191
+ df[['alpha', 'threshold_mean']].rename(
192
+ columns={'threshold_mean': 'lambda_threshold'}
193
+ ).to_csv(simple_output, index=False)
194
+ print(f"Simple lookup table saved to {simple_output}")
195
+
196
+ return df
197
+
198
+
199
+ if __name__ == '__main__':
200
+ main()
scripts/slurm_build_apptainer.sh CHANGED
@@ -32,7 +32,8 @@ echo ""
32
 
33
  # Build the container
34
  # The %setup section in apptainer.def creates mount points before container init
35
- apptainer build --fakeroot cpr.sif apptainer.def
 
36
 
37
  BUILD_STATUS=$?
38
 
 
32
 
33
  # Build the container
34
  # The %setup section in apptainer.def creates mount points before container init
35
+ # Use --userns instead of --fakeroot to avoid glibc version mismatch
36
+ apptainer build --userns cpr.sif apptainer.def
37
 
38
  BUILD_STATUS=$?
39
 
scripts/slurm_compute_fnr_thresholds.sh ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ #SBATCH --job-name=fnr-thresholds
3
+ #SBATCH --partition=standard
4
+ #SBATCH --nodes=1
5
+ #SBATCH --ntasks=1
6
+ #SBATCH --cpus-per-task=4
7
+ #SBATCH --mem=32G
8
+ #SBATCH --time=04:00:00
9
+ #SBATCH --output=/groups/doudna/projects/ronb/conformal-protein-retrieval/logs/fnr_thresholds_%j.log
10
+ #SBATCH --error=/groups/doudna/projects/ronb/conformal-protein-retrieval/logs/fnr_thresholds_%j.err
11
+
12
+ # Compute FNR thresholds at standard alpha levels for the lookup table
13
+
14
+ set -e
15
+
16
+ # Setup environment
17
+ export HOME2=/groups/doudna/projects/ronb
18
+ eval "$(/shared/software/miniconda3/latest/bin/conda shell.bash hook)"
19
+ conda activate conformal-s
20
+
21
+ cd /groups/doudna/projects/ronb/conformal-protein-retrieval
22
+
23
+ echo "============================================"
24
+ echo "Computing FNR Thresholds at Standard Alpha Levels"
25
+ echo "============================================"
26
+ echo "Start time: $(date)"
27
+ echo "Node: $(hostname)"
28
+ echo ""
29
+
30
+ # Compute exact match FNR thresholds
31
+ echo "=== Computing EXACT match FNR thresholds ==="
32
+ python scripts/compute_fnr_table.py \
33
+ --calibration data/pfam_new_proteins.npy \
34
+ --output results/fnr_thresholds.csv \
35
+ --n-trials 100 \
36
+ --n-calib 1000 \
37
+ --seed 42
38
+
39
+ echo ""
40
+ echo "=== Computing PARTIAL match FNR thresholds ==="
41
+ python scripts/compute_fnr_table.py \
42
+ --calibration data/pfam_new_proteins.npy \
43
+ --output results/fnr_thresholds_partial.csv \
44
+ --n-trials 100 \
45
+ --n-calib 1000 \
46
+ --seed 42 \
47
+ --partial
48
+
49
+ echo ""
50
+ echo "============================================"
51
+ echo "Completed: $(date)"
52
+ echo "============================================"