wrfrun 0.1.7__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.
- wrfrun/__init__.py +3 -0
- wrfrun/core/__init__.py +5 -0
- wrfrun/core/base.py +680 -0
- wrfrun/core/config.py +717 -0
- wrfrun/core/error.py +80 -0
- wrfrun/core/replay.py +113 -0
- wrfrun/core/server.py +212 -0
- wrfrun/data.py +418 -0
- wrfrun/extension/__init__.py +1 -0
- wrfrun/extension/littler/__init__.py +1 -0
- wrfrun/extension/littler/utils.py +599 -0
- wrfrun/extension/utils.py +66 -0
- wrfrun/model/__init__.py +7 -0
- wrfrun/model/base.py +14 -0
- wrfrun/model/plot.py +54 -0
- wrfrun/model/utils.py +34 -0
- wrfrun/model/wrf/__init__.py +6 -0
- wrfrun/model/wrf/_metgrid.py +71 -0
- wrfrun/model/wrf/_ndown.py +39 -0
- wrfrun/model/wrf/core.py +805 -0
- wrfrun/model/wrf/exec_wrap.py +101 -0
- wrfrun/model/wrf/geodata.py +301 -0
- wrfrun/model/wrf/namelist.py +377 -0
- wrfrun/model/wrf/scheme.py +311 -0
- wrfrun/model/wrf/vtable.py +65 -0
- wrfrun/pbs.py +86 -0
- wrfrun/plot/__init__.py +1 -0
- wrfrun/plot/wps.py +188 -0
- wrfrun/res/__init__.py +22 -0
- wrfrun/res/config.toml.template +136 -0
- wrfrun/res/extension/plotgrids.ncl +216 -0
- wrfrun/res/job_scheduler/pbs.template +6 -0
- wrfrun/res/job_scheduler/slurm.template +6 -0
- wrfrun/res/namelist/namelist.input.da_wrfvar.template +261 -0
- wrfrun/res/namelist/namelist.input.dfi.template +260 -0
- wrfrun/res/namelist/namelist.input.real.template +256 -0
- wrfrun/res/namelist/namelist.input.wrf.template +256 -0
- wrfrun/res/namelist/namelist.wps.template +44 -0
- wrfrun/res/namelist/parame.in.template +11 -0
- wrfrun/res/run.sh.template +16 -0
- wrfrun/run.py +264 -0
- wrfrun/utils.py +257 -0
- wrfrun/workspace.py +88 -0
- wrfrun-0.1.7.dist-info/METADATA +67 -0
- wrfrun-0.1.7.dist-info/RECORD +46 -0
- wrfrun-0.1.7.dist-info/WHEEL +4 -0
wrfrun/__init__.py
ADDED
wrfrun/core/__init__.py
ADDED
wrfrun/core/base.py
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from json import dumps
|
|
4
|
+
from os import chdir, getcwd, listdir, makedirs, remove, symlink
|
|
5
|
+
from os.path import abspath, basename, dirname, exists, isdir
|
|
6
|
+
from shutil import copyfile, make_archive, move
|
|
7
|
+
from typing import Optional, TypedDict, Union
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from .config import WRFRUNConfig
|
|
12
|
+
from .error import CommandError, ConfigError, OutputFileError
|
|
13
|
+
from ..utils import check_path, logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_subprocess_status(status: subprocess.CompletedProcess):
|
|
17
|
+
"""
|
|
18
|
+
Check subprocess return code and print log if ``return_code != 0``.
|
|
19
|
+
|
|
20
|
+
:param status: Status from subprocess.
|
|
21
|
+
:type status: CompletedProcess
|
|
22
|
+
"""
|
|
23
|
+
if status.returncode != 0:
|
|
24
|
+
# print command
|
|
25
|
+
command = status.args
|
|
26
|
+
logger.error(f"Failed to exec command: {command}")
|
|
27
|
+
|
|
28
|
+
# print log
|
|
29
|
+
logger.error("====== stdout ======")
|
|
30
|
+
logger.error(status.stdout.decode())
|
|
31
|
+
logger.error("====== ====== ======")
|
|
32
|
+
logger.error("====== stderr ======")
|
|
33
|
+
logger.error(status.stderr.decode())
|
|
34
|
+
logger.error("====== ====== ======")
|
|
35
|
+
|
|
36
|
+
# raise error
|
|
37
|
+
raise RuntimeError
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def call_subprocess(command: list[str], work_path: Optional[str] = None, print_output=False):
|
|
41
|
+
"""
|
|
42
|
+
Execute the given command in system shell.
|
|
43
|
+
|
|
44
|
+
:param command: A list contains the command and parameters to be executed.
|
|
45
|
+
:type command: list
|
|
46
|
+
:param work_path: The work path of the command.
|
|
47
|
+
If None, works in current directory.
|
|
48
|
+
:type work_path: str | None
|
|
49
|
+
:param print_output: If print standard output and error in the logger.
|
|
50
|
+
:type print_output: bool
|
|
51
|
+
:return:
|
|
52
|
+
:rtype:
|
|
53
|
+
"""
|
|
54
|
+
if work_path is not None:
|
|
55
|
+
origin_path = getcwd()
|
|
56
|
+
chdir(work_path)
|
|
57
|
+
else:
|
|
58
|
+
origin_path = None
|
|
59
|
+
|
|
60
|
+
status = subprocess.run(' '.join(command), shell=True, capture_output=True)
|
|
61
|
+
|
|
62
|
+
if origin_path is not None:
|
|
63
|
+
chdir(origin_path)
|
|
64
|
+
|
|
65
|
+
check_subprocess_status(status)
|
|
66
|
+
|
|
67
|
+
if print_output:
|
|
68
|
+
logger.info(status.stdout.decode())
|
|
69
|
+
logger.warning(status.stderr.decode())
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _json_default(obj):
|
|
73
|
+
"""
|
|
74
|
+
Used for json.dumps.
|
|
75
|
+
|
|
76
|
+
:param obj:
|
|
77
|
+
:type obj:
|
|
78
|
+
:return:
|
|
79
|
+
:rtype:
|
|
80
|
+
"""
|
|
81
|
+
if isinstance(obj, np.integer):
|
|
82
|
+
return int(obj)
|
|
83
|
+
elif isinstance(obj, np.floating):
|
|
84
|
+
return float(obj)
|
|
85
|
+
else:
|
|
86
|
+
raise TypeError(f"Object of type {type(obj)} is not JSON serializable.")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class InputFileType(Enum):
|
|
90
|
+
"""
|
|
91
|
+
Input file type.
|
|
92
|
+
``WRFRUN_RES`` means the input file is from the NWP or wrfrun package.
|
|
93
|
+
``CUSTOM_RES`` means the input file is from the user, which may be a customized file.
|
|
94
|
+
"""
|
|
95
|
+
WRFRUN_RES = 1
|
|
96
|
+
CUSTOM_RES = 2
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class FileConfigDict(TypedDict):
|
|
100
|
+
"""
|
|
101
|
+
Dict class to give information to process files.
|
|
102
|
+
"""
|
|
103
|
+
file_path: str
|
|
104
|
+
save_path: str
|
|
105
|
+
save_name: str
|
|
106
|
+
is_data: bool
|
|
107
|
+
is_output: bool
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ExecutableClassConfig(TypedDict):
|
|
111
|
+
"""
|
|
112
|
+
Executable class initialization config template.
|
|
113
|
+
"""
|
|
114
|
+
# only list essential config
|
|
115
|
+
class_args: tuple
|
|
116
|
+
class_kwargs: dict
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ExecutableConfig(TypedDict):
|
|
120
|
+
"""
|
|
121
|
+
Executable config template.
|
|
122
|
+
"""
|
|
123
|
+
name: str
|
|
124
|
+
cmd: Union[str, list[str]]
|
|
125
|
+
work_path: Optional[str]
|
|
126
|
+
mpi_use: bool
|
|
127
|
+
mpi_cmd: Optional[str]
|
|
128
|
+
mpi_core_num: Optional[int]
|
|
129
|
+
class_config: Optional[ExecutableClassConfig]
|
|
130
|
+
input_file_config: Optional[list[FileConfigDict]]
|
|
131
|
+
output_file_config: Optional[list[FileConfigDict]]
|
|
132
|
+
custom_config: Optional[dict]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class _ExecutableConfigRecord:
|
|
136
|
+
"""
|
|
137
|
+
Record executable configs and export them.
|
|
138
|
+
"""
|
|
139
|
+
_instance = None
|
|
140
|
+
_initialized = False
|
|
141
|
+
|
|
142
|
+
def __init__(self, save_path: Optional[str] = None, include_data=False):
|
|
143
|
+
"""
|
|
144
|
+
Record executable configs and export them.
|
|
145
|
+
|
|
146
|
+
:param save_path: Save path of the exported config file.
|
|
147
|
+
:type save_path: str
|
|
148
|
+
:param include_data: If includes input data.
|
|
149
|
+
:type include_data: bool
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
if self._initialized:
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
if save_path is None:
|
|
156
|
+
WRFRUNConfig.IS_RECORDING = False
|
|
157
|
+
else:
|
|
158
|
+
WRFRUNConfig.IS_RECORDING = True
|
|
159
|
+
|
|
160
|
+
self.save_path = save_path
|
|
161
|
+
self.include_data = include_data
|
|
162
|
+
|
|
163
|
+
self.work_path = WRFRUNConfig.parse_resource_uri(WRFRUNConfig.WRFRUN_REPLAY_WORK_PATH)
|
|
164
|
+
self.content_path = f"{self.work_path}/config_and_data"
|
|
165
|
+
check_path(self.content_path)
|
|
166
|
+
|
|
167
|
+
self._recorded_config = []
|
|
168
|
+
self._name_count = {}
|
|
169
|
+
|
|
170
|
+
self._initialized = True
|
|
171
|
+
|
|
172
|
+
def __new__(cls, *args, **kwargs):
|
|
173
|
+
if cls._instance is None:
|
|
174
|
+
cls._instance = super().__new__(cls)
|
|
175
|
+
|
|
176
|
+
return cls._instance
|
|
177
|
+
|
|
178
|
+
def reinit(self, save_path: Optional[str] = None, include_data=False):
|
|
179
|
+
"""
|
|
180
|
+
Reinitialize this instance.
|
|
181
|
+
|
|
182
|
+
:return:
|
|
183
|
+
:rtype:
|
|
184
|
+
"""
|
|
185
|
+
self._initialized = False
|
|
186
|
+
return _ExecutableConfigRecord(save_path, include_data)
|
|
187
|
+
|
|
188
|
+
def record(self, exported_config: ExecutableConfig):
|
|
189
|
+
"""
|
|
190
|
+
Record exported config for replay.
|
|
191
|
+
|
|
192
|
+
:param exported_config: Executable config.
|
|
193
|
+
:type exported_config: ExecutableConfig
|
|
194
|
+
:return:
|
|
195
|
+
:rtype:
|
|
196
|
+
"""
|
|
197
|
+
if not self.include_data:
|
|
198
|
+
self._recorded_config.append(exported_config)
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
check_path(self.content_path)
|
|
202
|
+
|
|
203
|
+
# process exported config so we can also include data.
|
|
204
|
+
# create directory to place data
|
|
205
|
+
name = exported_config["name"]
|
|
206
|
+
if name in self._name_count:
|
|
207
|
+
self._name_count[name] += 1
|
|
208
|
+
index = self._name_count[name]
|
|
209
|
+
else:
|
|
210
|
+
self._name_count[name] = 1
|
|
211
|
+
index = 1
|
|
212
|
+
|
|
213
|
+
data_save_uri = f"{WRFRUNConfig.WRFRUN_REPLAY_WORK_PATH}/{name}/{index}"
|
|
214
|
+
data_save_path = f"{self.content_path}/{name}/{index}"
|
|
215
|
+
makedirs(data_save_path)
|
|
216
|
+
|
|
217
|
+
input_file_config = exported_config["input_file_config"]
|
|
218
|
+
|
|
219
|
+
for _config_index, _config in enumerate(input_file_config):
|
|
220
|
+
if not _config["is_data"]:
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
if _config["is_output"]:
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
file_path = _config["file_path"]
|
|
227
|
+
file_path = WRFRUNConfig.parse_resource_uri(file_path)
|
|
228
|
+
filename = basename(file_path)
|
|
229
|
+
copyfile(file_path, f"{data_save_path}/{filename}")
|
|
230
|
+
|
|
231
|
+
_config["file_path"] = f"{data_save_uri}/{filename}"
|
|
232
|
+
input_file_config[_config_index] = _config
|
|
233
|
+
|
|
234
|
+
exported_config["input_file_config"] = input_file_config
|
|
235
|
+
self._recorded_config.append(exported_config)
|
|
236
|
+
|
|
237
|
+
def clear_records(self):
|
|
238
|
+
"""
|
|
239
|
+
Clean old configs.
|
|
240
|
+
|
|
241
|
+
:return:
|
|
242
|
+
:rtype:
|
|
243
|
+
"""
|
|
244
|
+
self._recorded_config = []
|
|
245
|
+
|
|
246
|
+
def export_replay_file(self):
|
|
247
|
+
"""
|
|
248
|
+
Save replay file to the specific save path.
|
|
249
|
+
|
|
250
|
+
:return:
|
|
251
|
+
:rtype:
|
|
252
|
+
"""
|
|
253
|
+
if len(self._recorded_config) == 0:
|
|
254
|
+
logger.warning("No replay config has been recorded.")
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
logger.info("Exporting replay config... It may take a few minutes if you include data.")
|
|
258
|
+
|
|
259
|
+
check_path(self.content_path)
|
|
260
|
+
|
|
261
|
+
with open(f"{self.content_path}/config.json", "w") as f:
|
|
262
|
+
f.write(dumps(self._recorded_config, indent=4, default=_json_default))
|
|
263
|
+
|
|
264
|
+
if exists(self.save_path):
|
|
265
|
+
if isdir(self.save_path):
|
|
266
|
+
self.save_path = f"{self.save_path}/1.replay"
|
|
267
|
+
else:
|
|
268
|
+
if not self.save_path.endswith(".replay"):
|
|
269
|
+
self.save_path = f"{self.save_path}.replay"
|
|
270
|
+
|
|
271
|
+
if exists(self.save_path):
|
|
272
|
+
logger.warning(f"Found existed replay file with the same name '{basename(self.save_path)}', overwrite it")
|
|
273
|
+
remove(self.save_path)
|
|
274
|
+
|
|
275
|
+
if not exists(dirname(self.save_path)):
|
|
276
|
+
makedirs(dirname(self.save_path))
|
|
277
|
+
|
|
278
|
+
temp_file = f"{self.work_path}/config_and_data"
|
|
279
|
+
make_archive(temp_file, "zip", self.content_path)
|
|
280
|
+
move(f"{temp_file}.zip", self.save_path)
|
|
281
|
+
|
|
282
|
+
logger.info(f"Replay config exported to {self.save_path}")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
ExecConfigRecorder = _ExecutableConfigRecord()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class ExecutableBase:
|
|
289
|
+
"""
|
|
290
|
+
Base class for all executables.
|
|
291
|
+
"""
|
|
292
|
+
_instance = None
|
|
293
|
+
|
|
294
|
+
def __init__(self, name: str, cmd: Union[str, list[str]], work_path: str, mpi_use=False, mpi_cmd: Optional[str] = None, mpi_core_num: Optional[int] = None):
|
|
295
|
+
"""
|
|
296
|
+
Base class for all executables.
|
|
297
|
+
|
|
298
|
+
:param name: Unique name to identify different executables.
|
|
299
|
+
:type name: str
|
|
300
|
+
:param cmd: Command to execute, can be a single string or a list contains the command and its parameters.
|
|
301
|
+
For example, ``"./geogrid.exe"``, ``["./link_grib.csh", "data/*", "."]``.
|
|
302
|
+
If you want to use mpi, then ``cmd`` must be a string.
|
|
303
|
+
:type cmd: str
|
|
304
|
+
:param work_path: Working directory path.
|
|
305
|
+
:type work_path: str
|
|
306
|
+
:param mpi_use: If you use mpi. You have to give ``mpi_cmd`` and ``mpi_core_num`` if you use mpi. Defaults to False.
|
|
307
|
+
:type mpi_use: bool
|
|
308
|
+
:param mpi_cmd: MPI command. For example, ``"mpirun"``. Defaults to None.
|
|
309
|
+
:type mpi_cmd: str
|
|
310
|
+
:param mpi_core_num: How many cores you use. Defaults to None.
|
|
311
|
+
:type mpi_core_num: int
|
|
312
|
+
"""
|
|
313
|
+
if mpi_use and isinstance(cmd, list):
|
|
314
|
+
logger.error("If you want to use mpi, then `cmd` must be a single string.")
|
|
315
|
+
raise CommandError("If you want to use mpi, then `cmd` must be a single string.")
|
|
316
|
+
|
|
317
|
+
self.name = name
|
|
318
|
+
self.cmd = cmd
|
|
319
|
+
self.work_path = work_path
|
|
320
|
+
self.mpi_use = mpi_use
|
|
321
|
+
self.mpi_cmd = mpi_cmd
|
|
322
|
+
self.mpi_core_num = mpi_core_num
|
|
323
|
+
|
|
324
|
+
self.class_config: ExecutableClassConfig = {"class_args": (), "class_kwargs": {}}
|
|
325
|
+
self.custom_config: dict = {}
|
|
326
|
+
self.input_file_config: list[FileConfigDict] = []
|
|
327
|
+
self.output_file_config: list[FileConfigDict] = []
|
|
328
|
+
|
|
329
|
+
# directory to save outputs
|
|
330
|
+
self._output_save_path = f"{WRFRUNConfig.WRFRUN_OUTPUT_PATH}/{self.name}"
|
|
331
|
+
self._log_save_path = f"{self._output_save_path}/logs"
|
|
332
|
+
|
|
333
|
+
def __new__(cls, *args, **kwargs):
|
|
334
|
+
if cls._instance is None:
|
|
335
|
+
cls._instance = super().__new__(cls)
|
|
336
|
+
|
|
337
|
+
return cls._instance
|
|
338
|
+
|
|
339
|
+
def generate_custom_config(self):
|
|
340
|
+
"""
|
|
341
|
+
Generate custom configs.
|
|
342
|
+
|
|
343
|
+
:return:
|
|
344
|
+
:rtype:
|
|
345
|
+
"""
|
|
346
|
+
logger.debug(f"Method 'generate_custom_config' not implemented in '{self.name}'")
|
|
347
|
+
|
|
348
|
+
def load_custom_config(self):
|
|
349
|
+
"""
|
|
350
|
+
Load custom configs.
|
|
351
|
+
|
|
352
|
+
:return:
|
|
353
|
+
:rtype:
|
|
354
|
+
"""
|
|
355
|
+
logger.debug(f"Method 'load_custom_config' not implemented in '{self.name}'")
|
|
356
|
+
|
|
357
|
+
def export_config(self) -> ExecutableConfig:
|
|
358
|
+
"""
|
|
359
|
+
Export config of this executable.
|
|
360
|
+
|
|
361
|
+
:return: A dict contains configs.
|
|
362
|
+
:rtype: ExecutableConfig
|
|
363
|
+
"""
|
|
364
|
+
self.generate_custom_config()
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
"name": self.name,
|
|
368
|
+
"cmd": self.cmd,
|
|
369
|
+
"work_path": self.work_path,
|
|
370
|
+
"mpi_use": self.mpi_use,
|
|
371
|
+
"mpi_cmd": self.mpi_cmd,
|
|
372
|
+
"mpi_core_num": self.mpi_core_num,
|
|
373
|
+
"class_config": self.class_config,
|
|
374
|
+
"custom_config": self.custom_config,
|
|
375
|
+
"input_file_config": self.input_file_config,
|
|
376
|
+
"output_file_config": self.output_file_config
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
def load_config(self, config: ExecutableConfig):
|
|
380
|
+
"""
|
|
381
|
+
Load config from a dict.
|
|
382
|
+
|
|
383
|
+
:param config: Config dict. It must contain some essential keys. Check ``ExecutableConfig`` for details.
|
|
384
|
+
:type config: ExecutableConfig
|
|
385
|
+
:return:
|
|
386
|
+
:rtype:
|
|
387
|
+
"""
|
|
388
|
+
if "name" not in config:
|
|
389
|
+
logger.error("A valid config is required. Please check ``ExecutableConfig``.")
|
|
390
|
+
raise ValueError("A valid config is required. Please check ``ExecutableConfig``.")
|
|
391
|
+
|
|
392
|
+
if self.name != config["name"]:
|
|
393
|
+
logger.error(f"Config belongs to '{config['name']}', not {self.name}")
|
|
394
|
+
raise ConfigError(f"Config belongs to '{config['name']}', not {self.name}")
|
|
395
|
+
|
|
396
|
+
self.cmd = config["cmd"]
|
|
397
|
+
self.work_path = config["work_path"]
|
|
398
|
+
self.mpi_use = config["mpi_use"]
|
|
399
|
+
self.mpi_cmd = config["mpi_cmd"]
|
|
400
|
+
self.mpi_core_num = config["mpi_core_num"]
|
|
401
|
+
self.class_config = config["class_config"]
|
|
402
|
+
self.custom_config = config["custom_config"]
|
|
403
|
+
self.input_file_config = config["input_file_config"]
|
|
404
|
+
self.output_file_config = config["output_file_config"]
|
|
405
|
+
|
|
406
|
+
self.load_custom_config()
|
|
407
|
+
|
|
408
|
+
def replay(self):
|
|
409
|
+
"""
|
|
410
|
+
This method will be called when replay the simulation.
|
|
411
|
+
This method should take care every job that will be done when replaying the simulation.
|
|
412
|
+
|
|
413
|
+
:return:
|
|
414
|
+
:rtype:
|
|
415
|
+
"""
|
|
416
|
+
logger.debug(f"Method 'replay' not implemented in '{self.name}', fall back to default action.")
|
|
417
|
+
self()
|
|
418
|
+
|
|
419
|
+
def add_input_files(self, input_files: Union[str, list[str], FileConfigDict, list[FileConfigDict]], is_data=True, is_output=True):
|
|
420
|
+
"""
|
|
421
|
+
Add input files the extension will use.
|
|
422
|
+
|
|
423
|
+
You can give a single file path or a list contains files' path.
|
|
424
|
+
|
|
425
|
+
>>> self.add_input_files("data/custom_file")
|
|
426
|
+
>>> self.add_input_files(["data/custom_file_1", "data/custom_file_2"])
|
|
427
|
+
|
|
428
|
+
You can give more information with a ``FileConfigDict``, like the path and the name to store, and if it is data.
|
|
429
|
+
|
|
430
|
+
>>> file_dict: FileConfigDict = {
|
|
431
|
+
... "file_path": "data/custom_file.nc",
|
|
432
|
+
... "save_path": f"{WRFRUNConfig.WPS_WORK_PATH}",
|
|
433
|
+
... "save_name": "custom_file.nc",
|
|
434
|
+
... "is_data": True,
|
|
435
|
+
... "is_output": False
|
|
436
|
+
... }
|
|
437
|
+
>>> self.add_input_files(file_dict)
|
|
438
|
+
|
|
439
|
+
>>> file_dict_1: FileConfigDict = {
|
|
440
|
+
... "file_path": "data/custom_file",
|
|
441
|
+
... "save_path": f"{WRFRUNConfig.WPS_WORK_PATH}/geogrid",
|
|
442
|
+
... "save_name": "GEOGRID.TBL",
|
|
443
|
+
... "is_data": False,
|
|
444
|
+
... "is_output": False
|
|
445
|
+
... }
|
|
446
|
+
>>> file_dict_2: FileConfigDict = {
|
|
447
|
+
... "file_path": "data/custom_file",
|
|
448
|
+
... "save_path": f"{WRFRUNConfig.WPS_WORK_PATH}/outputs",
|
|
449
|
+
... "save_name": "test_file",
|
|
450
|
+
... "is_data": True,
|
|
451
|
+
... "is_output": True
|
|
452
|
+
... }
|
|
453
|
+
>>> self.add_input_files([file_dict_1, file_dict_2])
|
|
454
|
+
|
|
455
|
+
:param input_files: Custom files' path.
|
|
456
|
+
:type input_files: str | list | dict
|
|
457
|
+
:param is_data: If its data. This parameter will be overwritten by the value in ``input_files``.
|
|
458
|
+
:type is_data: bool
|
|
459
|
+
:return:
|
|
460
|
+
:rtype:
|
|
461
|
+
"""
|
|
462
|
+
if isinstance(input_files, str):
|
|
463
|
+
self.input_file_config.append(
|
|
464
|
+
{
|
|
465
|
+
"file_path": input_files, "save_path": self.work_path, "save_name": basename(input_files),
|
|
466
|
+
"is_data": is_data, "is_output": is_output
|
|
467
|
+
}
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
elif isinstance(input_files, list):
|
|
471
|
+
for _file in input_files:
|
|
472
|
+
if isinstance(_file, dict):
|
|
473
|
+
self.input_file_config.append(_file) # type: ignore
|
|
474
|
+
|
|
475
|
+
elif isinstance(_file, str):
|
|
476
|
+
self.input_file_config.append(
|
|
477
|
+
{
|
|
478
|
+
"file_path": _file, "save_path": self.work_path, "save_name": basename(_file),
|
|
479
|
+
"is_data": is_data, "is_output": is_output
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
else:
|
|
484
|
+
logger.error(f"Input file config should be string or `FileConfigDict`, but got '{type(_file)}'")
|
|
485
|
+
raise TypeError(f"Input file config should be string or `FileConfigDict`, but got '{type(_file)}'")
|
|
486
|
+
|
|
487
|
+
elif isinstance(input_files, dict):
|
|
488
|
+
self.input_file_config.append(input_files) # type: ignore
|
|
489
|
+
|
|
490
|
+
else:
|
|
491
|
+
logger.error(f"Input file config should be string or `FileConfigDict`, but got '{type(input_files)}'")
|
|
492
|
+
raise TypeError(f"Input file config should be string or `FileConfigDict`, but got '{type(input_files)}'")
|
|
493
|
+
|
|
494
|
+
def add_output_files(
|
|
495
|
+
self, output_dir: Optional[str] = None, save_path: Optional[str] = None, startswith: Union[None, str, tuple[str, ...]] = None,
|
|
496
|
+
endswith: Union[None, str, tuple[str, ...]] = None, outputs: Union[None, str, list[str]] = None, no_file_error=True
|
|
497
|
+
):
|
|
498
|
+
"""
|
|
499
|
+
Add save file rules.
|
|
500
|
+
|
|
501
|
+
:param output_dir: Output dir paths.
|
|
502
|
+
:type output_dir: str
|
|
503
|
+
:param save_path: Save path.
|
|
504
|
+
:type save_path: str
|
|
505
|
+
:param startswith: Prefix string or prefix list of output files.
|
|
506
|
+
:type startswith: str | list
|
|
507
|
+
:param endswith: Postfix string or Postfix list of output files.
|
|
508
|
+
:type endswith: str | list
|
|
509
|
+
:param outputs: Files name list. All files in the list will be saved.
|
|
510
|
+
:type outputs: str | list
|
|
511
|
+
:param no_file_error: If True, an error will be raised with the ``error_message``. Defaults to True.
|
|
512
|
+
:type no_file_error: bool
|
|
513
|
+
:return:
|
|
514
|
+
:rtype:
|
|
515
|
+
"""
|
|
516
|
+
if WRFRUNConfig.FAKE_SIMULATION_MODE:
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
if output_dir is None:
|
|
520
|
+
output_dir = self.work_path
|
|
521
|
+
|
|
522
|
+
if save_path is None:
|
|
523
|
+
save_path = f"{WRFRUNConfig.WRFRUN_OUTPUT_PATH}/{self.name}"
|
|
524
|
+
|
|
525
|
+
file_list = listdir(WRFRUNConfig.parse_resource_uri(output_dir))
|
|
526
|
+
save_file_list = []
|
|
527
|
+
|
|
528
|
+
if startswith is not None:
|
|
529
|
+
_list = []
|
|
530
|
+
for _file in file_list:
|
|
531
|
+
if _file.startswith(startswith):
|
|
532
|
+
_list.append(_file)
|
|
533
|
+
save_file_list += _list
|
|
534
|
+
|
|
535
|
+
logger.debug(f"Collect files match `startswith`: {_list}")
|
|
536
|
+
|
|
537
|
+
if endswith is not None:
|
|
538
|
+
_list = []
|
|
539
|
+
for _file in file_list:
|
|
540
|
+
if _file.endswith(endswith):
|
|
541
|
+
_list.append(_file)
|
|
542
|
+
save_file_list += _list
|
|
543
|
+
|
|
544
|
+
logger.debug(f"Collect files match `endswith`: {_list}")
|
|
545
|
+
|
|
546
|
+
if outputs is not None:
|
|
547
|
+
if isinstance(outputs, str) and outputs in file_list:
|
|
548
|
+
save_file_list.append(outputs)
|
|
549
|
+
else:
|
|
550
|
+
outputs = [x for x in outputs if x in file_list]
|
|
551
|
+
save_file_list += outputs
|
|
552
|
+
|
|
553
|
+
if len(save_file_list) < 1:
|
|
554
|
+
if no_file_error:
|
|
555
|
+
logger.error(f"Can't find any files match the giving rules: startswith='{startswith}', endswith='{endswith}', outputs='{outputs}'")
|
|
556
|
+
raise OutputFileError(f"Can't find any files match the giving rules: startswith='{startswith}', endswith='{endswith}', outputs='{outputs}'")
|
|
557
|
+
|
|
558
|
+
else:
|
|
559
|
+
logger.warning(f"Can't find any files match the giving rules: startswith='{startswith}', endswith='{endswith}', outputs='{outputs}'. Skip it.")
|
|
560
|
+
return
|
|
561
|
+
|
|
562
|
+
save_file_list = list(set(save_file_list))
|
|
563
|
+
logger.debug(f"Files to be processed: {save_file_list}")
|
|
564
|
+
|
|
565
|
+
for _file in save_file_list:
|
|
566
|
+
self.output_file_config.append({"file_path": f"{output_dir}/{_file}", "save_path": save_path, "save_name": _file, "is_data": True, "is_output": True})
|
|
567
|
+
|
|
568
|
+
def before_exec(self):
|
|
569
|
+
"""
|
|
570
|
+
Prepare input files.
|
|
571
|
+
|
|
572
|
+
:return:
|
|
573
|
+
:rtype:
|
|
574
|
+
"""
|
|
575
|
+
if WRFRUNConfig.FAKE_SIMULATION_MODE:
|
|
576
|
+
logger.info(f"We are in fake simulation mode, skip preparing input files for '{self.name}'")
|
|
577
|
+
return
|
|
578
|
+
|
|
579
|
+
for input_file in self.input_file_config:
|
|
580
|
+
file_path = input_file["file_path"]
|
|
581
|
+
save_path = input_file["save_path"]
|
|
582
|
+
save_name = input_file["save_name"]
|
|
583
|
+
|
|
584
|
+
file_path = WRFRUNConfig.parse_resource_uri(file_path)
|
|
585
|
+
save_path = WRFRUNConfig.parse_resource_uri(save_path)
|
|
586
|
+
|
|
587
|
+
file_path = abspath(file_path)
|
|
588
|
+
save_path = abspath(save_path)
|
|
589
|
+
|
|
590
|
+
if not exists(file_path):
|
|
591
|
+
logger.error(f"File not found: '{file_path}'")
|
|
592
|
+
raise FileNotFoundError(f"File not found: '{file_path}'")
|
|
593
|
+
|
|
594
|
+
if not exists(save_path):
|
|
595
|
+
makedirs(save_path)
|
|
596
|
+
|
|
597
|
+
target_path = f"{save_path}/{save_name}"
|
|
598
|
+
if exists(target_path):
|
|
599
|
+
logger.debug(f"Target file {save_name} exists, overwrite it.")
|
|
600
|
+
remove(target_path)
|
|
601
|
+
|
|
602
|
+
symlink(file_path, target_path)
|
|
603
|
+
|
|
604
|
+
def after_exec(self):
|
|
605
|
+
"""
|
|
606
|
+
Save outputs and logs.
|
|
607
|
+
|
|
608
|
+
:return:
|
|
609
|
+
:rtype:
|
|
610
|
+
"""
|
|
611
|
+
if WRFRUNConfig.FAKE_SIMULATION_MODE:
|
|
612
|
+
logger.info(f"We are in fake simulation mode, skip saving outputs for '{self.name}'")
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
for output_file in self.output_file_config:
|
|
616
|
+
file_path = output_file["file_path"]
|
|
617
|
+
save_path = output_file["save_path"]
|
|
618
|
+
save_name = output_file["save_name"]
|
|
619
|
+
|
|
620
|
+
file_path = WRFRUNConfig.parse_resource_uri(file_path)
|
|
621
|
+
save_path = WRFRUNConfig.parse_resource_uri(save_path)
|
|
622
|
+
|
|
623
|
+
file_path = abspath(file_path)
|
|
624
|
+
save_path = abspath(save_path)
|
|
625
|
+
|
|
626
|
+
if not exists(file_path):
|
|
627
|
+
logger.error(f"File not found: '{file_path}'")
|
|
628
|
+
raise FileNotFoundError(f"File not found: '{file_path}'")
|
|
629
|
+
|
|
630
|
+
if not exists(save_path):
|
|
631
|
+
makedirs(save_path)
|
|
632
|
+
|
|
633
|
+
target_path = f"{save_path}/{save_name}"
|
|
634
|
+
if exists(target_path):
|
|
635
|
+
logger.warning(f"Found existed file, which means you already may have output files in '{save_path}'. If you are saving logs, ignore this warning.")
|
|
636
|
+
|
|
637
|
+
move(file_path, target_path)
|
|
638
|
+
|
|
639
|
+
def exec(self):
|
|
640
|
+
"""
|
|
641
|
+
Execute the given command.
|
|
642
|
+
|
|
643
|
+
:return:
|
|
644
|
+
:rtype:
|
|
645
|
+
"""
|
|
646
|
+
work_path = WRFRUNConfig.parse_resource_uri(self.work_path)
|
|
647
|
+
|
|
648
|
+
if not self.mpi_use or None in [self.mpi_cmd, self.mpi_core_num]:
|
|
649
|
+
if isinstance(self.cmd, str):
|
|
650
|
+
self.cmd = [self.cmd, ]
|
|
651
|
+
|
|
652
|
+
logger.info(f"Running `{' '.join(self.cmd)}` ...")
|
|
653
|
+
_cmd = self.cmd
|
|
654
|
+
|
|
655
|
+
else:
|
|
656
|
+
logger.info(f"Running `{self.mpi_cmd} --oversubscribe -np {self.mpi_core_num} {self.cmd}` ...")
|
|
657
|
+
_cmd = [self.mpi_cmd, "--oversubscribe", "-np", str(self.mpi_core_num), self.cmd]
|
|
658
|
+
|
|
659
|
+
if WRFRUNConfig.FAKE_SIMULATION_MODE:
|
|
660
|
+
logger.info(f"We are in fake simulation mode, skip calling numerical model for '{self.name}'")
|
|
661
|
+
return
|
|
662
|
+
|
|
663
|
+
call_subprocess(_cmd, work_path=work_path)
|
|
664
|
+
|
|
665
|
+
def __call__(self):
|
|
666
|
+
"""
|
|
667
|
+
Execute the given command.
|
|
668
|
+
|
|
669
|
+
:return:
|
|
670
|
+
:rtype:
|
|
671
|
+
"""
|
|
672
|
+
self.before_exec()
|
|
673
|
+
self.exec()
|
|
674
|
+
self.after_exec()
|
|
675
|
+
|
|
676
|
+
if not WRFRUNConfig.IS_IN_REPLAY and WRFRUNConfig.IS_RECORDING:
|
|
677
|
+
ExecConfigRecorder.record(self.export_config())
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
__all__ = ["ExecutableBase", "FileConfigDict", "InputFileType", "ExecutableConfig", "ExecutableClassConfig", "ExecConfigRecorder"]
|