Skip to content

Commit eee54da

Browse files
committed
Updated Gradio app with adaptive splitting strategy
1 parent 9b93b43 commit eee54da

1 file changed

Lines changed: 96 additions & 23 deletions

File tree

scripts/protac_splitter_app.py

Lines changed: 96 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
2. Batch processing via CSV file upload
1212
1313
Users choose a splitting strategy (see `model` in `protac_splitter.split_protac`):
14-
XGBoost, heuristic, Transformer, or a combination of these.
14+
XGBoost, heuristic, Transformer, a combination of these, or the QC-gated
15+
"adaptive" strategy that escalates through several of them.
1516
1617
Author: Stefano Ribes
1718
Date: 2025-06
@@ -71,6 +72,7 @@
7172
("XGBoost + Heuristic (best of both)", "xgboost+heuristic"),
7273
("Transformer", "transformer"),
7374
("Transformer → XGBoost", "transformer->xgboost"),
75+
("Adaptive (QC-gated escalation, best quality)", "adaptive"),
7476
]
7577
DEFAULT_MODEL = "heuristic->xgboost"
7678

@@ -98,6 +100,8 @@ def process_single_smiles(
98100
betweenness_threshold: float = 0.4,
99101
use_capacity_weight: bool = False,
100102
betweenness_approx_frac: float = None,
103+
adaptive_use_xgboost: bool = True,
104+
adaptive_use_transformer: bool = False,
101105
) -> tuple:
102106
"""
103107
Process a single SMILES string and generate PROTAC fragment predictions
@@ -111,9 +115,14 @@ def process_single_smiles(
111115
use_capacity_weight: Weight graph edges by bond capacity, heuristic only
112116
betweenness_approx_frac: Fraction of nodes sampled for approximate betweenness
113117
centrality, heuristic only. Leave empty for exact computation.
118+
adaptive_use_xgboost: Whether the XGBoost stage runs on molecules the
119+
heuristic grid left flagged, only used when `model == "adaptive"`
120+
adaptive_use_transformer: Whether the Transformer stage runs on molecules
121+
still flagged after XGBoost, only used when `model == "adaptive"`
114122
115123
Returns:
116-
Tuple containing input image, output images, SMILES texts and status message
124+
Tuple containing input image, output images, SMILES texts, substructure
125+
dataframe, and a status message with the winning model/QC info
117126
"""
118127
if not protac_smiles:
119128
raise gr.Error("Please provide a valid PROTAC SMILES string.", duration=5)
@@ -127,6 +136,8 @@ def process_single_smiles(
127136
betweenness_threshold=betweenness_threshold,
128137
use_capacity_weight=use_capacity_weight,
129138
betweenness_approx_frac=betweenness_approx_frac,
139+
adaptive_use_xgboost=adaptive_use_xgboost,
140+
adaptive_use_transformer=adaptive_use_transformer,
130141
verbose=1,
131142
)
132143
except Exception as e:
@@ -161,7 +172,19 @@ def process_single_smiles(
161172
"SMILES": [splits.get("e3") or "FAILED", splits.get("linker") or "FAILED", splits.get("poi") or "FAILED"],
162173
})
163174

164-
return input_img, images, smiles_texts, smiles_df
175+
# `n_flags` / `review_reasons` / `heuristic_params` are only present when
176+
# model="adaptive" (see evaluation.score_split / count_flags).
177+
info_lines = [f"Model used: {results.get('model_name')}"]
178+
if "n_flags" in results:
179+
reasons = results.get("review_reasons") or "none"
180+
params = results.get("heuristic_params")
181+
info_lines.append(
182+
f"Remaining QC flags: {results['n_flags']} ({reasons})"
183+
+ (f" [winning heuristic params: {params}]" if params else "")
184+
)
185+
info_text = "\n".join(info_lines)
186+
187+
return input_img, images, smiles_texts, smiles_df, info_text
165188

166189
def process_csv(
167190
file: gr.File,
@@ -173,6 +196,8 @@ def process_csv(
173196
betweenness_threshold: float = 0.4,
174197
use_capacity_weight: bool = False,
175198
betweenness_approx_frac: float = None,
199+
adaptive_use_xgboost: bool = True,
200+
adaptive_use_transformer: bool = False,
176201
# NOTE: `pr` is a progress tracker, it is used to track the progress but
177202
# it is not used in this function. Do not remove it.
178203
pr: gr.Progress = gr.Progress(track_tqdm=True),
@@ -190,6 +215,10 @@ def process_csv(
190215
use_capacity_weight: Weight graph edges by bond capacity, heuristic only
191216
betweenness_approx_frac: Fraction of nodes sampled for approximate betweenness
192217
centrality, heuristic only. Leave empty for exact computation.
218+
adaptive_use_xgboost: Whether the XGBoost stage runs on molecules the
219+
heuristic grid left flagged, only used when `model == "adaptive"`
220+
adaptive_use_transformer: Whether the Transformer stage runs on molecules
221+
still flagged after XGBoost, only used when `model == "adaptive"`
193222
194223
Returns:
195224
Path to output CSV file with predictions
@@ -211,6 +240,8 @@ def process_csv(
211240
betweenness_threshold=betweenness_threshold,
212241
use_capacity_weight=use_capacity_weight,
213242
betweenness_approx_frac=betweenness_approx_frac,
243+
adaptive_use_xgboost=adaptive_use_xgboost,
244+
adaptive_use_transformer=adaptive_use_transformer,
214245
verbose=1,
215246
)
216247
except Exception as e:
@@ -275,6 +306,10 @@ def create_interface():
275306
- **XGBoost → Heuristic** / **XGBoost + Heuristic**: alternative combinations of the two graph-based strategies.
276307
- **Transformer** / **Transformer → XGBoost**: often more accurate, but a much slower deep learning model.
277308
{"Disabled on this Hugging Face Space (CPU-only, too slow for interactive use)." if IS_HF_SPACE else "Runs on CPU, so it is slower, especially for large CSV files."}
309+
- **Adaptive**: QC-gated escalation, not just fallback-on-failure — tries a heuristic parameter grid first,
310+
then XGBoost, then (if enabled below) the Transformer, keeping whichever candidate scores best on
311+
automated plausibility checks. Slower than a single strategy, but generally the highest-quality split;
312+
also reports which method/parameters won and any remaining review flags. See **Adaptive Settings** below.
278313
""")
279314
with gr.Row():
280315
with gr.Column(scale=2):
@@ -285,10 +320,10 @@ def create_interface():
285320
)
286321

287322
heuristic_settings_label = gr.Markdown(
288-
"### Heuristic Settings\n\nOnly used when the heuristic algorithm is part of the selected splitting strategy above.",
289-
visible="heuristic" in DEFAULT_MODEL,
323+
"### Heuristic Settings\n\nOnly used when the heuristic algorithm is part of the selected splitting strategy above (including **Adaptive**, whose parameter grid is seeded with these values).",
324+
visible="heuristic" in DEFAULT_MODEL or DEFAULT_MODEL == "adaptive",
290325
)
291-
with gr.Row(visible="heuristic" in DEFAULT_MODEL) as heuristic_settings_row:
326+
with gr.Row(visible="heuristic" in DEFAULT_MODEL or DEFAULT_MODEL == "adaptive") as heuristic_settings_row:
292327
betweenness_threshold = gr.Slider(
293328
label="Betweenness Threshold",
294329
value=0.4,
@@ -311,6 +346,22 @@ def create_interface():
311346
info="Fraction of nodes to sample for approximate betweenness centrality. Leave empty for exact computation.",
312347
)
313348

349+
adaptive_settings_label = gr.Markdown(
350+
"### Adaptive Settings\n\nOnly used when the **Adaptive** splitting strategy is selected above.",
351+
visible=DEFAULT_MODEL == "adaptive",
352+
)
353+
with gr.Row(visible=DEFAULT_MODEL == "adaptive") as adaptive_settings_row:
354+
adaptive_use_xgboost = gr.Checkbox(
355+
label="Use XGBoost stage",
356+
value=True,
357+
info="Run the XGBoost edge classifier on molecules the heuristic grid left flagged.",
358+
)
359+
adaptive_use_transformer = gr.Checkbox(
360+
label="Use Transformer stage",
361+
value=False,
362+
info="Run the Transformer model on molecules still flagged after XGBoost. Requires the [transformer] extra; slow on CPU.",
363+
)
364+
314365
# ----------------------------------------------------------------------
315366
# Performance configuration section
316367
# ----------------------------------------------------------------------
@@ -344,7 +395,7 @@ def create_interface():
344395
maximum=10,
345396
step=1,
346397
info="Width of the beam search for the Transformer model. Higher values may improve accuracy but increase processing time.",
347-
visible="transformer" in DEFAULT_MODEL,
398+
visible="transformer" in DEFAULT_MODEL or DEFAULT_MODEL == "adaptive",
348399
)
349400

350401
# Add a batch size input, only relevant for Transformer-based strategies
@@ -356,19 +407,24 @@ def create_interface():
356407
maximum=64,
357408
step=1,
358409
info="Batch size for processing. Higher values may improve performance, especially on GPU machines, but require more memory.",
359-
visible="transformer" in DEFAULT_MODEL,
410+
visible="transformer" in DEFAULT_MODEL or DEFAULT_MODEL == "adaptive",
360411
)
361412

362-
# Show/hide the Transformer-only and heuristic-only options based on the selected strategy
413+
# Show/hide the Transformer-only, heuristic-only, and adaptive-only options based on the selected strategy
363414
model.change(
364415
lambda m: (
365-
gr.update(visible="transformer" in m),
366-
gr.update(visible="transformer" in m),
367-
gr.update(visible="heuristic" in m),
368-
gr.update(visible="heuristic" in m),
416+
gr.update(visible="transformer" in m or m == "adaptive"),
417+
gr.update(visible="transformer" in m or m == "adaptive"),
418+
gr.update(visible="heuristic" in m or m == "adaptive"),
419+
gr.update(visible="heuristic" in m or m == "adaptive"),
420+
gr.update(visible=m == "adaptive"),
421+
gr.update(visible=m == "adaptive"),
369422
),
370423
inputs=[model],
371-
outputs=[beam_size, batch_size, heuristic_settings_label, heuristic_settings_row],
424+
outputs=[
425+
beam_size, batch_size, heuristic_settings_label, heuristic_settings_row,
426+
adaptive_settings_label, adaptive_settings_row,
427+
],
372428
)
373429

374430
# ----------------------------------------------------------------------
@@ -407,18 +463,29 @@ def create_interface():
407463
lines=1,
408464
show_copy_button=True,
409465
)
466+
smiles_output_info = gr.Textbox(
467+
label="Split Info",
468+
interactive=False,
469+
lines=2,
470+
info="Winning model, and (for the Adaptive strategy) remaining QC flags / winning heuristic params.",
471+
)
410472

411473
# Add this Examples component
412474
gr.Examples(
413475
examples=[
414-
# SMILES, model, beam_size, betweenness_threshold, use_capacity_weight, betweenness_approx_frac
415-
["CC(C)(C)S(=O)(=O)c1cc2c(Nc3ccc4scnc4c3)ccnc2cc1OCCOCCOCCOCCOCC(=O)Nc1cccc2c1CN(C1CCC(=O)NC1=O)C2=O", "heuristic->xgboost", 5, 0.4, False, None],
416-
["Cc1nnc2n1-c1sc(C#Cc3cnn(-c4cccc5c4C(=O)N(C4CCC(=O)NC4=O)C5=O)c3)c(Cc3ccccc3)c1COC2", "heuristic->xgboost", 5, 0.4, False, None],
417-
["c1ccccc1CCC1CCCC1", "heuristic", 5, 0.4, False, None],
418-
["O=C(NCCOCCOCCN1CCCC1)Nc1cccc2c1CN(C1CCC(=O)NC1=O)C2=O", "heuristic", 5, 0.4, False, None],
476+
# SMILES, model, beam_size, betweenness_threshold, use_capacity_weight,
477+
# betweenness_approx_frac, adaptive_use_xgboost, adaptive_use_transformer
478+
["CC(C)(C)S(=O)(=O)c1cc2c(Nc3ccc4scnc4c3)ccnc2cc1OCCOCCOCCOCCOCC(=O)Nc1cccc2c1CN(C1CCC(=O)NC1=O)C2=O", "heuristic->xgboost", 5, 0.4, False, None, True, False],
479+
["Cc1nnc2n1-c1sc(C#Cc3cnn(-c4cccc5c4C(=O)N(C4CCC(=O)NC4=O)C5=O)c3)c(Cc3ccccc3)c1COC2", "heuristic->xgboost", 5, 0.4, False, None, True, False],
480+
["c1ccccc1CCC1CCCC1", "heuristic", 5, 0.4, False, None, True, False],
481+
["O=C(NCCOCCOCCN1CCCC1)Nc1cccc2c1CN(C1CCC(=O)NC1=O)C2=O", "heuristic", 5, 0.4, False, None, True, False],
482+
["CC(C)(C)S(=O)(=O)c1cc2c(Nc3ccc4scnc4c3)ccnc2cc1OCCOCCOCCOCCOCC(=O)Nc1cccc2c1CN(C1CCC(=O)NC1=O)C2=O", "adaptive", 5, 0.4, False, None, True, False],
419483
],
420-
inputs=[smiles_input, model, beam_size, betweenness_threshold, use_capacity_weight, betweenness_approx_frac],
421-
outputs=[smiles_input_image, smiles_output_images, smiles_output_texts, smiles_output_df],
484+
inputs=[
485+
smiles_input, model, beam_size, betweenness_threshold, use_capacity_weight,
486+
betweenness_approx_frac, adaptive_use_xgboost, adaptive_use_transformer,
487+
],
488+
outputs=[smiles_input_image, smiles_output_images, smiles_output_texts, smiles_output_df, smiles_output_info],
422489
fn=process_single_smiles,
423490
cache_examples=True,
424491
)
@@ -428,8 +495,11 @@ def create_interface():
428495
# cheap, so several can run at once without starving each other.
429496
submit_smiles.click(
430497
process_single_smiles,
431-
inputs=[smiles_input, model, beam_size, betweenness_threshold, use_capacity_weight, betweenness_approx_frac],
432-
outputs=[smiles_input_image, smiles_output_images, smiles_output_texts, smiles_output_df],
498+
inputs=[
499+
smiles_input, model, beam_size, betweenness_threshold, use_capacity_weight,
500+
betweenness_approx_frac, adaptive_use_xgboost, adaptive_use_transformer,
501+
],
502+
outputs=[smiles_input_image, smiles_output_images, smiles_output_texts, smiles_output_df, smiles_output_info],
433503
concurrency_limit=4,
434504
)
435505

@@ -458,6 +528,7 @@ def create_interface():
458528
inputs=[
459529
file_input, smiles_column, model, beam_size, batch_size, num_proc,
460530
betweenness_threshold, use_capacity_weight, betweenness_approx_frac,
531+
adaptive_use_xgboost, adaptive_use_transformer,
461532
],
462533
outputs=[download_output],
463534
concurrency_limit=1,
@@ -468,6 +539,8 @@ def create_interface():
468539
- `smiles_column`: The original PROTAC SMILES string
469540
- `default_pred_n0`: The predicted SMILES strings for the splits
470541
- `model_name`: The model used for the prediction
542+
- With the **Adaptive** strategy, three extra columns: `heuristic_params` (which grid point won,
543+
when `model_name == "Heuristic"`, else empty), `n_flags`, and `review_reasons`
471544
""")
472545

473546
# ----------------------------------------------------------------------

0 commit comments

Comments
 (0)