From 2170cdc6b8ebf041dea4a314fb6142a5708f5b9b Mon Sep 17 00:00:00 2001 From: "Gregory H. Halverson" Date: Tue, 13 Jan 2026 11:05:18 -0800 Subject: [PATCH 1/6] fixing RasterGrid.from_bbox to take individual edges of the bbox as parameters --- debug.py | 7 +++++++ rasters/raster_grid.py | 25 ++++++++++++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 debug.py diff --git a/debug.py b/debug.py new file mode 100644 index 0000000..c198829 --- /dev/null +++ b/debug.py @@ -0,0 +1,7 @@ +from rasters import RasterGrid + +# Define target area +geometry = RasterGrid.from_bbox( + xmin=-118.5, ymin=33.5, xmax=-117.5, ymax=34.5, + cell_size=30, crs="EPSG:4326" +) diff --git a/rasters/raster_grid.py b/rasters/raster_grid.py index 7f84efd..16a1760 100644 --- a/rasters/raster_grid.py +++ b/rasters/raster_grid.py @@ -159,25 +159,40 @@ def from_vectors(cls, x_vector: np.ndarray, y_vector: np.ndarray, crs: Union[CRS @classmethod def from_bbox( cls, - bbox: Union[BBox, Tuple[float]], + bbox: Union[BBox, Tuple[float]] = None, shape: Tuple[int, int] = None, cell_size: float = None, cell_width: float = None, cell_height: float = None, - crs: Union[CRS, str] = None): + crs: Union[CRS, str] = None, + xmin: float = None, + ymin: float = None, + xmax: float = None, + ymax: float = None): from .bbox import BBox + # Handle either bbox or individual coordinates + if bbox is not None and any(coord is not None for coord in [xmin, ymin, xmax, ymax]): + raise ValueError("Provide either bbox parameter or individual xmin/ymin/xmax/ymax, not both") + + if bbox is None and any(coord is None for coord in [xmin, ymin, xmax, ymax]): + raise ValueError("When not providing bbox, all of xmin, ymin, xmax, ymax must be provided") + + if bbox is None and all(coord is None for coord in [xmin, ymin, xmax, ymax]): + raise ValueError("Must provide either bbox parameter or xmin, ymin, xmax, ymax") + if (cell_width is None or cell_height is None) and cell_size is not None: cell_width = cell_size cell_height = -cell_size - if crs is None and isinstance(bbox, BBox): - crs = bbox.crs + if bbox is not None: + if crs is None and isinstance(bbox, BBox): + crs = bbox.crs + xmin, ymin, xmax, ymax = bbox if crs is None: crs = WGS84 - xmin, ymin, xmax, ymax = bbox width = xmax - xmin height = ymax - ymin x_origin = xmin From 2ac28b5b4551643da1e3c64a8c05009c4de41684 Mon Sep 17 00:00:00 2001 From: "Gregory H. Halverson" Date: Tue, 13 Jan 2026 11:29:57 -0800 Subject: [PATCH 2/6] docstrings for RasterGrid --- rasters/raster_grid.py | 477 +++++++++++++++++++++++++++++++++++------ 1 file changed, 417 insertions(+), 60 deletions(-) diff --git a/rasters/raster_grid.py b/rasters/raster_grid.py index 16a1760..d57e5ba 100644 --- a/rasters/raster_grid.py +++ b/rasters/raster_grid.py @@ -37,7 +37,7 @@ class RasterGrid(RasterGeometry): """ This class encapsulates the georeferencing of gridded data using affine transforms. - Gridded surfaces are assumed north-oriented. Row and column rotation are not supported. + Gridded surfaces are assumed to be north-oriented. Row and column rotation are not supported. """ geometry_type = "grid" @@ -51,36 +51,54 @@ def __init__( cols: int, crs: Union[CRS, str] = WGS84, **kwargs): + """ + Initialize a RasterGrid object. + + Args: + x_origin (float): X-coordinate of the top-left corner of the grid. + y_origin (float): Y-coordinate of the top-left corner of the grid. + cell_width (float): Width of each cell in the grid. + cell_height (float): Height of each cell in the grid (negative for north-oriented grids). + rows (int): Number of rows in the grid. + cols (int): Number of columns in the grid. + crs (Union[CRS, str], optional): Coordinate reference system. Defaults to WGS84. + **kwargs: Additional keyword arguments. + """ super(RasterGrid, self).__init__(crs=crs, **kwargs) - # assemble affine transform + # Assemble affine transform for the grid self._affine = Affine(cell_width, 0, x_origin, 0, cell_height, y_origin) - # store dimensions + # Store grid dimensions self._rows = int(rows) self._cols = int(cols) - # create blank geolocation attributes + # Create blank geolocation attributes self._x = None self._y = None def _subset_index(self, y_slice: slice, x_slice: slice) -> RasterGrid: + """ + Create a subset of the grid based on the provided slices. + + Args: + y_slice (slice): Slice for the rows. + x_slice (slice): Slice for the columns. + + Returns: + RasterGrid: A new RasterGrid object representing the subset. + """ y_start, y_end, y_step = y_slice.indices(self.rows) x_start, x_end, x_step = x_slice.indices(self.cols) rows = y_end - y_start cols = x_end - x_start - if y_start > 0: - y_origin = self.y_origin + y_start * self.cell_height - else: - y_origin = self.y_origin - - if x_start > 0: - x_origin = self.x_origin + x_start * self.cell_width - else: - x_origin = self.x_origin + # Calculate new origins based on the slices + y_origin = self.y_origin + y_start * self.cell_height if y_start > 0 else self.y_origin + x_origin = self.x_origin + x_start * self.cell_width if x_start > 0 else self.x_origin + # Create a new affine transform for the subset affine = Affine( self.affine.a, self.affine.b, @@ -90,16 +108,41 @@ def _subset_index(self, y_slice: slice, x_slice: slice) -> RasterGrid: y_origin ) + # Create and return the subset grid subset = RasterGrid.from_affine(affine, rows, cols, self.crs) - return subset def __eq__(self, other: RasterGrid) -> bool: - return isinstance(other, - RasterGrid) and self.crs == other.crs and self.affine == other.affine and self.shape == other.shape + """ + Check equality between two RasterGrid objects. + + Args: + other (RasterGrid): Another RasterGrid object to compare. + + Returns: + bool: True if the objects are equal, False otherwise. + """ + return ( + isinstance(other, RasterGrid) and + self.crs == other.crs and + self.affine == other.affine and + self.shape == other.shape + ) @classmethod - def from_affine(cls, affine: Affine, rows: int, cols: int, crs: Union[CRS, str] = WGS84): + def from_affine(cls, affine: Affine, rows: int, cols: int, crs: Union[CRS, str] = WGS84) -> RasterGrid: + """ + Create a RasterGrid from an affine transform. + + Args: + affine (Affine): Affine transform object. + rows (int): Number of rows in the grid. + cols (int): Number of columns in the grid. + crs (Union[CRS, str], optional): Coordinate reference system. Defaults to WGS84. + + Returns: + RasterGrid: A new RasterGrid object. + """ if not isinstance(affine, Affine): raise ValueError("affine is not an Affine object") @@ -107,23 +150,38 @@ def from_affine(cls, affine: Affine, rows: int, cols: int, crs: Union[CRS, str] @classmethod def from_rasterio(cls, file: DatasetReader, crs: Union[CRS, str] = None, **kwargs) -> RasterGrid: - # FIXME old version of rasterio uses affine, new version uses transform + """ + Create a RasterGrid from a rasterio DatasetReader object. - if hasattr(file, "affine"): - affine = file.affine - else: - affine = file.transform + Args: + file (DatasetReader): Rasterio dataset reader object. + crs (Union[CRS, str], optional): Coordinate reference system. Defaults to None. + **kwargs: Additional keyword arguments. + + Returns: + RasterGrid: A new RasterGrid object. + """ + # Handle compatibility with different rasterio versions + affine = file.affine if hasattr(file, "affine") else file.transform + # Use the CRS from the file if not provided if crs is None: - if file.crs is None: - crs = WGS84 - else: - crs = file.crs + crs = file.crs if file.crs is not None else WGS84 return cls.from_affine(affine, file.height, file.width, crs) @classmethod def from_raster_file(cls, filename: str, **kwargs) -> RasterGrid: + """ + Create a RasterGrid from a raster file. + + Args: + filename (str): Path to the raster file. + **kwargs: Additional keyword arguments. + + Returns: + RasterGrid: A new RasterGrid object. + """ os.environ["CPL_ZIP_ENCODING"] = "UTF-8" with rasterio.open(filename, "r", **kwargs) as file: @@ -131,20 +189,43 @@ def from_raster_file(cls, filename: str, **kwargs) -> RasterGrid: @classmethod def open(cls, filename: str, **kwargs) -> RasterGrid: + """ + Open a raster file and create a RasterGrid. + + Args: + filename (str): Path to the raster file. + **kwargs: Additional keyword arguments. + + Returns: + RasterGrid: A new RasterGrid object. + """ return cls.from_raster_file(filename=filename, **kwargs) @classmethod - def from_vectors(cls, x_vector: np.ndarray, y_vector: np.ndarray, crs: Union[CRS, str] = WGS84): + def from_vectors(cls, x_vector: np.ndarray, y_vector: np.ndarray, crs: Union[CRS, str] = WGS84) -> RasterGrid: + """ + Create a RasterGrid from x and y coordinate vectors. + + Args: + x_vector (np.ndarray): Array of x-coordinates. + y_vector (np.ndarray): Array of y-coordinates. + crs (Union[CRS, str], optional): Coordinate reference system. Defaults to WGS84. + + Returns: + RasterGrid: A new RasterGrid object. + """ cols = len(x_vector) rows = len(y_vector) + # Calculate cell dimensions cell_width = np.nanmean(np.diff(x_vector)) cell_height = np.nanmean(np.diff(y_vector)) + # Calculate origins x_origin = x_vector[0] - cell_width / 2.0 y_origin = y_vector[0] + cell_height / 2.0 - grid = RasterGrid( + return RasterGrid( x_origin=x_origin, y_origin=y_origin, cell_width=cell_width, @@ -154,8 +235,6 @@ def from_vectors(cls, x_vector: np.ndarray, y_vector: np.ndarray, crs: Union[CRS crs=crs ) - return grid - @classmethod def from_bbox( cls, @@ -169,8 +248,27 @@ def from_bbox( ymin: float = None, xmax: float = None, ymax: float = None): - from .bbox import BBox - + """ + Create a RasterGrid from a bounding box or individual coordinates. + + Args: + bbox (Union[BBox, Tuple[float]], optional): Bounding box object or tuple (xmin, ymin, xmax, ymax). + shape (Tuple[int, int], optional): Shape of the grid as (rows, cols). Defaults to None. + cell_size (float, optional): Uniform cell size. Defaults to None. + cell_width (float, optional): Width of each cell. Defaults to None. + cell_height (float, optional): Height of each cell. Defaults to None. + crs (Union[CRS, str], optional): Coordinate reference system. Defaults to None. + xmin (float, optional): Minimum x-coordinate. Defaults to None. + ymin (float, optional): Minimum y-coordinate. Defaults to None. + xmax (float, optional): Maximum x-coordinate. Defaults to None. + ymax (float, optional): Maximum y-coordinate. Defaults to None. + + Returns: + RasterGrid: A new RasterGrid object. + + Raises: + ValueError: If both bbox and individual coordinates are provided, or if required parameters are missing. + """ # Handle either bbox or individual coordinates if bbox is not None and any(coord is not None for coord in [xmin, ymin, xmax, ymax]): raise ValueError("Provide either bbox parameter or individual xmin/ymin/xmax/ymax, not both") @@ -225,8 +323,17 @@ def from_bbox( @classmethod def merge(cls, geometries: List[RasterGeometry], crs: CRS = None, cell_size: float = None) -> RasterGrid: - from .bbox import BBox - + """ + Merge multiple RasterGeometry objects into a single RasterGrid. + + Args: + geometries (List[RasterGeometry]): List of RasterGeometry objects to merge. + crs (CRS, optional): Coordinate reference system for the merged grid. Defaults to None. + cell_size (float, optional): Cell size for the merged grid. Defaults to None. + + Returns: + RasterGrid: A new RasterGrid object representing the merged geometries. + """ if crs is None: crs = geometries[0].crs @@ -241,8 +348,15 @@ def merge(cls, geometries: List[RasterGeometry], crs: CRS = None, cell_size: flo return geometry def get_bbox(self, CRS: Union[CRS, str] = None) -> BBox: - from .bbox import BBox + """ + Get the bounding box of the grid. + + Args: + CRS (Union[CRS, str], optional): Target coordinate reference system. Defaults to None. + Returns: + BBox: Bounding box of the grid. + """ bbox = BBox(xmin=self.x_min, ymin=self.y_min, xmax=self.x_max, ymax=self.y_max, crs=self.crs) if CRS is not None: @@ -255,92 +369,160 @@ def get_bbox(self, CRS: Union[CRS, str] = None) -> BBox: @property def affine(self) -> Affine: """ - Affine transform of top-left corners of cells. + Get the affine transform of the top-left corners of cells. + + Returns: + Affine: Affine transform object. """ return self._affine @property def affine_center(self) -> Affine: """ - Affine transform of cell centroids. + Get the affine transform of cell centroids. + + Returns: + Affine: Affine transform object. """ return self.affine * Affine.translation(0.5, 0.5) @property def cell_width(self) -> float: """ - Positive cell width in units of CRS. + Get the positive cell width in units of the CRS. + + Returns: + float: Cell width. """ return self.affine.a @property def width(self) -> float: + """ + Get the total width of the grid. + + Returns: + float: Total width of the grid. + """ return self.cell_width * self.cols @property def x_origin(self) -> float: """ - X-coordinate of top-left corner of extent. + Get the x-coordinate of the top-left corner of the grid extent. + + Returns: + float: X-coordinate of the top-left corner. """ return self.affine.c @property def cell_height(self) -> float: """ - Negative cell height in units of CRS. + Get the negative cell height in units of the CRS. + + Returns: + float: Cell height. """ return self.affine.e @property def height(self) -> float: + """ + Get the total height of the grid. + + Returns: + float: Total height of the grid. + """ return abs(self.cell_height) * self.rows @property def y_origin(self) -> float: """ - Y-coordinate of top-left corner of extent. + Get the y-coordinate of the top-left corner of the grid extent. + + Returns: + float: Y-coordinate of the top-left corner. """ return self.affine.f @property def rows(self) -> int: """ - Height of gridded surface in rows. + Get the number of rows in the grid. + + Returns: + int: Number of rows. """ return self._rows @property def cols(self) -> int: """ - Width of gridded surface in columns. + Get the number of columns in the grid. + + Returns: + int: Number of columns. """ return self._cols @property def xmin(self) -> float: + """ + Get the minimum x-coordinate of the grid extent. + + Returns: + float: Minimum x-coordinate. + """ return self.x_origin @property def xmax(self) -> float: + """ + Get the maximum x-coordinate of the grid extent. + + Returns: + float: Maximum x-coordinate. + """ return self.x_origin + self.width @property def ymin(self) -> float: + """ + Get the minimum y-coordinate of the grid extent. + + Returns: + float: Minimum y-coordinate. + """ return self.y_origin - self.height @property def ymax(self) -> float: + """ + Get the maximum y-coordinate of the grid extent. + + Returns: + float: Maximum y-coordinate. + """ return self.y_origin @property def grid(self) -> RasterGrid: + """ + Get a copy of the current grid. + + Returns: + RasterGrid: Copy of the current grid. + """ return RasterGrid.from_affine(self.affine, self.rows, self.cols, self.crs) @property def corner_polygon(self) -> Polygon: """ - Draw polygon through the corner coordinates of geolocation arrays. - :return: shapely.geometry.Polygon of corner coordinate boundary + Get a polygon representing the corners of the grid. + + Returns: + Polygon: Polygon of the grid corners. """ from .polygon import Polygon @@ -354,8 +536,10 @@ def corner_polygon(self) -> Polygon: @property def corner_polygon_latlon(self) -> Polygon: """ - Draw polygon through the corner coordinates of geolocation arrays in lat/lon. - :return: shapely.geometry.Polygon of corner coordinate boundary + Get a polygon representing the corners of the grid in latitude/longitude. + + Returns: + Polygon: Polygon of the grid corners in latitude/longitude. """ from .polygon import Polygon @@ -372,6 +556,12 @@ def corner_polygon_latlon(self) -> Polygon: @property def boundary(self) -> Polygon: + """ + Get the boundary of the grid as a polygon. + + Returns: + Polygon: Boundary polygon of the grid. + """ from .polygon import Polygon if self.shape == (1, 1): @@ -390,6 +580,15 @@ def boundary(self) -> Polygon: return boundary def resolution(self, cell_size: Union[float, Tuple[float, float]]) -> RasterGrid: + """ + Adjust the resolution of the grid. + + Args: + cell_size (Union[float, Tuple[float, float]]): New cell size as a single value or tuple (width, height). + + Returns: + RasterGrid: A new RasterGrid with adjusted resolution. + """ if len(cell_size) == 1: cell_width = cell_size cell_height = -cell_size @@ -407,6 +606,16 @@ def resolution(self, cell_size: Union[float, Tuple[float, float]]) -> RasterGrid return grid def resize(self, dimensions: Tuple[int, int], keep_square: bool = True) -> RasterGeometry: + """ + Resize the grid to new dimensions. + + Args: + dimensions (Tuple[int, int]): New dimensions as (rows, cols). + keep_square (bool, optional): Whether to maintain square cells. Defaults to True. + + Returns: + RasterGeometry: A new RasterGeometry object with resized dimensions. + """ rows, cols = dimensions cell_height = self.cell_height * (float(self.rows) / float(rows)) cell_width = self.cell_width * (float(self.cols) / float(cols)) @@ -427,6 +636,17 @@ def resize(self, dimensions: Tuple[int, int], keep_square: bool = True) -> Raste return resized_grid def rescale(self, cell_size: float = None, rows: int = None, cols: int = None): + """ + Rescale the grid based on cell size or dimensions. + + Args: + cell_size (float, optional): New cell size. Defaults to None. + rows (int, optional): New number of rows. Defaults to None. + cols (int, optional): New number of columns. Defaults to None. + + Returns: + RasterGrid: A new RasterGrid with rescaled dimensions. + """ if rows is None and cols is None: rows = int(self.height / cell_size) cols = int(self.width / cell_size) @@ -451,6 +671,15 @@ def rescale(self, cell_size: float = None, rows: int = None, cols: int = None): return grid def buffer(self, pixels: int) -> RasterGrid: + """ + Add a buffer around the grid. + + Args: + pixels (int): Number of pixels to buffer. + + Returns: + RasterGrid: A new RasterGrid with the buffer applied. + """ return RasterGrid( x_origin=self.x_origin - (pixels * self.cell_width), y_origin=self.y_origin - (pixels * self.cell_height), @@ -464,25 +693,43 @@ def buffer(self, pixels: int) -> RasterGrid: @property def x_vector(self) -> np.ndarray: """ - Vector of x-coordinates. + Get the vector of x-coordinates. + + Returns: + np.ndarray: Array of x-coordinates. """ return (self.affine_center * (np.arange(self.cols), np.full(self.cols, 0, dtype=np.float32)))[0] @property def y_vector(self) -> np.ndarray: """ - Vector of y-coordinates. + Get the vector of y-coordinates. + + Returns: + np.ndarray: Array of y-coordinates. """ return (self.affine_center * (np.full(self.rows, 0, dtype=np.float32), np.arange(self.rows)))[1] @property def xy(self) -> Tuple[np.ndarray, np.ndarray]: """ - Geolocation arrays. + Get the geolocation arrays for x and y coordinates. + + Returns: + Tuple[np.ndarray, np.ndarray]: Arrays of x and y coordinates. """ return self.affine_center * np.meshgrid(np.arange(self.cols), np.arange(self.rows)) def index_point(self, point: Point) -> Tuple[int, int]: + """ + Get the grid index of a point. + + Args: + point (Point): Point to index. + + Returns: + Tuple[int, int]: Grid index as (row, col). + """ native_point = point.to_crs(self.crs) x = native_point.x y = native_point.y @@ -494,6 +741,15 @@ def index_point(self, point: Point) -> Tuple[int, int]: return index def index(self, geometry: Union[SpatialGeometry, Tuple[float, float, float, float]]): + """ + Get the grid indices for a geometry. + + Args: + geometry (Union[SpatialGeometry, Tuple[float, float, float, float]]): Geometry to index. + + Returns: + Tuple[slice, slice]: Slices representing the grid indices. + """ from .point import Point geometry = wrap_geometry(geometry) @@ -523,6 +779,16 @@ def window( self, geometry: Union[SpatialGeometry, (float, float, float, float)], buffer: int = None) -> Window: + """ + Get the rasterio window for a geometry. + + Args: + geometry (Union[SpatialGeometry, Tuple[float, float, float, float]]): Geometry to create a window for. + buffer (int, optional): Buffer to apply to the window. Defaults to None. + + Returns: + Window: Rasterio window object. + """ from .point import Point geometry = wrap_geometry(geometry) @@ -559,6 +825,15 @@ def window( return window def subset(self, target: Union[Window, Point, Polygon, BBox, RasterGeometry]) -> RasterGrid: + """ + Subset the grid based on a target geometry. + + Args: + target (Union[Window, Point, Polygon, BBox, RasterGeometry]): Target geometry for subsetting. + + Returns: + RasterGrid: Subset of the grid. + """ if not isinstance(target, Window): target = self.window(target) @@ -566,14 +841,34 @@ def subset(self, target: Union[Window, Point, Polygon, BBox, RasterGeometry]) -> subset = self[slices] return subset - + def shift_xy(self, x_shift: float, y_shift: float) -> RasterGrid: + """ + Shift the grid by x and y distances. + + Args: + x_shift (float): Distance to shift in the x-direction. + y_shift (float): Distance to shift in the y-direction. + + Returns: + RasterGrid: A new RasterGrid with the shift applied. + """ new_affine = self.affine * Affine.translation(x_shift / self.cell_size, y_shift / self.cell_size) grid = RasterGrid.from_affine(new_affine, self.rows, self.cols, self.crs) return grid - + def shift_distance(self, distance: float, direction: float) -> RasterGrid: + """ + Shift the grid by a distance in a specific direction. + + Args: + distance (float): Distance to shift. + direction (float): Direction to shift in degrees. + + Returns: + RasterGrid: A new RasterGrid with the shift applied. + """ x_shift = distance * np.cos(np.radians(direction)) y_shift = distance * np.sin(np.radians(direction)) grid = self.shift_xy(x_shift, y_shift) @@ -583,7 +878,10 @@ def shift_distance(self, distance: float, direction: float) -> RasterGrid: @property def x(self) -> np.ndarray: """ - Geolocation array of x-coordinates. + Get the geolocation array of x-coordinates. + + Returns: + np.ndarray: Array of x-coordinates. """ # cache x-coordinate array if self._x is None: @@ -594,7 +892,10 @@ def x(self) -> np.ndarray: @property def y(self) -> np.ndarray: """ - Geolocation array of y-coordinates. + Get the geolocation array of y-coordinates. + + Returns: + np.ndarray: Array of y-coordinates. """ # cache y-coordinate array if self._y is None: @@ -605,23 +906,41 @@ def y(self) -> np.ndarray: @property def x_min(self) -> float: """ - Western boundary of extent. + Get the western boundary of the grid extent. + + Returns: + float: Western boundary. """ return self.x_origin @property def x_max(self) -> float: """ - Eastern boundary of extent. + Get the eastern boundary of the grid extent. + + Returns: + float: Eastern boundary. """ return self.x_origin + self.width @property def y_min(self) -> float: + """ + Get the southern boundary of the grid extent. + + Returns: + float: Southern boundary. + """ return self.y_origin - self.height @property def y_max(self) -> float: + """ + Get the northern boundary of the grid extent. + + Returns: + float: Northern boundary. + """ return self.y_origin def rasterize( @@ -633,8 +952,21 @@ def rasterize( merge_alg=MergeAlg.replace, default_value=1, dtype=None) -> Raster: - from .raster import Raster - + """ + Rasterize shapes into the grid. + + Args: + shapes (Iterable): Shapes to rasterize. + shape_crs (optional): CRS of the shapes. Defaults to None. + fill (int, optional): Fill value for empty cells. Defaults to 0. + all_touched (bool, optional): Whether to rasterize all touched cells. Defaults to False. + merge_alg (MergeAlg, optional): Merge algorithm. Defaults to MergeAlg.replace. + default_value (int, optional): Default value for shapes. Defaults to 1. + dtype (optional): Data type of the raster. Defaults to None. + + Returns: + Raster: Rasterized representation of the shapes. + """ if not isinstance(shapes, Iterable): shapes = [shapes] @@ -666,8 +998,17 @@ def mask( geometries: gpd.GeoDataFrame, all_touched: bool = False, invert: bool = False) -> RasterGrid: - from .raster import Raster + """ + Mask the grid using geometries. + Args: + geometries (gpd.GeoDataFrame): Geometries to mask with. + all_touched (bool, optional): Whether to mask all touched cells. Defaults to False. + invert (bool, optional): Whether to invert the mask. Defaults to False. + + Returns: + RasterGrid: Masked grid. + """ mask_array = rasterio.features.geometry_mask( geometries, self.shape, @@ -682,6 +1023,12 @@ def mask( @property def coverage(self) -> OrderedDict: + """ + Get the coverage metadata of the grid. + + Returns: + OrderedDict: Coverage metadata. + """ coverage = OrderedDict() coverage["type"] = "Coverage" domain = OrderedDict() @@ -704,7 +1051,17 @@ def coverage(self) -> OrderedDict: return coverage - def to_dict(self, output_dict=None, write_geolocation_arrays=False): + def to_dict(self, output_dict: dict = None, write_geolocation_arrays: bool = False) -> dict: + """ + Convert the RasterGrid to a dictionary representation. + + Args: + output_dict (dict, optional): Dictionary to populate. Defaults to None. + write_geolocation_arrays (bool, optional): Whether to include geolocation arrays. Defaults to False. + + Returns: + dict: Dictionary representation of the RasterGrid. + """ if output_dict is None: output_dict = {} From f1ba7dc4928d525888777cf985df21d09f79aade Mon Sep 17 00:00:00 2001 From: "Gregory H. Halverson" Date: Tue, 13 Jan 2026 11:31:02 -0800 Subject: [PATCH 3/6] docstrings for RasterGeolocation --- rasters/raster_geolocation.py | 55 +++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/rasters/raster_geolocation.py b/rasters/raster_geolocation.py index 724dabb..69ca015 100644 --- a/rasters/raster_geolocation.py +++ b/rasters/raster_geolocation.py @@ -292,6 +292,19 @@ def corner_polygon(self) -> Polygon: ]) def resize(self, dimensions: Tuple[int, int], order: int = 2) -> RasterGeometry: + """ + Resize the geolocation arrays to new dimensions. + + Args: + dimensions (Tuple[int, int]): Target dimensions as (rows, cols). + order (int, optional): The order of the spline interpolation. Defaults to 2. + + Returns: + RasterGeometry: A new RasterGeolocation object with resized dimensions. + + Raises: + ValueError: If dimensions are not two-dimensional or not integers. + """ if len(dimensions) != 2: raise ValueError("coordinate field dimensionality must be two-dimensional") @@ -321,9 +334,25 @@ def resize(self, dimensions: Tuple[int, int], order: int = 2) -> RasterGeometry: @property def grid(self) -> RasterGrid: + """ + Generate a RasterGrid representation of the geolocation. + + Returns: + RasterGrid: A RasterGrid object representing the geolocation. + """ return self.generate_grid(dest_crs=self.crs) def to_dict(self, output_dict: Dict = None, write_geolocation_arrays: bool = False) -> Dict: + """ + Convert the RasterGeolocation to a dictionary representation. + + Args: + output_dict (Dict, optional): Dictionary to populate. Defaults to None. + write_geolocation_arrays (bool, optional): Whether to include geolocation arrays. Defaults to False. + + Returns: + Dict: Dictionary representation of the RasterGeolocation. + """ # FIXME this should conform to the CoverageJSON standard if output_dict is None: output_dict = {} @@ -347,16 +376,18 @@ def window( geometry: Union[SpatialGeometry, Tuple[float, float, float, float]], buffer: int = None) -> Window: """ - Returns a rasterio.windows.Window covering the target geometry. - + Create a rasterio Window covering the target geometry. + Args: - geometry: The geometry to create a window for - buffer: Optional buffer in pixels to add around the geometry - + geometry (Union[SpatialGeometry, Tuple[float, float, float, float]]): Geometry to create a window for. + buffer (int, optional): Buffer in pixels to add around the geometry. Defaults to None. + Returns: - Window: A rasterio Window object covering the geometry + Window: A rasterio Window object covering the geometry. + + Raises: + ValueError: If no points are found within the target geometry. """ - mask = self.index(geometry) rows, cols = np.where(mask) @@ -379,13 +410,13 @@ def window( def subset(self, target: Union[Window, Point, Polygon, BBox, RasterGeometry]) -> 'RasterGeolocation': """ - Subset the raster geolocation using a Window or other geometry. - + Subset the RasterGeolocation using a Window or other geometry. + Args: - target: Window object or geometry to subset with - + target (Union[Window, Point, Polygon, BBox, RasterGeometry]): Target geometry or Window to subset with. + Returns: - RasterGeolocation: A new RasterGeolocation object representing the subset + RasterGeolocation: A new RasterGeolocation object representing the subset. """ if not isinstance(target, Window): target = self.window(target) From 85892fca57c33f5796f961af26435a80d9b6ec0f Mon Sep 17 00:00:00 2001 From: "Gregory H. Halverson" Date: Tue, 13 Jan 2026 11:40:10 -0800 Subject: [PATCH 4/6] docstrings for Raster --- rasters/raster.py | 302 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) diff --git a/rasters/raster.py b/rasters/raster.py index 72fde10..1a07c37 100644 --- a/rasters/raster.py +++ b/rasters/raster.py @@ -61,6 +61,21 @@ def __init__( cmap=None, metadata: dict = None, **kwargs): + """ + Initialize a Raster object. + + Args: + array (Union[np.ndarray, Raster]): The raster data as a 2D numpy array or another Raster object. + geometry (RasterGeometry): The georeferencing information for the raster. + nodata (optional): Value representing no data in the raster. Defaults to None. + cmap (optional): Colormap for visualization. Defaults to None. + metadata (dict, optional): Metadata dictionary. Defaults to None. + **kwargs: Additional keyword arguments. + + Raises: + TypeError: If the array is not a valid numpy.ndarray. + ValueError: If the array is not two-dimensional. + """ ALLOWED_ARRAY_TYPES = ( np.ndarray, h5py.Dataset @@ -111,10 +126,22 @@ def __init__( @property def array(self) -> np.ndarray: + """ + Get the raster data array. + + Returns: + np.ndarray: The raster data array. + """ return self._array @property def dtype(self): + """ + Get the data type of the raster array. + + Returns: + The data type of the raster array. + """ return self._array.dtype def __array_prepare__(self, other: Union[np.ndarray, Raster], *args, **kwargs) -> np.ndarray: @@ -148,6 +175,15 @@ def __array_finalize__(self, other: Union[np.ndarray, Raster], **kwargs) -> Rast return self.contain(other) def __add__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Add another raster or numpy array to this raster. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to add. + + Returns: + Raster: A new Raster object with the result of the addition. + """ if isinstance(other, Raster): data = other.array else: @@ -161,6 +197,15 @@ def __add__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: __radd__ = __add__ def __sub__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Subtract another raster or numpy array from this raster. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to subtract. + + Returns: + Raster: A new Raster object with the result of the subtraction. + """ if isinstance(other, Raster): data = other.array else: @@ -178,6 +223,15 @@ def __neg__(self): return self.contain(-(self.array)) def __mul__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Multiply this raster by another raster or numpy array. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to multiply. + + Returns: + Raster: A new Raster object with the result of the multiplication. + """ if isinstance(other, Raster): data = other.array else: @@ -191,6 +245,15 @@ def __mul__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: __rmul__ = __mul__ def __pow__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Raise this raster to a power using another raster or numpy array. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to raise to a power. + + Returns: + Raster: A new Raster object with the result of the power operation. + """ if isinstance(other, Raster): data = other.array else: @@ -202,6 +265,15 @@ def __pow__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: return result def __rpow__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Raise another raster or numpy array to a power using this raster. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to raise to a power. + + Returns: + Raster: A new Raster object with the result of the power operation. + """ if isinstance(other, Raster): data = other.array else: @@ -213,6 +285,15 @@ def __rpow__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: return result def __div__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Divide this raster by another raster or numpy array. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to divide by. + + Returns: + Raster: A new Raster object with the result of the division. + """ if isinstance(other, Raster): data = other.array else: @@ -224,6 +305,15 @@ def __div__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: return result def __truediv__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Divide this raster by another raster or numpy array. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to divide by. + + Returns: + Raster: A new Raster object with the result of the division. + """ if isinstance(other, Raster): data = other.array else: @@ -238,6 +328,15 @@ def __rtruediv__(self, other): return self.contain(other / self.array) def __floordiv__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Floor divide this raster by another raster or numpy array. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to divide by. + + Returns: + Raster: A new Raster object with the result of the floor division. + """ if isinstance(other, Raster): data = other.array else: @@ -249,6 +348,15 @@ def __floordiv__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Ras return result def __mod__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Modulo this raster by another raster or numpy array. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to modulo by. + + Returns: + Raster: A new Raster object with the result of the modulo operation. + """ if isinstance(other, Raster): data = other.array else: @@ -260,6 +368,15 @@ def __mod__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: return result def __lshift__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Left shift this raster by another raster or numpy array. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to shift by. + + Returns: + Raster: A new Raster object with the result of the left shift operation. + """ if isinstance(other, Raster): data = other.array else: @@ -271,6 +388,15 @@ def __lshift__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raste return result def __rshift__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Right shift this raster by another raster or numpy array. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to shift by. + + Returns: + Raster: A new Raster object with the result of the right shift operation. + """ if isinstance(other, Raster): data = other.array else: @@ -282,6 +408,15 @@ def __rshift__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raste return result def __and__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Bitwise AND this raster with another raster or numpy array. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to AND with. + + Returns: + Raster: A new Raster object with the result of the bitwise AND operation. + """ if isinstance(other, Raster): data = other.array else: @@ -293,6 +428,15 @@ def __and__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: return result def __or__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Bitwise OR this raster with another raster or numpy array. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to OR with. + + Returns: + Raster: A new Raster object with the result of the bitwise OR operation. + """ if isinstance(other, Raster): data = other.array else: @@ -304,6 +448,15 @@ def __or__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: return result def __xor__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Bitwise XOR this raster with another raster or numpy array. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to XOR with. + + Returns: + Raster: A new Raster object with the result of the bitwise XOR operation. + """ if isinstance(other, Raster): data = other.array else: @@ -315,12 +468,31 @@ def __xor__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: return result def __invert__(self, *args, **kwargs) -> Raster: + """ + Invert this raster. + + Args: + *args: Positional arguments. + **kwargs: Additional keyword arguments. + + Returns: + Raster: The inverted raster. + """ with np.errstate(invalid='ignore'): result = self.contain(self.array.__invert__(*args, **kwargs)) return result def __lt__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Less than comparison. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to compare. + + Returns: + Raster: A new Raster object with the result of the comparison. + """ if isinstance(other, Raster): data = other.array else: @@ -332,6 +504,15 @@ def __lt__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: return result def __le__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Less than or equal comparison. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to compare. + + Returns: + Raster: A new Raster object with the result of the comparison. + """ if isinstance(other, Raster): data = other.array else: @@ -343,6 +524,15 @@ def __le__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: return result def __gt__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Greater than comparison. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to compare. + + Returns: + Raster: A new Raster object with the result of the comparison. + """ if isinstance(other, Raster): data = other.array else: @@ -354,6 +544,15 @@ def __gt__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: return result def __ge__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Greater than or equal comparison. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to compare. + + Returns: + Raster: A new Raster object with the result of the comparison. + """ if isinstance(other, Raster): data = other.array else: @@ -365,6 +564,15 @@ def __ge__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: return result def __cmp__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Comparison. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to compare. + + Returns: + Raster: A new Raster object with the result of the comparison. + """ if isinstance(other, Raster): data = other.array else: @@ -376,6 +584,15 @@ def __cmp__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: return result def __eq__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Equal comparison. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to compare. + + Returns: + Raster: A new Raster object with the result of the comparison. + """ if isinstance(other, Raster): data = other.array else: @@ -387,6 +604,15 @@ def __eq__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: return result def __ne__(self, other: Union[Raster, np.ndarray], *args, **kwargs) -> Raster: + """ + Not equal comparison. + + Args: + other (Union[Raster, np.ndarray]): The other raster or numpy array to compare. + + Returns: + Raster: A new Raster object with the result of the comparison. + """ if isinstance(other, Raster): data = other.array else: @@ -417,6 +643,26 @@ def open( resampling: str = None, cmap: Union[Colormap, str] = None, **kwargs) -> Union[Raster, np.ndarray]: + """ + Open a raster file. + + Args: + filename (str): The path to the raster file. + nodata (optional): Value representing no data. Defaults to None. + remove (optional): Value to remove from the raster. Defaults to None. + geometry (Union[RasterGeometry, Point, MultiPoint]): The georeferencing information. + buffer (int, optional): Buffer size. Defaults to None. + window (Window, optional): Window to extract. Defaults to None. + resampling (str, optional): Resampling method. Defaults to None. + cmap (Union[Colormap, str], optional): Colormap for visualization. Defaults to None. + **kwargs: Additional keyword arguments. + + Returns: + Union[Raster, np.ndarray]: The opened raster or numpy array. + + Raises: + IOError: If the file not found. + """ from .point import Point from .multi_point import MultiPoint from .raster_grid import RasterGrid @@ -532,6 +778,22 @@ def merge( crs: CRS = None, cell_size: float = None, dtype: str = None) -> Raster: + """ + Merge multiple rasters into a single raster. + + Args: + images (List[Raster]): The rasters to merge. + geometry (RasterGeometry, optional): The target geometry. Defaults to None. + crs (CRS, optional): The target CRS. Defaults to None. + cell_size (float, optional): The cell size. Defaults to None. + dtype (str, optional): The data type. Defaults to None. + + Returns: + Raster: The merged raster. + + Raises: + ValueError: If the images are not all the same size. + """ if geometry is None: geometries = [image.geometry for image in images] geometry = RasterGrid.merge(geometries=geometries, crs=crs, cell_size=cell_size) @@ -573,6 +835,17 @@ def _raise_unrecognized_key(key): raise IndexError(f"key unrecognized: {key}") def contain(self, array=None, geometry=None, nodata=None) -> Raster: + """ + Create a new Raster object containing the given array and geometry. + + Args: + array (optional): The raster data array. Defaults to the current array. + geometry (optional): The georeferencing information. Defaults to the current geometry. + nodata (optional): Value representing no data. Defaults to the current nodata value. + + Returns: + Raster: A new Raster object. + """ if array is None: array = self.array @@ -1345,6 +1618,22 @@ def to_file( overwrite: bool = True, nodata: Any = None, **kwargs): + """ + Save the raster to a file. + + Args: + filename (str): The output file path. + driver (str, optional): The file format driver. Defaults to None. + compress (str, optional): Compression type. Defaults to None. + use_compression (bool, optional): Whether to use compression. Defaults to True. + overwrite (bool, optional): Whether to overwrite existing files. Defaults to True. + nodata (Any, optional): Value representing no data. Defaults to None. + **kwargs: Additional keyword arguments. + + Raises: + ValueError: If the filename is invalid. + IOError: If the output file already exists and overwrite is False. + """ if not isinstance(filename, str): raise ValueError("invalid filename") @@ -1394,6 +1683,19 @@ def to_geotiff( include_preview: bool = True, overwrite: bool = True, **kwargs): + """ + Save the raster as a GeoTIFF file. + + Args: + filename (str): The output file path. + compression (str, optional): Compression type. Defaults to None. + preview_quality (int, optional): Quality of the preview image. Defaults to 20. + cmap (Union[Colormap, str], optional): Colormap for visualization. Defaults to None. + use_compression (bool, optional): Whether to use compression. Defaults to True. + include_preview (bool, optional): Whether to include a preview image. Defaults to True. + overwrite (bool, optional): Whether to overwrite existing files. Defaults to True. + **kwargs: Additional keyword arguments. + """ self.to_file( filename=filename, driver=GEOTIFF_DRIVER, From 4fb9264d1e8ec302ae1ed519d7bb2f5fe5c8223f Mon Sep 17 00:00:00 2001 From: "Gregory H. Halverson" Date: Tue, 13 Jan 2026 11:42:44 -0800 Subject: [PATCH 5/6] usage section in README for Raster, RasterGrid, RasterGeolocation --- README.md | 326 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) diff --git a/README.md b/README.md index 1a12cf4..5fb5512 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,332 @@ raster ``` ![png](examples/Opening%20a%20GeoTIFF_3_0.png) + +## Usage + +### Working with Raster Objects + +The `Raster` class is the primary interface for working with raster data. It encapsulates both the data array and its georeferencing information. + +#### Opening and Creating Rasters + +```python +from rasters import Raster + +# Open a raster from file +raster = Raster.open("path/to/file.tif") + +# Create a raster from a numpy array and geometry +import numpy as np +from rasters import RasterGrid + +data = np.random.rand(100, 100) +geometry = RasterGrid.from_bbox( + xmin=-120, ymin=30, xmax=-110, ymax=40, + cell_size=0.1 +) +raster = Raster(data, geometry=geometry) +``` + +#### Raster Properties + +```python +# Access the data array +array = raster.array + +# Get dimensions +rows, cols = raster.shape +print(f"Raster size: {rows} rows x {cols} cols") + +# Get coordinate reference system +crs = raster.crs + +# Get bounding box +bbox = raster.bbox + +# Get cell size +cell_size = raster.cell_size +``` + +#### Raster Operations + +```python +# Arithmetic operations +raster_sum = raster1 + raster2 +raster_diff = raster1 - raster2 +raster_product = raster1 * raster2 +raster_quotient = raster1 / raster2 + +# Comparison operations +mask = raster > 0.5 + +# Reproject to different coordinate system +reprojected = raster.reproject(crs="EPSG:4326", target_cell_size=0.01) + +# Resample to a different geometry +resampled = raster.to_geometry(target_geometry, resampling="bilinear") + +# Sample at a point +from rasters import Point +point = Point(-115, 35, crs="EPSG:4326") +value = raster.to_point(point) +``` + +#### Saving Rasters + +```python +# Save as GeoTIFF +raster.to_geotiff("output.tif", compression="deflate") + +# Save as Cloud Optimized GeoTIFF (COG) +raster.to_COG("output_cog.tif") + +# Save as GeoPackage +raster.to_geopackage("output.gpkg") +``` + +#### Visualization + +```python +# Display in Jupyter notebook (automatic when last line in cell) +raster + +# Create a matplotlib figure +fig = raster.imshow( + title="My Raster", + cmap="viridis", + figsize=(10, 8) +) + +# Save as image +image = raster.to_pillow(cmap="jet") +image.save("preview.png") +``` + +### Working with RasterGrid + +The `RasterGrid` class represents gridded raster geometry with uniform cell spacing and north-oriented grids. + +#### Creating RasterGrid Objects + +```python +from rasters import RasterGrid + +# From bounding box and cell size +grid = RasterGrid.from_bbox( + xmin=-120, ymin=30, xmax=-110, ymax=40, + cell_size=0.001, + crs="EPSG:4326" +) + +# From bounding box and shape +grid = RasterGrid.from_bbox( + xmin=-120, ymin=30, xmax=-110, ymax=40, + shape=(1000, 1000), + crs="EPSG:4326" +) + +# From affine transform +from affine import Affine +affine = Affine(0.001, 0, -120, 0, -0.001, 40) +grid = RasterGrid.from_affine(affine, rows=1000, cols=1000, crs="EPSG:4326") + +# From coordinate vectors +x_vector = np.linspace(-120, -110, 1000) +y_vector = np.linspace(30, 40, 1000) +grid = RasterGrid.from_vectors(x_vector, y_vector, crs="EPSG:4326") + +# From a raster file +grid = RasterGrid.open("path/to/file.tif") +``` + +#### RasterGrid Properties + +```python +# Get grid dimensions +rows = grid.rows +cols = grid.cols + +# Get cell size +cell_width = grid.cell_width +cell_height = grid.cell_height + +# Get extent +xmin, ymin = grid.xmin, grid.ymin +xmax, ymax = grid.xmax, grid.ymax +width, height = grid.width, grid.height + +# Get coordinate arrays +x = grid.x # 2D array of x-coordinates +y = grid.y # 2D array of y-coordinates + +# Get coordinate vectors +x_vector = grid.x_vector # 1D array of x-coordinates +y_vector = grid.y_vector # 1D array of y-coordinates + +# Get affine transform +affine = grid.affine +``` + +#### RasterGrid Operations + +```python +# Subset to a region +from rasters import BBox +subset_bbox = BBox(-115, 32, -112, 38, crs="EPSG:4326") +subset_grid = grid.subset(subset_bbox) + +# Slice using indices +subset_grid = grid[100:200, 150:250] + +# Change resolution +rescaled_grid = grid.rescale(cell_size=0.002) + +# Buffer the grid +buffered_grid = grid.buffer(pixels=10) + +# Shift the grid +shifted_grid = grid.shift_xy(x_shift=100, y_shift=50) +``` + +### Working with RasterGeolocation + +The `RasterGeolocation` class represents swath raster geometry using 2D geolocation arrays, commonly used for satellite swath data. + +#### Creating RasterGeolocation Objects + +```python +from rasters import RasterGeolocation +import numpy as np + +# From x and y coordinate arrays +x_coords = np.random.uniform(-120, -110, (500, 500)) +y_coords = np.random.uniform(30, 40, (500, 500)) +geolocation = RasterGeolocation(x_coords, y_coords, crs="EPSG:4326") + +# From coordinate vectors (creates meshgrid) +x_vector = np.linspace(-120, -110, 500) +y_vector = np.linspace(30, 40, 500) +geolocation = RasterGeolocation.from_vectors(x_vector, y_vector, crs="EPSG:4326") +``` + +#### RasterGeolocation Properties + +```python +# Get dimensions +rows = geolocation.rows +cols = geolocation.cols + +# Get coordinate arrays +x = geolocation.x # 2D array of x-coordinates +y = geolocation.y # 2D array of y-coordinates + +# Get extent +xmin, ymin = geolocation.x_min, geolocation.y_min +xmax, ymax = geolocation.x_max, geolocation.y_max + +# Get cell size (estimated from median distances) +cell_size = geolocation.cell_size + +# Get boundary polygon +boundary = geolocation.boundary +``` + +#### RasterGeolocation Operations + +```python +# Index with geometry +from rasters import Polygon +polygon = Polygon([(-115, 32), (-112, 32), (-112, 38), (-115, 38)]) +mask = geolocation.index(polygon) + +# Generate a regular grid from geolocation +grid = geolocation.grid + +# Resize geolocation arrays +resized = geolocation.resize(dimensions=(250, 250)) + +# Subset to a region +subset = geolocation.subset(polygon) +``` + +### Working with Points and Vector Geometries + +```python +from rasters import Point, Polygon, BBox + +# Create a point +point = Point(-115.5, 35.2, crs="EPSG:4326") + +# Transform point to different CRS +utm_point = point.to_crs("EPSG:32611") + +# Create a polygon +polygon = Polygon([ + (-115, 32), + (-112, 32), + (-112, 38), + (-115, 38) +], crs="EPSG:4326") + +# Create a bounding box +bbox = BBox(xmin=-120, ymin=30, xmax=-110, ymax=40, crs="EPSG:4326") + +# Sample raster at point +value = raster.to_point(point) + +# Clip raster to polygon +clipped = raster.subset(polygon) +``` + +### Advanced Usage + +#### Merging Multiple Rasters + +```python +# Open multiple rasters +raster1 = Raster.open("tile1.tif") +raster2 = Raster.open("tile2.tif") +raster3 = Raster.open("tile3.tif") + +# Merge into a single raster +merged = Raster.merge([raster1, raster2, raster3]) +``` + +#### Resampling with KDTree + +```python +from rasters import KDTree + +# Create a KDTree for efficient resampling +kd_tree = KDTree( + source_geometry=source_raster.geometry, + target_geometry=target_grid, + radius_of_influence=1000 # meters +) + +# Resample using the KDTree +resampled = source_raster.resample(target_grid, kd_tree=kd_tree) + +# Save KDTree for reuse +kd_tree.save("kdtree.pkl") + +# Load KDTree +kd_tree = KDTree.load("kdtree.pkl") +``` + +#### Working with Masks + +```python +# Create a mask +mask = raster > 0.5 + +# Apply mask to raster +masked_raster = raster.mask(mask) + +# Fill masked values with another raster +filled = raster.fill(fill_raster) +``` ## Changelog From aea3018ffc56352956a9ea3702ff57d969b84f99 Mon Sep 17 00:00:00 2001 From: "Gregory H. Halverson" Date: Tue, 13 Jan 2026 11:46:44 -0800 Subject: [PATCH 6/6] v1.17.0 docstrings and README --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ad4611f..90fabba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "rasters" -version = "1.16.1" +version = "1.17.0" description = "raster processing toolkit" readme = "README.md" authors = [