ecological-agent-skills 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/AGENT_CONTEXT.md +191 -0
  2. package/CATALOG.md +329 -0
  3. package/LICENSE +692 -0
  4. package/README.md +347 -0
  5. package/bin/install.mjs +168 -0
  6. package/docs/comparison-with-alternatives.md +38 -0
  7. package/docs/global-examples-index.md +103 -0
  8. package/docs/repository-statistics.md +101 -0
  9. package/docs/theoretical-foundations.md +188 -0
  10. package/environment.yaml +106 -0
  11. package/examples/community/arctic_tundra_vegetation_example.md +247 -0
  12. package/examples/community/bird_landuse_example.md +63 -0
  13. package/examples/community/phytoplankton_reservoir_example.md +60 -0
  14. package/examples/community/reef_fish_indopacific_example.md +221 -0
  15. package/examples/impact/baci_road_example.md +57 -0
  16. package/examples/impact/ecosystem_services_atlantic_forest.md +83 -0
  17. package/examples/impact/forest_loss_borneo_timeseries_example.md +225 -0
  18. package/examples/occupancy/puma_camera_example.md +61 -0
  19. package/examples/occupancy/snow_leopard_himalayas_example.md +204 -0
  20. package/examples/reproducible/whittaker_biome_sdm_example.md +406 -0
  21. package/examples/sdm/anteater_cerrado_example.md +69 -0
  22. package/examples/sdm/jaguar_amazon_example.md +80 -0
  23. package/examples/sdm/koala_climate_change_example.md +170 -0
  24. package/examples/sdm/wolf_recolonization_europe_example.md +193 -0
  25. package/package.json +43 -0
  26. package/renv.lock +194 -0
  27. package/skills/SKILL_INDEX.json +1020 -0
  28. package/skills/acoustic-monitoring/SKILL.md +163 -0
  29. package/skills/acoustic-monitoring/examples/example-prompts.md +100 -0
  30. package/skills/acoustic-monitoring/examples/temperate_forest_birds_example.md +285 -0
  31. package/skills/acoustic-monitoring/resources/acoustic-indices-reference.md +93 -0
  32. package/skills/acoustic-monitoring/resources/soundscape-ecology-guide.md +90 -0
  33. package/skills/acoustic-monitoring/resources/species-id-tools-comparison.md +89 -0
  34. package/skills/acoustic-monitoring/scripts/batch_species_detection.py +360 -0
  35. package/skills/acoustic-monitoring/scripts/compute_acoustic_indices.R +235 -0
  36. package/skills/acoustic-monitoring/scripts/compute_acoustic_indices.py +374 -0
  37. package/skills/biostatistics-workbench/SKILL.md +140 -0
  38. package/skills/biostatistics-workbench/examples/example-prompts.md +39 -0
  39. package/skills/biostatistics-workbench/resources/effect-size-reference.md +81 -0
  40. package/skills/biostatistics-workbench/resources/glm-family-link-reference.md +47 -0
  41. package/skills/biostatistics-workbench/resources/test-selection-guide.md +93 -0
  42. package/skills/biostatistics-workbench/scripts/glm_pipeline.R +78 -0
  43. package/skills/biostatistics-workbench/scripts/glm_pipeline.py +210 -0
  44. package/skills/camera-trap-processing/SKILL.md +159 -0
  45. package/skills/camera-trap-processing/examples/example-prompts.md +103 -0
  46. package/skills/camera-trap-processing/examples/leopard_serengeti_example.md +231 -0
  47. package/skills/camera-trap-processing/resources/activity-patterns-reference.md +113 -0
  48. package/skills/camera-trap-processing/resources/camtrapR-workflow-guide.md +130 -0
  49. package/skills/camera-trap-processing/resources/detection-event-definition-guide.md +89 -0
  50. package/skills/camera-trap-processing/scripts/estimate_activity.R +169 -0
  51. package/skills/camera-trap-processing/scripts/process_camtrap_data.R +179 -0
  52. package/skills/camera-trap-processing/scripts/process_camtrap_data.py +192 -0
  53. package/skills/community-ecology-ordination/SKILL.md +133 -0
  54. package/skills/community-ecology-ordination/examples/example-prompts.md +35 -0
  55. package/skills/community-ecology-ordination/resources/dissimilarity-metric-guide.md +53 -0
  56. package/skills/community-ecology-ordination/resources/nmds-interpretation-guide.md +104 -0
  57. package/skills/community-ecology-ordination/scripts/__pycache__/community_analysis.cpython-311.pyc +0 -0
  58. package/skills/community-ecology-ordination/scripts/community_analysis.R +143 -0
  59. package/skills/community-ecology-ordination/scripts/community_analysis.py +231 -0
  60. package/skills/ecological-data-foundation/SKILL.md +129 -0
  61. package/skills/ecological-data-foundation/examples/example-prompts.md +40 -0
  62. package/skills/ecological-data-foundation/resources/coordinate-cleaning-flags.md +66 -0
  63. package/skills/ecological-data-foundation/resources/darwin-core-glossary.md +91 -0
  64. package/skills/ecological-data-foundation/resources/data-citation-guide.md +265 -0
  65. package/skills/ecological-data-foundation/resources/gbif-data-citation-guide.md +193 -0
  66. package/skills/ecological-data-foundation/resources/qa-checklist.md +83 -0
  67. package/skills/ecological-data-foundation/scripts/__pycache__/clean_occurrences.cpython-311.pyc +0 -0
  68. package/skills/ecological-data-foundation/scripts/__pycache__/download_from_ebird.cpython-311.pyc +0 -0
  69. package/skills/ecological-data-foundation/scripts/__pycache__/download_from_inat.cpython-311.pyc +0 -0
  70. package/skills/ecological-data-foundation/scripts/__pycache__/download_from_iucn.cpython-311.pyc +0 -0
  71. package/skills/ecological-data-foundation/scripts/__pycache__/download_from_obis.cpython-311.pyc +0 -0
  72. package/skills/ecological-data-foundation/scripts/clean_occurrences.R +230 -0
  73. package/skills/ecological-data-foundation/scripts/clean_occurrences.py +268 -0
  74. package/skills/ecological-data-foundation/scripts/download_from_ebird.R +251 -0
  75. package/skills/ecological-data-foundation/scripts/download_from_ebird.py +364 -0
  76. package/skills/ecological-data-foundation/scripts/download_from_gbif.R +315 -0
  77. package/skills/ecological-data-foundation/scripts/download_from_gbif.py +407 -0
  78. package/skills/ecological-data-foundation/scripts/download_from_inat.R +238 -0
  79. package/skills/ecological-data-foundation/scripts/download_from_inat.py +304 -0
  80. package/skills/ecological-data-foundation/scripts/download_from_iucn.R +273 -0
  81. package/skills/ecological-data-foundation/scripts/download_from_iucn.py +344 -0
  82. package/skills/ecological-data-foundation/scripts/download_from_obis.R +248 -0
  83. package/skills/ecological-data-foundation/scripts/download_from_obis.py +318 -0
  84. package/skills/ecological-impact-assessment/SKILL.md +123 -0
  85. package/skills/ecological-impact-assessment/examples/example-prompts.md +32 -0
  86. package/skills/ecological-impact-assessment/resources/baci-design-guide.md +55 -0
  87. package/skills/ecological-impact-assessment/resources/fragmentation-metrics-reference.md +86 -0
  88. package/skills/ecological-impact-assessment/resources/pressure-index-template.md +78 -0
  89. package/skills/ecological-impact-assessment/resources/study-design-guide.md +168 -0
  90. package/skills/ecological-impact-assessment/scripts/baci_analysis.R +161 -0
  91. package/skills/ecological-impact-assessment/scripts/fragmentation_analysis.py +141 -0
  92. package/skills/ecological-impact-assessment/scripts/power_analysis_baci.R +274 -0
  93. package/skills/ecosystem-services-assessment/SKILL.md +125 -0
  94. package/skills/ecosystem-services-assessment/examples/example-prompts.md +24 -0
  95. package/skills/ecosystem-services-assessment/resources/es-indicator-reference.md +45 -0
  96. package/skills/ecosystem-services-assessment/resources/invest-parameter-guide.md +86 -0
  97. package/skills/ecosystem-services-assessment/resources/rusle-coefficients.md +88 -0
  98. package/skills/ecosystem-services-assessment/scripts/__pycache__/compute_es.cpython-311.pyc +0 -0
  99. package/skills/ecosystem-services-assessment/scripts/compute_es.py +189 -0
  100. package/skills/ecosystem-services-assessment/scripts/tradeoff_analysis.R +161 -0
  101. package/skills/environmental-time-series/SKILL.md +125 -0
  102. package/skills/environmental-time-series/examples/example-prompts.md +33 -0
  103. package/skills/environmental-time-series/resources/anomaly-indices-reference.md +88 -0
  104. package/skills/environmental-time-series/resources/bfast-parameter-guide.md +69 -0
  105. package/skills/environmental-time-series/scripts/__pycache__/recovery_trajectory.cpython-311.pyc +0 -0
  106. package/skills/environmental-time-series/scripts/__pycache__/trend_analysis.cpython-311.pyc +0 -0
  107. package/skills/environmental-time-series/scripts/recovery_trajectory.R +305 -0
  108. package/skills/environmental-time-series/scripts/recovery_trajectory.py +178 -0
  109. package/skills/environmental-time-series/scripts/trend_analysis.R +192 -0
  110. package/skills/environmental-time-series/scripts/trend_analysis.py +184 -0
  111. package/skills/geoprocessing-for-ecology/SKILL.md +123 -0
  112. package/skills/geoprocessing-for-ecology/examples/example-prompts.md +32 -0
  113. package/skills/geoprocessing-for-ecology/resources/crs-reference.md +62 -0
  114. package/skills/geoprocessing-for-ecology/resources/global-predictor-sources.md +331 -0
  115. package/skills/geoprocessing-for-ecology/resources/resampling-methods.md +57 -0
  116. package/skills/geoprocessing-for-ecology/scripts/__pycache__/download_predictors.cpython-311.pyc +0 -0
  117. package/skills/geoprocessing-for-ecology/scripts/download_predictors.R +239 -0
  118. package/skills/geoprocessing-for-ecology/scripts/download_predictors.py +379 -0
  119. package/skills/geoprocessing-for-ecology/scripts/stack_and_extract.R +224 -0
  120. package/skills/geoprocessing-for-ecology/scripts/stack_and_extract.py +172 -0
  121. package/skills/landscape-connectivity/SKILL.md +170 -0
  122. package/skills/landscape-connectivity/examples/example-prompts.md +96 -0
  123. package/skills/landscape-connectivity/examples/jaguar_mesoamerica_corridor_example.md +271 -0
  124. package/skills/landscape-connectivity/resources/circuitscape-parameter-guide.md +155 -0
  125. package/skills/landscape-connectivity/resources/graph-theory-for-ecology.md +134 -0
  126. package/skills/landscape-connectivity/resources/resistance-surface-guide.md +141 -0
  127. package/skills/landscape-connectivity/scripts/connectivity_analysis.py +387 -0
  128. package/skills/landscape-connectivity/scripts/connectivity_metrics.R +274 -0
  129. package/skills/landscape-connectivity/scripts/resistance_surface.R +239 -0
  130. package/skills/model-validation-and-uncertainty/SKILL.md +131 -0
  131. package/skills/model-validation-and-uncertainty/examples/example-prompts.md +30 -0
  132. package/skills/model-validation-and-uncertainty/resources/extrapolation-risk-guide.md +236 -0
  133. package/skills/model-validation-and-uncertainty/resources/metric-selection-guide.md +52 -0
  134. package/skills/model-validation-and-uncertainty/resources/threshold-selection-guide.md +64 -0
  135. package/skills/model-validation-and-uncertainty/scripts/__pycache__/validate_model.cpython-311.pyc +0 -0
  136. package/skills/model-validation-and-uncertainty/scripts/extrapolation_risk.R +315 -0
  137. package/skills/model-validation-and-uncertainty/scripts/validate_model.py +226 -0
  138. package/skills/model-validation-and-uncertainty/scripts/validate_sdm.R +162 -0
  139. package/skills/occupancy-and-detection/SKILL.md +126 -0
  140. package/skills/occupancy-and-detection/examples/example-prompts.md +33 -0
  141. package/skills/occupancy-and-detection/resources/detection-history-format.md +100 -0
  142. package/skills/occupancy-and-detection/resources/occupancy-study-design.md +47 -0
  143. package/skills/occupancy-and-detection/scripts/__pycache__/occupancy_analysis.cpython-311.pyc +0 -0
  144. package/skills/occupancy-and-detection/scripts/occupancy_analysis.R +160 -0
  145. package/skills/occupancy-and-detection/scripts/occupancy_analysis.py +159 -0
  146. package/skills/population-viability-analysis/SKILL.md +161 -0
  147. package/skills/population-viability-analysis/examples/african_elephant_pva_example.md +266 -0
  148. package/skills/population-viability-analysis/examples/example-prompts.md +95 -0
  149. package/skills/population-viability-analysis/resources/extinction-risk-thresholds.md +128 -0
  150. package/skills/population-viability-analysis/resources/matrix-model-guide.md +139 -0
  151. package/skills/population-viability-analysis/resources/sensitivity-elasticity-reference.md +182 -0
  152. package/skills/population-viability-analysis/scripts/matrix_pva.R +258 -0
  153. package/skills/population-viability-analysis/scripts/pva_analysis.py +442 -0
  154. package/skills/population-viability-analysis/scripts/stochastic_pva.R +353 -0
  155. package/skills/predictive-modeling-best-practices/SKILL.md +136 -0
  156. package/skills/predictive-modeling-best-practices/examples/example-prompts.md +58 -0
  157. package/skills/predictive-modeling-best-practices/resources/collinearity-decision-tree.md +65 -0
  158. package/skills/predictive-modeling-best-practices/resources/sampling-bias-correction.md +267 -0
  159. package/skills/predictive-modeling-best-practices/resources/spatial-cv-guide.md +73 -0
  160. package/skills/predictive-modeling-best-practices/scripts/__pycache__/spatial_cv.cpython-311.pyc +0 -0
  161. package/skills/predictive-modeling-best-practices/scripts/collinearity_check.R +112 -0
  162. package/skills/predictive-modeling-best-practices/scripts/spatial_cv.py +182 -0
  163. package/skills/reproducible-ecology-pipeline/SKILL.md +139 -0
  164. package/skills/reproducible-ecology-pipeline/examples/example-prompts.md +35 -0
  165. package/skills/reproducible-ecology-pipeline/resources/directory-structure-template.md +94 -0
  166. package/skills/reproducible-ecology-pipeline/resources/params-yaml-template.yaml +84 -0
  167. package/skills/reproducible-ecology-pipeline/resources/reproducibility-checklist-template.md +66 -0
  168. package/skills/reproducible-ecology-pipeline/scripts/generate_file_manifest.py +110 -0
  169. package/skills/reproducible-ecology-pipeline/scripts/init_project.sh +53 -0
  170. package/skills/spatial-prioritization/SKILL.md +162 -0
  171. package/skills/spatial-prioritization/examples/biodiversity_hotspot_prioritization_example.md +289 -0
  172. package/skills/spatial-prioritization/examples/example-prompts.md +93 -0
  173. package/skills/spatial-prioritization/resources/cost-surface-reference.md +130 -0
  174. package/skills/spatial-prioritization/resources/marxan-vs-prioritizr-comparison.md +125 -0
  175. package/skills/spatial-prioritization/resources/prioritizr-formulation-guide.md +188 -0
  176. package/skills/spatial-prioritization/resources/representation-targets-guide.md +186 -0
  177. package/skills/spatial-prioritization/scripts/prioritization_sensitivity.R +320 -0
  178. package/skills/spatial-prioritization/scripts/run_prioritization.R +336 -0
  179. package/skills/species-distribution-modeling/SKILL.md +139 -0
  180. package/skills/species-distribution-modeling/examples/example-prompts.md +36 -0
  181. package/skills/species-distribution-modeling/resources/algorithm-comparison.md +25 -0
  182. package/skills/species-distribution-modeling/resources/calibration-area-guide.md +71 -0
  183. package/skills/species-distribution-modeling/resources/climate-scenario-preparation.md +170 -0
  184. package/skills/species-distribution-modeling/resources/maxent-calibration-guide.md +211 -0
  185. package/skills/species-distribution-modeling/resources/sdm-checklist.md +37 -0
  186. package/skills/species-distribution-modeling/scripts/predict_distribution.R +236 -0
  187. package/skills/species-distribution-modeling/scripts/predict_distribution.py +286 -0
  188. package/skills/species-distribution-modeling/scripts/prepare_future_layers.R +351 -0
  189. package/skills/species-distribution-modeling/scripts/project_scenarios.R +220 -0
  190. package/skills/species-distribution-modeling/scripts/run_ensemble_sdm.R +99 -0
  191. package/skills/species-distribution-modeling/scripts/sdm_pipeline.py +318 -0
  192. package/skills/species-distribution-modeling/scripts/tune_maxnet.R +344 -0
  193. package/templates/SKILL_TEMPLATE.md +225 -0
  194. package/templates/checklists/data-submission-checklist.md +38 -0
  195. package/templates/checklists/post-analysis-checklist.md +55 -0
  196. package/templates/checklists/pre-analysis-checklist.md +31 -0
  197. package/templates/prompts/debug-skill.md +47 -0
  198. package/templates/prompts/invoke-skill.md +34 -0
  199. package/templates/prompts/invoke-workflow.md +45 -0
  200. package/templates/reports/technical-report-template.md +80 -0
  201. package/templates/scripts/logger_setup.R +79 -0
  202. package/templates/scripts/logger_setup.py +119 -0
  203. package/templates/scripts/params_loader.R +28 -0
  204. package/templates/scripts/params_loader.py +38 -0
  205. package/workflows/analyze-community-structure/WORKFLOW.md +72 -0
  206. package/workflows/analyze-environmental-change/WORKFLOW.md +73 -0
  207. package/workflows/assess-ecological-impact/WORKFLOW.md +75 -0
  208. package/workflows/assess-ecosystem-services/WORKFLOW.md +68 -0
  209. package/workflows/assess-landscape-connectivity/WORKFLOW.md +84 -0
  210. package/workflows/build-fire-risk-map/WORKFLOW.md +79 -0
  211. package/workflows/produce-technical-report/WORKFLOW.md +113 -0
  212. package/workflows/run-camera-trap-occupancy/WORKFLOW.md +87 -0
  213. package/workflows/run-conservation-prioritization/WORKFLOW.md +89 -0
  214. package/workflows/run-multispecies-screening/WORKFLOW.md +197 -0
  215. package/workflows/run-occupancy-analysis/WORKFLOW.md +74 -0
  216. package/workflows/run-population-viability/WORKFLOW.md +90 -0
  217. package/workflows/run-sdm-study/WORKFLOW.md +99 -0
@@ -0,0 +1,320 @@
1
+ # ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ # Usage: Rscript prioritization_sensitivity.R <pu_raster> <features_dir>
5
+ # <output_dir> [targets] [locked_in_raster] [locked_out_raster]
6
+ #
7
+ # Sensitivity analysis for conservation prioritization:
8
+ # 1. BLM calibration (cost vs compactness tradeoff)
9
+ # 2. Target sensitivity (50%, 75%, 100%, 125%, 150% of baseline targets)
10
+ # 3. Cost scenario sensitivity (-30%, baseline, +30%)
11
+ # 4. Portfolio irreplaceability (selection frequency across scenarios)
12
+ #
13
+ # Outputs:
14
+ # blm_calibration.csv — Cost and boundary at each BLM value
15
+ # blm_calibration_plot.png — Elbow plot for BLM selection
16
+ # target_sensitivity.csv — Targets met and cost at each target scaling
17
+ # cost_scenario_sensitivity.csv — Cost and PU selection under cost uncertainty
18
+ # portfolio_frequency.tif — Selection frequency raster across all scenarios
19
+
20
+ # ── Inline logger ─────────────────────────────────────────────────────────────
21
+ SKILL_NAME <- "spatial-prioritization"
22
+ .log_ts <- function() format(Sys.time(), "[%Y-%m-%d %H:%M:%S]")
23
+ log_info <- function(...) message(.log_ts(), " [INFO] ", sprintf(...))
24
+ log_warn <- function(...) message(.log_ts(), " [WARN] ", sprintf(...))
25
+ log_error<- function(...) message(.log_ts(), " [ERROR] ", sprintf(...))
26
+ log_step <- function(n, d) log_info("-- STEP %d: %s", n, d)
27
+ log_decision <- function(v, val, why) log_info("DECISION | %s = %s | %s", v, val, why)
28
+ dir.create("logs", recursive=TRUE, showWarnings=FALSE)
29
+
30
+ suppressPackageStartupMessages(library(prioritizr))
31
+ suppressPackageStartupMessages(library(terra))
32
+ suppressPackageStartupMessages(library(dplyr))
33
+ suppressPackageStartupMessages(library(ggplot2))
34
+
35
+ args <- commandArgs(trailingOnly = TRUE)
36
+ if (length(args) < 3) {
37
+ cat("Usage: Rscript prioritization_sensitivity.R <pu_raster> <features_dir>",
38
+ "<output_dir> [targets] [locked_in] [locked_out]\n")
39
+ quit(status = 1)
40
+ }
41
+
42
+ pu_path <- args[1]
43
+ features_dir <- args[2]
44
+ output_dir <- args[3]
45
+ targets_arg <- if (length(args) >= 4 && args[4] != "NA") args[4] else "0.30"
46
+ locked_in_p <- if (length(args) >= 5 && args[5] != "NA") args[5] else NULL
47
+ locked_out_p <- if (length(args) >= 6 && args[6] != "NA") args[6] else NULL
48
+
49
+ # ── Input precondition checks ─────────────────────────────────────────────────
50
+ if (!file.exists(pu_path)) {
51
+ log_error("Input nao encontrado: %s\nCausa provavel: passo anterior nao concluiu.\nVerifique: outputs do skill anterior.\nSkill anterior: spatial-prioritization (run_prioritization)", pu_path)
52
+ stop("Missing input: ", pu_path)
53
+ }
54
+ if (!dir.exists(features_dir)) {
55
+ log_error("Diretorio de features nao encontrado: %s\nCausa provavel: passo anterior nao concluiu ou caminho incorreto.\nVerifique: outputs do skill anterior.\nSkill anterior: spatial-prioritization (run_prioritization)", features_dir)
56
+ stop("Missing features directory: ", features_dir)
57
+ }
58
+
59
+ log_decision("targets_arg", targets_arg, "Baseline targets: single proportion applied to all features, or path to CSV with per-feature targets")
60
+ log_decision("locked_in_p", ifelse(is.null(locked_in_p), "none", locked_in_p), "Locked-in raster constrains solver to always select these PUs")
61
+ log_decision("locked_out_p", ifelse(is.null(locked_out_p), "none", locked_out_p), "Locked-out raster constrains solver to never select these PUs")
62
+
63
+ dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
64
+
65
+ # ── Load data ─────────────────────────────────────────────────────────────────
66
+ log_step(1, "Load planning unit raster and feature layers")
67
+ tryCatch({
68
+ pu <- rast(pu_path)
69
+ feat_files <- list.files(features_dir, pattern = "\\.tif$",
70
+ full.names = TRUE, ignore.case = TRUE)
71
+ if (length(feat_files) == 0) {
72
+ log_error("Nenhum arquivo .tif encontrado em: %s\nCausa provavel: features_dir incorreto ou features nao geradas.\nVerifique: conteudo do diretorio de features.\nSkill anterior: spatial-prioritization (run_prioritization)", features_dir)
73
+ stop("No .tif feature files found in: ", features_dir)
74
+ }
75
+ features <- rast(feat_files)
76
+ features <- resample(features, pu, method = "bilinear")
77
+ names(features) <- tools::file_path_sans_ext(basename(feat_files))
78
+
79
+ # Remove zero-sum features
80
+ feat_sums <- global(features, "sum", na.rm = TRUE)[[1]]
81
+ zero_feats <- names(features)[feat_sums == 0]
82
+ if (length(zero_feats) > 0) {
83
+ log_warn("%d features com soma zero excluidas: %s", length(zero_feats), paste(zero_feats, collapse = ", "))
84
+ }
85
+ features <- features[[feat_sums > 0]]
86
+ n_feats <- nlyr(features)
87
+ log_info("Planning unit raster loaded: %d x %d cells.", nrow(pu), ncol(pu))
88
+ log_info("Feature layers loaded: %d (after removing zero-sum).", n_feats)
89
+
90
+ if (n_feats < 1) {
91
+ log_error("Nenhuma feature valida apos remocao de zero-sum.\nCausa provavel: todas as features tem distribuicao zero na area de estudo.\nVerifique: extensao espacial dos rasters de features.\nSkill anterior: spatial-prioritization (run_prioritization)")
92
+ stop("No valid feature layers remaining after zero-sum removal.")
93
+ }
94
+ }, error = function(e) {
95
+ log_error("Falha em load_data: %s\nCausa provavel: rasters corrompidos, incompativeis ou caminho incorreto.\nVerifique: arquivos .tif e resolucao espacial.\nSkill anterior: spatial-prioritization (run_prioritization)", conditionMessage(e))
96
+ stop(e)
97
+ })
98
+
99
+ # Baseline targets
100
+ log_step(2, "Set baseline targets")
101
+ tryCatch({
102
+ if (file.exists(targets_arg)) {
103
+ target_df <- read.csv(targets_arg)
104
+ targets_base <- target_df$target[match(names(features), target_df$feature_name)]
105
+ n_unmatched <- sum(is.na(targets_base))
106
+ targets_base[is.na(targets_base)] <- 0.30
107
+ if (n_unmatched > 0) {
108
+ log_warn("%d features sem alvo no CSV; usando padrao 0.30.", n_unmatched)
109
+ }
110
+ log_decision("targets_base", "from_csv", paste0("Per-feature targets loaded from ", targets_arg))
111
+ } else {
112
+ targets_base <- rep(as.numeric(targets_arg), n_feats)
113
+ log_decision("targets_base", targets_arg, "Single proportion applied uniformly to all features")
114
+ }
115
+ log_info("Baseline targets: min=%.2f, mean=%.2f, max=%.2f",
116
+ min(targets_base), mean(targets_base), max(targets_base))
117
+ }, error = function(e) {
118
+ log_error("Falha em set_targets: %s\nCausa provavel: CSV de alvos malformado ou proporcao invalida.\nVerifique: formato do arquivo de alvos e nomes das features.\nSkill anterior: spatial-prioritization (run_prioritization)", conditionMessage(e))
119
+ stop(e)
120
+ })
121
+
122
+ # Helper: build and solve base problem with given cost and targets
123
+ solve_scenario <- function(cost_r, targets_v, blm_val = 0, name = "scenario") {
124
+ p <- problem(cost_r, features) %>%
125
+ add_min_set_objective() %>%
126
+ add_relative_targets(pmin(targets_v, 0.999)) %>%
127
+ add_binary_decisions() %>%
128
+ add_highs_solver(gap = 0.05, time_limit = 300, verbose = FALSE)
129
+
130
+ if (!is.null(locked_in_p)) {
131
+ li <- resample(rast(locked_in_p), cost_r, method = "near")
132
+ p <- p %>% add_locked_in_constraints(li)
133
+ }
134
+ if (!is.null(locked_out_p)) {
135
+ lo <- resample(rast(locked_out_p), cost_r, method = "near")
136
+ p <- p %>% add_locked_out_constraints(lo)
137
+ }
138
+ if (blm_val > 0) {
139
+ p <- p %>% add_boundary_penalties(penalty = blm_val, data = NULL)
140
+ }
141
+
142
+ s <- tryCatch(solve(p), error = function(e) {
143
+ log_warn("Solver falhou para cenario '%s': %s. Retornando NULL.", name, conditionMessage(e))
144
+ return(NULL)
145
+ })
146
+ if (is.null(s)) return(NULL)
147
+
148
+ rep_s <- eval_feature_representation_summary(p, s)
149
+ cost_s <- eval_cost_summary(p, s)$cost
150
+ bound_s <- tryCatch(eval_boundary_summary(p, s)$boundary,
151
+ error = function(e) NA_real_)
152
+
153
+ list(
154
+ solution = s,
155
+ n_selected = sum(values(s) == 1, na.rm = TRUE),
156
+ total_cost = cost_s,
157
+ boundary = bound_s,
158
+ targets_met = sum(rep_s$relative_held >= targets_v, na.rm = TRUE),
159
+ mean_held = mean(rep_s$relative_held, na.rm = TRUE)
160
+ )
161
+ }
162
+
163
+ # ── 1. BLM Calibration ────────────────────────────────────────────────────────
164
+ log_step(3, "BLM calibration: cost vs compactness tradeoff")
165
+ log_info("Running BLM calibration...")
166
+ blm_values <- c(0, 0.001, 0.01, 0.05, 0.1, 0.5, 1.0)
167
+ log_decision("blm_values", paste(blm_values, collapse = ", "),
168
+ "Standard BLM range spanning several orders of magnitude to identify elbow in cost-boundary tradeoff")
169
+
170
+ tryCatch({
171
+ blm_results <- lapply(blm_values, function(blm) {
172
+ log_info(" BLM = %g", blm)
173
+ res <- solve_scenario(pu, targets_base, blm_val = blm,
174
+ name = paste0("blm_", blm))
175
+ if (is.null(res)) {
176
+ log_warn("Cenario BLM=%g nao produziu solucao.", blm)
177
+ return(NULL)
178
+ }
179
+ data.frame(blm = blm, cost = res$total_cost,
180
+ boundary = res$boundary, n_selected = res$n_selected)
181
+ })
182
+ blm_df <- dplyr::bind_rows(Filter(Negate(is.null), blm_results))
183
+ write.csv(blm_df, file.path(output_dir, "blm_calibration.csv"), row.names = FALSE)
184
+ log_info("BLM calibration done. %d / %d scenarios solved successfully.", nrow(blm_df), length(blm_values))
185
+
186
+ if (nrow(blm_df) < 3) {
187
+ log_warn("Menos de 3 cenarios BLM resolvidos. Grafico de cotovelo pode ser insuficiente para selecao de BLM.")
188
+ }
189
+ }, error = function(e) {
190
+ log_error("Falha em blm_calibration: %s\nCausa provavel: falha do solver HiGHS ou dados raster invalidos.\nVerifique: instalacao do HiGHS e integridade dos rasters.\nSkill anterior: spatial-prioritization (run_prioritization)", conditionMessage(e))
191
+ stop(e)
192
+ })
193
+
194
+ # BLM elbow plot
195
+ log_step(4, "Generate BLM elbow plot")
196
+ if (nrow(blm_df) > 2) {
197
+ tryCatch({
198
+ p_blm <- ggplot(blm_df, aes(x = boundary, y = cost, label = blm)) +
199
+ geom_path(colour = "steelblue") +
200
+ geom_point(size = 3, colour = "steelblue") +
201
+ ggrepel::geom_text_repel(size = 3) +
202
+ labs(x = "Total boundary length", y = "Total cost",
203
+ title = "BLM calibration: cost vs compactness tradeoff") +
204
+ theme_minimal(base_size = 10)
205
+ tryCatch(
206
+ ggsave(file.path(output_dir, "blm_calibration_plot.png"), p_blm,
207
+ width = 7, height = 5, dpi = 150),
208
+ error = function(e) log_warn("BLM plot falhou ao salvar: %s", conditionMessage(e))
209
+ )
210
+ log_info("BLM calibration plot saved.")
211
+ }, error = function(e) {
212
+ log_warn("Falha ao gerar grafico BLM: %s. Continuando sem o grafico.", conditionMessage(e))
213
+ })
214
+ } else {
215
+ log_warn("Dados insuficientes para grafico BLM (menos de 3 pontos). Grafico nao gerado.")
216
+ }
217
+
218
+ # ── 2. Target Sensitivity ─────────────────────────────────────────────────────
219
+ log_step(5, "Target sensitivity analysis")
220
+ log_info("Running target sensitivity analysis...")
221
+ target_scalings <- c(0.50, 0.75, 1.00, 1.25, 1.50)
222
+ log_decision("target_scalings", paste(target_scalings, collapse = ", "),
223
+ "Scaling factors applied to baseline targets to assess sensitivity of solution cost and coverage")
224
+
225
+ tryCatch({
226
+ target_results <- lapply(target_scalings, function(sc) {
227
+ log_info(" Target scaling = %.2fx", sc)
228
+ tgts <- pmin(targets_base * sc, 0.999)
229
+ res <- solve_scenario(pu, tgts, name = paste0("target_", sc))
230
+ if (is.null(res)) {
231
+ log_warn("Cenario de alvo %.2fx nao produziu solucao.", sc)
232
+ return(NULL)
233
+ }
234
+ data.frame(target_scaling = sc,
235
+ mean_target = mean(tgts),
236
+ cost = res$total_cost,
237
+ targets_met = res$targets_met,
238
+ n_selected = res$n_selected)
239
+ })
240
+ target_df <- dplyr::bind_rows(Filter(Negate(is.null), target_results))
241
+ write.csv(target_df, file.path(output_dir, "target_sensitivity.csv"),
242
+ row.names = FALSE)
243
+ log_info("Target sensitivity done. %d / %d scenarios solved.", nrow(target_df), length(target_scalings))
244
+ }, error = function(e) {
245
+ log_error("Falha em target_sensitivity: %s\nCausa provavel: falha do solver ou alvos fora do intervalo [0, 0.999].\nVerifique: valores de targets_base e instalacao do HiGHS.\nSkill anterior: spatial-prioritization (run_prioritization)", conditionMessage(e))
246
+ stop(e)
247
+ })
248
+
249
+ # ── 3. Cost Scenario Sensitivity ──────────────────────────────────────────────
250
+ log_step(6, "Cost scenario sensitivity analysis")
251
+ log_info("Running cost scenario sensitivity analysis...")
252
+ log_decision("cost_scenarios", "low=-30%, baseline, high=+30%",
253
+ "Standard cost uncertainty range to assess robustness of prioritization to cost data errors")
254
+
255
+ tryCatch({
256
+ cost_scenarios <- list(
257
+ low = pu * 0.70,
258
+ baseline = pu,
259
+ high = pu * 1.30
260
+ )
261
+
262
+ cost_results <- lapply(names(cost_scenarios), function(name) {
263
+ log_info(" Cost scenario: %s", name)
264
+ res <- solve_scenario(cost_scenarios[[name]], targets_base, name = name)
265
+ if (is.null(res)) {
266
+ log_warn("Cenario de custo '%s' nao produziu solucao.", name)
267
+ return(NULL)
268
+ }
269
+ data.frame(cost_scenario = name,
270
+ total_cost = res$total_cost,
271
+ n_selected = res$n_selected,
272
+ targets_met = res$targets_met)
273
+ })
274
+ cost_df <- dplyr::bind_rows(Filter(Negate(is.null), cost_results))
275
+ write.csv(cost_df, file.path(output_dir, "cost_scenario_sensitivity.csv"),
276
+ row.names = FALSE)
277
+ log_info("Cost scenario sensitivity done. %d / %d scenarios solved.", nrow(cost_df), length(cost_scenarios))
278
+ }, error = function(e) {
279
+ log_error("Falha em cost_scenario_sensitivity: %s\nCausa provavel: falha do solver ou raster de custo invalido.\nVerifique: valores do raster pu e instalacao do HiGHS.\nSkill anterior: spatial-prioritization (run_prioritization)", conditionMessage(e))
280
+ stop(e)
281
+ })
282
+
283
+ # ── 4. Portfolio Irreplaceability ─────────────────────────────────────────────
284
+ log_step(7, "Build portfolio irreplaceability (selection frequency across all scenarios)")
285
+ log_info("Building portfolio irreplaceability (selection frequency across scenarios)...")
286
+
287
+ tryCatch({
288
+ # Collect all solutions computed above
289
+ all_solutions <- list()
290
+ for (blm in blm_values) {
291
+ res <- solve_scenario(pu, targets_base, blm_val = blm, name = paste0("blm_", blm))
292
+ if (!is.null(res)) all_solutions[[length(all_solutions) + 1]] <- res$solution
293
+ }
294
+ for (sc in target_scalings) {
295
+ tgts <- pmin(targets_base * sc, 0.999)
296
+ res <- solve_scenario(pu, tgts, name = paste0("target_", sc))
297
+ if (!is.null(res)) all_solutions[[length(all_solutions) + 1]] <- res$solution
298
+ }
299
+
300
+ if (length(all_solutions) < 2) {
301
+ log_warn("Solucoes insuficientes para analise de portfolio (%d). Sao necessarias pelo menos 2.", length(all_solutions))
302
+ } else {
303
+ freq_raster <- Reduce("+", all_solutions) / length(all_solutions)
304
+ names(freq_raster) <- "selection_frequency"
305
+ writeRaster(freq_raster, file.path(output_dir, "portfolio_frequency.tif"),
306
+ overwrite = TRUE)
307
+ log_info("Portfolio frequency raster saved (%d scenarios).", length(all_solutions))
308
+ }
309
+ }, error = function(e) {
310
+ log_error("Falha em portfolio_irreplaceability: %s\nCausa provavel: solucoes incompativeis (extensoes diferentes) ou falha na soma de rasters.\nVerifique: consistencia espacial das solucoes individuais.\nSkill anterior: spatial-prioritization (run_prioritization)", conditionMessage(e))
311
+ stop(e)
312
+ })
313
+
314
+ # ── Summary report ────────────────────────────────────────────────────────────
315
+ log_step(8, "Print sensitivity analysis summary")
316
+ log_info("=== Sensitivity Analysis Summary ===")
317
+ log_info("BLM calibration:\n%s", paste(capture.output(print(blm_df)), collapse = "\n"))
318
+ log_info("Target sensitivity:\n%s", paste(capture.output(print(target_df)), collapse = "\n"))
319
+ log_info("Cost scenario sensitivity:\n%s", paste(capture.output(print(cost_df)), collapse = "\n"))
320
+ log_info("Sensitivity analysis complete.")
@@ -0,0 +1,336 @@
1
+ # ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ # Usage: Rscript run_prioritization.R <pu_raster> <features_dir> <output_dir>
5
+ # [targets] [locked_in_raster] [locked_out_raster] [blm] [budget]
6
+ #
7
+ # Runs systematic conservation prioritization using prioritizr (minimum-set
8
+ # or maximum-coverage ILP problem solved with HiGHS solver).
9
+ #
10
+ # Arguments:
11
+ # pu_raster — Planning unit cost raster (.tif); NA = excluded
12
+ # features_dir — Directory of feature rasters (.tif, one per species/habitat)
13
+ # output_dir — Directory for output files
14
+ # targets — Single value (applied to all features) or path to CSV
15
+ # with columns feature_name, target (default: 0.30)
16
+ # locked_in_raster — Binary raster, 1 = must select (default: none)
17
+ # locked_out_raster — Binary raster, 1 = must exclude (default: none)
18
+ # blm — Boundary length modifier (default: 0)
19
+ # budget — For maximum coverage problem; if provided, switches
20
+ # objective to max-features (default: NA = min-set)
21
+ #
22
+ # Outputs:
23
+ # solution.tif — Binary raster: 1 = selected PU
24
+ # feature_representation.csv — Amount of each feature in solution
25
+ # cost_summary.csv — Total cost and number of PUs selected
26
+ # irreplaceability.tif — Rarity-weighted importance of each PU
27
+ # prioritization_map.png — Visual output
28
+
29
+ # ── Inline logger ─────────────────────────────────────────────────────────────
30
+ SKILL_NAME <- "spatial-prioritization"
31
+ .log_ts <- function() format(Sys.time(), "[%Y-%m-%d %H:%M:%S]")
32
+ log_info <- function(...) message(.log_ts(), " [INFO] ", sprintf(...))
33
+ log_warn <- function(...) message(.log_ts(), " [WARN] ", sprintf(...))
34
+ log_error<- function(...) message(.log_ts(), " [ERROR] ", sprintf(...))
35
+ log_step <- function(n, d) log_info("-- STEP %d: %s", n, d)
36
+ log_decision <- function(v, val, why) log_info("DECISION | %s = %s | %s", v, val, why)
37
+ dir.create("logs", recursive=TRUE, showWarnings=FALSE)
38
+
39
+ suppressPackageStartupMessages(library(prioritizr))
40
+ suppressPackageStartupMessages(library(terra))
41
+ suppressPackageStartupMessages(library(sf))
42
+ suppressPackageStartupMessages(library(dplyr))
43
+ suppressPackageStartupMessages(library(ggplot2))
44
+
45
+ args <- commandArgs(trailingOnly = TRUE)
46
+ if (length(args) < 3) {
47
+ cat("Usage: Rscript run_prioritization.R <pu_raster> <features_dir>",
48
+ "<output_dir> [targets] [locked_in] [locked_out] [blm] [budget]\n")
49
+ quit(status = 1)
50
+ }
51
+
52
+ pu_path <- args[1]
53
+ features_dir <- args[2]
54
+ output_dir <- args[3]
55
+ targets_arg <- if (length(args) >= 4 && args[4] != "NA") args[4] else "0.30"
56
+ locked_in_p <- if (length(args) >= 5 && args[5] != "NA") args[5] else NULL
57
+ locked_out_p <- if (length(args) >= 6 && args[6] != "NA") args[6] else NULL
58
+ blm <- if (length(args) >= 7) as.numeric(args[7]) else 0
59
+ budget <- if (length(args) >= 8 && args[8] != "NA") as.numeric(args[8]) else NA_real_
60
+
61
+ # ── Input precondition checks ─────────────────────────────────────────────────
62
+ if (!file.exists(pu_path)) {
63
+ log_error("Input nao encontrado: %s\nCausa provavel: passo anterior nao concluiu.\nVerifique: outputs do skill anterior.\nSkill anterior: species-distribution-modeling", pu_path)
64
+ stop("Missing input: ", pu_path)
65
+ }
66
+ if (!dir.exists(features_dir)) {
67
+ log_error("Diretorio de features nao encontrado: %s\nCausa provavel: passo anterior nao concluiu ou caminho incorreto.\nVerifique: outputs do skill anterior.\nSkill anterior: species-distribution-modeling", features_dir)
68
+ stop("Missing features directory: ", features_dir)
69
+ }
70
+
71
+ log_decision("targets_arg", targets_arg, "Conservation targets: single proportion for all features or path to per-feature CSV")
72
+ log_decision("blm", blm, "Boundary length modifier: 0 = no compactness penalty; increase to promote spatially compact solutions")
73
+ log_decision("budget", ifelse(is.na(budget), "NA (min-set)", budget), "Budget for max-coverage objective; NA triggers minimum-set formulation")
74
+ log_decision("locked_in_p", ifelse(is.null(locked_in_p), "none", locked_in_p), "Locked-in raster: PUs that must always be selected (e.g., existing protected areas)")
75
+ log_decision("locked_out_p", ifelse(is.null(locked_out_p), "none", locked_out_p), "Locked-out raster: PUs that must never be selected (e.g., urban areas)")
76
+
77
+ dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
78
+
79
+ # ── Load planning units ───────────────────────────────────────────────────────
80
+ log_step(1, "Load planning unit raster")
81
+ tryCatch({
82
+ pu <- rast(pu_path)
83
+ log_info("Planning units: %d x %d cells, CRS: %s",
84
+ nrow(pu), ncol(pu), crs(pu, describe = TRUE)$name)
85
+
86
+ n_pu_total <- sum(!is.na(values(pu)))
87
+ log_info("Valid planning units: %d", n_pu_total)
88
+
89
+ if (n_pu_total < 10) {
90
+ log_warn("Numero muito baixo de unidades de planejamento validas (%d). Verifique a mascara de NA no raster pu.", n_pu_total)
91
+ }
92
+ }, error = function(e) {
93
+ log_error("Falha em load_planning_units: %s\nCausa provavel: raster corrompido, caminho incorreto ou CRS ausente.\nVerifique: arquivo pu_raster e sua integridade.\nSkill anterior: species-distribution-modeling", conditionMessage(e))
94
+ stop(e)
95
+ })
96
+
97
+ # ── Load features ─────────────────────────────────────────────────────────────
98
+ log_step(2, "Load feature rasters")
99
+ tryCatch({
100
+ feat_files <- list.files(features_dir, pattern = "\\.tif$",
101
+ full.names = TRUE, ignore.case = TRUE)
102
+ if (length(feat_files) == 0) {
103
+ log_error("Nenhum arquivo .tif encontrado em: %s\nCausa provavel: features nao geradas ou caminho incorreto.\nVerifique: conteudo do diretorio features_dir.\nSkill anterior: species-distribution-modeling", features_dir)
104
+ stop("No .tif feature files found in: ", features_dir)
105
+ }
106
+ log_info("Loading %d feature layers...", length(feat_files))
107
+
108
+ features <- rast(feat_files)
109
+ # Ensure same extent/resolution as planning units
110
+ features <- resample(features, pu, method = "bilinear")
111
+ names(features) <- tools::file_path_sans_ext(basename(feat_files))
112
+
113
+ # Check for zero-sum features
114
+ feat_sums <- global(features, "sum", na.rm = TRUE)[[1]]
115
+ zero_feats <- names(features)[feat_sums == 0]
116
+ if (length(zero_feats) > 0) {
117
+ log_warn("%d features com soma zero excluidas (nenhuma ocorrencia na area de estudo): %s",
118
+ length(zero_feats), paste(zero_feats, collapse = ", "))
119
+ features <- features[[!names(features) %in% zero_feats]]
120
+ }
121
+ n_feats <- nlyr(features)
122
+ log_info("Features loaded: %d (after removing zero-sum)", n_feats)
123
+
124
+ if (n_feats == 0) {
125
+ log_error("Nenhuma feature valida apos remocao de zero-sum.\nCausa provavel: features nao sobrepõem a area de planejamento.\nVerifique: extensao e CRS dos rasters de features vs pu_raster.\nSkill anterior: species-distribution-modeling")
126
+ stop("No valid features remaining after zero-sum removal.")
127
+ }
128
+ }, error = function(e) {
129
+ log_error("Falha em load_features: %s\nCausa provavel: rasters corrompidos, incompativeis ou diretorio vazio.\nVerifique: arquivos .tif em features_dir e compatibilidade com pu_raster.\nSkill anterior: species-distribution-modeling", conditionMessage(e))
130
+ stop(e)
131
+ })
132
+
133
+ # ── Set targets ────────────────────────────────────────────────────────────────
134
+ log_step(3, "Set conservation targets")
135
+ tryCatch({
136
+ if (file.exists(targets_arg)) {
137
+ target_df <- read.csv(targets_arg)
138
+ targets_vec <- target_df$target[match(names(features), target_df$feature_name)]
139
+ na_targets <- is.na(targets_vec)
140
+ targets_vec[na_targets] <- 0.30 # default for unmatched features
141
+ if (any(na_targets)) {
142
+ log_warn("Alvo nao encontrado para %d features; usando padrao 0.30: %s",
143
+ sum(na_targets), paste(names(features)[na_targets], collapse = ", "))
144
+ }
145
+ log_decision("targets_vec", "from_csv", paste0("Per-feature targets loaded from ", targets_arg))
146
+ } else {
147
+ targets_vec <- rep(as.numeric(targets_arg), n_feats)
148
+ log_decision("targets_vec", targets_arg, "Uniform proportion target applied to all features")
149
+ }
150
+ log_info("Targets: min=%.2f, mean=%.2f, max=%.2f",
151
+ min(targets_vec), mean(targets_vec), max(targets_vec))
152
+
153
+ if (any(targets_vec > 0.90)) {
154
+ log_warn("%d features com alvo > 90%%. Alvos muito altos podem tornar o problema inviavel.", sum(targets_vec > 0.90))
155
+ }
156
+ }, error = function(e) {
157
+ log_error("Falha em set_targets: %s\nCausa provavel: CSV de alvos malformado ou proporcao invalida fora de [0,1].\nVerifique: formato do arquivo de alvos e nomes das features.\nSkill anterior: species-distribution-modeling", conditionMessage(e))
158
+ stop(e)
159
+ })
160
+
161
+ # ── Build problem ─────────────────────────────────────────────────────────────
162
+ log_step(4, "Build prioritizr problem")
163
+ tryCatch({
164
+ if (is.na(budget)) {
165
+ # Minimum set problem
166
+ p <- problem(pu, features) %>%
167
+ add_min_set_objective() %>%
168
+ add_relative_targets(targets_vec)
169
+ log_info("Objective: minimum cost (min-set)")
170
+ log_decision("objective", "min_set", "No budget specified; minimize cost while meeting all targets")
171
+ } else {
172
+ # Maximum coverage problem
173
+ p <- problem(pu, features) %>%
174
+ add_max_features_objective(budget = budget) %>%
175
+ add_absolute_targets(1e-6) # minimum feasibility constraint
176
+ log_info("Objective: maximum coverage (budget = %g)", budget)
177
+ log_decision("objective", "max_features", paste0("Budget ", budget, " specified; maximize feature coverage within budget"))
178
+ }
179
+
180
+ p <- p %>%
181
+ add_binary_decisions()
182
+
183
+ # Locked-in
184
+ if (!is.null(locked_in_p)) {
185
+ li <- rast(locked_in_p)
186
+ li <- resample(li, pu, method = "near")
187
+ p <- p %>% add_locked_in_constraints(li)
188
+ n_li <- sum(values(li) == 1, na.rm = TRUE)
189
+ log_info("Locked-in: %d PUs", n_li)
190
+ if (n_li == 0) {
191
+ log_warn("Raster locked-in fornecido mas nenhuma PU com valor 1 encontrada.")
192
+ }
193
+ }
194
+
195
+ # Locked-out
196
+ if (!is.null(locked_out_p)) {
197
+ lo <- rast(locked_out_p)
198
+ lo <- resample(lo, pu, method = "near")
199
+ p <- p %>% add_locked_out_constraints(lo)
200
+ n_lo <- sum(values(lo) == 1, na.rm = TRUE)
201
+ log_info("Locked-out: %d PUs", n_lo)
202
+ if (n_lo == 0) {
203
+ log_warn("Raster locked-out fornecido mas nenhuma PU com valor 1 encontrada.")
204
+ }
205
+ }
206
+
207
+ # Boundary penalty
208
+ if (blm > 0) {
209
+ p <- p %>% add_boundary_penalties(penalty = blm, data = NULL)
210
+ log_info("Boundary length modifier applied: %g", blm)
211
+ }
212
+
213
+ # Solver (HiGHS preferred)
214
+ p <- p %>%
215
+ add_highs_solver(gap = 0.01, time_limit = 600, verbose = TRUE)
216
+
217
+ log_info("Problem built successfully.")
218
+ }, error = function(e) {
219
+ log_error("Falha em build_problem: %s\nCausa provavel: incompatibilidade entre rasters, alvos invalidos ou pacote prioritizr desatualizado.\nVerifique: versoes de prioritizr e terra, e compatibilidade espacial dos rasters.\nSkill anterior: species-distribution-modeling", conditionMessage(e))
220
+ stop(e)
221
+ })
222
+
223
+ # ── Solve ─────────────────────────────────────────────────────────────────────
224
+ log_step(5, "Solve prioritization problem with HiGHS solver")
225
+ log_info("Solving...")
226
+ tryCatch({
227
+ s <- tryCatch(
228
+ solve(p),
229
+ error = function(e) {
230
+ log_warn("HiGHS falhou: %s. Tentando fallback para lpsymphony...", conditionMessage(e))
231
+ p2 <- p %>% add_lpsymphony_solver(gap = 0.05, time_limit = 600)
232
+ solve(p2)
233
+ }
234
+ )
235
+ log_info("Solver completed successfully.")
236
+ }, error = function(e) {
237
+ log_error("Falha em solve_problem: %s\nCausa provavel: problema inviavel (alvos inalcancaveis), solver nao instalado, ou timeout.\nVerifique: alvos vs disponibilidade de features, e instalacao do HiGHS/lpsymphony.\nSkill anterior: species-distribution-modeling", conditionMessage(e))
238
+ stop(e)
239
+ })
240
+
241
+ # ── Write solution raster ─────────────────────────────────────────────────────
242
+ log_step(6, "Write solution raster and compute selection summary")
243
+ tryCatch({
244
+ sol_path <- file.path(output_dir, "solution.tif")
245
+ writeRaster(s, sol_path, overwrite = TRUE)
246
+ n_selected <- sum(values(s) == 1, na.rm = TRUE)
247
+ log_info("Solution: %d PUs selected (%.1f%% of valid PUs)",
248
+ n_selected, n_selected / n_pu_total * 100)
249
+
250
+ if (n_selected == 0) {
251
+ log_warn("Nenhuma PU selecionada na solucao. Verifique viabilidade do problema e restricoes locked-out.")
252
+ }
253
+ if (n_selected / n_pu_total > 0.80) {
254
+ log_warn("Mais de 80%% das PUs selecionadas (%.1f%%). Alvos podem ser muito altos ou area de estudo muito restrita.", n_selected / n_pu_total * 100)
255
+ }
256
+ }, error = function(e) {
257
+ log_error("Falha em write_solution: %s\nCausa provavel: permissoes de escrita ou solucao invalida.\nVerifique: output_dir e resultado do solver.\nSkill anterior: species-distribution-modeling", conditionMessage(e))
258
+ stop(e)
259
+ })
260
+
261
+ # ── Feature representation ────────────────────────────────────────────────────
262
+ log_step(7, "Evaluate feature representation in solution")
263
+ tryCatch({
264
+ rep_df <- eval_feature_representation_summary(p, s)
265
+ rep_df$target <- targets_vec
266
+ rep_df$target_met <- rep_df$relative_held >= targets_vec
267
+
268
+ write.csv(rep_df, file.path(output_dir, "feature_representation.csv"),
269
+ row.names = FALSE)
270
+ n_targets_met <- sum(rep_df$target_met, na.rm = TRUE)
271
+ log_info("Targets met: %d / %d features", n_targets_met, n_feats)
272
+
273
+ if (n_targets_met < n_feats) {
274
+ log_warn("%d features nao atingiram seus alvos de representacao. Verifique viabilidade e restricoes.", n_feats - n_targets_met)
275
+ }
276
+ }, error = function(e) {
277
+ log_error("Falha em feature_representation: %s\nCausa provavel: incompatibilidade entre problema e solucao, ou falha na avaliacao.\nVerifique: objetos p e s e versao do prioritizr.\nSkill anterior: species-distribution-modeling", conditionMessage(e))
278
+ stop(e)
279
+ })
280
+
281
+ # ── Cost summary ──────────────────────────────────────────────────────────────
282
+ log_step(8, "Compute and write cost summary")
283
+ tryCatch({
284
+ cost_df <- data.frame(
285
+ metric = c("total_cost", "n_pu_selected", "n_pu_total",
286
+ "pct_pu_selected", "n_targets_met", "n_features"),
287
+ value = c(eval_cost_summary(p, s)$cost,
288
+ n_selected, n_pu_total,
289
+ n_selected / n_pu_total * 100,
290
+ n_targets_met, n_feats)
291
+ )
292
+ write.csv(cost_df, file.path(output_dir, "cost_summary.csv"), row.names = FALSE)
293
+ log_info("Cost summary:\n%s", paste(capture.output(print(cost_df)), collapse = "\n"))
294
+ }, error = function(e) {
295
+ log_error("Falha em cost_summary: %s\nCausa provavel: falha na avaliacao de custo ou permissoes de escrita.\nVerifique: objetos p e s e output_dir.\nSkill anterior: species-distribution-modeling", conditionMessage(e))
296
+ stop(e)
297
+ })
298
+
299
+ # ── Irreplaceability ──────────────────────────────────────────────────────────
300
+ log_step(9, "Compute irreplaceability (rarity-weighted richness)")
301
+ tryCatch({
302
+ irr <- eval_rare_richness_importance(p, s)
303
+ writeRaster(irr, file.path(output_dir, "irreplaceability.tif"), overwrite = TRUE)
304
+ log_info("Irreplaceability map written.")
305
+ }, error = function(e) {
306
+ log_error("Falha em irreplaceability: %s\nCausa provavel: falha na avaliacao de raridade ou permissoes de escrita.\nVerifique: objetos p e s e output_dir.\nSkill anterior: species-distribution-modeling", conditionMessage(e))
307
+ stop(e)
308
+ })
309
+
310
+ # ── Map visualisation ─────────────────────────────────────────────────────────
311
+ log_step(10, "Generate prioritization map visualisation")
312
+ tryCatch({
313
+ s_agg <- aggregate(s, fact = max(1, floor(nrow(s) / 300)))
314
+ df_sol <- as.data.frame(s_agg, xy = TRUE)
315
+ names(df_sol)[3] <- "selected"
316
+ df_sol$selected <- factor(df_sol$selected, levels = c(0, 1),
317
+ labels = c("Not selected", "Selected"))
318
+
319
+ p_map <- ggplot(df_sol, aes(x = x, y = y, fill = selected)) +
320
+ geom_raster() +
321
+ scale_fill_manual(values = c("Not selected" = "grey90", "Selected" = "#2166AC"),
322
+ na.value = "white") +
323
+ coord_equal() +
324
+ labs(x = "Easting", y = "Northing", fill = "",
325
+ title = sprintf("Conservation solution — %d PUs selected, %d/%d targets met",
326
+ n_selected, n_targets_met, n_feats)) +
327
+ theme_minimal(base_size = 10)
328
+
329
+ ggsave(file.path(output_dir, "prioritization_map.png"), p_map,
330
+ width = 9, height = 7, dpi = 150)
331
+ log_info("Prioritization map saved.")
332
+ }, error = function(e) {
333
+ log_warn("Nao foi possivel gerar o mapa de priorizacao: %s. Continuando sem o grafico.", conditionMessage(e))
334
+ })
335
+
336
+ log_info("Prioritization complete.")