rastr 0.4.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rastr might be problematic. Click here for more details.

rastr/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.4.0'
32
- __version_tuple__ = version_tuple = (0, 4, 0)
31
+ __version__ = version = '0.6.0'
32
+ __version_tuple__ = version_tuple = (0, 6, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
rastr/arr/fill.py CHANGED
@@ -8,7 +8,7 @@ if TYPE_CHECKING:
8
8
  from numpy.typing import NDArray
9
9
 
10
10
 
11
- def fillna_nearest_neighbours(arr: NDArray[np.float64]) -> NDArray[np.float64]:
11
+ def fillna_nearest_neighbours(arr: NDArray) -> NDArray:
12
12
  """Fill NaN values in an N-dimensional array with their nearest neighbours' values.
13
13
 
14
14
  The nearest neighbour is determined using the Euclidean distance between array
@@ -28,4 +28,5 @@ def fillna_nearest_neighbours(arr: NDArray[np.float64]) -> NDArray[np.float64]:
28
28
  # Interpolate at the array indices
29
29
  interp = NearestNDInterpolator(nonnan_idxs, arr[nonnan_mask])
30
30
  filled_arr = interp(*np.indices(arr.shape))
31
- return filled_arr
31
+ # Preserve the original dtype
32
+ return filled_arr.astype(arr.dtype)
rastr/create.py CHANGED
@@ -13,10 +13,10 @@ from shapely.geometry import Point
13
13
 
14
14
  from rastr.gis.fishnet import create_point_grid, get_point_grid_shape
15
15
  from rastr.meta import RasterMeta
16
- from rastr.raster import RasterModel
16
+ from rastr.raster import Raster
17
17
 
18
18
  if TYPE_CHECKING:
19
- from collections.abc import Iterable
19
+ from collections.abc import Collection, Iterable
20
20
 
21
21
  import geopandas as gpd
22
22
  from numpy.typing import ArrayLike
@@ -49,9 +49,9 @@ def raster_distance_from_polygon(
49
49
  *,
50
50
  raster_meta: RasterMeta,
51
51
  extent_polygon: Polygon | None = None,
52
- snap_raster: RasterModel | None = None,
52
+ snap_raster: Raster | None = None,
53
53
  show_pbar: bool = False,
54
- ) -> RasterModel:
54
+ ) -> Raster:
55
55
  """Make a raster where each cell's value is its centre's distance to a polygon.
56
56
 
57
57
  The raster should use a projected coordinate system.
@@ -116,7 +116,7 @@ def raster_distance_from_polygon(
116
116
  distances = np.where(mask, np.array([polygon.distance(pt) for pt in _pts]), np.nan)
117
117
  distance_raster = distances.reshape(x.shape)
118
118
 
119
- return RasterModel(arr=distance_raster, raster_meta=raster_meta)
119
+ return Raster(arr=distance_raster, raster_meta=raster_meta)
120
120
 
121
121
 
122
122
  def _pbar(iterable: Iterable[_T], *, desc: str | None = None) -> Iterable[_T]:
@@ -130,19 +130,19 @@ def full_raster(
130
130
  *,
131
131
  bounds: tuple[float, float, float, float],
132
132
  fill_value: float = np.nan,
133
- ) -> RasterModel:
133
+ ) -> Raster:
134
134
  """Create a raster with a specified fill value for all cells."""
135
135
  shape = get_point_grid_shape(bounds=bounds, cell_size=raster_meta.cell_size)
136
136
  arr = np.full(shape, fill_value, dtype=np.float32)
137
- return RasterModel(arr=arr, raster_meta=raster_meta)
137
+ return Raster(arr=arr, raster_meta=raster_meta)
138
138
 
139
139
 
140
140
  def rasterize_gdf(
141
141
  gdf: gpd.GeoDataFrame,
142
142
  *,
143
143
  raster_meta: RasterMeta,
144
- target_cols: list[str],
145
- ) -> list[RasterModel]:
144
+ target_cols: Collection[str],
145
+ ) -> list[Raster]:
146
146
  """Rasterize geometries from a GeoDataFrame.
147
147
 
148
148
  Supports polygons, points, linestrings, and other geometry types.
@@ -205,14 +205,16 @@ def rasterize_gdf(
205
205
  dtype=np.float32,
206
206
  )
207
207
 
208
- # Create RasterModel
209
- raster = RasterModel(arr=raster_array, raster_meta=raster_meta)
208
+ # Create Raster
209
+ raster = Raster(arr=raster_array, raster_meta=raster_meta)
210
210
  rasters.append(raster)
211
211
 
212
212
  return rasters
213
213
 
214
214
 
215
- def _validate_columns_exist(gdf: gpd.GeoDataFrame, target_cols: list[str]) -> None:
215
+ def _validate_columns_exist(
216
+ gdf: gpd.GeoDataFrame, target_cols: Collection[str]
217
+ ) -> None:
216
218
  """Validate that all target columns exist in the GeoDataFrame.
217
219
 
218
220
  Args:
@@ -228,7 +230,9 @@ def _validate_columns_exist(gdf: gpd.GeoDataFrame, target_cols: list[str]) -> No
228
230
  raise MissingColumnsError(msg)
229
231
 
230
232
 
231
- def _validate_columns_numeric(gdf: gpd.GeoDataFrame, target_cols: list[str]) -> None:
233
+ def _validate_columns_numeric(
234
+ gdf: gpd.GeoDataFrame, target_cols: Collection[str]
235
+ ) -> None:
232
236
  """Validate that all target columns contain numeric data.
233
237
 
234
238
  Args:
@@ -286,7 +290,7 @@ def raster_from_point_cloud(
286
290
  *,
287
291
  crs: CRS | str,
288
292
  cell_size: float | None = None,
289
- ) -> RasterModel:
293
+ ) -> Raster:
290
294
  """Create a raster from a point cloud via interpolation.
291
295
 
292
296
  Interpolation is only possible within the convex hull of the points. Outside of
@@ -320,8 +324,18 @@ def raster_from_point_cloud(
320
324
  if len(x) != len(y) or len(x) != len(z):
321
325
  msg = "Length of x, y, and z must be equal."
322
326
  raise ValueError(msg)
327
+ xy_finite_mask = np.isfinite(x) & np.isfinite(y)
328
+ if np.any(~xy_finite_mask):
329
+ msg = "Some (x,y) points are NaN-valued or non-finite. These will be ignored."
330
+ warnings.warn(msg, stacklevel=2)
331
+ x = x[xy_finite_mask]
332
+ y = y[xy_finite_mask]
333
+ z = z[xy_finite_mask]
323
334
  if len(x) < 3:
324
- msg = "At least three (x, y, z) points are required to triangulate a surface."
335
+ msg = (
336
+ "At least three valid (x, y, z) points are required to triangulate a "
337
+ "surface."
338
+ )
325
339
  raise ValueError(msg)
326
340
  # Check for duplicate (x, y) points
327
341
  xy_points = np.column_stack((x, y))
@@ -332,8 +346,8 @@ def raster_from_point_cloud(
332
346
  # Heuristic for cell size if not provided
333
347
  if cell_size is None:
334
348
  # Half the 5th percentile of nearest neighbor distances between the (x,y) points
335
- tree = KDTree(np.column_stack((x, y)))
336
- distances, _ = tree.query(np.column_stack((x, y)), k=2)
349
+ tree = KDTree(xy_points)
350
+ distances, _ = tree.query(xy_points, k=2)
337
351
  distances: np.ndarray
338
352
  cell_size = float(np.percentile(distances[distances > 0], 5)) / 2
339
353
 
@@ -378,4 +392,4 @@ def raster_from_point_cloud(
378
392
  crs=crs,
379
393
  transform=transform,
380
394
  )
381
- return RasterModel(arr=arr, raster_meta=raster_meta)
395
+ return Raster(arr=arr, raster_meta=raster_meta)
rastr/gis/fishnet.py CHANGED
@@ -41,8 +41,20 @@ def get_point_grid_shape(
41
41
  """Calculate the shape of the point grid based on bounds and cell size."""
42
42
 
43
43
  xmin, ymin, xmax, ymax = bounds
44
- ncols = int(np.ceil((xmax - xmin) / cell_size))
45
- nrows = int(np.ceil((ymax - ymin) / cell_size))
44
+ ncols_exact = (xmax - xmin) / cell_size
45
+ nrows_exact = (ymax - ymin) / cell_size
46
+
47
+ # Use round for values very close to integers to avoid floating-point
48
+ # sensitivity while maintaining ceil behavior for truly fractional values
49
+ if np.isclose(ncols_exact, np.round(ncols_exact)):
50
+ ncols = int(np.round(ncols_exact))
51
+ else:
52
+ ncols = int(np.ceil(ncols_exact))
53
+
54
+ if np.isclose(nrows_exact, np.round(nrows_exact)):
55
+ nrows = int(np.round(nrows_exact))
56
+ else:
57
+ nrows = int(np.ceil(nrows_exact))
46
58
 
47
59
  return nrows, ncols
48
60
 
rastr/gis/smooth.py CHANGED
@@ -5,7 +5,7 @@ Fork + Port of <https://github.com/philipschall/shapelysmooth> (Public domain)
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from typing import TYPE_CHECKING, TypeAlias
8
+ from typing import TYPE_CHECKING, TypeVar
9
9
 
10
10
  import numpy as np
11
11
  from shapely.geometry import LineString, Polygon
@@ -14,7 +14,7 @@ from typing_extensions import assert_never
14
14
  if TYPE_CHECKING:
15
15
  from numpy.typing import NDArray
16
16
 
17
- T: TypeAlias = LineString | Polygon
17
+ T = TypeVar("T", bound=LineString | Polygon)
18
18
 
19
19
 
20
20
  class InputeTypeError(TypeError):
@@ -38,12 +38,12 @@ def catmull_rom_smooth(geometry: T, alpha: float = 0.5, subdivs: int = 10) -> T:
38
38
  coords, interior_coords = _get_coords(geometry)
39
39
  coords_smoothed = _catmull_rom(coords, alpha=alpha, subdivs=subdivs)
40
40
  if isinstance(geometry, LineString):
41
- return type(geometry)(coords_smoothed)
41
+ return geometry.__class__(coords_smoothed)
42
42
  elif isinstance(geometry, Polygon):
43
43
  interior_coords_smoothed = [
44
44
  _catmull_rom(c, alpha=alpha, subdivs=subdivs) for c in interior_coords
45
45
  ]
46
- return type(geometry)(coords_smoothed, holes=interior_coords_smoothed)
46
+ return geometry.__class__(coords_smoothed, holes=interior_coords_smoothed)
47
47
  else:
48
48
  assert_never(geometry)
49
49
 
rastr/io.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
- from typing import TYPE_CHECKING
4
+ from typing import TYPE_CHECKING, TypeVar
5
5
 
6
6
  import numpy as np
7
7
  import rasterio
@@ -9,39 +9,59 @@ import rasterio.merge
9
9
  from pyproj.crs.crs import CRS
10
10
 
11
11
  from rastr.meta import RasterMeta
12
- from rastr.raster import RasterModel
12
+ from rastr.raster import Raster
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from numpy.typing import NDArray
16
16
 
17
+ R = TypeVar("R", bound=Raster)
18
+
17
19
 
18
20
  def read_raster_inmem(
19
- raster_path: Path | str, *, crs: CRS | str | None = None
20
- ) -> RasterModel:
21
- """Read raster data from a file and return an in-memory Raster object."""
21
+ raster_path: Path | str,
22
+ *,
23
+ crs: CRS | str | None = None,
24
+ cls: type[R] = Raster,
25
+ ) -> R:
26
+ """Read raster data from a file and return an in-memory Raster object.
27
+
28
+ Args:
29
+ raster_path: Path to the raster file.
30
+ crs: Optional CRS to override the raster's native CRS.
31
+ cls: The Raster subclass to instantiate. This is mostly for internal use,
32
+ but can be useful if you have a custom `Raster` subclass.
33
+ """
22
34
  crs = CRS.from_user_input(crs) if crs is not None else None
23
35
 
24
36
  with rasterio.open(raster_path, mode="r") as dst:
25
37
  # Read the entire array
26
- arr: NDArray[np.float64] = dst.read()
27
- arr = arr.squeeze().astype(np.float64)
38
+ raw_arr: NDArray = dst.read()
39
+ raw_arr = raw_arr.squeeze()
40
+
28
41
  # Extract metadata
29
42
  cell_size = dst.res[0]
30
43
  if crs is None:
31
44
  crs = CRS.from_user_input(dst.crs)
32
45
  transform = dst.transform
33
46
  nodata = dst.nodata
47
+
48
+ # Cast integers to float16 to handle NaN values
49
+ if np.issubdtype(raw_arr.dtype, np.integer):
50
+ arr = raw_arr.astype(np.float16)
51
+ else:
52
+ arr = raw_arr
53
+
34
54
  if nodata is not None:
35
- arr[arr == nodata] = np.nan
55
+ arr[raw_arr == nodata] = np.nan
36
56
 
37
57
  raster_meta = RasterMeta(cell_size=cell_size, crs=crs, transform=transform)
38
- raster_obj = RasterModel(arr=arr, raster_meta=raster_meta)
58
+ raster_obj = cls(arr=arr, raster_meta=raster_meta)
39
59
  return raster_obj
40
60
 
41
61
 
42
62
  def read_raster_mosaic_inmem(
43
63
  mosaic_dir: Path | str, *, glob: str = "*.tif", crs: CRS | None = None
44
- ) -> RasterModel:
64
+ ) -> Raster:
45
65
  """Read a raster mosaic from a directory and return an in-memory Raster object.
46
66
 
47
67
  This assumes that all rasters have the same metadata, e.g. coordinate system,
@@ -81,13 +101,19 @@ def read_raster_mosaic_inmem(
81
101
  crs = CRS.from_user_input(sources[0].crs)
82
102
 
83
103
  nodata = sources[0].nodata
84
- if nodata is not None:
85
- arr[arr == nodata] = np.nan
104
+ raw_arr = arr.squeeze()
86
105
 
87
- arr = arr.squeeze().astype(np.float64)
106
+ # Cast integers to float16 to handle NaN values
107
+ if np.issubdtype(raw_arr.dtype, np.integer):
108
+ arr = raw_arr.astype(np.float16)
109
+ else:
110
+ arr = raw_arr
111
+
112
+ if nodata is not None:
113
+ arr[raw_arr == nodata] = np.nan
88
114
 
89
115
  raster_meta = RasterMeta(cell_size=cell_size, crs=crs, transform=transform)
90
- raster_obj = RasterModel(arr=arr, raster_meta=raster_meta)
116
+ raster_obj = Raster(arr=arr, raster_meta=raster_meta)
91
117
  return raster_obj
92
118
  finally:
93
119
  for src in sources: