Skip to content

Commit f1cb03e

Browse files
authored
Merge pull request #5 from SpatialPlanning/devel
Add Locked In/Out
2 parents b2cafe8 + a2b384f commit f1cb03e

File tree

14 files changed

+978
-20
lines changed

14 files changed

+978
-20
lines changed

R/data_structures.R

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#' Validate MinPatch inputs
22
#'
3-
#' Internal function to validate all inputs to the MinPatch algorithm
3+
#' Internal function to validate all inputs to the MinPatch algorithm,
4+
#' including locked-in and locked-out constraints
45
#'
56
#' @param solution Binary solution vector
67
#' @param planning_units sf object with planning units
@@ -9,11 +10,17 @@
910
#' @param min_patch_size minimum patch size
1011
#' @param patch_radius patch radius for adding patches
1112
#' @param boundary_penalty Boundary penalty value
13+
#' @param locked_in_indices Optional indices of locked-in planning units
14+
#' @param locked_out_indices Optional indices of locked-out planning units
15+
#' @param area_dict Optional area dictionary for locked-in patch size validation
16+
#' @param verbose Logical, whether to print warnings
1217
#'
1318
#' @return NULL (throws errors if validation fails)
1419
#' @keywords internal
1520
validate_inputs <- function(solution, planning_units, targets, costs,
16-
min_patch_size, patch_radius, boundary_penalty) {
21+
min_patch_size, patch_radius, boundary_penalty,
22+
locked_in_indices = NULL, locked_out_indices = NULL,
23+
area_dict = NULL, verbose = TRUE) {
1724

1825
# Check solution
1926
if (!is.numeric(solution) || !all(solution %in% c(0, 1))) {
@@ -69,11 +76,39 @@ validate_inputs <- function(solution, planning_units, targets, costs,
6976
stop("costs must be non-negative")
7077
}
7178
}
79+
80+
# Validate locked-in and locked-out constraints
81+
if (!is.null(locked_in_indices) && !is.null(locked_out_indices)) {
82+
# Check for conflicts between locked-in and locked-out
83+
conflicts <- intersect(locked_in_indices, locked_out_indices)
84+
if (length(conflicts) > 0) {
85+
stop(paste("Conflict detected: Planning units", paste(conflicts, collapse = ", "),
86+
"are both locked-in and locked-out. This is not allowed."))
87+
}
88+
}
89+
90+
# Warn if locked-in units form patches smaller than min_patch_size
91+
if (!is.null(locked_in_indices) && !is.null(area_dict) && length(locked_in_indices) > 0) {
92+
locked_in_area <- sum(area_dict[as.character(locked_in_indices)])
93+
if (as.numeric(locked_in_area) < as.numeric(min_patch_size_numeric)) {
94+
if (verbose) {
95+
warning(paste0("Locked-in planning units have total area (",
96+
round(as.numeric(locked_in_area), 4),
97+
") smaller than min_patch_size (",
98+
min_patch_size_numeric,
99+
"). These units will be preserved regardless of patch size constraints."))
100+
}
101+
}
102+
}
72103
}
73104

74105
#' Initialize MinPatch data structures
75106
#'
76-
#' Creates the internal data structures needed for MinPatch processing
107+
#' Creates the internal data structures needed for MinPatch processing.
108+
#' This function extracts locked-in and locked-out constraints from the
109+
#' prioritizr problem and applies them as status codes:
110+
#' - Status 2 (conserved) for locked-in units
111+
#' - Status 3 (excluded) for locked-out units
77112
#'
78113
#' @param solution Binary solution vector
79114
#' @param planning_units sf object with planning units
@@ -84,6 +119,7 @@ validate_inputs <- function(solution, planning_units, targets, costs,
84119
#' @param boundary_penalty Boundary penalty value
85120
#' @param prioritizr_problem A prioritizr problem object
86121
#' @param prioritizr_solution A solved prioritizr solution object
122+
#' @param verbose Logical, whether to print progress
87123
#'
88124
#' @return List containing all necessary data structures
89125
#' @keywords internal
@@ -98,7 +134,7 @@ initialize_minpatch_data <- function(solution, planning_units, targets, costs,
98134
costs <- rep(1, n_units) # Default unit costs
99135
}
100136

101-
# Status codes: 0 = available, 1 = selected, 2 = conserved, 3 = excluded
137+
# Status codes: 0 = available, 1 = selected, 2 = conserved (locked-in), 3 = excluded (locked-out)
102138
# Convert solution to status (1 = selected, 0 = available)
103139
unit_dict <- vector("list", n_units)
104140
names(unit_dict) <- as.character(seq_len(n_units))
@@ -110,14 +146,47 @@ initialize_minpatch_data <- function(solution, planning_units, targets, costs,
110146
)
111147
}
112148

113-
# Calculate planning unit areas
149+
# Extract locked-in and locked-out constraints from prioritizr problem
150+
locked_in_indices <- extract_locked_in_constraints(prioritizr_problem, verbose)
151+
locked_out_indices <- extract_locked_out_constraints(prioritizr_problem, verbose)
152+
153+
# Apply locked-in constraints (status = 2)
154+
if (length(locked_in_indices) > 0) {
155+
for (idx in locked_in_indices) {
156+
if (idx <= n_units) {
157+
unit_dict[[as.character(idx)]]$status <- 2L
158+
}
159+
}
160+
if (verbose) {
161+
cat("Applied", length(locked_in_indices), "locked-in constraints\n")
162+
}
163+
}
164+
165+
# Apply locked-out constraints (status = 3)
166+
if (length(locked_out_indices) > 0) {
167+
for (idx in locked_out_indices) {
168+
if (idx <= n_units) {
169+
unit_dict[[as.character(idx)]]$status <- 3L
170+
}
171+
}
172+
if (verbose) {
173+
cat("Applied", length(locked_out_indices), "locked-out constraints\n")
174+
}
175+
}
176+
177+
# Calculate planning unit areas (needed for validation)
114178
area_dict <- as.numeric(sf::st_area(planning_units))
115179
names(area_dict) <- as.character(seq_len(n_units))
116180

117181
# Create cost dictionary
118182
cost_dict <- costs
119183
names(cost_dict) <- as.character(seq_len(n_units))
120184

185+
# Validate locked constraints after applying them
186+
validate_inputs(solution, planning_units, targets, costs,
187+
min_patch_size, patch_radius, boundary_penalty,
188+
locked_in_indices, locked_out_indices, area_dict, verbose)
189+
121190
# Create boundary matrix (adjacency with shared boundary lengths)
122191
boundary_matrix <- create_boundary_matrix(planning_units, verbose)
123192

@@ -153,10 +222,62 @@ initialize_minpatch_data <- function(solution, planning_units, targets, costs,
153222
patch_radius = patch_radius,
154223
boundary_penalty = boundary_penalty,
155224
prioritizr_problem = prioritizr_problem,
156-
prioritizr_solution = prioritizr_solution
225+
prioritizr_solution = prioritizr_solution,
226+
locked_in_indices = locked_in_indices,
227+
locked_out_indices = locked_out_indices
157228
))
158229
}
159230

231+
#' Extract locked-in constraint indices from prioritizr problem
232+
#'
233+
#' @param prioritizr_problem A prioritizr problem object
234+
#' @param verbose Logical, whether to print messages
235+
#'
236+
#' @return Integer vector of locked-in planning unit indices
237+
#' @keywords internal
238+
extract_locked_in_constraints <- function(prioritizr_problem, verbose = TRUE) {
239+
locked_in <- integer(0)
240+
241+
if (!is.null(prioritizr_problem$constraints)) {
242+
for (constraint in prioritizr_problem$constraints) {
243+
# Check if this is a locked-in constraint
244+
if (inherits(constraint, "LockedInConstraint")) {
245+
# Extract indices using the constraint's data
246+
if (!is.null(constraint$data) && "pu" %in% names(constraint$data)) {
247+
locked_in <- unique(c(locked_in, constraint$data$pu))
248+
}
249+
}
250+
}
251+
}
252+
253+
return(sort(unique(locked_in)))
254+
}
255+
256+
#' Extract locked-out constraint indices from prioritizr problem
257+
#'
258+
#' @param prioritizr_problem A prioritizr problem object
259+
#' @param verbose Logical, whether to print messages
260+
#'
261+
#' @return Integer vector of locked-out planning unit indices
262+
#' @keywords internal
263+
extract_locked_out_constraints <- function(prioritizr_problem, verbose = TRUE) {
264+
locked_out <- integer(0)
265+
266+
if (!is.null(prioritizr_problem$constraints)) {
267+
for (constraint in prioritizr_problem$constraints) {
268+
# Check if this is a locked-out constraint
269+
if (inherits(constraint, "LockedOutConstraint")) {
270+
# Extract indices using the constraint's data
271+
if (!is.null(constraint$data) && "pu" %in% names(constraint$data)) {
272+
locked_out <- unique(c(locked_out, constraint$data$pu))
273+
}
274+
}
275+
}
276+
}
277+
278+
return(sort(unique(locked_out)))
279+
}
280+
160281
#' Create boundary matrix from planning units
161282
#'
162283
#' Creates a sparse matrix of shared boundary lengths between adjacent planning units.

R/minpatch.R

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@
3232
#' \item Whittle patches: Removes unnecessary planning units
3333
#' }
3434
#'
35+
#' **Locked Constraints**: MinPatch automatically respects locked-in and locked-out
36+
#' constraints from prioritizr problems (added via \code{add_locked_in_constraints()}
37+
#' and \code{add_locked_out_constraints()}):
38+
#' \itemize{
39+
#' \item **Locked-in units**: Will never be removed, regardless of patch size or
40+
#' whittling. They are treated as "conserved" areas that must be retained.
41+
#' \item **Locked-out units**: Will never be selected, even when adding new patches
42+
#' to meet conservation targets. They are completely excluded from consideration.
43+
#' }
44+
#' If locked-in units form patches smaller than \code{min_patch_size}, a warning
45+
#' will be issued, but these units will still be preserved.
46+
#'
3547
#' **Important**: If you set \code{remove_small_patches = TRUE} but
3648
#' \code{add_patches = FALSE}, the algorithm may remove patches without
3749
#' compensating, potentially violating conservation targets. In such cases,

README.Rmd

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,26 @@ pak::pak("SpatialPlanning/minpatch")
6363

6464
- **Full MinPatch Algorithm**: Complete implementation of all three stages
6565
- **prioritizr Integration**: Seamless workflow with prioritizr solutions
66+
- **Locked Constraints Support**: Automatically respects locked-in and locked-out constraints from prioritizr
6667
- **Flexible Parameters**: Control minimum patch sizes, patch radius, and boundary penalties
6768
- **Comprehensive Reporting**: Detailed statistics and comparisons
6869
- **Visualization Support**: Plot results with ggplot2 (optional)
6970

7071
## Algorithm Details
7172

73+
### Locked Constraints
74+
75+
MinPatch automatically respects locked-in and locked-out constraints from prioritizr problems:
76+
77+
- **Locked-in constraints** (from `add_locked_in_constraints()`): Planning units that are locked-in will never be removed, regardless of patch size or during the whittling stage. These units are treated as "conserved" areas that must be retained in the final solution.
78+
79+
- **Locked-out constraints** (from `add_locked_out_constraints()`): Planning units that are locked-out will never be selected, even when adding new patches to meet conservation targets. These units are completely excluded from consideration.
80+
81+
If locked-in units form patches smaller than `min_patch_size`, a warning will be issued, but these units will still be preserved in the solution.
82+
7283
### Stage 1: Remove Small Patches
7384

74-
Identifies connected components (patches) in the solution and removes those smaller than the minimum size threshold. Only removes patches that weren't originally designated as conserved areas.
85+
Identifies connected components (patches) in the solution and removes those smaller than the minimum size threshold. Locked-in planning units are never removed, even if they form small patches.
7586

7687
### Stage 2: Add New Patches (BestPatch Algorithm)
7788

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,38 @@ pak::pak("SpatialPlanning/minpatch")
7272
stages
7373
- **prioritizr Integration**: Seamless workflow with prioritizr
7474
solutions
75+
- **Locked Constraints Support**: Automatically respects locked-in and
76+
locked-out constraints from prioritizr
7577
- **Flexible Parameters**: Control minimum patch sizes, patch radius,
7678
and boundary penalties
7779
- **Comprehensive Reporting**: Detailed statistics and comparisons
7880
- **Visualization Support**: Plot results with ggplot2 (optional)
7981

8082
## Algorithm Details
8183

84+
### Locked Constraints
85+
86+
MinPatch automatically respects locked-in and locked-out constraints
87+
from prioritizr problems:
88+
89+
- **Locked-in constraints** (from `add_locked_in_constraints()`):
90+
Planning units that are locked-in will never be removed, regardless of
91+
patch size or during the whittling stage. These units are treated as
92+
“conserved” areas that must be retained in the final solution.
93+
94+
- **Locked-out constraints** (from `add_locked_out_constraints()`):
95+
Planning units that are locked-out will never be selected, even when
96+
adding new patches to meet conservation targets. These units are
97+
completely excluded from consideration.
98+
99+
If locked-in units form patches smaller than `min_patch_size`, a warning
100+
will be issued, but these units will still be preserved in the solution.
101+
82102
### Stage 1: Remove Small Patches
83103

84104
Identifies connected components (patches) in the solution and removes
85-
those smaller than the minimum size threshold. Only removes patches that
86-
weren’t originally designated as conserved areas.
105+
those smaller than the minimum size threshold. Locked-in planning units
106+
are never removed, even if they form small patches.
87107

88108
### Stage 2: Add New Patches (BestPatch Algorithm)
89109

0 commit comments

Comments
 (0)