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,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
@@ -0,0 +1,6 @@
1
+ """Remote execution support for UXarray MCP server via Globus Compute and Academy."""
2
+
3
+ from .agent import UXarrayComputeAgent
4
+ from .config import load_config
5
+
6
+ __all__ = ["load_config", "UXarrayComputeAgent"]