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.
Files changed (46) hide show
  1. wrfrun/__init__.py +3 -0
  2. wrfrun/core/__init__.py +5 -0
  3. wrfrun/core/base.py +680 -0
  4. wrfrun/core/config.py +717 -0
  5. wrfrun/core/error.py +80 -0
  6. wrfrun/core/replay.py +113 -0
  7. wrfrun/core/server.py +212 -0
  8. wrfrun/data.py +418 -0
  9. wrfrun/extension/__init__.py +1 -0
  10. wrfrun/extension/littler/__init__.py +1 -0
  11. wrfrun/extension/littler/utils.py +599 -0
  12. wrfrun/extension/utils.py +66 -0
  13. wrfrun/model/__init__.py +7 -0
  14. wrfrun/model/base.py +14 -0
  15. wrfrun/model/plot.py +54 -0
  16. wrfrun/model/utils.py +34 -0
  17. wrfrun/model/wrf/__init__.py +6 -0
  18. wrfrun/model/wrf/_metgrid.py +71 -0
  19. wrfrun/model/wrf/_ndown.py +39 -0
  20. wrfrun/model/wrf/core.py +805 -0
  21. wrfrun/model/wrf/exec_wrap.py +101 -0
  22. wrfrun/model/wrf/geodata.py +301 -0
  23. wrfrun/model/wrf/namelist.py +377 -0
  24. wrfrun/model/wrf/scheme.py +311 -0
  25. wrfrun/model/wrf/vtable.py +65 -0
  26. wrfrun/pbs.py +86 -0
  27. wrfrun/plot/__init__.py +1 -0
  28. wrfrun/plot/wps.py +188 -0
  29. wrfrun/res/__init__.py +22 -0
  30. wrfrun/res/config.toml.template +136 -0
  31. wrfrun/res/extension/plotgrids.ncl +216 -0
  32. wrfrun/res/job_scheduler/pbs.template +6 -0
  33. wrfrun/res/job_scheduler/slurm.template +6 -0
  34. wrfrun/res/namelist/namelist.input.da_wrfvar.template +261 -0
  35. wrfrun/res/namelist/namelist.input.dfi.template +260 -0
  36. wrfrun/res/namelist/namelist.input.real.template +256 -0
  37. wrfrun/res/namelist/namelist.input.wrf.template +256 -0
  38. wrfrun/res/namelist/namelist.wps.template +44 -0
  39. wrfrun/res/namelist/parame.in.template +11 -0
  40. wrfrun/res/run.sh.template +16 -0
  41. wrfrun/run.py +264 -0
  42. wrfrun/utils.py +257 -0
  43. wrfrun/workspace.py +88 -0
  44. wrfrun-0.1.7.dist-info/METADATA +67 -0
  45. wrfrun-0.1.7.dist-info/RECORD +46 -0
  46. wrfrun-0.1.7.dist-info/WHEEL +4 -0
wrfrun/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .core import *
2
+ from .run import *
3
+ from .res import *
@@ -0,0 +1,5 @@
1
+ from .base import *
2
+ from .config import *
3
+ from .error import *
4
+ from .replay import *
5
+ from .server import *
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"]