Goal: get energies, forces, stress, eigenvalues and occupations out of results.json in a form you can script against.
The terminal log is for humans. results.json is the contract — it is what the regression suite reads, what the Python workflow layer parses, and what you should build on. Run tutorial 01's silicon example first, then:
The file is grouped by domain: structure is the material the run computed, provenance what it was asked to do and by which build, properties the physical results, and runtime_log how the calculation itself went.
The headline results – total energy and its breakdown, Fermi energy, forces, stress, pressure, band gaps – are ESSE property records in the properties.records list, each carrying its own units:
Everything else is an ESSE shape that carries its unit the same way, in a units field beside the value, never in the key's name:
Q3 works in Rydberg atomic units throughout, and every quantity in the file states its unit in its own units field. q3.io.results.classes.Results reads the file into typed blocks; each ESSE record is ESSE's own pydantic model, read by its name as an attribute of properties.
An unconverged run still writes a complete, well-formed results file with a plausible-looking energy in it. runtime_log.converged is the only thing standing between you and publishing that number. Make this the first line of any script.
When a run did not converge, runtime_log.self_consistent_field_iterations says how far it got, and provenance.precision.maximum_self_consistent_field_iterations is the limit it was given. Equal numbers mean it ran out of iterations; the silicon example converges on iteration 7 of its 100.
The total is decomposed, which is how you diagnose a suspicious result:
The contributions are ESSE's total_energy_contributions record, keyed as ESSE keys them: eigenvalues is the band energy and temperatureEntropy the -TS smearing term. The terms ESSE has no key for yet (the double-counting correction, DFT+U, dispersion, PAW one-center) are under properties.energy_contributions, each an ESSE energy with its own units.
ewald is the classical ion–ion electrostatic energy. It depends only on the geometry and charges, not on the SCF, so it is a fast way to check that two calculations really used the same structure: if the Ewald energies differ, the geometries differ, whatever you believed about your inputs.
temperatureEntropy is zero here because silicon is an insulator run with fixed occupations. In a metal it is the -TS term, and a large value means your smearing width is doing more work than it should.
The forces record lists {"id", "value"} pairs, id 1-based as in structure.material; sorting by id puts them in the input's order. Nothing converts units on the way: check a record's units if your code assumes one. Stress is a 3×3 tensor; the trace over 3 is the pressure, and a positive pressure means the cell wants to expand.
properties.stress_contributions breaks the tensor into kinetic, hartree, exchange_correlation, ewald, local_pseudopotential, nonlocal_pseudopotential, core_correction, augmentation, dispersion and hubbard. These individual terms are large and cancel heavily — kinetic is +2176 kbar against Ewald's −2964 kbar to leave 3.71 kbar — which is exactly why a small absolute error in one term can matter, and why they are reported separately. Each is a stress_tensor shape (units, value), so results.properties.stress_contributions["ewald"] reads one; force_contributions holds the force terms the same way.
They are ESSE's eigenvalues_at_kpoints record, results.properties.eigenvalues_at_kpoints: one entry per k-point with its kpoint, its weight, and the band energies and occupations of each spin channel. eigenvalue_arrays turns that into arrays indexed [k-point][spin][band], with the occupations and weights beside them:
That is not silicon's band gap. Silicon's PBE gap is around 0.6 eV. The 2×2×2 grid used here contains only two inequivalent k-points, and neither is near the conduction-band minimum along Γ–X, so the smallest gap this grid can see is far too large. The number is arithmetically correct and physically wrong — a good illustration of why tutorial 03 exists, and why band gaps come from the k-path workflow in tutorial 04, not from an SCF grid.
A spin-polarized run lists every k-point twice, the spin-up states first and then the spin-down ones, each at half the k-point's weight. Its moment is reported directly, as ESSE's magnetization_total and magnetization_absolute records:
k-point weights are not normalized to 1 — in this example they are 0.5 and 1.5, summing to 2. Always divide by the sum when you average over them; assuming normalization is a common source of factor-of-two errors.
Alongside <prefix>_results.json, the outdir holds a <prefix>.save/ directory:
These are what a band-structure or DOS run reads back instead of redoing the SCF, and what a restart resumes from. docs/io_reference.md documents the formats.
03 — Converging a calculation: making these numbers mean something.