Hessians
Analytic nuclear Hessians of an integral are its second derivative with respect to the Cartesian coordinates of two atoms $A$, $B$ (which may be the same atom), e.g.
\[\frac{\partial^2 S_{\mu\nu}}{\partial \mathbf{R}_A \partial \mathbf{R}_B}, \qquad \frac{\partial^2 (\mu\nu|\lambda\sigma)}{\partial \mathbf{R}_A \partial \mathbf{R}_B}\]
returned as two extra trailing length-3 axes ($x,y,z$ for $A$, then $B$). These feed analytic vibrational frequency calculations and second-order response (CPHF/CPKS) machinery.
As with Gradients, a BasisSet stores and accepts nuclear coordinates in Angstrom, but every Hessian here is with respect to bohr displacement of each nucleus. So ∇2overlap(bset, iA, iB) gives ∂²S/∂R_iA∂R_iB per bohr², not per Angstrom². To convert to a per-Angstrom² derivative, divide by Molecules.bohr_to_angstrom^2 (≈0.529177 Å/bohr, squared).
Throughout this page the examples use
julia> using GaussianBasis, Molecules
julia> water = Molecules.parse_string("""
O 0.000000000000 -0.143225816552 0.000000000000
H 1.638036840407 1.136548822547 -0.000000000000
H -1.638036840407 1.136548822547 -0.000000000000
""");
julia> bset = BasisSet("sto-3g", water); # 5 shells, 7 basis functionsOverlap
\[\frac{\partial^2 S_{\mu\nu}}{\partial \mathbf{R}_A \partial \mathbf{R}_B}\]
A full dense Hessian is computed directly from a basis set and two atom indices:
julia> hS = ∇2overlap(bset, 1, 2); # ∂²S/∂R_O ∂R_H(first)
julia> size(hS)
(7, 7, 3, 3)An in-place (mutating) version writes into a preallocated array:
julia> out = zeros(bset.nbas, bset.nbas, 3, 3);
julia> ∇2overlap!(out, bset, 1, 2);
julia> out == hS
trueA per-shell-pair option is also available. This is usually what integral-direct and CPHF code wants, since it never materializes the full array:
julia> b = ∇2overlap(bset, 1, 2, 4, 3) # 4 and 3 are shell indexes
1×3×3×3 Array{Float64, 4}:
[:, :, 1, 1] =
-0.00951483 -0.0358454 0.0
...For maximum efficiency, use the mutating shell-pair form with a preallocated output and a caller-owned scratch buffer, which makes the call allocation-free:
julia> Nmax = maximum(num_basis, bset.shells);
julia> scratch = Vector{Float64}(undef, 9*Nmax^2);
julia> out = zeros(num_basis(bset[4]), num_basis(bset[3]), 3, 3);
julia> ∇2overlap!(out, bset, 1, 2, 4, 3; scratch=scratch);
julia> out == b
trueGaussianBasis.∇2overlap — Method
∇2overlap(BS::BasisSet, iA, iB) -> Array{Float64,4}Second derivative (Hessian) of the AO overlap matrix S w.r.t. atoms iA,iB's three Cartesian coordinates each, ∂²S/∂R_iA∂R_iB, with R_iA/R_iB in bohr (see Hessians for units). Returns a dense nbas × nbas × 3 × 3 array. For repeated calls, see ∇2overlap!.
GaussianBasis.∇2overlap! — Method
∇2overlap!(out, BS::BasisSet{LCint}, iA, iB, i::Int, j::Int; scratch=nothing)
∇2kinetic!(out, BS::BasisSet{LCint}, iA, iB, i::Int, j::Int; scratch=nothing)
∇2ERI_2e2c!(out, BS::BasisSet{LCint}, iA, iB, i::Int, j::Int; scratch=nothing)Shell-pair Hessian block: ∂²X_ij/∂R_iA∂R_iB for shells i,j of BS (shell indices, not AO indices). out must be (Ni,Nj,3,3) and is overwritten, so a reused buffer is safe.
Returns the free zero – no libcint call – whenever neither shell sits on iA, or neither sits on iB: the block cannot depend on both coordinates then.
Pass scratch (a vector of at least 9*Ni*Nj elements, or 9*Nmax^2 to cover any pair) to make the call allocation-free in a loop.
See ∇2overlap for the dense whole-matrix form, and ∇2overlap_μμ!/∇2overlap_μν! for the bare primitives underneath.
Kinetic Energy
\[\frac{\partial^2 T_{\mu\nu}}{\partial \mathbf{R}_A \partial \mathbf{R}_B}\]
Identical structure to overlap – T depends on the same two shell centers and nothing else, so the same four levels are available:
julia> hT = ∇2kinetic(bset, 1, 1); # diagonal block, A = B = O
julia> hT[1, 1, :, :]
3×3 Matrix{Float64}:
0.0 0.0 0.0
0.0 0.0 0.0
0.0 0.0 0.0That block is zero because shell 1 sits on atom 1, and translating a shell together with its own atom leaves $T$ unchanged.
GaussianBasis.∇2kinetic — Method
∇2kinetic(BS::BasisSet, iA, iB) -> Array{Float64,4}Second derivative (Hessian) of the AO kinetic energy matrix T w.r.t. atoms iA,iB's three Cartesian coordinates each, ∂²T/∂R_iA∂R_iB, with R_iA/R_iB in bohr (see Hessians for units). Returns a dense nbas × nbas × 3 × 3 array. For repeated calls, see ∇2kinetic!.
GaussianBasis.∇2kinetic! — Method
∇2overlap!(out, BS::BasisSet{LCint}, iA, iB, i::Int, j::Int; scratch=nothing)
∇2kinetic!(out, BS::BasisSet{LCint}, iA, iB, i::Int, j::Int; scratch=nothing)
∇2ERI_2e2c!(out, BS::BasisSet{LCint}, iA, iB, i::Int, j::Int; scratch=nothing)Shell-pair Hessian block: ∂²X_ij/∂R_iA∂R_iB for shells i,j of BS (shell indices, not AO indices). out must be (Ni,Nj,3,3) and is overwritten, so a reused buffer is safe.
Returns the free zero – no libcint call – whenever neither shell sits on iA, or neither sits on iB: the block cannot depend on both coordinates then.
Pass scratch (a vector of at least 9*Ni*Nj elements, or 9*Nmax^2 to cover any pair) to make the call allocation-free in a loop.
See ∇2overlap for the dense whole-matrix form, and ∇2overlap_μμ!/∇2overlap_μν! for the bare primitives underneath.
Nuclear-electron Attraction
\[\frac{\partial^2 V_{\mu\nu}}{\partial \mathbf{R}_A \partial \mathbf{R}_B}\]
Nuclear attraction differs from overlap and kinetic in an important way: it depends on three kinds of position – the two shell centers and the position of every nucleus supplying the potential. A derivative with respect to atom $A$ can therefore land on a shell center or on $A$'s own nuclear charge, so the second derivative splits into
- both derivatives on shell centers, summed over all nuclei, and
- at least one derivative on a moving nuclear charge, isolated one nucleus at a time via a repositionable point-charge operator.
∇2nuclear assembles both transparently:
julia> hV = ∇2nuclear(bset, 1, 1);
julia> hV[1, 1, :, :]
3×3 Matrix{Float64}:
-0.0284715 0.0 0.0
0.0 -0.00452429 0.0
0.0 0.0 0.0329958Unlike overlap and kinetic, this block is not zero even though both shells sit on atom 1 – the potential's own center moves with the atom.
GaussianBasis.∇2nuclear — Method
∇2nuclear(BS::BasisSet, iA, iB) -> Array{Float64,4}Second derivative (Hessian) of the AO nuclear attraction matrix V w.r.t. atoms iA,iB's three Cartesian coordinates each, ∂²V/∂R_iA∂R_iB (accounting for both shell-center and nuclear-charge-position derivatives), with R_iA/R_iB in bohr (see Hessians for units). Returns a dense nbas × nbas × 3 × 3 array. For repeated calls, see ∇2nuclear!.
GaussianBasis.∇2nuclear! — Function
∇2nuclear!(out, BS::BasisSet, iA, iB)Mutating counterpart of ∇2nuclear: writes the dense nbas × nbas × 3 × 3 Hessian into out instead of allocating it. out is zeroed first, so a reused buffer is safe.
Assembles both contributions – the piece with both derivatives on shell centers (summed over every nucleus) and the piece with at least one derivative on a moving nuclear charge (isolated one nucleus at a time through a repositionable point-charge operator, see ∇2nuclear_rinv_μμ!).
Two-Electron Four Centers
\[\frac{\partial^2 (\mu\nu|\lambda\sigma)}{\partial \mathbf{R}_A \partial \mathbf{R}_B}\]
There is deliberately no dense form here: the full tensor would be $nbas^4 \times 3 \times 3$, which is not worth materializing for any system where a Hessian is interesting. The shell-quartet form is the entry point:
julia> q = ∇2ERI_2e4c(bset, 1, 2, 3, 3, 1, 1); # atoms 1,2; shells 3,3,1,1
julia> size(q)
(3, 3, 1, 1, 3, 3)For a hot loop, use the mutating form with caller-owned scratch. The Xflag/Yflag form skips the atom-membership lookup for callers that have already screened:
julia> using GaussianBasis: on_atom_flags
julia> X = on_atom_flags(bset, 1, 3, 3, 1, 1); # which shells sit on atom 1
julia> Y = on_atom_flags(bset, 2, 3, 3, 1, 1); # ... and on atom 2
julia> buf = Vector{Float64}(undef, 9*Nmax^4);
julia> out = zeros(size(q));
julia> ∇2ERI_2e4c!(out, bset, X, Y, 3, 3, 1, 1, buf);
julia> out == q
trueGaussianBasis.∇2ERI_2e4c — Method
∇2ERI_2e4c(BS::BasisSet, iA::Int, iB::Int, i::Int, j::Int, k::Int, l::Int)
∇2ERI_2e4c(BS::BasisSet, Xflag::NTuple{4,Bool}, Yflag::NTuple{4,Bool}, i::Int, j::Int, k::Int, l::Int)Shell-quartet-level dense 4-center ERI Hessian: ∂²(ij|kl)/∂R_iA∂R_iB for shells i,j,k,l w.r.t. atoms iA,iB's three Cartesian directions each, with R_iA/R_iB in bohr (see Hessians for units), as an (Ni,Nj,Nk,Nl,3,3) block. No Schwarz screening either way – callers wanting that should screen before calling.
Two forms, same relationship as ∇ERI_2e4c's: the iA::Int,iB::Int form computes Xflag/Yflag (which of i,j,k,l sit on iA/iB) and returns the free zero (no libcint call) whenever no shell touches iA, no shell touches iB, or iA==iB with all four shells on that one atom (translational invariance). The Xflag,Yflag::NTuple{4,Bool} form is the unchecked core: no membership computation, no zero-skip – for callers that have already screened at a higher level and precomputed the flags once outside a hot loop.
GaussianBasis.∇2ERI_2e4c! — Method
∇2ERI_2e4c!(out, BS::BasisSet, Xflag, Yflag, i, j, k, l, buf)Scratch-buffer-accepting core: buf (sized >= 9*Nmax^4, Nmax = the largest num_basis over any shell the caller will ever pass) is caller-owned and reused across every call instead of allocated fresh – zero-allocation, for callers in a hot per-quartet loop. Not thread-safe to share: each concurrent caller (e.g. each worker task) needs its own buf.
Two-Electron Three Centers
\[\frac{\partial^2 (\mu\nu|P)}{\partial \mathbf{R}_A \partial \mathbf{R}_B}\]
Here $\mu,\nu$ run over the regular basis and $P$ over an auxiliary/fitting basis (density fitting). A dense form exists but is usually the wrong shape – prefer the shell-triple form:
julia> aux = BasisSet("cc-pvqz-jkfit", water);
julia> t = ∇2ERI_2e3c(bset, aux, 1, 2, 3, 3, 1); # atoms 1,2; shells 3,3 and aux shell 1
julia> size(t)
(3, 3, 1, 3, 3)In a loop, hoist both the scratch buffer and the merged basis (which depends only on the two basis sets, never on the atoms or shells):
julia> using GaussianBasis: merge_basis
julia> Bm = merge_basis(bset, aux);
julia> scratch = Vector{Float64}(undef, 9*Nmax^2*maximum(num_basis, aux.shells));
julia> out = zeros(size(t));
julia> ∇2ERI_2e3c!(out, bset, aux, 1, 2, 3, 3, 1; scratch=scratch, Bmerged=Bm);
julia> out == t
trueGaussianBasis.∇2ERI_2e3c — Method
∇2ERI_2e3c(BS1::BasisSet, BS2::BasisSet, iA, iB)Second derivative (Hessian, atoms iA,iB, with R_iA/R_iB in bohr – see Hessians for units) of the 3-center two-electron integral (μν|P) (BS1=regular basis, BS2=auxiliary/fitting basis – density fitting). Output (BS1.nbas,BS1.nbas,BS2.nbas,3,3). See this file's header comment for the shell-position combinatorics and kernel mapping.
GaussianBasis.∇2ERI_2e3c! — Method
∇2ERI_2e3c!(out, BS1::BasisSet, BS2::BasisSet, iA, iB, i::Int, j::Int, k::Int;
scratch=nothing, Bmerged=nothing)
∇2ERI_2e3c(BS1::BasisSet, BS2::BasisSet, iA, iB, i::Int, j::Int, k::Int)Shell-triple Hessian block of (μν|P): ∂²(ij|k)/∂R_iA∂R_iB for regular shells i,j of BS1 and auxiliary shell k of BS2 (shell indices, not AO indices). out must be (Ni,Nj,Nk,3,3) and is overwritten, so a reused buffer is safe.
This is the level integral-direct and CPHF code wants: the dense form is a BS1.nbas² × BS2.nbas × 3 × 3 tensor, which is rarely worth materializing.
Returns the free zero – no libcint call – unless at least one of the three shells sits on iA and at least one sits on iB.
Pass scratch (at least 9*Ni*Nj*Nk elements) and Bmerged (from the two bases; it depends only on them, never on the atoms or shells) to make the call allocation-free in a loop.
Two-Electron Two Centers
\[\frac{\partial^2 (P|Q)}{\partial \mathbf{R}_A \partial \mathbf{R}_B}\]
The density-fitting Coulomb metric. It depends on exactly two shell centers, so it shares the overlap/kinetic structure – only the basis set (auxiliary) and the kernel family differ:
julia> hJ = ∇2ERI_2e2c(aux, 1, 2);
julia> size(hJ)
(208, 208, 3, 3)GaussianBasis.∇2ERI_2e2c — Method
∇2ERI_2e2c(auxbset::BasisSet, iA, iB)Second derivative (Hessian, atoms iA,iB, with R_iA/R_iB in bohr – see Hessians for units) of the 2-center two-electron auxiliary metric (P|Q) (density fitting's J_PQ). Output (naux,naux,3,3). Reuses ∇21e!'s same-shell/cross-shell structure via the "metric" compute-case – see this file's header comment.
GaussianBasis.∇2ERI_2e2c! — Method
∇2overlap!(out, BS::BasisSet{LCint}, iA, iB, i::Int, j::Int; scratch=nothing)
∇2kinetic!(out, BS::BasisSet{LCint}, iA, iB, i::Int, j::Int; scratch=nothing)
∇2ERI_2e2c!(out, BS::BasisSet{LCint}, iA, iB, i::Int, j::Int; scratch=nothing)Shell-pair Hessian block: ∂²X_ij/∂R_iA∂R_iB for shells i,j of BS (shell indices, not AO indices). out must be (Ni,Nj,3,3) and is overwritten, so a reused buffer is safe.
Returns the free zero – no libcint call – whenever neither shell sits on iA, or neither sits on iB: the block cannot depend on both coordinates then.
Pass scratch (a vector of at least 9*Ni*Nj elements, or 9*Nmax^2 to cover any pair) to make the call allocation-free in a loop.
See ∇2overlap for the dense whole-matrix form, and ∇2overlap_μμ!/∇2overlap_μν! for the bare primitives underneath.
Choosing a level
Every Hessian here is available at the same levels as the Gradients, from most convenient to fastest. All of them go through the same libcint primitives, so they agree to the last bit – they differ only in how much bookkeeping is done for you.
| level | example | notes |
|---|---|---|
| dense, allocating | ∇2overlap(bs, A, B) | fresh nbas × nbas × 3 × 3 array; not offered for the 4-center ERI |
| dense, preallocated | ∇2overlap!(out, bs, A, B) | reuses out; zeroes it for you |
| shell pair/triple/quartet | ∇2overlap!(out, bs, A, B, i, j) | validates sizes, resolves shell membership, returns the free zero when the block vanishes |
| bare primitive | ∇2overlap_μμ!(out, bs, i, j) | one libcint call, no checks at all |
Two things are worth knowing before dropping a level.
A Hessian needs two primitives per integral, not one. Where the gradients express every case as the single _μ! primitive with permuted arguments, a second derivative can put both derivatives on the same center (_μμ!, libcint's ipip family) or one on each (_μν!, the ipXip family). These are genuinely different kernels; neither is an argument permutation of the other. The 3- and 4-center ERIs need more still, because their centers fall into groups – see the tables in their primitive docstrings.
Hoistable state should be hoisted. Passing it in makes the call allocation-free:
| routine | keyword | build it with |
|---|---|---|
∇2overlap!, ∇2kinetic!, ∇2ERI_2e2c! (shell pair) | scratch | Vector{Float64}(undef, 9*Nmax^2) |
∇2ERI_2e3c! (shell triple) | scratch, Bmerged | merge_basis |
∇2ERI_2e4c! (shell quartet) | buf (positional) | Vector{Float64}(undef, 9*Nmax^4) |
Bare libcint primitives
The lowest level. Each does exactly one libcint call – no bounds checking, no zero-block skipping, no output-size validation. Unlike the gradient primitives they apply no sign flip: libcint differentiates with respect to the electron coordinate, each derivative contributes a factor of $-1$, and a second derivative applies two, which cancel.
The cross primitives (_μν!, _μλ!, _μP!) return their two derivative-component axes in (q,p) order, reversed from the naive expectation. For same-center kernels this is invisible, since mixed partials of one point commute, but it is not invisible in general. Each primitive's docstring says which convention it uses.
GaussianBasis.∇2overlap_μμ! — Function
∇2overlap_μμ!(out, BS::BasisSet{LCint}, i::Int, j::Int)
∇2kinetic_μμ!(out, BS::BasisSet{LCint}, i::Int, j::Int)
∇2nuclear_μμ!(out, BS::BasisSet{LCint}, i::Int, j::Int)
∇2ERI_2e2c_μμ!(out, BS::BasisSet{LCint}, i::Int, j::Int)Libcint call placing BOTH derivatives on shell i (the "μ" shell), for shells i,j of BS. out comes back as a raw (Ni,Nj,3,3) block and may be a contiguous view.
∇2nuclear_μμ! covers only the piece of the nuclear-attraction Hessian where both derivatives land on shell centers, summed over every nucleus; the terms where a derivative lands on a nuclear charge position are added separately by ∇2nuclear!.
No bounds checking, no zero-block skipping, no output-size validation.
GaussianBasis.∇2overlap_μν! — Function
∇2overlap_μν!(out, BS::BasisSet{LCint}, i::Int, j::Int)
∇2kinetic_μν!(out, BS::BasisSet{LCint}, i::Int, j::Int)
∇2nuclear_μν!(out, BS::BasisSet{LCint}, i::Int, j::Int)
∇2ERI_2e2c_μν!(out, BS::BasisSet{LCint}, i::Int, j::Int)Libcint call placing one derivative on shell i and one on shell j, for shells i,j of BS. out comes back as a raw (Ni,Nj,3,3) block and may be a contiguous view.
The two derivative-component axes come back in (ket, bra) order: out[:,:,q,p] is d²X / d(shell i)_p d(shell j)_q. For overlap and kinetic this is invisible – those depend only on the shell-shell separation, so the cross-Hessian is symmetric under p<->q – but it is not invisible in general (nuclear attraction's shell-only piece has a fixed background charge distribution that breaks the symmetry), so callers must orient it deliberately.
No bounds checking, no zero-block skipping, no output-size validation.
GaussianBasis.∇2nuclear_rinv_μμ! — Function
∇2nuclear_rinv_μμ!(out, BS::BasisSet{LCint}, env, i::Int, j::Int)
∇2nuclear_rinv_μν!(out, BS::BasisSet{LCint}, env, i::Int, j::Int)Libcint calls for the 1/|r-C| (single-nucleus) Hessian pieces of the nuclear-attraction integral, for shells i,j of BS. env selects WHICH nucleus: it is a copy of BS.lib.env with the rinv origin overwritten (see rinv_env), the same way ∇nuclear_μ! takes a charge-fudged atm array to select which potential it differentiates.
_μμ! places both derivatives on shell i; _μν! places one on shell i and one on shell j. Both return a raw (Ni,Nj,3,3) block and accept a contiguous view.
_μν! returns its two derivative axes in (ket, bra) order. Unlike the two-center ipXip kernels, a mixed bra/ket derivative here involves three independent points (bra center, ket center, rinv origin), so it has no reason to be symmetric under swapping those axes and the caller must orient it explicitly.
No bounds checking, no zero-block skipping, no output-size validation.
GaussianBasis.∇2ERI_2e2c_μμ! — Function
∇2overlap_μμ!(out, BS::BasisSet{LCint}, i::Int, j::Int)
∇2kinetic_μμ!(out, BS::BasisSet{LCint}, i::Int, j::Int)
∇2nuclear_μμ!(out, BS::BasisSet{LCint}, i::Int, j::Int)
∇2ERI_2e2c_μμ!(out, BS::BasisSet{LCint}, i::Int, j::Int)Libcint call placing BOTH derivatives on shell i (the "μ" shell), for shells i,j of BS. out comes back as a raw (Ni,Nj,3,3) block and may be a contiguous view.
∇2nuclear_μμ! covers only the piece of the nuclear-attraction Hessian where both derivatives land on shell centers, summed over every nucleus; the terms where a derivative lands on a nuclear charge position are added separately by ∇2nuclear!.
No bounds checking, no zero-block skipping, no output-size validation.
GaussianBasis.∇2ERI_2e3c_μμ! — Function
∇2ERI_2e3c_μμ!(out, BS::BasisSet{LCint}, i, j, k)
∇2ERI_2e3c_PP!(out, BS::BasisSet{LCint}, i, j, k)
∇2ERI_2e3c_μν!(out, BS::BasisSet{LCint}, i, j, k)
∇2ERI_2e3c_μP!(out, BS::BasisSet{LCint}, i, j, k)Bare libcint calls for the 3-center (μν|P) Hessian. BS is the merged basis (regular shells then auxiliary), so an auxiliary shell k is addressed as k + BS1.nshells – same convention as ∇ERI_2e3c_μ!.
Four kernels are needed because the three centers fall into two groups (the two bra shells, and the lone ket/auxiliary shell), so a second derivative can land in four distinct ways:
| primitive | libcint | both/one derivative on |
|---|---|---|
_μμ! | ipip1 | both on the FIRST shell argument |
_PP! | ipip2 | both on the auxiliary shell |
_μν! | ipvip1 | one on each of the two bra shells |
_μP! | ip1ip2 | one on the first bra shell, one on the auxiliary |
ν is reached by passing it first (_μμ!(out, BS, j, i, k) etc.) and transposing the AO axes back.
All four return their two derivative-component axes in (q,p) order, i.e. out[a,b,c,q,p] is ∂²/∂(first)_p ∂(second)_q. Callers must orient this deliberately; ∇2ERI_2e3c! folds the correction into its scatter.
out is a raw (Na,Nb,Nc,3,3) block and may be a contiguous view.
No bounds checking, no zero-block skipping, no output-size validation.
GaussianBasis.∇2ERI_2e4c_μμ! — Function
∇2ERI_2e4c_μμ!(out, BS::BasisSet{LCint}, i, j, k, l)
∇2ERI_2e4c_μν!(out, BS::BasisSet{LCint}, i, j, k, l)
∇2ERI_2e4c_μλ!(out, BS::BasisSet{LCint}, i, j, k, l)Bare libcint calls for the 4-center (ij|kl) Hessian. Three kernels cover every placement, because the four centers form two bra/ket pairs and a second derivative can land within one pair or across them:
| primitive | libcint | placement |
|---|---|---|
_μμ! | ipip1 | both derivatives on shell-argument position 1 |
_μν! | ipvip1 | one on position 1, one on position 2 (same pair) |
_μλ! | ip1ip2 | one on position 1, one on position 3 (across pairs) |
Every other placement is reached by permuting the shell arguments – e.g. both derivatives on the third shell is _μμ!(out, BS, k, l, i, j) – and transposing the AO axes of the result back. ∇2ERI_2e4c! does exactly that, folding each permutation into the index expression of its accumulate.
All three emit their two derivative-component axes in (q,p) order, reversed from the naive expectation. For _μμ! this is invisible (mixed partials of the same point commute), but not for the cross kernels.
out is a raw (Na,Nb,Nc,Nd,3,3) block and may be a contiguous view.
No bounds checking, no zero-block skipping, no output-size validation.