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