yu-mcal 0.1.0__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.
@@ -0,0 +1,1123 @@
1
+ """Tcal"""
2
+ import argparse
3
+ import csv
4
+ from datetime import datetime
5
+ import functools
6
+ import math
7
+ import os
8
+ import platform
9
+ import re
10
+ import subprocess
11
+ from time import time
12
+ import traceback
13
+
14
+ import numpy as np
15
+
16
+
17
+ print = functools.partial(print, flush=True)
18
+
19
+
20
+ def main():
21
+ """This code is to execute tcal for command line."""
22
+ parser = argparse.ArgumentParser()
23
+ parser.add_argument('file', help='file name', type=str)
24
+ parser.add_argument(
25
+ '-a', '--apta', help='perform atomic pair transfer analysis', action='store_true'
26
+ )
27
+ parser.add_argument('-c', '--cube', help='generate cube files', action='store_true')
28
+ # parser.add_argument('-d', '--debug', help='enable detailed output', action='store_true')
29
+ parser.add_argument(
30
+ '-g', '--g09', help='use Gaussian 09 (default is Gaussian 16)', action='store_true'
31
+ )
32
+ parser.add_argument(
33
+ '-l', '--lumo', help='perform atomic pair transfer analysis of LUMO', action='store_true'
34
+ )
35
+ parser.add_argument(
36
+ '-m', '--matrix', help='print MO coefficients, overlap matrix and Fock matrix', action='store_true'
37
+ )
38
+ parser.add_argument(
39
+ '-o', '--output', action='store_true',
40
+ help='output csv file on the result of apta and transfer integrals between diffrent orbitals etc.',
41
+ )
42
+ parser.add_argument(
43
+ '-r', '--read', help='read log files without executing Gaussian', action='store_true'
44
+ )
45
+ parser.add_argument('-x', '--xyz', help='convert xyz to gjf', action='store_true')
46
+ parser.add_argument(
47
+ '--napta', type=int, nargs=2, metavar=('N1', 'N2'),
48
+ help='perform atomic pair transfer analysis between different levels. ' \
49
+ 'N1 is the number of level in the first monomer. N2 is the number of level in the second monomer.'
50
+ )
51
+ parser.add_argument(
52
+ '--hetero', type=int, default=-1, metavar='N',
53
+ help='calculate the transfer integral of heterodimer. N is the number of atoms in the first monomer.'
54
+ )
55
+ parser.add_argument(
56
+ '--nlevel', type=int, metavar='N',
57
+ help='calculate transfer integrals between different levels. N is the number of levels from HOMO-LUMO. ' \
58
+ + 'N=0 gives all levels.'
59
+ )
60
+ parser.add_argument(
61
+ '--skip', type=int, nargs='+', default=[0], metavar='N', choices=[1, 2, 3],
62
+ help='skip specified Gaussian calculation. If N is 1, skip 1st monomer calculation. ' \
63
+ + 'If N is 2, skip 2nd monomer calculation. If N is 3, skip dimer calculation.'
64
+ )
65
+ args = parser.parse_args()
66
+
67
+ print('--------------------------------------')
68
+ print(' tcal 3.0 (2024/09/21) by Matsui Lab. ')
69
+ print('--------------------------------------')
70
+ print(f'\nInput File Name: {args.file}')
71
+ Tcal.print_timestamp()
72
+ before = time()
73
+ print()
74
+
75
+ tcal = Tcal(args.file, monomer1_atom_num=args.hetero)
76
+
77
+ # convert xyz to gjf
78
+ if args.xyz:
79
+ tcal.convert_xyz_to_gjf()
80
+
81
+ if not args.read:
82
+ tcal.create_monomer_file()
83
+
84
+ if args.g09:
85
+ gaussian_command = 'g09'
86
+ else:
87
+ gaussian_command = 'g16'
88
+ res = tcal.run_gaussian(gaussian_command, skip_monomer_num=args.skip)
89
+
90
+ if res:
91
+ print()
92
+ Tcal.print_timestamp()
93
+ after = time()
94
+ print(f'Elapsed Time: {(after - before) * 1000:.0f} ms')
95
+ exit()
96
+
97
+ try:
98
+ tcal.check_extension_log()
99
+ tcal.read_monomer1(args.matrix)
100
+ tcal.read_monomer2(args.matrix)
101
+ tcal.read_dimer(args.matrix)
102
+
103
+ if args.cube:
104
+ tcal.create_cube_file()
105
+
106
+ tcal.print_transfer_integrals()
107
+ if args.nlevel is not None:
108
+ tcal.print_tranfer_integral_diff_levels(args.nlevel, output_ti_diff_levels=args.output)
109
+
110
+ if args.apta:
111
+ analyze_orbital = 'HOMO'
112
+ elif args.lumo:
113
+ analyze_orbital = 'LUMO'
114
+
115
+ if args.napta:
116
+ apta = tcal.custom_atomic_pair_transfer_analysis(
117
+ analyze_orb1=args.napta[0], analyze_orb2=args.napta[1], output_apta=args.output
118
+ )
119
+ pair_analysis = PairAnalysis(apta)
120
+ pair_analysis.print_largest_pairs()
121
+ pair_analysis.print_element_pairs()
122
+ elif args.apta or args.lumo:
123
+ apta = tcal.atomic_pair_transfer_analysis(analyze_orbital, output_apta=args.output)
124
+ pair_analysis = PairAnalysis(apta)
125
+ pair_analysis.print_largest_pairs()
126
+ pair_analysis.print_element_pairs()
127
+ print()
128
+ except:
129
+ print(traceback.format_exc().strip())
130
+
131
+ Tcal.print_timestamp()
132
+ after = time()
133
+ print(f'Elapsed Time: {(after - before) * 1000:.0f} ms')
134
+
135
+
136
+ class Tcal:
137
+ """Calculate transfer integrals."""
138
+ EV = 4.35974417e-18 / 1.60217653e-19 * 1000.0
139
+
140
+ def __init__(self, file, monomer1_atom_num=-1):
141
+ """Inits TcalClass.
142
+
143
+ Parameters
144
+ ----------
145
+ file : str
146
+ A path of gjf file.
147
+ monomer1_atom_num: int
148
+ Number of atoms in the first monomer. default -1
149
+ If monomer1_atom_num is -1, it is half the number of atoms in the dimer.
150
+ """
151
+ if platform.system() == 'Windows':
152
+ self._extension_log = '.out'
153
+ else:
154
+ self._extension_log = '.log'
155
+
156
+ self._base_path = os.path.splitext(file)[0]
157
+ self.monomer1_atom_num = monomer1_atom_num
158
+
159
+ self.n_basis1 = None
160
+ self.n_elect1 = None
161
+ self.n_bsuse1 = None
162
+ self.n_basis2 = None
163
+ self.n_elect2 = None
164
+ self.n_bsuse2 = None
165
+ self.n_basis_d = None
166
+ self.n_elect_d = None
167
+ self.mo1 = None
168
+ self.mo2 = None
169
+ self.overlap = None
170
+ self.fock = None
171
+ self.atom_index = None
172
+ self.atom_symbol = None
173
+ self.atom_orbital = None
174
+
175
+ self.n_atoms1 = None
176
+ self.n_atoms2 = None
177
+
178
+ @staticmethod
179
+ def cal_transfer_integrals(bra, overlap, fock, ket):
180
+ """Calculate intermolecular transfer integrals.
181
+
182
+ Parameters
183
+ ----------
184
+ bra : numpy.array
185
+ MO coefficients of one molecule.
186
+ overlap : numpy.array
187
+ Overlap matrix of dimer.
188
+ fock : numpy.array
189
+ Fock matrix of dimer.
190
+ ket : numpy.array
191
+ MO coefficients of the other molecule.
192
+
193
+ Returns
194
+ -------
195
+ double
196
+ Intermolecular transfer integrals.
197
+ """
198
+ s11 = bra @ overlap @ bra
199
+ s22 = ket @ overlap @ ket
200
+ s12 = bra @ overlap @ ket
201
+ f11 = bra @ fock @ bra
202
+ f22 = ket @ fock @ ket
203
+ f12 = bra @ fock @ ket
204
+
205
+ if abs(s11 - 1) > 1e-2:
206
+ print(f"WARNING! Self overlap is not unity: S11 = {s11}")
207
+ if abs(s22 - 1) > 1e-2:
208
+ print(f"WARNING! Self overlap is not unity: S22 = {s22}")
209
+ transfer = ((f12 - 0.5 * (f11 + f22) * s12) / (1 - s12 * s12)) * Tcal.EV
210
+ return transfer
211
+
212
+ @staticmethod
213
+ def check_normal_termination(reader):
214
+ """Whether the calculation of gaussian was successful or not.
215
+
216
+ Parameters
217
+ ----------
218
+ reader : _io.TextIOWrapper
219
+ Return value of open function.
220
+
221
+ Returns
222
+ -------
223
+ _io.TextIOWrapper
224
+ Return value of function.
225
+
226
+ Examples
227
+ --------
228
+ >>> with open('sample.log', 'r') as f:
229
+ ... f = Tcal.check_normal_termination(f)
230
+ """
231
+ while True:
232
+ line = reader.readline()
233
+ if not line:
234
+ return reader
235
+ if 'Normal termination' in line:
236
+ return reader
237
+
238
+ @staticmethod
239
+ def extract_coordinates(reader):
240
+ """Extract coordinates from gjf file of dimer.
241
+
242
+ Parameters
243
+ ----------
244
+ reader : _io.TextIOWrapper
245
+ Return value of open function.
246
+
247
+ Returns
248
+ -------
249
+ _io.TextIOWrapper
250
+ Return value of open function.
251
+ list
252
+ The list of coordinates.
253
+
254
+ Examples
255
+ --------
256
+ >>> import re
257
+ >>> with open(f'sample.gjf', 'r') as f:
258
+ ... while True:
259
+ ... line = f.readline()
260
+ ... if not line:
261
+ ... break
262
+ ... if re.search('[-0-9]+ [0-3]', line):
263
+ ... f, coordinates = Tcal.extract_coordinates(f)
264
+ """
265
+ coordinates = []
266
+ while True:
267
+ line = reader.readline()
268
+ if not line.strip():
269
+ return reader, coordinates
270
+ coordinates.append(line)
271
+
272
+ @staticmethod
273
+ def extract_num(pattern, line, idx=0):
274
+ """Extract integer in strings.
275
+
276
+ Parameters
277
+ ----------
278
+ pattern : str
279
+ Strings using regular expression.
280
+ line : str
281
+ String of target.
282
+
283
+ Returns
284
+ -------
285
+ int or None
286
+ If there is integer, return it.
287
+ """
288
+ res = re.search(pattern, line)
289
+ if res:
290
+ return int(res.group().split()[idx])
291
+ return None
292
+
293
+ @staticmethod
294
+ def output_csv(file_name, array):
295
+ """Output csv file of array.
296
+
297
+ Parameters
298
+ ----------
299
+ file_name : str
300
+ File name including extension.
301
+ array : array_like
302
+ Array to create csv file.
303
+ """
304
+ with open(file_name, 'w', encoding='UTF-8', newline='') as f:
305
+ writer = csv.writer(f)
306
+ writer.writerows(array)
307
+
308
+ @staticmethod
309
+ def print_matrix(matrix):
310
+ """Print matrix.
311
+
312
+ Parameters
313
+ ----------
314
+ matrix : array_like
315
+ """
316
+ for i, row in enumerate(matrix):
317
+ for j, cell in enumerate(row[:-1]):
318
+ if i == 0 or j == 0:
319
+ print(f'{cell:^9}', end='\t')
320
+ else:
321
+ print(f'{cell:>9}', end='\t')
322
+ if i == 0:
323
+ print(f'{row[-1]:^9}')
324
+ else:
325
+ print(f'{row[-1]:>9}')
326
+
327
+ @staticmethod
328
+ def print_timestamp():
329
+ """Print timestamp."""
330
+ month = {
331
+ 1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
332
+ 7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec',
333
+ }
334
+ dt_now = datetime.now()
335
+ print(f"Timestamp: {dt_now.strftime('%a')} {month[dt_now.month]} {dt_now.strftime('%d %H:%M:%S %Y')}")
336
+
337
+ @staticmethod
338
+ def read_matrix(reader, n_basis, n_bsuse):
339
+ """Read matrix.
340
+
341
+ Parameters
342
+ ----------
343
+ reader : _io.TextIOWrapper
344
+ Return value of open function.
345
+ n_basis : int
346
+ The number of row.
347
+ n_bsuse : int
348
+ The number of column.
349
+
350
+ Returns
351
+ -------
352
+ numpy.array
353
+ Read matrix like MO coefficients.
354
+ """
355
+ mat = np.zeros((n_basis, n_bsuse), dtype=np.float64)
356
+ for i in range(math.ceil(n_bsuse/5)):
357
+ if 'Alpha density matrix' in reader.readline():
358
+ break
359
+ for j in range(n_basis):
360
+ for k, val in enumerate(reader.readline().split()[1:]):
361
+ mat[j][i*5+k] = float(val.strip().replace('D', 'E'))
362
+
363
+ return mat
364
+
365
+ @staticmethod
366
+ def read_symmetric_matrix(reader, n_basis):
367
+ """Read symmetric matrix.
368
+
369
+ Parameters
370
+ ----------
371
+ reader : _io.TextIOWrapper
372
+ Return value of open function.
373
+ n_basis : int
374
+ The number of column or row.
375
+
376
+ Returns
377
+ -------
378
+ numpy.array
379
+ Read symmetrix matrix like overlap or fock matrix.
380
+ """
381
+ mat = np.zeros((n_basis, n_basis), dtype=np.float64)
382
+ for i in range(math.ceil(n_basis/5)):
383
+ reader.readline()
384
+ for j in range(i*5, n_basis):
385
+ for k, val in enumerate(reader.readline().split()[1:]):
386
+ val = float(val.strip().replace('D', 'E'))
387
+ mat[j][i*5+k] = val
388
+ mat[i*5+k][j] = val
389
+
390
+ return mat
391
+
392
+ def atomic_pair_transfer_analysis(self, analyze_orbital='HOMO', output_apta=False):
393
+ """Calculate atomic pair transfer integrals.
394
+
395
+ Parameters
396
+ ----------
397
+ analyze_orbital : str, optional
398
+ Analyze orbital., default 'HOMO'
399
+ output_apta : bool, optional
400
+ If it is True, output csv file of atomic pair transfer integrals., default False
401
+ """
402
+ if analyze_orbital.upper() == 'LUMO':
403
+ orb1 = self.mo1[self.n_elect1]
404
+ orb2 = self.mo2[self.n_elect2]
405
+ else:
406
+ orb1 = self.mo1[self.n_elect1 - 1]
407
+ orb2 = self.mo2[self.n_elect2 - 1]
408
+
409
+ f11 = orb1 @ self.fock @ orb1
410
+ f22 = orb2 @ self.fock @ orb2
411
+ s12 = orb1 @ self.overlap @ orb2
412
+
413
+ sum_f = f11 + f22
414
+ pow_s12 = 1 - s12*s12
415
+ atom_num = len(set(self.atom_index))
416
+ a_transfer = np.zeros((atom_num, atom_num))
417
+ for i in range(self.n_basis_d):
418
+ for j in range(self.n_basis_d):
419
+ orb_transfer = orb1[i] * orb2[j] * (self.fock[i][j] - 0.5 * sum_f * self.overlap[i][j]) / pow_s12
420
+ a_transfer[self.atom_index[i]][self.atom_index[j]] += orb_transfer
421
+
422
+ apta = self.print_apta(a_transfer)
423
+
424
+ if output_apta:
425
+ self.output_csv(f'{self._base_path}_apta_{analyze_orbital}.csv', apta)
426
+
427
+ return apta
428
+
429
+ def check_extension_log(self):
430
+ """Check the extension of log file."""
431
+ if os.path.exists(f'{self._base_path}.out'):
432
+ self._extension_log = '.out'
433
+ else :
434
+ self._extension_log = '.log'
435
+
436
+ def convert_xyz_to_gjf(self, function='B3LYP/6-31G(d,p)', nprocshared=4, mem=16, unit='GB'):
437
+ """Convert xyz file to gjf file.
438
+
439
+ Parameters
440
+ ----------
441
+ function : str, optional
442
+ _description_, default 'b3lyp/6-31g(d,p)'
443
+ nprocshared : int, optional
444
+ The number of nprocshared., default 4
445
+ mem : int, optional
446
+ The number of memory., default 16
447
+ unit : str, optional
448
+ The unit of memory., default 'GB'
449
+ """
450
+ coordinates = []
451
+ with open(f'{self._base_path}.xyz', 'r') as f:
452
+ while True:
453
+ line = f.readline()
454
+ if not line:
455
+ break
456
+ line_list = line.strip().split()
457
+ if len(line_list) == 4:
458
+ symbol, x, y, z = line_list
459
+ try:
460
+ x, y, z = map(float, [x, y, z])
461
+ x = f'{x:.8f}'.rjust(14, ' ')
462
+ y = f'{y:.8f}'.rjust(14, ' ')
463
+ z = f'{z:.8f}'.rjust(14, ' ')
464
+ coordinates.append(f' {symbol} {x} {y} {z}\n')
465
+ except ValueError:
466
+ continue
467
+
468
+ with open(f'{self._base_path}.gjf', 'w') as f:
469
+ f.write(f'%Chk={self._base_path}.chk')
470
+ f.write('\n')
471
+ f.write(f'%NProcShared={nprocshared}')
472
+ f.write('\n')
473
+ f.write(f'%Mem={mem}{unit}\n')
474
+ f.write(f'# {function}\n')
475
+ f.write('# Symmetry=None\n')
476
+ f.write('\n')
477
+ f.write(f'{self._base_path}.gjf created by tcal\n')
478
+ f.write('\n')
479
+ f.write('0 1\n')
480
+
481
+ for coordinate in coordinates:
482
+ f.write(coordinate)
483
+
484
+ f.write('\n')
485
+ f.write('--Link1--\n')
486
+ f.write(f'%Chk={self._base_path}.chk')
487
+ f.write('\n')
488
+ f.write(f'%NProcShared={nprocshared}')
489
+ f.write('\n')
490
+ f.write(f'%Mem={mem}{unit}\n')
491
+ f.write(f'# {function}\n')
492
+ f.write('# Symmetry=None\n')
493
+ f.write('# Geom=AllCheck\n')
494
+ f.write('# Guess=Read\n')
495
+ f.write('# Pop=Full\n')
496
+ f.write('# IOp(3/33=4,5/33=3)\n')
497
+ f.write('\n')
498
+
499
+ def create_cube_file(self):
500
+ """Create cube file."""
501
+ self._execute(['formchk', f'{self._base_path}.chk', f'{self._base_path}.fchk'])
502
+ self._create_dummy_cube_file()
503
+ self._execute(['formchk', f'{self._base_path}_m1.chk', f'{self._base_path}_m1.fchk'])
504
+ self._execute(['cubegen', '0', f'mo={self.n_elect1-1}', f'{self._base_path}_m1.fchk', f'{self._base_path}_m1_NHOMO.cube', '-1', 'h', f'{self._base_path}_dummy.cube'])
505
+ self._execute(['cubegen', '0', f'mo={self.n_elect1}', f'{self._base_path}_m1.fchk', f'{self._base_path}_m1_HOMO.cube', '-1', 'h', f'{self._base_path}_dummy.cube'])
506
+ self._execute(['cubegen', '0', f'mo={self.n_elect1+1}', f'{self._base_path}_m1.fchk', f'{self._base_path}_m1_LUMO.cube', '-1', 'h', f'{self._base_path}_dummy.cube'])
507
+ self._execute(['cubegen', '0', f'mo={self.n_elect1+2}', f'{self._base_path}_m1.fchk', f'{self._base_path}_m1_NLUMO.cube', '-1', 'h', f'{self._base_path}_dummy.cube'])
508
+ print('cube file of the 1st monomer created')
509
+ print(f' {self._base_path}_m1_NHOMO.cube')
510
+ print(f' {self._base_path}_m1_HOMO.cube')
511
+ print(f' {self._base_path}_m1_LUMO.cube')
512
+ print(f' {self._base_path}_m1_NLUMO.cube')
513
+
514
+ self._execute(['formchk', f'{self._base_path}_m2.chk', f'{self._base_path}_m2.fchk'])
515
+ self._execute(['cubegen', '0', f'mo={self.n_elect2-1}', f'{self._base_path}_m2.fchk', f'{self._base_path}_m2_NHOMO.cube', '-1', 'h', f'{self._base_path}_dummy.cube'])
516
+ self._execute(['cubegen', '0', f'mo={self.n_elect2}', f'{self._base_path}_m2.fchk', f'{self._base_path}_m2_HOMO.cube', '-1', 'h', f'{self._base_path}_dummy.cube'])
517
+ self._execute(['cubegen', '0', f'mo={self.n_elect2+1}', f'{self._base_path}_m2.fchk', f'{self._base_path}_m2_LUMO.cube', '-1', 'h', f'{self._base_path}_dummy.cube'])
518
+ self._execute(['cubegen', '0', f'mo={self.n_elect2+2}', f'{self._base_path}_m2.fchk', f'{self._base_path}_m2_NLUMO.cube', '-1', 'h', f'{self._base_path}_dummy.cube'])
519
+ print('cube file of the 2nd monomer created')
520
+ print(f' {self._base_path}_m2_NHOMO.cube')
521
+ print(f' {self._base_path}_m2_HOMO.cube')
522
+ print(f' {self._base_path}_m2_LUMO.cube')
523
+ print(f' {self._base_path}_m2_NLUMO.cube')
524
+
525
+ def create_monomer_file(self):
526
+ """Create gjf files of monomer from gjf file of dimer."""
527
+ link0 = []
528
+ # List for storing characters between coordinates and link1 when gem is used for the basis function.
529
+ basis_gem = ['\n']
530
+ link1 = []
531
+ is_link0 = True
532
+ is_link1 = False
533
+ with open(f'{self._base_path}.gjf', 'r', encoding='utf-8') as f:
534
+ while True:
535
+ line = f.readline()
536
+ if not line:
537
+ break
538
+
539
+ if re.search('[-0-9]+ [0-3]', line):
540
+ link0.append('0 1\n')
541
+ f, coordinates = self.extract_coordinates(f)
542
+ is_link0 = False
543
+ elif r'%chk=' in line.lower():
544
+ continue
545
+ elif '--link1--' == line.strip().lower():
546
+ is_link1 = True
547
+ link1.append(line)
548
+ elif is_link0:
549
+ link0.append(line)
550
+ elif is_link1:
551
+ link1.append(line)
552
+ else:
553
+ basis_gem.append(line)
554
+
555
+ if self.monomer1_atom_num == -1:
556
+ mono_coordinates = coordinates[:len(coordinates)//2]
557
+ else:
558
+ mono_coordinates = coordinates[:self.monomer1_atom_num]
559
+
560
+ for i in (1, 2):
561
+ with open(f'{self._base_path}_m{i}.gjf', 'w', encoding='utf-8') as f:
562
+ f.write(f'%Chk={self._base_path}_m{i}.chk\n')
563
+ f.writelines(link0)
564
+
565
+ if i == 2:
566
+ if self.monomer1_atom_num == -1:
567
+ mono_coordinates = coordinates[len(coordinates)//2:]
568
+ else:
569
+ mono_coordinates = coordinates[self.monomer1_atom_num:]
570
+
571
+ f.writelines(mono_coordinates)
572
+ f.writelines(basis_gem)
573
+
574
+ f.write(f'{link1[0]}')
575
+ f.write(f'%Chk={self._base_path}_m{i}.chk\n')
576
+ f.writelines(link1[1:])
577
+
578
+ print('monomer gjf file created')
579
+ print(f' {self._base_path}_m1.gjf')
580
+ print(f' {self._base_path}_m2.gjf')
581
+
582
+ def custom_atomic_pair_transfer_analysis(self, analyze_orb1, analyze_orb2, output_apta=False):
583
+ """Calculate atomic pair transfer integrals.
584
+
585
+ Parameters
586
+ ----------
587
+ analyze_orb1 : int, optional
588
+ Analyze orbital., default -1
589
+ analyze_orb2 : int, optional
590
+ Analyze orbital., default -1
591
+ output_apta : bool, optional
592
+ If it is True, output csv file of atomic pair transfer integrals., default False
593
+ """
594
+ orb1 = self.mo1[analyze_orb1-1]
595
+ orb2 = self.mo2[analyze_orb2-1]
596
+
597
+ f11 = orb1 @ self.fock @ orb1
598
+ f22 = orb2 @ self.fock @ orb2
599
+ s12 = orb1 @ self.overlap @ orb2
600
+
601
+ sum_f = f11 + f22
602
+ pow_s12 = 1 - s12*s12
603
+ atom_num = len(set(self.atom_index))
604
+ a_transfer = np.zeros((atom_num, atom_num))
605
+ for i in range(self.n_basis_d):
606
+ for j in range(self.n_basis_d):
607
+ orb_transfer = orb1[i] * orb2[j] * (self.fock[i][j] - 0.5 * sum_f * self.overlap[i][j]) / pow_s12
608
+ a_transfer[self.atom_index[i]][self.atom_index[j]] += orb_transfer
609
+
610
+ apta = self.print_apta(
611
+ a_transfer,
612
+ message=f'Atomic Pair Transfer Analysis (monomer 1 is {analyze_orb1}-th orbital, monomer 2 is {analyze_orb2}-th orbital)'
613
+ )
614
+
615
+ if output_apta:
616
+ self.output_csv(f'{self._base_path}_apta_{analyze_orb1}_{analyze_orb2}.csv', apta)
617
+
618
+ return apta
619
+
620
+ def print_apta(self, a_transfer, message='Atomic Pair Transfer Analysis'):
621
+ """Create list of apta and print it.
622
+
623
+ Parameters
624
+ ----------
625
+ a_transfer : numpy.array
626
+ Result of atomic pair transfer analysis.
627
+ message : str, optional
628
+ Message to print., default 'Atomic Pair Transfer Analysis'
629
+
630
+ Returns
631
+ -------
632
+ numpy.array
633
+ The array of atomic pair transfer analysis.
634
+ """
635
+ n_atoms = self.atom_index[-1] + 1
636
+
637
+ labels = [''] * n_atoms
638
+ for i in range(self.n_basis_d):
639
+ labels[self.atom_index[i]] = self.atom_symbol[i]
640
+
641
+ col_sum = np.sum(a_transfer, axis=1)
642
+ row_sum = np.sum(a_transfer, axis=0)
643
+ total_sum = np.sum(a_transfer)
644
+
645
+ print()
646
+ print('-' * (len(message)+2))
647
+ print(f' {message} ')
648
+ print('-' * (len(message)+2))
649
+ apta = []
650
+ tmp_list = ['atom']
651
+ for i in range(self.n_atoms1, n_atoms):
652
+ tmp_list.append(f'{i+1}{labels[i]}')
653
+ tmp_list.append('sum')
654
+ apta.append(tmp_list)
655
+
656
+ for i in range(self.n_atoms1):
657
+ tmp_list = []
658
+ tmp_list.append(f'{i+1}{labels[i]}')
659
+ for j in range(self.n_atoms1, n_atoms):
660
+ tmp_list.append(f'{a_transfer[i][j] * Tcal.EV:.3f}')
661
+ tmp_list.append(f'{col_sum[i] * Tcal.EV:.3f}')
662
+ apta.append(tmp_list)
663
+
664
+ tmp_list = ['sum']
665
+ for j in range(self.n_atoms1, n_atoms):
666
+ tmp_list.append(f'{row_sum[j] * Tcal.EV:.3f}')
667
+ tmp_list.append(f'{total_sum * Tcal.EV:.3f}')
668
+ apta.append(tmp_list)
669
+
670
+ self.print_matrix(apta)
671
+
672
+ return apta
673
+
674
+ def print_transfer_integrals(self):
675
+ """Print transfer integrals of NLUMO, LUMO, HOMO and NHOMO."""
676
+ print()
677
+ print("--------------------")
678
+ print(" Transfer Integrals ")
679
+ print("--------------------")
680
+ transfer = self.cal_transfer_integrals(
681
+ self.mo1[self.n_elect1 + 1], self.overlap, self.fock, self.mo2[self.n_elect2 + 1]
682
+ )
683
+ print(f'NLUMO\t{transfer:>9.3f}\tmeV')
684
+
685
+ transfer = self.cal_transfer_integrals(
686
+ self.mo1[self.n_elect1], self.overlap, self.fock, self.mo2[self.n_elect2]
687
+ )
688
+ print(f'LUMO \t{transfer:>9.3f}\tmeV')
689
+
690
+ transfer = self.cal_transfer_integrals(
691
+ self.mo1[self.n_elect1 - 1], self.overlap, self.fock, self.mo2[self.n_elect2 - 1]
692
+ )
693
+ print(f'HOMO \t{transfer:>9.3f}\tmeV')
694
+
695
+ transfer = self.cal_transfer_integrals(
696
+ self.mo1[self.n_elect1 - 2], self.overlap, self.fock, self.mo2[self.n_elect2 - 2]
697
+ )
698
+ print(f'NHOMO\t{transfer:>9.3f}\tmeV')
699
+
700
+ def print_tranfer_integral_diff_levels(self, nlevel, output_ti_diff_levels=False):
701
+ print()
702
+ print('----------------------------------------------')
703
+ print(' Tranfer Integrals between Different Orbitals ')
704
+ print('----------------------------------------------')
705
+
706
+ if nlevel == 0:
707
+ start1 = 0
708
+ start2 = 0
709
+ end1 = len(self.mo1)
710
+ end2 = len(self.mo2)
711
+ else:
712
+ start1 = max(0, self.n_elect1 - nlevel)
713
+ start2 = max(0, self.n_elect2 - nlevel)
714
+ end1 = min(len(self.mo1), self.n_elect1 + nlevel)
715
+ end2 = min(len(self.mo2), self.n_elect2 + nlevel)
716
+
717
+ print(f'HOMO of monomer 1 is {self.n_elect1}-th orbital')
718
+ print(f'HOMO of monomer 2 is {self.n_elect2}-th orbital')
719
+ print(f'print all combinations between ({start1+1}, {end1}) and ({start2+1}, {end2}).')
720
+ print()
721
+
722
+ ti_diff_levels = []
723
+ tmp_list = ['mono1/mono2']
724
+ for i in range(start2, end2):
725
+ if i+1 == self.n_elect2:
726
+ tmp_list.append(f'HOMO({i+1})')
727
+ elif i+1 == self.n_elect2+1:
728
+ tmp_list.append(f'LUMO({i+1})')
729
+ else:
730
+ tmp_list.append(f'{i+1}')
731
+ ti_diff_levels.append(tmp_list)
732
+
733
+ for i in range(start1, end1):
734
+ if i+1 == self.n_elect1:
735
+ tmp_list = [f'HOMO({i+1})']
736
+ elif i+1 == self.n_elect1+1:
737
+ tmp_list = [f'LUMO({i+1})']
738
+ else:
739
+ tmp_list = [f'{i+1}']
740
+ for j in range(start2, end2):
741
+ transfer = self.cal_transfer_integrals(
742
+ self.mo1[i], self.overlap, self.fock, self.mo2[j]
743
+ )
744
+ tmp_list.append(f'{transfer:.3f}')
745
+ ti_diff_levels.append(tmp_list)
746
+
747
+ self.print_matrix(ti_diff_levels)
748
+
749
+ if output_ti_diff_levels:
750
+ self.output_csv(f'{self._base_path}_ti_diff_levels.csv', ti_diff_levels)
751
+
752
+ def read_monomer1(self, is_matrix=False, output_matrix=False):
753
+ """Extract MO coefficients from log file of monomer.
754
+
755
+ Parameters
756
+ ----------
757
+ is_matrix : bool, optional
758
+ If it is True, print MO coefficients., default False
759
+ output_matrix : bool, optional
760
+ If it is True, Output MO coefficients., default False
761
+ """
762
+ print(f'reading {self._base_path}_m1{self._extension_log}')
763
+ with open(f'{self._base_path}_m1{self._extension_log}', 'r', encoding='utf-8') as f:
764
+ f = self.check_normal_termination(f)
765
+ while True:
766
+ line = f.readline()
767
+ if not line:
768
+ break
769
+
770
+ if self.n_basis1 is None:
771
+ self.n_basis1 = self.extract_num('[0-9]+ basis functions', line)
772
+
773
+ if self.n_bsuse1 is None:
774
+ self.n_bsuse1 = self.extract_num(r'NBsUse=\s*[0-9]+', line, idx=-1)
775
+
776
+ if self.n_elect1 is None:
777
+ self.n_elect1 = self.extract_num('[0-9]+ beta electrons', line)
778
+
779
+ if self.n_atoms1 is None:
780
+ self.n_atoms1 = self.extract_num(r'NAtoms=\s*[0-9]+', line, idx=-1)
781
+
782
+ if 'Alpha MO coefficients at cycle' in line:
783
+ self.mo1 = self.read_matrix(f, self.n_basis1, self.n_bsuse1)
784
+ break
785
+
786
+ if is_matrix:
787
+ print(' *** Alpha MO coefficients *** ')
788
+ self.print_matrix(self.mo1)
789
+
790
+ if output_matrix:
791
+ self.output_csv(f'{self._base_path}_mo1.csv', self.mo1)
792
+
793
+ def read_monomer2(self, is_matrix=False, output_matrix=False):
794
+ """Extract MO coefficients from log file of monomer.
795
+
796
+ Parameters
797
+ ----------
798
+ is_matrix : bool, optional
799
+ If it is True, print MO coefficients., default False
800
+ output_matrix : bool, optional
801
+ If it is True, Output MO coefficients., default False
802
+ """
803
+ print(f'reading {self._base_path}_m2{self._extension_log}')
804
+ with open(f'{self._base_path}_m2{self._extension_log}', 'r', encoding='utf-8') as f:
805
+ f = self.check_normal_termination(f)
806
+ while True:
807
+ line = f.readline()
808
+ if not line:
809
+ break
810
+
811
+ if self.n_basis2 is None:
812
+ self.n_basis2 = self.extract_num('[0-9]+ basis functions', line)
813
+
814
+ if self.n_bsuse2 is None:
815
+ self.n_bsuse2 = self.extract_num(r'NBsUse=\s*[0-9]+', line, idx=-1)
816
+
817
+ if self.n_elect2 is None:
818
+ self.n_elect2 = self.extract_num('[0-9]+ alpha electrons', line)
819
+
820
+ if self.n_atoms2 is None:
821
+ self.n_atoms2 = self.extract_num(r'NAtoms=\s*[0-9]+', line, idx=-1)
822
+
823
+ if 'Alpha MO coefficients at cycle' in line:
824
+ self.mo2 = self.read_matrix(f, self.n_basis2, self.n_bsuse2)
825
+ break
826
+
827
+ if is_matrix:
828
+ print(' *** Alpha MO coefficients *** ')
829
+ self.print_matrix(self.mo2)
830
+
831
+ if output_matrix:
832
+ self.output_csv(f'{self._base_path}_mo2.csv', self.mo2)
833
+
834
+ def read_dimer(self, is_matrix=False, output_matrix=False):
835
+ """Extract overlap and fock matrix from log file of dimer.
836
+
837
+ Parameters
838
+ ----------
839
+ is_matrix : bool, optional
840
+ If it is True, print overlap and fock matrix., default False
841
+ output_matrix : bool, optional
842
+ If it is True, Output overlap and fock matrix., default False
843
+ """
844
+ print(f'reading {self._base_path}{self._extension_log}')
845
+ with open(f'{self._base_path}{self._extension_log}', 'r', encoding='utf-8') as f:
846
+ f = self.check_normal_termination(f)
847
+ while True:
848
+ line = f.readline()
849
+ if not line:
850
+ break
851
+
852
+ if self.n_basis_d is None:
853
+ self.n_basis_d = self.extract_num('[0-9]+ basis functions', line)
854
+
855
+ if self.n_elect_d is None:
856
+ self.n_elect_d = self.extract_num('[0-9]+ beta electrons', line)
857
+
858
+ if '*** Overlap ***' in line:
859
+ self.overlap = self.read_symmetric_matrix(f, self.n_basis_d)
860
+
861
+ if 'Fock matrix (alpha):' in line:
862
+ self.fock = self.read_symmetric_matrix(f, self.n_basis_d)
863
+
864
+ if 'Gross orbital populations:' in line:
865
+ self._read_gross_orb_populations(f)
866
+ break
867
+
868
+ zeros_matrix = np.zeros((self.n_bsuse1, self.n_basis2))
869
+ self.mo1 = np.hstack([self.mo1.T, zeros_matrix])
870
+ zeros_matrix = np.zeros((self.n_bsuse2, self.n_basis1))
871
+ self.mo2 = np.hstack([zeros_matrix, self.mo2.T])
872
+
873
+ if is_matrix:
874
+ print()
875
+ print(' *** Overlap *** ')
876
+ self.print_matrix(self.overlap)
877
+ print()
878
+ print(' *** Fock matrix (alpha) *** ')
879
+ self.print_matrix(self.fock)
880
+
881
+ if output_matrix:
882
+ self.output_csv(f'{self._base_path}_overlap.csv', self.overlap)
883
+ self.output_csv(f'{self._base_path}_fock.csv', self.fock)
884
+
885
+ def run_gaussian(self, gaussian_command, skip_monomer_num=[0]):
886
+ """Execute gjf files using gaussian.
887
+
888
+ Parameters
889
+ ----------
890
+ gaussian_command : str
891
+ Command of gaussian.
892
+ skip_monomer_num: list[int], optional
893
+ If it is 1, skip 1st monomer calculation.
894
+ If it is 2, skip 2nd monomer calculation.
895
+ If it is 3, skip dimer calculation.
896
+
897
+ Returns
898
+ -------
899
+ int
900
+ Returncode of subprocess.run.
901
+ """
902
+ if 1 in skip_monomer_num:
903
+ print('skip 1st monomer calculation')
904
+ else:
905
+ res = self._execute([gaussian_command, f'{self._base_path}_m1.gjf'], complete_message='1st monomer calculation completed')
906
+
907
+ if res.returncode:
908
+ return res.returncode
909
+
910
+ if 2 in skip_monomer_num:
911
+ print('skip 2nd monomer calculation')
912
+ else:
913
+ res = self._execute([gaussian_command, f'{self._base_path}_m2.gjf'], complete_message='2nd monomer calculation completed')
914
+
915
+ if res.returncode:
916
+ return res.returncode
917
+
918
+ if 3 in skip_monomer_num:
919
+ print('skip dimer calculation')
920
+ else:
921
+ res = self._execute([gaussian_command, f'{self._base_path}.gjf'], complete_message='dimer calculation completed')
922
+
923
+ return res.returncode
924
+
925
+ return 0
926
+
927
+ def _create_dummy_cube_file(self):
928
+ """Create dummy cube file."""
929
+ min_x = 100
930
+ max_x = -100
931
+ min_y = 100
932
+ max_y = -100
933
+ min_z = 100
934
+ max_z = -100
935
+ cube_delta = 0.5
936
+ bohr = 0.52917721092
937
+
938
+ with open(f'{self._base_path}.gjf', 'r') as f:
939
+ while True:
940
+ line = f.readline()
941
+ if not line:
942
+ break
943
+ if re.search('[-0-9]+ [0-3]', line):
944
+ f, coordinates = self.extract_coordinates(f)
945
+ break
946
+
947
+ for line in coordinates:
948
+ _, x, y, z = line.strip().split()
949
+ x, y, z = map(float, [x, y, z])
950
+ min_x = min(min_x, x)
951
+ max_x = max(max_x, x)
952
+ min_y = min(min_y, y)
953
+ max_y = max(max_y, y)
954
+ min_z = min(min_z, z)
955
+ max_z = max(max_z, z)
956
+
957
+ min_x -= 5.0
958
+ max_x += 5.0
959
+ min_y -= 5.0
960
+ max_y += 5.0
961
+ min_z -= 5.0
962
+ max_z += 5.0
963
+ min_x = math.floor(min_x / cube_delta) * cube_delta
964
+ min_y = math.floor(min_y / cube_delta) * cube_delta
965
+ min_z = math.floor(min_z / cube_delta) * cube_delta
966
+
967
+ num_x = math.ceil((max_x - min_x) / cube_delta)
968
+ num_y = math.ceil((max_y - min_y) / cube_delta)
969
+ num_z = math.ceil((max_z - min_z) / cube_delta)
970
+
971
+ with open(f'{self._base_path}_dummy.cube', 'w') as f:
972
+ f.write('\n\n')
973
+ f.write(f'1, {min_x/bohr:.3f}, {min_y/bohr:.3f}, {min_z/bohr:.3f}\n')
974
+ f.write(f'{num_x}, {cube_delta/bohr:.3f}, 0.0, 0.0\n')
975
+ f.write(f'{num_y}, 0.0, {cube_delta/bohr:.3f}, 0.0\n')
976
+ f.write(f'{num_z}, 0.0, 0.0, {cube_delta/bohr:.3f}\n')
977
+
978
+ def _execute(self, command_list, complete_message='Calculation completed'):
979
+ """Execute command
980
+
981
+ Parameters
982
+ ----------
983
+ command_list : list
984
+ A list of space-separated commands.
985
+ complete_message : str, optional
986
+ The message when the calculation is completed., default 'Calculation completed'
987
+
988
+ Returns
989
+ -------
990
+ CompletedProcess
991
+ Return value of subprocess.run.
992
+ """
993
+ command = ' '.join(command_list)
994
+ print(f'> {command}')
995
+
996
+ res = subprocess.run(command_list, capture_output=True, text=True)
997
+
998
+ # check error
999
+ if res.returncode:
1000
+ print(f'Failed to execute {command}')
1001
+ else:
1002
+ print(complete_message)
1003
+ base_path = os.path.splitext(command_list[-1])[0]
1004
+ print(f' {base_path}{self._extension_log}')
1005
+
1006
+ return res
1007
+
1008
+ def _read_gross_orb_populations(self, reader):
1009
+ """Extract atomic orbitals and atomic symbols.
1010
+
1011
+ Parameters
1012
+ ----------
1013
+ reader : _io.TextIOWrapper
1014
+ Return value of open function.
1015
+ """
1016
+ reader.readline()
1017
+
1018
+ self.atom_index = []
1019
+ self.atom_symbol = []
1020
+ self.atom_orbital = []
1021
+
1022
+ idx_num = 1
1023
+ for i in range(1, self.n_basis_d+1):
1024
+ line = reader.readline().strip()
1025
+ # remove number of row
1026
+ line = line[re.match(f'{i}', line).span()[1]:].strip()
1027
+ # check atom
1028
+ change_atom = re.match(f'{idx_num} ', line)
1029
+
1030
+ if change_atom:
1031
+ self.atom_index.append(idx_num-1)
1032
+ line = line[change_atom.span()[1]:].strip()
1033
+ elem = line[:2].strip()
1034
+ self.atom_symbol.append(elem)
1035
+ line = line[re.match(f'{elem}', line).span()[1]:].strip()
1036
+ idx_num += 1
1037
+ else:
1038
+ self.atom_index.append(self.atom_index[-1])
1039
+ self.atom_symbol.append(self.atom_symbol[-1])
1040
+ self.atom_orbital.append(line[:5].strip())
1041
+
1042
+
1043
+ class PairAnalysis:
1044
+ """Analyze atomic pair transfer integrals."""
1045
+ def __init__(self, apta):
1046
+ """Inits PairAnalysisClass.
1047
+
1048
+ Parameters
1049
+ ----------
1050
+ apta : list
1051
+ List of atomic pair transfer integrals including labels.
1052
+ """
1053
+ self._labels = []
1054
+ self._a_transfer = []
1055
+
1056
+ for row in apta[1:-1]:
1057
+ symbol = re.sub('[0-9]+', '', row[0])
1058
+ self._labels.append(symbol)
1059
+ self._a_transfer.append(row[1:-1])
1060
+
1061
+ label = apta[0][1:-1]
1062
+ label = list(map(lambda x: re.sub('[0-9]+', '', x), label))
1063
+
1064
+ self._labels.extend(label)
1065
+ self._a_transfer = np.array(self._a_transfer, dtype=np.float64)
1066
+ self.n_atoms1 = self._a_transfer.shape[0]
1067
+ self.n_atoms2 = self._a_transfer.shape[1]
1068
+
1069
+ def print_largest_pairs(self):
1070
+ """Print largest pairs."""
1071
+ transfer = np.sum(self._a_transfer)
1072
+ a_transfer_flat = self._a_transfer.flatten()
1073
+ sorted_index = np.argsort(a_transfer_flat)
1074
+ print()
1075
+ print('---------------')
1076
+ print(' Largest Pairs ')
1077
+ print('---------------')
1078
+ print(f'rank\tpair{" " * 9}\ttransfer (meV)\tratio (%)')
1079
+
1080
+ rank_list = np.arange(1, len(sorted_index)+1)
1081
+ if len(sorted_index) <= 20:
1082
+ print_index = sorted_index
1083
+ ranks = rank_list
1084
+ else:
1085
+ print_index = np.hstack([sorted_index[:10], sorted_index[-10:]])
1086
+ ranks = np.hstack([rank_list[:10], rank_list[-10:]])
1087
+
1088
+ for i, a_i in enumerate(reversed(print_index)):
1089
+ row_i = a_i // self.n_atoms2
1090
+ col_i = a_i % self.n_atoms2
1091
+ pair = f'{row_i+1}{self._labels[row_i]}' + ' - ' + \
1092
+ f'{col_i+self.n_atoms2+1}{self._labels[col_i]}'
1093
+ ratio = np.divide(a_transfer_flat[a_i], transfer, out=np.array(0.0), where=(transfer!=0)) * 100
1094
+ print(f'{ranks[i]:<4}\t{pair:<13}\t{a_transfer_flat[a_i]:>14}\t{ratio:>9.1f}')
1095
+
1096
+ def print_element_pairs(self):
1097
+ """Print element pairs."""
1098
+ element_pair = {
1099
+ 'H-I': 0.0, 'C-C': 0.0, 'C-H': 0.0, 'C-S': 0.0, 'C-Se': 0.0,
1100
+ 'C-I': 0.0, 'S-S': 0.0, 'Se-Se': 0.0, 'N-S': 0.0, 'I-S': 0.0,
1101
+ 'I-I': 0.0,
1102
+ }
1103
+ keys = element_pair.keys()
1104
+
1105
+ for i in range(self.n_atoms1):
1106
+ sym1 = self._labels[i]
1107
+ for j in range(self.n_atoms2):
1108
+ sym2 = self._labels[self.n_atoms1 + j]
1109
+ key = '-'.join(sorted([sym1, sym2]))
1110
+ if key in keys:
1111
+ element_pair[key] += self._a_transfer[i][j]
1112
+
1113
+ print()
1114
+ print('---------------')
1115
+ print(' Element Pairs ')
1116
+ print('---------------')
1117
+ for k, value in element_pair.items():
1118
+ if value != 0:
1119
+ print(f'{k:<5}\t{value:>9.3f}')
1120
+
1121
+
1122
+ if __name__ == '__main__':
1123
+ main()