Skip to content

Commit 75586bf

Browse files
committed
Add SCC convergence diagnostics and benchmark
1 parent 1dcc7f6 commit 75586bf

9 files changed

Lines changed: 1052 additions & 3 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ __pycache__/
4646
*.py[cod]
4747
*$py.class
4848
.vscode
49+
.codegraph/
4950
# C extensions
5051
*.so
5152
dptb/tests/data/esk_orb/sktb.json
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# Carbon BCC Self-Consistent Total-Energy K-Point Convergence Test Plan
2+
3+
## Status
4+
5+
In progress. The current scope is the convergence of the self-consistent total
6+
energy of one standalone Carbon BCC calculation with respect to k-point
7+
density. Inner SCC charge convergence is a required diagnostic but is not the
8+
target convergence criterion. This is a validation plan, not an implementation
9+
proposal.
10+
11+
## Motivation
12+
13+
DeePTB's SCC-DFTB functionality should give a stable, reproducible total energy
14+
for a fixed Carbon BCC input as the k-point density is increased. An individual
15+
mesh reaching its SCC charge tolerance only proves that mesh was solved
16+
self-consistently; it does not prove Brillouin-zone integration or total-energy
17+
convergence. This work is not a direct test against the existing multi-
18+
structure DFTB+ pytest benchmark.
19+
20+
The downstream UniSK workflow exposed a practical concern. Its legacy Carbon
21+
Bayesian-optimization script uses per-structure k-point spacings such as
22+
`[0.05, 0.05, 0.05]` for BCC and FCC. For the Carbon BCC cell used by the
23+
DeePTB DFTB+ benchmark, that spacing generates a `75 x 75 x 75` mesh
24+
(`421875` raw points), whereas the existing DeePTB benchmark uses
25+
`20 x 20 x 20` (`8000` raw points). The former was copied from an old
26+
production-oriented workflow with the comment that it was chosen to "ensure
27+
convergence"; no k-point convergence study was preserved with that script.
28+
29+
This difference must not be interpreted as proof that DeePTB SCC is slow or
30+
incorrect. It is first a numerical-configuration question. A mesh that is
31+
overly fine can make a correct SCC calculation impractical inside a Bayesian
32+
optimization loop.
33+
34+
## Existing Assets
35+
36+
The repository contains a broader correctness regression:
37+
38+
- `dptb/tests/test_dftb_scc.py::test_dftbscc_matches_dftbp_benchmarks`
39+
compares DeePTB SCC electronic energies against DFTB+ reference data for
40+
Carbon BCC, graphene, and dimer EOS scans.
41+
- The scan contains seven lattice/bond-length scales for each structure.
42+
- DFTB+ reference tables and the corresponding structures/SK files live in
43+
`dptb/tests/data/dftb/structs_eos` and `dptb/tests/data/dftb`.
44+
- The current acceptance threshold is a maximum electronic-energy difference
45+
below `1e-2 eV` at every scale.
46+
- Its direct command is:
47+
48+
```bash
49+
python -m pytest -q dptb/tests/test_dftb_scc.py -k matches_dftbp_benchmarks
50+
```
51+
52+
That regression is useful background and supplies the canonical Carbon
53+
structure and SK data, but it is not the execution target for the present
54+
study. The current standalone entry point is:
55+
56+
- `examples/dftb_scc/carbon_bcc_scc_convergence/run.py`;
57+
- `examples/dftb_scc/carbon_bcc_scc_convergence/POSCAR`, containing only the
58+
scale-1.000 primitive Carbon BCC cell; and
59+
- the canonical `C-C.skf` under `dptb/tests/data/dftb`, shared without invoking
60+
any pytest helper or multi-structure EOS scan.
61+
62+
The standalone example records self-consistent total, electronic, and repulsive
63+
energies; energy changes between successive meshes and against all denser
64+
meshes in the scan; k-point counts; SCC iteration count and charge residual
65+
history; electron count; Mulliken charge; Fermi level; and wall time. Existing
66+
unit tests for SCC state, mixers, API equivalence, and k-point symmetry
67+
reduction remain useful but do not define this study's convergence criterion.
68+
69+
## Questions To Answer
70+
71+
The following effects are coupled in an SCC calculation but need separate
72+
tests and diagnostics:
73+
74+
1. **K-point integration:** What mesh keeps the self-consistent total energy per
75+
atom within the agreed tolerance of every denser mesh tested for the fixed
76+
Carbon BCC cell?
77+
2. **Occupations and smearing:** Are Fermi-Dirac and Gaussian paths correctly
78+
normalized, electron-number conserving, and stable in the relevant
79+
zero-/finite-temperature limits?
80+
3. **Fermi-level solve:** Does the root solve use the same k-point weights and
81+
occupation convention as the band-energy and charge calculations?
82+
4. **Symmetry reduction:** Do full, time-reversal-reduced, and
83+
rotational-symmetry-reduced meshes give equivalent observables with correct
84+
accumulated weights?
85+
5. **Inner SCC loop:** Does every k mesh reach the charge-residual tolerance,
86+
and can SCC numerical noise contaminate the outer total-energy trend?
87+
6. **Performance:** Which component dominates runtime after the numerical
88+
configuration is fixed: Hamiltonian construction, eigensolve, occupation,
89+
Mulliken population, Gamma/SCC shift, or mixing?
90+
91+
## Important Configuration Distinction
92+
93+
The current UniSK Carbon workflow and the DeePTB DFTB+ benchmark are not a
94+
direct apples-to-apples timing or numerical comparison. Among other settings,
95+
the legacy UniSK path uses Gaussian smearing (`"G"`) and a `0.05` spacing,
96+
while the DFTB+ benchmark uses Fermi-Dirac smearing (`"FD"`) and fixed meshes
97+
such as Carbon BCC `20 x 20 x 20`. Future comparisons must explicitly record
98+
all physical and numerical settings, including SK files, repulsive parameters,
99+
cutoffs, temperature, mesh, symmetry flags, mixer, tolerance, and maximum
100+
iteration count.
101+
102+
## Phased Work
103+
104+
### Phase 0: Lock the standalone Carbon BCC baseline
105+
106+
- Use only the scale-1.000 Carbon BCC structure in the standalone example.
107+
- Do not call `test_dftbscc_matches_dftbp_benchmarks` or scan graphene, dimer,
108+
or the seven-point Carbon EOS in this phase.
109+
- Record wall time, SCC iteration count, full residual history, final residual,
110+
electron count, Fermi level, total/electronic/repulsive energies, SCC shift
111+
energy, and Mulliken charge for every numerical case.
112+
- Keep the command runnable independently of notebooks and without generating
113+
or duplicating SK tables.
114+
115+
### Phase 1: K-point convergence matrix
116+
117+
For the fixed Carbon BCC cell, scan deliberately chosen fixed meshes and, where
118+
useful, equivalent spacing-derived meshes. At minimum record:
119+
120+
- total-energy change per atom from the preceding mesh;
121+
- maximum absolute total-energy change per atom relative to every denser mesh
122+
in the same run;
123+
- an explicit k-point convergence flag evaluated against a documented energy
124+
threshold; the last and densest point cannot validate itself;
125+
- charge and Fermi-level change relative to the finest accepted mesh;
126+
- SCC iteration count and convergence failures;
127+
- raw and symmetry-reduced k-point counts;
128+
- wall time split by SCC iteration and total calculation.
129+
130+
Use the results to define separate **smoke**, **development**, and
131+
**production** mesh presets. Do not retain `0.05` as a default solely because
132+
it appeared in a legacy script.
133+
134+
The initial `20^3, 24^3, 28^3, 32^3, 36^3, 40^3` even-grid scan shows that
135+
`20^3` differs from a denser result by up to `2.252 meV/atom`. It therefore does
136+
not meet a `1 meV/atom` criterion. `24^3` and denser tested even grids remain
137+
within `1 meV/atom`, but neighbouring odd grids and spacing-derived meshes must
138+
still be checked before fixing a production preset.
139+
140+
### Phase 2: Occupation and Fermi-level regression tests
141+
142+
- Add small, deterministic tests for occupation normalization and exact
143+
electron-number conservation with non-uniform k-point weights.
144+
- Test Fermi-Dirac and Gaussian implementations independently.
145+
- Verify expected low-temperature and high-temperature limiting behaviour.
146+
- First record Fermi level, band energy, SCC charge, and electron-count error
147+
for the standalone Carbon BCC input. Broader material-class coverage is a
148+
separate follow-up.
149+
150+
### Phase 3: Symmetry-reduction equivalence
151+
152+
- For the Carbon BCC cell, compare full meshes with time-reversal and
153+
rotational reductions.
154+
- Assert equivalence of electron count, Fermi level, charge, total/electronic
155+
energy, SCC shift energy, and converged SCC shift within explicit tolerances.
156+
- Assert that the sum of reduced k-point weights equals the full mesh weight.
157+
- Report the reduction factor as a performance metric.
158+
159+
Symmetry reduction is a performance optimization, not a change to the target
160+
physical result.
161+
162+
### Phase 4: SCC robustness and performance work
163+
164+
Only after Phases 0-3 identify the limiting configuration should solver work
165+
begin. Candidate changes include static-data caching across SCC iterations,
166+
batched k-point eigensolves, warm starts, and mixer/preconditioner tuning.
167+
Every change must run the numerical regression matrix and a separate timing
168+
benchmark.
169+
170+
The primitive Carbon BCC cell has one atom, so charge conservation and symmetry
171+
largely pin its Mulliken charge. It can reveal occupation-noise effects in a
172+
very tight SCC tolerance, but it is not a difficult charge-mixing problem. A
173+
true mixer stress test will require a separate cell with non-equivalent charge
174+
degrees of freedom or an explicitly perturbed initial charge; that must not be
175+
silently substituted into this Carbon BCC baseline.
176+
177+
## Tetrahedron Integration
178+
179+
Tetrahedron integration may be useful for zero-temperature Brillouin-zone
180+
integration, DOS, and difficult metallic cases, but it is not a drop-in
181+
replacement for smearing. It requires a separate design covering band
182+
interpolation, degeneracies, weights, self-consistency, and eventually force
183+
consistency. It should be evaluated only after the existing Fermi-Dirac and
184+
Gaussian paths are fully benchmarked and tested.
185+
186+
## Acceptance Criteria For Future Changes
187+
188+
A proposed SCC numerical or performance change is acceptable only when:
189+
190+
1. the standalone Carbon BCC total-energy matrix remains within its documented
191+
meV/atom tolerance against all denser meshes tested, while inner SCC charge
192+
residuals also meet their separate tolerance;
193+
2. symmetry-reduced and full-mesh results agree within documented tolerances;
194+
3. electron-number conservation is verified for every occupation path;
195+
4. convergence failures and iteration counts do not regress without a recorded
196+
numerical reason; and
197+
5. any claimed speedup reports the mesh, symmetry flags, precision, hardware,
198+
warm-up policy, and the same numerical acceptance criteria.
199+
200+
## Non-Goals
201+
202+
- Covering graphene, the Carbon dimer, or the seven-point Carbon EOS in the
203+
current standalone SCC convergence study.
204+
- Using `test_dftbscc_matches_dftbp_benchmarks` as the current execution entry
205+
point.
206+
- Changing default UniSK production parameters in this task.
207+
- Declaring a legacy `kmeshspacing` value physically necessary without a
208+
convergence study.
209+
- Replacing the SCC algorithm with a tetrahedron method before the current
210+
occupation and k-point paths have a complete regression suite.

dptb/nn/dftb/dftb_scc.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def reset(self) -> None:
8585
structures, as it avoids the overhead of reinitializing the expensive
8686
model, SK parameters, and transformers for each structure.
8787
88-
The following attributes are reset to None:
88+
The following per-calculation state is cleared or reset:
8989
- atomic_numbers: Atomic numbers of the current structure
9090
- elec_totE, elec_H0_bandE, elec_bandE: Electronic energies
9191
- E_fermi: Fermi energy
@@ -96,6 +96,9 @@ def reset(self) -> None:
9696
- inv_r: Inverse distance matrix
9797
- Gamma: Coulomb interaction matrix
9898
- total_energy, total_rep_energy: Total energies
99+
- n_iterations: Number of Mulliken/SCC iterations performed
100+
- charge_residual_history: Maximum charge residual from each SCC update
101+
- final_charge_residual: Last maximum charge residual, if one was evaluated
99102
100103
Also resets the Mulliken calculator state via mulliken.reset().
101104
'''
@@ -115,6 +118,9 @@ def reset(self) -> None:
115118
self.total_energy = None
116119
self.total_rep_energy = None
117120
self.is_converged = False # Track SCC convergence status
121+
self.n_iterations = 0
122+
self.charge_residual_history = []
123+
self.final_charge_residual = None
118124
# Also reset the Mulliken calculator state
119125
self.mulliken.reset()
120126

@@ -218,6 +224,12 @@ def run_iters(self,
218224
- self.elec_totE: Total electronic energy (eV)
219225
- self.E_fermi: Fermi energy (eV)
220226
- self.mulliken.mul_charge: Final Mulliken charges
227+
- self.is_converged: Whether the charge residual reached ``tol``
228+
- self.n_iterations: Number of Mulliken/SCC iterations performed
229+
- self.charge_residual_history: Maximum charge residual for each
230+
update after the initial charge evaluation
231+
- self.final_charge_residual: Last value in the residual history,
232+
or ``None`` when no residual was evaluated
221233
222234
'''
223235

@@ -328,10 +340,13 @@ def run_iters(self,
328340

329341
# Convergence criterion: ||q_diff|| = ||q_out - q_inp|| (DFTBplus convention)
330342
diff = np.abs(q_diff)
331-
log.debug(f' ITERATION: {int(iteration)} Max charge difference: {diff.max().item():.15f} e')
343+
max_charge_residual = float(diff.max())
344+
self.charge_residual_history.append(max_charge_residual)
345+
self.final_charge_residual = max_charge_residual
346+
log.debug(f' ITERATION: {int(iteration)} Max charge difference: {max_charge_residual:.15f} e')
332347
iteration += 1
333348

334-
if diff.max() < tol: # Converged and not exiting due to max_iter
349+
if max_charge_residual < tol: # Converged and not exiting due to max_iter
335350
assert self.scc_shift_energy is not None, "scc_shift_energy should not be None when converged."
336351
assert self.elec_H0_bandE is not None, "elec_H0_bandE should not be None when converged."
337352
log.debug(f'Convergence reached after {int(iteration)} iters.')
@@ -374,6 +389,7 @@ def run_iters(self,
374389
data[AtomicDataDict.EIGENVECTOR_KEY] = torch.cat(eigvecs, dim=0)
375390

376391
self.data = data
392+
self.n_iterations = iteration
377393
self.E_fermi = self.mulliken.estimated_E_fermi
378394
self.elec_bandE = self.mulliken.elec_bandE
379395
self.elec_totE = self.elec_H0_bandE + self.scc_shift_energy

dptb/tests/test_dftb_scc.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,9 @@ def test_dftbscc_reset_clears_state(rootdir=rootdir):
478478
assert dftbscc.expGamma_onsite is not None, "expGamma_onsite should be set after calculation"
479479
assert dftbscc.inv_r is not None, "inv_r should be set after calculation"
480480
assert dftbscc.Gamma is not None, "Gamma should be set after calculation"
481+
assert dftbscc.n_iterations > 0, "n_iterations should be populated after calculation"
482+
assert len(dftbscc.charge_residual_history) == dftbscc.n_iterations - 1
483+
assert dftbscc.final_charge_residual == dftbscc.charge_residual_history[-1]
481484

482485
# Call reset explicitly
483486
dftbscc.reset()
@@ -498,6 +501,9 @@ def test_dftbscc_reset_clears_state(rootdir=rootdir):
498501
assert dftbscc.Gamma is None, "Gamma should be None after reset"
499502
assert dftbscc.total_energy is None, "total_energy should be None after reset"
500503
assert dftbscc.total_rep_energy is None, "total_rep_energy should be None after reset"
504+
assert dftbscc.n_iterations == 0, "n_iterations should be zero after reset"
505+
assert dftbscc.charge_residual_history == [], "residual history should be empty after reset"
506+
assert dftbscc.final_charge_residual is None, "final residual should be None after reset"
501507

502508
# Verify persistent state (model, skp, etc.) is NOT cleared
503509
assert dftbscc.model is not None, "model should persist after reset"

examples/dftb_scc/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ Imports and obvious absolute/local SK paths were adjusted to use DeePTB modules
66

77
Recommended notebooks:
88

9+
- `carbon_bcc_scc_convergence/run.py`: standalone Carbon BCC self-consistent
10+
total-energy convergence against k meshes/spacings. Inner SCC, mixer, and
11+
smearing diagnostics are recorded separately. See the example README for
12+
commands and interpretation.
913
- `benchmark_eos_dftbp.ipynb`: SCC EOS comparison against DFTB+ reference data in `dptb/tests/data/dftb`.
1014
- `hBN_scc/test_scc_hBN.ipynb`: hBN SCC workflow and band calculation.
1115
- `hBN_scc/test_scc_hBN_nnsk.ipynb`: hBN nonorthogonal NNSK-SCC workflow.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
C BCC primitive cell, scale 1.000
2+
1.0000000000000000
3+
2.0557030981993387 0.0000005029268204 0.0000008733164838
4+
-0.6852331938919997 1.9381358756482154 0.0000008681559103
5+
-0.6852331920572658 -0.9690657046357299 1.6784761976400846
6+
C
7+
1
8+
Cartesian
9+
-0.0000000006781663 0.0000000005972072 -0.0000000000918888

0 commit comments

Comments
 (0)