Skip to main content

sci_form/gpu/
isosurface.rs

1//! Isosurface mesh generation for molecular orbital visualization.
2//!
3//! High-level API that combines orbital grid evaluation + marching cubes
4//! into a single pipeline with dual-lobe support (positive/negative ψ)
5//! and optional normal smoothing.
6
7use super::backend_report::IsosurfaceReport;
8use super::marching_cubes::{marching_cubes_cpu, smooth_mesh_normals, McOutput};
9use super::orbital_grid::{evaluate_orbital_with_report, GridParams};
10use crate::scf::basis::BasisSet;
11use nalgebra::DMatrix;
12
13/// Configuration for isosurface generation.
14#[derive(Debug, Clone)]
15pub struct IsosurfaceConfig {
16    /// Grid spacing in Bohr (smaller = finer mesh).
17    pub spacing: f64,
18    /// Padding around the molecule in Bohr.
19    pub padding: f64,
20    /// Isovalue for the surface. Typical orbital values: 0.02–0.05.
21    pub isovalue: f64,
22    /// Whether to generate both positive and negative lobes.
23    pub both_lobes: bool,
24    /// Whether to smooth vertex normals.
25    pub smooth_normals: bool,
26}
27
28impl Default for IsosurfaceConfig {
29    fn default() -> Self {
30        Self {
31            spacing: 0.3,
32            padding: 5.0,
33            isovalue: 0.02,
34            both_lobes: true,
35            smooth_normals: true,
36        }
37    }
38}
39
40/// Dual-lobe orbital isosurface.
41#[derive(Debug, Clone)]
42pub struct OrbitalIsosurface {
43    pub positive_lobe: McOutput,
44    pub negative_lobe: McOutput,
45    pub orbital_index: usize,
46    pub isovalue: f64,
47    pub total_triangles: usize,
48}
49
50/// Generate an isosurface mesh for a molecular orbital.
51///
52/// Uses GPU for grid evaluation when available, CPU fallback otherwise.
53/// Returns both the mesh and a report describing which backend was used.
54pub fn generate_orbital_isosurface(
55    basis: &BasisSet,
56    mo_coefficients: &DMatrix<f64>,
57    orbital_index: usize,
58    positions: &[[f64; 3]],
59    config: &IsosurfaceConfig,
60) -> (OrbitalIsosurface, IsosurfaceReport) {
61    let params = GridParams::from_molecule(positions, config.spacing, config.padding);
62
63    let (grid, grid_report) =
64        evaluate_orbital_with_report(basis, mo_coefficients, orbital_index, &params);
65
66    let mut positive = marching_cubes_cpu(&grid.values, &grid.params, config.isovalue);
67
68    let mut negative = if config.both_lobes {
69        let neg_values: Vec<f64> = grid.values.iter().map(|v| -v).collect();
70        marching_cubes_cpu(&neg_values, &grid.params, config.isovalue)
71    } else {
72        McOutput {
73            vertices: Vec::new(),
74            normals: Vec::new(),
75            indices: Vec::new(),
76            n_triangles: 0,
77        }
78    };
79
80    if config.smooth_normals {
81        smooth_mesh_normals(&mut positive);
82        if config.both_lobes {
83            smooth_mesh_normals(&mut negative);
84        }
85    }
86
87    let total = positive.n_triangles + negative.n_triangles;
88
89    let iso = OrbitalIsosurface {
90        positive_lobe: positive,
91        negative_lobe: negative,
92        orbital_index,
93        isovalue: config.isovalue,
94        total_triangles: total,
95    };
96
97    let report = IsosurfaceReport {
98        grid_backend: grid_report.backend,
99        grid_used_gpu: grid_report.used_gpu,
100        n_triangles: total,
101        isovalue: config.isovalue,
102    };
103
104    (iso, report)
105}
106
107/// Generate HOMO isosurface.
108pub fn generate_homo_isosurface(
109    basis: &BasisSet,
110    mo_coefficients: &DMatrix<f64>,
111    n_electrons: usize,
112    positions: &[[f64; 3]],
113    config: &IsosurfaceConfig,
114) -> (OrbitalIsosurface, IsosurfaceReport) {
115    let n_occ = n_electrons / 2;
116    let homo_index = if n_occ > 0 { n_occ - 1 } else { 0 };
117    generate_orbital_isosurface(basis, mo_coefficients, homo_index, positions, config)
118}
119
120/// Generate LUMO isosurface.
121pub fn generate_lumo_isosurface(
122    basis: &BasisSet,
123    mo_coefficients: &DMatrix<f64>,
124    n_electrons: usize,
125    positions: &[[f64; 3]],
126    config: &IsosurfaceConfig,
127) -> (OrbitalIsosurface, IsosurfaceReport) {
128    let lumo_index = n_electrons / 2;
129    generate_orbital_isosurface(basis, mo_coefficients, lumo_index, positions, config)
130}
131
132/// Estimate mesh complexity for a given grid resolution.
133pub fn estimate_mesh_complexity(
134    positions: &[[f64; 3]],
135    spacing: f64,
136    padding: f64,
137) -> (usize, usize) {
138    let params = GridParams::from_molecule(positions, spacing, padding);
139    let n_voxels = (params.dimensions[0].saturating_sub(1))
140        * (params.dimensions[1].saturating_sub(1))
141        * (params.dimensions[2].saturating_sub(1));
142    // ~5% of voxels produce triangles for molecular orbitals
143    let est_triangles = n_voxels / 20;
144    (est_triangles, est_triangles * 3)
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::scf::basis::BasisSet;
151
152    #[test]
153    fn test_isosurface_config_default() {
154        let config = IsosurfaceConfig::default();
155        assert!(config.spacing > 0.0);
156        assert!(config.isovalue > 0.0);
157        assert!(config.both_lobes);
158        assert!(config.smooth_normals);
159    }
160
161    #[test]
162    fn test_estimate_mesh_complexity() {
163        let positions = vec![[0.0, 0.0, 0.0], [3.0, 0.0, 0.0]];
164        let (tri, vert) = estimate_mesh_complexity(&positions, 0.5, 3.0);
165        assert!(tri > 0);
166        assert_eq!(vert, tri * 3);
167    }
168
169    #[test]
170    fn test_generate_orbital_isosurface_h2() {
171        let elements = [1u8, 1];
172        let positions = [[0.0, 0.0, 0.0], [0.0, 0.0, 1.4]];
173        let basis = BasisSet::sto3g(&elements, &positions);
174
175        let n = basis.n_basis;
176        let mut coeffs = DMatrix::zeros(n, n);
177        let c = 1.0 / (2.0f64).sqrt();
178        coeffs[(0, 0)] = c;
179        if n > 1 {
180            coeffs[(1, 0)] = c;
181        }
182
183        let config = IsosurfaceConfig {
184            spacing: 0.4,
185            padding: 3.0,
186            isovalue: 0.005,
187            both_lobes: true,
188            smooth_normals: false,
189        };
190
191        let (iso, report) = generate_orbital_isosurface(&basis, &coeffs, 0, &positions, &config);
192        assert!(!report.grid_backend.is_empty());
193        assert!((report.isovalue - 0.005).abs() < 1e-10);
194        assert_eq!(iso.orbital_index, 0);
195        // Pipeline runs without panic; triangle count depends on grid/isovalue
196    }
197
198    #[test]
199    fn test_generate_homo_lumo() {
200        let elements = [1u8, 1];
201        let positions = [[0.0, 0.0, 0.0], [0.0, 0.0, 1.4]];
202        let basis = BasisSet::sto3g(&elements, &positions);
203
204        let n = basis.n_basis;
205        let mut coeffs = DMatrix::zeros(n, n);
206        coeffs[(0, 0)] = 1.0 / (2.0f64).sqrt();
207        if n > 1 {
208            coeffs[(1, 0)] = 1.0 / (2.0f64).sqrt();
209            coeffs[(0, 1)] = 1.0 / (2.0f64).sqrt();
210            coeffs[(1, 1)] = -1.0 / (2.0f64).sqrt();
211        }
212
213        let config = IsosurfaceConfig {
214            spacing: 0.8,
215            padding: 2.0,
216            isovalue: 0.01,
217            both_lobes: false,
218            smooth_normals: false,
219        };
220
221        let (homo, _) = generate_homo_isosurface(&basis, &coeffs, 2, &positions, &config);
222        assert_eq!(homo.orbital_index, 0); // HOMO for 2 electrons
223
224        let (lumo, _) = generate_lumo_isosurface(&basis, &coeffs, 2, &positions, &config);
225        assert_eq!(lumo.orbital_index, 1); // LUMO
226    }
227}