Spaces:
Running
Running
| import streamlit as st | |
| import os | |
| import tempfile | |
| import numpy as np | |
| import py3Dmol | |
| import streamlit.components.v1 as components | |
| from io import StringIO | |
| import time | |
| from pyscf import gto, dft, scf | |
| from pyscf.dft import gen_grid | |
| import re | |
| import base64 | |
| import contextlib | |
| import sys | |
| import io as _io | |
| from ase import Atoms | |
| from ase.io import read as ase_read | |
| import pandas as pd | |
| # Set page configuration | |
| st.set_page_config( | |
| page_title='PyFock GUI - Interactive DFT Calculations', | |
| layout='wide', | |
| page_icon="⚛️", | |
| menu_items={ | |
| 'About': "PyFock GUI - A web interface for PyFock, a pure Python DFT code with Numba JIT acceleration" | |
| } | |
| ) | |
| # Sidebar with enhanced styling | |
| st.sidebar.image("https://raw.githubusercontent.com/manassharma07/PyFock/main/logo_crysx_pyfock.png", use_container_width=True) | |
| st.sidebar.markdown("---") | |
| # About PyFock section | |
| st.sidebar.markdown("### About PyFock") | |
| st.sidebar.markdown(""" | |
| **Pure Python DFT** with performance matching C++ codes! | |
| **Key Advantages:** | |
| - 100% Pure Python (including molecular integrals) | |
| - Numba JIT acceleration | |
| - GPU support (CUDA via CuPy) | |
| - Near-quadratic scaling (~O(N²·⁰⁵)) | |
| - Accuracy matching PySCF (<10⁻⁷ Ha) | |
| - Windows/Linux/MacOS compatible | |
| - Easy pip installation | |
| """) | |
| st.sidebar.markdown("---") | |
| # Features | |
| st.sidebar.markdown("### GUI Features") | |
| st.sidebar.markdown(""" | |
| * Run DFT in your browser | |
| * Visualize HOMO, LUMO, density | |
| * Compare with PySCF | |
| * Download cube files & scripts | |
| * Interactive 3D visualization | |
| * No installation required! | |
| """) | |
| st.sidebar.markdown("---") | |
| # Links section | |
| st.sidebar.markdown("### 🔗 Links & Resources") | |
| st.sidebar.markdown(""" | |
| [](https://github.com/manassharma07/PyFock) | |
| [](https://pypi.org/project/pyfock/) | |
| [](https://github.com/manassharma07/PyFock/blob/main/Documentation.md) | |
| 📄 **Paper:** [arXiv preprint](https://arxiv.org) *(coming soon)* | |
| 👨💻 **Developer:** [Manas Sharma](https://www.linkedin.com/in/manassharma07) | |
| ⭐ **Star the repo** if you find it useful! | |
| """) | |
| st.sidebar.markdown("---") | |
| # Installation | |
| with st.sidebar.expander("📦 Installation Instructions"): | |
| st.code(""" | |
| # Install PyFock | |
| pip install pyfock | |
| # For GPU support | |
| pip install cupy-cuda12x # or appropriate version | |
| # Install dependencies | |
| pip install pyscf numba numpy scipy | |
| """, language="bash") | |
| st.sidebar.markdown("---") | |
| # Performance highlights | |
| with st.sidebar.expander("⚡ Performance Highlights"): | |
| st.markdown(""" | |
| **CPU Performance:** | |
| - Upto 2x faster than PySCF | |
| - Strong scaling up to 32 cores | |
| - ~O(N²·⁰⁵) scaling with basis functions | |
| **GPU Acceleration:** | |
| - Up to **14× speedup** vs 4-core CPU | |
| - Single A100 GPU handles 4000+ basis functions | |
| - Consumer GPUs (RTX series) supported | |
| """) | |
| st.sidebar.markdown("---") | |
| st.sidebar.markdown("*Made with PyFock by PhysWhiz*") | |
| st.sidebar.markdown("*Pure Python • Numba JIT • GPU Ready*") | |
| def strip_ansi(text): | |
| ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') | |
| return ansi_escape.sub('', text) | |
| # Helper: create an ASE Atoms object or fallback | |
| def _parse_xyz_to_atoms(xyz_text): | |
| # ASE can read from string via io | |
| return ase_read(StringIO(xyz_text), format='xyz') | |
| # get_structure_viz2 implementation (based on sample provided) | |
| def get_structure_viz2(atoms_obj, style='stick', width=400, height=400): | |
| xyz_str = "" | |
| xyz_str += f"{len(atoms_obj)}\n" | |
| xyz_str += "Structure\n" | |
| for atom in atoms_obj: | |
| # atom may be ASE Atoms or fallback MiniAtom | |
| sym = atom.symbol if hasattr(atom, 'symbol') else atom.get_chemical_symbols()[0] | |
| pos = atom.position if hasattr(atom, 'position') else atom.position | |
| xyz_str += f"{sym} {pos[0]:.6f} {pos[1]:.6f} {pos[2]:.6f}\n" | |
| view = py3Dmol.view(width=width, height=height) | |
| view.addModel(xyz_str, "xyz") | |
| if style.lower() == 'ball-stick': | |
| view.setStyle({'stick': {'radius': 0.2}, 'sphere': {'scale': 0.3}}) | |
| elif style.lower() == 'stick': | |
| view.setStyle({'stick': {}}) | |
| elif style.lower() == 'ball': | |
| view.setStyle({'sphere': {'scale': 0.4}}) | |
| else: | |
| view.setStyle({'stick': {'radius': 0.15}}) | |
| try: | |
| pbc_any = atoms_obj.pbc.any() | |
| except Exception: | |
| pbc_any = False | |
| view.zoomTo() | |
| view.setBackgroundColor('white') | |
| return view | |
| # === Cube visualization function === | |
| def visualize_cube_in_component(cube_content, title, iso_val, opac, width=420, height=360): | |
| # return the HTML for embedding so it can be used inside columns | |
| view = py3Dmol.view(width=width, height=height) | |
| view.addModel(cube_content, 'cube') | |
| view.setStyle({'sphere': {'colorscheme': 'Jmol', 'scale': 0.3}, | |
| 'stick': {'colorscheme': 'Jmol', 'radius': 0.2}}) | |
| # For orbitals, show both lobes; for density, show only positive + negative appropriately | |
| if 'Density' not in title: | |
| view.addVolumetricData(cube_content, 'cube', | |
| {'isoval': -abs(iso_val), 'color': 'blue', 'opacity': opac}) | |
| view.addVolumetricData(cube_content, 'cube', | |
| {'isoval': abs(iso_val), 'color': 'red', 'opacity': opac}) | |
| view.zoomTo() | |
| view.setClickable({'clickable': 'true'}) | |
| view.enableContextMenu({'contextMenuEnabled': 'true'}) | |
| view.show() | |
| view.render() | |
| # don't spin automatically; allow user to toggle via JS later if desired | |
| t = view.js() | |
| html_content = f"{t.startjs}{t.endjs}" | |
| return html_content | |
| # Example XYZ files | |
| EXAMPLE_MOLECULES = { | |
| "Water": """3 | |
| Water molecule | |
| O 0.000000 0.000000 0.117790 | |
| H 0.000000 0.755453 -0.471161 | |
| H 0.000000 -0.755453 -0.471161""", | |
| "Acetone": """10 | |
| Acetone molecule | |
| O 0.000247197289657 -1.311344859924947 0.000033372829371 | |
| C 0.000008761532627 -0.103796835732344 0.000232428229233 | |
| C 1.285011287026515 0.689481114475586 -0.000005118102586 | |
| C -1.285310305026895 0.688972899031773 -0.000008308924344 | |
| H 1.326033303131908 1.335179621002258 -0.879574382138206 | |
| H 1.324106820690737 1.339904097018082 0.876116213728557 | |
| H 2.136706543431990 0.014597477886500 0.002551051783708 | |
| H -2.136748467815155 0.013761611436540 0.002550873429523 | |
| H -1.326572603227431 1.334639056170679 -0.879590243114624 | |
| H -1.324682534264540 1.339405821389821 0.876094109485741""", | |
| "Acetonitrile": """6 | |
| Acetonitrile molecule | |
| N 1.238504335855378 -0.000002648443155 -0.000006432209062 | |
| C -1.367114484335133 0.000020833177302 0.000019691866100 | |
| C 0.091462639284387 0.000004939432085 -0.000014578846105 | |
| H -1.737603541725548 -0.830506551932429 0.597694569897051 | |
| H -1.737658959168382 -0.102297452169442 -1.018057029309221 | |
| H -1.737589980824850 0.932880879510178 0.420463777423243""", | |
| "Methane": """5 | |
| Methane molecule | |
| C 0.000000 0.000000 0.000000 | |
| H 0.629118 0.629118 0.629118 | |
| H -0.629118 -0.629118 0.629118 | |
| H -0.629118 0.629118 -0.629118 | |
| H 0.629118 -0.629118 -0.629118""", | |
| "Benzene": """12 | |
| Benzene molecule | |
| C 1.395890 0.000000 0.000000 | |
| C 0.697945 1.209021 0.000000 | |
| C -0.697945 1.209021 0.000000 | |
| C -1.395890 0.000000 0.000000 | |
| C -0.697945 -1.209021 0.000000 | |
| C 0.697945 -1.209021 0.000000 | |
| H 2.482610 0.000000 0.000000 | |
| H 1.241305 2.149540 0.000000 | |
| H -1.241305 2.149540 0.000000 | |
| H -2.482610 0.000000 0.000000 | |
| H -1.241305 -2.149540 0.000000 | |
| H 1.241305 -2.149540 0.000000""", | |
| "Ammonia": """4 | |
| Ammonia molecule | |
| N 0.000000 0.000000 0.100000 | |
| H 0.945000 0.000000 -0.266000 | |
| H -0.472500 0.818000 -0.266000 | |
| H -0.472500 -0.818000 -0.266000""", | |
| "Carbon Dioxide": """3 | |
| Carbon dioxide molecule | |
| C 0.000000 0.000000 0.000000 | |
| O 0.000000 0.000000 1.160000 | |
| O 0.000000 0.000000 -1.160000""", | |
| "Hydrogen Peroxide": """4 | |
| Hydrogen peroxide molecule | |
| O 0.000000 0.000000 0.000000 | |
| O 1.450000 0.000000 0.000000 | |
| H 0.000000 0.930000 0.000000 | |
| H 1.450000 -0.930000 0.000000""", | |
| "Formaldehyde": """4 | |
| Formaldehyde molecule | |
| C 0.000000 0.000000 0.000000 | |
| O 1.200000 0.000000 0.000000 | |
| H -0.550000 0.940000 0.000000 | |
| H -0.550000 -0.940000 0.000000""", | |
| "Hydrogen Cyanide": """3 | |
| Hydrogen cyanide molecule | |
| H 0.000000 0.000000 0.000000 | |
| C 1.065000 0.000000 0.000000 | |
| N 2.232000 0.000000 0.000000""", | |
| "Acetylene": """4 | |
| Acetylene molecule | |
| H 0.000000 0.000000 0.000000 | |
| C 0.601000 0.000000 0.000000 | |
| C 1.764000 0.000000 0.000000 | |
| H 2.365000 0.000000 0.000000""", | |
| "Ethylene": """6 | |
| Ethylene molecule | |
| C 0.000000 0.000000 0.000000 | |
| C 1.339000 0.000000 0.000000 | |
| H -0.540000 0.930000 0.000000 | |
| H -0.540000 -0.930000 0.000000 | |
| H 1.879000 0.930000 0.000000 | |
| H 1.879000 -0.930000 0.000000""", | |
| "Ethane": """8 | |
| Ethane molecule | |
| C 0.000000 0.000000 0.000000 | |
| C 1.540000 0.000000 0.000000 | |
| H -0.540000 0.930000 0.000000 | |
| H -0.540000 -0.930000 0.000000 | |
| H 0.000000 0.000000 1.090000 | |
| H 2.080000 0.930000 0.000000 | |
| H 2.080000 -0.930000 0.000000 | |
| H 1.540000 0.000000 -1.090000""", | |
| "Formic Acid": """5 | |
| Formic acid molecule | |
| C 0.000000 0.000000 0.000000 | |
| O 1.200000 0.000000 0.000000 | |
| O -0.600000 1.100000 0.000000 | |
| H 1.700000 0.900000 0.000000 | |
| H -0.600000 -0.900000 0.000000""", | |
| "Hydrogen Sulfide": """3 | |
| Hydrogen sulfide molecule | |
| S 0.000000 0.000000 0.000000 | |
| H 0.960000 0.000000 0.000000 | |
| H -0.480000 0.830000 0.000000""", | |
| "Tetrahydrofuran": """13 | |
| Tetrahydrofuran molecule | |
| O 1.216699382773870 -0.000516422279060 -0.000000629115220 | |
| C -1.016993686921708 -0.728737145702576 -0.227053769599122 | |
| C -1.016379073694511 0.729554513122363 0.227039749920194 | |
| C 0.395888395971846 -1.160306071902647 0.143905791563541 | |
| C 0.396849106350064 1.159943366322044 -0.143953936744947 | |
| H -1.782338327214497 -1.336082444384147 0.254184465508221 | |
| H -1.159922021048819 -0.787714532025205 -1.307880287340100 | |
| H -1.781241112969484 1.337526054547069 -0.254174631443963 | |
| H -1.159223957468540 0.788641683930875 1.307872257115590 | |
| H 0.441717113009142 -1.507461993885669 1.181993316376597 | |
| H 0.789985254249466 -1.947807118924544 -0.499943162908684 | |
| H 0.442962196586526 1.507022288075474 -1.182056728365057 | |
| H 0.791596740442480 1.947137848999537 0.499867568609654""", | |
| "Pyrrole": """10 | |
| Pyrrole molecule | |
| N 0.003181105319591 -1.154989666506124 0.000060405177869 | |
| C -1.117737448486888 -0.370847266235924 -0.000011684111207 | |
| C 1.119766547077291 -0.364686948222411 0.000018496752116 | |
| C -0.713366217362578 0.937235325591941 -0.000096441964893 | |
| C 0.708206650120975 0.941149612298515 -0.000053474487459 | |
| H 0.005944376523274 -2.157707636293536 -0.000153625688957 | |
| H -2.102541509509273 -0.805490708525106 0.000110657494436 | |
| H 2.106949265704444 -0.793899550618045 0.000097097601888 | |
| H -1.362506178052089 1.796728992178547 -0.000156325233850 | |
| H 1.352603398906602 1.804207848609518 -0.000015105544776""", | |
| "Dimethyl Ether": """9 | |
| Dimethyl ether molecule | |
| O 0.000004977280923 0.530020309529034 -0.000005108317779 | |
| C 1.164212882530410 -0.260593898872961 0.000001269096639 | |
| C -1.164190191714442 -0.260612616856000 -0.000020896710708 | |
| H 1.210503469986643 -0.900685141083738 0.889522583088480 | |
| H 2.020207692369726 0.410753932008613 0.000001864695406 | |
| H 1.210505099946668 -0.900685096089216 -0.889519959770612 | |
| H -1.210516092616227 -0.900663538561855 0.889526642712922 | |
| H -1.210427870830911 -0.900750324054947 -0.889509722206098 | |
| H -2.020199970977986 0.410716373006091 -0.000096671211569""", | |
| } | |
| # XC functional mapping to libxc codes | |
| XC_FUNCTIONALS = { | |
| "LDA (SVWN5)": (1, 7), # Slater exchange + VWN5 correlation | |
| "PBE": (101, 130), # PBE exchange + PBE correlation | |
| "BLYP": (106, 131), # Becke88 exchange + LYP correlation | |
| "BP86": (106, 132), # Becke88 exchange + P86 correlation | |
| } | |
| BASIS_SETS = ["sto-3g", "sto-6g", "3-21G", "4-31G", "6-31G", "6-31+G", "6-31++G", "cc-pvDZ", "def2-SVP", "def2-TZVP"] | |
| # Main title | |
| st.title("⚛️ PyFock GUI - Interactive DFT Calculations") | |
| st.markdown("---") | |
| # Input section | |
| st.header("1. DFT Setup") | |
| col1, col2 = st.columns([1.3, 1]) | |
| with col1: | |
| st.subheader("Molecule Input") | |
| # Molecule selection | |
| molecule_choice = st.selectbox( | |
| "Select example molecule or paste custom XYZ:", | |
| [ | |
| "Water", # correct | |
| "Acetone", # correct | |
| "Tetrahydrofuran", # correct | |
| "Pyrrole", # correct | |
| "Dimethyl ether", # correct | |
| # "Acetonitrile", | |
| # "Methane", | |
| "Benzene", # correct | |
| # "Ammonia", | |
| "Carbon Dioxide", # correct | |
| "Hydrogen Peroxide", | |
| # "Formaldehyde", | |
| # "Hydrogen Cyanide", | |
| # "Acetylene", | |
| # "Ethylene", | |
| # "Ethane", | |
| "Formic Acid", | |
| "Hydrogen Sulfide", # correct | |
| "Custom" | |
| ] | |
| ) | |
| if molecule_choice == "Custom": | |
| xyz_content = st.text_area( | |
| "Paste XYZ coordinates:", | |
| height=200, | |
| placeholder="3\nWater molecule\nO 0.0 0.0 0.0\nH 0.757 0.586 0.0\nH -0.757 0.586 0.0" | |
| ) | |
| else: | |
| xyz_content = st.text_area( | |
| "XYZ coordinates:", | |
| value=EXAMPLE_MOLECULES[molecule_choice], | |
| height=200 | |
| ) | |
| # === NEW: Structure visualization right at molecule selection === | |
| # This uses ASE if available; otherwise, show a simple py3Dmol view from the XYZ string. | |
| with col2: | |
| if xyz_content and xyz_content.strip(): | |
| st.markdown("### Molecule Visualization", unsafe_allow_html=True) | |
| viz_style = st.selectbox("Select Visualization Style:", ["ball-stick", "stick", "ball"], key="viz_style_select") | |
| atoms_obj = _parse_xyz_to_atoms(xyz_content) | |
| # Render py3Dmol | |
| view_3d = get_structure_viz2(atoms_obj, style=viz_style, width=400, height=400) | |
| # Use components.html to insert the viewer HTML | |
| try: | |
| st.components.v1.html(view_3d._make_html(), width=420, height=420) | |
| except Exception: | |
| # fallback to js html | |
| t = view_3d.js() | |
| html_content = f"{t.startjs}{t.endjs}" | |
| components.html(html_content, height=420, width=420) | |
| # Structure information | |
| st.markdown("### Structure Information") | |
| atoms_info = { | |
| "Number of Atoms": len(atoms_obj), | |
| "Chemical Formula": atoms_obj.get_chemical_formula() if hasattr(atoms_obj, 'get_chemical_formula') else "".join(atoms_obj.get_chemical_symbols()), | |
| "Atom Types": ", ".join(sorted(list(set(atoms_obj.get_chemical_symbols())))) | |
| } | |
| for key, value in atoms_info.items(): | |
| st.write(f"**{key}:** {value}") | |
| with col1: | |
| st.subheader("Calculation Settings") | |
| basis_set = st.selectbox("Basis Set:", BASIS_SETS, index=0) | |
| auxbasis = st.text_input("Auxiliary Basis:", value="def2-universal-jfit") | |
| xc_functional = st.selectbox( | |
| "XC Functional:", | |
| list(XC_FUNCTIONALS.keys()), | |
| index=0 | |
| ) | |
| max_iterations = st.number_input("Max Iterations:", min_value=1, max_value=16, value=14) | |
| conv_crit = st.number_input("Convergence Criterion:", min_value=1e-7, max_value=1e-3, value=1e-6, format="%.1e") | |
| ncores = 1#st.number_input("Number of Cores:", min_value=1, max_value=8, value=4) | |
| use_pyscf_grids = st.checkbox("Use PySCF Grids", value=True, help="Use either PySCF grids for both PyFock and PySCF DFT calculation or PyFock grids for both PyFock and PySCF DFT calculaiton. Using PySCF grids is recommended as those are more efficient and also makes the comparison with PySCF consistent.") | |
| compare_pyscf = st.checkbox("Compare with PySCF (may take longer)", value=False, help="Runs a KS-DFT calculation using same settings in PySCF for energy comparison.") | |
| st.markdown("---") | |
| # Visualization settings | |
| st.header("2. Cube Generation and Visualization Settings") | |
| col3, col4, col5 = st.columns(3) | |
| with col3: | |
| cube_resolution = st.slider("Cube File Resolution (nx=ny=nz):", 30, 70, 40) | |
| with col4: | |
| isovalue = st.number_input("Isovalue:", 0.0, 1.0, value=0.05, step=0.001, format="%.6f") | |
| with col5: | |
| opacity = st.slider("Opacity:", 0.0, 1.0, value=0.90, step=0.01) | |
| st.markdown("---") | |
| # Run calculation button | |
| if st.button("🚀 Run DFT Calculation", type="primary"): | |
| # Validate XYZ input | |
| if not xyz_content.strip(): | |
| st.error("Please provide XYZ coordinates!") | |
| st.stop() | |
| # Progress tracking | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| try: | |
| # Capture stdout/stderr into buffer and show later in the app | |
| log_buffer = _io.StringIO() | |
| with contextlib.redirect_stdout(log_buffer), contextlib.redirect_stderr(log_buffer): | |
| # Setup environment variables | |
| status_text.text("Setting up environment...") | |
| progress_bar.progress(5) | |
| os.environ['OMP_NUM_THREADS'] = str(ncores) | |
| os.environ["OPENBLAS_NUM_THREADS"] = str(ncores) | |
| os.environ["MKL_NUM_THREADS"] = str(ncores) | |
| os.environ["VECLIB_MAXIMUM_THREADS"] = str(ncores) | |
| os.environ["NUMEXPR_NUM_THREADS"] = str(ncores) | |
| # Import PyFock modules | |
| status_text.text("Importing PyFock modules...") | |
| progress_bar.progress(10) | |
| from pyfock import Basis, Mol, DFT, Utils, Grids | |
| # Create temporary XYZ file | |
| status_text.text("Creating molecule object...") | |
| progress_bar.progress(15) | |
| with tempfile.NamedTemporaryFile(mode='w', suffix='.xyz', delete=False) as f: | |
| f.write(xyz_content) | |
| xyz_file = f.name | |
| # Initialize molecule | |
| mol = Mol(coordfile=xyz_file) | |
| # Initialize basis sets | |
| status_text.text(f"Loading basis set: {basis_set}...") | |
| progress_bar.progress(20) | |
| basis = Basis(mol, {'all': Basis.load(mol=mol, basis_name=basis_set)}) | |
| auxbasis_obj = Basis(mol, {'all': Basis.load(mol=mol, basis_name=auxbasis)}) | |
| # Check actual basis function count | |
| n_basis = basis.bfs_nao | |
| if n_basis > 120: | |
| st.error(f"❌ This system has {n_basis} basis functions, exceeding the limit of 120. Please use a smaller basis set or fewer atoms.") | |
| os.unlink(xyz_file) | |
| st.stop() | |
| st.info(f"✓ System has {n_basis} basis functions (within limit)") | |
| # Get XC functional codes | |
| funcx, funcc = XC_FUNCTIONALS[xc_functional] | |
| funcidcrysx = [funcx, funcc] | |
| funcidpyscf = f"{funcx},{funcc}" | |
| # Generating grids | |
| status_text.text("Generating numerical grids...") | |
| progress_bar.progress(22) | |
| if use_pyscf_grids: | |
| # PySCF grids | |
| molPySCF = gto.Mole() | |
| molPySCF.atom = xyz_file | |
| molPySCF.basis = basis_set | |
| molPySCF.cart = True | |
| molPySCF.verbose = 0 | |
| molPySCF.build() | |
| grids = gen_grid.Grids(molPySCF) | |
| grids.level = 3 # optional: quality of grid (0–9 approx) | |
| grids.prune = None # disable pruning if you want the full Lebedev mesh | |
| grids.build(with_non0tab=True) | |
| else: | |
| # PyFock grids | |
| grids = Grids(mol, basis=basis, level = 3, radial_precision=1.0e-13, ncores=ncores) | |
| # Initialize DFT object | |
| status_text.text("Initializing DFT calculation...") | |
| progress_bar.progress(25) | |
| dftObj = DFT(mol, basis, auxbasis_obj, xc=funcidcrysx, grids=grids, gridsLevel=3) | |
| dftObj.conv_crit = conv_crit | |
| dftObj.max_itr = max_iterations | |
| dftObj.ncores = 1 | |
| dftObj.save_ao_values = True | |
| # Run SCF calculation | |
| status_text.text("Running SCF calculation... This may take a few moments.") | |
| progress_bar.progress(30) | |
| start_time = time.time() | |
| energyPyFock, dmat = dftObj.scf() | |
| pyfock_time = time.time() - start_time | |
| progress_bar.progress(50) | |
| # Display results | |
| st.success(f"✅ PyFock calculation completed in {pyfock_time:.2f} seconds!") | |
| st.header("3. Results") | |
| # Energy and basic properties | |
| col6, col7, col8 = st.columns(3) | |
| with col6: | |
| st.metric("Total Energy (PyFock)", f"{energyPyFock:.8f} Ha") | |
| with st.expander("Energy Components"): | |
| import pandas as pd | |
| energy_df = pd.DataFrame({ | |
| "Component": [ | |
| "Kinetic Energy", | |
| "Nuclear-Electron Attraction", | |
| "Electron-Electron Repulsion", | |
| "Exchange-Correlation", | |
| "Nuclear Repulsion" | |
| ], | |
| "Energy (Ha)": [ | |
| f"{dftObj.Kinetic_energy:.8f}", | |
| f"{dftObj.Nuc_energy:.8f}", | |
| f"{dftObj.J_energy:.8f}", | |
| f"{dftObj.XC_energy:.8f}", | |
| f"{dftObj.Nuclear_repulsion_energy:.8f}" | |
| ] | |
| }) | |
| st.dataframe(energy_df, hide_index=True, use_container_width=True) | |
| with col7: | |
| # Calculate HOMO-LUMO gap | |
| occupied = np.where(dftObj.mo_occupations > 1e-8)[0] | |
| if len(occupied) > 0 and len(occupied) < len(dftObj.mo_energies): | |
| homo_idx = occupied[-1] | |
| lumo_idx = homo_idx + 1 | |
| homo_energy = dftObj.mo_energies[homo_idx] | |
| lumo_energy = dftObj.mo_energies[lumo_idx] | |
| gap = (lumo_energy - homo_energy) * 27.2114 # Convert to eV | |
| st.metric("HOMO-LUMO Gap", f"{gap:.4f} eV") | |
| else: | |
| homo_idx = None | |
| lumo_idx = None | |
| st.metric("HOMO-LUMO Gap", "N/A") | |
| with col8: | |
| # st.metric("SCF Iterations", f"{len(dftObj.energy_list)}") | |
| st.write('TODO: SCF Iterations') | |
| # MO energies | |
| st.subheader("Molecular Orbital Energies") | |
| mo_energies_ev = dftObj.mo_energies * 27.2114 # Convert to eV | |
| col9, col10 = st.columns(2) | |
| with col9: | |
| if homo_idx is not None: | |
| st.write(f"**HOMO (orbital {homo_idx}):** {mo_energies_ev[homo_idx]:.4f} eV") | |
| with col10: | |
| if lumo_idx is not None: | |
| st.write(f"**LUMO (orbital {lumo_idx}):** {mo_energies_ev[lumo_idx]:.4f} eV") | |
| # Show MO energies | |
| with st.expander("View All MO Energies"): | |
| mo_data = { | |
| "Orbital": list(range(len(mo_energies_ev))), | |
| "Energy (eV)": [f"{e:.6f}" for e in mo_energies_ev], | |
| "Occupation": dftObj.mo_occupations | |
| } | |
| st.dataframe(mo_data, height=300) | |
| # Density matrix expander and download === | |
| with st.expander("Density Matrix (dmat) — view / download"): | |
| try: | |
| st.write(dmat) | |
| except Exception as e: | |
| st.write("Failed to show density matrix:", str(e)) | |
| # Generate cube files | |
| status_text.text("Generating cube files for visualization...") | |
| progress_bar.progress(60) | |
| cube_files = {} | |
| if homo_idx is not None: | |
| # HOMO cube | |
| status_text.text("Generating HOMO cube file...") | |
| with tempfile.NamedTemporaryFile(mode='w', suffix='_HOMO.cube', delete=False) as f: | |
| homo_cube_file = f.name | |
| Utils.write_orbital_cube( | |
| mol, basis, dftObj.mo_coefficients[:, homo_idx], | |
| homo_cube_file, nx=cube_resolution, ny=cube_resolution, nz=cube_resolution, | |
| ncores=ncores | |
| ) | |
| with open(homo_cube_file, 'r') as f: | |
| cube_files['HOMO'] = f.read() | |
| progress_bar.progress(70) | |
| if lumo_idx is not None: | |
| # LUMO cube | |
| status_text.text("Generating LUMO cube file...") | |
| with tempfile.NamedTemporaryFile(mode='w', suffix='_LUMO.cube', delete=False) as f: | |
| lumo_cube_file = f.name | |
| Utils.write_orbital_cube( | |
| mol, basis, dftObj.mo_coefficients[:, lumo_idx], | |
| lumo_cube_file, nx=cube_resolution, ny=cube_resolution, nz=cube_resolution, | |
| ncores=ncores | |
| ) | |
| with open(lumo_cube_file, 'r') as f: | |
| cube_files['LUMO'] = f.read() | |
| progress_bar.progress(80) | |
| # Density cube | |
| status_text.text("Generating electron density cube file...") | |
| with tempfile.NamedTemporaryFile(mode='w', suffix='_density.cube', delete=False) as f: | |
| density_cube_file = f.name | |
| Utils.write_density_cube( | |
| mol, basis, dftObj.dmat, | |
| density_cube_file, nx=cube_resolution, ny=cube_resolution, nz=cube_resolution, | |
| ncores=ncores | |
| ) | |
| with open(density_cube_file, 'r') as f: | |
| cube_files['Density'] = f.read() | |
| progress_bar.progress(85) | |
| # Display visualizations side-by-side | |
| st.subheader("4. Visualizations") | |
| # create columns for HOMO, LUMO and Density (as available) | |
| vis_cols = [] | |
| num_vis = len([k for k in cube_files.keys() if cube_files.get(k)]) | |
| if num_vis == 0: | |
| st.info("No cube visualizations available.") | |
| else: | |
| # Arrange into up to three columns side-by-side | |
| if 'HOMO' in cube_files and 'LUMO' in cube_files and 'Density' in cube_files: | |
| c1, c2, c3 = st.columns(3) | |
| vis_cols = [c1, c2, c3] | |
| mapping = [('HOMO', c1), ('LUMO', c2), ('Density', c3)] | |
| else: | |
| # pack present visualizations into equal columns | |
| keys_present = list(cube_files.keys()) | |
| cols = st.columns(len(keys_present)) | |
| vis_cols = cols | |
| mapping = list(zip(keys_present, cols)) | |
| for title, col in mapping: | |
| if title in cube_files: | |
| with col: | |
| st.markdown(f"#### {title}") | |
| html_blob = visualize_cube_in_component(cube_files[title], title, isovalue, opacity) | |
| components.html(html_blob, height=380, width=420) | |
| col14, col15, col16 = st.columns(3) | |
| # Helper to create base64 download links (avoids widget-triggered reruns) | |
| def make_download_link(content, filename, mimetype="text/plain"): | |
| if isinstance(content, str): | |
| b = content.encode() | |
| else: | |
| b = content | |
| b64 = base64.b64encode(b).decode() | |
| return f'<a href="data:{mimetype};base64,{b64}" download="{filename}">📥 Download {filename}</a>' | |
| with col14: | |
| if 'HOMO' in cube_files: | |
| st.markdown(make_download_link(cube_files['HOMO'], "homo.cube"), unsafe_allow_html=True) | |
| with col15: | |
| if 'LUMO' in cube_files: | |
| st.markdown(make_download_link(cube_files['LUMO'], "lumo.cube"), unsafe_allow_html=True) | |
| with col16: | |
| if 'Density' in cube_files: | |
| st.markdown(make_download_link(cube_files['Density'], "density.cube"), unsafe_allow_html=True) | |
| progress_bar.progress(90) | |
| # === Allow visualizing any orbital === | |
| def func_viz_any_mo(): | |
| st.subheader("Visualize any MO") | |
| mo_idx_choice = None | |
| if hasattr(dftObj, 'mo_energies'): | |
| max_orb = len(dftObj.mo_energies) - 1 | |
| # Show a slider/selectbox to pick orbital | |
| mo_idx_choice = st.number_input("Select orbital index to visualize:", min_value=0, max_value=max_orb, value=homo_idx if homo_idx is not None else 0, step=1) | |
| # if st.button("Generate and Show Selected MO", key="gen_orb_btn"): | |
| status_text.text(f"Generating cube for MO index {mo_idx_choice} ...") | |
| with tempfile.NamedTemporaryFile(mode='w', suffix=f'_MO{mo_idx_choice}.cube', delete=False) as f: | |
| mo_cube_file = f.name | |
| Utils.write_orbital_cube( | |
| mol, basis, dftObj.mo_coefficients[:, int(mo_idx_choice)], | |
| mo_cube_file, nx=cube_resolution, ny=cube_resolution, nz=cube_resolution, | |
| ncores=ncores | |
| ) | |
| with open(mo_cube_file, 'r') as f: | |
| cube_files[f"MO_{mo_idx_choice}"] = f.read() | |
| # show it in a small area | |
| html_blob = visualize_cube_in_component(cube_files[f"MO_{mo_idx_choice}"], f"MO {mo_idx_choice}", isovalue, opacity) | |
| st.markdown(f"#### MO {mo_idx_choice}") | |
| components.html(html_blob, height=380, width=420) | |
| func_viz_any_mo() | |
| # PySCF comparison | |
| if compare_pyscf: | |
| status_text.text("Running PySCF calculation for comparison...") | |
| try: | |
| if not use_pyscf_grids: | |
| molPySCF = gto.Mole() | |
| molPySCF.atom = xyz_file | |
| molPySCF.basis = basis_set | |
| molPySCF.cart = True | |
| molPySCF.verbose = 5 | |
| molPySCF.build() | |
| mf = dft.rks.RKS(molPySCF).density_fit(auxbasis=auxbasis) | |
| mf.xc = funcidpyscf | |
| # mf.direct_scf = False | |
| mf.conv_tol = conv_crit | |
| mf.max_cycle = max_iterations | |
| # mf.grids.level = 5 | |
| mf.grids.coords = grids.coords | |
| mf.grids.weights = grids.weights | |
| # Disable PySCF's automatic grid generation | |
| mf.grids.build = lambda *args, **kwargs: None | |
| start_pyscf = time.time() | |
| energyPySCF = mf.kernel(dm0 = mf.init_guess_by_1e(molPySCF)) | |
| pyscf_time = time.time() - start_pyscf | |
| st.subheader("5. Comparison with PySCF") | |
| col11, col12, col13 = st.columns(3) | |
| with col11: | |
| st.metric("PySCF Energy", f"{energyPySCF:.8f} Ha") | |
| with col12: | |
| energy_diff = abs(energyPyFock - energyPySCF) * 1000 # in mHa | |
| st.metric("Energy Difference", f"{energy_diff:.6f} mHa") | |
| with col13: | |
| speedup = pyscf_time / pyfock_time | |
| # st.metric("PyFock Speedup", f"{speedup:.2f}x" if speedup > 1 else f"{1/speedup:.2f}x slower") | |
| # st.write(f"**PyFock time:** {pyfock_time:.2f} s | **PySCF time:** {pyscf_time:.2f} s") | |
| except Exception as e: | |
| st.warning(f"PySCF comparison failed: {str(e)}") | |
| progress_bar.progress(95) | |
| # Downloads section | |
| st.subheader("6. INPUT Script Generation") | |
| # Generate Python script | |
| status_text.text("Generating Python script...") | |
| python_script = """# PyFock DFT Calculation Script | |
| # Generated by PyFock GUI | |
| import os | |
| ncores = {ncores} | |
| os.environ['OMP_NUM_THREADS'] = str(ncores) | |
| os.environ["OPENBLAS_NUM_THREADS"] = str(ncores) | |
| os.environ["MKL_NUM_THREADS"] = str(ncores) | |
| os.environ["VECLIB_MAXIMUM_THREADS"] = str(ncores) | |
| os.environ["NUMEXPR_NUM_THREADS"] = str(ncores) | |
| from pyfock import Basis, Mol, DFT, Utils | |
| import numpy as np | |
| # XYZ coordinates | |
| xyz_content = \"\"\" | |
| {xyz_content} | |
| \"\"\" | |
| # Save XYZ to file | |
| with open('molecule.xyz', 'w') as f: | |
| f.write(xyz_content) | |
| # Calculation parameters | |
| basis_set_name = '{basis_set}' | |
| auxbasis_name = '{auxbasis}' | |
| funcx = {funcx} | |
| funcc = {funcc} | |
| funcidcrysx = [funcx, funcc] | |
| # Initialize molecule and basis | |
| mol = Mol(coordfile='molecule.xyz') | |
| basis = Basis(mol, {{'all': Basis.load(mol=mol, basis_name=basis_set_name)}}) | |
| auxbasis = Basis(mol, {{'all': Basis.load(mol=mol, basis_name=auxbasis_name)}}) | |
| # Setup DFT calculation | |
| dftObj = DFT(mol, basis, auxbasis, xc=funcidcrysx) | |
| dftObj.conv_crit = {conv_crit} | |
| dftObj.max_itr = {max_iterations} | |
| dftObj.ncores = ncores | |
| dftObj.save_ao_values = True | |
| # Run SCF | |
| energy, dmat = dftObj.scf() | |
| print(f"Total Energy: {{energy:.8f}} Ha") | |
| # Find HOMO and LUMO | |
| occupied = np.where(dftObj.mo_occupations > 1e-8)[0] | |
| if len(occupied) > 0 and len(occupied) < len(dftObj.mo_energies): | |
| homo_idx = occupied[-1] | |
| lumo_idx = homo_idx + 1 | |
| # Generate cube files | |
| Utils.write_orbital_cube(mol, basis, dftObj.mo_coefficients[:, homo_idx], | |
| 'HOMO.cube', nx={cube_resolution}, ny={cube_resolution}, | |
| nz={cube_resolution}, ncores=ncores) | |
| Utils.write_orbital_cube(mol, basis, dftObj.mo_coefficients[:, lumo_idx], | |
| 'LUMO.cube', nx={cube_resolution}, ny={cube_resolution}, | |
| nz={cube_resolution}, ncores=ncores) | |
| # Generate density cube | |
| Utils.write_density_cube(mol, basis, dftObj.dmat, 'density.cube', | |
| nx={cube_resolution}, ny={cube_resolution}, | |
| nz={cube_resolution}, ncores=ncores) | |
| print("Cube files generated successfully!") | |
| """ | |
| parameters = { | |
| 'ncores': ncores, | |
| 'xyz_content': xyz_content, | |
| 'basis_set': basis_set, | |
| 'auxbasis': auxbasis, # Often a different basis set for auxiliary functions | |
| 'funcx': funcx, # Example ID for exchange functional (like LDA_X) | |
| 'funcc': funcc, # Example ID for correlation functional (like LDA_C_PZ) | |
| 'conv_crit': conv_crit, | |
| 'max_iterations': max_iterations, | |
| 'cube_resolution': cube_resolution | |
| } | |
| python_script = python_script.format(**parameters) | |
| with st.expander("Input Script to Run the Above PyFock Calculation"): | |
| st.code(python_script) | |
| # Provide script as base64 link (no rerun on click) | |
| st.markdown(make_download_link(python_script, "pyfock_calculation.py", mimetype="text/x-python"), unsafe_allow_html=True) | |
| progress_bar.progress(100) | |
| status_text.text("✅ All tasks completed!") | |
| # === Show captured stdout/stderr logs === | |
| st.subheader("7. Calculation Log Output") | |
| log_buffer.seek(0) | |
| log_text = log_buffer.read() | |
| if log_text.strip(): | |
| st.code(strip_ansi(log_text)) | |
| else: | |
| st.write("No log output was captured.") | |
| # Cleanup | |
| os.unlink(xyz_file) | |
| if 'homo_cube_file' in locals(): | |
| os.unlink(homo_cube_file) | |
| if 'lumo_cube_file' in locals(): | |
| os.unlink(lumo_cube_file) | |
| if 'density_cube_file' in locals(): | |
| os.unlink(density_cube_file) | |
| if 'mo_cube_file' in locals(): | |
| try: | |
| os.unlink(mo_cube_file) | |
| except Exception: | |
| pass | |
| except ImportError as e: | |
| st.error(f"❌ Import Error: {str(e)}") | |
| st.info("Make sure PyFock is installed: `pip install pyfock`") | |
| progress_bar.empty() | |
| status_text.empty() | |
| except Exception as e: | |
| st.error(f"❌ Calculation failed: {str(e)}") | |
| st.info("Please check your input parameters and try again.") | |
| progress_bar.empty() | |
| status_text.empty() | |
| # Cleanup on error | |
| if 'xyz_file' in locals(): | |
| try: | |
| os.unlink(xyz_file) | |
| except: | |
| pass | |
| else: | |
| # Initial instructions | |
| st.info("👆 Configure your calculation parameters above and click 'Run DFT Calculation' to start!") | |
| st.markdown(""" | |
| ### Getting Started | |
| 1. **Choose a molecule**: Select from examples or paste your own XYZ coordinates | |
| 2. **Select calculation parameters**: Basis set, functional, convergence criteria | |
| 3. **Adjust visualization settings**: Cube resolution, isovalue, opacity | |
| 4. **Run calculation**: Click the button above | |
| 5. **Explore results**: View energies, MO properties, and 3D visualizations | |
| 6. **Download**: Get cube files and a Python script to reproduce the calculation | |
| ### Notes | |
| - Calculations are limited to ~120 basis functions for Streamlit Cloud | |
| - Smaller molecules and basis sets will run faster | |
| - PySCF comparison adds computation time but validates results | |
| - All calculations use density fitting for efficiency | |
| ### Example Systems | |
| - **Water**: Quick test system (7 basis functions with sto-3g) | |
| - **Methane**: Small hydrocarbon (9 basis functions with sto-3g) | |
| - **Benzene**: Aromatic system (42 basis functions with sto-3g) | |
| """) | |
| # Footer | |
| st.markdown("---") | |
| st.markdown(""" | |
| <div style='text-align: center'> | |
| <p>PyFock GUI - Pure Python DFT with Numba JIT acceleration</p> | |
| <p>⚡ Fast • 🎯 Accurate • 🐍 Pure Python</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |