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 @@
-[](https://github.com/NCCU-Schultz-Lab/plotlyMol/actions/workflows/test.yml)
-[](https://github.com/NCCU-Schultz-Lab/plotlyMol/actions/workflows/lint.yml)
+[](https://github.com/The-Schultz-Lab/plotlyMol/actions/workflows/test.yml)
+[](https://github.com/The-Schultz-Lab/plotlyMol/actions/workflows/lint.yml)
[](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
-
-
-
+Interactive 3D Molecular Visualization
+
+
+
+
-
-
+
+

**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)