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/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"]