uxarray-mcp 0.1.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.
@@ -0,0 +1,1151 @@
1
+ """Globus Compute functions for remote execution.
2
+
3
+ These functions are serialized and sent to HPC endpoints via AllCodeStrategies.
4
+ They must be FULLY SELF-CONTAINED — only import packages available on the HPC
5
+ environment (uxarray, numpy, etc.). Never import from uxarray_mcp here.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, Optional
11
+
12
+
13
+ def remote_runtime_probe() -> Dict[str, Any]:
14
+ """Return lightweight runtime diagnostics from the remote worker."""
15
+ import getpass
16
+ import importlib.util
17
+ import os
18
+ import platform
19
+ import shutil
20
+ import socket
21
+ import sys
22
+
23
+ modules: Dict[str, Any] = {}
24
+ for name in ("uxarray", "xarray", "numpy"):
25
+ spec = importlib.util.find_spec(name)
26
+ info: Dict[str, Any] = {"available": spec is not None}
27
+ if spec is not None:
28
+ try:
29
+ module = __import__(name)
30
+ info["version"] = getattr(module, "__version__", None)
31
+ info["file"] = getattr(module, "__file__", None)
32
+ except Exception as exc:
33
+ info["import_error"] = f"{type(exc).__name__}: {exc}"
34
+ modules[name] = info
35
+
36
+ yac_info: Dict[str, Any] = {}
37
+ try:
38
+ yac_spec = importlib.util.find_spec("yac")
39
+ yac_info["package_available"] = yac_spec is not None
40
+ if yac_spec is not None:
41
+ yac_info["package_origin"] = yac_spec.origin
42
+ except Exception as exc:
43
+ yac_info["package_available"] = False
44
+ yac_info["package_error"] = f"{type(exc).__name__}: {exc}"
45
+
46
+ try:
47
+ yac_core = importlib.import_module("yac.core")
48
+ yac_info["core_importable"] = True
49
+ yac_info["core_file"] = getattr(yac_core, "__file__", None)
50
+ yac_info["version"] = getattr(yac_core, "__version__", None)
51
+ except Exception as exc:
52
+ yac_info["core_importable"] = False
53
+ yac_info["core_import_error"] = f"{type(exc).__name__}: {exc}"
54
+
55
+ try:
56
+ from uxarray.remap.yac import _import_yac
57
+
58
+ yc = _import_yac()
59
+ yac_info["uxarray_helper_ok"] = True
60
+ yac_info["uxarray_helper_module"] = getattr(yc, "__name__", None)
61
+ yac_info["uxarray_helper_file"] = getattr(yc, "__file__", None)
62
+ yac_info["has_basicgrid"] = hasattr(yc, "BasicGrid")
63
+ except Exception as exc:
64
+ yac_info["uxarray_helper_ok"] = False
65
+ yac_info["uxarray_helper_error"] = f"{type(exc).__name__}: {exc}"
66
+ modules["yac"] = yac_info
67
+
68
+ return {
69
+ "hostname": socket.gethostname(),
70
+ "user": getpass.getuser(),
71
+ "cwd": os.getcwd(),
72
+ "python_executable": sys.executable,
73
+ "python_version": platform.python_version(),
74
+ "qsub_path": shutil.which("qsub"),
75
+ "path_head": os.environ.get("PATH", "").split(":")[:5],
76
+ "modules": modules,
77
+ }
78
+
79
+
80
+ def remote_probe_path(file_path: str, inspect_netcdf: bool = True) -> Dict[str, Any]:
81
+ """Return remote path accessibility details.
82
+
83
+ This is intentionally simpler than UXarray inspection. The goal is to prove
84
+ that a remote worker can reach and read the exact target path before
85
+ debugging mesh parsing or scheduler fan-out.
86
+ """
87
+ import os
88
+ import socket
89
+ from pathlib import Path
90
+
91
+ path = Path(file_path)
92
+ exists = path.exists()
93
+ is_file = path.is_file()
94
+ readable = os.access(path, os.R_OK) if exists else False
95
+
96
+ result: Dict[str, Any] = {
97
+ "path": str(path),
98
+ "hostname": socket.gethostname(),
99
+ "exists": exists,
100
+ "is_file": is_file,
101
+ "readable": readable,
102
+ }
103
+
104
+ if exists:
105
+ stat = path.stat()
106
+ result["size_bytes"] = int(stat.st_size)
107
+ result["mtime_epoch"] = float(stat.st_mtime)
108
+
109
+ try:
110
+ with path.open("rb") as handle:
111
+ result["header_hex"] = handle.read(8).hex()
112
+ except Exception as exc:
113
+ result["header_error"] = f"{type(exc).__name__}: {exc}"
114
+
115
+ if inspect_netcdf and exists and readable and is_file:
116
+ try:
117
+ import xarray as xr
118
+
119
+ with xr.open_dataset(file_path, decode_cf=False) as ds:
120
+ result["netcdf"] = {
121
+ "opened": True,
122
+ "dims": {name: int(size) for name, size in ds.sizes.items()},
123
+ "data_vars": list(ds.data_vars)[:20],
124
+ "coords": list(ds.coords)[:20],
125
+ "attrs_keys": sorted(list(ds.attrs.keys()))[:20],
126
+ }
127
+ except Exception as exc:
128
+ result["netcdf"] = {
129
+ "opened": False,
130
+ "error_type": type(exc).__name__,
131
+ "error": str(exc),
132
+ }
133
+
134
+ return result
135
+
136
+
137
+ def remote_inspect_mesh(file_path: str) -> Dict[str, Any]:
138
+ """Inspect mesh topology on remote HPC node.
139
+
140
+ Parameters
141
+ ----------
142
+ file_path : str
143
+ Path to mesh file on HPC filesystem
144
+
145
+ Returns
146
+ -------
147
+ dict
148
+ Mesh topology including n_face, n_node, n_edge, source
149
+
150
+ Notes
151
+ -----
152
+ This function executes on the HPC endpoint, not locally.
153
+ All imports must be within function scope for serialization.
154
+ """
155
+ import uxarray as ux
156
+
157
+ if file_path.startswith("healpix:"):
158
+ zoom = int(file_path.split(":")[1])
159
+ grid = ux.Grid.from_healpix(zoom)
160
+ else:
161
+ grid = ux.open_grid(file_path)
162
+
163
+ return {
164
+ "n_face": int(grid.n_face),
165
+ "n_node": int(grid.n_node),
166
+ "n_edge": int(grid.n_edge),
167
+ "source": file_path,
168
+ }
169
+
170
+
171
+ def remote_calculate_area(file_path: str) -> Dict[str, Any]:
172
+ """Calculate face areas on remote HPC node.
173
+
174
+ Parameters
175
+ ----------
176
+ file_path : str
177
+ Path to mesh file on HPC filesystem
178
+
179
+ Returns
180
+ -------
181
+ dict
182
+ Area statistics including total_area, mean_area, min_area, max_area
183
+
184
+ Notes
185
+ -----
186
+ This function executes on the HPC endpoint, not locally.
187
+ All imports must be within function scope for serialization.
188
+ """
189
+ import numpy as np
190
+ import uxarray as ux
191
+
192
+ if file_path.startswith("healpix:"):
193
+ zoom = int(file_path.split(":")[1])
194
+ grid = ux.Grid.from_healpix(zoom)
195
+ else:
196
+ grid = ux.open_grid(file_path)
197
+
198
+ areas = grid.face_areas
199
+ units = getattr(areas, "units", "m^2") if hasattr(areas, "units") else "m^2"
200
+ values = areas.values if hasattr(areas, "values") else np.asarray(areas)
201
+
202
+ return {
203
+ "total_area": float(np.sum(values)),
204
+ "mean_area": float(np.mean(values)),
205
+ "min_area": float(np.min(values)),
206
+ "max_area": float(np.max(values)),
207
+ "area_units": str(units),
208
+ "n_face": int(grid.n_face),
209
+ }
210
+
211
+
212
+ def remote_inspect_variable(
213
+ grid_path: str, data_path: str, variable_name: Optional[str] = None
214
+ ) -> Dict[str, Any]:
215
+ """Inspect data variables on remote HPC node.
216
+
217
+ Parameters
218
+ ----------
219
+ grid_path : str
220
+ Path to grid file on HPC filesystem
221
+ data_path : str
222
+ Path to data file on HPC filesystem
223
+ variable_name : str | None
224
+ Specific variable to inspect, or None for all variables
225
+
226
+ Returns
227
+ -------
228
+ dict
229
+ Variable metadata including dimensions, shapes, statistics
230
+
231
+ Notes
232
+ -----
233
+ This function executes on the HPC endpoint, not locally.
234
+ """
235
+ import numpy as np
236
+ import uxarray as ux
237
+
238
+ uxds = ux.open_dataset(grid_path, data_path)
239
+
240
+ face_dims = {"n_face", "nCells"}
241
+ node_dims = {"n_node", "nVertices"}
242
+ edge_dims = {"n_edge", "nEdges"}
243
+
244
+ var_names = [variable_name] if variable_name else list(uxds.keys())
245
+
246
+ if variable_name and variable_name not in uxds:
247
+ raise ValueError(f"Variable '{variable_name}' not found in dataset")
248
+
249
+ variables = []
250
+
251
+ for name in var_names:
252
+ if name not in uxds:
253
+ continue
254
+ var = uxds[name]
255
+ dims = var.dims
256
+
257
+ location = "other"
258
+ if any(d in face_dims for d in dims):
259
+ location = "faces"
260
+ elif any(d in node_dims for d in dims):
261
+ location = "nodes"
262
+ elif any(d in edge_dims for d in dims):
263
+ location = "edges"
264
+
265
+ stats = None
266
+ try:
267
+ values = var.values
268
+ finite = values[np.isfinite(values)]
269
+ if len(finite) > 0:
270
+ stats = {
271
+ "min": float(np.min(finite)),
272
+ "max": float(np.max(finite)),
273
+ "mean": float(np.mean(finite)),
274
+ }
275
+ except Exception:
276
+ pass
277
+
278
+ variables.append(
279
+ {
280
+ "name": name,
281
+ "dims": list(dims),
282
+ "shape": list(var.shape),
283
+ "dtype": str(var.dtype),
284
+ "location": location,
285
+ "attrs": dict(var.attrs),
286
+ "statistics": stats,
287
+ }
288
+ )
289
+
290
+ return {
291
+ "variables": variables,
292
+ "grid_info": {
293
+ "n_face": int(uxds.uxgrid.n_face),
294
+ "n_node": int(uxds.uxgrid.n_node),
295
+ "n_edge": int(uxds.uxgrid.n_edge),
296
+ },
297
+ }
298
+
299
+
300
+ def remote_plot_mesh(
301
+ grid_path: str,
302
+ width: int = 800,
303
+ height: int = 400,
304
+ ) -> Dict[str, Any]:
305
+ """Render a mesh wireframe on the remote HPC node and return base64 PNG.
306
+
307
+ Parameters
308
+ ----------
309
+ grid_path : str
310
+ Path to mesh file on HPC filesystem, or "healpix:<zoom>".
311
+ width : int
312
+ Image width in pixels.
313
+ height : int
314
+ Image height in pixels.
315
+
316
+ Returns
317
+ -------
318
+ dict
319
+ - png_b64: base64-encoded PNG string
320
+ - image_size_bytes: size of the PNG
321
+ - grid_info: n_face, n_node, n_edge
322
+ """
323
+ import base64
324
+ import io
325
+
326
+ import matplotlib
327
+
328
+ matplotlib.use("Agg")
329
+ import matplotlib.pyplot as plt
330
+ import uxarray as ux
331
+
332
+ if grid_path.startswith("healpix:"):
333
+ zoom = int(grid_path.split(":")[1])
334
+ grid = ux.Grid.from_healpix(zoom)
335
+ else:
336
+ grid = ux.open_grid(grid_path)
337
+
338
+ import holoviews as hv
339
+
340
+ hv.extension("matplotlib")
341
+
342
+ dpi = 100
343
+ element = grid.plot.mesh(backend="matplotlib")
344
+ renderer = hv.Store.renderers["matplotlib"]
345
+ plot = renderer.get_plot(element)
346
+ fig = plot.state
347
+ fig.set_size_inches(width / dpi, height / dpi)
348
+ fig.set_dpi(dpi)
349
+ fig.tight_layout()
350
+
351
+ buf = io.BytesIO()
352
+ fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight")
353
+ plt.close(fig)
354
+ buf.seek(0)
355
+ png_bytes = buf.read()
356
+ if not png_bytes:
357
+ raise ValueError("Rendered mesh plot is empty.")
358
+
359
+ return {
360
+ "png_b64": base64.b64encode(png_bytes).decode("utf-8"),
361
+ "image_size_bytes": len(png_bytes),
362
+ "grid_info": {
363
+ "n_face": int(grid.n_face),
364
+ "n_node": int(grid.n_node),
365
+ "n_edge": int(grid.n_edge),
366
+ },
367
+ }
368
+
369
+
370
+ def remote_plot_variable(
371
+ grid_path: str,
372
+ data_path: str,
373
+ variable_name: Optional[str] = None,
374
+ width: int = 800,
375
+ height: int = 400,
376
+ cmap: str = "viridis",
377
+ vmin: Optional[float] = None,
378
+ vmax: Optional[float] = None,
379
+ title: Optional[str] = None,
380
+ time_index: int = 0,
381
+ ) -> Dict[str, Any]:
382
+ """Render a face-centered variable plot on the remote HPC node and return base64 PNG.
383
+
384
+ Parameters
385
+ ----------
386
+ grid_path : str
387
+ Path to mesh grid file on HPC filesystem.
388
+ data_path : str
389
+ Path to data file on HPC filesystem.
390
+ variable_name : str | None
391
+ Variable to plot. If None, first face-centered variable is used.
392
+ width : int
393
+ Image width in pixels.
394
+ height : int
395
+ Image height in pixels.
396
+ cmap : str
397
+ Matplotlib colormap name.
398
+ vmin : float | None
399
+ Colormap minimum.
400
+ vmax : float | None
401
+ Colormap maximum.
402
+ title : str | None
403
+ Plot title.
404
+
405
+ Returns
406
+ -------
407
+ dict
408
+ - png_b64: base64-encoded PNG string
409
+ - image_size_bytes: size of the PNG
410
+ - variable_name: plotted variable name
411
+ - grid_info: n_face, n_node, n_edge
412
+ """
413
+ import base64
414
+ import io
415
+
416
+ import matplotlib
417
+
418
+ matplotlib.use("Agg")
419
+ import matplotlib.pyplot as plt
420
+ import uxarray as ux
421
+
422
+ uxds = ux.open_dataset(grid_path, data_path)
423
+
424
+ face_dims = {"n_face", "nCells"}
425
+
426
+ if variable_name is None:
427
+ for var in uxds.data_vars:
428
+ if any(d in face_dims for d in uxds[var].dims):
429
+ variable_name = var
430
+ break
431
+ if variable_name is None:
432
+ raise ValueError(
433
+ f"No face-centered variable found. Available: {list(uxds.data_vars.keys())}"
434
+ )
435
+
436
+ if variable_name not in uxds.data_vars:
437
+ raise ValueError(
438
+ f"Variable '{variable_name}' not found. Available: {list(uxds.data_vars.keys())}"
439
+ )
440
+
441
+ uxda = uxds[variable_name]
442
+ if not any(d in face_dims for d in uxda.dims):
443
+ raise ValueError(f"Variable '{variable_name}' is not face-centered.")
444
+
445
+ extra_dims = [d for d in uxda.dims if d not in face_dims]
446
+ if extra_dims:
447
+ uxda = uxda.isel(
448
+ **{d: 0 if uxda.sizes[d] == 1 else time_index for d in extra_dims}
449
+ )
450
+
451
+ import holoviews as hv
452
+
453
+ hv.extension("matplotlib")
454
+
455
+ dpi = 100
456
+ kwargs: Dict[str, Any] = {"backend": "matplotlib", "cmap": cmap}
457
+ if vmin is not None:
458
+ import numpy as np
459
+
460
+ kwargs["clim"] = (
461
+ vmin,
462
+ vmax if vmax is not None else float(np.nanmax(uxda.values)),
463
+ )
464
+ elif vmax is not None:
465
+ import numpy as np
466
+
467
+ kwargs["clim"] = (float(np.nanmin(uxda.values)), vmax)
468
+
469
+ element = uxda.plot.polygons(**kwargs)
470
+ renderer = hv.Store.renderers["matplotlib"]
471
+ plot = renderer.get_plot(element)
472
+ fig = plot.state
473
+ fig.set_size_inches(width / dpi, height / dpi)
474
+ fig.set_dpi(dpi)
475
+ if title is not None:
476
+ fig.axes[0].set_title(title)
477
+ fig.tight_layout()
478
+
479
+ buf = io.BytesIO()
480
+ fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight")
481
+ plt.close(fig)
482
+ buf.seek(0)
483
+ png_bytes = buf.read()
484
+ if not png_bytes:
485
+ raise ValueError("Rendered variable plot is empty.")
486
+
487
+ return {
488
+ "png_b64": base64.b64encode(png_bytes).decode("utf-8"),
489
+ "image_size_bytes": len(png_bytes),
490
+ "variable_name": variable_name,
491
+ "grid_info": {
492
+ "n_face": int(uxds.uxgrid.n_face),
493
+ "n_node": int(uxds.uxgrid.n_node),
494
+ "n_edge": int(uxds.uxgrid.n_edge),
495
+ },
496
+ }
497
+
498
+
499
+ def remote_plot_zonal_mean(
500
+ grid_path: str,
501
+ data_path: str,
502
+ variable_name: str,
503
+ width: int = 800,
504
+ height: int = 400,
505
+ lat_spec: Optional[tuple | float | list] = None,
506
+ conservative: bool = False,
507
+ line_color: str = "#1f77b4",
508
+ title: Optional[str] = None,
509
+ ) -> Dict[str, Any]:
510
+ """Render a zonal mean profile on the remote HPC node and return base64 PNG.
511
+
512
+ Parameters
513
+ ----------
514
+ grid_path : str
515
+ Path to mesh grid file on HPC filesystem.
516
+ data_path : str
517
+ Path to data file on HPC filesystem.
518
+ variable_name : str
519
+ Variable to compute zonal mean for (must be face-centered).
520
+ width : int
521
+ Image width in pixels.
522
+ height : int
523
+ Image height in pixels.
524
+ lat_spec : tuple | float | list | None
525
+ Latitude specification. None uses default 10-degree bands.
526
+ conservative : bool
527
+ Use area-weighted averaging.
528
+ line_color : str
529
+ Matplotlib color string for the profile line.
530
+ title : str | None
531
+ Plot title.
532
+
533
+ Returns
534
+ -------
535
+ dict
536
+ - png_b64: base64-encoded PNG string
537
+ - image_size_bytes: size of the PNG
538
+ - variable_name: plotted variable name
539
+ - latitudes: list of latitude values
540
+ - zonal_mean_values: list of zonal mean values
541
+ """
542
+ import base64
543
+ import io
544
+
545
+ import matplotlib
546
+
547
+ matplotlib.use("Agg")
548
+ import matplotlib.pyplot as plt
549
+ import uxarray as ux
550
+
551
+ uxds = ux.open_dataset(grid_path, data_path)
552
+
553
+ if variable_name not in uxds:
554
+ raise ValueError(f"Variable '{variable_name}' not found")
555
+
556
+ var = uxds[variable_name]
557
+ face_dims = {"n_face", "nCells"}
558
+ if not any(d in face_dims for d in var.dims):
559
+ raise ValueError(f"Variable '{variable_name}' is not face-centered")
560
+
561
+ if lat_spec is None:
562
+ lat_spec = (-90, 90, 10)
563
+
564
+ if conservative:
565
+ result = var.zonal_mean(lat=lat_spec, conservative=True)
566
+ else:
567
+ result = var.zonal_mean(lat=lat_spec)
568
+
569
+ latitudes = result.coords[result.dims[0]].values.tolist()
570
+ values = result.values.tolist()
571
+
572
+ dpi = 100
573
+ fig, ax = plt.subplots(figsize=(width / dpi, height / dpi), dpi=dpi)
574
+ ax.plot(latitudes, values, linewidth=1.5, color=line_color)
575
+ ax.set_xlabel("Latitude (°)")
576
+ ax.set_ylabel(variable_name)
577
+ ax.set_title(title if title is not None else f"Zonal Mean — {variable_name}")
578
+ ax.grid(True, alpha=0.3)
579
+ fig.tight_layout()
580
+
581
+ buf = io.BytesIO()
582
+ fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight")
583
+ plt.close(fig)
584
+ buf.seek(0)
585
+ png_bytes = buf.read()
586
+ if not png_bytes:
587
+ raise ValueError("Rendered zonal mean plot is empty.")
588
+
589
+ return {
590
+ "png_b64": base64.b64encode(png_bytes).decode("utf-8"),
591
+ "image_size_bytes": len(png_bytes),
592
+ "variable_name": variable_name,
593
+ "latitudes": latitudes,
594
+ "zonal_mean_values": values,
595
+ }
596
+
597
+
598
+ def remote_calculate_zonal_mean(
599
+ grid_path: str,
600
+ data_path: str,
601
+ variable_name: str,
602
+ lat_spec: Optional[tuple | float | list] = None,
603
+ conservative: bool = False,
604
+ ) -> Dict[str, Any]:
605
+ """Calculate zonal mean on remote HPC node.
606
+
607
+ Parameters
608
+ ----------
609
+ grid_path : str
610
+ Path to grid file on HPC filesystem
611
+ data_path : str
612
+ Path to data file on HPC filesystem
613
+ variable_name : str
614
+ Variable to compute zonal mean for
615
+ lat_spec : tuple | float | list | None
616
+ Latitude specification
617
+ conservative : bool
618
+ Whether to use conservative averaging
619
+
620
+ Returns
621
+ -------
622
+ dict
623
+ Zonal mean results including latitudes and values
624
+
625
+ Notes
626
+ -----
627
+ This function executes on the HPC endpoint, not locally.
628
+ """
629
+ import uxarray as ux
630
+
631
+ uxds = ux.open_dataset(grid_path, data_path)
632
+
633
+ if variable_name not in uxds:
634
+ raise ValueError(f"Variable '{variable_name}' not found")
635
+
636
+ var = uxds[variable_name]
637
+ face_dims = {"n_face", "nCells"}
638
+ if not any(d in face_dims for d in var.dims):
639
+ raise ValueError(f"Variable '{variable_name}' is not face-centered")
640
+
641
+ if lat_spec is None:
642
+ lat_spec = (-90, 90, 10)
643
+
644
+ if conservative:
645
+ result = var.zonal_mean(lat=lat_spec, conservative=True)
646
+ else:
647
+ result = var.zonal_mean(lat=lat_spec)
648
+
649
+ return {
650
+ "variable_name": variable_name,
651
+ "latitudes": result.coords[result.dims[0]].values.tolist(),
652
+ "zonal_mean_values": result.values.tolist(),
653
+ "conservative": conservative,
654
+ "grid_info": {
655
+ "n_face": int(uxds.uxgrid.n_face),
656
+ "n_node": int(uxds.uxgrid.n_node),
657
+ "n_edge": int(uxds.uxgrid.n_edge),
658
+ },
659
+ }
660
+
661
+
662
+ def remote_subset_bbox_plot(
663
+ grid_path: str,
664
+ lon_bounds: list,
665
+ lat_bounds: list,
666
+ region_name: str = "",
667
+ width: int = 800,
668
+ height: int = 450,
669
+ edgecolor: str = "steelblue",
670
+ facecolor: str = "lightcyan",
671
+ linewidth: float = 0.3,
672
+ ) -> Dict[str, Any]:
673
+ """Subset a mesh by bounding box and return stats + a wireframe PNG.
674
+
675
+ Runs entirely on the HPC worker so multi-GB files never leave the
676
+ facility filesystem. All rendering parameters are echoed back in the
677
+ return dict so the caller has a complete provenance record: to reproduce
678
+ or modify the plot, change any field and resubmit.
679
+
680
+ Parameters
681
+ ----------
682
+ grid_path : str
683
+ Path to mesh file on HPC filesystem.
684
+ lon_bounds : list
685
+ [lon_min, lon_max] in degrees.
686
+ lat_bounds : list
687
+ [lat_min, lat_max] in degrees.
688
+ region_name : str
689
+ Human-readable label for the plot title.
690
+ width, height : int
691
+ PNG dimensions in pixels.
692
+ edgecolor : str
693
+ Matplotlib color for cell edges.
694
+ facecolor : str
695
+ Matplotlib color for cell fill.
696
+ linewidth : float
697
+ Edge line width in points.
698
+
699
+ Returns
700
+ -------
701
+ dict
702
+ - region_name, lon_bounds, lat_bounds: inputs echoed for provenance
703
+ - plot_params: all rendering parameters (edgecolor, facecolor,
704
+ linewidth, width, height, dpi) — change any field and resubmit
705
+ to modify the plot without re-running the analysis
706
+ - n_face_total, n_face_subset, fraction_of_mesh: coverage statistics
707
+ - mean_area_full_sr, mean_area_subset_sr: face-area statistics (sr)
708
+ - resolution_ratio: mean_area_full / mean_area_subset (>1 = finer)
709
+ - uxarray_version: library version on the HPC worker
710
+ - png_b64: base64 PNG of the subset wireframe
711
+ - image_size_bytes: PNG size in bytes
712
+ """
713
+ import base64
714
+ import importlib.metadata
715
+ import io
716
+ import math
717
+
718
+ import matplotlib
719
+
720
+ matplotlib.use("Agg")
721
+ import matplotlib.pyplot as plt
722
+ import uxarray as ux
723
+
724
+ grid = ux.open_grid(grid_path)
725
+ n_face_total = int(grid.n_face)
726
+
727
+ # full-mesh mean area
728
+ full_areas = grid.face_areas
729
+ mean_area_full = float(full_areas.values.mean())
730
+
731
+ # subset by bounding box
732
+ subset = grid.subset.bounding_box(
733
+ lon_bounds=lon_bounds,
734
+ lat_bounds=lat_bounds,
735
+ )
736
+ n_face_subset = int(subset.n_face)
737
+
738
+ subset_areas = subset.face_areas
739
+ mean_area_subset = (
740
+ float(subset_areas.values.mean()) if n_face_subset > 0 else float("nan")
741
+ )
742
+
743
+ resolution_ratio = (
744
+ mean_area_full / mean_area_subset
745
+ if mean_area_subset and not math.isnan(mean_area_subset)
746
+ else None
747
+ )
748
+
749
+ # plot subset: draw face-edge polygons using node coordinates
750
+ dpi = 100
751
+ fig, ax = plt.subplots(figsize=(width / dpi, height / dpi), dpi=dpi)
752
+ title = region_name if region_name else f"lon{lon_bounds} lat{lat_bounds}"
753
+ subtitle = (
754
+ f"{n_face_subset:,} faces | res_ratio={resolution_ratio:.2f}x"
755
+ if resolution_ratio
756
+ else f"{n_face_subset:,} faces"
757
+ )
758
+
759
+ from matplotlib.collections import PolyCollection
760
+
761
+ # Build polygon vertices from face-node connectivity
762
+ try:
763
+ face_lon = subset.node_lon.values # (n_node,)
764
+ face_lat = subset.node_lat.values # (n_node,)
765
+ conn = subset.face_node_connectivity.values # (n_face, max_nodes)
766
+ fill_val = getattr(subset.face_node_connectivity, "_FillValue", -1)
767
+ polys = []
768
+ for row in conn:
769
+ idx = row[row != fill_val]
770
+ if len(idx) >= 3:
771
+ polys.append(list(zip(face_lon[idx], face_lat[idx])))
772
+ if polys:
773
+ col = PolyCollection(
774
+ polys, edgecolors=edgecolor, facecolors=facecolor, linewidths=linewidth
775
+ )
776
+ ax.add_collection(col)
777
+ ax.set_xlim(lon_bounds)
778
+ ax.set_ylim(lat_bounds)
779
+ else:
780
+ raise ValueError("no valid polygons")
781
+ except Exception:
782
+ # fallback: scatter face centres
783
+ lons = subset.face_lon.values
784
+ lats = subset.face_lat.values
785
+ ax.scatter(lons, lats, s=2, color=edgecolor, alpha=0.6)
786
+ ax.set_xlim(lon_bounds)
787
+ ax.set_ylim(lat_bounds)
788
+
789
+ ax.set_title(f"{title}\n{subtitle}", fontsize=10)
790
+ ax.set_xlabel("Longitude")
791
+ ax.set_ylabel("Latitude")
792
+ ax.set_aspect("equal", adjustable="box")
793
+ fig.tight_layout()
794
+
795
+ buf = io.BytesIO()
796
+ fig.savefig(buf, format="png", dpi=dpi)
797
+ plt.close(fig)
798
+ buf.seek(0)
799
+ png_bytes = buf.read()
800
+
801
+ try:
802
+ ux_version = importlib.metadata.version("uxarray")
803
+ except Exception:
804
+ ux_version = "unknown"
805
+
806
+ return {
807
+ "region_name": region_name,
808
+ "lon_bounds": lon_bounds,
809
+ "lat_bounds": lat_bounds,
810
+ "plot_params": {
811
+ "edgecolor": edgecolor,
812
+ "facecolor": facecolor,
813
+ "linewidth": linewidth,
814
+ "width_px": width,
815
+ "height_px": height,
816
+ "dpi": dpi,
817
+ },
818
+ "n_face_total": n_face_total,
819
+ "n_face_subset": n_face_subset,
820
+ "fraction_of_mesh": n_face_subset / n_face_total if n_face_total else None,
821
+ "mean_area_full_sr": mean_area_full,
822
+ "mean_area_subset_sr": mean_area_subset,
823
+ "resolution_ratio": resolution_ratio,
824
+ "uxarray_version": ux_version,
825
+ "png_b64": base64.b64encode(png_bytes).decode(),
826
+ "image_size_bytes": len(png_bytes),
827
+ }
828
+
829
+
830
+ def remote_yac_remap_smoke() -> Dict[str, Any]:
831
+ """Smoke-test YAC's availability on the remote worker.
832
+
833
+ Loads YAC via uxarray's canonical helper (works around the broken upstream
834
+ ``yac/__init__.py`` that does ``from ._yac import *``), reports the YAC
835
+ surface, and runs a minimal nearest-neighbour remap from one HEALPix grid
836
+ to another using uxarray's YAC-backed remap path. Returns a dict that
837
+ includes both the static surface check and, if a remap was attempted, its
838
+ shape and timing.
839
+
840
+ The function is self-contained and serialised via AllCodeStrategies so the
841
+ Python 3.13/3.11 mismatch between local SDK and worker doesn't bite.
842
+ """
843
+ import importlib.metadata
844
+ import time
845
+ import traceback
846
+
847
+ out: Dict[str, Any] = {}
848
+
849
+ def _surface(mod):
850
+ return {
851
+ name: hasattr(mod, name)
852
+ for name in (
853
+ "BasicGrid",
854
+ "InterpField",
855
+ "InterpolationStack",
856
+ "compute_weights",
857
+ "Reg2dGrid",
858
+ )
859
+ }
860
+
861
+ yc = None
862
+ try:
863
+ from uxarray.remap.yac import _import_yac
864
+
865
+ yc = _import_yac()
866
+ out["yac_helper_ok"] = True
867
+ out["yac_loader"] = "uxarray.remap.yac._import_yac"
868
+ out["yac_module"] = getattr(yc, "__name__", None)
869
+ out["yac_file"] = getattr(yc, "__file__", None)
870
+ out["surface"] = _surface(yc)
871
+ except Exception as exc:
872
+ out["yac_helper_ok"] = False
873
+ out["yac_helper_error"] = f"{type(exc).__name__}: {exc}"
874
+
875
+ # Fallback: load core.so directly via ExtensionFileLoader, bypassing
876
+ # YAC's broken __init__.py. Lets us report surface even when the
877
+ # site uxarray is unavailable or itself broken.
878
+ try:
879
+ import importlib.machinery as _m
880
+ import importlib.util as _u
881
+ import os as _os
882
+ import sys as _sys
883
+ import sysconfig as _sc
884
+ import types as _t
885
+ from pathlib import Path as _P
886
+
887
+ search_roots = [_sc.get_paths()["purelib"]]
888
+ search_roots.extend(p for p in _sys.path if p)
889
+ for env_var in ("YAC_PREFIX", "YAC_ROOT"):
890
+ if _os.environ.get(env_var):
891
+ search_roots.append(_os.environ[env_var])
892
+ home = _os.path.expanduser("~")
893
+ search_roots.append(_os.path.join(home, "yac"))
894
+ search_roots.append("/opt/yac")
895
+
896
+ so_candidates: list[str] = []
897
+ for root in search_roots:
898
+ rp = _P(root)
899
+ if not rp.exists():
900
+ continue
901
+ so_candidates.extend(str(p) for p in rp.rglob("yac/core.cpython-*.so"))
902
+ so_candidates = list(dict.fromkeys(so_candidates)) # dedupe
903
+ if not so_candidates:
904
+ raise FileNotFoundError(f"No yac/core*.so found in: {search_roots[:6]}")
905
+ so = so_candidates[0]
906
+ out["yac_search_hits"] = so_candidates[:5]
907
+
908
+ pkg = _t.ModuleType("yacshim")
909
+ pkg.__path__ = []
910
+ _sys.modules["yacshim"] = pkg
911
+ loader = _m.ExtensionFileLoader("yacshim.core", so)
912
+ spec = _u.spec_from_loader("yacshim.core", loader)
913
+ assert spec is not None and spec.loader is not None
914
+ yc = _u.module_from_spec(spec)
915
+ spec.loader.exec_module(yc)
916
+ out["yac_loader"] = "direct_so"
917
+ out["yac_file"] = so
918
+ out["surface"] = _surface(yc)
919
+ except Exception as exc2:
920
+ out["yac_direct_load_ok"] = False
921
+ out["yac_direct_load_error"] = f"{type(exc2).__name__}: {exc2}"
922
+ out["yac_direct_load_traceback"] = traceback.format_exc()
923
+ return out
924
+
925
+ try:
926
+ out["uxarray_version"] = importlib.metadata.version("uxarray")
927
+ except Exception:
928
+ out["uxarray_version"] = "unknown"
929
+
930
+ try:
931
+ import numpy as np
932
+ import uxarray as ux
933
+ import xarray as xr
934
+
935
+ src = ux.Grid.from_healpix(zoom=2)
936
+ dst = ux.Grid.from_healpix(zoom=3)
937
+ out["src_n_face"] = int(src.n_face)
938
+ out["dst_n_face"] = int(dst.n_face)
939
+
940
+ rng = np.random.default_rng(0)
941
+ face_data = rng.standard_normal(int(src.n_face))
942
+ uxda = ux.UxDataArray(
943
+ xr.DataArray(face_data, dims=("n_face",), name="field"),
944
+ uxgrid=src,
945
+ )
946
+
947
+ t0 = time.perf_counter()
948
+ try:
949
+ remapped = uxda.remap.nearest_neighbor(
950
+ destination_grid=dst, remap_to="face centers"
951
+ )
952
+ out["remap_method"] = "nearest_neighbor"
953
+ out["remap_ok"] = True
954
+ out["remap_seconds"] = time.perf_counter() - t0
955
+ out["remap_dst_shape"] = list(remapped.shape)
956
+ out["remap_dst_mean"] = float(np.asarray(remapped).mean())
957
+ except Exception as exc:
958
+ out["remap_method"] = "nearest_neighbor"
959
+ out["remap_ok"] = False
960
+ out["remap_error"] = f"{type(exc).__name__}: {exc}"
961
+ out["remap_traceback"] = traceback.format_exc()
962
+ except Exception as exc:
963
+ out["remap_setup_ok"] = False
964
+ out["remap_setup_error"] = f"{type(exc).__name__}: {exc}"
965
+ out["remap_setup_traceback"] = traceback.format_exc()
966
+
967
+ return out
968
+
969
+
970
+ # ---------------------------------------------------------------------------
971
+ # Vector calculus remote functions
972
+ # Each function is fully self-contained — no uxarray_mcp imports.
973
+ # Serialized via AllCodeStrategies so the HPC worker only needs uxarray+numpy.
974
+ # ---------------------------------------------------------------------------
975
+
976
+
977
+ def remote_calculate_gradient(
978
+ grid_path: str, data_path: str, variable_name: str
979
+ ) -> Dict[str, Any]:
980
+ """Compute the spatial gradient of a face-centered scalar field on HPC."""
981
+ import numpy as np
982
+ import uxarray as ux
983
+
984
+ uxds = ux.open_dataset(grid_path, data_path)
985
+ if variable_name not in uxds.data_vars:
986
+ raise ValueError(
987
+ f"Variable '{variable_name}' not found. Available: {list(uxds.data_vars)}"
988
+ )
989
+ var = uxds[variable_name]
990
+ if "n_face" not in var.dims and "nCells" not in var.dims:
991
+ raise ValueError(
992
+ f"Variable '{variable_name}' is not face-centered. "
993
+ "Gradient requires face-centered data."
994
+ )
995
+
996
+ grad = var.gradient()
997
+ comp_names = list(grad.data_vars)
998
+
999
+ def _stats(arr: Any) -> Dict[str, Any]:
1000
+ vals = arr.values
1001
+ finite = vals[np.isfinite(vals)]
1002
+ if finite.size == 0:
1003
+ return {"min": None, "max": None, "mean": None}
1004
+ return {
1005
+ "min": float(finite.min()),
1006
+ "max": float(finite.max()),
1007
+ "mean": float(finite.mean()),
1008
+ }
1009
+
1010
+ return {
1011
+ "variable_name": variable_name,
1012
+ "components": comp_names,
1013
+ "component_stats": {name: _stats(grad[name]) for name in comp_names},
1014
+ "n_face": int(uxds.uxgrid.n_face),
1015
+ "interpretation": "zonal (d/dx) and meridional (d/dy) components of the gradient",
1016
+ }
1017
+
1018
+
1019
+ def remote_calculate_curl(
1020
+ grid_path: str, data_path: str, u_variable: str, v_variable: str
1021
+ ) -> Dict[str, Any]:
1022
+ """Compute relative vorticity (curl) of a 2-D wind field on HPC.
1023
+
1024
+ zeta = dv/dx - du/dy
1025
+ """
1026
+ import numpy as np
1027
+ import uxarray as ux
1028
+
1029
+ uxds = ux.open_dataset(grid_path, data_path)
1030
+ for name in (u_variable, v_variable):
1031
+ if name not in uxds.data_vars:
1032
+ raise ValueError(
1033
+ f"Variable '{name}' not found. Available: {list(uxds.data_vars)}"
1034
+ )
1035
+ u, v = uxds[u_variable], uxds[v_variable]
1036
+ for name, var in ((u_variable, u), (v_variable, v)):
1037
+ if "n_face" not in var.dims and "nCells" not in var.dims:
1038
+ raise ValueError(
1039
+ f"Variable '{name}' is not face-centered. "
1040
+ "Curl requires face-centered vector components."
1041
+ )
1042
+
1043
+ result = u.curl(v)
1044
+ vals = result.values
1045
+ finite = vals[np.isfinite(vals)]
1046
+ stats: Dict[str, Any] = (
1047
+ {
1048
+ "min": float(finite.min()),
1049
+ "max": float(finite.max()),
1050
+ "mean": float(finite.mean()),
1051
+ "std": float(finite.std()),
1052
+ }
1053
+ if finite.size > 0
1054
+ else {"min": None, "max": None, "mean": None, "std": None}
1055
+ )
1056
+ return {
1057
+ "u_variable": u_variable,
1058
+ "v_variable": v_variable,
1059
+ "interpretation": "relative vorticity zeta = dv/dx - du/dy",
1060
+ "n_face": int(uxds.uxgrid.n_face),
1061
+ "stats": stats,
1062
+ }
1063
+
1064
+
1065
+ def remote_calculate_divergence(
1066
+ grid_path: str, data_path: str, u_variable: str, v_variable: str
1067
+ ) -> Dict[str, Any]:
1068
+ """Compute horizontal divergence of a 2-D vector field on HPC.
1069
+
1070
+ divergence = du/dx + dv/dy
1071
+ """
1072
+ import numpy as np
1073
+ import uxarray as ux
1074
+
1075
+ uxds = ux.open_dataset(grid_path, data_path)
1076
+ for name in (u_variable, v_variable):
1077
+ if name not in uxds.data_vars:
1078
+ raise ValueError(
1079
+ f"Variable '{name}' not found. Available: {list(uxds.data_vars)}"
1080
+ )
1081
+ u, v = uxds[u_variable], uxds[v_variable]
1082
+ for name, var in ((u_variable, u), (v_variable, v)):
1083
+ if "n_face" not in var.dims and "nCells" not in var.dims:
1084
+ raise ValueError(
1085
+ f"Variable '{name}' is not face-centered. "
1086
+ "Divergence requires face-centered vector components."
1087
+ )
1088
+
1089
+ result = u.divergence(v)
1090
+ vals = result.values
1091
+ finite = vals[np.isfinite(vals)]
1092
+ stats: Dict[str, Any] = (
1093
+ {
1094
+ "min": float(finite.min()),
1095
+ "max": float(finite.max()),
1096
+ "mean": float(finite.mean()),
1097
+ "std": float(finite.std()),
1098
+ }
1099
+ if finite.size > 0
1100
+ else {"min": None, "max": None, "mean": None, "std": None}
1101
+ )
1102
+ return {
1103
+ "u_variable": u_variable,
1104
+ "v_variable": v_variable,
1105
+ "interpretation": "horizontal divergence du/dx + dv/dy",
1106
+ "n_face": int(uxds.uxgrid.n_face),
1107
+ "stats": stats,
1108
+ }
1109
+
1110
+
1111
+ def remote_calculate_azimuthal_mean(
1112
+ grid_path: str,
1113
+ data_path: str,
1114
+ variable_name: str,
1115
+ center_lon: float,
1116
+ center_lat: float,
1117
+ outer_radius: float,
1118
+ radius_step: float,
1119
+ ) -> Dict[str, Any]:
1120
+ """Compute the azimuthal (radial) mean around a centre point on HPC."""
1121
+ import uxarray as ux
1122
+
1123
+ uxds = ux.open_dataset(grid_path, data_path)
1124
+ if variable_name not in uxds.data_vars:
1125
+ raise ValueError(
1126
+ f"Variable '{variable_name}' not found. Available: {list(uxds.data_vars)}"
1127
+ )
1128
+ var = uxds[variable_name]
1129
+ if "n_face" not in var.dims and "nCells" not in var.dims:
1130
+ raise ValueError(
1131
+ f"Variable '{variable_name}' is not face-centered. "
1132
+ "Azimuthal mean requires face-centered data."
1133
+ )
1134
+
1135
+ result = var.azimuthal_mean(
1136
+ center_coord=(center_lon, center_lat),
1137
+ outer_radius=outer_radius,
1138
+ radius_step=radius_step,
1139
+ )
1140
+ radii = result.coords[result.dims[0]].values.tolist()
1141
+ values = result.values.tolist()
1142
+
1143
+ return {
1144
+ "variable_name": variable_name,
1145
+ "center": {"lon": center_lon, "lat": center_lat},
1146
+ "outer_radius_deg": outer_radius,
1147
+ "radius_step_deg": radius_step,
1148
+ "radii_deg": radii,
1149
+ "azimuthal_mean_values": values,
1150
+ "n_face": int(uxds.uxgrid.n_face),
1151
+ }