MultiOptPy 1.20.5__py3-none-any.whl → 1.20.7__py3-none-any.whl

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.
@@ -1,185 +1,298 @@
1
1
  import numpy as np
2
2
  import copy
3
-
4
3
  from multioptpy.Parameters.parameter import UnitValueLib, atomic_mass
5
4
 
6
-
7
5
  class Thermostat:
8
- def __init__(self, momentum_list, temperature, pressure, element_list=[]):
6
+ def __init__(self, momentum_list, temperature, pressure, element_list=None):
7
+ # Mutable default argument fix
8
+ if element_list is None:
9
+ self.element_list = []
10
+ else:
11
+ self.element_list = element_list
9
12
 
10
- self.momentum_list = momentum_list #list
13
+ # ---------------------------------------------------------
14
+ # [Optimization] Pre-compute masses for vectorization
15
+ # shape: (N_atoms, 1) to allow broadcasting: momentum (N,3) / mass (N,1)
16
+ # ---------------------------------------------------------
17
+ self.masses = np.array([atomic_mass(e) for e in self.element_list], dtype=np.float64)[:, None]
18
+ self.inverse_masses = 1.0 / self.masses
11
19
 
12
- self.temperature = temperature #K
13
- self.initial_temperature = temperature #K
14
- self.pressure = pressure * (3.39893 * 10 ** (-11)) #kPa -> a.u.
15
- self.initial_pressure = pressure * (3.39893 * 10 ** (-11)) #kPa -> a.u.
20
+ # Keep momentum as numpy array internally for performance
21
+ self.momentum_list = np.array(momentum_list, dtype=np.float64)
16
22
 
23
+ self.temperature = temperature # K
24
+ self.initial_temperature = temperature # K
25
+
26
+ # Pressure conversion
27
+ self.pressure = pressure * (3.39893 * 10 ** (-11)) # kPa -> a.u.
28
+ self.initial_pressure = self.pressure
29
+
30
+ # Thermostat Parameters
17
31
  self.Langevin_zeta = 0.01
18
32
  self.zeta = 0.0
19
33
  self.eta = 0.0
20
34
  self.scaling = 1.0
21
35
  self.Ps_momentum = 0.0
22
36
 
37
+ # Degrees of freedom (3N)
23
38
  self.g_value = len(momentum_list) * 3
24
- self.Q_value = 1e-1
25
-
26
39
 
40
+ # Constants
41
+ self.Q_value = 1e-1
27
42
  self.M_value = 1e+12
28
43
  self.Boltzmann_constant = 3.16681 * 10 ** (-6) # hartree/K
29
44
  self.delta_timescale = 1e-1
30
- self.volume = 1e-23 * (1/UnitValueLib().bohr2m) ** 3#m^3 -> Bohr^3
45
+ self.volume = 1e-23 * (1/UnitValueLib().bohr2m) ** 3 # m^3 -> Bohr^3
31
46
 
32
- # Nose-Hoover-chain
33
- self.Q_value_chain = [1.0, 2.0, 3.0, 6.0, 10.0, 20, 40, 50, 100, 200]#mass of thermostat
34
- self.zeta_chain = [0.0 for i in range(len(self.Q_value_chain))]
47
+ # Nose-Hoover Chain Parameters
48
+ self.Q_value_chain = np.array([1.0, 2.0, 3.0, 6.0, 10.0, 20, 40, 50, 100, 200], dtype=np.float64)
49
+ self.zeta_chain = np.zeros(len(self.Q_value_chain), dtype=np.float64)
35
50
  self.timestep = None
36
51
 
52
+ # History
37
53
  self.Instantaneous_temperatures_list = []
38
54
  self.Instantaneous_momentum_list = []
55
+ self.tot_kinetic_ene = 0.0
56
+ self.Instantaneous_temperature = 0.0
57
+
58
+ # =========================================================================
59
+ # Internal Helpers (Vectorized & Logical Separation)
60
+ # =========================================================================
61
+
62
+ def _update_momentum(self, forces, scaling=1.0):
63
+ """Vectorized momentum update"""
64
+ pass
65
+
66
+ def _update_position(self, current_geometry_arr, dt):
67
+ """Vectorized position update: r(t+dt) = r(t) + v(t)*dt"""
68
+ # v = p / m
69
+ velocities = self.momentum_list * self.inverse_masses
70
+ return current_geometry_arr + velocities * dt
71
+
72
+ def _propagate_nhc_zeta(self, dt, kinetic_energy_2x):
73
+ """
74
+ Propagate Nose-Hoover Chain variables.
75
+ """
76
+ # 1. Update first chain link force
77
+ driving_force = (kinetic_energy_2x - self.g_value * self.Boltzmann_constant * self.initial_temperature)
39
78
 
40
- self.element_list = element_list
41
- return
42
-
43
-
79
+ self.zeta_chain[0] += dt * driving_force / self.Q_value_chain[0]
80
+ self.zeta_chain[0] -= dt * self.zeta_chain[0] * self.zeta_chain[1] # Coupling with next
81
+
82
+ # 2. Update middle chain links
83
+ for j in range(1, len(self.zeta_chain)-1):
84
+ driving_force_j = (self.Q_value_chain[j-1] * self.zeta_chain[j-1]**2 - self.Boltzmann_constant * self.initial_temperature)
85
+ self.zeta_chain[j] += dt * driving_force_j / self.Q_value_chain[j]
86
+ self.zeta_chain[j] -= dt * self.zeta_chain[j] * self.zeta_chain[j+1]
87
+
88
+ # 3. Update last chain link
89
+ last = -1
90
+ driving_force_last = (self.Q_value_chain[last-1] * self.zeta_chain[last-1]**2 - self.Boltzmann_constant * self.initial_temperature)
91
+ self.zeta_chain[last] += dt * driving_force_last / self.Q_value_chain[last]
92
+
93
+ # =========================================================================
94
+ # Public API (Compatible with moleculardynamics.py)
95
+ # =========================================================================
96
+
44
97
  def calc_tot_kinetic_energy(self):
45
- tot_kinetic_ene = 0.0
46
-
47
- for i, elem in enumerate(self.element_list):
48
- tot_kinetic_ene += (np.sum(self.momentum_list[i] ** 2) /(2 * atomic_mass(elem)))
49
- self.tot_kinetic_ene = tot_kinetic_ene
50
- return tot_kinetic_ene
98
+ """
99
+ Vectorized calculation of total kinetic energy.
100
+ KE = sum(p^2 / 2m)
101
+ """
102
+ p_sq = self.momentum_list ** 2
103
+ p_sq_atom = np.sum(p_sq, axis=1)
104
+ self.tot_kinetic_ene = np.sum(p_sq_atom / (2.0 * self.masses.flatten()))
105
+ return self.tot_kinetic_ene
51
106
 
52
107
  def calc_inst_temperature(self):
53
- #temperature
54
- tot_kinetic_ene = self.calc_tot_kinetic_energy()
55
- Instantaneous_temperature = 2 * tot_kinetic_ene / (self.g_value * self.Boltzmann_constant)
56
- print("Instantaneous_temperature: ",Instantaneous_temperature ," K")
57
-
58
- self.Instantaneous_temperature = Instantaneous_temperature
59
- #-----------------
60
- return Instantaneous_temperature
108
+ """Calculates and stores instantaneous temperature."""
109
+ self.calc_tot_kinetic_energy()
110
+ self.Instantaneous_temperature = 2.0 * self.tot_kinetic_ene / (self.g_value * self.Boltzmann_constant)
111
+ print(f"Instantaneous_temperature: {self.Instantaneous_temperature:.8f} K")
112
+ return self.Instantaneous_temperature
61
113
 
62
114
  def add_inst_temperature_list(self):
63
- #self.add_inst_temperature_list()
64
115
  self.Instantaneous_temperatures_list.append(self.Instantaneous_temperature)
65
-
66
-
67
- def Nose_Hoover_thermostat(self, geom_num_list, new_g):#fixed volume #NVT ensemble
68
- new_g *= -1
69
- self.momentum_list = self.momentum_list * np.exp(-self.delta_timescale * self.zeta * 0.5)
70
116
 
71
- self.momentum_list += new_g * self.delta_timescale * 0.5
72
- print("NVT ensemble (Nose_Hoover) : Sum of momenta (absolute value):", np.sum(np.abs(self.momentum_list)))
73
- tmp_list = []
74
- for i, elem in enumerate(self.element_list):
75
- tmp_list.append(self.delta_timescale * self.momentum_list[i] / atomic_mass(elem))
117
+ def Nose_Hoover_thermostat(self, geom_num_list, new_g): # fixed volume #NVT ensemble
118
+ """
119
+ Single Nose-Hoover implementation.
120
+ """
121
+ geom_arr = np.array(geom_num_list, dtype=np.float64)
122
+ force = -1.0 * np.array(new_g, dtype=np.float64)
123
+
124
+ # 1. First half-step thermostat scaling
125
+ self.momentum_list *= np.exp(-self.delta_timescale * self.zeta * 0.5)
126
+
127
+ # 2. First half-step momentum update (Force contribution)
128
+ self.momentum_list += force * self.delta_timescale * 0.5
76
129
 
77
- new_geometry = geom_num_list + tmp_list
78
- #------------
130
+ print("NVT ensemble (Nose_Hoover) : Sum of momenta (absolute value):", np.sum(np.abs(self.momentum_list)))
131
+
132
+ # 3. Position update (Full step)
133
+ new_geometry = self._update_position(geom_arr, self.delta_timescale)
134
+
135
+ # 4. Thermostat Propagation
79
136
  self.calc_inst_temperature()
80
137
  self.add_inst_temperature_list()
81
- #----------
82
- self.zeta += self.delta_timescale * (2 * self.tot_kinetic_ene - self.g_value * self.Boltzmann_constant * self.initial_temperature) / self.Q_value
83
-
84
- #print(tmp_value, self.g_value * self.Boltzmann_constant * self.temperature)
138
+
139
+ driving_force = (2 * self.tot_kinetic_ene - self.g_value * self.Boltzmann_constant * self.initial_temperature)
140
+ self.zeta += self.delta_timescale * driving_force / self.Q_value
141
+
142
+ # 5. Second half-step momentum update
143
+ self.momentum_list += force * self.delta_timescale * 0.5
144
+
145
+ # 6. Second half-step thermostat scaling
146
+ self.momentum_list *= np.exp(-self.delta_timescale * self.zeta * 0.5)
147
+
148
+ return new_geometry # Corrected: Returns numpy array, not list
149
+
150
+ def Nose_Hoover_chain_thermostat(self, geom_num_list, new_g): # fixed volume #NVT ensemble
151
+ """
152
+ Nose-Hoover Chain implementation.
153
+ """
154
+ geom_arr = np.array(geom_num_list, dtype=np.float64)
155
+ force = -1.0 * np.array(new_g, dtype=np.float64)
156
+
157
+ # 1. First half-step thermostat scaling
158
+ self.momentum_list *= np.exp(-self.delta_timescale * self.zeta_chain[0] * 0.5)
159
+
160
+ # 2. First half-step momentum update
161
+ self.momentum_list += force * self.delta_timescale * 0.5
85
162
 
163
+ print("NVT ensemble (Nose_Hoover chain) : Sum of momenta (absolute value):", np.sum(np.abs(self.momentum_list)))
86
164
 
87
- self.momentum_list += new_g * self.delta_timescale * 0.5
88
- self.momentum_list = self.momentum_list * np.exp(-self.delta_timescale * self.zeta * 0.5)
165
+ # 3. Position update
166
+ new_geometry = self._update_position(geom_arr, self.delta_timescale)
167
+
168
+ # 4. Thermostat Propagation
169
+ self.calc_inst_temperature()
170
+ self.add_inst_temperature_list()
89
171
 
172
+ self._propagate_nhc_zeta(self.delta_timescale, 2 * self.tot_kinetic_ene)
90
173
 
91
- return new_geometry
174
+ print("zeta_list (Coefficient of friction): ", self.zeta_chain)
92
175
 
93
- def Nose_Hoover_chain_thermostat(self, geom_num_list, new_g):#fixed volume #NVT ensemble
94
- #ref. J. Chem. Phys. 97, 2635-2643 (1992)
95
- new_g *= -1
96
- self.momentum_list = self.momentum_list * np.exp(-self.delta_timescale * self.zeta_chain[0] * 0.5)
176
+ # 5. Second half-step momentum update
177
+ self.momentum_list += force * self.delta_timescale * 0.5
97
178
 
98
- self.momentum_list += new_g * self.delta_timescale * 0.5
99
- print("NVT ensemble (Nose_Hoover chain) : Sum of momenta (absolute value):", np.sum(np.abs(self.momentum_list)))
179
+ # 6. Second half-step thermostat scaling
180
+ self.momentum_list *= np.exp(-self.delta_timescale * self.zeta_chain[0] * 0.5)
100
181
 
101
- tmp_list = []
102
- for i, elem in enumerate(self.element_list):
103
- tmp_list.append(self.delta_timescale * self.momentum_list[i] / atomic_mass(elem))
182
+ return new_geometry # Corrected: Returns numpy array, not list
183
+
184
+ def Velocity_Verlet(self, geom_num_list, new_g, prev_g, iter): # NVE ensemble
185
+ """
186
+ Velocity Verlet integration.
187
+ """
188
+ geom_arr = np.array(geom_num_list, dtype=np.float64)
104
189
 
105
- new_geometry = geom_num_list + tmp_list
106
- #------------
107
- self.calc_inst_temperature()
108
- self.add_inst_temperature_list()
109
- #----------
110
- self.zeta_chain[0] += self.delta_timescale * (2 * self.tot_kinetic_ene - self.g_value * self.Boltzmann_constant * self.initial_temperature) / self.Q_value_chain[0] -1* self.delta_timescale * (self.zeta_chain[0] * self.zeta_chain[1])
190
+ force_new = -1.0 * np.array(new_g, dtype=np.float64)
191
+ force_prev = -1.0 * np.array(prev_g, dtype=np.float64)
111
192
 
112
- for j in range(1, len(self.zeta_chain)-1):
113
- self.zeta_chain[j] += self.delta_timescale * (self.Q_value_chain[j-1]*self.zeta_chain[j-1]**2 - self.Boltzmann_constant * self.initial_temperature) / self.Q_value_chain[j] -1* self.delta_timescale * (self.zeta_chain[j] * self.zeta_chain[j+1])
193
+ # 1. Update Momentum
194
+ self.momentum_list += (force_new + force_prev) * self.delta_timescale * 0.5
195
+
196
+ # 2. Position Update
197
+ term1 = (self.momentum_list * self.inverse_masses) * self.delta_timescale
198
+ term2 = (force_new * self.inverse_masses) * (self.delta_timescale**2 / 2.0)
114
199
 
115
- self.zeta_chain[-1] += self.delta_timescale * (self.Q_value_chain[-2]*self.zeta_chain[-2]**2 -1*self.Boltzmann_constant * self.initial_temperature) / self.Q_value_chain[-1]
200
+ new_geometry = geom_arr + term1 + term2
116
201
 
117
- #print(tmp_value, self.g_value * self.Boltzmann_constant * self.temperature)
118
-
119
-
120
- self.momentum_list += new_g * self.delta_timescale * 0.5
121
- self.momentum_list = self.momentum_list * np.exp(-self.delta_timescale * self.zeta_chain[0] * 0.5)
122
- print("zeta_list (Coefficient of friction): ", self.zeta_chain)
123
- return new_geometry
124
-
125
- def Velocity_Verlet(self, geom_num_list, new_g, prev_g, iter):#NVE ensemble
126
- tmp_new_g = copy.copy(-1*new_g)
127
- tmp_prev_g = copy.copy(-1*prev_g)
128
- #print("NVE ensemble (Velocity_Verlet)")
129
- self.momentum_list += ( tmp_new_g + tmp_prev_g ) * (self.delta_timescale) * 0.5 #+ third_term + fourth_term
130
- #-----------
131
- tmp_list = []
132
- for i, elem in enumerate(self.element_list):
133
- tmp_list.append(self.delta_timescale * self.momentum_list[i] / atomic_mass(elem) + self.delta_timescale ** 2 * tmp_new_g[i] / (2.0 * atomic_mass(elem)))
134
- new_geometry = geom_num_list + tmp_list
135
- #------------
202
+ # Stats
136
203
  self.calc_inst_temperature()
137
204
  self.add_inst_temperature_list()
138
- #-------------
139
205
 
140
- return new_geometry
141
-
206
+ return new_geometry # Corrected: Returns numpy array, not list
142
207
 
143
-
144
208
  def generate_normal_random_variables(self, n_variables):
145
- random_variables = []
146
- for _ in range(n_variables // 2):
147
- u1, u2 = np.random.rand(2)
148
- #Box-Muller method
149
- z1 = np.sqrt(-2 * np.log(u1)) * np.cos(2 * np.pi * u2)
150
- z2 = np.sqrt(-2 * np.log(u1)) * np.sin(2 * np.pi * u2)
151
- random_variables.extend([z1, z2])
209
+ """Vectorized Box-Muller transformation"""
210
+ n_pairs = (n_variables + 1) // 2
211
+ u1 = np.random.rand(n_pairs)
212
+ u2 = np.random.rand(n_pairs)
152
213
 
153
- if n_variables % 2 == 1:
154
- u1, u2 = np.random.rand(2)
155
- z1 = np.sqrt(-2 * np.log(u1)) * np.cos(2 * np.pi * u2)
156
- random_variables.append(z1)
214
+ r = np.sqrt(-2 * np.log(u1))
215
+ theta = 2 * np.pi * u2
157
216
 
158
- return np.array([random_variables], dtype="float64")
159
-
160
- def calc_rand_moment_based_on_boltzman_const(self, random_variables):
161
- rand_moment = random_variables
162
-
163
- for i in range(len(self.element_list)):
164
- random_variables[i] *= np.sqrt(self.Boltzmann_constant * self.temperature / atomic_mass(self.element_list[i]))
217
+ z1 = r * np.cos(theta)
218
+ z2 = r * np.sin(theta)
165
219
 
220
+ result = np.empty(n_pairs * 2)
221
+ result[0::2] = z1
222
+ result[1::2] = z2
166
223
 
224
+ return result[:n_variables]
225
+
226
+ def calc_rand_moment_based_on_boltzman_const(self, random_variables):
227
+ """
228
+ Scales random variables by sqrt(kB * T * m).
229
+ """
230
+ rand_moment = np.array(random_variables, dtype=np.float64)
231
+ scale_factors = np.sqrt(self.Boltzmann_constant * self.temperature * self.masses)
232
+ rand_moment *= scale_factors
167
233
  return rand_moment
168
234
 
169
-
170
235
  def init_purtubation(self, geometry, B_e, B_g):
171
- random_variables = self.generate_normal_random_variables(len(self.element_list)*3).reshape(len(self.element_list), 3)
172
-
173
- addtional_velocity = self.calc_rand_moment_based_on_boltzman_const(random_variables) # velocity
174
- init_momentum = addtional_velocity * 0.0
175
-
176
- for i in range(len(self.element_list)):
177
- init_momentum[i] += addtional_velocity[i] * atomic_mass(self.element_list[i])
178
-
236
+ """Initializes momenta with random thermal noise."""
237
+ N = len(self.element_list)
238
+ random_vars = self.generate_normal_random_variables(N * 3).reshape(N, 3)
239
+ v_thermal = random_vars * np.sqrt(self.Boltzmann_constant * self.temperature * self.inverse_masses)
240
+ init_momentum = v_thermal * self.masses
179
241
 
180
242
  self.momentum_list += init_momentum
181
243
  self.init_energy = B_e
182
- #self.init_hamiltonian = B_e
183
- #for i, elem in enumerate(element_list):
184
- # self.init_hamiltonian += (np.sum(self.momentum_list[i]) ** 2 / (2 * atomic_mass(elem)))
185
244
  return
245
+ def Langevin_thermostat(self, geom_num_list, new_g):
246
+ """
247
+ Langevin Dynamics (BAOAB integrator)
248
+ Reference: B. Leimkuhler and C. Matthews, J. Chem. Phys. 138, 174102 (2013).
249
+
250
+ Structure:
251
+ B: Momentum += 0.5 * dt * Force
252
+ A: Position += 0.5 * dt * Velocity
253
+ O: Momentum = c1 * Momentum + c2 * Noise (Ornstein-Uhlenbeck)
254
+ A: Position += 0.5 * dt * Velocity
255
+ B: Momentum += 0.5 * dt * Force
256
+ """
257
+ geom_arr = np.array(geom_num_list, dtype=np.float64)
258
+ force = -1.0 * np.array(new_g, dtype=np.float64)
259
+
260
+
261
+ dt = self.delta_timescale
262
+ gamma = self.Langevin_zeta # (1/time)
263
+ target_temp = self.initial_temperature
264
+
265
+ #
266
+ c1 = np.exp(-gamma * dt)
267
+ c2 = np.sqrt(1.0 - c1**2)
268
+
269
+ # sigma = sqrt(m * kB * T)
270
+ # self.masses shape: (N, 1) -> broadcasting works
271
+ sigma = np.sqrt(self.masses * self.Boltzmann_constant * target_temp)
272
+
273
+ # 1. B: Half-step Momentum Update (Force)
274
+ self.momentum_list += 0.5 * dt * force
275
+
276
+ # 2. A: Half-step Position Update
277
+ # r(t+dt/2) = r(t) + 0.5 * dt * v(t+dt/2)
278
+ new_geometry = self._update_position(geom_arr, 0.5 * dt)
279
+
280
+ # 3. O: Fluctuation-Dissipation (Thermostat)
281
+ # p' = c1 * p + c2 * sigma * noise
282
+
283
+ noise = np.random.normal(0.0, 1.0, self.momentum_list.shape)
284
+ self.momentum_list = c1 * self.momentum_list + c2 * sigma * noise
285
+
286
+ # 4. A: Half-step Position Update
287
+ # r(t+dt) = r(t+dt/2) + 0.5 * dt * v'
288
+ new_geometry = self._update_position(new_geometry, 0.5 * dt)
289
+
290
+ # 5. B: Half-step Momentum Update (Force)
291
+
292
+ self.momentum_list += 0.5 * dt * force
293
+
294
+
295
+ self.calc_inst_temperature()
296
+ self.add_inst_temperature_list()
297
+
298
+ return new_geometry