plaknit 0.3.2__tar.gz → 0.3.3__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.
- {plaknit-0.3.2/plaknit.egg-info → plaknit-0.3.3}/PKG-INFO +1 -1
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/__init__.py +1 -1
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/cli/ensemble_cli.py +13 -1
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/data/sampling.py +51 -21
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/models/brt.py +35 -4
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/models/ensemble.py +191 -9
- {plaknit-0.3.2 → plaknit-0.3.3/plaknit.egg-info}/PKG-INFO +1 -1
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit.egg-info/SOURCES.txt +1 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/pyproject.toml +2 -2
- plaknit-0.3.3/tests/test_ensemble_prediction.py +47 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/LICENSE +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/MANIFEST.in +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/README.md +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/acquisition/__init__.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/acquisition/geometry.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/acquisition/mosaic.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/acquisition/orders.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/acquisition/planner.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/classify.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/cli/__init__.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/cli/classify_cli.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/cli/main.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/data/__init__.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/data/raster.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/models/__init__.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/models/rf.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/processing/__init__.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit/processing/evaluation.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit.egg-info/dependency_links.txt +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit.egg-info/entry_points.txt +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit.egg-info/requires.txt +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/plaknit.egg-info/top_level.txt +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/requirements.txt +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/setup.cfg +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/tests/test_classify.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/tests/test_ensemble_cli.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/tests/test_geometry.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/tests/test_mosaic.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/tests/test_orders.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/tests/test_plaknit.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/tests/test_planner.py +0 -0
- {plaknit-0.3.2 → plaknit-0.3.3}/tests/test_shared_helpers.py +0 -0
|
@@ -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=
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
246
|
+
np.vstack(feature_chunks),
|
|
247
|
+
np.concatenate(row_chunks),
|
|
248
|
+
np.concatenate(col_chunks),
|
|
219
249
|
)
|
|
220
250
|
|
|
221
251
|
|
|
@@ -151,12 +151,38 @@ def _prepare_brt_training_data(
|
|
|
151
151
|
|
|
152
152
|
def _detect_gpu_available() -> bool:
|
|
153
153
|
"""Detect if GPU is available for XGBoost."""
|
|
154
|
+
return bool(_available_cuda_devices())
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _available_cuda_devices() -> List[int]:
|
|
158
|
+
"""Return logical CUDA device IDs visible to the current process."""
|
|
154
159
|
try:
|
|
155
|
-
import cupy as cp
|
|
160
|
+
import cupy as cp
|
|
161
|
+
|
|
162
|
+
count = int(cp.cuda.runtime.getDeviceCount())
|
|
163
|
+
except (ImportError, RuntimeError, AttributeError):
|
|
164
|
+
return []
|
|
165
|
+
return list(range(max(0, count)))
|
|
166
|
+
|
|
156
167
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
def _configure_prediction_device(
|
|
169
|
+
model: xgb.XGBClassifier, gpu: bool, device_id: Optional[int] = None
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Configure the loaded XGBoost model for CPU or CUDA inference."""
|
|
172
|
+
if not gpu:
|
|
173
|
+
return
|
|
174
|
+
if not _detect_gpu_available():
|
|
175
|
+
raise RuntimeError(
|
|
176
|
+
"GPU prediction requested, but CuPy/CUDA is unavailable. "
|
|
177
|
+
"Install the GPU dependencies or omit --gpu."
|
|
178
|
+
)
|
|
179
|
+
device = "cuda" if device_id is None else f"cuda:{device_id}"
|
|
180
|
+
try:
|
|
181
|
+
model.set_params(device=device, tree_method="hist", n_jobs=1)
|
|
182
|
+
except (AttributeError, TypeError, ValueError):
|
|
183
|
+
model.set_params(
|
|
184
|
+
predictor="gpu_predictor", tree_method="gpu_hist", n_jobs=1
|
|
185
|
+
)
|
|
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] = {}
|
|
@@ -7,7 +7,9 @@ import concurrent.futures
|
|
|
7
7
|
import contextlib
|
|
8
8
|
import csv
|
|
9
9
|
import json
|
|
10
|
+
import multiprocessing
|
|
10
11
|
import os
|
|
12
|
+
import queue
|
|
11
13
|
import tempfile
|
|
12
14
|
import warnings
|
|
13
15
|
from pathlib import Path
|
|
@@ -35,6 +37,20 @@ from ..processing.evaluation import (
|
|
|
35
37
|
|
|
36
38
|
PathLike = Union[str, Path]
|
|
37
39
|
WindowTuple = Tuple[int, int, int, int]
|
|
40
|
+
_WorkerResult = Tuple[int, List[int], np.ndarray]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _assign_models_to_workers(n_models: int, n_workers: int) -> List[List[int]]:
|
|
44
|
+
"""Assign every model exactly once across workers in round-robin order."""
|
|
45
|
+
if n_models < 1:
|
|
46
|
+
raise ValueError("n_models must be at least 1.")
|
|
47
|
+
if n_workers < 1:
|
|
48
|
+
raise ValueError("n_workers must be at least 1.")
|
|
49
|
+
worker_count = min(n_models, n_workers)
|
|
50
|
+
return [
|
|
51
|
+
[model_idx for model_idx in range(n_models) if model_idx % worker_count == worker_id]
|
|
52
|
+
for worker_id in range(worker_count)
|
|
53
|
+
]
|
|
38
54
|
|
|
39
55
|
|
|
40
56
|
def _write_model_summary_csv(
|
|
@@ -131,6 +147,23 @@ def _write_feature_importance_csv(path: Path, models: Sequence[Any]) -> None:
|
|
|
131
147
|
writer.writerow(row)
|
|
132
148
|
|
|
133
149
|
|
|
150
|
+
def _aggregate_ensemble_probabilities(
|
|
151
|
+
member_probs: np.ndarray,
|
|
152
|
+
ci_t_crit: Optional[float],
|
|
153
|
+
) -> Tuple[np.ndarray, Optional[np.ndarray], Optional[np.ndarray]]:
|
|
154
|
+
"""Aggregate complete ensemble probabilities for one output window."""
|
|
155
|
+
mean = member_probs.mean(axis=0)
|
|
156
|
+
if ci_t_crit is None or member_probs.shape[0] < 2:
|
|
157
|
+
return mean, None, None
|
|
158
|
+
std = member_probs.std(axis=0, ddof=1)
|
|
159
|
+
half_width = ci_t_crit * std / np.sqrt(member_probs.shape[0])
|
|
160
|
+
return (
|
|
161
|
+
mean,
|
|
162
|
+
np.clip(mean - half_width, 0.0, 1.0),
|
|
163
|
+
np.clip(mean + half_width, 0.0, 1.0),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
134
167
|
def _predict_ensemble_block(
|
|
135
168
|
stack: _RasterStack,
|
|
136
169
|
models: List[Any],
|
|
@@ -192,7 +225,9 @@ def _predict_ensemble_block(
|
|
|
192
225
|
],
|
|
193
226
|
axis=0,
|
|
194
227
|
)
|
|
195
|
-
mean =
|
|
228
|
+
mean, lower, upper = _aggregate_ensemble_probabilities(
|
|
229
|
+
probs_stack, ci_t_crit
|
|
230
|
+
)
|
|
196
231
|
|
|
197
232
|
def _scatter(values: np.ndarray) -> np.ndarray:
|
|
198
233
|
full = np.full((samples.shape[0], num_classes), np.nan, dtype="float32")
|
|
@@ -203,12 +238,8 @@ def _predict_ensemble_block(
|
|
|
203
238
|
|
|
204
239
|
mean_cube = _scatter(mean)
|
|
205
240
|
|
|
206
|
-
lower_cube =
|
|
207
|
-
|
|
208
|
-
std = probs_stack.std(axis=0, ddof=1)
|
|
209
|
-
half_width = ci_t_crit * std / np.sqrt(n_models)
|
|
210
|
-
lower_cube = _scatter(np.clip(mean - half_width, 0.0, 1.0))
|
|
211
|
-
upper_cube = _scatter(np.clip(mean + half_width, 0.0, 1.0))
|
|
241
|
+
lower_cube = _scatter(lower) if lower is not None else None
|
|
242
|
+
upper_cube = _scatter(upper) if upper is not None else None
|
|
212
243
|
|
|
213
244
|
member_cube = np.full(
|
|
214
245
|
(n_models, samples.shape[0], num_classes), np.nan, dtype="float32"
|
|
@@ -251,6 +282,7 @@ def _init_ensemble_predict_worker(
|
|
|
251
282
|
model_paths: List[str],
|
|
252
283
|
classes: List[Any],
|
|
253
284
|
ci_t_crit: Optional[float],
|
|
285
|
+
gpu: bool,
|
|
254
286
|
) -> None:
|
|
255
287
|
global _ENSEMBLE_PREDICT_STACK
|
|
256
288
|
global _ENSEMBLE_PREDICT_MODELS
|
|
@@ -261,6 +293,8 @@ def _init_ensemble_predict_worker(
|
|
|
261
293
|
stack.__enter__()
|
|
262
294
|
_ENSEMBLE_PREDICT_STACK = stack
|
|
263
295
|
_ENSEMBLE_PREDICT_MODELS = [joblib.load(path) for path in model_paths]
|
|
296
|
+
for model in _ENSEMBLE_PREDICT_MODELS:
|
|
297
|
+
brt._configure_prediction_device(model, gpu)
|
|
264
298
|
_ENSEMBLE_PREDICT_CLASSES = np.asarray(classes)
|
|
265
299
|
_ENSEMBLE_PREDICT_CI_T_CRIT = ci_t_crit
|
|
266
300
|
atexit.register(_close_ensemble_predict_worker)
|
|
@@ -285,6 +319,43 @@ def _predict_ensemble_block_worker(
|
|
|
285
319
|
)
|
|
286
320
|
|
|
287
321
|
|
|
322
|
+
def _ensemble_gpu_worker(
|
|
323
|
+
image_paths: List[str],
|
|
324
|
+
band_indices: Optional[Sequence[int]],
|
|
325
|
+
model_paths: List[str],
|
|
326
|
+
model_indices: List[int],
|
|
327
|
+
classes: List[Any],
|
|
328
|
+
device_id: int,
|
|
329
|
+
task_queue: Any,
|
|
330
|
+
result_queue: Any,
|
|
331
|
+
) -> None:
|
|
332
|
+
"""Run assigned ensemble models on one persistent CUDA worker."""
|
|
333
|
+
try:
|
|
334
|
+
with _open_raster_stack(image_paths, band_indices=band_indices) as stack:
|
|
335
|
+
models = [joblib.load(model_paths[idx]) for idx in model_indices]
|
|
336
|
+
for model in models:
|
|
337
|
+
brt._configure_prediction_device(model, True, device_id=device_id)
|
|
338
|
+
|
|
339
|
+
while True:
|
|
340
|
+
task = task_queue.get()
|
|
341
|
+
if task is None:
|
|
342
|
+
return
|
|
343
|
+
window_id, win_tuple, block_overlap = task
|
|
344
|
+
member_probs, _, _, _ = _predict_ensemble_block(
|
|
345
|
+
stack,
|
|
346
|
+
models,
|
|
347
|
+
brt._tuple_to_window(win_tuple),
|
|
348
|
+
classes=np.asarray(classes),
|
|
349
|
+
block_overlap=block_overlap,
|
|
350
|
+
ci_t_crit=None,
|
|
351
|
+
)
|
|
352
|
+
result_queue.put(
|
|
353
|
+
("result", window_id, model_indices, member_probs)
|
|
354
|
+
)
|
|
355
|
+
except BaseException as exc:
|
|
356
|
+
result_queue.put(("error", repr(exc)))
|
|
357
|
+
|
|
358
|
+
|
|
288
359
|
class BRTEnsemble:
|
|
289
360
|
"""Ensemble of independent BRT models with probability summaries.
|
|
290
361
|
|
|
@@ -508,6 +579,7 @@ class BRTEnsemble:
|
|
|
508
579
|
feature_importance_out: Optional[PathLike] = None,
|
|
509
580
|
block_overlap: int = 0,
|
|
510
581
|
jobs: int = 1,
|
|
582
|
+
gpu: bool = False,
|
|
511
583
|
) -> Path:
|
|
512
584
|
"""Write unweighted ensemble probability summaries for a raster stack.
|
|
513
585
|
|
|
@@ -534,7 +606,11 @@ class BRTEnsemble:
|
|
|
534
606
|
block_overlap
|
|
535
607
|
Number of pixels to overlap blocks for smoothing edge artifacts.
|
|
536
608
|
jobs
|
|
537
|
-
Number of parallel worker processes.
|
|
609
|
+
Number of parallel worker processes. In GPU mode, this is capped by
|
|
610
|
+
the number of visible CUDA devices and ensemble models.
|
|
611
|
+
gpu
|
|
612
|
+
If True, configure each XGBoost model for CUDA prediction. Worker
|
|
613
|
+
processes run concurrently when ``jobs`` is greater than one.
|
|
538
614
|
|
|
539
615
|
Returns
|
|
540
616
|
-------
|
|
@@ -566,6 +642,14 @@ class BRTEnsemble:
|
|
|
566
642
|
)
|
|
567
643
|
|
|
568
644
|
models = [joblib.load(path) for path in model_paths]
|
|
645
|
+
cuda_devices = brt._available_cuda_devices() if gpu else []
|
|
646
|
+
if gpu and not cuda_devices:
|
|
647
|
+
raise RuntimeError(
|
|
648
|
+
"GPU prediction requested, but no visible CUDA devices were found."
|
|
649
|
+
)
|
|
650
|
+
if gpu and jobs <= 1:
|
|
651
|
+
for model in models:
|
|
652
|
+
brt._configure_prediction_device(model, True, device_id=0)
|
|
569
653
|
self.models_ = models
|
|
570
654
|
|
|
571
655
|
# Verify class consistency
|
|
@@ -676,7 +760,104 @@ class BRTEnsemble:
|
|
|
676
760
|
if upper_dst is not None and upper_probs is not None:
|
|
677
761
|
upper_dst.write(upper_probs, window=win)
|
|
678
762
|
|
|
679
|
-
if jobs > 1:
|
|
763
|
+
if gpu and jobs > 1:
|
|
764
|
+
worker_count = min(jobs, len(models), len(cuda_devices))
|
|
765
|
+
assignments = _assign_models_to_workers(
|
|
766
|
+
len(models), worker_count
|
|
767
|
+
)
|
|
768
|
+
context = multiprocessing.get_context("spawn")
|
|
769
|
+
task_queues = [context.Queue(maxsize=2) for _ in assignments]
|
|
770
|
+
result_queue = context.Queue()
|
|
771
|
+
workers = [
|
|
772
|
+
context.Process(
|
|
773
|
+
target=_ensemble_gpu_worker,
|
|
774
|
+
args=(
|
|
775
|
+
[str(path) for path in stack.paths],
|
|
776
|
+
band_indices_to_use,
|
|
777
|
+
[str(path) for path in model_paths],
|
|
778
|
+
assignment,
|
|
779
|
+
classes_arr.tolist(),
|
|
780
|
+
cuda_devices[worker_id],
|
|
781
|
+
task_queue,
|
|
782
|
+
result_queue,
|
|
783
|
+
),
|
|
784
|
+
)
|
|
785
|
+
for worker_id, (assignment, task_queue) in enumerate(
|
|
786
|
+
zip(assignments, task_queues)
|
|
787
|
+
)
|
|
788
|
+
]
|
|
789
|
+
for worker in workers:
|
|
790
|
+
worker.start()
|
|
791
|
+
try:
|
|
792
|
+
for window_id, win in enumerate(window_iter):
|
|
793
|
+
task = (
|
|
794
|
+
window_id,
|
|
795
|
+
brt._window_to_tuple(win),
|
|
796
|
+
block_overlap,
|
|
797
|
+
)
|
|
798
|
+
for task_queue in task_queues:
|
|
799
|
+
task_queue.put(task)
|
|
800
|
+
|
|
801
|
+
partial_results: Dict[int, np.ndarray] = {}
|
|
802
|
+
for _ in workers:
|
|
803
|
+
try:
|
|
804
|
+
message = result_queue.get(timeout=300)
|
|
805
|
+
except queue.Empty as exc:
|
|
806
|
+
dead = [
|
|
807
|
+
worker.pid
|
|
808
|
+
for worker in workers
|
|
809
|
+
if not worker.is_alive()
|
|
810
|
+
]
|
|
811
|
+
raise RuntimeError(
|
|
812
|
+
"GPU prediction worker stopped before "
|
|
813
|
+
f"completing window {window_id}; pids={dead}."
|
|
814
|
+
) from exc
|
|
815
|
+
if message[0] == "error":
|
|
816
|
+
raise RuntimeError(
|
|
817
|
+
f"GPU prediction worker failed: {message[1]}"
|
|
818
|
+
)
|
|
819
|
+
_, returned_window_id, indices, member_probs = message
|
|
820
|
+
if returned_window_id != window_id:
|
|
821
|
+
raise RuntimeError(
|
|
822
|
+
"GPU prediction returned an unexpected window."
|
|
823
|
+
)
|
|
824
|
+
for local_idx, model_idx in enumerate(indices):
|
|
825
|
+
if model_idx in partial_results:
|
|
826
|
+
raise RuntimeError(
|
|
827
|
+
f"Duplicate GPU prediction for model {model_idx}."
|
|
828
|
+
)
|
|
829
|
+
partial_results[model_idx] = member_probs[local_idx]
|
|
830
|
+
|
|
831
|
+
if len(partial_results) != len(models):
|
|
832
|
+
raise RuntimeError(
|
|
833
|
+
"GPU prediction returned an incomplete ensemble "
|
|
834
|
+
f"for window {window_id}."
|
|
835
|
+
)
|
|
836
|
+
full_member_probs = np.stack(
|
|
837
|
+
[partial_results[idx] for idx in range(len(models))],
|
|
838
|
+
axis=0,
|
|
839
|
+
)
|
|
840
|
+
mean_probs, lower_probs, upper_probs = (
|
|
841
|
+
_aggregate_ensemble_probabilities(
|
|
842
|
+
full_member_probs, ci_t_crit
|
|
843
|
+
)
|
|
844
|
+
)
|
|
845
|
+
_write_block_result(
|
|
846
|
+
win,
|
|
847
|
+
full_member_probs,
|
|
848
|
+
mean_probs,
|
|
849
|
+
lower_probs,
|
|
850
|
+
upper_probs,
|
|
851
|
+
)
|
|
852
|
+
finally:
|
|
853
|
+
for task_queue in task_queues:
|
|
854
|
+
task_queue.put(None)
|
|
855
|
+
for worker in workers:
|
|
856
|
+
worker.join(timeout=10)
|
|
857
|
+
if worker.is_alive():
|
|
858
|
+
worker.terminate()
|
|
859
|
+
worker.join()
|
|
860
|
+
elif jobs > 1:
|
|
680
861
|
max_workers = jobs
|
|
681
862
|
max_pending = max_workers * 2
|
|
682
863
|
image_paths = [str(path) for path in stack.paths]
|
|
@@ -690,6 +871,7 @@ class BRTEnsemble:
|
|
|
690
871
|
model_path_strs,
|
|
691
872
|
classes_arr.tolist(),
|
|
692
873
|
ci_t_crit,
|
|
874
|
+
gpu,
|
|
693
875
|
),
|
|
694
876
|
) as executor:
|
|
695
877
|
futures: Dict[concurrent.futures.Future, windows.Window] = (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "plaknit"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.3"
|
|
4
4
|
description = "Processing Large-Scale PlanetScope Data"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.8"
|
|
@@ -43,7 +43,7 @@ exclude = ["docs*"]
|
|
|
43
43
|
dependencies = { file = ["requirements.txt"] }
|
|
44
44
|
|
|
45
45
|
[tool.bumpversion]
|
|
46
|
-
current_version = "0.3.
|
|
46
|
+
current_version = "0.3.3"
|
|
47
47
|
commit = true
|
|
48
48
|
tag = true
|
|
49
49
|
tag_name = "v{new_version}"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from plaknit.models.ensemble import (
|
|
5
|
+
_aggregate_ensemble_probabilities,
|
|
6
|
+
_assign_models_to_workers,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_assign_models_to_workers_round_robin() -> None:
|
|
11
|
+
assert _assign_models_to_workers(5, 3) == [[0, 3], [1, 4], [2]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_assign_models_to_workers_caps_workers_at_model_count() -> None:
|
|
15
|
+
assert _assign_models_to_workers(2, 8) == [[0], [1]]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_assign_models_to_workers_rejects_empty_inputs() -> None:
|
|
19
|
+
with pytest.raises(ValueError):
|
|
20
|
+
_assign_models_to_workers(0, 1)
|
|
21
|
+
with pytest.raises(ValueError):
|
|
22
|
+
_assign_models_to_workers(1, 0)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_aggregate_ensemble_probabilities_matches_sample_ci() -> None:
|
|
26
|
+
member_probs = np.asarray(
|
|
27
|
+
[
|
|
28
|
+
[[[0.1, 0.9]]],
|
|
29
|
+
[[[0.2, 0.8]]],
|
|
30
|
+
[[[0.3, 0.7]]],
|
|
31
|
+
],
|
|
32
|
+
dtype="float32",
|
|
33
|
+
)
|
|
34
|
+
mean, lower, upper = _aggregate_ensemble_probabilities(member_probs, 2.0)
|
|
35
|
+
expected_mean = member_probs.mean(axis=0)
|
|
36
|
+
expected_half_width = 2.0 * member_probs.std(axis=0, ddof=1) / np.sqrt(3)
|
|
37
|
+
np.testing.assert_allclose(mean, expected_mean)
|
|
38
|
+
np.testing.assert_allclose(lower, np.clip(expected_mean - expected_half_width, 0, 1))
|
|
39
|
+
np.testing.assert_allclose(upper, np.clip(expected_mean + expected_half_width, 0, 1))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_aggregate_single_model_has_no_interval() -> None:
|
|
43
|
+
member_probs = np.asarray([[[[0.4]]]], dtype="float32")
|
|
44
|
+
mean, lower, upper = _aggregate_ensemble_probabilities(member_probs, None)
|
|
45
|
+
np.testing.assert_array_equal(mean, member_probs[0])
|
|
46
|
+
assert lower is None
|
|
47
|
+
assert upper is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|