Goal: drive Q3 from Python instead of the command line, and use the layer that turns single-point calculations into workflows — relaxation, phonons, and the ASE calculator interface everything else is built on.
The binary computes one thing at a time. Anything that needs a sequence of calculations — move the atoms, run again, decide where to move next — needs a driver. That driver is the q3 Python package, and it is deliberately thin: it writes manifests, runs the binary, and parses results.json.
Python 3.12 or newer is required. The python3 on your PATH may well be older; check before you build a virtual environment around it, because the failure comes at install time with a message that is easy to misread:
ASE, which everything in this lesson needs, is a base dependency; test pulls in pytest, which tutorial 07 needs. Verify:
The package is layered the same way the C++ is, and for the same reason — an upward import is a cycle waiting to happen. q3.constants and q3.errors at the bottom; then the adapters q3.binary, q3.io, q3.structure; then q3.calculators and q3.flavors, which own one run of the engine; then q3.workflows and q3.jax, which orchestrate many. A module imports from its own tier or below, never above. AGENTS.md documents the rule and a pre-commit hook enforces it.
Q3ASECalculator makes Q3 look like any other ASE calculator: give it a manifest to use as a template, hand it Atoms, ask for properties.
The manifest supplies the physics — cutoffs, k-grid, pseudopotentials, functional — and the Atoms object supplies the geometry. For each property request the calculator writes a fresh input.yml and material.json with the current positions into a numbered step directory, runs the binary, and reads the results back.
Units change at this boundary. ASE speaks eV and eV/Å; Q3 speaks Rydberg and Ry/bohr. The conversion is exact in intent but not in the last digits, because the two projects use Ry→eV constants that differ in the ninth significant figure (13.605693127 in Q3's, 13.605693012 in ase.units.Ry). Converting back gives −16.913418074 Ry, while the results.json this run wrote holds −16.913417932 Ry — the same number lesson 01 produced from the command line, bit for bit. When you need Q3's exact value, read results.json; the ASE value is for ASE's algorithms.
Two practical notes:
get_potential_energy() and get_forces() share one calculation, but get_stress() triggers a second. On a variable-cell relaxation that doubles your cost — worth knowing before you attribute it to the optimizer.q3_ase_workdir/ in the current directory and accumulates one step_NNNN/ per calculation, each holding input.yml, material.json and output/. That is a feature when a relaxation goes wrong at step 40 and you want to see what was submitted, and a disk leak when you forget. Pass workdir= to control it.With an ASE calculator in hand, relaxation is ASE's optimizer driving Q3's forces. run_relax_workflow wires the two together:
That example starts from silicon_displaced.json — silicon with the atoms pushed off their symmetry positions, so there is something to relax:
Three force evaluations to go from 0.53 to 0.0008 eV/Å. fmax is the convergence criterion in eV/Å (ASE's convention), not Ry/bohr — one of the places the unit boundary above will bite you if you copy a threshold across from a manifest.
The returned dict carries converged, steps, final_energy_ry and final_forces_ry_per_bohr. Check converged before using the geometry, for exactly the reason tutorial 02 gave: hitting steps without reaching fmax still returns a complete-looking result.
You will also see this on the way past:
It is what it says — the relaxation runs unconstrained, so symmetry is not preserved by construction and can drift as forces get small. For a displaced structure relaxing back to a symmetric one that is harmless; if you need symmetry held exactly, verify the final structure rather than assuming it.
Force constants come from finite differences: displace each atom along each Cartesian axis, compute forces, assemble the dynamical matrix. That is 6N SCF calculations for N atoms plus an equilibrium reference — 13 for silicon's two-atom cell — which is why this is a workflow and not something you do by hand.
Six modes for two atoms, and both halves are checks you should make:
You will also see:
That is an acoustic mode landing a hair below zero — 5×10⁻⁹, which is numerical noise, not an instability. A real imaginary mode is a substantial number and means the structure is not at a minimum. Read the magnitude before reacting to the word "imaginary".
This workflow is Γ-only and does no symmetry reduction of the displacement set. It is the cheapest thing that can answer the question, and it answers it only at the zone centre.
For frequencies at any q you need force constants that reach beyond one unit cell, which means displacing atoms in a supercell. run_phonopy_workflow hands that problem to Phonopy: Phonopy works out which displacements the crystal's symmetry actually requires, Q3 computes the forces on each displaced supercell, and Phonopy turns the force sets into force constants, a dispersion and harmonic thermodynamics.
Install it alongside Q3 with pip install q3[phonons].
Two SCF calculations for a 16-atom supercell — that is the whole cost, and it buys the entire Brillouin zone. Silicon's diamond structure leaves exactly one inequivalent displacement direction; the workflow runs it at +u and -u because a one-sided difference on an unconverged k-mesh picks up a systematic error that the central difference cancels (the workflow's docstring has the measured numbers).
The result is the ESSE phonon_dispersions property: xDataArray holds the q-points along the path, in the material's own crystal coordinates, and yDataSeries one series per branch, so the line above read each branch at its first point, Γ. At X, one 20-point segment along:
Three pairs, and each degeneracy is required by symmetry at X. The two transverse acoustic branches come first — low and flat, the signature feature of silicon's dispersion — then LA and LO, which meet at X, then the two transverse optical branches. Measured values are 4.49, 12.32 and 13.90 THz. The shortfall is not the method: Quantum ESPRESSO, through the same Phonopy on the same nominal k-mesh, puts Γ at 15.22 THz, and the difference is how Q3 samples a shifted mesh on the displaced supercell (FUNCTIONALITY.md has the comparison).
The thermodynamics are free once the force constants exist, because they come from Fourier-interpolating them onto a q-mesh rather than from more DFT. ESSE has no temperature-dependent property for them, so each of free energy, entropy and heat capacity comes back as ESSE's abstract two-dimensional plot: temperature in xDataArray, the values in yDataSeries[0], in Phonopy's units:
That is per mole of unit cells, and silicon's cell has two atoms, so 20.2 J/K/mol of atoms against a measured 20.0 at room temperature. At high temperature it climbs to the Dulong–Petit limit of 3 × 2 × R = 49.9; the zero-point energy, an ESSE property again, is 0.058 eV/atom.
q3.workflows exports run_band_structure_workflow and run_dos_workflow (both used in tutorial 04), run_gamma_phonons_workflow and run_phonopy_workflow, run_relax_workflow, run_vc_relax_workflow for variable-cell relaxation, and run_pdos_workflow for the projected density of states. They share the shape you have now seen twice: point them at an SCF manifest, pass the workflow's own parameters as keywords, get a dict back.
q3.jax is a different thing entirely — it exposes Q3 as a differentiable primitive. See docs/differentiability.md.
06 — Bringing existing inputs: running a Quantum ESPRESSO namelist without converting it first.