From aea2f26e0ec71cfd4213b75964107ba9e4d004c0 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Thu, 24 Jul 2025 16:29:20 +0200 Subject: [PATCH 1/3] Create pyscript to run powerfit browser without server computations --- app/index.html | 506 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 app/index.html diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..194ce04 --- /dev/null +++ b/app/index.html @@ -0,0 +1,506 @@ + + + + + + PowerFit Web Interface + + + + + + + + +
+

PowerFit Web Interface

+

Upload your target density map and template structure to perform molecular fitting.

+

Note: This interface requires the PowerFit wheel file to be present in the same directory. + Download it from the releases page if not available.

+ +
+ +
+ +
+ + +
+
Upload your target density map in MRC or CCP4 format
+
+ +
+ +
+ + +
+
Upload your atomic model in PDB or mmCIF format
+
+ + +
+ + +
Resolution of the target density map in angstroms
+
+ +
+ + +
Rotational sampling density. Decreasing by factor 2 increases rotations ~8x
+
+ + +
+ + +
Comma-separated list of chain IDs to fit. Leave empty for whole structure
+
+ + +
+
+ + +
+
Apply Laplace pre-filter to density data
+
+ +
+
+ + +
+
Use core-weighted scoring function
+
+ + +
+
+ + +
+
Skip density map resampling step
+
+ +
+ + +
Resampling rate compared to Nyquist frequency
+
+ + +
+
+ + +
+
Skip density map trimming step
+
+ +
+ + +
Intensity cutoff for trimming. Leave empty for 10% of maximum
+
+ + +
+ + +
Number of best-fitting models to output
+
+ +
+ + +
Number of CPU cores to use for computation
+
+ + + +
+ +
+ +
+

Results

+

PowerFit analysis completed successfully!

+ + 📄 Download Solutions File (solutions.out) + +
+
+ + + + + { + "packages": [ + "numpy", + "scipy", + "tqdm", + "rich", + "pygments", + "http://localhost:8000/powerfit_em-3.0.5-cp312-cp312-pyodide_2024_0_wasm32.whl" + ] + } + + + + import js + import asyncio + from functools import partial + from pathlib import Path + from pyodide.ffi import create_proxy + from pyscript import document, fetch, window + import io + from tqdm.auto import tqdm + + progress = lambda x: x + + def show_status(message, status_type="info"): + """Display status message to user""" + status_div = js.document.getElementById("status") + status_div.innerHTML = message + status_div.className = f"status {status_type}" + status_div.style.display = "block" + + def hide_status(): + """Hide status message""" + status_div = js.document.getElementById("status") + status_div.style.display = "none" + + def show_download_section(): + """Show download section with results""" + download_section = js.document.getElementById("download-section") + download_section.style.display = "block" + + def create_download_link(content, filename): + """Create downloadable file link""" + # Create blob with file content + blob = js.Blob.new([content], {"type": "text/plain"}) + url = js.URL.createObjectURL(blob) + + # Update download link + download_link = js.document.getElementById("download-link") + download_link.href = url + download_link.download = filename + + async def run_powerfit(event): + """Main function to run PowerFit analysis""" + # No need for preventDefault since we're handling click, not form submit + + try: + # Disable submit button + submit_btn = js.document.getElementById("submit-btn") + submit_btn.disabled = True + submit_btn.innerHTML = "Running PowerFit..." + + show_status("Reading input files...", "info") + + # Get file inputs + target_file_input = js.document.getElementById("target-file") + template_file_input = js.document.getElementById("template-file") + + if not target_file_input.files.length or not template_file_input.files.length: + show_status("Please select both target and template files.", "error") + return + + # Read file contents + target_file = target_file_input.files.item(0) + template_file = template_file_input.files.item(0) + + + + target_url = window.URL.createObjectURL(target_file) + target_fn = f'./{target_file.name}' + with open(target_fn, 'wb') as d: + d.write(await fetch(target_url).bytearray()) + window.URL.revokeObjectURL(target_url) + + template_url = window.URL.createObjectURL(template_file) + template_fn = f'./{template_file.name}' + with open(template_fn, 'wb') as d: + d.write(await fetch(template_url).bytearray()) + window.URL.revokeObjectURL(template_url) + + # Get form parameters + resolution = float(js.document.getElementById("resolution").value) + angle = float(js.document.getElementById("angle").value) + chain = js.document.getElementById("chain").value or None + laplace = js.document.getElementById("laplace").checked + core_weighted = js.document.getElementById("core-weighted").checked + no_resampling = js.document.getElementById("no-resampling").checked + resampling_rate = float(js.document.getElementById("resampling-rate").value) + no_trimming = js.document.getElementById("no-trimming").checked + trimming_cutoff_val = js.document.getElementById("trimming-cutoff").value + trimming_cutoff = float(trimming_cutoff_val) if trimming_cutoff_val else None + num_models = int(js.document.getElementById("num-models").value) + nproc = int(js.document.getElementById("nproc").value) + + show_status("Running PowerFit analysis... This may take several minutes.", "info") + + # Import PowerFit after installation + from powerfit_em.powerfit import powerfit + + # Run PowerFit analysis + with open(template_fn, 'r') as template_io, open(target_fn, 'rb') as target_io: + print("Starting PowerFit...") # Debug info + powerfit( + target_volume=target_io, + resolution=resolution, + template_structure=template_io, + angle=angle, + laplace=laplace, + core_weighted=core_weighted, + no_resampling=no_resampling, + resampling_rate=resampling_rate, + no_trimming=no_trimming, + trimming_cutoff=trimming_cutoff, + chain=chain, + directory=".", + num=num_models, + gpu=None, # GPU not supported in PyScript + nproc=1, # Single-threaded in PyScript + delimiter=',', + progress=progress + ) + print("PowerFit analysis completed successfully.") # Debug info + + # Read the solutions.out file + solutions_path = Path("solutions.out") + if solutions_path.exists(): + solutions_content = solutions_path.read_text() + create_download_link(solutions_content, "solutions.out") + show_download_section() + show_status("PowerFit analysis completed successfully!", "success") + else: + show_status("Analysis completed but solutions file not found.", "error") + + except Exception as e: + show_status(f"Error running PowerFit: {str(e)}", "error") + print(f"PowerFit error: {e}") # Debug info + + finally: + # Re-enable submit button + submit_btn.disabled = False + submit_btn.innerHTML = "Run PowerFit" + + def clear_form(event): + """Clear all form fields""" + form = js.document.getElementById("powerfit-form") + form.reset() + hide_status() + download_section = js.document.getElementById("download-section") + download_section.style.display = "none" + + def update_file_labels(): + """Update file input labels with selected filenames""" + def update_target_label(event): + file_input = event.target + label = file_input.nextElementSibling + if file_input.files.length > 0: + label.innerHTML = f"Selected: {file_input.files.item(0).name}" + label.style.color = "#007bff" + else: + label.innerHTML = "Click to select target density map file" + label.style.color = "" + + def update_template_label(event): + file_input = event.target + label = file_input.nextElementSibling + if file_input.files.length > 0: + label.innerHTML = f"Selected: {file_input.files.item(0).name}" + label.style.color = "#007bff" + else: + label.innerHTML = "Click to select template structure file" + label.style.color = "" + + # Add event listeners for file inputs + target_input = js.document.getElementById("target-file") + template_input = js.document.getElementById("template-file") + + target_input.addEventListener("change", create_proxy(update_target_label)) + template_input.addEventListener("change", create_proxy(update_template_label)) + + # Set up event listeners when DOM is ready + def setup_event_listeners(): + submit_btn = js.document.getElementById("submit-btn") + clear_btn = js.document.getElementById("clear-btn") + + submit_btn.addEventListener("click", create_proxy(run_powerfit)) + clear_btn.addEventListener("click", create_proxy(clear_form)) + + update_file_labels() + + # Initialize when page loads + setup_event_listeners() + + # Show initial status + show_status("Page loaded. Upload files to get started.", "info") + + + \ No newline at end of file From f664a9f4f4d06c9516f7665ef852cf1d4fbb1020 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Thu, 24 Jul 2025 16:43:13 +0200 Subject: [PATCH 2/3] Use relative url --- app/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/index.html b/app/index.html index 194ce04..36186ce 100644 --- a/app/index.html +++ b/app/index.html @@ -297,7 +297,7 @@

Results

"tqdm", "rich", "pygments", - "http://localhost:8000/powerfit_em-3.0.5-cp312-cp312-pyodide_2024_0_wasm32.whl" + "./powerfit_em-3.0.5-cp312-cp312-pyodide_2024_0_wasm32.whl" ] } From 78ed8837588485d88c134675b314e6ebe00c506c Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Wed, 6 Aug 2025 10:17:18 +0200 Subject: [PATCH 3/3] Report run time in console --- app/index.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/index.html b/app/index.html index 36186ce..4d3737a 100644 --- a/app/index.html +++ b/app/index.html @@ -407,6 +407,7 @@

Results

from powerfit_em.powerfit import powerfit # Run PowerFit analysis + start_time = js.Date.now() with open(template_fn, 'r') as template_io, open(target_fn, 'rb') as target_io: print("Starting PowerFit...") # Debug info powerfit( @@ -429,7 +430,8 @@

Results

progress=progress ) print("PowerFit analysis completed successfully.") # Debug info - + end_time = js.Date.now() + print(f"PowerFit completed in {end_time - start_time} ms") # Debug info # Read the solutions.out file solutions_path = Path("solutions.out") if solutions_path.exists():