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.
Files changed (179) hide show
  1. am1_rs_python-0.2.2/CHANGELOG.md +1406 -0
  2. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/Cargo.lock +2 -1
  3. am1_rs_python-0.2.2/Cargo.toml +72 -0
  4. am1_rs_python-0.2.2/PKG-INFO +430 -0
  5. am1_rs_python-0.2.2/README.md +393 -0
  6. am1_rs_python-0.2.2/THIRD_PARTY_NOTICES.md +207 -0
  7. am1_rs_python-0.2.2/docs/divide-conquer.md +439 -0
  8. am1_rs_python-0.2.2/docs/methods.md +99 -0
  9. am1_rs_python-0.2.2/docs/pbc.md +816 -0
  10. am1_rs_python-0.2.2/docs/python-api.md +484 -0
  11. am1_rs_python-0.2.2/docs/rust-api.md +405 -0
  12. am1_rs_python-0.2.2/docs/scope.md +183 -0
  13. am1_rs_python-0.2.2/docs/theory.md +369 -0
  14. am1_rs_python-0.2.2/examples/bench/water_chain_102.xyz +104 -0
  15. am1_rs_python-0.2.2/examples/bench/water_chain_201.xyz +203 -0
  16. am1_rs_python-0.2.2/examples/bench/water_chain_399.xyz +401 -0
  17. am1_rs_python-0.2.2/examples/bench/water_chain_801.xyz +803 -0
  18. am1_rs_python-0.2.2/examples/bench/water_cluster_102.xyz +104 -0
  19. am1_rs_python-0.2.2/examples/bench/water_cluster_201.xyz +203 -0
  20. am1_rs_python-0.2.2/examples/bench/water_cluster_399.xyz +401 -0
  21. am1_rs_python-0.2.2/examples/bench/water_cluster_48.xyz +50 -0
  22. am1_rs_python-0.2.2/examples/bench/water_cluster_801.xyz +803 -0
  23. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/examples/bench102.xyz +104 -104
  24. am1_rs_python-0.2.2/pyproject.toml +85 -0
  25. am1_rs_python-0.2.2/python/am1_rs/__init__.py +70 -0
  26. am1_rs_python-0.2.2/python/am1_rs/__main__.py +397 -0
  27. am1_rs_python-0.2.2/python/am1_rs/_native.pyi +344 -0
  28. am1_rs_python-0.2.2/python/am1_rs/ase.py +1135 -0
  29. am1_rs_python-0.2.2/python/am1_rs/native.py +1084 -0
  30. am1_rs_python-0.2.2/python/am1_rs/py.typed +0 -0
  31. am1_rs_python-0.2.2/src/bcc/atomtype.rs +667 -0
  32. am1_rs_python-0.2.2/src/bcc/mod.rs +366 -0
  33. am1_rs_python-0.2.2/src/bin/am1_rs.rs +492 -0
  34. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/data/bccparm.dat +405 -405
  35. am1_rs_python-0.2.2/src/data/rm1_parameters.csv +27 -0
  36. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/data_tables.rs +5 -0
  37. am1_rs_python-0.2.2/src/dipole.rs +249 -0
  38. am1_rs_python-0.2.2/src/divide_conquer.rs +1548 -0
  39. am1_rs_python-0.2.2/src/dual.rs +448 -0
  40. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/dual2.rs +71 -8
  41. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/error.rs +44 -2
  42. am1_rs_python-0.2.2/src/farfield.rs +561 -0
  43. am1_rs_python-0.2.2/src/fermi.rs +415 -0
  44. am1_rs_python-0.2.2/src/fock.rs +575 -0
  45. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/gradient.rs +327 -38
  46. am1_rs_python-0.2.2/src/hamiltonian.rs +366 -0
  47. am1_rs_python-0.2.2/src/hessian.rs +2189 -0
  48. am1_rs_python-0.2.2/src/integrals.rs +875 -0
  49. am1_rs_python-0.2.2/src/ir.rs +247 -0
  50. am1_rs_python-0.2.2/src/lattice.rs +813 -0
  51. am1_rs_python-0.2.2/src/lib.rs +100 -0
  52. am1_rs_python-0.2.2/src/linalg.rs +513 -0
  53. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/math.rs +26 -0
  54. am1_rs_python-0.2.2/src/method.rs +68 -0
  55. am1_rs_python-0.2.2/src/molden.rs +358 -0
  56. am1_rs_python-0.2.2/src/neighbors.rs +397 -0
  57. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/optimizer.rs +7 -1
  58. am1_rs_python-0.2.2/src/overlap.rs +749 -0
  59. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/overlap_numeric.rs +41 -9
  60. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/params.rs +69 -4
  61. am1_rs_python-0.2.2/src/pbc/berry.rs +781 -0
  62. am1_rs_python-0.2.2/src/pbc/complex.rs +500 -0
  63. am1_rs_python-0.2.2/src/pbc/dfpt.rs +2014 -0
  64. am1_rs_python-0.2.2/src/pbc/ewald.rs +1916 -0
  65. am1_rs_python-0.2.2/src/pbc/ewald1d.rs +685 -0
  66. am1_rs_python-0.2.2/src/pbc/ewald2d.rs +773 -0
  67. am1_rs_python-0.2.2/src/pbc/extent.rs +689 -0
  68. am1_rs_python-0.2.2/src/pbc/finite_field.rs +888 -0
  69. am1_rs_python-0.2.2/src/pbc/gradient.rs +441 -0
  70. am1_rs_python-0.2.2/src/pbc/hessian.rs +2156 -0
  71. am1_rs_python-0.2.2/src/pbc/kpoints.rs +315 -0
  72. am1_rs_python-0.2.2/src/pbc/mod.rs +63 -0
  73. am1_rs_python-0.2.2/src/pbc/phonon.rs +556 -0
  74. am1_rs_python-0.2.2/src/pbc/scf.rs +1510 -0
  75. am1_rs_python-0.2.2/src/python.rs +2166 -0
  76. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/repulsion.rs +113 -120
  77. am1_rs_python-0.2.2/src/scf.rs +1318 -0
  78. am1_rs_python-0.2.2/src/system.rs +321 -0
  79. am1_rs_python-0.2.2/src/timing.rs +126 -0
  80. am1_rs_python-0.2.2/src/topology.rs +822 -0
  81. am1_rs_python-0.2.2/tests/auxiliary_integral_impact.rs +76 -0
  82. am1_rs_python-0.2.2/tests/axis_alignment.rs +278 -0
  83. am1_rs_python-0.2.2/tests/bcc_atom_types.rs +313 -0
  84. am1_rs_python-0.2.2/tests/bcc_bond_types.rs +386 -0
  85. am1_rs_python-0.2.2/tests/charged_cell_warning.rs +172 -0
  86. am1_rs_python-0.2.2/tests/core_core_derivatives.rs +292 -0
  87. am1_rs_python-0.2.2/tests/cphf_convergence.rs +105 -0
  88. am1_rs_python-0.2.2/tests/dc_convergence_probe.rs +190 -0
  89. am1_rs_python-0.2.2/tests/dc_open_shell_stress.rs +249 -0
  90. am1_rs_python-0.2.2/tests/dc_periodic.rs +338 -0
  91. am1_rs_python-0.2.2/tests/dc_profile.rs +86 -0
  92. am1_rs_python-0.2.2/tests/dc_where_the_time_goes.rs +86 -0
  93. am1_rs_python-0.2.2/tests/divide_conquer.rs +655 -0
  94. am1_rs_python-0.2.2/tests/element_coverage.rs +185 -0
  95. am1_rs_python-0.2.2/tests/external_field.rs +236 -0
  96. am1_rs_python-0.2.2/tests/farfield.rs +210 -0
  97. am1_rs_python-0.2.2/tests/farfield_tree.rs +213 -0
  98. am1_rs_python-0.2.2/tests/ir.rs +300 -0
  99. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/tests/molecules.rs +12 -2
  100. am1_rs_python-0.2.2/tests/mopac_reference.rs +354 -0
  101. am1_rs_python-0.2.2/tests/orbital_response.rs +287 -0
  102. am1_rs_python-0.2.2/tests/parameter_cache.rs +94 -0
  103. am1_rs_python-0.2.2/tests/pbc_berry.rs +379 -0
  104. am1_rs_python-0.2.2/tests/pbc_born_charges.rs +209 -0
  105. am1_rs_python-0.2.2/tests/pbc_charged.rs +359 -0
  106. am1_rs_python-0.2.2/tests/pbc_dense_stress.rs +168 -0
  107. am1_rs_python-0.2.2/tests/pbc_dfpt.rs +1009 -0
  108. am1_rs_python-0.2.2/tests/pbc_dielectric.rs +689 -0
  109. am1_rs_python-0.2.2/tests/pbc_dielectric_extent.rs +545 -0
  110. am1_rs_python-0.2.2/tests/pbc_ewald.rs +265 -0
  111. am1_rs_python-0.2.2/tests/pbc_exchange_diagnosis.rs +112 -0
  112. am1_rs_python-0.2.2/tests/pbc_external_field.rs +293 -0
  113. am1_rs_python-0.2.2/tests/pbc_finite_field.rs +397 -0
  114. am1_rs_python-0.2.2/tests/pbc_gamma.rs +301 -0
  115. am1_rs_python-0.2.2/tests/pbc_gradient.rs +232 -0
  116. am1_rs_python-0.2.2/tests/pbc_hessian.rs +261 -0
  117. am1_rs_python-0.2.2/tests/pbc_klopman_ohno_tail.rs +309 -0
  118. am1_rs_python-0.2.2/tests/pbc_kpoint_hessian.rs +373 -0
  119. am1_rs_python-0.2.2/tests/pbc_kpoints.rs +315 -0
  120. am1_rs_python-0.2.2/tests/pbc_lo_to.rs +400 -0
  121. am1_rs_python-0.2.2/tests/pbc_lowdim_ewald.rs +216 -0
  122. am1_rs_python-0.2.2/tests/pbc_phased_ewald.rs +352 -0
  123. am1_rs_python-0.2.2/tests/pbc_phonon.rs +334 -0
  124. am1_rs_python-0.2.2/tests/pbc_scf_convergence.rs +345 -0
  125. am1_rs_python-0.2.2/tests/pbc_truncation_study.rs +166 -0
  126. am1_rs_python-0.2.2/tests/pbc_uhf_response.rs +670 -0
  127. am1_rs_python-0.2.2/tests/phased_lowdim.rs +420 -0
  128. am1_rs_python-0.2.2/tests/phonon_determinism.rs +153 -0
  129. am1_rs_python-0.2.2/tests/response_memory.rs +209 -0
  130. am1_rs_python-0.2.2/tests/rm1.rs +219 -0
  131. am1_rs_python-0.2.2/tests/scalar_special.rs +155 -0
  132. am1_rs_python-0.2.2/tests/scaling.rs +366 -0
  133. am1_rs_python-0.2.2/tests/test_ase_pbc_md.py +878 -0
  134. am1_rs_python-0.2.2/tests/test_cli.py +283 -0
  135. am1_rs_python-0.2.2/tests/test_divide_conquer.py +244 -0
  136. am1_rs_python-0.2.2/tests/test_lazy_cache.py +170 -0
  137. am1_rs_python-0.2.2/tests/test_new_api_0_2_1.py +712 -0
  138. am1_rs_python-0.2.2/tests/theory_components.rs +791 -0
  139. am1_rs_python-0.2.2/tests/topology_bcc.rs +532 -0
  140. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/third_party/antechamber/ATOMTYPE_BCC.DEF +154 -154
  141. am1_rs_python-0.2.2/third_party/antechamber/LICENSE +674 -0
  142. am1_rs_python-0.2.2/third_party/antechamber/README.md +45 -0
  143. am1_rs_python-0.2.2/third_party/mopac/LICENSE +176 -0
  144. am1_rs_python-0.1.3/CHANGELOG.md +0 -49
  145. am1_rs_python-0.1.3/Cargo.toml +0 -38
  146. am1_rs_python-0.1.3/PKG-INFO +0 -238
  147. am1_rs_python-0.1.3/README.md +0 -224
  148. am1_rs_python-0.1.3/THIRD_PARTY_NOTICES.md +0 -80
  149. am1_rs_python-0.1.3/docs/python-api.md +0 -182
  150. am1_rs_python-0.1.3/docs/rust-api.md +0 -221
  151. am1_rs_python-0.1.3/docs/scope.md +0 -26
  152. am1_rs_python-0.1.3/docs/theory.md +0 -105
  153. am1_rs_python-0.1.3/pyproject.toml +0 -21
  154. am1_rs_python-0.1.3/python/am1_rs/__init__.py +0 -27
  155. am1_rs_python-0.1.3/python/am1_rs/ase.py +0 -134
  156. am1_rs_python-0.1.3/python/am1_rs/native.py +0 -144
  157. am1_rs_python-0.1.3/src/bcc.rs +0 -304
  158. am1_rs_python-0.1.3/src/bin/am1_rs.rs +0 -283
  159. am1_rs_python-0.1.3/src/dual.rs +0 -268
  160. am1_rs_python-0.1.3/src/fock.rs +0 -161
  161. am1_rs_python-0.1.3/src/hamiltonian.rs +0 -125
  162. am1_rs_python-0.1.3/src/hessian.rs +0 -1074
  163. am1_rs_python-0.1.3/src/integrals.rs +0 -473
  164. am1_rs_python-0.1.3/src/lib.rs +0 -54
  165. am1_rs_python-0.1.3/src/linalg.rs +0 -238
  166. am1_rs_python-0.1.3/src/overlap.rs +0 -443
  167. am1_rs_python-0.1.3/src/python.rs +0 -282
  168. am1_rs_python-0.1.3/src/scf.rs +0 -887
  169. am1_rs_python-0.1.3/src/system.rs +0 -149
  170. am1_rs_python-0.1.3/src/topology.rs +0 -274
  171. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/LICENSE +0 -0
  172. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/examples/ethanol.xyz +0 -0
  173. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/examples/methane.xyz +0 -0
  174. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/examples/water.xyz +0 -0
  175. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/basis.rs +0 -0
  176. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/constants.rs +0 -0
  177. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/src/data/am1_parameters.csv +0 -0
  178. {am1_rs_python-0.1.3 → am1_rs_python-0.2.2}/tests/test_python_api.py +0 -0
  179. {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²).