Skip to content

Commit

Permalink
ADD: Transform function CF1 to CF2 and vice versa (#224)
Browse files Browse the repository at this point in the history
* ADD: Transform function CF1 to CF2
* FIX: Linting
* Replace datatree with xarray in docstrings
* ADD: Transform notebook to docs/usage
* ADD: transform module to history
* ENH: Tests for accessors
* ENH: Enhance pytest and coverage for accessors
* FIX: Markdown Fix
  • Loading branch information
syedhamidali authored Oct 29, 2024
1 parent 1336b60 commit be4730e
Show file tree
Hide file tree
Showing 10 changed files with 557 additions and 15 deletions.
1 change: 1 addition & 0 deletions docs/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

This is the first version which uses datatree directly from xarray. Thus, xarray is pinned to version >= 2024.10.0.

* ADD: Added new module `transform` for transforming CF1 data to CF2 and vice versa ({pull}`224`) by [@syedhamidali](https://github.com/syedhamidali).
* Use DataTree from xarray and add xarray nightly run ({pull}`213`, {pull}`214`, {pull}`215`, {pull}`218`) by [@kmuehlbauer](https://github.com/kmuehlbauer).
* ADD: Added new accessor `map_over_sweeps` for volume operations on DataTrees and a matching decorator ({pull}`203`) by [@syedhamidali](https://github.com/syedhamidali).

Expand Down
1 change: 1 addition & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ notebooks/Mapping_Sweeps
notebooks/CfRadial1_Model_Transformation
notebooks/CfRadial1
notebooks/CfRadial1_Export
notebooks/Transform
notebooks/ODIM_H5
notebooks/GAMIC
notebooks/Furuno
Expand Down
164 changes: 164 additions & 0 deletions examples/notebooks/Transform.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Transform"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Imports"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import xarray as xr\n",
"from open_radar_data import DATASETS\n",
"\n",
"import xradar as xd"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Load Data"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"file = DATASETS.fetch(\"cfrad.20080604_002217_000_SPOL_v36_SUR.nc\")\n",
"dtree = xd.io.open_cfradial1_datatree(file)\n",
"display(dtree)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Transform CF2 to CF1"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"ds = dtree.xradar.to_cfradial1_dataset()\n",
"display(ds)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Transform CF1 to CF2"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dtree = ds.xradar.to_cfradial2_datatree()\n",
"display(dtree)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"del ds, dtree"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Alternate Method"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can directly use xarray to read the data and then transform it to CF2 datatree."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"ds = xr.open_dataset(file)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"ds"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"radar = ds.xradar.to_cf2()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"display(radar)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
120 changes: 120 additions & 0 deletions tests/test_accessors.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,66 @@ def test_georeference_datatree():
)


def test_crs_dataarray():
"""Test the add_crs and get_crs methods on a DataArray."""
# Create a sample DataArray with radar data
radar = xd.model.create_sweep_dataset()
radar["sample_field"] = radar.azimuth + radar.range
dataarray = radar.sample_field

# Apply add_crs to add coordinate system
da_with_crs = dataarray.xradar.add_crs()

# Check if either 'spatial_ref' or 'crs_wkt' was added as coordinate
assert (
"spatial_ref" in da_with_crs.coords or "crs_wkt" in da_with_crs.coords
), "CRS coordinate is missing in DataArray"

# Retrieve CRS using get_crs
crs = da_with_crs.xradar.get_crs()

# Verify that the CRS is valid and projected
assert crs is not None, "CRS could not be retrieved from DataArray"
assert crs.is_projected, "Expected a projected CRS in DataArray"


def test_crs_dataset():
"""Test the add_crs and get_crs methods on a Dataset."""
# Create a sample Dataset with radar data
radar = xd.model.create_sweep_dataset()

# Apply add_crs to add coordinate system
ds_with_crs = radar.xradar.add_crs()

# Check if either 'spatial_ref' or 'crs_wkt' was added as coordinate
assert (
"spatial_ref" in ds_with_crs.coords or "crs_wkt" in ds_with_crs.coords
), "CRS coordinate is missing in Dataset"

# Retrieve CRS using get_crs
crs = ds_with_crs.xradar.get_crs()

# Verify that the CRS is valid and projected
assert crs is not None, "CRS could not be retrieved from Dataset"
assert crs.is_projected, "Expected a projected CRS in Dataset"


def test_crs_datatree():
"""Test the add_crs method on a DataTree."""
# Create a sample DataTree with radar data
radar = xd.model.create_sweep_dataset()
tree = xr.DataTree.from_dict({"sweep_0": radar})

# Apply add_crs to add coordinate system
tree_with_crs = tree.xradar.add_crs()

# Check if either 'spatial_ref' or 'crs_wkt' was added as coordinate in each sweep
assert (
"spatial_ref" in tree_with_crs["sweep_0"].coords
or "crs_wkt" in tree_with_crs["sweep_0"].coords
), "CRS coordinate is missing in DataTree"


def test_map_over_sweeps_apply_dummy_function():
"""
Test applying a dummy function to all sweep nodes using map_over_sweeps.
Expand Down Expand Up @@ -128,3 +188,63 @@ def invalid_rain_rate(ds, ref_field="INVALID_FIELD"):
# Expect a KeyError when applying the function with an invalid field
with assert_raises(KeyError):
tree.xradar.map_over_sweeps(invalid_rain_rate, ref_field="INVALID_FIELD")


def test_accessor_to_cfradial1():
"""Test the accessor function to convert DataTree to CfRadial1 Dataset."""
file = DATASETS.fetch("cfrad.20080604_002217_000_SPOL_v36_SUR.nc")
dtree = xd.io.open_cfradial1_datatree(file)

# Use accessor method to convert to CfRadial1
ds_cf1 = dtree.xradar.to_cfradial1_dataset()

# Test alias for conversion to CfRadial1
ds_cf1_alias = dtree.xradar.to_cf1()

# Verify key properties of the resulting dataset
assert isinstance(ds_cf1, xr.Dataset), "Conversion to CfRadial1 failed"
assert "sweep_mode" in ds_cf1.variables, "Missing sweep_mode in CfRadial1 dataset"

# Verify alias
assert isinstance(ds_cf1_alias, xr.Dataset), "Alias conversion to CfRadial1 failed"
assert (
"sweep_mode" in ds_cf1_alias.variables
), "Missing sweep_mode in CfRadial1 dataset"


def test_accessor_to_cfradial2():
"""Test the accessor function to convert CfRadial1 Dataset back to DataTree."""
file = DATASETS.fetch("cfrad.20080604_002217_000_SPOL_v36_SUR.nc")
dtree = xd.io.open_cfradial1_datatree(file)

# Convert to CfRadial1 dataset
ds_cf1 = dtree.xradar.to_cfradial1_dataset()

# Use accessor method to convert back to CfRadial2 DataTree
dtree_cf2 = ds_cf1.xradar.to_cfradial2_datatree()

# Test aliases for CfRadial2 conversion
dtree_cf2_alias1 = ds_cf1.xradar.to_cfradial2()
dtree_cf2_alias2 = ds_cf1.xradar.to_cf2()

# Verify the properties of the resulting DataTree
assert isinstance(dtree_cf2, xr.DataTree), "Conversion to CfRadial2 failed"
assert (
"radar_parameters" in dtree_cf2
), "Missing radar_parameters in CfRadial2 DataTree"

# Verify alias1
assert isinstance(
dtree_cf2_alias1, xr.DataTree
), "Alias conversion to CfRadial2 failed"
assert (
"radar_parameters" in dtree_cf2_alias1
), "Missing radar_parameters in CfRadial2 DataTree"

# Verify alias2
assert isinstance(
dtree_cf2_alias2, xr.DataTree
), "Alias conversion to CfRadial2 failed"
assert (
"radar_parameters" in dtree_cf2_alias2
), "Missing radar_parameters in CfRadial2 DataTree"
40 changes: 40 additions & 0 deletions tests/transform/test_cfradial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env python
# Copyright (c) 2024, openradar developers.
# Distributed under the MIT License. See LICENSE for more info.

import xarray as xr
from open_radar_data import DATASETS

import xradar as xd


def test_to_cfradial1():
"""Test the conversion from DataTree to CfRadial1 format."""
file = DATASETS.fetch("cfrad.20080604_002217_000_SPOL_v36_SUR.nc")
dtree = xd.io.open_cfradial1_datatree(file)

# Call the conversion function
ds_cf1 = xd.transform.to_cfradial1(dtree)

# Verify key attributes and data structures in the resulting dataset
assert isinstance(ds_cf1, xr.Dataset), "Output is not a valid xarray Dataset"
assert "Conventions" in ds_cf1.attrs and ds_cf1.attrs["Conventions"] == "Cf/Radial"
assert "sweep_mode" in ds_cf1.variables, "Missing sweep_mode in converted dataset"
assert ds_cf1.attrs["version"] == "1.2", "Incorrect CfRadial version"


def test_to_cfradial2():
"""Test the conversion from CfRadial1 to CfRadial2 DataTree format."""
file = DATASETS.fetch("cfrad.20080604_002217_000_SPOL_v36_SUR.nc")
dtree = xd.io.open_cfradial1_datatree(file)

# Convert to CfRadial1 dataset first
ds_cf1 = xd.transform.to_cfradial1(dtree)

# Call the conversion back to CfRadial2
dtree_cf2 = xd.transform.to_cfradial2(ds_cf1)

# Verify key attributes and data structures in the resulting datatree
assert isinstance(dtree_cf2, xr.DataTree), "Output is not a valid DataTree"
assert "radar_parameters" in dtree_cf2, "Missing radar_parameters in DataTree"
assert dtree_cf2.attrs == ds_cf1.attrs, "Attributes mismatch between formats"
1 change: 1 addition & 0 deletions xradar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@
from . import model # noqa
from . import util # noqa
from .util import map_over_sweeps # noqa
from . import transform # noqa

__all__ = [s for s in dir() if not s.startswith("_")]
Loading

0 comments on commit be4730e

Please sign in to comment.