Summary
With the Julia extension active, an ensemble returned by Ensemble.split() has forces_qspace equal to zero. The Fourier (Julia) gradient then ignores the real forces, and a minimization on a split ensemble produces a wrong gradient and diverges.
Where (master 70c3060d, same code in v1.6 / 1.6.1)
When fourier_gradient is on, the ensemble keeps q-space copies of displacements and forces. The Julia gradient uses only those copies:
delta_forces = self.forces_qspace - self.sscha_forces_qspace # Modules/Ensemble.py L2561, get_fourier_gradient
These copies are built from the real-space forces in Ensemble.init() (L434 onwards). load_bin and compute_ensemble call self.init() after the forces are set (e.g. L1242), so those paths are consistent.
Ensemble.split() (L4194-4235) does not do this:
ens = Ensemble(self.dyn_0, ...) and ens.init_from_structures(structs) (L4219). At this point the forces are not known yet, so init_from_structures sets self.forces_qspace = np.zeros_like(self.u_disps_qspace) (L1306).
- The forces are copied in place into the real-space array:
ens.forces[:, :, :] = self.forces[split_mask, :, :] (L4225). Nothing updates forces_qspace.
- It calls the real-space
ens.update_weights(...) (L4231), not update_weights_fourier, and never calls ens.init().
So in the split ensemble forces_qspace == 0, and the Julia gradient becomes <u (0 - f_sscha)> instead of <u (f - f_sscha)>.
Observed (cubic perovskite, 5 atoms, 4x4x4, 10032 configurations, Julia 1.12.5)
| ensemble |
FC gradient at step 0 |
full ensemble (load_bin) |
208 bohr^2 |
ens.split(np.ones(N, bool)), i.e. the same configurations |
142800 bohr^2, then nan at the next step |
| same split, without Julia |
208 bohr^2 (correct: the real-space path uses forces, which are copied correctly) |
Minimizations on split subsets of 1000, 2000, 5000 and 8000 configurations all ran away to |gc| ~ 1.38e5 and stopped on the Kong-Liu threshold (KL/N ~ 0.46). The final value was the same whatever N was, i.e. independent of the data.
Minimal reproduction (Julia available)
ens = sscha.Ensemble.Ensemble(dyn, T, supercell=dyn.GetSupercell()); ens.load_bin(data_dir, 0)
sub = ens.split(np.ones(ens.N, dtype=bool))
print(np.abs(ens.forces_qspace).max(), np.abs(sub.forces_qspace).max()) # nonzero vs 0.0
Suggested fix
Call ens.init() at the end of split(), after forces and stresses are copied. Alternatively, assign the arrays (ens.forces = ...) so that the q-space copies are rebuilt.
Related
Workaround we use
Slice the *_pop0.npy arrays and reload with load_bin, which calls init(). On the full set this reproduces the original minimization exactly (same 22 steps, |gc| = 0.9615).
Environment
python-sscha 1.6.0 (installed), checked against tag v1.6.1 and master; cellconstructor 1.6.0; Julia 1.12.5.
Summary
With the Julia extension active, an ensemble returned by
Ensemble.split()hasforces_qspaceequal to zero. The Fourier (Julia) gradient then ignores the real forces, and a minimization on a split ensemble produces a wrong gradient and diverges.Where (master
70c3060d, same code in v1.6 / 1.6.1)When
fourier_gradientis on, the ensemble keeps q-space copies of displacements and forces. The Julia gradient uses only those copies:These copies are built from the real-space forces in
Ensemble.init()(L434 onwards).load_binandcompute_ensemblecallself.init()after the forces are set (e.g. L1242), so those paths are consistent.Ensemble.split()(L4194-4235) does not do this:ens = Ensemble(self.dyn_0, ...)andens.init_from_structures(structs)(L4219). At this point the forces are not known yet, soinit_from_structuressetsself.forces_qspace = np.zeros_like(self.u_disps_qspace)(L1306).ens.forces[:, :, :] = self.forces[split_mask, :, :](L4225). Nothing updatesforces_qspace.ens.update_weights(...)(L4231), notupdate_weights_fourier, and never callsens.init().So in the split ensemble
forces_qspace == 0, and the Julia gradient becomes<u (0 - f_sscha)>instead of<u (f - f_sscha)>.Observed (cubic perovskite, 5 atoms, 4x4x4, 10032 configurations, Julia 1.12.5)
load_bin)ens.split(np.ones(N, bool)), i.e. the same configurationsnanat the next stepforces, which are copied correctly)Minimizations on split subsets of 1000, 2000, 5000 and 8000 configurations all ran away to |gc| ~ 1.38e5 and stopped on the Kong-Liu threshold (KL/N ~ 0.46). The final value was the same whatever N was, i.e. independent of the data.
Minimal reproduction (Julia available)
Suggested fix
Call
ens.init()at the end ofsplit(), after forces and stresses are copied. Alternatively, assign the arrays (ens.forces = ...) so that the q-space copies are rebuilt.Related
init_from_structures(), but it does not touchforces_qspaceaftersplit(), so this issue remains after Ensemble: opt-in linear-memory q-space mode, plus four fixes on the Fourier path #428.get_preconditioned_gradient_parallelitself callssplit()at every step. It is only used without Julia, so this bug does not combine with the constant-error issue ([BUG] Without Julia, the FC-gradient error is a constant matrix of ones (get_preconditioned_gradient_parallel), so the FC stop criterion is meaningless #435), but any future use ofsplit()with Julia is affected.Workaround we use
Slice the
*_pop0.npyarrays and reload withload_bin, which callsinit(). On the full set this reproduces the original minimization exactly (same 22 steps, |gc| = 0.9615).Environment
python-sscha 1.6.0 (installed), checked against tag v1.6.1 and master; cellconstructor 1.6.0; Julia 1.12.5.