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.
- uxarray_mcp/__init__.py +7 -0
- uxarray_mcp/__main__.py +16 -0
- uxarray_mcp/cli.py +356 -0
- uxarray_mcp/domain/__init__.py +27 -0
- uxarray_mcp/domain/area.py +32 -0
- uxarray_mcp/domain/mesh.py +26 -0
- uxarray_mcp/domain/plotting.py +499 -0
- uxarray_mcp/domain/variable.py +77 -0
- uxarray_mcp/domain/vector_calc.py +256 -0
- uxarray_mcp/domain/zonal.py +66 -0
- uxarray_mcp/provenance.py +79 -0
- uxarray_mcp/py.typed +0 -0
- uxarray_mcp/remote/__init__.py +6 -0
- uxarray_mcp/remote/agent.py +493 -0
- uxarray_mcp/remote/compute_functions.py +1151 -0
- uxarray_mcp/remote/config.py +322 -0
- uxarray_mcp/remote/health.py +372 -0
- uxarray_mcp/server.py +230 -0
- uxarray_mcp/state.py +521 -0
- uxarray_mcp/tools/__init__.py +115 -0
- uxarray_mcp/tools/advanced.py +1110 -0
- uxarray_mcp/tools/capabilities.py +669 -0
- uxarray_mcp/tools/catalog.py +369 -0
- uxarray_mcp/tools/execution_control.py +763 -0
- uxarray_mcp/tools/inspection.py +557 -0
- uxarray_mcp/tools/orchestration.py +327 -0
- uxarray_mcp/tools/plotting.py +854 -0
- uxarray_mcp/tools/remote_tools.py +702 -0
- uxarray_mcp/tools/scientific_agent.py +367 -0
- uxarray_mcp/tools/stateful.py +402 -0
- uxarray_mcp/tools/vector_calc.py +432 -0
- uxarray_mcp-0.1.0.dist-info/METADATA +468 -0
- uxarray_mcp-0.1.0.dist-info/RECORD +35 -0
- uxarray_mcp-0.1.0.dist-info/WHEEL +4 -0
- uxarray_mcp-0.1.0.dist-info/entry_points.txt +3 -0
uxarray_mcp/__init__.py
ADDED
uxarray_mcp/__main__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Run the UXarray MCP CLI as a module (``python -m uxarray_mcp``)."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from uxarray_mcp.cli import main as cli_main
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
"""Default to ``serve`` when invoked without a subcommand for back-compat."""
|
|
10
|
+
if len(sys.argv) == 1:
|
|
11
|
+
sys.argv.append("serve")
|
|
12
|
+
raise SystemExit(cli_main())
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
main()
|
uxarray_mcp/cli.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Command-line interface for the UXarray MCP server.
|
|
2
|
+
|
|
3
|
+
Subcommands
|
|
4
|
+
-----------
|
|
5
|
+
- ``serve`` — run the MCP server (stdio transport)
|
|
6
|
+
- ``setup`` — write a minimal user config to ``~/.config/uxarray-mcp/config.yaml``
|
|
7
|
+
- ``doctor`` — validate local Globus auth, endpoint health, and optional remote probes
|
|
8
|
+
- ``endpoints`` — manage named Globus Compute endpoints in the user config
|
|
9
|
+
- ``install-claude`` — print or write the Claude Desktop ``mcpServers`` block
|
|
10
|
+
|
|
11
|
+
The CLI is registered via the ``uxarray-mcp`` entry point in pyproject.toml.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import yaml
|
|
25
|
+
|
|
26
|
+
from uxarray_mcp.remote.config import (
|
|
27
|
+
USER_CONFIG_PATH,
|
|
28
|
+
discover_config_path,
|
|
29
|
+
discover_config_search_paths,
|
|
30
|
+
load_config,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _read_user_config(path: Path) -> dict[str, Any]:
|
|
35
|
+
if not path.exists():
|
|
36
|
+
return {}
|
|
37
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
38
|
+
data = yaml.safe_load(fh) or {}
|
|
39
|
+
if not isinstance(data, dict):
|
|
40
|
+
return {}
|
|
41
|
+
return data
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _write_user_config(path: Path, data: dict[str, Any]) -> None:
|
|
45
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
47
|
+
yaml.safe_dump(data, fh, sort_keys=False)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _ensure_hpc_block(data: dict[str, Any]) -> dict[str, Any]:
|
|
51
|
+
hpc = data.setdefault("hpc", {})
|
|
52
|
+
if not isinstance(hpc, dict):
|
|
53
|
+
hpc = {}
|
|
54
|
+
data["hpc"] = hpc
|
|
55
|
+
endpoints = hpc.setdefault("endpoints", {})
|
|
56
|
+
if not isinstance(endpoints, dict):
|
|
57
|
+
hpc["endpoints"] = {}
|
|
58
|
+
return hpc
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# serve
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cmd_serve(args: argparse.Namespace) -> int:
|
|
67
|
+
"""Run the MCP server on stdio."""
|
|
68
|
+
from uxarray_mcp.server import mcp
|
|
69
|
+
|
|
70
|
+
mcp.run()
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# setup
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def cmd_setup(args: argparse.Namespace) -> int:
|
|
80
|
+
"""Write a starter config to the user config path."""
|
|
81
|
+
target = Path(args.path).expanduser() if args.path else USER_CONFIG_PATH
|
|
82
|
+
if target.exists() and not args.force:
|
|
83
|
+
print(
|
|
84
|
+
f"Config already exists at {target}. Pass --force to overwrite.",
|
|
85
|
+
file=sys.stderr,
|
|
86
|
+
)
|
|
87
|
+
return 2
|
|
88
|
+
|
|
89
|
+
starter: dict[str, Any] = {
|
|
90
|
+
"hpc": {
|
|
91
|
+
"execution_mode": args.execution_mode,
|
|
92
|
+
"timeout_seconds": 300,
|
|
93
|
+
"default_endpoint": None,
|
|
94
|
+
"endpoints": {},
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
_write_user_config(target, starter)
|
|
98
|
+
print(f"Wrote starter config to {target}")
|
|
99
|
+
print("Add an endpoint with: uxarray-mcp endpoints add <name> <uuid>")
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# endpoints
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _user_write_target() -> Path:
|
|
109
|
+
"""Path that mutating commands (`endpoints add/remove`) should write to.
|
|
110
|
+
|
|
111
|
+
Honors `$UXARRAY_MCP_CONFIG` so add/remove stay consistent with the
|
|
112
|
+
config the rest of the server reads. Falls back to ``USER_CONFIG_PATH``.
|
|
113
|
+
"""
|
|
114
|
+
env_path = os.environ.get("UXARRAY_MCP_CONFIG")
|
|
115
|
+
if env_path:
|
|
116
|
+
return Path(env_path).expanduser()
|
|
117
|
+
return USER_CONFIG_PATH
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def cmd_endpoints_list(args: argparse.Namespace) -> int:
|
|
121
|
+
cfg = load_config()
|
|
122
|
+
loaded = discover_config_path()
|
|
123
|
+
if not cfg.endpoints and not cfg.endpoint_id:
|
|
124
|
+
if loaded is None:
|
|
125
|
+
print("No endpoints configured. No config file was found. Searched:")
|
|
126
|
+
for p in discover_config_search_paths():
|
|
127
|
+
print(f" - {p}")
|
|
128
|
+
print("\nWrite a starter config with: uxarray-mcp setup")
|
|
129
|
+
else:
|
|
130
|
+
print(f"No endpoints configured in {loaded}.")
|
|
131
|
+
print("\nAdd one with: uxarray-mcp endpoints add <name> <uuid>")
|
|
132
|
+
return 0
|
|
133
|
+
payload: dict[str, Any] = {
|
|
134
|
+
"config_path": str(discover_config_path() or ""),
|
|
135
|
+
"default_endpoint": cfg.default_endpoint,
|
|
136
|
+
"execution_mode": cfg.execution_mode,
|
|
137
|
+
"endpoints": {
|
|
138
|
+
name: {
|
|
139
|
+
"endpoint_id": p.endpoint_id,
|
|
140
|
+
"path_prefixes": list(p.path_prefixes),
|
|
141
|
+
"timeout_seconds": p.timeout_seconds,
|
|
142
|
+
}
|
|
143
|
+
for name, p in cfg.endpoints.items()
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
if cfg.endpoint_id and not cfg.endpoints:
|
|
147
|
+
payload["legacy_endpoint_id"] = cfg.endpoint_id
|
|
148
|
+
print(json.dumps(payload, indent=2))
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def cmd_endpoints_add(args: argparse.Namespace) -> int:
|
|
153
|
+
target = _user_write_target()
|
|
154
|
+
data = _read_user_config(target)
|
|
155
|
+
hpc = _ensure_hpc_block(data)
|
|
156
|
+
endpoints = hpc["endpoints"]
|
|
157
|
+
profile: dict[str, Any] = {"endpoint_id": args.uuid}
|
|
158
|
+
if args.path_prefix:
|
|
159
|
+
profile["path_prefixes"] = list(args.path_prefix)
|
|
160
|
+
if args.timeout_seconds is not None:
|
|
161
|
+
profile["timeout_seconds"] = args.timeout_seconds
|
|
162
|
+
endpoints[args.name] = profile
|
|
163
|
+
if args.set_default or not hpc.get("default_endpoint"):
|
|
164
|
+
hpc["default_endpoint"] = args.name
|
|
165
|
+
if hpc.get("execution_mode") in (None, "local"):
|
|
166
|
+
hpc["execution_mode"] = "auto"
|
|
167
|
+
_write_user_config(target, data)
|
|
168
|
+
print(f"Added endpoint {args.name!r} → {args.uuid} in {target}")
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def cmd_endpoints_remove(args: argparse.Namespace) -> int:
|
|
173
|
+
target = _user_write_target()
|
|
174
|
+
if not target.exists():
|
|
175
|
+
print(f"No user config at {target}", file=sys.stderr)
|
|
176
|
+
return 2
|
|
177
|
+
data = _read_user_config(target)
|
|
178
|
+
hpc = _ensure_hpc_block(data)
|
|
179
|
+
endpoints = hpc["endpoints"]
|
|
180
|
+
if args.name not in endpoints:
|
|
181
|
+
print(f"Endpoint {args.name!r} is not configured.", file=sys.stderr)
|
|
182
|
+
return 2
|
|
183
|
+
del endpoints[args.name]
|
|
184
|
+
if hpc.get("default_endpoint") == args.name:
|
|
185
|
+
hpc["default_endpoint"] = next(iter(endpoints), None)
|
|
186
|
+
_write_user_config(target, data)
|
|
187
|
+
print(f"Removed endpoint {args.name!r} from {target}")
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# doctor
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def cmd_doctor(args: argparse.Namespace) -> int:
|
|
197
|
+
from uxarray_mcp.tools.execution_control import (
|
|
198
|
+
probe_path_access,
|
|
199
|
+
validate_hpc_setup,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
sample_path = args.sample_path[0] if args.sample_path else None
|
|
203
|
+
report = validate_hpc_setup(
|
|
204
|
+
run_remote_probe=not args.skip_remote_probe,
|
|
205
|
+
probe_timeout_seconds=args.timeout_seconds,
|
|
206
|
+
sample_path=sample_path,
|
|
207
|
+
endpoint=args.endpoint,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if len(args.sample_path) > 1:
|
|
211
|
+
report["additional_path_probes"] = [
|
|
212
|
+
probe_path_access(
|
|
213
|
+
path,
|
|
214
|
+
use_remote=True,
|
|
215
|
+
inspect_netcdf=not args.no_netcdf,
|
|
216
|
+
endpoint=args.endpoint,
|
|
217
|
+
)
|
|
218
|
+
for path in args.sample_path[1:]
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
print(json.dumps(report, indent=2, sort_keys=True))
|
|
222
|
+
return 0 if report.get("passed") else 1
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
# install-claude
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _build_claude_block(name: str) -> dict[str, Any]:
|
|
231
|
+
bin_path = shutil.which("uxarray-mcp") or "uxarray-mcp"
|
|
232
|
+
return {
|
|
233
|
+
"mcpServers": {
|
|
234
|
+
name: {
|
|
235
|
+
"command": bin_path,
|
|
236
|
+
"args": ["serve"],
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def cmd_install_claude(args: argparse.Namespace) -> int:
|
|
243
|
+
block = _build_claude_block(args.name)
|
|
244
|
+
if args.print_only:
|
|
245
|
+
print(json.dumps(block, indent=2))
|
|
246
|
+
return 0
|
|
247
|
+
|
|
248
|
+
target = Path(args.config_path).expanduser() if args.config_path else None
|
|
249
|
+
if target is None:
|
|
250
|
+
print(
|
|
251
|
+
"Pass --config-path to merge into a Claude Desktop config, "
|
|
252
|
+
"or --print-only to dump the block to stdout.",
|
|
253
|
+
file=sys.stderr,
|
|
254
|
+
)
|
|
255
|
+
print(json.dumps(block, indent=2))
|
|
256
|
+
return 0
|
|
257
|
+
|
|
258
|
+
existing: dict[str, Any] = {}
|
|
259
|
+
if target.exists():
|
|
260
|
+
with open(target, "r", encoding="utf-8") as fh:
|
|
261
|
+
existing = json.load(fh) or {}
|
|
262
|
+
servers = existing.setdefault("mcpServers", {})
|
|
263
|
+
servers[args.name] = block["mcpServers"][args.name]
|
|
264
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
265
|
+
with open(target, "w", encoding="utf-8") as fh:
|
|
266
|
+
json.dump(existing, fh, indent=2)
|
|
267
|
+
print(f"Wrote Claude config to {target}")
|
|
268
|
+
return 0
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
# parser
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
277
|
+
p = argparse.ArgumentParser(
|
|
278
|
+
prog="uxarray-mcp",
|
|
279
|
+
description="UXarray MCP server CLI: serve, configure, and diagnose.",
|
|
280
|
+
)
|
|
281
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
282
|
+
|
|
283
|
+
serve = sub.add_parser("serve", help="Run the MCP server on stdio.")
|
|
284
|
+
serve.set_defaults(func=cmd_serve)
|
|
285
|
+
|
|
286
|
+
setup = sub.add_parser("setup", help="Write a starter user config.")
|
|
287
|
+
setup.add_argument("--path", default=None, help="Override target path.")
|
|
288
|
+
setup.add_argument(
|
|
289
|
+
"--execution-mode",
|
|
290
|
+
default="auto",
|
|
291
|
+
choices=["local", "auto", "hpc"],
|
|
292
|
+
help="Default execution mode (default: auto).",
|
|
293
|
+
)
|
|
294
|
+
setup.add_argument("--force", action="store_true", help="Overwrite existing file.")
|
|
295
|
+
setup.set_defaults(func=cmd_setup)
|
|
296
|
+
|
|
297
|
+
endpoints = sub.add_parser("endpoints", help="Manage Globus Compute endpoints.")
|
|
298
|
+
ep_sub = endpoints.add_subparsers(dest="endpoints_command", required=True)
|
|
299
|
+
|
|
300
|
+
ep_list = ep_sub.add_parser("list", help="Show configured endpoints.")
|
|
301
|
+
ep_list.set_defaults(func=cmd_endpoints_list)
|
|
302
|
+
|
|
303
|
+
ep_add = ep_sub.add_parser("add", help="Add or update an endpoint.")
|
|
304
|
+
ep_add.add_argument("name")
|
|
305
|
+
ep_add.add_argument("uuid")
|
|
306
|
+
ep_add.add_argument(
|
|
307
|
+
"--path-prefix",
|
|
308
|
+
action="append",
|
|
309
|
+
default=[],
|
|
310
|
+
help="Filesystem prefix this endpoint owns. Repeatable.",
|
|
311
|
+
)
|
|
312
|
+
ep_add.add_argument("--timeout-seconds", type=int, default=None)
|
|
313
|
+
ep_add.add_argument(
|
|
314
|
+
"--set-default", action="store_true", help="Mark this endpoint as default."
|
|
315
|
+
)
|
|
316
|
+
ep_add.set_defaults(func=cmd_endpoints_add)
|
|
317
|
+
|
|
318
|
+
ep_remove = ep_sub.add_parser("remove", help="Remove a named endpoint.")
|
|
319
|
+
ep_remove.add_argument("name")
|
|
320
|
+
ep_remove.set_defaults(func=cmd_endpoints_remove)
|
|
321
|
+
|
|
322
|
+
doctor = sub.add_parser("doctor", help="Validate HPC readiness.")
|
|
323
|
+
doctor.add_argument("--sample-path", action="append", default=[])
|
|
324
|
+
doctor.add_argument("--timeout-seconds", type=int, default=180)
|
|
325
|
+
doctor.add_argument("--endpoint", default=None)
|
|
326
|
+
doctor.add_argument("--skip-remote-probe", action="store_true")
|
|
327
|
+
doctor.add_argument("--no-netcdf", action="store_true")
|
|
328
|
+
doctor.set_defaults(func=cmd_doctor)
|
|
329
|
+
|
|
330
|
+
claude = sub.add_parser(
|
|
331
|
+
"install-claude", help="Print or merge a Claude Desktop mcpServers block."
|
|
332
|
+
)
|
|
333
|
+
claude.add_argument("--name", default="uxarray", help="MCP server name.")
|
|
334
|
+
claude.add_argument(
|
|
335
|
+
"--config-path",
|
|
336
|
+
default=None,
|
|
337
|
+
help="Claude config to merge into (e.g. ~/Library/Application Support/Claude/claude_desktop_config.json).",
|
|
338
|
+
)
|
|
339
|
+
claude.add_argument(
|
|
340
|
+
"--print-only",
|
|
341
|
+
action="store_true",
|
|
342
|
+
help="Print the JSON block without writing anywhere.",
|
|
343
|
+
)
|
|
344
|
+
claude.set_defaults(func=cmd_install_claude)
|
|
345
|
+
|
|
346
|
+
return p
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def main(argv: list[str] | None = None) -> int:
|
|
350
|
+
parser = build_parser()
|
|
351
|
+
args = parser.parse_args(argv)
|
|
352
|
+
return int(args.func(args))
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
if __name__ == "__main__":
|
|
356
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Shared scientific computation layer for UXarray MCP Server.
|
|
2
|
+
|
|
3
|
+
Functions here contain the pure domain logic used by both local tools
|
|
4
|
+
(inspection.py) and remote HPC functions (compute_functions.py).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .area import compute_area_stats
|
|
8
|
+
from .mesh import load_grid
|
|
9
|
+
from .variable import compute_variable_info
|
|
10
|
+
from .vector_calc import (
|
|
11
|
+
compute_azimuthal_mean,
|
|
12
|
+
compute_curl,
|
|
13
|
+
compute_divergence,
|
|
14
|
+
compute_gradient,
|
|
15
|
+
)
|
|
16
|
+
from .zonal import compute_zonal_mean_stats
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"load_grid",
|
|
20
|
+
"compute_area_stats",
|
|
21
|
+
"compute_variable_info",
|
|
22
|
+
"compute_zonal_mean_stats",
|
|
23
|
+
"compute_gradient",
|
|
24
|
+
"compute_curl",
|
|
25
|
+
"compute_divergence",
|
|
26
|
+
"compute_azimuthal_mean",
|
|
27
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Shared face area computation logic."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def compute_area_stats(grid: Any) -> dict:
|
|
7
|
+
"""Compute face area statistics for a loaded grid.
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
grid : ux.Grid
|
|
12
|
+
Loaded UXarray grid.
|
|
13
|
+
|
|
14
|
+
Returns
|
|
15
|
+
-------
|
|
16
|
+
dict
|
|
17
|
+
Keys: total_area, mean_area, min_area, max_area, area_units, n_face
|
|
18
|
+
"""
|
|
19
|
+
face_areas = grid.face_areas
|
|
20
|
+
|
|
21
|
+
area_units = "m^2"
|
|
22
|
+
if hasattr(face_areas, "attrs") and "units" in face_areas.attrs:
|
|
23
|
+
area_units = face_areas.attrs["units"]
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
"total_area": float(face_areas.sum()),
|
|
27
|
+
"mean_area": float(face_areas.mean()),
|
|
28
|
+
"min_area": float(face_areas.min()),
|
|
29
|
+
"max_area": float(face_areas.max()),
|
|
30
|
+
"area_units": area_units,
|
|
31
|
+
"n_face": int(grid.n_face),
|
|
32
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Shared grid loading with HEALPix support."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def load_grid(file_path: str) -> Any:
|
|
7
|
+
"""Load a UXarray Grid from a file path or HEALPix spec.
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
file_path : str
|
|
12
|
+
Path to mesh file, or "healpix:<zoom>" for virtual HEALPix meshes.
|
|
13
|
+
|
|
14
|
+
Returns
|
|
15
|
+
-------
|
|
16
|
+
ux.Grid
|
|
17
|
+
Loaded grid object.
|
|
18
|
+
"""
|
|
19
|
+
import uxarray as ux
|
|
20
|
+
|
|
21
|
+
if file_path.lower().startswith("healpix"):
|
|
22
|
+
parts = file_path.split(":")
|
|
23
|
+
zoom = int(parts[1]) if len(parts) > 1 else 1
|
|
24
|
+
return ux.Grid.from_healpix(zoom=zoom)
|
|
25
|
+
|
|
26
|
+
return ux.open_grid(file_path)
|