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/mcal.py ADDED
@@ -0,0 +1,844 @@
1
+ """mcal"""
2
+ import argparse
3
+ import functools
4
+ import pickle
5
+ from pathlib import Path
6
+ from time import time
7
+ from typing import Dict, List, Literal, Optional, Tuple, Union
8
+
9
+ import numpy as np
10
+ from numpy.typing import NDArray
11
+ from tcal import Tcal
12
+
13
+ from mcal.utils.cif_reader import CifReader
14
+ from mcal.utils.gaus_log_reader import check_normal_termination
15
+ from mcal.utils.gjf_maker import GjfMaker
16
+ from mcal.calculations.hopping_mobility_model import (
17
+ diffusion_coefficient_tensor,
18
+ diffusion_coefficient_tensor_MC,
19
+ diffusion_coefficient_tensor_ODE,
20
+ marcus_rate,
21
+ mobility_tensor
22
+ )
23
+ from mcal.calculations.rcal import Rcal
24
+
25
+
26
+ print = functools.partial(print, flush=True)
27
+
28
+
29
+ def main():
30
+ """Calculate mobility tensor considering anisotropy and path continuity.
31
+
32
+ Examples
33
+ --------
34
+ Basic usage:
35
+ - Calculate p-type mobility for xxx crystal\n
36
+ $ python hop_mcal.py xxx.cif p
37
+
38
+ - Calculate n-type mobility for xxx crystal\n
39
+ $ python hop_mcal.py xxx.cif n
40
+
41
+ With resource options:
42
+ - Use 8 CPUs and 16GB memory\n
43
+ $ python hop_mcal.py xxx.cif p -c 8 -m 16
44
+
45
+ - Use different calculation method (default is B3LYP/6-31G(d,p))\n
46
+ $ python hop_mcal.py xxx.cif p -M "B3LYP/6-311G(d,p)"
47
+
48
+ High-precision calculation:
49
+ - Calculate all transfer integrals without speedup using moment of inertia and distance between centers of weight\n
50
+ $ python hop_mcal.py xxx.cif p --fullcal
51
+
52
+ - Expand calculation range to 3x3x3 supercell\n
53
+ $ python hop_mcal.py xxx.cif p --cellsize 1
54
+
55
+ - Expand calculation range to 5x5x5 supercell to widen transfer integral calculation range\n
56
+ $ python hop_mcal.py xxx.cif p --cellsize 2
57
+
58
+ Resume and save results:
59
+ - Resume from existing calculations\n
60
+ $ python hop_mcal.py xxx.cif p --resume
61
+
62
+ - Save results to pickle file\n
63
+ $ python hop_mcal.py xxx.cif p --pickle
64
+
65
+ - Read results from existing pickle file\n
66
+ $ python hop_mcal.py xxx_result.pkl p -rp
67
+
68
+ - Read results from existing log files without running Gaussian\n
69
+ $ python hop_mcal.py xxx.cif p -r
70
+
71
+ Compare calculation methods:
72
+ - Compare results using kinetic Monte Carlo and ODE methods\n
73
+ $ python hop_mcal.py xxx.cif p --mc --ode
74
+ """
75
+ # Error range for skipping calculation of transfer integrals using moment of inertia and distance between centers of weight.
76
+ CENTER_OF_WEIGHT_ERROR = 1.0e-7
77
+ MOMENT_OF_INERTIA_ERROR = np.array([[1.0e-3, 1.0e-3, 1.0e-3]])
78
+
79
+ """This code is to execute hop_mcal for command line."""
80
+ parser = argparse.ArgumentParser()
81
+ parser.add_argument('file', help='cif file name or pickle file name if you want to use -rp option', type=str)
82
+ parser.add_argument('osc_type', help='organic semiconductor type', type=str)
83
+ parser.add_argument(
84
+ '-M', '--method',
85
+ help='calculation method used in Gaussian calculations (default is B3LYP/6-31G(d,p))',
86
+ type=str,
87
+ default='B3LYP/6-31G(d,p)',
88
+ )
89
+ parser.add_argument('-c', '--cpu', help='setting the number of cpu (default is 4)', type=int, default=4)
90
+ parser.add_argument(
91
+ '-m', '--mem',
92
+ help='setting the number of memory [GB] (default is 10 GB)',
93
+ type=int,
94
+ default=10,
95
+ )
96
+ parser.add_argument('-g', '--g09', help='use Gaussian 09 (default is Gaussian 16)', action='store_true')
97
+ parser.add_argument('-r', '--read', help='read log files without executing Gaussian', action='store_true')
98
+ parser.add_argument(
99
+ '-rp', '--read_pickle',
100
+ help='read results from existing pickle file',
101
+ action='store_true'
102
+ )
103
+ parser.add_argument('-p', '--pickle', help='save to pickle the result of calculation', action='store_true')
104
+ parser.add_argument(
105
+ '--cellsize',
106
+ help='number of unit cells to expand in each direction around the central unit cell '
107
+ '(Examples: 1 creates 3x3x3, 2 creates 5x5x5 supercell (default is 2))',
108
+ type=int,
109
+ default=2,
110
+ )
111
+ parser.add_argument(
112
+ '--fullcal',
113
+ help='do not process for speeding up using moment of inertia and distance between centers of weight',
114
+ action='store_true',
115
+ )
116
+ parser.add_argument('--mc', help='use Monte Carlo method to calculate diffusion coefficient', action='store_true')
117
+ parser.add_argument(
118
+ '--ode',
119
+ help='use Ordinary Differential Equation method to calculate diffusion coefficient',
120
+ action='store_true',
121
+ )
122
+ parser.add_argument(
123
+ '--resume',
124
+ help='resume calculation',
125
+ action='store_true',
126
+ )
127
+ args = parser.parse_args()
128
+
129
+ args.osc_type = args.osc_type.lower()
130
+
131
+ if args.g09:
132
+ gau_com = 'g09'
133
+ else:
134
+ gau_com = 'g16'
135
+
136
+ # file info
137
+ cif_file = Path(args.file)
138
+ directory = cif_file.parent
139
+ filename = cif_file.stem
140
+ cif_path_without_ext = f'{directory}/{filename}'
141
+
142
+ print('----------------------------------------')
143
+ print(' mcal 0.1.4 (2026/01/20) by Matsui Lab. ')
144
+ print('----------------------------------------')
145
+
146
+ if args.read_pickle:
147
+ read_pickle(args.file)
148
+ exit()
149
+
150
+ print(f'\nCalculate as {args.osc_type}-type organic semiconductor.')
151
+ print(f'\nInput File Name: {args.file}')
152
+ Tcal.print_timestamp()
153
+ print()
154
+ start_time = time()
155
+
156
+ ##### Calculate reorganization energy #####
157
+ cif_reader = CifReader(cif_path=cif_file)
158
+ print(f'Export {cif_path_without_ext}_unit_cell.mol')
159
+ cif_reader.export_unit_cell_file(f'{cif_path_without_ext}_unit_cell.mol', format='mol')
160
+ print('Please verify that the created unit cell is correct.\n')
161
+ symbols = cif_reader.unique_symbols[0]
162
+ coordinates = cif_reader.unique_coords[0]
163
+ coordinates = cif_reader.convert_frac_to_cart(coordinates)
164
+
165
+ if not args.read:
166
+ print('Create gjf for reorganization energy.')
167
+ create_reorg_gjf(
168
+ symbols,
169
+ coordinates,
170
+ filename,
171
+ directory,
172
+ args.cpu,
173
+ args.mem,
174
+ args.method,
175
+ )
176
+
177
+ if args.osc_type == 'p':
178
+ rcal = Rcal(gjf_file=f'{cif_path_without_ext}_opt_n.gjf')
179
+ elif args.osc_type == 'n':
180
+ rcal = Rcal(gjf_file=f'{cif_path_without_ext}_opt_n.gjf', osc_type='n')
181
+ else:
182
+ raise OSCTypeError
183
+
184
+ skip_specified_cal = []
185
+ if args.read:
186
+ print('Skip calculation of reorganization energy.')
187
+ elif args.resume:
188
+ rcal.check_extension_log(f'{cif_path_without_ext}_opt_n.gjf')
189
+ skip_specified_cal = check_reorganization_energy_completion(cif_path_without_ext, args.osc_type, extension_log=rcal._extension_log)
190
+ else:
191
+ print('Calculate reorganization energy.')
192
+
193
+ reorg_energy = rcal.calc_reorganization(gau_com=gau_com, only_read=args.read, is_output_detail=True, skip_specified_cal=skip_specified_cal)
194
+
195
+ print_reorg_energy(args.osc_type, reorg_energy)
196
+
197
+ ##### Calculate transfer integrals #####
198
+ transfer_integrals = []
199
+ mom_dis_ti = [] # Store moment of inertia, distance between centers of weight and transfer integral
200
+
201
+ expand_mols = cif_reader.expand_mols(args.cellsize)
202
+ for s in range(len(cif_reader.unique_symbols.keys())):
203
+ unique_symbols = cif_reader.unique_symbols[s]
204
+ unique_coords = cif_reader.unique_coords[s]
205
+ unique_coords = cif_reader.convert_frac_to_cart(unique_coords)
206
+ for (i, j, k), expand_mol in expand_mols.items():
207
+ for t, (symbols, coordinates) in expand_mol.items():
208
+ # Skip creating gjf for transfer integrals because they are molecules with translation symmetry
209
+ if s > t:
210
+ continue
211
+ elif s == t:
212
+ if (i, j, k) == (0, 0, 0):
213
+ continue
214
+ elif i < 0 or (i == 0 and (j < 0 or (j == 0 and k < 0))):
215
+ continue
216
+
217
+ coordinates = cif_reader.convert_frac_to_cart(coordinates)
218
+
219
+ min_distance = cal_min_distance(
220
+ unique_symbols, unique_coords,
221
+ symbols, coordinates
222
+ )
223
+ if min_distance > 5:
224
+ print()
225
+ print(f'Skip calculation of transfer integral from {s}-th in (0,0,0) cell to {t}-th in ({i},{j},{k}) cell because the minimum distance is over 5 \u212B.\n')
226
+ continue
227
+
228
+ moment, _ = cal_moment_of_inertia(
229
+ unique_symbols, unique_coords,
230
+ symbols, coordinates
231
+ )
232
+
233
+ distance = cal_distance_between_cen_of_weight(
234
+ unique_symbols, unique_coords,
235
+ symbols, coordinates
236
+ )
237
+
238
+ is_run_ti = True
239
+ same_ti = 0
240
+
241
+ # skip calculation of transfer integrals using moment of inertia and distance between centers of weight.
242
+ if not args.fullcal:
243
+ for m, d, ti in mom_dis_ti:
244
+ if (np.all(m - MOMENT_OF_INERTIA_ERROR < moment) and np.all(moment < m + MOMENT_OF_INERTIA_ERROR)) and (d - CENTER_OF_WEIGHT_ERROR < distance < d + CENTER_OF_WEIGHT_ERROR):
245
+ is_run_ti = False
246
+ same_ti = ti
247
+ break
248
+
249
+ if is_run_ti:
250
+ gjf_name = f'{filename}-({s}_{t}_{i}_{j}_{k})'
251
+ gjf_file = f'{directory}/{gjf_name}'
252
+
253
+ tcal = Tcal(gjf_file)
254
+
255
+ is_normal_term = False
256
+ if args.resume:
257
+ tcal.check_extension_log()
258
+ is_normal_term = check_transfer_integral_completion(gjf_file, extension_log=tcal._extension_log)
259
+
260
+ if not args.read and not is_normal_term:
261
+ print()
262
+ print('Create gjf for transfer integral.')
263
+ create_ti_gjf(
264
+ {'symbols': unique_symbols, 'coordinates': unique_coords},
265
+ {'symbols': symbols, 'coordinates': coordinates},
266
+ gjf_basename=gjf_name,
267
+ save_dir=directory,
268
+ cpu=args.cpu,
269
+ mem=args.mem,
270
+ method=args.method,
271
+ )
272
+ tcal.create_monomer_file()
273
+
274
+ if args.g09:
275
+ gaussian_command = 'g09'
276
+ else:
277
+ gaussian_command = 'g16'
278
+ print(f'Calculate transfer integral from {s}-th in (0,0,0) cell to {t}-th in ({i},{j},{k}) cell.')
279
+ tcal.run_gaussian(gaussian_command)
280
+ else:
281
+ print()
282
+ print(f'Skip calculation of transfer integral from {s}-th in (0,0,0) cell to {t}-th in ({i},{j},{k}) cell.')
283
+
284
+ tcal.check_extension_log()
285
+ tcal.read_monomer1()
286
+ tcal.read_monomer2()
287
+ tcal.read_dimer()
288
+
289
+ if args.osc_type == 'p':
290
+ transfer = Tcal.cal_transfer_integrals(
291
+ tcal.mo1[tcal.n_elect1-1], tcal.overlap, tcal.fock, tcal.mo2[tcal.n_elect2-1]
292
+ )
293
+ elif args.osc_type == 'n':
294
+ transfer = Tcal.cal_transfer_integrals(
295
+ tcal.mo1[tcal.n_elect1], tcal.overlap, tcal.fock, tcal.mo2[tcal.n_elect2]
296
+ )
297
+
298
+ transfer = transfer * 1e-3 # meV to eV
299
+ print_transfer_integral(args.osc_type, transfer)
300
+ transfer_integrals.append((s, t, i, j, k, transfer))
301
+ mom_dis_ti.append((moment, distance, transfer))
302
+ else:
303
+ print()
304
+ print(f'Skip calculation of transfer integral from {s}-th in (0,0,0) cell to {t}-th in ({i},{j},{k}) cell due to identical moment of inertia and distance between centers of weight.')
305
+ print_transfer_integral(args.osc_type, same_ti)
306
+ transfer_integrals.append((s, t, i, j, k, same_ti))
307
+
308
+ ##### Calculate mobility tensor considering anisotropy. #####
309
+ hop = []
310
+
311
+ for s, t, i, j, k, ti in transfer_integrals:
312
+ hop.append((s, t, i, j, k, marcus_rate(ti, reorg_energy)))
313
+
314
+ diffusion_coef_tensor = diffusion_coefficient_tensor(cif_reader.lattice * 1e-8, hop)
315
+ print_tensor(diffusion_coef_tensor, msg="Diffusion coefficient tensor (cm^2/s)")
316
+ mu = mobility_tensor(diffusion_coef_tensor)
317
+ print_tensor(mu, msg="Mobility tensor (cm^2/Vs)")
318
+ value, vector = cal_eigenvalue_decomposition(mu)
319
+ print_mobility(value, vector)
320
+
321
+ ##### Simulate mobility tensor calculation using Monte Carlo method #####
322
+ if args.mc:
323
+ D_MC = diffusion_coefficient_tensor_MC(cif_reader.lattice * 1e-8, hop)
324
+ print_tensor(D_MC, msg="Diffusion coefficient tensor (cm^2/s) (MC)")
325
+ mu_MC = mobility_tensor(D_MC)
326
+ print_tensor(mu_MC, msg="Mobility tensor (cm^2/Vs) (MC)")
327
+ value_MC, vector_MC = cal_eigenvalue_decomposition(mu_MC)
328
+ print_mobility(value_MC, vector_MC, sim_type='MC')
329
+
330
+ ##### Simulate mobility tensor calculation using Ordinary Differential Equation method #####
331
+ if args.ode:
332
+ D_ODE = diffusion_coefficient_tensor_ODE(cif_reader.lattice * 1e-8, hop)
333
+ print_tensor(D_ODE, msg="Diffusion coefficient tensor (cm^2/s) (ODE)")
334
+ mu_ODE = mobility_tensor(D_ODE)
335
+ print_tensor(mu_ODE, msg="Mobility tensor (cm^2/Vs) (ODE)")
336
+ value_ODE, vector_ODE = cal_eigenvalue_decomposition(mu_ODE)
337
+ print_mobility(value_ODE, vector_ODE, sim_type='ODE')
338
+
339
+ # Save reorganization, transfer integrals, hop, mobility tensor
340
+ if args.pickle:
341
+ with open(f'{cif_path_without_ext}_result.pkl', 'wb') as f:
342
+ pickle.dump({
343
+ 'osc_type': args.osc_type,
344
+ 'lattice': cif_reader.lattice,
345
+ 'z_value': cif_reader.z_value,
346
+ 'reorganization': reorg_energy,
347
+ 'transfer_integrals': transfer_integrals,
348
+ 'hop': hop,
349
+ 'diffusion_coefficient_tensor': diffusion_coef_tensor,
350
+ 'mobility_tensor': mu,
351
+ 'mobility_value': value,
352
+ 'mobility_vector': vector
353
+ }, f)
354
+
355
+ Tcal.print_timestamp()
356
+ end_time = time()
357
+ elapsed_time = end_time - start_time
358
+ elapsed_time_h = int(elapsed_time // 3600)
359
+ elapsed_time_min = int((elapsed_time - elapsed_time_h * 3600) // 60)
360
+ elapsed_time_sec = int(elapsed_time - elapsed_time_h * 3600 - elapsed_time_min * 60)
361
+ elapsed_time_ms = (elapsed_time - elapsed_time_h * 3600 - elapsed_time_min * 60 - elapsed_time_sec) * 1000
362
+ if elapsed_time < 1:
363
+ print(f'Elapsed Time: {elapsed_time_ms:.0f} ms')
364
+ elif elapsed_time < 60:
365
+ print(f'Elapsed Time: {elapsed_time_sec} sec')
366
+ elif elapsed_time < 3600:
367
+ print(f'Elapsed Time: {elapsed_time_min} min {elapsed_time_sec} sec')
368
+ else:
369
+ print(f'Elapsed Time: {elapsed_time_h} h {elapsed_time_min} min {elapsed_time_sec} sec')
370
+
371
+
372
+ def atom_weight(symbol: str) -> float:
373
+ """Get atom weight
374
+
375
+ Parameters
376
+ ----------
377
+ symbol : str
378
+ Symbol of atom
379
+
380
+ Returns
381
+ -------
382
+ float
383
+ Atomic weight
384
+ """
385
+ ELEMENT_PROP = CifReader.ELEMENT_PROP
386
+ weight = ELEMENT_PROP[ELEMENT_PROP['symbol'] == symbol]['weight'].values[0]
387
+
388
+ return weight
389
+
390
+
391
+ def cal_cen_of_weight(
392
+ symbols1: NDArray[str],
393
+ coordinates1: NDArray[np.float64],
394
+ symbols2: Optional[NDArray[str]] = None,
395
+ coordinates2: Optional[NDArray[np.float64]] = None,
396
+ ) -> NDArray[np.float64]:
397
+ """Calculate center of weight
398
+
399
+ Parameters
400
+ ----------
401
+ symbols1 : NDArray[str]
402
+ Symbols of atoms in one monomer
403
+ coordinates1 : NDArray[np.float64]
404
+ Coordinates of atoms in one monomer
405
+ symbols2 : Optional[NDArray[str]], optional
406
+ Symbols of atoms in another monomer, by default None
407
+ coordinates2 : Optional[NDArray[np.float64]], optional
408
+ Coordinates of atoms in another monomer, by default None
409
+
410
+ Returns
411
+ -------
412
+ NDArray[np.float64]
413
+ Center of weight
414
+ """
415
+ if symbols2 is not None and coordinates2 is not None:
416
+ symbols1 = np.concatenate((symbols1, symbols2), axis=0)
417
+ coordinates1 = np.concatenate((coordinates1, coordinates2), axis=0)
418
+
419
+ weights = np.array([atom_weight(sym) for sym in symbols1])
420
+ total_weight = np.sum(weights)
421
+
422
+ weighted_coords = weights[:, np.newaxis] * coordinates1
423
+ weighted_sum = np.sum(weighted_coords, axis=0)
424
+
425
+ cen_of_weight = weighted_sum / total_weight
426
+
427
+ return cen_of_weight
428
+
429
+
430
+ def cal_distance_between_cen_of_weight(
431
+ symbols1: NDArray[str],
432
+ coordinates1: NDArray[np.float64],
433
+ symbols2: NDArray[str],
434
+ coordinates2: NDArray[np.float64],
435
+ ) -> float:
436
+ """Calculate distance between centers of weight
437
+
438
+ Parameters
439
+ ----------
440
+ symbols1 : NDArray[str]
441
+ Symbols of atoms in one monomer
442
+ coordinates1 : NDArray[np.float64]
443
+ Coordinates of atoms in one monomer
444
+ symbols2 : NDArray[str]
445
+ Symbols of atoms in another monomer
446
+ coordinates2 : NDArray[np.float64]
447
+ Coordinates of atoms in another monomer
448
+
449
+ Returns
450
+ -------
451
+ float
452
+ Distance between centers of weight
453
+ """
454
+ mol1_cen_coord = cal_cen_of_weight(symbols1, coordinates1)
455
+ mol2_cen_coord = cal_cen_of_weight(symbols2, coordinates2)
456
+ distance = np.sqrt(np.sum(np.square(mol1_cen_coord-mol2_cen_coord)))
457
+
458
+ return distance
459
+
460
+
461
+ def cal_eigenvalue_decomposition(mobility_tensor: NDArray[np.float64]) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
462
+ """Calculate eigenvalue decomposition of mobility tensor
463
+
464
+ Parameters
465
+ ----------
466
+ mobility_tensor : NDArray[np.float64]
467
+ Mobility tensor
468
+
469
+ Returns
470
+ -------
471
+ Tuple[NDArray[np.float64], NDArray[np.float64]]
472
+ Eigenvalue(mobility value) and eigenvector(mobility vector)
473
+ """
474
+ value, vector = np.linalg.eig(mobility_tensor)
475
+ return value, vector
476
+
477
+
478
+ def cal_min_distance(
479
+ symbols1: NDArray[str],
480
+ coords1: NDArray[np.float64],
481
+ symbols2: NDArray[str],
482
+ coords2: NDArray[np.float64],
483
+ ) -> float:
484
+ """Calculate minimum distance between two sets of atoms.
485
+
486
+ Parameters
487
+ ----------
488
+ symbols1 : NDArray[str]
489
+ Symbols of atoms in one monomer
490
+ coords1 : NDArray[np.float64]
491
+ Coordinates of atoms in one monomer
492
+ symbols2 : NDArray[str]
493
+ Symbols of atoms in another monomer
494
+ coords2 : NDArray[np.float64]
495
+ Coordinates of atoms in another monomer
496
+
497
+ Returns
498
+ -------
499
+ float
500
+ Minimum distance between two sets of atoms
501
+ """
502
+ ELEMENT_PROP = CifReader.ELEMENT_PROP
503
+ VDW_RADII = ELEMENT_PROP[['symbol', 'vdw_radius']].set_index('symbol').to_dict()['vdw_radius']
504
+
505
+ radii1 = np.array(
506
+ [VDW_RADII[symbol] for symbol in symbols1]
507
+ )
508
+ radii2 = np.array(
509
+ [VDW_RADII[symbol] for symbol in symbols2]
510
+ )
511
+
512
+ distances = np.sqrt(np.sum((coords1[:, np.newaxis] - coords2)**2, axis=2)) - radii1[:, np.newaxis] - radii2
513
+
514
+ min_distance = np.min(distances)
515
+
516
+ return min_distance
517
+
518
+
519
+ def cal_moment_of_inertia(
520
+ symbols1: NDArray[str],
521
+ coordinates1: NDArray[np.float64],
522
+ symbols2: NDArray[str],
523
+ coordinates2: NDArray[np.float64],
524
+ ) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
525
+ """Calculate moment of inertia and eigenvectors of the inertia tensor.
526
+
527
+ Parameters
528
+ ----------
529
+ symbols1 : NDArray[str]
530
+ Symbols of atoms in one monomer
531
+ coordinates1 : NDArray[np.float64]
532
+ Coordinates of atoms in one monomer
533
+ symbols2 : NDArray[str]
534
+ Symbols of atoms in another monomer
535
+ coordinates2 : NDArray[np.float64]
536
+ Coordinates of atoms in another monomer
537
+
538
+ Returns
539
+ -------
540
+ Tuple[NDArray[np.float64], NDArray[np.float64]]
541
+ Moment of inertia and eigenvectors of the inertia tensor
542
+ """
543
+ symbols1 = np.concatenate((symbols1, symbols2), axis=0)
544
+ coordinates1 = np.concatenate((coordinates1, coordinates2), axis=0)
545
+
546
+ cen_of_weight = cal_cen_of_weight(symbols1, coordinates1)
547
+
548
+ weights = np.array([atom_weight(sym) for sym in symbols1])
549
+
550
+ xi = coordinates1[:, 0] - cen_of_weight[0]
551
+ yi = coordinates1[:, 1] - cen_of_weight[1]
552
+ zi = coordinates1[:, 2] - cen_of_weight[2]
553
+
554
+ tmp_coords = np.column_stack((xi, yi, zi))
555
+
556
+ moment = np.zeros((3, 3))
557
+
558
+ for i in range(3):
559
+ moment[i, i] = np.sum(weights * (tmp_coords[:, (i+1)%3]**2 + tmp_coords[:, (i+2)%3]**2))
560
+
561
+ for i in range(3):
562
+ for j in range(i+1, 3):
563
+ moment[i, j] = moment[j, i] = -np.sum(weights * tmp_coords[:, i] * tmp_coords[:, j])
564
+
565
+ moment, p = np.linalg.eig(moment)
566
+
567
+ return moment, p
568
+
569
+
570
+ def check_reorganization_energy_completion(
571
+ cif_path_without_ext: str,
572
+ osc_type: Literal['p', 'n'],
573
+ extension_log: str = '.log'
574
+ ) -> List[Literal['opt_neutral', 'opt_ion', 'neutral', 'ion']]:
575
+ """Check if all reorganization energy calculations are completed normally.
576
+
577
+ Parameters
578
+ ----------
579
+ cif_path_without_ext : str
580
+ Base path of cif file (without extension)
581
+ osc_type : Literal['p', 'n']
582
+ Semiconductor type (p-type or n-type)
583
+ extension_log : str
584
+ Extension of log file
585
+
586
+ Returns
587
+ -------
588
+ List[Literal['opt_neutral', 'opt_ion', 'neutral', 'ion']]
589
+ List of calculations to skip
590
+ """
591
+ skip_specified_cal = []
592
+ if check_normal_termination(f'{cif_path_without_ext}_opt_n{extension_log}'):
593
+ skip_specified_cal.append('opt_neutral')
594
+ if check_normal_termination(f'{cif_path_without_ext}_n{extension_log}'):
595
+ skip_specified_cal.append('neutral')
596
+
597
+ if osc_type == 'p':
598
+ if check_normal_termination(f'{cif_path_without_ext}_opt_c{extension_log}'):
599
+ skip_specified_cal.append('opt_ion')
600
+ if check_normal_termination(f'{cif_path_without_ext}_c{extension_log}'):
601
+ skip_specified_cal.append('ion')
602
+ elif osc_type == 'n':
603
+ if check_normal_termination(f'{cif_path_without_ext}_opt_a{extension_log}'):
604
+ skip_specified_cal.append('opt_ion')
605
+ if check_normal_termination(f'{cif_path_without_ext}_a{extension_log}'):
606
+ skip_specified_cal.append('ion')
607
+
608
+ return skip_specified_cal
609
+
610
+
611
+ def check_transfer_integral_completion(gjf_file: str, extension_log: str = '.log') -> bool:
612
+ """Check if all transfer integral calculations are completed normally.
613
+
614
+ Parameters
615
+ ----------
616
+ gjf_file : str
617
+ Base path of gjf file (without extension)
618
+
619
+ Returns
620
+ -------
621
+ bool
622
+ True if all calculations (dimer, monomer1, monomer2) terminated normally
623
+ """
624
+ required_files = ['', '_m1', '_m2']
625
+ return all(
626
+ check_normal_termination(f'{gjf_file}{suffix}{extension_log}')
627
+ for suffix in required_files
628
+ )
629
+
630
+
631
+ def create_reorg_gjf(
632
+ symbols: NDArray[str],
633
+ coordinates: NDArray[np.float64],
634
+ basename: str,
635
+ save_dir: str,
636
+ cpu: int,
637
+ mem: int,
638
+ method: str,
639
+ ) -> None:
640
+ """Create gjf file for reorganization energy calculation.
641
+
642
+ Parameters
643
+ ----------
644
+ symbols : NDArray[str]
645
+ Symbols of atoms
646
+ coordinates : NDArray[np.float64]
647
+ Coordinates of atoms
648
+ basename : str
649
+ Base name of gjf file
650
+ save_dir : str
651
+ Directory to save gjf file
652
+ cpu : int
653
+ Number of cpu
654
+ mem : int
655
+ Number of memory [GB]
656
+ method : str
657
+ Calculation method used in Gaussian calculations
658
+ """
659
+ gjf_maker = GjfMaker()
660
+ gjf_maker.set_function(method)
661
+ gjf_maker.create_chk_file()
662
+ gjf_maker.output_detail()
663
+ gjf_maker.opt()
664
+
665
+ gjf_maker.set_symbols(symbols)
666
+ gjf_maker.set_coordinates(coordinates)
667
+ gjf_maker.set_resource(cpu_num=cpu, mem_num=mem)
668
+
669
+ gjf_maker.export_gjf(
670
+ file_name=f'{basename}_opt_n',
671
+ save_dir=save_dir,
672
+ chk_rwf_name=f'{save_dir}/{basename}_opt_n'
673
+ )
674
+
675
+
676
+ def create_ti_gjf(
677
+ unique_mol: Dict[str, Union[NDArray[str], NDArray[np.float64]]],
678
+ neighbor_mol: Dict[str, Union[NDArray[str], NDArray[np.float64]]],
679
+ gjf_basename: str,
680
+ save_dir: str = '.',
681
+ cpu: int = 4,
682
+ mem: int = 16,
683
+ method: str = 'B3LYP/6-31G*',
684
+ ) -> None:
685
+ """Create gjf file for transfer integral calculation.
686
+
687
+ Parameters
688
+ ----------
689
+ unique_mol : Dict[str, Union[NDArray[str], NDArray[np.float64]]]
690
+ Dictionary containing symbols and coordinates of unique monomer
691
+ neighbor_mol : Dict[str, Union[NDArray[str], NDArray[np.float64]]]
692
+ Dictionary containing symbols and coordinates of neighbor monomer
693
+ gjf_basename : str
694
+ Base name of gjf file
695
+ save_dir : str
696
+ Directory to save gjf file, by default '.'
697
+ cpu : int
698
+ Number of cpu, by default 4
699
+ mem : int
700
+ Number of memory [GB], by default 16
701
+ method : str
702
+ Calculation method used in Gaussian calculations, by default 'B3LYP/6-31G(d,p)'
703
+ """
704
+ gjf_maker = GjfMaker()
705
+ gjf_maker.set_resource(cpu_num=cpu, mem_num=mem)
706
+ gjf_maker.set_function(method)
707
+ gjf_maker.create_chk_file()
708
+ gjf_maker.add_root('Symmetry=None')
709
+
710
+ gjf_maker.set_symbols(unique_mol['symbols'])
711
+ gjf_maker.set_coordinates(unique_mol['coordinates'])
712
+ gjf_maker.set_symbols(neighbor_mol['symbols'])
713
+ gjf_maker.set_coordinates(neighbor_mol['coordinates'])
714
+
715
+ gjf_maker.add_link()
716
+ gjf_maker.add_root('Symmetry=None')
717
+ gjf_maker.add_root('Pop=Full')
718
+ gjf_maker.add_root('IOp(3/33=4,5/33=3)')
719
+
720
+ gjf_maker.export_gjf(file_name=gjf_basename, save_dir=save_dir)
721
+
722
+
723
+ def print_mobility(value: NDArray[np.float64], vector: NDArray[np.float64], sim_type: Literal['MC', 'ODE'] = ''):
724
+ """Print mobility and mobility vector
725
+
726
+ Parameters
727
+ ----------
728
+ value : NDArray[np.float64]
729
+ Mobility value
730
+ vector : NDArray[np.float64]
731
+ Mobility vector
732
+ sim_type : str
733
+ Simulation type (MC or ODE)
734
+ """
735
+ msg_value = 'Mobility eigenvalues (cm^2/Vs)'
736
+ msg_vector = 'Mobility eigenvectors'
737
+ direction = ['x', 'y', 'z']
738
+
739
+ if sim_type:
740
+ msg_value += f' ({sim_type})'
741
+ msg_vector += f' ({sim_type})'
742
+
743
+ print()
744
+ print('-' * (len(msg_value)+2))
745
+ print(f' {msg_value} ')
746
+ print('-' * (len(msg_value)+2))
747
+ print(f"{value[0]:12.6g} {value[1]:12.6g} {value[2]:12.6g}")
748
+ print()
749
+
750
+ print()
751
+ print('-' * (len(msg_vector)+2))
752
+ print(f' {msg_vector} ')
753
+ print('-' * (len(msg_vector)+2))
754
+ print(' vector1 vector2 vector3')
755
+ for v, d in zip(vector, direction):
756
+ print(f'{d} {v[0]:12.6g} {v[1]:12.6g} {v[2]:12.6g}')
757
+ print()
758
+
759
+
760
+ def print_reorg_energy(osc_type: Literal['p', 'n'], reorg_energy: float):
761
+ """Print reorganization energy
762
+
763
+ Parameters
764
+ ----------
765
+ osc_type : Literal['p', 'n']
766
+ Semiconductor type (p-type or n-type)
767
+ reorg_energy : float
768
+ Reorganization energy [eV]
769
+ """
770
+ print()
771
+ print('-----------------------')
772
+ print(' Reorganization energy ')
773
+ print('-----------------------')
774
+ print(f'{osc_type}-type: {reorg_energy:10.6g} eV\n')
775
+
776
+
777
+ def print_tensor(mu: NDArray[np.float64], msg: str = 'Mobility tensor'):
778
+ """Print mobility tensor
779
+
780
+ Parameters
781
+ ----------
782
+ mu : NDArray[np.float64]
783
+ Mobility tensor
784
+ msg : str
785
+ Message, by default 'Mobility tensor'
786
+ """
787
+ print()
788
+ print('-' * (len(msg)+2))
789
+ print(f' {msg} ')
790
+ print('-' * (len(msg)+2))
791
+ for a in mu:
792
+ print(f"{a[0]:12.6g} {a[1]:12.6g} {a[2]:12.6g}")
793
+ print()
794
+
795
+
796
+ def print_transfer_integral(osc_type: Literal['p', 'n'], transfer: float):
797
+ """Print transfer integral
798
+
799
+ Parameters
800
+ ----------
801
+ osc_type : Literal['p', 'n']
802
+ Semiconductor type (p-type or n-type)
803
+ transfer : float
804
+ Transfer integral [eV]
805
+ """
806
+ mol_orb = {'p': 'HOMO', 'n': 'LUMO'}
807
+ print()
808
+ print('-------------------')
809
+ print(' Transfer integral ')
810
+ print('-------------------')
811
+ print(f'{mol_orb[osc_type]}: {transfer:12.6g} eV\n')
812
+
813
+
814
+ def read_pickle(file_name: str):
815
+ print(f'\nInput File Name: {file_name}')
816
+
817
+ with open(file_name, 'rb') as f:
818
+ results = pickle.load(f)
819
+
820
+ # print(results)
821
+
822
+ print(f'\nCalculate as {results["osc_type"]}-type organic semiconductor.')
823
+
824
+ print_reorg_energy(results['osc_type'], results['reorganization'])
825
+
826
+ for s, t, i, j, k, ti in results['transfer_integrals']:
827
+ print()
828
+ print(f'{s}-th in (0,0,0) cell to {t}-th in ({i},{j},{k}) cell')
829
+ print_transfer_integral(results['osc_type'], ti)
830
+
831
+ print_tensor(results['diffusion_coefficient_tensor'], msg="Diffusion coefficient tensor (cm^2/s)")
832
+
833
+ print_tensor(results['mobility_tensor'], msg="Mobility tensor (cm^2/Vs)")
834
+
835
+ print_mobility(results['mobility_value'], results['mobility_vector'])
836
+
837
+
838
+ class OSCTypeError(Exception):
839
+ """Exception for semiconductor type"""
840
+ pass
841
+
842
+
843
+ if __name__ == '__main__':
844
+ main()