|
6 | 6 |
|
7 | 7 | import cf_xarray # noqa: F401 |
8 | 8 | import numpy as np |
| 9 | +import uxarray as ux |
9 | 10 | import xarray as xr |
10 | 11 | import xgcm |
11 | 12 |
|
|
14 | 15 | from parcels._core.utils.string import _assert_str_and_python_varname |
15 | 16 | from parcels._core.utils.time import get_datetime_type_calendar |
16 | 17 | from parcels._core.utils.time import is_compatible as datetime_is_compatible |
| 18 | +from parcels._core.uxgrid import UxGrid |
17 | 19 | from parcels._core.xgrid import _DEFAULT_XGCM_KWARGS, XGrid |
18 | 20 | from parcels._logger import logger |
19 | 21 | from parcels._typing import Mesh |
20 | | -from parcels.interpolators import XConstantField |
| 22 | +from parcels.interpolators import UXPiecewiseConstantFace, UXPiecewiseLinearNode, XConstantField, XLinear |
21 | 23 |
|
22 | 24 | if TYPE_CHECKING: |
23 | 25 | from parcels._core.basegrid import BaseGrid |
@@ -238,22 +240,62 @@ def from_copernicusmarine(ds: xr.Dataset): |
238 | 240 |
|
239 | 241 | fields = {} |
240 | 242 | if "U" in ds.data_vars and "V" in ds.data_vars: |
241 | | - fields["U"] = Field("U", ds["U"], grid) |
242 | | - fields["V"] = Field("V", ds["V"], grid) |
| 243 | + fields["U"] = Field("U", ds["U"], grid, XLinear) |
| 244 | + fields["V"] = Field("V", ds["V"], grid, XLinear) |
243 | 245 | fields["U"].units = GeographicPolar() |
244 | 246 | fields["V"].units = Geographic() |
245 | 247 |
|
246 | 248 | if "W" in ds.data_vars: |
247 | 249 | ds["W"] -= ds[ |
248 | 250 | "W" |
249 | 251 | ] # Negate W to convert from up positive to down positive (as that's the direction of positive z) |
250 | | - fields["W"] = Field("W", ds["W"], grid) |
| 252 | + fields["W"] = Field("W", ds["W"], grid, XLinear) |
251 | 253 | fields["UVW"] = VectorField("UVW", fields["U"], fields["V"], fields["W"]) |
252 | 254 | else: |
253 | 255 | fields["UV"] = VectorField("UV", fields["U"], fields["V"]) |
254 | 256 |
|
255 | 257 | for varname in set(ds.data_vars) - set(fields.keys()): |
256 | | - fields[varname] = Field(varname, ds[varname], grid) |
| 258 | + fields[varname] = Field(varname, ds[varname], grid, XLinear) |
| 259 | + |
| 260 | + return FieldSet(list(fields.values())) |
| 261 | + |
| 262 | + def from_fesom2(ds: ux.UxDataset): |
| 263 | + """Create a FieldSet from a FESOM2 uxarray.UxDataset. |
| 264 | +
|
| 265 | + Parameters |
| 266 | + ---------- |
| 267 | + ds : uxarray.UxDataset |
| 268 | + uxarray.UxDataset as obtained from the uxarray package. |
| 269 | +
|
| 270 | + Returns |
| 271 | + ------- |
| 272 | + FieldSet |
| 273 | + FieldSet object containing the fields from the dataset that can be used for a Parcels simulation. |
| 274 | + """ |
| 275 | + ds = ds.copy() |
| 276 | + ds_dims = list(ds.dims) |
| 277 | + if not all(dim in ds_dims for dim in ["time", "nz", "nz1"]): |
| 278 | + raise ValueError( |
| 279 | + f"Dataset missing one of the required dimensions 'time', 'nz', or 'nz1'. Found dimensions {ds_dims}" |
| 280 | + ) |
| 281 | + grid = UxGrid(ds.uxgrid, z=ds.coords["nz"]) |
| 282 | + ds = _discover_fesom2_U_and_V(ds) |
| 283 | + |
| 284 | + fields = {} |
| 285 | + if "U" in ds.data_vars and "V" in ds.data_vars: |
| 286 | + fields["U"] = Field("U", ds["U"], grid, _select_uxinterpolator(ds["U"])) |
| 287 | + fields["V"] = Field("V", ds["V"], grid, _select_uxinterpolator(ds["U"])) |
| 288 | + fields["U"].units = GeographicPolar() |
| 289 | + fields["V"].units = Geographic() |
| 290 | + |
| 291 | + if "W" in ds.data_vars: |
| 292 | + fields["W"] = Field("W", ds["W"], grid, _select_uxinterpolator(ds["U"])) |
| 293 | + fields["UVW"] = VectorField("UVW", fields["U"], fields["V"], fields["W"]) |
| 294 | + else: |
| 295 | + fields["UV"] = VectorField("UV", fields["U"], fields["V"]) |
| 296 | + |
| 297 | + for varname in set(ds.data_vars) - set(fields.keys()): |
| 298 | + fields[varname] = Field(varname, ds[varname], grid, _select_uxinterpolator(ds[varname])) |
257 | 299 |
|
258 | 300 | return FieldSet(list(fields.values())) |
259 | 301 |
|
@@ -365,11 +407,86 @@ def _discover_copernicusmarine_U_and_V(ds: xr.Dataset) -> xr.Dataset: |
365 | 407 | return ds |
366 | 408 |
|
367 | 409 |
|
368 | | -def _ds_rename_using_standard_names(ds: xr.Dataset, name_dict: dict[str, str]) -> xr.Dataset: |
| 410 | +def _discover_fesom2_U_and_V(ds: ux.UxDataset) -> ux.UxDataset: |
| 411 | + # Common variable names for U and V found in UxDatasets |
| 412 | + common_fesom_UV = [("unod", "vnod"), ("u", "v")] |
| 413 | + common_fesom_W = ["w"] |
| 414 | + |
| 415 | + if "W" not in ds: |
| 416 | + for common_W in common_fesom_W: |
| 417 | + if common_W in ds: |
| 418 | + ds = _ds_rename_using_standard_names(ds, {common_W: "W"}) |
| 419 | + break |
| 420 | + |
| 421 | + if "U" in ds and "V" in ds: |
| 422 | + return ds # U and V already present |
| 423 | + elif "U" in ds or "V" in ds: |
| 424 | + raise ValueError( |
| 425 | + "Dataset has only one of the two variables 'U' and 'V'. Please rename the appropriate variable in your dataset to have both 'U' and 'V' for Parcels simulation." |
| 426 | + ) |
| 427 | + |
| 428 | + for common_U, common_V in common_fesom_UV: |
| 429 | + if common_U in ds: |
| 430 | + if common_V not in ds: |
| 431 | + raise ValueError( |
| 432 | + f"Dataset has variable with standard name {common_U!r}, " |
| 433 | + f"but not the matching variable with standard name {common_V!r}. " |
| 434 | + "Please rename the appropriate variables in your dataset to have both 'U' and 'V' for Parcels simulation." |
| 435 | + ) |
| 436 | + else: |
| 437 | + ds = _ds_rename_using_standard_names(ds, {common_U: "U", common_V: "V"}) |
| 438 | + break |
| 439 | + |
| 440 | + else: |
| 441 | + if common_V in ds: |
| 442 | + raise ValueError( |
| 443 | + f"Dataset has variable with standard name {common_V!r}, " |
| 444 | + f"but not the matching variable with standard name {common_U!r}. " |
| 445 | + "Please rename the appropriate variables in your dataset to have both 'U' and 'V' for Parcels simulation." |
| 446 | + ) |
| 447 | + continue |
| 448 | + |
| 449 | + return ds |
| 450 | + |
| 451 | + |
| 452 | +def _ds_rename_using_standard_names(ds: xr.Dataset | ux.UxDataset, name_dict: dict[str, str]) -> xr.Dataset: |
369 | 453 | for standard_name, rename_to in name_dict.items(): |
370 | 454 | name = ds.cf[standard_name].name |
371 | 455 | ds = ds.rename({name: rename_to}) |
372 | 456 | logger.info( |
373 | 457 | f"cf_xarray found variable {name!r} with CF standard name {standard_name!r} in dataset, renamed it to {rename_to!r} for Parcels simulation." |
374 | 458 | ) |
375 | 459 | return ds |
| 460 | + |
| 461 | + |
| 462 | +def _select_uxinterpolator(da: ux.UxDataArray): |
| 463 | + """Selects the appropriate uxarray interpolator for a given uxarray UxDataArray""" |
| 464 | + supported_uxinterp_mapping = { |
| 465 | + # (nz1,n_face): face-center laterally, layer centers vertically — piecewise constant |
| 466 | + "nz1,n_face": UXPiecewiseConstantFace, |
| 467 | + # (nz,n_node): node/corner laterally, layer interfaces vertically — barycentric lateral & linear vertical |
| 468 | + "nz,n_node": UXPiecewiseLinearNode, |
| 469 | + } |
| 470 | + # Extract only spatial dimensions, neglecting time |
| 471 | + da_spatial_dims = tuple(d for d in da.dims if d not in ("time",)) |
| 472 | + if len(da_spatial_dims) != 2: |
| 473 | + raise ValueError( |
| 474 | + "Fields on unstructured grids must have two spatial dimensions, one vertical (nz or nz1) and one lateral (n_face, n_edge, or n_node)" |
| 475 | + ) |
| 476 | + |
| 477 | + # Construct key (string) for mapping to interpolator |
| 478 | + # Find vertical and lateral tokens |
| 479 | + vdim = None |
| 480 | + ldim = None |
| 481 | + for d in da_spatial_dims: |
| 482 | + if d in ("nz", "nz1"): |
| 483 | + vdim = d |
| 484 | + if d in ("n_face", "n_node"): |
| 485 | + ldim = d |
| 486 | + # Map to supported interpolators |
| 487 | + if vdim and ldim: |
| 488 | + key = f"{vdim},{ldim}" |
| 489 | + if key in supported_uxinterp_mapping.keys(): |
| 490 | + return supported_uxinterp_mapping[key] |
| 491 | + |
| 492 | + return None |
0 commit comments