Coverage for gamdpy/integrators/NPT_Langevin.py: 44%

154 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1import numpy as np 

2import numba 

3from numba import cuda 

4import math 

5from numba.cuda.random import create_xoroshiro128p_states 

6from numba.cuda.random import xoroshiro128p_normal_float32 

7import gamdpy as gp 

8from .integrator import Integrator 

9 

10class NPT_Langevin(Integrator): 

11 """ Constant NPT Langevin integrator 

12 NPT Langevin Leap-frog integrator based on  

13 N. Grønbech-Jensen and Oded Farago, J. Chem. Phys. 141, 194108 (2014), 

14 `doi:10.1063/1.4901303 <https://doi.org/10.1063/1.4901303>`_  

15 """ 

16 

17 def __init__(self, temperature, pressure, alpha:float, alpha_baro, mass_baro, 

18 volume_velocity, barostatModeISO, boxFlucCoord, dt:float, seed:int) -> None: 

19 self.temperature = temperature 

20 self.pressure = pressure 

21 self.alpha = alpha 

22 self.alpha_baro = alpha_baro 

23 self.mass_baro = mass_baro 

24 self.volume_velocity = volume_velocity 

25 self.barostatModeISO = barostatModeISO 

26 self.boxFlucCoord = boxFlucCoord 

27 self.dt = dt 

28 self.seed = seed 

29 

30 def get_params(self, configuration: gp.Configuration, interactions_params: tuple, verbose=False) -> tuple: 

31 dt = np.float32(self.dt) 

32 alpha = np.float32(self.alpha) 

33 alpha_baro = np.float32(self.alpha_baro) 

34 mass_baro = np.float32(self.mass_baro) 

35 rng_states = create_xoroshiro128p_states(configuration.N+1, seed=self.seed) # +1 for barostat dynamics  

36 barostat_state = np.array([1.0, self.volume_velocity], dtype=np.float64) # [0] = new_vol / old_vol , [1] = vol velocity 

37 d_barostat_state = cuda.to_device(barostat_state) 

38 barostatVirial = np.array([0.0], dtype=np.float32) 

39 d_barostatVirial = cuda.to_device(barostatVirial) 

40 d_length_ratio = cuda.to_device(np.ones(3, dtype=np.float32)) 

41 return (dt, alpha, alpha_baro, mass_baro, # Needs to be compatible with unpacking in step() below 

42 self.barostatModeISO, np.int32(self.boxFlucCoord), 

43 rng_states, d_barostat_state, d_barostatVirial, d_length_ratio) 

44 

45 def get_kernel(self, configuration: gp.Configuration, compute_plan: dict, compute_flags:dict[str,bool], interactions_kernel, verbose=False): 

46 

47 # Unpack parameters from configuration and compute_plan 

48 D, num_part = configuration.D, configuration.N 

49 pb, tp, gridsync = [compute_plan[key] for key in ['pb', 'tp', 'gridsync']] 

50 num_blocks = (num_part - 1) // pb + 1 

51 

52 # Convert temperature and pressure to a functions id needed 

53 if callable(self.temperature): 

54 temperature_function = self.temperature 

55 else: 

56 temperature_function = gp.make_function_constant(value=float(self.temperature)) 

57 if callable(self.pressure): 

58 pressure_function = self.pressure 

59 else: 

60 pressure_function = gp.make_function_constant(value=float(self.pressure)) 

61 

62 if verbose: 

63 print(f'Generating NPT langevin integrator for {num_part} particles in {D} dimensions:') 

64 print(f'\tpb: {pb}, tp:{tp}, num_blocks:{num_blocks}') 

65 print(f'\tNumber (virtual) particles: {num_blocks * pb}') 

66 print(f'\tNumber of threads {num_blocks * pb * tp}') 

67 

68 # Unpack indices for vectors and scalars to be compiled into kernel 

69 compute_k = compute_flags['K'] 

70 compute_fsq = compute_flags['Fsq'] 

71 r_id, v_id, f_id = [configuration.vectors.indices[key] for key in ['r', 'v', 'f']] 

72 m_id = configuration.sid['m'] 

73 if not compute_flags['W']: 

74 raise ValueError("NPT_Langevin requires virial") 

75 else: 

76 w_id = configuration.sid['W'] 

77 

78 if compute_k: 

79 k_id = configuration.sid['K'] 

80 if compute_fsq: 

81 fsq_id = configuration.sid['Fsq'] 

82 

83 

84 # JIT compile functions to be compiled into kernel 

85 temperature_function = numba.njit(temperature_function) 

86 pressure_function = numba.njit(pressure_function) 

87 apply_PBC = numba.njit(configuration.simbox.get_apply_PBC()) 

88 

89 

90 def copyParticleVirial(scalars, integrator_params): 

91 dt, alpha, alpha_baro, mass_baro, barostatModeISO, boxFlucCoord, rng_states, barostat_state, barostatVirial, length_ratio = integrator_params 

92 global_id, my_t = cuda.grid(2) 

93 my_w = scalars[global_id][w_id] 

94 

95 #reset the barostatVirial to zero  

96 #if global_id == 0 and my_t == 0: 

97 # barostatVirial[0] = numba.float32(0.0) 

98 # # Not safe to set to zero like this! (moved to end of update_barostat_state) 

99 #cuda.syncthreads() # - particles in other blocks might allready have added their w 

100 

101 if global_id < num_part and my_t == 0: 

102 cuda.atomic.add(barostatVirial, 0, my_w) # factor of 6 already accounted for using virial_factor and virial_factor_NIII 

103 

104 return 

105 

106 def update_barostat_state(sim_box, integrator_params, time): 

107 dt, alpha, alpha_baro, mass_baro, barostatModeISO, boxFlucCoord, rng_states, barostat_state, barostatVirial, length_ratio = integrator_params 

108 temperature = temperature_function(time) 

109 pressure = pressure_function(time) 

110 

111 global_id, my_t = cuda.grid(2) 

112 if global_id == 0 and my_t == 0: 

113 

114 #Copy barostat_state into current_barostat_state using a local aaray 

115 current_barostat_state = cuda.local.array(2, numba.float64) 

116 current_barostat_state[0] = barostat_state[0] 

117 current_barostat_state[1] = barostat_state[1] 

118 

119 volume = sim_box[0]*sim_box[1]*sim_box[2] 

120 targetConfPressure = pressure - temperature * num_part / volume 

121 barostatForce = barostatVirial[0] / volume - targetConfPressure 

122 

123 random_number = xoroshiro128p_normal_float32(rng_states, 0) # 0th random number state is reserved for barostat 

124 barostatRandomForce = math.sqrt(numba.float32(2.0) * alpha_baro * temperature * dt) * random_number 

125 

126 current_volume_velocity = current_barostat_state[1] 

127 inv_baro_mass = numba.float32(1.0) / mass_baro 

128 scaled_dt = numba.float32(0.5) * dt * alpha_baro * inv_baro_mass 

129 b_tilde = numba.float64(1.0) / (numba.float64(1.0) + scaled_dt) 

130 a_tilde = b_tilde * (numba.float64(1.0) - scaled_dt) 

131 

132 new_volume_vel = a_tilde * current_volume_velocity + b_tilde * inv_baro_mass * (barostatForce * dt + barostatRandomForce) 

133 new_volume = volume + dt * new_volume_vel 

134 

135 # Update the barostat state 

136 barostat_state[0] = new_volume / volume 

137 barostat_state[1] = new_volume_vel 

138 

139 # reset length_ratio to 1.0. WHY? 

140 for i in range(3): 

141 length_ratio[i] = numba.float64(1.0) 

142 

143 vol_scale_factor = barostat_state[0] 

144 lr_iso = math.pow(vol_scale_factor, numba.float64(1.0) / numba.float64(3.0)) 

145 

146 #update box length 

147 sim_box[0] += sim_box[0] * (lr_iso - 1.) # Better: sim_box[0] = sim_box[0] * lr_iso ? 

148 sim_box[1] += sim_box[1] * (lr_iso - 1.) 

149 sim_box[2] += sim_box[2] * (lr_iso - 1.) 

150 

151 # update length_ratio using cuda loop  

152 for i in range(3): 

153 length_ratio[i] = lr_iso 

154 

155 barostatVirial[0] = numba.float32(0.0) 

156 

157 return 

158 

159 

160 def step(grid, vectors, scalars, r_im, sim_box, integrator_params, time): 

161 """ Make one NPT timestep using Leap-frog 

162 Kernel configuration: [num_blocks, (pb, 1)] 

163 REF: https://arxiv.org/pdf/1303.7011.pdf 

164 """ 

165 

166 dt, alpha, alpha_baro, mass_baro, barostatModeISO, boxFlucCoord, rng_states, barostat_state, barostatVirial, length_ratio = integrator_params 

167 temperature = temperature_function(time) 

168 

169 global_id, my_t = cuda.grid(2) 

170 if global_id < num_part and my_t == 0: 

171 my_r = vectors[r_id][global_id] 

172 my_v = vectors[v_id][global_id] 

173 my_f = vectors[f_id][global_id] 

174 my_m = scalars[global_id][m_id] 

175 if compute_k: 

176 my_k = numba.float32(0.0) # Kinetic energy 

177 if compute_fsq: 

178 my_fsq = numba.float32(0.0) # force squared energy 

179 

180 for k in range(D): 

181 random_number = xoroshiro128p_normal_float32(rng_states, global_id + 1) # +1 to avoid using the same random number state as the barostat 

182 beta = math.sqrt(numba.float32(2.0) * alpha * temperature * dt) * random_number 

183 

184 scaled_dt = numba.float32(0.5) * dt * alpha * numba.float32(1.0)/my_m 

185 prm_b = numba.float64(1.0) / (numba.float64(1.0) + scaled_dt) 

186 prm_a = prm_b * (numba.float64(1.0) - scaled_dt) 

187 

188 if compute_k: 

189 my_k += numba.float32(0.5) * my_m * my_v[k] * my_v[k] # ke before 

190 if compute_fsq: 

191 my_fsq += my_f[k] * my_f[k] 

192 

193 my_v[k] = prm_a * my_v[k] + prm_b * (numba.float32(1.0)/my_m) * (my_f[k] * dt + beta) 

194 if compute_k: 

195 my_k += numba.float32(0.5) * my_m * my_v[k] * my_v[k] # ke after 

196 

197 L_factor = 2.*length_ratio[k] / (1. + length_ratio[k]) 

198 my_r[k] = length_ratio[k] * my_r[k] + L_factor * my_v[k] * dt 

199 

200 apply_PBC(my_r, r_im[global_id], sim_box) 

201 

202 if compute_k: 

203 scalars[global_id][k_id] = numba.float32(0.5) * my_k 

204 if compute_fsq: 

205 scalars[global_id][fsq_id] = my_fsq 

206 

207 return 

208 

209 copyParticleVirial = cuda.jit(device=gridsync)(copyParticleVirial) 

210 update_barostat_state = cuda.jit(device=gridsync)(update_barostat_state) 

211 step = cuda.jit(device=gridsync)(step) 

212 

213 if gridsync: 

214 

215 def kernel(grid, vectors, scalars, r_im, sim_box, integrator_params, time, ptype): 

216 copyParticleVirial(scalars, integrator_params) 

217 grid.sync() 

218 update_barostat_state(sim_box, integrator_params, time) 

219 grid.sync() 

220 step(grid, vectors, scalars, r_im, sim_box, integrator_params, time) 

221 return 

222 

223 return cuda.jit(device=gridsync)(kernel) 

224 

225 else: 

226 

227 def kernel(grid, vectors, scalars, r_im, sim_box, integrator_params, time, ptype): 

228 copyParticleVirial[num_blocks, (pb, 1)](scalars, integrator_params) 

229 update_barostat_state[1, (1, 1)](sim_box, integrator_params, time) 

230 step[num_blocks, (pb, 1)](grid, vectors, scalars, r_im, sim_box, integrator_params, time) 

231 return 

232 

233 return kernel