hartree-cli 0.2.3

Command-line driver for hartree: an XYZ file plus a basis and method, to an SCF energy.
Documentation
use std::process::Command;

fn water_xyz(name: &str) -> std::path::PathBuf {
    let path = std::env::temp_dir().join(name);
    std::fs::write(
        &path,
        "3\nwater\nO 0.0 0.0 0.117\nH 0.0 0.757 -0.470\nH 0.0 -0.757 -0.470\n",
    )
    .unwrap();
    path
}

fn run_hartree(xyz: &std::path::Path, args: &[&str]) -> std::process::Output {
    Command::new(env!("CARGO_BIN_EXE_hartree"))
        .arg(xyz)
        .args(args)
        .output()
        .expect("run hartree")
}

fn total_energy(stdout: &str) -> f64 {
    stdout
        .lines()
        .find(|l| l.trim_start().starts_with("total energy"))
        .and_then(|l| l.split_whitespace().nth(2))
        .and_then(|v| v.parse().ok())
        .unwrap_or_else(|| panic!("no total-energy line in:\n{stdout}"))
}

#[test]
fn cosx_rhf_def2svp_matches_incore() {
    let xyz = water_xyz("hartree_cli_cosx_water.xyz");
    let reference = run_hartree(&xyz, &["--basis", "def2-svp", "--method", "rhf"]);
    assert!(reference.status.success());
    let e_ref = total_energy(&String::from_utf8_lossy(&reference.stdout));

    let output = run_hartree(&xyz, &["--basis", "def2-svp", "--method", "rhf", "--cosx"]);
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        output.status.success(),
        "exit failure.\nstdout:\n{stdout}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(
        stdout.contains("COSX semi-numerical exchange: grid medium"),
        "no COSX header:\n{stdout}"
    );
    assert!(stdout.contains("overlap-fitted"), "no fit note:\n{stdout}");
    assert!(stdout.contains("converged in"), "not converged:\n{stdout}");

    let e_cosx = total_energy(&stdout);
    let de = (e_cosx - e_ref).abs();
    assert!(
        de <= 5e-5,
        "COSX RHF energy {e_cosx} vs in-core {e_ref}: |ΔE| = {de:e} > 5e-5 Eh"
    );
}

#[test]
fn cosx_pbe0_runs() {
    let xyz = water_xyz("hartree_cli_cosx_pbe0.xyz");
    let output = run_hartree(
        &xyz,
        &[
            "--basis", "sto-3g", "--method", "pbe0", "--cosx", "--grid", "1",
        ],
    );
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        output.status.success(),
        "exit failure.\nstdout:\n{stdout}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(
        stdout.contains("COSX semi-numerical exchange"),
        "no COSX header:\n{stdout}"
    );
    assert!(
        stdout.contains("exact exchange c_x"),
        "no hybrid c_x line:\n{stdout}"
    );
}

#[test]
fn cosx_with_ri_j_runs() {
    let xyz = water_xyz("hartree_cli_cosx_ri.xyz");
    let output = run_hartree(
        &xyz,
        &["--basis", "sto-3g", "--method", "rhf", "--ri", "--cosx"],
    );
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        output.status.success(),
        "exit failure.\nstdout:\n{stdout}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(stdout.contains("RI-JK density fitting"), "{stdout}");
    assert!(stdout.contains("COSX semi-numerical exchange"), "{stdout}");
    assert!(stdout.contains("converged in"), "{stdout}");
}

#[test]
fn cosx_runs_range_separated_functionals() {
    let xyz = water_xyz("hartree_cli_cosx_rs.xyz");
    let reference = run_hartree(
        &xyz,
        &["--basis", "sto-3g", "--method", "wb97m-v", "--grid", "1"],
    );
    assert!(reference.status.success());
    let e_ref = total_energy(&String::from_utf8_lossy(&reference.stdout));

    let output = run_hartree(
        &xyz,
        &[
            "--basis", "sto-3g", "--method", "wb97m-v", "--cosx", "--grid", "1",
        ],
    );
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        output.status.success(),
        "exit failure.\nstdout:\n{stdout}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(
        stdout.contains("COSX semi-numerical exchange")
            && stdout.contains("RS Coulomb + erf(omega=0.3) kernels"),
        "no RS-COSX header:\n{stdout}"
    );
    assert!(
        stdout.contains("VV10"),
        "VV10 must compose with RS-COSX:\n{stdout}"
    );
    let e_cosx = total_energy(&stdout);
    let de = (e_cosx - e_ref).abs();
    assert!(
        de <= 5e-5,
        "RS-COSX wb97m-v energy {e_cosx} vs in-core {e_ref}: |dE| = {de:e} > 5e-5 Eh"
    );
}

#[test]
fn cosx_guards_reject_incompatible_flags() {
    let xyz = water_xyz("hartree_cli_cosx_guards.xyz");
    let cases: &[(&[&str], &str)] = &[
        (&["--cosx", "--direct"], "--cosx with --direct"),
        (&["--cosx", "--method", "mp2"], "SCF-level methods only"),
        (&["--cosx", "--method", "ccsd"], "SCF-level methods only"),
        (&["--cosx", "--opt"], "geometry optimization"),
        (&["--cosx", "--freq"], "frequencies"),
        (&["--cosx", "--fod"], "FOD"),
        (&["--cosx", "--method", "b2plyp"], "double hybrid"),
    ];
    for (args, needle) in cases {
        let output = run_hartree(&xyz, &[&["--basis", "sto-3g"][..], args].concat());
        assert!(!output.status.success(), "{args:?} must fail");
        let stderr = String::from_utf8_lossy(&output.stderr);
        assert!(
            stderr.contains(needle),
            "{args:?}: expected {needle:?} in:\n{stderr}"
        );
    }
}