@@ -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 ) {
0 commit comments