Skip to content

Parsing Output Files

CalcFlow parses quantum chemistry output files into structured, immutable CalculationResult objects.

The Parsers

from pathlib import Path
from calcflow import parse_qchem_output

result = parse_qchem_output(Path("calculation.out").read_text())
from pathlib import Path
from calcflow import parse_orca_output

result = parse_orca_output(Path("calculation.out").read_text())

The raw output text is stored on result.raw_output but is excluded from JSON serialization.

Multi-job Q-Chem outputs

Q-Chem concatenates multiple jobs into a single output file (e.g. a two-job MOM calculation). Use the multi-job parser to get a list of results:

from pathlib import Path
from calcflow import parse_qchem_multi_job_output

results = parse_qchem_multi_job_output(Path("mom_calc.out").read_text())
gs_result  = results[0]  # ground state job
es_result  = results[1]  # excited state job

Checking Termination

Always check termination_status before trusting the results:

if result.termination_status != "NORMAL":
    raise RuntimeError(f"Calculation failed: {result.termination_status}")
Status Meaning
"NORMAL" Calculation completed successfully
"ERROR" Program reported an error
"UNKNOWN" Termination could not be determined from the output

Energetics

result.final_energy              # float | None — total energy in Hartree
result.nuclear_repulsion_energy  # float | None — Hartree
result.dispersion                # DispersionCorrection | None
result.smd                       # SmdResults | None

Dispersion correction

if result.dispersion:
    print(result.dispersion.energy_hartree)   # DFT-D correction in Hartree
    print(result.dispersion.method)           # e.g. "D3BJ"

SMD solvation

if result.smd:
    print(result.smd.total_energy_kcal_mol)
    print(result.smd.electrostatic_kcal_mol)
    print(result.smd.non_electrostatic_kcal_mol)

SCF Results

scf = result.scf
if scf:
    print(scf.converged)       # bool
    print(scf.energy)          # float — final SCF energy in Hartree
    print(scf.n_iterations)    # int

The per-iteration history is useful for diagnosing convergence problems:

    for i, it in enumerate(scf.iterations):
        print(f"  iter {i+1}: {it.energy:.10f} Hartree  delta={it.delta_energy:.2e}")

SCF energy components

Q-Chem outputs include a breakdown:

if scf.components:
    c = scf.components
    print(c.nuclear_repulsion)    # Hartree
    print(c.one_electron)         # Hartree
    print(c.coulomb)              # Hartree
    print(c.exchange_correlation) # Hartree

Molecular Orbitals

orbs = result.orbitals
if orbs:
    for orb in orbs.alpha_orbitals:
        occ = "occ" if orb.occupied else "virt"
        print(f"  MO {orb.index}: {orb.energy:.4f} Hartree  [{occ}]")

    homo = next(o for o in reversed(orbs.alpha_orbitals) if o.occupied)
    lumo = next(o for o in orbs.alpha_orbitals if not o.occupied)
    gap  = lumo.energy - homo.energy
    print(f"HOMO-LUMO gap: {gap * 27.211:.2f} eV")

    # Unrestricted: beta orbitals
    if orbs.beta_orbitals:
        for orb in orbs.beta_orbitals:
            print(f"  beta MO {orb.index}: {orb.energy:.4f} Hartree")

Atomic Charges

Multiple charge methods can be present in a single result. Use get_charges() to retrieve by method name:

mulliken  = result.get_charges("Mulliken")
hirshfeld = result.get_charges("Hirshfeld")
cm5       = result.get_charges("CM5")
loewdin   = result.get_charges("Loewdin")   # ORCA

if mulliken:
    # charges is a dict: atom_index (0-based) -> charge value
    for idx, charge in mulliken.charges.items():
        print(f"  atom {idx}: {charge:+.4f}")

    # spin densities (unrestricted calculations)
    if mulliken.spins:
        for idx, spin in mulliken.spins.items():
            print(f"  atom {idx} spin: {spin:+.4f}")

Atom indices are 0-based throughout CalcFlow. Q-Chem output uses 1-based numbering — CalcFlow converts on ingestion.

Multipole Moments

mp = result.multipole
if mp:
    dx, dy, dz = mp.dipole.x, mp.dipole.y, mp.dipole.z
    total = (dx**2 + dy**2 + dz**2) ** 0.5
    print(f"Dipole moment: {total:.4f} Debye")

Available moments: dipole, quadrupole, octopole, hexadecapole.

TDDFT / TDA Excited States

tddft = result.tddft
if tddft:
    # TDA states (Tamm-Dancoff approximation)
    for state in tddft.tda_states:
        print(f"TDA S{state.state_number}: {state.excitation_energy_ev:.3f} eV"
              f"  f={state.oscillator_strength:.4f}")

    # Full TDDFT states
    for state in tddft.tddft_states:
        ev = state.excitation_energy_ev
        nm = 1239.8 / ev
        print(f"S{state.state_number}: {ev:.3f} eV ({nm:.0f} nm)"
              f"  f={state.oscillator_strength:.4f}")

    # Orbital transitions for a specific state
    state = tddft.tddft_states[0]
    for trans in state.transitions:
        print(f"  {trans.from_orbital} -> {trans.to_orbital}  coeff={trans.coefficient:.4f}")

When a TDDFT geometry optimization is run, state.total_energy_au gives the total energy of the excited state on the optimized geometry.

Natural Transition Orbitals

if tddft.nto_analyses:
    for nto in tddft.nto_analyses:
        print(f"State {nto.state_number}: NTO analysis available")

Transition density matrix analysis

if tddft.transition_density_matrices:
    for tdm in tddft.transition_density_matrices:
        print(f"State {tdm.state_number}:")
        print(f"  hole-electron separation: {tdm.exciton.rhe:.3f} Å")
        print(f"  CT number: {tdm.exciton.ct_number:.4f}")

Unrelaxed difference density matrix analysis

if tddft.unrelaxed_density_matrices:
    for udm in tddft.unrelaxed_density_matrices:
        print(f"State {udm.state_number}:")
        if udm.mulliken:
            print(f"  hole/electron Mulliken charges available")

ADC Excited States

CalcFlow parses ADC(2) outputs from Q-Chem:

adc = result.adc
if adc:
    print(f"Method: {adc.method}")  # e.g. "ADC(2)"

    gs = adc.ground_state
    print(f"HF energy:  {gs.hf_energy:.6f} Hartree")
    print(f"MP2 energy: {gs.mp2_energy:.6f} Hartree")

    for state in adc.excited_states:
        print(f"State {state.state_number} ({state.multiplicity}):")
        print(f"  {state.excitation_energy_ev:.3f} eV"
              f"  OPA f={state.opa_oscillator_strength:.4f}")
        if state.tpa_cross_section is not None:
            print(f"  TPA cross section: {state.tpa_cross_section:.2f} GM")

Geometry

if result.input_geometry:
    print(result.input_geometry.to_xyz_str())

if result.final_geometry:
    result.final_geometry.to_xyz_file("optimized.xyz")
    print(f"Optimized energy: {result.final_geometry.energy}")

Timing

if result.timing:
    for module, duration in result.timing.modules.items():
        print(f"{module}: {duration:.1f} s")

Serialization

# Serialize — raw_output excluded, schema_version included
json_str = result.to_json()

# Reload — works across calcflow versions (with migrations)
from calcflow.common.results import CalculationResult
result2 = CalculationResult.from_json(json_str)

# Dict form
data = result.to_dict()
result3 = CalculationResult.from_dict(data)

The full output text (result.raw_output) is intentionally excluded from JSON. Save it separately if you need it.

Spectrum Broadening

For UV/Vis spectrum simulation, install the numpy extra and use the postprocess module:

from pathlib import Path
from calcflow import parse_qchem_output, spectrum_from_excited_states, make_energy_grid
import numpy as np

result = parse_qchem_output(Path("tddft.out").read_text())
states = result.tddft.tddft_states

grid     = make_energy_grid(
    energies=[s.excitation_energy_ev for s in states],
    padding=1.0, n_points=2000
)
spectrum = spectrum_from_excited_states(states, grid, fwhm=0.3)  # eV FWHM

np.savetxt("spectrum.dat", np.column_stack([grid, spectrum]))

ADC OPA and TPA spectra: opa_spectrum_from_adc_states and tpa_spectrum_from_adc_states.