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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Vector calculus and azimuthal averaging on unstructured meshes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compute_gradient(uxds: Any, variable_name: str) -> dict:
|
|
9
|
+
"""Compute the gradient of a face-centered scalar field.
|
|
10
|
+
|
|
11
|
+
Uses UXarray's Green-Gauss finite-volume gradient, which forms a closed
|
|
12
|
+
control volume around each cell by connecting centroids of neighbouring
|
|
13
|
+
cells.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
uxds : ux.UxDataset
|
|
18
|
+
Loaded UXarray dataset.
|
|
19
|
+
variable_name : str
|
|
20
|
+
Face-centered scalar variable to differentiate.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
dict
|
|
25
|
+
Keys: variable_name, zonal_component_name, meridional_component_name,
|
|
26
|
+
n_face, stats (min/max/mean for each component).
|
|
27
|
+
"""
|
|
28
|
+
if variable_name not in uxds.data_vars:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"Variable '{variable_name}' not found. Available: {list(uxds.data_vars)}"
|
|
31
|
+
)
|
|
32
|
+
var = uxds[variable_name]
|
|
33
|
+
if "n_face" not in var.dims and "nCells" not in var.dims:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"Variable '{variable_name}' is not face-centered. "
|
|
36
|
+
"Gradient requires face-centered data."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
import numpy as np
|
|
40
|
+
|
|
41
|
+
grad = var.gradient()
|
|
42
|
+
# gradient() returns a UxDataset with two variables
|
|
43
|
+
comp_names = list(grad.data_vars)
|
|
44
|
+
|
|
45
|
+
def _stats(arr: Any) -> dict:
|
|
46
|
+
vals = arr.values
|
|
47
|
+
finite = vals[np.isfinite(vals)]
|
|
48
|
+
if finite.size == 0:
|
|
49
|
+
return {"min": None, "max": None, "mean": None}
|
|
50
|
+
return {
|
|
51
|
+
"min": float(finite.min()),
|
|
52
|
+
"max": float(finite.max()),
|
|
53
|
+
"mean": float(finite.mean()),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
components = {name: _stats(grad[name]) for name in comp_names}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"variable_name": variable_name,
|
|
60
|
+
"components": comp_names,
|
|
61
|
+
"component_stats": components,
|
|
62
|
+
"n_face": int(uxds.uxgrid.n_face),
|
|
63
|
+
"interpretation": "zonal (∂/∂x) and meridional (∂/∂y) components of the gradient",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def compute_curl(uxds: Any, u_variable: str, v_variable: str) -> dict:
|
|
68
|
+
"""Compute the curl (relative vorticity) of a 2-D vector field (u, v).
|
|
69
|
+
|
|
70
|
+
The curl is the vertical component of ∇ × (u, v):
|
|
71
|
+
ζ = ∂v/∂x − ∂u/∂y
|
|
72
|
+
|
|
73
|
+
For atmospheric wind fields this is the relative vorticity.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
uxds : ux.UxDataset
|
|
78
|
+
Loaded UXarray dataset containing both components.
|
|
79
|
+
u_variable : str
|
|
80
|
+
Zonal (east–west) component variable name.
|
|
81
|
+
v_variable : str
|
|
82
|
+
Meridional (north–south) component variable name.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
dict
|
|
87
|
+
Keys: u_variable, v_variable, n_face, stats (min/max/mean/std of curl).
|
|
88
|
+
"""
|
|
89
|
+
for name in (u_variable, v_variable):
|
|
90
|
+
if name not in uxds.data_vars:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Variable '{name}' not found. Available: {list(uxds.data_vars)}"
|
|
93
|
+
)
|
|
94
|
+
u = uxds[u_variable]
|
|
95
|
+
v = uxds[v_variable]
|
|
96
|
+
for name, var in ((u_variable, u), (v_variable, v)):
|
|
97
|
+
if "n_face" not in var.dims and "nCells" not in var.dims:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Variable '{name}' is not face-centered. "
|
|
100
|
+
"Curl requires face-centered vector components."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
import numpy as np
|
|
104
|
+
|
|
105
|
+
result = u.curl(v)
|
|
106
|
+
vals = result.values
|
|
107
|
+
finite = vals[np.isfinite(vals)]
|
|
108
|
+
|
|
109
|
+
stats: dict = {}
|
|
110
|
+
if finite.size > 0:
|
|
111
|
+
stats = {
|
|
112
|
+
"min": float(finite.min()),
|
|
113
|
+
"max": float(finite.max()),
|
|
114
|
+
"mean": float(finite.mean()),
|
|
115
|
+
"std": float(finite.std()),
|
|
116
|
+
}
|
|
117
|
+
else:
|
|
118
|
+
stats = {"min": None, "max": None, "mean": None, "std": None}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"u_variable": u_variable,
|
|
122
|
+
"v_variable": v_variable,
|
|
123
|
+
"interpretation": "relative vorticity ζ = ∂v/∂x − ∂u/∂y",
|
|
124
|
+
"n_face": int(uxds.uxgrid.n_face),
|
|
125
|
+
"stats": stats,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def compute_divergence(uxds: Any, u_variable: str, v_variable: str) -> dict:
|
|
130
|
+
"""Compute the horizontal divergence of a 2-D vector field (u, v).
|
|
131
|
+
|
|
132
|
+
Divergence = ∂u/∂x + ∂v/∂y.
|
|
133
|
+
|
|
134
|
+
Positive values indicate divergence (outflow), negative values indicate
|
|
135
|
+
convergence (inflow). Surface wind convergence drives rising motion and
|
|
136
|
+
convection.
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
uxds : ux.UxDataset
|
|
141
|
+
Loaded UXarray dataset.
|
|
142
|
+
u_variable : str
|
|
143
|
+
Zonal (east–west) component variable name.
|
|
144
|
+
v_variable : str
|
|
145
|
+
Meridional (north–south) component variable name.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
dict
|
|
150
|
+
Keys: u_variable, v_variable, n_face, stats (min/max/mean/std).
|
|
151
|
+
"""
|
|
152
|
+
for name in (u_variable, v_variable):
|
|
153
|
+
if name not in uxds.data_vars:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"Variable '{name}' not found. Available: {list(uxds.data_vars)}"
|
|
156
|
+
)
|
|
157
|
+
u = uxds[u_variable]
|
|
158
|
+
v = uxds[v_variable]
|
|
159
|
+
for name, var in ((u_variable, u), (v_variable, v)):
|
|
160
|
+
if "n_face" not in var.dims and "nCells" not in var.dims:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"Variable '{name}' is not face-centered. "
|
|
163
|
+
"Divergence requires face-centered vector components."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
import numpy as np
|
|
167
|
+
|
|
168
|
+
result = u.divergence(v)
|
|
169
|
+
vals = result.values
|
|
170
|
+
finite = vals[np.isfinite(vals)]
|
|
171
|
+
|
|
172
|
+
stats: dict = {}
|
|
173
|
+
if finite.size > 0:
|
|
174
|
+
stats = {
|
|
175
|
+
"min": float(finite.min()),
|
|
176
|
+
"max": float(finite.max()),
|
|
177
|
+
"mean": float(finite.mean()),
|
|
178
|
+
"std": float(finite.std()),
|
|
179
|
+
}
|
|
180
|
+
else:
|
|
181
|
+
stats = {"min": None, "max": None, "mean": None, "std": None}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
"u_variable": u_variable,
|
|
185
|
+
"v_variable": v_variable,
|
|
186
|
+
"interpretation": "horizontal divergence ∂u/∂x + ∂v/∂y",
|
|
187
|
+
"n_face": int(uxds.uxgrid.n_face),
|
|
188
|
+
"stats": stats,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def compute_azimuthal_mean(
|
|
193
|
+
uxds: Any,
|
|
194
|
+
variable_name: str,
|
|
195
|
+
center_lon: float,
|
|
196
|
+
center_lat: float,
|
|
197
|
+
outer_radius: float,
|
|
198
|
+
radius_step: float,
|
|
199
|
+
) -> dict:
|
|
200
|
+
"""Compute the azimuthal (radial) mean around a centre point.
|
|
201
|
+
|
|
202
|
+
Averages the variable along circles of constant great-circle distance from
|
|
203
|
+
the centre, producing a radial profile. Useful for analysing tropical
|
|
204
|
+
cyclones, polar vortex structure, or any feature with approximate radial
|
|
205
|
+
symmetry.
|
|
206
|
+
|
|
207
|
+
Parameters
|
|
208
|
+
----------
|
|
209
|
+
uxds : ux.UxDataset
|
|
210
|
+
Loaded UXarray dataset.
|
|
211
|
+
variable_name : str
|
|
212
|
+
Face-centered variable to average.
|
|
213
|
+
center_lon : float
|
|
214
|
+
Longitude of the centre point (degrees).
|
|
215
|
+
center_lat : float
|
|
216
|
+
Latitude of the centre point (degrees).
|
|
217
|
+
outer_radius : float
|
|
218
|
+
Maximum radius in great-circle degrees.
|
|
219
|
+
radius_step : float
|
|
220
|
+
Radial bin width in great-circle degrees.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
dict
|
|
225
|
+
Keys: variable_name, center, radii, azimuthal_mean_values, n_face.
|
|
226
|
+
"""
|
|
227
|
+
if variable_name not in uxds.data_vars:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"Variable '{variable_name}' not found. Available: {list(uxds.data_vars)}"
|
|
230
|
+
)
|
|
231
|
+
var = uxds[variable_name]
|
|
232
|
+
if "n_face" not in var.dims and "nCells" not in var.dims:
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"Variable '{variable_name}' is not face-centered. "
|
|
235
|
+
"Azimuthal mean requires face-centered data."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
result = var.azimuthal_mean(
|
|
239
|
+
center_coord=(center_lon, center_lat),
|
|
240
|
+
outer_radius=outer_radius,
|
|
241
|
+
radius_step=radius_step,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# result is an xr.DataArray with a radius coordinate
|
|
245
|
+
radii = result.coords[result.dims[0]].values.tolist()
|
|
246
|
+
values = result.values.tolist()
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
"variable_name": variable_name,
|
|
250
|
+
"center": {"lon": center_lon, "lat": center_lat},
|
|
251
|
+
"outer_radius_deg": outer_radius,
|
|
252
|
+
"radius_step_deg": radius_step,
|
|
253
|
+
"radii_deg": radii,
|
|
254
|
+
"azimuthal_mean_values": values,
|
|
255
|
+
"n_face": int(uxds.uxgrid.n_face),
|
|
256
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Shared zonal mean computation logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compute_zonal_mean_stats(
|
|
9
|
+
uxds: Any,
|
|
10
|
+
variable_name: str,
|
|
11
|
+
lat_spec: Optional[tuple | float | list] = None,
|
|
12
|
+
conservative: bool = False,
|
|
13
|
+
) -> dict:
|
|
14
|
+
"""Compute zonal mean statistics from a loaded UXarray dataset.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
uxds : ux.UxDataset
|
|
19
|
+
Loaded UXarray dataset.
|
|
20
|
+
variable_name : str
|
|
21
|
+
Name of face-centered variable to average.
|
|
22
|
+
lat_spec : tuple | float | list | None
|
|
23
|
+
Latitude specification for zonal bands.
|
|
24
|
+
conservative : bool
|
|
25
|
+
If True, use area-weighted conservative averaging.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
dict
|
|
30
|
+
Keys: variable_name, latitudes, zonal_mean_values, conservative, grid_info
|
|
31
|
+
"""
|
|
32
|
+
if variable_name not in uxds.data_vars:
|
|
33
|
+
available = list(uxds.data_vars.keys())
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"Variable '{variable_name}' not found. Available variables: {available}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
var = uxds[variable_name]
|
|
39
|
+
|
|
40
|
+
if "n_face" not in var.dims and "nCells" not in var.dims:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Variable '{variable_name}' is not face-centered. "
|
|
43
|
+
"Zonal mean only supports face-centered data."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if lat_spec is not None:
|
|
47
|
+
zonal_result = var.zonal_mean(lat=lat_spec, conservative=conservative)
|
|
48
|
+
else:
|
|
49
|
+
zonal_result = var.zonal_mean(conservative=conservative)
|
|
50
|
+
|
|
51
|
+
latitudes = zonal_result.coords["latitudes"].values.tolist()
|
|
52
|
+
zonal_mean_values = zonal_result.values.tolist()
|
|
53
|
+
|
|
54
|
+
grid_info = {
|
|
55
|
+
"n_face": int(uxds.uxgrid.n_face),
|
|
56
|
+
"n_node": int(uxds.uxgrid.n_node),
|
|
57
|
+
"n_edge": int(uxds.uxgrid.n_edge),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
"variable_name": variable_name,
|
|
62
|
+
"latitudes": latitudes,
|
|
63
|
+
"zonal_mean_values": zonal_mean_values,
|
|
64
|
+
"conservative": conservative,
|
|
65
|
+
"grid_info": grid_info,
|
|
66
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Provenance tracking for UXarray MCP tool outputs.
|
|
2
|
+
|
|
3
|
+
Appends a _provenance key to every tool result so scientific workflows
|
|
4
|
+
can trace what ran, when, where, and with what software.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_uxarray_version() -> str:
|
|
15
|
+
"""Return the installed UXarray version or ``unknown`` on failure."""
|
|
16
|
+
try:
|
|
17
|
+
import uxarray
|
|
18
|
+
|
|
19
|
+
return uxarray.__version__
|
|
20
|
+
except Exception:
|
|
21
|
+
return "unknown"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def attach_provenance(
|
|
25
|
+
result: dict[str, Any],
|
|
26
|
+
tool: str,
|
|
27
|
+
inputs: dict[str, Any],
|
|
28
|
+
venue: str = "local",
|
|
29
|
+
warnings: list[str] | None = None,
|
|
30
|
+
validation_summary: dict[str, Any] | None = None,
|
|
31
|
+
selected_variable: str | None = None,
|
|
32
|
+
artifacts: list[dict[str, Any]] | None = None,
|
|
33
|
+
) -> dict[str, Any]:
|
|
34
|
+
"""Attach a _provenance block to a tool result dict.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
result : dict
|
|
39
|
+
The tool output to annotate.
|
|
40
|
+
tool : str
|
|
41
|
+
Name of the tool that produced the result.
|
|
42
|
+
inputs : dict
|
|
43
|
+
The input arguments passed to the tool.
|
|
44
|
+
venue : str
|
|
45
|
+
Execution venue, for example ``"local"`` or ``"hpc:<endpoint-name>"``.
|
|
46
|
+
Public results should not expose raw endpoint UUIDs.
|
|
47
|
+
warnings : list[str] | None
|
|
48
|
+
Any warnings generated during execution.
|
|
49
|
+
validation_summary : dict | None
|
|
50
|
+
Summary from validate_dataset: passed, n_variables_checked,
|
|
51
|
+
n_variables_failed. Included when a validation step ran upstream.
|
|
52
|
+
selected_variable : str | None
|
|
53
|
+
The variable name that was analysed, when applicable.
|
|
54
|
+
artifacts : list[dict] | None
|
|
55
|
+
Computational outputs produced by this run. Each entry is a dict
|
|
56
|
+
with at minimum a "type" key describing what was computed
|
|
57
|
+
(e.g. mesh_topology, face_areas, zonal_mean, validation).
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
dict
|
|
62
|
+
The result dict with a _provenance key added.
|
|
63
|
+
"""
|
|
64
|
+
provenance: dict[str, Any] = {
|
|
65
|
+
"tool": tool,
|
|
66
|
+
"inputs": inputs,
|
|
67
|
+
"execution_venue": venue,
|
|
68
|
+
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
|
|
69
|
+
"uxarray_version": _get_uxarray_version(),
|
|
70
|
+
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
|
71
|
+
"warnings": warnings if warnings is not None else [],
|
|
72
|
+
"artifacts": artifacts if artifacts is not None else [],
|
|
73
|
+
}
|
|
74
|
+
if selected_variable is not None:
|
|
75
|
+
provenance["selected_variable"] = selected_variable
|
|
76
|
+
if validation_summary is not None:
|
|
77
|
+
provenance["validation_summary"] = validation_summary
|
|
78
|
+
result["_provenance"] = provenance
|
|
79
|
+
return result
|
uxarray_mcp/py.typed
ADDED
|
File without changes
|