uxarray-mcp 0.2.0__tar.gz → 0.2.2__tar.gz
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-0.2.0 → uxarray_mcp-0.2.2}/PKG-INFO +3 -2
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/pyproject.toml +3 -2
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/pyproject.toml.orig +4 -2
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/__init__.py +1 -1
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/domain/vector_calc.py +9 -1
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/provenance.py +5 -1
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/remote/agent.py +29 -1
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/remote/compute_functions.py +107 -1
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/remote/config.py +18 -1
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/tools/catalog.py +14 -34
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/tools/execution_control.py +3 -2
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/tools/frontdoor.py +5 -2
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/tools/orchestration.py +23 -10
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/tools/remote_tools.py +28 -3
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/tools/stateful.py +44 -7
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/LICENSE +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/README.md +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/__main__.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/app.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/cli.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/domain/__init__.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/domain/area.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/domain/mesh.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/domain/plotting.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/domain/variable.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/domain/zonal.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/py.typed +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/registry.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/remote/__init__.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/remote/health.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/state.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/tools/__init__.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/tools/advanced.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/tools/capabilities.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/tools/inspection.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/tools/plotting.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/tools/scientific_agent.py +0 -0
- {uxarray_mcp-0.2.0 → uxarray_mcp-0.2.2}/src/uxarray_mcp/tools/vector_calc.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: uxarray-mcp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: MCP server for analyzing unstructured meshes with UXarray
|
|
5
5
|
Keywords: uxarray,mcp,unstructured grids,scientific computing,globus compute
|
|
6
6
|
Author: Rajeev Jain, Dayan Abdulla
|
|
@@ -215,10 +215,11 @@ Classifier: Programming Language :: Python :: 3
|
|
|
215
215
|
Classifier: Programming Language :: Python :: 3.12
|
|
216
216
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
217
217
|
Requires-Dist: toolregistry-server[mcp]>=0.4.0
|
|
218
|
+
Requires-Dist: mcp>=1.20,<2
|
|
218
219
|
Requires-Dist: holoviews>=1.19.0
|
|
219
220
|
Requires-Dist: matplotlib>=3.9.0
|
|
220
221
|
Requires-Dist: pyyaml>=6.0
|
|
221
|
-
Requires-Dist: uxarray>=2026.
|
|
222
|
+
Requires-Dist: uxarray>=2026.7.0
|
|
222
223
|
Requires-Dist: sphinx>=7.0 ; extra == 'docs'
|
|
223
224
|
Requires-Dist: sphinx-book-theme>=1.1.0 ; extra == 'docs'
|
|
224
225
|
Requires-Dist: myst-parser>=3.0 ; extra == 'docs'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "uxarray-mcp"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "MCP server for analyzing unstructured meshes with UXarray"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
keywords = [
|
|
@@ -22,10 +22,11 @@ classifiers = [
|
|
|
22
22
|
requires-python = ">=3.12,<3.13"
|
|
23
23
|
dependencies = [
|
|
24
24
|
"toolregistry-server[mcp]>=0.4.0",
|
|
25
|
+
"mcp>=1.20,<2",
|
|
25
26
|
"holoviews>=1.19.0",
|
|
26
27
|
"matplotlib>=3.9.0",
|
|
27
28
|
"pyyaml>=6.0",
|
|
28
|
-
"uxarray>=2026.
|
|
29
|
+
"uxarray>=2026.7.0",
|
|
29
30
|
]
|
|
30
31
|
|
|
31
32
|
[[project.authors]]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "uxarray-mcp"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "MCP server for analyzing unstructured meshes with UXarray"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
keywords = ["uxarray", "mcp", "unstructured grids", "scientific computing", "globus compute"]
|
|
@@ -26,10 +26,12 @@ license = { file = "LICENSE" }
|
|
|
26
26
|
requires-python = ">=3.12,<3.13"
|
|
27
27
|
dependencies = [
|
|
28
28
|
"toolregistry-server[mcp]>=0.4.0",
|
|
29
|
+
# toolregistry-server 0.4.x imports McpError, renamed to MCPError in mcp 2.
|
|
30
|
+
"mcp>=1.20,<2",
|
|
29
31
|
"holoviews>=1.19.0",
|
|
30
32
|
"matplotlib>=3.9.0",
|
|
31
33
|
"pyyaml>=6.0",
|
|
32
|
-
"uxarray>=2026.
|
|
34
|
+
"uxarray>=2026.7.0",
|
|
33
35
|
]
|
|
34
36
|
|
|
35
37
|
[project.optional-dependencies]
|
|
@@ -354,7 +354,15 @@ def compute_curl(
|
|
|
354
354
|
from uxarray_mcp.provenance import attach_scientific_status
|
|
355
355
|
|
|
356
356
|
return attach_scientific_status(
|
|
357
|
-
result,
|
|
357
|
+
result,
|
|
358
|
+
warnings=component_warnings,
|
|
359
|
+
warning_codes=warning_codes,
|
|
360
|
+
extra={
|
|
361
|
+
"physical_scaling_requested": bool(scale_by_radius),
|
|
362
|
+
"physical_scaling_applied": bool(
|
|
363
|
+
scale_by_radius and not deduped_uxarray_warnings
|
|
364
|
+
),
|
|
365
|
+
},
|
|
358
366
|
)
|
|
359
367
|
|
|
360
368
|
|
|
@@ -84,13 +84,17 @@ def attach_scientific_status(
|
|
|
84
84
|
*,
|
|
85
85
|
warnings: list[str] | None = None,
|
|
86
86
|
warning_codes: list[str] | None = None,
|
|
87
|
+
extra: dict[str, Any] | None = None,
|
|
87
88
|
) -> dict[str, Any]:
|
|
88
89
|
"""Attach a machine-actionable scientific interpretation status."""
|
|
89
90
|
messages = warnings or []
|
|
90
|
-
|
|
91
|
+
status = {
|
|
91
92
|
"status": "warning" if messages else "complete",
|
|
92
93
|
"physically_interpretable": not messages,
|
|
93
94
|
"warning_codes": warning_codes or [],
|
|
94
95
|
"warnings": messages,
|
|
95
96
|
}
|
|
97
|
+
if extra:
|
|
98
|
+
status.update(extra)
|
|
99
|
+
result["scientific_status"] = status
|
|
96
100
|
return result
|
|
@@ -166,6 +166,19 @@ class UXarrayComputeAgent(_AcademyAgent):
|
|
|
166
166
|
else:
|
|
167
167
|
return self._run_local_inspect_variable(grid_path, data_path, variable_name)
|
|
168
168
|
|
|
169
|
+
@action
|
|
170
|
+
async def validate_dataset_remote(
|
|
171
|
+
self, grid_path: str, data_path: str, use_remote: bool = False
|
|
172
|
+
) -> Dict[str, Any]:
|
|
173
|
+
"""Validate a dataset in the environment that can read its paths."""
|
|
174
|
+
from uxarray_mcp.remote.compute_functions import remote_validate_dataset
|
|
175
|
+
|
|
176
|
+
if use_remote and self.config.endpoint_id:
|
|
177
|
+
return await self._run_on_hpc(remote_validate_dataset, grid_path, data_path)
|
|
178
|
+
from uxarray_mcp.tools.inspection import validate_dataset
|
|
179
|
+
|
|
180
|
+
return validate_dataset(grid_path, data_path)
|
|
181
|
+
|
|
169
182
|
@action
|
|
170
183
|
async def calculate_zonal_mean_remote(
|
|
171
184
|
self,
|
|
@@ -453,9 +466,14 @@ class UXarrayComputeAgent(_AcademyAgent):
|
|
|
453
466
|
# — not the local submitter — and warn on local/remote version drift.
|
|
454
467
|
drift_warnings: list[str] = []
|
|
455
468
|
worker_uxarray = None
|
|
469
|
+
worker_runtime: dict[str, Any] = {}
|
|
456
470
|
if isinstance(result, dict):
|
|
457
471
|
worker_uxarray = result.pop("_worker_uxarray_version", None)
|
|
458
|
-
result.pop("
|
|
472
|
+
worker_runtime = result.pop("_worker_runtime", {}) or {}
|
|
473
|
+
worker_python = result.pop("_worker_python_version", None)
|
|
474
|
+
if worker_python and "python_version" not in worker_runtime:
|
|
475
|
+
worker_runtime["python_version"] = worker_python
|
|
476
|
+
worker_uxarray = worker_runtime.get("uxarray_version") or worker_uxarray
|
|
459
477
|
local_uxarray = _get_uxarray_version()
|
|
460
478
|
if (
|
|
461
479
|
worker_uxarray
|
|
@@ -484,6 +502,16 @@ class UXarrayComputeAgent(_AcademyAgent):
|
|
|
484
502
|
# Record the worker's uxarray version explicitly alongside the local one.
|
|
485
503
|
if worker_uxarray:
|
|
486
504
|
annotated["_provenance"]["remote_uxarray_version"] = worker_uxarray
|
|
505
|
+
for key in (
|
|
506
|
+
"hostname",
|
|
507
|
+
"python_version",
|
|
508
|
+
"xarray_version",
|
|
509
|
+
"numpy_version",
|
|
510
|
+
"slurm_job_id",
|
|
511
|
+
"pbs_job_id",
|
|
512
|
+
):
|
|
513
|
+
if worker_runtime.get(key):
|
|
514
|
+
annotated["_provenance"][f"remote_{key}"] = worker_runtime[key]
|
|
487
515
|
return annotated
|
|
488
516
|
|
|
489
517
|
def _run_local_inspect_mesh(self, file_path: str) -> Dict[str, Any]:
|
|
@@ -162,6 +162,80 @@ def remote_inspect_mesh(file_path: str) -> Dict[str, Any]:
|
|
|
162
162
|
"n_edge": int(grid.n_edge),
|
|
163
163
|
"source": file_path,
|
|
164
164
|
"_worker_uxarray_version": getattr(ux, "__version__", "unknown"),
|
|
165
|
+
"_worker_python_version": __import__("platform").python_version(),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def remote_validate_dataset(grid_path: str, data_path: str) -> Dict[str, Any]:
|
|
170
|
+
"""Validate numeric variables on the worker that can read the dataset."""
|
|
171
|
+
import os
|
|
172
|
+
import platform
|
|
173
|
+
|
|
174
|
+
import numpy as np
|
|
175
|
+
import uxarray as ux
|
|
176
|
+
|
|
177
|
+
if grid_path.startswith("healpix:"):
|
|
178
|
+
grid = ux.Grid.from_healpix(int(grid_path.split(":")[1]))
|
|
179
|
+
xr_ds = __import__("xarray").open_dataset(data_path)
|
|
180
|
+
uxds = ux.UxDataset(xr_ds, uxgrid=grid)
|
|
181
|
+
elif os.path.splitext(grid_path.lower())[1] in [".shp", ".geojson"]:
|
|
182
|
+
grid = ux.Grid.from_file(grid_path, backend="geopandas")
|
|
183
|
+
xr_ds = __import__("xarray").open_dataset(data_path)
|
|
184
|
+
uxds = ux.UxDataset(xr_ds, uxgrid=grid)
|
|
185
|
+
else:
|
|
186
|
+
uxds = ux.open_dataset(grid_path, data_path)
|
|
187
|
+
|
|
188
|
+
fill_candidates = [1e20, 9.96920996838687e36, -999.0, -9999.0]
|
|
189
|
+
results = []
|
|
190
|
+
all_warnings: list[str] = []
|
|
191
|
+
for name in uxds.data_vars:
|
|
192
|
+
values = uxds[name].values
|
|
193
|
+
if not np.issubdtype(values.dtype, np.number):
|
|
194
|
+
continue
|
|
195
|
+
is_float = np.issubdtype(values.dtype, np.floating)
|
|
196
|
+
n_nan = int(np.sum(np.isnan(values))) if is_float else 0
|
|
197
|
+
n_inf = int(np.sum(np.isinf(values))) if is_float else 0
|
|
198
|
+
n_fill = 0
|
|
199
|
+
detected_fill = None
|
|
200
|
+
if is_float:
|
|
201
|
+
for candidate in fill_candidates:
|
|
202
|
+
count = int(np.sum(np.isclose(values, candidate, rtol=1e-3)))
|
|
203
|
+
if count:
|
|
204
|
+
n_fill = count
|
|
205
|
+
detected_fill = candidate
|
|
206
|
+
break
|
|
207
|
+
warnings = []
|
|
208
|
+
if n_nan:
|
|
209
|
+
warnings.append(f"{name}: contains {n_nan} NaN values")
|
|
210
|
+
if n_inf:
|
|
211
|
+
warnings.append(f"{name}: contains {n_inf} Inf values")
|
|
212
|
+
if n_fill:
|
|
213
|
+
warnings.append(f"{name}: contains {n_fill} fill values")
|
|
214
|
+
all_warnings.extend(warnings)
|
|
215
|
+
results.append(
|
|
216
|
+
{
|
|
217
|
+
"name": name,
|
|
218
|
+
"passed": not warnings,
|
|
219
|
+
"n_nan": n_nan,
|
|
220
|
+
"n_inf": n_inf,
|
|
221
|
+
"n_fill_values": n_fill,
|
|
222
|
+
"detected_fill_value": detected_fill,
|
|
223
|
+
"shape": list(values.shape),
|
|
224
|
+
"dtype": str(values.dtype),
|
|
225
|
+
"warnings": warnings,
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
passed = all(item["passed"] for item in results)
|
|
230
|
+
return {
|
|
231
|
+
"passed": passed,
|
|
232
|
+
"is_valid": passed,
|
|
233
|
+
"n_variables_checked": len(results),
|
|
234
|
+
"n_variables_failed": sum(not item["passed"] for item in results),
|
|
235
|
+
"variables": results,
|
|
236
|
+
"issues": all_warnings,
|
|
237
|
+
"_worker_uxarray_version": getattr(ux, "__version__", "unknown"),
|
|
238
|
+
"_worker_python_version": platform.python_version(),
|
|
165
239
|
}
|
|
166
240
|
|
|
167
241
|
|
|
@@ -212,6 +286,7 @@ def remote_calculate_area(file_path: str) -> Dict[str, Any]:
|
|
|
212
286
|
"area_units": units,
|
|
213
287
|
"n_face": int(grid.n_face),
|
|
214
288
|
"_worker_uxarray_version": getattr(ux, "__version__", "unknown"),
|
|
289
|
+
"_worker_python_version": __import__("platform").python_version(),
|
|
215
290
|
}
|
|
216
291
|
|
|
217
292
|
|
|
@@ -264,6 +339,17 @@ def remote_inspect_variable(
|
|
|
264
339
|
|
|
265
340
|
variables = []
|
|
266
341
|
|
|
342
|
+
def _jsonable(value: Any) -> Any:
|
|
343
|
+
if isinstance(value, np.generic):
|
|
344
|
+
return value.item()
|
|
345
|
+
if isinstance(value, np.ndarray):
|
|
346
|
+
return value.tolist()
|
|
347
|
+
if isinstance(value, dict):
|
|
348
|
+
return {str(key): _jsonable(item) for key, item in value.items()}
|
|
349
|
+
if isinstance(value, (list, tuple)):
|
|
350
|
+
return [_jsonable(item) for item in value]
|
|
351
|
+
return value
|
|
352
|
+
|
|
267
353
|
for name in var_names:
|
|
268
354
|
if name not in uxds:
|
|
269
355
|
continue
|
|
@@ -298,7 +384,7 @@ def remote_inspect_variable(
|
|
|
298
384
|
"shape": list(var.shape),
|
|
299
385
|
"dtype": str(var.dtype),
|
|
300
386
|
"location": location,
|
|
301
|
-
"attrs": dict(var.attrs),
|
|
387
|
+
"attrs": _jsonable(dict(var.attrs)),
|
|
302
388
|
"statistics": stats,
|
|
303
389
|
}
|
|
304
390
|
)
|
|
@@ -311,6 +397,7 @@ def remote_inspect_variable(
|
|
|
311
397
|
"n_edge": int(uxds.uxgrid.n_edge),
|
|
312
398
|
},
|
|
313
399
|
"_worker_uxarray_version": getattr(ux, "__version__", "unknown"),
|
|
400
|
+
"_worker_python_version": __import__("platform").python_version(),
|
|
314
401
|
}
|
|
315
402
|
|
|
316
403
|
|
|
@@ -707,6 +794,7 @@ def remote_calculate_zonal_mean(
|
|
|
707
794
|
"n_edge": int(uxds.uxgrid.n_edge),
|
|
708
795
|
},
|
|
709
796
|
"_worker_uxarray_version": getattr(ux, "__version__", "unknown"),
|
|
797
|
+
"_worker_python_version": __import__("platform").python_version(),
|
|
710
798
|
}
|
|
711
799
|
|
|
712
800
|
|
|
@@ -1155,6 +1243,7 @@ def remote_calculate_gradient(
|
|
|
1155
1243
|
"warnings": uxarray_warnings,
|
|
1156
1244
|
},
|
|
1157
1245
|
"_worker_uxarray_version": getattr(ux, "__version__", "unknown"),
|
|
1246
|
+
"_worker_python_version": __import__("platform").python_version(),
|
|
1158
1247
|
}
|
|
1159
1248
|
|
|
1160
1249
|
|
|
@@ -1260,6 +1349,12 @@ def remote_calculate_curl(
|
|
|
1260
1349
|
applied_scale = bool(scale_by_radius)
|
|
1261
1350
|
else:
|
|
1262
1351
|
result = u.curl(v)
|
|
1352
|
+
if scale_by_radius:
|
|
1353
|
+
component_warnings.append(
|
|
1354
|
+
"Worker UXarray does not support scale_by_radius; returned "
|
|
1355
|
+
"the unit-sphere curl."
|
|
1356
|
+
)
|
|
1357
|
+
warning_codes.append("SCALE_BY_RADIUS_UNSUPPORTED")
|
|
1263
1358
|
_seen: set = set()
|
|
1264
1359
|
for _w in _caught:
|
|
1265
1360
|
_msg = str(_w.message)
|
|
@@ -1267,6 +1362,7 @@ def remote_calculate_curl(
|
|
|
1267
1362
|
_seen.add(_msg)
|
|
1268
1363
|
component_warnings.append(_msg)
|
|
1269
1364
|
warning_codes.append("SPHERE_RADIUS_UNAVAILABLE")
|
|
1365
|
+
applied_scale = False
|
|
1270
1366
|
vals = result.values
|
|
1271
1367
|
finite = vals[np.isfinite(vals)]
|
|
1272
1368
|
stats: Dict[str, Any] = (
|
|
@@ -1290,10 +1386,13 @@ def remote_calculate_curl(
|
|
|
1290
1386
|
"scientific_status": {
|
|
1291
1387
|
"status": "warning" if component_warnings else "complete",
|
|
1292
1388
|
"physically_interpretable": not component_warnings,
|
|
1389
|
+
"physical_scaling_requested": bool(scale_by_radius),
|
|
1390
|
+
"physical_scaling_applied": bool(applied_scale),
|
|
1293
1391
|
"warning_codes": warning_codes,
|
|
1294
1392
|
"warnings": component_warnings,
|
|
1295
1393
|
},
|
|
1296
1394
|
"_worker_uxarray_version": getattr(ux, "__version__", "unknown"),
|
|
1395
|
+
"_worker_python_version": __import__("platform").python_version(),
|
|
1297
1396
|
}
|
|
1298
1397
|
|
|
1299
1398
|
|
|
@@ -1420,6 +1519,7 @@ def remote_calculate_divergence(
|
|
|
1420
1519
|
"warnings": component_warnings,
|
|
1421
1520
|
},
|
|
1422
1521
|
"_worker_uxarray_version": getattr(ux, "__version__", "unknown"),
|
|
1522
|
+
"_worker_python_version": __import__("platform").python_version(),
|
|
1423
1523
|
}
|
|
1424
1524
|
|
|
1425
1525
|
|
|
@@ -1474,6 +1574,7 @@ def remote_calculate_azimuthal_mean(
|
|
|
1474
1574
|
"azimuthal_mean_values": values,
|
|
1475
1575
|
"n_face": int(uxds.uxgrid.n_face),
|
|
1476
1576
|
"_worker_uxarray_version": getattr(ux, "__version__", "unknown"),
|
|
1577
|
+
"_worker_python_version": __import__("platform").python_version(),
|
|
1477
1578
|
}
|
|
1478
1579
|
|
|
1479
1580
|
|
|
@@ -1516,6 +1617,7 @@ def remote_grid_facts(
|
|
|
1516
1617
|
"n_node": int(grid.n_node) if hasattr(grid, "n_node") else 0,
|
|
1517
1618
|
"n_edge": int(grid.n_edge) if hasattr(grid, "n_edge") else 0,
|
|
1518
1619
|
"_worker_uxarray_version": getattr(ux, "__version__", "unknown"),
|
|
1620
|
+
"_worker_python_version": __import__("platform").python_version(),
|
|
1519
1621
|
}
|
|
1520
1622
|
|
|
1521
1623
|
if data_path is not None:
|
|
@@ -1634,6 +1736,7 @@ def remote_remap_variable(
|
|
|
1634
1736
|
"result_shape": list(remapped.shape),
|
|
1635
1737
|
"stats": stats,
|
|
1636
1738
|
"_worker_uxarray_version": getattr(ux, "__version__", "unknown"),
|
|
1739
|
+
"_worker_python_version": __import__("platform").python_version(),
|
|
1637
1740
|
}
|
|
1638
1741
|
|
|
1639
1742
|
|
|
@@ -1715,6 +1818,7 @@ def remote_regrid_dataset(
|
|
|
1715
1818
|
},
|
|
1716
1819
|
"per_variable_stats": per_variable,
|
|
1717
1820
|
"_worker_uxarray_version": getattr(ux, "__version__", "unknown"),
|
|
1821
|
+
"_worker_python_version": __import__("platform").python_version(),
|
|
1718
1822
|
}
|
|
1719
1823
|
|
|
1720
1824
|
|
|
@@ -1781,6 +1885,7 @@ def remote_remap_to_rectilinear(
|
|
|
1781
1885
|
"target_lon": lon,
|
|
1782
1886
|
"target_lat": lat,
|
|
1783
1887
|
"_worker_uxarray_version": getattr(ux, "__version__", "unknown"),
|
|
1888
|
+
"_worker_python_version": __import__("platform").python_version(),
|
|
1784
1889
|
}
|
|
1785
1890
|
|
|
1786
1891
|
|
|
@@ -1860,4 +1965,5 @@ def remote_calculate_zonal_anomaly(
|
|
|
1860
1965
|
"n_edge": int(uxds.uxgrid.n_edge),
|
|
1861
1966
|
},
|
|
1862
1967
|
"_worker_uxarray_version": getattr(ux, "__version__", "unknown"),
|
|
1968
|
+
"_worker_python_version": __import__("platform").python_version(),
|
|
1863
1969
|
}
|
|
@@ -162,7 +162,7 @@ class HPCConfig:
|
|
|
162
162
|
def resolve_endpoint(
|
|
163
163
|
self, endpoint: str | None = None, path: str | None = None
|
|
164
164
|
) -> EndpointProfile | None:
|
|
165
|
-
"""Resolve an explicit endpoint
|
|
165
|
+
"""Resolve an explicit endpoint, matching path prefix, or default."""
|
|
166
166
|
if endpoint:
|
|
167
167
|
if endpoint in self.endpoints:
|
|
168
168
|
return self.endpoints[endpoint]
|
|
@@ -185,6 +185,23 @@ class HPCConfig:
|
|
|
185
185
|
"Pass a configured endpoint name or a Globus Compute endpoint UUID."
|
|
186
186
|
)
|
|
187
187
|
|
|
188
|
+
if path:
|
|
189
|
+
matches: list[tuple[int, EndpointProfile, str]] = []
|
|
190
|
+
for profile in self.endpoints.values():
|
|
191
|
+
for prefix in profile.path_prefixes:
|
|
192
|
+
if path.startswith(prefix):
|
|
193
|
+
matches.append((len(prefix), profile, prefix))
|
|
194
|
+
if matches:
|
|
195
|
+
longest = max(length for length, _, _ in matches)
|
|
196
|
+
best = [item for item in matches if item[0] == longest]
|
|
197
|
+
names = {profile.name for _, profile, _ in best}
|
|
198
|
+
if len(names) > 1:
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"Path {path!r} matches equally specific endpoint prefixes: "
|
|
201
|
+
+ ", ".join(sorted(names))
|
|
202
|
+
)
|
|
203
|
+
return best[0][1]
|
|
204
|
+
|
|
188
205
|
if self.default_endpoint and self.default_endpoint in self.endpoints:
|
|
189
206
|
return self.endpoints[self.default_endpoint]
|
|
190
207
|
|
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import warnings
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from typing import Any, Dict, List
|
|
8
7
|
|
|
9
8
|
from uxarray_mcp.provenance import attach_provenance
|
|
10
9
|
from uxarray_mcp.remote.agent import get_agent
|
|
11
|
-
from uxarray_mcp.remote.config import load_config
|
|
12
10
|
from uxarray_mcp.tools.remote_tools import _endpoint_is_ready
|
|
13
11
|
|
|
14
12
|
# File extensions recognised as potential mesh/data files
|
|
@@ -36,6 +34,7 @@ def list_datasets(
|
|
|
36
34
|
recursive: bool = False,
|
|
37
35
|
max_files: int = 200,
|
|
38
36
|
use_remote: bool = False,
|
|
37
|
+
endpoint: str | None = None,
|
|
39
38
|
) -> Dict[str, Any]:
|
|
40
39
|
"""Scan a directory for mesh and data files and return a structured catalog.
|
|
41
40
|
|
|
@@ -80,7 +79,7 @@ def list_datasets(
|
|
|
80
79
|
list_datasets("/lus/grand/projects/climate", recursive=True, max_files=500)
|
|
81
80
|
"""
|
|
82
81
|
if use_remote:
|
|
83
|
-
return _list_datasets_remote(directory, recursive, max_files)
|
|
82
|
+
return _list_datasets_remote(directory, recursive, max_files, endpoint)
|
|
84
83
|
|
|
85
84
|
root = Path(directory)
|
|
86
85
|
if not root.exists():
|
|
@@ -187,6 +186,7 @@ def list_datasets(
|
|
|
187
186
|
"recursive": recursive,
|
|
188
187
|
"max_files": max_files,
|
|
189
188
|
"use_remote": False,
|
|
189
|
+
"endpoint": endpoint,
|
|
190
190
|
},
|
|
191
191
|
)
|
|
192
192
|
|
|
@@ -307,49 +307,28 @@ def _remote_catalog_fn(
|
|
|
307
307
|
|
|
308
308
|
|
|
309
309
|
def _list_datasets_remote(
|
|
310
|
-
directory: str, recursive: bool, max_files: int
|
|
310
|
+
directory: str, recursive: bool, max_files: int, endpoint: str | None = None
|
|
311
311
|
) -> Dict[str, Any]:
|
|
312
|
-
"""Run list_datasets on the
|
|
313
|
-
|
|
314
|
-
|
|
312
|
+
"""Run list_datasets on the selected HPC endpoint."""
|
|
313
|
+
agent = get_agent(endpoint=endpoint, path=directory)
|
|
314
|
+
config = agent.config
|
|
315
|
+
if not config.endpoint_id:
|
|
315
316
|
raise RuntimeError(
|
|
316
317
|
"No HPC endpoint configured. Set endpoint_id in config.yaml "
|
|
317
318
|
"or the GLOBUS_COMPUTE_ENDPOINT_ID environment variable."
|
|
318
319
|
)
|
|
319
320
|
|
|
320
|
-
agent = get_agent()
|
|
321
321
|
ready, reason = _endpoint_is_ready(agent)
|
|
322
322
|
if not ready:
|
|
323
323
|
raise RuntimeError(
|
|
324
324
|
f"HPC endpoint not ready ({reason}). Remote catalog scan not submitted."
|
|
325
325
|
)
|
|
326
326
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
"HPC dependencies not installed. Run: uv sync --extra hpc"
|
|
333
|
-
) from exc
|
|
334
|
-
|
|
335
|
-
with warnings.catch_warnings():
|
|
336
|
-
warnings.filterwarnings(
|
|
337
|
-
"ignore",
|
|
338
|
-
message=r"(?s).*Environment differences detected between local SDK and endpoint.*",
|
|
339
|
-
category=UserWarning,
|
|
340
|
-
)
|
|
341
|
-
executor = Executor(
|
|
342
|
-
endpoint_id=config.endpoint_id,
|
|
343
|
-
serializer=ComputeSerializer(strategy_code=AllCodeStrategies()),
|
|
344
|
-
)
|
|
345
|
-
with warnings.catch_warnings():
|
|
346
|
-
warnings.filterwarnings(
|
|
347
|
-
"ignore",
|
|
348
|
-
message=r"(?s).*Environment differences detected between local SDK and endpoint.*",
|
|
349
|
-
category=UserWarning,
|
|
350
|
-
)
|
|
351
|
-
future = executor.submit(_remote_catalog_fn, directory, recursive, max_files)
|
|
352
|
-
raw = future.result(timeout=config.timeout_seconds)
|
|
327
|
+
from uxarray_mcp.tools.remote_tools import _run_sync
|
|
328
|
+
|
|
329
|
+
raw = _run_sync(
|
|
330
|
+
lambda: agent._run_on_hpc(_remote_catalog_fn, directory, recursive, max_files)
|
|
331
|
+
)
|
|
353
332
|
|
|
354
333
|
if "error" in raw:
|
|
355
334
|
raise FileNotFoundError(raw["error"])
|
|
@@ -364,6 +343,7 @@ def _list_datasets_remote(
|
|
|
364
343
|
"recursive": recursive,
|
|
365
344
|
"max_files": max_files,
|
|
366
345
|
"use_remote": True,
|
|
346
|
+
"endpoint": endpoint,
|
|
367
347
|
},
|
|
368
348
|
venue=f"hpc:{endpoint_label}",
|
|
369
349
|
)
|
|
@@ -145,11 +145,12 @@ def _run_sync(awaitable_factory) -> Dict[str, Any]:
|
|
|
145
145
|
"""Run an async call from sync code in CLI and server contexts."""
|
|
146
146
|
try:
|
|
147
147
|
asyncio.get_running_loop()
|
|
148
|
-
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
149
|
-
return pool.submit(asyncio.run, awaitable_factory()).result()
|
|
150
148
|
except RuntimeError:
|
|
151
149
|
return asyncio.run(awaitable_factory())
|
|
152
150
|
|
|
151
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
152
|
+
return pool.submit(lambda: asyncio.run(awaitable_factory())).result()
|
|
153
|
+
|
|
153
154
|
|
|
154
155
|
def probe_path_access(
|
|
155
156
|
file_path: str,
|
|
@@ -113,12 +113,13 @@ def run_analysis(
|
|
|
113
113
|
subset_polygon,
|
|
114
114
|
write_result,
|
|
115
115
|
)
|
|
116
|
-
from uxarray_mcp.tools.inspection import calculate_zonal_anomaly
|
|
116
|
+
from uxarray_mcp.tools.inspection import calculate_zonal_anomaly
|
|
117
117
|
from uxarray_mcp.tools.remote_tools import (
|
|
118
118
|
calculate_area,
|
|
119
119
|
calculate_zonal_mean,
|
|
120
120
|
inspect_mesh,
|
|
121
121
|
inspect_variable,
|
|
122
|
+
validate_dataset,
|
|
122
123
|
)
|
|
123
124
|
from uxarray_mcp.tools.vector_calc import (
|
|
124
125
|
calculate_azimuthal_mean,
|
|
@@ -146,10 +147,12 @@ def run_analysis(
|
|
|
146
147
|
session_id=session_id,
|
|
147
148
|
)
|
|
148
149
|
if op == "validate_dataset":
|
|
149
|
-
_reject_unsupported_remote(use_remote, op)
|
|
150
150
|
return validate_dataset(
|
|
151
151
|
_require(grid_path, "grid_path", op),
|
|
152
152
|
_require(data_path, "data_path", op),
|
|
153
|
+
use_remote=use_remote,
|
|
154
|
+
endpoint=endpoint,
|
|
155
|
+
session_id=session_id,
|
|
153
156
|
)
|
|
154
157
|
if op == "calculate_area":
|
|
155
158
|
return calculate_area(
|
|
@@ -46,6 +46,7 @@ def _png_meta(items: list[Any]) -> dict[str, Any]:
|
|
|
46
46
|
"image_size_bytes": image_size_bytes,
|
|
47
47
|
"grid_info": meta.get("grid_info"),
|
|
48
48
|
"variable_name": meta.get("variable_name"),
|
|
49
|
+
"_provenance": meta.get("_provenance", {}),
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
|
|
@@ -126,6 +127,7 @@ def analyze_dataset(
|
|
|
126
127
|
inspect_variable,
|
|
127
128
|
plot_mesh,
|
|
128
129
|
plot_variable,
|
|
130
|
+
validate_dataset,
|
|
129
131
|
)
|
|
130
132
|
|
|
131
133
|
resolved_grid, resolved_data = _resolve_plot_paths(
|
|
@@ -155,14 +157,15 @@ def analyze_dataset(
|
|
|
155
157
|
selected_variable: Optional[str] = variable_name
|
|
156
158
|
|
|
157
159
|
if resolved_data is not None:
|
|
158
|
-
# validate_dataset is local-only by design (data is read directly);
|
|
159
|
-
# if the dispatcher were ever added, prefer that. For now use the
|
|
160
|
-
# local variant via inspection.validate_dataset.
|
|
161
|
-
from .inspection import validate_dataset as _validate_dataset
|
|
162
|
-
|
|
163
160
|
validation = _safe_call(
|
|
164
161
|
"validate_dataset",
|
|
165
|
-
lambda:
|
|
162
|
+
lambda: validate_dataset(
|
|
163
|
+
resolved_grid,
|
|
164
|
+
resolved_data,
|
|
165
|
+
use_remote=use_remote,
|
|
166
|
+
endpoint=endpoint,
|
|
167
|
+
session_id=session_id,
|
|
168
|
+
),
|
|
166
169
|
warnings,
|
|
167
170
|
)
|
|
168
171
|
if validation is not None:
|
|
@@ -204,7 +207,7 @@ def analyze_dataset(
|
|
|
204
207
|
|
|
205
208
|
# ── Stage 5: zonal mean (needs data + face-centered variable) ───────────
|
|
206
209
|
zonal_mean: Optional[dict[str, Any]] = None
|
|
207
|
-
validation_passed = validation is None
|
|
210
|
+
validation_passed = validation is not None and validation.get("passed") is True
|
|
208
211
|
if (
|
|
209
212
|
resolved_data is not None
|
|
210
213
|
and selected_variable is not None
|
|
@@ -226,7 +229,7 @@ def analyze_dataset(
|
|
|
226
229
|
stages_run.append("calculate_zonal_mean")
|
|
227
230
|
elif resolved_data is not None and selected_variable is not None:
|
|
228
231
|
warnings.append(
|
|
229
|
-
"calculate_zonal_mean: skipped because dataset validation failed."
|
|
232
|
+
"calculate_zonal_mean: skipped because dataset validation failed or was unavailable."
|
|
230
233
|
)
|
|
231
234
|
|
|
232
235
|
# ── Stage 6 + 7: plots (optional) ────────────────────────────────────────
|
|
@@ -269,7 +272,9 @@ def analyze_dataset(
|
|
|
269
272
|
variable_plot = _png_meta(var_plot_items)
|
|
270
273
|
stages_run.append("plot_variable")
|
|
271
274
|
elif resolved_data is not None and selected_variable is not None:
|
|
272
|
-
warnings.append(
|
|
275
|
+
warnings.append(
|
|
276
|
+
"plot_variable: skipped because dataset validation failed or was unavailable."
|
|
277
|
+
)
|
|
273
278
|
|
|
274
279
|
# ── Recommended next steps ──────────────────────────────────────────────
|
|
275
280
|
next_steps: list[str] = []
|
|
@@ -322,7 +327,15 @@ def analyze_dataset(
|
|
|
322
327
|
# (used by tests that import `inspect_mesh` directly from this module).
|
|
323
328
|
_ = inspect_mesh # keep import alive
|
|
324
329
|
|
|
325
|
-
stage_results = [
|
|
330
|
+
stage_results = [
|
|
331
|
+
mesh,
|
|
332
|
+
validation,
|
|
333
|
+
variables,
|
|
334
|
+
area,
|
|
335
|
+
zonal_mean,
|
|
336
|
+
mesh_plot,
|
|
337
|
+
variable_plot,
|
|
338
|
+
]
|
|
326
339
|
venues = {
|
|
327
340
|
stage.get("_provenance", {}).get("execution_venue")
|
|
328
341
|
for stage in stage_results
|
|
@@ -48,13 +48,15 @@ def _run_sync(async_call: Callable[[], Any]) -> Dict[str, Any]:
|
|
|
48
48
|
"""
|
|
49
49
|
try:
|
|
50
50
|
asyncio.get_running_loop()
|
|
51
|
-
# Inside async context (e.g. MCP server) — run in a new thread
|
|
52
|
-
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
53
|
-
return pool.submit(asyncio.run, async_call()).result()
|
|
54
51
|
except RuntimeError:
|
|
55
52
|
# No event loop running (tests, CLI) — run directly
|
|
56
53
|
return asyncio.run(async_call())
|
|
57
54
|
|
|
55
|
+
# Inside async context (e.g. MCP server) — run in a new thread. Keep the
|
|
56
|
+
# operation outside the loop-detection try so its RuntimeError is preserved.
|
|
57
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
58
|
+
return pool.submit(lambda: asyncio.run(async_call())).result()
|
|
59
|
+
|
|
58
60
|
|
|
59
61
|
def _path_is_locally_reachable(path_hint: str | None) -> bool:
|
|
60
62
|
"""True when local fallback can plausibly handle ``path_hint``.
|
|
@@ -220,6 +222,29 @@ def inspect_mesh(
|
|
|
220
222
|
)
|
|
221
223
|
|
|
222
224
|
|
|
225
|
+
def validate_dataset(
|
|
226
|
+
grid_path: str,
|
|
227
|
+
data_path: str,
|
|
228
|
+
use_remote: bool = False,
|
|
229
|
+
endpoint: str | None = None,
|
|
230
|
+
session_id: str | None = None,
|
|
231
|
+
) -> Dict[str, Any]:
|
|
232
|
+
"""Validate a dataset locally or on the selected endpoint."""
|
|
233
|
+
from uxarray_mcp.tools.inspection import validate_dataset as local_validate
|
|
234
|
+
|
|
235
|
+
return _run_with_optional_hpc(
|
|
236
|
+
tool_name="validate_dataset",
|
|
237
|
+
use_remote=use_remote,
|
|
238
|
+
endpoint=endpoint,
|
|
239
|
+
path_hint=grid_path,
|
|
240
|
+
session_id=session_id,
|
|
241
|
+
local_call=lambda: local_validate(grid_path, data_path),
|
|
242
|
+
remote_call=lambda agent: _run_sync(
|
|
243
|
+
lambda: agent.validate_dataset_remote(grid_path, data_path, True)
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
223
248
|
def calculate_area(
|
|
224
249
|
file_path: str,
|
|
225
250
|
use_remote: bool = False,
|
|
@@ -159,12 +159,12 @@ def _execute_workflow(workflow_id: str, *, resume: bool) -> dict[str, Any]:
|
|
|
159
159
|
probe_path_access,
|
|
160
160
|
validate_hpc_setup,
|
|
161
161
|
)
|
|
162
|
-
from uxarray_mcp.tools.inspection import validate_dataset
|
|
163
162
|
from uxarray_mcp.tools.remote_tools import (
|
|
164
163
|
calculate_area,
|
|
165
164
|
calculate_zonal_mean,
|
|
166
165
|
inspect_mesh,
|
|
167
166
|
inspect_variable,
|
|
167
|
+
validate_dataset,
|
|
168
168
|
)
|
|
169
169
|
|
|
170
170
|
workflow = get_workflow(workflow_id)
|
|
@@ -182,6 +182,8 @@ def _execute_workflow(workflow_id: str, *, resume: bool) -> dict[str, Any]:
|
|
|
182
182
|
data_path = workflow["inputs"].get("data_path")
|
|
183
183
|
variable_name = workflow["inputs"].get("variable_name")
|
|
184
184
|
sample_path = workflow["inputs"].get("sample_path")
|
|
185
|
+
use_remote = bool(workflow["inputs"].get("use_remote", False))
|
|
186
|
+
endpoint = workflow["inputs"].get("endpoint")
|
|
185
187
|
session_id = workflow.get("session_id")
|
|
186
188
|
|
|
187
189
|
steps: list[tuple[str, Any]] = [
|
|
@@ -190,17 +192,31 @@ def _execute_workflow(workflow_id: str, *, resume: bool) -> dict[str, Any]:
|
|
|
190
192
|
lambda: validate_hpc_setup(
|
|
191
193
|
run_remote_probe=True,
|
|
192
194
|
sample_path=sample_path,
|
|
195
|
+
endpoint=endpoint,
|
|
193
196
|
),
|
|
194
197
|
),
|
|
195
198
|
(
|
|
196
199
|
"probe_path_access",
|
|
197
|
-
lambda: probe_path_access(
|
|
200
|
+
lambda: probe_path_access(
|
|
201
|
+
sample_path or file_path,
|
|
202
|
+
use_remote=use_remote,
|
|
203
|
+
endpoint=endpoint,
|
|
204
|
+
),
|
|
205
|
+
),
|
|
206
|
+
(
|
|
207
|
+
"inspect_mesh",
|
|
208
|
+
lambda: inspect_mesh(file_path, use_remote=use_remote, endpoint=endpoint),
|
|
198
209
|
),
|
|
199
|
-
("inspect_mesh", lambda: inspect_mesh(file_path)),
|
|
200
210
|
(
|
|
201
211
|
"inspect_variable",
|
|
202
212
|
lambda: (
|
|
203
|
-
inspect_variable(
|
|
213
|
+
inspect_variable(
|
|
214
|
+
file_path,
|
|
215
|
+
data_path,
|
|
216
|
+
variable_name,
|
|
217
|
+
use_remote=use_remote,
|
|
218
|
+
endpoint=endpoint,
|
|
219
|
+
)
|
|
204
220
|
if data_path is not None
|
|
205
221
|
else {"skipped": True}
|
|
206
222
|
),
|
|
@@ -208,12 +224,20 @@ def _execute_workflow(workflow_id: str, *, resume: bool) -> dict[str, Any]:
|
|
|
208
224
|
(
|
|
209
225
|
"validate_dataset",
|
|
210
226
|
lambda: (
|
|
211
|
-
validate_dataset(
|
|
227
|
+
validate_dataset(
|
|
228
|
+
file_path,
|
|
229
|
+
data_path,
|
|
230
|
+
use_remote=use_remote,
|
|
231
|
+
endpoint=endpoint,
|
|
232
|
+
)
|
|
212
233
|
if data_path is not None
|
|
213
234
|
else {"skipped": True}
|
|
214
235
|
),
|
|
215
236
|
),
|
|
216
|
-
(
|
|
237
|
+
(
|
|
238
|
+
"calculate_area",
|
|
239
|
+
lambda: calculate_area(file_path, use_remote=use_remote, endpoint=endpoint),
|
|
240
|
+
),
|
|
217
241
|
]
|
|
218
242
|
|
|
219
243
|
# `calculate_zonal_mean` depends on inspected variables and validation.
|
|
@@ -286,7 +310,13 @@ def _execute_workflow(workflow_id: str, *, resume: bool) -> dict[str, Any]:
|
|
|
286
310
|
)
|
|
287
311
|
tracker.stage(zonal_step_name, f"Running {zonal_step_name}")
|
|
288
312
|
update_workflow_step(workflow_id, zonal_step_name, status="running")
|
|
289
|
-
zonal_result = calculate_zonal_mean(
|
|
313
|
+
zonal_result = calculate_zonal_mean(
|
|
314
|
+
file_path,
|
|
315
|
+
data_path,
|
|
316
|
+
selected_variable,
|
|
317
|
+
use_remote=use_remote,
|
|
318
|
+
endpoint=endpoint,
|
|
319
|
+
)
|
|
290
320
|
last_results[zonal_step_name] = zonal_result
|
|
291
321
|
update_workflow_step(
|
|
292
322
|
workflow_id,
|
|
@@ -341,6 +371,8 @@ def run_workflow(
|
|
|
341
371
|
session_id: str | None = None,
|
|
342
372
|
dataset_handle: str | None = None,
|
|
343
373
|
sample_path: str | None = None,
|
|
374
|
+
use_remote: bool = False,
|
|
375
|
+
endpoint: str | None = None,
|
|
344
376
|
) -> dict[str, Any]:
|
|
345
377
|
"""Run the canonical scientific workflow with persisted state and progress."""
|
|
346
378
|
resolved_grid, resolved_data = _resolve_workflow_inputs(
|
|
@@ -356,6 +388,8 @@ def run_workflow(
|
|
|
356
388
|
"data_path": resolved_data,
|
|
357
389
|
"variable_name": variable_name,
|
|
358
390
|
"sample_path": sample_path,
|
|
391
|
+
"use_remote": use_remote,
|
|
392
|
+
"endpoint": endpoint,
|
|
359
393
|
},
|
|
360
394
|
session_id=session_id,
|
|
361
395
|
steps=[
|
|
@@ -379,7 +413,10 @@ def run_workflow(
|
|
|
379
413
|
"session_id": session_id,
|
|
380
414
|
"dataset_handle": dataset_handle,
|
|
381
415
|
"sample_path": sample_path,
|
|
416
|
+
"use_remote": use_remote,
|
|
417
|
+
"endpoint": endpoint,
|
|
382
418
|
},
|
|
419
|
+
venue=(f"hpc:{endpoint}" if use_remote and endpoint else "local"),
|
|
383
420
|
)
|
|
384
421
|
|
|
385
422
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|