11112. Batch processing via CSV file upload
1212
1313Users 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
1617Author: Stefano Ribes
1718Date: 2025-06
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]
7577DEFAULT_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
166189def 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 \n Only used when the heuristic algorithm is part of the selected splitting strategy above." ,
289- visible = "heuristic" in DEFAULT_MODEL ,
323+ "### Heuristic Settings\n \n Only 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 \n Only 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