am1-rs-python 0.1.3__tar.gz → 0.2.3__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 (211) hide show
  1. am1_rs_python-0.2.3/CHANGELOG.md +2083 -0
  2. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/Cargo.lock +2 -1
  3. am1_rs_python-0.2.3/Cargo.toml +72 -0
  4. am1_rs_python-0.2.3/PKG-INFO +418 -0
  5. am1_rs_python-0.2.3/README.md +377 -0
  6. am1_rs_python-0.2.3/THIRD_PARTY_LICENSES.md +4249 -0
  7. am1_rs_python-0.2.3/THIRD_PARTY_NOTICES.md +271 -0
  8. am1_rs_python-0.2.3/docs/README.md +67 -0
  9. am1_rs_python-0.2.3/docs/divide-conquer.md +478 -0
  10. am1_rs_python-0.2.3/docs/methods.md +104 -0
  11. am1_rs_python-0.2.3/docs/pbc.md +983 -0
  12. am1_rs_python-0.2.3/docs/python-api.md +622 -0
  13. am1_rs_python-0.2.3/docs/rust-api.md +484 -0
  14. am1_rs_python-0.2.3/docs/scope.md +190 -0
  15. am1_rs_python-0.2.3/docs/theory.md +439 -0
  16. am1_rs_python-0.2.3/examples/bench/water_chain_102.xyz +104 -0
  17. am1_rs_python-0.2.3/examples/bench/water_chain_201.xyz +203 -0
  18. am1_rs_python-0.2.3/examples/bench/water_chain_399.xyz +401 -0
  19. am1_rs_python-0.2.3/examples/bench/water_chain_801.xyz +803 -0
  20. am1_rs_python-0.2.3/examples/bench/water_cluster_102.xyz +104 -0
  21. am1_rs_python-0.2.3/examples/bench/water_cluster_201.xyz +203 -0
  22. am1_rs_python-0.2.3/examples/bench/water_cluster_399.xyz +401 -0
  23. am1_rs_python-0.2.3/examples/bench/water_cluster_48.xyz +50 -0
  24. am1_rs_python-0.2.3/examples/bench/water_cluster_801.xyz +803 -0
  25. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/examples/bench102.xyz +104 -104
  26. am1_rs_python-0.2.3/pyproject.toml +109 -0
  27. am1_rs_python-0.2.3/python/am1_rs/__init__.py +72 -0
  28. am1_rs_python-0.2.3/python/am1_rs/__main__.py +1074 -0
  29. am1_rs_python-0.2.3/python/am1_rs/_native.pyi +356 -0
  30. am1_rs_python-0.2.3/python/am1_rs/ase.py +1257 -0
  31. am1_rs_python-0.2.3/python/am1_rs/native.py +1322 -0
  32. am1_rs_python-0.2.3/python/am1_rs/py.typed +0 -0
  33. am1_rs_python-0.2.3/src/bcc/atomtype.rs +667 -0
  34. am1_rs_python-0.2.3/src/bcc/mod.rs +384 -0
  35. am1_rs_python-0.2.3/src/bin/am1_rs.rs +1359 -0
  36. am1_rs_python-0.2.3/src/data/rm1_parameters.csv +27 -0
  37. am1_rs_python-0.2.3/src/data_tables.rs +224 -0
  38. am1_rs_python-0.2.3/src/dipole.rs +249 -0
  39. am1_rs_python-0.2.3/src/divide_conquer.rs +1706 -0
  40. am1_rs_python-0.2.3/src/dual.rs +448 -0
  41. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/src/dual2.rs +71 -8
  42. am1_rs_python-0.2.3/src/error.rs +187 -0
  43. am1_rs_python-0.2.3/src/farfield.rs +561 -0
  44. am1_rs_python-0.2.3/src/fermi.rs +439 -0
  45. am1_rs_python-0.2.3/src/fock.rs +575 -0
  46. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/src/gradient.rs +327 -38
  47. am1_rs_python-0.2.3/src/gto.rs +517 -0
  48. am1_rs_python-0.2.3/src/hamiltonian.rs +366 -0
  49. am1_rs_python-0.2.3/src/hessian.rs +2687 -0
  50. am1_rs_python-0.2.3/src/integrals.rs +901 -0
  51. am1_rs_python-0.2.3/src/ir.rs +253 -0
  52. am1_rs_python-0.2.3/src/lattice.rs +813 -0
  53. am1_rs_python-0.2.3/src/lib.rs +116 -0
  54. am1_rs_python-0.2.3/src/linalg.rs +540 -0
  55. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/src/math.rs +26 -0
  56. am1_rs_python-0.2.3/src/method.rs +84 -0
  57. am1_rs_python-0.2.3/src/molden.rs +777 -0
  58. am1_rs_python-0.2.3/src/neighbors.rs +397 -0
  59. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/src/optimizer.rs +102 -57
  60. am1_rs_python-0.2.3/src/overlap.rs +749 -0
  61. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/src/overlap_numeric.rs +41 -9
  62. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/src/params.rs +78 -8
  63. am1_rs_python-0.2.3/src/pbc/berry.rs +781 -0
  64. am1_rs_python-0.2.3/src/pbc/complex.rs +500 -0
  65. am1_rs_python-0.2.3/src/pbc/dfpt.rs +2407 -0
  66. am1_rs_python-0.2.3/src/pbc/ewald.rs +1916 -0
  67. am1_rs_python-0.2.3/src/pbc/ewald1d.rs +685 -0
  68. am1_rs_python-0.2.3/src/pbc/ewald2d.rs +773 -0
  69. am1_rs_python-0.2.3/src/pbc/extent.rs +689 -0
  70. am1_rs_python-0.2.3/src/pbc/finite_field.rs +888 -0
  71. am1_rs_python-0.2.3/src/pbc/gradient.rs +441 -0
  72. am1_rs_python-0.2.3/src/pbc/hessian.rs +2170 -0
  73. am1_rs_python-0.2.3/src/pbc/kerker.rs +232 -0
  74. am1_rs_python-0.2.3/src/pbc/kpoints.rs +315 -0
  75. am1_rs_python-0.2.3/src/pbc/mod.rs +83 -0
  76. am1_rs_python-0.2.3/src/pbc/optimizer.rs +479 -0
  77. am1_rs_python-0.2.3/src/pbc/phonon.rs +859 -0
  78. am1_rs_python-0.2.3/src/pbc/scf.rs +2080 -0
  79. am1_rs_python-0.2.3/src/python.rs +2571 -0
  80. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/src/repulsion.rs +113 -120
  81. am1_rs_python-0.2.3/src/scf.rs +1461 -0
  82. am1_rs_python-0.2.3/src/system.rs +321 -0
  83. am1_rs_python-0.2.3/src/timing.rs +126 -0
  84. am1_rs_python-0.2.3/src/topology.rs +822 -0
  85. am1_rs_python-0.2.3/tests/atomic_data.rs +175 -0
  86. am1_rs_python-0.2.3/tests/attribution.rs +346 -0
  87. am1_rs_python-0.2.3/tests/auxiliary_integral_impact.rs +76 -0
  88. am1_rs_python-0.2.3/tests/axis_alignment.rs +278 -0
  89. am1_rs_python-0.2.3/tests/bcc_atom_types.rs +313 -0
  90. am1_rs_python-0.2.3/tests/bcc_bond_types.rs +386 -0
  91. am1_rs_python-0.2.3/tests/charged_cell_warning.rs +172 -0
  92. am1_rs_python-0.2.3/tests/core_core_derivatives.rs +292 -0
  93. am1_rs_python-0.2.3/tests/cphf_controls.rs +115 -0
  94. am1_rs_python-0.2.3/tests/cphf_convergence.rs +105 -0
  95. am1_rs_python-0.2.3/tests/cpscf_kerker.rs +240 -0
  96. am1_rs_python-0.2.3/tests/dc_convergence_probe.rs +190 -0
  97. am1_rs_python-0.2.3/tests/dc_open_shell_stress.rs +249 -0
  98. am1_rs_python-0.2.3/tests/dc_optimize.rs +311 -0
  99. am1_rs_python-0.2.3/tests/dc_periodic.rs +338 -0
  100. am1_rs_python-0.2.3/tests/dc_profile.rs +86 -0
  101. am1_rs_python-0.2.3/tests/dc_where_the_time_goes.rs +86 -0
  102. am1_rs_python-0.2.3/tests/dfpt_profile.rs +115 -0
  103. am1_rs_python-0.2.3/tests/divide_conquer.rs +655 -0
  104. am1_rs_python-0.2.3/tests/element_coverage.rs +185 -0
  105. am1_rs_python-0.2.3/tests/external_field.rs +236 -0
  106. am1_rs_python-0.2.3/tests/farfield.rs +210 -0
  107. am1_rs_python-0.2.3/tests/farfield_tree.rs +213 -0
  108. am1_rs_python-0.2.3/tests/fractional_occupations.rs +326 -0
  109. am1_rs_python-0.2.3/tests/ir.rs +323 -0
  110. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/tests/molecules.rs +12 -2
  111. am1_rs_python-0.2.3/tests/mopac_reference.rs +354 -0
  112. am1_rs_python-0.2.3/tests/orbital_response.rs +287 -0
  113. am1_rs_python-0.2.3/tests/parameter_cache.rs +94 -0
  114. am1_rs_python-0.2.3/tests/pbc_berry.rs +379 -0
  115. am1_rs_python-0.2.3/tests/pbc_bloch_batch.rs +136 -0
  116. am1_rs_python-0.2.3/tests/pbc_born_charges.rs +209 -0
  117. am1_rs_python-0.2.3/tests/pbc_charged.rs +359 -0
  118. am1_rs_python-0.2.3/tests/pbc_dense_stress.rs +168 -0
  119. am1_rs_python-0.2.3/tests/pbc_dfpt.rs +1009 -0
  120. am1_rs_python-0.2.3/tests/pbc_dfpt_contraction.rs +253 -0
  121. am1_rs_python-0.2.3/tests/pbc_dielectric.rs +689 -0
  122. am1_rs_python-0.2.3/tests/pbc_dielectric_extent.rs +545 -0
  123. am1_rs_python-0.2.3/tests/pbc_ewald.rs +265 -0
  124. am1_rs_python-0.2.3/tests/pbc_exchange_diagnosis.rs +112 -0
  125. am1_rs_python-0.2.3/tests/pbc_external_field.rs +293 -0
  126. am1_rs_python-0.2.3/tests/pbc_finite_field.rs +397 -0
  127. am1_rs_python-0.2.3/tests/pbc_gamma.rs +301 -0
  128. am1_rs_python-0.2.3/tests/pbc_gradient.rs +232 -0
  129. am1_rs_python-0.2.3/tests/pbc_hessian.rs +298 -0
  130. am1_rs_python-0.2.3/tests/pbc_klopman_ohno_tail.rs +309 -0
  131. am1_rs_python-0.2.3/tests/pbc_kpoint_hessian.rs +373 -0
  132. am1_rs_python-0.2.3/tests/pbc_kpoints.rs +315 -0
  133. am1_rs_python-0.2.3/tests/pbc_lo_to.rs +400 -0
  134. am1_rs_python-0.2.3/tests/pbc_lowdim_ewald.rs +216 -0
  135. am1_rs_python-0.2.3/tests/pbc_phased_ewald.rs +352 -0
  136. am1_rs_python-0.2.3/tests/pbc_phonon.rs +417 -0
  137. am1_rs_python-0.2.3/tests/pbc_scf_convergence.rs +345 -0
  138. am1_rs_python-0.2.3/tests/pbc_truncation_study.rs +166 -0
  139. am1_rs_python-0.2.3/tests/pbc_uhf_response.rs +670 -0
  140. am1_rs_python-0.2.3/tests/phased_lowdim.rs +420 -0
  141. am1_rs_python-0.2.3/tests/phonon_acoustic.rs +317 -0
  142. am1_rs_python-0.2.3/tests/phonon_determinism.rs +153 -0
  143. am1_rs_python-0.2.3/tests/phonon_interpolation.rs +266 -0
  144. am1_rs_python-0.2.3/tests/response_memory.rs +209 -0
  145. am1_rs_python-0.2.3/tests/rm1.rs +219 -0
  146. am1_rs_python-0.2.3/tests/scalar_special.rs +155 -0
  147. am1_rs_python-0.2.3/tests/scaling.rs +366 -0
  148. am1_rs_python-0.2.3/tests/scf_hardening.rs +136 -0
  149. am1_rs_python-0.2.3/tests/test_ase_pbc_md.py +878 -0
  150. am1_rs_python-0.2.3/tests/test_binding_arity.py +127 -0
  151. am1_rs_python-0.2.3/tests/test_cli.py +319 -0
  152. am1_rs_python-0.2.3/tests/test_cli_matrix.py +442 -0
  153. am1_rs_python-0.2.3/tests/test_divide_conquer.py +244 -0
  154. am1_rs_python-0.2.3/tests/test_fractional_occupations.py +111 -0
  155. am1_rs_python-0.2.3/tests/test_lazy_cache.py +170 -0
  156. am1_rs_python-0.2.3/tests/test_new_api_0_2_1.py +735 -0
  157. am1_rs_python-0.2.3/tests/test_phonon_eigenvectors.py +112 -0
  158. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/tests/test_python_api.py +8 -1
  159. am1_rs_python-0.2.3/tests/test_result_contract.py +285 -0
  160. am1_rs_python-0.2.3/tests/theory_components.rs +791 -0
  161. am1_rs_python-0.2.3/tests/topology_bcc.rs +532 -0
  162. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/third_party/antechamber/ATOMTYPE_BCC.DEF +154 -154
  163. am1_rs_python-0.1.3/src/data/bccparm.dat → am1_rs_python-0.2.3/third_party/antechamber/BCCPARM.DAT +405 -405
  164. am1_rs_python-0.2.3/third_party/antechamber/LICENSE +674 -0
  165. am1_rs_python-0.2.3/third_party/antechamber/README.md +53 -0
  166. am1_rs_python-0.2.3/third_party/mopac/LICENSE +176 -0
  167. am1_rs_python-0.2.3/third_party/mopac/README.md +127 -0
  168. am1_rs_python-0.2.3/third_party/pyseqm/README.md +60 -0
  169. am1_rs_python-0.2.3/tools/bench.py +159 -0
  170. am1_rs_python-0.2.3/tools/collect_dependency_licenses.py +289 -0
  171. am1_rs_python-0.2.3/tools/extract_rm1_parameters.py +145 -0
  172. am1_rs_python-0.2.3/tools/make_water_cluster.py +132 -0
  173. am1_rs_python-0.2.3/tools/profile.py +82 -0
  174. am1_rs_python-0.2.3/tools/profile_dc.py +110 -0
  175. am1_rs_python-0.1.3/CHANGELOG.md +0 -49
  176. am1_rs_python-0.1.3/Cargo.toml +0 -38
  177. am1_rs_python-0.1.3/PKG-INFO +0 -238
  178. am1_rs_python-0.1.3/README.md +0 -224
  179. am1_rs_python-0.1.3/THIRD_PARTY_NOTICES.md +0 -80
  180. am1_rs_python-0.1.3/docs/python-api.md +0 -182
  181. am1_rs_python-0.1.3/docs/rust-api.md +0 -221
  182. am1_rs_python-0.1.3/docs/scope.md +0 -26
  183. am1_rs_python-0.1.3/docs/theory.md +0 -105
  184. am1_rs_python-0.1.3/pyproject.toml +0 -21
  185. am1_rs_python-0.1.3/python/am1_rs/__init__.py +0 -27
  186. am1_rs_python-0.1.3/python/am1_rs/ase.py +0 -134
  187. am1_rs_python-0.1.3/python/am1_rs/native.py +0 -144
  188. am1_rs_python-0.1.3/src/bcc.rs +0 -304
  189. am1_rs_python-0.1.3/src/bin/am1_rs.rs +0 -283
  190. am1_rs_python-0.1.3/src/data_tables.rs +0 -123
  191. am1_rs_python-0.1.3/src/dual.rs +0 -268
  192. am1_rs_python-0.1.3/src/error.rs +0 -49
  193. am1_rs_python-0.1.3/src/fock.rs +0 -161
  194. am1_rs_python-0.1.3/src/hamiltonian.rs +0 -125
  195. am1_rs_python-0.1.3/src/hessian.rs +0 -1074
  196. am1_rs_python-0.1.3/src/integrals.rs +0 -473
  197. am1_rs_python-0.1.3/src/lib.rs +0 -54
  198. am1_rs_python-0.1.3/src/linalg.rs +0 -238
  199. am1_rs_python-0.1.3/src/overlap.rs +0 -443
  200. am1_rs_python-0.1.3/src/python.rs +0 -282
  201. am1_rs_python-0.1.3/src/scf.rs +0 -887
  202. am1_rs_python-0.1.3/src/system.rs +0 -149
  203. am1_rs_python-0.1.3/src/topology.rs +0 -274
  204. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/LICENSE +0 -0
  205. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/examples/ethanol.xyz +0 -0
  206. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/examples/methane.xyz +0 -0
  207. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/examples/water.xyz +0 -0
  208. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/src/basis.rs +0 -0
  209. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/src/constants.rs +0 -0
  210. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/src/data/am1_parameters.csv +0 -0
  211. {am1_rs_python-0.1.3 → am1_rs_python-0.2.3}/third_party/pyseqm/LICENSE +0 -0
@@ -0,0 +1,2083 @@
1
+ # Changelog
2
+
3
+ ## 0.2.3
4
+
5
+ ### Added
6
+
7
+ - **A periodic response on a partially occupied ground state is now an error, not a number.**
8
+ Every response path here — the `q = 0` Hessian, `Z*`, `ε_∞`, the polarizability, DFPT — is
9
+ derived at fixed integer occupation. There is no `∂f/∂ε` term anywhere in the coupled-perturbed
10
+ equations, so the Fermi surface cannot redistribute charge under the perturbation: a partially
11
+ filled band is a term the equations **do not have**, not a small error in the answer.
12
+
13
+ Through 0.2.2 both paths ran regardless, and each went wrong in its own quiet way. The CPHF path
14
+ classifies every band as occupied (`f > full − 1e-6`) or virtual (`f < 1e-6`) and a band between
15
+ the two is **neither** — dropped from the response entirely, taking its whole orbital-relaxation
16
+ contribution with it. DFPT keeps every band pair but weights each by a *frozen*
17
+ `f_n(k) − f_m(k+q)`. Both returned a plausible number, and nothing in the result said it was
18
+ outside what the equations cover. `Am1Error::FractionalOccupation` now names the k-point, the
19
+ band, the occupation and the smearing, in both paths.
20
+
21
+ **This does not gate smearing**, which is what the previous release made available precisely so
22
+ that a dense inorganic solid could reach a converged ground state at all. The judgement is on the
23
+ **converged occupations**, not on `smearing_ev > 0` — a gapped conduction band holds about
24
+ `exp(−gap/2kT)` electrons, which crosses the cut only when the gap falls under roughly `27·kT`.
25
+ Measured, in `tests/fractional_occupations.rs`: cubic BN at 4×4×4 and `kT = 0.3` — the case
26
+ `docs/pbc.md` tells a user to reach for — is **accepted**, as is AlP at 6×6×6 with none, which is
27
+ the mesh AlP's reported frequencies come from. AlP *under* smearing is refused, at a worst
28
+ fractional occupation of `1.8e-6` (4×4×4) and `2.0e-6` (6×6×6).
29
+
30
+ Refusing at `2e-6` reads like a threshold that wants loosening, and it is the opposite: at `2e-6`
31
+ the classifier already puts that band in neither set, so what is being refused is a response
32
+ missing an entire band. The cut is not an accuracy tolerance but the exact point at which the
33
+ surrounding code stops being able to represent the level, which is why `INTEGER_OCCUPATION_TOL`
34
+ is a constant rather than an option, and why the way out —
35
+ `PbcOptions::require_integer_occupations`, `allow_fractional_occupations=True` in Python and on
36
+ the ASE calculator — is a yes/no. The choice is between an error and a known-wrong number, not
37
+ between two accuracies.
38
+
39
+ - **Phonon modes come back with their eigenvectors.** `ForceConstants::modes` in Rust,
40
+ `eigenvectors=True` on `native.phonons` and on the ASE calculator's `get_phonons`. Through 0.2.2
41
+ the diagonalization had the vectors in hand and threw them away, so learning *what* a mode does
42
+ meant running the whole calculation again in another program — and this release's own
43
+ investigation of Al₂(CH₃)₆ had to borrow the direction from the molecular Hessian for exactly
44
+ that reason.
45
+
46
+ **Two conventions come back, because they answer different questions and confusing them is a
47
+ mass-weighting error of `√(m_a/m_b)`** — a factor of 4 between hydrogen and oxygen, which looks
48
+ plausible in a picture. `polarization` is the orthonormal eigenvector `e(q)` of the mass-weighted
49
+ dynamical matrix, which is what belongs in any sum over modes; `displacements` is `e_a/√m_a`,
50
+ what an atom actually does and what to add to a geometry, deliberately **not** renormalized so
51
+ that a heavy and a light atom keep their true relative amplitude. Columns are modes, ordered like
52
+ `frequencies_cm`, and complex because away from Γ the pattern carries a Bloch phase between
53
+ cells.
54
+
55
+ Opt-in: `n_q · (3N)²` complex numbers is a large array to return unasked for a band structure
56
+ over a few hundred `q`. The native layer hands back the real and imaginary halves separately
57
+ since it has no complex type in a plain nested list; the ASE layer, which has numpy as a hard
58
+ dependency, joins them into one `complex128` array. `eigenvectors` is part of the ASE cache key —
59
+ without that the second of two calls at one geometry gets the first one's vectorless answer.
60
+
61
+ - **The periodic SCF has a fallback pipeline, and says what it did.** One mixing scheme does not
62
+ converge every system, and the two ways it fails are distinguishable from the residual trace
63
+ alone: a residual that *grows or oscillates* is charge sloshing, and one that *decays at a ratio
64
+ above 0.99* is a near-degenerate Fermi level. So the controller watches for each and switches —
65
+ Kerker preconditioning with the mixing dropped to 0.1 and the DIIS history discarded for the
66
+ first, a raised electronic temperature for the second — starting from a superposition of atomic
67
+ densities and Pulay/DIIS at a mixing of 0.3.
68
+
69
+ **The raised temperature is annealed back down**, because smearing changes the *fixed point* and
70
+ not merely the path to it: a system's energy at 0.5 eV is not its energy at 0.05 eV. Convergence
71
+ is not accepted until the requested smearing is restored, so `converged` means converged at what
72
+ the caller asked for. `PbcResult::fallback` (`ScfFallback`) carries `kerker_kappa`,
73
+ `final_mixing`, `max_smearing_ev` and `diis_resets`, so a number knows its own provenance — a
74
+ system that needed 1 eV to move at all is telling you something about its Fermi surface, and that
75
+ should not be invisible in the result.
76
+
77
+ **Kerker had to be rederived to be usable here.** The plane-wave factor `A(q) = q²/(q² + q₀²)`
78
+ indexes the density by `q`, and this one is real-space blocks over an atom-centred basis;
79
+ transforming over the lattice-translation index instead is a projection onto far fewer k-points
80
+ than translations, which throws most of the residual away. Written against the Coulomb kernel it
81
+ is `A = (1 + κγ)⁻¹` with `κ = q₀²/4π`, and *that* form needs no plane waves: NDDO's monopole
82
+ kernel `γ_ab` is the atom-resolved Coulomb interaction and is already assembled. It is applied to
83
+ the **net charge per atom only** — intra-atomic rehybridization and bond-order changes are
84
+ short-ranged, are not what sloshes, and damping them would slow every system down to fix a few.
85
+ `DfptOptions::cpscf_kerker_kappa` exposes the same preconditioner to the response solve.
86
+
87
+ - **Divide-and-conquer has a CLI switch and does geometry optimization.** `--dc` turns it on for
88
+ the `energy`, `gradient` and `optimize` modes, with `--dc-core` and `--dc-buffer` setting the
89
+ subsystem radii; the modes it does not cover refuse rather than silently running the full
90
+ calculation. `optimize_divide_conquer` (Rust) and `optimize=True` on `native.divide_conquer`
91
+ (Python) drive L-BFGS on the DC gradient, so a structure too large for a full SCF can be relaxed
92
+ rather than only measured. The result carries the per-step trace, the optimized geometry and the
93
+ usual `electronic_ev`/`core_ev`/`converged` keys.
94
+
95
+ - **The coupled-perturbed solver's limits are settable from both APIs.** `cphf_max_iter` and
96
+ `cphf_tol` were private constants — 100 and `1e-9` for the molecular CPHF, 200 and `1e-8` for
97
+ the periodic one — so a system whose response needed more passes could not be given them from
98
+ Rust or from Python without editing the crate. They are fields on `Am1Options` and `PbcOptions`
99
+ now, which is what both surfaces already carry, and reach `hessian`, `frequencies`,
100
+ `ir_spectrum`, `vibrations` and the ASE calculator.
101
+
102
+ They are separate from `max_scf` on purpose: the response is a different fixed point from the
103
+ ground state, converging at its own rate and failing for its own reasons. `tests/cphf_controls.rs`
104
+ pins the defaults to the constants they replaced — turning a constant into an option is a chance
105
+ to change a number by accident, and that would show up as a slightly different Hessian rather
106
+ than as a failure — and checks that a cap of one iteration is honoured *and reported as one*,
107
+ which is what distinguishes a field that reaches the solver from one plumbed to nowhere.
108
+
109
+ - **Geometry optimization under periodic boundary conditions** (`pbc::optimize_periodic`,
110
+ `native.pbc_optimize`, and the CLI's `optimize` mode whenever a cell is present). L-BFGS on the
111
+ k-point forces, and with `--relax-cell` on the analytic stress as well, at an optional target
112
+ pressure. The variables are scaled coordinates plus a strain measured from the starting cell, so
113
+ an atom on a symmetry site stays on it while the lattice deforms; a strain component is a
114
+ variable only when both its axes are periodic, so a slab's vacuum direction and a chain's two
115
+ transverse directions are frozen by construction rather than by an option.
116
+
117
+ - **Periodic boundary conditions on the command line**, per axis. `--cell` takes 1, 3, 6 or 9
118
+ numbers (cube; `a b c`; `a b c α β γ`; or three lattice vectors), `--pbc x`/`xy`/`xyz`/`none`
119
+ sets the periodic axes, and `--pbc-x`/`--pbc-y`/`--pbc-z` set them one at a time and accumulate,
120
+ so a slab in the *xz* plane is `--pbc-x --pbc-z`. A cell also comes from an extended-XYZ
121
+ `Lattice="…"` comment line, which the Rust reader has always understood and the Python one now
122
+ does too. `--kpts` sets the SCF's Monkhorst–Pack mesh; `--no-pbc` drops the cell and runs the
123
+ contents of it as a molecule.
124
+
125
+ `energy`, `gradient`, `optimize` and `frequencies` take the periodic path when a cell is
126
+ present. `charges`, `ir` and `molden` are molecular-only and now **say so** instead of silently
127
+ ignoring the cell.
128
+
129
+ - **A `phonons` mode**, with `--supercell`, and explicit control of where the bands are evaluated:
130
+ `--qpoints QX QY QZ …` for a list, or `--qpath` plus `--qpath-points` for a straight-line path
131
+ through named corners. Without either it reports the supercell's commensurate `q` — the only
132
+ ones it represents exactly — and says which case the output is.
133
+
134
+ - **Orbital coefficients on the command line.** `orbitals --orbital-coefficients` prints the MO
135
+ coefficient matrix with per-AO labels (`O1:px`), for both spin channels, and every `orbitals`
136
+ run now ends with a frontier block giving HOMO, LUMO and the gap in Hartree *and* eV.
137
+
138
+ - The Python front end gained `--mol2-output`, which only the Rust one had. The mol2 text is
139
+ rendered by the crate (`bcc::to_mol2`) so the two write the same bytes rather than the same
140
+ format.
141
+
142
+ ### Changed
143
+
144
+ - **Molden output is `[GTO]` by default, and the coefficients now belong to the basis the file
145
+ declares.** Two independent problems, both of which made the rendered orbital not the orbital.
146
+
147
+ 1. **`[STO]` is a section almost nothing reads.** The AM1 valence basis really is Slater-type,
148
+ so `[STO]` describes it exactly — and Jmol, VMD, Avogadro and Multiwfn all implement `[GTO]`
149
+ and skip `[STO]`, which leaves a viewer drawing orbitals from no basis at all. The default is
150
+ now a Gaussian expansion; `--molden-basis sto` keeps the old section for anything that reads
151
+ it.
152
+
153
+ The expansion is **fitted at run time, not tabulated** (`src/gto.rs`). The published STO-*n*G
154
+ tables stop at `3d` and at fixed exponents, and this crate needs `4s`–`5p` for Ge, As, Se,
155
+ Br, Sb, Te, I and Hg at whatever `ζ` a parameter file carries. So the contraction is solved
156
+ for: exact closed-form Gram matrix, quadratured Slater overlaps, an even-tempered start and
157
+ coordinate descent on the exponents, cached per `(n, l, ngauss)` and transferred across `ζ`
158
+ by the exact scaling `α → ζ²α`. Six primitives reproduce every shell in the parameter sets to
159
+ an overlap better than **0.99999** — measured, and written into the file's header so the
160
+ number travels with the data. Nothing is taken from a third-party table.
161
+
162
+ 2. **NDDO's coefficients were being written against the wrong functions.** Its working equations
163
+ are `FC = Cε` with no overlap matrix, so `C` is expressed in an implicitly orthogonalized
164
+ (Löwdin) basis while the file listed the raw, non-orthogonal Slater functions. Through 0.2.2
165
+ the file carried a note apologizing for it. It no longer needs one: `S` is available — it is
166
+ the analytic Slater overlap the resonance integrals already use — so `S^{−1/2}C` is written
167
+ instead. On water the largest off-diagonal `S` is **0.35**, so this is not a small
168
+ correction. `--molden-orthogonal` restores the 0.2.2 coefficients.
169
+
170
+ The occupation/orbital correspondence is now checked by **rebuilding the density from the file's
171
+ own text** — parsing the `Occup=` fields and contracting `Σ_k occ_k C_k C_kᵀ` — and comparing it
172
+ against `scf.density`, for RHF and UHF. The old test compared the writer's output against the
173
+ expression the writer used, which cannot detect a transposition or an off-by-one and says so in
174
+ its own comment.
175
+
176
+ - **Vibrational frequencies have translations and rotations projected out, and the `|ν| < 50 cm⁻¹`
177
+ rule is gone.** `vibrational_analysis` now diagonalizes `QᵀH'Q` on an orthonormal basis of the
178
+ complement of the rigid-body subspace, so `frequencies_cm` holds `3N − n_rigid` entries and
179
+ every one of them is a vibration by construction. `n_rigid` is the *rank* of the
180
+ translation/rotation span — 6, or 5 for a linear molecule, or 3 for an atom — and is discovered
181
+ rather than assumed.
182
+
183
+ The threshold it replaces was wrong in both directions. A torsion below 50 cm⁻¹ was labelled a
184
+ rotation; and away from a stationary point a rotation carries real curvature and lands well
185
+ above 50. Measured on a deliberately stretched water: the old route returned nine numbers, three
186
+ of them rigid-body modes sitting at **1238, 1234 and 1238 cm⁻¹**, and the three genuine
187
+ vibrations were contaminated by up to **311 cm⁻¹**. The new `rigid_body_frequencies_cm` reports
188
+ that residual instead of hiding it, and it is zero only at a stationary point — which is the
189
+ diagnostic the tag was standing in for.
190
+
191
+ `IrSpectrum` follows: `3N − n_rigid` intensities, and no rigid-body bands to filter out.
192
+
193
+ ### Fixed
194
+
195
+ - **A field along `pbc_optimize`.** `electric_field` reached `pbc_point` and the Hessian but was
196
+ silently dropped by the periodic optimizer and by the Python CLI's periodic path, so a relaxation
197
+ under a field quietly relaxed without one.
198
+
199
+ - **`native.dfpt(smearing_ev=…)` reached the binding.** The Rust entry point gained the parameter
200
+ earlier in this release, the wrapper's signature and docstring advertised it, and the forwarding
201
+ call never passed it — so from Python it did nothing, and a caller who set it got a calculation
202
+ at zero smearing that they believed was smeared.
203
+
204
+ Nothing in the suite could catch that. `tests/test_result_contract.py` checks the keys a result
205
+ carries, and every key was there; `tests/test_cli_matrix.py` diffs the two front ends, and the
206
+ CLI does not expose this parameter. A **dropped argument** is invisible to both — the call
207
+ succeeds and returns a plausible number. `tests/test_binding_arity.py` closes the class rather
208
+ than the instance: it parses every `#[pyo3(signature = …)]` and counts what the matching wrapper
209
+ forwards, so the next parameter added to a signature and forgotten in the call fails at the
210
+ boundary. Writing it found its own false positive first — `divide_conquer`'s signature carries a
211
+ `//` comment whose prose contains commas, which the parser counted as a parameter.
212
+
213
+ - **A mercury phonon calculation failed with "eigendecomposition failed: NoConvergence".** The
214
+ message named the decomposition; the cause was fourteen array slots away.
215
+
216
+ MOPAC's per-element tables are indexed by *parameter slot*, not by atomic number: the
217
+ lanthanides have no NDDO parameters, so La-Yb (57-70) are simply absent and every element above
218
+ caesium sits fourteen places too low. Transcribed into a Z-indexed array without reopening that
219
+ gap, mercury's 200.59 amu landed at index 66 and `MASS[80]` was left at `0.0`.
220
+
221
+ A zero mass is not a wrong frequency. Every vibrational quantity divides by `sqrt(M_a M_b)`, so
222
+ `D(q) = a0^2 C(q)/sqrt(M_a M_b)` returned `-inf` for the mercury rows, and the first thing to
223
+ notice was the eigensolver. Instrumenting the whole DFPT assembly found the Ewald sums, the
224
+ bare perturbation, the band energies, the response denominators and `C(q)` itself all finite --
225
+ which is what pointed at the one step after them. Hg is the only element above Z = 56 that AM1
226
+ parameterizes, which is why nothing else could reach it.
227
+
228
+ `EHEAT_KCAL` carried the identical shift, papered over: someone had written mercury's 14.690
229
+ into index 80 by hand and left the shifted copy at index 66, so the symptom was invisible for
230
+ the only element that reaches it while every entry between was still a neighbour's value. Both
231
+ tables now reopen the gap. `data_tables::atomic_mass` refuses a zero rather than dividing by
232
+ it, and the three mass-weighting sites call it, so a future element added without a mass is
233
+ named where it is missing instead of at the eigensolver.
234
+
235
+ `tests/atomic_data.rs` pins the indexing at the elements either side of the gap and, separately,
236
+ asserts that atomic mass rises with atomic number everywhere except the three places it famously
237
+ does not (Ar/K, Co/Ni, Te/I) -- a structural check that catches a shift introduced anywhere,
238
+ not only where someone thought to write an anchor. Fluorite HgF2 now returns three acoustic
239
+ modes at 1e-6 cm^-1 and a triply degenerate optic mode at 176 cm^-1.
240
+ - **A periodic single point through the Python CLI died with `am1-rs: 'electronic_ev'`.** One
241
+ word, no verb: that is what a `KeyError` looks like after the CLI's blanket `except Exception`
242
+ has caught it. `native.pbc_point` never emitted `electronic_ev` or `core_ev`, which the
243
+ molecular `single_point` has reported since 0.1, so the periodic energy report reached for a key
244
+ that was not there — *after* running the whole SCF.
245
+
246
+ This is the second time a CLI mode has been broken by a key the bindings do not emit (0.2.2
247
+ fixed three of five modes), and reading the two front ends side by side is evidently not a way
248
+ to catch it. Two tests now do:
249
+
250
+ - `tests/test_result_contract.py` writes the contract down. For every native entry point it
251
+ lists the keys a *consumer* — the CLI, the ASE calculator, the documented API — depends on,
252
+ and asserts they are present, together with the shape agreements between them (the mode count
253
+ of `frequencies_cm` against `modes`, the polar tensor's `3N` columns against the modes'
254
+ `3N − 6`). Adding a key is free; removing one fails at the boundary.
255
+ - `tests/test_cli_matrix.py` runs **every mode against every combination of the options that
256
+ apply to it** — several hundred command lines — asserting that none produces a traceback or a
257
+ bare quoted identifier, and then diffs a representative subset against the Rust binary byte
258
+ for byte. It found this bug, and three more listed below, on its first run.
259
+
260
+ The CLI's reads now also go through a checked accessor, so an unforeseen missing key becomes a
261
+ message naming the key, the mode and the module path rather than a one-word mystery.
262
+
263
+ - **The reported periodic Fermi energy was zero for every bound system.** `PbcResult::fermi_energy_ev`
264
+ was folded with `max` from an accumulator initialised to `0.0`, and a chemical potential is a few
265
+ eV *below* vacuum, so `max(0, −4.6)` is `0`. It reached users through `native.pbc_point` and
266
+ through `Calculator.results["fermi_energy"]`. The accumulator starts at `−∞` now. The one test
267
+ that looked at the value printed it rather than asserting on it; it asserts.
268
+
269
+ - **Two more front-end divergences the matrix test found.** `native.pbc_optimize` inherited
270
+ `fold_time_reversal = false` from the shared periodic setup — which the *response* entry points
271
+ need and a ground-state relaxation does not — so it sampled 4 k-points where `run_pbc_scf`
272
+ samples 3. And the periodic `optimize` mode printed the optimizer's step count where the SCF's
273
+ belonged, reporting "3 iterations" for a 16-iteration SCF.
274
+
275
+ #### Phonons
276
+
277
+ - **Force constants were filed under the wrong lattice translation, so every interpolated band was
278
+ wrong.** A supercell's Γ Hessian is an aliased sum `H_{(0,a),(t,b)} = Σ_N Φ_ab(T_t + N)`, and
279
+ `Φ_ab(T_t)` was read off it with `T_t` taken from the order the atoms were *built* in — `0, 1, …
280
+ n−1` along each axis. On a three-fold cell that files the neighbour on one side under `+2` when
281
+ it physically sits at `−1`.
282
+
283
+ At a **commensurate** `q = m/n` the two labels give the same Bloch phase, which is why every
284
+ phonon test in the suite passed: `tests/pbc_phonon.rs` only ever asked for the points the
285
+ supercell represents exactly. Between them they are simply different numbers — on an H₂ chain at
286
+ the zone boundary the two spectra differ by **22 cm⁻¹**, and `Φ(T) = Φ(−T)ᵀ`, an exact symmetry
287
+ of the real force constants, was violated by **7.1e-3** where it is now **6.1e-11**.
288
+
289
+ Each block is now assigned to the **minimum image** of `r_b + T − r_a` over the translations
290
+ congruent to `T_t`, split equally when several tie. `tests/phonon_interpolation.rs` reconstructs
291
+ the old labelling from the new one and shows the two agree at every commensurate `q` to `4e-12`
292
+ and disagree elsewhere. It also records what does *not* distinguish them: `ω(q) = ω(−q)` holds
293
+ for both, because `Φ` is real and `dynamical_matrix` symmetrizes.
294
+
295
+ - **Three modes at `q = 0` were not zero, while the acoustic sum rule read `1e-15`.** Diamond in
296
+ its fcc primitive cell came out with acoustic frequencies of **−275, −275 and −65 cm⁻¹**.
297
+
298
+ The sum rule `Σ_{T,b} Φ_ab(T) = 0` is a statement about the **rows** of `Φ`, and
299
+ `dynamical_matrix` symmetrizes `D(q)` before diagonalizing it — so the spectrum sees the average
300
+ of the row and column sums, and the column sums vanish only if `Φ_ab(T) = Φ_ba(−T)ᵀ`. The
301
+ supercell Hessian satisfies that only as well as its real-space and exchange cutoffs do:
302
+ measured on graphene, row sums `1.8e-9` and column sums `8.2e-4`. The rule genuinely held, and
303
+ it was the wrong rule to hold alone.
304
+
305
+ `enforce_acoustic_sum_rule` now imposes both, by alternating projection, and
306
+ `transpose_asymmetry()` reports the second one. The alternation has a fixed point at the
307
+ *antisymmetric* part of the sum-rule violation — a rotational residue the translational rule
308
+ does not constrain — which on graphene is `6.5e-6` eV/Bohr² against a `|Φ|` of 18.7 and reaches
309
+ the acoustic branch as **0.7 cm⁻¹**. That is documented rather than iterated against.
310
+
311
+ #### Two more defects the crystal survey turned up
312
+
313
+ - **The rigid-body projector removed rotations from a periodic structure.** A rotation is a
314
+ symmetry of an isolated molecule and not of a crystal — rotating the contents of a cell without
315
+ rotating the lattice costs energy — so its directions are ordinary vibrations of a supercell, and
316
+ projecting them out deleted two or three genuine modes. `translation_rotation_basis` now returns
317
+ translations only under a cell. `tests/pbc_phonon.rs` is what caught it: the supercell-folding
318
+ identity is a statement about the **full** spectrum, and the mode counts stopped matching.
319
+
320
+ `VibrationalModes::all_frequencies_cm` is added alongside it — the unprojected `3N` spectrum,
321
+ which is what 0.2.2 returned — so the projection is auditable rather than taken on trust, and so
322
+ that identity can be stated against the quantity it is actually about.
323
+
324
+ - **DFPT did not impose the acoustic sum rule.** The supercell route corrects `Φ(T)`; DFPT
325
+ computes the response directly at each `q` and so has no `Φ(T)` to correct — but the ground
326
+ state underneath it still carries the real-space and exchange cutoffs, and what those leave
327
+ behind lands on the acoustic branch. `C(0)` now has its row sums subtracted from the on-site
328
+ block, exactly as the supercell path does, and only at `q = 0` — the only `q` where a uniform
329
+ translation is a solution and the rule says anything. The row sums go to **exactly zero**.
330
+
331
+ `tests/phonon_acoustic.rs` checks that on the **matrix**, not on a spectrum, and deliberately:
332
+ "the three lowest frequencies are zero" is a *different* claim, true only at a stationary point.
333
+ An H₂ chain has two genuinely imaginary transverse modes that sort below its three acoustic
334
+ ones, and wurtzite ZnO likewise comes out `−80, −80, 0, 0, 0` — three zeros and an instability,
335
+ not five near-zeros. Reading the sorted list's first three entries as "the acoustic modes" is
336
+ how that gets mistaken for a sum-rule failure, which is exactly what happened while this was
337
+ being written.
338
+
339
+ - **The eigensolver reported `NoConvergence` for a matrix that was not finite.** faer says that
340
+ for a matrix containing `NaN` or `∞`, which reads as an iteration-count problem and is not one —
341
+ no tolerance or restart can decompose something that is not a matrix of numbers.
342
+ `symmetric_eigen` now checks its input first, at `O(n²)` against an `O(n³)` decomposition, and
343
+ names the first offending element and the likely upstream cause; a *finite* matrix that still
344
+ fails reports the largest element alongside, because the scale is what makes a conditioning
345
+ failure readable.
346
+
347
+ It immediately paid for itself. A fluorite HgF₂ DFPT calculation used to fail with
348
+ `faer eigendecomposition failed: NoConvergence`, which points at the decomposition. It now says
349
+ `the 18x18 matrix handed to the eigensolver is not finite: element (0, 0) is -inf`, which points
350
+ where the problem actually is — HgF₂'s *ground state* converges in 17 iterations, so the `−∞` is
351
+ produced inside the response assembly. That is a real remaining defect, now localized rather
352
+ than mistaken for a linear-algebra one.
353
+
354
+ - **DFPT and every other periodic response entry point could not use Fermi smearing.**
355
+ `periodic_setup` hardcoded `smearing_ev = 0.0`, so the ground state a response is built on had to
356
+ converge under sharp aufbau filling — which, on a coarse mesh, an inorganic solid does not.
357
+ `dfpt` takes `smearing_ev` now. It converges the ground state; the coupled-perturbed equations
358
+ above it still assume integer occupations, and the docstring says so rather than implying a
359
+ metal is in scope.
360
+
361
+ ### SCF robustness
362
+
363
+ A phonon calculation on any dense inorganic crystal failed with `AM1 SCF did not converge after
364
+ 900 iterations (error=NaN)`. That message was wrong in a way worth stating first.
365
+
366
+ - **`error=NaN` was a hardcoded `f64::NAN` placeholder**, not a measurement. `run_am1` filled the
367
+ error variant's `error` field with it on every non-convergence, so the one number a user had to
368
+ go on was a constant — and it reads as an arithmetic overflow, which is a different problem with
369
+ a different remedy. It now carries the commutator norm the last iteration actually reached.
370
+ Measured on zincblende AlP: `1.344e0`, not `NaN`.
371
+
372
+ - **`AM1_SCF_DEBUG=1`** now traces the iteration — energy, `dE`, `dP`, `‖[F,P]‖`, the HOMO–LUMO
373
+ gap and the DIIS step weight. The gap and the weight are there because they separate the two
374
+ ways an SCF fails, and from the outside both look like a residual that stops falling.
375
+
376
+ With that in place, three plausible causes were **ruled out by measurement** rather than by
377
+ argument:
378
+
379
+ | suspected cause | what the trace shows |
380
+ |---|---|
381
+ | band crossing at the Fermi level | the gap is **183 eV** and never closes; the occupied set cannot change identity |
382
+ | DIIS charge sloshing / runaway extrapolation | the step weight is pinned at **1.00** — A-DIIS coefficients live on the simplex |
383
+ | the wrong accelerator | plain iteration, CDIIS and the A-DIIS→CDIIS hybrid **all fail identically** (`tests/scf_hardening.rs`) |
384
+ | no damping in the molecular solver | added it; with damping down to 0.05 the residual still plateaus at 1.3 |
385
+
386
+ What is left is that the aufbau fixed point is not reachable for these systems at all, which
387
+ together with `max_image_overlap = 0.20` — NDDO's working equations assume zero overlap between an
388
+ atom and its own periodic images — reads as the model being outside its domain rather than the
389
+ solver being at fault. `docs/pbc.md` says so, and DFPT is the route that does work for them.
390
+
391
+ Two candidate fixes were then implemented, measured, and **removed again** — measuring them is
392
+ what the exercise was for. Both are recorded in the source where a future reader will look
393
+ (`scf::diis_extrapolate_packed`, and the note above `scf::scf_debug`):
394
+
395
+ - **A `Sum|c_i|` bound on the molecular DIIS.** The periodic solver caps it at 40 and this one did
396
+ not, which looked like an oversight. Refusing an over-weight step made the A-DIIS->CDIIS hybrid
397
+ fall back to plain iteration at handover: 23 iterations on formaldehyde against **11** unbounded.
398
+ Shrinking the history window instead recovered formaldehyde but broke a diamond supercell, whose
399
+ error vectors are near-dependent in *every* window near convergence. And it fixed nothing --
400
+ AlP fails identically under all three accelerators. Only the non-finite check survives.
401
+
402
+ - **A stagnation escape**: twelve iterations without beating the best residual halves the density
403
+ mixing and clears the history. It did not converge AlP (still `1.3` at a mixing of 0.05), and it
404
+ broke cases that worked. "Not beating the best" is also true of a run improving steadily but
405
+ *slowly*, and a graphene supercell improves 0.4 % per iteration -- the rule fired five times and
406
+ turned a comfortable 800-iteration budget into an insufficient one. It also fired in a lone
407
+ carbon atom's convergent tail at `|[F,P]| = 9.3e-6` and moved a converged energy by `1e-4` eV.
408
+
409
+ What is kept is what was measured to help:
410
+
411
+ - **A non-finite iterate is now `ScfDiverged`, reported at the iteration it happens.** Once `NaN`
412
+ reaches the density every comparison against a tolerance is false, so the old loop could neither
413
+ converge nor notice, and ran out its whole budget first.
414
+
415
+ - **The Rust CLI printed "SCF converged in N iterations" for a periodic SCF that had not
416
+ converged.** `run_pbc_scf` reports `converged: false` rather than erroring, and the front end
417
+ did not look. `native.pbc_point` has always raised, so the two front ends disagreed about
418
+ whether the same input was a result or a failure; the Rust side now agrees with the Python one,
419
+ in the same words.
420
+
421
+ - **The UHF CPHF got the conjugate-gradient solver the restricted path has had since 0.2.1.** Its
422
+ DIIS-accelerated fixed point does not always converge: on the RM1 methyl radical the residual
423
+ falls to `2e-8` and then **oscillates** between `2e-8` and `6e-7` for the rest of the iteration
424
+ limit, so `am1-rs frequencies --method rm1 --uhf` and `ir` failed outright. CG on the coupled
425
+ `(alpha, beta)` orbital Hessian has no such failure mode and takes about half the Fock builds.
426
+ Where the curvature is not positive -- an unstable UHF solution, which that radical genuinely
427
+ has -- it falls back to the fixed point, and the error now **says which** case it is instead of
428
+ advising more iterations for a saddle point no iteration count can fix.
429
+
430
+ ### Performance
431
+
432
+ - **The periodic paths had no timing instrumentation at all**, which is why the two findings below
433
+ went unnoticed. The molecular side has had `AM1_TIMING` since 0.1; `pbc::scf` and `pbc::dfpt` had
434
+ none, so every claim about where periodic time went was a guess. They are instrumented now
435
+ (`pbc:core`, `pbc:ewald`, `pbc:fock`, `pbc:bloch`, `pbc:eigen`, `pbc:fill`, `dfpt:scf`,
436
+ `dfpt:skeleton`, `dfpt:bands`, `dfpt:cpscf`, `dfpt:contract`), and both findings were the first
437
+ thing the profile said.
438
+
439
+ - **The Bloch sum was 41 % of a periodic SCF** — more than the eigendecompositions it feeds.
440
+ `H(k) = Σ_T e^{iq·T} H(T)` was a scalar loop through 2D indexing, run once per k point, so the
441
+ same `n_T · nao²` array was streamed `n_k` times per iteration. Written across the mesh it is two
442
+ matrix products: pack the blocks as one `n_T × nao²` panel and the phases as `n_k × n_T` cosine
443
+ and sine matrices. Measured interleaved in one process, best of seven: 699 translations × 24 AOs
444
+ at 19 k-points, **11.8 ms → 4.4 ms (2.7x)**, agreeing with the loop to 1e-12.
445
+
446
+ - **The inverse Bloch sum inside the CPSCF was 92 % of a DFPT phonon run.**
447
+ `Δp(T) = Σ_k w_k e^{−ik·T} ΔP(k)` ran once per spin channel per CPSCF iteration per
448
+ perturbation — the innermost thing in the calculation — as `n_k · n_T · nao²` calls to
449
+ `ComplexBlocks::add`, **each of which hashed the translation to find its block**. On zincblende
450
+ AlP at a 3×3×3 mesh that is 1.6 million hashed lookups per iteration per perturbation. It is the
451
+ same two matrix products as the forward direction. Measured A/B in one process:
452
+ **5.47 s → 2.28 s for the whole run (2.4x)**, `dfpt:cpscf` from 147 to 33 thread-seconds, with
453
+ the frequencies bit-identical.
454
+
455
+ - **The periodic Fock build now contracts in the packed pair basis**, as the molecular one has
456
+ since 0.1. `(μν|λσ)` depends on its index pairs only through their packed forms, so a `4×4×4×4`
457
+ nest visits each stored integral 2.56 times over and recomputes `pack` for every visit. Folding
458
+ each on-site density onto the same packed index turns both Coulomb directions into one pass over
459
+ `w`: `10×10` multiply-adds on contiguous rows instead of `2 × 256` through a strided symmetric
460
+ matrix. The exchange hoists its bra row out of the inner loop, sixteen lookups instead of two
461
+ hundred and fifty-six. This is an **operation count, not a measurement** — per-phase wall clock
462
+ on the development machine moves by 2.5x on unchanged arithmetic, which is larger than the
463
+ effect.
464
+
465
+ - **`C(q)`'s contraction now picks the cheaper of two routes** instead of assuming. Holding the
466
+ bare perturbation sparsely costs `ndof² · n_k · nnz`; Bloch-summing it once and contracting
467
+ densely costs `ndof · n_k · nnz + ndof² · n_k · nao²`. The module assumed sparse always wins, and
468
+ on the H₂ chain it was validated against it does — but `nnz` counts entries over every
469
+ translation inside the 40 Bohr cutoff, and a *small* cell admits hundreds. Rutile GeO₂ measures
470
+ `nnz = 110944` against `nao² = 576`, where sparse does 190 times the work. `select_contraction`
471
+ compares the two counts, and the memory the dense route would need, per calculation.
472
+ `tests/pbc_dfpt_contraction.rs` exercises both sides of the crossover and checks they agree
473
+ (1.8e-15). On AlP the two are within measurement noise of each other, because after the fix
474
+ above the contraction is 0.5 % of the run; the choice matters at larger `ndof`, where the sparse
475
+ cost grows quadratically in it and the dense cost linearly.
476
+
477
+ - **The CPSCF's Pulay Gram matrix is carried between iterations.** `pulay_coefficients` rebuilds
478
+ `⟨r_i, r_j⟩` for every pair on every call — `n(n+1)/2` dot products over vectors as long as the
479
+ real-space response. In the ground-state SCF that is once per iteration and lost in the noise; in
480
+ the CPSCF it is once per iteration **per perturbation**, and it measured as the largest single
481
+ piece of the solve: `cpscf:mix` was **17.4 %** of a DFPT phonon run on zincblende AlP, more than
482
+ the two-electron kernel build beside it. Between iterations the history gains one residual and
483
+ loses at most one, so exactly one row is new. Keeping it costs `depth²` floats and turns 55 dot
484
+ products into 10: `cpscf:mix` fell to **10.3 %**, and the coefficients are bit-identical to the
485
+ rebuilt ones (`tests/pbc_dfpt_contraction.rs`).
486
+ - **Known limit.** Rutile GeO₂'s CPSCF does not converge: it stops at the 200-iteration cap with a
487
+ residual of 8.2e-8 against a 1e-10 tolerance. The response solve, not the ground state, is the
488
+ stiff part of a phonon calculation on a dense oxide. Recorded rather than worked around.
489
+
490
+ ### Validation
491
+
492
+ - **The crystal phonon battery, re-run through DFPT.** Every structure type: rocksalt, zincblende,
493
+ wurtzite, rutile, fluorite, perovskite, spinel and a layered rocksalt. AM1 has no sodium, so the
494
+ rocksalt entry is ZnO in NaCl's structure rather than NaCl itself.
495
+
496
+ Against measured Raman/infrared frequencies, where the structure converges:
497
+
498
+ | structure | mode | measured | am1-rs | |
499
+ |---|---|---|---|---|
500
+ | rutile GeO₂ | Eg | 680 | 680 | +0.0 % |
501
+ | rutile GeO₂ | A1g | 702 | 717 | +2.1 % |
502
+ | rutile GeO₂ | B2g | 870 | 890 | +2.3 % |
503
+ | zincblende ZnS | TO / LO | 274 / 352 | 294 / 345 | +7.2 / −2.1 % |
504
+ | zincblende AlP | TO / LO | 440 / 501 | 459 / 563 | +4.3 / +12 % |
505
+ | zincblende BN | see note | 1055 / 1305 | 1084 / 1139 | |
506
+ | perovskite NH₄ZnF₃ | NH₄⁺ stretch | 3145 | 3133 | −0.4 % |
507
+ | perovskite NH₄ZnF₃ | NH₄⁺ bend | 1450 | 1569 | +8.2 % |
508
+ | fluorite HgF₂ | T2g | 236 | 176 | −25 % |
509
+ | wurtzite ZnO | E2(high) | 437 | 535 | +22 % |
510
+
511
+ The acoustic sum rule holds throughout: three modes at `10⁻⁵ cm⁻¹` or better on every structure
512
+ that converges. Fluorite HgF₂ returns its T2g as an exactly threefold degenerate `176/176/176`,
513
+ which is the cubic symmetry the code has to reproduce and not an average of three numbers that
514
+ happen to be close.
515
+
516
+ **BN is the one whose splitting pattern is wrong, and it is left open.** ZnS gives `294/294/345`
517
+ and AlP `459/459/563` — a doubly degenerate TO with the LO above it, which is the right shape.
518
+ BN gives `1084/1139/1139`: the *singlet* is below the doublet, so the LO would be under the TO.
519
+ The magnitudes are reasonable and the acoustic modes are clean, so this is not the AlP failure
520
+ again; it is either a mesh that is still too coarse at 6×6×6 or something in the polar term for
521
+ this cell. Recorded rather than presented as agreement.
522
+
523
+ - **AlP's spurious 2657 cm⁻¹ Γ mode was a k-mesh artifact, not a bug.** It had been on the open
524
+ list since the first battery. Zincblende's Γ optic modes are `T₂` — the LO–TO splitting is the
525
+ non-analytic term and needs a direction — so `463 / 561 / 2657` is not a physical spectrum, and
526
+ 2657 cm⁻¹ is not a possible Al–P frequency at all. Sweeping the mesh settles it:
527
+
528
+ ```text
529
+ 2×2×2 511 535 535 ground state E = −259.96 eV
530
+ 3×3×3 the periodic SCF does not converge
531
+ 4×4×4 463 562 2657 ground state E = −179.85 eV
532
+ 6×6×6 459 459 563 ground state E = −179.92 eV
533
+ ```
534
+
535
+ At 6×6×6 the doublet is the TO and the singlet the LO above it, which is the right pattern. The
536
+ 2×2×2 ground state is 80 eV below the other two on a **two-atom cell** — that is a different
537
+ electronic state, not k-convergence. All four meshes report `max_image_overlap = 0.20` against
538
+ NDDO's assumption of zero, which is the standing diagnostic for exactly this: a cell where the
539
+ model is out of domain has more than one SCF solution, and a coarse mesh finds the wrong one.
540
+
541
+ - **Spinel ZnAl₂O₄ is what found the response-tolerance bug, and it is the best agreement in the
542
+ battery.** It first failed at the SCF — through the *supercell* route, which runs the molecular
543
+ SCF at Γ with no k-mesh. On the k-point route its ground state converges without difficulty, 79
544
+ iterations at a 2×2×2 mesh. The response then reached `1.025e-9` and was **refused** against
545
+ `cpscf_tol = 1e-10`. That default is three orders tighter than the `p_tol = 1e-7` of the density
546
+ being differentiated, and it was throwing away a converged answer. With the corrected default:
547
+
548
+ | Raman (gahnite) | measured | am1-rs | |
549
+ |---|---|---|---|
550
+ | | 417 | 418 | **+0.1 %** |
551
+ | | 512 | 486 | −5.0 % |
552
+ | | 660 | 650 | −1.5 % |
553
+ | | 720 | 704 | −2.2 % |
554
+
555
+ - **Organometallics.** Dimethylzinc Zn(CH₃)₂ gives three acoustic modes at `2.8e-5 cm⁻¹`, no
556
+ imaginary branch, and ν_s/ν_as(ZnC) within 6 % with δ_as(CH₃) at −0.2 %. Trimethylaluminium
557
+ Al₂(CH₃)₆ resolves the bridging and terminal Al–C stretches *separately* — 577 against a measured
558
+ ~560, and 667 against ~700 — which is the structure-sensitive part.
559
+
560
+ It also keeps two imaginary modes, −1277 and −321 cm⁻¹, at a geometry relaxed to
561
+ `max|F| = 4e-4 eV/Bohr`. **That is a genuine index-2 saddle, and it was checked rather than
562
+ asserted.** Following the mode downhill — displace 0.3 Å along its eigenvector and re-relax —
563
+ drops the energy by **6.96 eV**, moves atoms by up to 2.2 Å, and lands on a structure with **no
564
+ imaginary modes at all**. The mode is carried by the hydrogens of a bridging methyl.
565
+
566
+ What it descends *to* is the interesting part: the minimum has **no bridging carbons left** — six
567
+ terminal Al–C at 1.78 Å and Al–Al at 3.67 Å, against a measured dimer's 2.14 Å bridges and 2.60 Å
568
+ Al–Al. **AM1 does not bind the methyl-bridged dimer**; it dissociates Al₂(CH₃)₆ into two AlMe₃.
569
+ That is a statement about the model rather than about this crate, and an unsurprising one — the
570
+ bridge is a three-centre two-electron bond and NDDO has no description of one — but it means the
571
+ "ν(AlC) bridge" row above is comparing against a frequency the model does not have. At the true
572
+ AM1 minimum the terminal stretch is −1.7 % and ν(CH) +1.8 %, which is what a monomer should give.
573
+
574
+ Two things had to be verified before that conclusion could be drawn, because a first attempt got
575
+ it backwards by comparing two different geometries:
576
+
577
+ * the **periodic Hessian is correct** — at the same geometry in a 24×22×22 Å cell it reproduces
578
+ the molecular Hessian to 0.3 % (−1404 against −1400, −633 against −631, −281 against −281), so
579
+ the −1277 in the 11×9×9 cell is real intermolecular stabilization of a real saddle and not an
580
+ artifact of the periodic path;
581
+ * `native.phonons` returned frequencies **without eigenvectors**, so a mode could not be followed
582
+ from a phonon result at all; the direction had to come from the molecular `frequencies`, whose
583
+ `cartesian_displacements` was a valid source only because the two Hessians agree in a large
584
+ cell. That gap is closed in this release — see `eigenvectors=True` under **Added** — so the
585
+ same investigation is now one calculation rather than two.
586
+
587
+ Zn(CH₃)₂'s **phonon bands** along Γ–Z–Γ–X are the clearest check of the interpolation: the
588
+ branches disperse along the stacking axis, where the molecules touch, and are flat to the eye
589
+ across the 8 Å of vacuum, with every intramolecular branch flat in both. A 1×1×3 supercell
590
+ resolves `Φ(T)` along `c` only, and that shows: the row sum rule reaches `9.1e-16` while the
591
+ transpose asymmetry only reaches `3.8e-3`, leaving a −27 cm⁻¹ dip near Γ.
592
+
593
+ - **Still failing, and recorded as such.** The layered rocksalt does not reach a converged SCF, and
594
+ nor does AlP through the supercell route at any size tried. The structures themselves are
595
+ verified — the spinel builder checks Zn tetrahedral and Al octahedral coordination before
596
+ running, and the layered cell checks both cations are octahedral — so this is the SCF, not a
597
+ mis-built cell.
598
+
599
+ ### Attribution and documentation
600
+
601
+ - **The MOPAC attribution was checked against the upstream tree, and one claim in this release was
602
+ withdrawn.** `src/data_tables.rs` was added to MOPAC's provenance table earlier in 0.2.3 on the
603
+ strength of the mercury bug: `EHEAT_KCAL` and `MASS` both skipped exactly the fourteen
604
+ lanthanides MOPAC omits for want of NDDO parameters, which looked like the signature of a copied
605
+ slot-indexed array. Checking a working tree at the pinned commit shows otherwise.
606
+
607
+ MOPAC's `parameters_C.F90` is indexed by **atomic number**: `eheat(80)` is mercury's 14.690 and
608
+ the lanthanides carry real values. Nothing upstream has the gap. Comparing values as well:
609
+ `MASS` is not MOPAC's table at all — twelve of fifteen sampled elements differ (Tc **97.0**
610
+ against 98.9062, Ti 47.867 against 47.90, W 183.84 against 183.85), which is modern IUPAC
611
+ against MOPAC's older set. `EHEAT_KCAL` differs at eighteen entries below `Z = 86`, sodium and
612
+ vanadium among them. `MASS` has been removed from the provenance table, `N_S`/`N_P`/`QN` were
613
+ listed there in error and are not, and the origin of the fourteen-slot gap is now recorded as
614
+ **unknown** rather than attributed to a source that has been checked and does not have it.
615
+
616
+ **§4(d) is now verified rather than open.** `git ls-tree -r HEAD` at
617
+ `052691223d19935a89f0fe18cd12301bd83e4201` lists no `NOTICE` file anywhere, so the clause does
618
+ not apply; the upstream copyright travels in per-file headers, and our `LICENSE` is
619
+ byte-identical to upstream's. The clause-by-clause position on §4(a)–(d) is written out in
620
+ `third_party/mopac/README.md`.
621
+
622
+ - **`BCCPARM.DAT` moved to `third_party/antechamber/`.** It was the one embedded data file with no
623
+ provenance anywhere near it: the two parameter CSVs carry a header naming their source, and a
624
+ bare numeric table has no comment syntax to hold one. Adding a header would have meant either
625
+ editing a file this project calls verbatim or teaching the parser to skip something upstream
626
+ does not have. It now sits beside the licence covering it and the README recording where it came
627
+ from, which is what `ATOMTYPE_BCC.DEF` was already doing.
628
+
629
+ - **The provenance notes ship in the wheel.** PEP 639 `license-files` listed
630
+ `third_party/*/LICENSE` only, so the per-work READMEs were sdist-only — and those are where
631
+ Apache-2.0 §4(b)'s statement of changes lives, so the clause that needed the binary distribution
632
+ was the one that did not reach it. Three separate documents meanwhile claimed CI checked for
633
+ them in the wheel. Both are true now: they are declared in `license-files`, land under
634
+ `dist-info/licenses/`, and CI fails the release without them.
635
+
636
+ - **The linked Rust crates had no notices at all, and now have `THIRD_PARTY_LICENSES.md`.**
637
+ `THIRD_PARTY_NOTICES.md` accounted for the bundled *data* — three parameter tables. It said
638
+ nothing about the linked *code*, which is 112 crates: `faer`, `rayon`, `libm`, `pyo3` and their
639
+ transitive dependencies, all statically inside `am1_rs._native` and `am1_rs_cli`. Forty-three
640
+ are MIT-only, and MIT requires its copyright and permission notice in "all copies or substantial
641
+ portions of the Software"; a statically linked binary is a copy. The rest are Apache-2.0 or
642
+ dual, and Apache-2.0 §4(a) requires the licence text. None of it was recorded anywhere, which
643
+ makes every wheel shipped through 0.2.2 non-compliant with several dozen licences at once — a
644
+ larger gap than anything in the parameter tables, and invisible next to them.
645
+
646
+ The file is **generated**, by `tools/collect_dependency_licenses.py`, from the resolved graph and
647
+ the licence files in the cargo registry cache: a hand-written list is wrong the first time a
648
+ dependency changes and nothing fails when it is. Scope is the *normal* dependency closure —
649
+ `dev-dependencies` are not linked into anything shipped and `build-dependencies` run on the
650
+ builder's machine. Licence texts are deduplicated by exact content, so two crates share an entry
651
+ only when their copyright lines match too, which keeps 112 crates in 52 texts without dropping a
652
+ notice.
653
+
654
+ Twelve crates, `faer` among them, publish **no licence file** in their crate tarball at all, so
655
+ there is no upstream copyright line to reproduce. Those are listed separately with the authors
656
+ their own manifests declare and the canonical permission notice for the licence they claim —
657
+ marked as what it is rather than filled in with a plausible-looking copyright line.
658
+
659
+ `python tools/collect_dependency_licenses.py --check` regenerates and diffs; CI runs it, so the
660
+ file has to move with `Cargo.lock`.
661
+ - **`tests/attribution.rs`.** Seven invariants, asserted from the Rust suite rather than only from
662
+ the release job: every bundled work has both a licence and a provenance note; the notices file
663
+ accounts for every one of them; `license-files` covers licences *and* notes; `Cargo.toml`
664
+ excludes neither from the `.crate`; every source file carries an SPDX identifier (119 of them);
665
+ every embedded data file either has a provenance header or sits beside its licence; and the CI
666
+ check the prose describes is the one that actually runs. Attribution fails silently — nothing
667
+ breaks when it goes missing, it just goes missing — which is why it is a test.
668
+
669
+ - `third_party/mopac/` and `third_party/pyseqm/` gained the per-directory `README.md` that
670
+ `third_party/antechamber/` already had: which file came from where, under what name, what was
671
+ ported versus merely cited, and the licence's own requirements. A licence says what the terms
672
+ are; it does not say which files it covers. `THIRD_PARTY_NOTICES.md` opens with a table of the
673
+ three bundled works, and CI now fails the release if any `third_party/` subdirectory reaches the
674
+ sdist without **both** its `LICENSE` and its `README.md`.
675
+
676
+ - The **RM1** citation (Rocha, Freire, Simas & Stewart, *J. Comput. Chem.* **27**, 1101 (2006))
677
+ now appears in the crate-level documentation, in `method.rs`, and beside the table it belongs to
678
+ in `data_tables.rs` — not only in `THIRD_PARTY_NOTICES.md` and `docs/methods.md`.
679
+
680
+ ## 0.2.2
681
+
682
+ ### Fixed
683
+
684
+ - **A periodic SCF could not converge a symmetry-degenerate cell, and three separate defects were
685
+ in the way.** A two-dimensional lattice of methane — closed shell, 9 eV gap, no hydrogen bonds,
686
+ no magnetism, about as easy as a periodic calculation gets — could not reach `p_tol = 1e-10` at
687
+ any iteration count, mesh, cutoff, mixing fraction or smearing width. The isolated molecule
688
+ converged in 28 iterations, so nothing in the parameterization or the integrals was at fault.
689
+ What it does have is a **threefold degenerate HOMO**.
690
+
691
+ 1. **`pbc::complex::hermitian_eigen` lost `√ε` on a degenerate level.** It solves the complex
692
+ Hermitian problem through a `2n × 2n` real embedding, in which every eigenvalue appears twice
693
+ — `(x, y)` and `(−y, x)` are the same complex vector, one times `i`. Picking one per pair used
694
+ a **single** classical Gram–Schmidt pass and accepted any residual above `1e-8`. On a
695
+ degenerate level the duplicate genuinely lies in the span already, so what survived the
696
+ subtraction was cancellation noise, renormalized to unit length and accepted as a physical
697
+ eigenvector. The occupied projector built from it carried about **3e-8**, which is exactly the
698
+ floor the SCF stalled at, and `3e-8` is `√ε` — the signature of the loss. Fixed by projecting
699
+ twice ("twice is enough") and cutting at `0.1`: a duplicate's residual is `O(ε)` and a
700
+ genuinely new direction inside a `k`-fold block has residual² of at least `1 − 1/k ≥ ½` for
701
+ some remaining column, so the two are seven orders apart and the old threshold sat between
702
+ them. The degenerate projector goes from 3e-8 to **2.2e-16** against its closed form.
703
+
704
+ 2. **The periodic SCF had no convergence acceleration at all** — plain linear mixing at 0.3,
705
+ while the molecular path had A-DIIS→CDIIS. It did not show because the covered systems were
706
+ stiff: a hydrogen fluoride slab reached `1e-10` in about 140 passes, which reads as a slow
707
+ system rather than a missing feature. The near-constant 139–141 across unrelated systems was
708
+ the tell. Now Pulay (DIIS) mixing on the real-space density, `PbcOptions::diis_history`
709
+ (8 by default; `0` restores the old behaviour). Hydrogen fluoride 140 → **22**, water
710
+ 140 → **28**, a methane slab 130 → **23**. Memory is `2 × depth` copies of the density, which
711
+ for a large cell is the dominant allocation of the run.
712
+
713
+ 3. **The energy was not the energy of any density.** It was contracted from the *mixed* density
714
+ against the *unmixed* Fock — `½Tr[P_mixed(H + F(P_in))]` — and inconsistent with the
715
+ `total_origin` the same iteration built from `P_in`. At the fixed point all of them agree, so
716
+ the converged number was right; what it corrupted was `de`, half the convergence test, which
717
+ measured the mixer as much as the iteration. Moved above the mix.
718
+
719
+ And even then, `E[P] = ½Tr[P(H + F(P))]` is stationary **only on the idempotent manifold**,
720
+ which a mixed input is not on — a Pulay step is a signed combination of past densities and can
721
+ sit further off idempotency than its distance to the fixed point suggests. Evaluating there
722
+ leaves a *first-order* error that the energy hides and anything differenced does not. Once the
723
+ tolerances are met the solver now spends one further pass at the converged **output** density,
724
+ which is idempotent, so the returned energy is the variational energy of the returned density.
725
+ Measured on a water dimer: a central-differenced total energy against the analytic gradient
726
+ went **1.20e-6 → 3.04e-7** eV/Bohr, now slightly better than plain mixing rather than four
727
+ times worse.
728
+
729
+ The three share a lesson about ordering. A defect that only costs iterations hides a defect that
730
+ costs correctness, because both present as "it needs more iterations" — and the unconverged runs
731
+ reported energies differing by up to **0.6 eV** between k-meshes, every one of them plausible.
732
+ `tests/pbc_scf_convergence.rs` pins all three, and `AM1_SCF_TRACE=1` prints `dE` and `dP` per
733
+ iteration, which is the only way to tell a slow contraction from a stall from the outside.
734
+
735
+ - **A performance test asserted on wall clock and failed under load.**
736
+ `factoring_project_ov_lowers_its_scaling_exponent` claimed a drop from `O(nao⁴)` to `O(nao³)` and
737
+ checked it by requiring the measured *speedup ratio* to grow with `nao` — on the reasoning that a
738
+ ratio divides the machine's load out. It does not: both halves are wall clock, and the largest
739
+ case carries the most memory traffic and so suffers most. Idle it measured 54/108/357, 46/137/385
740
+ and 48/94/376 across three runs; with a build running alongside it measured 61/258/**46** and
741
+ failed. An exponent is a count of operations, not a duration, so the count is now what is
742
+ asserted — 10.7× → 21.3× → 42.7×, exactly doubling per doubling of `nao`, which *is* the missing
743
+ power — and the timings are printed with a load-proof 5× floor beneath them. Renamed to
744
+ `factoring_project_ov_lowers_its_operation_count`, which is what it now measures. Same rule
745
+ `DcResult::diagonalization_work` already followed.
746
+
747
+ - **AM1-BCC atom typing was wrong in ten places, and silently.** The typing was a hand
748
+ transcription of antechamber's `ATOMTYPE_BCC.DEF` into Rust `match` arms, and it had drifted from
749
+ the file. Every case is silent, because a parameter exists for the wrong type too — no warning
750
+ fires, a plausible number comes back.
751
+
752
+ The errors are counted **per atom**, not per bond, and that distinction is the point: a
753
+ correction is looked up on the *pair* of types, so changing one atom's type changes **every bond
754
+ at that atom**.
755
+
756
+ | group | file says | 0.2.1 said | bonds affected | error on that atom |
757
+ |---|---|---|---|---|
758
+ | nitro N | 23 | 21 | N–O ×2, C–N | **0.6745 e** |
759
+ | 3-coordinate P with a double bond | 42 | 41 | P=O, P–C ×2 | 0.44 e |
760
+ | pyrrole-type α carbon | 16 | 17 | C–N, C–C, C–H | 0.315 e |
761
+ | ester / carboxylic acid carbonyl O | 32 | 33 | C=O | 0.086 e |
762
+ | ketone / aldehyde carbonyl O | 31 | 32 | C=O | 0.050 e |
763
+ | amide carbonyl O | 31 | 33 | C=O | 0.036 e |
764
+
765
+ plus four nitrogen rules with no counterpart at all (a three-coordinate N with a double bond, a
766
+ two-coordinate amide N, a two-coordinate N with two single bonds, and a four-coordinate aromatic
767
+ N, which the file types 21 because `21 * 7 4 &` precedes the aromatic rule).
768
+
769
+ The cause was not a set of typos. It was that the *conditions* had been dropped: both `33` rules
770
+ carry `[RG]` and apply to lactones and lactams only, but the code looked at no ring and instead
771
+ counted the oxygens on the carbon; the `32` rule asks whether the carbon bears a **two-connected**
772
+ oxygen; the `17` rule requires an aromatic **`N2`**, and pyrrole's nitrogen has three connections;
773
+ and there is a nitro rule, `(O1,O1)`, that the transcription simply did not contain.
774
+
775
+ **The fix reads the file.** `src/bcc/atomtype.rs` parses `ATOMTYPE_BCC.DEF` and evaluates it —
776
+ `WILDATOM` expansion, atom properties, nested chemical-environment patterns with their `'`
777
+ bond-to-predecessor suffix, and the file's own top-to-bottom first-match-wins order, which its
778
+ closing note says is crucial and which is exactly what the four missing nitrogen rules turned on.
779
+ The rules are now antechamber's rather than a reading of them.
780
+
781
+ **This changes published charges** for every molecule with a carbonyl, a nitro group or a pyrrole
782
+ ring. The 0.2.1 values for those were wrong.
783
+
784
+ Two supporting changes were needed and are worth naming separately:
785
+
786
+ - **A Kekulé assignment** (`Topology::kekule_double`). `[2sb]` and `[sb,db]` separate nitrogen
787
+ types 21 and 24, so an aromatic ring bond has to be formally single *or* double, and which one
788
+ is not a local property. The parameter file settles the intent: it carries a `17–24` entry at
789
+ bond type 7, and since the `17` rule itself requires an aromatic two-connected nitrogen
790
+ neighbour, that pair is pyridine and nothing else. Found by perfect matching over the atoms
791
+ contributing one π electron.
792
+ - **The indole rule** from the definition file's own closing note: a five-membered ring sharing
793
+ an edge with a six-membered aromatic ring is not aromatic for AM1-BCC.
794
+
795
+ What is *not* needed, on measurement: the AR1..AR5 sub-classification, which earlier notes listed
796
+ as a gap. Every rule in this file asks for the union `[AR1.AR2]` and none asks for either class
797
+ alone, so splitting them would be machinery with no consumer.
798
+
799
+ - **An H–H or halogen–halogen bond reported itself as uncorrected.** `BCCPARM.DAT` has a bond type
800
+ for a pair with the *same atom type on both ends* — code 11, whose 26 entries are all `X–X` and
801
+ all exactly 0.0 — and ten of those types, hydrogen and every halogen among them, have **no
802
+ single-bond entry at all**. Emitting only codes 1, 2, 3, 6, 7 and 9 therefore left H₂, F₂, Cl₂,
803
+ Br₂ and I₂ with no parameter for their one bond, and `BccResult::warnings` said the bond was left
804
+ at its raw Mulliken charges. The charges were in fact right — the correction is zero — but the
805
+ warning is the thing callers are told to check, and "a molecule that returns no warnings is one
806
+ the rules covered" has to mean something.
807
+
808
+ All nine bond types are emitted now. Types 7 (aromatic single), 8 (aromatic double) and 10
809
+ (aromatic, no resolved order) are separated by the Kekulé structure the atom typing already
810
+ needs: a six-membered aromatic has two equivalent Kekulé structures, so none of its bonds is
811
+ *the* double bond and they take 10, while a five-membered heteroaromatic has a unique one and
812
+ takes 7 and 8. Type 11 is a fallback, not an override — where the ordinary code is tabulated it
813
+ wins, which `the_same_type_code_does_not_pre_empt_a_tabulated_one` pins.
814
+
815
+ **No charge moves**: 8 and 10 are byte-identical to 7 on every shared pair and every type-11
816
+ value is zero, both asserted against the parameter file rather than recited.
817
+
818
+ - **The antechamber licence was not in the repository, the crate, or the wheel.** `BCCPARM.DAT`
819
+ (GPL-3) is `include_str!`-ed into every binary, and `ATOMTYPE_BCC.DEF` now is too, while MOPAC's
820
+ and PySEQM's licences were retained and antechamber's was not. The CI job that checks
821
+ "third-party licences are inside the wheel" did not catch it because it asserted only that the
822
+ list was non-empty — two of three passed that. It now compares against the number of
823
+ `third_party/` subdirectories, so a fourth bundled work cannot repeat this, and the sdist check
824
+ additionally requires `ATOMTYPE_BCC.DEF`, which the build now needs. `third_party/antechamber/`
825
+ gains the GPL-3 text and a `README.md` recording which upstream file each copy is and where it
826
+ came from. `THIRD_PARTY_NOTICES.md` §4 and §6 are rewritten, including what "or later" does and
827
+ does not say for this material.
828
+
829
+ - **The lazy `get_*()` cache was never invalidated by a geometry change.** `docs/scope.md` claimed
830
+ these methods "cache into `results` and are invalidated by `check_state`". The first half was
831
+ true and the second was not: `results` is cleared by `Calculator.get_property`, which calls
832
+ `check_state` and then `reset()` — and none of the lazy methods go through `get_property`. So
833
+
834
+ ```python
835
+ f1 = atoms.calc.get_frequencies(atoms)
836
+ atoms.positions += 0.5
837
+ f2 = atoms.calc.get_frequencies(atoms) # returned f1
838
+ ```
839
+
840
+ with nothing to announce it. The existing test could not catch it because it never moved the
841
+ geometry between calls. They now memoize into their own store keyed on the geometry's **bytes**,
842
+ the resolved state (including the `atoms.info` overrides ASE's own comparison cannot see) and
843
+ their own arguments; `tests/test_lazy_cache.py` asserts every method in the family against a
844
+ displacement, and asserts that it is still a cache.
845
+
846
+ - **`AM1.optimize(apply=True)` cleared `results` but left `self.atoms`** holding the
847
+ pre-optimization geometry. It calls `reset()` now.
848
+
849
+ - **The charged-cell warning described a version of the code that no longer existed.** It said no
850
+ compensating background is applied "because Ewald summation is not implemented", that "THE TOTAL
851
+ ENERGY IS NOT CONVERGED", and quoted a −331 eV to +72 eV swing across real-space cutoffs. Ewald
852
+ has been implemented since 0.2.0, in all three dimensionalities, and is on by default — those
853
+ were the pre-Ewald numbers. It was telling users their converged 3D energies were meaningless,
854
+ through every surface (a `RuntimeWarning` in ASE and a line in both CLIs).
855
+
856
+ Measured rather than reasoned about (`tests/charged_cell_warning.rs`): a +1 water cell in an 8 Å
857
+ cube across a 6.5× range of cutoff moves **0.197 eV with Ewald and 403.4 eV without**. The
858
+ warning is now per-dimensionality — in 3D the tin-foil sum defines the energy and the residual is
859
+ the `R⁻³` tail; in 1D/2D the monopole sum is applied but the neutralizing background's placement
860
+ is a *convention* (`SheetConvention` / `AxisConvention`) that nothing in the SCF path consults,
861
+ so the absolute energy is not defined there. Both texts are ASCII, for the cp932/C-locale reason
862
+ 0.2.1 recorded.
863
+
864
+ - **The phonon spectrum was not reproducible.** `ForceConstants::blocks` is a `HashMap`, and three
865
+ float sums iterated it directly: the Bloch sum `D(q) = Σ_T Φ(T) e^{iq·T}`, the acoustic-sum-rule
866
+ residual, and the acoustic-sum-rule *correction*. Rust seeds each `HashMap` instance from a
867
+ thread-local counter, so two maps built from the same insertions in the same process iterate in
868
+ different orders — and floating-point addition is not associative.
869
+
870
+ Measured: five identical `lo_to_frequencies` calls in one process, on a water crystal in a 4.5 Å
871
+ cube, agreed on four and differed by **1798 cm⁻¹** on the fifth — one O–H stretch collapsing into
872
+ a near-zero mode. The periodic SCF underneath was bit-identical every time (same energy to the
873
+ last digit, same 115 iterations), which is what located it in the phonon assembly rather than the
874
+ electronic structure.
875
+
876
+ The correction is the one that mattered: it is *subtracted* from the on-site block, so an
877
+ order-dependent value there changes `Φ` itself and every `D(q)` built from it afterwards. The
878
+ other two stayed within last bits.
879
+
880
+ All three now iterate a translation-sorted view. `tests/phonon_determinism.rs` asserts
881
+ **bit-identical** repeats rather than a tolerance — the claim is that the same input gives the
882
+ same output, which is a property of the code, not of any crystal's conditioning. Found because
883
+ `test_lo_to_frequencies_splits_and_matches_across_surfaces` failed intermittently and only in a
884
+ full-file run; the ASE and native paths it compares call the same function, so the disagreement
885
+ could not have been between them.
886
+
887
+ - **`LongRangeMonopole::for_molecule`'s documentation contradicted its code**, saying the
888
+ correction applies "only to a fully three-dimensional cell" while the code accepts any
889
+ `n_periodic() >= 1` and dispatches to the 1D and 2D kernels. Only the *phased* (DFPT) path is 3D
890
+ only. Corrected in the docstring and in `docs/scope.md`.
891
+
892
+ ### Added
893
+
894
+ - **`ε_∞` for a slab or a chain**, once the caller says how thick the material is:
895
+ `pbc::dielectric_tensor_with_extent`, `am1_rs.dielectric_with_extent`,
896
+ `AM1.get_dielectric_tensor_with_extent`. `ExtentConvention::SlabThickness` (Bohr) or
897
+ `WireCrossSection` (Bohr²) is **required and never defaulted** — a supercell says where the atoms
898
+ are, not where the material stops, and every choice changes `ε`. Same rule as `chain_radius` and
899
+ `AxisConvention`.
900
+
901
+ **It is not a division.** The `α` this crate computes is the response to the *external* field —
902
+ the induced charges interact through the same Coulomb operator the SCF uses, so for a slab
903
+ polarized along its normal the depolarizing field is already inside `α`. The conversion therefore
904
+ carries the depolarization factor of the assumed body,
905
+ `ε = 1 + 4πχ/(1 − 4πNχ)` with `χ = α/(measure · extent)`: `N` = 0 in a slab's plane and along a
906
+ wire's axis, 1 along a slab normal, ½ transverse to a wire's circular section. Three-dimensional
907
+ tin-foil summation removes the macroscopic depolarizing field, so `N = 0` there — which means the
908
+ same arithmetic reproduces `dielectric_tensor` rather than sitting beside it, measured at 1e-13.
909
+
910
+ Getting that factor backwards returns a plausible number: both laws are positive and monotonic in
911
+ `α`. What separates them is a **sign asymmetry** in the response itself, and
912
+ `tests/pbc_dielectric_extent.rs` measures it — tightening a 2D methane lattice from 14 Å to 6.5 Å
913
+ moves `α_xx` up (8.293 → 8.439 Bohr³) and `α_zz` down (8.230 → 7.920), which is what a sheet of
914
+ induced dipoles does and what an internal-field response would not show at all.
915
+
916
+ The thickness is a choice, so `ε` is a choice; two combinations are not, and are returned
917
+ alongside it: `(ε_∥ − 1)d = 4πα_∥/A` and `(1 − 1/ε_⊥)d = 4πα_⊥/A`, half the first being the
918
+ Rytova–Keldysh screening length. Read the other way they are capacitor stacking — parallel and
919
+ series — so the two formulas are forced rather than chosen once the thickness is named, which is
920
+ a second derivation and is tested as one. The first must also equal what `dielectric_function`
921
+ reaches through a reciprocal-space Coulomb kernel: measured ratio **2.0000000000**. And `ε` does
922
+ not move when only the vacuum padding changes, which is precisely what 0.2.0 got wrong.
923
+
924
+ Eleven component tests sit next to the arithmetic in `src/pbc/extent.rs`, where they can use a
925
+ synthetic `α`, because the conversion is the model-dependent step and deserves to be checked
926
+ without an SCF in the way.
927
+
928
+ - **`native.vibrations`** — the Hessian, frequencies, normal modes, atomic polar tensor,
929
+ intensities and orbital response from **one** SCF and one CPHF solve. `hessian`, `frequencies`,
930
+ `ir_spectrum`, `dipole_derivatives` and `orbital_response` each ran the whole analytic-Hessian
931
+ solve and kept a different contraction of it, so a caller wanting a spectrum *and* the Hessian it
932
+ came from — the ordinary case — paid for the CPHF once per question. The ASE calculator routes
933
+ all five through it, and `tests/test_lazy_cache.py` asserts that the family leaves exactly one
934
+ entry in the cache. The five original functions are unchanged.
935
+
936
+ - **A Barnes–Hut far field**, so the NDDO Coulomb is no longer `O(N²)`. `docs/scope.md` recorded
937
+ "linear-scaling Coulomb ⛔ — stays `O(N²)` by construction", which was true: `FarField` keeps the
938
+ interaction in full and simplifies only its *shape*, so the prefactor fell a hundredfold and the
939
+ exponent did not move. `FarField::tree(theta)` moves it: fitted **1.65 against 2.13** over 24 to
940
+ 1029 atoms, with 131 515 partner evaluations against 1 043 490 at the top — an 8× reduction that
941
+ grows with size.
942
+
943
+ Each accepted cluster becomes **two** pseudo-atoms, the positive and negative charge at their own
944
+ centroids. One would be a monopole expansion, and a monopole expansion is worthless here: the
945
+ clusters are made of neutral molecules, so the net charge is near zero and the interaction is
946
+ dipolar. The first draft did exactly that and the error against the direct sum was 64 % and did
947
+ not shrink with the acceptance angle — there was no monopole for the angle to resolve. Splitting
948
+ by sign carries the dipole while keeping the property that makes the design safe: every consumer
949
+ evaluates the *ordinary pair kernel* against a shorter list, so the potential, the gradient and
950
+ the virial cannot drift apart the way three separately truncated expansions would.
951
+
952
+ At `theta = 0` the tree visits **exactly** the pairs the direct sum does — asserted as an
953
+ equality on the count — and agrees to 5.3e-15, the residual being summation order. In between the
954
+ error is monotone in `theta`: 2.7 % at 0.8, 0.3 % at 0.05.
955
+
956
+ **Opt-in**, because an acceptance angle makes the energy a discontinuous function of the geometry
957
+ where an atom crosses the boundary. The jump is of the order of the truncation error, but it is a
958
+ jump, and molecular dynamics should either leave it off or accept it knowingly.
959
+
960
+ - **Berry-phase polarization** (`pbc::berry`), the modern theory of polarization. Listed as ⛔
961
+ through 0.2.1 — "`ε_∞` is the clamped-ion dipole response, not a Berry phase" — which was accurate
962
+ and was a gap: the dipole of a periodic cell is not a property of the crystal, so the crate had
963
+ polarization's *second* derivative and not polarization.
964
+
965
+ `P_el = (e/Ω) Σ_α a_α · Im ln Π_j det S(k_j, k_{j+1})/2π` over strings of k points, with the
966
+ occupied-manifold overlap in this basis being `S_mn = Σ_μ c*_{μm}(k) e^{−ib·τ_μ} c_{μn}(k+b)` —
967
+ the `e^{−ib·τ_μ}` being the same "an orbital sits at its atom" approximation the dipole operator
968
+ already makes. Returned modulo the polarization quantum, with `BerryPolarization::difference`
969
+ reducing two values to a common branch, because subtracting absolute polarizations is the
970
+ standard way to be wrong by exactly one quantum.
971
+
972
+ **The sign was derived rather than looked up**, sources differing on the convention: for a single
973
+ electron whose only orbital sits at `τ`, every link contributes `e^{−ib·τ}` and the string product
974
+ is `e^{−iB·τ}`, so `φ = −τ_α/a_α` in turns; that electron is a charge `−1` at `τ`, which fixes the
975
+ prefactor to `+e/Ω`. The first draft had it negative and the acoustic sum rule found it at once —
976
+ the Born charges summed to `+2 n_elec` instead of zero.
977
+
978
+ Validated four ways, none of which compares `P` to a number (an absolute polarization is not a
979
+ physical prediction): translating the cell by a lattice vector leaves it unchanged **exactly**, a
980
+ centrosymmetric cell gives zero to 8.5e-18, the phase converges to 4.0e-8 by 32 points per string,
981
+ and — the sharp one — `Ω ∂P/∂τ_A` reproduces the **Born effective charges** the CPHF dipole
982
+ response produces, two formalisms sharing only the SCF.
983
+
984
+ That last comparison differs by 0.207 e on hydrogen fluoride, and the reason is *measured* rather
985
+ than asserted: the dipole operator additionally carries the on-site `s`–`p` hybridization moment
986
+ `dd`, which this basis's Berry phase does not. On a **hydrogen-only** cell, where hydrogen has no
987
+ `p` shell and the `dd` term is structurally unreachable, the two routes agree to **7.5e-13 e**.
988
+
989
+ - **The long-range monopole term in the DFPT response, in 1D and 2D.** 0.2.1 shipped it for 3D
990
+ cells and named its absence on a chain or a slab as the release's one unfinished item:
991
+ `LongRange::Require` was an error there, and `Auto` silently dropped the channel. Both
992
+ dimensionalities now have a phased kernel, each the `q`-shifted form of the machinery its
993
+ unphased sum already used:
994
+
995
+ - **2D** — Parry's slab sum over the **full shifted in-plane set** with prefactor `π/(A|k|)`,
996
+ `k = G − q`. There is no ±G folding to exploit once `q ≠ 0`, and at `q = 0` the full set with
997
+ `π` reproduces the folded half set with `2π` — which is what makes a wrong factor of two here
998
+ visible to the splitting-parameter test and almost nothing else.
999
+ - **1D** — the chain's direct summation, phased image by image, with the truncated tail summed by
1000
+ **repeated Abel transformation**. Truncating the oscillating sum directly is only `O(1/N)`:
1001
+ Dirichlet converges it because the partial sums of `e^{iθn}` are bounded by `1/(2|sin(θ/2)|)`,
1002
+ but that bound multiplies the first neglected term and blows up as `q → 0`. Summation by parts
1003
+ trades it for a series over exact forward differences of the kernel, truncated at its smallest
1004
+ term.
1005
+
1006
+ Both delegate to their unphased counterpart where `q` is a reciprocal lattice vector — not where
1007
+ `q = 0`, which is the silent version of the same test — so the neutralizing background, the sheet
1008
+ term and the chain's line charge each keep exactly one derivation.
1009
+
1010
+ Validated against a **direct lattice sum**, Cesàro-averaged to damp the conditionally convergent
1011
+ boundary term (without which the oracle is less accurate than the thing it checks): 1D agrees to
1012
+ **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
1013
+ the internal ones a wrong prefactor cannot survive — the slab sum is independent of the splitting
1014
+ parameter to **8.9e-16** across a 2.8× range, the chain sum independent of its explicit image
1015
+ count to **7.2e-16** across a 6× range — plus `S(−q) = S(q)*`, periodicity in `q`, and derivatives
1016
+ against finite differences to 8e-12. On a polar HF chain the term moves `D(q)` by 3.9e-5 eV/Bohr²,
1017
+ so it is doing something rather than merely running.
1018
+
1019
+ **What `q → 0` does, corrected.** An earlier draft of this work recorded "2D is discontinuous at
1020
+ Γ". That conflated two levels. The *kernel* diverges in every dimensionality — `4π/(Vq²)`,
1021
+ `2π/(A|q|)`, `−(2/L)ln|q|`, all three measured here — but the contribution to `D(q)` carries two
1022
+ factors of `q` from charge conservation, so only **3D** is left with a finite direction-dependent
1023
+ limit. 2D goes as `O(|q|)` and 1D as `q² ln(1/q)`: both continuous at Γ, with a non-analytic
1024
+ approach. There is no LO–TO splitting at Γ in 2D, only a linear kink.
1025
+
1026
+ - **Divide-and-conquer open-shell analytic stress.** Refused through 0.2.1 for want of a
1027
+ spin-resolved pair virial. `electronic_gradient_and_virial_fixed_density_spin` is the restricted
1028
+ loop with the exchange coefficient reading `Pα`/`Pβ` instead of half the total, and returns the
1029
+ virial alongside the gradient from one pass for the same reason the restricted one does.
1030
+
1031
+ Validated two ways, because either alone would pass for the wrong reason: forced UHF on a closed
1032
+ shell reproduces the restricted stress to **2.5e-14** (different code, algebraically identical
1033
+ answer), and a neutral triplet chain matches a strain finite difference to **1.9e-8 eV/Bohr³**.
1034
+ The finite difference is reported across three step sizes rather than one, and shows the V a
1035
+ correct derivative makes: 1.1e-5 at `h = 1e-6` where the SCF's own convergence dominates,
1036
+ 1.9e-8 at `1e-5`, 2.2e-2 at `1e-4` where harmonic truncation does.
1037
+
1038
+ - **The `R⁻³` Klopman–Ohno tail beyond the pair list is summed.** `Am1Options::klopman_ohno_tail`
1039
+ and `PbcOptions::klopman_ohno_tail`, default `true`; `false` restores 0.2.1.
1040
+
1041
+ `ewald` made the `1/R` channel exact, but NDDO's kernel is `γ_η(R) = 1/√(R² + η²)`, and
1042
+ `γ_η − 1/R = −η²/2R³ + …` was left truncated at the cutoff. `Σ_T |T|⁻³` diverges logarithmically
1043
+ in three dimensions, so the total energy drifted with `realspace_cutoff` and converged to
1044
+ nothing. `docs/scope.md` recorded it as "⛔ real-space; logarithmically divergent, 0.10 eV per
1045
+ unit `ln r_c`".
1046
+
1047
+ The translations the pair list dropped are now summed **explicitly**, out to three cutoffs, using
1048
+ the exact `γ_η − 1/R` and not its expansion — the sum depends on the pair only through
1049
+ `η_ab = ρ_a + ρ_b`, so it costs one lattice sum per *element* pair. Past that a continuum
1050
+ remainder takes over through a quintic taper, and its integrand is per-dimensionality. Only the
1051
+ three-dimensional remainder carries a logarithm, and only there is a reference length needed;
1052
+ `Σ_T |T|⁻³` converges outright in 1D and 2D.
1053
+
1054
+ Measured on a +1 water cell over a 6.5× range of cutoff, the residual per unit `ln r_c` went from
1055
+ **−0.118, −0.098, −0.097 eV** — constant, which is what identifies it as the logarithm — to
1056
+ **0.000, −0.000, −0.000**, and the energy spread from 0.197 eV to 6e-5 eV. Forces move by 0.08 %
1057
+ of their scale, which is the density shifting under the Fock diagonal the tail adds; the stress
1058
+ matches its strain finite difference to 6.9e-9 eV/Bohr³.
1059
+
1060
+ Two things went wrong on the way and are worth recording, because both are invisible in a
1061
+ passing test:
1062
+
1063
+ - **The first draft applied the three-dimensional formula in every dimensionality**, and moved a
1064
+ charged chain's energy by 3e-2 eV. This is the same error `docs/scope.md` already records for
1065
+ `ε_∞` and LO–TO, committed a second time in the same file.
1066
+ - **The response was left without it** while the ground state had it, which is the response of a
1067
+ Hamiltonian the SCF never converged. It showed up as `D(q = 0)` missing the `q = 0` Hessian by
1068
+ 4.6e-4 eV/Bohr² — two numbers that are the same number. The tail is now carried through
1069
+ `solve_bands` and the DFPT response kernel; the *cutoff-dependent* part of the tail is
1070
+ `−(4π/V) ln r_c`, which does not depend on `q`, so the same constant is correct at every `q`.
1071
+
1072
+ - **`tests/dc_open_shell_stress.rs` was passing on a coincidence, and now measures something.** Its
1073
+ finite difference used a **triplet water chain**, whose energy is not a smooth function of strain
1074
+ at all: it jumps in quanta of about 1.7e-5 eV as an occupation switches at the Fermi level, which
1075
+ a triplet built from closed-shell waters invites. The quoted 1.9e-8 eV/Bohr³ agreement was
1076
+ `E(+h)` and `E(-h)` happening to land on the same branch. Perturbing the Hamiltonian at the 1e-8
1077
+ level — all the Klopman–Ohno tail does there — moved that "agreement" to 1.0e-1.
1078
+
1079
+ The fixture is now a **methyl-radical chain**: a doublet with one well-separated singly-occupied
1080
+ orbital, whose energy over the same strain sweep is linear to eight figures. The finite
1081
+ difference now shows a real V — 1.07e-5, **2.41e-9**, 4.53e-8 across `h = 1e-6, 1e-5, 1e-4` —
1082
+ so the minimum is a converged derivative and not a slope. A finite difference quoted at one step
1083
+ size cannot tell those apart.
1084
+ - **The k-point periodic response handles open shells.** `pbc_hessian`, `born_charges` and the
1085
+ CPHF behind them accept an unrestricted ground state; 0.2.1 refused with "the k-point periodic
1086
+ response is restricted-only". This is the one item of the 0.2.2 list with no sibling crate to
1087
+ port from — pm6-rs and pm7-rs refuse in the same place.
1088
+
1089
+ The restricted path solves one CPHF; this solves two, **coupled**, because the kernel is
1090
+ `G^σ(ΔP) = J(ΔP_tot) − K(ΔP_σ)` and α reads β's response density through the Coulomb half.
1091
+ Solving the channels independently would drop `J(ΔP_β)` from `G^α` and return a plausible
1092
+ number. Three factor conventions move with it — what one orbital holds (2 restricted, 1 per
1093
+ channel), the exchange weight in both the skeleton and the perturbed Fock, and the relaxation
1094
+ term's 4 becoming 2 per channel.
1095
+
1096
+ Forcing UHF on a **closed** shell reproduces the restricted answer to **8.9e-16** eV/Bohr² on a
1097
+ 3-point mesh, and the Born charges exactly. A genuine doublet chain matches a finite difference
1098
+ of the analytic gradient to 5.2e-7 of 15.9. The first of those is the sharp check: on a closed
1099
+ shell `P^α = P^β = P/2` makes the two algebraically identical, so any one of the three factors
1100
+ being wrong breaks it loudly.
1101
+
1102
+ It found one such break immediately, and it is worth naming because it fails **silently**: the
1103
+ occupied/virtual classification tested each level's occupation against a hard-coded `2.0`. On
1104
+ the unrestricted path a full level holds 1, so no level was ever classified occupied, `n_ov` was
1105
+ zero at every k, and the entire orbital-relaxation term vanished — 74 % of the force constants,
1106
+ with no error raised.
1107
+
1108
+ Two things deliberately stay restricted and now say so rather than being answered with the
1109
+ restricted equations: **DFPT at finite `q`** (a larger machine — band pairs across `k` and
1110
+ `k + q` weighted by occupation differences) and the **field response** behind `ε_∞` and the
1111
+ polarizability, which is already three-dimensional-only.
1112
+
1113
+ - **The CLI printed `-0.0` for a rigid-body frequency**, and the Rust and Python front ends
1114
+ disagreed about which side of zero it fell on. Both are numerically zero; the sign of a value
1115
+ below the print precision is not information, but printing it made `tests/test_cli.py` compare
1116
+ the two front ends' last bits. Both now print `0.0`.
1117
+ - **An external electric field works under periodic boundary conditions**, when it is orthogonal to
1118
+ every lattice vector. `PbcOptions::electric_field`, and `Am1Options::electric_field` no longer
1119
+ refuses a cell outright.
1120
+
1121
+ 0.2.1 rejected any field under any cell, with the reason "`F·R` is unbounded along a periodic
1122
+ direction". The reason is right and the rule drawn from it was too broad: `F·R` shifts by `F·T`
1123
+ under translation by `T`, so the perturbation repeats with the lattice **exactly when
1124
+ `F·T = 0` for every lattice vector**. A slab in a field along its normal and a chain in a
1125
+ transverse field satisfy that and are ordinary calculations; they were being refused along with
1126
+ the ill-defined case.
1127
+
1128
+ The check is now on the direction and names the offending component when it fires. Measured: the
1129
+ periodic gradient in a transverse field matches a finite difference of the periodic energy to
1130
+ **8.8e-8** eV/Bohr, and a water molecule in a 60 Bohr cell with a field along a non-periodic axis
1131
+ reproduces the isolated-molecule path to **5.0e-6** eV — two code paths sharing only
1132
+ `crate::dipole`, one number.
1133
+
1134
+ **Not** done, and named so it is not mistaken for done: a finite field *along* a periodic
1135
+ direction. That needs the Berry-phase electric enthalpy `E − Ω F·P`, whose field term couples
1136
+ neighbouring k-points through `S⁻¹` and therefore requires the SCF to solve its k-points
1137
+ together rather than one at a time. The **linear** response along a periodic direction is
1138
+ available and validated — `dielectric_tensor` / `ε_∞` through the CPHF — so what is missing is
1139
+ the non-linear regime and finite-field geometry optimization. The polarization half of the
1140
+ machinery already exists (`pbc::berry`, new in this release).
1141
+ - **A finite electric field along a periodic direction**, by the Berry-phase electric enthalpy.
1142
+ `pbc::run_finite_field`.
1143
+
1144
+ `F·R` is unbounded there, so there is nothing to fix about it: what replaces it is Nunes and
1145
+ Gonze's `E − Ω 𝓔·P`, minimized instead of the energy, with `P` the Berry phase rather than `⟨r⟩`.
1146
+ Its derivative with respect to the orbitals is built from overlaps between **neighbouring k
1147
+ points**, so the k points can no longer be solved one at a time — the SCF gained a `pub(crate)`
1148
+ entry point taking a k-resolved additive operator, and an outer loop refreshes it until it stops
1149
+ moving.
1150
+
1151
+ The coupling constant is derived from this crate's own polarization convention rather than
1152
+ quoted, because the conventions differ between sources and a wrong factor here does not fail —
1153
+ it returns a plausible polarizability. **What says it is right** is that `α = Ω ∂P/∂𝓔` by finite
1154
+ differences matches the **CPHF** polarizability, two formalisms sharing only the SCF: on a
1155
+ hydrogen-only cell they agree to **0.03–0.47 %**, and the residual falls as `O(1/J²)` with the
1156
+ string length (1.06 → 0.47 → 0.26 % for J = 4, 6, 8). It caught the one real error on the way:
1157
+ the first draft symmetrized the field operator as `(M + M†)/2`, which halves the
1158
+ occupied–virtual coupling — the whole of the response — and gave 0.56 of the CPHF value. The
1159
+ construction that is both Hermitian and faithful is `A = H − ½PHP` with `H = M + M†`.
1160
+
1161
+ **The comparison is exact only where the two compute the same object.** On a p-block cell they
1162
+ differ by 12 %, and that is the Berry phase's own limitation, not the field's: in an atom-centred
1163
+ minimal basis the phase tracks the charge *centres* and carries no `dd`, the on-site moment
1164
+ between an `s` and a `p` on the same atom. `pbc::berry` already records the same gap for the Born
1165
+ charges (0.207 e on HF, 7.5e-13 e with no p orbitals). A planar cell's out-of-plane response from
1166
+ this path is **exactly zero**, because there that moment is the whole of it — recorded as a test
1167
+ rather than left as a surprise.
1168
+
1169
+ 3D, restricted, no smearing, and at least three k points along any direction the field has a
1170
+ component in.
1171
+
1172
+ Reachable from all three surfaces: `pbc::run_finite_field`, `am1_rs.finite_field`, and
1173
+ `AM1.get_finite_field` (which takes **V/Å** like the rest of the ASE layer and converts with the
1174
+ crate's own constants). **Berry-phase polarization**, added earlier in this release, was
1175
+ Rust-only until now and gained the same three — `am1_rs.polarization` and `AM1.get_polarization`
1176
+ — which is what the project's own native↔ASE parity rule asks for and what
1177
+ `tests/test_new_api_0_2_1.py` enforces.
1178
+
1179
+ - **The open-shell k-point response now covers DFPT at finite `q` and the dielectric response.**
1180
+ Both refused earlier in this release's own notes; both go through the same two coupled spin
1181
+ channels as the `q = 0` Hessian, from one shared split of the density
1182
+ (`pbc::scf::spin_channel_densities`) so the three cannot disagree about it.
1183
+
1184
+ Forcing UHF on a closed shell reproduces `D(q = 0.3)` on a 4-point mesh to **6.1e-9** eV/Bohr²,
1185
+ and gives the restricted `α` and `ε_∞` back **exactly**. A genuine doublet chain's DFPT
1186
+ `D(q = 0)` matches the open-shell `q = 0` Hessian to **1.3e-7** of 15.9 (8.2e-9 relative) — the
1187
+ same number by two different machines, each running two coupled channels. An open-shell radical
1188
+ in a 12 Å box has the isolated radical's finite-field polarizability to **0.41 %**, against 0.17 %
1189
+ for the restricted analogue at the same box size.
1190
+ - **The Berry phase carries the on-site `s`–`p` moment.** It tracked only the charge *centres*
1191
+ until now, and that was the single largest reason the Berry route and the CPHF route disagreed.
1192
+
1193
+ The link operator `Λ_{μν} = ⟨χ_μ| e^{−i b·r} |χ_ν⟩` was the diagonal `e^{−i b·τ_μ}`: each orbital
1194
+ treated as a point at its own atom. The exact same-atom block, which is all NDDO keeps, is
1195
+ `e^{−i b·τ_a}` times `exp(−i b·D^a)` with `D^a_{μν} = ⟨χ_μ|(r − τ_a)|χ_ν⟩` — and in a minimal
1196
+ `sp` basis that is exactly the `dd` [`crate::dipole::dipole_operator`] already puts on the
1197
+ `(s, p_α)` elements. Both now read it from the same parameter. `b·D^a` is a rank-two operator, so
1198
+ its exponential is a rotation in the `(s, u)` subspace and is available in closed form;
1199
+ exponentiating rather than truncating at `I − i b·D` keeps `|det Λ| = 1`, so the string's product
1200
+ drifts only in phase.
1201
+
1202
+ Measured three ways:
1203
+
1204
+ | | before | after |
1205
+ |---|---|---|
1206
+ | Born charges vs CPHF, HF cell | 0.207 e | **1.2e-3 e** at 8 points per string, falling as `O(1/J²)` |
1207
+ | finite-field `α` vs CPHF, water crystal | 12 % | **0.05 %** |
1208
+ | `α_zz` of a planar cell, which is *entirely* this moment | **exactly 0** | 0.25527 against the CPHF's 0.25564 |
1209
+
1210
+ The planar case is the sharpest: with a diagonal `Λ` the `z → −z` mirror made the occupied bands
1211
+ parity eigenstates and the link overlaps block-diagonal in that parity, so the field operator
1212
+ could not mix them and the out-of-plane response was identically zero. It is the on-site moment
1213
+ that couples `s` to `p_z`, so a wrong sign there would have moved it to the wrong number rather
1214
+ than merely scaling it.
1215
+
1216
+ A second finding came out of the same comparison: the old 0.207 e was **not** all on-site moment.
1217
+ `tests/pbc_berry.rs` compared a Γ-only CPHF against a 12-point string — two different samplings
1218
+ of the Brillouin zone — and read the difference as physics. With the sampling matched the
1219
+ residual is 1.2e-3 and converging. The test now matches them and asserts the convergence.
1220
+
1221
+ - **The polarizability is available for a chain and a slab.** `pbc::polarizability`,
1222
+ `am1_rs.polarizability`, `AM1.get_polarizability`.
1223
+
1224
+ `dielectric_tensor` was the only entry point and refused a reduced-dimensional cell — correctly,
1225
+ for the `ε_∞ = 1 + 4πα/Ω` step, which needs `Ω` to be a volume — but it took `α` down with it.
1226
+ `α` is a *response*, and a response is well defined whatever the cell is periodic in: the origin
1227
+ dependence that would spoil an absolute dipole cancels in the derivative because charge is
1228
+ conserved. The two are now separate functions, and the 3D refusal names the one that works.
1229
+
1230
+ What stays refused is only the conversion. A slab's `α/A` has units of **length** and is the
1231
+ quantity the monolayer literature reports; turning it into a dielectric constant needs a
1232
+ thickness, which is a choice about the material rather than something a supercell fixes. The
1233
+ units per dimensionality are tabulated on `polarizability` so the 0.2.0 mistake — dividing by a
1234
+ length and calling the result `ε_∞` — cannot be repeated by accident.
1235
+
1236
+ - **`E_inf(q)` in every dimensionality**, `pbc::dielectric_function` / `am1_rs.dielectric_function`.
1237
+
1238
+ `eps_inf = 1 + 4*pi*alpha/Omega` is a **constant**, and that is a three-dimensional accident
1239
+ rather than the general case. The general relation is `eps(q) = 1 - v_d(q) chi0(q)` with `v_d`
1240
+ the bare Coulomb kernel of that dimensionality — the same object `pbc::ewald::LongRangeKernel`
1241
+ is built around — and `chi0 -> -q^2 (qhat.alpha.qhat)/measure`. Putting the three kernels in:
1242
+
1243
+ | | `v_d(q)` | `eps(q)` | at `q -> 0` |
1244
+ |---|---|---|---|
1245
+ | crystal | `4pi/q^2` | `1 + 4pi (qhat.alpha.qhat)/Omega` | a constant — this is `eps_inf` |
1246
+ | slab, `q` in plane | `2pi/|q|` | `1 + 2pi (qhat.alpha.qhat)|q|/A` | **-> 1** |
1247
+ | chain, `q` along it | `2 K0(|q|rho)` | `1 + 2 K0 q^2 (qhat.alpha.qhat)/L` | **-> 1** |
1248
+
1249
+ So a sheet or a wire has no long-wavelength dielectric constant: it does not screen a field whose
1250
+ wavelength exceeds its own extent. That is not a limitation of the implementation — it is *why*
1251
+ `1 + 4pi*alpha/Omega` cannot be evaluated there, and it is the same fact as a slab having no
1252
+ LO-TO splitting at Gamma. Measured: in three dimensions `eps(q)` reproduces `dielectric_tensor`'s
1253
+ constant at every `q` to 1e-9; a slab's `eps(q) - 1` fits an exponent of **1.000**; a chain's
1254
+ climbs 1.64 -> 1.71 toward the 2 that `q^2 K0` gives up to its logarithm.
1255
+
1256
+ The two-dimensional form is thickness-free, which is what makes `2pi chi_2D` — the
1257
+ Rytova-Keldysh screening length — an intrinsic property of the layer. Assigning a slab a
1258
+ thickness and quoting `1 + 4pi chi_2D/d` is a different, model-dependent number and is
1259
+ deliberately not offered. A chain needs a transverse radius for its logarithm, and it is
1260
+ **required** rather than guessed.
1261
+
1262
+ `K0` is the one special function the crate carries beyond `erf`, and it is checked against
1263
+ Abramowitz & Stegun's own table before anything is built on it — which caught a real bug on the
1264
+ way in: the `I0` series inside it runs in `(x/3.75)^2` and the `K0` series beside it in
1265
+ `(x/2)^2`, and writing one variable for both put `K0(0.1)` 0.8 % off with nothing else in the
1266
+ crate able to notice.
1267
+ - **LO–TO below three dimensions: there is nothing to add, and it is now measured rather than
1268
+ argued.** The long-range kernel diverges in every dimensionality — `4π/(Vq²)`, `2π/(A|q|)`,
1269
+ `−(2/L)ln|q|` — but the *contribution to `D(q)`* carries `q²` from charge conservation, so only
1270
+ three dimensions keeps a finite direction-dependent limit and is discontinuous at Γ. That
1271
+ discontinuity **is** the LO–TO splitting.
1272
+
1273
+ `|D(q) − D(0)|` at `q = 0.02, 0.01, 0.005` along the periodic axis:
1274
+
1275
+ | | | | |
1276
+ |---|---|---|---|
1277
+ | 1D chain | 3.6e-3 | 1.8e-3 | **9.0e-4** |
1278
+ | 2D slab | 4.6e-3 | 2.2e-3 | **1.1e-3** |
1279
+ | 3D crystal | 1.071e-1 | 1.074e-1 | **1.075e-1** |
1280
+
1281
+ The low-dimensional cases converge to Γ; the crystal does not. So `frequencies_with_lo_to`
1282
+ refusing a chain or a slab is the physics and not a gap — there is no splitting at Γ to add —
1283
+ and the non-analytic *approach*, which is real, the DFPT path already carries exactly. 0.2.0's
1284
+ "127 cm⁻¹ of splitting on a polar chain" was an artifact of applying the 3D kernel.
1285
+
1286
+ ### Performance
1287
+
1288
+ - **The parameter set is cached per method.** `Am1Parameters::for_method` re-parsed the embedded
1289
+ CSV and re-ran the `rho1`/`rho2` secant solves for every element on **every call** — and every
1290
+ function on the Python surface calls it at its top. Measured at **270 µs**, against a 1361 µs
1291
+ water single point: about 17 % of every small-molecule call, paid again on every step of a
1292
+ molecular-dynamics loop. It is a fixed per-call cost, which is exactly the shape a large-system
1293
+ profile cannot see. Now 2 µs for a clone, and `Am1Parameters::shared` borrows for the callers
1294
+ that only read.
1295
+
1296
+ - **The infrared atomic polar tensor is `O(N³)`, not `O(N⁴)`.** It built `∂P/∂R_j` — an `nao²`
1297
+ matrix, `O(nao² n_occ)` each — for all `3N` perturbations and traced each against `M_α`, to keep
1298
+ three numbers per perturbation. Writing `∂P = B + Bᵀ` and using that `M_α` is symmetric gives
1299
+ `Tr[∂P M_α] = 2w Tr[Uᵀ (C_vᵀ M_α C_o)]`, so the `nao²` object never has to exist: project `M_α`
1300
+ into the occupied–virtual block once, and each perturbation is one Frobenius product of
1301
+ `n_vir × n_occ`. The factor `2w` is 4 for RHF and 2 per spin for UHF — the same convention the
1302
+ periodic relaxation term uses. Exact, and checked by the three independent identities already in
1303
+ `tests/ir.rs` (the sum rule at 3e-15, a dipole finite difference, and the interchange theorem).
1304
+
1305
+ - **A pack-index table** in the two-centre Fock contraction, replacing a branch and a multiply in
1306
+ the innermost loop. Bit-identical to the closed form and strictly less work, with
1307
+ `the_pack_table_is_the_closed_form` asserting the equivalence over the whole domain.
1308
+
1309
+ - **The `q = 0` periodic response no longer holds a density per perturbation.** The
1310
+ coupled-perturbed solve consumes one perturbation's response density at a time, but built all
1311
+ `3N` of them before the loop and kept a second array of the same size for the spin-summed total.
1312
+ The arithmetic is identical either way — the loop nest is the same, only its order changed — but
1313
+ the resident set was `(1 + n_channels) · ndof · n_T · nao²` doubles where
1314
+ `(1 + n_channels) · n_T · nao²` will do. That is a factor of `3N` on two of the three arrays of
1315
+ that shape, and the Born charges and the polarizability, which read only each perturbation's
1316
+ **origin** block, now stream as well.
1317
+
1318
+ Measured with a peak-tracking global allocator rather than reasoned about
1319
+ (`tests/response_memory.rs`): on 27 atoms with 7 translations the response adds **14.7 MB** over
1320
+ the ground-state SCF's own, against the **39.7 MB** the old shape needed. The remaining third is
1321
+ the bare `∂F/∂R`, which is assembled pair-major and so cannot be streamed without `O(N)` passes
1322
+ over the pair list; holding it sparsely is an `O(N)` win asymptotically but costs more below
1323
+ about a dozen atoms, so it is left as it is and named here rather than half-done.
1324
+ - **`Matrix::frobenius_dot` accumulates in eight lanes.** A single running total is a dependency
1325
+ chain the compiler may not reorder, so the loop ran at one add per latency however wide the
1326
+ machine. This changes the summation order, as the 0.2.1 DIIS packing did.
1327
+
1328
+ - AM1-BCC no longer perceives the topology twice (`write_mol2` re-derived it, which also meant the
1329
+ file could in principle disagree with the charges beside it — `BccResult` carries the bonds now),
1330
+ and the 405-entry parameter table is parsed once per process rather than per call.
1331
+
1332
+ ### Not done, and named so it is not mistaken for done
1333
+
1334
+ - **The Berry phase and the finite field below three dimensions, or open-shell.** Both are
1335
+ *implemented* in 0.2.2 — the heading is about where they stop. `pbc::berry` and
1336
+ `pbc::run_finite_field` require a three-dimensional restricted cell: the polarization quantum is
1337
+ `e a/Ω` and Ω has to be a volume, a slab or a chain has a polarization along its periodic
1338
+ directions only which the module does not separate out, and an open-shell cell would need each
1339
+ spin manifold's phase separately. Both refuse rather than answering with the three-dimensional
1340
+ closed-shell expression. A field *orthogonal* to every lattice vector needs none of this and is
1341
+ supported in every dimensionality and for open shells (`PbcOptions::electric_field`); so is the
1342
+ polarizability itself, which is a response rather than a phase.
1343
+ - **The CPHF perturbation batching named in 0.2.1's `fock.rs` as "the next thing to try" is not
1344
+ done, because it was measured and it is slower.** The experiment was run in a sibling NDDO crate
1345
+ with the same loop: batching the response Fock across degrees of freedom went 5.2 → 8.8 s on a
1346
+ 102-atom Hessian, gathering the density sub-blocks 5.2 → 9.8 s, and packing the Coulomb
1347
+ contraction 4.17 → 4.39 s. The batching did what it was meant to structurally — 70 Fock passes
1348
+ instead of 3961 — and was still slower: at that size the whole integral set is about 4 MB, so it
1349
+ sits in L3 across calls and there is no traffic to save, while batching costs the per-DOF
1350
+ parallelism the `par_iter` gets for free. At NDDO block sizes this loop is bound by **per-pair
1351
+ overhead**, not by memory or arithmetic. The measurement is recorded in `src/fock.rs` in place of
1352
+ the suggestion, so the afternoon is not spent again.
1353
+
1354
+ ## 0.2.1
1355
+
1356
+ ### Added
1357
+
1358
+ - **External electric field** for molecules: energy, analytic gradient and analytic Hessian.
1359
+ `Am1Options::electric_field` (eV per e·Bohr). `E(F) = E₀ − μ·F` with this model's own dipole;
1360
+ the operator is the new `dipole` module, which the molecular field and the periodic field
1361
+ response both call rather than transcribing. `born_charges_from_response` still writes its own
1362
+ three-term derivative form, so the sign convention is shared by two of its three consumers.
1363
+ Because that operator is *linear* in the nuclear positions the field adds nothing to the
1364
+ fixed-density second derivative and reaches the Hessian only through the CPHF response — which
1365
+ is why the Hessian is checked against finite differences under a field rather than assumed.
1366
+ Measured: gradient 1.8e-6 eV/Bohr and Hessian 8.1e-7 relative against a full-SCF finite
1367
+ difference; `−∂E/∂F` reproduces the reported dipole to 3e-8 e·Bohr. Refused under a cell, since
1368
+ `F·R` is unbounded along a periodic direction.
1369
+ - **Infrared spectra** (`ir`): the atomic polar tensor `∂μ_α/∂R_{a,β}` as a raw `3 × 3N` matrix,
1370
+ and km/mol intensities projected onto normal modes. Validated three ways — the translational
1371
+ sum rule `Σ_a ∂μ/∂R_a = q δ` (3e-15), a full-SCF dipole finite difference (7e-7 e), and the
1372
+ interchange theorem `∂μ_α/∂R_j = −∂²E/∂F_α∂R_j`, the right-hand side taken as a finite
1373
+ difference of the analytic gradient in the field (1.2e-6 e). The two routes share no code past
1374
+ the SCF, which is the point; a *field* CPHF would make the second route analytic too, and there
1375
+ is no molecular field-CPHF path in the crate. CO₂'s symmetric stretch comes out dark at 1.6e-15
1376
+ against 8.75 for the antisymmetric one.
1377
+ - **Wavefunction output in Molden format** (`molden`): `[Atoms]`, `[STO]` and `[MO]`. The AM1
1378
+ basis is Slater-type, so `[STO]` represents it exactly with no Gaussian expansion invented. The
1379
+ file and the docs both state the caveat that matters: NDDO *assumes* an orthonormal AO basis, so
1380
+ the coefficients are in an implicitly orthogonalized basis while the listed Slater functions are
1381
+ the raw ones.
1382
+ - **First-order orbital response** is returned rather than discarded:
1383
+ `analytic_hessian_with_response` hands back `U`, `G` and the response density the CPHF already
1384
+ solved for. An infrared spectrum therefore costs a Hessian and nothing more.
1385
+ - **Normal modes** on `VibrationalModes` — mass-weighted eigenvectors, Cartesian displacements,
1386
+ and each mode's overlap with the rigid-body subspace, so a linear molecule's five rigid-body
1387
+ modes are *discovered* rather than assumed from `3N − 6`.
1388
+ - **β orbitals for UHF.** `Am1Result` carries the β energies and coefficients; the SCF solved for
1389
+ them and then threw them away, which made a spin-polarized wavefunction unreportable.
1390
+ - **DFPT is generalized in `k` as well as `q`.** `DfptOptions` takes an arbitrary mesh or an
1391
+ explicit k-point list, and `DfptResult` returns the `(k, k+q)` band energies, occupations and
1392
+ first-order densities. `PbcOptions::kpoints` lets the response and the ground state share one
1393
+ *resolved* k-set rather than two independent resolutions of the same description.
1394
+ - **The long-range monopole term is in the DFPT response**, on a 3D cell, at every `q`
1395
+ (`LongRange::Auto`, the default). `EwaldSum::phased_pair_potential` returns the value, gradient
1396
+ and Hessian of the phased sum `Σ_T e^{iq·T} erfc(α|d+T|)/|d+T| + …` in one pass, with the
1397
+ reciprocal half summed over `k = G − q` — the phase moves the *shell*, not just the summand.
1398
+
1399
+ **The element dropped is `k = 0`, not `G = 0`**, which is a correction to the convention 0.2.0's
1400
+ docs recorded as settled. `k = 0` arises only when `q` folds to Γ, where it is exactly the
1401
+ divergent term the neutralizing background cancels, so the rule reduces to this crate's tin-foil
1402
+ `Σ_{G≠0}` at `q = 0`. Dropping the long-wavelength element `k = −q` instead — the alternative,
1403
+ which keeps the direction-dependent part out of `D(q)` so LO–TO can supply it — was implemented
1404
+ and **rejected on measurement**: that rule is not periodic in `q`, failing `Δ(q+G) = Δ(q)` by
1405
+ 1.2e1 where the accepted rule gives 9.2e-14, and it has no well-defined answer at a zone
1406
+ boundary where several `k` tie for smallest.
1407
+
1408
+ So `D(q)` is now the **full** dynamical matrix and its `q → 0` limit is direction dependent,
1409
+ which is the physics. It must **not** be combined with `frequencies_with_lo_to`, which exists to
1410
+ restore that same physics to the supercell route; use one or the other.
1411
+
1412
+ A phase error here leaves the matrix Hermitian and the frequencies real, so this is validated by
1413
+ identities: the kernel is independent of the real-space cutoff to **2.2e-16 at `q = ¼`**, where
1414
+ the truncated sum alone moves by 1.4e-1; `Δ(−q) = Δ(q)*` to 1.5e-15; derivatives against finite
1415
+ differences to 4e-10; at Γ it reproduces `pbc_hessian` to 1.7e-8 relative while contributing
1416
+ 3.6e-2 eV/Bohr²; and the acoustic sum rule holds to 1.2e-9, which it does because the
1417
+ fixed-charge second derivative phases the pair term and never the self term.
1418
+
1419
+ What the correction does *not* cover is the `R⁻³` Klopman–Ohno tail, which stays with the
1420
+ real-space sum — so the assembled `D(q)`'s cutoff dependence at `q = ¼` falls from 3.1e-2 to
1421
+ 2.0e-2 rather than to zero. That residual is the tail, not the monopole channel; the 2.2e-16
1422
+ above is what separates the two claims.
1423
+
1424
+ ### Fixed
1425
+
1426
+ - **DFPT sampled a different Brillouin zone from its own ground state.** The response mesh was a
1427
+ hand-rolled Γ-centred grid built from `kmesh.sizes()` alone, so a `MonkhorstPackShifted` request
1428
+ gave the ground state `{−1/4, +1/4}` and the response `{0, 1/2}`. Nothing announced it: the
1429
+ force constants stayed real and the frequencies plausible. The regression test asserts the
1430
+ `q = 0` identity on a shifted mesh — agreement is now 1.2e-9 relative, and the test also
1431
+ measures that the two meshes differ by 2.9e-2 eV/Bohr², so it could not have passed by accident.
1432
+ Non-periodic axes are collapsed too, which removes `n²` redundant diagonalizations on a slab.
1433
+ - **`ε_∞` was 27× too close to 1: the polarizability was never converted to atomic units.**
1434
+ The field CPHF is solved in this crate's interior units — orbital energies in eV, positions in
1435
+ Bohr — so `U ~ M/Δε` carries Bohr/eV and the assembled `α = Σ_a R_a ΔQ_a` is in `e²·Bohr²/eV`.
1436
+ It was returned labelled Bohr³ and fed straight into `ε_∞ = 1 + 4πα/Ω`, which needs atomic
1437
+ units. The missing factor is one Hartree in eV.
1438
+
1439
+ Nothing caught it because every test checked `α`'s **shape** — symmetric, positive-definite,
1440
+ independent of the cell origin — and a value wrong by a constant factor satisfies all three.
1441
+ The new `a_molecule_in_a_large_box_has_the_isolated_molecule_polarizability` checks its
1442
+ *magnitude* instead, against the finite-field polarizability of the same molecule with no cell:
1443
+ two routes sharing only the SCF and the dipole operator, one an analytic CPHF and the other two
1444
+ extra SCF solves per axis. Water's mean `α` is 3.379 Bohr³ isolated, and the periodic value
1445
+ converges to it as the box grows — 0.85 % at 7 Å, 0.40 % at 9 Å, **0.17 % at 12 Å** — where
1446
+ before the fix it sat at 0.125 Bohr³ and did not converge to anything.
1447
+
1448
+ This changes every `ε_∞` and therefore every LO–TO splitting reported by 0.2.0 and by earlier
1449
+ drafts of 0.2.1.
1450
+ - **LO–TO splitting and `ε∞` were three-dimensional formulas applied to chains.**
1451
+ `ε∞ = 1 + 4πα/Ω` and `D_NA ∝ 4π/(Ω q·ε∞·q)` need `Ω` to be a volume, but `Lattice::measure`
1452
+ returns a *length* for a chain and an *area* for a slab, and `tests/pbc_lo_to.rs` ran both on 1D
1453
+ chains. A genuinely 1D-periodic chain has **no** LO–TO splitting as `q → 0` (the term vanishes
1454
+ as `q² ln q`), so the 127 cm⁻¹ and 1631 cm⁻¹ figures recorded in the 0.2.0 notes below were
1455
+ artifacts. Both functions now require a fully periodic cell, and the tests were moved to a 3D
1456
+ polar crystal, where the added term matches its closed form to 2e-15.
1457
+ - **DFPT reported `residual: NaN`** when the coupled-perturbed solve hit its iteration cap, so a
1458
+ caller could not tell a stiff system from a broken one. It now reports the residual it reached,
1459
+ and the tolerances and iteration cap are options rather than private constants.
1460
+ - **The analytic UHF Hessian built a different Hamiltonian from the SCF**, always molecular and
1461
+ always without the long-range or far-field corrections, whatever the options said. Its skeleton
1462
+ loop is structurally molecular, so a periodic or far-field-screened request is now refused
1463
+ rather than silently answered with a molecular result.
1464
+ - A time-reversal-folded mesh, a `q` component along a non-periodic axis, and an explicit k-list
1465
+ whose weights do not sum to 1 are all refused by DFPT instead of quietly producing an answer.
1466
+ - **`am1-rs energy` crashed part-way through its output on any machine whose locale is not
1467
+ UTF-8.** Python encodes `print` with the locale's codec, and the dipole line was written
1468
+ `e·a0`: on a Japanese Windows (cp932) or under the `C` locale that minimal Docker images ship
1469
+ with, that raised `UnicodeEncodeError` after six lines had already gone to stdout, and the
1470
+ command exited 1 with a truncated report. `gradient` and `optimize` went the same way. The Rust
1471
+ binary never raised — it writes UTF-8 whatever the locale — but rendered mojibake on the same
1472
+ console, so the two front ends did not agree there either.
1473
+
1474
+ Both CLIs now print **ASCII only** (`e*a0`, `cm^-1`, `eV/A`), which is the fix that works on
1475
+ every console rather than merely avoiding the exception, and the Python front end additionally
1476
+ forces its streams to UTF-8 so that a non-ASCII message from the native layer cannot kill it.
1477
+ `pip install` and the `am1-rs` console script were verified end to end in a clean virtualenv
1478
+ under both cp932 and a forced ASCII stdout.
1479
+
1480
+ The test suite had not caught this because the development machine had `PYTHONIOENCODING`
1481
+ set in its shell and pytest's subprocesses inherited it, giving every child a UTF-8 stdout that
1482
+ no user would have. `tests/test_cli.py` now strips that variable from the child environment,
1483
+ asserts that both CLIs' bytes are ASCII in every mode, and runs `energy` under a deliberately
1484
+ ASCII stdout.
1485
+
1486
+ ### Performance
1487
+
1488
+ - **The molecular SCF's DIIS history is half the size, and peak memory fell 28 %.** `rhf_loop`
1489
+ kept three depth-8 histories — Fock, `[F,P]` error, density — as dense `nao²` matrices: at 1602
1490
+ AOs (an 801-atom water cluster) twenty-four of them are **492 MB**, against a measured 877 MB
1491
+ peak for the whole run. It was the single largest term.
1492
+
1493
+ Every matrix in all three is either symmetric (`F`, `P`) or **anti**symmetric (`[F,P] = FP−PF`,
1494
+ whose diagonal is identically zero), so one triangle determines the other and packing loses
1495
+ nothing — `packing_a_diis_history_preserves_it_exactly` checks the round trip and the Frobenius
1496
+ products the extrapolation actually consumes, including that the commutator's diagonal really
1497
+ is zeroed. The density history is additionally only built for the accelerator that reads it
1498
+ (`AdiisCdiis`), which is a further third off for a CDIIS-only run. `uhf_loop` gets the same
1499
+ treatment, its stacked two-spin error packed as two triangles end to end.
1500
+
1501
+ Measured on the 801-atom cluster: **877 MB → 632 MB** peak working set, same energies, and the
1502
+ SCF converging in 13 iterations against 14 — the histories are bit-equivalent, but the
1503
+ Frobenius products are summed in a different order, which moves the last iteration across the
1504
+ convergence threshold.
1505
+ - **The Hessian's orbital-relaxation contraction is a matrix product.** `H_relax[a][b] =
1506
+ 4 G^a : U^b` was `ndof²` independent Frobenius dots, which re-reads every `G` row `ndof` times
1507
+ and is memory bound; stacking the ov-blocks makes it `G Uᵀ`. The periodic version was the same
1508
+ nest four levels deep and *not parallelized at all*; it is now two products per k point.
1509
+
1510
+ The molecular one is **tiled**, and that is not incidental: stacking `G` and `U` whole would
1511
+ add two `ndof × n_ov` buffers, which is `O(N³)` and would double the largest array the Hessian
1512
+ holds. Copying 64-row tiles bounds the extra at `O(N²)` and trades for redundant copying worth
1513
+ `1/64` of the arithmetic.
1514
+ - **The DFPT response is streamed and solved in parallel.** Each perturbation is solved,
1515
+ contracted into `C(q)`, and dropped, so the resident response is `O(threads · n_k · nao²)`
1516
+ rather than `O(ndof · n_k · nao²)` — and the `j'` loop, which was serial, now runs under rayon.
1517
+ The `pbc_dfpt` suite fell from 4.25 s to 2.50 s. `keep_response` restores the full array for a
1518
+ caller that wants it, and a test asserts that asking for it leaves `C(q)` bit-identical.
1519
+ - **Assembling `C(q)` is `O(N³·n_k)`, not `O(N⁴·n_k)`.** The bare perturbation is held as its
1520
+ nonzero entries grouped by translation, not as a dense `nao²` matrix per k point. Displacing
1521
+ one atom changes the Hamiltonian only where that atom appears — `O(1)` blocks — plus, on a 3D
1522
+ cell, the on-site diagonal of every atom from the long-range monopole channel, which is `O(N)`.
1523
+ Contracting every pair of perturbations against that is an order cheaper than against `nao²`.
1524
+
1525
+ Measured on a chain grown by repeating its cell, with the counts returned on `DfptResult` so
1526
+ the claim is checkable rather than asserted: the contraction's extent scales as **N^-0.04**
1527
+ against **N^2.00** for the dense one it replaces — 2.0 orders removed — and is 4.2× smaller
1528
+ already at twelve atoms. Below about eight atoms the sparse form is *larger*, which the test
1529
+ prints rather than hides.
1530
+ - **The divide-and-conquer DIIS history is now linear in the atom count, not quadratic.** It
1531
+ stored a dense packed triangle while the divide-and-conquer density is *identically* zero
1532
+ beyond the buffer radius — so most of what it held was zeros. Storing the density's actual
1533
+ sparsity pattern instead gives a measured scaling exponent of **1.05**, against **1.99** for
1534
+ the dense triangle it replaces; both are asserted in `tests/divide_conquer.rs`, because a
1535
+ linear number on its own could be an accident of size. This is the dominant memory term of a
1536
+ large run. `DcResult` gains `diis_pattern_elements` and `dense_triangle_elements` so the claim
1537
+ is inspectable rather than believed.
1538
+
1539
+ The same change cuts the memory traffic: on a 1029-atom cluster `dc:diis` fell from 1.070 s to
1540
+ 0.419 s and the run from 5.11 s to 4.45 s, with identical energies and the same iteration count.
1541
+ - **The DFPT response is DIIS-accelerated**, and it needed to be: the coupled-perturbed solve is
1542
+ a linearly mixed fixed point, and on a polar 3D cell it did not converge at all within its
1543
+ 200-iteration cap — a water crystal stalled at `5.6 × 10⁻⁹` against a `10⁻¹⁰` tolerance and
1544
+ raised. Every one of those iterations is a real-space two-electron build plus a diagonalization
1545
+ per k point. With Pulay extrapolation on the fixed-point residual the same system converges at
1546
+ the default tolerance, and every DFPT identity reproduces bit for bit — it is the same fixed
1547
+ point, reached sooner.
1548
+ - **The periodic CPHF's two basis transforms were `O(nao⁴)`; they are now `O(nao³)`.** Both
1549
+ `project_ov` (`Cᵥ† M C_o`) and the response density (`C_v U C_o† + h.c.`) were written as a
1550
+ single loop nest over `(v, o, μ, ν)`, which rebuilds the inner `M C_o` — a quantity
1551
+ independent of the virtual index — once for **every** virtual. Since `n_v` and `n_o` both grow
1552
+ with `nao`, `n_v n_o nao²` is a fourth power. Factoring each into two products makes it
1553
+ `nao² n_o + nao n_v n_o`.
1554
+
1555
+ Measured against the loop nest it replaces, which is kept in the test suite as the reference:
1556
+
1557
+ | `nao` | loop nest | factored | speedup |
1558
+ |---|---|---|---|
1559
+ | 32 | 1.197 ms | 0.041 ms | **29×** |
1560
+ | 64 | 17.043 ms | 0.185 ms | **92×** |
1561
+ | 128 | 360.044 ms | 0.996 ms | **362×** |
1562
+
1563
+ The two agree to 3.1 × 10⁻¹⁵ relative, and `factoring_project_ov_lowers_its_scaling_exponent`
1564
+ asserts that the advantage *grows* with size, since a constant-factor win would not.
1565
+
1566
+ The compact occupied/virtual coefficient blocks the products need are gathered once per k
1567
+ point, into `KOrbitals`. Gathering them per call instead — the obvious first cut — made the
1568
+ DFPT suite **five times slower** (3.7 s to 18.5 s), because at the `nao ≈ 8–40` of a small
1569
+ cell the allocations cost more than the fourth power saved. The complex arithmetic likewise
1570
+ accumulates in place (`matmul_acc_seq`) rather than allocating a matrix per real product and
1571
+ combining afterwards.
1572
+ - **The molecular CPHF called faer's *parallel* matmul from inside its own rayon loop.**
1573
+ `Matrix::matmul_seq` exists precisely for this and its documentation names the CPHF
1574
+ perturbation loop as the case, but `project_ov` and `ao_response_density` used the parallel
1575
+ form — so faer's workers contended with the outer pool over the same threads. Both now use the
1576
+ sequential, transpose-free products. `cphf:to_ao` fell from 1.447 s to 1.034 s and
1577
+ `cphf:to_mo` from 1.346 s to 1.000 s of thread time on a 48-atom Hessian, about 27 % off each.
1578
+ The divide-and-conquer density build had the same nesting and is fixed with it.
1579
+ - **Four more hand-written `n³` loop nests went to the blocked kernel**, all of them on inner
1580
+ paths. These are counted rather than timed — the machine they were developed on was running
1581
+ other work, and a stopwatch there measures the load, not the code:
1582
+
1583
+ | site | what it is | per what |
1584
+ |---|---|---|
1585
+ | `pbc::dfpt`'s `mul` / `adjoint_mul` / `mul_adjoint` | the CPSCF's complex transforms | 4 × per k point per iteration |
1586
+ | `pbc::scf`'s `P(k) = Σ_i f_i c_{μi} c*_{νi}` | the periodic density build | per k point per SCF iteration |
1587
+
1588
+ The periodic density build also stopped walking the empty levels. It looped every orbital and
1589
+ `continue`d on `f_i = 0`, which skipped the arithmetic but not the traversal; gathering the
1590
+ filled columns first makes the products `nao² · n_occ` rather than `nao³`.
1591
+ - **The Fock build no longer copies the density to halve it.** `build_fock` and `build_g_matrix`
1592
+ each cloned the density and scaled it by ½ to make the same-spin matrix for the exchange. The
1593
+ exchange is linear in that argument, so `build_fock_spin_with` now takes a `spin_scale` and the
1594
+ callers pass the total density with `0.5` — exactly as `pbc::scf::build_realspace_fock` already
1595
+ did. That removes an `nao²` allocation, copy and scale from every call, and `build_g_matrix` is
1596
+ called `3N` times per CPHF iteration: at 1602 AOs each of those copies was 20 MB.
1597
+ - **The perturbed Fock's long-range term evaluates a quarter of the lattice sums.** The nest
1598
+ built `Δ'(R_b − R_a)` about `2·nat²` times — the `a == c` branch walks every `b` for each `c`,
1599
+ and the other branch asks for every ordered pair — where only `nat(nat−1)/2` are distinct.
1600
+ `Δ'` is **odd** in the separation (`Δ` is even), so one triangle tabulated once serves both
1601
+ halves. `the_pair_gradient_is_odd_in_the_separation` pins that at 1.6e-15.
1602
+ - **The Ewald pair Hessian is evaluated on half the pairs.** `LongRangeMonopole::energy_hessian`
1603
+ called `delta_hessian` for both `(a,b)` and `(b,a)` — the same lattice sum twice, and the
1604
+ lattice sum over every translation and reciprocal vector is what costs. That Hessian is *even*
1605
+ in the separation (it is built from `d̂_i d̂_j` and even powers of `|d|`, and the translation
1606
+ set is symmetric), so one triangle suffices: exactly half the work, with
1607
+ `the_pair_hessian_is_even_in_the_separation` pinning the symmetry it rests on at 1.2e-15. The
1608
+ region also gained the `ewald:hessian` timer it had never had, which is why the plan's
1609
+ "profile before optimizing" step had never been possible there.
1610
+ - **No `unwrap` or `expect` outside tests, anywhere in `src/`** — down from eighteen. Most were
1611
+ `blocks.get(ImageOffset::origin()).unwrap()`: "the origin is always in the translation set" is
1612
+ a property of how that set is built, not one the type enforces, and a panic from inside an SCF
1613
+ iteration is the worst way to learn otherwise. `RealSpaceBlocks::origin`/`origin_mut` return a
1614
+ `Result` and every site propagates it. The two that remained were genuinely infallible and are
1615
+ now infallible *structurally*: the longest-axis search is a fold over three fixed axes rather
1616
+ than `max_by(..).unwrap()`, and an empty level set is handled rather than indexed into.
1617
+ - `Matrix` gained transpose-free products (`transpose_matmul`, `matmul_transpose`, and their
1618
+ `_seq`/accumulating variants). Materializing a transpose to multiply by it is an extra
1619
+ allocation and copy per call; on `P = C_occ C_occᵀ`, once per SCF iteration, the transposed
1620
+ view is **3.2×** faster than the copy at 600 AOs and 400 occupied orbitals — measured, because
1621
+ handing a kernel a non-native layout can as easily cost as save.
1622
+
1623
+ That figure is a **minimum over repetitions**, not a mean, and the difference matters: a
1624
+ three-run mean of the same code reported 1.24× on an idle machine and 1.65× *slower* on a busy
1625
+ one. Interference only ever makes a sample slower, so the minimum is the least-contended
1626
+ estimate; the test asserts only against a 3× catastrophe, because anything tighter would be
1627
+ asserting that the machine is idle.
1628
+ - DFPT no longer rebuilds `h_j(k)` inside the `j'` loop or inside the CPSCF iteration; it is
1629
+ invariant in both, and the Bloch sum costs `O(n_T · nao²)` each time.
1630
+ - `farfield` is instrumented, and `tests/dc_where_the_time_goes.rs` reports where a large run's
1631
+ time actually goes.
1632
+
1633
+ ### Not done, and named so it is not mistaken for done
1634
+
1635
+ An audit of this release against its own plan turned these up. They are recorded here rather than
1636
+ left for a reader to discover.
1637
+
1638
+ - **Parameter structs landed for two of the plan's four targets.** `build_core_with_neighbors`
1639
+ takes `CoreBuildOptions`, and the CPHF trio — `apply_orbital_hessian`, `cphf_ov` and
1640
+ `cphf_ov_fixed_point` — now share a `CphfContext` holding what does not change from one
1641
+ perturbation to the next, which also makes it impossible to hand the fixed-point fallback a
1642
+ different Hamiltonian from the solver that gave up. `skeleton_fock_ov` and the DFPT helpers
1643
+ still take long positional lists; 20 `#[allow(clippy::too_many_arguments)]` remain.
1644
+ - **The MOPAC oracle covers one molecule.** Verified, not assumed: of the 61 cases in MOPAC's
1645
+ `tests/keywords`, `AM1.mop` and `RM1.mop` are the only ones selecting these methods and both
1646
+ are CO₂. Widening it means *running* MOPAC rather than reading it. The comparison was deepened
1647
+ instead — the whole orbital spectrum rather than one eigenvalue.
1648
+ - **The long-range monopole term in the DFPT response on a chain or a slab.** It landed for 3D
1649
+ cells (above). `LongRangeMonopole` is itself three-dimensional, so in 1D and 2D there is no such
1650
+ correction anywhere in the crate and nothing to generalize; `LongRange::Require` errors there
1651
+ rather than approximating quietly. Implementing the low-dimensional kernels — 2D `2π/(A q)` with
1652
+ a slab convention, 1D vanishing as `q² ln q` — is a separate piece of work.
1653
+
1654
+ - **No Barnes–Hut tree for the far field**, despite the module documentation suggesting one
1655
+ belongs there. Measured first: on a 1029-atom divide-and-conquer run with the far field on,
1656
+ `farfield:potential` is **0.5 %** of the runtime, against 36 % for the subsystem
1657
+ diagonalizations. A monopole pair costs about ten flops and the loop is embarrassingly
1658
+ parallel, so making it `O(N log N)` would save half a percent — and would put an
1659
+ acceptance-angle discontinuity into the energy surface to do it. The `O(N²)` term does win
1660
+ eventually, but the crossover is around `10⁴–10⁵` atoms. `src/farfield.rs` records the
1661
+ measurement, so the decision can be revisited against a number rather than re-argued.
1662
+
1663
+ ### Changed
1664
+
1665
+ - `build_core_with_neighbors` takes a `CoreBuildOptions` struct instead of four positional
1666
+ arguments; `Am1Options::core_build()` derives it, so a path that builds `H_core` for itself
1667
+ cannot disagree with the SCF about which corrections are on.
1668
+ - `#![forbid(unsafe_code)]` — there was none, and now there cannot be.
1669
+ - **The Python API roughly doubled.** New in `am1_rs.native`: `orbitals`, `molden`,
1670
+ `ir_spectrum`, `dipole_derivatives`, `orbital_response`, `pbc_hessian`, `born_charges`,
1671
+ `dielectric`, `dfpt`, `lo_to_frequencies`; `electric_field=` on `single_point`, `gradient`,
1672
+ `optimize`, `hessian`, `frequencies` and six more; `multipole_cutoff=` on `divide_conquer`.
1673
+ The ASE calculator gains the matching `get_ir_spectrum`, `get_dipole_derivatives`,
1674
+ `get_orbitals`, `get_orbital_response`, `write_molden`, `get_frequencies`,
1675
+ `get_am1_bcc_charges`, `get_phonons`, `get_born_charges`, `get_dielectric_tensor`,
1676
+ `get_dfpt_frequencies`, `get_lo_to_frequencies` and `optimize`, plus `field=` and an
1677
+ `atoms.info["field"]` override routed through `check_state`. Both CLIs gain the modes
1678
+ `orbitals`, `ir`, `molden` and the flags `--field FX FY FZ`, `--molden-output FILE`.
1679
+
1680
+ `tests/test_new_api_0_2_1.py` now *enumerates* `am1_rs.native`'s public functions and requires
1681
+ each to name its ASE counterpart, instead of checking a hand-written list. The hand-written
1682
+ list is what let `lo_to_frequencies` exist in Rust and in neither Python surface.
1683
+ - **Divide-and-conquer under a periodic cell, from Python and ASE.** `native.divide_conquer`
1684
+ takes `cell`/`pbc` (with `realspace_cutoff`/`exchange_cutoff`, which matter under a cell and
1685
+ not for a molecule), and `AM1(divide_conquer=True)` now routes a periodic structure through it
1686
+ instead of raising. The Rust API had accepted a lattice since 0.2.0; the ASE error message
1687
+ saying the buffers were "not wired up yet" described 0.2.0 and had outlived it.
1688
+ - **`tests/theory_components.rs`** — the *pieces* of the formulas, against what theory says each
1689
+ one must be. Everything else in the suite is an end-to-end identity, which is strong and also
1690
+ blunt: it says a chain is wrong without saying which link, and a compensating pair of errors
1691
+ passes it. These twelve tests each check a property that follows from the mathematics alone.
1692
+
1693
+ The sharpest is the monopole limit. `(ss|ss) = e²/√(R² + ρ²)` implies that the relative
1694
+ deviation from `e²/R` is `−ρ²/(2R²)`, so `deviation × R²` is constant *and identifies `ρ`* —
1695
+ and the value recovered from the integral's long-range behaviour, 1.9873…1.9946 Bohr across
1696
+ four radii, is the parameter table's own `rho0(O) + rho0(C) = 1.994724`. That pins the
1697
+ functional form, both elements' parameters and the `AM1_EV` conversion in one measurement.
1698
+
1699
+ The rest: each multipole channel decays at the order its expansion demands (measured
1700
+ `R^-0.998`, `R^-1.996`, `R^-2.997` against `−1, −2, −3`); the two-electron integrals have their
1701
+ three permutation symmetries, including the electron exchange that swaps the two *atoms* and
1702
+ reaches them through different branches; the whole `10 × 10` block is rotation covariant, tested
1703
+ on a contracted quantity so every index has to transform correctly; the overlap has the
1704
+ inversion parity its orbitals imply and is unchanged by relabelling; the converged density is
1705
+ an idempotent projector with `Tr P` the electron count; `[F, P] = 0` at the SCF solution; the
1706
+ reported electronic energy really is `½Tr[P(H + F)]`; Koopmans uses the level it should; and
1707
+ the energy is invariant under rigid motion.
1708
+ - **`tests/orbital_response.rs`**, checking `U^j_{ai}` against a finite difference of the MO
1709
+ coefficients rather than only through what it is contracted into. Two things make that
1710
+ comparison delicate and both are handled explicitly: eigenvector *phase* (aligned against the
1711
+ response channel's own coefficients — aligning against a separately re-run SCF flips `U`
1712
+ wholesale, which the first draft did and measured as `|Δ| = 2|U|`), and *degeneracy* (methyl's
1713
+ `e′` pair mixes arbitrarily under displacement, so the coefficient comparison is done on the
1714
+ non-degenerate `H₂O⁺` while the phase-invariant response *density* is checked on methyl).
1715
+ Measured: `U` to 9.6e-7 (RHF), 7.0e-7 and 5.6e-7 (UHF α and β); `∂P/∂R` to 1.7e-7.
1716
+ - **The MOPAC oracle now compares the whole orbital spectrum**, twelve eigenvalues per method
1717
+ rather than the single Koopmans IP, including CO₂'s two degenerate pairs — which a broken
1718
+ two-centre rotation would split while leaving `ΔHf` and the HOMO almost unmoved. Worst case
1719
+ across all twelve: **0.0022 eV for AM1, 0.0034 eV for RM1**, both at the deepest level.
1720
+
1721
+ It still covers one molecule, and that was verified rather than assumed: of the 61 cases in
1722
+ MOPAC's `tests/keywords`, `AM1.mop` and `RM1.mop` are the only ones selecting these methods and
1723
+ both are CO₂. Widening it means *running* MOPAC, not reading it.
1724
+
1725
+
1726
+ ## 0.2.0
1727
+
1728
+ ### Added
1729
+
1730
+ - **RM1** (Rocha *et al.* 2006), sharing AM1's functional form and therefore its entire code
1731
+ path — gradients, Hessians, periodic boundary conditions and divide-and-conquer all work
1732
+ unchanged. Select with `method="rm1"` on any Python entry point, `--method rm1` on the CLI, or
1733
+ `Am1Parameters::for_method(NddoMethod::Rm1)` in Rust. Covers H, C, N, O, P, S, F, Cl, Br, I.
1734
+ Parameter provenance in `THIRD_PARTY_NOTICES.md` §3. See `docs/methods.md`.
1735
+
1736
+ - **Periodic boundary conditions** — 1D chains, 2D slabs and 3D crystals, dimensionality taken
1737
+ from `atoms.pbc`:
1738
+ - Γ-point and Monkhorst–Pack k-point sampling, with time-reversal folding and automatic
1739
+ collapse of non-periodic axes.
1740
+ - Fermi–Dirac smearing with a bisected chemical potential, electronic entropy and `T→0`
1741
+ extrapolation (new `fermi` module).
1742
+ - **RHF and UHF at both Γ and k-points.** Forced-UHF reproduces RHF on a closed shell to
1743
+ 1e-7 eV with an exactly vanishing spin density.
1744
+ - Analytic forces and **analytic stress** for all three dimensionalities. Stress components
1745
+ touching a non-periodic axis are exactly zero; the periodic measure is a volume, area or
1746
+ length as appropriate.
1747
+ - The AM1 core–core Gaussian corrections are included in the lattice sum, its gradient and its
1748
+ virial.
1749
+ - Net charge per cell, including the absolute energy in 3D — see Ewald summation below.
1750
+ - New `lattice`, `neighbors` and `pbc` modules; extended-XYZ `Lattice=`/`pbc=` parsing.
1751
+ - `docs/pbc.md`.
1752
+
1753
+ - **Ewald summation** for the long-range monopole electrostatics of a 3D cell (`ewald`, default
1754
+ on), under the tin-foil boundary condition:
1755
+ - Makes a **charged cell's total energy meaningful**. Across a 6.5× range of real-space cutoff
1756
+ a +1 water cell moves 0.20 eV with it and 403 eV without.
1757
+ - Applied through the **net** charges as `−V_a`, `V_a = Σ_b Δ_ab Q_b`, rather than split
1758
+ across the electron–core, Coulomb and core–core terms the way `γ_ab` is. The split form is
1759
+ algebraically identical and numerically ruinous — it shifts `H_core` and the Coulomb term by
1760
+ ±660 eV for a carbon in a 12 Bohr cell — and it stopped a lone neutral carbon from
1761
+ converging at all.
1762
+ - Energy, analytic gradient, analytic stress and analytic Hessian, the last including both the
1763
+ fixed-charge second derivative and the charge-response term in the CPHF.
1764
+ - Validated four independent ways: the rock-salt Madelung constant to 10 digits, exact
1765
+ independence of the splitting parameter `α` for both the potential and the **stress**, the
1766
+ dipole surface term `2π|p|²/3V` against a direct lattice sum, and finite differences for
1767
+ every derivative.
1768
+ - **Ewald in 2D and 1D**, so a slab or a chain gets the same treatment a crystal does rather than
1769
+ no treatment at all:
1770
+ - **2D by Parry**, not by the Yeh–Berkowitz vacuum-slab trick. Parry gives the in-plane 2×2
1771
+ stress directly; a vacuum slab has no meaningful `∂E/∂ε_zz` because the `c` axis is fictitious,
1772
+ and is only asymptotically exact. The implementation needs `erfcx(x) = e^{x²}erfc(x)`, added
1773
+ to the `Scalar` trait with its derivative rules, because the naive `e^{hz}·erfc(h/2α + αz)`
1774
+ overflows at moderate `hz`; the exponentials are composed analytically instead.
1775
+ - **1D without a Bessel-function reciprocal sum.** Only the monopole channel needs summing in
1776
+ 1D, so this is a real-space sum plus an analytic tail: the `ρ²/(nL)²` and `z/(nL)` expansion
1777
+ of `1/√(ρ²+(z+nL)²)`, whose coefficients are Hurwitz zeta values. No special functions, and
1778
+ exactly differentiable — which is what makes forces and stress available.
1779
+ - Validated the same way 3D was: Madelung constants to 10–12 digits, independence of `α` and of
1780
+ the image count, and finite differences for every derivative.
1781
+
1782
+ - **k-point analytic Hessian at `q = 0`**, so second derivatives no longer depend on the Γ-point
1783
+ exchange taper. Matches finite differences to 1.7e-7 eV/Bohr² on a polar chain with a mesh, and
1784
+ the acoustic sum rule holds exactly. Three defects found on the way there, each invisible at Γ:
1785
+ the Coulomb and exchange factors were 2× too small on unordered pairs, the exchange derivative
1786
+ Fock was missing its `−T` mirror, and the resonance derivative Fock was missing a factor of ½.
1787
+
1788
+ - **Born effective charges `Z*` and the electronic dielectric tensor `ε_∞`**, sharing the phonon
1789
+ response solve so the two cannot drift apart. `Σ_a Z*_a = 0` to 1e-16; `ε_∞` is origin
1790
+ independent to 1.6e-15, which was measured rather than predicted — the module doc had predicted
1791
+ a dependence and was corrected to record the measurement.
1792
+
1793
+ - **LO–TO splitting.** `D(q) = D_analytic(q) + D_NA(q)` with the non-analytic term built from
1794
+ `Z*` and `ε_∞`. Exactly zero for a non-polar system.
1795
+
1796
+ > **Corrected in 0.2.1.** The "127 cm⁻¹ shift and 1631 cm⁻¹ of direction dependence for a
1797
+ > polar one" measured here was taken on a **1D chain**, where the three-dimensional
1798
+ > `4π/(Ω q·ε∞·q)` kernel does not apply and `Ω` was silently a length. Those numbers are
1799
+ > artifacts. See the 0.2.1 entry.
1800
+
1801
+ - **DFPT at arbitrary `q`** — a CPSCF connecting `k` to `k+q`, so a phonon at any `q` no longer
1802
+ needs a commensurate supercell. Reproduces the `q = 0` Hessian to 4e-13 relative and a 2-fold
1803
+ supercell's frozen phonon to 3e-4. The identities that pin the phases down (`D(−q) = D(q)*`,
1804
+ continuity in `q`) are asserted too, because a wrong phase leaves the matrix Hermitian and the
1805
+ frequencies real — it does not announce itself.
1806
+
1807
+ - **Divide-and-conquer under periodic boundary conditions and with an analytic stress.** Γ with a
1808
+ minimum-image buffer, exact once the buffer reaches `L/2` (3.4e-10 eV); the stress matches a
1809
+ strain finite difference to 3.6e-8 eV/Bohr³.
1810
+
1811
+ - **Far-field monopole screening** (`multipole_cutoff`, opt-in, default off): pairs beyond the
1812
+ cutoff contribute through atomic monopoles instead of the full multipole block.
1813
+
1814
+ - **Divide-and-conquer SCF** for large molecules, restricted and unrestricted:
1815
+ - Disjoint cores by recursive spatial bisection, Yang partition weights, one common chemical
1816
+ potential shared across all subsystems (two, one per spin channel, when unrestricted).
1817
+ - The density is truncated explicitly at the buffer radius, which makes the Yang sum rule
1818
+ exact for every geometry — verified at `0.0` deviation — and makes the two-centre exchange
1819
+ exactly linear-scaling rather than approximately so.
1820
+ - Non-neutral systems; Mulliken charges conserve the formal charge to 1e-8 e.
1821
+ - Hellmann–Feynman gradient at the assembled density.
1822
+ - Scaling counters (`diagonalization_work`, `coulomb_work`, `exchange_work`,
1823
+ `retained_density_blocks`) returned on every result, so the cost claim is inspectable.
1824
+ - `docs/divide-conquer.md`.
1825
+
1826
+ - **ASE calculator** now reads `atoms.pbc` and `atoms.cell`, implements `stress`, and exposes
1827
+ `method`, `kpts`, `smearing`, the cutoffs, the SCF tolerances and `divide_conquer`. Parameters
1828
+ moved into `self.parameters`, so `todict()`, `set()` and restart work.
1829
+
1830
+ - `method=` on every Python entry point; new `pbc_point` and `divide_conquer` native functions;
1831
+ the GIL is released around every solver.
1832
+
1833
+ - `tests/test_ase_pbc_md.py` — real molecular dynamics as an acceptance test: NVE in 1D/2D/3D,
1834
+ Parrinello–Rahman NPT, NPT-Berendsen pressure response, NVT on partially periodic cells.
1835
+
1836
+ ### Fixed
1837
+
1838
+ - **The `pip`-installed CLI was broken in three of its five modes.** `am1-rs energy` exited
1839
+ non-zero on a water molecule while every other test in the suite passed: `__main__.py` read keys
1840
+ the bindings never emitted — `total_ev`, `dipole_magnitude`, `iterations`, `max_gradient`,
1841
+ `forces`, `positions`, `steps` — and a missing dictionary key raises only at the moment it is
1842
+ printed, which no test reached. Found by installing the sdist into a clean environment and
1843
+ running the console script, which is now `tests/test_cli.py` and a CI job rather than something
1844
+ that happened to get tried. Every mode's output is diffed against the Rust CLI's, which is what
1845
+ the packaging has been claiming; four of the five needed fixing to make that true, including
1846
+ Rust's `{:.6e}` exponent format, which differs from Python's.
1847
+ - **`iterations`, `unrestricted` and `dipole_magnitude` were missing from the molecular Python
1848
+ results** while the periodic ones carried them, so a caller could report how an SCF went for a
1849
+ crystal and not for a molecule. `gradient` and `optimize` now also return the SCF breakdown
1850
+ they already computed, instead of forcing a second SCF to get it. New `native.constants()`
1851
+ exports the model's unit conversions — deliberately MOPAC7's `ev = 27.21` rather than CODATA —
1852
+ so nothing on the Python side has to write them down and drift.
1853
+ - **The Hessian bug.** `rotation_to_x_g` replaced live dual numbers with constants when an atom
1854
+ pair was antiparallel to the reference axis, zeroing the derivatives. The gradient was
1855
+ protected by symmetry; the **second** derivative was not, so transverse force constants of any
1856
+ molecule with a bond on that axis were wrong. Fixed by removing the local frame entirely: the
1857
+ integrals are now written in terms of the internuclear unit vector and the transverse
1858
+ projector `δ_ij − n_i n_j`, which is branch-free, exactly differentiable at every order, and
1859
+ faster. This was also a prerequisite for periodic boundary conditions, where axis-aligned
1860
+ lattice vectors would have hit the branch constantly.
1861
+
1862
+ - **Periodic stress unit conversion** at the Python boundary used `ANGSTROM_TO_BOHR^(d−1)`
1863
+ instead of `^d`, so every periodic stress reaching Python was 1.89× too small. Found by the
1864
+ new NPT acceptance test; the Rust tests could not see it because they never cross that
1865
+ boundary.
1866
+
1867
+ - The divide-and-conquer UHF initial guess did not preserve the electron count, which put the
1868
+ open-shell case in a different SCF basin (0.66 eV high). Now split in proportion to the α/β
1869
+ counts, matching the full SCF.
1870
+
1871
+ - CPHF non-convergence is now an error (`Am1Error::CphfNotConverged`) rather than a silently
1872
+ returned plausible Hessian.
1873
+
1874
+ - Documented accuracy claims corrected to the values the tests actually assert: the numerical
1875
+ Slater overlap agrees with the analytic kernel to ~1e-7 (`1s|1s`) and ~5e-4 (`2s|2s`), not
1876
+ 1e-8. The CLI `gradient` help said eV/Å; it prints Hartree/Bohr.
1877
+
1878
+ - Be and B are parameterized for AM1 and were undocumented.
1879
+
1880
+ - **AM1-BCC perception**, several distinct defects:
1881
+ - Ring perception was a union-find spanning tree with fundamental cycles, so a fused system
1882
+ could return the 10-membered perimeter of naphthalene instead of its two 6-rings. Replaced
1883
+ by the smallest ring through each bond.
1884
+ - Aromaticity never looked at ring size at all — the detector returned only a boolean — so
1885
+ cycloheptatriene and macrocyclic lactones came out aromatic. Now ring size, a planarity test
1886
+ and a Hückel 4n+2 π count, which also correctly rejects cyclooctatetraene.
1887
+ - Sulfur could **never** be aromatic: `perceive_hybridization` returned `Sp3` for every
1888
+ sulfur, and the aromaticity test required `Sp2`, so the `| 16` in its element match was
1889
+ unreachable code. Thiophene is now aromatic, as are pyrrole and furan, each by its own π
1890
+ contribution.
1891
+ - The bond-order reference table held only six C/N/O pairs, so every C=S, P=O and S=O was
1892
+ perceived as a single bond and every thiocarbonyl, phosphate and sulfonyl group was
1893
+ mistyped. Extended to P and S pairs.
1894
+ - Bond types 6 and 9 — the symmetric delocalized groups (nitro, N-oxide; carboxylate,
1895
+ phosphate, sulfonate) — were never emitted, leaving 27 consequential parameters unreachable.
1896
+ Now selected by a chemical rule rather than a bond length. Measured: of the remaining 66
1897
+ unreachable entries, 26 are identically zero and 40 are byte-identical to the aromatic type,
1898
+ so nothing that can affect a charge is now unreachable.
1899
+ - An unparameterized element, or a typed bond with no tabulated parameter, returned raw
1900
+ Mulliken charges **in silence**. Both now appear in `BccResult::warnings`.
1901
+
1902
+ ### Performance
1903
+
1904
+ - **8.2× faster** on the molecular path, from profiling rather than guessing: faer global
1905
+ parallelism enabled (it defaults to sequential with `default-features = false`), blocked
1906
+ parallel `matmul` through faer views, a parallel chunked Fock build, `C_occ·C_occᵀ` density
1907
+ formation, a single matrix product for the DIIS commutator, and flattened pair-integral
1908
+ storage.
1909
+ - **CPHF solved by preconditioned conjugate gradient** instead of DIIS-accelerated Richardson.
1910
+ The CPHF equations are linear and their operator — the orbital Hessian — is symmetric and
1911
+ positive definite at a stable SCF solution, which is exactly what CG is for. Each application
1912
+ of that operator is a full Fock build, and those builds are **two thirds of an entire frequency
1913
+ calculation**, so the figure of merit is simply how many are needed: 6296 → 4931 for a 150-atom
1914
+ cluster. The convergence test is deliberately the same quantity the fixed-point solver used
1915
+ (the fixed point's step `‖U_{n+1} − U_n‖` *is* the preconditioned residual), so the tolerance
1916
+ did not have to be retuned and the two are directly comparable. If the operator turns out not
1917
+ to be positive definite along a search direction, the solve falls back to the fixed-point
1918
+ iteration rather than returning something meaningless.
1919
+ - **`fock::build_g_matrix`** builds the two-electron matrix directly instead of assembling the
1920
+ full Fock matrix and subtracting `H_core` again — two wasted `nao²` passes per call, about
1921
+ 9 GB of memory traffic over one Hessian. The CPHF also now uses a sequential pair loop
1922
+ (`fock::PairLoop`), because it already runs under rayon across the `3N` perturbations and an
1923
+ inner rayon pool was contending with the outer one for the same threads.
1924
+ - Together: a 150-atom frequency calculation went **23.2 s → 15.4 s (1.51×)**, with the Rust
1925
+ suite unchanged.
1926
+ - Opt-in phase timing with `AM1_TIMING=1`.
1927
+ - **Fixed a blind spot in that timing.** `report` was called from inside `run_am1`, and reporting
1928
+ clears the accumulator — so profiling a *gradient* or a *Hessian* printed only the SCF phases,
1929
+ and the single most expensive phase of those commands was invisible in the profile meant to
1930
+ find it. Reporting now belongs to the top-level caller. The CPHF work above is what that
1931
+ immediately revealed.
1932
+ - The ASE molecular path now runs **one** SCF per force call instead of two.
1933
+ - The divide-and-conquer DIIS history is stored as **packed upper triangles** with a memory
1934
+ budget rather than as full matrices at a fixed depth. A depth-8 history of densities and
1935
+ residuals is 16 dense matrices — 1.2 GB at 1536 atoms, most of the peak footprint, and growing
1936
+ quadratically, which is the wrong shape for the one part of the code meant for large systems.
1937
+ Packing is exact (both matrices are symmetric) and halves it; the budget shortens the history
1938
+ instead of letting it grow without bound.
1939
+ - **1.7× on the divide-and-conquer path** at 1029 atoms — 14.0–14.6 s down to 8.2–8.6 s, measured
1940
+ three times each on the same machine because a single pair of runs on this one differs by 70 %.
1941
+ The cost was in the DIIS, and it was invisible: the labelled phases summed to 8.8 s of a 16.1 s
1942
+ run, and the missing 7.3 s sat *between* the timers. `extrapolate` rebuilt the entire B matrix
1943
+ every iteration — all `n²` ordered pairs, both triangles — when every entry but the newest row
1944
+ is already known and cannot change. At 1029 atoms each packed residual is 16.9 MB, so that was
1945
+ 2.2 GB of memory traffic per SCF iteration for numbers already computed. Now the new row is
1946
+ computed on `push` and cached; `residual_dot` is a flat vectorizable `2·(packed dot) −
1947
+ (diagonal dot)` instead of a nested row walk; `pack` copies contiguous row runs instead of
1948
+ walking `nao²` through a 2D index; and the extrapolated density is accumulated packed and
1949
+ expanded once. Iteration counts are unchanged, as they must be — the cached values are the same
1950
+ values.
1951
+ - **The timing report says what it measures.** It sums *thread*-seconds, so a phase running on
1952
+ sixteen threads reports about sixteen times its wall clock. Read as wall clock it makes the
1953
+ best-parallelized phase look like the bottleneck: the CPHF Fock builds in a 102-atom frequency
1954
+ run report 39 s against a 4.8 s calculation. The header now states this, and the total is
1955
+ labelled `TOTAL (thread-seconds)`.
1956
+
1957
+ ### Test infrastructure
1958
+
1959
+ - The scaling benchmark's water clusters are spaced at 4.0 Å and the generator now **asserts**
1960
+ that no intermolecular contact is shorter than 1.8 Å. An earlier version used 3.1 Å with
1961
+ pseudo-random molecular orientations, which put hydrogens 1.22–1.35 Å apart — over a hundred
1962
+ pairs inside 1.6 Å at the larger sizes. The symptom was a cliff rather than a warning: the SCF
1963
+ converged in 14 iterations at 192, 375 and 648 atoms and then failed outright at 1029, which
1964
+ reads as a large-system divide-and-conquer defect and was nothing of the kind. Ice gets away
1965
+ with 2.76 Å because its molecules are oriented; a random-orientation benchmark cannot.
1966
+
1967
+ ### Known limitations
1968
+
1969
+ These are measured, not suspected. See `docs/scope.md`.
1970
+
1971
+ - **Ewald summation covers the monopole channel only.** In every dimensionality the `1/R` term is
1972
+ now summed exactly, but the `R⁻³` Klopman–Ohno correction and the higher multipoles are still a
1973
+ real-space cutoff on the lattice translation `|T|`. Consequences, all measured: a charged 3D
1974
+ cell converges to about 0.1 eV, limited by the logarithmically divergent `R⁻³` residual
1975
+ (0.10 eV per unit `ln r_c`); neutral cells still converge slowly in the residual channels
1976
+ (3e-4 eV between a 40 and a 640 Bohr cutoff).
1977
+ - **A charged slab or chain needs a stated convention.** The energy of a charged 2D or 1D cell is
1978
+ not defined without one — the neutralizing sheet's position enters a slab's energy, and a
1979
+ charged line's potential diverges logarithmically. Both are refused by default with an error
1980
+ naming the convention enum, rather than answered under a convention the caller never chose.
1981
+ - **NDDO exchange at Γ diverges** and is tapered by a quintic smoothstep at `exchange_cutoff`.
1982
+ This is a documented approximation, not a convergence parameter. k-point sampling makes the
1983
+ density matrix decay on its own and largely removes the dependence.
1984
+ - **Divide-and-conquer makes the diagonalization linear, not the whole calculation.** The NDDO
1985
+ Coulomb sum stays `O(N²)`. Measured exponents from operation counters: diagonalization 1.15,
1986
+ exchange 1.06, retained density blocks 1.05, Coulomb 2.02; on 3D clusters to 2187 atoms the
1987
+ fitted `Σn³` exponent is 1.25 against 3 for a full diagonalization. In wall clock it crosses
1988
+ over around 200 atoms; the 768-atom speedup ranged 1.4–6.3× across runs, which is machine load
1989
+ rather than the algorithm — hence counters, not a stopwatch. The open-shell analytic stress is
1990
+ refused rather than approximated: it needs the spin-resolved pair virial.
1991
+ - **The long-range monopole correction is not in the DFPT response.** Generalizing it to a
1992
+ `q`-point response needs a phased Ewald sum `Σ_T e^{iq·T}/|d+T|`, which is not implemented.
1993
+
1994
+ > **Corrected in 0.2.1.** This entry read "applied only at `q = 0`". In 0.2.0 it was not
1995
+ > applied at `q = 0` either — `force_constants_at_q` omitted it at every `q`, while
1996
+ > `pbc_hessian` included it. The phased sum **is** implemented in 0.2.1 for 3D cells; see the
1997
+ > 0.2.1 notes, including why the element dropped turned out to be `k = 0` and not the `G = 0`
1998
+ > this entry's successor originally assumed.
1999
+ - **`ε_∞` is a clamped-ion dipole response, not a Berry phase.** For a system where charge
2000
+ circulates around the periodic loop rather than responding locally, it is not the right
2001
+ quantity. Origin independence was measured (1.6e-15) rather than assumed.
2002
+ - **SAM1 is not implemented.** It replaces the multipole expansion with scaled STO-3G integrals,
2003
+ so it is a different integral engine rather than a reparameterization and does not fit the
2004
+ shared code path.
2005
+ - **AM1-BCC typing gaps** — 23 % of the bond parameters are unreachable, ring perception is not
2006
+ SSSR, aromaticity ignores ring size, and the bond-order table covers only C/N/O pairs. The
2007
+ correction values themselves are exact.
2008
+
2009
+ ### Packaging
2010
+
2011
+ - PEP 639 licence metadata with `license-files`, so `THIRD_PARTY_NOTICES.md` and the retained
2012
+ third-party licences ship **inside the wheel** — required by clause 2 of the BSD-3-Clause
2013
+ covering the bundled PySEQM-derived parameters.
2014
+ - `extension-module` split into its own Cargo feature, so `cargo test --features python` and the
2015
+ CLI link on Linux and macOS.
2016
+ - `abi3-py311`, `requires-python = ">=3.11"` (3.9 is end-of-life), trove classifiers,
2017
+ `[project.urls]`, keywords, and `dynamic = ["version"]` — the version now has one source,
2018
+ `Cargo.toml`, read back through `importlib.metadata`.
2019
+ - `rust-version = "1.75"`, crate metadata, and `docs.rs` configuration.
2020
+ - **Wheels are built and published for every common platform** (`.github/workflows/release.yml`):
2021
+ manylinux and musllinux on x86_64 and aarch64, macOS on both architectures, Windows x64, plus
2022
+ the sdist, published to PyPI by trusted publishing on a version tag. `abi3-py311` means one
2023
+ wheel per platform covers 3.11 and up. This is the real defence against a failed install: a
2024
+ source install builds under the shipping profile — fat LTO, one codegen unit — which measures
2025
+ 1.9 GB peak resident and over ten minutes on a warm dependency cache, and on a small VM that is
2026
+ an out-of-memory failure rather than a slow install.
2027
+ - **The sdist is tested by installing it**, in CI, into a clean virtual environment, followed by
2028
+ running the console script. Three things can break a source install silently, and all three are
2029
+ now asserted: a file the *build* needs missing from the tarball (`[[bin]]` points at
2030
+ `src/bin/am1_rs.rs`, so cargo aborts without it even though no wheel ever contains that binary;
2031
+ PEP 639 `license-files` names `third_party/*/LICENSE`; the parameter CSVs are `include_str!`-ed),
2032
+ `Cargo.lock` absent so dependencies re-resolve forward on the user's machine, and the
2033
+ `extension-module` feature reaching a target that has to link Python's symbols. On that last
2034
+ one: maturin builds only the lib target, so the CLI binary is never linked during a `pip
2035
+ install` — verified against the build log rather than assumed, and now held by the test.
2036
+
2037
+ ## 0.1.3
2038
+
2039
+ ### Added
2040
+ - **Explicit RHF/UHF reference selection**, independent of the spin multiplicity — a closed-shell
2041
+ singlet can now be run either restricted or unrestricted (e.g. as a broken-symmetry starting
2042
+ point):
2043
+ - Rust: new `ScfReference` enum (`Auto` / `Restricted` / `Unrestricted`) and an
2044
+ `Am1Options.reference` field (default `Auto`, preserving previous behavior). `run_am1` honors
2045
+ it; `Restricted` on an open shell is rejected (no ROHF).
2046
+ - Python native: every function (`single_point`, `gradient`, `optimize`, `frequencies`,
2047
+ `hessian`) takes a `reference="auto"|"rhf"|"uhf"` keyword.
2048
+ - ASE: `AM1(..., reference="auto")`, or per structure via `atoms.info["reference"]`; a change
2049
+ invalidates cached results.
2050
+ - CLI: `--reference auto|rhf|uhf`, with `--rhf` / `--uhf` shortcuts.
2051
+
2052
+ ### Notes
2053
+ - `Auto` reproduces the historical selection (RHF for a closed-shell singlet, UHF for an open
2054
+ shell), so existing callers are unaffected. Forcing UHF on a symmetric singlet converges to the
2055
+ RHF energy (zero spin density).
2056
+
2057
+ ## 0.1.2
2058
+
2059
+ ### Added
2060
+ - **Hessian API for Python.** Both layers now expose the analytic (CPHF) Cartesian Hessian,
2061
+ which previously was only reachable indirectly through `frequencies`:
2062
+ - `am1_rs.hessian(numbers, positions, charge=0.0, multiplicity=1)` returns the full `3N × 3N`
2063
+ matrix in **atomic units** (`hessian_hartree_per_bohr2`) and, for convenience, in eV/Ų
2064
+ (`hessian_ev_per_angstrom2`), plus `ndof`. Row/column `3*i + k` is atom `i`, axis `k`.
2065
+ - `am1_rs.ase.AM1.get_hessian(atoms=None)` returns the Hessian as a NumPy array in **eV/Ų**
2066
+ (ASE convention). Closed-shell RHF and open-shell UHF are both supported.
2067
+ - **Per-structure charge / multiplicity for the ASE calculator.** In addition to the constructor
2068
+ arguments `AM1(charge=…, multiplicity=…)`, charge and spin multiplicity may now be supplied at
2069
+ calculation time via `atoms.info["charge"]` / `atoms.info["multiplicity"]`. An `atoms.info`
2070
+ entry overrides the constructor value for that structure, and a change in either invalidates
2071
+ cached results (`check_state`).
2072
+
2073
+ ### Verified
2074
+ - Confirmed (with tests) that charge and spin multiplicity are received and actually used in
2075
+ both the Python-native functions (per call) and the ASE calculator (at construction *and* at
2076
+ calculation time): charge and multiplicity change the SCF energy, and an electron-count /
2077
+ multiplicity parity mismatch raises. See `tests/test_python_api.py`.
2078
+
2079
+ ### Notes
2080
+ - Units are unchanged and follow each layer's convention: the native surface reports atomic
2081
+ units (Hartree/Bohr²) with eV/Ų provided alongside; the ASE layer reports eV/Ų.
2082
+ - No changes to the Rust crate's public API (the analytic Hessian was already available there as
2083
+ `am1_rs::analytic_hessian`, eV/Bohr²).