|
1 | 1 | suppressPackageStartupMessages({ |
2 | 2 | library(RANN) |
3 | | - library(sf) |
4 | | - library(jsonlite) |
5 | 3 | }) |
6 | 4 |
|
7 | 5 | function(params) { |
8 | | - # 1. Handle input |
9 | | - # NOTE: from preprocess_params |
10 | | - point_data = params[['point_data']] |
11 | | - batch_size = params[['batch_size']] |
12 | | - uncertainty_fieldname = params[['uncertainty_fieldname']] |
| 6 | + coords <- params[['coordinates']] # matrix of [lon, lat] from preprocessing |
| 7 | + uncertainty <- params[['uncertainty']] # numeric vector from preprocessing |
| 8 | + batch_size <- params[['batch_size']] |
13 | 9 |
|
| 10 | + n <- nrow(coords) |
14 | 11 |
|
15 | | - # 2. Process |
16 | | - candidates_copy <- point_data |
| 12 | + # Replace zero uncertainty with small value to allow inclusion |
| 13 | + uncertainty[uncertainty == 0] <- 0.0001 |
| 14 | + uncertainty_prob <- uncertainty / sum(uncertainty) |
17 | 15 |
|
18 | | - # Filter out points that have already been visited (have n_positive value) |
19 | | - # Store original row indices for later mapping |
20 | | - if ("n_positive" %in% names(point_data)) { |
21 | | - # Exclude points where n_positive is not NA (already surveyed) |
22 | | - # Keep only points where n_positive is NA (not yet visited) |
23 | | - candidate_indices <- which(is.na(point_data$n_positive)) |
24 | | - candidates <- point_data[candidate_indices, ] |
25 | | - } else { |
26 | | - candidate_indices <- 1:nrow(point_data) |
27 | | - candidates <- point_data |
28 | | - } |
29 | | - |
30 | | - # Change any 0 probabilities to 0.0001 to allow them to be included (effectively randomly) |
31 | | - candidates[[uncertainty_fieldname]] [candidates[[uncertainty_fieldname]]==0] <- 0.0001 |
32 | | - |
33 | | - candidates$uncertainty_prob <- as.data.frame(candidates)[, uncertainty_fieldname] / sum(as.data.frame(candidates)[, uncertainty_fieldname]) |
34 | | - |
35 | | - # Give each an id (for internal tracking during sampling) |
36 | | - candidates$temp_id <- 1:nrow(candidates) |
37 | | - in_sample <- sample(1:nrow(candidates), 1, prob = candidates$uncertainty_prob) |
38 | | - |
| 16 | + # Select first point by uncertainty probability |
| 17 | + in_sample <- sample(1:n, 1, prob = uncertainty_prob) |
39 | 18 |
|
40 | | - # Loop |
| 19 | + # Iteratively select remaining points using uncertainty * spatial diversity |
41 | 20 | if (batch_size > 1) { |
42 | 21 | for (i in 1:(batch_size - 1)) { |
43 | | - |
44 | | - # Define which is in and out of sample |
45 | | - candidates_in_sample <- candidates[in_sample, , drop = FALSE] |
46 | | - candidates_not_in_sample <- candidates[-in_sample, , drop = FALSE] |
| 22 | + not_in_sample <- setdiff(1:n, in_sample) |
47 | 23 |
|
48 | | - # First calc distance between the in_sample and the rest |
49 | | - # Use centroids for both points and polygons |
50 | | - # st_centroid works on any geometry type (points return themselves) |
51 | | - # Suppress warnings about attributes being discarded |
52 | | - coords_in_sample <- suppressWarnings(st_coordinates(st_centroid(candidates_in_sample))) |
53 | | - coords_not_in_sample <- suppressWarnings(st_coordinates(st_centroid(candidates_not_in_sample))) |
54 | | - nn <- nn2(coords_in_sample, coords_not_in_sample) |
55 | | - |
56 | | - # convert distances to selection probability |
57 | | - min_dist_to_other_points <- apply(nn$nn.dists, 1, min) |
58 | | - min_dist_to_other_points <- min_dist_to_other_points / sum(min_dist_to_other_points) |
59 | | - |
60 | | - # Multiply by entropy |
61 | | - candidates_not_in_sample$pen_uncertainty <- |
62 | | - candidates_not_in_sample$uncertainty_prob * min_dist_to_other_points |
63 | | - |
64 | | - candidates_not_in_sample$uncertainty_prob <- |
65 | | - candidates_not_in_sample$pen_uncertainty / sum(candidates_not_in_sample$pen_uncertainty) |
66 | | - |
67 | | - # Sample |
68 | | - uncertainty_sample <- sample(1:nrow(candidates_not_in_sample), 1, prob = candidates_not_in_sample$uncertainty_prob) |
69 | | - in_sample <- c(in_sample, candidates_not_in_sample$temp_id[uncertainty_sample]) |
70 | | - } |
71 | | - } |
| 24 | + # Nearest-neighbor distances from sampled to unsampled points |
| 25 | + nn <- nn2(coords[in_sample, , drop = FALSE], coords[not_in_sample, , drop = FALSE]) |
72 | 26 |
|
73 | | - # 3. Package response |
| 27 | + # Min distance from each unsampled point to any sampled point |
| 28 | + min_dist <- apply(nn$nn.dists, 1, min) |
| 29 | + min_dist <- min_dist / sum(min_dist) |
74 | 30 |
|
75 | | - # Return points with additional column |
76 | | - # Initialize adaptively_selected if it doesn't exist, or preserve existing values |
77 | | - if (!"adaptively_selected" %in% names(candidates_copy)) { |
78 | | - candidates_copy$adaptively_selected <- 0 |
| 31 | + # Combine distance diversity with uncertainty |
| 32 | + pen_uncertainty <- uncertainty_prob[not_in_sample] * min_dist |
| 33 | + pen_uncertainty <- pen_uncertainty / sum(pen_uncertainty) |
| 34 | + |
| 35 | + # Sample next point |
| 36 | + selected <- sample(1:length(not_in_sample), 1, prob = pen_uncertainty) |
| 37 | + in_sample <- c(in_sample, not_in_sample[selected]) |
| 38 | + } |
79 | 39 | } |
80 | 40 |
|
81 | | - # Mark newly selected points - map from candidates temp_ids back to original indices |
82 | | - original_indices <- candidate_indices[in_sample] |
83 | | - candidates_copy$adaptively_selected[original_indices] <- 1 |
84 | | - |
85 | | - # Convert to geojson and handle NA values properly |
86 | | - result <- geojson_list(candidates_copy) |
87 | | - |
88 | | - # Convert the geo_list to a regular list to avoid serialization issues |
89 | | - result_list <- unclass(result) |
90 | | - |
91 | | - return(result_list) |
| 41 | + # Return 0-indexed selected indices for Python |
| 42 | + return(list(selected_indices = as.integer(in_sample - 1))) |
92 | 43 | } |
0 commit comments