07Moving nuclei and interpreting electron distribution

An energy, a derivative, and an atomic population are different outputs.

~23 min reading14 sections3 knowledge checks

Progress is saved only in this browser. No account or tracking.

A slope asks how the nuclei could move

Concept plate 07
ENuclear coordinate RLocal gradientMinimum
The energy curve is illustrative. A gradient at one geometry describes a local slope; optimizing moves to a different geometry and therefore asks a different question from a fixed-geometry benchmark.
  • 1Energy belongs to a geometry
  • 2Gradient measures a local slope
  • 3Optimization changes the geometry
Original technical lesson · 2 contextual figures addedDownload original MarkdownJump to the interactive model

An energy is only one kind of answer

Our fixed-geometry nitrogen calculation answers an energy question at 1.85 Å. It does not yet tell us how a chosen analysis partitions the electrons between atoms, which way the nuclei would be pushed, or where a particular electronic model predicts an equilibrium bond length. Those questions require different quantities and, sometimes, a different calculation.

We will keep three operations separate. Population analysis interprets an existing electronic state. Nuclear differentiation asks how its fully specified energy model changes with nuclear coordinates. Geometry optimization repeatedly uses energies and derivatives to choose new coordinates. None of these operations automatically adds electron correlation or validates the electronic branch it receives.

All examples are unexecuted API illustrations. Source tests are evidence of intended implementation behavior, not calculations run for this lesson. If we relax the N₂ geometry, the result would be a new scientific case, not a replacement for the original fixed 1.85-Å benchmark.

Atomic populations are a convention for assigning shared density

The electronic density extends across the molecule; it does not arrive with a unique label saying which atom owns each electron. An atomic population assigns part of that density to an atom using a specified rule. A partial charge is the corresponding nuclear charge minus assigned electron population. Such charges are useful summaries, but are not unique atom-by-atom observables independent of basis and analysis method.

The actual public algorithm is population_analyzer. Both the native "qdk" implementation and optional "pyscf" implementation expose Mulliken analysis, selected by method="mulliken". The native schema allows only that method. A method name is not an implementation name: create("population_analyzer", "mulliken") is not the native factory contract. Do not assume that every population scheme offered by the external PySCF project is exposed through this adapter.

Mulliken analysis uses the AO density matrix PP and overlap matrix SS. Let AA denote an atom and μ∈A\mu\in A denote basis functions assigned to that atom. Its electron population is

NA=∑μ∈A(PS)μμ=∑μ∈A∑νPμνSνμ.N_A=\sum_{\mu\in A}(PS)_{\mu\mu} =\sum_{\mu\in A}\sum_\nu P_{\mu\nu}S_{\nu\mu}.

This allocates overlap-associated density between the atoms carrying the basis functions. Summing all atomic populations recovers Tr⁡(PS)\operatorname{Tr}(PS), the total represented electron count. Summing only diagonal entries of PP would ignore the overlap convention and generally be wrong.

As a purely illustrative two-function example, assign one basis function to each of two centers and let

S=(10.20.21),P=56(1111).S=\begin{pmatrix}1&0.2\\0.2&1\end{pmatrix},\qquad P=\frac56\begin{pmatrix}1&1\\1&1\end{pmatrix}.

Each diagonal entry of PSPS is one, so the two Mulliken populations sum to two. The trace of PP alone is only five-thirds. These are invented matrices showing the mathematics, not computed H₂ or N₂ data.

For an all-electron calculation the partial charge is qA=ZA−NAq_A=Z_A-N_A, in units of the positive elementary charge. A positive value means fewer assigned electrons than nuclear charge. The molecular sum should agree with the specified total charge when all represented electrons are included. Individual Mulliken values need not behave like nonnegative probabilities, and they can be sensitive to diffuse functions or a changed basis. A large change after basis augmentation can be a partitioning effect rather than a large physical transfer of charge.

The implementation uses the actual one-electron density, including correlation

A correlated wavefunction need not have an AO density of the simple HF form 2CoccCoccT2C_{\mathrm{occ}}C_{\mathrm{occ}}^{\mathsf T}. Its one-particle reduced density matrix, abbreviated 1-RDM, is a compact representation of one-electron information. In an MO basis we denote it by γ\gamma: diagonal entries are orbital populations, and off-diagonal entries retain coherence between orbitals. Its full construction from many-electron amplitudes belongs to Lesson 9.

For a real restricted orbital representation, the transformation to the AO density is P=CγCTP=C\gamma C^{\mathsf T}. Unrestricted calculations transform alpha and beta blocks separately and add their AO densities. Using only the diagonal occupations in a non-natural MO basis would discard information needed for the population analysis.

The native analyzer reads the active-space 1-RDM, embeds it at the actual active orbital indices, and restores inactive occupied orbitals. For ordinary restricted electron orbitals those inactive diagonal entries are two; for each unrestricted spin block they are one. It then forms the AO density, multiplies by the overlap matrix, and uses the basis’s AO-to-atom mapping to assign the diagonal contributions.

This supports more than the initial HF determinant, provided the required density information exists. It requires real-valued 1-RDM data in the native implementation, appropriate spin-resolved blocks for unrestricted orbitals, and molecular orbitals carrying both a basis/structure and overlap matrix. Incompatible active-space dimensions or missing data are explicit errors. For ModelOrbitals, the native implementation instead reports populations from the site-basis density diagonal; that is a lattice-model interpretation, not atom-centered Gaussian analysis.

The PySCF adapter similarly builds the density from the supplied wavefunction’s 1-RDM, calls PySCF’s mulliken_pop, and sums AO populations using PySCF’s atom slices. It returns electron populations, not the partial-charge array returned by a differently named external routine. The native tests specifically check overlap contraction, heteronuclear atom assignment, unrestricted density, correlated off-diagonal density, and inactive-core restoration.

For our existing reference, an ordinary API use would be:

from qdk_chemistry.algorithms import create

populations = create("population_analyzer", "qdk", method="mulliken").run(reference)
basis = reference.get_orbitals().get_basis_set()
effective_charges = basis.get_effective_nuclear_charges()
partial_charges = [float(z) - n for z, n in zip(effective_charges, populations)]

For the all-electron N₂ case, effective and physical nuclear charges coincide. A density respecting interchange of the two nitrogen atoms should give equal populations; with fourteen represented electrons, that symmetry would imply seven on each. This is a symmetry expectation, not a measured result here.

The effective-charge expression also handles ECP bookkeeping correctly: compare the explicitly represented electron population with ZAeffZ_A^{\mathrm{eff}}, or equivalently restore removed core electrons before subtracting from physical ZAZ_A. Subtracting only explicit electrons from a heavy atom’s unreduced physical charge would manufacture a spurious large positive charge. Frozen inactive orbitals and ECP-replaced core electrons are separate accounting issues.

The energy surface has nuclear coordinates as its arguments

Now regard the electronic energy, including the appropriate nuclear/core constant, as a function of all nuclear positions. With NatN_{\mathrm{at}} atoms, collect the Cartesian coordinates into

R=(x0,y0,z0,x1,y1,z1,…),d=3Nat.R=(x_0,y_0,z_0,x_1,y_1,z_1,\ldots),\qquad d=3N_{\mathrm{at}}.

A component of the nuclear gradient is ga=∂E/∂Rag_a=\partial E/\partial R_a. It tells us how the energy changes for a small displacement in coordinate aa. The corresponding mechanical force is Fa=−gaF_a=-g_a: nuclei accelerate downhill in energy, not uphill. Coordinates are in Bohr, energies in Hartree, gradients in Hartree/Bohr.

The nuclear Hessian is

Force points opposite to the energy slope#

Supplementary · Analytical illustration

Slope d(E/Dₑ)/dx and force F/(Dₑ/Rₑ)
Force points opposite to the energy slopeSlope d(E/Dₑ)/dx and force F/(Dₑ/Rₑ) versus Separation x = R/Rₑ (dimensionless). Energy slope; Normalized force = −slope. Exact assumptions and selected numerical values follow this plot.-4-202411.522.53Force points opposite to the energy slopeSlope d(E/Dₑ)/dx and force F/(Dₑ/Rₑ) versus Separation x = R/Rₑ (dimensionless). Energy slope; Normalized force = −slope. Exact assumptions and selected numerical values follow this plot.-4-202411.522.53
Separation x = R/Rₑ (dimensionless)
  • Energy slope
  • Normalized force = −slope

Dotted reference: Equilibrium: slope = force = 0.

Model and source. The same dimensionless Morse model as Lesson 1: z = exp[−2(x − 1)], slope = 4z(1 − z), normalized force = −slope. At x = 1 the curvature is 8. Force here is F/(Dₑ/Rₑ), not newtons or a computed N₂ force.

How to read this. Below equilibrium the force pushes separation outward. Above equilibrium it pulls inward. Both force and slope vanish at the minimum, while curvature remains positive.

Inspect plotted values

Selected coordinates from the declared analytical or teaching data. Continuous curves are sampled for drawing; discrete outcomes remain discrete.

Force points opposite to the energy slope: plotted values
Seriesxy
Energy slope0.75-4.27824
Energy slope1.43750.972352
Energy slope2.1250.377161
Energy slope2.81250.103756
Energy slope3.50.02677
Normalized force = −slope0.754.27824
Normalized force = −slope1.4375-0.972352
Normalized force = −slope2.125-0.377161
Normalized force = −slope2.8125-0.103756
Normalized force = −slope3.5-0.02677
Return to this explanation
Habnuc=∂2E∂Ra∂Rb.H^{\mathrm{nuc}}_{ab}=\frac{\partial^2E}{\partial R_a\partial R_b}.

It is a dd-by-dd matrix, in Hartree/Bohr², describing how one coordinate’s gradient changes when another coordinate moves. It is not the electronic orbital Hessian used in Lesson 6. The latter varied orbitals at fixed nuclei; this one varies nuclei with the electronic calculation defined at each geometry.

For our two-atom case the gradient has six entries and the Hessian is six by six. A diatomic bond scan can be described by one chosen bond-length coordinate, but the Cartesian derivative API still returns all six nuclear components. In an isolated, well-converged calculation translational invariance implies the net force should be nearly zero. This provides a diagnostic, not an excuse to discard a nonzero net force before investigating it.

The derivative objects preserve the geometry they refer to

NuclearGradients stores atom-major components: x, y, z for atom zero, then x, y, z for atom one. get_values() gives the flat vector, as_matrix() gives an atom-by-three matrix, and get_atom_gradient(i) retrieves one atom. It is a gradient object, so a forces array requires explicit negation.

NuclearHessian stores the matrix in the same coordinate order. get_matrix() returns it, and get_atom_pair_block(i, j) extracts the three-by-three block coupling atoms ii and jj. Both objects retain a Structure identifying the geometry. Constructors reject a null structure or incorrect array shape; a correctly shaped matrix is not thereby a physically correct, converged Hessian.

Both support JSON and HDF5 with a serialization version, the associated structure, and explicit units. The gradient serializer writes hartree/bohr; the Hessian serializer writes hartree/bohr^2. File names follow the nuclear_gradients and nuclear_hessian type suffixes. No files are created in this preparation; this is the persistence contract for later use.

The computational interface is

energy, gradients, hessian, wavefunction = calculator.run(
    structure, charge=0, spin_multiplicity=1,
    seed_or_basis="cc-pvdz", n_inactive_orbitals=0,
)

hessian and wavefunction are optional outputs and may be None. Returning a gradient does not imply a Hessian was computed. These names and the keyword seed_or_basis come from the actual Python binding, rather than an assumed common name shared by every algorithm.

Analytic derivatives account for moving basis functions

An analytic gradient differentiates the mathematical energy expression and its integral ingredients. It does not mean an exact solution of the many-electron problem; an analytic HF gradient remains the derivative of an HF model.

At an accurately stationary variational solution, many first-order orbital-response terms can be eliminated through the stationarity conditions. But the Gaussian basis is attached to moving atoms. Moving a nucleus changes overlap and basis-dependent integrals as well as the explicit nuclear potential. The additional moving-basis contribution is commonly called the Pulay term. Keeping just electrostatic attraction derivatives and forgetting basis motion gives the wrong finite-basis force.

For a closed-shell SCF discussion, the gradient has the schematic structure

ga=one-electron integral derivatives+two-electron/XC derivative terms+∂Enuc∂Ra−∑μνWμν∂Sμν∂Ra,g_a=\text{one-electron integral derivatives} +\text{two-electron/XC derivative terms} +\frac{\partial E_{\mathrm{nuc}}}{\partial R_a} -\sum_{\mu\nu}W_{\mu\nu}\frac{\partial S_{\mu\nu}}{\partial R_a},

where WW is an energy-weighted occupied density matrix. The formula is a conceptual separation, not a complete replacement for the spin- and functional-dependent implementation.

The native gradient assembly includes nuclear repulsion, kinetic and attraction derivatives, overlap derivatives contracted with an energy-weighted quantity, ECP derivatives when present, electron-repulsion derivatives, and XC gradients for DFT. Libint provides ordinary Gaussian-integral derivatives; Libecpint provides ECP derivatives; GauXC supplies the native XC-gradient integration boundary. As established earlier, this native XC route uses ExchCXX built-in kernels rather than an assumed direct Libxc call.

The registered analytic implementation is create("nuclear_derivative_calculator", "qdk"), with alias "analytical_gradient". Its current wrapper requires its energy_calculator reference to be exactly scf_solver/qdk, requires n_inactive_orbitals=0, and rejects compute_hessian=True. There is no automatic numeric Hessian fallback inside that analytic implementation. It cannot be redirected to pyscf, qdk_stabilized, or a correlated calculator merely by changing the nested reference.

The wrapper uses native SCF’s analytic-gradient entry point and forwards its method settings. Representative native tests compare analytic and numeric GGA, hybrid-GGA, open-shell hybrid-GGA, and range-separated DFT gradients. Those establish concrete tested paths, not a blanket claim that every reference restriction or every external functional is validated. A returned derivative remains subject to the underlying SCF support and convergence requirements. Analytic second derivatives would require additional electronic-response machinery; this public analytic calculator does not supply them.

Finite differences trade extra electronic calculations for generality

The default derivative implementation is qdk_finite_difference, with alias "numeric". It approximates derivatives by evaluating the total energy at displaced structures. The step hh is a nuclear displacement in Bohr, not an SCF mixing factor.

For coordinate direction aa, central differences give

ga≈E(R+hea)−E(R−hea)2h,g_a\approx\frac{E(R+h e_a)-E(R-h e_a)}{2h},

where eae_a is a unit coordinate vector. The diagonal Hessian element is

Haanuc≈E(R+hea)−2E(R)+E(R−hea)h2.H^{\mathrm{nuc}}_{aa}\approx \frac{E(R+h e_a)-2E(R)+E(R-h e_a)}{h^2}.

For different coordinates a,ba,b, the mixed element uses four doubly displaced geometries:

Habnuc≈E++−E+−−E−++E−−4h2,H^{\mathrm{nuc}}_{ab}\approx \frac{E_{++}-E_{+-}-E_{-+}+E_{--}}{4h^2},

with, for example, E+−=E(R+hea−heb)E_{+-}=E(R+h e_a-h e_b). The implementation computes these expressions directly from energies, reuses the single-coordinate energies for diagonal elements, and writes symmetric off-diagonal pairs. It is not a Hessian obtained by finite-differencing analytic gradients.

finite_difference_step defaults to 1e-3 Bohr with a declared allowed range of 1e-8 to 1.0. For a smooth energy surface the central formulas have truncation error of order h2h^2. But energy noise is amplified by approximately 1/h1/h for gradients and 1/h21/h^2 for Hessians. Reducing the step can therefore make the result worse unless the electronic calculations are sufficiently accurate and stay on a consistent branch.

For illustration, an energy uncertainty scale of 10−810^{-8} Hartree with h=10−3h=10^{-3} Bohr can generate a gradient-noise scale around 10−510^{-5} Hartree/Bohr and a Hessian-noise scale around 10−210^{-2} Hartree/Bohr², with coefficients depending on the differences combined. These are order-of-magnitude arithmetic estimates, not error bars produced by the library.

The cost is explicit. With d=3Natd=3N_{\mathrm{at}}, a gradient uses 1+2d1+2d energy evaluations, including the central energy. With a full Hessian this implementation uses 1+2d21+2d^2 in total. For N₂, d=6d=6, so those counts are 13 and 73. They count complete energy evaluations, not SCF iterations. Each displaced calculation can have its own convergence cost. The water derivative test checks shapes, finite values, near-zero total gradient, and symmetric numeric Hessian data without implying that every step choice is reliable.

Which energy methods can finite differences actually call?

The shared derivative settings contain an energy_calculator AlgorithmRef. Unlike many fixed-type nested settings, this reference is intentionally allowed to change algorithm type. Nevertheless, the energy-dispatch code handles specific families, not every possible registered algorithm.

For scf_solver, it directly calls the selected solver and returns its total energy and wavefunction. The default is qdk; an optional registered PySCF SCF implementation can be selected for the numeric route, subject to its own conventions. orbital_solver is ignored for this direct SCF path.

For multi_configuration_scf, the numeric route supplies reference orbitals and active alpha/beta counts to an MCSCF calculator, such as the available PySCF implementation. For multi_configuration_calculator, it first builds a Hamiltonian using the nested hamiltonian_constructor, then calls a calculator such as macis_cas or macis_asci. Those correlated methods are taught later. A coupled-cluster calculator is not silently supported merely because it also reports an energy: unhandled algorithm types raise an explicit error.

The default orbital_solver and hamiltonian_constructor are native qdk. Optional localize_reference_orbitals=True acts only on multireference paths and uses a nested localizer, default qdk_pipek_mezey, on the current active orbital indices. This is a representation-control step, not a replacement for choosing a scientifically appropriate active space.

n_inactive_orbitals subtracts that many doubly occupied orbitals from each spin population to determine active counts. The helper validates positive multiplicity, nonnegative explicit electron count after ECP accounting, compatible charge/spin parity, and that inactive count does not exceed either spin population. The multireference paths currently require positive active counts for both spins. Even when the selected SCF path does not use an active space, invalid inactive-count input is still rejected; for our first examples it is zero.

Seed behavior is especially important for finite differences. The central energy may use an appropriate Orbitals or Wavefunction seed. Displaced evaluations intentionally regenerate orbitals from a named basis rather than reuse coefficient matrices whose basis centers belong to the undisplaced geometry. reuse_seed_active_space=True preserves active/inactive indices for multireference evaluations; it does not preserve displaced coefficients or guarantee chemical orbital continuity. This will motivate Lesson 8.

A seed whose basis has the explicit custom_basis_set name is rejected for displaced numeric evaluations. Other nonstandard names must actually be resolvable when reloaded. A basis object carrying custom shells but masquerading as a standard name should not be assumed to retain those custom shells after displacement. With an optional SCF backend, numeric derivative validation also needs a basis representation the QDK counting path can understand, not only a name recognized by the external engine.

The default suppress_child_algorithm_logging=True temporarily raises the process-global native logger threshold during child energy evaluations. For convergence diagnosis, setting it false preserves the current child logging. That makes a large numeric calculation easier to diagnose without pretending a logging setting changes its physics.

First obtain derivatives without changing the benchmark geometry

A useful analytic-gradient request for the nitrogen model is:

from qdk_chemistry.algorithms import create
from qdk_chemistry.data import AlgorithmRef

analytic = create(
    "nuclear_derivative_calculator", "qdk",
    energy_calculator=AlgorithmRef(
        "scf_solver", "qdk", method="hf", convergence_threshold=1e-9,
    ),
)
energy_at_stretch, gradients, no_hessian, reference_at_stretch = analytic.run(
    structure, 0, 1, "cc-pvdz", 0,
)
forces = -gradients.as_matrix()

The tighter electronic tolerance is a deliberate numerical choice for derivative work, not a new HF approximation. forces would have two rows and three columns. No values or force directions are being reported here.

If a Hessian is required, choose a separate numeric calculator:

numeric = create(
    "nuclear_derivative_calculator", "qdk_finite_difference",
    compute_hessian=True, finite_difference_step=1e-3,
    energy_calculator=AlgorithmRef(
        "scf_solver", "qdk", method="hf", convergence_threshold=1e-9,
    ),
)
energy_check, numeric_gradients, nuclear_hessian, numeric_reference = numeric.run(
    structure, 0, 1, "cc-pvdz", 0,
)

These are complete requested operations, not guarantees that this step and tolerance suffice near every electronic instability. Compare numeric gradients with analytic ones where supported and repeat a step-size study before interpreting delicate curvatures. The release example shows the same separation between analytic gradients and numeric Hessians on its own small H₂ example.

Geometry optimization asks a different scientific question

A geometry optimization repeatedly chooses nuclear displacements to reduce the energy or locate a specified saddle point. This differs from a scan, where we prescribe a sequence of geometries or bond lengths and evaluate them. It also differs from dynamics, where forces, masses, initial conditions, and time integration describe motion. An optimization step is not a physical time step.

Near a candidate geometry, a local quadratic approximation combines the gradient with a curvature model. A minimum-search method moves in a downhill direction and limits the step when the local model is not trustworthy. A transition-state search instead seeks a first-order saddle: one meaningful downhill curvature direction and uphill curvature in the remaining internal directions. It therefore cannot be understood as merely “minimize harder.”

A reaction barrier is not a diatomic dissociation curve#

Supplementary · Analytical illustration

Relative energy (arbitrary units)
A reaction barrier is not a diatomic dissociation curveRelative energy (arbitrary units) versus Synthetic reaction coordinate q (arbitrary). Profile (q² − 1)²; Minima and barrier. Exact assumptions and selected numerical values follow this plot.00.250.50.751-1-0.500.51A reaction barrier is not a diatomic dissociation curveRelative energy (arbitrary units) versus Synthetic reaction coordinate q (arbitrary). Profile (q² − 1)²; Minima and barrier. Exact assumptions and selected numerical values follow this plot.00.250.50.751-1-0.500.51
Synthetic reaction coordinate q (arbitrary)
  • Profile (q² − 1)²
  • Minima and barrier

Model and source. Synthetic one-dimensional profile E(q) = (q² − 1)², with arbitrary reaction coordinate q and energy units. Minima at q = ±1 and a maximum at q = 0. This one coordinate alone does not establish a full molecular transition state.

How to read this. A transition-state search requires one unstable internal direction and stable remaining directions. “Minimize harder” would move away from the barrier.

Inspect plotted values

Selected coordinates from the declared analytical or teaching data. Continuous curves are sampled for drawing; discrete outcomes remain discrete.

A reaction barrier is not a diatomic dissociation curve: plotted values
Seriesxy
Profile (q² − 1)²-1.40.9216
Profile (q² − 1)²-0.70.2601
Profile (q² − 1)²01
Profile (q² − 1)²0.70.2601
Profile (q² − 1)²1.40.9216
Minima and barrier-10
Minima and barrier01
Minima and barrier10
Return to this explanation

At a stationary geometry the nuclear gradient is small, but classification needs curvature. For an isolated nonlinear molecule, three translations and three rotations produce zero modes ideally; a linear molecule has three translations and two independent rotations. Thus linear N₂ has one internal vibrational mode after removing five rigid-motion directions. Small numerical deviations in those nominal zero modes must not automatically be interpreted as reaction-coordinate instabilities.

For harmonic vibrational interpretation, one forms the mass-weighted matrix M−1/2HnucM−1/2M^{-1/2}H^{\mathrm{nuc}}M^{-1/2}, where MM contains each atomic mass repeated for its three coordinates. Its internal eigenvectors describe coordinated motions, and eigenvalues relate to squared harmonic frequencies after unit conversion. A negative internal eigenvalue is associated with an imaginary harmonic frequency. This is the conceptual next analysis of a Hessian, not a claim that the data object automatically computes normal modes, vibrational thermochemistry, tunneling, or reaction rates.

Geometry convergence must also be conditional on a well-defined electronic energy branch. If one displaced SCF point is a different spin-broken solution or an unconverged iterate, its difference from its neighbor need not be a derivative of one smooth surface. A stabilized electronic solver can help find locally stable branches, but switching branches can introduce its own discontinuities. Keep electronic stability and nuclear stationary-point classification separate.

geomeTRIC supplies the nuclear optimizer; QDK supplies the engine

The available implementation here is the optional geometry_optimizer/geometric plugin. The C++ factory declares "geometric" as its default name but has no built-in concrete optimizer registration; the plugin registers it when the external geometric package is available. A default name therefore does not prove availability on this environment.

The plugin’s optimizer setting defaults to "tric". TRIC means translation–rotation–internal coordinates: molecular or fragment translation and rotation are represented explicitly alongside internal bond/angle-like motion, improving the coordinate description for many optimizations. Internal coordinates describe molecular distortions more naturally than independently stepping every x/y/z entry. They change the optimization coordinates, not the underlying energy model.

The adapter accepts the forwarded coordinate-system names tric, tric-p, dlc, hdlc, prim, and cart. Delocalized internal coordinates combine primitive internal-coordinate directions; hybrid schemes combine coordinate descriptions; cart uses Cartesian coordinates. The important API distinction is that these are optimizer choices passed to geomeTRIC, not separately registered algorithms named geometric_tric or geometric_cart. Their detailed coordinate construction is external geomeTRIC functionality, not reimplemented in this repository.

The adapter constructs a geomeTRIC molecule whose stored geometry is in Ångstrom using geomeTRIC’s own bohr2ang constant. The custom engine receives optimizer coordinates in Bohr, constructs a new QDK Structure preserving element/mass/nuclear-charge information, and calls the configured derivative calculator. It returns a Hartree energy and atom-major Hartree/Bohr gradient to geomeTRIC. It does not negate the gradient into forces because the optimizer asks for an energy gradient.

Final geomeTRIC geometries are converted back from Ångstrom to Bohr using that same external constant; the last engine geometry can be used as a fallback. Using one consistent conversion constant at both boundaries avoids tiny artificial mismatches with cached evaluations. The plugin tests explicitly check the Bohr engine contract and final-coordinate conversion.

The engine retains the original optimization input on repeated calls; it is not a general orbital/basis transport service. A basis-name string, as in our example, avoids assuming an arbitrary original orbital seed or custom basis has automatically followed every new geometry. The selected derivative calculator must be able to rebuild the scientific model at those coordinates.

The geometry controls and their exact units

The shared settings use a nested derivative_calculator, default nuclear_derivative_calculator/qdk_finite_difference, max_iterations=300, and compute_hessian=False. The iteration cap has a declared range from one to one million. This cap counts geometry steps, not the electronic evaluations inside each derivative call.

The geomeTRIC extension supplies these default convergence thresholds:

Quantity Setting Default and unit
Energy change convergence_energy 1e-6 Hartree
RMS gradient convergence_rms_gradient 3e-4 Hartree/Bohr
Maximum gradient convergence_max_gradient 4.5e-4 Hartree/Bohr
RMS displacement convergence_rms_displacement 1.2e-3 Å
Maximum displacement convergence_max_displacement 1.8e-3 Å

RMS means root mean square, a combined measure of many components; the maximum criterion prevents one large component from being hidden by the average. By default geomeTRIC requires the five criteria together. Each exposed threshold has a declared range of zero to one in its stated units. transition_state=False selects minimization; setting it true forwards geomeTRIC’s transition-state search flag. print_level=0 controls external verbosity.

compute_hessian has a precise separate meaning. During geometry iterations the adapter sets the derivative calculator’s Hessian flag false. After optimization, if a final Hessian was requested it creates a fresh derivative calculator and evaluates it at the optimized structure. This final reporting option is not a request to use that Hessian at every optimizer step, nor an explicit initial transition-state Hessian input.

Consequently, selecting analytic qdk derivatives and also compute_hessian=True on the optimizer fails at the final derivative request: the analytic implementation rejects Hessians. Use numeric derivatives for that combined workflow, or optimize with analytic gradients and explicitly request a separate numeric Hessian afterward.

The return contract is (energy, optimized_structure, hessian, wavefunction), with the latter two optional. If a matching final engine evaluation is cached and no Hessian is requested, the adapter reuses its energy/wavefunction; otherwise it evaluates the final geometry again. It does not return a new detailed optimization-history object or an independent convergence certificate. Underlying derivative/optimizer errors remain important; a plausible final geometry alone does not establish a transition state. A missing coordinate payload can fall back to the engine’s last coordinates, so meaningful success interpretation remains tied to the upstream run and requested criteria.

A controlled relaxation fork, not a silent change of benchmark

Here is a source-backed configuration pattern adapted to our nitrogen variables. It requests analytic gradients during optimization and leaves the original structure untouched:

optimizer = create(
    "geometry_optimizer", "geometric",
    optimizer="tric", max_iterations=50, compute_hessian=False,
    derivative_calculator=AlgorithmRef(
        "nuclear_derivative_calculator", "qdk",
        energy_calculator=AlgorithmRef(
            "scf_solver", "qdk", method="hf", convergence_threshold=1e-9,
        ),
    ),
)
relaxed_energy, relaxed_structure, no_hessian, relaxed_reference = optimizer.run(
    structure, 0, 1, "cc-pvdz", 0,
)

This is not an executed result, a recommendation that RHF is adequate for stretched N₂, or a promise of the experimental equilibrium distance. It defines an optimization on a particular model and orbital branch. To classify the resulting stationary point, apply a supported nuclear Hessian calculation and electronic checks separately.

The repository’s geomeTRIC release example instead starts H₂ at 1.4 Bohr and uses numeric derivatives with a 1e-2-Bohr step. Its smoke test compares to an HF/STO-3G reference bond length. Those are a different molecule, basis, and starting geometry, not evidence for our nitrogen equilibrium. The ordinary external optimizer uses working files and logging infrastructure; none of it was invoked in this read-only course preparation.

What these outputs can and cannot justify

A Mulliken population can explain how one chosen convention partitions density and can help compare related calculations with consistent bases. A nuclear gradient identifies first-order forces on a chosen electronic energy surface. A Hessian describes local nuclear curvature. A converged geometry optimizer can locate a stationary structure within its model and search setup. None alone establishes a full reaction mechanism, a thermodynamic free energy, or a reaction rate.

For the original fixed-geometry QPE problem, we may deliberately keep 1.85 Å because the scientific objective is a difficult stretched-bond electronic energy, not equilibrium optimization. Population and force diagnostics are still useful, but they do not authorize relabeling a relaxed calculation as the same benchmark. The extra classical cost also belongs to any honest end-to-end workflow accounting: 73 energy evaluations for a numeric diatomic Hessian are not one electronic calculation simply because one high-level method returned the result.

We have also encountered a representation problem that will grow along a bond scan. Orbital indices can remain numerically identical while their chemical character changes. The next lesson develops canonical, natural, and localized orbital descriptions so those handoffs become chemically intelligible, without assuming that a rotation automatically preserves every truncated many-electron approximation.

Make the idea your own

Supplementary learning

The original lesson above is preserved in full, with labeled supplemental figures placed beside the relevant explanations. The model and questions below are also learning aids, not newly executed chemistry results.

Allocate a scientific error budget

Interactive model

Consider derivative and finite-difference approximations as additional error sources, not just more precise energy labels.

Illustrative model, not a scientific calculation. Each input is an illustrative, justified absolute error allowance in millihartree (mEh). Their sum is a conservative worst-case bound only if the individual bounds are valid. These are not measured uncertainties or hardware failure probabilities.

Enable JavaScript to explore this model. The assumptions and original lesson remain readable without it.

Check your understanding

Supplementary learning

Reason through these 3 questions. Your answers do not mark the lesson as read, and nothing is locked.

1. What happens to a fixed-geometry benchmark when the nuclei are optimized?
2. Why can different atomic population schemes report different charges?
3. What is a cost of finite-difference derivatives?

Enable JavaScript to check and save answers locally.

Source notes & references (31)

Source and tests inspected at 97a3ff4f without execution. Population analyzer exposes Mulliken only; native requires real 1-RDM and proper overlap/basis data. Analytic derivative requires scf_solver/qdk and no inactive orbitals and supplies no Hessian; no automatic fallback. Numeric derivatives call only implemented SCF/MCSCF/MC energy families, reject explicit custom-named bases on displaced evaluations, and preserve seed active-space indices rather than orbital character. Geometry plugin is optional and uses original seed on repeated calls; examples use basis-name strings to avoid claiming general orbital transport. geomeTRIC internals are external; only adapter contracts/options and tests were traced. Final compute_hessian with analytic derivative raises. No normal-mode, thermochemistry, rates, or new nitrogen result is claimed.

Public source: microsoft/qdk-chemistry, pinned to 97a3ff4fb86e. This course describes that revision, not every later release.

Look up a term

On this page

  1. An energy is only one kind of answer
  2. Atomic populations are a convention for assigning shared density
  3. The implementation uses the actual one-electron density, including correlation
  4. The energy surface has nuclear coordinates as its arguments
  5. The derivative objects preserve the geometry they refer to
  6. Analytic derivatives account for moving basis functions
  7. Finite differences trade extra electronic calculations for generality
  8. Which energy methods can finite differences actually call?
  9. First obtain derivatives without changing the benchmark geometry
  10. Geometry optimization asks a different scientific question
  11. geomeTRIC supplies the nuclear optimizer; QDK supplies the engine
  12. The geometry controls and their exact units
  13. A controlled relaxation fork, not a silent change of benchmark
  14. What these outputs can and cannot justify
  15. Interactive model
  16. Knowledge check
  17. Source notes

The complete journey

Course homeGlossarySavedAbout

Search the course

Search titles and the full lesson text.

    Tab to a result, Enter to open. Escape closes search.