plaknit 0.3.2__tar.gz → 0.3.4__tar.gz

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.
Files changed (43) hide show
  1. {plaknit-0.3.2/plaknit.egg-info → plaknit-0.3.4}/PKG-INFO +1 -1
  2. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/__init__.py +1 -1
  3. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/cli/ensemble_cli.py +13 -1
  4. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/data/sampling.py +51 -21
  5. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/models/brt.py +35 -4
  6. plaknit-0.3.4/plaknit/models/ensemble.py +1366 -0
  7. {plaknit-0.3.2 → plaknit-0.3.4/plaknit.egg-info}/PKG-INFO +1 -1
  8. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit.egg-info/SOURCES.txt +1 -0
  9. {plaknit-0.3.2 → plaknit-0.3.4}/pyproject.toml +2 -2
  10. {plaknit-0.3.2 → plaknit-0.3.4}/tests/test_classify.py +33 -0
  11. plaknit-0.3.4/tests/test_ensemble_prediction.py +87 -0
  12. plaknit-0.3.2/plaknit/models/ensemble.py +0 -757
  13. {plaknit-0.3.2 → plaknit-0.3.4}/LICENSE +0 -0
  14. {plaknit-0.3.2 → plaknit-0.3.4}/MANIFEST.in +0 -0
  15. {plaknit-0.3.2 → plaknit-0.3.4}/README.md +0 -0
  16. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/acquisition/__init__.py +0 -0
  17. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/acquisition/geometry.py +0 -0
  18. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/acquisition/mosaic.py +0 -0
  19. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/acquisition/orders.py +0 -0
  20. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/acquisition/planner.py +0 -0
  21. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/classify.py +0 -0
  22. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/cli/__init__.py +0 -0
  23. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/cli/classify_cli.py +0 -0
  24. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/cli/main.py +0 -0
  25. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/data/__init__.py +0 -0
  26. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/data/raster.py +0 -0
  27. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/models/__init__.py +0 -0
  28. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/models/rf.py +0 -0
  29. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/processing/__init__.py +0 -0
  30. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit/processing/evaluation.py +0 -0
  31. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit.egg-info/dependency_links.txt +0 -0
  32. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit.egg-info/entry_points.txt +0 -0
  33. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit.egg-info/requires.txt +0 -0
  34. {plaknit-0.3.2 → plaknit-0.3.4}/plaknit.egg-info/top_level.txt +0 -0
  35. {plaknit-0.3.2 → plaknit-0.3.4}/requirements.txt +0 -0
  36. {plaknit-0.3.2 → plaknit-0.3.4}/setup.cfg +0 -0
  37. {plaknit-0.3.2 → plaknit-0.3.4}/tests/test_ensemble_cli.py +0 -0
  38. {plaknit-0.3.2 → plaknit-0.3.4}/tests/test_geometry.py +0 -0
  39. {plaknit-0.3.2 → plaknit-0.3.4}/tests/test_mosaic.py +0 -0
  40. {plaknit-0.3.2 → plaknit-0.3.4}/tests/test_orders.py +0 -0
  41. {plaknit-0.3.2 → plaknit-0.3.4}/tests/test_plaknit.py +0 -0
  42. {plaknit-0.3.2 → plaknit-0.3.4}/tests/test_planner.py +0 -0
  43. {plaknit-0.3.2 → plaknit-0.3.4}/tests/test_shared_helpers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plaknit
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Processing Large-Scale PlanetScope Data
5
5
  Author-email: Dryver Finch <dryver2206@gmail.com>
6
6
  License: MIT License
@@ -10,7 +10,7 @@ from .models.rf import predict_rf, smooth_probs, train_rf
10
10
 
11
11
  __author__ = """Dryver Finch"""
12
12
  __email__ = "dryver2206@gmail.com"
13
- __version__ = "0.3.2"
13
+ __version__ = "0.3.4"
14
14
 
15
15
  __all__ = [
16
16
  "BRTEnsemble",
@@ -263,7 +263,18 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
263
263
  "--jobs",
264
264
  type=int,
265
265
  default=1,
266
- help="Parallel workers for block prediction (default: 1). Use -1 for all cores.",
266
+ help=(
267
+ "Parallel workers for block prediction (default: 1). Use -1 for all "
268
+ "cores on CPU, or up to the number of visible GPUs with --gpu."
269
+ ),
270
+ )
271
+ predict_parser.add_argument(
272
+ "--gpu",
273
+ action="store_true",
274
+ help=(
275
+ "Use CUDA for XGBoost prediction when available. With --jobs > 1, "
276
+ "models are distributed across persistent GPU workers."
277
+ ),
267
278
  )
268
279
  predict_parser.add_argument(
269
280
  "--block-overlap",
@@ -338,6 +349,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
338
349
  feature_importance_out=args.feature_importance_out,
339
350
  block_overlap=args.block_overlap,
340
351
  jobs=args.jobs,
352
+ gpu=args.gpu,
341
353
  )
342
354
  return 0
343
355
 
@@ -189,33 +189,63 @@ def _collect_pseudo_absence_candidate_pool(
189
189
  *,
190
190
  buffer_meters: float,
191
191
  ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
192
- """Collect valid background features and coordinates eligible for sampling."""
192
+ """Collect valid background features and coordinates eligible for sampling.
193
+
194
+ Raster data is read in bounded windows so large VRTs do not require a full
195
+ raster-sized float32 allocation during candidate collection.
196
+ """
193
197
 
194
198
  if buffer_meters < 0:
195
199
  raise ValueError("training buffer must be non-negative.")
196
200
 
197
- full_window = windows.Window(0, 0, stack.width, stack.height)
198
- data = stack.read(window=full_window, out_dtype="float32")
199
- samples = data.reshape(stack.count, -1).T
200
- valid = ~_nodata_pixel_mask(samples, stack.nodata_values)
201
-
202
201
  geometries = _buffer_training_geometries(stack, gdf, buffer_meters)
203
- if geometries:
204
- excluded = features.rasterize(
205
- [(geom, 1) for geom in geometries],
206
- out_shape=(stack.height, stack.width),
207
- transform=stack.transform,
208
- fill=0,
209
- dtype="uint8",
210
- ).reshape(-1)
211
- valid &= excluded == 0
212
-
213
- candidate_indices = np.flatnonzero(valid)
214
- rows, cols = np.unravel_index(candidate_indices, (stack.height, stack.width))
202
+ feature_chunks: List[np.ndarray] = []
203
+ row_chunks: List[np.ndarray] = []
204
+ col_chunks: List[np.ndarray] = []
205
+ block_size = 512
206
+ for row_off in range(0, stack.height, block_size):
207
+ for col_off in range(0, stack.width, block_size):
208
+ win = windows.Window(
209
+ col_off=col_off,
210
+ row_off=row_off,
211
+ width=min(block_size, stack.width - col_off),
212
+ height=min(block_size, stack.height - row_off),
213
+ )
214
+ data = stack.read(window=win, out_dtype="float32")
215
+ samples = data.reshape(stack.count, -1).T
216
+ valid = ~_nodata_pixel_mask(samples, stack.nodata_values)
217
+
218
+ if geometries:
219
+ block_transform = windows.transform(win, stack.transform)
220
+ excluded = features.rasterize(
221
+ [(geom, 1) for geom in geometries],
222
+ out_shape=(int(win.height), int(win.width)),
223
+ transform=block_transform,
224
+ fill=0,
225
+ dtype="uint8",
226
+ ).reshape(-1)
227
+ valid &= excluded == 0
228
+
229
+ candidate_indices = np.flatnonzero(valid)
230
+ if candidate_indices.size == 0:
231
+ continue
232
+ rows, cols = np.unravel_index(
233
+ candidate_indices, (int(win.height), int(win.width))
234
+ )
235
+ feature_chunks.append(samples[candidate_indices])
236
+ row_chunks.append((rows + int(win.row_off)).astype("int32", copy=False))
237
+ col_chunks.append((cols + int(win.col_off)).astype("int32", copy=False))
238
+
239
+ if not feature_chunks:
240
+ return (
241
+ np.empty((0, stack.count), dtype="float32"),
242
+ np.empty(0, dtype="int32"),
243
+ np.empty(0, dtype="int32"),
244
+ )
215
245
  return (
216
- samples[candidate_indices],
217
- rows.astype("int32", copy=False),
218
- cols.astype("int32", copy=False),
246
+ np.vstack(feature_chunks),
247
+ np.concatenate(row_chunks),
248
+ np.concatenate(col_chunks),
219
249
  )
220
250
 
221
251
 
@@ -9,6 +9,7 @@ import os
9
9
  import warnings
10
10
  from dataclasses import dataclass
11
11
  from pathlib import Path
12
+ from functools import lru_cache
12
13
  from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
13
14
 
14
15
  import geopandas as gpd
@@ -151,12 +152,37 @@ def _prepare_brt_training_data(
151
152
 
152
153
  def _detect_gpu_available() -> bool:
153
154
  """Detect if GPU is available for XGBoost."""
155
+ return bool(_available_cuda_devices())
156
+
157
+
158
+ @lru_cache(maxsize=1)
159
+ def _available_cuda_devices() -> List[int]:
160
+ """Return logical CUDA device IDs visible to the current process."""
154
161
  try:
155
- import cupy as cp # noqa: F401
162
+ import cupy as cp
163
+
164
+ count = int(cp.cuda.runtime.getDeviceCount())
165
+ except (ImportError, RuntimeError, AttributeError):
166
+ return []
167
+ return list(range(max(0, count)))
168
+
156
169
 
157
- return True
158
- except ImportError:
159
- return False
170
+ def _configure_prediction_device(
171
+ model: xgb.XGBClassifier, gpu: bool, device_id: Optional[int] = None
172
+ ) -> None:
173
+ """Configure the loaded XGBoost model for CPU or CUDA inference."""
174
+ if not gpu:
175
+ return
176
+ if not _detect_gpu_available():
177
+ raise RuntimeError(
178
+ "GPU prediction requested, but CuPy/CUDA is unavailable. "
179
+ "Install the GPU dependencies or omit --gpu."
180
+ )
181
+ device = "cuda" if device_id is None else f"cuda:{device_id}"
182
+ try:
183
+ model.set_params(device=device, tree_method="hist", n_jobs=1)
184
+ except (AttributeError, TypeError, ValueError):
185
+ model.set_params(predictor="gpu_predictor", tree_method="gpu_hist", n_jobs=1)
160
186
 
161
187
 
162
188
  def train_brt(
@@ -658,6 +684,7 @@ def _init_predict_worker(
658
684
  out_dtype: str,
659
685
  nodata_value: Union[int, float],
660
686
  return_probs: bool,
687
+ gpu: bool,
661
688
  ) -> None:
662
689
  global _PREDICT_STACK
663
690
  global _PREDICT_MODEL
@@ -669,6 +696,7 @@ def _init_predict_worker(
669
696
  stack.__enter__()
670
697
  _PREDICT_STACK = stack
671
698
  _PREDICT_MODEL = joblib.load(model_path)
699
+ _configure_prediction_device(_PREDICT_MODEL, gpu)
672
700
  _PREDICT_OUT_DTYPE = out_dtype
673
701
  _PREDICT_NODATA = nodata_value
674
702
  _PREDICT_RETURN_PROBS = return_probs
@@ -706,6 +734,7 @@ def predict_brt(
706
734
  binary_out: Optional[PathLike] = None,
707
735
  block_overlap: int = 0,
708
736
  jobs: int = 1,
737
+ gpu: bool = False,
709
738
  ) -> Path:
710
739
  """Apply a trained BRT (XGBoost) model to a raster stack and write a classified GeoTIFF.
711
740
 
@@ -724,6 +753,7 @@ def predict_brt(
724
753
 
725
754
  _log("[bold cyan]Loading model...")
726
755
  model: xgb.XGBClassifier = joblib.load(model_path)
756
+ _configure_prediction_device(model, gpu)
727
757
  classes = getattr(model, "classes_", None)
728
758
  classes_dtype = getattr(classes, "dtype", np.int32)
729
759
  if np.issubdtype(classes_dtype, np.integer):
@@ -828,6 +858,7 @@ def predict_brt(
828
858
  out_dtype,
829
859
  nodata_value,
830
860
  return_probs,
861
+ gpu,
831
862
  ),
832
863
  ) as executor:
833
864
  futures: Dict[concurrent.futures.Future, windows.Window] = {}