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,408 @@
1
+ """Rcal"""
2
+ import argparse
3
+ import functools
4
+ import os
5
+ import subprocess
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from time import time
9
+ from typing import List, Literal
10
+
11
+ from mcal.utils.cif_reader import CifReader
12
+ from mcal.utils.gjf_maker import GjfMaker
13
+
14
+
15
+ print = functools.partial(print, flush=True)
16
+
17
+
18
+ def main():
19
+ """This code is to execute rcal for command line."""
20
+ parser = argparse.ArgumentParser()
21
+ parser.add_argument('file', help='cif file name or gjf file name', type=str)
22
+ parser.add_argument('osc_type', help='organic semiconductor type', type=str)
23
+ parser.add_argument(
24
+ '-M', '--method',
25
+ help='calculation method used in Gaussian calculations (default is B3LYP/6-31G(d,p)). ' \
26
+ + 'But if you use a gjf file instead of a cif file, the method in the gjf file will be used',
27
+ type=str,
28
+ default='B3LYP/6-31G(d,p)',
29
+ )
30
+ parser.add_argument(
31
+ '-c', '--cpu',
32
+ help='setting the number of cpu (default is 4). ' \
33
+ + 'But if you use a gjf file instead of a cif file, the number of cpu in the gjf file will be used',
34
+ type=int, default=4
35
+ )
36
+ parser.add_argument(
37
+ '-m', '--mem',
38
+ help='setting the number of memory [GB] (default is 10 GB). ' \
39
+ + 'But if you use a gjf file instead of a cif file, the number of memory in the gjf file will be used',
40
+ type=int,
41
+ default=10,
42
+ )
43
+ parser.add_argument('-g', '--g09', help='use Gaussian 09 (default is Gaussian 16)', action='store_true')
44
+ parser.add_argument('-r', '--read', help='read log files without executing Gaussian', action='store_true')
45
+ args = parser.parse_args()
46
+
47
+ print('---------------------------------------')
48
+ print(' rcal beta (2025/06/21) by Matsui Lab. ')
49
+ print('---------------------------------------')
50
+ print(f'\nInput File Name: {args.file}')
51
+ Rcal.print_timestamp()
52
+ before = time()
53
+
54
+ if args.file.endswith('.cif'):
55
+ cif_file = Path(args.file)
56
+ directory = cif_file.parent
57
+ filename = cif_file.stem.replace('_opt_n', '')
58
+ file_path_without_ext = f'{directory}/{filename}_opt_n'
59
+
60
+ cif_reader = CifReader(cif_path=cif_file)
61
+ symbols = cif_reader.unique_symbols[0]
62
+ coordinates = cif_reader.unique_coords[0]
63
+ coordinates = cif_reader.convert_frac_to_cart(coordinates)
64
+
65
+ gjf_maker = GjfMaker()
66
+ gjf_maker.create_chk_file()
67
+ gjf_maker.output_detail()
68
+ gjf_maker.opt()
69
+
70
+ gjf_maker.set_symbols(symbols)
71
+ gjf_maker.set_coordinates(coordinates)
72
+ gjf_maker.set_function(args.method)
73
+ gjf_maker.set_charge_spin(charge=0, spin=1)
74
+ gjf_maker.set_resource(cpu_num=args.cpu, mem_num=args.mem)
75
+
76
+ gjf_maker.export_gjf(
77
+ file_name=f'{file_path_without_ext}',
78
+ chk_rwf_name=f'{file_path_without_ext}',
79
+ )
80
+ elif args.file.endswith('.gjf'):
81
+ gjf_file = Path(args.file)
82
+ directory = gjf_file.parent
83
+ filename = gjf_file.stem
84
+
85
+ file_path_without_ext = f'{directory}/{filename}'
86
+ else:
87
+ raise ValueError('Input file must be a cif file or a gjf file.')
88
+
89
+ if args.osc_type.lower() == 'p':
90
+ rcal = Rcal(gjf_file=f'{file_path_without_ext}.gjf')
91
+ elif args.osc_type.lower() == 'n':
92
+ rcal = Rcal(gjf_file=f'{file_path_without_ext}.gjf', osc_type='n')
93
+ else:
94
+ raise OSCTypeError
95
+
96
+ if args.g09:
97
+ gau_com = 'g09'
98
+ else:
99
+ gau_com = 'g16'
100
+
101
+ reorg_energy = rcal.calc_reorganization(gau_com=gau_com, only_read=args.read, is_output_detail=True)
102
+
103
+ print()
104
+ print('-----------------------')
105
+ print(' Reorganization energy ')
106
+ print('-----------------------')
107
+ print(f'{reorg_energy:12.6f} eV')
108
+ print()
109
+
110
+ Rcal.print_timestamp()
111
+ after = time()
112
+ print(f'Elapsed Time: {(after - before) * 1000:.0f} ms')
113
+
114
+
115
+ class Rcal:
116
+ """Calculate organization energy."""
117
+ def __init__(self, gjf_file: str, osc_type: Literal['p', 'n'] = 'p'):
118
+ """
119
+ Initialize Rcal.
120
+
121
+ Parameters
122
+ ----------
123
+ gjf_file : str
124
+ gjf file name.
125
+ osc_type : Literal['p', 'n']
126
+ organic semiconductor type, 'p' is positive, 'n' is negative, by default 'p'.
127
+ """
128
+ self.gjf_file = gjf_file
129
+ self.ion = None
130
+ self._extension_log = '.log'
131
+ self._gjf_lines = {'%': [], '#': []}
132
+
133
+ self._input_gjf()
134
+
135
+ if osc_type.lower() == 'p':
136
+ self.ion = 'c'
137
+ elif osc_type.lower() == 'n':
138
+ self.ion = 'a'
139
+
140
+ @ staticmethod
141
+ def check_error_term(line: str) -> None:
142
+ """
143
+ Check the error term of Gaussian.
144
+
145
+ Parameters
146
+ ----------
147
+ line : str
148
+ last line of the log file.
149
+
150
+ Raises
151
+ ------
152
+ GausTermError
153
+ if the calculation of Gaussian was error termination.
154
+ """
155
+ line = line.strip()
156
+
157
+ if 'Normal termination' not in line:
158
+ raise GausTermError('The calculation of Gaussian was error termination.')
159
+
160
+ @staticmethod
161
+ def print_timestamp() -> None:
162
+ """Print timestamp."""
163
+ month = {
164
+ 1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
165
+ 7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec',
166
+ }
167
+ dt_now = datetime.now()
168
+ print(f"Timestamp: {dt_now.strftime('%a')} {month[dt_now.month]} {dt_now.strftime('%d %H:%M:%S %Y')}")
169
+
170
+ def calc_reorganization(
171
+ self,
172
+ gau_com: str = 'g16',
173
+ only_read: bool = False,
174
+ is_output_detail: bool = False,
175
+ skip_specified_cal: List[Literal['opt_neutral', 'opt_ion', 'neutral', 'ion']] = [],
176
+ ) -> float:
177
+ """
178
+ Calculate reorganization energy.
179
+
180
+ Parameters
181
+ ----------
182
+ gau_com : str
183
+ Gaussian command, by default 'g16'.
184
+ only_read : bool
185
+ if True, the calculation is only read, by default False.
186
+ is_output_detail : bool
187
+ if True, the calculation detail will be output, by default False.
188
+ skip_specified_cal : List[Literal['opt_neutral', 'opt_ion', 'neutral', 'ion']]
189
+ if specified, the calculation of the specified type will be skipped, by default [].
190
+
191
+ Returns
192
+ -------
193
+ float
194
+ reorganization energy [eV].
195
+ """
196
+ file_path = Path(self.gjf_file)
197
+ filename = file_path.stem.replace('_opt_n', '')
198
+ directory = file_path.parent
199
+ basename = f'{directory}/{filename}'
200
+
201
+ energy = []
202
+
203
+ # 中性分子の構造最適化とエネルギー計算
204
+ only_read_opt_n = only_read
205
+ if not only_read and 'opt_neutral' not in skip_specified_cal:
206
+ print('>', gau_com, self.gjf_file)
207
+ subprocess.run([gau_com, self.gjf_file])
208
+
209
+ skip_opt_neutral = True if 'opt_neutral' in skip_specified_cal else False
210
+
211
+ energy.append(self.extract_energy(self.gjf_file, only_read=only_read_opt_n, is_output_detail=is_output_detail, skip_cal=skip_opt_neutral))
212
+
213
+ # カチオンかアニオンのエネルギー計算
214
+ only_read_ion = only_read
215
+ previous_name, _ = os.path.splitext(self.gjf_file)
216
+ gjf = f'{basename}_{self.ion}.gjf'
217
+ if not only_read and 'ion' not in skip_specified_cal:
218
+ self._create_gjf(file_name=gjf, prevous_name=previous_name, ion=self.ion)
219
+ print('>', gau_com, gjf)
220
+ subprocess.run([gau_com, gjf])
221
+
222
+ skip_ion = True if 'ion' in skip_specified_cal else False
223
+
224
+ energy.append(self.extract_energy(gjf, only_read=only_read_ion, is_output_detail=is_output_detail, skip_cal=skip_ion))
225
+
226
+ # カチオンかアニオンの構造最適化とエネルギー計算
227
+ only_read_opt_ion = only_read
228
+ gjf = f'{basename}_opt_{self.ion}.gjf'
229
+ if not only_read and 'opt_ion' not in skip_specified_cal:
230
+ self._create_gjf(file_name=gjf, prevous_name=previous_name, ion=self.ion, is_opt=True)
231
+ print('>', gau_com, gjf)
232
+ subprocess.run([gau_com, gjf])
233
+
234
+ skip_opt_ion = True if 'opt_ion' in skip_specified_cal else False
235
+
236
+ energy.append(self.extract_energy(gjf, only_read=only_read_opt_ion, is_output_detail=is_output_detail, skip_cal=skip_opt_ion))
237
+
238
+
239
+ # 中性分子のエネルギー計算
240
+ only_read_neutral = only_read
241
+ previous_name, _ = os.path.splitext(gjf)
242
+ ion = 'n'
243
+ gjf = f'{basename}_{ion}.gjf'
244
+ if not only_read and 'neutral' not in skip_specified_cal:
245
+ self._create_gjf(file_name=gjf, prevous_name=previous_name, ion=ion)
246
+ print('>', gau_com, gjf)
247
+ subprocess.run([gau_com, gjf])
248
+
249
+ skip_neutral = True if 'neutral' in skip_specified_cal else False
250
+
251
+ energy.append(self.extract_energy(gjf, only_read=only_read_neutral, is_output_detail=is_output_detail, skip_cal=skip_neutral))
252
+
253
+ return ((energy[3] - energy[2]) + (energy[1] - energy[0]))
254
+
255
+ def check_extension_log(self, gjf: str) -> None:
256
+ """Check the extension of log file.
257
+
258
+ Parameters
259
+ ----------
260
+ gjf : str
261
+ gjf file name.
262
+ """
263
+ if os.path.exists(f'{os.path.splitext(gjf)[0]}.out'):
264
+ self._extension_log = '.out'
265
+ else :
266
+ self._extension_log = '.log'
267
+
268
+ def extract_energy(
269
+ self,
270
+ gjf: str,
271
+ only_read: bool = False,
272
+ is_output_detail: bool = False,
273
+ skip_cal: bool = False
274
+ ) -> float:
275
+ """Extract energy from log file.
276
+
277
+ Parameters
278
+ ----------
279
+ gjf : str
280
+ gjf file name.
281
+ only_read : bool
282
+ if True, the calculation is only read, by default False.
283
+ is_output_detail : bool
284
+ if True, the calculation detail will be output, by default False.
285
+
286
+ Returns
287
+ -------
288
+ float
289
+ total energy.
290
+ """
291
+ self.check_extension_log(gjf)
292
+ log_file = f'{os.path.splitext(gjf)[0]}{self._extension_log}'
293
+
294
+ with open(log_file) as f:
295
+ last_line = ''
296
+ while True:
297
+ line = f.readline()
298
+ if not line:
299
+ break
300
+ line = line.strip()
301
+
302
+ if line:
303
+ last_line = line
304
+
305
+ if line.startswith('SCF Done:'):
306
+ energy = float(line.split()[4]) * 27.2114
307
+
308
+ self.check_error_term(last_line)
309
+
310
+ if is_output_detail:
311
+ gjf = Path(gjf)
312
+ if not only_read and not skip_cal:
313
+ print(f'{gjf} calculation completed.')
314
+ elif skip_cal:
315
+ print(f'{gjf} calculation skipped.')
316
+
317
+ print(f'reading {gjf.parent}/{gjf.stem}{self._extension_log}')
318
+ print()
319
+ print('--------------')
320
+ print(' Total energy ')
321
+ print('--------------')
322
+ print(f'{energy:12.6f} eV')
323
+ print()
324
+
325
+ return energy
326
+
327
+ def _create_gjf(
328
+ self,
329
+ file_name: str,
330
+ prevous_name: str,
331
+ ion: Literal['c', 'a', 'n'],
332
+ is_opt: bool = False,
333
+ ) -> None:
334
+ """
335
+ Create gjf file.
336
+
337
+ Parameters
338
+ ----------
339
+ file_name : str
340
+ file name.
341
+ prevous_name : str
342
+ previous file name.
343
+ ion : Literal['c', 'a', 'n']
344
+ ion type. 'c' is cation, 'a' is anion, 'n' is neutral molecule.
345
+ is_opt : bool
346
+ if True, the calculation is optimization, by default False.
347
+ """
348
+ file_name, _ = os.path.splitext(file_name)
349
+
350
+ with open(f'{file_name}.gjf', 'w') as f:
351
+ for line in self._gjf_lines['%']:
352
+ if r'%oldchk' in line.lower():
353
+ continue
354
+ elif r'%chk' in line.lower():
355
+ continue
356
+ else:
357
+ f.write(line)
358
+
359
+ f.write(f'%oldchk={prevous_name}.chk\n')
360
+ if is_opt:
361
+ f.write(f'%chk={file_name}.chk\n')
362
+
363
+ for line in self._gjf_lines['#']:
364
+ if 'geom' in line.lower():
365
+ continue
366
+ elif 'opt' in line.lower():
367
+ continue
368
+ else:
369
+ f.write(line)
370
+
371
+ f.write('# Geom=Checkpoint\n')
372
+ if is_opt:
373
+ f.write('# Opt=Tight\n')
374
+ f.write('\n')
375
+ f.write('Defalut Title\n')
376
+ f.write('\n')
377
+
378
+ if ion == 'c':
379
+ f.write('1 2\n\n')
380
+ elif ion == 'a':
381
+ f.write('-1, 2\n\n')
382
+ else:
383
+ f.write('0 1\n\n')
384
+
385
+ def _input_gjf(self) -> None:
386
+ """Input link 0 command and root options from gjf file."""
387
+ with open(self.gjf_file, 'r') as f:
388
+ for line in f:
389
+ if line.startswith('%'):
390
+ self._gjf_lines['%'].append(line)
391
+ elif line.startswith('#'):
392
+ self._gjf_lines['#'].append(line)
393
+ elif 'link' in line.lower():
394
+ raise ValueError("Please do not use Link.")
395
+
396
+
397
+ class GausTermError(Exception):
398
+ """Exception for Gaussian error termination."""
399
+ pass
400
+
401
+
402
+ class OSCTypeError(Exception):
403
+ """Exception for organic semiconductor type."""
404
+ pass
405
+
406
+
407
+ if __name__ == '__main__':
408
+ main()