1use crate::config::{Config, QMProgram, RunMode};
16use std::path::Path;
17
18pub type ValidationResult<T> = Result<T, ValidationError>;
20
21#[derive(Debug, Clone)]
23pub struct ValidationError {
24 pub category: ErrorCategory,
26 pub message: String,
28 pub suggestion: Option<String>,
30 pub reference: Option<String>,
32}
33
34#[derive(Debug, Clone, PartialEq)]
36pub enum ErrorCategory {
37 IncompatibleCombination,
39 MissingWavefunctionFiles,
41 InvalidConfiguration,
43 MissingDependencies,
45 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
64pub fn validate_run_mode_compatibility(config: &Config) -> ValidationResult<()> {
94 validate_program_mode_compatibility(config)?;
96
97 validate_wavefunction_files(config)?;
99
100 validate_program_specific_requirements(config)?;
102
103 validate_run_mode_requirements(config)?;
105
106 Ok(())
107}
108
109fn validate_program_mode_compatibility(config: &Config) -> ValidationResult<()> {
111 match (config.program, config.run_mode) {
112 (QMProgram::Orca, RunMode::Stable) => {
114 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 (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 (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 (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 (_, 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 (_, 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 (_, 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 _ => {}
216 }
217
218 Ok(())
219}
220
221fn validate_wavefunction_files(config: &Config) -> ValidationResult<()> {
223 match config.run_mode {
224 RunMode::Read | RunMode::InterRead => {
225 match config.program {
226 QMProgram::Gaussian => {
227 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 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 }
286
287 QMProgram::Bagel => {
288 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 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 }
321
322 _ => {
323 }
325 }
326
327 Ok(())
328}
329
330fn validate_program_specific_requirements(config: &Config) -> ValidationResult<()> {
332 match config.program {
333 QMProgram::Gaussian => {
334 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 if config.mp2 {
351 println!("Warning: MP2 flag may not be applicable for ORCA calculations");
352 }
353 }
354
355 QMProgram::Bagel => {
356 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 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 }
383 }
384
385 Ok(())
386}
387
388fn validate_run_mode_requirements(config: &Config) -> ValidationResult<()> {
390 match config.run_mode {
391 RunMode::InterRead => {
392 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 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 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 }
451 }
452
453 Ok(())
454}
455
456pub fn provide_user_guidance(config: &Config) {
465 println!("\n****Configuration Guidance****");
466
467 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 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
532pub 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
560pub fn log_file_operation(
569 operation: &str,
570 source: &str,
571 destination: Option<&str>,
572 print_level: u32,
573) {
574 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#[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); }