marEx 2.0.0__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.
- marEx/__init__.py +78 -0
- marEx/detect.py +490 -0
- marEx/helper.py +251 -0
- marEx/plotX/__init__.py +83 -0
- marEx/plotX/base.py +387 -0
- marEx/plotX/gridded.py +55 -0
- marEx/plotX/unstructured.py +147 -0
- marEx/track.py +3646 -0
- marex-2.0.0.dist-info/METADATA +247 -0
- marex-2.0.0.dist-info/RECORD +12 -0
- marex-2.0.0.dist-info/WHEEL +5 -0
- marex-2.0.0.dist-info/top_level.txt +1 -0
marEx/helper.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HPC Dask Helper: Utilities for High-Performance Computing with Dask
|
|
3
|
+
--------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
This module provides utilities for setting up and managing Dask clusters
|
|
6
|
+
in HPC environments, with specific support for the DKRZ Levante Supercomputer.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import psutil
|
|
13
|
+
from tempfile import TemporaryDirectory
|
|
14
|
+
from getpass import getuser
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, Optional, Union, Any
|
|
17
|
+
|
|
18
|
+
import dask
|
|
19
|
+
from dask_jobqueue import SLURMCluster
|
|
20
|
+
from dask.distributed import Client, LocalCluster
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Default configuration values
|
|
24
|
+
DEFAULT_DASK_CONFIG = {
|
|
25
|
+
'array.slicing.split_large_chunks': False,
|
|
26
|
+
'distributed.comm.timeouts.connect': '120s', # Increased from default
|
|
27
|
+
'distributed.comm.timeouts.tcp': '240s', # Double the connection timeout
|
|
28
|
+
'distributed.comm.retry.count': 10, # More retries before giving up
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# DKRZ-specific paths and configuration
|
|
32
|
+
DKRZ_SCRATCH_PATH = Path('/scratch') / getuser()[0] / getuser() / 'clients'
|
|
33
|
+
DKRZ_LOG_PATH = Path('/home/b') / getuser() / '.log_trash'
|
|
34
|
+
DKRZ_ACCOUNT = 'bk1377'
|
|
35
|
+
|
|
36
|
+
# Memory configuration for different node types
|
|
37
|
+
MEMORY_CONFIGS = {
|
|
38
|
+
256: {'client_memory': '250GB', 'constraint': '256', 'job_extra': ['--mem=0']},
|
|
39
|
+
512: {'client_memory': '500GB', 'constraint': '512', 'job_extra': ['--constraint=512G --mem=0']},
|
|
40
|
+
1024: {'client_memory': '1000GB', 'constraint': '1024', 'job_extra': ['--constraint=1024G --mem=0']}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def configure_dask(scratch_dir: Optional[Union[str, Path]] = None,
|
|
45
|
+
config: Optional[Dict[str, Any]] = None) -> TemporaryDirectory:
|
|
46
|
+
"""
|
|
47
|
+
Configure Dask with appropriate settings for HPC environments.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
scratch_dir : str or Path, optional
|
|
52
|
+
Directory to use for temporary files.
|
|
53
|
+
config : dict, optional
|
|
54
|
+
Additional Dask configuration settings to apply.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
TemporaryDirectory
|
|
59
|
+
Temporary directory object that should be kept alive while Dask is in use.
|
|
60
|
+
"""
|
|
61
|
+
# Use provided scratch directory or default to DKRZ scratch
|
|
62
|
+
scratch_path = Path(scratch_dir) if scratch_dir else DKRZ_SCRATCH_PATH
|
|
63
|
+
|
|
64
|
+
# Create temporary directory
|
|
65
|
+
if not scratch_path.exists():
|
|
66
|
+
scratch_path.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
|
|
68
|
+
temp_dir = TemporaryDirectory(dir=scratch_path)
|
|
69
|
+
|
|
70
|
+
# Apply default configuration
|
|
71
|
+
dask.config.set(temporary_directory=temp_dir.name)
|
|
72
|
+
print(f"Dask Scratch: {repr(temp_dir.name)}")
|
|
73
|
+
|
|
74
|
+
# Apply default settings
|
|
75
|
+
for key, value in DEFAULT_DASK_CONFIG.items():
|
|
76
|
+
dask.config.set({key: value})
|
|
77
|
+
|
|
78
|
+
# Apply any additional configuration
|
|
79
|
+
if config:
|
|
80
|
+
dask.config.set(config)
|
|
81
|
+
|
|
82
|
+
return temp_dir
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_cluster_info(client: Client) -> Dict[str, str]:
|
|
86
|
+
"""
|
|
87
|
+
Get and print cluster connection information.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
client : Client
|
|
92
|
+
Dask client connected to a cluster.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
dict
|
|
97
|
+
Dictionary containing connection information.
|
|
98
|
+
"""
|
|
99
|
+
# Get hostname and dashboard port
|
|
100
|
+
remote_node = subprocess.run(['hostname'], capture_output=True, text=True).stdout.strip().split('.')[0]
|
|
101
|
+
port = re.search(r':(\d+)/', client.dashboard_link).group(1)
|
|
102
|
+
|
|
103
|
+
# Print connection information
|
|
104
|
+
print(f"Hostname: {remote_node}")
|
|
105
|
+
print(f"Forward Port: {remote_node}:{port}")
|
|
106
|
+
print(f"Dashboard Link: localhost:{port}/status")
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
'hostname': remote_node,
|
|
110
|
+
'port': port,
|
|
111
|
+
'dashboard_link': f"localhost:{port}/status"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def start_local_cluster(n_workers: int = 4, threads_per_worker: int = 1,
|
|
116
|
+
scratch_dir: Optional[Union[str, Path]] = None,
|
|
117
|
+
**kwargs) -> Client:
|
|
118
|
+
"""
|
|
119
|
+
Start a local Dask cluster.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
n_workers : int, default=4
|
|
124
|
+
Number of worker processes to start.
|
|
125
|
+
threads_per_worker : int, default=1
|
|
126
|
+
Number of threads per worker.
|
|
127
|
+
scratch_dir : str or Path, optional
|
|
128
|
+
Directory to use for temporary files.
|
|
129
|
+
**kwargs
|
|
130
|
+
Additional keyword arguments to pass to LocalCluster.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
Client
|
|
135
|
+
Dask client connected to the local cluster.
|
|
136
|
+
"""
|
|
137
|
+
# Configure Dask
|
|
138
|
+
temp_dir = configure_dask(scratch_dir)
|
|
139
|
+
|
|
140
|
+
# Check system resources
|
|
141
|
+
physical_cores = psutil.cpu_count(logical=False)
|
|
142
|
+
logical_cores = psutil.cpu_count(logical=True)
|
|
143
|
+
memory = psutil.virtual_memory()
|
|
144
|
+
|
|
145
|
+
# Warn if requested resources exceed available
|
|
146
|
+
total_threads = n_workers * threads_per_worker
|
|
147
|
+
if total_threads > physical_cores:
|
|
148
|
+
print(f"Warning: Requested {n_workers} workers with {threads_per_worker} threads each, but only {physical_cores} physical cores available.")
|
|
149
|
+
print("Hyper-threading can reduce performance for compute-intensive tasks!")
|
|
150
|
+
elif total_threads > logical_cores:
|
|
151
|
+
print(f"Warning: Requested {n_workers} workers with {threads_per_worker} threads each, but only {logical_cores} logical cores available.")
|
|
152
|
+
print(f"Reducing to {logical_cores // threads_per_worker} workers.")
|
|
153
|
+
n_workers = logical_cores // threads_per_worker
|
|
154
|
+
|
|
155
|
+
print(f"Memory per Worker: {memory.total / n_workers / (1024**3):.2f} GB")
|
|
156
|
+
|
|
157
|
+
# Create cluster and client
|
|
158
|
+
cluster = LocalCluster(n_workers=n_workers, threads_per_worker=threads_per_worker, **kwargs)
|
|
159
|
+
client = Client(cluster)
|
|
160
|
+
|
|
161
|
+
# Store temporary directory (so it isn't garbage collected)
|
|
162
|
+
client._temp_dir = temp_dir
|
|
163
|
+
|
|
164
|
+
# Print connection information
|
|
165
|
+
get_cluster_info(client)
|
|
166
|
+
|
|
167
|
+
return client
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def start_distributed_cluster(n_workers: int, workers_per_node: int,
|
|
171
|
+
runtime: int = 9, node_memory: int = 256,
|
|
172
|
+
dashboard_address: int = 8889, queue: str = 'compute',
|
|
173
|
+
scratch_dir: Optional[Union[str, Path]] = None,
|
|
174
|
+
account: Optional[str] = None, **kwargs) -> Client:
|
|
175
|
+
"""
|
|
176
|
+
Start a distributed Dask cluster on a SLURM-based supercomputer.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
n_workers : int
|
|
181
|
+
Total number of workers to request.
|
|
182
|
+
workers_per_node : int
|
|
183
|
+
Number of workers per node.
|
|
184
|
+
runtime : int, default=9
|
|
185
|
+
Maximum runtime in minutes.
|
|
186
|
+
node_memory : int, default=256
|
|
187
|
+
Memory per node in GB (256, 512, or 1024).
|
|
188
|
+
dashboard_address : int, default=8889
|
|
189
|
+
Port for the Dask dashboard.
|
|
190
|
+
queue : str, default='compute'
|
|
191
|
+
SLURM queue to submit jobs to.
|
|
192
|
+
scratch_dir : str or Path, optional
|
|
193
|
+
Directory to use for temporary files.
|
|
194
|
+
account : str, optional
|
|
195
|
+
SLURM account to charge. Defaults to DKRZ_ACCOUNT.
|
|
196
|
+
**kwargs
|
|
197
|
+
Additional keyword arguments to pass to SLURMCluster.
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
Client
|
|
202
|
+
Dask client connected to the distributed cluster.
|
|
203
|
+
"""
|
|
204
|
+
# Configure Dask
|
|
205
|
+
temp_dir = configure_dask(scratch_dir)
|
|
206
|
+
|
|
207
|
+
# Use default account if none specified
|
|
208
|
+
if account is None:
|
|
209
|
+
account = DKRZ_ACCOUNT
|
|
210
|
+
|
|
211
|
+
# Validate node_memory
|
|
212
|
+
if node_memory not in MEMORY_CONFIGS:
|
|
213
|
+
raise ValueError(f"Unsupported node_memory value: {node_memory}. Must be one of {list(MEMORY_CONFIGS.keys())}.")
|
|
214
|
+
|
|
215
|
+
config = MEMORY_CONFIGS[node_memory]
|
|
216
|
+
|
|
217
|
+
# Calculate runtime in hours and minutes
|
|
218
|
+
runtime_hrs = runtime // 60
|
|
219
|
+
runtime_mins = runtime % 60
|
|
220
|
+
|
|
221
|
+
# Create SLURM cluster
|
|
222
|
+
cluster = SLURMCluster(
|
|
223
|
+
name='dask-cluster',
|
|
224
|
+
cores=workers_per_node,
|
|
225
|
+
memory=config['client_memory'],
|
|
226
|
+
processes=workers_per_node, # One process per core
|
|
227
|
+
interface='ib0',
|
|
228
|
+
queue=queue,
|
|
229
|
+
account=account,
|
|
230
|
+
walltime=f'{runtime_hrs:02d}:{runtime_mins:02d}:00',
|
|
231
|
+
asynchronous=0,
|
|
232
|
+
job_extra_directives=config['job_extra'],
|
|
233
|
+
log_directory=DKRZ_LOG_PATH,
|
|
234
|
+
local_directory=temp_dir.name,
|
|
235
|
+
scheduler_options={'dashboard_address': f':{dashboard_address}'},
|
|
236
|
+
**kwargs
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
print(f"Memory per Worker: {node_memory / workers_per_node:.2f} GB")
|
|
240
|
+
|
|
241
|
+
# Scale the cluster
|
|
242
|
+
cluster.scale(n_workers)
|
|
243
|
+
client = Client(cluster)
|
|
244
|
+
|
|
245
|
+
# Store temporary directory (so it isn't garbage collected)
|
|
246
|
+
client._temp_dir = temp_dir
|
|
247
|
+
|
|
248
|
+
# Print connection information
|
|
249
|
+
get_cluster_info(client)
|
|
250
|
+
|
|
251
|
+
return client
|
marEx/plotX/__init__.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from .base import PlotterBase, PlotConfig
|
|
2
|
+
from .gridded import GriddedPlotter
|
|
3
|
+
from .unstructured import UnstructuredPlotter, clear_cache
|
|
4
|
+
import xarray as xr
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
# Global variables to store grid information
|
|
8
|
+
_fpath_tgrid = None
|
|
9
|
+
_fpath_ckdtree = None
|
|
10
|
+
_grid_type = None
|
|
11
|
+
|
|
12
|
+
def _detect_grid_type(xarray_obj):
|
|
13
|
+
"""
|
|
14
|
+
Deduce grid type based on coordinate structure.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
str: 'gridded' or 'unstructured'
|
|
18
|
+
"""
|
|
19
|
+
has_lat_lon_coords = 'lat' in xarray_obj.coords and 'lon' in xarray_obj.coords
|
|
20
|
+
has_lat_lon_dims = 'lat' in xarray_obj.dims and 'lon' in xarray_obj.dims
|
|
21
|
+
|
|
22
|
+
# For unstructured data, lat/lon are coordinates but not dimensions
|
|
23
|
+
return 'unstructured' if (has_lat_lon_coords and not has_lat_lon_dims) else 'gridded'
|
|
24
|
+
|
|
25
|
+
def register_plotter(xarray_obj):
|
|
26
|
+
"""
|
|
27
|
+
Determine the appropriate plotter to use based on the data structure.
|
|
28
|
+
This function is called automatically by xarray's accessor system.
|
|
29
|
+
|
|
30
|
+
First checks if grid type was specified via specify_grid(),
|
|
31
|
+
then falls back to coordinate-based detection if needed.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Appropriate plotter instance for the data structure
|
|
35
|
+
"""
|
|
36
|
+
global _grid_type
|
|
37
|
+
|
|
38
|
+
# Determine grid type
|
|
39
|
+
detected_type = _detect_grid_type(xarray_obj)
|
|
40
|
+
|
|
41
|
+
# If grid type was explicitly specified, check for consistency
|
|
42
|
+
if _grid_type is not None:
|
|
43
|
+
if _grid_type != detected_type:
|
|
44
|
+
warnings.warn(
|
|
45
|
+
f"Specified grid type '{_grid_type}' differs from detected type '{detected_type}' "
|
|
46
|
+
f"based on coordinate structure. Using specified type '{_grid_type}'."
|
|
47
|
+
)
|
|
48
|
+
final_type = _grid_type
|
|
49
|
+
else:
|
|
50
|
+
final_type = detected_type
|
|
51
|
+
|
|
52
|
+
# Create appropriate plotter
|
|
53
|
+
plotter_class = UnstructuredPlotter if final_type.lower() == 'unstructured' else GriddedPlotter
|
|
54
|
+
plotter = plotter_class(xarray_obj)
|
|
55
|
+
|
|
56
|
+
# Set grid path if available for unstructured grids
|
|
57
|
+
if final_type == 'unstructured' and _fpath_tgrid is not None and _fpath_ckdtree is not None:
|
|
58
|
+
plotter.specify_grid(fpath_tgrid=_fpath_tgrid, fpath_ckdtree=_fpath_ckdtree)
|
|
59
|
+
|
|
60
|
+
return plotter
|
|
61
|
+
|
|
62
|
+
def specify_grid(grid_type=None, fpath_tgrid=None, fpath_ckdtree=None):
|
|
63
|
+
"""
|
|
64
|
+
Set the global grid specification that will be used by all plotters.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
grid_type: str, either 'gridded' or 'unstructured'.
|
|
68
|
+
If specified, this will be used as the primary method
|
|
69
|
+
to determine grid type.
|
|
70
|
+
fpath_tgrid: Path to the triangulation grid file
|
|
71
|
+
fpath_ckdtree: Path to the pre-computed KDTree indices directory
|
|
72
|
+
"""
|
|
73
|
+
global _fpath_tgrid, _fpath_ckdtree, _grid_type
|
|
74
|
+
|
|
75
|
+
if grid_type is not None and grid_type.lower() not in ['gridded', 'unstructured']:
|
|
76
|
+
raise ValueError("grid_type must be either 'gridded' or 'unstructured'")
|
|
77
|
+
|
|
78
|
+
_fpath_tgrid = str(fpath_tgrid) if fpath_tgrid else None
|
|
79
|
+
_fpath_ckdtree = str(fpath_ckdtree) if fpath_ckdtree else None
|
|
80
|
+
_grid_type = grid_type.lower() if grid_type else None
|
|
81
|
+
|
|
82
|
+
# Register the accessor
|
|
83
|
+
xr.register_dataarray_accessor('plotX')(register_plotter)
|