Skip to main content
← OpenMECP Documentation

omecp/
validation.rs

1//! Run mode validation and compatibility checking for OpenMECP.
2//!
3//! This module implements comprehensive validation of run mode and program
4//! combinations to ensure calculations are set up correctly and provide
5//! helpful error messages and warnings to users.
6//!
7//! # Features
8//!
9//! - Run mode and QM program compatibility validation
10//! - Wavefunction file existence checking
11//! - Clear error messages for invalid combinations
12//! - Enhanced user guidance and warnings
13//! - Logging for mode transitions and file operations
14
15use crate::config::{Config, QMProgram, RunMode};
16use std::path::Path;
17
18/// Result type for validation operations.
19pub type ValidationResult<T> = Result<T, ValidationError>;
20
21/// Comprehensive validation error with detailed user guidance.
22#[derive(Debug, Clone)]
23pub struct ValidationError {
24    /// Error category for programmatic handling
25    pub category: ErrorCategory,
26    /// Human-readable error message
27    pub message: String,
28    /// Optional suggestion for fixing the issue
29    pub suggestion: Option<String>,
30    /// Optional reference to documentation or examples
31    pub reference: Option<String>,
32}
33
34/// Categories of validation errors for better error handling.
35#[derive(Debug, Clone, PartialEq)]
36pub enum ErrorCategory {
37    /// Incompatible run mode and QM program combination
38    IncompatibleCombination,
39    /// Missing required wavefunction files
40    MissingWavefunctionFiles,
41    /// Invalid configuration parameters
42    InvalidConfiguration,
43    /// Missing required files or dependencies
44    MissingDependencies,
45    /// Unsupported feature for the selected program
46    UnsupportedFeature,
47}
48
49impl std::fmt::Display for ValidationError {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(f, "{}", self.message)?;
52        if let Some(suggestion) = &self.suggestion {
53            write!(f, "\n\nSuggestion: {}", suggestion)?;
54        }
55        if let Some(reference) = &self.reference {
56            write!(f, "\n\nFor more information: {}", reference)?;
57        }
58        Ok(())
59    }
60}
61
62impl std::error::Error for ValidationError {}
63
64/// Validates run mode compatibility with the selected QM program and configuration.
65///
66/// This function performs comprehensive validation 
67/// and provides detailed error messages and suggestions for fixing issues.
68///
69/// # Arguments
70///
71/// * `config` - The configuration to validate
72///
73/// # Returns
74///
75/// Returns `Ok(())` if the configuration is valid, or a `ValidationError` with
76/// detailed information about the issue and how to fix it.
77///
78/// # Examples
79///
80/// ```
81/// use omecp::config::{Config, QMProgram, RunMode};
82/// use omecp::validation::validate_run_mode_compatibility;
83///
84/// let mut config = Config::default();
85/// config.program = QMProgram::Gaussian;
86/// config.run_mode = RunMode::Read;
87///
88/// match validate_run_mode_compatibility(&config) {
89///     Ok(()) => println!("Configuration is valid"),
90///     Err(e) => println!("Validation error: {}", e),
91/// }
92/// ```
93pub fn validate_run_mode_compatibility(config: &Config) -> ValidationResult<()> {
94    // Check basic program/mode compatibility
95    validate_program_mode_compatibility(config)?;
96
97    // Check wavefunction file requirements
98    validate_wavefunction_files(config)?;
99
100    // Check program-specific requirements
101    validate_program_specific_requirements(config)?;
102
103    // Check run mode specific requirements
104    validate_run_mode_requirements(config)?;
105
106    Ok(())
107}
108
109/// Validates basic compatibility between QM program and run mode.
110fn validate_program_mode_compatibility(config: &Config) -> ValidationResult<()> {
111    match (config.program, config.run_mode) {
112        // ORCA-specific validations
113        (QMProgram::Orca, RunMode::Stable) => {
114            // ORCA stability mode has limitations with RI
115            if config.method.contains("RI") || config.method.contains("ri") {
116                return Err(ValidationError {
117                    category: ErrorCategory::IncompatibleCombination,
118                    message: "ORCA stability analysis is incompatible with RI (Resolution of Identity) approximations".to_string(),
119                    suggestion: Some("Use the 'read' mode instead and manually obtain the correct wavefunction, or remove RI keywords from the method".to_string()),
120                    reference: Some("See ORCA manual section on stability analysis for details".to_string()),
121                });
122            }
123        }
124
125        // XTB-specific validations
126        (QMProgram::Xtb, RunMode::Stable) => {
127            return Err(ValidationError {
128                category: ErrorCategory::UnsupportedFeature,
129                message: "XTB does not support explicit stability analysis".to_string(),
130                suggestion: Some("Use 'normal' or 'noread' mode for XTB calculations. XTB handles stability internally".to_string()),
131                reference: Some("XTB automatically uses stable wavefunctions".to_string()),
132            });
133        }
134
135        (QMProgram::Xtb, RunMode::InterRead) => {
136            return Err(ValidationError {
137                category: ErrorCategory::UnsupportedFeature,
138                message: "XTB does not support inter_read mode (no wavefunction files)".to_string(),
139                suggestion: Some("Use 'normal' or 'noread' mode for XTB calculations".to_string()),
140                reference: Some("XTB uses internal wavefunction handling".to_string()),
141            });
142        }
143
144        // BAGEL-specific validations
145        (QMProgram::Bagel, RunMode::Read) | (QMProgram::Bagel, RunMode::InterRead) => {
146            if config.bagel_model.is_empty() {
147                return Err(ValidationError {
148                    category: ErrorCategory::InvalidConfiguration,
149                    message: "BAGEL calculations require a model file specification".to_string(),
150                    suggestion: Some(
151                        "Set the 'bagel_model' parameter to point to your BAGEL JSON template file"
152                            .to_string(),
153                    ),
154                    reference: Some("See BAGEL documentation for model file format".to_string()),
155                });
156            }
157        }
158
159        // Custom program validations
160        (QMProgram::Custom, _) => {
161            if config.custom_interface_file.is_empty() {
162                return Err(ValidationError {
163                    category: ErrorCategory::InvalidConfiguration,
164                    message: "Custom QM program requires interface configuration file".to_string(),
165                    suggestion: Some("Set the 'custom_interface_file' parameter to point to your JSON interface configuration".to_string()),
166                    reference: Some("See OpenMECP documentation for custom interface format".to_string()),
167                });
168            }
169        }
170
171        // Coordinate driving validations
172        (_, RunMode::CoordinateDrive) => {
173            if config.drive_type.is_empty() || config.drive_atoms.is_empty() {
174                return Err(ValidationError {
175                    category: ErrorCategory::InvalidConfiguration,
176                    message: "Coordinate driving requires drive_type and drive_atoms parameters".to_string(),
177                    suggestion: Some("Set drive_type (bond/angle/dihedral) and drive_atoms (list of atom indices)".to_string()),
178                    reference: Some("See coordinate driving examples in OpenMECP documentation".to_string()),
179                });
180            }
181        }
182
183        // Path optimization validations
184        (_, RunMode::PathOptimization) => {
185            if config.drive_type.is_empty() || config.drive_atoms.is_empty() {
186                return Err(ValidationError {
187                    category: ErrorCategory::InvalidConfiguration,
188                    message: "Path optimization requires initial path specification via drive_type and drive_atoms".to_string(),
189                    suggestion: Some("Set drive_type and drive_atoms to define the initial reaction coordinate".to_string()),
190                    reference: Some("Path optimization uses coordinate driving to create the initial path".to_string()),
191                });
192            }
193        }
194
195        // FixDE mode validations
196        (_, RunMode::FixDE) => {
197            if config.fix_de == 0.0 {
198                return Err(ValidationError {
199                    category: ErrorCategory::InvalidConfiguration,
200                    message: "FixDE mode requires a target energy difference specification"
201                        .to_string(),
202                    suggestion: Some(
203                        "Set the 'fix_de' parameter to your target energy difference in eV"
204                            .to_string(),
205                    ),
206                    reference: Some(
207                        "FixDE mode constrains the energy difference to the specified value"
208                            .to_string(),
209                    ),
210                });
211            }
212        }
213
214        // All other combinations are valid
215        _ => {}
216    }
217
218    Ok(())
219}
220
221/// Validates wavefunction file requirements for read-based modes.
222fn validate_wavefunction_files(config: &Config) -> ValidationResult<()> {
223    match config.run_mode {
224        RunMode::Read | RunMode::InterRead => {
225            match config.program {
226                QMProgram::Gaussian => {
227                    // Check for Gaussian checkpoint files
228                    let chk_files = [
229                        "state_A.chk",
230                        "state_B.chk",
231                        "running_dir/state_A.chk",
232                        "running_dir/state_B.chk",
233                    ];
234                    let mut found_files = Vec::new();
235
236                    for file in &chk_files {
237                        if Path::new(file).exists() {
238                            found_files.push(*file);
239                        }
240                    }
241
242                    if found_files.is_empty() {
243                        return Err(ValidationError {
244                            category: ErrorCategory::MissingWavefunctionFiles,
245                            message: "Read mode requires Gaussian checkpoint files (state_A.chk, state_B.chk) but none were found".to_string(),
246                            suggestion: Some("Run a calculation in 'normal' or 'noread' mode first to generate checkpoint files, or switch to 'noread' mode".to_string()),
247                            reference: Some("Checkpoint files are created automatically during normal calculations".to_string()),
248                        });
249                    }
250
251                    println!("Found Gaussian checkpoint files: {:?}", found_files);
252                }
253
254                QMProgram::Orca => {
255                    // Check for ORCA wavefunction files
256                    let gbw_files = [
257                        "state_A.gbw",
258                        "state_B.gbw",
259                        "running_dir/state_A.gbw",
260                        "running_dir/state_B.gbw",
261                    ];
262                    let mut found_files = Vec::new();
263
264                    for file in &gbw_files {
265                        if Path::new(file).exists() {
266                            found_files.push(*file);
267                        }
268                    }
269
270                    if found_files.is_empty() {
271                        return Err(ValidationError {
272                            category: ErrorCategory::MissingWavefunctionFiles,
273                            message: "Read mode requires ORCA wavefunction files (state_A.gbw, state_B.gbw) but none were found".to_string(),
274                            suggestion: Some("Run a calculation in 'normal' or 'noread' mode first to generate .gbw files, or switch to 'noread' mode".to_string()),
275                            reference: Some("ORCA .gbw files contain the molecular orbitals and are created during calculations".to_string()),
276                        });
277                    }
278
279                    println!("Found ORCA wavefunction files: {:?}", found_files);
280                }
281
282                QMProgram::Xtb => {
283                    // XTB doesn't use persistent wavefunction files in the same way
284                    // This is handled in the program compatibility check
285                }
286
287                QMProgram::Bagel => {
288                    // BAGEL wavefunction handling is different - check model file instead
289                    if !Path::new(&config.bagel_model).exists() {
290                        return Err(ValidationError {
291                            category: ErrorCategory::MissingDependencies,
292                            message: format!("BAGEL model file '{}' not found", config.bagel_model),
293                            suggestion: Some(
294                                "Ensure the BAGEL model file path is correct and the file exists"
295                                    .to_string(),
296                            ),
297                            reference: Some(
298                                "BAGEL model files define the calculation template".to_string(),
299                            ),
300                        });
301                    }
302                }
303
304                QMProgram::Custom => {
305                    // Custom interface validation is handled elsewhere
306                    if !Path::new(&config.custom_interface_file).exists() {
307                        return Err(ValidationError {
308                            category: ErrorCategory::MissingDependencies,
309                            message: format!("Custom interface file '{}' not found", config.custom_interface_file),
310                            suggestion: Some("Ensure the custom interface file path is correct and the file exists".to_string()),
311                            reference: Some("Custom interface files define how to interact with your QM program".to_string()),
312                        });
313                    }
314                }
315            }
316        }
317
318        RunMode::Normal | RunMode::NoRead | RunMode::Stable => {
319            // These modes don't require existing wavefunction files
320        }
321
322        _ => {
323            // Other modes (CoordinateDrive, PathOptimization, FixDE) don't have specific wavefunction requirements
324        }
325    }
326
327    Ok(())
328}
329
330/// Validates program-specific requirements and configurations.
331fn validate_program_specific_requirements(config: &Config) -> ValidationResult<()> {
332    match config.program {
333        QMProgram::Gaussian => {
334            // Validate Gaussian-specific settings
335            if config.mp2 && config.method.contains("DFT") {
336                return Err(ValidationError {
337                    category: ErrorCategory::InvalidConfiguration,
338                    message: "MP2 flag is incompatible with DFT methods in Gaussian".to_string(),
339                    suggestion: Some(
340                        "Either remove the MP2 flag or use a wavefunction method like HF or MP2"
341                            .to_string(),
342                    ),
343                    reference: Some("MP2 is a post-HF method, not compatible with DFT".to_string()),
344                });
345            }
346        }
347
348        QMProgram::Orca => {
349            // Validate ORCA-specific settings
350            if config.mp2 {
351                println!("Warning: MP2 flag may not be applicable for ORCA calculations");
352            }
353        }
354
355        QMProgram::Bagel => {
356            // BAGEL requires model file
357            if config.bagel_model.is_empty() {
358                return Err(ValidationError {
359                    category: ErrorCategory::InvalidConfiguration,
360                    message: "BAGEL calculations require a model file specification".to_string(),
361                    suggestion: Some(
362                        "Set the 'bagel_model' parameter to your BAGEL JSON template file"
363                            .to_string(),
364                    ),
365                    reference: Some(
366                        "BAGEL model files define the quantum chemistry method and basis set"
367                            .to_string(),
368                    ),
369                });
370            }
371        }
372
373        QMProgram::Xtb => {
374            // XTB has limited method options
375            if !config.method.is_empty() && !config.method.contains("GFN") {
376                println!("Warning: XTB typically uses GFN methods (GFN1-xTB, GFN2-xTB). Method '{}' may not be recognized", config.method);
377            }
378        }
379
380        QMProgram::Custom => {
381            // Custom program validation is handled in compatibility check
382        }
383    }
384
385    Ok(())
386}
387
388/// Validates run mode specific requirements.
389fn validate_run_mode_requirements(config: &Config) -> ValidationResult<()> {
390    match config.run_mode {
391        RunMode::InterRead => {
392            // Inter-read mode is specifically for open-shell singlets
393            if config.mult_state_a != 1 || config.mult_state_b != 1 {
394                println!("Warning: Inter-read mode is typically used for open-shell singlet calculations (mult_state_a=1, mult_state_b=1)");
395                println!(
396                    "Current multiplicities: mult_state_a={}, mult_state_b={}",
397                    config.mult_state_a, config.mult_state_b
398                );
399            }
400        }
401
402        RunMode::Stable => {
403            // Stability mode warnings
404            match config.program {
405                QMProgram::Orca => {
406                    println!("ORCA Stability Mode Guidance:");
407                    println!(
408                        "- RHF calculations will not restart automatically if instability is found"
409                    );
410                    println!("- Remember to use UKS for singlet state calculations");
411                    println!("- RI approximations are not supported in stability analysis");
412                    println!("- Consider using 'read' mode with manually converged wavefunctions for RI calculations");
413                }
414                QMProgram::Gaussian => {
415                    println!(
416                        "Gaussian Stability Mode: Will automatically handle wavefunction stability"
417                    );
418                }
419                _ => {}
420            }
421        }
422
423        RunMode::CoordinateDrive => {
424            // Validate coordinate driving parameters
425            if config.drive_start == config.drive_end {
426                return Err(ValidationError {
427                    category: ErrorCategory::InvalidConfiguration,
428                    message: "Coordinate driving requires different start and end values".to_string(),
429                    suggestion: Some("Set drive_start and drive_end to different values to define the scan range".to_string()),
430                    reference: Some("Coordinate driving varies a parameter from start to end value".to_string()),
431                });
432            }
433
434            if config.drive_steps == 0 {
435                return Err(ValidationError {
436                    category: ErrorCategory::InvalidConfiguration,
437                    message: "Coordinate driving requires at least one step".to_string(),
438                    suggestion: Some(
439                        "Set drive_steps to a positive integer (typically 10-50)".to_string(),
440                    ),
441                    reference: Some(
442                        "More steps give higher resolution but take longer".to_string(),
443                    ),
444                });
445            }
446        }
447
448        _ => {
449            // Other modes don't have specific additional requirements
450        }
451    }
452
453    Ok(())
454}
455
456/// Provides enhanced user guidance and warnings for specific configurations.
457///
458/// This function prints helpful information and warnings to guide users
459/// toward optimal configurations and avoid common pitfalls.
460///
461/// # Arguments
462///
463/// * `config` - The configuration to provide guidance for
464pub fn provide_user_guidance(config: &Config) {
465    println!("\n****Configuration Guidance****");
466
467    // Program-specific guidance
468    match config.program {
469        QMProgram::Orca => {
470            if config.run_mode == RunMode::InterRead {
471                println!("ORCA Inter-Read Mode Guidance:");
472                println!("- The inter_read mode is set for ORCA");
473                println!(
474                    "- Unlike Gaussian, ORCA will not automatically add guess=mix for state A"
475                );
476                println!("- For open-shell singlet convergence, add convergence control keywords to your tail section:");
477                println!("  %scf");
478                println!("    MaxIter 200");
479                println!("    ConvForced true");
480                println!("  end");
481            }
482
483            if config.run_mode == RunMode::Stable {
484                println!("ORCA Stability Mode Limitations:");
485                println!("- RI approximations are not supported in stability analysis");
486                println!("- Consider using 'read' mode with pre-converged wavefunctions for RI calculations");
487                println!("- UKS is recommended for singlet state calculations");
488            }
489        }
490
491        QMProgram::Gaussian => {
492            if config.run_mode == RunMode::InterRead {
493                println!("Gaussian Inter-Read Mode:");
494                println!("- Will automatically add guess=(read,mix) for state A");
495                println!("- Optimal for open-shell singlet calculations");
496            }
497        }
498
499        QMProgram::Xtb => {
500            println!("XTB Calculation Notes:");
501            println!("- XTB handles wavefunction stability internally");
502            println!("- Use 'normal' or 'noread' modes for best performance");
503            println!("- Method should typically be GFN1-xTB or GFN2-xTB");
504        }
505
506        _ => {}
507    }
508
509    // Run mode specific guidance
510    match config.run_mode {
511        RunMode::Normal => {
512            println!("Normal Mode: Balanced speed and robustness with checkpoint reading");
513        }
514        RunMode::Read => {
515            println!("Read Mode: Fast restart using existing wavefunction files");
516        }
517        RunMode::NoRead => {
518            println!("NoRead Mode: Fresh SCF at each step - slower but more robust");
519        }
520        RunMode::Stable => {
521            println!("Stable Mode: Includes wavefunction stability analysis");
522        }
523        RunMode::InterRead => {
524            println!("Inter-Read Mode: Optimized for open-shell singlet calculations");
525        }
526        _ => {}
527    }
528
529    println!("****End Configuration Guidance****\n");
530}
531
532/// Logs mode transitions and file operations for debugging and user information.
533///
534/// # Arguments
535///
536/// * `from_mode` - The original run mode
537/// * `to_mode` - The new run mode after transition
538/// * `reason` - Reason for the mode transition
539pub fn log_mode_transition(from_mode: RunMode, to_mode: RunMode, reason: &str) {
540    if from_mode != to_mode {
541        println!("****Mode Transition: {:?} -> {:?}****", from_mode, to_mode);
542        println!("Reason: {}", reason);
543
544        match (from_mode, to_mode) {
545            (RunMode::Stable, RunMode::Read) => {
546                println!("Stability analysis completed, switching to read mode for optimization");
547            }
548            (RunMode::InterRead, RunMode::Read) => {
549                println!(
550                    "Inter-read initialization completed, switching to read mode for optimization"
551                );
552            }
553            _ => {}
554        }
555
556        println!("****Mode Transition Complete****\n");
557    }
558}
559
560/// Validates and logs file operations for wavefunction management.
561///
562/// # Arguments
563///
564/// * `operation` - Description of the file operation
565/// * `source` - Source file path
566/// * `destination` - Destination file path (optional)
567/// * `print_level` - Print level (0=quiet, 1=normal, 2=verbose)
568pub fn log_file_operation(
569    operation: &str,
570    source: &str,
571    destination: Option<&str>,
572    print_level: u32,
573) {
574    // Only print file operations if print_level is 2 (verbose)
575    if print_level >= 2 {
576        match destination {
577            Some(dest) => {
578                println!("File Operation: {} - {} -> {}", operation, source, dest);
579                if !Path::new(source).exists() {
580                    println!("Warning: Source file '{}' does not exist", source);
581                }
582            }
583            None => {
584                println!("File Operation: {} - {}", operation, source);
585                if !Path::new(source).exists() {
586                    println!("Warning: File '{}' does not exist", source);
587                }
588            }
589        }
590    }
591}
592
593/// Logs file operations for debugging and validation purposes (legacy version).
594///
595/// This function maintains backward compatibility by defaulting to verbose output.
596/// New code should use `log_file_operation` with explicit print_level.
597///
598/// # Arguments
599///
600/// * `operation` - Description of the file operation
601/// * `source` - Source file path
602/// * `destination` - Destination file path (optional)
603#[deprecated(note = "Use log_file_operation with explicit print_level parameter")]
604pub fn log_file_operation_legacy(operation: &str, source: &str, destination: Option<&str>) {
605    log_file_operation(operation, source, destination, 2); // Default to verbose for backward compatibility
606}