Skip to main content
← OpenMECP Documentation

omecp/
io.rs

1//! File I/O utilities for geometry and checkpoint files.
2//!
3//! This module provides functions for reading and writing molecular geometries
4//! in various formats including XYZ, Gaussian input/output, and checkpoint files.
5
6use crate::geometry::Geometry;
7use std::fs;
8use std::io::Result;
9use std::path::Path;
10
11/// Writes a molecular geometry to an XYZ file.
12///
13/// The XYZ format is a simple plain-text format for molecular geometries,
14/// widely used in chemistry software. It consists of:
15/// 1. Number of atoms
16/// 2. A comment line (empty in this implementation)
17/// 3. Lines for each atom: Element X Y Z
18///
19/// # Arguments
20///
21/// * `geom` - The molecular geometry to write
22/// * `path` - The path to the output XYZ file
23///
24/// # Returns
25///
26/// Returns `Ok(())` on success, or an `std::io::Error` if file writing fails.
27///
28/// # Examples
29///
30/// ```
31/// use omecp::geometry::Geometry;
32/// use omecp::io;
33/// use std::path::Path;
34///
35/// fn main() -> std::io::Result<()> {
36///     let elements = vec!["C".to_string(), "H".to_string()];
37///     let coords = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
38///     let geometry = Geometry::new(elements, coords);
39///
40///     io::write_xyz(&geometry, Path::new("molecule.xyz"))?;
41///     std::fs::remove_file("molecule.xyz")?;
42///     Ok(())
43/// }
44/// ```
45pub fn write_xyz(geom: &Geometry, path: &Path) -> Result<()> {
46    let mut content = format!("{}\n\n", geom.num_atoms);
47
48    for i in 0..geom.num_atoms {
49        let coords = geom.get_atom_coords(i);
50        // Coordinates are already in Angstrom - write directly
51        content.push_str(&format!(
52            "{}  {:.8}  {:.8}  {:.8}\n",
53            geom.elements[i], coords[0], coords[1], coords[2]
54        ));
55    }
56
57    fs::write(path, content)
58}
59
60/// Cleans Gaussian keywords by removing comments and extra whitespace.
61///
62/// This function processes multi-line keyword strings to remove:
63/// - Lines starting with '#' (full-line comments)
64/// - Inline comments (text after '#' on the same line as valid keywords)
65/// - Empty lines
66/// - Leading and trailing whitespace from each line
67///
68/// The remaining valid keywords are joined with single spaces to create
69/// a clean string suitable for Gaussian route sections. If all content
70/// is filtered out (e.g., only comments), an empty string is returned,
71/// which is handled gracefully by the Gaussian header generation.
72///
73/// # Arguments
74///
75/// * `keywords` - The raw keyword string that may contain comments and extra whitespace
76///
77/// # Returns
78///
79/// Returns a `String` containing cleaned keywords joined with single spaces.
80/// Returns an empty string if no valid keywords remain after filtering.
81///
82/// # Examples
83///
84/// ```
85/// use omecp::io;
86///
87/// let raw_keywords = "# This is a comment\nTD(NStates=5)\n# Another comment\nRoot=1\n\n";
88/// let cleaned = io::clean_gaussian_keywords(raw_keywords);
89/// assert_eq!(cleaned, "TD(NStates=5) Root=1");
90///
91/// // Empty result when only comments are present
92/// let only_comments = "# Only comments\n# More comments";
93/// let empty_result = io::clean_gaussian_keywords(only_comments);
94/// assert_eq!(empty_result, "");
95///
96/// // Inline comments are removed
97/// let inline_comments = "TD(NStates=5) # This is an inline comment\nRoot=1 # Another inline comment";
98/// let cleaned_inline = io::clean_gaussian_keywords(inline_comments);
99/// assert_eq!(cleaned_inline, "TD(NStates=5) Root=1");
100/// ```
101pub fn clean_gaussian_keywords(keywords: &str) -> String {
102    let result = keywords
103        .lines()
104        .filter_map(|line| {
105            let trimmed = line.trim();
106
107            // Skip empty lines
108            if trimmed.is_empty() {
109                return None;
110            }
111
112            // Skip lines that start with '#' (full-line comments)
113            if trimmed.starts_with('#') {
114                return None;
115            }
116
117            // Remove inline comments (everything after '#' on the same line)
118            let cleaned_line = if let Some(comment_pos) = trimmed.find('#') {
119                trimmed[..comment_pos].trim()
120            } else {
121                trimmed
122            };
123
124            // Return the cleaned line if it's not empty after comment removal
125            if cleaned_line.is_empty() {
126                None
127            } else {
128                Some(cleaned_line)
129            }
130        })
131        .collect::<Vec<_>>()
132        .join(" ");
133
134    // The result may be empty if all content was filtered out (e.g., only comments)
135    // This is acceptable and will be handled gracefully by the caller
136    result
137}
138
139/// Cleans keywords by removing comments and extra whitespace (generic version).
140///
141/// This function works for any quantum chemistry program by removing:
142/// - Lines starting with '#' (full-line comments)
143/// - Inline comments (text after '#' on the same line as valid keywords)
144/// - Empty lines
145/// - Leading and trailing whitespace from each line
146///
147/// The remaining valid keywords are joined with single spaces.
148///
149/// # Arguments
150///
151/// * `keywords` - The raw keyword string that may contain comments and extra whitespace
152///
153/// # Returns
154///
155/// Returns a `String` containing cleaned keywords joined with single spaces.
156pub fn clean_keywords(keywords: &str) -> String {
157    keywords
158        .lines()
159        .filter_map(|line| {
160            let trimmed = line.trim();
161
162            // Skip empty lines
163            if trimmed.is_empty() {
164                return None;
165            }
166
167            // Skip lines that start with '#' (full-line comments)
168            if trimmed.starts_with('#') {
169                return None;
170            }
171
172            // Remove inline comments (everything after '#' on the same line)
173            let cleaned_line = if let Some(comment_pos) = trimmed.find('#') {
174                trimmed[..comment_pos].trim()
175            } else {
176                trimmed
177            };
178
179            // Return the cleaned line if it's not empty after comment removal
180            if cleaned_line.is_empty() {
181                None
182            } else {
183                Some(cleaned_line)
184            }
185        })
186        .collect::<Vec<_>>()
187        .join(" ")
188}
189
190/// Builds a Gaussian input file header string (legacy interface).
191///
192/// This function is maintained for backward compatibility. New code should use
193/// `build_program_header()` which includes dynamic method modification.
194///
195/// # Arguments
196///
197/// * `config` - The global configuration for the MECP calculation
198/// * `charge` - Molecular charge for the current state
199/// * `mult` - Spin multiplicity for the current state
200/// * `td` - TD-DFT keywords (e.g., "TD(NStates=5,Root=1)"), may contain comments
201///
202/// # Returns
203///
204/// Returns a `String` containing the formatted Gaussian input header with clean route section.
205pub fn build_gaussian_header(
206    config: &crate::config::Config,
207    charge: i32,
208    mult: usize,
209    td: &str,
210) -> String {
211    // Use the dynamic method modification for consistency
212    let modified_method =
213        modify_method_for_run_mode(&config.method, config.program, config.run_mode);
214
215    let mut temp_config = config.clone();
216    temp_config.method = modified_method;
217
218    build_gaussian_header_internal(&temp_config, charge, mult, td)
219}
220
221/// Internal Gaussian header builder that doesn't modify the method string.
222///
223/// This function constructs the route section and title card for a Gaussian
224/// input file based on the provided configuration and state-specific parameters.
225/// It assumes the method string has already been modified by `modify_method_for_run_mode()`.
226///
227/// # Arguments
228///
229/// * `config` - The global configuration with pre-modified method string
230/// * `charge` - Molecular charge for the current state
231/// * `mult` - Spin multiplicity for the current state
232/// * `td` - TD-DFT keywords (e.g., "TD(NStates=5,Root=1)"), may contain comments
233///
234/// # Returns
235///
236/// Returns a `String` containing the formatted Gaussian input header.
237fn build_gaussian_header_internal(
238    config: &crate::config::Config,
239    charge: i32,
240    mult: usize,
241    td: &str,
242) -> String {
243    build_gaussian_header_internal_with_chk(config, charge, mult, td, "calc.chk")
244}
245
246fn build_gaussian_header_internal_with_chk(
247    config: &crate::config::Config,
248    charge: i32,
249    mult: usize,
250    td: &str,
251    chk_file: &str,
252) -> String {
253    // Use the method string as-is (already modified by modify_method_for_run_mode)
254    let method_str = &config.method;
255
256    // Clean TD-DFT keywords to remove comments and extra whitespace
257    let clean_td = clean_gaussian_keywords(td);
258
259    // Build route section 
260    let route_section = if clean_td.is_empty() {
261        format!("# {} nosymm", method_str)
262    } else {
263        format!("# {} {} nosymm", method_str, clean_td)
264    };
265
266    format!(
267        "%chk={}\n%nprocshared={} \n%mem={} \n{}\n\n Title Card \n\n{} {}",
268        chk_file, config.nprocs, config.mem, route_section, charge, mult
269    )
270}
271
272/// Builds an ORCA input file header string with basename.
273///
274/// This function constructs the header for an ORCA input file based on the
275/// provided configuration and state-specific parameters. It requires a
276/// basename for proper .gbw file path construction.
277///
278/// # Arguments
279///
280/// * `config` - The global configuration for the MECP calculation
281/// * `charge` - Molecular charge for the current state
282/// * `mult` - Spin multiplicity for the current state
283/// * `tail` - Additional ORCA keywords (tail section content)
284/// * `input_basename` - Full path prefix for .gbw files
285///
286/// # Returns
287///
288/// Returns a `String` containing the formatted ORCA input header.
289
290pub fn build_orca_header(
291    config: &crate::config::Config,
292    charge: i32,
293    mult: usize,
294    tail: &str,
295    input_basename: &str,
296) -> String {
297    // Use the dynamic method modification for consistency
298    let modified_method =
299        modify_method_for_run_mode(&config.method, config.program, config.run_mode);
300
301    let mut temp_config = config.clone();
302    temp_config.method = modified_method;
303
304    build_orca_header_internal(&temp_config, charge, mult, tail, input_basename, None)
305}
306
307
308fn build_orca_header_internal(
309    config: &crate::config::Config,
310    charge: i32,
311    mult: usize,
312    tail: &str,
313    input_basename: &str,
314    chk_file: Option<&str>,
315) -> String {
316    // Use the method string as-is (already modified by modify_method_for_run_mode)
317    let method_str = &config.method;
318
319    // Clean tail keywords to remove comments
320    let clean_tail = clean_keywords(tail);
321
322    // Build the method line
323    let method_line = if clean_tail.is_empty() {
324        format!("! {}", method_str)
325    } else {
326        format!("! {} {}", method_str, clean_tail)
327    };
328
329    // If chk_file is provided (e.g. for chain-linking steps), use it directly.
330    // Otherwise, fall back to constructing it from input_basename.
331    let method_line = if method_line.contains("***") {
332        let gbw_file = if let Some(chk) = chk_file {
333            chk.to_string()
334        } else if mult == config.mult_state_a {
335            format!("{}_state_A.gbw", input_basename)
336        } else {
337            format!("{}_state_B.gbw", input_basename)
338        };
339        method_line.replace("***", &gbw_file)
340    } else {
341        method_line
342    };
343
344    format!(
345        "%pal nprocs {} end\n%maxcore {} \n{}\n\n *xyz {} {}",
346        config.nprocs, config.mem, method_line, charge, mult
347    )
348}
349
350/// Builds an XTB input file header string.
351///
352/// XTB uses a simple format with just charge and multiplicity information.
353/// The method is typically specified via command line arguments.
354///
355/// # Arguments
356///
357/// * `config` - The global configuration for the MECP calculation
358/// * `charge` - Molecular charge for the current state
359/// * `mult` - Spin multiplicity for the current state
360/// * `_tail` - Additional keywords (unused for XTB)
361///
362/// # Returns
363///
364/// Returns a `String` containing the formatted XTB input header.
365pub fn build_xtb_header(
366    _config: &crate::config::Config,
367    charge: i32,
368    mult: usize,
369    _tail: &str,
370) -> String {
371    // XTB uses a simple format - just charge and multiplicity
372    // The method is specified via command line arguments
373    format!("$chrg {}\n$uhf {}", charge, mult - 1)
374}
375
376/// Builds a BAGEL input file header string.
377///
378/// BAGEL uses JSON format and requires a model file. This function creates
379/// the basic structure that will be filled with geometry and other parameters.
380///
381/// # Arguments
382///
383/// * `config` - The global configuration for the MECP calculation
384/// * `charge` - Molecular charge for the current state
385/// * `mult` - Spin multiplicity for the current state
386/// * `state` - Electronic state index for multireference calculations
387///
388/// # Returns
389///
390/// Returns a `String` containing the formatted BAGEL input header.
391pub fn build_bagel_header(
392    config: &crate::config::Config,
393    charge: i32,
394    mult: usize,
395    state: usize,
396) -> String {
397    // BAGEL uses JSON format - this is a basic template
398    // The actual geometry will be inserted by the writeBAGEL equivalent function
399    let basis = if config.basis_set.is_empty() {
400        "cc-pVDZ"
401    } else {
402        &config.basis_set
403    };
404    let df_basis = if config.basis_set.is_empty() {
405        "cc-pVDZ-jkfit".to_string()
406    } else {
407        format!("{}-jkfit", config.basis_set)
408    };
409
410    format!(
411        r#"{{
412  "bagel" : [
413    {{
414      "title" : "molecule",
415      "basis" : "{}",
416      "df_basis" : "{}",
417      "charge" : {},
418      "nspin" : {},
419      "target" : {},
420      "geometry" : [
421        // Geometry will be inserted here
422      ]
423    }}
424  ]
425}}"#,
426        basis,
427        df_basis,
428        charge,
429        mult - 1, // nspin = 2S where mult = 2S+1
430        state
431    )
432}
433
434/// Dynamically modifies a QM method string based on run mode and program.
435///
436/// This function implements the core logic,
437/// adding program-specific keywords and run mode-specific modifications to the method string.
438/// This ensures that calculations use the correct keywords for each scenario.
439///
440/// # Method Modification Logic
441///
442/// 1. **Syntax Correction** (added for ORCA):
443///    - Replaces Gaussian-style basis set separators (`/`) with spaces.
444///    - E.g., "B3LYP/6-31G*" -> "B3LYP 6-31G*"
445///
446/// 2. **Program-specific keywords** (always added):
447///    - Gaussian: `force` (for gradient calculations)
448///    - ORCA: `engrad` (for energy and gradient calculations)
449///    - XTB/BAGEL: No modification needed
450///
451/// 3. **Stability keywords** (added for `Stable` mode):
452///    - Gaussian: `stable=opt` (perform stability analysis and reoptimize if unstable)
453///    - ORCA: `%scf stabperform true StabRestartUHFifUnstable true end` (stability analysis)
454///
455/// 4. **Guess keywords** (added for all modes except `NoRead`):
456///    - Gaussian: `guess=read` (read initial guess from checkpoint)
457///    - ORCA: `!moread` with `%moinp "***"` (read molecular orbitals)
458///
459/// # Arguments
460///
461/// * `method` - The base QM method string (e.g., "B3LYP/6-31G*")
462/// * `program` - The quantum chemistry program being used
463/// * `run_mode` - The execution mode for the calculation
464///
465/// # Returns
466///
467/// Returns a `String` containing the modified method with appropriate keywords added.
468///
469/// # Examples
470///
471/// ```
472/// use omecp::config::{QMProgram, RunMode};
473/// use omecp::io;
474///
475/// // Normal mode with Gaussian
476/// let modified = io::modify_method_for_run_mode("B3LYP/6-31G*", QMProgram::Gaussian, RunMode::Normal);
477/// assert_eq!(modified, "B3LYP/6-31G* force guess=read");
478///
479/// // Normal mode with ORCA (handling Gaussian syntax)
480/// let modified = io::modify_method_for_run_mode("B3LYP/6-31G*", QMProgram::Orca, RunMode::Normal);
481/// assert!(modified.contains("B3LYP 6-31G*"));
482/// assert!(modified.contains("engrad"));
483/// ```
484pub fn modify_method_for_run_mode(
485    method: &str,
486    program: crate::config::QMProgram,
487    run_mode: crate::config::RunMode,
488) -> String {
489    let mut modified_method = method.to_string();
490
491    // Fix Gaussian-style method/basis syntax (e.g., "B3LYP/6-31G*") for ORCA
492    if program == crate::config::QMProgram::Orca && modified_method.contains('/') {
493        modified_method = modified_method.replace('/', " ");
494    }
495
496    // Add program-specific keywords
497    match program {
498        crate::config::QMProgram::Gaussian | crate::config::QMProgram::Custom => {
499            if !modified_method.is_empty() {
500                modified_method.push_str(" force");
501            }
502        }
503        crate::config::QMProgram::Orca => {
504            if !modified_method.is_empty() {
505                modified_method.push_str(" engrad");
506            }
507        }
508        // XTB and BAGEL don't need method modification
509        _ => {}
510    }
511
512    // Add stability keywords for stable mode
513    if run_mode == crate::config::RunMode::Stable && !modified_method.is_empty() {
514        match program {
515            crate::config::QMProgram::Gaussian | crate::config::QMProgram::Custom => {
516                modified_method.push_str(" stable=opt");
517            }
518            crate::config::QMProgram::Orca => {
519                modified_method
520                    .push_str("\n %scf stabperform true StabRestartUHFifUnstable true end \n");
521            }
522            _ => {}
523        }
524    }
525
526    // Add guess keywords
527    if run_mode != crate::config::RunMode::NoRead && !modified_method.is_empty() {
528        match program {
529            crate::config::QMProgram::Gaussian | crate::config::QMProgram::Custom => {
530                modified_method.push_str(" guess=read");
531            }
532            crate::config::QMProgram::Orca => {
533                modified_method.push_str("\n!moread \n %moinp \"***\"\n");
534            }
535            _ => {}
536        }
537    }
538
539    modified_method
540}
541
542/// Builds a program-specific input file header string.
543///
544/// This function dispatches to the appropriate header building function
545/// based on the quantum chemistry program specified in the configuration.
546/// It now uses dynamic method modification to ensure run mode compatibility.
547///
548/// **Note**: For ORCA programs, this function will panic if no input basename is provided.
549/// Use `build_program_header_with_basename()` instead for ORCA calculations.
550///
551/// # Arguments
552///
553/// * `config` - The global configuration for the MECP calculation
554/// * `charge` - Molecular charge for the current state
555/// * `mult` - Spin multiplicity for the current state
556/// * `td_or_tail` - TD-DFT keywords (Gaussian) or tail section content (other programs)
557/// * `state` - Electronic state index (used for BAGEL)
558///
559/// # Returns
560///
561/// Returns a `String` containing the formatted input header for the specified program.
562///
563/// # Panics
564///
565/// Panics if `config.program` is `QMProgram::Orca` since ORCA requires an input basename.
566///
567/// # Examples
568///
569/// ```
570/// use omecp::config::{Config, QMProgram, RunMode};
571/// use omecp::io;
572///
573/// let mut config = Config::default();
574/// config.program = QMProgram::Gaussian; // Works for Gaussian
575/// config.method = "B3LYP/6-31G*".to_string();
576/// config.run_mode = RunMode::Normal;
577///
578/// let header = io::build_program_header(&config, 0, 1, "", 0);
579/// println!("{}", header);
580/// ```
581pub fn build_program_header(
582    config: &crate::config::Config,
583    charge: i32,
584    mult: usize,
585    td_or_tail: &str,
586    state: usize,
587) -> String {
588    if config.program == crate::config::QMProgram::Orca {
589        panic!("ORCA requires input basename for .gbw file paths. Use build_program_header_with_basename() instead.");
590    }
591    build_program_header_with_chk(config, charge, mult, td_or_tail, state, None, None)
592}
593
594/// Builds a program-specific input file header string with input basename for ORCA .gbw paths.
595///
596/// This function is specifically designed for cases where you need to specify the basename
597/// of the input file for ORCA calculations, which is used to construct proper .gbw file paths
598/// (e.g., "calc/state_A.gbw" instead of "running_dir/state_A.gbw").
599///
600/// # Arguments
601///
602/// * `config` - The global configuration for the MECP calculation
603/// * `charge` - Molecular charge for the current state
604/// * `mult` - Spin multiplicity for the current state
605/// * `td_or_tail` - TD-DFT keywords (Gaussian) or tail section content (ORCA)
606/// * `state` - State index for multi-reference calculations (BAGEL)
607/// * `input_basename` - Basename of input file for ORCA .gbw paths (e.g., "calc" for "calc.inp")
608///
609/// # Returns
610///
611/// Returns a `String` containing the formatted input header for the specified program.
612///
613/// # Examples
614///
615/// ```
616/// use omecp::config::{Config, QMProgram, RunMode};
617/// use omecp::io;
618///
619/// let mut config = Config::default();
620/// config.program = QMProgram::Orca;
621/// config.method = "B3LYP def2-SVP".to_string();
622/// config.run_mode = RunMode::Read;
623///
624/// // This will generate ORCA header with "calc/compound_xyz_123_state_A.gbw" paths
625/// let header = io::build_program_header_with_basename(&config, 0, 1, "", 0, "calc/compound_xyz_123");
626/// ```
627pub fn build_program_header_with_basename(
628    config: &crate::config::Config,
629    charge: i32,
630    mult: usize,
631    td_or_tail: &str,
632    state: usize,
633    input_basename: &str,
634) -> String {
635    build_program_header_with_chk(
636        config,
637        charge,
638        mult,
639        td_or_tail,
640        state,
641        None,
642        Some(input_basename),
643    )
644}
645
646/// Builds a program-specific input file header with custom checkpoint/GBW file support.
647///
648/// This function generates headers for any supported QM program, allowing precise control
649/// over the checkpoint or wavefunction file usage. This is critical for:
650/// - Gaussian: Specifying the `%chk` file path.
651/// - ORCA: Specifying the `%moinp` file path for chain-linked restarts.
652///
653/// It automatically applies run-mode specific modifications (like `force`, `guess=read`,
654/// or `!moread`) to the method string.
655///
656/// # Arguments
657///
658/// * `config` - The global configuration.
659/// * `charge` - Molecular charge.
660/// * `mult` - Spin multiplicity.
661/// * `td_or_tail` - TD-DFT keywords (Gaussian) or tail content (ORCA).
662/// * `state` - Electronic state index (for BAGEL).
663/// * `chk_file` - Optional custom checkpoint/GBW file path.
664///   - For Gaussian: Sets `%chk={chk_file}`.
665///   - For ORCA: Sets `%moinp "{chk_file}"` if `!moread` is active.
666/// * `input_basename` - Optional basename for default naming fallback (required for ORCA if chk_file is None).
667///
668/// # Returns
669///
670/// Returns the formatted input header string.
671
672pub fn build_program_header_with_chk(
673    config: &crate::config::Config,
674    charge: i32,
675    mult: usize,
676    td_or_tail: &str,
677    state: usize,
678    chk_file: Option<&str>,
679    input_basename: Option<&str>,
680) -> String {
681    // Get dynamically modified method based on run mode and program
682    let modified_method =
683        modify_method_for_run_mode(&config.method, config.program, config.run_mode);
684
685    // Create temporary config with modified method for header generation
686    let mut temp_config = config.clone();
687    temp_config.method = modified_method;
688
689    // Determine checkpoint file name
690    let checkpoint_file = chk_file.unwrap_or(
691        // Default checkpoint file names based on charge/mult
692        if mult == config.mult_state_a {
693            "state_A.chk"
694        } else {
695            "state_B.chk"
696        },
697    );
698
699    match config.program {
700        crate::config::QMProgram::Gaussian => build_gaussian_header_internal_with_chk(
701            &temp_config,
702            charge,
703            mult,
704            td_or_tail,
705            checkpoint_file,
706        ),
707        crate::config::QMProgram::Orca => {
708            let basename =
709                input_basename.expect("ORCA requires input_basename parameter for .gbw file paths");
710            // Pass chk_file explicitly to build_orca_header_internal.
711            // In Gaussian this parameter is 'checkpoint_file' (unwrapped), but for Orca
712            // we want the Option to allow fallback to basename logic if not provided.
713            // However, the 'checkpoint_file' variable above unwrap_or's it with "state_A.chk".
714            // We should pass the ORIGINAL chk_file option to Orca builder to preserve None.
715            build_orca_header_internal(&temp_config, charge, mult, td_or_tail, basename, chk_file)
716        }
717        crate::config::QMProgram::Xtb => build_xtb_header(&temp_config, charge, mult, td_or_tail),
718        crate::config::QMProgram::Bagel => build_bagel_header(&temp_config, charge, mult, state),
719        crate::config::QMProgram::Custom => {
720            // For custom programs, fall back to Gaussian format
721            // Users can override this via custom interface files
722            build_gaussian_header_internal_with_chk(
723                &temp_config,
724                charge,
725                mult,
726                td_or_tail,
727                checkpoint_file,
728            )
729        }
730    }
731}
732