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}