Skip to content

Commit b85cfff

Browse files
authored
Merge pull request #27 from onaio/feb3-cleanup
Feb3 cleanup
2 parents 6791e70 + e7852af commit b85cfff

23 files changed

Lines changed: 1601 additions & 3565 deletions
Lines changed: 26 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,43 @@
11
suppressPackageStartupMessages({
22
library(RANN)
3-
library(sf)
4-
library(jsonlite)
53
})
64

75
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']]
139

10+
n <- nrow(coords)
1411

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)
1715

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)
3918

40-
# Loop
19+
# Iteratively select remaining points using uncertainty * spatial diversity
4120
if (batch_size > 1) {
4221
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)
4723

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])
7226

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)
7430

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+
}
7939
}
8040

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)))
9243
}

fn-adaptive-sampling-0.3.1/fn-adaptive-sampling/install_packages.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33

44
# For some reason `geojsonio` is not yet in rocker/geospatial.
55
# Remove from below if you don't need it.
6-
install.packages(c('geojsonio', 'RANN'))
6+
install.packages('RANN')
Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,46 @@
1-
suppressPackageStartupMessages(library(sf))
2-
31
function(params) {
4-
# if batch_size exists, must be a number > 0
2+
# Validate batch_size
53
if (!is.null(params[['batch_size']])) {
64
if (!is.numeric(params[['batch_size']])) {
75
stop('Parameter `batch_size` is not numeric')
86
}
9-
107
if (params[['batch_size']] < 1) {
118
stop('Parameter `batch_size` must be greater than zero')
129
}
1310
} else {
1411
params[['batch_size']] = 1
1512
}
1613

17-
18-
19-
# Individual check for each parameter
20-
if (is.null(params[['point_data']])) {
21-
stop('Missing required `point_data` parameter')
14+
# Validate required parameters
15+
if (is.null(params[['coordinates']])) {
16+
stop('Missing required `coordinates` parameter')
2217
}
2318

24-
if (is.null(params[['uncertainty_fieldname']])) {
25-
stop('Mising required `uncertainty_fieldname` parameter')
19+
if (is.null(params[['uncertainty']])) {
20+
stop('Missing required `uncertainty` parameter')
2621
}
2722

28-
point_data = st_read(as.json(params[['point_data']]), quiet=T)
29-
params[['point_data']] = point_data
23+
# Convert coordinates to matrix and uncertainty to numeric vector.
24+
# fromJSON may return a matrix (uniform array) or a list (ragged),
25+
# so handle both.
26+
coords <- params[['coordinates']]
27+
if (!is.matrix(coords)) {
28+
coords <- do.call(rbind, coords)
29+
}
30+
params[['coordinates']] <- coords
31+
params[['uncertainty']] <- as.numeric(unlist(params[['uncertainty']]))
3032

31-
# Check
32-
uncertainty_fieldname = params[['uncertainty_fieldname']]
33+
n_coords <- nrow(params[['coordinates']])
34+
n_uncertainty <- length(params[['uncertainty']])
3335

34-
# Check if uncertainty field exists in the data
35-
point_data_df = as.data.frame(point_data)
36-
if (!(uncertainty_fieldname %in% names(point_data_df))) {
37-
stop(paste0('Uncertainty field "', uncertainty_fieldname, '" not found in data. Available fields: ', paste(names(point_data_df), collapse=', ')))
36+
if (n_coords != n_uncertainty) {
37+
stop(paste0('coordinates (', n_coords, ') and uncertainty (', n_uncertainty, ') must have same length'))
3838
}
3939

40-
rows_we_can_sample = sum(!is.na(point_data_df[, uncertainty_fieldname]))
41-
if (params[['batch_size']] > rows_we_can_sample) {
42-
stop('Batch size is larger than the number of points for which uncertainty is available (not NA)')
40+
rows_available <- sum(!is.na(params[['uncertainty']]))
41+
if (params[['batch_size']] > rows_available) {
42+
stop('Batch size is larger than the number of points with non-NA uncertainty')
4343
}
4444

45-
46-
# NOTE: Not all DiSARM functions use a `main.R` file that mutates the params. This one does.
4745
return(params)
48-
}
46+
}

0 commit comments

Comments
 (0)