Skip to content

Commit 8cf6ebd

Browse files
A common generate_report that leverages plot_utils
1 parent 52d802e commit 8cf6ebd

1 file changed

Lines changed: 179 additions & 0 deletions

File tree

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
from plot_utils import (
2+
reset_residue_numbers,
3+
sort_structures_by_rank,
4+
align_structures,
5+
plddt_from_struct_b_factor,
6+
generate_plddt_plot,
7+
generate_pae_plot,
8+
generate_sequence_coverage_plot,
9+
)
10+
import base64
11+
import argparse
12+
13+
# TODO: Barcelona team to implement AF3, others
14+
prog_name_mapping = {
15+
"proteinfold": "ProteinFold",
16+
"alphafold2": "AlphaFold2",
17+
"esmfold": "ESMFold",
18+
"colabfold": "ColabFold",
19+
"rosettafold-all-atom": "RoseTTAFold-All-Atom",
20+
"helixfold3": "HelixFold3",
21+
"boltz1": "Boltz1",
22+
}
23+
24+
def generate_report(name, out_dir, structures, num_structs_limit=5, msa_files=None, pae_files=None, prog="ProteinFold", type="standard", html_template=None, write_htmls=True, seq_cov_as_html=False):
25+
26+
# Change this to not just be ESMFold. HF3 resets on chainbreaks. Have structure res sequential just in case
27+
for structure in structures:
28+
structure = reset_residue_numbers(structure)
29+
30+
# Sort structures by name and limit to set set number
31+
if len(structures) > num_structs_limit:
32+
print(f"Warning: More than {num_structs_limit} structures provided. Sorting and using only the first {num_structs_limit} structures.")
33+
sorted_structures = sort_structures_by_rank(structures, prog)
34+
structures = sorted_structures[:num_structs_limit]
35+
36+
# Replace structures with aligned versions
37+
if type == "comparison":
38+
aligned_structures = align_structures(structures, save_ref_structure=True)
39+
structures = aligned_structures
40+
41+
# Keeping for parsing visibility purposes
42+
print("Structures:", structures)
43+
44+
#TODO: should really use a proper HTML parser for this, like BeautifulSoup or html5lib. strings prone to failure
45+
#However, most replacements are simple and this is faster
46+
template = open(html_template, "r").read()
47+
template = template.replace("*sample_name*", name)
48+
template = template.replace("*prog_name*", prog_name_mapping[prog])
49+
50+
lddt_averages = []
51+
for structure in structures:
52+
lddt_averages.append(round(plddt_from_struct_b_factor(structure).mean(), 2))
53+
averages_js_array = f"const LDDT_AVERAGES = {lddt_averages};"
54+
template = template.replace("const LDDT_AVERAGES = [];", averages_js_array)
55+
56+
# Populate MODELS into the HTML templat
57+
rank_names = [f"Rank {idx+1}" for idx, _ in enumerate(structures)]
58+
model_names_js = ("const MODELS = [" + ",\n".join([f'"{model}"' for model in rank_names]) + "];")
59+
template = template.replace("const MODELS = [];", model_names_js)
60+
61+
# Populate MODELS_DATA with the content of the PDB files
62+
# TODO: If the .cif string is written as a literal in the report, will it still render? Probably, not be see the logic
63+
pdb_strings = [open(structure, "r").read().replace("\n", "\\n") for structure in structures]
64+
models_data = ",\n".join([f'"{pdb_string}"' for pdb_string in pdb_strings])
65+
models_data_js = f"const MODELS_DATA = [{models_data}];"
66+
template = template.replace("const MODELS_DATA = [];", models_data_js)
67+
68+
# Generate sequence coverage plots and convert to HTML
69+
if msa_files:
70+
for msa_file in msa_files:
71+
seq_cov_fig, seq_cov_img_path = generate_sequence_coverage_plot(msa_file, out_dir, name, save_image=True)
72+
seq_cov_img_encoded = base64.b64encode(open(seq_cov_img_path, "rb").read()).decode("utf-8")
73+
seq_cov_img_tag = f'<img src="data:image/png;base64,{seq_cov_img_encoded}" alt="Sequence Coverage Image">'
74+
75+
seq_cov_html = seq_cov_fig.to_html(
76+
full_html=False,
77+
include_plotlyjs="cdn",
78+
config={"displayModeBar": True, "displaylogo": False, "scrollZoom": True},
79+
)
80+
if seq_cov_as_html == True:
81+
template = template.replace('<div id="seq_cov_placeholder"></div>', seq_cov_html)
82+
else:
83+
template = template.replace('<div id="seq_cov_placeholder"></div>', seq_cov_img_tag)
84+
85+
# Generate the pLDDT plot and convert to HTML
86+
plddt_fig = generate_plddt_plot(structures)
87+
plddt_html = plddt_fig.to_html(
88+
full_html=False,
89+
include_plotlyjs="cdn",
90+
config={"displayModeBar": True, "displaylogo": False, "scrollZoom": True},
91+
)
92+
template = template.replace('<div id="lddt_placeholder"></div>', plddt_html)
93+
94+
#Generate PAE plot and conver to HTML TODO: currently onlt the first
95+
if pae_files:
96+
pae_figs = []
97+
for pae_file in pae_files:
98+
# TODO: ensure PAE files are sorted and limited to num_structs_limit
99+
pae_figs.append(generate_pae_plot(pae_file, out_dir, name, save_image=True))
100+
pae_html = pae_figs[0].to_html(
101+
full_html=False,
102+
include_plotlyjs="cdn",
103+
config={"displayModeBar": True, "displaylogo": False, "scrollZoom": True},
104+
)
105+
template = template.replace('<div id="pae_placeholder"></div>', pae_html)
106+
# TODO: need logic to keep PAEs in sync with structure upon click
107+
# TODO: look at the Sequence coverage approach (e.g. ESMFold has none)
108+
else:
109+
pass
110+
# TODO: Remove the PAE div if no PAE files are provided.
111+
# The below approach will remove the div but needs dynamic resizing in the report
112+
# pae_section_text = """
113+
# <div id="pae-title" class="text-4xl font-bold tracking-tight mb-6">PAE</div>
114+
# <div class="p-6 bg-white shadow-md rounded">
115+
# <div id="pae_container" class="w-[660px] min-h-[600px] flex justify-center items-center mx-auto">
116+
# <div id="pae_placeholder"></div>
117+
# </div>
118+
# </div>
119+
# """
120+
# template = template.replace(pae_section_text.strip(), "")
121+
122+
if write_htmls:
123+
with open(f"{out_dir}/{name}_coverage_pLDDT.html", "w") as out_file:
124+
out_file.write(plddt_html)
125+
with open(f"{out_dir}/{name}_coverage_MSA.html", "w") as out_file:
126+
out_file.write(seq_cov_html)
127+
128+
# Write the final HTML report
129+
with open(f"{out_dir}/{name}_{type}_report.html", "w") as out_file:
130+
out_file.write(template)
131+
132+
def main():
133+
parser = argparse.ArgumentParser(description="Generate protein structure reports.")
134+
parser.add_argument("--name", required=True, help="Name of the report.")
135+
parser.add_argument("--output_dir", required=True, help="Output directory for the report.")
136+
parser.add_argument("--structs", required=True, nargs="+", help="List of structure file paths.")
137+
parser.add_argument("--msa", nargs="+", default=None, help="MSA file path.")
138+
parser.add_argument("--paes", nargs="+", default=None, help="List of PAE file paths (optional).")
139+
parser.add_argument("--prog", default="proteinfold", choices=["alphafold2", "esmfold", "colabfold", "rosettafold-all-atom", "helixfold3", "boltz1"], type=str.lower, help="The program used to generate the structures, can be called in the workflow")
140+
parser.add_argument("--type", default="standard", choices=["standard", "comparison"], help="The type of report file generated .") # TODO: change to --type with options in case there are other reports
141+
#TODO: remove --html_template as this is already determined by the type
142+
parser.add_argument("--html_template", default=None, help="Path to the HTML template for comparison (optional).")
143+
parser.add_argument("--write_htmls", default=True, help="Write out seperate files for each html plot (optional).")
144+
145+
args = parser.parse_args()
146+
147+
print("Generating report.....")
148+
149+
# TODO: want a better way of pathing this
150+
if args.type == "comparison":
151+
html_template = "../.../assets/comparison_template.html"
152+
elif args.type == "standard":
153+
html_template = "../../assets/report_template.html"
154+
else:
155+
html_template = args.html_template
156+
157+
158+
# Both these values could be missing - EMSFold for MSA, many others for PAE
159+
if os.path.basename(args.msa) == "NO_FILE":
160+
args.peas=None
161+
if os.path.basename(args.paes) == "NO_FILE":
162+
args.peas=None
163+
164+
generate_report(
165+
name=args.name,
166+
out_dir=args.output_dir,
167+
structures=args.structs,
168+
num_structs_limit=5,
169+
msa_files=args.msa,
170+
pae_files=args.paes,
171+
prog=args.prog,
172+
type=args.type,
173+
html_template=html_template,
174+
write_htmls=args.write_htmls,
175+
seq_cov_as_html=False,
176+
)
177+
178+
if __name__ == "__main__":
179+
main()

0 commit comments

Comments
 (0)