Skip to main content
← OpenMECP Documentation

omecp/
lst.rs

1//! Linear Synchronous Transit (LST) interpolation module.
2//!
3//! This module provides functionality for interpolating between molecular geometries
4//! using various methods including Linear Synchronous Transit (LST) 
5//!
6//! # Overview
7//!
8//! The LST method creates a series of intermediate geometries between two endpoint
9//! structures by linear interpolation in Cartesian coordinates.
10//!
11//! # Key Features
12//!
13//! - **Kabsch Algorithm**: Optimal alignment of geometries before interpolation
14//! - **Multiple Methods**: Linear, quadratic, and energy-weighted interpolation
15//! - **Validation**: Comprehensive geometry validation and error checking
16//! - **Path Analysis**: Path length calculation and geometry preview
17//!
18//! # Examples
19//!
20//! ```rust
21//! use omecp::lst::{interpolate, InterpolationMethod};
22//! use omecp::geometry::Geometry;
23//!
24//! // Create two geometries (example with water molecule)
25//! let elements = vec!["O".to_string(), "H".to_string(), "H".to_string()];
26//! let coords1 = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
27//! let coords2 = vec![0.0, 0.0, 0.5, 1.2, 0.0, 0.5, 0.0, 1.2, 0.5];
28//!
29//! let geom1 = Geometry::new(elements.clone(), coords1);
30//! let geom2 = Geometry::new(elements, coords2);
31//!
32//! // Generate 10 intermediate geometries using linear interpolation
33//! let path = interpolate(&geom1, &geom2, 10, InterpolationMethod::Linear);
34
35
36use crate::geometry::Geometry;
37use nalgebra::DMatrix;
38
39/// Interpolation methods available for LST calculations.
40///
41/// This enum defines the different interpolation strategies that can be used
42/// to generate intermediate geometries between molecular structures.
43///
44/// # Variants
45///
46/// - [`Linear`](InterpolationMethod::Linear): Simple linear interpolation between two geometries
47/// - [`Quadratic`](InterpolationMethod::Quadratic): Quadratic interpolation using three geometries
48/// - [`EnergyWeighted`](InterpolationMethod::EnergyWeighted): Energy-informed interpolation (future feature)
49///
50/// # Examples
51///
52/// ```rust
53/// use omecp::lst::InterpolationMethod;
54///
55/// let method = InterpolationMethod::Linear;
56/// assert_eq!(method, InterpolationMethod::Linear);
57/// ```
58#[derive(Debug, Clone, Copy, PartialEq)]
59pub enum InterpolationMethod {
60    /// Linear Synchronous Transit (LST) interpolation.
61    ///
62    /// Performs simple linear interpolation between two endpoint geometries.
63    /// This is the most commonly used method and provides a straight-line
64    /// path in Cartesian coordinate space after optimal alignment.
65    Linear,
66
67    /// Quadratic Synchronous Transit (QST) interpolation.
68    ///
69    /// Uses quadratic interpolation with three geometries: two endpoints and
70    /// one intermediate structure. If only two geometries are provided, a
71    /// midpoint geometry is automatically generated.
72    Quadratic,
73
74    /// Energy-weighted interpolation method.
75    ///
76    /// **Note**: This is a placeholder for future implementation. Currently
77    /// falls back to linear interpolation. Will incorporate energy information
78    /// to create more physically meaningful interpolation paths.
79    EnergyWeighted,
80}
81
82/// Performs LST interpolation between two geometries using the specified method.
83///
84/// This is the main entry point for LST interpolation. It dispatches to the
85/// appropriate interpolation function based on the selected method and returns
86/// a vector of intermediate geometries.
87///
88/// # Arguments
89///
90/// * `geom1` - The starting geometry (reactant structure)
91/// * `geom2` - The ending geometry (product structure)
92/// * `num_points` - Number of intermediate points to generate (excluding endpoints)
93/// * `method` - The interpolation method to use
94///
95/// # Returns
96///
97/// Returns a `Vec<Geometry>` containing the interpolated geometries. The vector
98/// will have `num_points + 2` elements (including both endpoints).
99///
100/// # Panics
101///
102/// Panics if the input geometries have different numbers of atoms or different
103/// element types.
104///
105/// # Examples
106///
107/// ```rust
108/// use omecp::lst::{interpolate, InterpolationMethod};
109/// use omecp::geometry::Geometry;
110///
111/// let elements = vec!["H".to_string(), "H".to_string()];
112/// let coords1 = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
113/// let coords2 = vec![0.0, 0.0, 0.0, 2.0, 0.0, 0.0];
114///
115/// let geom1 = Geometry::new(elements.clone(), coords1);
116/// let geom2 = Geometry::new(elements, coords2);
117///
118/// // Generate 5 intermediate points using linear interpolation
119/// let path = interpolate(&geom1, &geom2, 5, InterpolationMethod::Linear);
120/// assert_eq!(path.len(), 7); // 5 intermediate + 2 endpoints
121/// ```
122///
123/// # Implementation Notes
124///
125/// - For [`InterpolationMethod::Linear`]: Uses Kabsch algorithm for optimal alignment
126/// - For [`InterpolationMethod::Quadratic`]: Automatically generates midpoint if needed
127/// - For [`InterpolationMethod::EnergyWeighted`]: Currently falls back to linear interpolation
128pub fn interpolate(
129    geom1: &Geometry,
130    geom2: &Geometry,
131    num_points: usize,
132    method: InterpolationMethod,
133) -> Vec<Geometry> {
134    match method {
135        InterpolationMethod::Linear => interpolate_linear(geom1, geom2, num_points),
136        InterpolationMethod::Quadratic => {
137            // For QST, we need a third geometry (midpoint approximation)
138            let midpoint = create_midpoint_geometry(geom1, geom2);
139            interpolate_quadratic(geom1, &midpoint, geom2, num_points)
140        }
141        InterpolationMethod::EnergyWeighted => {
142            // For now, fall back to linear (would need energy values)
143            interpolate_linear(geom1, geom2, num_points)
144        }
145    }
146}
147
148/// Performs Linear Synchronous Transit (LST) interpolation between two geometries.
149///
150/// This function implements the LST method using the Kabsch algorithm for optimal
151/// alignment of the geometries before interpolation. The Kabsch algorithm finds
152/// the optimal rotation matrix that minimizes the root-mean-square deviation
153/// between corresponding atoms.
154///
155/// # Arguments
156///
157/// * `geom1` - The starting geometry (will be aligned to geom2)
158/// * `geom2` - The target geometry (reference for alignment)
159/// * `num_points` - Number of intermediate points to generate
160///
161/// # Returns
162///
163/// Returns a `Vec<Geometry>` containing `num_points + 2` geometries, including
164/// the aligned starting geometry and the target geometry.
165///
166/// # Panics
167///
168/// Panics if the input geometries have different numbers of atoms.
169///
170/// # Algorithm Details
171///
172/// 1. **Alignment**: Uses Kabsch algorithm to find optimal rotation matrix
173/// 2. **Rotation**: Applies rotation to align geom1 with geom2
174/// 3. **Interpolation**: Linear interpolation between aligned geometries
175///
176/// The Kabsch algorithm steps:
177/// - Compute cross-covariance matrix R = Y * X^T
178/// - Compute R^T * R and its eigendecomposition
179/// - Construct rotation matrix U from eigenvectors and eigenvalues
180///
181/// # Examples
182///
183/// ```rust
184/// use omecp::lst::interpolate_linear;
185/// use omecp::geometry::Geometry;
186///
187/// let elements = vec!["C".to_string(), "H".to_string()];
188/// let coords1 = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
189/// let coords2 = vec![0.0, 0.0, 1.0, 1.0, 0.0, 1.0];
190///
191/// let geom1 = Geometry::new(elements.clone(), coords1);
192/// let geom2 = Geometry::new(elements, coords2);
193///
194/// let path = interpolate_linear(&geom1, &geom2, 3);
195/// assert_eq!(path.len(), 5); // 3 intermediate + 2 endpoints
196/// ```
197///
198/// # References
199///
200/// Kabsch, W. "A solution for the best rotation to relate two sets of vectors."
201/// *Acta Crystallographica Section A* **1976**, 32(5), 922-923.
202pub fn interpolate_linear(geom1: &Geometry, geom2: &Geometry, num_points: usize) -> Vec<Geometry> {
203    if geom1.num_atoms != geom2.num_atoms {
204        panic!("Geometries must have same number of atoms");
205    }
206
207    let n = geom1.num_atoms;
208
209    // Convert to 3xN matrices
210    let mut x = DMatrix::zeros(3, n);
211    let mut y = DMatrix::zeros(3, n);
212
213    for i in 0..n {
214        let coords1 = geom1.get_atom_coords(i);
215        let coords2 = geom2.get_atom_coords(i);
216        x[(0, i)] = coords1[0];
217        x[(1, i)] = coords1[1];
218        x[(2, i)] = coords1[2];
219        y[(0, i)] = coords2[0];
220        y[(1, i)] = coords2[1];
221        y[(2, i)] = coords2[2];
222    }
223
224    // Kabsch algorithm: find optimal rotation matrix
225    let r = &y * x.transpose();
226    let rt_r = r.transpose() * &r;
227    let eigen = rt_r.symmetric_eigen();
228
229    let mut mius = DMatrix::zeros(3, 3);
230    for i in 0..3 {
231        mius[(i, i)] = 1.0 / eigen.eigenvalues[i].sqrt();
232    }
233
234    let a = eigen.eigenvectors;
235    let b = &mius * (&r * &a).transpose();
236    let u = b.transpose() * &a;
237
238    // Apply rotation to first geometry
239    let x_aligned = &u * &x;
240
241    // Linear interpolation
242    let mut geometries = Vec::new();
243
244    for i in 0..=num_points {
245        let t = i as f64 / (num_points + 1) as f64;
246        let interp = &x_aligned * (1.0 - t) + &y * t;
247
248        let mut coords = Vec::new();
249        for j in 0..n {
250            coords.push(interp[(0, j)]);
251            coords.push(interp[(1, j)]);
252            coords.push(interp[(2, j)]);
253        }
254
255        geometries.push(Geometry::new(geom1.elements.clone(), coords));
256    }
257
258    geometries
259}
260
261/// Performs Quadratic Synchronous Transit (QST) interpolation using three geometries.
262///
263/// This function implements quadratic interpolation between three geometries:
264/// a starting point, an intermediate point, and an ending point. The resulting
265/// path follows a quadratic curve in coordinate space, which can provide a
266/// more realistic reaction path than linear interpolation.
267///
268/// # Arguments
269///
270/// * `geom1` - The starting geometry (t = 0)
271/// * `geom_mid` - The intermediate geometry (t = 0.5)
272/// * `geom2` - The ending geometry (t = 1)
273/// * `num_points` - Number of intermediate points to generate
274///
275/// # Returns
276///
277/// Returns a `Vec<Geometry>` containing `num_points + 2` geometries following
278/// a quadratic interpolation path.
279///
280/// # Panics
281///
282/// Panics if the input geometries have different numbers of atoms or different
283/// element types.
284///
285/// # Mathematical Formula
286///
287/// The quadratic interpolation formula used is:
288/// ```text
289/// p(t) = (1-t)² × p₁ + 2(1-t)t × p_mid + t² × p₂
290/// ```
291/// where t ∈ [0, 1] is the interpolation parameter.
292///
293/// # Examples
294///
295/// ```rust
296/// use omecp::lst::interpolate_quadratic;
297/// use omecp::geometry::Geometry;
298///
299/// let elements = vec!["O".to_string(), "H".to_string(), "H".to_string()];
300/// let coords1 = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
301/// let coords_mid = vec![0.0, 0.0, 0.2, 1.1, 0.0, 0.2, 0.0, 1.1, 0.2];
302/// let coords2 = vec![0.0, 0.0, 0.5, 1.2, 0.0, 0.5, 0.0, 1.2, 0.5];
303///
304/// let geom1 = Geometry::new(elements.clone(), coords1);
305/// let geom_mid = Geometry::new(elements.clone(), coords_mid);
306/// let geom2 = Geometry::new(elements, coords2);
307///
308/// let path = interpolate_quadratic(&geom1, &geom_mid, &geom2, 4);
309/// assert_eq!(path.len(), 6); // 4 intermediate + 2 endpoints
310/// ```
311///
312/// # Notes
313///
314/// - The intermediate geometry should represent a reasonable guess for the
315///   transition state or a point along the reaction coordinate
316/// - For best results, the intermediate geometry should be optimized or
317///   at least chemically reasonable
318/// - If no intermediate geometry is available, use [`create_midpoint_geometry`]
319///   to generate a simple midpoint structure
320pub fn interpolate_quadratic(
321    geom1: &Geometry,
322    geom_mid: &Geometry,
323    geom2: &Geometry,
324    num_points: usize,
325) -> Vec<Geometry> {
326    if geom1.num_atoms != geom2.num_atoms || geom1.num_atoms != geom_mid.num_atoms {
327        panic!("All geometries must have same number of atoms");
328    }
329
330    let _n = geom1.num_atoms;
331    let mut geometries = Vec::new();
332
333    // Convert all geometries to coordinate vectors
334    let coords1 = geometry_to_coords(geom1);
335    let coords_mid = geometry_to_coords(geom_mid);
336    let coords2 = geometry_to_coords(geom2);
337
338    for i in 0..=num_points {
339        let t = i as f64 / (num_points + 1) as f64;
340
341        // Quadratic interpolation: p(t) = (1-t)^2 * p1 + 2*(1-t)*t * p_mid + t^2 * p2
342        let mut coords = Vec::new();
343        for j in 0..coords1.len() {
344            let val = (1.0 - t).powi(2) * coords1[j]
345                + 2.0 * (1.0 - t) * t * coords_mid[j]
346                + t.powi(2) * coords2[j];
347            coords.push(val);
348        }
349
350        geometries.push(Geometry::new(geom1.elements.clone(), coords));
351    }
352
353    geometries
354}
355
356/// Creates a simple midpoint geometry for QST when only two geometries are provided.
357///
358/// This function generates an intermediate geometry by taking the arithmetic
359/// mean of corresponding atomic coordinates. While this provides a reasonable
360/// starting point for QST interpolation, it may not represent a chemically
361/// meaningful structure.
362///
363/// # Arguments
364///
365/// * `geom1` - The first endpoint geometry
366/// * `geom2` - The second endpoint geometry
367///
368/// # Returns
369///
370/// Returns a new `Geometry` with coordinates that are the arithmetic mean
371/// of the input geometries.
372///
373/// # Examples
374///
375/// ```rust
376/// use omecp::lst::create_midpoint_geometry;
377/// use omecp::geometry::Geometry;
378///
379/// let elements = vec!["H".to_string(), "H".to_string()];
380/// let coords1 = vec![0.0, 0.0, 0.0, 2.0, 0.0, 0.0];
381/// let coords2 = vec![0.0, 0.0, 0.0, 4.0, 0.0, 0.0];
382///
383/// let geom1 = Geometry::new(elements.clone(), coords1);
384/// let geom2 = Geometry::new(elements, coords2);
385///
386/// let midpoint = create_midpoint_geometry(&geom1, &geom2);
387/// let mid_coords = midpoint.get_atom_coords(1);
388/// assert_eq!(mid_coords[0], 3.0); // (2.0 + 4.0) / 2.0
389/// ```
390///
391/// # Notes
392///
393/// - The resulting geometry uses the same element types as the input geometries
394/// - This is a simple geometric midpoint and may not be chemically reasonable
395/// - For better results, consider using an optimized transition state guess
396/// - The midpoint geometry is primarily used as a fallback when no intermediate
397///   structure is available for QST interpolation
398fn create_midpoint_geometry(geom1: &Geometry, geom2: &Geometry) -> Geometry {
399    let coords1 = geometry_to_coords(geom1);
400    let coords2 = geometry_to_coords(geom2);
401
402    let mut mid_coords = Vec::new();
403    for i in 0..coords1.len() {
404        mid_coords.push((coords1[i] + coords2[i]) / 2.0);
405    }
406
407    Geometry::new(geom1.elements.clone(), mid_coords)
408}
409
410/// Converts a geometry to a flat coordinate vector.
411///
412/// This utility function extracts all atomic coordinates from a geometry
413/// and returns them as a single flat vector in the order [x₁, y₁, z₁, x₂, y₂, z₂, ...].
414/// This format is convenient for mathematical operations and interpolation.
415///
416/// # Arguments
417///
418/// * `geom` - The geometry to convert
419///
420/// # Returns
421///
422/// Returns a `Vec<f64>` containing all coordinates in flat format.
423/// The vector length will be `3 × num_atoms`.
424///
425/// # Examples
426///
427/// ```rust
428/// use omecp::geometry::Geometry;
429/// // Note: geometry_to_coords is private, this example shows the concept
430///
431/// let elements = vec!["H".to_string(), "O".to_string()];
432/// let coords = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
433/// let geom = Geometry::new(elements, coords.clone());
434///
435/// // The function would return: vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
436/// ```
437fn geometry_to_coords(geom: &Geometry) -> Vec<f64> {
438    let mut coords = Vec::new();
439    for i in 0..geom.num_atoms {
440        let atom_coords = geom.get_atom_coords(i);
441        coords.push(atom_coords[0]);
442        coords.push(atom_coords[1]);
443        coords.push(atom_coords[2]);
444    }
445    coords
446}
447
448/// Validates that interpolated geometries are chemically and numerically reasonable.
449///
450/// This function performs comprehensive validation of a set of geometries,
451/// checking for common issues that can arise during interpolation such as
452/// inconsistent atom counts, non-finite coordinates, and unreasonably large
453/// coordinate values.
454///
455/// # Arguments
456///
457/// * `geometries` - A slice of geometries to validate
458///
459/// # Returns
460///
461/// Returns `Ok(())` if all geometries pass validation, or `Err(String)` with
462/// a descriptive error message if any issues are found.
463///
464/// # Validation Checks
465///
466/// 1. **Non-empty**: Ensures at least one geometry is provided
467/// 2. **Consistent atom count**: All geometries have the same number of atoms
468/// 3. **Consistent elements**: All geometries have the same element types
469/// 4. **Finite coordinates**: No NaN or infinite coordinate values
470/// 5. **Reasonable coordinates**: No coordinates with absolute value > 1000 Angstrom
471///
472/// # Examples
473///
474/// ```rust
475/// use omecp::lst::{interpolate_linear, validate_geometries};
476/// use omecp::geometry::Geometry;
477///
478/// let elements = vec!["H".to_string(), "H".to_string()];
479/// let coords1 = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
480/// let coords2 = vec![0.0, 0.0, 0.0, 2.0, 0.0, 0.0];
481///
482/// let geom1 = Geometry::new(elements.clone(), coords1);
483/// let geom2 = Geometry::new(elements, coords2);
484///
485/// let path = interpolate_linear(&geom1, &geom2, 3);
486/// assert!(validate_geometries(&path).is_ok());
487/// ```
488///
489/// # Error Conditions
490///
491/// The function returns an error in the following cases:
492/// - Empty geometry vector
493/// - Inconsistent number of atoms between geometries
494/// - Different element types between geometries
495/// - Non-finite coordinates (NaN or infinite values)
496/// - Coordinates with absolute values exceeding 1000 Angstrom
497///
498/// # Notes
499///
500/// - The 1000 Angstrom limit is a safety check for obviously incorrect structures
501/// - This validation should be called after any interpolation operation
502/// - Failed validation often indicates issues with input geometries or
503///   numerical problems during interpolation
504pub fn validate_geometries(geometries: &[Geometry]) -> Result<(), String> {
505    if geometries.is_empty() {
506        return Err("No geometries to validate".to_string());
507    }
508
509    let num_atoms = geometries[0].num_atoms;
510    let elements = &geometries[0].elements;
511
512    for (i, geom) in geometries.iter().enumerate() {
513        if geom.num_atoms != num_atoms {
514            return Err(format!(
515                "Geometry {} has {} atoms, expected {}",
516                i, geom.num_atoms, num_atoms
517            ));
518        }
519
520        if geom.elements != *elements {
521            return Err(format!(
522                "Geometry {} has different elements than reference",
523                i
524            ));
525        }
526
527        // Check for NaN or infinite coordinates
528        for j in 0..geom.num_atoms {
529            let coords = geom.get_atom_coords(j);
530            for &coord in &coords {
531                if !coord.is_finite() {
532                    return Err(format!(
533                        "Geometry {} atom {} has non-finite coordinate: {}",
534                        i, j, coord
535                    ));
536                }
537            }
538        }
539
540        // Check for unreasonably large coordinates (> 1000 Angstrom)
541        for j in 0..geom.num_atoms {
542            let coords = geom.get_atom_coords(j);
543            for &coord in &coords {
544                if coord.abs() > 1000.0 {
545                    return Err(format!(
546                        "Geometry {} atom {} has unreasonably large coordinate: {}",
547                        i, j, coord
548                    ));
549                }
550            }
551        }
552    }
553
554    Ok(())
555}
556
557/// Calculates the total path length along an interpolated reaction coordinate.
558///
559/// This function computes the cumulative Euclidean distance between consecutive
560/// geometries in the interpolation path. The path length provides a measure of
561/// the total geometric change along the reaction coordinate and can be useful
562/// for analyzing reaction paths and comparing different interpolation methods.
563///
564/// # Arguments
565///
566/// * `geometries` - A slice of geometries representing the interpolation path
567///
568/// # Returns
569///
570/// Returns the total path length as a `f64` value in the same units as the
571/// input coordinates (typically Angstroms).
572///
573/// # Algorithm
574///
575/// The path length is calculated as:
576/// ```text
577/// L = Σᵢ √(Σⱼ (xᵢ₊₁,ⱼ - xᵢ,ⱼ)²)
578/// ```
579/// where the outer sum is over geometry pairs and the inner sum is over
580/// all coordinate components.
581///
582/// # Examples
583///
584/// ```rust
585/// use omecp::lst::{interpolate_linear, calculate_path_length};
586/// use omecp::geometry::Geometry;
587///
588/// let elements = vec!["H".to_string(), "H".to_string()];
589/// let coords1 = vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
590/// let coords2 = vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
591///
592/// let geom1 = Geometry::new(elements.clone(), coords1);
593/// let geom2 = Geometry::new(elements, coords2);
594///
595/// let path = interpolate_linear(&geom1, &geom2, 1);
596/// let length = calculate_path_length(&path);
597/// // Length should be approximately sqrt(2) ≈ 1.414
598/// ```
599///
600/// # Notes
601///
602/// - Returns 0.0 for empty or single-geometry paths
603/// - The path length depends on the coordinate system and units used
604/// - Longer paths may indicate more significant structural changes
605/// - Can be used to compare the "smoothness" of different interpolation methods
606pub fn calculate_path_length(geometries: &[Geometry]) -> f64 {
607    let mut total_length = 0.0;
608
609    for i in 1..geometries.len() {
610        let coords1 = geometry_to_coords(&geometries[i - 1]);
611        let coords2 = geometry_to_coords(&geometries[i]);
612
613        let mut segment_length = 0.0;
614        for j in 0..coords1.len() {
615            let diff = coords1[j] - coords2[j];
616            segment_length += diff * diff;
617        }
618        total_length += segment_length.sqrt();
619    }
620
621    total_length
622}
623
624/// Prints a formatted preview of interpolated geometries for interactive confirmation.
625///
626/// This function displays a summary of the interpolation results, including
627/// the total number of geometries, path length, and coordinate details for
628/// key geometries (first, middle, and last). This is useful for visual
629/// inspection before proceeding with expensive quantum chemistry calculations.
630///
631/// # Arguments
632///
633/// * `geometries` - A slice of geometries to preview
634///
635/// # Output Format
636///
637/// The function prints to stdout with the following information:
638/// - Total number of geometries in the path
639/// - Total path length in Angstroms
640/// - Coordinate details for first, middle, and last geometries
641/// - Element symbols and Cartesian coordinates for each atom
642/// - Truncation indicator if more than 5 atoms per geometry
643///
644/// # Examples
645///
646/// ```rust
647/// use omecp::lst::{interpolate_linear, print_geometry_preview};
648/// use omecp::geometry::Geometry;
649///
650/// let elements = vec!["H".to_string(), "H".to_string()];
651/// let coords1 = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
652/// let coords2 = vec![0.0, 0.0, 0.0, 2.0, 0.0, 0.0];
653///
654/// let geom1 = Geometry::new(elements.clone(), coords1);
655/// let geom2 = Geometry::new(elements, coords2);
656///
657/// let path = interpolate_linear(&geom1, &geom2, 3);
658/// print_geometry_preview(&path);
659/// ```
660///
661/// # Sample Output
662///
663/// ```text
664/// ****Geometry Preview****
665/// Total geometries: 5
666/// Path length: 1.000 Angstrom
667///
668/// --- Geometry 1 ---
669///  H    0.000    0.000    0.000
670///  H    1.000    0.000    0.000
671///
672/// --- Geometry 3 ---
673///  H    0.000    0.000    0.000
674///  H    1.500    0.000    0.000
675///
676/// --- Geometry 5 ---
677///  H    0.000    0.000    0.000
678///  H    2.000    0.000    0.000
679/// ```
680///
681/// # Notes
682///
683/// - Only shows first 5 atoms per geometry to keep output manageable
684/// - Coordinates are displayed with 3 decimal places
685/// - Useful for interactive workflows where user confirmation is needed
686/// - Should be called before expensive QM calculations on the path
687pub fn print_geometry_preview(geometries: &[Geometry]) {
688    println!("\n****Geometry Preview****");
689    println!("Total geometries: {}", geometries.len());
690    println!(
691        "Path length: {:.3} Angstrom",
692        calculate_path_length(geometries)
693    );
694
695    // Show first, middle, and last geometries
696    let indices = vec![0, geometries.len() / 2, geometries.len() - 1];
697
698    for &idx in &indices {
699        if idx < geometries.len() {
700            println!("\n--- Geometry {} ---", idx + 1);
701            let geom = &geometries[idx];
702            for i in 0..geom.num_atoms.min(5) {
703                // Show first 5 atoms
704                let coords = geom.get_atom_coords(i);
705                println!(
706                    "{:>2} {:>8.3} {:>8.3} {:>8.3}",
707                    geom.elements[i], coords[0], coords[1], coords[2]
708                );
709            }
710            if geom.num_atoms > 5 {
711                println!("... ({} more atoms)", geom.num_atoms - 5);
712            }
713        }
714    }
715}