@@ -135,10 +135,38 @@ def inject_js(html, name, code):
135135 final_html = inject_js (final_html , "structures" , structures_js )
136136 final_html = inject_js (final_html , "modal" , modal_js )
137137
138- # Dynamically name the output file based on the mode
139- mode = export_data .get ("mode" )
140- file_name = "Dashboard_Pairwise.html" if mode == "pairwise" else "Dashboard_Multiple.html"
141-
138+ actual_mode = export_data .get ("actual_mode" , export_data .get ("mode" ))
139+ if actual_mode == "screening" :
140+ final_html = final_html .replace (
141+ "Pairwise View Mode" ,
142+ "Screening Mode (1 vs All)"
143+ )
144+ final_html = final_html .replace (
145+ "Global Pair Analysis" ,
146+ "Global Screening Analysis"
147+ )
148+
149+ patch_script = """
150+ <script>
151+ window.addEventListener('DOMContentLoaded', () => {
152+ if (typeof masterData !== 'undefined' && masterData.actual_mode === 'screening') {
153+ const observer = new MutationObserver(() => {
154+ const metaPanel = document.getElementById('metadata-panel');
155+ if (metaPanel && metaPanel.innerHTML.includes('pairwise')) {
156+ metaPanel.innerHTML = metaPanel.innerHTML.replace(/pairwise/g, 'screening');
157+ }
158+ });
159+ observer.observe(document.body, { childList: true, subtree: true });
160+ }
161+ });
162+ </script>
163+ """
164+ final_html = final_html .replace ("</body>" , f"{ patch_script } \n </body>" )
165+ file_name = "Dashboard_Screening.html"
166+ else :
167+ mode = export_data .get ("mode" )
168+ file_name = "Dashboard_Pairwise.html" if mode == "pairwise" else "Dashboard_Multiple.html"
169+
142170 full_path = output_dir / file_name
143171 with open (str (full_path ), "w+" , encoding = "utf-8" ) as out :
144172 out .write (final_html )
@@ -224,10 +252,8 @@ def run_multiple_mode(graphs, base_output, run_name, config, log):
224252 if G and G .associated_graphs is not None :
225253 global_proteins = [clean_graph_name (g ) for g in graphs ]
226254
227- # G.get_dashboard_data correctly formats nodes, edges, components & filtered_graphs
228255 master_export = G .get_dashboard_data (global_proteins )
229256
230- # Append the top-level parameters required by the JS frontend
231257 master_export ["mode" ] = "multiple"
232258 master_export ["run_name" ] = run_name
233259 master_export ["metadata" ] = config
@@ -303,15 +329,92 @@ def run_pairwise_mode(graphs, base_output, run_name, config, log):
303329
304330 create_master_dashboard (master_export , pair_base_dir , log )
305331
332+
333+ def run_screening_mode (ref_graph , target_graphs , base_output , run_name , config , log ):
334+ """
335+ Execute the association workflow in screening mode (1-vs-All).
336+
337+ This mode compares a single reference graph against a collection of target
338+ graphs. Each target is processed individually against the reference, and
339+ the results are aggregated into a single interactive dashboard. To leverage
340+ existing frontend logic, the dashboard payload mimics the "pairwise" mode
341+ structure but includes an `actual_mode` flag to trigger specific UI text
342+ replacements during HTML generation.
343+
344+ Parameters
345+ ----------
346+ ref_graph : tuple
347+ A tuple containing the reference graph data produced by the preprocessing
348+ stage. Typically structured as `(networkx.Graph, file_path, base_name)`.
349+ target_graphs : list of tuple
350+ A list of graph tuples to be compared against the reference graph.
351+ base_output : pathlib.Path
352+ The root directory where the screening results and the final HTML
353+ dashboard will be saved.
354+ run_name : str
355+ A unique base identifier for the current execution run.
356+ config : dict[str, Any]
357+ The association configuration dictionary controlling the graph
358+ association algorithm's parameters and thresholds.
359+ log : logging.Logger
360+ Logger instance used to record runtime progress, warnings, and errors.
361+
362+ Returns
363+ -------
364+ None
365+ """
366+ if not target_graphs :
367+ log .error ("Screening mode requires at least 1 target graph alongside the reference." )
368+ return
369+
370+ screening_base_dir = base_output / "SCREENING"
371+ ref_name = clean_graph_name (ref_graph )
372+
373+ # Reconstruct the global graph list to pass to get_dashboard_data
374+ all_graphs = [ref_graph ] + target_graphs
375+ global_proteins = [clean_graph_name (g ) for g in all_graphs ]
376+
377+ master_export = {
378+ "mode" : "pairwise" ,
379+ "actual_mode" : "screening" ,
380+ "reference_structure" : ref_name ,
381+ "run_name" : run_name ,
382+ "metadata" : config ,
383+ "proteins" : global_proteins ,
384+ "protein_paths" : [str (Path (g [1 ]).resolve ()) for g in all_graphs ],
385+ "pairs" : {}
386+ }
387+
388+
389+ for target_graph in target_graphs :
390+ target_name = clean_graph_name (target_graph )
391+
392+ pair_folder = f"{ ref_name } _vs_{ target_name } "
393+ pair_key = f"{ ref_name } _vs_{ target_name } "
394+ pair_run_name = f"{ run_name } _{ ref_name } _{ target_name } "
395+
396+ G = run_association_task (
397+ graphs = [ref_graph , target_graph ],
398+ output_path = screening_base_dir / pair_folder ,
399+ run_name = pair_run_name ,
400+ association_config = config ,
401+ log = log ,
402+ )
403+ if G and G .associated_graphs is not None :
404+ master_export ["pairs" ][pair_key ] = G .get_dashboard_data (global_proteins )
405+
406+ create_master_dashboard (master_export , screening_base_dir , log )
407+
408+
306409def run (args ):
307410 manifest = load_manifest (args .manifest )
308411 settings = manifest ["settings" ]
309412
310413 run_name = settings ["run_name" ]
311- run_mode = settings .get ("run_mode" , "multiple" )
414+ run_mode = settings .get ("run_mode" )
312415
313- if run_mode not in {"multiple" , "pairwise" }:
314- raise ValueError ("run_mode must be 'multiple' or 'pairwise '" )
416+ if run_mode not in {"multiple" , "pairwise" , "screening" }:
417+ raise ValueError ("run_mode must be 'multiple', 'pairwise' or 'screening '" )
315418
316419 base_output = Path (settings ["output_path" ])
317420 output_dir = base_output / run_name
@@ -329,23 +432,38 @@ def run(args):
329432
330433 if run_mode == "multiple" :
331434 run_multiple_mode (graphs , base_output , run_name , association_config , log )
332- else :
435+ elif run_mode == "pairwise" :
333436 run_pairwise_mode (graphs , base_output , run_name , association_config , log )
437+ elif run_mode == "screening" :
438+ ref_name = settings .get ("reference_structure" )
439+ if not ref_name :
440+ raise ValueError ("Screening mode requires 'reference_structure' to be defined in the manifest settings." )
441+
442+ ref_graph = next ((g for g in graphs if clean_graph_name (g ) == ref_name ), None )
443+
444+ if not ref_graph :
445+ raise ValueError (f"Reference structure '{ ref_name } ' not found among the input graphs." )
446+
447+ target_graphs = [g for g in graphs if clean_graph_name (g ) != ref_name ]
448+
449+ run_screening_mode (ref_graph , target_graphs , base_output , run_name , association_config , log )
334450
335451 if tracker_residues :
336452 out_path = tracker_residues .dump_json ()
337453 log .info (f"Residue tracking report saved to: { out_path } " )
338454
339455 if args .dashboard :
340456 log .info ("Opening dashboard in the default web browser..." )
457+ dash_path = None
341458 if run_mode == "multiple" :
342459 dash_path = base_output / "MULTIPLE" / "Dashboard_Multiple.html"
343- if dash_path .exists ():
344- webbrowser .open (f"file://{ dash_path .resolve ()} " )
345- else :
460+ elif run_mode == "pairwise" :
346461 dash_path = base_output / "PAIRWISE" / "Dashboard_Pairs.html"
347- if dash_path .exists ():
348- webbrowser .open (f"file://{ dash_path .resolve ()} " )
462+ elif run_mode == "screening" :
463+ dash_path = base_output / "SCREENING" / "Dashboard_Pairwise.html"
464+
465+ if dash_path .exists ():
466+ webbrowser .open (f"file://{ dash_path .resolve ()} " )
349467
350468
351469def renumber (args ):
0 commit comments