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/core/error.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
class WRFRunBasicError(Exception):
|
|
2
|
+
"""
|
|
3
|
+
Basic error type.
|
|
4
|
+
"""
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConfigError(WRFRunBasicError):
|
|
9
|
+
"""
|
|
10
|
+
Config cannot be used error.
|
|
11
|
+
"""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WRFRunContextError(WRFRunBasicError):
|
|
16
|
+
"""
|
|
17
|
+
Need to enter wrfrun context.
|
|
18
|
+
"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CommandError(WRFRunBasicError):
|
|
23
|
+
"""
|
|
24
|
+
The command is wrong.
|
|
25
|
+
"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LoadConfigError(WRFRunBasicError):
|
|
30
|
+
"""
|
|
31
|
+
Failed to load config.
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OutputFileError(WRFRunBasicError):
|
|
37
|
+
"""
|
|
38
|
+
No output found.
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ResourceURIError(WRFRunBasicError):
|
|
44
|
+
"""
|
|
45
|
+
Error about resource namespace.
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class InputFileError(WRFRunBasicError):
|
|
51
|
+
"""
|
|
52
|
+
Input file error.
|
|
53
|
+
"""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class NamelistError(WRFRunBasicError):
|
|
58
|
+
"""
|
|
59
|
+
Error about namelist.
|
|
60
|
+
"""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ExecRegisterError(WRFRunBasicError):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class GetExecClassError(WRFRunBasicError):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ModelNameError(WRFRunBasicError):
|
|
73
|
+
"""
|
|
74
|
+
Name of the model isn't found in the config file.
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ["WRFRunBasicError", "ConfigError", "WRFRunContextError", "CommandError", "LoadConfigError", "OutputFileError", "ResourceURIError", "InputFileError",
|
|
80
|
+
"NamelistError", "ExecRegisterError", "GetExecClassError", "ModelNameError"]
|
wrfrun/core/replay.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from json import loads
|
|
2
|
+
from os.path import exists
|
|
3
|
+
from shutil import unpack_archive
|
|
4
|
+
|
|
5
|
+
from .base import ExecutableBase, ExecutableConfig
|
|
6
|
+
from .config import WRFRUNConfig
|
|
7
|
+
from .error import ExecRegisterError, GetExecClassError
|
|
8
|
+
from ..utils import logger
|
|
9
|
+
|
|
10
|
+
WRFRUN_REPLAY_URI = ":WRFRUN_REPLAY:"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WRFRunExecutableRegisterCenter:
|
|
14
|
+
"""
|
|
15
|
+
Record all executable class.
|
|
16
|
+
"""
|
|
17
|
+
_instance = None
|
|
18
|
+
_initialized = False
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
if self._initialized:
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
self._exec_db = {}
|
|
25
|
+
|
|
26
|
+
self._initialized = True
|
|
27
|
+
|
|
28
|
+
def __new__(cls, *args, **kwargs):
|
|
29
|
+
if cls._instance is None:
|
|
30
|
+
cls._instance = super().__new__(cls)
|
|
31
|
+
|
|
32
|
+
return cls._instance
|
|
33
|
+
|
|
34
|
+
def register_exec(self, unique_iq: str, cls: type):
|
|
35
|
+
"""
|
|
36
|
+
Register an executable class.
|
|
37
|
+
|
|
38
|
+
:param unique_iq: Unique ID.
|
|
39
|
+
:type unique_iq: str
|
|
40
|
+
:param cls: Class.
|
|
41
|
+
:type cls: type
|
|
42
|
+
:return:
|
|
43
|
+
:rtype:
|
|
44
|
+
"""
|
|
45
|
+
if unique_iq in self._exec_db:
|
|
46
|
+
logger.error(f"'{unique_iq}' has been registered.")
|
|
47
|
+
raise ExecRegisterError(f"'{unique_iq}' has been registered.")
|
|
48
|
+
|
|
49
|
+
self._exec_db[unique_iq] = cls
|
|
50
|
+
|
|
51
|
+
def is_registered(self, unique_id: str) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
Check if an executable class has been registered.
|
|
54
|
+
|
|
55
|
+
:param unique_id: Unique ID.
|
|
56
|
+
:type unique_id: str
|
|
57
|
+
:return: True or False.
|
|
58
|
+
:rtype: bool
|
|
59
|
+
"""
|
|
60
|
+
if unique_id in self._exec_db:
|
|
61
|
+
return True
|
|
62
|
+
else:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def get_cls(self, unique_id: str) -> type:
|
|
66
|
+
"""
|
|
67
|
+
Get a registered executable class.
|
|
68
|
+
|
|
69
|
+
:param unique_id: Unique ID.
|
|
70
|
+
:type unique_id: str
|
|
71
|
+
:return: Executable class.
|
|
72
|
+
:rtype: type
|
|
73
|
+
"""
|
|
74
|
+
if unique_id not in self._exec_db:
|
|
75
|
+
logger.error(f"Executable class '{unique_id}' not found.")
|
|
76
|
+
raise GetExecClassError(f"Executable class '{unique_id}' not found.")
|
|
77
|
+
|
|
78
|
+
return self._exec_db[unique_id]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
WRFRUNExecDB = WRFRunExecutableRegisterCenter()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def replay_config_generator(replay_config_file: str):
|
|
85
|
+
"""
|
|
86
|
+
Replay the simulations.
|
|
87
|
+
|
|
88
|
+
:param replay_config_file: Replay file path.
|
|
89
|
+
:type replay_config_file: str
|
|
90
|
+
:return:
|
|
91
|
+
:rtype:
|
|
92
|
+
"""
|
|
93
|
+
logger.info(f"Loading replay resources from: {replay_config_file}")
|
|
94
|
+
work_path = WRFRUNConfig.parse_resource_uri(WRFRUNConfig.WRFRUN_REPLAY_WORK_PATH)
|
|
95
|
+
|
|
96
|
+
unpack_archive(replay_config_file, work_path, "zip")
|
|
97
|
+
|
|
98
|
+
if not exists(f"{work_path}/config.json"):
|
|
99
|
+
logger.error("Can't find replay config in the provided config file.")
|
|
100
|
+
raise FileNotFoundError("Can't find replay config in the provided config file.")
|
|
101
|
+
|
|
102
|
+
with open(f"{work_path}/config.json", "r") as f:
|
|
103
|
+
replay_config_list: list[ExecutableConfig] = loads(f.read())
|
|
104
|
+
|
|
105
|
+
for _config in replay_config_list:
|
|
106
|
+
args = _config["class_config"]["class_args"]
|
|
107
|
+
kwargs = _config["class_config"]["class_kwargs"]
|
|
108
|
+
executable: ExecutableBase = WRFRUNExecDB.get_cls(_config["name"])(*args, **kwargs)
|
|
109
|
+
executable.load_config(_config)
|
|
110
|
+
yield _config["name"], executable
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
__all__ = ["WRFRUNExecDB", "replay_config_generator"]
|
wrfrun/core/server.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import socketserver
|
|
3
|
+
import subprocess
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from time import time
|
|
6
|
+
from typing import Tuple, Optional
|
|
7
|
+
|
|
8
|
+
from .config import WRFRUNConfig
|
|
9
|
+
from ..utils import logger
|
|
10
|
+
|
|
11
|
+
WRFRUN_SERVER_INSTANCE = None
|
|
12
|
+
WRFRUN_SERVER_THREAD = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_wrf_simulated_seconds(start_datetime: datetime, log_file_path: Optional[str] = None) -> int:
|
|
16
|
+
"""
|
|
17
|
+
Get how many seconds wrf has integrated.
|
|
18
|
+
|
|
19
|
+
:param start_datetime: WRF start datetime.
|
|
20
|
+
:type start_datetime: datetime
|
|
21
|
+
:param log_file_path: Absolute path of the log file to be parsed.
|
|
22
|
+
:type log_file_path: str
|
|
23
|
+
:return: Seconds.
|
|
24
|
+
:rtype: int
|
|
25
|
+
"""
|
|
26
|
+
# use linux cmd to get the latest line of wrf log files
|
|
27
|
+
if log_file_path is None:
|
|
28
|
+
log_file_path = WRFRUNConfig.parse_resource_uri(f"{WRFRUNConfig.WRF_WORK_PATH}/rsl.out.0000")
|
|
29
|
+
res = subprocess.run(["tail", "-n", "1", log_file_path], capture_output=True)
|
|
30
|
+
log_text = res.stdout.decode()
|
|
31
|
+
|
|
32
|
+
if not (log_text.startswith("d01") or log_text.startswith("d02")):
|
|
33
|
+
return -1
|
|
34
|
+
|
|
35
|
+
time_string = log_text.split()[1]
|
|
36
|
+
|
|
37
|
+
seconds = -1
|
|
38
|
+
try:
|
|
39
|
+
current_datetime = datetime.strptime(time_string, "%Y-%m-%d_%H:%M:%S")
|
|
40
|
+
# remove timezone info so we can calculate.
|
|
41
|
+
date_delta = current_datetime - start_datetime.replace(tzinfo=None)
|
|
42
|
+
seconds = date_delta.days * 24 * 60 * 60 + date_delta.seconds
|
|
43
|
+
|
|
44
|
+
except ValueError:
|
|
45
|
+
seconds = -1
|
|
46
|
+
|
|
47
|
+
finally:
|
|
48
|
+
return seconds
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class WRFRunServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
|
52
|
+
"""
|
|
53
|
+
A socket server to report time usage.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, start_date: datetime, wrf_simulate_seconds: int, *args, **kwargs) -> None:
|
|
57
|
+
super().__init__(*args, **kwargs)
|
|
58
|
+
|
|
59
|
+
# record the time the server starts
|
|
60
|
+
self.start_timestamp = datetime.fromtimestamp(time())
|
|
61
|
+
|
|
62
|
+
# record when the wrf start to integral
|
|
63
|
+
self.start_date = start_date
|
|
64
|
+
|
|
65
|
+
# record how many seconds the wrf will integral
|
|
66
|
+
self.wrf_simulate_seconds = wrf_simulate_seconds
|
|
67
|
+
|
|
68
|
+
# we need to parse the log file to track the simulation progress.
|
|
69
|
+
self.wrf_log_path = WRFRUNConfig.parse_resource_uri(f"{WRFRUNConfig.WRF_WORK_PATH}/rsl.out.0000")
|
|
70
|
+
logger.debug("WRFRun Server will try to track simulation progress with following log files:")
|
|
71
|
+
logger.debug(f"WRF: {self.wrf_log_path}")
|
|
72
|
+
|
|
73
|
+
def server_bind(self):
|
|
74
|
+
"""
|
|
75
|
+
Bind address and port.
|
|
76
|
+
"""
|
|
77
|
+
# reuse address and port to prevent the error `Address already in use`
|
|
78
|
+
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
79
|
+
self.socket.bind(self.server_address)
|
|
80
|
+
|
|
81
|
+
def get_start_time(self) -> datetime:
|
|
82
|
+
"""
|
|
83
|
+
Get the start time of the socket.
|
|
84
|
+
As the socket server should start with NWP, it is also regarded as the start time of NWP.
|
|
85
|
+
|
|
86
|
+
:return: Start datetime.
|
|
87
|
+
:rtype: datetime
|
|
88
|
+
"""
|
|
89
|
+
return self.start_timestamp
|
|
90
|
+
|
|
91
|
+
def get_wrf_simulate_settings(self) -> Tuple[datetime, int]:
|
|
92
|
+
"""
|
|
93
|
+
Get the start date of the case the NWP simulates and the total seconds of the simulation.
|
|
94
|
+
|
|
95
|
+
:return: (start date, simulation seconds)
|
|
96
|
+
:rtype: tuple
|
|
97
|
+
"""
|
|
98
|
+
return self.start_date, self.wrf_simulate_seconds
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class WRFRunServerHandler(socketserver.StreamRequestHandler):
|
|
102
|
+
"""
|
|
103
|
+
Socket server handler.
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
def __init__(self, request, client_address, server: WRFRunServer) -> None:
|
|
107
|
+
super().__init__(request, client_address, server)
|
|
108
|
+
|
|
109
|
+
# get server
|
|
110
|
+
self.server: WRFRunServer = server
|
|
111
|
+
|
|
112
|
+
def calculate_time_usage(self) -> str:
|
|
113
|
+
"""Calculate time usage from server start (usually the time to run wrfrun)
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
str: Time usage in `"%H:%M:%S"`
|
|
117
|
+
"""
|
|
118
|
+
# get current timestamp
|
|
119
|
+
current_timestamp = datetime.fromtimestamp(time())
|
|
120
|
+
|
|
121
|
+
# get delta second
|
|
122
|
+
seconds_diff = current_timestamp - self.server.get_start_time()
|
|
123
|
+
seconds_diff = seconds_diff.seconds
|
|
124
|
+
|
|
125
|
+
# calculate hours, minutes and seconds
|
|
126
|
+
seconds = seconds_diff % 60
|
|
127
|
+
minutes = (seconds_diff % 3600) // 60
|
|
128
|
+
hours = seconds_diff // 3600
|
|
129
|
+
|
|
130
|
+
time_usage = ":".join([
|
|
131
|
+
str(hours).rjust(2, '0'),
|
|
132
|
+
str(minutes).rjust(2, '0'),
|
|
133
|
+
str(seconds).rjust(2, '0')
|
|
134
|
+
])
|
|
135
|
+
|
|
136
|
+
return time_usage
|
|
137
|
+
|
|
138
|
+
def calculate_progress(self) -> str:
|
|
139
|
+
"""
|
|
140
|
+
Calculate the simulation progress.
|
|
141
|
+
|
|
142
|
+
:return:
|
|
143
|
+
:rtype:
|
|
144
|
+
"""
|
|
145
|
+
start_date, simulate_seconds = self.server.get_wrf_simulate_settings()
|
|
146
|
+
|
|
147
|
+
simulated_seconds = get_wrf_simulated_seconds(start_date, self.server.wrf_log_path)
|
|
148
|
+
|
|
149
|
+
if simulated_seconds > 0:
|
|
150
|
+
progress = simulated_seconds * 100 // simulate_seconds
|
|
151
|
+
|
|
152
|
+
else:
|
|
153
|
+
progress = 0
|
|
154
|
+
|
|
155
|
+
status = WRFRUNConfig.WRFRUN_WORK_STATUS
|
|
156
|
+
|
|
157
|
+
if status == "":
|
|
158
|
+
status = "*"
|
|
159
|
+
|
|
160
|
+
return f"{status}: {progress}%"
|
|
161
|
+
|
|
162
|
+
def handle(self) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Request handler.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
# check if we will to stop server
|
|
168
|
+
msg = self.rfile.readline().decode().split('\n')[0]
|
|
169
|
+
|
|
170
|
+
if msg == "stop":
|
|
171
|
+
self.server.shutdown()
|
|
172
|
+
self.wfile.write(f"Server stop\n".encode())
|
|
173
|
+
|
|
174
|
+
elif msg == "debug":
|
|
175
|
+
start_date, simulate_seconds = self.server.get_wrf_simulate_settings()
|
|
176
|
+
start_date = start_date.strftime("%Y-%m-%d %H:%M")
|
|
177
|
+
|
|
178
|
+
self.wfile.write(f"{start_date}\n{simulate_seconds}\n".encode())
|
|
179
|
+
|
|
180
|
+
else:
|
|
181
|
+
progress = self.calculate_progress()
|
|
182
|
+
time_usage = self.calculate_time_usage()
|
|
183
|
+
|
|
184
|
+
# send the message
|
|
185
|
+
self.wfile.write(f"{progress}\n{time_usage}".encode())
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def stop_server(socket_ip: str, socket_port: int):
|
|
189
|
+
"""Try to stop server.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
socket_ip (str): Server IP.
|
|
193
|
+
socket_port (int): Server Port.
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
197
|
+
|
|
198
|
+
sock.connect((socket_ip, socket_port))
|
|
199
|
+
|
|
200
|
+
# send msg to server
|
|
201
|
+
sock.sendall("stop\n".encode())
|
|
202
|
+
|
|
203
|
+
# receive the message
|
|
204
|
+
msg = sock.recv(1024).decode().split('\n')[0]
|
|
205
|
+
|
|
206
|
+
logger.info(f"WRFRunServer: {msg}")
|
|
207
|
+
|
|
208
|
+
except (ConnectionRefusedError, ConnectionResetError):
|
|
209
|
+
logger.warning("Fail to stop WRFRunServer, maybe it doesn't start at all.")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
__all__ = ["WRFRunServer", "WRFRunServerHandler", "get_wrf_simulated_seconds", "stop_server"]
|