Skip to content

Commit bbe7b0c

Browse files
authored
Add attributes to generated variables. (#45)
1 parent a612017 commit bbe7b0c

File tree

3 files changed

+88
-6
lines changed

3 files changed

+88
-6
lines changed

docs/raster_index/design_choices.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,9 @@ Thus, {py:func}`assign_index` will delete the `"GeoTransform"` attribute on the
5959
to construct the affine matrix.
6060

6161
If you wish to extract the GeoTransform attribute to write it to a location of your choosing use {py:meth}`RasterIndex.as_geotransform`.
62+
63+
## Attributes on coordinate variables
64+
65+
Currently `assign_index` will first drop any existing coordinate variables and then recreate them as lazy coordinate variables.
66+
This loses any existing attributes, and new attributes appropriate to the coordinate system (generated by {py:meth}`pyproj.CRS.cs_to_cf`)
67+
are added.

src/rasterix/raster_index.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,8 @@ def from_transform(
494494
height: int,
495495
x_dim: str = "x",
496496
y_dim: str = "y",
497+
x_coord_name: str = "xc",
498+
y_coord_name: str = "yc",
497499
crs: CRS | Any | None = None,
498500
) -> RasterIndex:
499501
"""Create a RasterIndex from an affine transform and raster dimensions.
@@ -507,10 +509,14 @@ def from_transform(
507509
Number of pixels in the x direction.
508510
height : int
509511
Number of pixels in the y direction.
510-
x_dim : str, default "x"
512+
x_dim : str, optional
511513
Name for the x dimension.
512-
y_dim : str, default "y"
514+
y_dim : str, optional
513515
Name for the y dimension.
516+
x_coord_name : str, optional
517+
Name for the x dimension. For non-rectilinear transforms only.
518+
y_coord_name : str, optional
519+
Name for the y dimension. For non-rectilinear transforms only.
514520
crs : :class:`pyproj.crs.CRS` or any, optional
515521
The coordinate reference system. Any value accepted by
516522
:meth:`pyproj.crs.CRS.from_user_input`.
@@ -533,21 +539,35 @@ def from_transform(
533539
>>> from affine import Affine
534540
>>> transform = Affine(1.0, 0.0, 0.0, 0.0, -1.0, 100.0)
535541
>>> index = RasterIndex.from_transform(transform, width=100, height=100)
542+
543+
Create a non-rectilinear index:
544+
545+
>>> index = RasterIndex.from_transform(
546+
... Affine.rotation(45), width=100, height=100, x_coord_name="x1", y_coord_name="x2"
547+
... )
536548
"""
537549
index: WrappedIndex
538550

539551
# pixel centered coordinates
540552
affine = affine * Affine.translation(0.5, 0.5)
541553

542554
if affine.is_rectilinear and affine.b == affine.d == 0:
543-
x_transform = AxisAffineTransform(affine, width, "x", x_dim, is_xaxis=True)
544-
y_transform = AxisAffineTransform(affine, height, "y", y_dim, is_xaxis=False)
555+
x_transform = AxisAffineTransform(affine, width, x_dim, x_dim, is_xaxis=True)
556+
y_transform = AxisAffineTransform(affine, height, y_dim, y_dim, is_xaxis=False)
545557
index = (
546558
AxisAffineTransformIndex(x_transform),
547559
AxisAffineTransformIndex(y_transform),
548560
)
549561
else:
550-
xy_transform = AffineTransform(affine, width, height, x_dim=x_dim, y_dim=y_dim)
562+
xy_transform = AffineTransform(
563+
affine,
564+
width,
565+
height,
566+
x_dim=x_dim,
567+
y_dim=y_dim,
568+
x_coord_name=x_coord_name,
569+
y_coord_name=y_coord_name,
570+
)
551571
index = CoordinateTransformIndex(xy_transform)
552572

553573
return cls(index, crs=crs)
@@ -568,6 +588,18 @@ def create_variables(self, variables: Mapping[Any, Variable] | None = None) -> d
568588
for index in self._wrapped_indexes:
569589
new_variables.update(index.create_variables())
570590

591+
if self.crs is not None:
592+
xname, yname = self.xy_coord_names
593+
xattrs, yattrs = self.crs.cs_to_cf()
594+
if "axis" in xattrs and "axis" in yattrs:
595+
# The axis order is defined by the projection
596+
# So we have to figure which is which.
597+
# This is an ugly hack that works for common cases.
598+
if xattrs["axis"] == "Y":
599+
xattrs, yattrs = yattrs, xattrs
600+
new_variables[xname].attrs = xattrs
601+
new_variables[yname].attrs = yattrs
602+
571603
return new_variables
572604

573605
@property

tests/test_raster_index.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ def test_raster_index_properties():
7575
)
7676
assert index3.as_geotransform(decimals=6) == "0.000000 0.707107 -0.707107 0.000000 0.707107 0.707107"
7777

78+
index4 = RasterIndex.from_transform(
79+
Affine.rotation(45.0), width=12, height=10, x_coord_name="x1", y_coord_name="x2"
80+
)
81+
assert index4.xy_coord_names == ("x1", "x2")
82+
7883

7984
# TODO: parameterize over
8085
# 1. y points up;
@@ -107,12 +112,51 @@ def test_sel_slice():
107112
assert_identical(reverse.y, ds.y[:5:-1])
108113

109114

110-
def test_crs() -> None:
115+
def test_crs_generated_attributes() -> None:
111116
index = RasterIndex.from_transform(Affine.identity(), width=12, height=10)
112117
assert index.crs is None
118+
variables = index.create_variables()
119+
assert variables["x"].attrs == {}
120+
assert variables["y"].attrs == {}
113121

114122
index = RasterIndex.from_transform(Affine.identity(), width=12, height=10, crs="epsg:31370")
115123
assert index.crs == pyproj.CRS.from_user_input("epsg:31370")
124+
variables = index.create_variables()
125+
assert variables["x"].attrs == {
126+
"axis": "X",
127+
"long_name": "Easting",
128+
"standard_name": "projection_x_coordinate",
129+
"units": "metre",
130+
}
131+
assert variables["y"].attrs == {
132+
"axis": "Y",
133+
"long_name": "Northing",
134+
"standard_name": "projection_y_coordinate",
135+
"units": "metre",
136+
}
137+
138+
index = RasterIndex.from_transform(
139+
Affine.identity(),
140+
width=12,
141+
height=10,
142+
x_dim="lon",
143+
y_dim="lat",
144+
crs="epsg:4326",
145+
)
146+
assert index.crs == pyproj.CRS.from_user_input("epsg:4326")
147+
variables = index.create_variables()
148+
assert variables["lon"].attrs == {
149+
"axis": "X",
150+
"long_name": "longitude coordinate",
151+
"standard_name": "longitude",
152+
"units": "degrees_east",
153+
}
154+
assert variables["lat"].attrs == {
155+
"axis": "Y",
156+
"long_name": "latitude coordinate",
157+
"standard_name": "latitude",
158+
"units": "degrees_north",
159+
}
116160

117161

118162
# asserting (in)equality for both "x" and "y" is redundant but not harmful

0 commit comments

Comments
 (0)