Skip to content

Commit b2cafe8

Browse files
authored
Merge pull request #4 from SpatialPlanning/devel
Fix errors with checks
2 parents a51f768 + 2c5150d commit b2cafe8

36 files changed

Lines changed: 6046 additions & 272 deletions

.github/workflows/MacOS.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ jobs:
4646
any::rcmdcheck
4747
any::remotes
4848
any::lpsymphony
49+
any::slam
4950
needs: check
5051

5152
- uses: r-lib/actions/check-r-package@v2

.github/workflows/Ubuntu.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,15 @@ jobs:
4545
sudo apt-get -y install \
4646
libcurl4-gnutls-dev coinor-libsymphony-dev \
4747
libudunits2-dev libgdal-dev libgeos-dev libproj-dev libglpk-dev \
48-
libgmp3-dev libmpfr-dev chromium-browser
48+
libgmp3-dev libmpfr-dev
4949
5050
- uses: r-lib/actions/setup-r-dependencies@v2
5151
with:
5252
extra-packages: |
5353
any::rcmdcheck
5454
any::remotes
5555
any::lpsymphony
56+
any::slam
5657
needs: check
5758

5859
- uses: r-lib/actions/check-r-package@v2

.github/workflows/Windows.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ jobs:
3535

3636
- uses: r-lib/actions/setup-pandoc@v2
3737

38-
- uses: browser-actions/setup-chrome@v1
39-
4038
- uses: r-lib/actions/setup-r@v2
4139
with:
4240
r-version: ${{ matrix.config.r }}
@@ -50,6 +48,7 @@ jobs:
5048
any::rcmdcheck
5149
any::remotes
5250
any::lpsymphony
51+
any::slam
5352
needs: check
5453

5554
- uses: r-lib/actions/check-r-package@v2

.github/workflows/test-coverage.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
sudo apt-get -y install \
3131
libcurl4-gnutls-dev coinor-libsymphony-dev \
3232
libudunits2-dev libgdal-dev libgeos-dev libproj-dev libglpk-dev \
33-
libgmp3-dev libmpfr-dev chromium-browser
33+
libgmp3-dev libmpfr-dev
3434
3535
- name: Install R package dependencies
3636
uses: r-lib/actions/setup-r-dependencies@v2
@@ -40,6 +40,7 @@ jobs:
4040
any::xml2
4141
any::remotes
4242
any::lpsymphony
43+
any::slam
4344
needs: coverage
4445

4546
- name: Test coverage

DESCRIPTION

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ Imports:
4141
prioritizr (>= 7.0.0),
4242
rlang,
4343
stats,
44-
magrittr
44+
magrittr,
45+
igraph,
46+
Matrix
4547
Suggests:
4648
dplyr,
4749
ggplot2,
@@ -52,7 +54,8 @@ Suggests:
5254
patchwork,
5355
rmarkdown,
5456
stars,
55-
stringr
57+
stringr,
58+
parallelly
5659
VignetteBuilder: knitr
5760
URL: https://github.com/SpatialPlanning/minpatch
5861
BugReports: https://github.com/SpatialPlanning/minpatch/issues

R/data_structures.R

Lines changed: 142 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -159,78 +159,161 @@ initialize_minpatch_data <- function(solution, planning_units, targets, costs,
159159

160160
#' Create boundary matrix from planning units
161161
#'
162-
#' Creates a matrix of shared boundary lengths between adjacent planning units
162+
#' Creates a sparse matrix of shared boundary lengths between adjacent planning units.
163+
#' Returns a Matrix::sparseMatrix for efficient storage and operations.
164+
#' This optimized version supports parallel processing via the parallelly package.
165+
#' When n_cores = 1, runs sequentially with no parallel overhead.
163166
#'
164167
#' @param planning_units sf object with planning unit geometries
168+
#' @param verbose Logical, whether to print progress
169+
#' @param n_cores Integer, number of cores to use. If NULL, uses availableCores(omit=1).
170+
#' Set to 1 for sequential processing.
165171
#'
166-
#' @return Named list where each element contains neighbors and shared boundary lengths
172+
#' @return Matrix::dgCMatrix sparse matrix where [i,j] is the shared boundary length
167173
#' @keywords internal
168-
create_boundary_matrix <- function(planning_units, verbose = TRUE) {
174+
create_boundary_matrix <- function(planning_units, verbose = TRUE, n_cores = NULL) {
169175

170176
n_units <- nrow(planning_units)
171-
boundary_matrix <- vector("list", n_units)
172-
names(boundary_matrix) <- as.character(seq_len(n_units))
173177

174-
# Initialize empty lists for each planning unit
175-
for (i in seq_len(n_units)) {
176-
boundary_matrix[[i]] <- list()
178+
# Determine number of cores
179+
if (is.null(n_cores)) {
180+
if (requireNamespace("parallelly", quietly = TRUE)) {
181+
n_cores <- parallelly::availableCores(omit = 2)
182+
} else {
183+
n_cores <- 1
184+
}
185+
}
186+
# Only use parallel for larger datasets (overhead not worth it for small ones)
187+
if (n_units < 500) {
188+
n_cores <- 1
189+
} else {
190+
n_cores <- min(n_cores, n_units)
177191
}
178192

179-
# Find adjacent planning units and calculate shared boundary lengths
180-
# This is computationally intensive, so we'll use sf::st_touches for adjacency
181-
# and sf::st_intersection for boundary lengths
193+
# Final safety check: ensure n_cores is always between 1 and n_units
194+
n_cores <- max(1, min(n_cores, n_units))
182195

183-
if (verbose) cat("Calculating boundary matrix (this may take a while)...\n")
196+
if (verbose) {
197+
if (n_cores > 1) {
198+
cat("Calculating boundary matrix using", n_cores, "cores...\n")
199+
} else {
200+
cat("Calculating boundary matrix (optimized version)...\n")
201+
}
202+
}
184203

185204
# Check for invalid geometries and repair if needed
186205
if (any(!sf::st_is_valid(planning_units))) {
187206
cat("Warning: Invalid geometries detected, attempting to repair...\n")
188207
planning_units <- sf::st_make_valid(planning_units)
189208
}
190209

191-
# Get adjacency matrix using a more robust method
192-
# sf::st_touches() can be unreliable due to precision issues
193-
# Use st_intersects() with boundaries instead
210+
# Pre-compute all boundaries once (major optimization)
194211
boundaries <- sf::st_boundary(planning_units)
195-
touches <- sf::st_intersects(boundaries, boundaries, sparse = FALSE)
196-
197-
# Remove self-intersections (diagonal)
198-
diag(touches) <- FALSE
199212

213+
# Pre-compute all perimeters once for diagonal
214+
perimeters <- as.numeric(sf::st_length(boundaries))
215+
216+
# Get sparse adjacency list (much more efficient than dense matrix)
217+
touches_sparse <- sf::st_intersects(boundaries, boundaries)
218+
219+
# Split work into chunks - handle edge cases properly
220+
if (n_cores == 1) {
221+
# Single core: all units in one chunk
222+
chunks <- list(seq_len(n_units))
223+
} else {
224+
# Multiple cores: split evenly
225+
# Ensure we don't try to create more chunks than units
226+
actual_cores <- min(n_cores, n_units)
227+
if (actual_cores >= n_units) {
228+
# If cores >= units, each unit gets its own chunk
229+
chunks <- as.list(seq_len(n_units))
230+
} else {
231+
# Normal case: split into chunks
232+
chunks <- split(seq_len(n_units), cut(seq_len(n_units), actual_cores, labels = FALSE))
233+
}
234+
}
200235

201-
# Calculate shared boundaries
202-
for (i in seq_len(n_units)) {
203-
for (j in seq_len(n_units)) {
204-
if (i != j && touches[i, j]) {
205-
# Calculate shared boundary length (suppress sf warnings)
206-
intersection <- suppressWarnings(sf::st_intersection(
207-
sf::st_boundary(planning_units[i, ]),
208-
sf::st_boundary(planning_units[j, ])
209-
))
210-
211-
if (nrow(intersection) > 0) {
212-
shared_length <- sum(as.numeric(sf::st_length(intersection)))
213-
# Use tolerance for very small shared lengths (floating-point precision issues)
214-
if (shared_length > 1e-10) {
215-
boundary_matrix[[i]][[as.character(j)]] <- shared_length
216-
} else if (shared_length > 0) {
217-
# For very small but non-zero lengths, use a minimal positive value
218-
boundary_matrix[[i]][[as.character(j)]] <- 1e-6
236+
# Function to process a chunk of units
237+
process_chunk <- function(unit_indices) {
238+
local_i <- integer()
239+
local_j <- integer()
240+
local_lengths <- numeric()
241+
242+
for (i in unit_indices) {
243+
neighbors <- touches_sparse[[i]]
244+
neighbors <- neighbors[neighbors != i]
245+
246+
if (length(neighbors) > 0) {
247+
for (j in neighbors) {
248+
if (i < j) { # Only process each pair once
249+
intersection <- suppressWarnings(sf::st_intersection(
250+
boundaries[i, ],
251+
boundaries[j, ]
252+
))
253+
254+
if (nrow(intersection) > 0) {
255+
shared_length <- sum(as.numeric(sf::st_length(intersection)))
256+
if (shared_length > 1e-10) {
257+
local_i <- c(local_i, i, j)
258+
local_j <- c(local_j, j, i)
259+
local_lengths <- c(local_lengths, shared_length, shared_length)
260+
} else if (shared_length > 0) {
261+
local_i <- c(local_i, i, j)
262+
local_j <- c(local_j, j, i)
263+
local_lengths <- c(local_lengths, 1e-6, 1e-6)
264+
}
265+
}
219266
}
220267
}
221268
}
222269
}
223270

224-
# Add self-boundary (external edge) - approximate as perimeter
225-
perimeter <- as.numeric(sf::st_length(sf::st_boundary(planning_units[i, ])))
226-
boundary_matrix[[i]][[as.character(i)]] <- perimeter
271+
list(i = local_i, j = local_j, x = local_lengths)
272+
}
227273

228-
if (verbose && i %% 100 == 0) {
229-
cat("Processed", i, "of", n_units, "planning units\n")
230-
}
274+
# Process chunks (parallel if n_cores > 1, sequential if n_cores = 1)
275+
if (n_cores > 1 && requireNamespace("parallelly", quietly = TRUE)) {
276+
# Parallel processing
277+
cl <- parallelly::makeClusterPSOCK(n_cores, autoStop = TRUE, verbose = FALSE)
278+
on.exit(parallel::stopCluster(cl), add = TRUE)
279+
280+
parallel::clusterExport(cl, c("boundaries", "touches_sparse"),
281+
envir = environment())
282+
parallel::clusterEvalQ(cl, library(sf))
283+
284+
if (verbose) cat("Processing chunks in parallel...\n")
285+
results <- parallel::parLapply(cl, chunks, process_chunk)
286+
} else {
287+
# Sequential processing
288+
results <- lapply(chunks, function(chunk) {
289+
result <- process_chunk(chunk)
290+
if (verbose && max(chunk) %% 100 == 0) {
291+
cat("Processed", max(chunk), "of", n_units, "planning units\n")
292+
}
293+
result
294+
})
231295
}
232296

233-
return(boundary_matrix)
297+
# Combine results
298+
if (verbose && n_cores > 1) cat("Combining results...\n")
299+
i_indices <- unlist(lapply(results, function(r) r$i))
300+
j_indices <- unlist(lapply(results, function(r) r$j))
301+
boundary_lengths <- unlist(lapply(results, function(r) r$x))
302+
303+
# Add perimeters on diagonal
304+
i_indices <- c(i_indices, seq_len(n_units))
305+
j_indices <- c(j_indices, seq_len(n_units))
306+
boundary_lengths <- c(boundary_lengths, perimeters)
307+
308+
# Create sparse matrix
309+
Matrix::sparseMatrix(
310+
i = i_indices,
311+
j = j_indices,
312+
x = boundary_lengths,
313+
dims = c(n_units, n_units),
314+
dimnames = list(as.character(seq_len(n_units)),
315+
as.character(seq_len(n_units)))
316+
)
234317
}
235318

236319
#' Create abundance matrix from planning units
@@ -282,7 +365,8 @@ create_abundance_matrix <- function(planning_units, prioritizr_problem) {
282365

283366
#' Create patch radius dictionary
284367
#'
285-
#' For each planning unit, find all units within the specified patch radius
368+
#' For each planning unit, find all units within the specified patch radius.
369+
#' Optimized version computes full distance matrix once instead of n times.
286370
#'
287371
#' @param planning_units sf object with planning unit geometries
288372
#' @param patch_radius radius for patch creation
@@ -299,16 +383,21 @@ create_patch_radius_dict <- function(planning_units, patch_radius, verbose = TRU
299383
centroids <- sf::st_centroid(planning_units %>%
300384
dplyr::select("geometry"))
301385

302-
if (verbose) cat("Creating patch radius dictionary...\n")
386+
if (verbose) cat("Creating patch radius dictionary (optimized)...\n")
303387

304-
for (i in seq_len(n_units)) {
305-
# Find all units within patch_radius of unit i
306-
distances <- sf::st_distance(centroids[i, ], centroids)
307-
# Convert both to numeric to avoid units mismatch
308-
distances_numeric <- as.numeric(distances)
309-
patch_radius_numeric <- as.numeric(patch_radius)
310-
within_radius <- which(distances_numeric <= patch_radius_numeric & seq_len(n_units) != i)
388+
# OPTIMIZATION: Compute full distance matrix ONCE instead of n times
389+
# This changes from O(n²) distance calculations to O(n²/2) calculations
390+
dist_matrix <- sf::st_distance(centroids, centroids)
391+
dist_matrix_numeric <- as.numeric(dist_matrix)
392+
patch_radius_numeric <- as.numeric(patch_radius)
311393

394+
# Create matrix of dimensions n x n
395+
dist_mat <- matrix(dist_matrix_numeric, nrow = n_units, ncol = n_units)
396+
397+
# For each unit, find neighbors within radius
398+
for (i in seq_len(n_units)) {
399+
# Use vectorized comparison on pre-computed distances
400+
within_radius <- which(dist_mat[i, ] <= patch_radius_numeric & seq_len(n_units) != i)
312401
patch_radius_dict[[i]] <- as.character(within_radius)
313402

314403
if (verbose && i %% 100 == 0) {

R/minpatch.R

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ run_minpatch <- function(prioritizr_problem,
9999
solution_column = "solution_1",
100100
verbose = TRUE) {
101101

102+
# Stage 0: Checks and data preparation -----
103+
102104
# Check if prioritizr is available
103105
if (!requireNamespace("prioritizr", quietly = TRUE)) {
104106
stop("prioritizr package is required for this function")
@@ -206,7 +208,9 @@ run_minpatch <- function(prioritizr_problem,
206208
minpatch_data <- calculate_patch_stats(minpatch_data)
207209
initial_patch_stats <- minpatch_data$patch_stats
208210

209-
# Stage 1: Remove small patches (conditional)
211+
212+
213+
# Stage 1: Remove small patches (conditional) -----
210214
if (remove_small_patches) {
211215
if (verbose) cat("Stage 1: Removing small patches...\n")
212216
minpatch_data <- remove_small_patches_from_solution(minpatch_data)
@@ -231,15 +235,13 @@ run_minpatch <- function(prioritizr_problem,
231235
minpatch_data$prioritizr_solution$minpatch <- create_solution_vector(minpatch_data$unit_dict)
232236
}
233237

234-
# Stage 2: Add new patches to meet targets (conditional)
238+
# Stage 2: Add new patches to meet targets (conditional) ----
235239
unmet_targets <- character(0)
236240
if (add_patches) {
237241
if (verbose) cat("Stage 2: Adding new patches...\n")
238242

239-
240243
minpatch_data <- add_new_patches(minpatch_data, verbose)
241244

242-
243245
# Check final unmet targets
244246
unmet_targets <- identify_unmet_targets(minpatch_data)
245247

@@ -253,7 +255,7 @@ run_minpatch <- function(prioritizr_problem,
253255
if (verbose) cat("Stage 2: Skipping addition of new patches...\n")
254256
}
255257

256-
# Stage 3: Simulated whittling (conditional)
258+
# Stage 3: Simulated whittling (conditional) ----
257259
if (whittle_patches) {
258260
if (verbose) cat("Stage 3: Removing unnecessary planning units...\n")
259261
minpatch_data <- simulated_whittling(minpatch_data, verbose)

0 commit comments

Comments
 (0)