|
13 | 13 | MutableMapping,
|
14 | 14 | Sequence,
|
15 | 15 | )
|
| 16 | +from dataclasses import dataclass |
16 | 17 | from datetime import datetime
|
17 | 18 | from typing import (
|
18 | 19 | Any,
|
|
82 | 83 |
|
83 | 84 | FlagParam = namedtuple("FlagParam", ["flag_mask", "flag_value"])
|
84 | 85 |
|
| 86 | + |
| 87 | +@dataclass(frozen=True, kw_only=True) |
| 88 | +class GridMapping: |
| 89 | + """ |
| 90 | + Represents a CF grid mapping with its properties and associated coordinate variables. |
| 91 | +
|
| 92 | + Attributes |
| 93 | + ---------- |
| 94 | + name : str |
| 95 | + The CF grid mapping name (e.g., ``'latitude_longitude'``, ``'transverse_mercator'``) |
| 96 | + crs : pyproj.CRS |
| 97 | + The coordinate reference system object |
| 98 | + array : xarray.DataArray |
| 99 | + The grid mapping variable as a DataArray containing the CRS parameters |
| 100 | + coordinates : tuple[str, ...] |
| 101 | + Names of coordinate variables associated with this grid mapping. For grid mappings |
| 102 | + that are explicitly listed with coordinates in the grid_mapping attribute |
| 103 | + (e.g., ``'spatial_ref: crs_4326: latitude longitude'``), this contains those coordinates. |
| 104 | + For grid mappings (e.g. ``spatial_ref``) that don't explicitly specify coordinates, |
| 105 | + this falls back to the dimension names of the data variable that references |
| 106 | + this grid mapping. |
| 107 | + """ |
| 108 | + |
| 109 | + name: str |
| 110 | + crs: Any # really pyproj.CRS |
| 111 | + array: xr.DataArray |
| 112 | + coordinates: tuple[str, ...] |
| 113 | + |
| 114 | + def __repr__(self) -> str: |
| 115 | + # Try to get EPSG code first, fallback to shorter description |
| 116 | + try: |
| 117 | + if hasattr(self.crs, "to_epsg") and self.crs.to_epsg(): |
| 118 | + crs_repr = f"<CRS: EPSG:{self.crs.to_epsg()}>" |
| 119 | + else: |
| 120 | + # Use the name if available, otherwise authority:code |
| 121 | + crs_name = getattr(self.crs, "name", str(self.crs)[:50] + "...") |
| 122 | + crs_repr = f"<CRS: {crs_name}>" |
| 123 | + except Exception: |
| 124 | + # Fallback to generic representation |
| 125 | + crs_repr = "<CRS>" |
| 126 | + |
| 127 | + # Short array representation - name and shape |
| 128 | + array_repr = f"<DataArray '{self.array.name}' {self.array.shape}>" |
| 129 | + |
| 130 | + # Format coordinates nicely |
| 131 | + coords_repr = f"({', '.join(repr(c) for c in self.coordinates)})" |
| 132 | + |
| 133 | + return ( |
| 134 | + f"GridMapping(name={self.name!r}, " |
| 135 | + f"crs={crs_repr}, " |
| 136 | + f"array={array_repr}, " |
| 137 | + f"coordinates={coords_repr})" |
| 138 | + ) |
| 139 | + |
| 140 | + |
85 | 141 | #: Classes wrapped by cf_xarray.
|
86 | 142 | _WRAPPED_CLASSES = (Resample, GroupBy, Rolling, Coarsen, Weighted)
|
87 | 143 |
|
@@ -2406,6 +2462,160 @@ def add_canonical_attributes(
|
2406 | 2462 |
|
2407 | 2463 | return obj
|
2408 | 2464 |
|
| 2465 | + def _create_grid_mapping( |
| 2466 | + self, |
| 2467 | + var_name: str, |
| 2468 | + obj_dataset: Dataset, |
| 2469 | + grid_mapping_dict: dict[str, list[str]], |
| 2470 | + ) -> GridMapping: |
| 2471 | + """ |
| 2472 | + Create a GridMapping dataclass instance from a grid mapping variable. |
| 2473 | +
|
| 2474 | + Parameters |
| 2475 | + ---------- |
| 2476 | + var_name : str |
| 2477 | + Name of the grid mapping variable |
| 2478 | + obj_dataset : Dataset |
| 2479 | + Dataset containing the grid mapping variable |
| 2480 | + grid_mapping_dict : dict[str, list[str]] |
| 2481 | + Dictionary mapping grid mapping variable names to their coordinate variables |
| 2482 | +
|
| 2483 | + Returns |
| 2484 | + ------- |
| 2485 | + GridMapping |
| 2486 | + GridMapping dataclass instance |
| 2487 | +
|
| 2488 | + Notes |
| 2489 | + ----- |
| 2490 | + Assumes pyproj is available (should be checked by caller). |
| 2491 | + """ |
| 2492 | + from pyproj import ( |
| 2493 | + CRS, # Safe to import since grid_mappings property checks availability |
| 2494 | + ) |
| 2495 | + |
| 2496 | + var = obj_dataset._variables[var_name] |
| 2497 | + |
| 2498 | + # Create DataArray from Variable, preserving the name |
| 2499 | + # Use reset_coords(drop=True) to avoid coordinate conflicts |
| 2500 | + if var_name in obj_dataset.coords: |
| 2501 | + da = obj_dataset.coords[var_name].reset_coords(drop=True) |
| 2502 | + else: |
| 2503 | + da = obj_dataset[var_name].reset_coords(drop=True) |
| 2504 | + |
| 2505 | + # Get the CF grid mapping name from the variable's attributes |
| 2506 | + cf_name = var.attrs.get("grid_mapping_name", var_name) |
| 2507 | + |
| 2508 | + # Create CRS from the grid mapping variable |
| 2509 | + try: |
| 2510 | + crs = CRS.from_cf(var.attrs) |
| 2511 | + except Exception: |
| 2512 | + # If CRS creation fails, use None |
| 2513 | + crs = None |
| 2514 | + |
| 2515 | + # Get associated coordinate variables, fallback to dimension names |
| 2516 | + coordinates = grid_mapping_dict.get(var_name, []) |
| 2517 | + if not coordinates: |
| 2518 | + # For DataArrays, find the data variable that references this grid mapping |
| 2519 | + for _data_var_name, data_var in obj_dataset.data_vars.items(): |
| 2520 | + if "grid_mapping" in data_var.attrs: |
| 2521 | + gm_attr = data_var.attrs["grid_mapping"] |
| 2522 | + if var_name in gm_attr: |
| 2523 | + coordinates = list(data_var.dims) |
| 2524 | + break |
| 2525 | + |
| 2526 | + return GridMapping( |
| 2527 | + name=cf_name, crs=crs, array=da, coordinates=tuple(coordinates) |
| 2528 | + ) |
| 2529 | + |
| 2530 | + @property |
| 2531 | + def grid_mappings(self) -> tuple[GridMapping, ...]: |
| 2532 | + """ |
| 2533 | + Return a tuple of GridMapping objects for all grid mappings in this object. |
| 2534 | +
|
| 2535 | + For DataArrays, the order in the tuple matches the order that grid mappings appear |
| 2536 | + in the grid_mapping attribute string. |
| 2537 | +
|
| 2538 | + Parameters |
| 2539 | + ---------- |
| 2540 | + None |
| 2541 | +
|
| 2542 | + Returns |
| 2543 | + ------- |
| 2544 | + tuple[GridMapping, ...] |
| 2545 | + Tuple of GridMapping dataclass instances, each containing: |
| 2546 | + - name: CF grid mapping name |
| 2547 | + - crs: pyproj.CRS object |
| 2548 | + - array: xarray.DataArray containing the grid mapping variable |
| 2549 | + - coordinates: tuple of coordinate variable names |
| 2550 | +
|
| 2551 | + Raises |
| 2552 | + ------ |
| 2553 | + ImportError |
| 2554 | + If pyproj is not available. This property requires pyproj for CRS creation. |
| 2555 | +
|
| 2556 | + Examples |
| 2557 | + -------- |
| 2558 | + >>> ds.cf.grid_mappings |
| 2559 | + (GridMapping(name='latitude_longitude', crs=<CRS: EPSG:4326>, ...),) |
| 2560 | +
|
| 2561 | + Notes |
| 2562 | + ----- |
| 2563 | + This property requires pyproj to be installed for creating CRS objects from |
| 2564 | + CF grid mapping parameters. Install with: ``conda install pyproj`` or |
| 2565 | + ``pip install pyproj``. |
| 2566 | + """ |
| 2567 | + # Check pyproj availability upfront |
| 2568 | + try: |
| 2569 | + import pyproj # noqa: F401 |
| 2570 | + except ImportError: |
| 2571 | + raise ImportError( |
| 2572 | + "pyproj is required for .cf.grid_mappings property. " |
| 2573 | + "Install with: conda install pyproj or pip install pyproj" |
| 2574 | + ) from None |
| 2575 | + # For DataArrays, preserve order from grid_mapping attribute |
| 2576 | + if isinstance(self._obj, DataArray) and "grid_mapping" in self._obj.attrs: |
| 2577 | + grid_mapping_dict = _parse_grid_mapping_attribute( |
| 2578 | + self._obj.attrs["grid_mapping"] |
| 2579 | + ) |
| 2580 | + # Get grid mappings in the order they appear in the string |
| 2581 | + ordered_var_names = list(grid_mapping_dict.keys()) |
| 2582 | + else: |
| 2583 | + # For Datasets, look for grid_mapping attributes in data variables |
| 2584 | + grid_mapping_dict = {} |
| 2585 | + ordered_var_names = [] |
| 2586 | + |
| 2587 | + # Search all data variables for grid_mapping attributes |
| 2588 | + for _var_name, var in self._obj.data_vars.items(): |
| 2589 | + if "grid_mapping" in var.attrs: |
| 2590 | + parsed = _parse_grid_mapping_attribute(var.attrs["grid_mapping"]) |
| 2591 | + grid_mapping_dict.update(parsed) |
| 2592 | + # Add variables in order they appear in this grid_mapping string |
| 2593 | + for gm_var in parsed.keys(): |
| 2594 | + if gm_var not in ordered_var_names: |
| 2595 | + ordered_var_names.append(gm_var) |
| 2596 | + |
| 2597 | + # If no grid_mapping attributes found in data vars, try grid_mapping_names property |
| 2598 | + if not ordered_var_names and hasattr(self, "grid_mapping_names"): |
| 2599 | + grid_mapping_names = self.grid_mapping_names |
| 2600 | + for var_names in grid_mapping_names.values(): |
| 2601 | + ordered_var_names.extend(var_names) |
| 2602 | + |
| 2603 | + if not ordered_var_names: |
| 2604 | + return () |
| 2605 | + |
| 2606 | + grid_mappings = [] |
| 2607 | + obj_dataset = self._maybe_to_dataset() |
| 2608 | + |
| 2609 | + for var_name in ordered_var_names: |
| 2610 | + if var_name not in obj_dataset._variables: |
| 2611 | + continue |
| 2612 | + |
| 2613 | + grid_mappings.append( |
| 2614 | + self._create_grid_mapping(var_name, obj_dataset, grid_mapping_dict) |
| 2615 | + ) |
| 2616 | + |
| 2617 | + return tuple(grid_mappings) |
| 2618 | + |
2409 | 2619 |
|
2410 | 2620 | @xr.register_dataset_accessor("cf")
|
2411 | 2621 | class CFDatasetAccessor(CFAccessor):
|
@@ -3009,8 +3219,7 @@ def grid_mapping_names(self) -> dict[str, list[str]]:
|
3009 | 3219 | grid_mapping_var = da.coords[grid_mapping_var_name]
|
3010 | 3220 | if gmn := grid_mapping_var.attrs.get("grid_mapping_name"):
|
3011 | 3221 | results[gmn].append(grid_mapping_var_name)
|
3012 |
| - |
3013 |
| - return results |
| 3222 | + return dict(results) |
3014 | 3223 |
|
3015 | 3224 | @property
|
3016 | 3225 | def grid_mapping_name(self) -> str:
|
|
0 commit comments