88 generate_pae_plot ,
99 generate_sequence_coverage_plot ,
1010)
11- from bs4 import BeautifulSoup
1211import json
1312import argparse
1413import 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
147131def 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 ,
0 commit comments