am1-rs-python 0.1.3__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- am1_rs_python-0.2.2/CHANGELOG.md +1406 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/Cargo.lock +2 -1
- am1_rs_python-0.2.2/Cargo.toml +72 -0
- am1_rs_python-0.2.2/PKG-INFO +430 -0
- am1_rs_python-0.2.2/README.md +393 -0
- am1_rs_python-0.2.2/THIRD_PARTY_NOTICES.md +207 -0
- am1_rs_python-0.2.2/docs/divide-conquer.md +439 -0
- am1_rs_python-0.2.2/docs/methods.md +99 -0
- am1_rs_python-0.2.2/docs/pbc.md +816 -0
- am1_rs_python-0.2.2/docs/python-api.md +484 -0
- am1_rs_python-0.2.2/docs/rust-api.md +405 -0
- am1_rs_python-0.2.2/docs/scope.md +183 -0
- am1_rs_python-0.2.2/docs/theory.md +369 -0
- am1_rs_python-0.2.2/examples/bench/water_chain_102.xyz +104 -0
- am1_rs_python-0.2.2/examples/bench/water_chain_201.xyz +203 -0
- am1_rs_python-0.2.2/examples/bench/water_chain_399.xyz +401 -0
- am1_rs_python-0.2.2/examples/bench/water_chain_801.xyz +803 -0
- am1_rs_python-0.2.2/examples/bench/water_cluster_102.xyz +104 -0
- am1_rs_python-0.2.2/examples/bench/water_cluster_201.xyz +203 -0
- am1_rs_python-0.2.2/examples/bench/water_cluster_399.xyz +401 -0
- am1_rs_python-0.2.2/examples/bench/water_cluster_48.xyz +50 -0
- am1_rs_python-0.2.2/examples/bench/water_cluster_801.xyz +803 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/examples/bench102.xyz +104 -104
- am1_rs_python-0.2.2/pyproject.toml +85 -0
- am1_rs_python-0.2.2/python/am1_rs/__init__.py +70 -0
- am1_rs_python-0.2.2/python/am1_rs/__main__.py +397 -0
- am1_rs_python-0.2.2/python/am1_rs/_native.pyi +344 -0
- am1_rs_python-0.2.2/python/am1_rs/ase.py +1135 -0
- am1_rs_python-0.2.2/python/am1_rs/native.py +1084 -0
- am1_rs_python-0.2.2/python/am1_rs/py.typed +0 -0
- am1_rs_python-0.2.2/src/bcc/atomtype.rs +667 -0
- am1_rs_python-0.2.2/src/bcc/mod.rs +366 -0
- am1_rs_python-0.2.2/src/bin/am1_rs.rs +492 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/data/bccparm.dat +405 -405
- am1_rs_python-0.2.2/src/data/rm1_parameters.csv +27 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/data_tables.rs +5 -0
- am1_rs_python-0.2.2/src/dipole.rs +249 -0
- am1_rs_python-0.2.2/src/divide_conquer.rs +1548 -0
- am1_rs_python-0.2.2/src/dual.rs +448 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/dual2.rs +71 -8
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/error.rs +44 -2
- am1_rs_python-0.2.2/src/farfield.rs +561 -0
- am1_rs_python-0.2.2/src/fermi.rs +415 -0
- am1_rs_python-0.2.2/src/fock.rs +575 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/gradient.rs +327 -38
- am1_rs_python-0.2.2/src/hamiltonian.rs +366 -0
- am1_rs_python-0.2.2/src/hessian.rs +2189 -0
- am1_rs_python-0.2.2/src/integrals.rs +875 -0
- am1_rs_python-0.2.2/src/ir.rs +247 -0
- am1_rs_python-0.2.2/src/lattice.rs +813 -0
- am1_rs_python-0.2.2/src/lib.rs +100 -0
- am1_rs_python-0.2.2/src/linalg.rs +513 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/math.rs +26 -0
- am1_rs_python-0.2.2/src/method.rs +68 -0
- am1_rs_python-0.2.2/src/molden.rs +358 -0
- am1_rs_python-0.2.2/src/neighbors.rs +397 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/optimizer.rs +7 -1
- am1_rs_python-0.2.2/src/overlap.rs +749 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/overlap_numeric.rs +41 -9
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/params.rs +69 -4
- am1_rs_python-0.2.2/src/pbc/berry.rs +781 -0
- am1_rs_python-0.2.2/src/pbc/complex.rs +500 -0
- am1_rs_python-0.2.2/src/pbc/dfpt.rs +2014 -0
- am1_rs_python-0.2.2/src/pbc/ewald.rs +1916 -0
- am1_rs_python-0.2.2/src/pbc/ewald1d.rs +685 -0
- am1_rs_python-0.2.2/src/pbc/ewald2d.rs +773 -0
- am1_rs_python-0.2.2/src/pbc/extent.rs +689 -0
- am1_rs_python-0.2.2/src/pbc/finite_field.rs +888 -0
- am1_rs_python-0.2.2/src/pbc/gradient.rs +441 -0
- am1_rs_python-0.2.2/src/pbc/hessian.rs +2156 -0
- am1_rs_python-0.2.2/src/pbc/kpoints.rs +315 -0
- am1_rs_python-0.2.2/src/pbc/mod.rs +63 -0
- am1_rs_python-0.2.2/src/pbc/phonon.rs +556 -0
- am1_rs_python-0.2.2/src/pbc/scf.rs +1510 -0
- am1_rs_python-0.2.2/src/python.rs +2166 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/repulsion.rs +113 -120
- am1_rs_python-0.2.2/src/scf.rs +1318 -0
- am1_rs_python-0.2.2/src/system.rs +321 -0
- am1_rs_python-0.2.2/src/timing.rs +126 -0
- am1_rs_python-0.2.2/src/topology.rs +822 -0
- am1_rs_python-0.2.2/tests/auxiliary_integral_impact.rs +76 -0
- am1_rs_python-0.2.2/tests/axis_alignment.rs +278 -0
- am1_rs_python-0.2.2/tests/bcc_atom_types.rs +313 -0
- am1_rs_python-0.2.2/tests/bcc_bond_types.rs +386 -0
- am1_rs_python-0.2.2/tests/charged_cell_warning.rs +172 -0
- am1_rs_python-0.2.2/tests/core_core_derivatives.rs +292 -0
- am1_rs_python-0.2.2/tests/cphf_convergence.rs +105 -0
- am1_rs_python-0.2.2/tests/dc_convergence_probe.rs +190 -0
- am1_rs_python-0.2.2/tests/dc_open_shell_stress.rs +249 -0
- am1_rs_python-0.2.2/tests/dc_periodic.rs +338 -0
- am1_rs_python-0.2.2/tests/dc_profile.rs +86 -0
- am1_rs_python-0.2.2/tests/dc_where_the_time_goes.rs +86 -0
- am1_rs_python-0.2.2/tests/divide_conquer.rs +655 -0
- am1_rs_python-0.2.2/tests/element_coverage.rs +185 -0
- am1_rs_python-0.2.2/tests/external_field.rs +236 -0
- am1_rs_python-0.2.2/tests/farfield.rs +210 -0
- am1_rs_python-0.2.2/tests/farfield_tree.rs +213 -0
- am1_rs_python-0.2.2/tests/ir.rs +300 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/tests/molecules.rs +12 -2
- am1_rs_python-0.2.2/tests/mopac_reference.rs +354 -0
- am1_rs_python-0.2.2/tests/orbital_response.rs +287 -0
- am1_rs_python-0.2.2/tests/parameter_cache.rs +94 -0
- am1_rs_python-0.2.2/tests/pbc_berry.rs +379 -0
- am1_rs_python-0.2.2/tests/pbc_born_charges.rs +209 -0
- am1_rs_python-0.2.2/tests/pbc_charged.rs +359 -0
- am1_rs_python-0.2.2/tests/pbc_dense_stress.rs +168 -0
- am1_rs_python-0.2.2/tests/pbc_dfpt.rs +1009 -0
- am1_rs_python-0.2.2/tests/pbc_dielectric.rs +689 -0
- am1_rs_python-0.2.2/tests/pbc_dielectric_extent.rs +545 -0
- am1_rs_python-0.2.2/tests/pbc_ewald.rs +265 -0
- am1_rs_python-0.2.2/tests/pbc_exchange_diagnosis.rs +112 -0
- am1_rs_python-0.2.2/tests/pbc_external_field.rs +293 -0
- am1_rs_python-0.2.2/tests/pbc_finite_field.rs +397 -0
- am1_rs_python-0.2.2/tests/pbc_gamma.rs +301 -0
- am1_rs_python-0.2.2/tests/pbc_gradient.rs +232 -0
- am1_rs_python-0.2.2/tests/pbc_hessian.rs +261 -0
- am1_rs_python-0.2.2/tests/pbc_klopman_ohno_tail.rs +309 -0
- am1_rs_python-0.2.2/tests/pbc_kpoint_hessian.rs +373 -0
- am1_rs_python-0.2.2/tests/pbc_kpoints.rs +315 -0
- am1_rs_python-0.2.2/tests/pbc_lo_to.rs +400 -0
- am1_rs_python-0.2.2/tests/pbc_lowdim_ewald.rs +216 -0
- am1_rs_python-0.2.2/tests/pbc_phased_ewald.rs +352 -0
- am1_rs_python-0.2.2/tests/pbc_phonon.rs +334 -0
- am1_rs_python-0.2.2/tests/pbc_scf_convergence.rs +345 -0
- am1_rs_python-0.2.2/tests/pbc_truncation_study.rs +166 -0
- am1_rs_python-0.2.2/tests/pbc_uhf_response.rs +670 -0
- am1_rs_python-0.2.2/tests/phased_lowdim.rs +420 -0
- am1_rs_python-0.2.2/tests/phonon_determinism.rs +153 -0
- am1_rs_python-0.2.2/tests/response_memory.rs +209 -0
- am1_rs_python-0.2.2/tests/rm1.rs +219 -0
- am1_rs_python-0.2.2/tests/scalar_special.rs +155 -0
- am1_rs_python-0.2.2/tests/scaling.rs +366 -0
- am1_rs_python-0.2.2/tests/test_ase_pbc_md.py +878 -0
- am1_rs_python-0.2.2/tests/test_cli.py +283 -0
- am1_rs_python-0.2.2/tests/test_divide_conquer.py +244 -0
- am1_rs_python-0.2.2/tests/test_lazy_cache.py +170 -0
- am1_rs_python-0.2.2/tests/test_new_api_0_2_1.py +712 -0
- am1_rs_python-0.2.2/tests/theory_components.rs +791 -0
- am1_rs_python-0.2.2/tests/topology_bcc.rs +532 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/third_party/antechamber/ATOMTYPE_BCC.DEF +154 -154
- am1_rs_python-0.2.2/third_party/antechamber/LICENSE +674 -0
- am1_rs_python-0.2.2/third_party/antechamber/README.md +45 -0
- am1_rs_python-0.2.2/third_party/mopac/LICENSE +176 -0
- am1_rs_python-0.1.3/CHANGELOG.md +0 -49
- am1_rs_python-0.1.3/Cargo.toml +0 -38
- am1_rs_python-0.1.3/PKG-INFO +0 -238
- am1_rs_python-0.1.3/README.md +0 -224
- am1_rs_python-0.1.3/THIRD_PARTY_NOTICES.md +0 -80
- am1_rs_python-0.1.3/docs/python-api.md +0 -182
- am1_rs_python-0.1.3/docs/rust-api.md +0 -221
- am1_rs_python-0.1.3/docs/scope.md +0 -26
- am1_rs_python-0.1.3/docs/theory.md +0 -105
- am1_rs_python-0.1.3/pyproject.toml +0 -21
- am1_rs_python-0.1.3/python/am1_rs/__init__.py +0 -27
- am1_rs_python-0.1.3/python/am1_rs/ase.py +0 -134
- am1_rs_python-0.1.3/python/am1_rs/native.py +0 -144
- am1_rs_python-0.1.3/src/bcc.rs +0 -304
- am1_rs_python-0.1.3/src/bin/am1_rs.rs +0 -283
- am1_rs_python-0.1.3/src/dual.rs +0 -268
- am1_rs_python-0.1.3/src/fock.rs +0 -161
- am1_rs_python-0.1.3/src/hamiltonian.rs +0 -125
- am1_rs_python-0.1.3/src/hessian.rs +0 -1074
- am1_rs_python-0.1.3/src/integrals.rs +0 -473
- am1_rs_python-0.1.3/src/lib.rs +0 -54
- am1_rs_python-0.1.3/src/linalg.rs +0 -238
- am1_rs_python-0.1.3/src/overlap.rs +0 -443
- am1_rs_python-0.1.3/src/python.rs +0 -282
- am1_rs_python-0.1.3/src/scf.rs +0 -887
- am1_rs_python-0.1.3/src/system.rs +0 -149
- am1_rs_python-0.1.3/src/topology.rs +0 -274
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/LICENSE +0 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/examples/ethanol.xyz +0 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/examples/methane.xyz +0 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/examples/water.xyz +0 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/basis.rs +0 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/constants.rs +0 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/data/am1_parameters.csv +0 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/tests/test_python_api.py +0 -0
- {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/third_party/pyseqm/LICENSE +0 -0
|
@@ -0,0 +1,1406 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **A periodic SCF could not converge a symmetry-degenerate cell, and three separate defects were
|
|
8
|
+
in the way.** A two-dimensional lattice of methane — closed shell, 9 eV gap, no hydrogen bonds,
|
|
9
|
+
no magnetism, about as easy as a periodic calculation gets — could not reach `p_tol = 1e-10` at
|
|
10
|
+
any iteration count, mesh, cutoff, mixing fraction or smearing width. The isolated molecule
|
|
11
|
+
converged in 28 iterations, so nothing in the parameterization or the integrals was at fault.
|
|
12
|
+
What it does have is a **threefold degenerate HOMO**.
|
|
13
|
+
|
|
14
|
+
1. **`pbc::complex::hermitian_eigen` lost `√ε` on a degenerate level.** It solves the complex
|
|
15
|
+
Hermitian problem through a `2n × 2n` real embedding, in which every eigenvalue appears twice
|
|
16
|
+
— `(x, y)` and `(−y, x)` are the same complex vector, one times `i`. Picking one per pair used
|
|
17
|
+
a **single** classical Gram–Schmidt pass and accepted any residual above `1e-8`. On a
|
|
18
|
+
degenerate level the duplicate genuinely lies in the span already, so what survived the
|
|
19
|
+
subtraction was cancellation noise, renormalized to unit length and accepted as a physical
|
|
20
|
+
eigenvector. The occupied projector built from it carried about **3e-8**, which is exactly the
|
|
21
|
+
floor the SCF stalled at, and `3e-8` is `√ε` — the signature of the loss. Fixed by projecting
|
|
22
|
+
twice ("twice is enough") and cutting at `0.1`: a duplicate's residual is `O(ε)` and a
|
|
23
|
+
genuinely new direction inside a `k`-fold block has residual² of at least `1 − 1/k ≥ ½` for
|
|
24
|
+
some remaining column, so the two are seven orders apart and the old threshold sat between
|
|
25
|
+
them. The degenerate projector goes from 3e-8 to **2.2e-16** against its closed form.
|
|
26
|
+
|
|
27
|
+
2. **The periodic SCF had no convergence acceleration at all** — plain linear mixing at 0.3,
|
|
28
|
+
while the molecular path had A-DIIS→CDIIS. It did not show because the covered systems were
|
|
29
|
+
stiff: a hydrogen fluoride slab reached `1e-10` in about 140 passes, which reads as a slow
|
|
30
|
+
system rather than a missing feature. The near-constant 139–141 across unrelated systems was
|
|
31
|
+
the tell. Now Pulay (DIIS) mixing on the real-space density, `PbcOptions::diis_history`
|
|
32
|
+
(8 by default; `0` restores the old behaviour). Hydrogen fluoride 140 → **22**, water
|
|
33
|
+
140 → **28**, a methane slab 130 → **23**. Memory is `2 × depth` copies of the density, which
|
|
34
|
+
for a large cell is the dominant allocation of the run.
|
|
35
|
+
|
|
36
|
+
3. **The energy was not the energy of any density.** It was contracted from the *mixed* density
|
|
37
|
+
against the *unmixed* Fock — `½Tr[P_mixed(H + F(P_in))]` — and inconsistent with the
|
|
38
|
+
`total_origin` the same iteration built from `P_in`. At the fixed point all of them agree, so
|
|
39
|
+
the converged number was right; what it corrupted was `de`, half the convergence test, which
|
|
40
|
+
measured the mixer as much as the iteration. Moved above the mix.
|
|
41
|
+
|
|
42
|
+
And even then, `E[P] = ½Tr[P(H + F(P))]` is stationary **only on the idempotent manifold**,
|
|
43
|
+
which a mixed input is not on — a Pulay step is a signed combination of past densities and can
|
|
44
|
+
sit further off idempotency than its distance to the fixed point suggests. Evaluating there
|
|
45
|
+
leaves a *first-order* error that the energy hides and anything differenced does not. Once the
|
|
46
|
+
tolerances are met the solver now spends one further pass at the converged **output** density,
|
|
47
|
+
which is idempotent, so the returned energy is the variational energy of the returned density.
|
|
48
|
+
Measured on a water dimer: a central-differenced total energy against the analytic gradient
|
|
49
|
+
went **1.20e-6 → 3.04e-7** eV/Bohr, now slightly better than plain mixing rather than four
|
|
50
|
+
times worse.
|
|
51
|
+
|
|
52
|
+
The three share a lesson about ordering. A defect that only costs iterations hides a defect that
|
|
53
|
+
costs correctness, because both present as "it needs more iterations" — and the unconverged runs
|
|
54
|
+
reported energies differing by up to **0.6 eV** between k-meshes, every one of them plausible.
|
|
55
|
+
`tests/pbc_scf_convergence.rs` pins all three, and `AM1_SCF_TRACE=1` prints `dE` and `dP` per
|
|
56
|
+
iteration, which is the only way to tell a slow contraction from a stall from the outside.
|
|
57
|
+
|
|
58
|
+
- **A performance test asserted on wall clock and failed under load.**
|
|
59
|
+
`factoring_project_ov_lowers_its_scaling_exponent` claimed a drop from `O(nao⁴)` to `O(nao³)` and
|
|
60
|
+
checked it by requiring the measured *speedup ratio* to grow with `nao` — on the reasoning that a
|
|
61
|
+
ratio divides the machine's load out. It does not: both halves are wall clock, and the largest
|
|
62
|
+
case carries the most memory traffic and so suffers most. Idle it measured 54/108/357, 46/137/385
|
|
63
|
+
and 48/94/376 across three runs; with a build running alongside it measured 61/258/**46** and
|
|
64
|
+
failed. An exponent is a count of operations, not a duration, so the count is now what is
|
|
65
|
+
asserted — 10.7× → 21.3× → 42.7×, exactly doubling per doubling of `nao`, which *is* the missing
|
|
66
|
+
power — and the timings are printed with a load-proof 5× floor beneath them. Renamed to
|
|
67
|
+
`factoring_project_ov_lowers_its_operation_count`, which is what it now measures. Same rule
|
|
68
|
+
`DcResult::diagonalization_work` already followed.
|
|
69
|
+
|
|
70
|
+
- **AM1-BCC atom typing was wrong in ten places, and silently.** The typing was a hand
|
|
71
|
+
transcription of antechamber's `ATOMTYPE_BCC.DEF` into Rust `match` arms, and it had drifted from
|
|
72
|
+
the file. Every case is silent, because a parameter exists for the wrong type too — no warning
|
|
73
|
+
fires, a plausible number comes back.
|
|
74
|
+
|
|
75
|
+
The errors are counted **per atom**, not per bond, and that distinction is the point: a
|
|
76
|
+
correction is looked up on the *pair* of types, so changing one atom's type changes **every bond
|
|
77
|
+
at that atom**.
|
|
78
|
+
|
|
79
|
+
| group | file says | 0.2.1 said | bonds affected | error on that atom |
|
|
80
|
+
|---|---|---|---|---|
|
|
81
|
+
| nitro N | 23 | 21 | N–O ×2, C–N | **0.6745 e** |
|
|
82
|
+
| 3-coordinate P with a double bond | 42 | 41 | P=O, P–C ×2 | 0.44 e |
|
|
83
|
+
| pyrrole-type α carbon | 16 | 17 | C–N, C–C, C–H | 0.315 e |
|
|
84
|
+
| ester / carboxylic acid carbonyl O | 32 | 33 | C=O | 0.086 e |
|
|
85
|
+
| ketone / aldehyde carbonyl O | 31 | 32 | C=O | 0.050 e |
|
|
86
|
+
| amide carbonyl O | 31 | 33 | C=O | 0.036 e |
|
|
87
|
+
|
|
88
|
+
plus four nitrogen rules with no counterpart at all (a three-coordinate N with a double bond, a
|
|
89
|
+
two-coordinate amide N, a two-coordinate N with two single bonds, and a four-coordinate aromatic
|
|
90
|
+
N, which the file types 21 because `21 * 7 4 &` precedes the aromatic rule).
|
|
91
|
+
|
|
92
|
+
The cause was not a set of typos. It was that the *conditions* had been dropped: both `33` rules
|
|
93
|
+
carry `[RG]` and apply to lactones and lactams only, but the code looked at no ring and instead
|
|
94
|
+
counted the oxygens on the carbon; the `32` rule asks whether the carbon bears a **two-connected**
|
|
95
|
+
oxygen; the `17` rule requires an aromatic **`N2`**, and pyrrole's nitrogen has three connections;
|
|
96
|
+
and there is a nitro rule, `(O1,O1)`, that the transcription simply did not contain.
|
|
97
|
+
|
|
98
|
+
**The fix reads the file.** `src/bcc/atomtype.rs` parses `ATOMTYPE_BCC.DEF` and evaluates it —
|
|
99
|
+
`WILDATOM` expansion, atom properties, nested chemical-environment patterns with their `'`
|
|
100
|
+
bond-to-predecessor suffix, and the file's own top-to-bottom first-match-wins order, which its
|
|
101
|
+
closing note says is crucial and which is exactly what the four missing nitrogen rules turned on.
|
|
102
|
+
The rules are now antechamber's rather than a reading of them.
|
|
103
|
+
|
|
104
|
+
**This changes published charges** for every molecule with a carbonyl, a nitro group or a pyrrole
|
|
105
|
+
ring. The 0.2.1 values for those were wrong.
|
|
106
|
+
|
|
107
|
+
Two supporting changes were needed and are worth naming separately:
|
|
108
|
+
|
|
109
|
+
- **A Kekulé assignment** (`Topology::kekule_double`). `[2sb]` and `[sb,db]` separate nitrogen
|
|
110
|
+
types 21 and 24, so an aromatic ring bond has to be formally single *or* double, and which one
|
|
111
|
+
is not a local property. The parameter file settles the intent: it carries a `17–24` entry at
|
|
112
|
+
bond type 7, and since the `17` rule itself requires an aromatic two-connected nitrogen
|
|
113
|
+
neighbour, that pair is pyridine and nothing else. Found by perfect matching over the atoms
|
|
114
|
+
contributing one π electron.
|
|
115
|
+
- **The indole rule** from the definition file's own closing note: a five-membered ring sharing
|
|
116
|
+
an edge with a six-membered aromatic ring is not aromatic for AM1-BCC.
|
|
117
|
+
|
|
118
|
+
What is *not* needed, on measurement: the AR1..AR5 sub-classification, which earlier notes listed
|
|
119
|
+
as a gap. Every rule in this file asks for the union `[AR1.AR2]` and none asks for either class
|
|
120
|
+
alone, so splitting them would be machinery with no consumer.
|
|
121
|
+
|
|
122
|
+
- **An H–H or halogen–halogen bond reported itself as uncorrected.** `BCCPARM.DAT` has a bond type
|
|
123
|
+
for a pair with the *same atom type on both ends* — code 11, whose 26 entries are all `X–X` and
|
|
124
|
+
all exactly 0.0 — and ten of those types, hydrogen and every halogen among them, have **no
|
|
125
|
+
single-bond entry at all**. Emitting only codes 1, 2, 3, 6, 7 and 9 therefore left H₂, F₂, Cl₂,
|
|
126
|
+
Br₂ and I₂ with no parameter for their one bond, and `BccResult::warnings` said the bond was left
|
|
127
|
+
at its raw Mulliken charges. The charges were in fact right — the correction is zero — but the
|
|
128
|
+
warning is the thing callers are told to check, and "a molecule that returns no warnings is one
|
|
129
|
+
the rules covered" has to mean something.
|
|
130
|
+
|
|
131
|
+
All nine bond types are emitted now. Types 7 (aromatic single), 8 (aromatic double) and 10
|
|
132
|
+
(aromatic, no resolved order) are separated by the Kekulé structure the atom typing already
|
|
133
|
+
needs: a six-membered aromatic has two equivalent Kekulé structures, so none of its bonds is
|
|
134
|
+
*the* double bond and they take 10, while a five-membered heteroaromatic has a unique one and
|
|
135
|
+
takes 7 and 8. Type 11 is a fallback, not an override — where the ordinary code is tabulated it
|
|
136
|
+
wins, which `the_same_type_code_does_not_pre_empt_a_tabulated_one` pins.
|
|
137
|
+
|
|
138
|
+
**No charge moves**: 8 and 10 are byte-identical to 7 on every shared pair and every type-11
|
|
139
|
+
value is zero, both asserted against the parameter file rather than recited.
|
|
140
|
+
|
|
141
|
+
- **The antechamber licence was not in the repository, the crate, or the wheel.** `BCCPARM.DAT`
|
|
142
|
+
(GPL-3) is `include_str!`-ed into every binary, and `ATOMTYPE_BCC.DEF` now is too, while MOPAC's
|
|
143
|
+
and PySEQM's licences were retained and antechamber's was not. The CI job that checks
|
|
144
|
+
"third-party licences are inside the wheel" did not catch it because it asserted only that the
|
|
145
|
+
list was non-empty — two of three passed that. It now compares against the number of
|
|
146
|
+
`third_party/` subdirectories, so a fourth bundled work cannot repeat this, and the sdist check
|
|
147
|
+
additionally requires `ATOMTYPE_BCC.DEF`, which the build now needs. `third_party/antechamber/`
|
|
148
|
+
gains the GPL-3 text and a `README.md` recording which upstream file each copy is and where it
|
|
149
|
+
came from. `THIRD_PARTY_NOTICES.md` §4 and §6 are rewritten, including what "or later" does and
|
|
150
|
+
does not say for this material.
|
|
151
|
+
|
|
152
|
+
- **The lazy `get_*()` cache was never invalidated by a geometry change.** `docs/scope.md` claimed
|
|
153
|
+
these methods "cache into `results` and are invalidated by `check_state`". The first half was
|
|
154
|
+
true and the second was not: `results` is cleared by `Calculator.get_property`, which calls
|
|
155
|
+
`check_state` and then `reset()` — and none of the lazy methods go through `get_property`. So
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
f1 = atoms.calc.get_frequencies(atoms)
|
|
159
|
+
atoms.positions += 0.5
|
|
160
|
+
f2 = atoms.calc.get_frequencies(atoms) # returned f1
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
with nothing to announce it. The existing test could not catch it because it never moved the
|
|
164
|
+
geometry between calls. They now memoize into their own store keyed on the geometry's **bytes**,
|
|
165
|
+
the resolved state (including the `atoms.info` overrides ASE's own comparison cannot see) and
|
|
166
|
+
their own arguments; `tests/test_lazy_cache.py` asserts every method in the family against a
|
|
167
|
+
displacement, and asserts that it is still a cache.
|
|
168
|
+
|
|
169
|
+
- **`AM1.optimize(apply=True)` cleared `results` but left `self.atoms`** holding the
|
|
170
|
+
pre-optimization geometry. It calls `reset()` now.
|
|
171
|
+
|
|
172
|
+
- **The charged-cell warning described a version of the code that no longer existed.** It said no
|
|
173
|
+
compensating background is applied "because Ewald summation is not implemented", that "THE TOTAL
|
|
174
|
+
ENERGY IS NOT CONVERGED", and quoted a −331 eV to +72 eV swing across real-space cutoffs. Ewald
|
|
175
|
+
has been implemented since 0.2.0, in all three dimensionalities, and is on by default — those
|
|
176
|
+
were the pre-Ewald numbers. It was telling users their converged 3D energies were meaningless,
|
|
177
|
+
through every surface (a `RuntimeWarning` in ASE and a line in both CLIs).
|
|
178
|
+
|
|
179
|
+
Measured rather than reasoned about (`tests/charged_cell_warning.rs`): a +1 water cell in an 8 Å
|
|
180
|
+
cube across a 6.5× range of cutoff moves **0.197 eV with Ewald and 403.4 eV without**. The
|
|
181
|
+
warning is now per-dimensionality — in 3D the tin-foil sum defines the energy and the residual is
|
|
182
|
+
the `R⁻³` tail; in 1D/2D the monopole sum is applied but the neutralizing background's placement
|
|
183
|
+
is a *convention* (`SheetConvention` / `AxisConvention`) that nothing in the SCF path consults,
|
|
184
|
+
so the absolute energy is not defined there. Both texts are ASCII, for the cp932/C-locale reason
|
|
185
|
+
0.2.1 recorded.
|
|
186
|
+
|
|
187
|
+
- **The phonon spectrum was not reproducible.** `ForceConstants::blocks` is a `HashMap`, and three
|
|
188
|
+
float sums iterated it directly: the Bloch sum `D(q) = Σ_T Φ(T) e^{iq·T}`, the acoustic-sum-rule
|
|
189
|
+
residual, and the acoustic-sum-rule *correction*. Rust seeds each `HashMap` instance from a
|
|
190
|
+
thread-local counter, so two maps built from the same insertions in the same process iterate in
|
|
191
|
+
different orders — and floating-point addition is not associative.
|
|
192
|
+
|
|
193
|
+
Measured: five identical `lo_to_frequencies` calls in one process, on a water crystal in a 4.5 Å
|
|
194
|
+
cube, agreed on four and differed by **1798 cm⁻¹** on the fifth — one O–H stretch collapsing into
|
|
195
|
+
a near-zero mode. The periodic SCF underneath was bit-identical every time (same energy to the
|
|
196
|
+
last digit, same 115 iterations), which is what located it in the phonon assembly rather than the
|
|
197
|
+
electronic structure.
|
|
198
|
+
|
|
199
|
+
The correction is the one that mattered: it is *subtracted* from the on-site block, so an
|
|
200
|
+
order-dependent value there changes `Φ` itself and every `D(q)` built from it afterwards. The
|
|
201
|
+
other two stayed within last bits.
|
|
202
|
+
|
|
203
|
+
All three now iterate a translation-sorted view. `tests/phonon_determinism.rs` asserts
|
|
204
|
+
**bit-identical** repeats rather than a tolerance — the claim is that the same input gives the
|
|
205
|
+
same output, which is a property of the code, not of any crystal's conditioning. Found because
|
|
206
|
+
`test_lo_to_frequencies_splits_and_matches_across_surfaces` failed intermittently and only in a
|
|
207
|
+
full-file run; the ASE and native paths it compares call the same function, so the disagreement
|
|
208
|
+
could not have been between them.
|
|
209
|
+
|
|
210
|
+
- **`LongRangeMonopole::for_molecule`'s documentation contradicted its code**, saying the
|
|
211
|
+
correction applies "only to a fully three-dimensional cell" while the code accepts any
|
|
212
|
+
`n_periodic() >= 1` and dispatches to the 1D and 2D kernels. Only the *phased* (DFPT) path is 3D
|
|
213
|
+
only. Corrected in the docstring and in `docs/scope.md`.
|
|
214
|
+
|
|
215
|
+
### Added
|
|
216
|
+
|
|
217
|
+
- **`ε_∞` for a slab or a chain**, once the caller says how thick the material is:
|
|
218
|
+
`pbc::dielectric_tensor_with_extent`, `am1_rs.dielectric_with_extent`,
|
|
219
|
+
`AM1.get_dielectric_tensor_with_extent`. `ExtentConvention::SlabThickness` (Bohr) or
|
|
220
|
+
`WireCrossSection` (Bohr²) is **required and never defaulted** — a supercell says where the atoms
|
|
221
|
+
are, not where the material stops, and every choice changes `ε`. Same rule as `chain_radius` and
|
|
222
|
+
`AxisConvention`.
|
|
223
|
+
|
|
224
|
+
**It is not a division.** The `α` this crate computes is the response to the *external* field —
|
|
225
|
+
the induced charges interact through the same Coulomb operator the SCF uses, so for a slab
|
|
226
|
+
polarized along its normal the depolarizing field is already inside `α`. The conversion therefore
|
|
227
|
+
carries the depolarization factor of the assumed body,
|
|
228
|
+
`ε = 1 + 4πχ/(1 − 4πNχ)` with `χ = α/(measure · extent)`: `N` = 0 in a slab's plane and along a
|
|
229
|
+
wire's axis, 1 along a slab normal, ½ transverse to a wire's circular section. Three-dimensional
|
|
230
|
+
tin-foil summation removes the macroscopic depolarizing field, so `N = 0` there — which means the
|
|
231
|
+
same arithmetic reproduces `dielectric_tensor` rather than sitting beside it, measured at 1e-13.
|
|
232
|
+
|
|
233
|
+
Getting that factor backwards returns a plausible number: both laws are positive and monotonic in
|
|
234
|
+
`α`. What separates them is a **sign asymmetry** in the response itself, and
|
|
235
|
+
`tests/pbc_dielectric_extent.rs` measures it — tightening a 2D methane lattice from 14 Å to 6.5 Å
|
|
236
|
+
moves `α_xx` up (8.293 → 8.439 Bohr³) and `α_zz` down (8.230 → 7.920), which is what a sheet of
|
|
237
|
+
induced dipoles does and what an internal-field response would not show at all.
|
|
238
|
+
|
|
239
|
+
The thickness is a choice, so `ε` is a choice; two combinations are not, and are returned
|
|
240
|
+
alongside it: `(ε_∥ − 1)d = 4πα_∥/A` and `(1 − 1/ε_⊥)d = 4πα_⊥/A`, half the first being the
|
|
241
|
+
Rytova–Keldysh screening length. Read the other way they are capacitor stacking — parallel and
|
|
242
|
+
series — so the two formulas are forced rather than chosen once the thickness is named, which is
|
|
243
|
+
a second derivation and is tested as one. The first must also equal what `dielectric_function`
|
|
244
|
+
reaches through a reciprocal-space Coulomb kernel: measured ratio **2.0000000000**. And `ε` does
|
|
245
|
+
not move when only the vacuum padding changes, which is precisely what 0.2.0 got wrong.
|
|
246
|
+
|
|
247
|
+
Eleven component tests sit next to the arithmetic in `src/pbc/extent.rs`, where they can use a
|
|
248
|
+
synthetic `α`, because the conversion is the model-dependent step and deserves to be checked
|
|
249
|
+
without an SCF in the way.
|
|
250
|
+
|
|
251
|
+
- **`native.vibrations`** — the Hessian, frequencies, normal modes, atomic polar tensor,
|
|
252
|
+
intensities and orbital response from **one** SCF and one CPHF solve. `hessian`, `frequencies`,
|
|
253
|
+
`ir_spectrum`, `dipole_derivatives` and `orbital_response` each ran the whole analytic-Hessian
|
|
254
|
+
solve and kept a different contraction of it, so a caller wanting a spectrum *and* the Hessian it
|
|
255
|
+
came from — the ordinary case — paid for the CPHF once per question. The ASE calculator routes
|
|
256
|
+
all five through it, and `tests/test_lazy_cache.py` asserts that the family leaves exactly one
|
|
257
|
+
entry in the cache. The five original functions are unchanged.
|
|
258
|
+
|
|
259
|
+
- **A Barnes–Hut far field**, so the NDDO Coulomb is no longer `O(N²)`. `docs/scope.md` recorded
|
|
260
|
+
"linear-scaling Coulomb ⛔ — stays `O(N²)` by construction", which was true: `FarField` keeps the
|
|
261
|
+
interaction in full and simplifies only its *shape*, so the prefactor fell a hundredfold and the
|
|
262
|
+
exponent did not move. `FarField::tree(theta)` moves it: fitted **1.65 against 2.13** over 24 to
|
|
263
|
+
1029 atoms, with 131 515 partner evaluations against 1 043 490 at the top — an 8× reduction that
|
|
264
|
+
grows with size.
|
|
265
|
+
|
|
266
|
+
Each accepted cluster becomes **two** pseudo-atoms, the positive and negative charge at their own
|
|
267
|
+
centroids. One would be a monopole expansion, and a monopole expansion is worthless here: the
|
|
268
|
+
clusters are made of neutral molecules, so the net charge is near zero and the interaction is
|
|
269
|
+
dipolar. The first draft did exactly that and the error against the direct sum was 64 % and did
|
|
270
|
+
not shrink with the acceptance angle — there was no monopole for the angle to resolve. Splitting
|
|
271
|
+
by sign carries the dipole while keeping the property that makes the design safe: every consumer
|
|
272
|
+
evaluates the *ordinary pair kernel* against a shorter list, so the potential, the gradient and
|
|
273
|
+
the virial cannot drift apart the way three separately truncated expansions would.
|
|
274
|
+
|
|
275
|
+
At `theta = 0` the tree visits **exactly** the pairs the direct sum does — asserted as an
|
|
276
|
+
equality on the count — and agrees to 5.3e-15, the residual being summation order. In between the
|
|
277
|
+
error is monotone in `theta`: 2.7 % at 0.8, 0.3 % at 0.05.
|
|
278
|
+
|
|
279
|
+
**Opt-in**, because an acceptance angle makes the energy a discontinuous function of the geometry
|
|
280
|
+
where an atom crosses the boundary. The jump is of the order of the truncation error, but it is a
|
|
281
|
+
jump, and molecular dynamics should either leave it off or accept it knowingly.
|
|
282
|
+
|
|
283
|
+
- **Berry-phase polarization** (`pbc::berry`), the modern theory of polarization. Listed as ⛔
|
|
284
|
+
through 0.2.1 — "`ε_∞` is the clamped-ion dipole response, not a Berry phase" — which was accurate
|
|
285
|
+
and was a gap: the dipole of a periodic cell is not a property of the crystal, so the crate had
|
|
286
|
+
polarization's *second* derivative and not polarization.
|
|
287
|
+
|
|
288
|
+
`P_el = (e/Ω) Σ_α a_α · Im ln Π_j det S(k_j, k_{j+1})/2π` over strings of k points, with the
|
|
289
|
+
occupied-manifold overlap in this basis being `S_mn = Σ_μ c*_{μm}(k) e^{−ib·τ_μ} c_{μn}(k+b)` —
|
|
290
|
+
the `e^{−ib·τ_μ}` being the same "an orbital sits at its atom" approximation the dipole operator
|
|
291
|
+
already makes. Returned modulo the polarization quantum, with `BerryPolarization::difference`
|
|
292
|
+
reducing two values to a common branch, because subtracting absolute polarizations is the
|
|
293
|
+
standard way to be wrong by exactly one quantum.
|
|
294
|
+
|
|
295
|
+
**The sign was derived rather than looked up**, sources differing on the convention: for a single
|
|
296
|
+
electron whose only orbital sits at `τ`, every link contributes `e^{−ib·τ}` and the string product
|
|
297
|
+
is `e^{−iB·τ}`, so `φ = −τ_α/a_α` in turns; that electron is a charge `−1` at `τ`, which fixes the
|
|
298
|
+
prefactor to `+e/Ω`. The first draft had it negative and the acoustic sum rule found it at once —
|
|
299
|
+
the Born charges summed to `+2 n_elec` instead of zero.
|
|
300
|
+
|
|
301
|
+
Validated four ways, none of which compares `P` to a number (an absolute polarization is not a
|
|
302
|
+
physical prediction): translating the cell by a lattice vector leaves it unchanged **exactly**, a
|
|
303
|
+
centrosymmetric cell gives zero to 8.5e-18, the phase converges to 4.0e-8 by 32 points per string,
|
|
304
|
+
and — the sharp one — `Ω ∂P/∂τ_A` reproduces the **Born effective charges** the CPHF dipole
|
|
305
|
+
response produces, two formalisms sharing only the SCF.
|
|
306
|
+
|
|
307
|
+
That last comparison differs by 0.207 e on hydrogen fluoride, and the reason is *measured* rather
|
|
308
|
+
than asserted: the dipole operator additionally carries the on-site `s`–`p` hybridization moment
|
|
309
|
+
`dd`, which this basis's Berry phase does not. On a **hydrogen-only** cell, where hydrogen has no
|
|
310
|
+
`p` shell and the `dd` term is structurally unreachable, the two routes agree to **7.5e-13 e**.
|
|
311
|
+
|
|
312
|
+
- **The long-range monopole term in the DFPT response, in 1D and 2D.** 0.2.1 shipped it for 3D
|
|
313
|
+
cells and named its absence on a chain or a slab as the release's one unfinished item:
|
|
314
|
+
`LongRange::Require` was an error there, and `Auto` silently dropped the channel. Both
|
|
315
|
+
dimensionalities now have a phased kernel, each the `q`-shifted form of the machinery its
|
|
316
|
+
unphased sum already used:
|
|
317
|
+
|
|
318
|
+
- **2D** — Parry's slab sum over the **full shifted in-plane set** with prefactor `π/(A|k|)`,
|
|
319
|
+
`k = G − q`. There is no ±G folding to exploit once `q ≠ 0`, and at `q = 0` the full set with
|
|
320
|
+
`π` reproduces the folded half set with `2π` — which is what makes a wrong factor of two here
|
|
321
|
+
visible to the splitting-parameter test and almost nothing else.
|
|
322
|
+
- **1D** — the chain's direct summation, phased image by image, with the truncated tail summed by
|
|
323
|
+
**repeated Abel transformation**. Truncating the oscillating sum directly is only `O(1/N)`:
|
|
324
|
+
Dirichlet converges it because the partial sums of `e^{iθn}` are bounded by `1/(2|sin(θ/2)|)`,
|
|
325
|
+
but that bound multiplies the first neglected term and blows up as `q → 0`. Summation by parts
|
|
326
|
+
trades it for a series over exact forward differences of the kernel, truncated at its smallest
|
|
327
|
+
term.
|
|
328
|
+
|
|
329
|
+
Both delegate to their unphased counterpart where `q` is a reciprocal lattice vector — not where
|
|
330
|
+
`q = 0`, which is the silent version of the same test — so the neutralizing background, the sheet
|
|
331
|
+
term and the chain's line charge each keep exactly one derivation.
|
|
332
|
+
|
|
333
|
+
Validated against a **direct lattice sum**, Cesàro-averaged to damp the conditionally convergent
|
|
334
|
+
boundary term (without which the oracle is less accurate than the thing it checks): 1D agrees to
|
|
335
|
+
**1.6e-12**, 2D to 1.2e-5…9.7e-5 where the oracle's own drift is 8e-5…2.3e-4. The sharp checks are
|
|
336
|
+
the internal ones a wrong prefactor cannot survive — the slab sum is independent of the splitting
|
|
337
|
+
parameter to **8.9e-16** across a 2.8× range, the chain sum independent of its explicit image
|
|
338
|
+
count to **7.2e-16** across a 6× range — plus `S(−q) = S(q)*`, periodicity in `q`, and derivatives
|
|
339
|
+
against finite differences to 8e-12. On a polar HF chain the term moves `D(q)` by 3.9e-5 eV/Bohr²,
|
|
340
|
+
so it is doing something rather than merely running.
|
|
341
|
+
|
|
342
|
+
**What `q → 0` does, corrected.** An earlier draft of this work recorded "2D is discontinuous at
|
|
343
|
+
Γ". That conflated two levels. The *kernel* diverges in every dimensionality — `4π/(Vq²)`,
|
|
344
|
+
`2π/(A|q|)`, `−(2/L)ln|q|`, all three measured here — but the contribution to `D(q)` carries two
|
|
345
|
+
factors of `q` from charge conservation, so only **3D** is left with a finite direction-dependent
|
|
346
|
+
limit. 2D goes as `O(|q|)` and 1D as `q² ln(1/q)`: both continuous at Γ, with a non-analytic
|
|
347
|
+
approach. There is no LO–TO splitting at Γ in 2D, only a linear kink.
|
|
348
|
+
|
|
349
|
+
- **Divide-and-conquer open-shell analytic stress.** Refused through 0.2.1 for want of a
|
|
350
|
+
spin-resolved pair virial. `electronic_gradient_and_virial_fixed_density_spin` is the restricted
|
|
351
|
+
loop with the exchange coefficient reading `Pα`/`Pβ` instead of half the total, and returns the
|
|
352
|
+
virial alongside the gradient from one pass for the same reason the restricted one does.
|
|
353
|
+
|
|
354
|
+
Validated two ways, because either alone would pass for the wrong reason: forced UHF on a closed
|
|
355
|
+
shell reproduces the restricted stress to **2.5e-14** (different code, algebraically identical
|
|
356
|
+
answer), and a neutral triplet chain matches a strain finite difference to **1.9e-8 eV/Bohr³**.
|
|
357
|
+
The finite difference is reported across three step sizes rather than one, and shows the V a
|
|
358
|
+
correct derivative makes: 1.1e-5 at `h = 1e-6` where the SCF's own convergence dominates,
|
|
359
|
+
1.9e-8 at `1e-5`, 2.2e-2 at `1e-4` where harmonic truncation does.
|
|
360
|
+
|
|
361
|
+
- **The `R⁻³` Klopman–Ohno tail beyond the pair list is summed.** `Am1Options::klopman_ohno_tail`
|
|
362
|
+
and `PbcOptions::klopman_ohno_tail`, default `true`; `false` restores 0.2.1.
|
|
363
|
+
|
|
364
|
+
`ewald` made the `1/R` channel exact, but NDDO's kernel is `γ_η(R) = 1/√(R² + η²)`, and
|
|
365
|
+
`γ_η − 1/R = −η²/2R³ + …` was left truncated at the cutoff. `Σ_T |T|⁻³` diverges logarithmically
|
|
366
|
+
in three dimensions, so the total energy drifted with `realspace_cutoff` and converged to
|
|
367
|
+
nothing. `docs/scope.md` recorded it as "⛔ real-space; logarithmically divergent, 0.10 eV per
|
|
368
|
+
unit `ln r_c`".
|
|
369
|
+
|
|
370
|
+
The translations the pair list dropped are now summed **explicitly**, out to three cutoffs, using
|
|
371
|
+
the exact `γ_η − 1/R` and not its expansion — the sum depends on the pair only through
|
|
372
|
+
`η_ab = ρ_a + ρ_b`, so it costs one lattice sum per *element* pair. Past that a continuum
|
|
373
|
+
remainder takes over through a quintic taper, and its integrand is per-dimensionality. Only the
|
|
374
|
+
three-dimensional remainder carries a logarithm, and only there is a reference length needed;
|
|
375
|
+
`Σ_T |T|⁻³` converges outright in 1D and 2D.
|
|
376
|
+
|
|
377
|
+
Measured on a +1 water cell over a 6.5× range of cutoff, the residual per unit `ln r_c` went from
|
|
378
|
+
**−0.118, −0.098, −0.097 eV** — constant, which is what identifies it as the logarithm — to
|
|
379
|
+
**0.000, −0.000, −0.000**, and the energy spread from 0.197 eV to 6e-5 eV. Forces move by 0.08 %
|
|
380
|
+
of their scale, which is the density shifting under the Fock diagonal the tail adds; the stress
|
|
381
|
+
matches its strain finite difference to 6.9e-9 eV/Bohr³.
|
|
382
|
+
|
|
383
|
+
Two things went wrong on the way and are worth recording, because both are invisible in a
|
|
384
|
+
passing test:
|
|
385
|
+
|
|
386
|
+
- **The first draft applied the three-dimensional formula in every dimensionality**, and moved a
|
|
387
|
+
charged chain's energy by 3e-2 eV. This is the same error `docs/scope.md` already records for
|
|
388
|
+
`ε_∞` and LO–TO, committed a second time in the same file.
|
|
389
|
+
- **The response was left without it** while the ground state had it, which is the response of a
|
|
390
|
+
Hamiltonian the SCF never converged. It showed up as `D(q = 0)` missing the `q = 0` Hessian by
|
|
391
|
+
4.6e-4 eV/Bohr² — two numbers that are the same number. The tail is now carried through
|
|
392
|
+
`solve_bands` and the DFPT response kernel; the *cutoff-dependent* part of the tail is
|
|
393
|
+
`−(4π/V) ln r_c`, which does not depend on `q`, so the same constant is correct at every `q`.
|
|
394
|
+
|
|
395
|
+
- **`tests/dc_open_shell_stress.rs` was passing on a coincidence, and now measures something.** Its
|
|
396
|
+
finite difference used a **triplet water chain**, whose energy is not a smooth function of strain
|
|
397
|
+
at all: it jumps in quanta of about 1.7e-5 eV as an occupation switches at the Fermi level, which
|
|
398
|
+
a triplet built from closed-shell waters invites. The quoted 1.9e-8 eV/Bohr³ agreement was
|
|
399
|
+
`E(+h)` and `E(-h)` happening to land on the same branch. Perturbing the Hamiltonian at the 1e-8
|
|
400
|
+
level — all the Klopman–Ohno tail does there — moved that "agreement" to 1.0e-1.
|
|
401
|
+
|
|
402
|
+
The fixture is now a **methyl-radical chain**: a doublet with one well-separated singly-occupied
|
|
403
|
+
orbital, whose energy over the same strain sweep is linear to eight figures. The finite
|
|
404
|
+
difference now shows a real V — 1.07e-5, **2.41e-9**, 4.53e-8 across `h = 1e-6, 1e-5, 1e-4` —
|
|
405
|
+
so the minimum is a converged derivative and not a slope. A finite difference quoted at one step
|
|
406
|
+
size cannot tell those apart.
|
|
407
|
+
- **The k-point periodic response handles open shells.** `pbc_hessian`, `born_charges` and the
|
|
408
|
+
CPHF behind them accept an unrestricted ground state; 0.2.1 refused with "the k-point periodic
|
|
409
|
+
response is restricted-only". This is the one item of the 0.2.2 list with no sibling crate to
|
|
410
|
+
port from — pm6-rs and pm7-rs refuse in the same place.
|
|
411
|
+
|
|
412
|
+
The restricted path solves one CPHF; this solves two, **coupled**, because the kernel is
|
|
413
|
+
`G^σ(ΔP) = J(ΔP_tot) − K(ΔP_σ)` and α reads β's response density through the Coulomb half.
|
|
414
|
+
Solving the channels independently would drop `J(ΔP_β)` from `G^α` and return a plausible
|
|
415
|
+
number. Three factor conventions move with it — what one orbital holds (2 restricted, 1 per
|
|
416
|
+
channel), the exchange weight in both the skeleton and the perturbed Fock, and the relaxation
|
|
417
|
+
term's 4 becoming 2 per channel.
|
|
418
|
+
|
|
419
|
+
Forcing UHF on a **closed** shell reproduces the restricted answer to **8.9e-16** eV/Bohr² on a
|
|
420
|
+
3-point mesh, and the Born charges exactly. A genuine doublet chain matches a finite difference
|
|
421
|
+
of the analytic gradient to 5.2e-7 of 15.9. The first of those is the sharp check: on a closed
|
|
422
|
+
shell `P^α = P^β = P/2` makes the two algebraically identical, so any one of the three factors
|
|
423
|
+
being wrong breaks it loudly.
|
|
424
|
+
|
|
425
|
+
It found one such break immediately, and it is worth naming because it fails **silently**: the
|
|
426
|
+
occupied/virtual classification tested each level's occupation against a hard-coded `2.0`. On
|
|
427
|
+
the unrestricted path a full level holds 1, so no level was ever classified occupied, `n_ov` was
|
|
428
|
+
zero at every k, and the entire orbital-relaxation term vanished — 74 % of the force constants,
|
|
429
|
+
with no error raised.
|
|
430
|
+
|
|
431
|
+
Two things deliberately stay restricted and now say so rather than being answered with the
|
|
432
|
+
restricted equations: **DFPT at finite `q`** (a larger machine — band pairs across `k` and
|
|
433
|
+
`k + q` weighted by occupation differences) and the **field response** behind `ε_∞` and the
|
|
434
|
+
polarizability, which is already three-dimensional-only.
|
|
435
|
+
|
|
436
|
+
- **The CLI printed `-0.0` for a rigid-body frequency**, and the Rust and Python front ends
|
|
437
|
+
disagreed about which side of zero it fell on. Both are numerically zero; the sign of a value
|
|
438
|
+
below the print precision is not information, but printing it made `tests/test_cli.py` compare
|
|
439
|
+
the two front ends' last bits. Both now print `0.0`.
|
|
440
|
+
- **An external electric field works under periodic boundary conditions**, when it is orthogonal to
|
|
441
|
+
every lattice vector. `PbcOptions::electric_field`, and `Am1Options::electric_field` no longer
|
|
442
|
+
refuses a cell outright.
|
|
443
|
+
|
|
444
|
+
0.2.1 rejected any field under any cell, with the reason "`F·R` is unbounded along a periodic
|
|
445
|
+
direction". The reason is right and the rule drawn from it was too broad: `F·R` shifts by `F·T`
|
|
446
|
+
under translation by `T`, so the perturbation repeats with the lattice **exactly when
|
|
447
|
+
`F·T = 0` for every lattice vector**. A slab in a field along its normal and a chain in a
|
|
448
|
+
transverse field satisfy that and are ordinary calculations; they were being refused along with
|
|
449
|
+
the ill-defined case.
|
|
450
|
+
|
|
451
|
+
The check is now on the direction and names the offending component when it fires. Measured: the
|
|
452
|
+
periodic gradient in a transverse field matches a finite difference of the periodic energy to
|
|
453
|
+
**8.8e-8** eV/Bohr, and a water molecule in a 60 Bohr cell with a field along a non-periodic axis
|
|
454
|
+
reproduces the isolated-molecule path to **5.0e-6** eV — two code paths sharing only
|
|
455
|
+
`crate::dipole`, one number.
|
|
456
|
+
|
|
457
|
+
**Not** done, and named so it is not mistaken for done: a finite field *along* a periodic
|
|
458
|
+
direction. That needs the Berry-phase electric enthalpy `E − Ω F·P`, whose field term couples
|
|
459
|
+
neighbouring k-points through `S⁻¹` and therefore requires the SCF to solve its k-points
|
|
460
|
+
together rather than one at a time. The **linear** response along a periodic direction is
|
|
461
|
+
available and validated — `dielectric_tensor` / `ε_∞` through the CPHF — so what is missing is
|
|
462
|
+
the non-linear regime and finite-field geometry optimization. The polarization half of the
|
|
463
|
+
machinery already exists (`pbc::berry`, new in this release).
|
|
464
|
+
- **A finite electric field along a periodic direction**, by the Berry-phase electric enthalpy.
|
|
465
|
+
`pbc::run_finite_field`.
|
|
466
|
+
|
|
467
|
+
`F·R` is unbounded there, so there is nothing to fix about it: what replaces it is Nunes and
|
|
468
|
+
Gonze's `E − Ω 𝓔·P`, minimized instead of the energy, with `P` the Berry phase rather than `⟨r⟩`.
|
|
469
|
+
Its derivative with respect to the orbitals is built from overlaps between **neighbouring k
|
|
470
|
+
points**, so the k points can no longer be solved one at a time — the SCF gained a `pub(crate)`
|
|
471
|
+
entry point taking a k-resolved additive operator, and an outer loop refreshes it until it stops
|
|
472
|
+
moving.
|
|
473
|
+
|
|
474
|
+
The coupling constant is derived from this crate's own polarization convention rather than
|
|
475
|
+
quoted, because the conventions differ between sources and a wrong factor here does not fail —
|
|
476
|
+
it returns a plausible polarizability. **What says it is right** is that `α = Ω ∂P/∂𝓔` by finite
|
|
477
|
+
differences matches the **CPHF** polarizability, two formalisms sharing only the SCF: on a
|
|
478
|
+
hydrogen-only cell they agree to **0.03–0.47 %**, and the residual falls as `O(1/J²)` with the
|
|
479
|
+
string length (1.06 → 0.47 → 0.26 % for J = 4, 6, 8). It caught the one real error on the way:
|
|
480
|
+
the first draft symmetrized the field operator as `(M + M†)/2`, which halves the
|
|
481
|
+
occupied–virtual coupling — the whole of the response — and gave 0.56 of the CPHF value. The
|
|
482
|
+
construction that is both Hermitian and faithful is `A = H − ½PHP` with `H = M + M†`.
|
|
483
|
+
|
|
484
|
+
**The comparison is exact only where the two compute the same object.** On a p-block cell they
|
|
485
|
+
differ by 12 %, and that is the Berry phase's own limitation, not the field's: in an atom-centred
|
|
486
|
+
minimal basis the phase tracks the charge *centres* and carries no `dd`, the on-site moment
|
|
487
|
+
between an `s` and a `p` on the same atom. `pbc::berry` already records the same gap for the Born
|
|
488
|
+
charges (0.207 e on HF, 7.5e-13 e with no p orbitals). A planar cell's out-of-plane response from
|
|
489
|
+
this path is **exactly zero**, because there that moment is the whole of it — recorded as a test
|
|
490
|
+
rather than left as a surprise.
|
|
491
|
+
|
|
492
|
+
3D, restricted, no smearing, and at least three k points along any direction the field has a
|
|
493
|
+
component in.
|
|
494
|
+
|
|
495
|
+
Reachable from all three surfaces: `pbc::run_finite_field`, `am1_rs.finite_field`, and
|
|
496
|
+
`AM1.get_finite_field` (which takes **V/Å** like the rest of the ASE layer and converts with the
|
|
497
|
+
crate's own constants). **Berry-phase polarization**, added earlier in this release, was
|
|
498
|
+
Rust-only until now and gained the same three — `am1_rs.polarization` and `AM1.get_polarization`
|
|
499
|
+
— which is what the project's own native↔ASE parity rule asks for and what
|
|
500
|
+
`tests/test_new_api_0_2_1.py` enforces.
|
|
501
|
+
|
|
502
|
+
- **The open-shell k-point response now covers DFPT at finite `q` and the dielectric response.**
|
|
503
|
+
Both refused earlier in this release's own notes; both go through the same two coupled spin
|
|
504
|
+
channels as the `q = 0` Hessian, from one shared split of the density
|
|
505
|
+
(`pbc::scf::spin_channel_densities`) so the three cannot disagree about it.
|
|
506
|
+
|
|
507
|
+
Forcing UHF on a closed shell reproduces `D(q = 0.3)` on a 4-point mesh to **6.1e-9** eV/Bohr²,
|
|
508
|
+
and gives the restricted `α` and `ε_∞` back **exactly**. A genuine doublet chain's DFPT
|
|
509
|
+
`D(q = 0)` matches the open-shell `q = 0` Hessian to **1.3e-7** of 15.9 (8.2e-9 relative) — the
|
|
510
|
+
same number by two different machines, each running two coupled channels. An open-shell radical
|
|
511
|
+
in a 12 Å box has the isolated radical's finite-field polarizability to **0.41 %**, against 0.17 %
|
|
512
|
+
for the restricted analogue at the same box size.
|
|
513
|
+
- **The Berry phase carries the on-site `s`–`p` moment.** It tracked only the charge *centres*
|
|
514
|
+
until now, and that was the single largest reason the Berry route and the CPHF route disagreed.
|
|
515
|
+
|
|
516
|
+
The link operator `Λ_{μν} = ⟨χ_μ| e^{−i b·r} |χ_ν⟩` was the diagonal `e^{−i b·τ_μ}`: each orbital
|
|
517
|
+
treated as a point at its own atom. The exact same-atom block, which is all NDDO keeps, is
|
|
518
|
+
`e^{−i b·τ_a}` times `exp(−i b·D^a)` with `D^a_{μν} = ⟨χ_μ|(r − τ_a)|χ_ν⟩` — and in a minimal
|
|
519
|
+
`sp` basis that is exactly the `dd` [`crate::dipole::dipole_operator`] already puts on the
|
|
520
|
+
`(s, p_α)` elements. Both now read it from the same parameter. `b·D^a` is a rank-two operator, so
|
|
521
|
+
its exponential is a rotation in the `(s, u)` subspace and is available in closed form;
|
|
522
|
+
exponentiating rather than truncating at `I − i b·D` keeps `|det Λ| = 1`, so the string's product
|
|
523
|
+
drifts only in phase.
|
|
524
|
+
|
|
525
|
+
Measured three ways:
|
|
526
|
+
|
|
527
|
+
| | before | after |
|
|
528
|
+
|---|---|---|
|
|
529
|
+
| Born charges vs CPHF, HF cell | 0.207 e | **1.2e-3 e** at 8 points per string, falling as `O(1/J²)` |
|
|
530
|
+
| finite-field `α` vs CPHF, water crystal | 12 % | **0.05 %** |
|
|
531
|
+
| `α_zz` of a planar cell, which is *entirely* this moment | **exactly 0** | 0.25527 against the CPHF's 0.25564 |
|
|
532
|
+
|
|
533
|
+
The planar case is the sharpest: with a diagonal `Λ` the `z → −z` mirror made the occupied bands
|
|
534
|
+
parity eigenstates and the link overlaps block-diagonal in that parity, so the field operator
|
|
535
|
+
could not mix them and the out-of-plane response was identically zero. It is the on-site moment
|
|
536
|
+
that couples `s` to `p_z`, so a wrong sign there would have moved it to the wrong number rather
|
|
537
|
+
than merely scaling it.
|
|
538
|
+
|
|
539
|
+
A second finding came out of the same comparison: the old 0.207 e was **not** all on-site moment.
|
|
540
|
+
`tests/pbc_berry.rs` compared a Γ-only CPHF against a 12-point string — two different samplings
|
|
541
|
+
of the Brillouin zone — and read the difference as physics. With the sampling matched the
|
|
542
|
+
residual is 1.2e-3 and converging. The test now matches them and asserts the convergence.
|
|
543
|
+
|
|
544
|
+
- **The polarizability is available for a chain and a slab.** `pbc::polarizability`,
|
|
545
|
+
`am1_rs.polarizability`, `AM1.get_polarizability`.
|
|
546
|
+
|
|
547
|
+
`dielectric_tensor` was the only entry point and refused a reduced-dimensional cell — correctly,
|
|
548
|
+
for the `ε_∞ = 1 + 4πα/Ω` step, which needs `Ω` to be a volume — but it took `α` down with it.
|
|
549
|
+
`α` is a *response*, and a response is well defined whatever the cell is periodic in: the origin
|
|
550
|
+
dependence that would spoil an absolute dipole cancels in the derivative because charge is
|
|
551
|
+
conserved. The two are now separate functions, and the 3D refusal names the one that works.
|
|
552
|
+
|
|
553
|
+
What stays refused is only the conversion. A slab's `α/A` has units of **length** and is the
|
|
554
|
+
quantity the monolayer literature reports; turning it into a dielectric constant needs a
|
|
555
|
+
thickness, which is a choice about the material rather than something a supercell fixes. The
|
|
556
|
+
units per dimensionality are tabulated on `polarizability` so the 0.2.0 mistake — dividing by a
|
|
557
|
+
length and calling the result `ε_∞` — cannot be repeated by accident.
|
|
558
|
+
|
|
559
|
+
- **`E_inf(q)` in every dimensionality**, `pbc::dielectric_function` / `am1_rs.dielectric_function`.
|
|
560
|
+
|
|
561
|
+
`eps_inf = 1 + 4*pi*alpha/Omega` is a **constant**, and that is a three-dimensional accident
|
|
562
|
+
rather than the general case. The general relation is `eps(q) = 1 - v_d(q) chi0(q)` with `v_d`
|
|
563
|
+
the bare Coulomb kernel of that dimensionality — the same object `pbc::ewald::LongRangeKernel`
|
|
564
|
+
is built around — and `chi0 -> -q^2 (qhat.alpha.qhat)/measure`. Putting the three kernels in:
|
|
565
|
+
|
|
566
|
+
| | `v_d(q)` | `eps(q)` | at `q -> 0` |
|
|
567
|
+
|---|---|---|---|
|
|
568
|
+
| crystal | `4pi/q^2` | `1 + 4pi (qhat.alpha.qhat)/Omega` | a constant — this is `eps_inf` |
|
|
569
|
+
| slab, `q` in plane | `2pi/|q|` | `1 + 2pi (qhat.alpha.qhat)|q|/A` | **-> 1** |
|
|
570
|
+
| chain, `q` along it | `2 K0(|q|rho)` | `1 + 2 K0 q^2 (qhat.alpha.qhat)/L` | **-> 1** |
|
|
571
|
+
|
|
572
|
+
So a sheet or a wire has no long-wavelength dielectric constant: it does not screen a field whose
|
|
573
|
+
wavelength exceeds its own extent. That is not a limitation of the implementation — it is *why*
|
|
574
|
+
`1 + 4pi*alpha/Omega` cannot be evaluated there, and it is the same fact as a slab having no
|
|
575
|
+
LO-TO splitting at Gamma. Measured: in three dimensions `eps(q)` reproduces `dielectric_tensor`'s
|
|
576
|
+
constant at every `q` to 1e-9; a slab's `eps(q) - 1` fits an exponent of **1.000**; a chain's
|
|
577
|
+
climbs 1.64 -> 1.71 toward the 2 that `q^2 K0` gives up to its logarithm.
|
|
578
|
+
|
|
579
|
+
The two-dimensional form is thickness-free, which is what makes `2pi chi_2D` — the
|
|
580
|
+
Rytova-Keldysh screening length — an intrinsic property of the layer. Assigning a slab a
|
|
581
|
+
thickness and quoting `1 + 4pi chi_2D/d` is a different, model-dependent number and is
|
|
582
|
+
deliberately not offered. A chain needs a transverse radius for its logarithm, and it is
|
|
583
|
+
**required** rather than guessed.
|
|
584
|
+
|
|
585
|
+
`K0` is the one special function the crate carries beyond `erf`, and it is checked against
|
|
586
|
+
Abramowitz & Stegun's own table before anything is built on it — which caught a real bug on the
|
|
587
|
+
way in: the `I0` series inside it runs in `(x/3.75)^2` and the `K0` series beside it in
|
|
588
|
+
`(x/2)^2`, and writing one variable for both put `K0(0.1)` 0.8 % off with nothing else in the
|
|
589
|
+
crate able to notice.
|
|
590
|
+
- **LO–TO below three dimensions: there is nothing to add, and it is now measured rather than
|
|
591
|
+
argued.** The long-range kernel diverges in every dimensionality — `4π/(Vq²)`, `2π/(A|q|)`,
|
|
592
|
+
`−(2/L)ln|q|` — but the *contribution to `D(q)`* carries `q²` from charge conservation, so only
|
|
593
|
+
three dimensions keeps a finite direction-dependent limit and is discontinuous at Γ. That
|
|
594
|
+
discontinuity **is** the LO–TO splitting.
|
|
595
|
+
|
|
596
|
+
`|D(q) − D(0)|` at `q = 0.02, 0.01, 0.005` along the periodic axis:
|
|
597
|
+
|
|
598
|
+
| | | | |
|
|
599
|
+
|---|---|---|---|
|
|
600
|
+
| 1D chain | 3.6e-3 | 1.8e-3 | **9.0e-4** |
|
|
601
|
+
| 2D slab | 4.6e-3 | 2.2e-3 | **1.1e-3** |
|
|
602
|
+
| 3D crystal | 1.071e-1 | 1.074e-1 | **1.075e-1** |
|
|
603
|
+
|
|
604
|
+
The low-dimensional cases converge to Γ; the crystal does not. So `frequencies_with_lo_to`
|
|
605
|
+
refusing a chain or a slab is the physics and not a gap — there is no splitting at Γ to add —
|
|
606
|
+
and the non-analytic *approach*, which is real, the DFPT path already carries exactly. 0.2.0's
|
|
607
|
+
"127 cm⁻¹ of splitting on a polar chain" was an artifact of applying the 3D kernel.
|
|
608
|
+
|
|
609
|
+
### Performance
|
|
610
|
+
|
|
611
|
+
- **The parameter set is cached per method.** `Am1Parameters::for_method` re-parsed the embedded
|
|
612
|
+
CSV and re-ran the `rho1`/`rho2` secant solves for every element on **every call** — and every
|
|
613
|
+
function on the Python surface calls it at its top. Measured at **270 µs**, against a 1361 µs
|
|
614
|
+
water single point: about 17 % of every small-molecule call, paid again on every step of a
|
|
615
|
+
molecular-dynamics loop. It is a fixed per-call cost, which is exactly the shape a large-system
|
|
616
|
+
profile cannot see. Now 2 µs for a clone, and `Am1Parameters::shared` borrows for the callers
|
|
617
|
+
that only read.
|
|
618
|
+
|
|
619
|
+
- **The infrared atomic polar tensor is `O(N³)`, not `O(N⁴)`.** It built `∂P/∂R_j` — an `nao²`
|
|
620
|
+
matrix, `O(nao² n_occ)` each — for all `3N` perturbations and traced each against `M_α`, to keep
|
|
621
|
+
three numbers per perturbation. Writing `∂P = B + Bᵀ` and using that `M_α` is symmetric gives
|
|
622
|
+
`Tr[∂P M_α] = 2w Tr[Uᵀ (C_vᵀ M_α C_o)]`, so the `nao²` object never has to exist: project `M_α`
|
|
623
|
+
into the occupied–virtual block once, and each perturbation is one Frobenius product of
|
|
624
|
+
`n_vir × n_occ`. The factor `2w` is 4 for RHF and 2 per spin for UHF — the same convention the
|
|
625
|
+
periodic relaxation term uses. Exact, and checked by the three independent identities already in
|
|
626
|
+
`tests/ir.rs` (the sum rule at 3e-15, a dipole finite difference, and the interchange theorem).
|
|
627
|
+
|
|
628
|
+
- **A pack-index table** in the two-centre Fock contraction, replacing a branch and a multiply in
|
|
629
|
+
the innermost loop. Bit-identical to the closed form and strictly less work, with
|
|
630
|
+
`the_pack_table_is_the_closed_form` asserting the equivalence over the whole domain.
|
|
631
|
+
|
|
632
|
+
- **The `q = 0` periodic response no longer holds a density per perturbation.** The
|
|
633
|
+
coupled-perturbed solve consumes one perturbation's response density at a time, but built all
|
|
634
|
+
`3N` of them before the loop and kept a second array of the same size for the spin-summed total.
|
|
635
|
+
The arithmetic is identical either way — the loop nest is the same, only its order changed — but
|
|
636
|
+
the resident set was `(1 + n_channels) · ndof · n_T · nao²` doubles where
|
|
637
|
+
`(1 + n_channels) · n_T · nao²` will do. That is a factor of `3N` on two of the three arrays of
|
|
638
|
+
that shape, and the Born charges and the polarizability, which read only each perturbation's
|
|
639
|
+
**origin** block, now stream as well.
|
|
640
|
+
|
|
641
|
+
Measured with a peak-tracking global allocator rather than reasoned about
|
|
642
|
+
(`tests/response_memory.rs`): on 27 atoms with 7 translations the response adds **14.7 MB** over
|
|
643
|
+
the ground-state SCF's own, against the **39.7 MB** the old shape needed. The remaining third is
|
|
644
|
+
the bare `∂F/∂R`, which is assembled pair-major and so cannot be streamed without `O(N)` passes
|
|
645
|
+
over the pair list; holding it sparsely is an `O(N)` win asymptotically but costs more below
|
|
646
|
+
about a dozen atoms, so it is left as it is and named here rather than half-done.
|
|
647
|
+
- **`Matrix::frobenius_dot` accumulates in eight lanes.** A single running total is a dependency
|
|
648
|
+
chain the compiler may not reorder, so the loop ran at one add per latency however wide the
|
|
649
|
+
machine. This changes the summation order, as the 0.2.1 DIIS packing did.
|
|
650
|
+
|
|
651
|
+
- AM1-BCC no longer perceives the topology twice (`write_mol2` re-derived it, which also meant the
|
|
652
|
+
file could in principle disagree with the charges beside it — `BccResult` carries the bonds now),
|
|
653
|
+
and the 405-entry parameter table is parsed once per process rather than per call.
|
|
654
|
+
|
|
655
|
+
### Not done, and named so it is not mistaken for done
|
|
656
|
+
|
|
657
|
+
- **The Berry phase and the finite field below three dimensions, or open-shell.** Both are
|
|
658
|
+
*implemented* in 0.2.2 — the heading is about where they stop. `pbc::berry` and
|
|
659
|
+
`pbc::run_finite_field` require a three-dimensional restricted cell: the polarization quantum is
|
|
660
|
+
`e a/Ω` and Ω has to be a volume, a slab or a chain has a polarization along its periodic
|
|
661
|
+
directions only which the module does not separate out, and an open-shell cell would need each
|
|
662
|
+
spin manifold's phase separately. Both refuse rather than answering with the three-dimensional
|
|
663
|
+
closed-shell expression. A field *orthogonal* to every lattice vector needs none of this and is
|
|
664
|
+
supported in every dimensionality and for open shells (`PbcOptions::electric_field`); so is the
|
|
665
|
+
polarizability itself, which is a response rather than a phase.
|
|
666
|
+
- **The CPHF perturbation batching named in 0.2.1's `fock.rs` as "the next thing to try" is not
|
|
667
|
+
done, because it was measured and it is slower.** The experiment was run in a sibling NDDO crate
|
|
668
|
+
with the same loop: batching the response Fock across degrees of freedom went 5.2 → 8.8 s on a
|
|
669
|
+
102-atom Hessian, gathering the density sub-blocks 5.2 → 9.8 s, and packing the Coulomb
|
|
670
|
+
contraction 4.17 → 4.39 s. The batching did what it was meant to structurally — 70 Fock passes
|
|
671
|
+
instead of 3961 — and was still slower: at that size the whole integral set is about 4 MB, so it
|
|
672
|
+
sits in L3 across calls and there is no traffic to save, while batching costs the per-DOF
|
|
673
|
+
parallelism the `par_iter` gets for free. At NDDO block sizes this loop is bound by **per-pair
|
|
674
|
+
overhead**, not by memory or arithmetic. The measurement is recorded in `src/fock.rs` in place of
|
|
675
|
+
the suggestion, so the afternoon is not spent again.
|
|
676
|
+
|
|
677
|
+
## 0.2.1
|
|
678
|
+
|
|
679
|
+
### Added
|
|
680
|
+
|
|
681
|
+
- **External electric field** for molecules: energy, analytic gradient and analytic Hessian.
|
|
682
|
+
`Am1Options::electric_field` (eV per e·Bohr). `E(F) = E₀ − μ·F` with this model's own dipole;
|
|
683
|
+
the operator is the new `dipole` module, which the molecular field and the periodic field
|
|
684
|
+
response both call rather than transcribing. `born_charges_from_response` still writes its own
|
|
685
|
+
three-term derivative form, so the sign convention is shared by two of its three consumers.
|
|
686
|
+
Because that operator is *linear* in the nuclear positions the field adds nothing to the
|
|
687
|
+
fixed-density second derivative and reaches the Hessian only through the CPHF response — which
|
|
688
|
+
is why the Hessian is checked against finite differences under a field rather than assumed.
|
|
689
|
+
Measured: gradient 1.8e-6 eV/Bohr and Hessian 8.1e-7 relative against a full-SCF finite
|
|
690
|
+
difference; `−∂E/∂F` reproduces the reported dipole to 3e-8 e·Bohr. Refused under a cell, since
|
|
691
|
+
`F·R` is unbounded along a periodic direction.
|
|
692
|
+
- **Infrared spectra** (`ir`): the atomic polar tensor `∂μ_α/∂R_{a,β}` as a raw `3 × 3N` matrix,
|
|
693
|
+
and km/mol intensities projected onto normal modes. Validated three ways — the translational
|
|
694
|
+
sum rule `Σ_a ∂μ/∂R_a = q δ` (3e-15), a full-SCF dipole finite difference (7e-7 e), and the
|
|
695
|
+
interchange theorem `∂μ_α/∂R_j = −∂²E/∂F_α∂R_j`, the right-hand side taken as a finite
|
|
696
|
+
difference of the analytic gradient in the field (1.2e-6 e). The two routes share no code past
|
|
697
|
+
the SCF, which is the point; a *field* CPHF would make the second route analytic too, and there
|
|
698
|
+
is no molecular field-CPHF path in the crate. CO₂'s symmetric stretch comes out dark at 1.6e-15
|
|
699
|
+
against 8.75 for the antisymmetric one.
|
|
700
|
+
- **Wavefunction output in Molden format** (`molden`): `[Atoms]`, `[STO]` and `[MO]`. The AM1
|
|
701
|
+
basis is Slater-type, so `[STO]` represents it exactly with no Gaussian expansion invented. The
|
|
702
|
+
file and the docs both state the caveat that matters: NDDO *assumes* an orthonormal AO basis, so
|
|
703
|
+
the coefficients are in an implicitly orthogonalized basis while the listed Slater functions are
|
|
704
|
+
the raw ones.
|
|
705
|
+
- **First-order orbital response** is returned rather than discarded:
|
|
706
|
+
`analytic_hessian_with_response` hands back `U`, `G` and the response density the CPHF already
|
|
707
|
+
solved for. An infrared spectrum therefore costs a Hessian and nothing more.
|
|
708
|
+
- **Normal modes** on `VibrationalModes` — mass-weighted eigenvectors, Cartesian displacements,
|
|
709
|
+
and each mode's overlap with the rigid-body subspace, so a linear molecule's five rigid-body
|
|
710
|
+
modes are *discovered* rather than assumed from `3N − 6`.
|
|
711
|
+
- **β orbitals for UHF.** `Am1Result` carries the β energies and coefficients; the SCF solved for
|
|
712
|
+
them and then threw them away, which made a spin-polarized wavefunction unreportable.
|
|
713
|
+
- **DFPT is generalized in `k` as well as `q`.** `DfptOptions` takes an arbitrary mesh or an
|
|
714
|
+
explicit k-point list, and `DfptResult` returns the `(k, k+q)` band energies, occupations and
|
|
715
|
+
first-order densities. `PbcOptions::kpoints` lets the response and the ground state share one
|
|
716
|
+
*resolved* k-set rather than two independent resolutions of the same description.
|
|
717
|
+
- **The long-range monopole term is in the DFPT response**, on a 3D cell, at every `q`
|
|
718
|
+
(`LongRange::Auto`, the default). `EwaldSum::phased_pair_potential` returns the value, gradient
|
|
719
|
+
and Hessian of the phased sum `Σ_T e^{iq·T} erfc(α|d+T|)/|d+T| + …` in one pass, with the
|
|
720
|
+
reciprocal half summed over `k = G − q` — the phase moves the *shell*, not just the summand.
|
|
721
|
+
|
|
722
|
+
**The element dropped is `k = 0`, not `G = 0`**, which is a correction to the convention 0.2.0's
|
|
723
|
+
docs recorded as settled. `k = 0` arises only when `q` folds to Γ, where it is exactly the
|
|
724
|
+
divergent term the neutralizing background cancels, so the rule reduces to this crate's tin-foil
|
|
725
|
+
`Σ_{G≠0}` at `q = 0`. Dropping the long-wavelength element `k = −q` instead — the alternative,
|
|
726
|
+
which keeps the direction-dependent part out of `D(q)` so LO–TO can supply it — was implemented
|
|
727
|
+
and **rejected on measurement**: that rule is not periodic in `q`, failing `Δ(q+G) = Δ(q)` by
|
|
728
|
+
1.2e1 where the accepted rule gives 9.2e-14, and it has no well-defined answer at a zone
|
|
729
|
+
boundary where several `k` tie for smallest.
|
|
730
|
+
|
|
731
|
+
So `D(q)` is now the **full** dynamical matrix and its `q → 0` limit is direction dependent,
|
|
732
|
+
which is the physics. It must **not** be combined with `frequencies_with_lo_to`, which exists to
|
|
733
|
+
restore that same physics to the supercell route; use one or the other.
|
|
734
|
+
|
|
735
|
+
A phase error here leaves the matrix Hermitian and the frequencies real, so this is validated by
|
|
736
|
+
identities: the kernel is independent of the real-space cutoff to **2.2e-16 at `q = ¼`**, where
|
|
737
|
+
the truncated sum alone moves by 1.4e-1; `Δ(−q) = Δ(q)*` to 1.5e-15; derivatives against finite
|
|
738
|
+
differences to 4e-10; at Γ it reproduces `pbc_hessian` to 1.7e-8 relative while contributing
|
|
739
|
+
3.6e-2 eV/Bohr²; and the acoustic sum rule holds to 1.2e-9, which it does because the
|
|
740
|
+
fixed-charge second derivative phases the pair term and never the self term.
|
|
741
|
+
|
|
742
|
+
What the correction does *not* cover is the `R⁻³` Klopman–Ohno tail, which stays with the
|
|
743
|
+
real-space sum — so the assembled `D(q)`'s cutoff dependence at `q = ¼` falls from 3.1e-2 to
|
|
744
|
+
2.0e-2 rather than to zero. That residual is the tail, not the monopole channel; the 2.2e-16
|
|
745
|
+
above is what separates the two claims.
|
|
746
|
+
|
|
747
|
+
### Fixed
|
|
748
|
+
|
|
749
|
+
- **DFPT sampled a different Brillouin zone from its own ground state.** The response mesh was a
|
|
750
|
+
hand-rolled Γ-centred grid built from `kmesh.sizes()` alone, so a `MonkhorstPackShifted` request
|
|
751
|
+
gave the ground state `{−1/4, +1/4}` and the response `{0, 1/2}`. Nothing announced it: the
|
|
752
|
+
force constants stayed real and the frequencies plausible. The regression test asserts the
|
|
753
|
+
`q = 0` identity on a shifted mesh — agreement is now 1.2e-9 relative, and the test also
|
|
754
|
+
measures that the two meshes differ by 2.9e-2 eV/Bohr², so it could not have passed by accident.
|
|
755
|
+
Non-periodic axes are collapsed too, which removes `n²` redundant diagonalizations on a slab.
|
|
756
|
+
- **`ε_∞` was 27× too close to 1: the polarizability was never converted to atomic units.**
|
|
757
|
+
The field CPHF is solved in this crate's interior units — orbital energies in eV, positions in
|
|
758
|
+
Bohr — so `U ~ M/Δε` carries Bohr/eV and the assembled `α = Σ_a R_a ΔQ_a` is in `e²·Bohr²/eV`.
|
|
759
|
+
It was returned labelled Bohr³ and fed straight into `ε_∞ = 1 + 4πα/Ω`, which needs atomic
|
|
760
|
+
units. The missing factor is one Hartree in eV.
|
|
761
|
+
|
|
762
|
+
Nothing caught it because every test checked `α`'s **shape** — symmetric, positive-definite,
|
|
763
|
+
independent of the cell origin — and a value wrong by a constant factor satisfies all three.
|
|
764
|
+
The new `a_molecule_in_a_large_box_has_the_isolated_molecule_polarizability` checks its
|
|
765
|
+
*magnitude* instead, against the finite-field polarizability of the same molecule with no cell:
|
|
766
|
+
two routes sharing only the SCF and the dipole operator, one an analytic CPHF and the other two
|
|
767
|
+
extra SCF solves per axis. Water's mean `α` is 3.379 Bohr³ isolated, and the periodic value
|
|
768
|
+
converges to it as the box grows — 0.85 % at 7 Å, 0.40 % at 9 Å, **0.17 % at 12 Å** — where
|
|
769
|
+
before the fix it sat at 0.125 Bohr³ and did not converge to anything.
|
|
770
|
+
|
|
771
|
+
This changes every `ε_∞` and therefore every LO–TO splitting reported by 0.2.0 and by earlier
|
|
772
|
+
drafts of 0.2.1.
|
|
773
|
+
- **LO–TO splitting and `ε∞` were three-dimensional formulas applied to chains.**
|
|
774
|
+
`ε∞ = 1 + 4πα/Ω` and `D_NA ∝ 4π/(Ω q·ε∞·q)` need `Ω` to be a volume, but `Lattice::measure`
|
|
775
|
+
returns a *length* for a chain and an *area* for a slab, and `tests/pbc_lo_to.rs` ran both on 1D
|
|
776
|
+
chains. A genuinely 1D-periodic chain has **no** LO–TO splitting as `q → 0` (the term vanishes
|
|
777
|
+
as `q² ln q`), so the 127 cm⁻¹ and 1631 cm⁻¹ figures recorded in the 0.2.0 notes below were
|
|
778
|
+
artifacts. Both functions now require a fully periodic cell, and the tests were moved to a 3D
|
|
779
|
+
polar crystal, where the added term matches its closed form to 2e-15.
|
|
780
|
+
- **DFPT reported `residual: NaN`** when the coupled-perturbed solve hit its iteration cap, so a
|
|
781
|
+
caller could not tell a stiff system from a broken one. It now reports the residual it reached,
|
|
782
|
+
and the tolerances and iteration cap are options rather than private constants.
|
|
783
|
+
- **The analytic UHF Hessian built a different Hamiltonian from the SCF**, always molecular and
|
|
784
|
+
always without the long-range or far-field corrections, whatever the options said. Its skeleton
|
|
785
|
+
loop is structurally molecular, so a periodic or far-field-screened request is now refused
|
|
786
|
+
rather than silently answered with a molecular result.
|
|
787
|
+
- A time-reversal-folded mesh, a `q` component along a non-periodic axis, and an explicit k-list
|
|
788
|
+
whose weights do not sum to 1 are all refused by DFPT instead of quietly producing an answer.
|
|
789
|
+
- **`am1-rs energy` crashed part-way through its output on any machine whose locale is not
|
|
790
|
+
UTF-8.** Python encodes `print` with the locale's codec, and the dipole line was written
|
|
791
|
+
`e·a0`: on a Japanese Windows (cp932) or under the `C` locale that minimal Docker images ship
|
|
792
|
+
with, that raised `UnicodeEncodeError` after six lines had already gone to stdout, and the
|
|
793
|
+
command exited 1 with a truncated report. `gradient` and `optimize` went the same way. The Rust
|
|
794
|
+
binary never raised — it writes UTF-8 whatever the locale — but rendered mojibake on the same
|
|
795
|
+
console, so the two front ends did not agree there either.
|
|
796
|
+
|
|
797
|
+
Both CLIs now print **ASCII only** (`e*a0`, `cm^-1`, `eV/A`), which is the fix that works on
|
|
798
|
+
every console rather than merely avoiding the exception, and the Python front end additionally
|
|
799
|
+
forces its streams to UTF-8 so that a non-ASCII message from the native layer cannot kill it.
|
|
800
|
+
`pip install` and the `am1-rs` console script were verified end to end in a clean virtualenv
|
|
801
|
+
under both cp932 and a forced ASCII stdout.
|
|
802
|
+
|
|
803
|
+
The test suite had not caught this because the development machine had `PYTHONIOENCODING`
|
|
804
|
+
set in its shell and pytest's subprocesses inherited it, giving every child a UTF-8 stdout that
|
|
805
|
+
no user would have. `tests/test_cli.py` now strips that variable from the child environment,
|
|
806
|
+
asserts that both CLIs' bytes are ASCII in every mode, and runs `energy` under a deliberately
|
|
807
|
+
ASCII stdout.
|
|
808
|
+
|
|
809
|
+
### Performance
|
|
810
|
+
|
|
811
|
+
- **The molecular SCF's DIIS history is half the size, and peak memory fell 28 %.** `rhf_loop`
|
|
812
|
+
kept three depth-8 histories — Fock, `[F,P]` error, density — as dense `nao²` matrices: at 1602
|
|
813
|
+
AOs (an 801-atom water cluster) twenty-four of them are **492 MB**, against a measured 877 MB
|
|
814
|
+
peak for the whole run. It was the single largest term.
|
|
815
|
+
|
|
816
|
+
Every matrix in all three is either symmetric (`F`, `P`) or **anti**symmetric (`[F,P] = FP−PF`,
|
|
817
|
+
whose diagonal is identically zero), so one triangle determines the other and packing loses
|
|
818
|
+
nothing — `packing_a_diis_history_preserves_it_exactly` checks the round trip and the Frobenius
|
|
819
|
+
products the extrapolation actually consumes, including that the commutator's diagonal really
|
|
820
|
+
is zeroed. The density history is additionally only built for the accelerator that reads it
|
|
821
|
+
(`AdiisCdiis`), which is a further third off for a CDIIS-only run. `uhf_loop` gets the same
|
|
822
|
+
treatment, its stacked two-spin error packed as two triangles end to end.
|
|
823
|
+
|
|
824
|
+
Measured on the 801-atom cluster: **877 MB → 632 MB** peak working set, same energies, and the
|
|
825
|
+
SCF converging in 13 iterations against 14 — the histories are bit-equivalent, but the
|
|
826
|
+
Frobenius products are summed in a different order, which moves the last iteration across the
|
|
827
|
+
convergence threshold.
|
|
828
|
+
- **The Hessian's orbital-relaxation contraction is a matrix product.** `H_relax[a][b] =
|
|
829
|
+
4 G^a : U^b` was `ndof²` independent Frobenius dots, which re-reads every `G` row `ndof` times
|
|
830
|
+
and is memory bound; stacking the ov-blocks makes it `G Uᵀ`. The periodic version was the same
|
|
831
|
+
nest four levels deep and *not parallelized at all*; it is now two products per k point.
|
|
832
|
+
|
|
833
|
+
The molecular one is **tiled**, and that is not incidental: stacking `G` and `U` whole would
|
|
834
|
+
add two `ndof × n_ov` buffers, which is `O(N³)` and would double the largest array the Hessian
|
|
835
|
+
holds. Copying 64-row tiles bounds the extra at `O(N²)` and trades for redundant copying worth
|
|
836
|
+
`1/64` of the arithmetic.
|
|
837
|
+
- **The DFPT response is streamed and solved in parallel.** Each perturbation is solved,
|
|
838
|
+
contracted into `C(q)`, and dropped, so the resident response is `O(threads · n_k · nao²)`
|
|
839
|
+
rather than `O(ndof · n_k · nao²)` — and the `j'` loop, which was serial, now runs under rayon.
|
|
840
|
+
The `pbc_dfpt` suite fell from 4.25 s to 2.50 s. `keep_response` restores the full array for a
|
|
841
|
+
caller that wants it, and a test asserts that asking for it leaves `C(q)` bit-identical.
|
|
842
|
+
- **Assembling `C(q)` is `O(N³·n_k)`, not `O(N⁴·n_k)`.** The bare perturbation is held as its
|
|
843
|
+
nonzero entries grouped by translation, not as a dense `nao²` matrix per k point. Displacing
|
|
844
|
+
one atom changes the Hamiltonian only where that atom appears — `O(1)` blocks — plus, on a 3D
|
|
845
|
+
cell, the on-site diagonal of every atom from the long-range monopole channel, which is `O(N)`.
|
|
846
|
+
Contracting every pair of perturbations against that is an order cheaper than against `nao²`.
|
|
847
|
+
|
|
848
|
+
Measured on a chain grown by repeating its cell, with the counts returned on `DfptResult` so
|
|
849
|
+
the claim is checkable rather than asserted: the contraction's extent scales as **N^-0.04**
|
|
850
|
+
against **N^2.00** for the dense one it replaces — 2.0 orders removed — and is 4.2× smaller
|
|
851
|
+
already at twelve atoms. Below about eight atoms the sparse form is *larger*, which the test
|
|
852
|
+
prints rather than hides.
|
|
853
|
+
- **The divide-and-conquer DIIS history is now linear in the atom count, not quadratic.** It
|
|
854
|
+
stored a dense packed triangle while the divide-and-conquer density is *identically* zero
|
|
855
|
+
beyond the buffer radius — so most of what it held was zeros. Storing the density's actual
|
|
856
|
+
sparsity pattern instead gives a measured scaling exponent of **1.05**, against **1.99** for
|
|
857
|
+
the dense triangle it replaces; both are asserted in `tests/divide_conquer.rs`, because a
|
|
858
|
+
linear number on its own could be an accident of size. This is the dominant memory term of a
|
|
859
|
+
large run. `DcResult` gains `diis_pattern_elements` and `dense_triangle_elements` so the claim
|
|
860
|
+
is inspectable rather than believed.
|
|
861
|
+
|
|
862
|
+
The same change cuts the memory traffic: on a 1029-atom cluster `dc:diis` fell from 1.070 s to
|
|
863
|
+
0.419 s and the run from 5.11 s to 4.45 s, with identical energies and the same iteration count.
|
|
864
|
+
- **The DFPT response is DIIS-accelerated**, and it needed to be: the coupled-perturbed solve is
|
|
865
|
+
a linearly mixed fixed point, and on a polar 3D cell it did not converge at all within its
|
|
866
|
+
200-iteration cap — a water crystal stalled at `5.6 × 10⁻⁹` against a `10⁻¹⁰` tolerance and
|
|
867
|
+
raised. Every one of those iterations is a real-space two-electron build plus a diagonalization
|
|
868
|
+
per k point. With Pulay extrapolation on the fixed-point residual the same system converges at
|
|
869
|
+
the default tolerance, and every DFPT identity reproduces bit for bit — it is the same fixed
|
|
870
|
+
point, reached sooner.
|
|
871
|
+
- **The periodic CPHF's two basis transforms were `O(nao⁴)`; they are now `O(nao³)`.** Both
|
|
872
|
+
`project_ov` (`Cᵥ† M C_o`) and the response density (`C_v U C_o† + h.c.`) were written as a
|
|
873
|
+
single loop nest over `(v, o, μ, ν)`, which rebuilds the inner `M C_o` — a quantity
|
|
874
|
+
independent of the virtual index — once for **every** virtual. Since `n_v` and `n_o` both grow
|
|
875
|
+
with `nao`, `n_v n_o nao²` is a fourth power. Factoring each into two products makes it
|
|
876
|
+
`nao² n_o + nao n_v n_o`.
|
|
877
|
+
|
|
878
|
+
Measured against the loop nest it replaces, which is kept in the test suite as the reference:
|
|
879
|
+
|
|
880
|
+
| `nao` | loop nest | factored | speedup |
|
|
881
|
+
|---|---|---|---|
|
|
882
|
+
| 32 | 1.197 ms | 0.041 ms | **29×** |
|
|
883
|
+
| 64 | 17.043 ms | 0.185 ms | **92×** |
|
|
884
|
+
| 128 | 360.044 ms | 0.996 ms | **362×** |
|
|
885
|
+
|
|
886
|
+
The two agree to 3.1 × 10⁻¹⁵ relative, and `factoring_project_ov_lowers_its_scaling_exponent`
|
|
887
|
+
asserts that the advantage *grows* with size, since a constant-factor win would not.
|
|
888
|
+
|
|
889
|
+
The compact occupied/virtual coefficient blocks the products need are gathered once per k
|
|
890
|
+
point, into `KOrbitals`. Gathering them per call instead — the obvious first cut — made the
|
|
891
|
+
DFPT suite **five times slower** (3.7 s to 18.5 s), because at the `nao ≈ 8–40` of a small
|
|
892
|
+
cell the allocations cost more than the fourth power saved. The complex arithmetic likewise
|
|
893
|
+
accumulates in place (`matmul_acc_seq`) rather than allocating a matrix per real product and
|
|
894
|
+
combining afterwards.
|
|
895
|
+
- **The molecular CPHF called faer's *parallel* matmul from inside its own rayon loop.**
|
|
896
|
+
`Matrix::matmul_seq` exists precisely for this and its documentation names the CPHF
|
|
897
|
+
perturbation loop as the case, but `project_ov` and `ao_response_density` used the parallel
|
|
898
|
+
form — so faer's workers contended with the outer pool over the same threads. Both now use the
|
|
899
|
+
sequential, transpose-free products. `cphf:to_ao` fell from 1.447 s to 1.034 s and
|
|
900
|
+
`cphf:to_mo` from 1.346 s to 1.000 s of thread time on a 48-atom Hessian, about 27 % off each.
|
|
901
|
+
The divide-and-conquer density build had the same nesting and is fixed with it.
|
|
902
|
+
- **Four more hand-written `n³` loop nests went to the blocked kernel**, all of them on inner
|
|
903
|
+
paths. These are counted rather than timed — the machine they were developed on was running
|
|
904
|
+
other work, and a stopwatch there measures the load, not the code:
|
|
905
|
+
|
|
906
|
+
| site | what it is | per what |
|
|
907
|
+
|---|---|---|
|
|
908
|
+
| `pbc::dfpt`'s `mul` / `adjoint_mul` / `mul_adjoint` | the CPSCF's complex transforms | 4 × per k point per iteration |
|
|
909
|
+
| `pbc::scf`'s `P(k) = Σ_i f_i c_{μi} c*_{νi}` | the periodic density build | per k point per SCF iteration |
|
|
910
|
+
|
|
911
|
+
The periodic density build also stopped walking the empty levels. It looped every orbital and
|
|
912
|
+
`continue`d on `f_i = 0`, which skipped the arithmetic but not the traversal; gathering the
|
|
913
|
+
filled columns first makes the products `nao² · n_occ` rather than `nao³`.
|
|
914
|
+
- **The Fock build no longer copies the density to halve it.** `build_fock` and `build_g_matrix`
|
|
915
|
+
each cloned the density and scaled it by ½ to make the same-spin matrix for the exchange. The
|
|
916
|
+
exchange is linear in that argument, so `build_fock_spin_with` now takes a `spin_scale` and the
|
|
917
|
+
callers pass the total density with `0.5` — exactly as `pbc::scf::build_realspace_fock` already
|
|
918
|
+
did. That removes an `nao²` allocation, copy and scale from every call, and `build_g_matrix` is
|
|
919
|
+
called `3N` times per CPHF iteration: at 1602 AOs each of those copies was 20 MB.
|
|
920
|
+
- **The perturbed Fock's long-range term evaluates a quarter of the lattice sums.** The nest
|
|
921
|
+
built `Δ'(R_b − R_a)` about `2·nat²` times — the `a == c` branch walks every `b` for each `c`,
|
|
922
|
+
and the other branch asks for every ordered pair — where only `nat(nat−1)/2` are distinct.
|
|
923
|
+
`Δ'` is **odd** in the separation (`Δ` is even), so one triangle tabulated once serves both
|
|
924
|
+
halves. `the_pair_gradient_is_odd_in_the_separation` pins that at 1.6e-15.
|
|
925
|
+
- **The Ewald pair Hessian is evaluated on half the pairs.** `LongRangeMonopole::energy_hessian`
|
|
926
|
+
called `delta_hessian` for both `(a,b)` and `(b,a)` — the same lattice sum twice, and the
|
|
927
|
+
lattice sum over every translation and reciprocal vector is what costs. That Hessian is *even*
|
|
928
|
+
in the separation (it is built from `d̂_i d̂_j` and even powers of `|d|`, and the translation
|
|
929
|
+
set is symmetric), so one triangle suffices: exactly half the work, with
|
|
930
|
+
`the_pair_hessian_is_even_in_the_separation` pinning the symmetry it rests on at 1.2e-15. The
|
|
931
|
+
region also gained the `ewald:hessian` timer it had never had, which is why the plan's
|
|
932
|
+
"profile before optimizing" step had never been possible there.
|
|
933
|
+
- **No `unwrap` or `expect` outside tests, anywhere in `src/`** — down from eighteen. Most were
|
|
934
|
+
`blocks.get(ImageOffset::origin()).unwrap()`: "the origin is always in the translation set" is
|
|
935
|
+
a property of how that set is built, not one the type enforces, and a panic from inside an SCF
|
|
936
|
+
iteration is the worst way to learn otherwise. `RealSpaceBlocks::origin`/`origin_mut` return a
|
|
937
|
+
`Result` and every site propagates it. The two that remained were genuinely infallible and are
|
|
938
|
+
now infallible *structurally*: the longest-axis search is a fold over three fixed axes rather
|
|
939
|
+
than `max_by(..).unwrap()`, and an empty level set is handled rather than indexed into.
|
|
940
|
+
- `Matrix` gained transpose-free products (`transpose_matmul`, `matmul_transpose`, and their
|
|
941
|
+
`_seq`/accumulating variants). Materializing a transpose to multiply by it is an extra
|
|
942
|
+
allocation and copy per call; on `P = C_occ C_occᵀ`, once per SCF iteration, the transposed
|
|
943
|
+
view is **3.2×** faster than the copy at 600 AOs and 400 occupied orbitals — measured, because
|
|
944
|
+
handing a kernel a non-native layout can as easily cost as save.
|
|
945
|
+
|
|
946
|
+
That figure is a **minimum over repetitions**, not a mean, and the difference matters: a
|
|
947
|
+
three-run mean of the same code reported 1.24× on an idle machine and 1.65× *slower* on a busy
|
|
948
|
+
one. Interference only ever makes a sample slower, so the minimum is the least-contended
|
|
949
|
+
estimate; the test asserts only against a 3× catastrophe, because anything tighter would be
|
|
950
|
+
asserting that the machine is idle.
|
|
951
|
+
- DFPT no longer rebuilds `h_j(k)` inside the `j'` loop or inside the CPSCF iteration; it is
|
|
952
|
+
invariant in both, and the Bloch sum costs `O(n_T · nao²)` each time.
|
|
953
|
+
- `farfield` is instrumented, and `tests/dc_where_the_time_goes.rs` reports where a large run's
|
|
954
|
+
time actually goes.
|
|
955
|
+
|
|
956
|
+
### Not done, and named so it is not mistaken for done
|
|
957
|
+
|
|
958
|
+
An audit of this release against its own plan turned these up. They are recorded here rather than
|
|
959
|
+
left for a reader to discover.
|
|
960
|
+
|
|
961
|
+
- **Parameter structs landed for two of the plan's four targets.** `build_core_with_neighbors`
|
|
962
|
+
takes `CoreBuildOptions`, and the CPHF trio — `apply_orbital_hessian`, `cphf_ov` and
|
|
963
|
+
`cphf_ov_fixed_point` — now share a `CphfContext` holding what does not change from one
|
|
964
|
+
perturbation to the next, which also makes it impossible to hand the fixed-point fallback a
|
|
965
|
+
different Hamiltonian from the solver that gave up. `skeleton_fock_ov` and the DFPT helpers
|
|
966
|
+
still take long positional lists; 20 `#[allow(clippy::too_many_arguments)]` remain.
|
|
967
|
+
- **The MOPAC oracle covers one molecule.** Verified, not assumed: of the 61 cases in MOPAC's
|
|
968
|
+
`tests/keywords`, `AM1.mop` and `RM1.mop` are the only ones selecting these methods and both
|
|
969
|
+
are CO₂. Widening it means *running* MOPAC rather than reading it. The comparison was deepened
|
|
970
|
+
instead — the whole orbital spectrum rather than one eigenvalue.
|
|
971
|
+
- **The long-range monopole term in the DFPT response on a chain or a slab.** It landed for 3D
|
|
972
|
+
cells (above). `LongRangeMonopole` is itself three-dimensional, so in 1D and 2D there is no such
|
|
973
|
+
correction anywhere in the crate and nothing to generalize; `LongRange::Require` errors there
|
|
974
|
+
rather than approximating quietly. Implementing the low-dimensional kernels — 2D `2π/(A q)` with
|
|
975
|
+
a slab convention, 1D vanishing as `q² ln q` — is a separate piece of work.
|
|
976
|
+
|
|
977
|
+
- **No Barnes–Hut tree for the far field**, despite the module documentation suggesting one
|
|
978
|
+
belongs there. Measured first: on a 1029-atom divide-and-conquer run with the far field on,
|
|
979
|
+
`farfield:potential` is **0.5 %** of the runtime, against 36 % for the subsystem
|
|
980
|
+
diagonalizations. A monopole pair costs about ten flops and the loop is embarrassingly
|
|
981
|
+
parallel, so making it `O(N log N)` would save half a percent — and would put an
|
|
982
|
+
acceptance-angle discontinuity into the energy surface to do it. The `O(N²)` term does win
|
|
983
|
+
eventually, but the crossover is around `10⁴–10⁵` atoms. `src/farfield.rs` records the
|
|
984
|
+
measurement, so the decision can be revisited against a number rather than re-argued.
|
|
985
|
+
|
|
986
|
+
### Changed
|
|
987
|
+
|
|
988
|
+
- `build_core_with_neighbors` takes a `CoreBuildOptions` struct instead of four positional
|
|
989
|
+
arguments; `Am1Options::core_build()` derives it, so a path that builds `H_core` for itself
|
|
990
|
+
cannot disagree with the SCF about which corrections are on.
|
|
991
|
+
- `#![forbid(unsafe_code)]` — there was none, and now there cannot be.
|
|
992
|
+
- **The Python API roughly doubled.** New in `am1_rs.native`: `orbitals`, `molden`,
|
|
993
|
+
`ir_spectrum`, `dipole_derivatives`, `orbital_response`, `pbc_hessian`, `born_charges`,
|
|
994
|
+
`dielectric`, `dfpt`, `lo_to_frequencies`; `electric_field=` on `single_point`, `gradient`,
|
|
995
|
+
`optimize`, `hessian`, `frequencies` and six more; `multipole_cutoff=` on `divide_conquer`.
|
|
996
|
+
The ASE calculator gains the matching `get_ir_spectrum`, `get_dipole_derivatives`,
|
|
997
|
+
`get_orbitals`, `get_orbital_response`, `write_molden`, `get_frequencies`,
|
|
998
|
+
`get_am1_bcc_charges`, `get_phonons`, `get_born_charges`, `get_dielectric_tensor`,
|
|
999
|
+
`get_dfpt_frequencies`, `get_lo_to_frequencies` and `optimize`, plus `field=` and an
|
|
1000
|
+
`atoms.info["field"]` override routed through `check_state`. Both CLIs gain the modes
|
|
1001
|
+
`orbitals`, `ir`, `molden` and the flags `--field FX FY FZ`, `--molden-output FILE`.
|
|
1002
|
+
|
|
1003
|
+
`tests/test_new_api_0_2_1.py` now *enumerates* `am1_rs.native`'s public functions and requires
|
|
1004
|
+
each to name its ASE counterpart, instead of checking a hand-written list. The hand-written
|
|
1005
|
+
list is what let `lo_to_frequencies` exist in Rust and in neither Python surface.
|
|
1006
|
+
- **Divide-and-conquer under a periodic cell, from Python and ASE.** `native.divide_conquer`
|
|
1007
|
+
takes `cell`/`pbc` (with `realspace_cutoff`/`exchange_cutoff`, which matter under a cell and
|
|
1008
|
+
not for a molecule), and `AM1(divide_conquer=True)` now routes a periodic structure through it
|
|
1009
|
+
instead of raising. The Rust API had accepted a lattice since 0.2.0; the ASE error message
|
|
1010
|
+
saying the buffers were "not wired up yet" described 0.2.0 and had outlived it.
|
|
1011
|
+
- **`tests/theory_components.rs`** — the *pieces* of the formulas, against what theory says each
|
|
1012
|
+
one must be. Everything else in the suite is an end-to-end identity, which is strong and also
|
|
1013
|
+
blunt: it says a chain is wrong without saying which link, and a compensating pair of errors
|
|
1014
|
+
passes it. These twelve tests each check a property that follows from the mathematics alone.
|
|
1015
|
+
|
|
1016
|
+
The sharpest is the monopole limit. `(ss|ss) = e²/√(R² + ρ²)` implies that the relative
|
|
1017
|
+
deviation from `e²/R` is `−ρ²/(2R²)`, so `deviation × R²` is constant *and identifies `ρ`* —
|
|
1018
|
+
and the value recovered from the integral's long-range behaviour, 1.9873…1.9946 Bohr across
|
|
1019
|
+
four radii, is the parameter table's own `rho0(O) + rho0(C) = 1.994724`. That pins the
|
|
1020
|
+
functional form, both elements' parameters and the `AM1_EV` conversion in one measurement.
|
|
1021
|
+
|
|
1022
|
+
The rest: each multipole channel decays at the order its expansion demands (measured
|
|
1023
|
+
`R^-0.998`, `R^-1.996`, `R^-2.997` against `−1, −2, −3`); the two-electron integrals have their
|
|
1024
|
+
three permutation symmetries, including the electron exchange that swaps the two *atoms* and
|
|
1025
|
+
reaches them through different branches; the whole `10 × 10` block is rotation covariant, tested
|
|
1026
|
+
on a contracted quantity so every index has to transform correctly; the overlap has the
|
|
1027
|
+
inversion parity its orbitals imply and is unchanged by relabelling; the converged density is
|
|
1028
|
+
an idempotent projector with `Tr P` the electron count; `[F, P] = 0` at the SCF solution; the
|
|
1029
|
+
reported electronic energy really is `½Tr[P(H + F)]`; Koopmans uses the level it should; and
|
|
1030
|
+
the energy is invariant under rigid motion.
|
|
1031
|
+
- **`tests/orbital_response.rs`**, checking `U^j_{ai}` against a finite difference of the MO
|
|
1032
|
+
coefficients rather than only through what it is contracted into. Two things make that
|
|
1033
|
+
comparison delicate and both are handled explicitly: eigenvector *phase* (aligned against the
|
|
1034
|
+
response channel's own coefficients — aligning against a separately re-run SCF flips `U`
|
|
1035
|
+
wholesale, which the first draft did and measured as `|Δ| = 2|U|`), and *degeneracy* (methyl's
|
|
1036
|
+
`e′` pair mixes arbitrarily under displacement, so the coefficient comparison is done on the
|
|
1037
|
+
non-degenerate `H₂O⁺` while the phase-invariant response *density* is checked on methyl).
|
|
1038
|
+
Measured: `U` to 9.6e-7 (RHF), 7.0e-7 and 5.6e-7 (UHF α and β); `∂P/∂R` to 1.7e-7.
|
|
1039
|
+
- **The MOPAC oracle now compares the whole orbital spectrum**, twelve eigenvalues per method
|
|
1040
|
+
rather than the single Koopmans IP, including CO₂'s two degenerate pairs — which a broken
|
|
1041
|
+
two-centre rotation would split while leaving `ΔHf` and the HOMO almost unmoved. Worst case
|
|
1042
|
+
across all twelve: **0.0022 eV for AM1, 0.0034 eV for RM1**, both at the deepest level.
|
|
1043
|
+
|
|
1044
|
+
It still covers one molecule, and that was verified rather than assumed: of the 61 cases in
|
|
1045
|
+
MOPAC's `tests/keywords`, `AM1.mop` and `RM1.mop` are the only ones selecting these methods and
|
|
1046
|
+
both are CO₂. Widening it means *running* MOPAC, not reading it.
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
## 0.2.0
|
|
1050
|
+
|
|
1051
|
+
### Added
|
|
1052
|
+
|
|
1053
|
+
- **RM1** (Rocha *et al.* 2006), sharing AM1's functional form and therefore its entire code
|
|
1054
|
+
path — gradients, Hessians, periodic boundary conditions and divide-and-conquer all work
|
|
1055
|
+
unchanged. Select with `method="rm1"` on any Python entry point, `--method rm1` on the CLI, or
|
|
1056
|
+
`Am1Parameters::for_method(NddoMethod::Rm1)` in Rust. Covers H, C, N, O, P, S, F, Cl, Br, I.
|
|
1057
|
+
Parameter provenance in `THIRD_PARTY_NOTICES.md` §3. See `docs/methods.md`.
|
|
1058
|
+
|
|
1059
|
+
- **Periodic boundary conditions** — 1D chains, 2D slabs and 3D crystals, dimensionality taken
|
|
1060
|
+
from `atoms.pbc`:
|
|
1061
|
+
- Γ-point and Monkhorst–Pack k-point sampling, with time-reversal folding and automatic
|
|
1062
|
+
collapse of non-periodic axes.
|
|
1063
|
+
- Fermi–Dirac smearing with a bisected chemical potential, electronic entropy and `T→0`
|
|
1064
|
+
extrapolation (new `fermi` module).
|
|
1065
|
+
- **RHF and UHF at both Γ and k-points.** Forced-UHF reproduces RHF on a closed shell to
|
|
1066
|
+
1e-7 eV with an exactly vanishing spin density.
|
|
1067
|
+
- Analytic forces and **analytic stress** for all three dimensionalities. Stress components
|
|
1068
|
+
touching a non-periodic axis are exactly zero; the periodic measure is a volume, area or
|
|
1069
|
+
length as appropriate.
|
|
1070
|
+
- The AM1 core–core Gaussian corrections are included in the lattice sum, its gradient and its
|
|
1071
|
+
virial.
|
|
1072
|
+
- Net charge per cell, including the absolute energy in 3D — see Ewald summation below.
|
|
1073
|
+
- New `lattice`, `neighbors` and `pbc` modules; extended-XYZ `Lattice=`/`pbc=` parsing.
|
|
1074
|
+
- `docs/pbc.md`.
|
|
1075
|
+
|
|
1076
|
+
- **Ewald summation** for the long-range monopole electrostatics of a 3D cell (`ewald`, default
|
|
1077
|
+
on), under the tin-foil boundary condition:
|
|
1078
|
+
- Makes a **charged cell's total energy meaningful**. Across a 6.5× range of real-space cutoff
|
|
1079
|
+
a +1 water cell moves 0.20 eV with it and 403 eV without.
|
|
1080
|
+
- Applied through the **net** charges as `−V_a`, `V_a = Σ_b Δ_ab Q_b`, rather than split
|
|
1081
|
+
across the electron–core, Coulomb and core–core terms the way `γ_ab` is. The split form is
|
|
1082
|
+
algebraically identical and numerically ruinous — it shifts `H_core` and the Coulomb term by
|
|
1083
|
+
±660 eV for a carbon in a 12 Bohr cell — and it stopped a lone neutral carbon from
|
|
1084
|
+
converging at all.
|
|
1085
|
+
- Energy, analytic gradient, analytic stress and analytic Hessian, the last including both the
|
|
1086
|
+
fixed-charge second derivative and the charge-response term in the CPHF.
|
|
1087
|
+
- Validated four independent ways: the rock-salt Madelung constant to 10 digits, exact
|
|
1088
|
+
independence of the splitting parameter `α` for both the potential and the **stress**, the
|
|
1089
|
+
dipole surface term `2π|p|²/3V` against a direct lattice sum, and finite differences for
|
|
1090
|
+
every derivative.
|
|
1091
|
+
- **Ewald in 2D and 1D**, so a slab or a chain gets the same treatment a crystal does rather than
|
|
1092
|
+
no treatment at all:
|
|
1093
|
+
- **2D by Parry**, not by the Yeh–Berkowitz vacuum-slab trick. Parry gives the in-plane 2×2
|
|
1094
|
+
stress directly; a vacuum slab has no meaningful `∂E/∂ε_zz` because the `c` axis is fictitious,
|
|
1095
|
+
and is only asymptotically exact. The implementation needs `erfcx(x) = e^{x²}erfc(x)`, added
|
|
1096
|
+
to the `Scalar` trait with its derivative rules, because the naive `e^{hz}·erfc(h/2α + αz)`
|
|
1097
|
+
overflows at moderate `hz`; the exponentials are composed analytically instead.
|
|
1098
|
+
- **1D without a Bessel-function reciprocal sum.** Only the monopole channel needs summing in
|
|
1099
|
+
1D, so this is a real-space sum plus an analytic tail: the `ρ²/(nL)²` and `z/(nL)` expansion
|
|
1100
|
+
of `1/√(ρ²+(z+nL)²)`, whose coefficients are Hurwitz zeta values. No special functions, and
|
|
1101
|
+
exactly differentiable — which is what makes forces and stress available.
|
|
1102
|
+
- Validated the same way 3D was: Madelung constants to 10–12 digits, independence of `α` and of
|
|
1103
|
+
the image count, and finite differences for every derivative.
|
|
1104
|
+
|
|
1105
|
+
- **k-point analytic Hessian at `q = 0`**, so second derivatives no longer depend on the Γ-point
|
|
1106
|
+
exchange taper. Matches finite differences to 1.7e-7 eV/Bohr² on a polar chain with a mesh, and
|
|
1107
|
+
the acoustic sum rule holds exactly. Three defects found on the way there, each invisible at Γ:
|
|
1108
|
+
the Coulomb and exchange factors were 2× too small on unordered pairs, the exchange derivative
|
|
1109
|
+
Fock was missing its `−T` mirror, and the resonance derivative Fock was missing a factor of ½.
|
|
1110
|
+
|
|
1111
|
+
- **Born effective charges `Z*` and the electronic dielectric tensor `ε_∞`**, sharing the phonon
|
|
1112
|
+
response solve so the two cannot drift apart. `Σ_a Z*_a = 0` to 1e-16; `ε_∞` is origin
|
|
1113
|
+
independent to 1.6e-15, which was measured rather than predicted — the module doc had predicted
|
|
1114
|
+
a dependence and was corrected to record the measurement.
|
|
1115
|
+
|
|
1116
|
+
- **LO–TO splitting.** `D(q) = D_analytic(q) + D_NA(q)` with the non-analytic term built from
|
|
1117
|
+
`Z*` and `ε_∞`. Exactly zero for a non-polar system.
|
|
1118
|
+
|
|
1119
|
+
> **Corrected in 0.2.1.** The "127 cm⁻¹ shift and 1631 cm⁻¹ of direction dependence for a
|
|
1120
|
+
> polar one" measured here was taken on a **1D chain**, where the three-dimensional
|
|
1121
|
+
> `4π/(Ω q·ε∞·q)` kernel does not apply and `Ω` was silently a length. Those numbers are
|
|
1122
|
+
> artifacts. See the 0.2.1 entry.
|
|
1123
|
+
|
|
1124
|
+
- **DFPT at arbitrary `q`** — a CPSCF connecting `k` to `k+q`, so a phonon at any `q` no longer
|
|
1125
|
+
needs a commensurate supercell. Reproduces the `q = 0` Hessian to 4e-13 relative and a 2-fold
|
|
1126
|
+
supercell's frozen phonon to 3e-4. The identities that pin the phases down (`D(−q) = D(q)*`,
|
|
1127
|
+
continuity in `q`) are asserted too, because a wrong phase leaves the matrix Hermitian and the
|
|
1128
|
+
frequencies real — it does not announce itself.
|
|
1129
|
+
|
|
1130
|
+
- **Divide-and-conquer under periodic boundary conditions and with an analytic stress.** Γ with a
|
|
1131
|
+
minimum-image buffer, exact once the buffer reaches `L/2` (3.4e-10 eV); the stress matches a
|
|
1132
|
+
strain finite difference to 3.6e-8 eV/Bohr³.
|
|
1133
|
+
|
|
1134
|
+
- **Far-field monopole screening** (`multipole_cutoff`, opt-in, default off): pairs beyond the
|
|
1135
|
+
cutoff contribute through atomic monopoles instead of the full multipole block.
|
|
1136
|
+
|
|
1137
|
+
- **Divide-and-conquer SCF** for large molecules, restricted and unrestricted:
|
|
1138
|
+
- Disjoint cores by recursive spatial bisection, Yang partition weights, one common chemical
|
|
1139
|
+
potential shared across all subsystems (two, one per spin channel, when unrestricted).
|
|
1140
|
+
- The density is truncated explicitly at the buffer radius, which makes the Yang sum rule
|
|
1141
|
+
exact for every geometry — verified at `0.0` deviation — and makes the two-centre exchange
|
|
1142
|
+
exactly linear-scaling rather than approximately so.
|
|
1143
|
+
- Non-neutral systems; Mulliken charges conserve the formal charge to 1e-8 e.
|
|
1144
|
+
- Hellmann–Feynman gradient at the assembled density.
|
|
1145
|
+
- Scaling counters (`diagonalization_work`, `coulomb_work`, `exchange_work`,
|
|
1146
|
+
`retained_density_blocks`) returned on every result, so the cost claim is inspectable.
|
|
1147
|
+
- `docs/divide-conquer.md`.
|
|
1148
|
+
|
|
1149
|
+
- **ASE calculator** now reads `atoms.pbc` and `atoms.cell`, implements `stress`, and exposes
|
|
1150
|
+
`method`, `kpts`, `smearing`, the cutoffs, the SCF tolerances and `divide_conquer`. Parameters
|
|
1151
|
+
moved into `self.parameters`, so `todict()`, `set()` and restart work.
|
|
1152
|
+
|
|
1153
|
+
- `method=` on every Python entry point; new `pbc_point` and `divide_conquer` native functions;
|
|
1154
|
+
the GIL is released around every solver.
|
|
1155
|
+
|
|
1156
|
+
- `tests/test_ase_pbc_md.py` — real molecular dynamics as an acceptance test: NVE in 1D/2D/3D,
|
|
1157
|
+
Parrinello–Rahman NPT, NPT-Berendsen pressure response, NVT on partially periodic cells.
|
|
1158
|
+
|
|
1159
|
+
### Fixed
|
|
1160
|
+
|
|
1161
|
+
- **The `pip`-installed CLI was broken in three of its five modes.** `am1-rs energy` exited
|
|
1162
|
+
non-zero on a water molecule while every other test in the suite passed: `__main__.py` read keys
|
|
1163
|
+
the bindings never emitted — `total_ev`, `dipole_magnitude`, `iterations`, `max_gradient`,
|
|
1164
|
+
`forces`, `positions`, `steps` — and a missing dictionary key raises only at the moment it is
|
|
1165
|
+
printed, which no test reached. Found by installing the sdist into a clean environment and
|
|
1166
|
+
running the console script, which is now `tests/test_cli.py` and a CI job rather than something
|
|
1167
|
+
that happened to get tried. Every mode's output is diffed against the Rust CLI's, which is what
|
|
1168
|
+
the packaging has been claiming; four of the five needed fixing to make that true, including
|
|
1169
|
+
Rust's `{:.6e}` exponent format, which differs from Python's.
|
|
1170
|
+
- **`iterations`, `unrestricted` and `dipole_magnitude` were missing from the molecular Python
|
|
1171
|
+
results** while the periodic ones carried them, so a caller could report how an SCF went for a
|
|
1172
|
+
crystal and not for a molecule. `gradient` and `optimize` now also return the SCF breakdown
|
|
1173
|
+
they already computed, instead of forcing a second SCF to get it. New `native.constants()`
|
|
1174
|
+
exports the model's unit conversions — deliberately MOPAC7's `ev = 27.21` rather than CODATA —
|
|
1175
|
+
so nothing on the Python side has to write them down and drift.
|
|
1176
|
+
- **The Hessian bug.** `rotation_to_x_g` replaced live dual numbers with constants when an atom
|
|
1177
|
+
pair was antiparallel to the reference axis, zeroing the derivatives. The gradient was
|
|
1178
|
+
protected by symmetry; the **second** derivative was not, so transverse force constants of any
|
|
1179
|
+
molecule with a bond on that axis were wrong. Fixed by removing the local frame entirely: the
|
|
1180
|
+
integrals are now written in terms of the internuclear unit vector and the transverse
|
|
1181
|
+
projector `δ_ij − n_i n_j`, which is branch-free, exactly differentiable at every order, and
|
|
1182
|
+
faster. This was also a prerequisite for periodic boundary conditions, where axis-aligned
|
|
1183
|
+
lattice vectors would have hit the branch constantly.
|
|
1184
|
+
|
|
1185
|
+
- **Periodic stress unit conversion** at the Python boundary used `ANGSTROM_TO_BOHR^(d−1)`
|
|
1186
|
+
instead of `^d`, so every periodic stress reaching Python was 1.89× too small. Found by the
|
|
1187
|
+
new NPT acceptance test; the Rust tests could not see it because they never cross that
|
|
1188
|
+
boundary.
|
|
1189
|
+
|
|
1190
|
+
- The divide-and-conquer UHF initial guess did not preserve the electron count, which put the
|
|
1191
|
+
open-shell case in a different SCF basin (0.66 eV high). Now split in proportion to the α/β
|
|
1192
|
+
counts, matching the full SCF.
|
|
1193
|
+
|
|
1194
|
+
- CPHF non-convergence is now an error (`Am1Error::CphfNotConverged`) rather than a silently
|
|
1195
|
+
returned plausible Hessian.
|
|
1196
|
+
|
|
1197
|
+
- Documented accuracy claims corrected to the values the tests actually assert: the numerical
|
|
1198
|
+
Slater overlap agrees with the analytic kernel to ~1e-7 (`1s|1s`) and ~5e-4 (`2s|2s`), not
|
|
1199
|
+
1e-8. The CLI `gradient` help said eV/Å; it prints Hartree/Bohr.
|
|
1200
|
+
|
|
1201
|
+
- Be and B are parameterized for AM1 and were undocumented.
|
|
1202
|
+
|
|
1203
|
+
- **AM1-BCC perception**, several distinct defects:
|
|
1204
|
+
- Ring perception was a union-find spanning tree with fundamental cycles, so a fused system
|
|
1205
|
+
could return the 10-membered perimeter of naphthalene instead of its two 6-rings. Replaced
|
|
1206
|
+
by the smallest ring through each bond.
|
|
1207
|
+
- Aromaticity never looked at ring size at all — the detector returned only a boolean — so
|
|
1208
|
+
cycloheptatriene and macrocyclic lactones came out aromatic. Now ring size, a planarity test
|
|
1209
|
+
and a Hückel 4n+2 π count, which also correctly rejects cyclooctatetraene.
|
|
1210
|
+
- Sulfur could **never** be aromatic: `perceive_hybridization` returned `Sp3` for every
|
|
1211
|
+
sulfur, and the aromaticity test required `Sp2`, so the `| 16` in its element match was
|
|
1212
|
+
unreachable code. Thiophene is now aromatic, as are pyrrole and furan, each by its own π
|
|
1213
|
+
contribution.
|
|
1214
|
+
- The bond-order reference table held only six C/N/O pairs, so every C=S, P=O and S=O was
|
|
1215
|
+
perceived as a single bond and every thiocarbonyl, phosphate and sulfonyl group was
|
|
1216
|
+
mistyped. Extended to P and S pairs.
|
|
1217
|
+
- Bond types 6 and 9 — the symmetric delocalized groups (nitro, N-oxide; carboxylate,
|
|
1218
|
+
phosphate, sulfonate) — were never emitted, leaving 27 consequential parameters unreachable.
|
|
1219
|
+
Now selected by a chemical rule rather than a bond length. Measured: of the remaining 66
|
|
1220
|
+
unreachable entries, 26 are identically zero and 40 are byte-identical to the aromatic type,
|
|
1221
|
+
so nothing that can affect a charge is now unreachable.
|
|
1222
|
+
- An unparameterized element, or a typed bond with no tabulated parameter, returned raw
|
|
1223
|
+
Mulliken charges **in silence**. Both now appear in `BccResult::warnings`.
|
|
1224
|
+
|
|
1225
|
+
### Performance
|
|
1226
|
+
|
|
1227
|
+
- **8.2× faster** on the molecular path, from profiling rather than guessing: faer global
|
|
1228
|
+
parallelism enabled (it defaults to sequential with `default-features = false`), blocked
|
|
1229
|
+
parallel `matmul` through faer views, a parallel chunked Fock build, `C_occ·C_occᵀ` density
|
|
1230
|
+
formation, a single matrix product for the DIIS commutator, and flattened pair-integral
|
|
1231
|
+
storage.
|
|
1232
|
+
- **CPHF solved by preconditioned conjugate gradient** instead of DIIS-accelerated Richardson.
|
|
1233
|
+
The CPHF equations are linear and their operator — the orbital Hessian — is symmetric and
|
|
1234
|
+
positive definite at a stable SCF solution, which is exactly what CG is for. Each application
|
|
1235
|
+
of that operator is a full Fock build, and those builds are **two thirds of an entire frequency
|
|
1236
|
+
calculation**, so the figure of merit is simply how many are needed: 6296 → 4931 for a 150-atom
|
|
1237
|
+
cluster. The convergence test is deliberately the same quantity the fixed-point solver used
|
|
1238
|
+
(the fixed point's step `‖U_{n+1} − U_n‖` *is* the preconditioned residual), so the tolerance
|
|
1239
|
+
did not have to be retuned and the two are directly comparable. If the operator turns out not
|
|
1240
|
+
to be positive definite along a search direction, the solve falls back to the fixed-point
|
|
1241
|
+
iteration rather than returning something meaningless.
|
|
1242
|
+
- **`fock::build_g_matrix`** builds the two-electron matrix directly instead of assembling the
|
|
1243
|
+
full Fock matrix and subtracting `H_core` again — two wasted `nao²` passes per call, about
|
|
1244
|
+
9 GB of memory traffic over one Hessian. The CPHF also now uses a sequential pair loop
|
|
1245
|
+
(`fock::PairLoop`), because it already runs under rayon across the `3N` perturbations and an
|
|
1246
|
+
inner rayon pool was contending with the outer one for the same threads.
|
|
1247
|
+
- Together: a 150-atom frequency calculation went **23.2 s → 15.4 s (1.51×)**, with the Rust
|
|
1248
|
+
suite unchanged.
|
|
1249
|
+
- Opt-in phase timing with `AM1_TIMING=1`.
|
|
1250
|
+
- **Fixed a blind spot in that timing.** `report` was called from inside `run_am1`, and reporting
|
|
1251
|
+
clears the accumulator — so profiling a *gradient* or a *Hessian* printed only the SCF phases,
|
|
1252
|
+
and the single most expensive phase of those commands was invisible in the profile meant to
|
|
1253
|
+
find it. Reporting now belongs to the top-level caller. The CPHF work above is what that
|
|
1254
|
+
immediately revealed.
|
|
1255
|
+
- The ASE molecular path now runs **one** SCF per force call instead of two.
|
|
1256
|
+
- The divide-and-conquer DIIS history is stored as **packed upper triangles** with a memory
|
|
1257
|
+
budget rather than as full matrices at a fixed depth. A depth-8 history of densities and
|
|
1258
|
+
residuals is 16 dense matrices — 1.2 GB at 1536 atoms, most of the peak footprint, and growing
|
|
1259
|
+
quadratically, which is the wrong shape for the one part of the code meant for large systems.
|
|
1260
|
+
Packing is exact (both matrices are symmetric) and halves it; the budget shortens the history
|
|
1261
|
+
instead of letting it grow without bound.
|
|
1262
|
+
- **1.7× on the divide-and-conquer path** at 1029 atoms — 14.0–14.6 s down to 8.2–8.6 s, measured
|
|
1263
|
+
three times each on the same machine because a single pair of runs on this one differs by 70 %.
|
|
1264
|
+
The cost was in the DIIS, and it was invisible: the labelled phases summed to 8.8 s of a 16.1 s
|
|
1265
|
+
run, and the missing 7.3 s sat *between* the timers. `extrapolate` rebuilt the entire B matrix
|
|
1266
|
+
every iteration — all `n²` ordered pairs, both triangles — when every entry but the newest row
|
|
1267
|
+
is already known and cannot change. At 1029 atoms each packed residual is 16.9 MB, so that was
|
|
1268
|
+
2.2 GB of memory traffic per SCF iteration for numbers already computed. Now the new row is
|
|
1269
|
+
computed on `push` and cached; `residual_dot` is a flat vectorizable `2·(packed dot) −
|
|
1270
|
+
(diagonal dot)` instead of a nested row walk; `pack` copies contiguous row runs instead of
|
|
1271
|
+
walking `nao²` through a 2D index; and the extrapolated density is accumulated packed and
|
|
1272
|
+
expanded once. Iteration counts are unchanged, as they must be — the cached values are the same
|
|
1273
|
+
values.
|
|
1274
|
+
- **The timing report says what it measures.** It sums *thread*-seconds, so a phase running on
|
|
1275
|
+
sixteen threads reports about sixteen times its wall clock. Read as wall clock it makes the
|
|
1276
|
+
best-parallelized phase look like the bottleneck: the CPHF Fock builds in a 102-atom frequency
|
|
1277
|
+
run report 39 s against a 4.8 s calculation. The header now states this, and the total is
|
|
1278
|
+
labelled `TOTAL (thread-seconds)`.
|
|
1279
|
+
|
|
1280
|
+
### Test infrastructure
|
|
1281
|
+
|
|
1282
|
+
- The scaling benchmark's water clusters are spaced at 4.0 Å and the generator now **asserts**
|
|
1283
|
+
that no intermolecular contact is shorter than 1.8 Å. An earlier version used 3.1 Å with
|
|
1284
|
+
pseudo-random molecular orientations, which put hydrogens 1.22–1.35 Å apart — over a hundred
|
|
1285
|
+
pairs inside 1.6 Å at the larger sizes. The symptom was a cliff rather than a warning: the SCF
|
|
1286
|
+
converged in 14 iterations at 192, 375 and 648 atoms and then failed outright at 1029, which
|
|
1287
|
+
reads as a large-system divide-and-conquer defect and was nothing of the kind. Ice gets away
|
|
1288
|
+
with 2.76 Å because its molecules are oriented; a random-orientation benchmark cannot.
|
|
1289
|
+
|
|
1290
|
+
### Known limitations
|
|
1291
|
+
|
|
1292
|
+
These are measured, not suspected. See `docs/scope.md`.
|
|
1293
|
+
|
|
1294
|
+
- **Ewald summation covers the monopole channel only.** In every dimensionality the `1/R` term is
|
|
1295
|
+
now summed exactly, but the `R⁻³` Klopman–Ohno correction and the higher multipoles are still a
|
|
1296
|
+
real-space cutoff on the lattice translation `|T|`. Consequences, all measured: a charged 3D
|
|
1297
|
+
cell converges to about 0.1 eV, limited by the logarithmically divergent `R⁻³` residual
|
|
1298
|
+
(0.10 eV per unit `ln r_c`); neutral cells still converge slowly in the residual channels
|
|
1299
|
+
(3e-4 eV between a 40 and a 640 Bohr cutoff).
|
|
1300
|
+
- **A charged slab or chain needs a stated convention.** The energy of a charged 2D or 1D cell is
|
|
1301
|
+
not defined without one — the neutralizing sheet's position enters a slab's energy, and a
|
|
1302
|
+
charged line's potential diverges logarithmically. Both are refused by default with an error
|
|
1303
|
+
naming the convention enum, rather than answered under a convention the caller never chose.
|
|
1304
|
+
- **NDDO exchange at Γ diverges** and is tapered by a quintic smoothstep at `exchange_cutoff`.
|
|
1305
|
+
This is a documented approximation, not a convergence parameter. k-point sampling makes the
|
|
1306
|
+
density matrix decay on its own and largely removes the dependence.
|
|
1307
|
+
- **Divide-and-conquer makes the diagonalization linear, not the whole calculation.** The NDDO
|
|
1308
|
+
Coulomb sum stays `O(N²)`. Measured exponents from operation counters: diagonalization 1.15,
|
|
1309
|
+
exchange 1.06, retained density blocks 1.05, Coulomb 2.02; on 3D clusters to 2187 atoms the
|
|
1310
|
+
fitted `Σn³` exponent is 1.25 against 3 for a full diagonalization. In wall clock it crosses
|
|
1311
|
+
over around 200 atoms; the 768-atom speedup ranged 1.4–6.3× across runs, which is machine load
|
|
1312
|
+
rather than the algorithm — hence counters, not a stopwatch. The open-shell analytic stress is
|
|
1313
|
+
refused rather than approximated: it needs the spin-resolved pair virial.
|
|
1314
|
+
- **The long-range monopole correction is not in the DFPT response.** Generalizing it to a
|
|
1315
|
+
`q`-point response needs a phased Ewald sum `Σ_T e^{iq·T}/|d+T|`, which is not implemented.
|
|
1316
|
+
|
|
1317
|
+
> **Corrected in 0.2.1.** This entry read "applied only at `q = 0`". In 0.2.0 it was not
|
|
1318
|
+
> applied at `q = 0` either — `force_constants_at_q` omitted it at every `q`, while
|
|
1319
|
+
> `pbc_hessian` included it. The phased sum **is** implemented in 0.2.1 for 3D cells; see the
|
|
1320
|
+
> 0.2.1 notes, including why the element dropped turned out to be `k = 0` and not the `G = 0`
|
|
1321
|
+
> this entry's successor originally assumed.
|
|
1322
|
+
- **`ε_∞` is a clamped-ion dipole response, not a Berry phase.** For a system where charge
|
|
1323
|
+
circulates around the periodic loop rather than responding locally, it is not the right
|
|
1324
|
+
quantity. Origin independence was measured (1.6e-15) rather than assumed.
|
|
1325
|
+
- **SAM1 is not implemented.** It replaces the multipole expansion with scaled STO-3G integrals,
|
|
1326
|
+
so it is a different integral engine rather than a reparameterization and does not fit the
|
|
1327
|
+
shared code path.
|
|
1328
|
+
- **AM1-BCC typing gaps** — 23 % of the bond parameters are unreachable, ring perception is not
|
|
1329
|
+
SSSR, aromaticity ignores ring size, and the bond-order table covers only C/N/O pairs. The
|
|
1330
|
+
correction values themselves are exact.
|
|
1331
|
+
|
|
1332
|
+
### Packaging
|
|
1333
|
+
|
|
1334
|
+
- PEP 639 licence metadata with `license-files`, so `THIRD_PARTY_NOTICES.md` and the retained
|
|
1335
|
+
third-party licences ship **inside the wheel** — required by clause 2 of the BSD-3-Clause
|
|
1336
|
+
covering the bundled PySEQM-derived parameters.
|
|
1337
|
+
- `extension-module` split into its own Cargo feature, so `cargo test --features python` and the
|
|
1338
|
+
CLI link on Linux and macOS.
|
|
1339
|
+
- `abi3-py311`, `requires-python = ">=3.11"` (3.9 is end-of-life), trove classifiers,
|
|
1340
|
+
`[project.urls]`, keywords, and `dynamic = ["version"]` — the version now has one source,
|
|
1341
|
+
`Cargo.toml`, read back through `importlib.metadata`.
|
|
1342
|
+
- `rust-version = "1.75"`, crate metadata, and `docs.rs` configuration.
|
|
1343
|
+
- **Wheels are built and published for every common platform** (`.github/workflows/release.yml`):
|
|
1344
|
+
manylinux and musllinux on x86_64 and aarch64, macOS on both architectures, Windows x64, plus
|
|
1345
|
+
the sdist, published to PyPI by trusted publishing on a version tag. `abi3-py311` means one
|
|
1346
|
+
wheel per platform covers 3.11 and up. This is the real defence against a failed install: a
|
|
1347
|
+
source install builds under the shipping profile — fat LTO, one codegen unit — which measures
|
|
1348
|
+
1.9 GB peak resident and over ten minutes on a warm dependency cache, and on a small VM that is
|
|
1349
|
+
an out-of-memory failure rather than a slow install.
|
|
1350
|
+
- **The sdist is tested by installing it**, in CI, into a clean virtual environment, followed by
|
|
1351
|
+
running the console script. Three things can break a source install silently, and all three are
|
|
1352
|
+
now asserted: a file the *build* needs missing from the tarball (`[[bin]]` points at
|
|
1353
|
+
`src/bin/am1_rs.rs`, so cargo aborts without it even though no wheel ever contains that binary;
|
|
1354
|
+
PEP 639 `license-files` names `third_party/*/LICENSE`; the parameter CSVs are `include_str!`-ed),
|
|
1355
|
+
`Cargo.lock` absent so dependencies re-resolve forward on the user's machine, and the
|
|
1356
|
+
`extension-module` feature reaching a target that has to link Python's symbols. On that last
|
|
1357
|
+
one: maturin builds only the lib target, so the CLI binary is never linked during a `pip
|
|
1358
|
+
install` — verified against the build log rather than assumed, and now held by the test.
|
|
1359
|
+
|
|
1360
|
+
## 0.1.3
|
|
1361
|
+
|
|
1362
|
+
### Added
|
|
1363
|
+
- **Explicit RHF/UHF reference selection**, independent of the spin multiplicity — a closed-shell
|
|
1364
|
+
singlet can now be run either restricted or unrestricted (e.g. as a broken-symmetry starting
|
|
1365
|
+
point):
|
|
1366
|
+
- Rust: new `ScfReference` enum (`Auto` / `Restricted` / `Unrestricted`) and an
|
|
1367
|
+
`Am1Options.reference` field (default `Auto`, preserving previous behavior). `run_am1` honors
|
|
1368
|
+
it; `Restricted` on an open shell is rejected (no ROHF).
|
|
1369
|
+
- Python native: every function (`single_point`, `gradient`, `optimize`, `frequencies`,
|
|
1370
|
+
`hessian`) takes a `reference="auto"|"rhf"|"uhf"` keyword.
|
|
1371
|
+
- ASE: `AM1(..., reference="auto")`, or per structure via `atoms.info["reference"]`; a change
|
|
1372
|
+
invalidates cached results.
|
|
1373
|
+
- CLI: `--reference auto|rhf|uhf`, with `--rhf` / `--uhf` shortcuts.
|
|
1374
|
+
|
|
1375
|
+
### Notes
|
|
1376
|
+
- `Auto` reproduces the historical selection (RHF for a closed-shell singlet, UHF for an open
|
|
1377
|
+
shell), so existing callers are unaffected. Forcing UHF on a symmetric singlet converges to the
|
|
1378
|
+
RHF energy (zero spin density).
|
|
1379
|
+
|
|
1380
|
+
## 0.1.2
|
|
1381
|
+
|
|
1382
|
+
### Added
|
|
1383
|
+
- **Hessian API for Python.** Both layers now expose the analytic (CPHF) Cartesian Hessian,
|
|
1384
|
+
which previously was only reachable indirectly through `frequencies`:
|
|
1385
|
+
- `am1_rs.hessian(numbers, positions, charge=0.0, multiplicity=1)` returns the full `3N × 3N`
|
|
1386
|
+
matrix in **atomic units** (`hessian_hartree_per_bohr2`) and, for convenience, in eV/Ų
|
|
1387
|
+
(`hessian_ev_per_angstrom2`), plus `ndof`. Row/column `3*i + k` is atom `i`, axis `k`.
|
|
1388
|
+
- `am1_rs.ase.AM1.get_hessian(atoms=None)` returns the Hessian as a NumPy array in **eV/Ų**
|
|
1389
|
+
(ASE convention). Closed-shell RHF and open-shell UHF are both supported.
|
|
1390
|
+
- **Per-structure charge / multiplicity for the ASE calculator.** In addition to the constructor
|
|
1391
|
+
arguments `AM1(charge=…, multiplicity=…)`, charge and spin multiplicity may now be supplied at
|
|
1392
|
+
calculation time via `atoms.info["charge"]` / `atoms.info["multiplicity"]`. An `atoms.info`
|
|
1393
|
+
entry overrides the constructor value for that structure, and a change in either invalidates
|
|
1394
|
+
cached results (`check_state`).
|
|
1395
|
+
|
|
1396
|
+
### Verified
|
|
1397
|
+
- Confirmed (with tests) that charge and spin multiplicity are received and actually used in
|
|
1398
|
+
both the Python-native functions (per call) and the ASE calculator (at construction *and* at
|
|
1399
|
+
calculation time): charge and multiplicity change the SCF energy, and an electron-count /
|
|
1400
|
+
multiplicity parity mismatch raises. See `tests/test_python_api.py`.
|
|
1401
|
+
|
|
1402
|
+
### Notes
|
|
1403
|
+
- Units are unchanged and follow each layer's convention: the native surface reports atomic
|
|
1404
|
+
units (Hartree/Bohr²) with eV/Ų provided alongside; the ASE layer reports eV/Ų.
|
|
1405
|
+
- No changes to the Rust crate's public API (the analytic Hessian was already available there as
|
|
1406
|
+
`am1_rs::analytic_hessian`, eV/Bohr²).
|