|
| 1 | +# ADR 0004: Two-Center-Factorized Three-Center Hamiltonian Terms |
| 2 | + |
| 3 | +## Status |
| 4 | + |
| 5 | +Accepted |
| 6 | + |
| 7 | +## Context |
| 8 | + |
| 9 | +The fixed (non-SCF) LCAO Hamiltonian contains two-center and three-center contributions. The |
| 10 | +kinetic term is two-center; the neutral-atom/local potential and nonlocal-pseudopotential parts |
| 11 | +generate three-center contributions when the potential/projector center `C` differs from the |
| 12 | +orbital centers `A` and `B`. DeePTB's existing two-body SK path parameterizes only two-center |
| 13 | +reduced matrix elements and expands them with a CG basis + `wigner_D` rotation |
| 14 | +(`dptb/nn/hamiltonian.py`). Message-passing embeddings (`Slem`/`Lem`/`Trinity`) capture many-body |
| 15 | +effects, but they mix `(Σ_C P_AC)(Σ_{C'} P_{BC'})` through node features and do not represent a |
| 16 | +genuine, shared-center three-center object. |
| 17 | + |
| 18 | +We want an explicit three-center term that (a) stays inside DeePTB's "parameterize reduced values, |
| 19 | +never integrate" philosophy (ADR 0001) and (b) reuses the existing CG/rotation machinery and |
| 20 | +`OrbitalMapper` indexing (ADR 0002). |
| 21 | + |
| 22 | +## Decision |
| 23 | + |
| 24 | +Represent the three-center block by a **two-center factorization**: |
| 25 | + |
| 26 | + H^(3)_{AB} = Σ_{C ≠ A,B} P_{AC} · D_C · P_{CB}, P_{CB} = P_{BC}^† |
| 27 | + |
| 28 | +where `P_{AC}^{μα} = <φ_{μA}|β_{Cα}>` is a two-center orbital↔projector "integral value" and `D_C` |
| 29 | +is a small center-local, species-`C` coupling matrix. Consistent with ADR 0001, **the network emits |
| 30 | +the reduced (direction-independent) values of `P_{AC}` and the matrix `D_C` directly** — there are |
| 31 | +no orbitals on `C` and nothing is integrated. `P_{AC}` is placed into the local A–C frame by the |
| 32 | +same SK selection matrices and rotated to the global frame with `wigner_D`; `D_C` is block-diagonal |
| 33 | +in `l` (identity in `m`), hence rotation invariant. |
| 34 | + |
| 35 | +The term is **additive** in block form, exactly like Trinity's existing two-body SK contribution |
| 36 | +(`deeptb.py`: `EDGE_FEATURES += EDGE_ATTRS`): `H^0_{AB} = H^(2)_{AB} + H^(3)_{AB}`. |
| 37 | + |
| 38 | +Two cutoffs are used, matching the physics and DeePTB's existing neighbour lists: the target blocks |
| 39 | +`H^(3)_{AB}` are produced **only for the basis-cutoff bonds** (`EDGE_INDEX`, the user's `r_max`), so |
| 40 | +they occupy exactly the same block set as `<A|B>`; the projector reach `A-C`/`C-B` uses the separate, |
| 41 | +user-tunable **environment cutoff** (`ENV_INDEX` at `er_max`, built by |
| 42 | +`AtomicData.from_points(er_max=...)`). `er_max` is a knob alongside the projector basis size and |
| 43 | +`l_max`. |
| 44 | + |
| 45 | +Implementation lives in `dptb/nn/threecenter.py` (`ThreeCenterFactorized`, `CenterCouplingMatrix`); |
| 46 | +it consumes the graph already in the `AtomicDataDict` (edge list, edge vectors) and uses |
| 47 | +`OrbitalMapper` for all orbital and **projector** indexing (the projector set is simply another |
| 48 | +`OrbitalMapper` basis), reusing `wigner_D`, `BesselBasis`, `ScalarMLPFunction`, `polynomial_cutoff` |
| 49 | +and `scatter` rather than re-implementing them. |
| 50 | + |
| 51 | +Both the **hopping** correction `H^(3)_{AB}` (basis-bond edge blocks) and the **onsite** correction |
| 52 | +`H^(3)_{AA} = sum_{C != A} P_{AC} D_C P_{CA}` (node blocks) are assembled; the onsite term is the |
| 53 | +`A==B` "triangle", i.e. each env edge `C->A` on its own, scattered to atom `A`. |
| 54 | + |
| 55 | +It is a **Trinity-only** feature, wired **entirely inside `dptb/nn/embedding/trinity.py`** (plus its |
| 56 | +config schema in `dptb/utils/argcheck.py`); `dptb/nn/deeptb.py` and the rest of the pipeline are |
| 57 | +untouched. An optional `three_center` block **inside the trinity embedding config** builds the module |
| 58 | +in `Trinity.__init__`, and `Trinity.forward` assembles the correction and adds it to the reduced |
| 59 | +`EDGE_FEATURES`/`NODE_FEATURES`. Because Trinity emits *reduced-equivariant* features (the block |
| 60 | +transform happens later in `NNENV`), the correction is added via `to_reduced` — the CG decomposition |
| 61 | +that is the exact inverse of `E3Hamiltonian(decompose=False)` — so the existing `NNENV` transform then |
| 62 | +turns (many-body + three-center) into blocks together, with the two-body `EDGE_ATTRS` added on top as |
| 63 | +before. The config lives under the trinity variant, so `three_center` on any other embedding is an |
| 64 | +argcheck error. |
| 65 | + |
| 66 | +A single trinity `mode` (Trinity-only; replaces the old `only2b`/`exclusive`) selects the channels on |
| 67 | +top of the always-present two-body base: `"2b"` = 2b only; `"3b"`/`"2b+3b"` = add the three-body term; |
| 68 | +`"full"` = add three-body + env (message passing). Following the original convention the 2b base is |
| 69 | +always produced by NNENV, so `deeptb.py` is untouched; the 3b/2b+3b/full modes require a `three_center` |
| 70 | +block, and the projector reach needs the environment neighbour list, so `er_max` must be set. The |
| 71 | +`three_center` module (hence its parameters) is built whenever the block is configured, *independent of* |
| 72 | +`mode`, so the `state_dict` is identical across modes and one can warm-start progressively. |
| 73 | + |
| 74 | +Trainability is controlled by a separate, orthogonal `freeze` list (any subset of `["2b","3b","env"]`) |
| 75 | +that sets `requires_grad=False` on the two-body term, the three-center term, and/or the message-passing |
| 76 | +pathway respectively. This makes progressive training explicit — e.g. train `2b`, then `freeze:["2b"]` |
| 77 | +with `mode:"2b+3b"` to train only the three-center term, then `freeze:["2b","3b"]` with `mode:"full"` to |
| 78 | +train only env. If `freeze` is unset the historical default is kept: `"full"` freezes `["2b","3b"]` |
| 79 | +(train only env) and every other mode freezes nothing; pass `freeze:[]` to train everything. |
| 80 | + |
| 81 | +## Consequences |
| 82 | + |
| 83 | +- Heterogeneous species are handled in `OrbitalMapper.full_basis` (the union of all species' shells) |
| 84 | + with `mask_to_basis` zeroing absent channels, so tensors stay uniform (batched GEMM unchanged) and |
| 85 | + blocks come out in full_basis order like `E3Hamiltonian`. Single shared layouts are the special |
| 86 | + case where the masks are all-ones. |
| 87 | +- Hermiticity is structural (`P_{CB}=P_{BC}^†`, `D_C=D_C^†`), so upper/lower blocks stay consistent. |
| 88 | +- Rotation covariance follows from `wigner_D` on `P` plus rotation-invariant `D_C`. |
| 89 | +- Auxiliary rank (`N_l` projectors per `l`) is the accuracy/cost knob; the factorization rank of a |
| 90 | + triangle block is bounded by the projector count. |
| 91 | +- Assembly is fully vectorized for GPU: `P_{XC}` is built for all edges at once (CG selection + |
| 92 | + `wigner_D`), the triplet index (all `(A,C,B)` sharing center `C`) is built with |
| 93 | + `argsort`/`cumsum`/`repeat_interleave` with **no Python loop over centers**, target edges are |
| 94 | + matched with `searchsorted` (no `N_atoms^2` map), and `sum_C P_AC D_C P_CB` runs as one batched |
| 95 | + GEMM over all triangles. Cost tracks the triplet count `sum_C deg(C)^2`, inherent to three-center. |
| 96 | +- Changes to the assembler are compatibility-sensitive once wired into a trainable model and must be |
| 97 | + covered by `dptb/tests/test_threecenter.py`. |
0 commit comments