Skip to content

Commit 1839c30

Browse files
separate MSA and PAE DUMMY files to avoid clashes and check generically
1 parent 04f07f3 commit 1839c30

6 files changed

Lines changed: 38 additions & 46 deletions

File tree

File renamed without changes.
File renamed without changes.

assets/report_template.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@
249249
</div>
250250
<div>
251251
Average pLDDT:
252-
<span id="lddt-average" class="font-normal"></span>
252+
<span id="plddt-average" class="font-normal"></span>
253253
</div>
254254
</div>
255255
</div>
@@ -396,7 +396,7 @@
396396
Residue confidence - pLDDT
397397
</div>
398398
<div class="p-4 xl:p-6 bg-white shadow-md rounded">
399-
<div id="lddt_placeholder"></div>
399+
<div id="plddt_placeholder"></div>
400400
</div>
401401
</div>
402402

@@ -508,7 +508,7 @@
508508
structFormat: "pdb",
509509
models: [],
510510
models_data: [],
511-
lddt_averages: [],
511+
plddt_averages: [],
512512
};
513513

514514
// Load report configuration from embedded JSON
@@ -524,7 +524,7 @@
524524

525525
const MODELS = config.models;
526526
const MODELS_DATA = config.models_data;
527-
const LDDT_AVERAGES = config.lddt_averages;
527+
const PLDDT_AVERAGES = config.plddt_averages;
528528
const PROGRAM_NAME = config.programName;
529529
const SAMPLE_NAME = config.sampleName;
530530
const STRUCT_FORMAT = config.structFormat;
@@ -560,7 +560,7 @@
560560
// Handle window resizing
561561
window.addEventListener("resize", () => stage.handleResize());
562562

563-
document.getElementById("lddt-average").textContent = LDDT_AVERAGES[0];
563+
document.getElementById("plddt-average").textContent = PLDDT_AVERAGES[0];
564564

565565
loadModel();
566566
loadModelImage();
@@ -581,7 +581,7 @@
581581

582582
const setModel = (ix) => {
583583
state.model = ix;
584-
document.getElementById("lddt-average").textContent = LDDT_AVERAGES[ix];
584+
document.getElementById("plddt-average").textContent = PLDDT_AVERAGES[ix];
585585
stage.removeComponent(state.modelObject);
586586
setLoading(1);
587587

bin/generate_report.py

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
generate_pae_plot,
99
generate_sequence_coverage_plot,
1010
)
11-
from bs4 import BeautifulSoup
1211
import json
1312
import argparse
1413
import os
@@ -66,9 +65,9 @@ def generate_report(name, out_dir, structures, num_structs_limit=5, msa_files=No
6665

6766
print("Structures:", structure_paths)
6867

69-
# Parse HTML template with BeautifulSoup
68+
# Read HTML template
7069
with open(html_template, "r") as f:
71-
soup = BeautifulSoup(f.read(), "html.parser")
70+
html = f.read()
7271

7372
# Build configuration JSON for JavaScript
7473
config = {
@@ -77,23 +76,13 @@ def generate_report(name, out_dir, structures, num_structs_limit=5, msa_files=No
7776
"programName": prog_name_mapping.get(prog, prog),
7877
"structFormat": struct_format,
7978
"models": [f"Rank {idx+1}" for idx, _ in enumerate(parsed_structures)],
80-
"lddt_averages": [round(plddt_from_struct_b_factor(s).mean(), 2) for s in parsed_structures],
79+
"plddt_averages": [round(plddt_from_struct_b_factor(s).mean(), 2) for s in parsed_structures],
8180
"models_data": [open(s, "r").read().replace("\n", "\\n") for s in structure_paths],
8281
}
8382

84-
# Inject configuration as JSON into a script tag
85-
config_script = soup.new_tag("script", type="application/json", attrs={"id": "report-config"})
86-
config_script.string = json.dumps(config)
87-
88-
# Find or create head section and add config script
89-
head = soup.find("head")
90-
if head:
91-
head.append(config_script)
92-
else:
93-
# Fallback: add before first script tag
94-
first_script = soup.find("script")
95-
if first_script:
96-
first_script.insert_before(config_script)
83+
# Inject configuration as a JSON script tag before </head>
84+
config_script = f'<script type="application/json" id="report-config">{json.dumps(config)}</script>'
85+
html = html.replace('</head>', f'{config_script}\n</head>', 1)
9786

9887
# Generate sequence coverage plot from first MSA file
9988
seq_cov_html = None
@@ -105,10 +94,9 @@ def generate_report(name, out_dir, structures, num_structs_limit=5, msa_files=No
10594
config=PLOTLY_CONFIG,
10695
)
10796

108-
# Replace placeholder divs with content using BeautifulSoup
109-
seq_cov_placeholder = soup.find("div", attrs={"id": "seq_cov_placeholder"})
110-
if seq_cov_placeholder and seq_cov_html:
111-
seq_cov_placeholder.replace_with(BeautifulSoup(seq_cov_html, "html.parser"))
97+
# Replace placeholder divs with plot HTML
98+
if seq_cov_html:
99+
html = html.replace('<div id="seq_cov_placeholder"></div>', seq_cov_html, 1)
112100

113101
# Generate the pLDDT plot and convert to HTML
114102
plddt_fig = generate_plddt_plot(parsed_structures)
@@ -117,9 +105,7 @@ def generate_report(name, out_dir, structures, num_structs_limit=5, msa_files=No
117105
include_plotlyjs="cdn",
118106
config=PLOTLY_CONFIG,
119107
)
120-
lddt_placeholder = soup.find("div", attrs={"id": "lddt_placeholder"})
121-
if lddt_placeholder:
122-
lddt_placeholder.replace_with(BeautifulSoup(plddt_html, "html.parser"))
108+
html = html.replace('<div id="plddt_placeholder"></div>', plddt_html, 1)
123109

124110
# Generate PAE plot from first PAE file (TODO: toggle PAE with model selection)
125111
if pae_files:
@@ -129,9 +115,7 @@ def generate_report(name, out_dir, structures, num_structs_limit=5, msa_files=No
129115
include_plotlyjs="cdn",
130116
config=PLOTLY_CONFIG,
131117
)
132-
pae_placeholder = soup.find("div", attrs={"id": "pae_placeholder"})
133-
if pae_placeholder:
134-
pae_placeholder.replace_with(BeautifulSoup(pae_html, "html.parser"))
118+
html = html.replace('<div id="pae_placeholder"></div>', pae_html, 1)
135119

136120
if write_htmls:
137121
with open(f"{out_dir}/{name}_coverage_pLDDT.html", "w") as out_file:
@@ -142,7 +126,7 @@ def generate_report(name, out_dir, structures, num_structs_limit=5, msa_files=No
142126

143127
# Write the final HTML report
144128
with open(f"{out_dir}/{name}_{type}_report.html", "w") as out_file:
145-
out_file.write(str(soup))
129+
out_file.write(html)
146130

147131
def main():
148132
parser = argparse.ArgumentParser(description="Generate protein structure reports.")
@@ -163,10 +147,15 @@ def main():
163147
html_template = args.html_template or get_template_path()
164148

165149
# Both these values could be missing - ESMFold for MSA, many others for PAE
166-
if args.msa and os.path.basename(args.msa[0]) == "NO_FILE":
150+
if args.msa and os.path.basename(args.msa[0]).startswith("DUMMY_"):
167151
args.msa = None
168-
if args.pae and os.path.basename(args.pae[0]) == "NO_FILE":
152+
if args.pae and os.path.basename(args.pae[0]).startswith("DUMMY_"):
169153
args.pae = None
154+
# Catch-all for any future optional metric args, if we have plots for pTM or other missing values. The above two are more common and explicit
155+
for attr in vars(args):
156+
val = getattr(args, attr)
157+
if isinstance(val, list) and val and os.path.basename(val[0]).startswith("DUMMY_"):
158+
setattr(args, attr, None)
170159

171160
generate_report(
172161
name=args.name,

main.nf

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ workflow NFCORE_PROTEINFOLD {
7474
requested_modes = params.mode.toLowerCase().split(",")
7575
requested_modes_size = requested_modes.size()
7676

77-
ch_dummy_file = channel.fromPath("$projectDir/assets/NO_FILE")
78-
ch_dummy_file_pae = channel.fromPath("$projectDir/assets/NO_FILE_PAE")
77+
ch_dummy_msa = channel.fromPath("$projectDir/assets/DUMMY_MSA")
78+
ch_dummy_pae = channel.fromPath("$projectDir/assets/DUMMY_PAE")
7979

8080
//
8181
// WORKFLOW: Run alphafold2
@@ -324,8 +324,8 @@ workflow NFCORE_PROTEINFOLD {
324324
ch_versions = ch_versions.mix(ESMFOLD.out.versions)
325325
ch_report_input = ch_report_input.mix(
326326
ESMFOLD.out.pdb
327-
.combine(ch_dummy_file)
328-
.combine(ch_dummy_file_pae)
327+
.combine(ch_dummy_msa)
328+
.combine(ch_dummy_pae)
329329
)
330330
ch_top_ranked_model = ch_top_ranked_model.mix(ESMFOLD.out.pdb)
331331
}

modules/local/generate_report/generate_report.py

100644100755
Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def generate_report(name, out_dir, structures, num_structs_limit=5, msa_files=No
7676
"programName": prog_name_mapping.get(prog, prog),
7777
"structFormat": struct_format,
7878
"models": [f"Rank {idx+1}" for idx, _ in enumerate(parsed_structures)],
79-
"lddt_averages": [round(plddt_from_struct_b_factor(s).mean(), 2) for s in parsed_structures],
79+
"plddt_averages": [round(plddt_from_struct_b_factor(s).mean(), 2) for s in parsed_structures],
8080
"models_data": [open(s, "r").read().replace("\n", "\\n") for s in structure_paths],
8181
}
8282

@@ -105,7 +105,7 @@ def generate_report(name, out_dir, structures, num_structs_limit=5, msa_files=No
105105
include_plotlyjs="cdn",
106106
config=PLOTLY_CONFIG,
107107
)
108-
html = html.replace('<div id="lddt_placeholder"></div>', plddt_html, 1)
108+
html = html.replace('<div id="plddt_placeholder"></div>', plddt_html, 1)
109109

110110
# Generate PAE plot from first PAE file (TODO: toggle PAE with model selection)
111111
if pae_files:
@@ -147,11 +147,14 @@ def main():
147147
html_template = args.html_template or get_template_path()
148148

149149
# Both these values could be missing - ESMFold for MSA, many others for PAE
150-
if args.msa and os.path.basename(args.msa[0]) == "NO_FILE":
150+
if args.msa and os.path.basename(args.msa[0]).startswith("DUMMY_MSA"):
151151
args.msa = None
152-
if args.pae and os.path.basename(args.pae[0]) == "NO_FILE":
153-
args.pae = None
154-
152+
if args.pae and os.path.basename(args.pae[0]).startswith("DUMMY_PAE"):
153+
args.pae = None # Catch-all for any future optional metric args
154+
for attr in vars(args):
155+
val = getattr(args, attr)
156+
if isinstance(val, list) and val and os.path.basename(val[0]).startswith("DUMMY_"):
157+
setattr(args, attr, None)
155158
generate_report(
156159
name=args.name,
157160
out_dir=args.output_dir,

0 commit comments

Comments
 (0)