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 | |
| import pyfock | |
| # 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" | |
| } | |
| ) | |
| # === Background video styling === | |
| # CSS for Background Video and Content Styling | |
| def set_css(): | |
| st.markdown(""" | |
| <style> | |
| #myVideo { | |
| position: fixed; | |
| right: 0; | |
| bottom: 0; | |
| min-width: 100%; | |
| min-height: 100%; | |
| opacity: 0.12; /* <--- adjust video opacity here (0 to 1) */ | |
| pointer-events: none; /* FIX: allow scrolling */ | |
| } | |
| .content { | |
| position: fixed; | |
| bottom: 0; | |
| background: rgba(0, 0, 0, 0.5); | |
| color: #f1f1f1; | |
| width: 100%; | |
| padding: 20px; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Embed Background Video | |
| def embed_video(): | |
| video_link = "https://raw.githubusercontent.com/manassharma07/Website_Files_for_PyFock/main/background_video_pyfock.mp4" | |
| st.sidebar.markdown(f""" | |
| <video autoplay muted loop id="myVideo"> | |
| <source src="{video_link}"> | |
| Your browser does not support HTML5 video. | |
| </video> | |
| """, unsafe_allow_html=True) | |
| set_css() | |
| embed_video() | |
| # 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.logo("https://raw.githubusercontent.com/manassharma07/PyFock/main/logo_crysx_pyfock.png", size="large", link="https://github.com/manassharma07/pyfock",) | |
| # 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 | |
| * Calculate molecular integrals | |
| * No installation required! | |
| """) | |
| st.sidebar.markdown("---") | |
| # Links section | |
| st.sidebar.markdown("### 🔗 Links & Resources") | |
| st.sidebar.markdown(""" | |
| [](https://github.com/manassharma07/PyFock) | |
| [](https://github.com/manassharma07/PyFock-GUI) | |
| [](https://pypi.org/project/pyfock/) | |
| [](https://pyfock-docs.bragitoff.com) | |
| 📄 **Article:** *(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 for PyFock"): | |
| st.code(""" | |
| # Install LibXC — required by PyFock | |
| # For Python < 3.10: | |
| # Install system LibXC and then install pylibxc2 via pip | |
| sudo apt-get install libxc-dev # Ubuntu/Debian | |
| pip install pylibxc2 | |
| # For Python >= 3.10: | |
| # pip wheels for pylibxc2 may be unavailable | |
| # Use conda-forge instead (recommended) | |
| conda install -c conda-forge pylibxc -y | |
| # Install PyFock | |
| pip install pyfock | |
| # Optional: GPU support | |
| pip install cupy-cuda12x # choose version appropriate for your CUDA setup | |
| """, 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 | |
| - Suitable for large systems (upto ~10,000 basis functions) | |
| **GPU Acceleration:** | |
| - Up to **14× speedup** on A100 GPU 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 for XC Term", value=True, help="Use either PySCF grids or PyFock grids for DFT calculaiton. Using PySCF grids is recommended as those are relatively smaller. NOTE: This does not perform a PySCF DFT calculation, only grid generation.") | |
| compare_pyscf = st.checkbox("Compare energy with PySCF (will 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, 50, 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 = ncores | |
| 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 | |
| if dftObj.converged: | |
| st.success(f"✅ PyFock KS-DFT calculation converged in {pyfock_time:.2f} seconds and {dftObj.niter} iterations!") | |
| else: | |
| st.warning(f"⚠️ PyFock KS-DFT calculation did not converge in {dftObj.niter} iterations and {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"{dftObj.niter}") | |
| with st.expander("SCF Convergence Details"): | |
| # Create a DataFrame for the energies | |
| import pandas as pd | |
| scf_data = pd.DataFrame({ | |
| 'Iteration': range(1, len(dftObj.scf_energies) + 1), | |
| 'Energy (Ha)': dftObj.scf_energies | |
| }) | |
| # Calculate energy change between iterations | |
| scf_data['ΔE (Ha)'] = scf_data['Energy (Ha)'].diff() | |
| # Display the table | |
| st.dataframe(scf_data, use_container_width=True, hide_index=True) | |
| # Plot convergence | |
| import plotly.graph_objects as go | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter( | |
| x=scf_data['Iteration'], | |
| y=scf_data['Energy (Ha)'], | |
| mode='lines+markers', | |
| name='Energy', | |
| line=dict(color='#1f77b4', width=2), | |
| marker=dict(size=6) | |
| )) | |
| fig.update_layout( | |
| title='SCF Energy Convergence', | |
| xaxis_title='Iteration', | |
| yaxis_title='Energy (Hartree)', | |
| hovermode='x unified', | |
| height=400 | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # 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, language='python') | |
| # 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 when running on cloud. Download the repo and run the streamlit app locally for larger systems. | |
| - Smaller molecules and basis sets will run faster. | |
| - PySCF comparison adds computation time but validates energy. | |
| - All calculations use density fitting (resolution of identity) for efficiency. | |
| ### Example Systems | |
| - **Water**: Quick test system (7 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) | |
| st.sidebar.write('PyFock version being used for this GUI:', pyfock.__version__) | |