diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4ab9277..680d5af 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -52,5 +52,8 @@ jobs: - name: Install package in editable mode run: pip install -e . + - name: Install mypy stubs + run: pip install types-requests + - name: Type check with mypy - run: mypy src/plotlymol3d --ignore-missing-imports + run: mypy src/plotlymol3d --ignore-missing-imports --disable-error-code=import-untyped diff --git a/.gitignore b/.gitignore index c1817b5..0568bf7 100644 --- a/.gitignore +++ b/.gitignore @@ -159,3 +159,4 @@ update_repo.ipynb # Pip installation logs (artifacts from version specifiers) =*.*.0 nul +CLAUDE.md diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..3be0129 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,10 @@ +{ + "MD013": false, + "MD041": false, + "MD030": false, + "MD032": false, + "MD036": false, + "MD033": { + "allowed_elements": ["video", "source", "div", "p", "img"] + } +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a03ad2..d665249 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,7 @@ repos: - id: check-yaml args: [--unsafe] - id: check-added-large-files + exclude: ^docs/assets/ - id: check-merge-conflict - id: debug-statements @@ -30,6 +31,6 @@ repos: rev: v1.8.0 hooks: - id: mypy - additional_dependencies: [numpy, plotly] + additional_dependencies: [numpy, plotly, types-requests] args: [--ignore-missing-imports] exclude: ^(src/plotlymol3d/test\.py|src/plotlymol3d/Cube_to_Blender.*)$ diff --git a/CHANGELOG.md b/CHANGELOG.md index 62b562e..02c635b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- GUI migrated from Streamlit to Dash β€” launch with `python examples/gui_app.py` +- `gui` optional dependency updated: `dash>=2.14.0` + `dash-bootstrap-components>=1.5.0` (replaces `streamlit`) +- `launch_app.bat` and `stop_app.bat` updated for Dash process management + ## [0.2.0] - 2026-04-04 ### Added @@ -37,7 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated README installation instructions for conda workflow - Expanded test suite from 26 to 47 tests - Bumped `requires-python` from `>=3.8` to `>=3.9` -- Fixed repo URLs in `pyproject.toml` (now correctly point to NCCU-Schultz-Lab org) +- Fixed repo URLs in `pyproject.toml` (now correctly point to The-Schultz-Lab org) ## [0.1.0] - 2026-01-31 diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 76eb1fe..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,966 +0,0 @@ -# plotlyMol - Context for Claude - -**Last Updated:** 2026-02-03 -**Version:** 0.1.0 -**Python Package:** plotlymol -**Primary Module:** plotlymol3d - -## Project Overview - -plotlyMol is a Python package for creating **interactive 3D molecular visualizations** using Plotly. It enables chemists, researchers, and students to visualize molecular structures with various rendering modes and includes support for orbital visualization from quantum chemistry calculations. - -### Core Capabilities - -- **3D Molecular Visualization**: Ball-and-stick, stick-only, and van der Waals (VDW) representations -- **Multiple Input Formats**: SMILES strings, XYZ files, MOL/SDF files, PDB files, and Gaussian cube files -- **SMILES-to-3D**: Automatic 3D coordinate generation from SMILES via RDKit -- **Orbital Visualization**: Isosurface rendering from cube files using marching cubes algorithm -- **Vibrational Mode Visualization**: πŸ†• Visualize molecular vibrations from quantum chemistry calculations - - Support for Gaussian (.log), ORCA (.out), and Molden (.molden) formats - - Three visualization modes: static displacement arrows, animated vibrations, heatmap coloring - - Interactive controls for mode selection and parameters -- **Bond Order Display**: Visual differentiation of single, double, triple, and aromatic bonds -- **Interactive GUI**: Streamlit-based web interface for visual exploration -- **Export Options**: Interactive HTML, static PNG (via Kaleido) - -### Key Features - -- Uses Plotly's `Mesh3d` traces for performant 3D rendering -- Fibonacci sphere algorithm for smooth atom spheres -- Cylinder mesh generation for bonds -- Customizable lighting (ambient, diffuse, specular, roughness) -- Aromatic bonds displayed with dashed rendering -- Hover tooltips showing atom/bond information - ---- - -## Repository Structure - -``` -plotlyMol/ -β”œβ”€β”€ src/ -β”‚ └── plotlymol3d/ # Main package (note: package name vs module name) -β”‚ β”œβ”€β”€ __init__.py # Exports: draw_3D_rep, draw_3D_mol, vibration functions, etc. -β”‚ β”œβ”€β”€ plotlyMol3D.py # Core visualization module (~800 lines) -β”‚ β”œβ”€β”€ atomProperties.py # Atomic data (CPK colors, VDW radii, symbols, symbol_to_number mapping) -β”‚ β”œβ”€β”€ cube.py # Marching cubes for orbital isosurfaces -β”‚ β”œβ”€β”€ vibrations.py # πŸ†• Vibrational mode visualization (~1000 lines) -β”‚ β”œβ”€β”€ app.py # Streamlit GUI application (with vibration settings) -β”‚ β”œβ”€β”€ test.py # Legacy test script (use pytest instead) -β”‚ β”œβ”€β”€ Cube_to_Blender v3.py # Legacy Blender export (excluded from linting) -β”‚ └── *.{xyz,mol,pdb,cube} # Sample molecular data files -β”‚ -β”œβ”€β”€ examples/ -β”‚ β”œβ”€β”€ demo_visualizations.py # Demo script showing various features -β”‚ └── gui_app.py # Streamlit GUI (can be run directly) -β”‚ -β”œβ”€β”€ tests/ -β”‚ β”œβ”€β”€ __init__.py -β”‚ β”œβ”€β”€ conftest.py # pytest fixtures (sample molecules + vibration files) -β”‚ β”œβ”€β”€ test_input_processing.py # Tests for file parsing and conversion -β”‚ β”œβ”€β”€ test_visualization.py # Tests for rendering functions -β”‚ β”œβ”€β”€ test_vibrations.py # πŸ†• Tests for vibration parsers and visualization -β”‚ └── fixtures/ # πŸ†• Test data directory -β”‚ β”œβ”€β”€ water_gaussian.log # Sample Gaussian frequency calculation -β”‚ β”œβ”€β”€ water_orca.out # Sample ORCA frequency calculation -β”‚ └── water.molden # Sample Molden format file -β”‚ -β”œβ”€β”€ docs/ -β”‚ └── ROADMAP.md # Detailed development roadmap -β”‚ -β”œβ”€β”€ .github/ -β”‚ └── workflows/ -β”‚ β”œβ”€β”€ test.yml # CI: pytest on 3 OSes, 4 Python versions -β”‚ └── lint.yml # CI: black, ruff, mypy -β”‚ -β”œβ”€β”€ pyproject.toml # Modern Python packaging config -β”œβ”€β”€ requirements.txt # All dependencies (runtime + dev) -β”œβ”€β”€ README.md # User-facing documentation (with vibration examples) -β”œβ”€β”€ CHANGELOG.md # Version history -β”œβ”€β”€ LICENSE # MIT License -β”œβ”€β”€ .gitignore # Comprehensive Python gitignore -β”œβ”€β”€ .pre-commit-config.yaml # Local pre-commit hooks -β”œβ”€β”€ launch_app.{bat,vbs} # Windows scripts to launch Streamlit GUI -└── stop_app.{bat,vbs} # Windows scripts to stop Streamlit GUI -``` - -### Important Note on Naming - -- **Package name (PyPI/pip):** `plotlymol` (no "3d") -- **Module name (import):** `plotlymol3d` (with "3d") -- Usage: `from plotlymol3d import draw_3D_rep` - ---- - -## Core Modules - -### 1. plotlyMol3D.py - Main Visualization Engine - -**Location:** [src/plotlymol3d/plotlyMol3D.py](src/plotlymol3d/plotlyMol3D.py) - -This is the heart of the package (~800 lines). Key components: - -#### Data Classes -```python -@dataclass -class Atom: - atom_id: int - atom_number: int - atom_symbol: str - atom_xyz: List[float] - atom_vdw: float - -@dataclass -class Bond: - a1_id: int - a2_id: int - a1_number: int - a2_number: int - a1_xyz: List[float] - a2_xyz: List[float] - a1_vdw: float - a2_vdw: float - bond_order: float # 1.0=single, 2.0=double, 3.0=triple, 1.5=aromatic -``` - -#### Input Processing Functions -- `smiles_to_rdkitmol(smiles)` - Parse SMILES, add H, generate 3D coords -- `xyzfile_to_xyzblock(xyzfile)` - Read XYZ file to text block -- `xyzblock_to_rdkitmol(xyzblock, charge)` - Convert XYZ to RDKit Mol (uses rdDetermineBonds) -- `cubefile_to_xyzblock(cubefile)` - Extract coords from Gaussian cube file -- `molfile_to_rdkitmol(molfile)` - Read MOL/SDF file -- `pdbfile_to_rdkitmol(pdbfile)` - Read PDB file - -#### Molecule Processing -- `rdkitmol_to_atoms_bonds_lists(mol)` - Extract Atom and Bond dataclasses from RDKit Mol - -#### 3D Mesh Generation -- `fibonacci_sphere(samples, radius)` - Generate sphere vertices using Fibonacci spiral -- `cylinder_mesh(start, end, radius, resolution)` - Generate cylinder for bond -- `dashed_cylinder_mesh(...)` - Generate dashed cylinder for aromatic bonds - -#### Rendering Functions -- `make_atom_mesh_trace(atom, resolution, mode)` - Create Mesh3d for atom -- `make_bond_mesh_trace(bond, resolution, mode)` - Create Mesh3d for bond(s) -- `draw_3D_mol(mol, mode, resolution)` - Generate all traces from RDKit Mol -- `draw_3D_rep(...)` - **Main entry point** - handles all input types - -#### Formatting -- `format_figure(fig, bgcolor, title)` - Apply scene/layout settings -- `format_lighting(fig, ambient, diffuse, specular, roughness)` - Customize lighting - -#### Visualization Modes -- `"ball+stick"` - Full-size atoms + bonds -- `"stick"` - Small atoms + bonds -- `"vdw"` - Van der Waals spheres only (no bonds) - -### 2. atomProperties.py - Atomic Data - -**Location:** [src/plotlymol3d/atomProperties.py](src/plotlymol3d/atomProperties.py) - -Contains dictionaries mapping atomic numbers to: -- CPK color schemes (standard element colors) -- Van der Waals radii -- Element symbols - -### 3. cube.py - Orbital Visualization - -**Location:** [src/plotlymol3d/cube.py](src/plotlymol3d/cube.py) - -Implements marching cubes algorithm for generating isosurfaces from volumetric data (electron density, molecular orbitals). - -**Key Function:** -- `draw_cube_orbitals(cubefile, isovalue, colors, opacity)` - Generate orbital mesh - -**Note:** This file has relaxed linting rules due to its mathematical complexity and legacy code style. - -### 4. app.py - Streamlit GUI - -**Location:** [src/plotlymol3d/app.py](src/plotlymol3d/app.py) - -Interactive web-based GUI for visual testing and demonstrations. Features: -- Multiple input methods (SMILES, file upload, random molecules) -- Real-time parameter adjustment (lighting, mode, resolution) -- Orbital visualization from cube files -- πŸ†• **Vibration Settings** expandable section with file upload and visualization controls -- Configuration persistence to `.plotlymol3d_config.json` -- Caching for performance (`@st.cache_resource`) - -**Run with:** `streamlit run src/plotlymol3d/app.py` or `streamlit run examples/gui_app.py` - -### 5. vibrations.py - Vibrational Mode Visualization πŸ†• - -**Location:** [src/plotlymol3d/vibrations.py](src/plotlymol3d/vibrations.py) - -Comprehensive module for visualizing molecular vibrations from quantum chemistry calculations (~1000 lines). - -#### Data Classes -```python -@dataclass -class VibrationalMode: - mode_number: int # 1-based index - frequency: float # cm⁻¹ (negative if imaginary) - ir_intensity: Optional[float] # km/mol (if available) - displacement_vectors: np.ndarray # (n_atoms, 3) Cartesian displacements - is_imaginary: bool # True for imaginary frequencies - -@dataclass -class VibrationalData: - coordinates: np.ndarray # (n_atoms, 3) in Angstroms - atomic_numbers: List[int] # Atomic numbers - modes: List[VibrationalMode] # All vibrational modes - source_file: str # Original filename - program: str # "gaussian", "orca", or "molden" - - def get_mode(self, mode_number: int) -> Optional[VibrationalMode] - def get_displacement_magnitudes(self, mode_number: int) -> np.ndarray -``` - -#### Parser Functions -- `parse_gaussian_vibrations(filepath)` - Parse Gaussian .log files - - Extracts coordinates from last "Standard orientation" - - Parses "Harmonic frequencies" section (3 modes per block) - - Extracts frequencies, IR intensities, and displacement vectors -- `parse_orca_vibrations(filepath)` - Parse ORCA .out files - - Extracts "CARTESIAN COORDINATES (ANGSTROEM)" - - Parses "VIBRATIONAL FREQUENCIES" section - - Filters first 6 translation/rotation modes - - Extracts displacement vectors from "NORMAL MODES" (6 modes per block) -- `parse_molden_vibrations(filepath)` - Parse Molden .molden files - - Parses [Atoms], [FREQ], [INT], [FR-NORM-COORD] sections - - Handles Angs/AU unit conversion - - Well-structured format with clear section markers -- `parse_vibrations(filepath)` - Auto-detect format and route to appropriate parser - -#### Visualization Functions -- `create_displacement_arrows(vib_data, mode_number, amplitude, ...)` - Generate 3D arrow traces - - Uses Plotly Cone traces for vector field visualization - - Filters small displacements by threshold - - Custom hover info with mode and displacement data -- `create_vibration_animation(vib_data, mode_number, mol, n_frames, ...)` - Create animated vibration - - Sinusoidal motion: coords(t) = coords_eq + AΒ·sin(2Ο€t)Β·displacement - - Plotly frames with play/pause controls and slider - - Typical usage: 20-30 frames for smooth motion -- `create_heatmap_colored_figure(fig, vib_data, mode_number, colorscale, ...)` - Color by displacement - - Modifies existing atom traces with displacement magnitude coloring - - Normalizes magnitudes to 0-1 range - - Applies Plotly colorscale (Reds, Blues, Viridis, etc.) -- `add_vibrations_to_figure(fig, vib_data, mode_number, display_type, ...)` - Main integration function - - Entry point following `draw_cube_orbitals` pattern - - Handles "arrows", "heatmap", or "both" display types - - Updates figure title with mode info - -#### Key Features -- **Format Support**: Gaussian, ORCA, Molden with auto-detection -- **Three Visualization Modes**: Arrows, Animation, Heatmap -- **Performance**: Caching with `@st.cache_resource` in Streamlit -- **Error Handling**: Informative messages for parsing failures -- **Testing**: Comprehensive test suite with 21 tests covering all parsers and visualization modes - ---- - -## Architecture & Design Decisions - -### 1. RDKit as Core Dependency - -RDKit provides: -- SMILES parsing (`Chem.MolFromSmiles`) -- 3D coordinate generation (`AllChem.EmbedMolecule`, `UFFOptimizeMolecule`) -- Bond perception from XYZ (`rdDetermineBonds.DetermineBonds`) -- File format readers (MOL, PDB) -- Molecular graph operations - -**Known Limitation:** XYZ bond perception can fail for: -- Charged molecules without proper charge specification -- Complex functional groups (NITRO groups, some transition metals) -- Molecules with unusual bonding - -**Workaround:** Use MOL/SDF files instead when RDKit bond perception fails. - -### 2. Plotly Mesh3d Traces - -Instead of using scatter plots with markers, the package uses `Mesh3d` traces: -- **Performance:** More efficient for complex molecules -- **Visual Quality:** Smooth surfaces with proper lighting -- **Interactivity:** Native Plotly zoom, rotate, pan - -Each atom and bond is a separate `Mesh3d` trace, enabling: -- Individual coloring (half-bonds colored by atom) -- Custom hover info per atom/bond -- Show/hide capabilities (future feature) - -### 3. Fibonacci Sphere Algorithm - -Atoms are rendered using Fibonacci sphere tessellation: -- Distributes points evenly on sphere surface -- Creates triangular mesh with good uniformity -- Adjustable resolution (default: 32 points) - -### 4. Bond Order Visualization - -- **Single bonds:** One cylinder -- **Double bonds:** Two parallel cylinders with offset -- **Triple bonds:** Three cylinders arranged triangularly -- **Aromatic bonds:** One solid + one dashed cylinder - -### 5. Half-Bond Coloring - -Bonds are split at the midpoint and colored by the atom at each end: -- Visual clarity for heteroatom bonds (C-O, C-N) -- Each half is a separate mesh - -**Future Enhancement:** Weight split point by VDW radii instead of 50/50. - ---- - -## Development Setup - -### Installation from Source - -```bash -# Clone repository -git clone https://github.com/NCCU-Schultz-Lab/plotlyMol.git -cd plotlyMol - -# Create virtual environment (recommended) -python -m venv .venv -source .venv/bin/activate # On Windows: .venv\Scripts\activate - -# Install in editable mode with all dependencies -pip install -r requirements.txt -pip install -e . -``` - -### Dependencies - -**Runtime (required):** -- `plotly>=5.0.0` - Interactive plotting -- `numpy>=1.20.0` - Numerical arrays -- `rdkit>=2022.3.1` - Chemistry toolkit -- `kaleido>=0.2.1` - Static image export - -**Development (optional):** -- `pytest>=7.0.0` - Testing framework -- `pytest-cov>=4.0.0` - Coverage reporting -- `black>=23.0.0` - Code formatter -- `ruff>=0.1.0` - Fast linter -- `flake8>=6.0.0` - Additional linting -- `mypy>=1.0.0` - Type checker -- `pre-commit>=3.0.0` - Git hooks -- `streamlit>=1.30.0` - GUI framework - -### Python Version Support - -- **Minimum:** Python 3.8 -- **Tested:** Python 3.9, 3.10, 3.11, 3.12 -- **CI Matrix:** Tests on Ubuntu, macOS, Windows - ---- - -## Testing Strategy - -### Test Suite Structure - -**Location:** [tests/](tests/) - -- `conftest.py` - Fixtures providing sample molecules and vibration files -- `test_input_processing.py` - Input format parsers -- `test_visualization.py` - Rendering functions -- `test_vibrations.py` - πŸ†• Vibration parsers and visualization (21 tests) - -### Current Coverage - -- **Overall:** Significantly improved with vibration tests -- **plotlyMol3D.py:** ~73% -- **vibrations.py:** πŸ†• ~95% (comprehensive test coverage) -- **cube.py:** Low (marching cubes needs more tests) -- **Target:** >60% achieved with vibration module - -### Running Tests - -```bash -# Run all tests -pytest - -# Run with coverage -pytest --cov=plotlymol3d --cov-report=term-missing - -# Run specific test file -pytest tests/test_input_processing.py - -# Run specific test -pytest tests/test_visualization.py::test_make_atom_mesh_trace -``` - -### Test Status - -**Passing:** 47/47 tests βœ… - -Key tests: -- βœ… SMILES parsing -- βœ… XYZ file reading -- βœ… XYZ to molecule conversion (with known charge limitation) -- βœ… Cube file parsing -- βœ… Atom/bond extraction -- βœ… Mesh generation (sphere, cylinder) -- βœ… Bond order handling -- πŸ†• βœ… Gaussian vibration parsing (3 tests) -- πŸ†• βœ… ORCA vibration parsing (2 tests) -- πŸ†• βœ… Molden vibration parsing (2 tests) -- πŸ†• βœ… Displacement arrow generation (3 tests) -- πŸ†• βœ… Vibration animation (2 tests) -- πŸ†• βœ… Heatmap coloring (2 tests) -- πŸ†• βœ… Dataclass methods (3 tests) -- πŸ†• βœ… Integration with draw_3D_rep (4 tests) - ---- - -## CI/CD Pipeline - -### GitHub Actions Workflows - -**Location:** [.github/workflows/](.github/workflows/) - -#### 1. test.yml - Automated Testing - -**Triggers:** Push/PR to `main` or `develop` branches - -**Matrix:** -- **OS:** Ubuntu, macOS, Windows -- **Python:** 3.9, 3.10, 3.11, 3.12 -- **Total:** 10 combinations (some excluded to reduce CI time) - -**Steps:** -1. Checkout code -2. Set up Python with pip caching -3. Install dependencies from `requirements.txt` -4. Install package in editable mode -5. Run pytest with coverage -6. Upload coverage to Codecov (Ubuntu 3.11 only) - -#### 2. lint.yml - Code Quality - -**Triggers:** Push/PR to any branch - -**Checks:** -1. **Black** - Code formatting (line length: 88) -2. **Ruff** - Fast linting (replaces flake8) -3. **Mypy** - Static type checking - -**Files Excluded from Linting:** -- `cube.py` - Mathematical code with relaxed rules -- `Cube_to_Blender*.py` - Legacy code -- `test.py` - Old test file - -### Pre-commit Hooks - -**Location:** [.pre-commit-config.yaml](.pre-commit-config.yaml) - -Local hooks run before each commit: -- Trailing whitespace removal -- End-of-file fixer -- YAML validation -- Black formatting -- Ruff linting - -**Setup:** -```bash -pre-commit install -``` - ---- - -## Known Issues & Limitations - -### Current Issues - -1. **XYZ Bond Perception Failures** - - **Issue:** RDKit's `rdDetermineBonds` fails on some charged molecules - - **Example:** NITRO groups, charged transition metal complexes - - **Workaround:** Use MOL/SDF files with explicit bond information - - **Status:** Phase 5 roadmap item - -2. **Half-Bond Positioning** - - **Issue:** Bonds split at 50/50 midpoint, not weighted by atom size - - **Impact:** Visual accuracy could be improved - - **Solution:** Implement VDW-weighted midpoint calculation - - **Status:** Phase 5 roadmap item - -3. **Single Molecule Limitation** - - **Issue:** Can only visualize one molecule per call - - **Requested:** Handle lists of SMILES or structures - - **Use Case:** Comparing multiple conformers or related structures - - **Status:** Phase 5 roadmap item - -4. **Orbital Integration** - - **Issue:** Orbital drawing exists but needs better integration - - **Status:** Basic cube file support works, needs polish - -5. **Test Coverage** - - **Issue:** Only ~27% overall coverage (cube.py has minimal tests) - - **Target:** >60% - - **Status:** Ongoing improvement - -### Platform-Specific Notes - -**Windows:** -- Uses `.bat` and `.vbs` scripts for Streamlit GUI launching -- File paths use backslashes (handled by pathlib) - -**macOS/Linux:** -- Standard Unix paths work correctly -- Shell scripts would be more natural (could add in future) - ---- - -## Future Roadmap - -**Full details in:** [docs/ROADMAP.md](docs/ROADMAP.md) - -### Phase Status -- βœ… **Phase 1:** Project Foundation (Complete) -- βœ… **Phase 2:** Code Quality (Complete) -- βœ… **Phase 3:** Testing & CI/CD (Complete) -- βœ… **Phase 4:** Documentation (Complete) -- πŸ”„ **Phase 5:** Feature Development (In Progress) - - βœ… Vibrational Mode Visualization (Complete) -- ⏳ **Phase 6:** Advanced Features (Pending) -- ⏳ **Phase 7:** Community & Distribution (Pending) - -### Recent Milestones (Phase 4-5) βœ… - -1. **Documentation** βœ… - - Comprehensive README with vibration examples - - CHANGELOG updates - - CLAUDE.md context updates - -2. **Vibrational Mode Visualization** βœ… COMPLETED - - Three file format parsers (Gaussian, ORCA, Molden) - - Three visualization modes (arrows, animation, heatmap) - - Streamlit UI integration with interactive controls - - Comprehensive test suite (21 tests, 95% coverage) - -### Near-Term Priorities (Phase 5-6) - -1. **Vibration Feature Enhancements** - - 🎯 Test with real quantum chemistry data - - 🎯 Create example Jupyter notebooks - - 🎯 IR spectrum viewer with clickable peaks - - 🎯 Export animations as GIF/MP4 - - 🎯 Additional formats (ADF, Q-Chem, NWChem) - - 🎯 Raman intensity support - - 🎯 Transition state reaction coordinate visualization - -2. **Documentation Expansion** - - API documentation (Sphinx or MkDocs) - - Tutorial notebooks showcasing vibration features - - Example gallery with quantum chemistry workflows - - CONTRIBUTING.md - -3. **Core Feature Enhancements** - - VDW-weighted bond splitting - - Partial charge coloring (Gasteiger) - - Enhanced hover tooltips - - SDF multi-molecule support - - 2D structure rendering (ChemDraw-like) - -4. **RDKit Integration Expansion** - - More input formats (MOL2, InChI) - - Substructure highlighting - - Conformer generation/viewing - - Property calculations - -### Long-Term Goals (Phase 6-7) - -- Molecular dynamics trajectory visualization (4D animations) -- Interactive measurement tools (distances, angles, dihedrals) -- Performance optimization for large molecules (>1000 atoms) -- Side-by-side mode comparison for vibrational analysis -- PyPI publication -- conda-forge package -- Community building (GitHub Discussions, website) - ---- - -## Code Style & Standards - -### Python Style - -**Formatter:** Black (line length: 88) -**Linter:** Ruff (replaces flake8) -**Type Checker:** Mypy - -### Docstring Style - -**Format:** Google Style - -Example: -```python -def draw_3D_rep(smiles: Optional[str] = None, ...) -> go.Figure: - """Create interactive 3D molecular visualization. - - Args: - smiles: SMILES string for molecule. - mode: Visualization mode ("ball+stick", "stick", "vdw"). - resolution: Sphere tessellation resolution (default: 32). - - Returns: - Plotly Figure object with 3D molecular visualization. - - Raises: - ValueError: If no input is provided or input is invalid. - - Example: - >>> fig = draw_3D_rep(smiles="CCO", mode="ball+stick") - >>> fig.show() - """ -``` - -### Type Hints - -**Required:** All functions should have type hints -**Style:** Use `typing` module for complex types - -```python -from typing import List, Optional, Tuple, Union - -def example_func( - param1: str, - param2: Optional[int] = None, - param3: List[float] = None -) -> Tuple[int, str]: - ... -``` - -### Import Organization - -**Order (enforced by ruff):** -1. Standard library -2. Third-party packages -3. Local modules - -```python -import os -from pathlib import Path - -import numpy as np -import plotly.graph_objects as go -from rdkit import Chem - -from .atomProperties import * -from .cube import * -``` - ---- - -## Key Files Reference - -### Configuration Files - -| File | Purpose | -|------|---------| -| `pyproject.toml` | Modern Python packaging, tool configs (pytest, black, ruff, mypy, coverage) | -| `requirements.txt` | All dependencies (runtime + dev consolidated) | -| `.pre-commit-config.yaml` | Pre-commit hook configurations | -| `.gitignore` | Comprehensive Python exclusions | - -### Package Files - -| File | Lines | Purpose | -|------|-------|---------| -| `plotlyMol3D.py` | ~800 | Main visualization engine | -| `atomProperties.py` | ~290 | Atomic data (colors, radii, symbols, symbol_to_number mapping) | -| `cube.py` | ~400 | Marching cubes for orbitals | -| `vibrations.py` | πŸ†• ~1030 | Vibrational mode visualization (parsers + visualization) | -| `app.py` | ~527 | Streamlit GUI application (with vibration settings) | -| `__init__.py` | ~14 | Package exports (includes vibration functions) | - -### Documentation Files - -| File | Purpose | -|------|---------| -| `README.md` | User-facing documentation, quick start | -| `ROADMAP.md` | Detailed development plan (25+ phases) | -| `CHANGELOG.md` | Version history | -| `LICENSE` | MIT License | -| `CLAUDE.md` | This file - AI assistant context | - -### Sample Data Files - -Located in `src/plotlymol3d/`: -- `cube.xyz` - Sample XYZ molecular coordinates -- `cube.mol` - Sample MOL file -- `cube.pdb` - Sample PDB file -- `anto_occ_1-min2.cube` - Sample Gaussian cube file with orbital data - ---- - -## Git Workflow - -### Branch Strategy - -- **main** - Stable releases -- **develop** - Development branch (if used) -- Feature branches for new work - -### Current Git Status - -``` -M pyproject.toml # Modified -M requirements.txt # Modified -M src/plotlymol3d/app.py # Modified -``` - -### Commit Message Style - -Based on repository history, commits follow conventional format: -- `Add ` - New functionality -- `Update ` - Modifications -- `Fix ` - Bug fixes -- `Merge pull request #N` - PR merges - -Example: -``` -Add Streamlit app module and Windows scripts - -- Created app.py for interactive GUI -- Added launch/stop batch files for Windows -- Configured persistent settings - -Co-Authored-By: Claude Sonnet 4.5 -``` - -### Repository URLs - -- **GitHub:** https://github.com/NCCU-Schultz-Lab/plotlyMol -- **Issues:** https://github.com/NCCU-Schultz-Lab/plotlyMol/issues -- **CI Badges:** Tests and Lint workflows visible in README - ---- - -## Quick Start for AI Assistants - -### Common Tasks - -**1. Adding a new visualization feature:** -- Modify `plotlyMol3D.py` -- Add corresponding test in `tests/test_visualization.py` -- Update docstrings -- Run `black` and `ruff --fix` -- Run `pytest` - -**2. Supporting a new input format:** -- Add parser function in `plotlyMol3D.py` (section: Input Processing Functions) -- Add test in `tests/test_input_processing.py` -- Update `draw_3D_rep()` to accept new parameter -- Update README with example - -**3. Fixing a bug:** -- Write a failing test first -- Implement fix -- Verify test passes -- Check coverage didn't decrease - -**4. Improving documentation:** -- Update docstrings in code -- Update README.md for user-facing changes -- Update ROADMAP.md for completed items -- Update CHANGELOG.md for version history - -### Testing Commands - -```bash -# Format code -black src/plotlymol3d tests - -# Lint code -ruff check src/plotlymol3d tests -ruff check --fix src/plotlymol3d tests # Auto-fix - -# Type check -mypy src/plotlymol3d - -# Run tests -pytest -v - -# Run tests with coverage -pytest --cov=plotlymol3d --cov-report=term-missing - -# Run GUI for visual testing -streamlit run src/plotlymol3d/app.py -``` - ---- - -## Design Patterns & Best Practices - -### 1. Separation of Concerns - -- **Input Processing** β†’ `*_to_rdkitmol()` functions -- **Data Extraction** β†’ `rdkitmol_to_atoms_bonds_lists()` -- **Mesh Generation** β†’ `fibonacci_sphere()`, `cylinder_mesh()` -- **Rendering** β†’ `make_*_trace()` functions -- **Formatting** β†’ `format_figure()`, `format_lighting()` - -### 2. Dataclasses for Clarity - -Use `@dataclass` instead of dicts or tuples for atom/bond data: -- Type safety -- Named fields -- Default values -- Self-documenting - -### 3. Optional Parameters with Sensible Defaults - -Most functions use optional parameters with defaults: -```python -def draw_3D_rep( - smiles: Optional[str] = None, - mode: str = "ball+stick", - resolution: int = DEFAULT_RESOLUTION, - ambient: float = 0.5, - ... -) -``` - -### 4. Error Handling Philosophy - -- Use `ValueError` for invalid inputs -- Let RDKit exceptions propagate (better error messages) -- Document potential failures in docstrings - -### 5. Caching in GUI - -Streamlit app uses `@st.cache_resource` for expensive operations: -- SMILES parsing -- XYZ conversion -- File reading - -Prevents recomputation on every widget interaction. - ---- - -## Performance Considerations - -### 1. Mesh Resolution - -- **Default:** 32 samples per sphere -- **Trade-off:** Higher resolution = smoother but slower -- **Range:** 16-64 typically sufficient - -### 2. Large Molecules - -- Each atom/bond is a separate trace -- Molecules with 100+ atoms may render slowly -- Future optimization: Combine meshes, WebGL improvements - -### 3. Marching Cubes (Orbitals) - -- Most computationally expensive operation -- Large cube files (>100Β³ grid) take several seconds -- Potential improvements: Numba JIT, parallel processing - ---- - -## Debugging Tips - -### 1. RDKit Issues - -If bond perception fails: -```python -# Check RDKit Mol object -mol = xyzblock_to_rdkitmol(xyzblock, charge=0) -if mol is None: - print("RDKit failed to create molecule") -else: - print(f"Atoms: {mol.GetNumAtoms()}, Bonds: {mol.GetNumBonds()}") -``` - -### 2. Visualization Problems - -If rendering looks wrong: -- Check `mode` parameter ("ball+stick", "stick", "vdw") -- Adjust `resolution` (try 16 for fast preview) -- Check lighting parameters (too high ambient = washed out) - -### 3. Import Errors - -If `from plotlymol3d import *` fails: -- Verify package installed: `pip list | grep plotlymol` -- Check working directory -- Try explicit import: `from plotlymol3d.plotlyMol3D import draw_3D_rep` - ---- - -## Downstream Integrations - -### QuantUI Integration (Planned) - -**Project:** [QuantUI](https://github.com/NCCU-Schultz-Lab/QuantUI) - Educational quantum chemistry interface for SLURM clusters - -**Integration Status:** Phase 3 (Visualization) - Planning Complete - -**Purpose:** Provide 3D molecular visualization for students learning quantum chemistry - -**Key Integration Points:** - -1. **Molecule Input Validation** - Visualize student-entered XYZ coordinates -2. **Calculation Results** - Show molecular structure when reviewing PySCF output -3. **Educational Tool** - Help students understand 3D molecular geometry - -**Technical Details:** - -- Uses `draw_3D_rep()` with `xyzblock` parameter -- Converts QuantUI's `Molecule` objects to XYZ strings -- Optional dependency with graceful fallback -- Integration module: `quantui/visualization.py` - -**Benefits:** - -- Students get immediate visual feedback on coordinate entry -- Visual inspection helps detect input errors -- Interactive rotation improves spatial understanding -- Consistent visualization tooling across NCCU Schultz Lab projects - -**Documentation:** See `QuantUI/PLOTLYMOL_INTEGRATION_PLAN.md` for complete details - -**Future Possibilities:** - -- Orbital visualization from PySCF cube files -- Geometry optimization trajectory animation -- Vibrational mode visualization (using PlotlyMol's vibration features) -- IR spectrum viewer with mode animations - ---- - -## Authors & License - -**Authors:** -- Jonathan Schultz - North Carolina Central University, Assistant Professor of Chemistry (jonathanschultzNU@users.noreply.github.com) -- Benjamin Lear - The Pennsylvania State University, Professor of Chemistry - -**License:** MIT (see [LICENSE](LICENSE) file) - -**Repository:** https://github.com/NCCU-Schultz-Lab/plotlyMol - ---- - -## Additional Resources - -- **RDKit Documentation:** https://www.rdkit.org/docs/ -- **Plotly Python Documentation:** https://plotly.com/python/ -- **Plotly 3D Mesh:** https://plotly.com/python/3d-mesh/ -- **SMILES Tutorial:** https://en.wikipedia.org/wiki/Simplified_molecular-input_line-entry_system -- **Gaussian Cube Format:** http://paulbourke.net/dataformats/cube/ -- **Marching Cubes:** https://en.wikipedia.org/wiki/Marching_cubes - ---- - -**End of Context Document** - -This document should be updated when: -- Major architectural changes occur -- New modules are added -- Development phase changes (see ROADMAP.md) -- API significantly changes -- New dependencies are added diff --git a/README.md b/README.md index ec671da..4214c6a 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ plotlyMol Logo

-[![Tests](https://github.com/NCCU-Schultz-Lab/plotlyMol/actions/workflows/test.yml/badge.svg)](https://github.com/NCCU-Schultz-Lab/plotlyMol/actions/workflows/test.yml) -[![Lint](https://github.com/NCCU-Schultz-Lab/plotlyMol/actions/workflows/lint.yml/badge.svg)](https://github.com/NCCU-Schultz-Lab/plotlyMol/actions/workflows/lint.yml) +[![Tests](https://github.com/The-Schultz-Lab/plotlyMol/actions/workflows/test.yml/badge.svg)](https://github.com/The-Schultz-Lab/plotlyMol/actions/workflows/test.yml) +[![Lint](https://github.com/The-Schultz-Lab/plotlyMol/actions/workflows/lint.yml/badge.svg)](https://github.com/The-Schultz-Lab/plotlyMol/actions/workflows/lint.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) Interactive molecular visualizations with Plotly. Supports SMILES, XYZ, MOL/PDB, and cube orbitals. @@ -27,7 +27,7 @@ Interactive molecular visualizations with Plotly. Supports SMILES, XYZ, MOL/PDB, ### From source (recommended for now) ```bash -git clone https://github.com/NCCU-Schultz-Lab/plotlyMol.git +git clone https://github.com/The-Schultz-Lab/plotlyMol.git cd plotlyMol # Create and activate the conda environment (includes all dependencies) @@ -242,4 +242,3 @@ plotlyMol/ ## Roadmap See the current roadmap in [docs/ROADMAP.md](docs/ROADMAP.md). - diff --git a/docs/CI_TROUBLESHOOTING_SUMMARY.md b/docs/CI_TROUBLESHOOTING_SUMMARY.md deleted file mode 100644 index c1b4b72..0000000 --- a/docs/CI_TROUBLESHOOTING_SUMMARY.md +++ /dev/null @@ -1,37 +0,0 @@ -# CI Troubleshooting Summary (2026-01-31) - -## Progress Summary -- Identified CI failures: Black formatting, Ruff linting, and mypy type-check. -- Updated formatting and imports across tests and core modules to satisfy Black/Ruff. -- Adjusted Black configuration to skip legacy scripts that are not maintained: - - plotlymol3d/cube.py - - plotlymol3d/Cube_to_Blender v3.py -- Updated Ruff per-file ignores to avoid legacy/compatibility warnings in: - - plotlymol3d/cube.py - - plotlymol3d/plotlyMol3D.py - - plotlymol3d/__init__.py -- Fixed lint issues in test files (imports sorted, unused imports removed). -- Fixed several type-check issues in plotlymol3d/plotlyMol3D.py (typing defaults, float/int, Optional default). -- Added a type annotation for the cube interpolation `cache` to satisfy mypy. -- Reformatted demo scripts with Black and resolved Ruff import/lint issues in GUI/demo scripts. - -## Current Status -- Black: passing on current scope; legacy files excluded. -- Ruff: passing (including demo/GUI scripts). -- mypy: passing (cache annotation added). - -## Next Steps -1. Re-run CI checks (Black, Ruff, mypy) to verify green in CI. - -## Files Touched -- pyproject.toml (Black exclusions, Ruff per-file-ignores, mypy exclusions) -- plotlymol3d/plotlyMol3D.py -- plotlymol3d/atomProperties.py -- plotlymol3d/__init__.py -- plotlymol3d/cube.py -- plotlymol3d/test.py -- demo_visualizations.py -- gui_app.py -- tests/test_input_processing.py -- tests/test_visualization.py -- tests/conftest.py diff --git a/docs/PERFORMANCE_TESTING_GUIDE.md b/docs/PERFORMANCE_TESTING_GUIDE.md deleted file mode 100644 index 0c397cf..0000000 --- a/docs/PERFORMANCE_TESTING_GUIDE.md +++ /dev/null @@ -1,653 +0,0 @@ -# Performance Testing Guide - -**Date:** 2026-02-03 -**Purpose:** Quantitative performance testing and optimization for plotlyMol - ---- - -## Overview - -This guide provides methods for **quantitatively measuring** plotlyMol's performance, particularly for GUI/Streamlit applications. Instead of subjectively saying "the GUI is laggy," you can now measure exact rendering times, memory usage, and identify specific bottlenecks. - ---- - -## Available Tools - -### 1. Performance Testing Script - -**Location:** [tests/test_performance.py](../tests/test_performance.py) - -A standalone Python script that runs comprehensive benchmarks and saves results. - -**Usage:** -```bash -# Run all benchmarks -python tests/test_performance.py - -# Results saved to: benchmark_results/ -``` - -**What it measures:** -- Rendering time vs molecule size -- Resolution impact on performance -- Vibration file parsing speed -- Vibration visualization modes (arrows, heatmap, animation) -- Animation frame count impact -- Memory usage for all operations - -**Output:** -- CSV files with detailed results -- Console summary with recommendations -- Performance ratios and speedup calculations - ---- - -### 2. Performance Benchmarking Notebook - -**Location:** [examples/performance_benchmarking.ipynb](../examples/performance_benchmarking.ipynb) - -Interactive Jupyter notebook for in-depth performance analysis. - -**Features:** -- Interactive visualizations (matplotlib charts) -- Customizable benchmarks -- Statistical analysis (mean, std, min, max) -- Memory profiling with `tracemalloc` -- Real-time results display - -**Usage:** -```bash -jupyter notebook examples/performance_benchmarking.ipynb -``` - ---- - -## Key Metrics - -### 1. Rendering Time - -**What it measures:** Time to generate 3D molecular visualization - -**Factors affecting performance:** -- **Molecule size** (number of atoms) -- **Rendering mode** (`ball+stick`, `stick`, `vdw`) -- **Resolution** (sphere tessellation quality) -- **Bond count** (especially for complex molecules) - -**Typical values (from benchmarks):** -- Small molecules (<20 atoms): 50-200 ms -- Medium molecules (20-50 atoms): 200-500 ms -- Large molecules (>50 atoms): 500-2000 ms - -**Optimization tips:** -- Use `resolution=16` for preview (2-3x faster than default 32) -- Use `stick` mode for molecules >50 atoms -- Use `vdw` mode for very large systems (>100 atoms) - ---- - -### 2. Memory Usage - -**What it measures:** Peak memory consumption during rendering - -**Typical values:** -- Small molecules: 5-20 MB -- Medium molecules: 20-50 MB -- Large molecules: 50-200 MB -- Animations: 2-5x base memory (depends on frame count) - -**Memory profiling:** -```python -import tracemalloc - -tracemalloc.start() -fig = draw_3D_rep(smiles="CCO", mode="ball+stick") -current, peak = tracemalloc.get_traced_memory() -tracemalloc.stop() - -print(f"Peak memory: {peak / 1024 / 1024:.1f} MB") -``` - ---- - -### 3. Vibration Parsing Speed - -**What it measures:** Time to parse vibrational data from quantum chemistry files - -**Typical values (water molecule):** -- Gaussian (.log): 10-30 ms -- ORCA (.out): 10-30 ms -- Molden (.molden): 5-15 ms - -**Scaling:** Roughly linear with file size and number of modes - -**Optimization:** -- Cache parsed results with `@st.cache_resource` in Streamlit -- Parse once, visualize many times - ---- - -### 4. Vibration Visualization Performance - -**What it measures:** Time to add vibration overlays to molecular figure - -**Typical values (water molecule):** -- Static arrows: +20-50 ms -- Heatmap coloring: +30-60 ms -- Animation (20 frames): 500-1500 ms - -**Optimization tips:** -- Use static arrows for fastest visualization -- Limit animations to 20-30 frames during development -- Use lower resolution (16) for animation preview - ---- - -## Quantifying GUI Lag - -### Problem: "The Streamlit GUI feels laggy" - -### Solution: Measure Specific Operations - -#### Step 1: Identify the Slow Operation - -Run the performance script to get baseline measurements: - -```bash -python tests/test_performance.py -``` - -Look at the console output: - -``` -BENCHMARK: Rendering Performance vs Molecule Size -================================================================== - -Water (3 atoms): - ball+stick : 85.3 Β± 12.1 ms - stick : 45.2 Β± 8.4 ms - vdw : 92.1 Β± 15.3 ms - -Cholesterol (74 atoms): - ball+stick : 1823.5 Β± 145.2 ms <-- THIS IS SLOW! - stick : 892.3 Β± 67.8 ms - vdw : 2145.7 Β± 178.9 ms -``` - -**Conclusion:** Large molecules in `ball+stick` mode are the bottleneck. - ---- - -#### Step 2: Test Optimization Strategies - -Compare different settings: - -```python -# Benchmark different resolutions -python tests/test_performance.py - -# Look at resolution results: -# Resolution 16: 456.2 ms (2.0x faster) -# Resolution 32: 912.3 ms (baseline) -# Resolution 64: 1834.5 ms (2.0x slower) -``` - -**Conclusion:** Using `resolution=16` gives 2x speedup with minimal visual quality loss. - ---- - -#### Step 3: Measure in Your Actual GUI - -Add timing to your Streamlit app: - -```python -import time -import streamlit as st - -# Before rendering -start_time = time.perf_counter() - -# Your rendering code -fig = draw_3D_rep(smiles=smiles, mode=mode, resolution=resolution) - -# After rendering -end_time = time.perf_counter() -render_time_ms = (end_time - start_time) * 1000 - -# Display to user -st.sidebar.metric("Render Time", f"{render_time_ms:.0f} ms") -``` - -Now you have **quantitative data** showing exactly how long operations take! - ---- - -## Performance Testing Workflow - -### For Development - -1. **Run baseline benchmarks** - ```bash - python tests/test_performance.py - ``` - -2. **Identify bottlenecks** from the output - -3. **Make optimizations** (change resolution, mode, etc.) - -4. **Re-run benchmarks** to measure improvement - -5. **Compare results** using saved CSV files - ---- - -### For Production - -1. **Profile real-world molecules** that your users will visualize - -2. **Add custom benchmark** in `performance_benchmarking.ipynb`: - ```python - # Your specific molecule - my_smiles = "CC(C)CCCC(C)C1CCC2C1(CCC3C2CC=C4C3(CCC(C4)O)C)C" - - stats = benchmark_multiple_runs( - draw_3D_rep, - n_runs=5, - smiles=my_smiles, - mode="ball+stick", - resolution=32 - ) - - print(f"Your molecule: {stats['mean_time_ms']:.1f} ms") - ``` - -3. **Set performance budgets**: - - Interactive preview: <200 ms target - - Final render: <2000 ms acceptable - - Animation: <5000 ms total - -4. **Monitor performance** over time as codebase evolves - ---- - -## Streamlit-Specific Optimizations - -### 1. Caching - -**Problem:** Re-parsing files on every widget interaction - -**Solution:** Use `@st.cache_resource` - -```python -@st.cache_resource(show_spinner=False) -def cached_parse_vibrations(vib_bytes: bytes, filename: str): - """Cache vibration parsing results.""" - # ... parsing logic ... - return parse_vibrations(temp_path) -``` - -**Speedup:** 10-100x for repeated access - ---- - -### 2. Performance Mode - -**Problem:** High-resolution rendering is slow during exploration - -**Solution:** Implement a performance toggle - -```python -# In sidebar -perf_mode = st.sidebar.selectbox( - "Mode", - ["Balanced", "Performance"], - help="Performance uses lower resolution for faster rendering" -) - -# Use lower resolution in performance mode -resolution_used = 16 if perf_mode == "Performance" else 32 -``` - -**Speedup:** ~2x faster rendering - ---- - -### 3. Lazy Loading - -**Problem:** Generating expensive visualizations upfront - -**Solution:** Generate only when needed - -```python -# Don't generate animation until user requests it -if vib_display_type == "Animation": - with st.spinner("Creating animation..."): - fig = create_vibration_animation(...) -else: - # Regular figure (faster) - fig = draw_3D_rep(...) -``` - ---- - -### 4. Profiling in Streamlit - -Add a performance profiler to your app: - -```python -import time -import streamlit as st - -class PerformanceProfiler: - def __init__(self): - self.timings = {} - - def measure(self, name): - """Context manager for timing operations.""" - class Timer: - def __enter__(self_): - self_.start = time.perf_counter() - return self_ - - def __exit__(self_, *args): - end = time.perf_counter() - self.timings[name] = (end - self_.start) * 1000 - - return Timer() - - def display_metrics(self): - """Show performance metrics in sidebar.""" - st.sidebar.markdown("### Performance") - for name, time_ms in self.timings.items(): - # Color indicators removed - st.sidebar.metric(name, f"{time_ms:.0f} ms", delta=None) - -# Usage in your app -profiler = PerformanceProfiler() - -with profiler.measure("Molecule Rendering"): - fig = draw_3D_rep(smiles=smiles, mode=mode) - -with profiler.measure("Vibration Overlay"): - fig = add_vibrations_to_figure(fig, vib_data, mode_number=1) - -profiler.display_metrics() -``` - -Now you have **real-time performance monitoring** in your GUI! - ---- - -## Interpreting Results - -### Good Performance - -``` -Water (3 atoms): - ball+stick : 85.3 ms Fast - -Benzene (12 atoms): - ball+stick : 234.5 ms Acceptable - -Glucose (24 atoms): - ball+stick : 456.8 ms Acceptable -``` - -**Response:** No optimization needed, feels snappy. - ---- - -### Borderline Performance - -``` -Cholesterol (74 atoms): - ball+stick : 1823.5 ms Noticeable lag -``` - -**Response:** Consider performance mode or optimize: -- Switch to `resolution=16`: ~900 ms Better -- Switch to `stick` mode: ~900 ms Better - ---- - -### Poor Performance - -``` -Large Protein (200 atoms): - ball+stick : 8234.5 ms Very slow (8+ seconds) -``` - -**Response:** Aggressive optimization needed: -- Use `stick` mode: ~3500 ms Still slow -- Use `vdw` mode: ~4200 ms Still slow -- Use `resolution=16`: ~4000 ms Still slow -- **Consider:** Downsampling, progressive loading, or WebGL optimization - ---- - -## Advanced Profiling - -### Line-by-Line Profiling - -For deep analysis, use `line_profiler`: - -```bash -pip install line-profiler -``` - -```python -# Add @profile decorator -@profile -def draw_3D_rep(smiles, mode, resolution): - # ... function code ... - -# Run with line profiler -kernprof -l -v your_script.py -``` - ---- - -### Memory Profiling - -Use `memory_profiler` for detailed memory analysis: - -```bash -pip install memory-profiler -``` - -```python -from memory_profiler import profile - -@profile -def render_large_molecule(): - fig = draw_3D_rep(smiles="CC(C)CCCC(C)...", mode="ball+stick") - return fig - -render_large_molecule() -``` - -Run with: -```bash -python -m memory_profiler your_script.py -``` - ---- - -## Performance Budgets - -Set targets for your application: - -| Operation | Target | Acceptable | Needs Optimization | -|-----------|--------|------------|-------------------| -| Small molecule render | <100 ms | <300 ms | >500 ms | -| Medium molecule render | <300 ms | <800 ms | >1500 ms | -| Large molecule render | <800 ms | <2000 ms | >5000 ms | -| Vibration parsing | <50 ms | <200 ms | >500 ms | -| Animation generation (30 frames) | <2000 ms | <5000 ms | >10000 ms | -| GUI interaction | <100 ms | <300 ms | >500 ms | - -**Use these benchmarks to guide optimization priorities!** - ---- - -## Common Bottlenecks - -### 1. Fibonacci Sphere Generation - -**Issue:** Generating sphere vertices for atoms - -**Impact:** Scales with resolutionΒ² and number of atoms - -**Optimization:** -- Pre-compute sphere meshes for common resolutions -- Cache generated meshes -- Use lower resolution - ---- - -### 2. Cylinder Mesh Generation - -**Issue:** Creating bond cylinders - -**Impact:** Scales with number of bonds and resolution - -**Optimization:** -- Use `stick` mode (smaller cylinders) -- Pre-compute cylinder templates -- Reduce resolution - ---- - -### 3. RDKit Coordinate Generation - -**Issue:** 3D embedding from SMILES - -**Impact:** Varies greatly with molecule size - -**Optimization:** -- Use pre-computed coordinates when available -- Cache RDKit molecules -- Skip UFF optimization for preview - ---- - -### 4. Plotly Figure Serialization - -**Issue:** Converting figure to JSON for display - -**Impact:** Scales with number of traces - -**Optimization:** -- Reduce number of separate traces -- Combine meshes when possible -- Use Plotly's WebGL mode for large datasets - ---- - -## Recommended Workflow - -### Day-to-Day Development - -1. Run benchmarks weekly to catch regressions -2. Add timing metrics to GUI for user visibility -3. Use performance mode by default during development -4. Profile slow operations with the notebooks - -### Before Release - -1. Run full benchmark suite -2. Test with real-world molecules from target users -3. Verify all operations meet performance budgets -4. Document expected performance characteristics - -### After User Reports Lag - -1. Ask user for specific molecule (SMILES or file) -2. Benchmark that exact molecule -3. Identify bottleneck (size, mode, resolution) -4. Provide specific recommendation -5. Consider adding automatic optimization suggestions to GUI - ---- - -## Example: Diagnosing Lag - -**User Report:** "The GUI is laggy when I visualize my molecules" - -**Your Response:** - -1. **Gather information:** - ``` - Can you share: - - The molecule (SMILES or file) - - What rendering mode you're using - - What operations feel slow - ``` - -2. **Run benchmarks with their molecule:** - ```python - # In performance_benchmarking.ipynb - user_smiles = "CC(C)CCCC(C)..." # Their molecule - - for mode in ["ball+stick", "stick"]: - for resolution in [16, 32]: - stats = benchmark_multiple_runs( - draw_3D_rep, - smiles=user_smiles, - mode=mode, - resolution=resolution - ) - print(f"{mode}, res={resolution}: {stats['mean_time_ms']:.1f} ms") - ``` - -3. **Provide data-driven recommendation:** - ``` - Results for your molecule (74 atoms): - - ball+stick, resolution=32: 1823 ms (slow) - - ball+stick, resolution=16: 912 ms (2x faster) - - stick, resolution=16: 456 ms (4x faster) - - Recommendation: Use "stick" mode with Performance setting - for this size molecule. This will reduce lag from 1.8s to 0.5s. - ``` - -**Now you have objective, quantitative data to support your recommendation!** - ---- - -## Summary - -### Tools Available - -1. **Performance test script** (`tests/test_performance.py`) -2. **Benchmark notebook** (`examples/performance_benchmarking.ipynb`) -3. **Profiling utilities** in notebooks -4. **This guide** for interpretation - -### Metrics to Track - -- Rendering time (ms) -- Memory usage (MB) -- Parsing speed (ms) -- Frame generation rate (ms/frame) - -### Optimization Strategies - -- Resolution adjustment (8-64) -- Mode selection (ball+stick, stick, vdw) -- Caching (@st.cache_resource) -- Performance mode toggle -- Lazy loading - -### Next Steps - -1. Run baseline benchmarks on your system -2. Test with your specific molecules -3. Add performance monitoring to your GUI -4. Set performance budgets for your use case -5. Profile and optimize bottlenecks - ---- - -**Last Updated:** 2026-02-03 -**Status:** Production Ready \ No newline at end of file diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md deleted file mode 100644 index aa40ce8..0000000 --- a/docs/ROADMAP.md +++ /dev/null @@ -1,856 +0,0 @@ -# plotlyMol Development Roadmap - -This document outlines the development plan for plotlyMol, a Python package for creating interactive molecular visualizations using Plotly. - ---- - -## Progress Summary - -| Phase | Status | Description | -|-------|--------|-------------| -| Phase 1 | Complete | Project Foundation - Package structure, pyproject.toml, requirements | -| Phase 2 | Complete | Code Quality - Type hints, docstrings, error handling | -| Phase 3 | Complete | Testing & CI/CD - GitHub Actions, coverage, linting | -| Phase 4 | Complete | Documentation - README, CHANGELOG, comprehensive docs | -| Phase 5 | In Progress | Feature Development - Vibrational Visualization COMPLETE | -| Phase 6 | Pending | Advanced Features | -| Phase 7 | Pending | Community & Distribution | - -### Recommended Next Actions - -1. **Merge PR and verify CI** - Push changes, verify workflows run on GitHub -2. **Set up Codecov** - Add `CODECOV_TOKEN` secret to repository for coverage tracking -3. **Fix linting issues** - Run `black src/plotlymol3d tests` and `ruff check --fix src/plotlymol3d tests` to auto-fix style issues -4. **Enhance README** - Add CI badges and usage examples -5. **Create CHANGELOG.md** - Document version history -6. **Fix failing test** - Address `test_xyzblock_to_rdkitmol` charge detection issue - ---- - -## Current State - -The repository has completed **Phases 1-3** and now contains: - -### Core Package -- **3D Visualization Module** (`src/plotlymol3d/plotlyMol3D.py`): Main module for 3D molecular rendering with ball-and-stick and VDW representations -- **Atom Properties** (`src/plotlymol3d/atomProperties.py`): Atom colors, symbols, and VDW radii data -- **Marching Cubes Implementation** (`src/plotlymol3d/cube.py`): Orbital visualization from cube files -- **Sample Data Files**: Various molecular structure files (.xyz, .mol, .pdb, .cube) - -### Testing & CI/CD (NEW) -- **Test Suite** (`tests/`): 25 unit tests covering input processing and visualization -- **GitHub Actions** (`.github/workflows/`): Automated testing and linting on push/PR -- **Pre-commit Hooks** (`.pre-commit-config.yaml`): Local development quality checks -- **Coverage**: ~27% (main module ~73%, cube.py needs more tests) - -### Configuration -- **Package Configuration** (`pyproject.toml`): Modern Python packaging with tool configs -- **Dependencies** (`requirements.txt`): Core and development dependencies (consolidated) -- **Comprehensive `.gitignore`**: Proper exclusions for Python projects - -## Development Phases - -### Phase 1: Project Foundation (Immediate) COMPLETED - -**Goal**: Establish proper Python package infrastructure and fix critical issues - -#### Tasks: -- [x] **Fix `__init__.py` syntax error** - - ~~Current: `from .3D.plotlyMol3D import *` is invalid (module names cannot start with digits)~~ - - Solution: Renamed `3D` directory to `plotlymol3d` and fixed import structure - -- [x] **Improve `.gitignore`** - - Added `__pycache__/` directories - - Added `*.pyc`, `*.pyo`, `*.pyd` files - - Added `.Python`, `*.egg-info/`, `dist/`, `build/` - - Added virtual environment directories (`venv/`, `env/`, `.venv/`) - - Added IDE-specific files (`.vscode/`, `.idea/`, `*.swp`) - - Added OS-specific files (`.DS_Store`, `Thumbs.db`) - -- [x] **Create package configuration** - - Added `pyproject.toml` (modern Python packaging) - - Defined package metadata (name, version, author, description, license) - - Specified package structure and entry points - - Configured build system (setuptools) - -- [x] **Add `requirements.txt`** - - Listed core dependencies: - - `plotly>=5.0.0` - Interactive plotting library - - `numpy>=1.20.0` - Numerical operations - - `rdkit>=2022.3.1` - Chemistry toolkit for molecular operations - - Consolidated development dependencies into `requirements.txt` - -- [x] **Reorganize directory structure** - - Renamed `3D/` to `plotlymol3d/` (valid Python module name) - - Updated all relative imports accordingly - - Fixed `__init__.py` to properly export module contents - -#### Success Criteria: All Met -- Package can be installed with `pip install -e .` -- No `__pycache__` or `.pyc` files in version control -- Module can be imported without syntax errors - ---- - -### Phase 2: Code Quality (Short-term) COMPLETED - -**Goal**: Improve code maintainability, readability, and robustness - -#### Tasks: -- [x] **Add comprehensive type hints** - - Annotated all function parameters and return types - - Used `typing` module for complex types (List, Dict, Optional, Union, Tuple) - - Added type hints to dataclass fields - -- [x] **Add docstrings** - - Used Google style docstrings - - Documented all modules, classes, and functions - - Included parameters, return values, and examples - -- [x] **Remove hardcoded paths** - - Replaced absolute paths in `test.py` with `pathlib.Path` relative paths - - Used `__file__` to locate package data - - Added proper imports and documentation - -- [x] **Implement proper error handling** - - Added input validation for file formats - - Included descriptive error messages in docstrings - -- [x] **Clean up or remove `graveyard.py`** - - Reviewed deprecated code - confirmed it was obsolete Surface-based traces - - Removed entirely (current Mesh3d implementation is superior) - -#### Success Criteria: All Met -- All functions have type hints and docstrings -- No hardcoded paths in test files -- Graceful error handling with informative messages -- Code organization improved with section headers - ---- - -### Phase 3: Testing & CI/CD (Short-term) COMPLETED - -**Goal**: Establish automated testing and continuous integration - -#### Tasks: -- [x] **Create test infrastructure** - - Created `tests/` directory in repository root - - Added `tests/__init__.py` - - Created `tests/conftest.py` for pytest fixtures - - Sample test data uses files already in `src/plotlymol3d/` directory - -- [x] **Write unit tests** - - Test input format parsers: - - `test_smiles_to_rdkitmol` - SMILES parsing - - `test_xyzfile_to_xyzblock` - XYZ file reading - - `test_xyzblock_to_rdkitmol` - XYZ to molecule conversion (known issue with charge detection) - - `test_cubefile_to_xyzblock` - Cube file parsing - - Test molecular structure handling: - - `test_rdkitmol_to_atoms_bonds_lists` - Atom/bond extraction - - Test visualization components: - - `test_make_atom_mesh_trace` - Atom rendering - - `test_make_bond_mesh_trace` - Bond rendering - - `test_fibonacci_sphere` - Sphere generation - - `test_cylinder_mesh` - Cylinder generation - -- [x] **Add GitHub Actions CI workflow** - - Created `.github/workflows/test.yml` - - Runs on Python 3.9, 3.10, 3.11, 3.12 - - Runs on Ubuntu, macOS, Windows - - Installs dependencies and runs pytest with coverage - - Uploads coverage to Codecov - -- [x] **Add code coverage reporting** - - Configured `pytest-cov` in `pyproject.toml` - - Coverage threshold set to 25% (increase as tests improve) - - Current coverage: ~27% overall, ~73% on main module - -- [x] **Add linting and formatting checks** - - Created `.github/workflows/lint.yml` - - **Ruff**: Fast Python linter (replaces flake8) - - **Black**: Code formatter - - **mypy**: Static type checker - - Created `.pre-commit-config.yaml` for local hooks - -#### Files Created: -- `.github/workflows/test.yml` - Test automation -- `.github/workflows/lint.yml` - Linting automation -- `.pre-commit-config.yaml` - Pre-commit hooks -- `gui_app.py` - Streamlit GUI for visual testing -- `demo_visualizations.py` - Demo script for testing -- Updated `pyproject.toml` with tool configurations -- Updated `requirements.txt` with new development dependencies - -#### Recent Improvements (2026-01-31): -- Added bond order support (single, double, triple, aromatic bonds displayed differently) -- Fixed XYZ charge detection test (now handles expected errors gracefully) -- All 26 tests now pass -- Aromatic/resonance bonds now display as dashed (one solid + one dashed cylinder) -- Streamlit GUI app created with random molecule button, lighting controls, and multiple input methods - -#### Known Issues: -- Coverage is low on `cube.py` (marching cubes code) -- XYZ file bond detection can fail for charged molecules without correct charge specification - -#### Success Criteria: Met -- [x] pytest runs successfully (26/26 tests pass) -- [x] CI/CD pipeline configured for all supported platforms -- [x] Linting and formatting tools configured -- [x] All tests run automatically on pull requests - ---- - -### Phase 4: Documentation (Medium-term) - -**Goal**: Create comprehensive documentation for users and contributors - -#### Tasks: -- [x] **Enhance README.md** - - Add clear project description and features - - Add badges (build status, coverage, PyPI version, license) - - Add installation instructions: - ```bash - pip install plotlymol - # or for development - git clone https://github.com/jonathanschultzNU/plotlyMol.git - cd plotlyMol - pip install -e . - ``` - - Add quick start guide with code examples - - Document repository layout - - Add examples for each input format (SMILES, XYZ, MOL, PDB, cube) - - Add screenshots/GIFs of visualizations - - Link to full documentation - -- [ ] **Create API documentation** - - Choose documentation tool (Sphinx or MkDocs) - - Set up documentation structure: - - `docs/` directory - - `docs/api/` - Auto-generated API reference - - `docs/tutorials/` - Step-by-step guides - - `docs/examples/` - Example gallery - - Configure auto-generation from docstrings - - Host documentation on Read the Docs or GitHub Pages - -- [ ] **Create example notebooks** - - Create `examples/` directory - - Add Jupyter notebooks demonstrating: - - Basic 3D molecule visualization - - Different representation modes (ball+stick, VDW, stick) - - Orbital visualization from cube files - - Customizing colors and lighting - - Exporting visualizations - - Ensure notebooks are tested and up-to-date - -- [ ] **Add CONTRIBUTING.md** - - Explain how to contribute (bug reports, feature requests, pull requests) - - Code style guidelines - - Testing requirements - - Pull request process - - Code of conduct reference - -- [x] **Add LICENSE file** - - Recommend MIT or BSD license for open source - - Include copyright notice - - Add license badge to README - -- [x] **Create CHANGELOG.md** - - Document version history - - Follow Keep a Changelog format - - Include: - - Added features - - Changed functionality - - Deprecated features - - Removed features - - Fixed bugs - - Security patches - -#### Success Criteria: -- Clear, comprehensive README with examples -- API documentation available online -- Example notebooks run without errors -- Contribution guidelines are clear and welcoming - ---- - -### Phase 5: Feature Development - Vibrational Mode Visualization COMPLETED (2026-02-03) - -**Goal**: Add comprehensive vibrational mode visualization from quantum chemistry calculations - -#### Tasks Completed: - -- [x] **Created vibrations.py module** (~1000 lines) - - Dataclasses: `VibrationalMode`, `VibrationalData` - - Three format-specific parsers with auto-detection - - Three visualization modes (arrows, animation, heatmap) - - Comprehensive error handling and validation - -- [x] **Implemented Gaussian parser** (`parse_gaussian_vibrations`) - - Extracts coordinates from last "Standard orientation" - - Parses "Harmonic frequencies" section (3 modes per block) - - Extracts frequencies, IR intensities, and displacement vectors - -- [x] **Implemented ORCA parser** (`parse_orca_vibrations`) - - Parses "CARTESIAN COORDINATES (ANGSTROEM)" - - Extracts vibrational frequencies (filters first 6 translation/rotation modes) - - Parses "NORMAL MODES" section (6 modes per block) - -- [x] **Implemented Molden parser** (`parse_molden_vibrations`) - - Well-structured format with [Atoms], [FREQ], [INT], [FR-NORM-COORD] sections - - Handles Angstroms/Bohr unit conversion - - Cleanest parser implementation - -- [x] **Created visualization functions** - - `create_displacement_arrows()` - Static 3D arrows using Plotly Cone traces - - `create_vibration_animation()` - Animated vibration with Plotly frames (play/pause controls, slider) - - `create_heatmap_colored_figure()` - Color atoms by displacement magnitude - - `add_vibrations_to_figure()` - Main integration function - -- [x] **Integrated with Streamlit GUI** - - " Vibration Settings" expandable section - - File uploader for .log, .out, .molden files - - Mode selection dropdown with frequencies and IR intensities - - Display type radio buttons (Static arrows, Animation, Heatmap, Arrows + Heatmap) - - Interactive parameter controls (amplitude, arrow color/size, colorscale, frames) - - Cached parsing with `@st.cache_resource` - -- [x] **Comprehensive test suite** (21 tests, ~95% coverage) - - `test_vibrations.py` with fixtures for all three formats - - Parser tests (Gaussian, ORCA, Molden, auto-detect) - - Dataclass method tests - - Visualization tests (arrows, animation, heatmap) - - Integration tests with `draw_3D_rep()` - - Error handling tests - -- [x] **Documentation updates** - - README with vibration examples (arrows, animation, heatmap, parsers) - - CHANGELOG entry documenting all new features - - CLAUDE.md with comprehensive module documentation - -#### Files Created: -- `src/plotlymol3d/vibrations.py` - Complete vibration visualization module -- `tests/test_vibrations.py` - Comprehensive test suite -- `tests/fixtures/water_gaussian.log` - Gaussian test fixture -- `tests/fixtures/water_orca.out` - ORCA test fixture -- `tests/fixtures/water.molden` - Molden test fixture - -#### Files Modified: -- `src/plotlymol3d/__init__.py` - Added vibration function exports -- `src/plotlymol3d/atomProperties.py` - Added `symbol_to_number` mapping -- `src/plotlymol3d/app.py` - Added vibration settings section (~100 lines) -- `tests/conftest.py` - Added vibration file fixtures -- `README.md` - Added vibration documentation with examples -- `CHANGELOG.md` - Documented vibration feature -- `CLAUDE.md` - Updated with vibration module documentation - -#### Key Features: -- **Format Support**: Gaussian (.log), ORCA (.out), Molden (.molden) -- **Auto-Detection**: Automatically detects file format from extension and content -- **Three Visualization Modes**: - - Static displacement arrows (Plotly Cone traces) - - Animated vibrations (sinusoidal motion with interactive controls) - - Heatmap coloring (displacement magnitude mapping) -- **Performance**: File parsing cached in Streamlit for instant re-rendering -- **Error Handling**: Informative error messages for parsing failures -- **Test Coverage**: 21 new tests, 47 total tests passing - -#### Success Criteria: All Met -- [x] Parse all three file formats correctly (Gaussian, ORCA, Molden) -- [x] All three visualization modes work (arrows, animation, heatmap) -- [x] Streamlit UI integration with interactive controls -- [x] Comprehensive test coverage (~95% for vibrations module) -- [x] Documentation with examples in README -- [x] All 47 tests passing - ---- - -### Phase 5.1: Example Notebooks & Performance Testing COMPLETED (2026-02-03) - -**Goal**: Provide comprehensive examples and quantitative performance testing tools - -#### Tasks Completed: - -- [x] **Created Vibration Visualization Basics Notebook** - - Getting started tutorial covering all three visualization modes - - Parsing vibrational data from Gaussian, ORCA, Molden files - - Static arrows, animations, and heatmap examples - - Accessing mode data programmatically - - Comparing multiple vibrational modes - - 7 complete working examples with explanations - -- [x] **Created Advanced Vibration Analysis Notebook** - - Batch processing multiple calculations - - Creating vibrational mode summary tables with pandas - - IR spectrum visualization (matplotlib and Plotly) - - Interactive spectrum with clickable peaks - - Comparing modes across molecules - - Identifying functional group vibrations - - Creating publication-quality figures - - Exporting animations as HTML - - Statistical analysis of vibrational properties - - 9 advanced workflows with production-ready code - -- [x] **Created Performance Benchmarking Notebook** - - Quantitative performance measurement utilities - - Rendering time vs molecule size benchmarks - - Resolution impact analysis (8-64) - - Vibration parsing performance tests - - Animation frame count optimization - - Memory profiling with tracemalloc - - Statistical analysis (mean, std, min, max) - - Interactive visualizations of results - - Performance recommendations generator - -- [x] **Created Performance Testing Script** (`tests/test_performance.py`) - - Standalone Python script for comprehensive benchmarking - - Automated testing of rendering performance - - Vibration parsing speed measurement - - Animation generation profiling - - Memory usage tracking with psutil - - Saves results as CSV files with timestamps - - Generates performance analysis and recommendations - - Can be run in CI/CD for regression detection - -- [x] **Created Performance Testing Guide** (`docs/PERFORMANCE_TESTING_GUIDE.md`) - - Comprehensive guide for quantitative performance testing - - How to measure GUI lag objectively - - Interpreting benchmark results - - Optimization strategies with quantitative data - - Streamlit-specific optimizations - - Performance budgets for different operations - - Identifying bottlenecks systematically - - Real-world examples and troubleshooting - -- [x] **Updated documentation** - - README with Examples section (notebooks + performance testing) - - Added Performance Testing section to README - - Links to all new notebooks and guides - - Usage instructions for performance tools - -#### Files Created (Phase 5.1) - -- `examples/vibration_visualization_basics.ipynb` - Basic vibration tutorial -- `examples/vibration_analysis_advanced.ipynb` - Advanced analysis workflows -- `examples/performance_benchmarking.ipynb` - Interactive performance testing -- `tests/test_performance.py` - Automated performance testing script -- `docs/PERFORMANCE_TESTING_GUIDE.md` - Comprehensive performance guide - -#### Files Modified (Phase 5.1) - -- `README.md` - Added Examples and Performance Testing sections - -#### Key Features (Phase 5.1) - -- **Three Tutorial Notebooks**: Basic, advanced, and performance benchmarking -- **Quantitative Performance Testing**: Objective measurements instead of "feels laggy" -- **Comprehensive Metrics**: Rendering time, memory usage, parsing speed, frame generation -- **Optimization Guidance**: Data-driven recommendations with specific speedup numbers -- **Production-Ready Examples**: All code tested and ready to use -- **Statistical Analysis**: Mean, std, confidence intervals for reliable measurements - -#### Success Criteria (Phase 5.1) - All Met - -- [x] Basic vibration tutorial notebook created -- [x] Advanced analysis notebook with 9+ workflows -- [x] Performance benchmarking notebook with interactive plots -- [x] Standalone performance testing script -- [x] Comprehensive performance testing guide -- [x] Documentation updated with links to all resources -- [x] All notebooks executable and well-documented - -#### Impact - -- **For Users**: Clear learning path from basics to advanced workflows -- **For Performance**: Objective data to identify and fix bottlenecks -- **For GUI Optimization**: Quantitative metrics showing 2-3x speedups possible -- **For Research**: Publication-quality figure generation examples -- **For Development**: Automated performance regression detection - ---- - -#### Future Enhancements (Phase 6): -- IR spectrum viewer with clickable peaks -- Export animations as GIF/MP4 -- Additional formats (ADF, Q-Chem, NWChem) -- Raman intensity support -- Transition state reaction coordinate visualization -- Example Jupyter notebooks with quantum chemistry workflows -- Side-by-side mode comparison -- VCD/ROA chiral spectroscopy - ---- - -### RDKit Integration Status & Opportunities - -The package is well-integrated with RDKit for core functionality. This section documents current integration and expansion opportunities. - -#### Current RDKit Integration | Feature | RDKit Function | Status | -|---------|----------------|--------| -| SMILES parsing | `Chem.MolFromSmiles()` | Solid | -| 3D coordinate generation | `AllChem.EmbedMolecule()` + `UFFOptimizeMolecule()` | Solid | -| Hydrogen addition | `Chem.AddHs()` | Solid | -| Bond perception from XYZ | `rdDetermineBonds` | Can fail on complex molecules | -| Bond order detection | `bond.GetBondType()` | Solid | -| Atom properties | `GetAtomicNum()`, `GetSymbol()` | Solid | -| MOL/SDF file reading | `Chem.MolFromMolBlock()` | Solid | - -#### Expansion Opportunities - -##### High Priority (Quick Wins) -- [ ] **Partial charge coloring** - - Use `AllChem.ComputeGasteigerCharges(mol)` to compute Gasteiger charges - - Color atoms by charge (red=negative, blue=positive gradient) - - Add as optional coloring mode in `draw_atoms()` - -- [ ] **Enhanced hover tooltips** - - Show element, atom index, formal charge on hover - - Display bond order and length for bonds - - Use Plotly's `hovertemplate` for rich formatting - -- [ ] **SDF multi-molecule support** - - Use `Chem.SDMolSupplier()` to read SDF files with multiple molecules - - Display molecules in grid layout or overlay - - Common request for comparing conformers or related structures - -##### Medium Priority (RDKit Features) -- [ ] **More input formats via RDKit** - - MOL2 files: `Chem.MolFromMol2File()` - - InChI strings: `Chem.MolFromInchi()` - - Enhanced PDB support - -- [ ] **Substructure highlighting** - - Use `mol.GetSubstructMatches(pattern)` with SMARTS patterns - - Highlight functional groups with different colors - - Useful for teaching and analysis - -- [ ] **Conformer generation and viewing** - - Use `AllChem.EmbedMultipleConfs()` for conformer sampling - - Navigate between conformers with slider - - Animate conformer transitions - -- [ ] **2D structure rendering** - - Use RDKit's `Draw.MolToImage()` for 2D depictions - - Combine with Plotly image traces for hybrid views - - ChemDraw-like 2D molecular structures - -##### Lower Priority (Alternative Toolkits) -- [ ] **Open Babel integration (optional)** - - Fallback for XYZβ†’MOL conversion failures - - Additional file format support - - Better handling of complex bonding situations - -- [ ] **MDAnalysis integration (optional)** - - For molecular dynamics trajectory files - - Support DCD, TRR, XTC formats - - Required for 4D animation features - -- [ ] **ASE integration (optional)** - - For periodic systems and crystals - - CIF file support - - Unit cell visualization - -##### Plotly-Native Enhancements (No New Dependencies) -- [ ] **Interactive property display** - - Molecular weight, formula in sidebar/annotation - - Atom count by element - - Bond statistics - -- [ ] **Distance/angle measurements** - - Click two atoms to show distance - - Click three atoms to show angle - - Use Plotly annotations and shapes - -- [ ] **Animation frames for conformers** - - Use Plotly's `frames` parameter - - Add play/pause controls - - Smooth transitions between states - ---- - -### Phase 5: Feature Development (Medium-term) - -**Goal**: Implement planned features and address known issues - -#### 2D Structures -- [ ] **Implement ChemDraw-like 2D structure rendering** - - Use RDKit's 2D depiction capabilities - - Create Plotly-based 2D molecular structure plots - - Support SMILES input for 2D generation - - Add customization options (atom labels, bond types, colors) - -#### Enhanced 3D Features -- [ ] **Scale half-bond positions by VDW radii** - - Currently bonds are split at midpoint - - Implement VDW-weighted bond splitting - - Update `draw_bonds` function to use weighted midpoints - - Formula: `weighted_mid = (r1*pos1 + r2*pos2) / (r1 + r2)` - -- [ ] **Improve XYZ to MOL conversion** - - Address rdkit XYZ block to MOL conversion failures - - Handle edge cases (NITRO groups, charged species) - - Consider alternative approaches: - - Use Open Babel for XYZ to SMILES conversion - - Implement custom bond perception algorithm - - Add fallback methods - -- [ ] **Add orbital drawing integration** - - Full integration of marching cubes orbital visualization - - Make orbital drawing more user-friendly - - Add documentation and examples - - Support different isosurface values - - Allow multiple orbitals in single visualization - -#### Multiple Molecule Support -- [ ] **Handle lists of structures in single plot** - - Accept list of SMILES strings - - Accept list of file paths - - Create grid layouts or overlays - - Add molecule labeling/naming - - Support comparison visualizations - -#### Additional Enhancements -- [ ] **Add support for molecular properties** - - Display molecular weight, formula - - Show partial charges (Gasteiger) - - [x] Display bond orders (single, double, triple, aromatic with dashed display) - - Interactive property tooltips - -#### Success Criteria: -- 2D structure rendering works for common molecules -- VDW-scaled bonds improve visual accuracy -- Robust handling of various input formats -- Support for visualizing multiple molecules - ---- - -### Phase 6: Advanced Features (Long-term) - -**Goal**: Implement advanced visualization and interaction capabilities - -#### 4D Animations -- [ ] **Implement trajectory/animation support** - - Read molecular dynamics trajectory files - - Support common formats (XYZ trajectory, DCD, TRR) - - Create Plotly animation frames - - Add timeline controls - - Export as animated HTML or video - -#### GUI Development -- [x] **Create interactive web interface** PARTIAL - - Framework: Streamlit (`gui_app.py`) - - Implemented features: - - [x] Dynamic molecule input (SMILES, file upload) - - [x] Real-time lighting parameter adjustment - - [x] Multiple visualization modes (ball+stick, VDW, stick) - - [x] Random molecule selector with common examples - - [x] Cube file orbital visualization - - Remaining features: - - [ ] Export options (PNG, HTML, SVG) - - [ ] Save/load visualization settings - - [ ] Toggle hover/presentation modes - -- [ ] **Add visualization controls** - - Rotation/zoom controls (Plotly built-in) - - [ ] Show/hide atoms by element - - [ ] Show/hide hydrogens - - [ ] Measurement tools (distances, angles) - - [ ] Selection and highlighting - -#### Performance Optimization -- [ ] **Optimize marching cubes algorithm** - - Profile performance on large cube files - - Implement parallel processing (multiprocessing/numba) - - Add progress indicators for long operations - - Cache computed meshes - - Optimize mesh reduction/decimation - -#### Additional Input Formats -- [ ] **Support more file formats** - - SDF (Structure-Data File) - multiple molecules - - CIF (Crystallographic Information File) - crystals - - PDBQT (AutoDock) - docking results - - GRO (GROMACS) - MD simulations - - JSON - custom molecular formats - -#### Success Criteria: -- Smooth animations for molecular dynamics trajectories -- Functional GUI for non-programmers -- Acceptable performance on large molecular systems -- Support for diverse input formats - ---- - -### Phase 7: Community & Distribution (Long-term) - -**Goal**: Make plotlyMol widely accessible and build a community - -#### Package Distribution -- [ ] **Publish to PyPI** - - Set up PyPI account and project - - Configure `pyproject.toml` for publishing - - Create distribution packages (sdist and wheel) - - Upload to PyPI: `pip install plotlymol` - - Set up automated releases via GitHub Actions - -- [ ] **Create conda-forge recipe** - - Submit feedstock to conda-forge - - Maintain conda package - - Enable: `conda install -c conda-forge plotlymol` - -#### Community Building -- [ ] **Set up GitHub Discussions** - - Enable Discussions on repository - - Create categories: - - Announcements - - Q&A - - Show and Tell (user examples) - - Feature Requests - - General Discussion - -- [ ] **Create project website** - - Dedicated domain or GitHub Pages - - Features: - - Interactive demos - - Gallery of examples - - Tutorial walkthrough - - API documentation - - Download links - - Community showcase - -- [ ] **Outreach and promotion** - - Write blog posts or tutorials - - Present at Python or chemistry conferences - - Submit to Awesome Python lists - - Create demo videos - - Engage with chemistry/Python communities - -#### Success Criteria: -- Package available via pip and conda -- Active community engagement -- Professional documentation website -- Growing user base and contributors - ---- - -## Additional Files to Consider Creating - -### Immediate Priority COMPLETED -- **`.gitignore`**: Comprehensive exclusions - **`pyproject.toml`**: Package configuration - **`requirements.txt`**: Dependencies list - **`requirements.txt`**: Core + development dependencies -### Short-term Priority (Phase 2-3) -- **`LICENSE`**: Open source license (recommend MIT) Created -- **`CONTRIBUTING.md`**: Contribution guidelines -- **`CHANGELOG.md`**: Version history - -### Medium-term Priority -- **`.github/workflows/`**: CI/CD workflows - - `test.yml` - Automated testing - - `lint.yml` - Code quality checks - - `publish.yml` - PyPI publishing -- **`tests/`**: Test suite directory - - `tests/conftest.py` - pytest configuration - - `tests/test_*.py` - Unit tests -- **`docs/`**: Documentation directory - - `docs/conf.py` - Sphinx/MkDocs config - - `docs/index.md` - Documentation homepage - - `docs/tutorials/` - Tutorials - - `docs/api/` - API reference - -### Long-term Priority -- **`examples/`**: Example notebooks and scripts -- **`.pre-commit-config.yaml`**: Pre-commit hooks configuration -- **`CODE_OF_CONDUCT.md`**: Community guidelines - ---- - -## Notes from README Future Plans - -The following goals were identified in the original README: - -### 2D Structures -- ChemDraw-like 2D molecular structures - -### 3D Enhancements -- Scale half-bond positions by van der Waals radii of atoms at either end -- Add orbitals and other molecular surfaces (marching cubes code exists) -- Handle SMILES structures (currently implemented) -- Fix XYZ block to MOL conversion issues (NITRO groups, etc.) -- Need to integrate orbital drawing code - -### 4D Features -- Molecular dynamics animations and trajectories - -### GUI Features -- Toggle visualization aspects (presentation mode, hover info, etc.) -- Dynamic molecule entry interface -- Interactive parameter controls - -### Multiple Molecule Support -- Handle lists of SMILES or structures in a single plot -- Currently only handles single molecules per call - ---- - -## Implementation Priority - -### High Priority (Completed) 1. ~~Fix `__init__.py` syntax error~~ 2. ~~Create proper `.gitignore`~~ 3. ~~Add `pyproject.toml` or `setup.py`~~ 4. ~~Add `requirements.txt`~~ 5. ~~Fix directory structure (rename `3D/` to valid Python module name)~~ -### Medium Priority (Current Focus) -1. ~~Remove hardcoded paths from `test.py`~~ 2. ~~Add type hints and docstrings~~ 3. ~~Add LICENSE file (MIT)~~ 4. ~~Create test suite with pytest~~ 5. ~~Set up CI/CD with GitHub Actions~~ 6. **Fix code style issues** Run `black` and `ruff --fix` -7. **Enhance README** - Add badges and examples -8. **Create CHANGELOG.md** - Document version history - -### Lower Priority (3-6 months) -1. Implement 2D structure rendering -2. Fix VDW bond scaling -3. Complete API documentation (Sphinx/MkDocs) -4. Create example Jupyter notebooks -5. Publish to PyPI -6. Increase test coverage to >60% - -### Future Considerations (6+ months) -1. GUI development (Streamlit/Dash) -2. Animation support for molecular dynamics -3. Performance optimization for large molecules -4. Additional file format support (SDF, CIF, etc.) -5. Community building initiatives - ---- - -## Success Metrics - -- **Code Quality**: All functions documented, >80% test coverage, passing linting -- **Usability**: Clear installation instructions, working examples, responsive issue resolution -- **Distribution**: Available on PyPI and conda-forge -- **Community**: Active contributors, growing user base, regular releases -- **Documentation**: Comprehensive docs with tutorials and API reference - ---- - -## Conclusion - -This roadmap provides a clear path from the current state to a mature, well-documented, and widely-used Python package for molecular visualization. The phased approach ensures that critical infrastructure is established first, followed by quality improvements, testing, documentation, and finally advanced features and community building. - -Contributions are welcome at any phase of this roadmap! See CONTRIBUTING.md (to be created in Phase 4) for details on how to get involved. - ---- - -**Last Updated**: 2026-01-31 -**Current Phase**: Phase 4 (Documentation) - Starting -**Phase 1 Status**: Completed -**Phase 2 Status**: Completed -**Phase 3 Status**: Completed - -### Recent Additions -- Added comprehensive RDKit Integration section with current status and expansion opportunities -- Documented Plotly-native enhancement possibilities -- Identified optional toolkit integrations (Open Babel, MDAnalysis, ASE) -- Consolidated repository to a src/ layout with examples/ for demos -- Merged dev and runtime dependencies into a single requirements.txt - ---- - -## New Ideas (2026-01-31) - -- Add a small example image or GIF to README once a stable demo is chosen -- Provide a `docs/examples/` gallery with rendered HTML outputs -- Add a `make docs` or `nox` task for documentation builds -- Add a pre-commit hook for `ruff` and `black` to prevent style regressions diff --git a/docs/VIBRATION_FEATURE_SUMMARY.md b/docs/VIBRATION_FEATURE_SUMMARY.md deleted file mode 100644 index 1a86a00..0000000 --- a/docs/VIBRATION_FEATURE_SUMMARY.md +++ /dev/null @@ -1,445 +0,0 @@ -# Vibrational Mode Visualization - Feature Summary - -**Date Completed:** 2026-02-03 -**Status:** **COMPLETE** - All phases (1-5) implemented and tested - ---- - -## Overview - -Successfully implemented a comprehensive molecular vibration visualization system for plotlyMol, supporting three quantum chemistry file formats and three visualization modes. The feature includes complete parsing infrastructure, interactive Streamlit UI, and extensive test coverage. - ---- - -## What Was Accomplished - -### 1. Core Vibration Module (`vibrations.py`) - ~1030 lines - -**Data Structures:** -- `VibrationalMode` dataclass - Stores mode data (frequency, IR intensity, displacement vectors, imaginary flag) -- `VibrationalData` dataclass - Container for coordinates, atomic numbers, modes, source info - -**Three Format Parsers:** -- `parse_gaussian_vibrations()` - Parses Gaussian .log files - - Extracts coordinates from "Standard orientation" - - Parses "Harmonic frequencies" section - - Handles 3-5 modes per block format -- `parse_orca_vibrations()` - Parses ORCA .out files - - Extracts "CARTESIAN COORDINATES (ANGSTROEM)" - - Filters first 6 translation/rotation modes - - Parses "NORMAL MODES" (6 modes per block) -- `parse_molden_vibrations()` - Parses Molden .molden files - - Handles [Atoms], [FREQ], [INT], [FR-NORM-COORD] sections - - Supports Angstroms/Bohr unit conversion -- `parse_vibrations()` - Auto-detects format from file extension/content - -**Three Visualization Modes:** -- `create_displacement_arrows()` - Static 3D displacement arrows - - Uses Plotly Cone traces for vector field - - Filters small displacements by threshold - - Custom hover info with mode details -- `create_vibration_animation()` - Animated molecular vibration - - Sinusoidal motion: coords(t) = coords_eq + AΒ·sin(2Ο€t)Β·displacement - - Plotly frames with play/pause controls and slider - - Configurable frame count (5-50 frames) -- `create_heatmap_colored_figure()` - Heatmap coloring by displacement - - Colors atoms by normalized displacement magnitude - - Configurable colorscale (Reds, Blues, Viridis, etc.) - - Optional colorbar with legend -- `add_vibrations_to_figure()` - Main integration function - - Supports "arrows", "heatmap", or "both" display types - - Updates figure title with mode information - -### 2. Streamlit GUI Integration (`app.py`) - -**New " Vibration Settings" Section:** -- File uploader accepting .log, .out, .molden files -- Success message showing program type and mode count -- Mode selection dropdown with frequencies and IR intensities -- Display type radio buttons: - - Static arrows - - Animation - - Heatmap - - Arrows + Heatmap -- Interactive parameter controls: - - Amplitude slider (0.1 - 5.0) - - Arrow color picker - - Arrow size slider - - Heatmap colorscale selector - - Animation frames slider (5-50) -- Cached parsing with `@st.cache_resource` for performance -- Smart parameter visibility (shows/hides controls based on display type) -- Separate animation handling (creates new figure vs. overlaying on existing) - -### 3. Comprehensive Test Suite - -**21 New Tests (`test_vibrations.py`):** -- **Parser Tests** (7 tests): - - Gaussian parser with water.log fixture - - ORCA parser with water.out fixture - - Molden parser with water.molden fixture - - Auto-detection for all three formats - - Missing file error handling -- **Dataclass Tests** (3 tests): - - Mode retrieval by number - - Displacement magnitude calculation - - Invalid mode handling -- **Visualization Tests** (8 tests): - - Arrow trace generation - - Arrow filtering by threshold - - Heatmap coloring application - - Animation frame generation - - Integration with draw_3D_rep() - - Invalid mode error handling -- **Imaginary Frequency Tests** (1 test) -- **Missing IR Intensity Tests** (1 test) -- **End-to-End Tests** (1 test) - -**Test Fixtures:** -- `water_gaussian.log` - Sample Gaussian frequency calculation -- `water_orca.out` - Sample ORCA frequency calculation -- `water.molden` - Sample Molden format file - -**Coverage:** ~95% of vibrations.py module - -### 4. Documentation Updates - -**README.md:** -- Added vibrational visualization to Features list -- New "Vibrational mode visualization" section with: - - Static displacement arrows example - - Animated vibration example - - Heatmap coloring example - - Available parsers documentation - - Mode data access examples - -**CHANGELOG.md:** -- Comprehensive entry in [Unreleased] section documenting: - - Three file format parsers - - Three visualization modes - - New vibrations.py module - - Streamlit integration - - 21 new tests - - All modified files - -**CLAUDE.md (AI Context Document):** -- Updated Core Capabilities with vibration features -- Added vibrations.py to repository structure -- New section documenting vibrations.py module: - - Dataclasses - - Parser functions - - Visualization functions - - Key features -- Updated test coverage statistics (26 β†’ 47 tests) -- Updated Package Files table -- Updated Phase Status (Phase 4 complete, Phase 5 in progress) -- Added vibration enhancement suggestions to roadmap - -**docs/ROADMAP.md:** -- Updated Progress Summary table -- New "Phase 5: Feature Development - Vibrational Mode Visualization" section - - Complete task breakdown - - Files created/modified - - Key features - - Success criteria (all met ) - - Future enhancement suggestions - -**Additional Files:** -- `docs/VIBRATION_FEATURE_SUMMARY.md` - This comprehensive summary document - -### 5. Supporting Changes - -**atomProperties.py:** -- Added `symbol_to_number` dictionary mapping element symbols to atomic numbers -- Used by ORCA and Molden parsers for element symbol lookup - -**__init__.py:** -- Exported 8 new vibration functions: - - `VibrationalData`, `VibrationalMode` (dataclasses) - - `parse_gaussian_vibrations`, `parse_orca_vibrations`, `parse_molden_vibrations`, `parse_vibrations` (parsers) - - `create_displacement_arrows`, `create_vibration_animation`, `create_heatmap_colored_figure`, `add_vibrations_to_figure` (visualization) - -**conftest.py:** -- Added fixtures for vibration test files: - - `water_gaussian_log` - - `water_orca_out` - - `water_molden` - ---- - -## Implementation Statistics - -| Metric | Value | -|--------|-------| -| **Lines of Code Added** | ~1,200 | -| **New Module** | vibrations.py (~1030 lines) | -| **Tests Added** | 21 tests | -| **Total Tests** | 47 tests (26 β†’ 47) | -| **Test Coverage** | ~95% for vibrations module | -| **File Formats Supported** | 3 (Gaussian, ORCA, Molden) | -| **Visualization Modes** | 3 (Arrows, Animation, Heatmap) | -| **Documentation Updated** | 5 files | -| **Implementation Time** | Phases 1-5 completed incrementally | - ---- - -## Key Technical Achievements - -### 1. Robust Parsing Architecture -- **Auto-Detection**: Automatically determines file format from extension and content patterns -- **Error Handling**: Informative error messages for malformed files -- **Format-Specific Strategies**: Each parser optimized for its format's structure -- **Unit Conversion**: Molden parser handles Angstroms/Bohr conversion - -### 2. Three Visualization Approaches -- **Static Arrows**: Efficient Plotly Cone traces for instant visualization -- **Animation**: Smooth sinusoidal motion with interactive play/pause controls -- **Heatmap**: Displacement magnitude mapping with customizable colorscales - -### 3. Seamless Integration -- **Streamlit GUI**: Expandable section with full parameter control -- **Public API**: All functions exported for programmatic use -- **Caching**: File parsing cached for instant re-rendering -- **Error Recovery**: Graceful fallbacks with informative messages - -### 4. Comprehensive Testing -- **All Three Formats**: Test fixtures for Gaussian, ORCA, Molden -- **All Three Modes**: Tests for arrows, animation, heatmap -- **Edge Cases**: Invalid modes, missing intensities, imaginary frequencies -- **Integration**: End-to-end tests with draw_3D_rep() - ---- - -## Usage Examples - -### Quick Start (Static Arrows) -```python -from plotlymol3d import draw_3D_rep - -fig = draw_3D_rep( - smiles="O", # Water molecule - vibration_file="water_freq.log", - vibration_mode=1, - vibration_display="arrows", - vibration_amplitude=1.5 -) -fig.show() -``` - -### Advanced (Animation) -```python -from plotlymol3d import parse_vibrations, create_vibration_animation -from rdkit.Chem import MolFromSmiles, AddHs -from rdkit.Chem.AllChem import EmbedMolecule - -# Parse vibration data -vib_data = parse_vibrations("water_freq.log") - -# Create molecule -mol = MolFromSmiles("O") -mol = AddHs(mol) -EmbedMolecule(mol) - -# Generate animation -fig = create_vibration_animation( - vib_data=vib_data, - mode_number=1, - mol=mol, - amplitude=0.5, - n_frames=20, - mode="ball+stick" -) -fig.show() -``` - -### Heatmap Coloring -```python -from plotlymol3d import draw_3D_rep, parse_vibrations, add_vibrations_to_figure - -fig = draw_3D_rep(smiles="O", mode="ball+stick") -vib_data = parse_vibrations("water_freq.log") - -fig = add_vibrations_to_figure( - fig=fig, - vib_data=vib_data, - mode_number=1, - display_type="heatmap", - heatmap_colorscale="Reds" -) -fig.show() -``` - -### Accessing Mode Data -```python -from plotlymol3d import parse_vibrations - -vib_data = parse_vibrations("calculation.log") - -for mode in vib_data.modes: - print(f"Mode {mode.mode_number}: {mode.frequency:.1f} cm⁻¹") - if mode.ir_intensity: - print(f" IR Intensity: {mode.ir_intensity:.1f} km/mol") -``` - ---- - -## Suggested Next Steps - -### Immediate (Testing & Validation) -1. **Test with Real Data**: Use actual Gaussian/ORCA/Molden files from research calculations -2. **Performance Profiling**: Test with large molecules (>100 atoms, >50 modes) -3. **User Feedback**: Share with quantum chemistry researchers for feedback -4. **Documentation Review**: Ensure all examples work as documented - -### Short-Term (Enhancements) -1. **Example Notebooks**: Create Jupyter notebooks demonstrating: - - Basic vibration visualization workflow - - Comparing modes across different molecules - - Creating publication-quality figures - - Batch processing multiple calculations - -2. **IR Spectrum Viewer**: Interactive spectrum with clickable peaks - - Plot IR spectrum (intensity vs frequency) - - Click peak to display corresponding mode - - Highlight imaginary frequencies - - Export spectrum as PNG/SVG - -3. **Animation Export**: Save animations as GIF/MP4 - - Use imageio or moviepy for video generation - - Configurable resolution and frame rate - - Progress bar for rendering - -4. **Enhanced Error Messages**: Improve debugging for failed parses - - Show problematic section of file - - Suggest fixes for common issues - - Better handling of non-standard formats - -### Medium-Term (Additional Features) -1. **Additional File Formats**: - - ADF (Amsterdam Density Functional) - - Q-Chem - - NWChem - - GAMESS - - CP2K - -2. **Raman Intensity Support**: - - Parse Raman intensities where available - - Dual IR/Raman spectrum visualization - - Resonance Raman support - -3. **Transition State Visualization**: - - Reaction coordinate animation - - Forward/reverse reaction pathways - - IRC (Intrinsic Reaction Coordinate) visualization - -4. **Mode Combination Tools**: - - Linear combination of normal modes - - Mode mixing for complex vibrations - - Custom displacement vector generation - -5. **VCD/ROA Spectroscopy**: - - Vibrational Circular Dichroism (VCD) - - Raman Optical Activity (ROA) - - Chiral spectroscopy visualization - -### Long-Term (Advanced Features) -1. **Comparison Tools**: - - Side-by-side mode comparison - - Overlay multiple calculations - - Difference mapping between modes - -2. **Publication-Ready Exports**: - - High-resolution figures with labels - - Customizable styling (fonts, colors, sizes) - - Multi-panel layouts - - Vector format exports (SVG, EPS) - -3. **Integration with Other Tools**: - - Export to VMD format - - Import from other visualization tools - - API for external program integration - -4. **Performance Optimization**: - - Parallel processing for multiple modes - - WebGL optimization for large molecules - - Progressive loading for animations - - Memory-efficient file parsing - ---- - -## Modified Files Summary - -### New Files Created (5) -1. `src/plotlymol3d/vibrations.py` - Core vibration module (~1030 lines) -2. `tests/test_vibrations.py` - Test suite (~415 lines) -3. `tests/fixtures/water_gaussian.log` - Gaussian test fixture (~100 lines) -4. `tests/fixtures/water_orca.out` - ORCA test fixture (~63 lines) -5. `tests/fixtures/water.molden` - Molden test fixture (~40 lines) - -### Files Modified (9) -1. `src/plotlymol3d/__init__.py` - Added vibration exports (~11 lines added) -2. `src/plotlymol3d/atomProperties.py` - Added symbol_to_number mapping (~1 line added) -3. `src/plotlymol3d/app.py` - Added vibration settings section (~100 lines added) -4. `tests/conftest.py` - Added vibration fixtures (~17 lines added) -5. `README.md` - Added vibration documentation (~87 lines added) -6. `CHANGELOG.md` - Documented vibration feature (~18 lines added) -7. `CLAUDE.md` - Updated with vibration module docs (~150 lines added) -8. `docs/ROADMAP.md` - Added Phase 5 section (~90 lines added) -9. `docs/VIBRATION_FEATURE_SUMMARY.md` - This summary document (created) - ---- - -## Success Criteria - All Met - -- [x] Parse all three file formats correctly (Gaussian, ORCA, Molden) -- [x] Auto-detect format from file extension and content -- [x] All three visualization modes functional (arrows, animation, heatmap) -- [x] Streamlit UI integration with interactive controls -- [x] Cached parsing for performance -- [x] Comprehensive test coverage (~95% for vibrations module) -- [x] All 47 tests passing (21 new vibration tests) -- [x] Complete documentation with examples -- [x] Public API exports for programmatic use -- [x] Error handling with informative messages - ---- - -## Lessons Learned - -### Technical Insights -1. **Regex Patterns**: Required precise anchors (`\s*$`) to distinguish header lines from data lines in ORCA parser -2. **Coordinate Matching**: Needed generous threshold (1.0 Γ…) to handle differences between SMILES-generated and QM-optimized coordinates -3. **Animation Performance**: Lower resolution (16 vs 32) significantly improves animation rendering speed -4. **Plotly Colorbar**: Required nested dict format for title configuration - -### Implementation Strategy -1. **Incremental Phases**: Breaking into 5 phases enabled systematic testing at each stage -2. **Test-Driven Development**: Writing tests first revealed parser edge cases early -3. **Fixture-Based Testing**: Small, focused test fixtures (water molecule) kept tests fast -4. **Auto-Detection**: Worth the extra complexity for user experience - -### Documentation Best Practices -1. **Multiple Audiences**: README for users, CLAUDE.md for AI assistants, ROADMAP.md for developers -2. **Code Examples**: Executable examples in documentation prevent confusion -3. **Architecture Diagrams**: Data structure descriptions help understand flow -4. **Changelog Discipline**: Detailed changelog entries valuable for tracking changes - ---- - -## Final Status - -**Feature Status:** **PRODUCTION READY** - -The molecular vibration visualization system is fully implemented, tested, documented, and integrated into plotlyMol. Users can now: -- Parse vibrational data from three quantum chemistry programs -- Visualize vibrations using three complementary modes -- Interact with vibrations via Streamlit GUI -- Programmatically access all functionality via public API -- Extend functionality for additional formats or visualization modes - -**Next Recommended Action:** Begin testing with real quantum chemistry calculations and gather user feedback for refinement. - ---- - -**Document Prepared:** 2026-02-03 -**Status:** Complete and Verified \ No newline at end of file diff --git a/docs/about.md b/docs/about.md index 21b7a97..3fd8109 100644 --- a/docs/about.md +++ b/docs/about.md @@ -192,7 +192,7 @@ Thanks to the developers of: ## License -plotlyMol is released under the [MIT License](https://github.com/NCCU-Schultz-Lab/plotlyMol/blob/main/LICENSE). +plotlyMol is released under the [MIT License](https://github.com/The-Schultz-Lab/plotlyMol/blob/main/LICENSE). ``` MIT License @@ -212,8 +212,8 @@ copies or substantial portions of the Software. ## Contact -- **GitHub Issues**: [Report issues or request features](https://github.com/NCCU-Schultz-Lab/plotlyMol/issues) -- **GitHub Discussions**: [Ask questions or share ideas](https://github.com/NCCU-Schultz-Lab/plotlyMol/discussions) +- **GitHub Issues**: [Report issues or request features](https://github.com/The-Schultz-Lab/plotlyMol/issues) +- **GitHub Discussions**: [Ask questions or share ideas](https://github.com/The-Schultz-Lab/plotlyMol/discussions) - **Email**: jonathanschultzNU@users.noreply.github.com ## Citation @@ -225,7 +225,7 @@ If you use plotlyMol in your research, please cite: title = {plotlyMol: Interactive 3D Molecular Visualizations}, author = {Schultz, Jonathan and Lear, Benjamin}, year = {2026}, - url = {https://github.com/NCCU-Schultz-Lab/plotlyMol}, + url = {https://github.com/The-Schultz-Lab/plotlyMol}, note = {Version 0.1.0} } ``` diff --git a/docs/assets/dash-example.webm b/docs/assets/dash-example.webm new file mode 100644 index 0000000..6458bbf Binary files /dev/null and b/docs/assets/dash-example.webm differ diff --git a/docs/assets/demo.webm b/docs/assets/demo.webm new file mode 100644 index 0000000..fc61970 Binary files /dev/null and b/docs/assets/demo.webm differ diff --git a/docs/contributing.md b/docs/contributing.md index b763aae..ddce0a1 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -6,7 +6,7 @@ Thank you for your interest in contributing to plotlyMol! This guide will help y ### Report Bugs -Found a bug? Please [open an issue](https://github.com/NCCU-Schultz-Lab/plotlyMol/issues/new) with: +Found a bug? Please [open an issue](https://github.com/The-Schultz-Lab/plotlyMol/issues/new) with: - Clear description of the problem - Steps to reproduce @@ -16,7 +16,7 @@ Found a bug? Please [open an issue](https://github.com/NCCU-Schultz-Lab/plotlyMo ### Suggest Features -Have an idea? We'd love to hear it! [Open an issue](https://github.com/NCCU-Schultz-Lab/plotlyMol/issues/new) describing: +Have an idea? We'd love to hear it! [Open an issue](https://github.com/The-Schultz-Lab/plotlyMol/issues/new) describing: - The feature and its use case - Why it would be valuable @@ -45,7 +45,7 @@ git clone https://github.com/YOUR_USERNAME/plotlyMol.git cd plotlyMol # Add upstream remote -git remote add upstream https://github.com/NCCU-Schultz-Lab/plotlyMol.git +git remote add upstream https://github.com/The-Schultz-Lab/plotlyMol.git ``` ### 2. Create Virtual Environment @@ -411,8 +411,8 @@ Contributors are recognized in: Have questions about contributing? -- Open a [Discussion](https://github.com/NCCU-Schultz-Lab/plotlyMol/discussions) +- Open a [Discussion](https://github.com/The-Schultz-Lab/plotlyMol/discussions) - Check existing documentation - Reach out to maintainers -Thank you for contributing to plotlyMol! \ No newline at end of file +Thank you for contributing to plotlyMol! diff --git a/docs/examples/index.md b/docs/examples/index.md index 1c19668..9eb3fa9 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -373,7 +373,7 @@ python examples/demo_visualizations.py ## Download Examples -All example code is available in the [GitHub repository](https://github.com/NCCU-Schultz-Lab/plotlyMol/tree/main/examples). +All example code is available in the [GitHub repository](https://github.com/The-Schultz-Lab/plotlyMol/tree/main/examples). ## Contributing Examples diff --git a/docs/index.md b/docs/index.md index e06186a..4563371 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,11 +1,17 @@ +
+ # plotlyMol -

- plotlyMol Logo -

+Interactive 3D Molecular Visualization + +
+ + -![Tests](https://github.com/NCCU-Schultz-Lab/plotlyMol/actions/workflows/test.yml/badge.svg) -![Lint](https://github.com/NCCU-Schultz-Lab/plotlyMol/actions/workflows/lint.yml/badge.svg) +![Tests](https://github.com/The-Schultz-Lab/plotlyMol/actions/workflows/test.yml/badge.svg) +![Lint](https://github.com/The-Schultz-Lab/plotlyMol/actions/workflows/lint.yml/badge.svg) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) **Interactive 3D molecular visualizations with Plotly** @@ -19,7 +25,7 @@ plotlyMol is a Python package for creating beautiful, interactive 3D molecular v - **SMILES-to-3D**: Automatic 3D coordinate generation from SMILES via RDKit - **Orbital Visualization**: Isosurface rendering from quantum chemistry cube files - **Bond Order Display**: Visual differentiation of single, double, triple, and aromatic bonds -- **Interactive GUI**: Streamlit-based web interface for easy exploration +- **Interactive GUI**: Dash-based web interface for easy exploration - **Export Options**: Save as interactive HTML or static PNG images ## Quick Example @@ -94,8 +100,8 @@ Ready to visualize molecules? Check out the [Installation](installation.md) guid ## Community & Support -- **GitHub Repository**: [NCCU-Schultz-Lab/plotlyMol](https://github.com/NCCU-Schultz-Lab/plotlyMol) -- **Issue Tracker**: [Report bugs or request features](https://github.com/NCCU-Schultz-Lab/plotlyMol/issues) +- **GitHub Repository**: [The-Schultz-Lab/plotlyMol](https://github.com/The-Schultz-Lab/plotlyMol) +- **Issue Tracker**: [Report bugs or request features](https://github.com/The-Schultz-Lab/plotlyMol/issues) - **License**: MIT License ## Citation @@ -107,6 +113,6 @@ If you use plotlyMol in your research, please cite: title = {plotlyMol: Interactive 3D Molecular Visualizations}, author = {Schultz, Jonathan and Lear, Benjamin}, year = {2026}, - url = {https://github.com/NCCU-Schultz-Lab/plotlyMol} + url = {https://github.com/The-Schultz-Lab/plotlyMol} } ``` diff --git a/docs/installation.md b/docs/installation.md index 9de8093..8690561 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -10,7 +10,7 @@ Currently, plotlyMol is best installed from source: ```bash # Clone the repository -git clone https://github.com/NCCU-Schultz-Lab/plotlyMol.git +git clone https://github.com/The-Schultz-Lab/plotlyMol.git cd plotlyMol # Create a virtual environment (recommended) diff --git a/docs/quickstart.md b/docs/quickstart.md index 62dbc64..7ea14c8 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -206,23 +206,25 @@ fig.show() ## Using the GUI -Launch the interactive Streamlit app: + -```bash -streamlit run examples/gui_app.py -``` +Launch the interactive Dash app: -Or on Windows, use the provided scripts: ```bash -launch_app.bat # Launch GUI -stop_app.bat # Stop GUI +python examples/gui_app.py ``` +Or on Windows, double-click `launch_app.bat`. + The GUI provides: -- Input via SMILES, file upload, or random molecules -- Real-time parameter adjustment -- Orbital visualization controls -- Export options + +- Molecule search by name via PubChem +- Input via SMILES string or built-in sample library +- Visualization mode selection (ball+stick, stick, VDW) +- Lighting presets +- Formula and atom/bond count display ## Complete Example diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..b3da5f2 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,25 @@ +.hero-title { + text-align: center; + padding: 2.5rem 0 0.25rem; +} + +.hero-title h1 { + font-size: 3.5rem; + font-weight: 800; + letter-spacing: -2px; + line-height: 1.1; + margin-bottom: 0.4rem; + background: linear-gradient(135deg, var(--md-primary-fg-color), var(--md-accent-fg-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-title p { + font-size: 1.2rem; + font-weight: 400; + letter-spacing: 0.08em; + opacity: 0.65; + margin: 0; + text-transform: uppercase; +} diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index 19027a0..ed0ae37 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -115,8 +115,8 @@ See the [Examples](../examples/index.md) page for a gallery of molecular visuali - Check the [API Reference](../api/index.md) - Read the [User Guide](../user-guide/basic-usage.md) -- Ask in [GitHub Discussions](https://github.com/NCCU-Schultz-Lab/plotlyMol/discussions) -- Report issues on [GitHub](https://github.com/NCCU-Schultz-Lab/plotlyMol/issues) +- Ask in [GitHub Discussions](https://github.com/The-Schultz-Lab/plotlyMol/discussions) +- Report issues on [GitHub](https://github.com/The-Schultz-Lab/plotlyMol/issues) ## Contributing Tutorials @@ -131,4 +131,4 @@ See the [Contributing Guide](../contributing.md) for details on how to: --- -**Note**: This tutorials section is actively being developed. Check the [GitHub repository](https://github.com/NCCU-Schultz-Lab/plotlyMol) for the latest additions. +**Note**: This tutorials section is actively being developed. Check the [GitHub repository](https://github.com/The-Schultz-Lab/plotlyMol) for the latest additions. diff --git a/examples/gui_app.py b/examples/gui_app.py index 71e3020..d7e3ee7 100644 --- a/examples/gui_app.py +++ b/examples/gui_app.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 """ -Streamlit GUI for plotlyMol3D - Visual Testing & Demo App. +Dash GUI for plotlyMol3D - Interactive 3D Molecular Viewer. Run with: - streamlit run examples/gui_app.py + python examples/gui_app.py Requirements: - pip install streamlit + pip install plotlymol3d[gui] """ -from pathlib import Path + import sys +from pathlib import Path try: from plotlymol3d.app import main diff --git a/examples/render_demo_webm.py b/examples/render_demo_webm.py new file mode 100644 index 0000000..c175e4b --- /dev/null +++ b/examples/render_demo_webm.py @@ -0,0 +1,125 @@ +""" +Generate a rotating-molecule demo animation and save as docs/assets/demo.webm. + +Usage: + python examples/render_demo_webm.py +""" + +from __future__ import annotations + +import math +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from plotly.subplots import make_subplots # noqa: E402 + +from plotlymol3d import ( # noqa: E402 + draw_3D_mol, + format_figure, + format_lighting, + smiles_to_rdkitmol, +) + +# --------------------------------------------------------------------------- +# Config +# --------------------------------------------------------------------------- +SMILES = "CN1C=NC2=C1C(=O)N(C(=O)N2C)C" # caffeine +MODE = "ball+stick" +RESOLUTION = 64 + +N_FRAMES = 120 # one full 360Β° rotation (smoother at 30fps = 4s) +FPS = 30 +CAMERA_DISTANCE = 1.8 # eye distance from origin +CAMERA_ELEVATION = 0.4 # z component of eye (slight upward tilt) +WIDTH = 1920 +HEIGHT = 1080 + +OUT_PATH = Path(__file__).resolve().parents[1] / "docs" / "assets" / "demo.webm" +FFMPEG = r"C:\Users\schul\ffmpeg-7.1.1-full_build\bin\ffmpeg.exe" + +# --------------------------------------------------------------------------- +# Build base figure +# --------------------------------------------------------------------------- +print("Building molecule figure...") +mol = smiles_to_rdkitmol(SMILES) +fig = make_subplots() +fig = format_figure(fig) +fig = draw_3D_mol(fig, mol, mode=MODE, resolution=RESOLUTION) +fig = format_lighting(fig) +fig.update_layout( + width=WIDTH, + height=HEIGHT, + margin={"l": 0, "r": 0, "t": 0, "b": 0}, + paper_bgcolor="white", + scene={ + "bgcolor": "white", + "xaxis": { + "showticklabels": False, + "showgrid": False, + "zeroline": False, + "visible": False, + }, + "yaxis": { + "showticklabels": False, + "showgrid": False, + "zeroline": False, + "visible": False, + }, + "zaxis": { + "showticklabels": False, + "showgrid": False, + "zeroline": False, + "visible": False, + }, + }, +) + +# --------------------------------------------------------------------------- +# Render frames +# --------------------------------------------------------------------------- +with tempfile.TemporaryDirectory() as tmpdir: + print(f"Rendering {N_FRAMES} frames...") + for i in range(N_FRAMES): + angle = 2 * math.pi * i / N_FRAMES + eye_x = CAMERA_DISTANCE * math.cos(angle) + eye_y = CAMERA_DISTANCE * math.sin(angle) + eye_z = CAMERA_ELEVATION + fig.update_layout(scene_camera={"eye": {"x": eye_x, "y": eye_y, "z": eye_z}}) + frame_path = os.path.join(tmpdir, f"frame_{i:04d}.png") + fig.write_image(frame_path, format="png") + if (i + 1) % 10 == 0: + print(f" {i + 1}/{N_FRAMES}") + + # ----------------------------------------------------------------------- + # Encode with ffmpeg + # ----------------------------------------------------------------------- + OUT_PATH.parent.mkdir(parents=True, exist_ok=True) + print(f"Encoding {OUT_PATH} ...") + cmd = [ + FFMPEG, + "-y", + "-framerate", + str(FPS), + "-i", + os.path.join(tmpdir, "frame_%04d.png"), + "-c:v", + "libvpx-vp9", + "-b:v", + "0", + "-crf", + "24", + "-pix_fmt", + "yuva420p", # supports transparency if background changes + str(OUT_PATH), + ] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print("ffmpeg error:\n", result.stderr) + sys.exit(1) + +print(f"Done -> {OUT_PATH}") diff --git a/launch_app.bat b/launch_app.bat index cf05e81..90be21a 100644 --- a/launch_app.bat +++ b/launch_app.bat @@ -3,9 +3,9 @@ setlocal set "ROOT=%~dp0" cd /d "%ROOT%" -set "VENV_PY=%ROOT%.venv\Scripts\python.exe" -if exist "%VENV_PY%" ( - "%VENV_PY%" -m streamlit run examples\gui_app.py +set "CONDA_PY=C:\Users\schul\miniconda3\envs\plotlymol\python.exe" +if exist "%CONDA_PY%" ( + "%CONDA_PY%" examples\gui_app.py ) else ( - python -m streamlit run examples\gui_app.py + python examples\gui_app.py ) diff --git a/launch_app.command b/launch_app.command new file mode 100644 index 0000000..70db238 --- /dev/null +++ b/launch_app.command @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# launch_app.command β€” macOS launcher for plotlyMol3D +# +# Double-click this file in Finder to launch the app, or run: +# bash launch_app.command +# +# To make it double-clickable, run once in Terminal: +# chmod +x launch_app.command + +# Change to the directory containing this script (works when double-clicked) +cd "$(dirname "$0")" + +# Use the virtual environment's Python if it exists, otherwise fall back to +# the system/conda Python on $PATH +VENV_PY=".venv/bin/python" +if [ -f "$VENV_PY" ]; then + "$VENV_PY" examples/gui_app.py +else + python3 examples/gui_app.py +fi diff --git a/mkdocs.yml b/mkdocs.yml index 1d6c96d..d8fea6a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,10 +1,10 @@ site_name: plotlyMol Documentation site_description: Interactive 3D molecular visualizations with Plotly site_author: Jonathan Schultz & Benjamin Lear -site_url: https://nccu-schultz-lab.github.io/plotlyMol +site_url: https://the-schultz-lab.github.io/plotlyMol -repo_name: NCCU-Schultz-Lab/plotlyMol -repo_url: https://github.com/NCCU-Schultz-Lab/plotlyMol +repo_name: The-Schultz-Lab/plotlyMol +repo_url: https://github.com/The-Schultz-Lab/plotlyMol edit_uri: edit/main/docs/ theme: @@ -46,11 +46,9 @@ extra_css: extra: social: - icon: fontawesome/brands/github - link: https://github.com/NCCU-Schultz-Lab/plotlyMol + link: https://github.com/The-Schultz-Lab/plotlyMol - icon: fontawesome/brands/python link: https://pypi.org/project/plotlymol/ - version: - provider: mike plugins: - search: diff --git a/pyproject.toml b/pyproject.toml index c0e4653..0271790 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,14 +44,15 @@ dev = [ "pre-commit>=3.0.0", ] gui = [ - "streamlit>=1.30.0", + "dash>=2.14.0", + "dash-bootstrap-components>=1.5.0", ] [project.urls] -Homepage = "https://github.com/NCCU-Schultz-Lab/plotlyMol" -Repository = "https://github.com/NCCU-Schultz-Lab/plotlyMol" -Issues = "https://github.com/NCCU-Schultz-Lab/plotlyMol/issues" -Documentation = "https://nccu-schultz-lab.github.io/plotlyMol" +Homepage = "https://github.com/The-Schultz-Lab/plotlyMol" +Repository = "https://github.com/The-Schultz-Lab/plotlyMol" +Issues = "https://github.com/The-Schultz-Lab/plotlyMol/issues" +Documentation = "https://the-schultz-lab.github.io/plotlyMol" [tool.setuptools] packages = ["plotlymol3d"] diff --git a/src/plotlymol3d/__init__.py b/src/plotlymol3d/__init__.py index 7613d55..0dc286b 100644 --- a/src/plotlymol3d/__init__.py +++ b/src/plotlymol3d/__init__.py @@ -1,5 +1,8 @@ from .atomProperties import * # noqa: F403 from .plotlyMol3D import * # noqa: F403 +from .plotlyMol3D import ( + create_trajectory_animation as create_trajectory_animation, +) from .vibrations import ( VibrationalData as VibrationalData, ) diff --git a/src/plotlymol3d/app.py b/src/plotlymol3d/app.py index 1eedba5..762d033 100644 --- a/src/plotlymol3d/app.py +++ b/src/plotlymol3d/app.py @@ -1,949 +1,453 @@ """ -Streamlit GUI for plotlyMol3D - Visual Testing & Demo App. +Dash GUI for plotlyMol3D β€” Interactive 3D Molecular Visualizer. Run with: - streamlit run examples/gui_app.py + python examples/gui_app.py + or + python src/plotlymol3d/app.py """ from __future__ import annotations -import json import os -import tempfile -from pathlib import Path - -import plotly.io as pio -import streamlit as st +import signal +import webbrowser +from threading import Timer +from typing import Any + +import dash_bootstrap_components as dbc +import requests +from dash import Dash, Input, Output, State, dcc, html from plotly.subplots import make_subplots -from rdkit import Chem - -from plotlymol3d import ( - add_vibrations_to_figure, - create_vibration_animation, - cubefile_to_xyzblock, - draw_3D_mol, - format_figure, - format_lighting, - parse_vibrations, - smiles_to_rdkitmol, - xyzblock_to_rdkitmol, -) -from plotlymol3d.cube import draw_cube_orbitals - -CONFIG_PATH = Path(__file__).resolve().parents[2] / ".plotlymol3d_config.json" - - -@st.cache_resource(show_spinner=False) -def cached_smiles_to_mol(smiles: str): - return smiles_to_rdkitmol(smiles) - - -@st.cache_resource(show_spinner=False) -def cached_xyzblock_to_mol(xyzblock: str, charge: int = 0): - return xyzblock_to_rdkitmol(xyzblock, charge=charge) - -@st.cache_resource(show_spinner=False) -def cached_molblock_to_mol(molblock: str): - return Chem.MolFromMolBlock(molblock) - - -@st.cache_data(show_spinner=False) -def cached_cube_bytes_to_xyzblock(cube_bytes: bytes): - temp_path = None - try: - with tempfile.NamedTemporaryFile(delete=False, suffix=".cube") as tmp: - tmp.write(cube_bytes) - temp_path = tmp.name - return cubefile_to_xyzblock(temp_path) - finally: - if temp_path and os.path.exists(temp_path): - os.unlink(temp_path) - - -@st.cache_resource(show_spinner=False) -def cached_parse_vibrations(vib_bytes: bytes, filename: str): - """Parse vibration file from bytes.""" - temp_path = None - try: - # Determine file extension - suffix = Path(filename).suffix - with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: - tmp.write(vib_bytes) - temp_path = tmp.name - return parse_vibrations(temp_path) - finally: - if temp_path and os.path.exists(temp_path): - os.unlink(temp_path) - - -def get_cached_vibration_animation( - vib_data_id: str, - mode_number: int, - mol_pkl: bytes, - amplitude: float, - n_frames: int, - mode: str, - resolution: int, - ambient: float, - diffuse: float, - specular: float, - roughness: float, - fresnel: float, -): - """Get or create vibration animation with progress feedback. - - Uses manual caching in session_state to enable progress bar during generation. - - Args: - vib_data_id: Unique identifier for the vibration data (source filename) - mode_number: Vibrational mode to animate - mol_pkl: Pickled RDKit molecule - amplitude: Displacement amplitude - n_frames: Number of animation frames - mode: Visualization mode - resolution: Sphere resolution - ambient: Lighting ambient component - diffuse: Lighting diffuse component - specular: Lighting specular component - roughness: Lighting roughness - fresnel: Lighting fresnel - - Returns: - Animated Plotly figure - """ - import hashlib - import pickle - - # Create cache key from parameters - cache_key_data = ( - vib_data_id, - mode_number, - amplitude, - n_frames, - mode, - resolution, - ambient, - diffuse, - specular, - roughness, - fresnel, +from plotlymol3d import draw_3D_mol, format_figure, format_lighting, smiles_to_rdkitmol + +_HOST = "127.0.0.1" +_PORT = 8050 +_VIEWER_H = "calc(100vh - 90px)" + +_PUBCHEM_BASE = "https://pubchem.ncbi.nlm.nih.gov/rest/pug" +_TIMEOUT = 10 + +_SAMPLE_MOLECULES: list[dict[str, str]] = [ + {"label": "β€” choose a sample β€”", "value": ""}, + {"label": "Water (H2O)", "value": "O"}, + {"label": "Ethanol", "value": "CCO"}, + {"label": "Benzene", "value": "c1ccccc1"}, + {"label": "Caffeine", "value": "CN1C=NC2=C1C(=O)N(C(=O)N2C)C"}, + {"label": "Aspirin", "value": "CC(=O)OC1=CC=CC=C1C(=O)O"}, + {"label": "Naphthalene", "value": "c1ccc2ccccc2c1"}, + {"label": "Glucose", "value": "OC[C@H]1OC(O)[C@H](O)[C@@H](O)[C@@H]1O"}, + {"label": "Alanine", "value": "CC(N)C(=O)O"}, + {"label": "Cyclohexane", "value": "C1CCCCC1"}, +] + +_MODES = [ + {"label": "Ball + Stick", "value": "ball+stick"}, + {"label": "Ball", "value": "ball"}, + {"label": "Stick", "value": "stick"}, + {"label": "Van der Waals", "value": "vdw"}, +] + +_LIGHTING_PRESETS: dict[str, dict] = { + "soft": {"ambient": 0.4, "diffuse": 0.8, "specular": 0.1, "roughness": 0.8}, + "default": {"ambient": 0.0, "diffuse": 1.0, "specular": 0.0, "roughness": 1.0}, + "bright": {"ambient": 0.5, "diffuse": 0.8, "specular": 0.3, "roughness": 0.5}, + "metallic": {"ambient": 0.2, "diffuse": 0.7, "specular": 1.0, "roughness": 0.1}, + "dramatic": {"ambient": 0.0, "diffuse": 1.0, "specular": 0.6, "roughness": 0.2}, +} + +_LIGHTING_OPTIONS = [ + {"label": "Soft", "value": "soft"}, + {"label": "Default", "value": "default"}, + {"label": "Bright", "value": "bright"}, + {"label": "Metallic", "value": "metallic"}, + {"label": "Dramatic", "value": "dramatic"}, +] + + +def _pubchem_name_to_smiles(name: str) -> tuple[str, int]: + """Return (isomeric_smiles, cid) for a compound name via PubChem REST API.""" + resp = requests.get( + f"{_PUBCHEM_BASE}/compound/name/{requests.utils.quote(name)}/cids/JSON", + timeout=_TIMEOUT, ) - cache_key = f"vib_anim_{hashlib.md5(str(cache_key_data).encode()).hexdigest()}" - - # Check if already cached - if "animation_cache" not in st.session_state: - st.session_state.animation_cache = {} - - if cache_key in st.session_state.animation_cache: - st.caption("Loading cached animation...") - return st.session_state.animation_cache[cache_key] - - # Not cached - generate with progress bar - vib_data = st.session_state.get("vib_data") - if vib_data is None: - raise ValueError("Vibration data not found in session state") - - mol = pickle.loads(mol_pkl) - - # Create progress bar - progress_bar = st.progress(0.0) - status_text = st.empty() - - def update_progress(current, total): - progress = current / total - progress_bar.progress(progress) - status_text.text(f"Generating frame {current}/{total}...") - - # Generate animation with progress callback - fig = create_vibration_animation( - vib_data=vib_data, - mode_number=mode_number, - mol=mol, - amplitude=amplitude, - n_frames=n_frames, - mode=mode, - resolution=resolution, - progress_callback=update_progress, - ) - - # Apply lighting - fig = format_lighting( - fig, - ambient=ambient, - diffuse=diffuse, - specular=specular, - roughness=roughness, - fresnel=fresnel, - ) - - # Clear progress indicators - progress_bar.empty() - status_text.empty() - - # Cache the result - st.session_state.animation_cache[cache_key] = fig - - return fig + if resp.status_code == 404: + raise ValueError( + f'"{name}" was not found in PubChem. Check spelling or try a SMILES string.' + ) + resp.raise_for_status() + cid = resp.json()["IdentifierList"]["CID"][0] + for prop in ("IsomericSMILES", "CanonicalSMILES"): + prop_resp = requests.get( + f"{_PUBCHEM_BASE}/compound/cid/{cid}/property/{prop}/JSON", + timeout=_TIMEOUT, + ) + if prop_resp.ok: + record = ( + prop_resp.json().get("PropertyTable", {}).get("Properties", [{}])[0] + ) + # PubChem may return the value under a different key than requested + smiles = next( + (v for k, v in record.items() if k != "CID" and isinstance(v, str)), + None, + ) + if smiles: + return smiles, cid + raise ValueError(f'Could not retrieve a SMILES string for "{name}" (CID {cid}).') -@st.cache_data(show_spinner=False) -def cached_figure_from_mol_pickle( - mol_pkl: bytes, - mode: str, - resolution: int, - ambient: float, - diffuse: float, - specular: float, - roughness: float, - fresnel: float, -): - """Cache figures using pickled mol to preserve hydrogens.""" - import pickle - - mol = pickle.loads(mol_pkl) - return create_figure_from_mol( - mol, - mode, - resolution, - ambient, - diffuse, - specular, - roughness, - fresnel, - ) +def _empty_figure(): + import plotly.graph_objects as go -@st.cache_data(show_spinner=False) -def cached_image_bytes( - mol_pkl: bytes, - mode: str, - resolution: int, - ambient: float, - diffuse: float, - specular: float, - roughness: float, - fresnel: float, - width: int, - height: int, - fmt: str, -): - fig = cached_figure_from_mol_pickle( - mol_pkl, - mode, - resolution, - ambient, - diffuse, - specular, - roughness, - fresnel, - ) - fig.update_layout(width=width, height=height) - return pio.to_image(fig, format=fmt) - - -def create_figure_from_mol( - rdkitmol, - mode, - resolution, - ambient, - diffuse, - specular, - roughness, - fresnel, -): - """Create a Plotly figure from an RDKit molecule.""" - fig = make_subplots() - fig = format_figure(fig) # Transparent background to match theme - fig = draw_3D_mol(fig, rdkitmol, mode=mode, resolution=resolution) - fig = format_lighting( - fig, - ambient=ambient, - diffuse=diffuse, - specular=specular, - roughness=roughness, - fresnel=fresnel, - ) + fig = go.Figure() fig.update_layout( - height=600, + paper_bgcolor="rgba(0,0,0,0)", + autosize=True, + scene={ + "bgcolor": "#f8f9fa", + "xaxis": {"showticklabels": False, "showgrid": False, "zeroline": False}, + "yaxis": {"showticklabels": False, "showgrid": False, "zeroline": False}, + "zaxis": {"showticklabels": False, "showgrid": False, "zeroline": False}, + }, margin={"l": 0, "r": 0, "t": 30, "b": 0}, ) return fig -def display_molecule_info(rdkitmol): - """Display molecule information in sidebar.""" - st.sidebar.markdown("---") - st.sidebar.markdown("### Molecule Info") - st.sidebar.write(f"**Atoms:** {rdkitmol.GetNumAtoms()}") - st.sidebar.write(f"**Bonds:** {rdkitmol.GetNumBonds()}") - - atom_counts = {} - for atom in rdkitmol.GetAtoms(): - symbol = atom.GetSymbol() - atom_counts[symbol] = atom_counts.get(symbol, 0) + 1 - +def _mol_info_children(mol) -> list: + atom_counts: dict[str, int] = {} + for atom in mol.GetAtoms(): + sym = atom.GetSymbol() + atom_counts[sym] = atom_counts.get(sym, 0) + 1 formula = "".join( f"{sym}{cnt if cnt > 1 else ''}" for sym, cnt in sorted(atom_counts.items()) ) - st.sidebar.write(f"**Formula:** {formula}") + return [ + html.Div(f"Formula: {formula}"), + html.Div(f"Atoms: {mol.GetNumAtoms()}"), + html.Div(f"Bonds: {mol.GetNumBonds()}"), + ] -def main(): - """Run the Streamlit app.""" - st.set_page_config( - page_title="plotlyMol3D Viewer", - page_icon="βš›", - layout="wide", - ) - - st.sidebar.title("plotlyMol3D") - st.sidebar.markdown("**Interactive 3D Molecular Visualization**") +def _render(smiles: str, mode: str, lighting: str = "soft") -> tuple: + """Render a SMILES string and return callback outputs for the viewer.""" + _show = {"display": "block"} + _hide = {"display": "none"} + _placeholder_visible = {"height": _VIEWER_H, "fontSize": "1.1rem"} - input_method = st.sidebar.radio( - "Input Method", - ["SMILES", "MOL File", "XYZ File", "Cube File", "Sample Molecules"], - index=0, - ) + def _err(msg: str) -> tuple[Any, ...]: + return _empty_figure(), "", msg, _hide, _placeholder_visible - mode = st.sidebar.selectbox( - "Visualization Mode", - ["ball+stick", "ball", "vdw", "stick"], - index=0, - help="ball+stick: atoms and bonds | ball: atoms only | vdw: space-filling | stick: thin atoms", + if not smiles or not smiles.strip(): + return _err("Enter a SMILES string, search by name, or choose a sample.") + try: + mol = smiles_to_rdkitmol(smiles.strip()) + except Exception as exc: + return _err(f"Invalid SMILES: {exc}") + try: + fig = make_subplots() + fig = format_figure(fig) + fig = draw_3D_mol(fig, mol, mode=mode, resolution=32) + fig = format_lighting( + fig, **_LIGHTING_PRESETS.get(lighting, _LIGHTING_PRESETS["soft"]) + ) + fig.update_layout(autosize=True, margin={"l": 0, "r": 0, "t": 30, "b": 0}) + except Exception as exc: + return _err(f"Rendering error: {exc}") + + return fig, _mol_info_children(mol), "", _show, _hide + + +def _build_layout() -> dbc.Container: + return dbc.Container( + [ + # -- Header --------------------------------------------------------- + dbc.Row( + dbc.Col( + [ + html.H4("plotlyMol3D", className="mb-0 text-primary"), + html.P( + "Interactive 3D Molecular Visualization", + className="text-muted mb-0 small", + ), + ], + className="py-2 border-bottom mb-2", + ) + ), + # -- Body ----------------------------------------------------------- + dbc.Row( + [ + # Controls panel -------------------------------------------- + dbc.Col( + dbc.Card( + dbc.CardBody( + [ + # PubChem search ------------------------- + html.H6("Search PubChem", className="card-title"), + dbc.Label( + "Molecule name", + html_for="pubchem-input", + className="small", + ), + dbc.InputGroup( + [ + dbc.Input( + id="pubchem-input", + placeholder="e.g. aspirin, caffeine…", + type="text", + debounce=False, + ), + dbc.Button( + "Search", + id="pubchem-btn", + color="info", + n_clicks=0, + ), + ], + className="mb-1", + ), + html.Div( + id="pubchem-status", + className="text-muted small mb-3", + ), + html.Hr(), + # SMILES / sample ------------------------- + html.H6( + "Or enter directly", className="card-title" + ), + dbc.Label( + "SMILES string", + html_for="smiles-input", + className="small", + ), + dbc.Input( + id="smiles-input", + placeholder="e.g. CCO for ethanol", + type="text", + debounce=False, + className="mb-2", + ), + dbc.Label( + "Or choose a sample", + html_for="sample-select", + className="small", + ), + dbc.Select( + id="sample-select", + options=_SAMPLE_MOLECULES, + value="", + className="mb-3", + ), + html.Hr(), + # Display options ------------------------- + html.H6("Display Options", className="card-title"), + dbc.Label( + "Visualization mode", + html_for="mode-select", + className="small", + ), + dbc.Select( + id="mode-select", + options=_MODES, + value="ball+stick", + className="mb-2", + ), + dbc.Label( + "Lighting", + html_for="lighting-select", + className="small", + ), + dbc.Select( + id="lighting-select", + options=_LIGHTING_OPTIONS, + value="soft", + className="mb-3", + ), + dbc.Button( + "Visualize", + id="visualize-btn", + color="primary", + n_clicks=0, + className="w-100 mb-3", + ), + html.Div( + id="mol-info", className="text-muted small" + ), + html.Hr(), + dbc.Button( + "Exit", + id="exit-btn", + color="secondary", + outline=True, + n_clicks=0, + className="w-100", + title="Shut down the server and close the app", + ), + dbc.Alert( + id="exit-msg", + is_open=False, + color="success", + className="mt-2 mb-0 small text-center", + ), + ], + style={"overflowY": "auto", "maxHeight": _VIEWER_H}, + ), + className="h-100", + ), + md=3, + className="mb-2", + ), + # 3D Viewer ------------------------------------------------- + dbc.Col( + [ + html.Div( + "Search by name, enter a SMILES string, or choose a sample β€” then click Visualize.", + id="graph-placeholder", + className="text-muted d-flex align-items-center justify-content-center text-center px-4", + style={"height": _VIEWER_H, "fontSize": "1.1rem"}, + ), + html.Div( + dcc.Graph( + id="mol-graph", + style={"height": _VIEWER_H}, + config={"displayModeBar": True, "scrollZoom": True}, + figure=_empty_figure(), + ), + id="graph-container", + style={"display": "none"}, + ), + html.Div( + id="error-msg", className="text-danger mt-2 small" + ), + ], + md=9, + ), + ], + className="g-2", + ), + ], + fluid=True, + style={"paddingBottom": "0"}, ) - with st.sidebar.expander("Lighting Settings", expanded=False): - # Initialize defaults only if not present (fixes Session State warning) - if "ambient" not in st.session_state: - st.session_state["ambient"] = 0.2 - if "diffuse" not in st.session_state: - st.session_state["diffuse"] = 0.8 - if "specular" not in st.session_state: - st.session_state["specular"] = 0.3 - if "roughness" not in st.session_state: - st.session_state["roughness"] = 0.5 - if "fresnel" not in st.session_state: - st.session_state["fresnel"] = 0.1 - - presets = {} - if CONFIG_PATH.exists(): - try: - presets = json.loads(CONFIG_PATH.read_text(encoding="utf-8")) - except json.JSONDecodeError: - presets = {} - - lighting_presets = presets.get("lighting_presets", {}) - preset_names = sorted(lighting_presets.keys()) - - if preset_names: - selected_preset = st.selectbox( - "Load preset", - preset_names, - key="lighting_preset", - ) - def apply_preset(): - data = lighting_presets.get(selected_preset, {}) - if data: - st.session_state["ambient"] = data.get("ambient", 0.2) - st.session_state["diffuse"] = data.get("diffuse", 0.8) - st.session_state["specular"] = data.get("specular", 0.3) - st.session_state["roughness"] = data.get("roughness", 0.5) - st.session_state["fresnel"] = data.get("fresnel", 0.1) - - st.button("Load preset", on_click=apply_preset) - else: - st.caption("No saved presets yet.") - - # Use sliders without default value since we're using session state - ambient = st.slider("Ambient", 0.0, 1.0, key="ambient", step=0.05) - diffuse = st.slider("Diffuse", 0.0, 1.0, key="diffuse", step=0.05) - specular = st.slider("Specular", 0.0, 1.0, key="specular", step=0.05) - roughness = st.slider("Roughness", 0.0, 1.0, key="roughness", step=0.05) - fresnel = st.slider("Fresnel", 0.0, 1.0, key="fresnel", step=0.05) - - st.markdown("---") - preset_name = st.text_input( - "Preset name", - value="default", - help="Save the current lighting settings under this name", - ) - if st.button("Save lighting preset"): - presets = {} - if CONFIG_PATH.exists(): - try: - presets = json.loads(CONFIG_PATH.read_text(encoding="utf-8")) - except json.JSONDecodeError: - presets = {} - - lighting_presets = presets.get("lighting_presets", {}) - lighting_presets[preset_name] = { - "ambient": ambient, - "diffuse": diffuse, - "specular": specular, - "roughness": roughness, - "fresnel": fresnel, - } - presets["lighting_presets"] = lighting_presets - CONFIG_PATH.write_text(json.dumps(presets, indent=2), encoding="utf-8") - st.success(f"Saved preset: {preset_name}") - - with st.sidebar.expander("Settings", expanded=False): - perf_mode = st.selectbox( - "Mode", - ["Balanced", "Performance"], - index=0, - help="Balanced keeps full quality; Performance reduces render cost.", - ) +def create_app() -> Dash: + """Create and return the configured Dash application instance.""" + app = Dash( + __name__, + external_stylesheets=[dbc.themes.FLATLY], + title="plotlyMol3D Viewer", + ) + app.layout = _build_layout() - resolution = st.sidebar.slider( - "Resolution", 8, 64, 32, 8, help="Higher = smoother spheres" + @app.callback( + Output("smiles-input", "value"), + Input("sample-select", "value"), + prevent_initial_call=True, ) - resolution_used = 16 if perf_mode == "Performance" else resolution - if perf_mode == "Performance": - st.sidebar.caption("Performance mode uses lower resolution for faster UI.") - - rdkitmol = None - show_orbitals = False - cube_path = None - - if input_method == "SMILES": - st.markdown("### Enter SMILES String") - - if "smiles_input" not in st.session_state: - st.session_state.smiles_input = "" - - def set_random_smiles(): - import random - - examples = [ - ("CCO", "Ethanol"), - ("c1ccccc1", "Benzene"), - ("CC(=O)O", "Acetic acid"), - ("CN1C=NC2=C1C(=O)N(C(=O)N2C)C", "Caffeine"), - ("CC(N)C(=O)O", "Alanine"), - ("C1CCCCC1", "Cyclohexane"), - ("c1ccc2ccccc2c1", "Naphthalene"), - ("CCCCCCCC", "Octane"), - ("C=C", "Ethene"), - ("C#C", "Ethyne"), - ("C1=CC=C(C=C1)C=O", "Benzaldehyde"), - ("CC(=O)OC1=CC=CC=C1C(=O)O", "Aspirin"), - ] - choice = random.choice(examples) - st.session_state["smiles_input"] = choice[0] - st.session_state["random_molecule_name"] = choice[1] - - col1, col2 = st.columns([3, 1]) - with col1: - st.text_input( - "SMILES", - placeholder="Enter a SMILES string (e.g., CCO for ethanol)", - label_visibility="collapsed", - key="smiles_input", - ) - with col2: - st.button( - "Random", - help="Try a random molecule", - on_click=set_random_smiles, + def fill_smiles_from_sample(value: str) -> str: + return value or "" + + @app.callback( + Output("mol-graph", "figure"), + Output("mol-info", "children"), + Output("error-msg", "children"), + Output("graph-container", "style"), + Output("graph-placeholder", "style"), + Input("visualize-btn", "n_clicks"), + State("smiles-input", "value"), + State("mode-select", "value"), + State("lighting-select", "value"), + prevent_initial_call=True, + ) + def update_figure(n_clicks: int, smiles: str, mode: str, lighting: str): + return _render(smiles, mode, lighting) + + @app.callback( + Output("smiles-input", "value", allow_duplicate=True), + Output("pubchem-status", "children"), + Output("pubchem-status", "className"), + Output("mol-graph", "figure", allow_duplicate=True), + Output("mol-info", "children", allow_duplicate=True), + Output("error-msg", "children", allow_duplicate=True), + Output("graph-container", "style", allow_duplicate=True), + Output("graph-placeholder", "style", allow_duplicate=True), + Input("pubchem-btn", "n_clicks"), + State("pubchem-input", "value"), + State("mode-select", "value"), + State("lighting-select", "value"), + prevent_initial_call=True, + ) + def search_pubchem(n_clicks: int, name: str, mode: str, lighting: str): + _placeholder_visible = {"height": _VIEWER_H, "fontSize": "1.1rem"} + _hide = {"display": "none"} + + def _no_render(smiles_val: str, status: str, cls: str) -> tuple[Any, ...]: + return ( + smiles_val, + status, + cls, + _empty_figure(), + "", + "", + _hide, + _placeholder_visible, ) - if "random_molecule_name" in st.session_state: - st.toast(f"Selected: {st.session_state.random_molecule_name}") - del st.session_state.random_molecule_name - - if st.session_state.smiles_input: - try: - with st.spinner("Parsing SMILES..."): - rdkitmol = cached_smiles_to_mol(st.session_state.smiles_input) - st.toast(f"βœ“ Parsed: {Chem.MolToSmiles(rdkitmol)}", icon="βœ…") - except Exception as e: - st.error(f"Invalid SMILES: {e}") - - elif input_method == "MOL File": - st.markdown("### Upload MOL File") - - uploaded_file = st.file_uploader("Choose a .mol file", type=["mol", "sdf"]) - - if uploaded_file is not None: - try: - with st.spinner("Loading MOL file..."): - mol_content = uploaded_file.read().decode("utf-8") - rdkitmol = cached_molblock_to_mol(mol_content) - if rdkitmol is None: - st.error("Could not parse MOL file") - else: - st.success(f"Loaded: {uploaded_file.name}") - except Exception as e: - st.error(f"Error reading file: {e}") - - elif input_method == "XYZ File": - st.markdown("### Upload XYZ File") - - col1, col2 = st.columns([3, 1]) - with col1: - uploaded_file = st.file_uploader("Choose a .xyz file", type=["xyz"]) - with col2: - charge = st.number_input( - "Molecular Charge", value=0, min_value=-5, max_value=5 + if not name or not name.strip(): + return _no_render( + "", "Enter a molecule name to search.", "text-warning small mb-3" ) - if uploaded_file is not None: - try: - with st.spinner("Parsing XYZ file..."): - xyz_content = uploaded_file.read().decode("utf-8") - rdkitmol = cached_xyzblock_to_mol(xyz_content, charge=charge) - st.success(f"Loaded: {uploaded_file.name}") - except Exception as e: - st.error(f"Error: {e}") - st.info( - "Tip: XYZ bond detection can be tricky. Try specifying the correct charge, or use a MOL file instead." - ) - - elif input_method == "Cube File": - st.markdown("### Upload Cube File (Orbital Visualization)") - - uploaded_file = st.file_uploader("Choose a .cube file", type=["cube", "cub"]) - - col1, col2 = st.columns(2) - with col1: - show_molecule = st.checkbox("Show Molecule", value=True) - with col2: - show_orbitals = st.checkbox("Show Orbitals", value=True) - - if show_orbitals: - col1, col2 = st.columns(2) - with col1: - orbital_opacity = st.slider("Orbital Opacity", 0.1, 1.0, 0.3, 0.05) - with col2: - pos_color = st.color_picker("Positive Lobe", "#FF8C00") - neg_color = st.color_picker("Negative Lobe", "#1E90FF") - - if uploaded_file is not None: - try: - with st.spinner("Processing cube file..."): - cube_bytes = uploaded_file.read() - with tempfile.NamedTemporaryFile( - delete=False, suffix=".cube" - ) as tmp: - tmp.write(cube_bytes) - cube_path = tmp.name - - if show_molecule: - xyzblock, cube_charge = cached_cube_bytes_to_xyzblock( - cube_bytes - ) - rdkitmol = cached_xyzblock_to_mol(xyzblock, charge=cube_charge) - - st.success(f"Loaded: {uploaded_file.name}") - except Exception as e: - st.error(f"Error: {e}") - - elif input_method == "Sample Molecules": - st.markdown("### Select a Sample Molecule") - - samples = { - "Ethanol": "CCO", - "Benzene": "c1ccccc1", - "Caffeine": "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", - "Aspirin": "CC(=O)OC1=CC=CC=C1C(=O)O", - "Glucose": "OC[C@H]1OC(O)[C@H](O)[C@@H](O)[C@@H]1O", - "Alanine": "CC(N)C(=O)O", - "Cyclohexane": "C1CCCCC1", - "Naphthalene": "c1ccc2ccccc2c1", - "Methane": "C", - "Water": "O", - } - - selected = st.selectbox("Choose molecule", list(samples.keys())) - smiles = samples[selected] - st.code(smiles, language=None) - try: - with st.spinner("Generating 3D structure..."): - rdkitmol = cached_smiles_to_mol(smiles) - except Exception as e: - st.error(f"Error: {e}") - - # Create two separate panels for independent visualization - if rdkitmol is not None: - display_molecule_info(rdkitmol) - - # Initialize active panel in session state - if "active_panel" not in st.session_state: - st.session_state.active_panel = "Molecule Visualization" - - # Panel selector - active_panel = st.radio( - "Select Panel", - ["Molecule Visualization", "Vibrational Analysis"], - key="active_panel", - horizontal=True, - label_visibility="collapsed", - ) - - # ====================================================================== - # PANEL 1: Molecule Visualization - # ====================================================================== - if active_panel == "Molecule Visualization": - st.markdown("### Molecule Visualization") - - # Regular molecule figure (no vibrations) - with st.spinner("Rendering 3D visualization..."): - import pickle - - mol_pkl = pickle.dumps(rdkitmol) - fig = cached_figure_from_mol_pickle( - mol_pkl, - mode, - resolution_used, - ambient, - diffuse, - specular, - roughness, - fresnel, - ) - - # Apply orbitals if from cube file - if show_orbitals and cube_path is not None: - try: - with st.spinner("Rendering orbitals..."): - draw_cube_orbitals( - fig, cube_path, orbital_opacity, [pos_color, neg_color] - ) - except Exception as e: - st.warning(f"Could not render orbitals: {e}") - - st.plotly_chart(fig, width="stretch") - - with st.expander("Save Image", expanded=False): - preset = st.selectbox( - "Preset", - ["Small (800x600)", "HD (1280x720)", "Large (1920x1080)"], - index=1, - key="save_base_preset", - ) - fmt = st.selectbox( - "Format", ["png", "svg"], index=0, key="save_base_fmt" - ) - - preset_sizes = { - "Small (800x600)": (800, 600), - "HD (1280x720)": (1280, 720), - "Large (1920x1080)": (1920, 1080), - } - width, height = preset_sizes[preset] - - file_name = st.text_input( - "File name", - value=f"plotlymol3d_{width}x{height}.{fmt}", - key="save_base_filename", - ) - - try: - with st.spinner("Preparing image..."): - image_bytes = cached_image_bytes( - mol_pkl, - mode, - resolution_used, - ambient, - diffuse, - specular, - roughness, - fresnel, - width, - height, - fmt, - ) - - st.download_button( - "Download image", - data=image_bytes, - file_name=file_name, - mime=f"image/{fmt}", - key="save_base_download", - ) - except Exception as e: - st.error( - f"Image export failed: {e}. If this is a missing dependency, install kaleido." - ) - - # ====================================================================== - # PANEL 2: Vibrational Analysis - # ====================================================================== - if active_panel == "Vibrational Analysis": - st.markdown("### Vibrational Analysis") - - # File uploader - vib_file = st.file_uploader( - "Upload Vibration File", - type=["log", "out", "molden"], - help="Gaussian .log, ORCA .out, or Molden .molden files", + smiles, cid = _pubchem_name_to_smiles(name.strip()) + except ValueError as exc: + return _no_render("", str(exc), "text-danger small mb-3") + except requests.RequestException: + msg = ( + "Could not reach PubChem. Check your internet connection and try again." ) + return _no_render("", msg, "text-danger small mb-3") - vib_data = None - if vib_file is not None: - try: - with st.spinner("Parsing vibration file..."): - vib_bytes = vib_file.read() - vib_data = cached_parse_vibrations(vib_bytes, vib_file.name) - - # Store in session state for caching animations - st.session_state["vib_data"] = vib_data - - st.success( - f"βœ“ Loaded {len(vib_data.modes)} modes from {vib_data.program.upper()} file" - ) - - # Create molecule from vibration data - try: - from plotlymol3d.atomProperties import atom_symbols - - # Convert vib_data coordinates to XYZ block - n_atoms = len(vib_data.atomic_numbers) - xyz_lines = [ - str(n_atoms), - f"Structure from {vib_data.source_file}", - ] - - for _i, (atomic_num, coord) in enumerate( - zip(vib_data.atomic_numbers, vib_data.coordinates) - ): - symbol = atom_symbols[atomic_num] - xyz_lines.append( - f"{symbol} {coord[0]:.6f} {coord[1]:.6f} {coord[2]:.6f}" - ) - - xyzblock = "\n".join(xyz_lines) - vib_rdkitmol = cached_xyzblock_to_mol(xyzblock, charge=0) - - if vib_rdkitmol is not None: - # Force molecule coordinates to exactly match vib_data - conf = vib_rdkitmol.GetConformer() - for atom_idx in range(vib_rdkitmol.GetNumAtoms()): - x, y, z = vib_data.coordinates[atom_idx] - conf.SetAtomPosition( - atom_idx, (float(x), float(y), float(z)) - ) - except Exception as e: - st.error(f"Error creating molecule from vibration data: {e}") - vib_rdkitmol = None - - if vib_rdkitmol is not None: - # Controls in columns - col1, col2 = st.columns([2, 1]) - - with col1: - # Mode selection dropdown - mode_options = [] - for vib_mode in vib_data.modes: - freq_str = f"{vib_mode.frequency:.1f} cm⁻¹" - if vib_mode.is_imaginary: - freq_str += " (imaginary)" - - if vib_mode.ir_intensity is not None: - mode_label = f"Mode {vib_mode.mode_number}: {freq_str} (IR: {vib_mode.ir_intensity:.1f})" - else: - mode_label = ( - f"Mode {vib_mode.mode_number}: {freq_str}" - ) - - mode_options.append(mode_label) - - selected_mode = st.selectbox( - "Select Vibrational Mode", - options=range(len(mode_options)), - format_func=lambda i: mode_options[i], - index=0, - ) - vib_mode_number = vib_data.modes[selected_mode].mode_number - - with col2: - # Display type selection - vib_display_type = st.selectbox( - "Display Type", - [ - "Static arrows", - "Animation", - "Heatmap", - "Arrows + Heatmap", - ], - index=0, - help="How to visualize the vibrational mode", - ) - - # Visualization parameters - # Initialize defaults for all display types - vib_arrow_color = "#FF0000" - vib_arrow_scale = 0.15 - vib_heatmap_colorscale = "Reds" - vib_n_frames = 20 - - with st.expander("Visualization Parameters", expanded=True): - # Common parameters - vib_amplitude = st.slider( - "Amplitude", - 0.05, - 2.0, - 0.3, - 0.05, - help="Displacement amplitude multiplier", - ) - - # Arrow-specific parameters - if vib_display_type in [ - "Static arrows", - "Arrows + Heatmap", - ]: - vib_arrow_color = st.color_picker( - "Arrow Color", - value="#FF0000", - help="Color for displacement arrows", - ) - vib_arrow_scale = st.slider( - "Arrow Size", - 0.05, - 1.0, - 0.15, - 0.05, - help="Visual scale for arrow size", - ) - - # Heatmap-specific parameters - if vib_display_type in ["Heatmap", "Arrows + Heatmap"]: - vib_heatmap_colorscale = st.selectbox( - "Heatmap Colorscale", - [ - "Reds", - "Blues", - "Viridis", - "Plasma", - "Hot", - "YlOrRd", - ], - index=0, - help="Color scheme for displacement magnitude", - ) - - # Animation-specific parameters - if vib_display_type == "Animation": - vib_n_frames = st.slider( - "Animation Frames", - 5, - 50, - 20, - 5, - help="Number of frames (more = smoother but slower)", - ) - - # Render vibration visualization - st.markdown("---") - - import pickle - - # Handle vibration animation separately (creates new figure) - if vib_display_type == "Animation": - try: - mol_pkl_vib = pickle.dumps(vib_rdkitmol) - - # Generate animation with progress feedback - vib_fig = get_cached_vibration_animation( - vib_data_id=vib_data.source_file, - mode_number=vib_mode_number, - mol_pkl=mol_pkl_vib, - amplitude=vib_amplitude, - n_frames=vib_n_frames, - mode=mode, - resolution=resolution_used, - ambient=ambient, - diffuse=diffuse, - specular=specular, - roughness=roughness, - fresnel=fresnel, - ) - - st.caption( - "Animation cached - switching back to this mode/settings will be instant!" - ) - except Exception as e: - st.error(f"Error creating animation: {e}") - vib_fig = None - else: - # Regular figure with vibration overlays - with st.spinner("Rendering vibration visualization..."): - mol_pkl_vib = pickle.dumps(vib_rdkitmol) - vib_fig = cached_figure_from_mol_pickle( - mol_pkl_vib, - mode, - resolution_used, - ambient, - diffuse, - specular, - roughness, - fresnel, - ) - - # Apply vibrations (non-animation modes) - try: - with st.spinner("Adding vibration visualization..."): - # Map display type to internal format - display_map = { - "Static arrows": "arrows", - "Heatmap": "heatmap", - "Arrows + Heatmap": "both", - } - display_type_internal = display_map.get( - vib_display_type - ) - - if display_type_internal: - vib_fig = add_vibrations_to_figure( - fig=vib_fig, - vib_data=vib_data, - mode_number=vib_mode_number, - display_type=display_type_internal, - amplitude=vib_amplitude, - arrow_scale=vib_arrow_scale, - arrow_color=vib_arrow_color, - heatmap_colorscale=vib_heatmap_colorscale, - show_colorbar=True, - ) - except Exception as e: - st.warning(f"Could not add vibrations: {e}") - - if vib_fig is not None: - st.plotly_chart(vib_fig, width="stretch") - - except Exception as e: - st.error(f"Error parsing vibration file: {e}") - - else: - st.info( - "πŸ‘† Upload a vibration file (.log, .out, or .molden) to get started" - ) + fig, info, err, container_style, placeholder_style = _render( + smiles, mode, lighting + ) + status = f"Found: {name.strip().title()} (CID {cid})" + return ( + smiles, + status, + "text-success small mb-3", + fig, + info, + err, + container_style, + placeholder_style, + ) - # Clean up temporary cube files - if cube_path and os.path.exists(cube_path): - os.unlink(cube_path) - - elif input_method not in ["Sample Molecules"]: - st.info("Enter a molecule above to visualize it") - - st.sidebar.markdown("---") - st.sidebar.markdown(""" - **Quick Examples:** - - `CCO` - Ethanol - - `c1ccccc1` - Benzene - - `CC(=O)O` - Acetic acid - - `CN1C=NC2=C1C(=O)N(C(=O)N2C)C` - Caffeine - """) - - st.sidebar.markdown("---") - st.sidebar.caption( - "plotlyMol3D v0.2.0 | [GitHub](https://github.com/NCCU-Schultz-Lab/plotlyMol)" + @app.callback( + Output("exit-msg", "children"), + Output("exit-msg", "is_open"), + Input("exit-btn", "n_clicks"), + prevent_initial_call=True, ) + def shutdown(n_clicks: int): + if n_clicks: + Timer(0.5, lambda: os.kill(os.getpid(), signal.SIGTERM)).start() + return "Server stopped. You may close this tab.", True + return "", False + + return app + + +def main() -> None: + """Start the Dash server and open the browser.""" + app = create_app() + Timer(1.5, lambda: webbrowser.open(f"http://{_HOST}:{_PORT}")).start() + print(f"\n plotlyMol3D is running at http://{_HOST}:{_PORT}") + print(" Press Ctrl+C to stop.\n") + app.run(debug=False, host=_HOST, port=_PORT) if __name__ == "__main__": diff --git a/src/plotlymol3d/plotlyMol3D.py b/src/plotlymol3d/plotlyMol3D.py index 6f8d5e6..2886b5e 100644 --- a/src/plotlymol3d/plotlyMol3D.py +++ b/src/plotlymol3d/plotlyMol3D.py @@ -456,6 +456,7 @@ def make_bond_mesh_trace( radius: float = DEFAULT_RADIUS, resolution: int = DEFAULT_RESOLUTION, color: str = "grey", + add_caps: bool = True, ) -> go.Mesh3d: """Create a Plotly Mesh3d trace for a bond (cylinder). @@ -469,24 +470,42 @@ def make_bond_mesh_trace( Returns: Plotly Mesh3d trace object for the bond segment. """ - x, y, z = generate_cylinder_mesh_rectangles(point1, point2, radius, resolution) - - # Create the faces for the cylinder using rectangles - i, j, k, l = [], [], [], [] - num_vertices = resolution - for n in range(num_vertices): - next_n = (n + 1) % num_vertices - i.extend([n, next_n, next_n, n]) - j.extend([n, n, n + num_vertices, n + num_vertices]) - k.extend( - [ - n + num_vertices, - n + num_vertices, - next_n + num_vertices, - next_n + num_vertices, - ] - ) - l.extend([next_n, next_n + num_vertices, next_n + num_vertices, next_n]) + p1 = np.array(point1) + p2 = np.array(point2) + x, y, z = generate_cylinder_mesh_rectangles(p1, p2, radius, resolution) + + # Append center points for the two end-cap disks + x = np.append(x, [p1[0], p2[0]]) + y = np.append(y, [p1[1], p2[1]]) + z = np.append(z, [p1[2], p2[2]]) + + res = resolution + c_bottom = 2 * res # center of bottom cap (at p1) + c_top = 2 * res + 1 # center of top cap (at p2) + + i, j, k = [], [], [] + + # Side wall: two triangles per quad segment + for n in range(res): + nxt = (n + 1) % res + i.extend([n, n]) + j.extend([n + res, nxt + res]) + k.extend([nxt + res, nxt]) + + if add_caps: + # Bottom cap (fan from c_bottom into bottom-circle rim) + for n in range(res): + nxt = (n + 1) % res + i.append(c_bottom) + j.append(nxt) + k.append(n) + + # Top cap (fan from c_top into top-circle rim) + for n in range(res): + nxt = (n + 1) % res + i.append(c_top) + j.append(n + res) + k.append(nxt + res) bond_trace = go.Mesh3d( x=x, @@ -502,6 +521,45 @@ def make_bond_mesh_trace( return bond_trace +def _make_oval_cap( + center: np.ndarray, + bond_dir: np.ndarray, + perp_major: np.ndarray, + semi_a: float, + semi_b: float, + resolution: int, + color: str, +) -> go.Mesh3d: + """Flat elliptical end cap for multi-bond termini.""" + perp_minor = np.cross(bond_dir, perp_major) + norm = np.linalg.norm(perp_minor) + if norm > 0: + perp_minor /= norm + + theta = np.linspace(0, 2 * np.pi, resolution, endpoint=False) + rim = ( + center[:, None] + + semi_a * perp_major[:, None] * np.cos(theta) + + semi_b * perp_minor[:, None] * np.sin(theta) + ) + + x = np.append(rim[0], center[0]) + y = np.append(rim[1], center[1]) + z = np.append(rim[2], center[2]) + + c_idx = resolution + i, j, k = [], [], [] + for n in range(resolution): + nxt = (n + 1) % resolution + i.append(c_idx) + j.append(n) + k.append(nxt) + + return go.Mesh3d( + x=x, y=y, z=z, i=i, j=j, k=k, color=color, opacity=1, hoverinfo="skip" + ) + + def draw_bonds( fig: go.Figure, bondList: List[Bond], @@ -679,6 +737,7 @@ def draw_bonds( fig.add_trace(bond_trace) else: # Solid bond: single cylinder per half + use_oval_caps = bond_order in (2.0, 3.0) # First half of bond (atom 1 color) bond_trace = make_bond_mesh_trace( p1.tolist(), @@ -686,6 +745,7 @@ def draw_bonds( color=atom_colors[bond.a1_number], resolution=resolution, radius=r, + add_caps=not use_oval_caps, ) fig.add_trace(bond_trace) @@ -696,9 +756,30 @@ def draw_bonds( color=atom_colors[bond.a2_number], resolution=resolution, radius=r, + add_caps=not use_oval_caps, ) fig.add_trace(bond_trace) + # Oval end caps for double and triple bonds + if bond_order in (2.0, 3.0): + bond_dir = bond_vec / np.linalg.norm(bond_vec) + max_offset = max(np.linalg.norm(o) for o in offsets) + r0 = radii[0] + semi_a = max_offset + r0 + semi_b = r0 + for center, color_num in [(a1, bond.a1_number), (a2, bond.a2_number)]: + fig.add_trace( + _make_oval_cap( + center, + bond_dir, + perp, + semi_a, + semi_b, + resolution, + atom_colors[color_num], + ) + ) + return fig @@ -827,6 +908,174 @@ def draw_3D_mol( return fig +def create_trajectory_animation( + xyzblocks: List[str], + energies_hartree: Optional[List[float]] = None, + charge: int = 0, + mode: str = "ball+stick", + resolution: int = 16, + title: str = "Geometry Optimization Trajectory", +) -> go.Figure: + """Create an animated Plotly figure stepping through geometry optimization frames. + + Each XYZ block is one step in a BFGS or similar optimization trajectory. + The first frame is the starting geometry; the last is the optimized structure. + + Args: + xyzblocks: List of XYZ-format strings (count line + title line + coord lines). + Must contain at least 2 entries. + energies_hartree: SCF energy in Hartrees for each frame (same length as + xyzblocks). Used to annotate frame labels. Optional. + charge: Molecular charge for bond-order perception. + mode: Visualization mode - "ball+stick", "stick", or "vdw". + resolution: Sphere/cylinder mesh resolution (lower = faster). + title: Figure title. + + Returns: + Plotly Figure with animation frames and play/step controls. + + Raises: + ValueError: If fewer than 2 xyzblocks are provided. + + Example: + >>> blocks = [xyz_block_1, xyz_block_2, xyz_block_3] + >>> energies = [-75.0, -75.5, -75.6] + >>> fig = create_trajectory_animation(blocks, energies) + >>> fig.show() + """ + if len(xyzblocks) < 2: + raise ValueError( + f"create_trajectory_animation requires at least 2 frames, " + f"got {len(xyzblocks)}" + ) + + n_frames = len(xyzblocks) + + # Parse the reference mol (first frame) for bond connectivity. + ref_mol = xyzblock_to_rdkitmol(xyzblocks[0], charge=charge) + + frames = [] + first_frame_data = None + + for i, xyzblock in enumerate(xyzblocks): + # Update atom positions from this frame's XYZ block. + raw = Chem.MolFromXYZBlock(xyzblock) + frame_mol = Chem.RWMol(ref_mol) + conf = frame_mol.GetConformer() + raw_conf = raw.GetConformer() + for atom_idx in range(frame_mol.GetNumAtoms()): + pos = raw_conf.GetAtomPosition(atom_idx) + conf.SetAtomPosition(atom_idx, (pos.x, pos.y, pos.z)) + + # Build traces for this frame. + empty_fig = go.Figure() + fig_frame = draw_3D_mol( + empty_fig, frame_mol.GetMol(), mode=mode, resolution=resolution + ) + frame_traces = list(fig_frame.data) + + # Build frame label. + if energies_hartree is not None and i < len(energies_hartree): + e_label = f"Step {i}: E = {energies_hartree[i]:.6f} Hₐ" + else: + e_label = f"Step {i}" + + frames.append( + go.Frame( + data=frame_traces, + name=f"frame_{i}", + layout=go.Layout(title_text=f"{title} β€” {e_label}"), + ) + ) + if i == 0: + first_frame_data = frame_traces + + fig = go.Figure(data=first_frame_data, frames=frames) + + # Animation controls: play/pause button + step slider. + fig.update_layout( + title=title, + updatemenus=[ + { + "type": "buttons", + "showactive": False, + "buttons": [ + { + "label": "β–Ά Play", + "method": "animate", + "args": [ + None, + { + "frame": {"duration": 300, "redraw": True}, + "fromcurrent": False, + "mode": "immediate", + "transition": {"duration": 0}, + }, + ], + }, + { + "label": "⏸ Pause", + "method": "animate", + "args": [ + [None], + { + "frame": {"duration": 0, "redraw": False}, + "mode": "immediate", + "transition": {"duration": 0}, + }, + ], + }, + ], + "x": 0.1, + "y": 0.0, + "xanchor": "left", + "yanchor": "bottom", + } + ], + sliders=[ + { + "active": 0, + "steps": [ + { + "args": [ + [f"frame_{k}"], + { + "frame": {"duration": 0, "redraw": True}, + "mode": "immediate", + "transition": {"duration": 0}, + }, + ], + "label": str(k), + "method": "animate", + } + for k in range(n_frames) + ], + "x": 0.1, + "len": 0.85, + "xanchor": "left", + "y": 0.0, + "yanchor": "top", + "pad": {"b": 10, "t": 50}, + "currentvalue": { + "visible": True, + "prefix": "Step: ", + "xanchor": "right", + "font": {"size": 14}, + }, + "transition": {"duration": 0}, + } + ], + scene={ + "xaxis": {"visible": False}, + "yaxis": {"visible": False}, + "zaxis": {"visible": False}, + "aspectmode": "data", + }, + ) + + return fig + + def draw_3D_rep( smiles: Optional[str] = None, xyzfile: Optional[str] = None, diff --git a/stop_app.bat b/stop_app.bat deleted file mode 100644 index d683a63..0000000 --- a/stop_app.bat +++ /dev/null @@ -1,6 +0,0 @@ -@echo off -setlocal -set "ROOT=%~dp0" -cd /d "%ROOT%" - -powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*streamlit*run*examples\\gui_app.py*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }" diff --git a/stop_app.vbs b/stop_app.vbs deleted file mode 100644 index 96a7d31..0000000 --- a/stop_app.vbs +++ /dev/null @@ -1,5 +0,0 @@ -Set shell = CreateObject("WScript.Shell") -Set fso = CreateObject("Scripting.FileSystemObject") -root = fso.GetParentFolderName(WScript.ScriptFullName) -batPath = Chr(34) & root & "\stop_app.bat" & Chr(34) -shell.Run batPath, 0 diff --git a/tests/test_visualization.py b/tests/test_visualization.py index 56cc943..9c3fca5 100644 --- a/tests/test_visualization.py +++ b/tests/test_visualization.py @@ -190,3 +190,85 @@ def test_draw_bonds_adds_traces(self, sample_smiles): # Each bond creates 2 traces (one per half) assert len(fig.data) == initial_traces + 2 * len(bondList) + + +# --------------------------------------------------------------------------- +# create_trajectory_animation +# --------------------------------------------------------------------------- + + +def _water_xyzblocks(n: int = 3) -> list: + """Return n slightly perturbed XYZ blocks for water.""" + blocks = [] + for i in range(n): + o_z = i * 0.01 + block = ( + f"3\nH2O step {i}\n" + f"O 0.0 0.0 {o_z:.4f}\n" + f"H 0.757 0.587 0.0\n" + f"H -0.757 0.587 0.0" + ) + blocks.append(block) + return blocks + + +class TestCreateTrajectoryAnimation: + """Tests for create_trajectory_animation in plotlyMol3D.""" + + def test_returns_plotly_figure(self): + """Returns a Plotly Figure object.""" + from plotlymol3d import create_trajectory_animation + + fig = create_trajectory_animation(_water_xyzblocks(3)) + assert isinstance(fig, Figure) + + def test_frame_count_matches_input(self): + """Number of frames equals number of XYZ blocks.""" + from plotlymol3d import create_trajectory_animation + + blocks = _water_xyzblocks(4) + fig = create_trajectory_animation(blocks) + assert len(fig.frames) == 4 + + def test_minimum_two_frames_accepted(self): + """Two frames is the valid minimum.""" + from plotlymol3d import create_trajectory_animation + + fig = create_trajectory_animation(_water_xyzblocks(2)) + assert len(fig.frames) == 2 + + def test_single_frame_raises_value_error(self): + """Providing only one frame raises ValueError.""" + from plotlymol3d import create_trajectory_animation + + with pytest.raises(ValueError): + create_trajectory_animation(_water_xyzblocks(1)) + + def test_slider_step_count(self): + """Slider has one step per frame.""" + from plotlymol3d import create_trajectory_animation + + n = 5 + fig = create_trajectory_animation(_water_xyzblocks(n)) + assert len(fig.layout.sliders) > 0 + assert len(fig.layout.sliders[0].steps) == n + + def test_initial_traces_not_empty(self): + """Initial figure data contains at least one trace.""" + from plotlymol3d import create_trajectory_animation + + fig = create_trajectory_animation(_water_xyzblocks(2)) + assert len(fig.data) > 0 + + def test_energies_in_frame_labels(self): + """Energy values appear in frame layout titles.""" + from plotlymol3d import create_trajectory_animation + + energies = [-75.0, -75.3, -75.6] + fig = create_trajectory_animation( + _water_xyzblocks(3), energies_hartree=energies + ) + titles = [ + f.layout.title.text for f in fig.frames if f.layout and f.layout.title + ] + assert any("-75" in (t or "") for t in titles)