am1-rs-python 0.2.2__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 (187) hide show
  1. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/CHANGELOG.md +677 -0
  2. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/Cargo.lock +1 -1
  3. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/Cargo.toml +1 -1
  4. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/PKG-INFO +83 -95
  5. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/README.md +78 -94
  6. am1_rs_python-0.2.3/THIRD_PARTY_LICENSES.md +4249 -0
  7. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/THIRD_PARTY_NOTICES.md +67 -3
  8. am1_rs_python-0.2.3/docs/README.md +67 -0
  9. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/docs/divide-conquer.md +39 -0
  10. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/docs/methods.md +9 -4
  11. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/docs/pbc.md +168 -1
  12. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/docs/python-api.md +146 -8
  13. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/docs/rust-api.md +80 -1
  14. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/docs/scope.md +9 -2
  15. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/docs/theory.md +75 -5
  16. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/pyproject.toml +24 -0
  17. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/python/am1_rs/__init__.py +2 -0
  18. am1_rs_python-0.2.3/python/am1_rs/__main__.py +1074 -0
  19. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/python/am1_rs/_native.pyi +21 -9
  20. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/python/am1_rs/ase.py +141 -19
  21. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/python/am1_rs/native.py +262 -24
  22. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/bcc/mod.rs +24 -6
  23. am1_rs_python-0.2.3/src/bin/am1_rs.rs +1359 -0
  24. am1_rs_python-0.2.3/src/data_tables.rs +224 -0
  25. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/divide_conquer.rs +162 -4
  26. am1_rs_python-0.2.3/src/error.rs +187 -0
  27. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/fermi.rs +24 -0
  28. am1_rs_python-0.2.3/src/gto.rs +517 -0
  29. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/hessian.rs +575 -77
  30. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/integrals.rs +26 -0
  31. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/ir.rs +14 -8
  32. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/lib.rs +24 -8
  33. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/linalg.rs +30 -3
  34. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/method.rs +16 -0
  35. am1_rs_python-0.2.3/src/molden.rs +777 -0
  36. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/optimizer.rs +95 -56
  37. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/params.rs +9 -4
  38. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/dfpt.rs +464 -71
  39. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/hessian.rs +24 -10
  40. am1_rs_python-0.2.3/src/pbc/kerker.rs +232 -0
  41. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/mod.rs +20 -0
  42. am1_rs_python-0.2.3/src/pbc/optimizer.rs +479 -0
  43. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/phonon.rs +325 -22
  44. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/scf.rs +666 -96
  45. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/python.rs +449 -44
  46. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/scf.rs +153 -10
  47. am1_rs_python-0.2.3/tests/atomic_data.rs +175 -0
  48. am1_rs_python-0.2.3/tests/attribution.rs +346 -0
  49. am1_rs_python-0.2.3/tests/cphf_controls.rs +115 -0
  50. am1_rs_python-0.2.3/tests/cpscf_kerker.rs +240 -0
  51. am1_rs_python-0.2.3/tests/dc_optimize.rs +311 -0
  52. am1_rs_python-0.2.3/tests/dfpt_profile.rs +115 -0
  53. am1_rs_python-0.2.3/tests/fractional_occupations.rs +326 -0
  54. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/ir.rs +39 -16
  55. am1_rs_python-0.2.3/tests/pbc_bloch_batch.rs +136 -0
  56. am1_rs_python-0.2.3/tests/pbc_dfpt_contraction.rs +253 -0
  57. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_hessian.rs +41 -4
  58. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_phonon.rs +87 -4
  59. am1_rs_python-0.2.3/tests/phonon_acoustic.rs +317 -0
  60. am1_rs_python-0.2.3/tests/phonon_interpolation.rs +266 -0
  61. am1_rs_python-0.2.3/tests/scf_hardening.rs +136 -0
  62. am1_rs_python-0.2.3/tests/test_binding_arity.py +127 -0
  63. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/test_cli.py +40 -4
  64. am1_rs_python-0.2.3/tests/test_cli_matrix.py +442 -0
  65. am1_rs_python-0.2.3/tests/test_fractional_occupations.py +111 -0
  66. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/test_new_api_0_2_1.py +34 -11
  67. am1_rs_python-0.2.3/tests/test_phonon_eigenvectors.py +112 -0
  68. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/test_python_api.py +8 -1
  69. am1_rs_python-0.2.3/tests/test_result_contract.py +285 -0
  70. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/third_party/antechamber/README.md +12 -4
  71. am1_rs_python-0.2.3/third_party/mopac/README.md +127 -0
  72. am1_rs_python-0.2.3/third_party/pyseqm/README.md +60 -0
  73. am1_rs_python-0.2.3/tools/bench.py +159 -0
  74. am1_rs_python-0.2.3/tools/collect_dependency_licenses.py +289 -0
  75. am1_rs_python-0.2.3/tools/extract_rm1_parameters.py +145 -0
  76. am1_rs_python-0.2.3/tools/make_water_cluster.py +132 -0
  77. am1_rs_python-0.2.3/tools/profile.py +82 -0
  78. am1_rs_python-0.2.3/tools/profile_dc.py +110 -0
  79. am1_rs_python-0.2.2/python/am1_rs/__main__.py +0 -397
  80. am1_rs_python-0.2.2/src/bin/am1_rs.rs +0 -492
  81. am1_rs_python-0.2.2/src/data_tables.rs +0 -128
  82. am1_rs_python-0.2.2/src/error.rs +0 -91
  83. am1_rs_python-0.2.2/src/molden.rs +0 -358
  84. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/LICENSE +0 -0
  85. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_chain_102.xyz +0 -0
  86. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_chain_201.xyz +0 -0
  87. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_chain_399.xyz +0 -0
  88. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_chain_801.xyz +0 -0
  89. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_cluster_102.xyz +0 -0
  90. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_cluster_201.xyz +0 -0
  91. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_cluster_399.xyz +0 -0
  92. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_cluster_48.xyz +0 -0
  93. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_cluster_801.xyz +0 -0
  94. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench102.xyz +0 -0
  95. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/ethanol.xyz +0 -0
  96. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/methane.xyz +0 -0
  97. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/water.xyz +0 -0
  98. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/python/am1_rs/py.typed +0 -0
  99. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/basis.rs +0 -0
  100. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/bcc/atomtype.rs +0 -0
  101. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/constants.rs +0 -0
  102. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/data/am1_parameters.csv +0 -0
  103. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/data/rm1_parameters.csv +0 -0
  104. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/dipole.rs +0 -0
  105. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/dual.rs +0 -0
  106. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/dual2.rs +0 -0
  107. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/farfield.rs +0 -0
  108. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/fock.rs +0 -0
  109. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/gradient.rs +0 -0
  110. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/hamiltonian.rs +0 -0
  111. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/lattice.rs +0 -0
  112. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/math.rs +0 -0
  113. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/neighbors.rs +0 -0
  114. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/overlap.rs +0 -0
  115. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/overlap_numeric.rs +0 -0
  116. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/berry.rs +0 -0
  117. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/complex.rs +0 -0
  118. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/ewald.rs +0 -0
  119. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/ewald1d.rs +0 -0
  120. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/ewald2d.rs +0 -0
  121. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/extent.rs +0 -0
  122. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/finite_field.rs +0 -0
  123. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/gradient.rs +0 -0
  124. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/kpoints.rs +0 -0
  125. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/repulsion.rs +0 -0
  126. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/system.rs +0 -0
  127. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/timing.rs +0 -0
  128. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/topology.rs +0 -0
  129. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/auxiliary_integral_impact.rs +0 -0
  130. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/axis_alignment.rs +0 -0
  131. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/bcc_atom_types.rs +0 -0
  132. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/bcc_bond_types.rs +0 -0
  133. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/charged_cell_warning.rs +0 -0
  134. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/core_core_derivatives.rs +0 -0
  135. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/cphf_convergence.rs +0 -0
  136. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/dc_convergence_probe.rs +0 -0
  137. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/dc_open_shell_stress.rs +0 -0
  138. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/dc_periodic.rs +0 -0
  139. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/dc_profile.rs +0 -0
  140. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/dc_where_the_time_goes.rs +0 -0
  141. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/divide_conquer.rs +0 -0
  142. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/element_coverage.rs +0 -0
  143. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/external_field.rs +0 -0
  144. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/farfield.rs +0 -0
  145. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/farfield_tree.rs +0 -0
  146. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/molecules.rs +0 -0
  147. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/mopac_reference.rs +0 -0
  148. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/orbital_response.rs +0 -0
  149. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/parameter_cache.rs +0 -0
  150. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_berry.rs +0 -0
  151. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_born_charges.rs +0 -0
  152. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_charged.rs +0 -0
  153. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_dense_stress.rs +0 -0
  154. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_dfpt.rs +0 -0
  155. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_dielectric.rs +0 -0
  156. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_dielectric_extent.rs +0 -0
  157. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_ewald.rs +0 -0
  158. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_exchange_diagnosis.rs +0 -0
  159. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_external_field.rs +0 -0
  160. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_finite_field.rs +0 -0
  161. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_gamma.rs +0 -0
  162. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_gradient.rs +0 -0
  163. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_klopman_ohno_tail.rs +0 -0
  164. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_kpoint_hessian.rs +0 -0
  165. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_kpoints.rs +0 -0
  166. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_lo_to.rs +0 -0
  167. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_lowdim_ewald.rs +0 -0
  168. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_phased_ewald.rs +0 -0
  169. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_scf_convergence.rs +0 -0
  170. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_truncation_study.rs +0 -0
  171. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_uhf_response.rs +0 -0
  172. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/phased_lowdim.rs +0 -0
  173. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/phonon_determinism.rs +0 -0
  174. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/response_memory.rs +0 -0
  175. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/rm1.rs +0 -0
  176. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/scalar_special.rs +0 -0
  177. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/scaling.rs +0 -0
  178. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/test_ase_pbc_md.py +0 -0
  179. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/test_divide_conquer.py +0 -0
  180. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/test_lazy_cache.py +0 -0
  181. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/theory_components.rs +0 -0
  182. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/topology_bcc.rs +0 -0
  183. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/third_party/antechamber/ATOMTYPE_BCC.DEF +0 -0
  184. /am1_rs_python-0.2.2/src/data/bccparm.dat → /am1_rs_python-0.2.3/third_party/antechamber/BCCPARM.DAT +0 -0
  185. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/third_party/antechamber/LICENSE +0 -0
  186. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/third_party/mopac/LICENSE +0 -0
  187. {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/third_party/pyseqm/LICENSE +0 -0
@@ -1,5 +1,682 @@
1
1
  # Changelog
2
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
+
3
680
  ## 0.2.2
4
681
 
5
682
  ### Fixed