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.
- BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
- BUCToolkit/BatchGenerate/__init__.py +6 -0
- BUCToolkit/BatchGenerate/coords_interp.py +127 -0
- BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
- BUCToolkit/BatchGenerate/random_replace.py +88 -0
- BUCToolkit/BatchMC/MetropolisMC.py +210 -0
- BUCToolkit/BatchMC/_BaseMC.py +863 -0
- BUCToolkit/BatchMC/__init__.py +8 -0
- BUCToolkit/BatchMD/BiasedMD.py +276 -0
- BUCToolkit/BatchMD/NVE.py +92 -0
- BUCToolkit/BatchMD/NVT.py +510 -0
- BUCToolkit/BatchMD/_BaseMD.py +1251 -0
- BUCToolkit/BatchMD/__init__.py +18 -0
- BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
- BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
- BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
- BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
- BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
- BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
- BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
- BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
- BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
- BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
- BUCToolkit/BatchOptim/TS/__init__.py +10 -0
- BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
- BUCToolkit/BatchOptim/__init__.py +22 -0
- BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
- BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
- BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
- BUCToolkit/BatchOptim/frequency.py +496 -0
- BUCToolkit/BatchOptim/minimize/CG.py +256 -0
- BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
- BUCToolkit/BatchOptim/minimize/QN.py +205 -0
- BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
- BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
- BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
- BUCToolkit/BatchStructures/__init__.py +15 -0
- BUCToolkit/Preprocessing/__init__.py +7 -0
- BUCToolkit/Preprocessing/load_files.py +1680 -0
- BUCToolkit/Preprocessing/preprocessing.py +923 -0
- BUCToolkit/Preprocessing/sampling.py +299 -0
- BUCToolkit/Preprocessing/supercells.py +140 -0
- BUCToolkit/Preprocessing/write_files.py +524 -0
- BUCToolkit/StandardTemplate.py +417 -0
- BUCToolkit/__init__.py +33 -0
- BUCToolkit/_version.py +7 -0
- BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
- BUCToolkit/api/DataLoaders.py +321 -0
- BUCToolkit/api/Losses.py +170 -0
- BUCToolkit/api/Metrics.py +84 -0
- BUCToolkit/api/ModelOptims.py +296 -0
- BUCToolkit/api/MolecularDynamics.py +282 -0
- BUCToolkit/api/MonteCarlo.py +280 -0
- BUCToolkit/api/NEB.py +325 -0
- BUCToolkit/api/Predictor.py +292 -0
- BUCToolkit/api/StructureOptimization.py +484 -0
- BUCToolkit/api/Trainer.py +635 -0
- BUCToolkit/api/VibrationAnalysis.py +279 -0
- BUCToolkit/api/__init__.py +38 -0
- BUCToolkit/api/_io.py +1026 -0
- BUCToolkit/cli/__init__.py +20 -0
- BUCToolkit/cli/cli.py +934 -0
- BUCToolkit/cli/input_stub.py +140 -0
- BUCToolkit/cli/main.py +368 -0
- BUCToolkit/cli/print_logo.py +149 -0
- BUCToolkit/cli/template_model.py +6 -0
- BUCToolkit/io.py +30 -0
- BUCToolkit/utils/AtomSelector.py +64 -0
- BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
- BUCToolkit/utils/ElemListReduce.py +52 -0
- BUCToolkit/utils/Ewald.py +210 -0
- BUCToolkit/utils/IrregularTensorReformat.py +59 -0
- BUCToolkit/utils/_CheckModules.py +30 -0
- BUCToolkit/utils/_Element_info.py +527 -0
- BUCToolkit/utils/__init__.py +22 -0
- BUCToolkit/utils/_para_flatt_list.py +131 -0
- BUCToolkit/utils/_print_formatter.py +59 -0
- BUCToolkit/utils/check_structures.py +204 -0
- BUCToolkit/utils/function_utils.py +147 -0
- BUCToolkit/utils/grad_functions.py +69 -0
- BUCToolkit/utils/index_ops.py +432 -0
- BUCToolkit/utils/setup_loggers.py +137 -0
- buctoolkit-1.0b0.dist-info/METADATA +28 -0
- buctoolkit-1.0b0.dist-info/RECORD +88 -0
- buctoolkit-1.0b0.dist-info/WHEEL +5 -0
- buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
- buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
- 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()
|