Cumulative sum plots of layers for determining target per layer and allowing weights to have differential impact on scenario solution.

Get planning units as hexagons

librarian::shelf(
  BenioffOceanInitiative/bbnj, dplyr, DT,
  exactextractr, fs, ggplot2, glue, here, mapview, 
  prioritizr,
  purrr, raster, rlang, scales, sf, stringr, tidyr, units)
source(here("libs/plots.R"))
select <- dplyr::select
mapviewOptions(fgb = FALSE)
mapviewOptions(
  vector.palette = mapviewPalette("mapviewSpectralColors"))
# devtools::load_all(here("~/Github/bbest/prioritizr"))
# devtools::install_local(here("~/Github/bbest/prioritizr"))

hex_geo <- "data/abnj_hex_res2.geojson"
# hex_geo <- "data/abnj_hex_res4.geojson"

pu_rds  <- glue("{path_ext_remove(hex_geo)}_area.rds")
if (!file.exists(pu_rds)){
  
  pu <- read_sf(hex_geo)
  
  # calculate area
  pu <- pu %>%
    # st_make_valid() %>%      # dateline issues
    filter(st_is_valid(.)) %>% # 8 hexagons dropped 
    mutate(
      area_km2 = st_area(geometry) %>% 
        set_units(km^2) %>% 
        drop_units(),
      area_pct = area_km2 / sum(area_km2))
  # sum(pu$area_pct) # check adds to 12

  saveRDS(pu, file = pu_rds)
} else {
  pu <- readRDS(pu_rds)
  # paste(names(pu), collapse = ", ")
  # hexid, on_dtln, lon, lat, FID, geometry, area_km2, area_pct
}

# mapview(pu, zcol="area_pct")
# plot(pu)

Get layers

lyrs7_grd <- here("data/lyrs7_mol.grd")

if (!file.exists(lyrs7_grd)){
  tifs1 <- list.files(here("../bbnj-scripts/map_layers"), "tif$", full.names = T)
  tifs2 <- list.files(here("../bbnj-scripts/presentation_maps"), "tif$", full.names = T)[-1] # drop redundant bio_vgpm.tif
  lyrs <- stack(c(tifs1, tifs2))
  sort(names(lyrs))
  
  s_nspp <- subset(lyrs, sort(names(lyrs))[26:48])
  r_nspp <- calc(s_nspp, sum, na.rm = T)
  
  s_mounts <- subset(lyrs, sort(names(lyrs))[49:51])
  r_mounts <- calc(s_mounts, sum, na.rm = T)
  
  r_vents <- raster(lyrs, layer = "phys_vents")
  r_scapes <- raster(lyrs, layer = "scapes_hetero")
  
  r_vgpm <- raster(lyrs, layer = "bio_vgpm")
  r_rls  <- raster(lyrs, layer = "rls_all")
  r_fishing <- raster(lyrs, layer = "fishing_KWH")
  
  # TODO: consider log/log10/normalize transforms of nspp, fish effort 
  #plot(r_vgpm)
  s_lyrs <- stack(
    r_fishing,
    r_vgpm,
    r_nspp,
    r_rls,
    r_vents,
    r_mounts,
    r_scapes)
  names(s_lyrs) <- c(
    "fishing",
    "vgpm",
    "nspp",
    "rls",
    "vents",
    "mounts",
    "scapes")
  terra::writeRaster(s_lyrs, lyrs7_grd)
}
s_lyrs7 <- terra::rast(lyrs7_grd)
  
# plot(s_lyrs7)

Extract layers to planning units

# extract feature values
f_0 <- exact_extract(
  s_lyrs7, pu, fun = "mean", append_cols = "hexid", progress = F) %>% 
  tibble() %>% 
  rename_with(~str_replace(., "mean.", ""), everything())

# extra calculations
f <- f_0 %>% 
  mutate(
    fishing = 1/fishing,            # TODO: log(1/fishing)? 
    spp     = rls * nspp,
    benthic = vents + mounts) %>%   # TODO: rescale(vents) + rescale(mounts)? 
  mutate(
    across(where(is.numeric), ~replace_na(., 0)))

pu_f <- pu %>%               # 129,906 × 8
  left_join(f, by = "hexid") # 129,906 × 10

get_pu_v_ks <- function(pu_f, v, ks){
  var <- as_label(enquo(v))

  d_v <- pu_f %>% 
    st_drop_geometry() %>% 
    arrange(desc({{v}})) %>% 
    mutate(
      var = var,
      km2 = cumsum(area_km2),
      val = cumsum({{v}})) %>% 
    select(var, km2, val)
  
  val_sum <- d_v %>% pull(val) %>% last()
    
  d_k <- lapply(
    ks,
    function(k){
      i <- max(which(d_v$km2 < d_v$km2[nrow(d_v)]*k))
      tibble(
        var = var, 
        k   = k,
        km2 = d_v$km2[i],
        val = d_v$val[i],
        pct_val = val/val_sum) }) %>% 
    bind_rows()

  p <- d_v %>% 
    ggplot(aes(x = km2, y = val)) + 
    geom_area(fill = "gray", color=NA)
  
  add_k_paths <- function(k, d_k){
    
    # https://colorbrewer2.org/#type=sequential&scheme=Blues&n=3
    col <- c(
      `0.3` = "#9ecae1",
      `0.5` = "#3182bd")[as.character(k)]
    
    x <- d_k %>% filter(k == !!k) %>% pull(km2)
    y <- d_k %>% filter(k == !!k) %>% pull(val)
    y_pct <- d_k %>% filter(k == !!k) %>% pull(pct_val)
    z <- tribble(
      ~x, ~y,
      x,  0,
      x,  y,
      0,  y)
    
    list(
      geom_path(data = z, aes(x,y), color=col),
      annotate(
        "text", x = x, y = y,
        label = glue("{k*100}%: {comma(x, 0.1)} ({percent(y_pct, 0.1)})")))
  }

  p <- p +
    lapply(
      ks,
      add_k_paths,
      d_k) +
    labs(
      x = "area_km2",
      y = var) +
    theme_minimal()
    
  list(
    k=d_k, p=p)
}
# kp1 <- get_pu_v_ks(pu_f, fishing, c(0.3, 0.5))
# names(k_nspp)
# k_nspp$k
# kp1$p

k_p <- sapply(
  c("fishing", "benthic", "scapes", "vgpm", "spp"),
  function(var){
    get_pu_v_ks(pu_f, !!sym(var), c(0.3, 0.5))
  }, 
  simplify = F)

p <- map(k_p, "p")

k <- map(k_p, "k") %>% 
  bind_rows() %>% 
  select(-km2) %>% 
  rename(pct_area = k)

datatable(k) %>% 
  formatPercentage(c("pct_area", "pct_val"), 1)
# walk(p, print)
# knitr::opts_chunk$set(eval = F)
s_name <- "30% area; targets 1; equal weights"

Solution: 30% area; targets 1; equal weights

# ocean area
a <- 0.3

# feature weights
f_w <- c(
  fishing = 1,
  vgpm    = 1,
  spp     = 1,
  benthic = 1,
  scapes  = 1)

# problem ----
p <- pu_f %>% 
  problem(features = names(f_w), cost_column = "area_pct") %>%
  add_min_shortfall_objective(a) %>% 
  add_relative_targets(1) %>%
  add_feature_weights(f_w)
  
# solve ----
s <- solve(p, force = T)
## Gurobi Optimizer version 9.5.1 build v9.5.1rc2 (mac64[x86])
## Thread count: 6 physical cores, 12 logical processors, using up to 1 threads
## Optimize a model with 6 rows, 3125 columns and 16654 nonzeros
## Model fingerprint: 0xccfe803c
## Variable types: 5 continuous, 3120 integer (3120 binary)
## Coefficient statistics:
##   Matrix range     [3e-10, 1e+05]
##   Objective range  [4e-08, 9e-01]
##   Bounds range     [1e+00, 1e+00]
##   RHS range        [3e-01, 3e+07]
## Warning: Model contains large matrix coefficient range
##          Consider reformulating model or setting NumericFocus parameter
##          to avoid numerical issues.
## Found heuristic solution: objective 5.0000000
## Found heuristic solution: objective 5.0000000
## Presolve removed 4 rows and 47 columns
## Presolve time: 0.01s
## Presolved: 2 rows, 3078 columns, 6072 nonzeros
## Variable types: 1 continuous, 3077 integer (3077 binary)
## Found heuristic solution: objective 2.9842121
## Root relaxation presolved: 2 rows, 3078 columns, 6072 nonzeros
## 
## Extra simplex iterations after uncrush: 1
## 
## Root relaxation: objective 1.957470e+00, 7 iterations, 0.01 seconds (0.00 work units)
## 
##     Nodes    |    Current Node    |     Objective Bounds      |     Work
##  Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
## 
##      0     0    1.95747    0    1    2.98421    1.95747  34.4%     -    0s
## H    0     0                       1.9582720    1.95747  0.04%     -    0s
## 
## Cleanup yields a better solution
## 
## Explored 1 nodes (7 simplex iterations) in 0.03 seconds (0.02 work units)
## Thread count was 1 (of 12 available processors)
## 
## Solution count 4: 1.95825 1.95827 2.98421 5 
## 
## Optimal solution found (tolerance 1.00e-01)
## Best objective 1.958247199146e+00, best bound 1.957470317916e+00, gap 0.0397%
mapview(s %>% filter(solution_1==1), layer.name = s_name)
gmap(s, "solution_1", s_name)

s %>% filter(solution_1==1) %>% pull(area_pct) %>% sum()
## [1] 0.2999911
# calculate feature representation statistics based on the prioritization
s_cov <- eval_target_coverage_summary(p, s[, "solution_1"])
datatable(s_cov)
s_name <- "30% area; targets 1; spp*5 weights"

Solution: 30% area; targets 1; spp*5 weights

# ocean area
a <- 0.3

# feature weights
f_w <- c(
  fishing = 1,
  vgpm    = 1,
  spp     = 5,
  benthic = 1,
  scapes  = 1)

# problem ----
p <- pu_f %>% 
  problem(features = names(f_w), cost_column = "area_pct") %>%
  add_min_shortfall_objective(a) %>% 
  add_relative_targets(1) %>%
  add_feature_weights(f_w)
  
# solve ----
s <- solve(p, force = T)
## Gurobi Optimizer version 9.5.1 build v9.5.1rc2 (mac64[x86])
## Thread count: 6 physical cores, 12 logical processors, using up to 1 threads
## Optimize a model with 6 rows, 3125 columns and 16654 nonzeros
## Model fingerprint: 0x6e55e23f
## Variable types: 5 continuous, 3120 integer (3120 binary)
## Coefficient statistics:
##   Matrix range     [3e-10, 1e+05]
##   Objective range  [2e-07, 9e-01]
##   Bounds range     [1e+00, 1e+00]
##   RHS range        [3e-01, 3e+07]
## Warning: Model contains large matrix coefficient range
##          Consider reformulating model or setting NumericFocus parameter
##          to avoid numerical issues.
## Found heuristic solution: objective 9.0000000
## Found heuristic solution: objective 9.0000000
## Presolve removed 4 rows and 47 columns
## Presolve time: 0.01s
## Presolved: 2 rows, 3078 columns, 6072 nonzeros
## Variable types: 1 continuous, 3077 integer (3077 binary)
## Found heuristic solution: objective 6.9842121
## Root relaxation presolved: 2 rows, 3078 columns, 6072 nonzeros
## 
## Extra simplex iterations after uncrush: 1
## 
## Root relaxation: objective 3.599048e+00, 6 iterations, 0.01 seconds (0.00 work units)
## 
##     Nodes    |    Current Node    |     Objective Bounds      |     Work
##  Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
## 
##      0     0    3.59905    0    1    6.98421    3.59905  48.5%     -    0s
## H    0     0                       3.5992778    3.59905  0.01%     -    0s
## 
## Explored 1 nodes (6 simplex iterations) in 0.04 seconds (0.02 work units)
## Thread count was 1 (of 12 available processors)
## 
## Solution count 3: 3.59928 6.98421 9 
## 
## Optimal solution found (tolerance 1.00e-01)
## Best objective 3.599274687434e+00, best bound 3.599047515147e+00, gap 0.0063%
#mapview(s %>% filter(solution_1==1), layer.name = "solution")

gmap(s, "solution_1", s_name)

# s %>% filter(solution_1==1) %>% pull(area_pct) %>% sum()

# calculate  feature representation statistics based on the prioritization
s_cov <- eval_target_coverage_summary(p, s[, "solution_1"])
datatable(s_cov)
s_name <- "30% area; targets 99% of max; spp*5 weights"

Solution: 30% area; targets 99% of max; spp*5 weights

# ocean area
a <- 0.3

# feature targets
d_t <- k %>% 
  filter(pct_area == a) %>% 
  mutate(
    target = 0.99 * pct_val)
f_t <- setNames(d_t$target, d_t$var)

# feature weights
f_w <- c(
  fishing = 1,
  vgpm    = 1,
  spp     = 5,
  benthic = 1,
  scapes  = 1)

# problem ----
p <- pu_f %>% 
  problem(features = names(f_w), cost_column = "area_pct") %>%
  add_min_shortfall_objective(a) %>% 
  add_relative_targets(f_t) %>%
  add_feature_weights(f_w)
  
# solve ----
s <- solve(p, force = T)
## Gurobi Optimizer version 9.5.1 build v9.5.1rc2 (mac64[x86])
## Thread count: 6 physical cores, 12 logical processors, using up to 1 threads
## Optimize a model with 6 rows, 3125 columns and 16654 nonzeros
## Model fingerprint: 0x24be0774
## Variable types: 5 continuous, 3120 integer (3120 binary)
## Coefficient statistics:
##   Matrix range     [3e-10, 1e+05]
##   Objective range  [4e-07, 9e-01]
##   Bounds range     [1e+00, 1e+00]
##   RHS range        [3e-01, 1e+07]
## Warning: Model contains large matrix coefficient range
##          Consider reformulating model or setting NumericFocus parameter
##          to avoid numerical issues.
## Found heuristic solution: objective 9.0000001
## Found heuristic solution: objective 9.0000000
## Presolve removed 0 rows and 43 columns
## Presolve time: 0.02s
## Presolved: 6 rows, 3082 columns, 16606 nonzeros
## Found heuristic solution: objective 8.9962142
## Variable types: 5 continuous, 3077 integer (3077 binary)
## Root relaxation presolve removed 0 rows and 3 columns
## Root relaxation presolved: 6 rows, 3079 columns, 16599 nonzeros
## 
## 
## Root relaxation: objective 4.924788e-01, 63 iterations, 0.02 seconds (0.01 work units)
## 
##     Nodes    |    Current Node    |     Objective Bounds      |     Work
##  Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
## 
##      0     0    0.49248    0    2    8.99621    0.49248  94.5%     -    0s
## H    0     0                       0.4962997    0.49248  0.77%     -    0s
## 
## Explored 1 nodes (63 simplex iterations) in 0.04 seconds (0.02 work units)
## Thread count was 1 (of 12 available processors)
## 
## Solution count 3: 0.4963 8.99621 9 
## 
## Optimal solution found (tolerance 1.00e-01)
## Best objective 4.962995984681e-01, best bound 4.924787530511e-01, gap 0.7699%
# mapview(s %>% filter(solution_1==1), layer.name = s_name)

gmap(s, "solution_1", s_name)

# s %>% filter(solution_1==1) %>% pull(area_pct) %>% sum()

# calculate feature representation statistics based on the prioritization
s_cov <- eval_target_coverage_summary(p, s[, "solution_1"])
datatable(s_cov)
s_name <- "30% area; targets 90% of max; spp*5 weights"

Solution: 30% area; targets 90% of max; spp*5 weights

# ocean area
a <- 0.3

# feature targets
d_t <- k %>% 
  filter(pct_area == a) %>% 
  mutate(
    target = 0.90 * pct_val)
f_t <- setNames(d_t$target, d_t$var)

# feature weights
f_w <- c(
  fishing = 1,
  vgpm    = 1,
  spp     = 5,
  benthic = 1,
  scapes  = 1)

# problem ----
p <- pu_f %>% 
  problem(features = names(f_w), cost_column = "area_pct") %>%
  add_min_shortfall_objective(a) %>% 
  add_relative_targets(f_t) %>%
  add_feature_weights(f_w)
  
# solve ----
s <- solve(p, force = T)
## Gurobi Optimizer version 9.5.1 build v9.5.1rc2 (mac64[x86])
## Thread count: 6 physical cores, 12 logical processors, using up to 1 threads
## Optimize a model with 6 rows, 3125 columns and 16654 nonzeros
## Model fingerprint: 0xa9afd028
## Variable types: 5 continuous, 3120 integer (3120 binary)
## Coefficient statistics:
##   Matrix range     [3e-10, 1e+05]
##   Objective range  [4e-07, 1e+00]
##   Bounds range     [1e+00, 1e+00]
##   RHS range        [3e-01, 1e+07]
## Warning: Model contains large matrix coefficient range
##          Consider reformulating model or setting NumericFocus parameter
##          to avoid numerical issues.
## Found heuristic solution: objective 9.0000000
## Found heuristic solution: objective 9.0000000
## Presolve removed 0 rows and 43 columns
## Presolve time: 0.02s
## Presolved: 6 rows, 3082 columns, 16606 nonzeros
## Found heuristic solution: objective 8.9958356
## Variable types: 5 continuous, 3077 integer (3077 binary)
## Root relaxation presolve removed 0 rows and 3 columns
## Root relaxation presolved: 6 rows, 3079 columns, 16599 nonzeros
## 
## 
## Root relaxation: objective 2.825901e-01, 91 iterations, 0.02 seconds (0.01 work units)
## 
##     Nodes    |    Current Node    |     Objective Bounds      |     Work
##  Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
## 
##      0     0    0.28259    0    3    8.99584    0.28259  96.9%     -    0s
## H    0     0                       0.2839301    0.28259  0.47%     -    0s
## 
## Explored 1 nodes (91 simplex iterations) in 0.04 seconds (0.02 work units)
## Thread count was 1 (of 12 available processors)
## 
## Solution count 3: 0.28393 8.99584 9 
## 
## Optimal solution found (tolerance 1.00e-01)
## Best objective 2.839301246790e-01, best bound 2.825900788719e-01, gap 0.4720%
# mapview(s %>% filter(solution_1==1), layer.name = s_name)

gmap(s, "solution_1", s_name)

s %>% filter(solution_1==1) %>% pull(area_pct) %>% sum()
## [1] 0.29981
# calculate feature representation statistics based on the prioritization
s_cov <- eval_target_coverage_summary(p, s[, "solution_1"])
datatable(s_cov)