Coverage for gpaw/test/response/test_heisenberg.py: 100%

92 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2025-07-09 00:21 +0000

1"""Test the Heisenberg model based methodology of the response code.""" 

2 

3import numpy as np 

4import pytest 

5 

6from gpaw.response.heisenberg import (calculate_fm_magnon_energies, 

7 calculate_single_site_magnon_energies) 

8 

9 

10@pytest.mark.ci 

11def test_single_site_magnons(): 

12 """Check the single site magnon dispersion functionality.""" 

13 rng = get_rng() 

14 # ---------- Inputs ---------- # 

15 

16 # Magnetic moment 

17 mm = 1. 

18 # q-point grid 

19 nq = 11 

20 q_qc = get_randomized_qpoints(nq, rng) 

21 

22 # Random J_q, with J=0 at q=0 

23 J_q = rng.rand(q_qc.shape[0]) 

24 J_q[list(q_qc[:, 2]).index(0.)] = 0. 

25 

26 # Cosine J_qD with different spin wave stiffnesses D 

27 D_D = np.linspace(400., 800., 5) 

28 J_qD = np.outer(np.cos(q_qc[:, 2]), D_D) 

29 

30 # ---------- Script ---------- # 

31 

32 # Calculate magnon energies 

33 E_q = calculate_single_site_magnon_energies(J_q, q_qc, mm) 

34 E_qD = calculate_single_site_magnon_energies(J_qD, q_qc, mm) 

35 

36 # Check dimensions of arrays 

37 assert E_q.shape == (q_qc.shape[0],) 

38 assert E_qD.shape == J_qD.shape 

39 

40 # Check versus formulas 

41 assert np.allclose(E_q, -2. / mm * J_q) # Remember: J(0) = 0 

42 assert np.allclose(E_qD, 2. / mm * D_D[np.newaxis, :] 

43 * (1. - np.cos(q_qc[:, 2]))[:, np.newaxis]) 

44 

45 

46@pytest.mark.ci 

47def test_single_site_magnons_consistency(): 

48 """Check that the generalized magnon dispersion calculation is consistent 

49 for a single site system with the simple analytical formula valid in that 

50 case.""" 

51 rng = get_rng() 

52 # ---------- Inputs ---------- # 

53 

54 # Magnetic moment 

55 mm = 1. 

56 # q-point grid 

57 nq = 11 

58 q_qc = get_randomized_qpoints(nq, rng) 

59 

60 # Random isotropic exchange constants 

61 nJsamples = 6 # sample some different random combinations 

62 J_qx = rng.rand(q_qc.shape[0], nJsamples) 

63 

64 # ---------- Script ---------- # 

65 

66 # Calculate assuming a single site 

67 E_qx = calculate_single_site_magnon_energies(J_qx, q_qc, mm) 

68 

69 # Calcualte using generalized functionality 

70 E_qnx = calculate_fm_magnon_energies(J_qx[:, np.newaxis, np.newaxis, :], 

71 q_qc, mm * np.ones((1, nJsamples))) 

72 

73 # Test self-consistency 

74 assert E_qnx.shape[0] == E_qx.shape[0] 

75 assert E_qnx.shape[1] == 1 

76 assert E_qnx.shape[-1] == E_qx.shape[-1] 

77 assert np.allclose(E_qnx[:, 0, :], E_qx, atol=1e-8) 

78 

79 

80@pytest.mark.ci 

81def test_fm_random_magnons(): 

82 """Check that the functionality to calculate the magnon dispersion of a 

83 ferromagnetic system with multiple sites works for a randomized system with 

84 three sites.""" 

85 rng = get_rng() 

86 # ---------- Inputs ---------- # 

87 

88 # Magnetic moments 

89 nsites = 3 

90 mm_a = 5. * rng.rand(nsites) 

91 # q-point grid 

92 nq = 11 

93 q_qc = get_randomized_qpoints(nq, rng) 

94 

95 # Random isotropic exchange constants 

96 J_qab = 1.j * rng.rand(q_qc.shape[0], nsites, nsites) 

97 J_qab += rng.rand(q_qc.shape[0], nsites, nsites) 

98 # Take the Hermitian part of random tensor 

99 J_qab = (J_qab + np.transpose(np.conjugate(J_qab), (0, 2, 1))) / 2. 

100 # The q=0 component should furthermore be real 

101 J_qab[list(q_qc[:, 2]).index(0.)].imag = 0. 

102 

103 # ---------- Script ---------- # 

104 

105 # Calculate magnon energies 

106 E_qn = calculate_fm_magnon_energies(J_qab, q_qc, mm_a) 

107 E_qn = np.sort(E_qn, axis=1) # Make sure the eigenvalues are sorted 

108 

109 # Calculate the magnon energies manually 

110 mm_inv_ab = 2. / np.sqrt(np.outer(mm_a, mm_a)) 

111 J0_ab = np.diag(np.sum(J_qab[list(q_qc[:, 2]).index(0.)], axis=1)) 

112 H_qab = mm_inv_ab[np.newaxis] * (J0_ab[np.newaxis] - J_qab) 

113 test_E_qn, _ = np.linalg.eig(H_qab) 

114 

115 assert E_qn.shape == (q_qc.shape[0], nsites) 

116 assert np.allclose(test_E_qn.imag, 0.) 

117 assert np.allclose(E_qn, np.sort(test_E_qn.real, axis=1)) 

118 

119 

120@pytest.mark.ci 

121def test_fm_vectorized_magnons(): 

122 """Check that the functionality to calculate the magnon dispersion of a 

123 ferromagnetic system with multiple sites works when supplying multiple 

124 sets of parameters for the same two-site systems.""" 

125 rng = get_rng() 

126 # ---------- Inputs ---------- # 

127 

128 # Magnetic moments 

129 nsites = 2 

130 nmagmoms = 4 # Test the same J_qab, but with different site magnetizations 

131 mm_ax = 5. * rng.rand(nsites, nmagmoms) 

132 # q-point grid 

133 nq = 11 

134 q_qc = get_randomized_qpoints(nq, rng) 

135 

136 # Use a fixed structure for J_qab with known eigenvalues 

137 cos_q = np.cos(q_qc[:, 2]) 

138 sin_q = np.sin(q_qc[:, 2]) 

139 J_qab = np.empty((nq, nsites, nsites), dtype=complex) 

140 J_qab[:, 0, 0] = cos_q 

141 J_qab[:, 0, 1] = 1. + 1.j * sin_q 

142 J_qab[:, 1, 0] = 1. - 1.j * sin_q 

143 J_qab[:, 1, 1] = 2. * cos_q 

144 

145 # Test different energy scales for the exchange interactions 

146 nJscales = 6 

147 Jscale_y = 800. * rng.rand(nJscales) 

148 

149 # Combine different magnetic moments and scale for the exchange 

150 J_qabxy = np.empty(J_qab.shape + (nmagmoms, nJscales,), dtype=complex) 

151 J_qabxy[:] = np.tensordot(J_qab, Jscale_y, 

152 axes=((), ()))[..., np.newaxis, :] 

153 mm_axy = np.moveaxis(np.tile(mm_ax, (nJscales, 1, 1)), 0, -1) 

154 

155 # ---------- Script ---------- # 

156 

157 # Calculate magnon energies 

158 E_qnxy = calculate_fm_magnon_energies(J_qabxy, q_qc, mm_axy) 

159 E_qnxy = np.sort(E_qnxy, axis=1) # Make sure the eigenvalues are sorted 

160 

161 # Calculate magnon energies analytically 

162 H_diag1_qxy = np.sqrt(mm_axy[1][np.newaxis] / mm_axy[0][np.newaxis])\ 

163 * (2. - cos_q[:, np.newaxis, np.newaxis]) 

164 H_diag2_qxy = np.sqrt(mm_axy[0][np.newaxis] / mm_axy[1][np.newaxis])\ 

165 * (3. - 2. * cos_q[:, np.newaxis, np.newaxis]) 

166 H_diag_avg_qxy = (H_diag1_qxy + H_diag2_qxy) / 2. 

167 H_diag_diff_qxy = (H_diag1_qxy - H_diag2_qxy) / 2. 

168 pm_n = np.array([-1., 1.]) 

169 E_test_qnxy = H_diag_avg_qxy[:, np.newaxis]\ 

170 + pm_n[np.newaxis, :, np.newaxis, np.newaxis]\ 

171 * np.sqrt(H_diag_diff_qxy[:, np.newaxis]**2. 

172 + (1 + sin_q[:, np.newaxis, np.newaxis, np.newaxis]**2.)) 

173 E_test_qnxy *= 2. / np.sqrt(np.prod(mm_axy, axis=0)) 

174 E_test_qnxy *= Jscale_y 

175 

176 assert np.allclose(E_qnxy, E_test_qnxy) 

177 

178 

179# ---------- Test functionality ---------- # 

180 

181 

182def get_randomized_qpoints(nq, rng): 

183 """Make a simple, but shuffled, q-point array.""" 

184 q_qc = np.zeros((nq, 3), dtype=float) 

185 q_qc[:, 2] = np.linspace(0., np.pi, nq) 

186 rng.shuffle(q_qc[:, 2]) 

187 

188 return q_qc 

189 

190 

191def get_rng(): 

192 """Choose a specific random seed to make the tests reproducible.""" 

193 rng = np.random.RandomState(23) 

194 

195 return rng