yu-mcal 0.1.4__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.
mcal/__init__.py ADDED
@@ -0,0 +1 @@
1
+
File without changes
@@ -0,0 +1,391 @@
1
+ """hopping_mobility_model.py (2025/10/06)"""
2
+ import math
3
+ import random
4
+ from typing import List, Tuple
5
+
6
+ import numpy as np
7
+ from numpy.typing import NDArray
8
+
9
+
10
+ const_kb = 1.380649e-23 # Boltzmann constant [J/K]
11
+ const_e = 1.60217663e-19 # Elementary charge [C(=J/eV)]
12
+ const_hbar = 6.62607015e-34 / (2 * math.pi) # Dirac constant [Js]
13
+
14
+
15
+ def demo():
16
+ # 一次元系で、粒子は1 sごとに0.01の確率で右へ1 m、0.01の確率で左へ1 m移動し、0.98の確率でその場に留まる。
17
+ print("\nOne-dimensional system")
18
+ lattice = np.array(((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)))
19
+ hop = ((0, 0, 1, 0, 0, 0.01),)
20
+ D = diffusion_coefficient_tensor(lattice, hop)
21
+ print("Diffusion coefficient tensor (analytical):")
22
+ for d in D:
23
+ print(f"{d[0]:9.6f} {d[1]:9.6f} {d[2]:9.6f}")
24
+ print("Diffusion coefficient tensor (ODE):")
25
+ D_ode = diffusion_coefficient_tensor_ODE(lattice, hop)
26
+ for d in D_ode:
27
+ print(f"{d[0]:9.6f} {d[1]:9.6f} {d[2]:9.6f}")
28
+ print("Diffusion coefficient tensor (MC):")
29
+ D_mc = diffusion_coefficient_tensor_MC(lattice, hop)
30
+ for d in D_mc:
31
+ print(f"{d[0]:9.6f} {d[1]:9.6f} {d[2]:9.6f}")
32
+
33
+ # 一次元系で、粒子は偶数サイトにいる時は1sごとに0.02の確率で右へ1m、0.01の確率で左へ1m移動し、0.97の確率でその場に留まる。
34
+ # 奇数サイトにいる時は1sごとに0.01の確率で右へ1m、0.02の確率で左へ1m移動し、0.97の確率でその場に留まる。
35
+ print("\nOne-dimensional dimer system")
36
+ lattice = np.array(((2.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)))
37
+ hop = (
38
+ (0, 1, 0, 0, 0, 0.02),
39
+ (0, 1, -1, 0, 0, 0.01),
40
+ )
41
+ D = diffusion_coefficient_tensor(lattice, hop)
42
+ print("Diffusion coefficient tensor (analytical):")
43
+ for d in D:
44
+ print(f"{d[0]:9.6f} {d[1]:9.6f} {d[2]:9.6f}")
45
+ print("Diffusion coefficient tensor (ODE):")
46
+ D_ode = diffusion_coefficient_tensor_ODE(lattice, hop)
47
+ for d in D_ode:
48
+ print(f"{d[0]:9.6f} {d[1]:9.6f} {d[2]:9.6f}")
49
+ print("Diffusion coefficient tensor (MC):")
50
+ D_mc = diffusion_coefficient_tensor_MC(lattice, hop)
51
+ for d in D_mc:
52
+ print(f"{d[0]:9.6f} {d[1]:9.6f} {d[2]:9.6f}")
53
+
54
+ # ランダムな条件で検証
55
+ print("\nRandom system")
56
+ lattice = np.random.random((3, 3)) * 2.0
57
+ lattice[0, 1:] = 0.0
58
+ lattice[1, 2:] = 0.0
59
+ hop = []
60
+ for _ in range(6):
61
+ s = random.randint(0, 1)
62
+ t = random.randint(0, 1)
63
+ i = random.randint(-1, 1)
64
+ j = random.randint(-1, 1)
65
+ k = random.randint(-1, 1)
66
+ p = random.random() * 0.02
67
+ hop.append((s, t, i, j, k, p))
68
+ D = diffusion_coefficient_tensor(lattice, hop)
69
+ print("Diffusion coefficient tensor (analytical):")
70
+ for d in D:
71
+ print(f"{d[0]:9.6f} {d[1]:9.6f} {d[2]:9.6f}")
72
+ print("Diffusion coefficient tensor (ODE):")
73
+ D_ode = diffusion_coefficient_tensor_ODE(lattice, hop)
74
+ for d in D_ode:
75
+ print(f"{d[0]:9.6f} {d[1]:9.6f} {d[2]:9.6f}")
76
+ print("Diffusion coefficient tensor (MC):")
77
+ D_mc = diffusion_coefficient_tensor_MC(lattice, hop)
78
+ for d in D_mc:
79
+ print(f"{d[0]:9.6f} {d[1]:9.6f} {d[2]:9.6f}")
80
+
81
+
82
+ def cal_pinv(array: NDArray[np.float64], rcond: float = 1e-3) -> NDArray[np.float64]:
83
+ """Calculate pseudo-inverse matrix using eigenvalue decomposition
84
+
85
+ Parameters
86
+ ----------
87
+ array : NDArray[np.float64]
88
+ Input matrix
89
+ rcond : float, optional
90
+ Cutoff for small singular values, by default 1e-9
91
+
92
+ Returns
93
+ -------
94
+ NDArray[np.float64]
95
+ Pseudo-inverse matrix
96
+
97
+ Raises
98
+ ------
99
+ ValueError
100
+ The last eigenvalue is not zero.
101
+ ValueError
102
+ All eigenvalues except the last one should be negative.
103
+ """
104
+ eigvals, eigvecs = np.linalg.eigh(array)
105
+
106
+ # Calculate pseudo-inverse matrix using eigenvalue decomposition
107
+ inveigvals = np.zeros_like(eigvals)
108
+ if abs(eigvals[-1] / eigvals[0]) > rcond:
109
+ raise ValueError(f"The last eigenvalue is not zero, which is unexpected for this test case. {eigvals}")
110
+ if any(eigvals[0:-1] > 0):
111
+ raise ValueError(f"All eigenvalues except the last one should be negative, which is unexpected for this test case. {eigvals}")
112
+
113
+ inveigvals[0:-1] = 1.0 / eigvals[0:-1]
114
+ inveigvals[-1] = 0.0
115
+ pinv = eigvecs @ np.diag(inveigvals) @ eigvecs.T
116
+
117
+ return pinv
118
+
119
+
120
+ def marcus_rate(transfer: float, reorganization: float, T: float = 300.0) -> float:
121
+ """Calculate hopping rate (1/s) from transfer integral (eV) and reorganization energy (eV)
122
+
123
+ Parameters
124
+ ----------
125
+ transfer : float
126
+ Transfer integral [eV]
127
+ reorganization : float
128
+ Reorganization energy [eV]
129
+ T : float
130
+ Temperature [K], by default 300.0
131
+
132
+ Returns
133
+ -------
134
+ float
135
+ Hopping rate [1/s]
136
+ """
137
+ kbT = const_kb * T
138
+ return (
139
+ (transfer * const_e) ** 2
140
+ / const_hbar
141
+ * math.sqrt(math.pi / (reorganization * const_e * kbT))
142
+ * math.exp(-reorganization * const_e / (4 * kbT))
143
+ )
144
+
145
+
146
+ def mobility_tensor(D: NDArray[np.float64], T: float = 300.0) -> NDArray[np.float64]:
147
+ """Calculate mobility tensor from diffusion coefficient tensor
148
+
149
+ Parameters
150
+ ----------
151
+ D : 3x3 numpy.array
152
+ Diffusion coefficient tensor
153
+ T : float
154
+ Temperature [K], by default 300.0
155
+
156
+ Returns
157
+ -------
158
+ 3x3 numpy.array
159
+ Mobility tensor
160
+ """
161
+ return D * const_e / (const_kb * T)
162
+
163
+
164
+ def diffusion_coefficient_tensor(
165
+ lattice: NDArray[np.float64],
166
+ hop: List[Tuple[int, int, int, int, int, float]]
167
+ ) -> NDArray[np.float64]:
168
+ """Calculate diffusion coefficient tensor from hopping rate
169
+
170
+ Parameters
171
+ ----------
172
+ lattice : 3x3 numpy.array
173
+ lattice[0,:] is a-axis vector, lattice[1,:] b-axis vector, lattice[2,:] c-axis vector
174
+ hop : list of (int, int, int, int, int, float) tuple.
175
+ (s, t, i, j, k, p) means that the hopping rate from s-th molecule in (0, 0, 0) cell to t-th molecule in (i, j, k) cell is p.
176
+
177
+ Returns
178
+ -------
179
+ 3x3 numpy.array
180
+ Diffusion coefficient tensor
181
+ """
182
+
183
+ # Standardize hop list
184
+ hop = _standardize_hop_list(hop)
185
+
186
+ # Number of molecules in the unit cell
187
+ n = len(set([h[0] for h in hop]) | set([h[1] for h in hop]))
188
+
189
+ # Prepare arrays
190
+ D = np.zeros((3, 3))
191
+ B = np.zeros((n, n))
192
+ C = np.zeros((n, 3))
193
+
194
+ for s, t, i, j, k, p in hop:
195
+ vec = np.array((i, j, k)) @ lattice
196
+ D[:, :] += p * np.outer(vec, vec) * 2 # Consider hopping in both directions
197
+ B[s, t] += p
198
+ B[t, s] += p # Consider hopping in both directions
199
+ B[s, s] -= p
200
+ B[t, t] -= p
201
+ C[s, :] += p * vec
202
+ C[t, :] -= p * vec # Consider hopping in both directions
203
+
204
+ # For n = 1 case, skip C.T @ B_pinv @ C term as it equals zero
205
+ if n > 1:
206
+ B_pinv = cal_pinv(B)
207
+ D = (D / 2 + C.T @ B_pinv @ C) / n
208
+ else:
209
+ D = D / 2
210
+
211
+ # Check computational errors
212
+ threshold = np.max(abs(D)) * 1e-6
213
+ D_diff = abs(D - D.T)
214
+ if np.any(D_diff > threshold):
215
+ raise ValueError(f"Diffusion coefficient tensor D should be symmetric: {D}")
216
+
217
+ # Make symmetric matrix considering computational errors
218
+ D = (D + D.T) / 2
219
+
220
+ return D
221
+
222
+
223
+ def diffusion_coefficient_tensor_ODE(
224
+ lattice: NDArray[np.float64],
225
+ hop: List[Tuple[int, int, int, int, int, float]],
226
+ max_steps: int = 200,
227
+ size: int = 40,
228
+ max_rate: float = 0.05
229
+ ) -> NDArray[np.float64]:
230
+ """Calculate diffusion coefficient tensor from numerical solution of Ordinary Differential Equation (ODE)
231
+
232
+ Parameters
233
+ ----------
234
+ lattice : 3x3 numpy.array
235
+ lattice[0,:] is a-axis vector, lattice[1,:] b-axis vector, lattice[2,:] c-axis vector
236
+ hop : list of (int, int, int, int, int, float) tuple.
237
+ (s, t, i, j, k, p) means that the hopping rate from s-th molecule in (0, 0, 0) cell to t-th molecule in (i, j, k) cell is p.
238
+ max_steps : int
239
+ Maximum number of steps
240
+ size : int
241
+ Size of the simulation box
242
+ max_rate : float
243
+ Maximum rate of hopping
244
+
245
+ Returns
246
+ -------
247
+ 3x3 numpy.array
248
+ Diffusion coefficient tensor
249
+ """
250
+ # Standardize hop list
251
+ hop = _standardize_hop_list(hop)
252
+
253
+ # Number of molecules in the unit cell
254
+ n = len(set([h[0] for h in hop]) | set([h[1] for h in hop]))
255
+
256
+ prob = np.zeros((2 * size, 2 * size, 2 * size, n))
257
+ prob[size, size, size, :] = 1.0 / n
258
+ pre_prob = np.zeros_like(prob)
259
+
260
+ dt = max_rate / max(h[5] for h in hop) # Time step
261
+ for _ in range(max_steps):
262
+ pre_prob[:, :, :, :] = prob
263
+ for s, t, i, j, k, p in hop:
264
+ prob[:, :, :, t] += np.roll(pre_prob[:, :, :, s], (i, j, k), axis=(0, 1, 2)) * p * dt
265
+ prob[:, :, :, s] += np.roll(pre_prob[:, :, :, t], (-i, -j, -k), axis=(0, 1, 2)) * p * dt
266
+ prob[:, :, :, s] -= pre_prob[:, :, :, s] * p * dt
267
+ prob[:, :, :, t] -= pre_prob[:, :, :, t] * p * dt
268
+
269
+ # Check the sum of probabilities
270
+ total_prob = np.sum(prob)
271
+ assert np.isclose(total_prob, 1.0), f"Total probability is not 1: {total_prob}"
272
+
273
+ # Average of outer products of positions
274
+ avg_outer_product = np.zeros((3, 3))
275
+ for i, j, k, l in np.ndindex(2 * size, 2 * size, 2 * size, n):
276
+ vec = np.array((i - size, j - size, k - size)) @ lattice
277
+ avg_outer_product += prob[i, j, k, l] * np.outer(vec, vec)
278
+
279
+ D = avg_outer_product / (2 * max_steps * dt)
280
+ return D
281
+
282
+
283
+ def diffusion_coefficient_tensor_MC(
284
+ lattice: NDArray[np.float64],
285
+ hop: List[Tuple[int, int, int, int, int, float]],
286
+ steps: int = 100,
287
+ particles: int = 10000
288
+ ) -> NDArray[np.float64]:
289
+ """Calculate diffusion coefficient tensor from Monte Carlo simulation using Gillespie algorithm.
290
+
291
+ Parameters
292
+ ----------
293
+ lattice : 3x3 numpy.array
294
+ lattice[0,:] is a-axis vector, lattice[1,:] b-axis vector, lattice[2,:] c-axis vector
295
+ hop : list of (int, int, int, int, int, float) tuple.
296
+ (s, t, i, j, k, p) means that the hopping rate from s-th molecule in (0, 0, 0) cell to t-th molecule in (i, j, k) cell is p.
297
+ steps : int
298
+ Number of steps
299
+ particles : int
300
+ Number of particles
301
+
302
+ Returns
303
+ -------
304
+ 3x3 numpy.array
305
+ Diffusion coefficient tensor
306
+ """
307
+ # Standardize hop list
308
+ hop = _standardize_hop_list(hop)
309
+
310
+ # Number of molecules in the unit cell
311
+ n = len(set([h[0] for h in hop]) | set([h[1] for h in hop]))
312
+
313
+ paths = [[] for _ in range(n)]
314
+ probs = [[] for _ in range(n)]
315
+ total_rates = [0] * n
316
+ for s, t, i, j, k, p in hop:
317
+ paths[s].append((t, i, j, k))
318
+ paths[t].append((s, -i, -j, -k))
319
+ probs[s].append(p)
320
+ probs[t].append(p)
321
+ total_rates[s] += p
322
+ total_rates[t] += p
323
+ max_time = steps / np.mean(total_rates)
324
+
325
+ # Simulation
326
+ sum_outer_product = np.zeros((3, 3))
327
+ for _ in range(particles):
328
+ xyz = np.zeros(3, dtype=int)
329
+ mol = random.choice(range(n)) # ランダムに初期位置を選ぶ
330
+ t = 0.0
331
+ while t < max_time:
332
+ t += -math.log(1.0 - random.random()) / total_rates[mol]
333
+ path = random.choices(paths[mol], weights=probs[mol])[0]
334
+ xyz += path[1], path[2], path[3]
335
+ mol = path[0]
336
+ xyz = xyz @ lattice
337
+ sum_outer_product += np.outer(xyz, xyz)
338
+
339
+ # Calculate diffusion coefficient
340
+ D = sum_outer_product / (particles * 2 * max_time)
341
+ return D
342
+
343
+
344
+ def print_tensor(tensor: NDArray[np.float64], msg: str = 'Mobility tensor'):
345
+ print('-' * (len(msg)+2))
346
+ print(f' {msg} ')
347
+ print('-' * (len(msg)+2))
348
+ if tensor.shape == (3, ):
349
+ print(f"{tensor[0]:12.6g} {tensor[1]:12.6g} {tensor[2]:12.6g}")
350
+ elif tensor.shape == (3, 3):
351
+ for a in tensor:
352
+ print(f"{a[0]:12.6g} {a[1]:12.6g} {a[2]:12.6g}")
353
+ print()
354
+
355
+
356
+ def _standardize_hop_list(hop: List[Tuple[int, int, int, int, int, float]]) -> List[Tuple[int, int, int, int, int, float]]:
357
+ """
358
+ Standardize the hop list by ensuring that s <= t.
359
+ If s == t, ensure that the first non-zero component of (i, j, k) is positive.
360
+
361
+ Parameters
362
+ ----------
363
+ hop: list of (int, int, int, int, int, float) tuples.
364
+ (s, t, i, j, k, p) means that the hopping rate from s-th molecule in (0, 0, 0) cell to t-th molecule in (i, j, k) cell is p.
365
+
366
+ Returns
367
+ -------
368
+ list of (int, int, int, int, int, float) tuple.
369
+ List of standardized hopping rate tuples.
370
+ """
371
+ hop = list(hop)
372
+ standardized_hop = []
373
+ standardized_hop_keys = set()
374
+ for s, t, i, j, k, p in hop:
375
+ if s > t:
376
+ s, t, i, j, k, p = t, s, -i, -j, -k, p
377
+ elif s == t:
378
+ if (i, j, k) == (0, 0, 0):
379
+ continue
380
+ elif i < 0 or (i == 0 and (j < 0 or (j == 0 and k < 0))):
381
+ s, t, i, j, k, p = t, s, -i, -j, -k, p
382
+
383
+ if (s, t, i, j, k) not in standardized_hop_keys:
384
+ standardized_hop_keys.add((s, t, i, j, k))
385
+ standardized_hop.append((s, t, i, j, k, p))
386
+
387
+ return standardized_hop
388
+
389
+
390
+ if __name__ == "__main__":
391
+ demo()