Grid¶
aereo.grid ¶
Grid module built on the Major TOM grid (via majortom_eg).
GridCell represents a raw MajorTOM grid cell. Use cell.to_geobox(resolution=..., ...) to obtain an extraction-ready GeoBox.
GridCell ¶
A single raw MajorTOM grid cell.
A GridCell only describes the grid geometry: its identifier, size, and WGS84 footprint. Extraction-specific parameters such as resolution and margin are provided when building a GeoBox through :meth:to_geobox.
to_geodataframe ¶
to_geodataframe()
Convert the GridCell to a GeoDataFrame.
Source code in components/aereo/grid/core.py
def to_geodataframe(self) -> GeoDataFrame[GridSchema]:
"""Convert the GridCell to a GeoDataFrame."""
gdf = gpd.GeoDataFrame(
{
"grid_cell": [self.id],
"grid_dist": [self.d],
"cell_geometry": gpd.GeoSeries([self.cell_geometry], crs="EPSG:4326"),
"cell_utm_crs": [self.utm_crs],
"cell_utm_footprint": gpd.GeoSeries(
[self.utm_footprint], crs=self.utm_crs
),
},
geometry="cell_geometry",
crs="EPSG:4326",
)
return cast(GeoDataFrame, GridSchema.validate(gdf))
area_name ¶
area_name(resolution)
Get the area name based on grid cell and resolution in metres.
Source code in components/aereo/grid/core.py
def area_name(self, resolution: float) -> str:
"""Get the area name based on grid cell and *resolution* in metres."""
if isinstance(resolution, float) and resolution.is_integer():
res_str = str(int(resolution))
else:
res_str = str(resolution)
return f"{self.id}_dist-{self.d}m_res-{res_str}m"
to_geobox ¶
to_geobox(
resolution, margin=0.0, alignment_resolution=None
)
Return an odc-geo GeoBox for this cell's UTM footprint.
The GeoBox is centred on the reprojected WGS84 centroid and its dimensions are an integer number of pixels, so it covers at least d * (1 + margin/100) metres in each direction. The centre and half-width are snapped to alignment_resolution (defaulting to resolution) and the bounding box is built with anchor='edge', which preserves the snapped extent.
To guarantee that nested resolutions share the same origin (e.g. VIIRS at 400 m and GOES at 2000 m), pass the coarser resolution as alignment_resolution when building the finer-resolution GeoBox. The half-width is then rounded up to a multiple of both resolutions, so the finer grid is an exact refinement of the coarser one.
| PARAMETER | DESCRIPTION |
|---|---|
resolution | Target pixel resolution in metres. TYPE: |
margin | Percentage margin added to the patch's nominal size (e.g. 5.0 for 5%). TYPE: |
alignment_resolution | Grid to which the centre and half-width are snapped. Defaults to TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
GeoBox | A GeoBox aligned to the cell's UTM grid point. |
Source code in components/aereo/grid/core.py
def to_geobox(
self,
resolution: float,
margin: float = 0.0,
alignment_resolution: float | None = None,
) -> GeoBox:
"""Return an odc-geo GeoBox for this cell's UTM footprint.
The GeoBox is centred on the reprojected WGS84 centroid and its
dimensions are an integer number of pixels, so it covers at least
``d * (1 + margin/100)`` metres in each direction. The centre and
half-width are snapped to ``alignment_resolution`` (defaulting to
``resolution``) and the bounding box is built with ``anchor='edge'``,
which preserves the snapped extent.
To guarantee that nested resolutions share the same origin (e.g. VIIRS
at 400 m and GOES at 2000 m), pass the coarser resolution as
``alignment_resolution`` when building the finer-resolution GeoBox. The
half-width is then rounded up to a multiple of both resolutions, so the
finer grid is an exact refinement of the coarser one.
Args:
resolution: Target pixel resolution in metres.
margin: Percentage margin added to the patch's nominal size
(e.g. 5.0 for 5%).
alignment_resolution: Grid to which the centre and half-width are
snapped. Defaults to ``resolution``. Use a coarser resolution
when you need pixel-level alignment across nested extractions.
Returns:
A GeoBox aligned to the cell's UTM grid point.
"""
utm_centroid = cast(
Point,
reproject_geom(
self.cell_geometry.centroid, src_epsg=_WGS84_CRS, dst_epsg=self.utm_crs
),
)
align_res = (
alignment_resolution if alignment_resolution is not None else resolution
)
# Snap the centre to the alignment grid. Using the same alignment grid
# for different resolutions keeps origins stable.
cx = round(utm_centroid.x / align_res) * align_res
cy = round(utm_centroid.y / align_res) * align_res
crs = self.utm_crs
nominal_half = (self.d * (1 + margin / 100)) / 2
# Round the half-width up to a multiple of both the target resolution
# and the alignment resolution so the output shape is an integer number
# of pixels at both resolutions.
step = math.lcm(int(round(resolution)), int(round(align_res)))
exact_half = math.ceil(nominal_half / step) * step
bbox = (cx - exact_half, cy - exact_half, cx + exact_half, cy + exact_half)
# anchor='edge' preserves the snapped bounding box instead of expanding
# it to centre a pixel, giving deterministic origins across resolutions.
return GeoBox.from_bbox(bbox, crs, resolution=resolution, anchor="edge")
GridDefinition ¶
GridDefinition(d=10000, overlap=False)
Bases: MajorTomGrid
A grid definition that generates cells intersecting a given polygon.
Wraps MajorTomGrid to produce raw geographic cells and IDs, and is consumed by build_grid_cells to create GridCell instances.
Initialize a grid definition.
| PARAMETER | DESCRIPTION |
|---|---|
d | Cell size in meters passed to TYPE: |
overlap | Whether to generate half-offset overlap cells. TYPE: |
Source code in components/aereo/grid/core.py
def __init__(self, d: int = 10000, overlap: bool = False) -> None:
"""Initialize a grid definition.
Args:
d: Cell size in meters passed to ``MajorTomGrid``.
overlap: Whether to generate half-offset overlap cells.
"""
super().__init__(d=d, overlap=overlap)
raw_cell_from_id ¶
raw_cell_from_id(cell_id)
Return a raw cell (geom, id, is_primary) for the given cell ID.
Source code in components/aereo/grid/core.py
def raw_cell_from_id(self, cell_id: str) -> tuple[Polygon, str, bool]:
"""Return a raw cell (geom, id, is_primary) for the given cell ID."""
if "_" not in cell_id:
base_cell = super().cell_from_id(cell_id)
return base_cell.geom, cell_id, base_cell.is_primary
parts = cell_id.split("_")
y_str, x_str = parts[0], parts[1]
is_primary = _OVERLAP_SUFFIX not in parts
row_idx = self._row_id_to_index(y_str)
row_lat = self.get_row_lat(row_idx)
col_idx = self._col_id_to_index(x_str, row_lat)
lon_spacing = self.get_lon_spacing(row_lat)
return self._cell_from_indices(
row_idx, col_idx, lon_spacing, is_primary, cell_id
)
get_cell_name ¶
get_cell_name(
row_idx, col_idx, lon_spacing, is_primary=True
)
Build a cell identifier from row/column indices.
| PARAMETER | DESCRIPTION |
|---|---|
row_idx | Row index in the global grid. TYPE: |
col_idx | Column index in the global grid. TYPE: |
lon_spacing | Longitude spacing at the row's latitude. TYPE: |
is_primary | If False, append the overlap suffix. TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
str | Cell identifier such as |
Source code in components/aereo/grid/core.py
def get_cell_name(
self,
row_idx: int,
col_idx: int,
lon_spacing: float,
is_primary: bool = True,
) -> str:
"""Build a cell identifier from row/column indices.
Args:
row_idx: Row index in the global grid.
col_idx: Column index in the global grid.
lon_spacing: Longitude spacing at the row's latitude.
is_primary: If False, append the overlap suffix.
Returns:
Cell identifier such as ``"0U_0R"`` or ``"0U_0R_OV"``.
"""
name = f"{self._row_index_to_id(row_idx)}_{self._col_index_to_id(col_idx, lon_spacing)}"
if not is_primary:
name += f"_{_OVERLAP_SUFFIX}"
return name
generate_raw_cells ¶
generate_raw_cells(polygon)
Generate raw grid cells that intersect with the given polygon.
Source code in components/aereo/grid/core.py
def generate_raw_cells(
self, polygon: BaseGeometry
) -> Sequence[tuple[Polygon, str, bool]]:
"""Generate raw grid cells that intersect with the given polygon."""
shapely.prepare(polygon)
min_lon, min_lat, max_lon, max_lat = polygon.bounds
if min_lon > max_lon:
max_lon += 360
start_row = int(np.floor((min_lat + 90 - self._lat_offset) / self.lat_spacing))
end_row = int(np.ceil((max_lat + 90 - self._lat_offset) / self.lat_spacing))
start_row, end_row = _expand_bounds(
start_row, end_row, min_lat, max_lat, self.get_row_lat
)
cells: list[tuple[Polygon, str, bool]] = []
for row_idx in range(start_row, end_row + 1):
lat = self.get_row_lat(row_idx)
lon_spacing = self.get_lon_spacing(lat)
lon_offset = self.get_lon_offset(lon_spacing)
start_col = int(np.floor((min_lon + 180 - lon_offset) / lon_spacing))
end_col = int(np.ceil((max_lon + 180 - lon_offset) / lon_spacing))
start_col, end_col = _expand_bounds(
start_col,
end_col,
min_lon,
max_lon,
lambda col: self.get_col_lon(col, lon_spacing, lon_offset),
)
cols = np.arange(start_col, end_col + 1)
lons = self.get_col_lon(cols, lon_spacing, lon_offset)
xmin = lons
ymin = lat
xmax = lons + lon_spacing
ymax = lat + self.lat_spacing
primary_polys = shapely.box(xmin, ymin, xmax, ymax) # pyright: ignore[reportCallIssue, reportArgumentType]
primary_mask = shapely.intersects(primary_polys, polygon)
matched_cols = cols[primary_mask]
for poly, c_idx in zip(primary_polys[primary_mask], matched_cols):
c_id = self.get_cell_name(row_idx, c_idx, lon_spacing, is_primary=True)
cells.append((poly, c_id, True))
if self.overlap:
self._add_overlap_cells(
lons, lat, lon_spacing, cols, row_idx, polygon, cells
)
return cells
build_grid_cells ¶
build_grid_cells(aoi, grid_dist)
Build raw GridCell objects intersecting an AOI.
| PARAMETER | DESCRIPTION |
|---|---|
aoi | Area of interest geometry in WGS84. TYPE: |
grid_dist | MajorTOM cell size in metres. TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
Sequence[GridCell] | Sequence of raw |
Source code in components/aereo/grid/core.py
def build_grid_cells(
aoi: BaseGeometry,
grid_dist: int,
) -> Sequence[GridCell]:
"""Build raw ``GridCell`` objects intersecting an AOI.
Args:
aoi: Area of interest geometry in WGS84.
grid_dist: MajorTOM cell size in metres.
Returns:
Sequence of raw ``GridCell`` objects, one per intersecting cell.
"""
grid_def = GridDefinition(d=grid_dist)
return [
GridCell(
id=cell_id,
d=grid_dist,
cell_geometry=geom,
)
for geom, cell_id, _ in grid_def.generate_raw_cells(aoi)
]
cells_bounds ¶
cells_bounds(cells, *, buffer_m=0.0)
Return the WGS84 bounding box of a set of grid cells.
| PARAMETER | DESCRIPTION |
|---|---|
cells | Grid cells to bound. TYPE: |
buffer_m | Optional padding in metres around each cell. Each cell's UTM footprint is expanded by this amount before computing the overall WGS84 bounding box, so the result remains accurate even when cells fall in different UTM zones. TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
tuple[float, float, float, float] | A WGS84 bounding box as |
| RAISES | DESCRIPTION |
|---|---|
ValueError | If cells is empty. |
Source code in components/aereo/grid/core.py
def cells_bounds(
cells: Iterable[GridCell],
*,
buffer_m: float = 0.0,
) -> tuple[float, float, float, float]:
"""Return the WGS84 bounding box of a set of grid cells.
Args:
cells: Grid cells to bound.
buffer_m: Optional padding in metres around each cell. Each cell's
UTM footprint is expanded by this amount before computing the
overall WGS84 bounding box, so the result remains accurate even
when cells fall in different UTM zones.
Returns:
A WGS84 bounding box as ``(minx, miny, maxx, maxy)``.
Raises:
ValueError: If *cells* is empty.
"""
cells = list(cells)
if not cells:
raise ValueError("At least one GridCell is required")
if buffer_m == 0:
geoms = [cell.cell_geometry for cell in cells]
return shapely.unary_union(geoms).bounds
buffered: list[BaseGeometry] = []
for cell in cells:
expanded = cell.utm_footprint.buffer(buffer_m)
buffered.append(
reproject_geom(expanded, src_epsg=cell.utm_crs, dst_epsg=_WGS84_CRS)
)
return shapely.unary_union(buffered).bounds
intersect_cells ¶
intersect_cells(bounds, grid_cells, crs=None)
Return the grid cells from grid_cells that intersect bounds.
| PARAMETER | DESCRIPTION |
|---|---|
bounds |
TYPE: |
grid_cells | Candidate grid cells. TYPE: |
crs | CRS of bounds. If provided, cell geometries are reprojected from their native WGS84 CRS to this CRS before intersection. If TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
Sequence[GridCell] | Sequence of intersecting |
Source code in components/aereo/grid/core.py
def intersect_cells(
bounds: tuple[float, float, float, float],
grid_cells: Sequence[GridCell],
crs: str | None = None,
) -> Sequence[GridCell]:
"""Return the grid cells from *grid_cells* that intersect *bounds*.
Args:
bounds: ``(minx, miny, maxx, maxy)`` bounding box.
grid_cells: Candidate grid cells.
crs: CRS of *bounds*. If provided, cell geometries are reprojected from
their native WGS84 CRS to this CRS before intersection. If ``None``,
intersection is done in WGS84.
Returns:
Sequence of intersecting ``GridCell`` objects.
"""
from shapely.geometry import box as _box
query_box = _box(*bounds)
result: list[GridCell] = []
for cell in grid_cells:
geom = cell.cell_geometry
if crs is not None:
geom = reproject_geom(geom, src_epsg=_WGS84_CRS, dst_epsg=crs)
if geom.intersects(query_box):
result.append(cell)
return result