BUCToolkit 1.0b0__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.
Files changed (88) hide show
  1. BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
  2. BUCToolkit/BatchGenerate/__init__.py +6 -0
  3. BUCToolkit/BatchGenerate/coords_interp.py +127 -0
  4. BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
  5. BUCToolkit/BatchGenerate/random_replace.py +88 -0
  6. BUCToolkit/BatchMC/MetropolisMC.py +210 -0
  7. BUCToolkit/BatchMC/_BaseMC.py +863 -0
  8. BUCToolkit/BatchMC/__init__.py +8 -0
  9. BUCToolkit/BatchMD/BiasedMD.py +276 -0
  10. BUCToolkit/BatchMD/NVE.py +92 -0
  11. BUCToolkit/BatchMD/NVT.py +510 -0
  12. BUCToolkit/BatchMD/_BaseMD.py +1251 -0
  13. BUCToolkit/BatchMD/__init__.py +18 -0
  14. BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
  15. BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
  16. BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
  17. BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
  18. BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
  19. BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
  20. BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
  21. BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
  22. BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
  23. BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
  24. BUCToolkit/BatchOptim/TS/__init__.py +10 -0
  25. BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
  26. BUCToolkit/BatchOptim/__init__.py +22 -0
  27. BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
  28. BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
  29. BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
  30. BUCToolkit/BatchOptim/frequency.py +496 -0
  31. BUCToolkit/BatchOptim/minimize/CG.py +256 -0
  32. BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
  33. BUCToolkit/BatchOptim/minimize/QN.py +205 -0
  34. BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
  35. BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
  36. BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
  37. BUCToolkit/BatchStructures/__init__.py +15 -0
  38. BUCToolkit/Preprocessing/__init__.py +7 -0
  39. BUCToolkit/Preprocessing/load_files.py +1680 -0
  40. BUCToolkit/Preprocessing/preprocessing.py +923 -0
  41. BUCToolkit/Preprocessing/sampling.py +299 -0
  42. BUCToolkit/Preprocessing/supercells.py +140 -0
  43. BUCToolkit/Preprocessing/write_files.py +524 -0
  44. BUCToolkit/StandardTemplate.py +417 -0
  45. BUCToolkit/__init__.py +33 -0
  46. BUCToolkit/_version.py +7 -0
  47. BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
  48. BUCToolkit/api/DataLoaders.py +321 -0
  49. BUCToolkit/api/Losses.py +170 -0
  50. BUCToolkit/api/Metrics.py +84 -0
  51. BUCToolkit/api/ModelOptims.py +296 -0
  52. BUCToolkit/api/MolecularDynamics.py +282 -0
  53. BUCToolkit/api/MonteCarlo.py +280 -0
  54. BUCToolkit/api/NEB.py +325 -0
  55. BUCToolkit/api/Predictor.py +292 -0
  56. BUCToolkit/api/StructureOptimization.py +484 -0
  57. BUCToolkit/api/Trainer.py +635 -0
  58. BUCToolkit/api/VibrationAnalysis.py +279 -0
  59. BUCToolkit/api/__init__.py +38 -0
  60. BUCToolkit/api/_io.py +1026 -0
  61. BUCToolkit/cli/__init__.py +20 -0
  62. BUCToolkit/cli/cli.py +934 -0
  63. BUCToolkit/cli/input_stub.py +140 -0
  64. BUCToolkit/cli/main.py +368 -0
  65. BUCToolkit/cli/print_logo.py +149 -0
  66. BUCToolkit/cli/template_model.py +6 -0
  67. BUCToolkit/io.py +30 -0
  68. BUCToolkit/utils/AtomSelector.py +64 -0
  69. BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
  70. BUCToolkit/utils/ElemListReduce.py +52 -0
  71. BUCToolkit/utils/Ewald.py +210 -0
  72. BUCToolkit/utils/IrregularTensorReformat.py +59 -0
  73. BUCToolkit/utils/_CheckModules.py +30 -0
  74. BUCToolkit/utils/_Element_info.py +527 -0
  75. BUCToolkit/utils/__init__.py +22 -0
  76. BUCToolkit/utils/_para_flatt_list.py +131 -0
  77. BUCToolkit/utils/_print_formatter.py +59 -0
  78. BUCToolkit/utils/check_structures.py +204 -0
  79. BUCToolkit/utils/function_utils.py +147 -0
  80. BUCToolkit/utils/grad_functions.py +69 -0
  81. BUCToolkit/utils/index_ops.py +432 -0
  82. BUCToolkit/utils/setup_loggers.py +137 -0
  83. buctoolkit-1.0b0.dist-info/METADATA +28 -0
  84. buctoolkit-1.0b0.dist-info/RECORD +88 -0
  85. buctoolkit-1.0b0.dist-info/WHEEL +5 -0
  86. buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
  87. buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
  88. buctoolkit-1.0b0.dist-info/top_level.txt +1 -0
BUCToolkit/cli/cli.py ADDED
@@ -0,0 +1,934 @@
1
+ """
2
+ An Advanced Interactive Command-Line Interface which can run end-to-end tasks of model training, structure optimization, molecular dynamics,
3
+ and Monte Carlo simulations.
4
+ """
5
+ # Copyright (c) 2026.3.26, BUCToolkit.
6
+ # Authors: Pu Pengxin, Song Xin
7
+ # Version: 0.9a
8
+ # File: __init__.py
9
+ # Environment: Python 3.12
10
+
11
+ import os
12
+ import re
13
+ import time
14
+ import sys
15
+ import logging
16
+ import traceback
17
+ from typing import Literal
18
+ import shlex
19
+ import copy
20
+
21
+ import yaml
22
+
23
+ from BUCToolkit.cli.main import launch_task
24
+ from BUCToolkit.utils._CheckModules import check_module
25
+ from BUCToolkit.cli.print_logo import generate_display_art
26
+ from BUCToolkit.cli.input_stub import CONFIG_STUB
27
+
28
+ has_prmt = (check_module('prompt_toolkit') is not None)
29
+
30
+ if has_prmt:
31
+ from prompt_toolkit import prompt
32
+ from prompt_toolkit.history import FileHistory
33
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
34
+ from prompt_toolkit.completion import WordCompleter # TODO, auto-completion
35
+ prompt_config = dict(history=FileHistory('.cli_history'), auto_suggest=AutoSuggestFromHistory())
36
+ else:
37
+ prompt = input
38
+ prompt_config = dict()
39
+
40
+ class BaseCLI:
41
+
42
+ def __init__(self, *args, **kwargs):
43
+ self.closed = False
44
+ self.INPUT_FILE = None # current input information
45
+ self._is_config = False
46
+ self._TASK = {
47
+ 'TRAIN': 'TRAIN',
48
+ 'PREDICT': 'PREDICT',
49
+ 'OPT': 'OPT',
50
+ 'STRUCTURE_OPTIMIZATION': 'OPT',
51
+ 'STRUC_OPT': 'OPT',
52
+ 'DIMER': 'TS',
53
+ 'TS': 'TS',
54
+ 'VIB': 'VIB',
55
+ 'VIBRATIONAL_ANALYSIS': 'VIB',
56
+ 'NEB': 'NEB',
57
+ 'CINEB': 'NEB',
58
+ 'CI_NEB': 'NEB',
59
+ 'MD': 'MD',
60
+ 'MOLECULAR_DYNAMICS': 'MD',
61
+ 'CMD': 'CMD',
62
+ 'CONSTRAINED_MOLECULAR_DYNAMICS': 'CMD',
63
+ 'CONSTR_MD': 'CMD',
64
+ 'MC': 'MC',
65
+ 'MONTE_CARLO': 'MC',
66
+ }
67
+ self._COMMANDS = {
68
+ "help": (self.print_help, 'Show this help message.'),
69
+ "exit": (self.do_exit, 'Exit the cli program. One can also press "Ctrl+C" to exit.'),
70
+ "verbose": (
71
+ self.set_verbose,
72
+ "Reset the verbosity level. Available values: 'FATAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET'"
73
+ ),
74
+ "logto": (
75
+ self.reset_handler,
76
+ "Usage: logto `logger` - Reset where the log information is printed. If `None` is given, logs will be printed into stdout."
77
+ ),
78
+ "show": (
79
+ self.show_current_config,
80
+ "Usage: `show [-v]` - Show the current input file path. `-v` is to fully print the input file content."
81
+ ),
82
+ "edit": (
83
+ self.edit_sub_cli,
84
+ "Usage: `edit [path]` - Edit current configuration file. if [path] is set, current file would try to change to the specified path."
85
+ ),
86
+ "task": (
87
+ self.task_sub_cli,
88
+ "usage: task `task_name` [current input file path]. "
89
+ "- Create or edit the configurations of a task. "
90
+ "If [current input file path] is not given, default of './task.inp' will be used.\n"
91
+ f"{' '*12} Available task_name: {", ".join(self._TASK.keys())}"
92
+ ),
93
+ "run": (
94
+ self.run_task,
95
+ "Launch a task the cli.\n"
96
+ )
97
+ }
98
+ # colours
99
+ self._GREEN = '\033[32m'
100
+ self._YELLOW = '\033[33m'
101
+ self._RED = '\033[31m'
102
+ self._RESET = '\033[0m'
103
+
104
+ self.logger = logging.getLogger('cli')
105
+ self.logger.setLevel(logging.INFO)
106
+ self._log_formatter = logging.Formatter('%(message)s')
107
+ if len(self.logger.handlers) <= 0:
108
+ log_handler = logging.StreamHandler(sys.stdout, )
109
+ log_handler.setLevel(logging.INFO)
110
+ log_handler.setFormatter(self._log_formatter)
111
+ self.logger.addHandler(log_handler)
112
+ self._current_log_level = self.logger.getEffectiveLevel()
113
+
114
+ pass
115
+
116
+ @staticmethod
117
+ def _input_parser(raw_input: str):
118
+ """
119
+ Parse the input to identify the commands and args
120
+ Args:
121
+ raw_input: raw input string
122
+
123
+ Returns:
124
+ Command key: str
125
+ args: tuple
126
+ kwargs: dict
127
+
128
+ """
129
+ inp_list = shlex.split(raw_input)
130
+ command_key = inp_list[0]
131
+ _kwargs = dict()
132
+ _args = list()
133
+ skip_times = 0 # times to skip
134
+ for i, inp in enumerate(inp_list[1:]):
135
+ if skip_times > 0:
136
+ skip_times -= 1
137
+ continue
138
+ if inp.startswith('-') or inp.startswith('--'):
139
+ _inp = inp.strip('-')
140
+ if '=' in _inp: # use '=' to align value
141
+ _ = _inp.split('=')
142
+ _kwargs[_[0]] = _[1] if _[1] != '' else None
143
+ else: # otherwise use space ' '
144
+ if i + 1 >= len(inp_list): # the last one
145
+ _kwargs[_inp] = None
146
+ else:
147
+ _prefetch = inp_list[i + 1]
148
+ if not _prefetch.startswith('-'):
149
+ _kwargs[_inp] = inp_list[i + 1]
150
+ skip_times += 1
151
+ else:
152
+ _kwargs[_inp] = None
153
+ continue
154
+ else:
155
+ _args.append(inp)
156
+
157
+ return command_key, _args, _kwargs
158
+
159
+ def print_help(self, *args, **kwargs):
160
+ collect_help_info = list()
161
+ for k, v in self._COMMANDS.items():
162
+ collect_help_info.append(
163
+ f"{self._GREEN}{k: <10s}{self._RESET}: {v[1]}\n"
164
+ )
165
+ self.logger.info(
166
+ '\n'.join(collect_help_info)
167
+ )
168
+
169
+ def _close(self):
170
+ try:
171
+ self._purge_handlers()
172
+ return 0
173
+ except Exception as e:
174
+ print(f"An exception occurred when closing loggers: {e}. File may not be properly closed.")
175
+ return 1
176
+
177
+ def do_exit(self, *args, **kwargs):
178
+ _exit_code = self._close()
179
+ self.closed = True
180
+ self.logger.info('BYE!')
181
+ exit(_exit_code)
182
+
183
+ def set_verbose(
184
+ self,
185
+ verbose: int|Literal['FATAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET'] = 'INFO',
186
+ *args,
187
+ **kwargs
188
+ ):
189
+ if not isinstance(verbose, int):
190
+ verb = getattr(logging, verbose.upper(), None)
191
+ if verb is None:
192
+ self.logger.error(f"ERROR: Invalid verbosity level '{verbose}'.")
193
+ return
194
+ else:
195
+ verb = verbose
196
+ self._current_log_level = verb
197
+ self.logger.setLevel(verb)
198
+
199
+ def reset_handler(self, handler, *args, **kwargs):
200
+ if (handler is None) or (handler == 'None'):
201
+ handler = logging.StreamHandler(sys.stdout)
202
+ else:
203
+ handler = logging.FileHandler(str(handler))
204
+ handler.setFormatter(self._log_formatter)
205
+ handler.setLevel(self._current_log_level)
206
+ # purge old handlers
207
+ self._purge_handlers()
208
+ self.logger.addHandler(handler)
209
+
210
+ def _purge_handlers(self):
211
+ """ Remove all handlers """
212
+ while self.logger.handlers:
213
+ try:
214
+ self.logger.handlers[0].close()
215
+ except Exception as e:
216
+ print(f"Failed to close a logger handler: {e}.")
217
+ self.logger.removeHandler(self.logger.handlers[0])
218
+
219
+ def args_exhibitor(self, arg_dict: dict, indent: int = 0):
220
+ """
221
+ Show formatted args dict to the CLI
222
+ Args:
223
+ arg_dict: dict load from YAML.
224
+ indent: the indent for printing formatted args.
225
+
226
+ Returns: None
227
+
228
+ """
229
+ if indent == 0: # print a path info as title
230
+ self.logger.info(f"Configurations saving path: {self.INPUT_FILE}\n")
231
+
232
+ if indent >= 20:
233
+ self.logger.critical(
234
+ f"How frightful that you set an input containing over 20 times nesting. "
235
+ f"I refuse such a horrible input."
236
+ )
237
+ raise ValueError(
238
+ f"How frightful that you set an input containing over 20 times nesting. "
239
+ f"I refuse such a horrible input."
240
+ )
241
+ for k, v in arg_dict.items():
242
+ if (isinstance(v, dict)) and (len(v) > 1):
243
+ self.logger.info(f"\n{' '*indent}{self._RED}{k}{self._RESET}:")
244
+ self.args_exhibitor(v, indent + 1)
245
+ #if indent == 1: self.logger.info("\n") # for 1 level printing, add a line break
246
+ else:
247
+ # print
248
+ self.logger.info(f"{' '*indent}{self._RED}{k}{self._RESET}: {v}")
249
+
250
+ def show_current_config(self, v: str | None = '8Yc3TmYIgdGABhQbtLuL+3RdIjQHY3/eTELeYkkFMQ4='):
251
+ """
252
+ The default `is_v` is a placeholder that No one may precisely match this string. if is_v is set to None, print verbosely.
253
+ """
254
+ if v == '8Yc3TmYIgdGABhQbtLuL+3RdIjQHY3/eTELeYkkFMQ4=':
255
+ self.logger.info(f"Current input file: {self.INPUT_FILE}")
256
+ if self.INPUT_FILE is None: return
257
+ if not os.path.isfile(self.INPUT_FILE):
258
+ self.logger.warning(f"The current input file does not exist yet. YOU MAY CREATE IT BEFORE RUN CALCULATION.")
259
+ elif v is None:
260
+ if (self.INPUT_FILE is not None) and (os.path.isfile(self.INPUT_FILE)):
261
+ with open(self.INPUT_FILE, 'r') as fv:
262
+ argdict = yaml.safe_load(fv)
263
+ self.args_exhibitor(argdict)
264
+ else:
265
+ self.logger.warning(f"The input file does not exist. Showing details is impossible.")
266
+ else:
267
+ self.logger.error(f"Unknown value of v: {v}.")
268
+
269
+ def rec_find_key(self, inp_dict, key, val, n_target: int = 1):
270
+ """
271
+ Recursively find the key in a nested dict and modify its value.
272
+ Args:
273
+ inp_dict: dict to modify.
274
+ key: the key to find.
275
+ val: the value to modify.
276
+ n_target: the times of finding key.
277
+
278
+ Returns: the times of finding key.
279
+
280
+ """
281
+ n_count = 0
282
+ if n_target < 0: n_target = float('inf')
283
+ for k, v in inp_dict.items():
284
+ if n_count >= n_target:
285
+ break
286
+ if k == key:
287
+ inp_dict[k] = val
288
+ n_count += 1
289
+ elif isinstance(v, dict):
290
+ n_count += self.rec_find_key(v, key, val, n_target - n_count) # limit to the rest available times
291
+ return n_count
292
+
293
+ def rec_modify_val(self, inp_dict: dict, nest_key_list: list, val, k_ptr: int = 0):
294
+ """
295
+ Recursively modify a nested dict by a given key list.
296
+ The i-th elem. in the key list repr. the key of i-level nested dict.
297
+ Args:
298
+ inp_dict: dict to modify.
299
+ nest_key_list: the key list of the nested dict.
300
+ val: the value to modify.
301
+ k_ptr: the current level of nesting, as well as the key index of `nest_key_list`.
302
+
303
+ Returns:
304
+ has_matched: bool, whether catches the value to modify
305
+
306
+ """
307
+ if len(nest_key_list) == 0: return True
308
+ is_last = (k_ptr == (len(nest_key_list) - 1))
309
+ has_matched = False
310
+ if is_last:
311
+ inp_dict[nest_key_list[k_ptr]] = val # compatible with both addition and modification cases.
312
+ return True
313
+ for k in list(inp_dict.keys()):
314
+ v = inp_dict[k]
315
+ if k == nest_key_list[k_ptr]: # match the i-level nested key
316
+ if isinstance(v, dict):
317
+ has_matched = self.rec_modify_val(v, nest_key_list, val, k_ptr + 1)
318
+ if not has_matched: # failed at deeper level, thus success is impossible
319
+ return False
320
+ else: break
321
+ else: # matched the key but not a dict value. search aborted
322
+ return False
323
+ # if not matched the key but not at last, adding empty key-dict pairs
324
+ if not has_matched:
325
+ sub_dict_to_update = dict()
326
+ u = {nest_key_list[k_ptr]: dict()}
327
+ v = sub_dict_to_update
328
+ ik = k_ptr
329
+ while ik < (len(nest_key_list) - 1):
330
+ v.update(u)
331
+ v = u[nest_key_list[ik]]
332
+ ik += 1
333
+ u = {nest_key_list[ik]: dict()}
334
+ v[nest_key_list[ik]] = val
335
+ inp_dict.update(sub_dict_to_update)
336
+
337
+ return True
338
+
339
+ def rec_rm_key(self, inp_dict: dict, nest_key_list: list, k_ptr: int = 0):
340
+ """
341
+ Recursively remove the key given by the key list in a nested dict.
342
+ The i-th elem. in the key list repr. the key of i-level nested dict.
343
+ Args:
344
+ inp_dict: dict to modify.
345
+ nest_key_list: the key list of the nested dict.
346
+ k_ptr: the current level of nesting, as well as the key index of `nest_key_list`.
347
+
348
+ Returns:
349
+ has_matched: bool, whether catches the key to remove
350
+
351
+ """
352
+ if len(nest_key_list) == 0: return True
353
+ is_last = (k_ptr == (len(nest_key_list) - 1))
354
+ has_matched = False
355
+ if is_last:
356
+ if nest_key_list[k_ptr] in inp_dict:
357
+ inp_dict.pop(nest_key_list[k_ptr]) # compatible with both addition and modification cases.
358
+ # even if not match, goal of deletion can be viewed as successful, so return True
359
+ return True
360
+ for k in list(inp_dict.keys()):
361
+ v = inp_dict[k]
362
+ if k == nest_key_list[k_ptr]: # match the i-level nested key
363
+ if isinstance(v, dict):
364
+ has_matched = self.rec_rm_key(v, nest_key_list, k_ptr + 1)
365
+ if not has_matched: # failed at deeper level, thus success is impossible
366
+ return False
367
+ else:
368
+ break
369
+ else: # matched the key but not a dict value. search aborted
370
+ return False
371
+
372
+ return has_matched # not matched
373
+
374
+ def rec_check_key(self, inp_dict: dict, nest_key_list: list, k_ptr: int = 0):
375
+ """
376
+ Recursively search and show the value of the key given by the key list in a nested dict.
377
+ The i-th elem. in the key list repr. the key of i-level nested dict.
378
+ Args:
379
+ inp_dict: dict to show.
380
+ nest_key_list: the key list of the nested dict.
381
+ k_ptr: the current level of nesting, as well as the key index of `nest_key_list`.
382
+
383
+ Returns:
384
+ content: the value of given keys. If failed, content will be `None`.
385
+ has_matched: bool, whether catches the key to show
386
+
387
+ """
388
+ if len(nest_key_list) == 0: return None, True
389
+ is_last = (k_ptr == (len(nest_key_list) - 1))
390
+ has_matched = False
391
+ if is_last:
392
+ if nest_key_list[k_ptr] in inp_dict:
393
+ cont = inp_dict[nest_key_list[k_ptr]]
394
+ return cont, True
395
+ else:
396
+ cont = None
397
+ return cont, False
398
+ for k in list(inp_dict.keys()):
399
+ v = inp_dict[k]
400
+ if k == nest_key_list[k_ptr]: # match the i-level nested key
401
+ if isinstance(v, dict):
402
+ cont, has_matched = self.rec_check_key(v, nest_key_list, k_ptr + 1)
403
+ return cont, has_matched
404
+ else: # matched the key but not a dict value. search aborted
405
+ return None, False
406
+
407
+ return None, has_matched # not matched
408
+
409
+ def task_sub_cli(self, task: str|None = None, inp_file: str|None = None):
410
+ """
411
+
412
+ Returns:
413
+
414
+ """
415
+
416
+ if task is None:
417
+ task = prompt('>>> Enter a task (TRAIN, PREDICT, OPT, TS, VIB, NEB, MD, CMD, MC): ')
418
+ task = task.upper()
419
+ if task not in self._TASK:
420
+ self.logger.error(f"Unknown task: {task}\nAvailable task_name: {", ".join(self._TASK.keys())}")
421
+ return
422
+ else:
423
+ ARGS_WORK = eval(f"ARGS_{self._TASK[task]}")
424
+ if inp_file is None: # if not input file, use default configs
425
+ AGRS_NOW = '\n'.join([ARGS_GLOBAL, ARGS_IO, ARGS_MODEL, ARGS_WORK])
426
+ inp_args = yaml.safe_load(AGRS_NOW)
427
+ else:
428
+ with open(inp_file, 'r') as fx:
429
+ inp_args = yaml.safe_load(fx)
430
+ self.INPUT_FILE = inp_file
431
+ inp_args['TASK'] = self._TASK[task]
432
+ # Show once
433
+ self.logger.info(f'Current configuration:\n')
434
+ self.args_exhibitor(inp_args)
435
+ self.logger.info("*" * 89)
436
+ # To edit
437
+ self.edit_sub_cli(inp_args)
438
+
439
+ def edit_sub_cli(self, inp_args: dict|str|None = None):
440
+ """
441
+
442
+ Returns:
443
+
444
+ """
445
+ if inp_args is None: # if not given, try to read from self.INPUT_FILE
446
+ if self.INPUT_FILE is None:
447
+ self.logger.warning(f"Nothing to edit. EDIT ABORTED.")
448
+ return
449
+ else:
450
+ self.logger.info(f"Try to load configs from {self.INPUT_FILE} ...")
451
+ try:
452
+ with open(self.INPUT_FILE, 'r') as fx:
453
+ inp_args = yaml.safe_load(fx)
454
+ except Exception as e:
455
+ self.logger.error(f"ERROR: Failed to load configs from {self.INPUT_FILE} due to \"{e}\"")
456
+ return
457
+ elif isinstance(inp_args, str): # try to read file
458
+ self.logger.info(f"Try to load configs from {inp_args} ...")
459
+ _inp_args_path = inp_args
460
+ try:
461
+ with open(_inp_args_path, 'r') as fx:
462
+ inp_args = yaml.safe_load(fx)
463
+ self.INPUT_FILE = _inp_args_path
464
+ except Exception as e:
465
+ self.logger.error(f"ERROR: Failed to load configs from {_inp_args_path} due to \"{e}\"")
466
+ return
467
+
468
+ help_info = """
469
+ Commands:
470
+ help: show this help message.
471
+ exit: save & exit edit.
472
+ load: `load [path]`, load a configuration file from given path. If path is given, current path will be changed synchronously.
473
+ save: `save [path]`, save current configuration. If path is given, current path will be changed synchronously.
474
+ chpt: `chpt [path]`, change the file saving path.
475
+ quit: exit edit without saving
476
+ show: show all current configurations
477
+ list: alias of 'show'
478
+ To show the information of arguments, just directly input the keyword:
479
+ `[Section(s)].KEYWORDS`
480
+ example:
481
+ `MD.THERMOSTAT_CONFIG.TIME_CONST`, which will show the required data type and corresponding docstring of
482
+ the time constant (of CSVR thermostat) under MD.THERMOSTAT_CONFIG sections,
483
+ where `THERMOSTAT_CONFIG` is a sub-section of the section `MD`.
484
+ Two ways to change configurations:
485
+ 1. `[Section(s)].KEYWORDS = new_value`
486
+ 2. `[Section(s)].KEYWORDS: new_value`
487
+ example:
488
+ `MD.THERMOSTAT_CONFIG.TIME_CONST = 100.`, which modifies the time constant above.
489
+ To delete keywords in current configuration:
490
+ `del [Section(s)].KEYWORDS`
491
+ example:
492
+ `del OPT.USE_BB`, which deletes the `USE_BB` (whether to use Barzilai-Borwein step) argument under section `OPT`.
493
+ One can also use 'Ctrl+C' to quit.
494
+ """
495
+ if 'TASK' not in inp_args:
496
+ self.logger.error(
497
+ f"ERROR: Argument 'TASK' is absent in the given configurations. "
498
+ f"YOU SHOULD SPECIFY ONE."
499
+ )
500
+ _xtask = prompt(f'\n>>> Type the TASK: ')
501
+ if _xtask not in self._TASK:
502
+ self.logger.error(f"Unknown task: {_xtask}\nAvailable task_name: {", ".join(self._TASK.keys())}")
503
+ self.logger.error(f"EDIT ABORTED.")
504
+ return
505
+ else:
506
+ inp_args['TASK'] = self._TASK[_xtask]
507
+ inp_args_bak = copy.deepcopy(inp_args)
508
+ task = inp_args['TASK']
509
+ # main cli loop
510
+ while True:
511
+ try:
512
+ content = prompt(f'\n>>> {task}: ', **prompt_config)
513
+ if len(content) == 0:
514
+ self.logger.info(f'{task} configuration done.')
515
+ self.dump_inpfile(inp_args)
516
+ break
517
+ elif content.lower() == 'help':
518
+ self.logger.info(f"{help_info}")
519
+ continue
520
+ elif content.lower() == 'exit':
521
+ self.logger.info(f'{task} configuration done.')
522
+ self.dump_inpfile(inp_args)
523
+ break
524
+ elif content.lower() == 'quit':
525
+ self.logger.info('All changes have been cancelled.')
526
+ while True:
527
+ try:
528
+ is_save = prompt('>>> Do you want to save current configurations? (y/N): ')
529
+ except KeyboardInterrupt:
530
+ is_save = 'n'
531
+ if len(is_save) == 0: # viewed as 'n'
532
+ break
533
+ elif is_save.lower() == 'y':
534
+ self.dump_inpfile(inp_args_bak)
535
+ break
536
+ elif is_save.lower() == 'n':
537
+ break
538
+ else:
539
+ self.logger.error(f"Please input 'y' or 'n'.")
540
+ break
541
+ elif content.lower()[:5] == 'save ' or content.lower() == 'save':
542
+ true_cont = content.lower().split()
543
+ if len(true_cont) == 2:
544
+ self.INPUT_FILE = true_cont[1]
545
+ self.dump_inpfile(inp_args, force=True)
546
+ continue
547
+ elif content.lower()[:5] == 'load ' or content.lower() == 'load':
548
+ true_cont = content.split()
549
+ if len(true_cont) == 2:
550
+ self.INPUT_FILE = true_cont[1]
551
+ # load
552
+ try:
553
+ with open(self.INPUT_FILE, 'r') as fs:
554
+ _inp_args = yaml.safe_load(fs)
555
+ if 'TASK' not in _inp_args:
556
+ self.logger.error(f"Failed to load: Argument 'TASK' is absent in the given file.")
557
+ continue
558
+ elif _inp_args['TASK'] != self._TASK[task]:
559
+ self.logger.error(f"The task of loaded file does not match current task.")
560
+ continue
561
+ inp_args = _inp_args
562
+ inp_args_bak = copy.deepcopy(inp_args)
563
+ except FileNotFoundError:
564
+ self.logger.error(f"File not found: {self.INPUT_FILE}.")
565
+ continue
566
+ elif (content.lower() == 'show') or (content.lower() == 'list'):
567
+ self.args_exhibitor(inp_args)
568
+ continue
569
+ elif content.lower()[:5] == 'chpt ' or content.lower() == 'chpt':
570
+ _ = content.split(maxsplit=1)
571
+ if len(_) == 1:
572
+ _path = prompt(f">>> I/O: Please enter a path to save current configuration file: ")
573
+ if len(_path) == 0:
574
+ self.logger.info(f"Path change cancelled.")
575
+ continue
576
+ else:
577
+ _path = _path.strip()
578
+ else:
579
+ _path = str(_[1]).strip()
580
+ self.INPUT_FILE = _path
581
+ continue
582
+ elif content.lower()[:4] == 'del ':
583
+ _ = content.split(maxsplit=1)
584
+ if len(_) != 2:
585
+ self.logger.error(f"Invalid argument: {content}. Usage: del `KEYWORDS`")
586
+ continue
587
+ key = _[1].strip().upper()
588
+ key_list = key.split('.')
589
+ is_succ = self.rec_rm_key(inp_args, key_list)
590
+ if not is_succ:
591
+ self.logger.error(f"No keyword {key} matched in current configuration.")
592
+ continue
593
+ else: # show & change keywords
594
+ key = None
595
+ val = None
596
+ try:
597
+ # if not alignment symbol, SHOW info.
598
+ if ('=' not in content) and (':' not in content):
599
+ key = content.strip().upper()
600
+ # handle keychains
601
+ key_list = key.split('.')
602
+ chk_res, is_succ = self.rec_check_key(CONFIG_STUB, key_list)
603
+ if not is_succ:
604
+ self.logger.error(f"Unknown keyword {key}.")
605
+ else:
606
+ # print info
607
+ # normal result: (default value, data type, docstring)
608
+ self.logger.info(
609
+ f"{'.'.join(key_list)}: {chk_res[1]}, {chk_res[2]}. Default: {chk_res[0]}"
610
+ )
611
+ continue
612
+ # else CHANGE value
613
+ _valid_flag = False
614
+ for try_delimiter in ['=', ':']:
615
+ cont = content.split(try_delimiter, maxsplit=1)
616
+ if len(cont) != 2:
617
+ continue
618
+ else:
619
+ key = cont[0].strip().upper()
620
+ # handle keychains
621
+ key_list = key.split('.')
622
+ # handle values
623
+ val = cont[1].strip()
624
+ if val.lower() == 'true':
625
+ val = True
626
+ elif val.lower() == 'false':
627
+ val = False
628
+ elif val.lower() == 'none':
629
+ val = None
630
+ else:
631
+ try: # try to convert to python object
632
+ val = eval(val)
633
+ except (SyntaxError, NameError, TypeError, ValueError):
634
+ val = str(val)
635
+ is_succ = self.rec_modify_val(inp_args, key_list, val)
636
+ # double check
637
+ chk_res, is_succ_ = self.rec_check_key(CONFIG_STUB, key_list)
638
+ _valid_flag = True
639
+ if is_succ:
640
+ if not is_succ_:
641
+ self.logger.warning(
642
+ f"WARNING: {key} is not a valid keyword in all possible configurations. "
643
+ f"While it still added/modified to current configuration anyway."
644
+ )
645
+ # check type:
646
+ elif not isinstance(val, chk_res[1]):
647
+ self.logger.warning(
648
+ f"WARNING: The type of keyword's value should be {chk_res[1]}, "
649
+ f"but now the type ({type(val)}) of value ({val}) is given. "
650
+ f"PLEASE CAREFULLY CHECK!!!"
651
+ )
652
+ self.logger.info(
653
+ f'The value of `{key}` has been successfully changed to `{val}` that belongs to {type(val)}.'
654
+ )
655
+ else:
656
+ self.logger.info(f"Failed to change {key} in current configuration. Try again.")
657
+ break
658
+ if not _valid_flag:
659
+ self.logger.error(f'ERROR: Unknown command. help:\n{help_info}')
660
+ continue
661
+ except Exception as e:
662
+ self.logger.error(f'ERROR: Failed to change the key-val pair `{key}:{val}` due to {e}. Try again.')
663
+ continue
664
+
665
+ #self.args_exhibitor(inp_args)
666
+ except KeyboardInterrupt:
667
+ self.logger.info('All changes have been cancelled.')
668
+ while True:
669
+ try: # handle Ctrl+C during input cli
670
+ is_save = prompt('>>> Do you want to save current configurations? (y/N): ')
671
+ except KeyboardInterrupt:
672
+ is_save = 'n'
673
+ if len(is_save) == 0:
674
+ break
675
+ elif is_save.lower() == 'y':
676
+ self.dump_inpfile(inp_args_bak)
677
+ break
678
+ elif is_save.lower() == 'n':
679
+ break
680
+ else:
681
+ self.logger.error(f"Please input 'y' or 'n'.")
682
+ break
683
+ except Exception as e:
684
+ self.logger.error(f'ERROR: {e}.\n{traceback.format_exc()}\nAll changes have been cancelled without saving.')
685
+ #self.dump_inpfile(inp_args_bak, force=True)
686
+ break
687
+
688
+ def dump_inpfile(self, inp, disable = False, force=False):
689
+ """
690
+ Dumping input file
691
+ Args:
692
+ inp: input content
693
+ disable: whether to really dump. A placeholder.
694
+ force: whether to force dump even overwritten.
695
+ Returns:
696
+
697
+ """
698
+ if disable:
699
+ return
700
+ if self.INPUT_FILE is None:
701
+ self.INPUT_FILE = prompt(f">>> I/O: Please enter a path to save current configuration file: ")
702
+ if (not force) and os.path.exists(self.INPUT_FILE):
703
+ while True:
704
+ content = prompt(f'\n>>> I/O: Found an existing configuration file. Do you want to overwrite it? (y/N): ')
705
+ if len(content) == 0:
706
+ content = 'n'
707
+ if content.lower() == 'y':
708
+ with open(self.INPUT_FILE, 'w') as f:
709
+ yaml.dump(inp, f)
710
+ break
711
+ elif content.lower() == 'n':
712
+ break
713
+ else:
714
+ self.logger.info('Please enter y or n. Default: n.')
715
+ else:
716
+ with open(self.INPUT_FILE, 'w') as f:
717
+ yaml.dump(inp, f)
718
+
719
+ def run_task(self, ):
720
+ """ run """
721
+ if self.INPUT_FILE is None:
722
+ self.logger.error(f'ERROR: No input file was provided, please input `task [name]` to configure it first.')
723
+ return
724
+ elif os.path.exists(self.INPUT_FILE):
725
+ launch_task(self.INPUT_FILE)
726
+ else:
727
+ self.logger.error(f'ERROR: Current input file `{self.INPUT_FILE}` does not exist. Please check or try another one.')
728
+ return
729
+
730
+ def run(self):
731
+ try:
732
+ # LOGO
733
+ self.logger.info(generate_display_art())
734
+ self.logger.info('Please type the content below. Type "help" to show all commands.')
735
+ while True:
736
+ try:
737
+ if has_prmt:
738
+ content = prompt('\n>>> ', **prompt_config)
739
+ else:
740
+ content = input('\n>>> ')
741
+ if len(content) == 0: continue
742
+ command_key, _args, _kwargs = self._input_parser(content)
743
+ if command_key in self._COMMANDS:
744
+ self._COMMANDS[command_key][0](*_args, **_kwargs)
745
+ continue
746
+ else:
747
+ self.logger.error(f"Unknown command: '{command_key}'. Please input `help` to show all commands.")
748
+ continue
749
+
750
+ except KeyboardInterrupt:
751
+ self.do_exit()
752
+
753
+ except Exception as e:
754
+ self.logger.error(f"An exception occurred in cli: {e}.\n{traceback.format_exc()}")
755
+ continue
756
+ finally:
757
+ if not self.closed:
758
+ self._close()
759
+
760
+
761
+ ARGS_GLOBAL = """
762
+ # global configs
763
+ TASK: !!str MD
764
+ START: !!int 1 # 0: from scratch; 1: load checkpoint from LOAD_CHK_FILE_PATH
765
+ VERBOSE: !!int 1
766
+ DEVICE: !!str 'cuda:0'
767
+ BATCH_SIZE: !!int 16
768
+ """
769
+
770
+ ARGS_IO = """
771
+ LOAD_CHK_FILE_PATH: !!str your/model/checkpoint/file/path
772
+ OUTPUT_PATH: !!str your/log/output/path
773
+ OUTPUT_POSTFIX: !!str your_logfile_suffix
774
+ PREDICTIONS_SAVE_FILE: !!str your/model/predictions/save/path # path of saving predictions
775
+ STRICT_LOAD: !!bool true # whether to strictly load model parameter
776
+ REDIRECT: !!bool true # whether output training logs to `OUTPUT_PATH` or directly print on screen.
777
+ SAVE_PREDICTIONS: !!bool true # only for predictions. Whether output predictions to a dump file.
778
+ DATA_TYPE: !!str BS # Literal['POSCAR', 'OUTCAR', 'CIF', 'ASE_TRAJ', 'BS', 'OPT', 'MD']
779
+ DATA_PATH: !!str /your/data/path # the path of data used for calculation. if training, it will be viewed as the training set.
780
+ DATA_NAME_SELECTOR: !!str ".*$" # regular express to select data names. Only matched name will be finally load.
781
+ FSDATA_PATH: !!str your/final/state/data/path # used for calc. requiring both initial and final states, e.g., CI-NEB
782
+ DISPDATA_PATH: !!str your/displacement/data/path # used for calc. requiring initial guess of a direction, e.g., Dimer
783
+ VAL_SET_PATH: !!str your/validation/set/path # used for training that requires validation data
784
+ VAL_SPLIT_RATIO: !!float 0.1 # the ratio of validation set in the total dataset. if `VAL_SET` is given, this arg will be ignored.
785
+ DATA_LOADER_KWARGS: {} # other kwargs for data loader.
786
+ IS_SHUFFLE: !!bool false # whether to randomly shuffle dataset before calculating.
787
+ """
788
+
789
+ ARGS_MODEL = """
790
+ # model configs
791
+ MODEL_FILE: !!str your/model/file/path/template_model.py # function file path of torch model
792
+ MODEL_NAME: !!str YourModel # the specific name of the model in `MODEL_FILE`
793
+ MODEL_CONFIG: # model hyperparameters used for `MODEL_NAME.__init__(**MODEL_CONFIG)`
794
+ hyperparameter1: 'xxx'
795
+ hyperparameter2: 'xxx'
796
+ """
797
+
798
+ ARGS_TRAIN = """
799
+ # training
800
+ TRAIN:
801
+ # epoches & val set
802
+ EPOCH: !!int 10
803
+ VAL_BATCH_SIZE: !!int 20 # batch size for validation. default is the same as BATCH_SIZE
804
+ VAL_PER_STEP: !!int 100 # validate every `VAL_PER_STEP` steps. step = `BATCH_SIZE` * `ACCUMULATE_STEP`
805
+ VAL_IF_TRN_LOSS_BELOW: !!float 1.e5 # only validating after training loss < `VAL_IF_TRN_LOSS_BELOW`
806
+ ACCUMULATE_STEP: !!int 12 # gradient accumulation steps
807
+ # loss configs
808
+ LOSS: !!str Energy_Loss # 'MSE': nn.MSELoss, 'MAE': nn.L1Loss, 'Hubber': nn.HuberLoss, 'CrossEntropy': nn.CrossEntropyLoss 'Energy_Force_Loss': Energy_Force_Loss, 'Energy_Loss': Energy_Loss
809
+ LOSS_CONFIG: # other kwargs for loss function
810
+ loss_E: !!str SmoothMAE
811
+
812
+ METRICS: # tuple of ones in [E_MAE, F_MAE, F_MaxE, E_R2, MSE, MAE, R2, RMSE], F_MaxE is the max absolute error of forces.
813
+ - !!str E_MAE
814
+ - !!str E_R2
815
+ METRICS_CONFIG: {} # other kwargs for metrics
816
+ # - F_MaxE
817
+
818
+ # optimizer configs
819
+ OPTIM: !!str AdamW # model optimizer. Available values:
820
+ # 'Adam': th.optim.Adam, 'SGD': th.optim.SGD, 'AdamW': th.optim.AdamW, 'Adadelta': th.optim.Adadelta,
821
+ # 'Adagrad': th.optim.Adagrad, 'ASGD': th.optim.ASGD, 'Adamax': th.optim.Adamax, 'FIRE': FIRELikeOptimizer,
822
+ OPTIM_CONFIG: # optimizer kwargs
823
+ lr: !!float 2.e-4
824
+ # ...
825
+ LAYERWISE_OPTIM_CONFIG: # Supporting regular expression to selection layers and set them.
826
+ 'force_block.*': { 'lr': 5.e-4 }
827
+ 'energy_block.*': { 'lr': 2.e-4 }
828
+ '.*_bias_layer.*': { 'lr': 2.e-4 }
829
+
830
+ GRAD_CLIP: !!bool true # whether to toggle on gradient clip
831
+ GRAD_CLIP_MAX_NORM: !!float 10. # maximum grad. norm to clip
832
+ GRAD_CLIP_CONFIG: {} # other kwargs for `nn.utils.clip_grad_norm_` function
833
+ LR_SCHEDULER: !!str None # learning rate scheduler. Available values:
834
+ # 'StepLR': StepLR, 'ExponentialLR': ExponentialLR, 'ChainedScheduler': ChainedScheduler,
835
+ # 'ConstantLR': ConstantLR, 'LambdaLR': LambdaLR, 'LinearLR': LinearLR,
836
+ # 'CosineAnnealingWarmRestarts': CosineAnnealingWarmRestarts, 'CyclicLR': CyclicLR,
837
+ # 'MultiStepLR': MultiStepLR, 'CosineAnnealingLR': CosineAnnealingLR, 'None': None,
838
+ LR_SCHEDULER_CONFIG: {} # kwargs of above `LR_SCHEDULER`
839
+ EMA: !!bool false # whether to toggle on EMA (Exponential Moving Average)
840
+ EMA_DECAY: !!float 0.999 # EMA decay rate
841
+ """
842
+
843
+ ARGS_PREDICT = """"""
844
+
845
+ ARGS_OPT = """
846
+ # relaxation
847
+ RELAXATION:
848
+ ALGO: !!str 'FIRE' # CG, BFGS, FIRE
849
+ ITER_SCHEME: !!str 'PR+' # only for ALGO=CG, 'PR+', 'FR', 'PR', 'WYL'
850
+ E_THRES: !!float 1.e4 # threshold of Energy difference
851
+ F_THRES: !!float 0.05 # threshold of max Force
852
+ MAXITER: !!int 300
853
+ STEPLENGTH: !!float 0.5
854
+ USE_BB: !!bool true
855
+ LINESEARCH: !!str 'B' # 'Backtrack' with Armijo's cond., 'Golden' for exact line search by golden section algo., 'Wolfe' for advance & retreat algo. with weak Wolfe cond.
856
+ LINESEARCH_MAXITER: !!int 8 # max iterations of linear search.
857
+ LINESEARCH_THRES: !!float 0.02 # only for LINESEARCH = 'Golden', threshold of exact line search.
858
+ LINESEARCH_FACTOR: !!float 0.5 # A factor in linesearch. Shrinkage factor for "Backtrack", scaling factor in interval search for "Golden"
859
+ REQUIRE_GRAD: !!bool False
860
+ """
861
+
862
+ ARGS_TS = """
863
+ # transition state
864
+ TRANSITION_STATE:
865
+ ALGO: !!str DIMER
866
+ #X_DIFF: None
867
+ E_THRES: !!float 1.e-4
868
+ TORQ_THRES: !!float 1.e-2
869
+ F_THRES: !!float 5.e-2
870
+ MAXITER_TRANS: !!int 300
871
+ MAXITER_ROT: !!int 5
872
+ MAX_STEPLENGTH: !!float 0.5
873
+ DX: !!float 1.e-1
874
+ REQUIRE_GRAD: !!bool False
875
+ """
876
+
877
+ ARGS_VIB = """
878
+ # vibration analyses (harmonic)
879
+ VIBRATION:
880
+ METHOD: !!str 'Coord' # Coord/Grad corresponding to finite difference and auto-grad scheme.
881
+ BLOCK_SIZE: !!int 1
882
+ DELTA: !!float 1e-2
883
+ """
884
+
885
+ ARGS_NEB = """
886
+ # NEB transition state
887
+ NEB:
888
+ ALGO: !!str 'CI-NEB'
889
+ N_IMAGES: !!int 7
890
+ SPRING_CONST: 5.0
891
+ OPTIMIZER: !!str FIRE
892
+ #OPTIMIZER_CONFIGS: Optional[Dict[str, Any]] = None, other kwargs of optimizer.
893
+ STEPLENGTH: !!float 0.2
894
+ E_THRESHOLD: !!float 1.e-3
895
+ F_THRESHOLD: !!float 0.05
896
+ MAXITER: !!int 20
897
+ REQUIRE_GRAD: !!bool False
898
+ """
899
+
900
+ ARGS_MD = """
901
+ # molecular dynamics
902
+ MD:
903
+ ENSEMBLE: !!str NVT
904
+ THERMOSTAT: !!str CSVR # only for ENSEMBLE=NVT, 'Langevin', 'VR', 'Nose-Hoover', 'CSVR'
905
+ THERMOSTAT_CONFIG:
906
+ DAMPING_COEFF: !!float 0.01
907
+ TIME_CONST: !!float 120
908
+ TIME_STEP: !!float 1 # Unit: fs
909
+ MAX_STEP: !!int 100 # total time (fs) = TIME_STEP * MAX_STEP
910
+ T_INIT: !!float 298.15 # Initial Temperature, Unit: K. For ENSEMBLE=NVE, T_INIT is only used to generate ramdom initial velocities by Boltzmann dist.
911
+ OUTPUT_COORDS_PER_STEP: !!int 1 # To control the frequency of outputting atom coordinates. If verbose = 3, atom velocities would also be outputted.
912
+ REQUIRE_GRAD: !!bool False
913
+ """
914
+
915
+ ARGS_CMD = """
916
+ MD:
917
+ ENSEMBLE: !!str NVT
918
+ THERMOSTAT: !!str CSVR # only for ENSEMBLE=NVT, 'Langevin', 'VR', 'Nose-Hoover', 'CSVR'
919
+ THERMOSTAT_CONFIG:
920
+ DAMPING_COEFF: !!float 0.01
921
+ TIME_CONST: !!float 120
922
+ TIME_STEP: !!float 1 # Unit: fs
923
+ MAX_STEP: !!int 100 # total time (fs) = TIME_STEP * MAX_STEP
924
+ T_INIT: !!float 298.15 # Initial Temperature, Unit: K. For ENSEMBLE=NVE, T_INIT is only used to generate ramdom initial velocities by Boltzmann dist.
925
+ OUTPUT_COORDS_PER_STEP: !!int 1 # To control the frequency of outputting atom coordinates. If verbose = 3, atom velocities would also be outputted.
926
+ REQUIRE_GRAD: !!bool False
927
+ # Optional: constraints
928
+ CONSTRAINTS_FILE: !!str ./constraints.py
929
+ CONSTRAINTS_FUNC: !!str func
930
+ """
931
+
932
+ if __name__ == '__main__':
933
+ f = BaseCLI()
934
+ f.run()