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;
use std::sync::atomic::{AtomicU32, Ordering};

static XYZ_SEQ: AtomicU32 = AtomicU32::new(0);

fn write_xyz(name: &str, body: &str) -> std::path::PathBuf {
    let seq = XYZ_SEQ.fetch_add(1, Ordering::Relaxed);
    let stem = name.strip_suffix(".xyz").unwrap_or(name);
    let path = std::env::temp_dir().join(format!("{stem}_{}_{seq}.xyz", std::process::id()));
    std::fs::write(&path, body).unwrap();
    path
}

fn water() -> std::path::PathBuf {
    write_xyz(
        "hartree_r2scan3c_water.xyz",
        "3\nwater\nO 0.0 0.0 0.117\nH 0.0 0.757 -0.470\nH 0.0 -0.757 -0.470\n",
    )
}

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

fn grab(stdout: &str, label: &str) -> f64 {
    stdout
        .lines()
        .find(|l| l.trim_start().starts_with(label))
        .unwrap_or_else(|| panic!("no '{label}' line in:\n{stdout}"))
        .split_whitespace()
        .rev()
        .nth(1)
        .unwrap()
        .parse()
        .unwrap()
}

#[test]
fn conflicting_basis_is_rejected() {
    let xyz = water();
    let out = hartree(&[
        xyz.to_str().unwrap(),
        "--method",
        "r2scan-3c",
        "--basis",
        "cc-pvdz",
    ]);
    assert!(!out.status.success(), "conflicting --basis should fail");
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        stderr.contains("implies the def2-mTZVPP basis"),
        "unexpected error:\n{stderr}"
    );
}

#[test]
fn dispersion_suffixes_are_rejected() {
    let xyz = water();
    for method in ["r2scan-3c-d3", "r2scan-3c-d4"] {
        let out = hartree(&[xyz.to_str().unwrap(), "--method", method]);
        assert!(!out.status.success(), "{method} should fail");
        let stderr = String::from_utf8_lossy(&out.stderr);
        assert!(
            stderr.contains("defines its own D4 and short-range corrections"),
            "{method} unexpected error:\n{stderr}"
        );
    }
}

#[test]
#[ignore = "TZ-class composite run (def2-mTZVPP, grid 3); run with --release -- --ignored"]
fn composite_runs_without_basis_flag_and_reports_components() {
    let xyz = water();
    let out = hartree(&[xyz.to_str().unwrap(), "--method", "r2scan-3c"]);
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        out.status.success(),
        "exit failure:\n{stdout}\n{}",
        String::from_utf8_lossy(&out.stderr)
    );
    assert!(
        stdout.contains("def2-mtzvpp"),
        "implied basis not reported:\n{stdout}"
    );
    assert!(stdout.contains("grid level 3"), "grid default:\n{stdout}");
    let scf = grab(&stdout, "E_SCF (r2scan)");
    let d4 = grab(&stdout, "E_D4  (r2scan-3c)");
    let gcp = grab(&stdout, "E_gCP (def2-mTZVPP)");
    let total = grab(&stdout, "composite total");
    assert!(gcp > 0.0, "gCP is a repulsive BSSE correction: {gcp}");
    assert!(d4 < 0.0, "D4 should be attractive here: {d4}");
    assert!(
        (scf + d4 + gcp - total).abs() < 1e-11,
        "components do not sum: {scf} + {d4} + {gcp} != {total}"
    );
    let out2 = hartree(&[
        xyz.to_str().unwrap(),
        "--method",
        "r2scan-3c",
        "--basis",
        "def2-mtzvpp",
    ]);
    assert!(out2.status.success(), "matching --basis must be accepted");
}