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.
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/CHANGELOG.md +677 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/Cargo.lock +1 -1
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/Cargo.toml +1 -1
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/PKG-INFO +83 -95
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/README.md +78 -94
- am1_rs_python-0.2.3/THIRD_PARTY_LICENSES.md +4249 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/THIRD_PARTY_NOTICES.md +67 -3
- am1_rs_python-0.2.3/docs/README.md +67 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/docs/divide-conquer.md +39 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/docs/methods.md +9 -4
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/docs/pbc.md +168 -1
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/docs/python-api.md +146 -8
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/docs/rust-api.md +80 -1
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/docs/scope.md +9 -2
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/docs/theory.md +75 -5
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/pyproject.toml +24 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/python/am1_rs/__init__.py +2 -0
- am1_rs_python-0.2.3/python/am1_rs/__main__.py +1074 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/python/am1_rs/_native.pyi +21 -9
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/python/am1_rs/ase.py +141 -19
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/python/am1_rs/native.py +262 -24
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/bcc/mod.rs +24 -6
- am1_rs_python-0.2.3/src/bin/am1_rs.rs +1359 -0
- am1_rs_python-0.2.3/src/data_tables.rs +224 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/divide_conquer.rs +162 -4
- am1_rs_python-0.2.3/src/error.rs +187 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/fermi.rs +24 -0
- am1_rs_python-0.2.3/src/gto.rs +517 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/hessian.rs +575 -77
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/integrals.rs +26 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/ir.rs +14 -8
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/lib.rs +24 -8
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/linalg.rs +30 -3
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/method.rs +16 -0
- am1_rs_python-0.2.3/src/molden.rs +777 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/optimizer.rs +95 -56
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/params.rs +9 -4
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/dfpt.rs +464 -71
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/hessian.rs +24 -10
- am1_rs_python-0.2.3/src/pbc/kerker.rs +232 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/mod.rs +20 -0
- am1_rs_python-0.2.3/src/pbc/optimizer.rs +479 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/phonon.rs +325 -22
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/scf.rs +666 -96
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/python.rs +449 -44
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/scf.rs +153 -10
- am1_rs_python-0.2.3/tests/atomic_data.rs +175 -0
- am1_rs_python-0.2.3/tests/attribution.rs +346 -0
- am1_rs_python-0.2.3/tests/cphf_controls.rs +115 -0
- am1_rs_python-0.2.3/tests/cpscf_kerker.rs +240 -0
- am1_rs_python-0.2.3/tests/dc_optimize.rs +311 -0
- am1_rs_python-0.2.3/tests/dfpt_profile.rs +115 -0
- am1_rs_python-0.2.3/tests/fractional_occupations.rs +326 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/ir.rs +39 -16
- am1_rs_python-0.2.3/tests/pbc_bloch_batch.rs +136 -0
- am1_rs_python-0.2.3/tests/pbc_dfpt_contraction.rs +253 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_hessian.rs +41 -4
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_phonon.rs +87 -4
- am1_rs_python-0.2.3/tests/phonon_acoustic.rs +317 -0
- am1_rs_python-0.2.3/tests/phonon_interpolation.rs +266 -0
- am1_rs_python-0.2.3/tests/scf_hardening.rs +136 -0
- am1_rs_python-0.2.3/tests/test_binding_arity.py +127 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/test_cli.py +40 -4
- am1_rs_python-0.2.3/tests/test_cli_matrix.py +442 -0
- am1_rs_python-0.2.3/tests/test_fractional_occupations.py +111 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/test_new_api_0_2_1.py +34 -11
- am1_rs_python-0.2.3/tests/test_phonon_eigenvectors.py +112 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/test_python_api.py +8 -1
- am1_rs_python-0.2.3/tests/test_result_contract.py +285 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/third_party/antechamber/README.md +12 -4
- am1_rs_python-0.2.3/third_party/mopac/README.md +127 -0
- am1_rs_python-0.2.3/third_party/pyseqm/README.md +60 -0
- am1_rs_python-0.2.3/tools/bench.py +159 -0
- am1_rs_python-0.2.3/tools/collect_dependency_licenses.py +289 -0
- am1_rs_python-0.2.3/tools/extract_rm1_parameters.py +145 -0
- am1_rs_python-0.2.3/tools/make_water_cluster.py +132 -0
- am1_rs_python-0.2.3/tools/profile.py +82 -0
- am1_rs_python-0.2.3/tools/profile_dc.py +110 -0
- am1_rs_python-0.2.2/python/am1_rs/__main__.py +0 -397
- am1_rs_python-0.2.2/src/bin/am1_rs.rs +0 -492
- am1_rs_python-0.2.2/src/data_tables.rs +0 -128
- am1_rs_python-0.2.2/src/error.rs +0 -91
- am1_rs_python-0.2.2/src/molden.rs +0 -358
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/LICENSE +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_chain_102.xyz +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_chain_201.xyz +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_chain_399.xyz +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_chain_801.xyz +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_cluster_102.xyz +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_cluster_201.xyz +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_cluster_399.xyz +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_cluster_48.xyz +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench/water_cluster_801.xyz +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/bench102.xyz +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/ethanol.xyz +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/methane.xyz +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/examples/water.xyz +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/python/am1_rs/py.typed +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/basis.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/bcc/atomtype.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/constants.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/data/am1_parameters.csv +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/data/rm1_parameters.csv +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/dipole.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/dual.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/dual2.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/farfield.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/fock.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/gradient.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/hamiltonian.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/lattice.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/math.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/neighbors.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/overlap.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/overlap_numeric.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/berry.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/complex.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/ewald.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/ewald1d.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/ewald2d.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/extent.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/finite_field.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/gradient.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/pbc/kpoints.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/repulsion.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/system.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/timing.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/src/topology.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/auxiliary_integral_impact.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/axis_alignment.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/bcc_atom_types.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/bcc_bond_types.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/charged_cell_warning.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/core_core_derivatives.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/cphf_convergence.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/dc_convergence_probe.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/dc_open_shell_stress.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/dc_periodic.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/dc_profile.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/dc_where_the_time_goes.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/divide_conquer.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/element_coverage.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/external_field.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/farfield.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/farfield_tree.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/molecules.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/mopac_reference.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/orbital_response.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/parameter_cache.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_berry.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_born_charges.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_charged.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_dense_stress.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_dfpt.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_dielectric.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_dielectric_extent.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_ewald.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_exchange_diagnosis.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_external_field.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_finite_field.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_gamma.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_gradient.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_klopman_ohno_tail.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_kpoint_hessian.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_kpoints.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_lo_to.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_lowdim_ewald.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_phased_ewald.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_scf_convergence.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_truncation_study.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/pbc_uhf_response.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/phased_lowdim.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/phonon_determinism.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/response_memory.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/rm1.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/scalar_special.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/scaling.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/test_ase_pbc_md.py +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/test_divide_conquer.py +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/test_lazy_cache.py +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/theory_components.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/tests/topology_bcc.rs +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/third_party/antechamber/ATOMTYPE_BCC.DEF +0 -0
- /am1_rs_python-0.2.2/src/data/bccparm.dat → /am1_rs_python-0.2.3/third_party/antechamber/BCCPARM.DAT +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/third_party/antechamber/LICENSE +0 -0
- {am1_rs_python-0.2.2 → am1_rs_python-0.2.3}/third_party/mopac/LICENSE +0 -0
- {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
|