Skip to content

Commit 58913ef

Browse files
committed
Added screening mode
1 parent dc4f0ec commit 58913ef

4 files changed

Lines changed: 179 additions & 40 deletions

File tree

MHCXGraph/app.py

Lines changed: 133 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
306409
def 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

351469
def renumber(args):

MHCXGraph/assets/dashboard/js/init_functions.js

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -52,27 +52,6 @@ function init() {
5252
}
5353
}
5454

55-
function initMetadataGlobalFallback() {
56-
try {
57-
if (masterData.mode !== 'pairwise') return;
58-
const firstPair = Object.keys(masterData.pairs)[0];
59-
const c = masterData.pairs[firstPair].metadata || {};
60-
document.getElementById('metadata-panel').innerHTML = `
61-
<h3>Execution Metadata</h3>
62-
<div style="margin-bottom: 10px;"><b>Run Name:</b> ${masterData.run_name || 'N/A'}</div>
63-
<div style="margin-bottom: 12px; font-size: 12px; font-style: italic;">Select a specific pair focus or view colors in the grid to see node mappings.</div>
64-
<div><b>Parameters:</b>
65-
<ul style="margin: 5px 0 0 0; padding-left: 20px; line-height: 1.5;">
66-
<li><b>Mode:</b> ${c.run_mode}</li>
67-
<li><b>Granularity:</b> ${c.node_granularity || 'N/A'}</li>
68-
<li><b>Edge Thresh:</b> ${c.edge_threshold || 'N/A'} Å</li>
69-
<li><b>Global Diff:</b> ${c.global_distance_diff_threshold || 'N/A'} Å</li>
70-
</ul>
71-
</div>
72-
`;
73-
} catch(e) { logError("initMetadataGlobalFallback failed", e); }
74-
}
75-
7655
function initAdvancedOptions() {
7756
document.getElementById('optPalette').addEventListener('change', function(e) {
7857
activePaletteName = e.target.value;
@@ -136,6 +115,34 @@ function initSplitter() {
136115
document.addEventListener('mouseup', function(e) { if (isDragging) { isDragging = false; document.body.style.cursor = 'default'; } });
137116
}
138117

118+
119+
function initMetadataGlobalFallback() {
120+
try {
121+
if (masterData.mode !== 'pairwise') return;
122+
const c = masterData.metadata || {};
123+
124+
// Highlight the reference structure if in screening mode
125+
const refHtml = (masterData.actual_mode === 'screening' && masterData.reference_structure)
126+
? `<div style="margin-bottom: 10px; padding: 6px 10px; background: rgba(37, 99, 235, 0.1); border-left: 3px solid var(--btn-bg); border-radius: 4px; color: var(--text-main);"><b>Reference Structure:</b> ${masterData.reference_structure}</div>`
127+
: '';
128+
129+
document.getElementById('metadata-panel').innerHTML = `
130+
<h3>Execution Metadata</h3>
131+
<div style="margin-bottom: 10px;"><b>Run Name:</b> ${masterData.run_name || 'N/A'}</div>
132+
${refHtml}
133+
<div style="margin-bottom: 12px; font-size: 12px; font-style: italic;">Select a specific pair focus or view colors in the grid to see node mappings.</div>
134+
<div><b>Parameters:</b>
135+
<ul style="margin: 5px 0 0 0; padding-left: 20px; line-height: 1.5;">
136+
<li><b>Mode:</b> ${c.run_mode || 'N/A'}</li>
137+
<li><b>Granularity:</b> ${c.node_granularity || 'N/A'}</li>
138+
<li><b>Edge Thresh:</b> ${c.edge_threshold || 'N/A'} Å</li>
139+
<li><b>Global Diff:</b> ${c.global_distance_diff_threshold || 'N/A'} Å</li>
140+
</ul>
141+
</div>
142+
`;
143+
} catch(e) { logError("initMetadataGlobalFallback failed", e); }
144+
}
145+
139146
function initMetadata() {
140147
try {
141148
if (!graphData) return;
@@ -145,18 +152,29 @@ function initMetadata() {
145152
let p_html = graphData.proteins.map((p, localIdx) => {
146153
let globalIdx = masterData.proteins.indexOf(p);
147154
const color = pal[globalIdx % pal.length];
148-
return `<div style="margin-bottom: 3px;"><span class="color-dot" style="background-color: ${color};"></span><b>Prot ${localIdx}:</b> ${p}</div>`;
155+
156+
// Tag the reference structure in the tuple list
157+
const isRef = (masterData.actual_mode === 'screening' && p === masterData.reference_structure);
158+
const refIcon = isRef ? ' <span style="color: var(--text-muted); font-size: 11px;"><i>(Ref)</i></span>' : '';
159+
160+
return `<div style="margin-bottom: 3px;"><span class="color-dot" style="background-color: ${color};"></span><b>Prot ${localIdx}:</b> ${p}${refIcon}</div>`;
149161
}).join('');
150162

163+
// Highlight the reference structure at the top
164+
const refHtml = (masterData.actual_mode === 'screening' && masterData.reference_structure)
165+
? `<div style="margin-bottom: 10px; padding: 6px 10px; background: rgba(37, 99, 235, 0.1); border-left: 3px solid var(--btn-bg); border-radius: 4px; color: var(--text-main);"><b>Reference Structure:</b> ${masterData.reference_structure}</div>`
166+
: '';
167+
151168
document.getElementById('metadata-panel').innerHTML = `
152169
<h3>Execution Metadata</h3>
153170
<div style="margin-bottom: 10px;"><b>Run Name:</b> ${graphData.run_name || masterData.run_name || 'N/A'}</div>
171+
${refHtml}
154172
<div style="margin-bottom: 12px;"><b>Protein Order (Tuple Layout):</b><div style="margin-top: 5px;">${p_html}</div></div>
155173
<div>
156174
<b>Parameters:</b>
157175
<ul style="margin: 5px 0 0 0; padding-left: 20px; line-height: 1.5;">
158-
<li><b>Mode:</b> ${c.run_mode}</li>
159-
<li><b>Granularity:</b> ${c.node_granularity}</li>
176+
<li><b>Mode:</b> ${c.run_mode || 'N/A'}</li>
177+
<li><b>Granularity:</b> ${c.node_granularity || 'N/A'}</li>
160178
<li><b>Edge Thresh:</b> ${c.edge_threshold || 'N/A'} Å</li>
161179
<li><b>Global Diff:</b> ${c.global_distance_diff_threshold || 'N/A'} Å</li>
162180
</ul>

MHCXGraph/workflow/manifest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def load_manifest(manifest_path: str) -> dict[str, Any]:
1919

2020
settings.setdefault("run_name", "test")
2121
settings.setdefault("run_mode", "multiple")
22+
settings.setdefault("reference_structure", None)
2223
settings.setdefault("output_path", "./outputs")
2324

2425
os.makedirs(settings["output_path"], exist_ok=True)
@@ -93,6 +94,7 @@ def build_association_config(settings: dict[str, Any], run_mode: str, tracker_re
9394
"debug_tracking": settings.get("debug_tracking"),
9495
"verbose": settings.get("verbose"),
9596
"show_std_edges": settings.get("show_std_edges"),
96-
"max_gap_helix": settings.get("max_gap_helix")
97+
"max_gap_helix": settings.get("max_gap_helix"),
98+
"reference_structure": settings.setdefault("reference_structure", None)
9799
}
98100

examples/minimal/manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"settings": {
33
"run_name": "minimal",
44
"run_mode": "pairwise",
5+
"reference_structure": "bst2",
56
"max_chunks": 5,
67
"output_path": "results/minimal",
78
"debug_logs": false,

0 commit comments

Comments
 (0)