Repository: ecmwf/earthkit-data
Pull request #513 (2024-11-06 16:25:50)

Add more fieldlist methods to fields
This PR adds the following methods, which already exist for Fieldlists, to Fields:

- to_xarray()
- ls()
- describe() 


Patch:
diff --git a/docs/guide/sources.rst b/docs/guide/sources.rst
index 75aedca..dfa6417 100644
--- a/docs/guide/sources.rst
+++ b/docs/guide/sources.rst
@@ -510,7 +510,7 @@ list-of-dicts
 
   Other keys:
 
-    Other keys can be used to store additional metadata. They are not used by the field functionalities.
+    Other keys can be used to store additional metadata.
 
   Further examples:
 
diff --git a/src/earthkit/data/core/fieldlist.py b/src/earthkit/data/core/fieldlist.py
index 6e650b7..a1e863f 100644
--- a/src/earthkit/data/core/fieldlist.py
+++ b/src/earthkit/data/core/fieldlist.py
@@ -760,6 +760,48 @@ class Field(Base):
             self._metadata.override(**kwargs),
         )
 
+    def to_xarray(self, *args, **kwargs):
+        """Convert the Field into an Xarray Dataset.
+
+        Parameters
+        ----------
+        *args: tuple
+            Positional arguments passed to :obj:`FieldList.to_xarray`.
+        **kwargs: dict, optional
+            Other keyword arguments passed to :obj:`FieldList.to_xarray`.
+
+        Returns
+        -------
+        Xarray Dataset
+
+        """
+        return self._to_fieldlist().to_xarray(*args, **kwargs)
+
+    def ls(self, *args, **kwargs):
+        r"""Generate a list like summary using a set of metadata keys.
+
+        Parameters
+        ----------
+        *args: tuple
+            Positional arguments passed to :obj:`FieldList.ls`.
+        **kwargs: dict, optional
+            Other keyword arguments passed to :obj:`FieldList.ls`.
+
+        Returns
+        -------
+        Pandas DataFrame
+            DataFrame with one row.
+
+        """
+        return self._to_fieldlist().ls(*args, **kwargs)
+
+    def describe(self, *args, **kwargs):
+        r"""Generate a summary of the Field."""
+        return self._to_fieldlist().describe(*args, **kwargs)
+
+    def _to_fieldlist(self):
+        return FieldList.from_fields([self])
+
     @staticmethod
     def _flatten(v):
         """Flatten the array without copying the data."
@@ -1229,8 +1271,7 @@ class FieldList(Index):
             return []
 
     def head(self, n=5, **kwargs):
-        r"""Generate a list like summary of the first ``n``
-        :obj:`GribField <data.readers.grib.codes.GribField>`\ s using a set of metadata keys.
+        r"""Generate a list like summary of the first ``n`` :obj:`Field`\ s.
         Same as calling :obj:`ls` with ``n``.
 
         Parameters
@@ -1266,8 +1307,7 @@ class FieldList(Index):
         return self.ls(n=n, **kwargs)
 
     def tail(self, n=5, **kwargs):
-        r"""Generate a list like summary of the last ``n``
-        :obj:`GribField <data.readers.grib.codes.GribField>`\ s using a set of metadata keys.
+        r"""Generate a list like summary of the last ``n`` :obj:`Field`\ s.
         Same as calling :obj:`ls` with ``-n``.
 
         Parameters
diff --git a/src/earthkit/data/readers/grib/xarray.py b/src/earthkit/data/readers/grib/xarray.py
index df34afc..b906651 100644
--- a/src/earthkit/data/readers/grib/xarray.py
+++ b/src/earthkit/data/readers/grib/xarray.py
@@ -297,7 +297,7 @@ class XarrayMixIn:
 
         Returns
         -------
-        xarray Dataset or list of xarray Datasets
+        Xarray Dataset or list of Xarray Datasets
 
 
         The default values of ``xarray_open_dataset_kwargs`` or ``**kwargs`` passed


Test patch:
diff --git a/tests/grib/test_grib_summary.py b/tests/grib/test_grib_summary.py
index c064ea0..e0c1870 100644
--- a/tests/grib/test_grib_summary.py
+++ b/tests/grib/test_grib_summary.py
@@ -145,6 +145,35 @@ def test_grib_describe(fl_type):
     assert ref[0] == df[0].to_dict()
 
 
+@pytest.mark.parametrize("fl_type", FL_TYPES)
+def test_grib_describe_single_field(fl_type):
+    f_in, _ = load_grib_data("tuv_pl.grib", fl_type)
+    f = f_in[0]
+
+    # full contents
+    df = f.describe()
+    df = df.data
+
+    ref = {
+        "level": {("t", "isobaricInhPa"): "1000"},
+        "date": {("t", "isobaricInhPa"): "20180801"},
+        "time": {("t", "isobaricInhPa"): "1200"},
+        "step": {("t", "isobaricInhPa"): "0"},
+        "paramId": {("t", "isobaricInhPa"): "130"},
+        "class": {("t", "isobaricInhPa"): "od"},
+        "stream": {("t", "isobaricInhPa"): "oper"},
+        "type": {("t", "isobaricInhPa"): "an"},
+        "experimentVersionNumber": {("t", "isobaricInhPa"): "0001"},
+    }
+
+    assert ref == df.to_dict()
+
+    # repeated use
+    df = f.describe()
+    df = df.data
+    assert ref == df.to_dict()
+
+
 @pytest.mark.parametrize("fl_type", FL_TYPES)
 def test_grib_ls(fl_type):
     f, _ = load_grib_data("tuv_pl.grib", fl_type)
@@ -311,6 +340,52 @@ def test_grib_ls_num(fl_type):
     assert ref == df.to_dict()
 
 
+@pytest.mark.parametrize("fl_type", FL_TYPES)
+def test_grib_ls_single_field(fl_type):
+    f, _ = load_grib_data("tuv_pl.grib", fl_type)
+
+    # default keys
+    f1 = f[0]
+    df = f1.ls()
+
+    ref = {
+        "centre": {0: "ecmf"},
+        "shortName": {0: "t"},
+        "typeOfLevel": {
+            0: "isobaricInhPa",
+        },
+        "level": {0: 1000},
+        "dataDate": {0: 20180801},
+        "dataTime": {0: 1200},
+        "stepRange": {0: "0"},
+        "dataType": {0: "an"},
+        "number": {0: 0},
+        "gridType": {0: "regular_ll"},
+    }
+
+    assert ref == df.to_dict()
+
+    # extra keys
+    f1 = f[0]
+    df = f1.ls(extra_keys=["paramId"])
+
+    ref = {
+        "centre": {0: "ecmf"},
+        "shortName": {0: "t"},
+        "typeOfLevel": {0: "isobaricInhPa"},
+        "level": {0: 1000},
+        "dataDate": {0: 20180801},
+        "dataTime": {0: 1200},
+        "stepRange": {0: "0"},
+        "dataType": {0: "an"},
+        "number": {0: 0},
+        "gridType": {0: "regular_ll"},
+        "paramId": {0: 130},
+    }
+
+    assert ref == df.to_dict()
+
+
 @pytest.mark.parametrize("fl_type", FL_TYPES)
 def test_grib_head_num(fl_type):
     f, _ = load_grib_data("tuv_pl.grib", fl_type)
diff --git a/tests/xr_engine/test_xr_engine.py b/tests/xr_engine/test_xr_engine.py
index 1409224..f68ecc2 100644
--- a/tests/xr_engine/test_xr_engine.py
+++ b/tests/xr_engine/test_xr_engine.py
@@ -70,9 +70,7 @@ def test_xr_engine_detailed_check(api):
             decode_timedelta=False,
             add_valid_time_coord=False,
         )
-    ds = ds_ek.to_xarray(
-        time_dim_mode="raw", decode_times=False, decode_timedelta=False, add_valid_time_coord=False
-    )
+
     assert ds is not None
 
     # dataset
@@ -451,3 +449,59 @@ def test_xr_engine_dtype():
 
     ds = ds_ek.to_xarray(dtype=np.float64)
     assert ds["t"].values.dtype == np.float64
+
+
+@pytest.mark.cache
+def test_xr_engine_single_field():
+    ds_ek = from_source("url", earthkit_remote_test_data_file("test-data/xr_engine/level/pl.grib"))
+    ds_ek = ds_ek[0]
+
+    ds = ds_ek.to_xarray(
+        time_dim_mode="raw",
+        decode_times=False,
+        decode_timedelta=False,
+        add_valid_time_coord=False,
+        squeeze=True,
+    )
+
+    lats = np.linspace(90, -90, 19)
+    lons = np.linspace(0, 350, 36)
+
+    attrs_ref = {
+        "param": "t",
+        "standard_name": "air_temperature",
+        "long_name": "Temperature",
+        "paramId": 130,
+        "class": "od",
+        "stream": "oper",
+        "levtype": "pl",
+        "type": "fc",
+        "expver": "0001",
+        "date": 20240603,
+        "time": 0,
+        "domain": "g",
+        "number": 0,
+        "levelist": 1000,
+        "Conventions": "CF-1.8",
+        "institution": "ECMWF",
+    }
+
+    assert ds.attrs == attrs_ref
+
+    data_vars = ["t"]
+
+    coords_ref_full = {
+        "latitude": lats,
+        "longitude": lons,
+    }
+
+    dims_ref_full = {
+        "latitude": 19,
+        "longitude": 36,
+    }
+
+    assert len(ds.dims) == len(dims_ref_full)
+    assert len(ds.coords) == len(coords_ref_full)
+    for k, v in coords_ref_full.items():
+        assert np.allclose(ds.coords[k].values, v)
+    assert [v for v in ds.data_vars] == data_vars