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,274 @@
1
+ # ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ # Usage: Rscript connectivity_metrics.R <patches_shp> <output_dir> [dmax_m] [area_col]
5
+ #
6
+ # Computes graph-based landscape connectivity metrics (IIC, PC, dIIC, dPC,
7
+ # betweenness centrality) for a set of habitat patches.
8
+ #
9
+ # Arguments:
10
+ # patches_shp — Shapefile or GeoPackage of habitat patches (.shp or .gpkg)
11
+ # output_dir — Directory for output files
12
+ # dmax_m — Maximum dispersal distance in metres (default: 1000)
13
+ # area_col — Column name for patch area in patches layer (default: "area_ha")
14
+ #
15
+ # Outputs:
16
+ # patch_metrics.csv — IIC, PC, dIIC, dPC, BC per patch
17
+ # landscape_summary.csv — IIC, PC, number of components, largest component
18
+ # connectivity_graph.png — Network visualisation coloured by dPC
19
+
20
+ # ── Inline logger ─────────────────────────────────────────────────────────────
21
+ SKILL_NAME <- "landscape-connectivity"
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(sf))
31
+ suppressPackageStartupMessages(library(igraph))
32
+ suppressPackageStartupMessages(library(dplyr))
33
+ suppressPackageStartupMessages(library(ggplot2))
34
+
35
+ args <- commandArgs(trailingOnly = TRUE)
36
+ if (length(args) < 2) {
37
+ log_error("Argumentos insuficientes. Uso: Rscript connectivity_metrics.R <patches_shp> <output_dir> [dmax_m] [area_col]")
38
+ cat("Usage: Rscript connectivity_metrics.R <patches_shp> <output_dir>",
39
+ "[dmax_m] [area_col]\n")
40
+ quit(status = 1)
41
+ }
42
+
43
+ patches_path <- args[1]
44
+ output_dir <- args[2]
45
+ dmax_m <- if (length(args) >= 3) as.numeric(args[3]) else 1000
46
+ area_col <- if (length(args) >= 4) args[4] else "area_ha"
47
+
48
+ # ── Input precondition checks ────────────────────────────────────────────────
49
+ if (!file.exists(patches_path)) {
50
+ log_error("Input nao encontrado: %s\nCausa provavel: arquivo shapefile/GeoPackage nao existe ou caminho incorreto\nVerifique: se o arquivo .shp ou .gpkg existe no caminho especificado\nSkill anterior: [nenhuma — etapa inicial ou saida de processamento GIS]", patches_path)
51
+ stop("Missing patches file: ", patches_path)
52
+ }
53
+
54
+ log_decision("dmax_m", dmax_m,
55
+ "distancia maxima de dispersao em metros; define conectividade estrutural entre manchas")
56
+ log_decision("area_col", area_col,
57
+ "coluna de area das manchas; usada para calculo de IIC e PC ponderados por area")
58
+
59
+ dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
60
+
61
+ log_step(1, "Carregando camada de manchas de habitat")
62
+ # ── Load patches ────────────────────────────────────────────────────────────
63
+ patches <- tryCatch({
64
+ sf::st_read(patches_path, quiet = TRUE)
65
+ }, error = function(e) {
66
+ log_error("Falha ao ler shapefile de manchas: %s\nCausa provavel: arquivo corrompido ou CRS invalido\nVerifique: integridade do shapefile\nSkill anterior: [nenhuma]", conditionMessage(e))
67
+ stop(e)
68
+ })
69
+
70
+ if (!area_col %in% names(patches)) {
71
+ # Compute area from geometry if column missing
72
+ log_warn("Coluna '%s' nao encontrada; calculando area a partir da geometria", area_col)
73
+ patches[[area_col]] <- as.numeric(st_area(patches)) / 10000 # m² → ha
74
+ }
75
+
76
+ n <- nrow(patches)
77
+ A <- sum(patches[[area_col]]) # total landscape area proxy (sum of patch areas)
78
+ log_info("Manchas carregadas: %d. Area total das manchas: %.1f ha", n, A)
79
+
80
+ if (n < 3) {
81
+ log_error("Minimo de 3 manchas necessario para analise de conectividade. Encontradas: %d\nCausa provavel: dado de entrada com poucas feicoes\nVerifique: arquivo de manchas e filtros de area minima\nSkill anterior: [nenhuma]", n)
82
+ stop("At least 3 patches required for connectivity analysis. ",
83
+ "Found ", n, " patches.")
84
+ }
85
+
86
+ log_step(2, "Calculando distancias entre centroides das manchas")
87
+ # ── Compute pairwise distances between patch centroids ──────────────────────
88
+ centroids <- st_centroid(patches)
89
+ # Reproject to projected CRS if needed
90
+ if (st_is_longlat(centroids)) {
91
+ log_warn("Entrada em CRS geografico. Reprojetando para UTM para calculo de distancias")
92
+ utm_crs <- 32700 + round((mean(st_coordinates(centroids)[, 1]) + 180) / 6) + 1
93
+ centroids <- st_transform(centroids, crs = utm_crs)
94
+ log_info("CRS UTM usado: EPSG:%d", utm_crs)
95
+ }
96
+ coords <- st_coordinates(centroids)
97
+ dist_mat <- as.matrix(dist(coords)) # metres
98
+
99
+ log_step(3, "Construindo grafo de adjacencia binaria")
100
+ # ── Build adjacency matrix (binary, distance < dmax) ───────────────────────
101
+ adj_binary <- (dist_mat < dmax_m) * 1
102
+ diag(adj_binary) <- 0
103
+
104
+ g_binary <- graph_from_adjacency_matrix(adj_binary, mode = "undirected",
105
+ weighted = FALSE)
106
+ sp_hops <- distances(g_binary, algorithm = "bfs")
107
+
108
+ n_edges <- sum(adj_binary) / 2
109
+ log_info("Grafo construido: %d nos, %d arestas (dmax = %g m)", n, n_edges, dmax_m)
110
+ if (n_edges == 0) {
111
+ log_warn("Nenhuma mancha conectada dentro de dmax = %g m; aumente dmax_m ou verifique CRS", dmax_m)
112
+ }
113
+
114
+ log_step(4, "Calculando IIC — Integral Index of Connectivity")
115
+ # ── IIC (Integral Index of Connectivity) ────────────────────────────────────
116
+ areas <- patches[[area_col]]
117
+
118
+ compute_iic_internal <- function(areas_vec, sp_hops_mat) {
119
+ n <- length(areas_vec)
120
+ num <- 0
121
+ for (i in seq_len(n)) {
122
+ for (j in seq_len(n)) {
123
+ nij <- sp_hops_mat[i, j]
124
+ if (is.finite(nij)) {
125
+ num <- num + (areas_vec[i] * areas_vec[j]) / (1 + nij)
126
+ }
127
+ }
128
+ }
129
+ num / sum(areas_vec)^2
130
+ }
131
+
132
+ IIC_full <- compute_iic_internal(areas, sp_hops)
133
+ log_info("IIC (paisagem completa) = %.6f", IIC_full)
134
+
135
+ log_step(5, "Calculando dIIC por mancha (leave-one-out)")
136
+ # ── dIIC per patch ──────────────────────────────────────────────────────────
137
+ log_info("Calculando dIIC para cada mancha (pode levar um momento)...")
138
+ dIIC_vec <- numeric(n)
139
+ for (i in seq_len(n)) {
140
+ areas_i <- areas[-i]
141
+ adj_i <- adj_binary[-i, -i]
142
+ g_i <- graph_from_adjacency_matrix(adj_i, mode = "undirected")
143
+ sp_i <- distances(g_i, algorithm = "bfs")
144
+ IIC_i <- compute_iic_internal(areas_i, sp_i)
145
+ dIIC_vec[i] <- (IIC_full - IIC_i) / IIC_full * 100
146
+ }
147
+
148
+ log_step(6, "Calculando PC — Probability of Connectivity")
149
+ # ── PC (Probability of Connectivity) ────────────────────────────────────────
150
+ # Dispersal probability: p = exp(-d / dmax) (negative exponential kernel)
151
+ # dmax serves as mean dispersal distance parameter
152
+ p_mat <- exp(-dist_mat / dmax_m)
153
+ diag(p_mat) <- 0
154
+
155
+ log_decision("dispersal_kernel", "exponencial negativo exp(-d/dmax)",
156
+ "nucleo padrao para probabilidade de colonizacao; ajuste se dados de telemetria disponíveis")
157
+
158
+ # Build weighted graph for shortest path probabilities
159
+ p_weight <- ifelse(p_mat > 0.001, -log(p_mat), Inf)
160
+ diag(p_weight) <- 0
161
+ g_prob <- graph_from_adjacency_matrix(p_weight, mode = "undirected",
162
+ weighted = TRUE)
163
+ sp_prob_dist <- distances(g_prob, algorithm = "dijkstra")
164
+ pij_star <- exp(-sp_prob_dist) # convert back to probability
165
+
166
+ PC_full <- sum(outer(areas, areas) * pij_star) / sum(areas)^2
167
+ log_info("PC (paisagem completa) = %.6f", PC_full)
168
+
169
+ log_step(7, "Calculando dPC por mancha (leave-one-out)")
170
+ # ── dPC per patch ────────────────────────────────────────────────────────────
171
+ log_info("Calculando dPC para cada mancha...")
172
+ dPC_vec <- numeric(n)
173
+ for (i in seq_len(n)) {
174
+ areas_i <- areas[-i]
175
+ p_mat_i <- p_mat[-i, -i]
176
+ pw_i <- ifelse(p_mat_i > 0.001, -log(p_mat_i), Inf)
177
+ g_i <- graph_from_adjacency_matrix(pw_i, mode = "undirected",
178
+ weighted = TRUE)
179
+ sp_i <- distances(g_i, algorithm = "dijkstra")
180
+ pij_i <- exp(-sp_i)
181
+ PC_i <- sum(outer(areas_i, areas_i) * pij_i) / sum(areas_i)^2
182
+ dPC_vec[i] <- (PC_full - PC_i) / PC_full * 100
183
+ }
184
+
185
+ # ── Betweenness centrality ───────────────────────────────────────────────────
186
+ BC <- betweenness(g_binary, normalized = TRUE)
187
+
188
+ log_step(8, "Escrevendo resumo da paisagem e metricas por mancha")
189
+ # ── Landscape summary ────────────────────────────────────────────────────────
190
+ components_g <- components(g_binary)
191
+ largest_comp <- max(components_g$csize)
192
+
193
+ summary_df <- data.frame(
194
+ metric = c("IIC", "PC", "n_patches", "n_components",
195
+ "largest_component_size", "dmax_m"),
196
+ value = c(IIC_full, PC_full, n, components_g$no,
197
+ largest_comp, dmax_m)
198
+ )
199
+ sum_path <- file.path(output_dir, "landscape_summary.csv")
200
+ write.csv(summary_df, sum_path, row.names = FALSE)
201
+ log_info("Resumo da paisagem gravado: %s", sum_path)
202
+
203
+ if (components_g$no > n / 2) {
204
+ log_warn("Paisagem altamente fragmentada: %d componentes para %d manchas; considere aumentar dmax_m",
205
+ components_g$no, n)
206
+ }
207
+
208
+ # ── Patch metrics ────────────────────────────────────────────────────────────
209
+ patch_id <- if ("id" %in% names(patches)) patches$id else seq_len(n)
210
+ patch_metrics <- data.frame(
211
+ patch_id = patch_id,
212
+ area_ha = areas,
213
+ dIIC_pct = round(dIIC_vec, 4),
214
+ dPC_pct = round(dPC_vec, 4),
215
+ BC_norm = round(BC, 4),
216
+ component = components_g$membership
217
+ )
218
+ patch_metrics <- patch_metrics[order(-patch_metrics$dPC_pct), ]
219
+
220
+ patch_path <- file.path(output_dir, "patch_metrics.csv")
221
+ write.csv(patch_metrics, patch_path, row.names = FALSE)
222
+ log_info("Metricas por mancha gravadas: %s (%d manchas)", patch_path, n)
223
+
224
+ # Report top patches
225
+ log_info("Top 5 manchas por dPC:")
226
+ top5 <- head(patch_metrics[, c("patch_id", "area_ha", "dPC_pct", "BC_norm")], 5)
227
+ for (r in seq_len(nrow(top5))) {
228
+ log_info(" patch_id=%s | area=%.1f ha | dPC=%.2f%% | BC=%.4f",
229
+ top5$patch_id[r], top5$area_ha[r], top5$dPC_pct[r], top5$BC_norm[r])
230
+ }
231
+
232
+ log_step(9, "Gerando visualizacao do grafo de conectividade")
233
+ # ── Network visualisation ────────────────────────────────────────────────────
234
+ V(g_binary)$dPC <- dPC_vec
235
+ V(g_binary)$area <- areas
236
+ layout_coords <- coords[, 1:2]
237
+
238
+ patch_df <- data.frame(
239
+ x = coords[, 1],
240
+ y = coords[, 2],
241
+ dPC = dPC_vec,
242
+ area = areas / max(areas) * 3 + 0.5 # size scaled
243
+ )
244
+
245
+ edges_df <- as.data.frame(get.edgelist(g_binary))
246
+ edges_df$x_from <- coords[as.integer(edges_df$V1), 1]
247
+ edges_df$y_from <- coords[as.integer(edges_df$V1), 2]
248
+ edges_df$x_to <- coords[as.integer(edges_df$V2), 1]
249
+ edges_df$y_to <- coords[as.integer(edges_df$V2), 2]
250
+
251
+ p <- ggplot() +
252
+ geom_segment(data = edges_df,
253
+ aes(x = x_from, y = y_from, xend = x_to, yend = y_to),
254
+ colour = "grey60", linewidth = 0.5, alpha = 0.7) +
255
+ geom_point(data = patch_df,
256
+ aes(x = x, y = y, size = area, fill = dPC),
257
+ shape = 21, colour = "grey30") +
258
+ scale_fill_viridis_c(option = "plasma", name = "dPC (%)") +
259
+ scale_size_continuous(range = c(2, 8), guide = "none") +
260
+ labs(x = "Easting", y = "Northing",
261
+ title = sprintf("Connectivity graph (dmax = %g m, IIC = %.4f)",
262
+ dmax_m, IIC_full)) +
263
+ theme_minimal(base_size = 10) +
264
+ coord_equal()
265
+
266
+ plot_path <- file.path(output_dir, "connectivity_graph.png")
267
+ tryCatch({
268
+ ggsave(plot_path, p, width = 8, height = 7, dpi = 150)
269
+ log_info("Visualizacao do grafo salva: %s", plot_path)
270
+ }, error = function(e) {
271
+ log_warn("Nao foi possivel salvar visualizacao do grafo: %s", conditionMessage(e))
272
+ })
273
+
274
+ log_info("Analise de conectividade concluida")
@@ -0,0 +1,239 @@
1
+ # ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ # Usage: Rscript resistance_surface.R <landcover_tif> <resistance_csv> <output_dir>
5
+ # [dem_tif] [road_shp]
6
+ #
7
+ # Constructs a resistance surface from a land cover raster and resistance
8
+ # lookup table. Optionally adds slope-based and road-proximity penalties.
9
+ #
10
+ # Arguments:
11
+ # landcover_tif — Land cover raster (.tif) with integer class codes
12
+ # resistance_csv — CSV with columns: lc_code (int), resistance (num), description (char)
13
+ # output_dir — Directory for output files
14
+ # dem_tif — (optional) DEM raster for slope penalty
15
+ # road_shp — (optional) Road vector layer for proximity penalty
16
+ #
17
+ # Outputs:
18
+ # resistance_lc.tif — Land cover resistance only
19
+ # resistance_combined.tif — Combined resistance (lc + optional slope + road)
20
+ # resistance_stats.csv — Summary statistics of resistance surface
21
+ # resistance_map.png — Visualisation of combined resistance
22
+
23
+ # ── Inline logger ─────────────────────────────────────────────────────────────
24
+ SKILL_NAME <- "landscape-connectivity"
25
+ .log_ts <- function() format(Sys.time(), "[%Y-%m-%d %H:%M:%S]")
26
+ log_info <- function(...) message(.log_ts(), " [INFO] ", sprintf(...))
27
+ log_warn <- function(...) message(.log_ts(), " [WARN] ", sprintf(...))
28
+ log_error<- function(...) message(.log_ts(), " [ERROR] ", sprintf(...))
29
+ log_step <- function(n, d) log_info("-- STEP %d: %s", n, d)
30
+ log_decision <- function(v, val, why) log_info("DECISION | %s = %s | %s", v, val, why)
31
+ dir.create("logs", recursive=TRUE, showWarnings=FALSE)
32
+
33
+ suppressPackageStartupMessages(library(terra))
34
+ suppressPackageStartupMessages(library(sf))
35
+ suppressPackageStartupMessages(library(dplyr))
36
+ suppressPackageStartupMessages(library(ggplot2))
37
+
38
+ args <- commandArgs(trailingOnly = TRUE)
39
+ if (length(args) < 3) {
40
+ log_error("Argumentos insuficientes. Uso: Rscript resistance_surface.R <landcover_tif> <resistance_csv> <output_dir> [dem_tif] [road_shp]")
41
+ cat("Usage: Rscript resistance_surface.R <landcover_tif> <resistance_csv>",
42
+ "<output_dir> [dem_tif] [road_shp]\n")
43
+ quit(status = 1)
44
+ }
45
+
46
+ lc_path <- args[1]
47
+ res_csv <- args[2]
48
+ output_dir <- args[3]
49
+ dem_path <- if (length(args) >= 4 && args[4] != "NA") args[4] else NULL
50
+ road_path <- if (length(args) >= 5 && args[5] != "NA") args[5] else NULL
51
+
52
+ # ── Input precondition checks ────────────────────────────────────────────────
53
+ if (!file.exists(lc_path)) {
54
+ log_error("Input nao encontrado: %s\nCausa provavel: raster de cobertura de terra nao existe\nVerifique: caminho e existencia do arquivo .tif\nSkill anterior: [nenhuma — entrada do usuario]", lc_path)
55
+ stop("Missing landcover_tif: ", lc_path)
56
+ }
57
+ if (!file.exists(res_csv)) {
58
+ log_error("Input nao encontrado: %s\nCausa provavel: CSV de resistencia nao existe\nVerifique: se o arquivo contem colunas lc_code e resistance\nSkill anterior: [nenhuma — entrada do usuario]", res_csv)
59
+ stop("Missing resistance_csv: ", res_csv)
60
+ }
61
+ if (!is.null(dem_path) && !file.exists(dem_path)) {
62
+ log_error("DEM opcional nao encontrado: %s\nCausa provavel: caminho incorreto para DEM\nVerifique: existencia do arquivo .tif do DEM\nSkill anterior: [nenhuma]", dem_path)
63
+ stop("Missing dem_tif: ", dem_path)
64
+ }
65
+ if (!is.null(road_path) && !file.exists(road_path)) {
66
+ log_error("Shapefile de estradas opcional nao encontrado: %s\nCausa provavel: caminho incorreto\nVerifique: existencia do shapefile de estradas\nSkill anterior: [nenhuma]", road_path)
67
+ stop("Missing road_shp: ", road_path)
68
+ }
69
+
70
+ log_decision("dem_path", ifelse(is.null(dem_path), "NULL", dem_path),
71
+ "DEM incluido na penalidade de declividade; NULL = sem penalidade de relevo")
72
+ log_decision("road_path", ifelse(is.null(road_path), "NULL", road_path),
73
+ "shapefile de estradas para penalidade de proximidade; NULL = sem penalidade viaria")
74
+
75
+ dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
76
+
77
+ log_step(1, "Carregando raster de cobertura de terra e tabela de resistencia")
78
+ # ── Load inputs ──────────────────────────────────────────────────────────────
79
+ lc <- tryCatch({
80
+ rast(lc_path)
81
+ }, error = function(e) {
82
+ log_error("Falha ao carregar raster de cobertura de terra: %s\nCausa provavel: arquivo .tif corrompido ou formato invalido\nVerifique: integridade do raster\nSkill anterior: [nenhuma]", conditionMessage(e))
83
+ stop(e)
84
+ })
85
+ rt <- tryCatch({
86
+ read.csv(res_csv)
87
+ }, error = function(e) {
88
+ log_error("Falha ao ler CSV de resistencia: %s\nCausa provavel: formato incorreto\nVerifique: colunas lc_code e resistance\nSkill anterior: [nenhuma]", conditionMessage(e))
89
+ stop(e)
90
+ })
91
+
92
+ required_cols <- c("lc_code", "resistance")
93
+ if (!all(required_cols %in% names(rt))) {
94
+ log_error("CSV de resistencia deve conter colunas: lc_code, resistance. Encontradas: %s\nCausa provavel: cabecalho do CSV incorreto\nVerifique: estrutura do arquivo resistance_csv\nSkill anterior: [nenhuma]",
95
+ paste(names(rt), collapse = ", "))
96
+ stop("resistance_csv must contain columns: lc_code, resistance. Found: ",
97
+ paste(names(rt), collapse = ", "))
98
+ }
99
+
100
+ log_info("Raster de cobertura: %d linhas x %d colunas, CRS: %s",
101
+ nrow(lc), ncol(lc), crs(lc, describe = TRUE)$name)
102
+ log_info("Tabela de resistencia: %d classes", nrow(rt))
103
+
104
+ log_step(2, "Reclassificando cobertura de terra para resistencia")
105
+ # ── Reclassify land cover to resistance ──────────────────────────────────────
106
+ rcl_mat <- as.matrix(rt %>%
107
+ arrange(lc_code) %>%
108
+ mutate(from = lc_code - 0.5, to = lc_code + 0.5) %>%
109
+ select(from, to, resistance))
110
+
111
+ res_lc <- tryCatch({
112
+ classify(lc, rcl_mat, include.lowest = TRUE, right = FALSE)
113
+ }, error = function(e) {
114
+ log_error("Falha em reclassificacao: %s\nCausa provavel: codigos de classe no raster fora do intervalo da tabela\nVerifique: consistencia entre lc_code e valores do raster\nSkill anterior: [nenhuma]", conditionMessage(e))
115
+ stop(e)
116
+ })
117
+ names(res_lc) <- "resistance"
118
+
119
+ # Check for unmatched codes
120
+ lc_vals <- unique(values(lc, na.rm = TRUE))
121
+ unmatched <- setdiff(lc_vals, rt$lc_code)
122
+ if (length(unmatched) > 0) {
123
+ log_warn("%d codigos de cobertura de terra sem atribuicao de resistencia: %s",
124
+ length(unmatched), paste(unmatched, collapse = ", "))
125
+ }
126
+
127
+ lc_path_out <- file.path(output_dir, "resistance_lc.tif")
128
+ writeRaster(res_lc, lc_path_out, overwrite = TRUE)
129
+ log_info("Resistencia LC gravada: %s", lc_path_out)
130
+
131
+ log_step(3, "Adicionando penalidades opcionais (declividade, estradas)")
132
+ # ── Optional: slope penalty ──────────────────────────────────────────────────
133
+ if (!is.null(dem_path)) {
134
+ log_info("Adicionando resistencia baseada em declividade...")
135
+ tryCatch({
136
+ dem <- rast(dem_path)
137
+ dem_proj <- project(dem, lc, method = "bilinear")
138
+ slope_deg <- terrain(dem_proj, v = "slope", unit = "degrees")
139
+ # Exponential cost: doubles every ~10° of slope
140
+ slope_res <- exp(slope_deg / 15)
141
+ slope_res <- slope_res / global(slope_res, "min", na.rm = TRUE)[[1]]
142
+ names(slope_res) <- "slope_resistance"
143
+ writeRaster(slope_res, file.path(output_dir, "slope_resistance.tif"),
144
+ overwrite = TRUE)
145
+ log_info("Resistencia de declividade gravada")
146
+ log_decision("slope_decay_param", 15,
147
+ "parametro de decaimento exponencial; resistencia dobra a cada 15 graus de declividade")
148
+ }, error = function(e) {
149
+ log_error("Falha ao processar DEM para declividade: %s\nCausa provavel: DEM com CRS incompativel ou valores invalidos\nVerifique: CRS e extensao do DEM\nSkill anterior: [nenhuma]", conditionMessage(e))
150
+ stop(e)
151
+ })
152
+ } else {
153
+ slope_res <- NULL
154
+ }
155
+
156
+ # ── Optional: road proximity penalty ─────────────────────────────────────────
157
+ if (!is.null(road_path)) {
158
+ log_info("Adicionando resistencia de proximidade a estradas...")
159
+ tryCatch({
160
+ roads <- st_read(road_path, quiet = TRUE)
161
+ roads_v <- vect(roads)
162
+ # Compute distance to nearest road
163
+ road_dist <- distance(res_lc, roads_v)
164
+ road_dist <- project(road_dist, lc, method = "bilinear")
165
+ # Resistance peaks at road (dist=0) and decays with distance
166
+ # max_penalty = 10× at road edge, decays to 1 at 500 m
167
+ road_res <- pmax(1, 10 * exp(-road_dist / 200))
168
+ names(road_res) <- "road_resistance"
169
+ writeRaster(road_res, file.path(output_dir, "road_resistance.tif"),
170
+ overwrite = TRUE)
171
+ log_info("Resistencia de estradas gravada")
172
+ log_decision("road_max_penalty", 10,
173
+ "penalidade maxima na beira da estrada (10x); decai a 1 a 500 m")
174
+ }, error = function(e) {
175
+ log_error("Falha ao processar shapefile de estradas: %s\nCausa provavel: shapefile invalido ou CRS incompativel\nVerifique: integridade do shapefile de estradas\nSkill anterior: [nenhuma]", conditionMessage(e))
176
+ stop(e)
177
+ })
178
+ } else {
179
+ road_res <- NULL
180
+ }
181
+
182
+ log_step(4, "Combinando camadas de resistencia e normalizando")
183
+ # ── Combine resistance layers ─────────────────────────────────────────────────
184
+ combined <- res_lc
185
+ if (!is.null(slope_res)) combined <- combined * slope_res
186
+ if (!is.null(road_res)) combined <- combined * road_res
187
+
188
+ # Cap at maximum to avoid instability
189
+ max_cap <- 1000
190
+ combined[combined > max_cap] <- max_cap
191
+ log_decision("max_cap_resistance", max_cap,
192
+ "resistencia maxima para evitar instabilidade numerica nos algoritmos de menor custo")
193
+
194
+ # Rescale so minimum = 1
195
+ min_val <- global(combined, "min", na.rm = TRUE)[[1]]
196
+ combined <- combined / min_val
197
+ combined_path <- file.path(output_dir, "resistance_combined.tif")
198
+ writeRaster(combined, combined_path, overwrite = TRUE)
199
+ log_info("Resistencia combinada gravada: %s", combined_path)
200
+
201
+ log_step(5, "Calculando estatisticas da superficie de resistencia")
202
+ # ── Statistics ────────────────────────────────────────────────────────────────
203
+ vals <- values(combined, na.rm = TRUE)
204
+ stats_df <- data.frame(
205
+ statistic = c("min", "q25", "median", "mean", "q75", "q95", "max"),
206
+ value = round(quantile(vals, c(0, 0.25, 0.5, NA, 0.75, 0.95, 1),
207
+ na.rm = TRUE), 3)
208
+ )
209
+ stats_df$value[4] <- round(mean(vals, na.rm = TRUE), 3)
210
+ stats_path <- file.path(output_dir, "resistance_stats.csv")
211
+ write.csv(stats_df, stats_path, row.names = FALSE)
212
+ log_info("Estatisticas da superficie de resistencia:")
213
+ for (r in seq_len(nrow(stats_df))) {
214
+ log_info(" %s = %.3f", stats_df$statistic[r], stats_df$value[r])
215
+ }
216
+
217
+ log_step(6, "Gerando visualizacao do mapa de resistencia")
218
+ # ── Visualisation ─────────────────────────────────────────────────────────────
219
+ tryCatch({
220
+ plot_r <- aggregate(combined, fact = max(1, floor(nrow(combined) / 500)))
221
+ plot_df <- as.data.frame(plot_r, xy = TRUE)
222
+ names(plot_df)[3] <- "resistance"
223
+
224
+ p <- ggplot(plot_df, aes(x = x, y = y, fill = log1p(resistance))) +
225
+ geom_raster() +
226
+ scale_fill_viridis_c(option = "magma", name = "log(resistance + 1)") +
227
+ coord_equal() +
228
+ labs(x = "Easting", y = "Northing",
229
+ title = "Combined resistance surface") +
230
+ theme_minimal(base_size = 10)
231
+
232
+ map_path <- file.path(output_dir, "resistance_map.png")
233
+ ggsave(map_path, p, width = 8, height = 7, dpi = 150)
234
+ log_info("Mapa de resistencia salvo: %s", map_path)
235
+ }, error = function(e) {
236
+ log_warn("Nao foi possivel gerar mapa de resistencia: %s", conditionMessage(e))
237
+ })
238
+
239
+ log_info("Construcao da superficie de resistencia concluida")
@@ -0,0 +1,131 @@
1
+ ---
2
+ name: model-validation-and-uncertainty
3
+ description: "Validates predictive models and quantifies uncertainty including AUC/TSS metrics, calibration, extrapolation risk (MOP/MESS/ExDet), and ensemble uncertainty maps. Use this skill when the user needs model performance evaluation, ROC curves, cross-validation results, calibration curves, overfitting diagnostics, prediction intervals, bootstrap uncertainty, sensitivity/specificity assessment, or extrapolation risk analysis."
4
+ skill_version: 1.0.0
5
+ ---
6
+
7
+ # Skill: model-validation-and-uncertainty
8
+
9
+ **Domain:** Metrics · Calibration · Sensitivity · External validation · Uncertainty
10
+ **Phase:** 2 — Modeling
11
+ **Used by:** run-sdm-study, assess-ecological-impact, analyze-community-structure, build-fire-risk-map, run-occupancy-analysis
12
+
13
+ ---
14
+
15
+ ## Purpose
16
+
17
+ Guides the agent through rigorous evaluation of any fitted model: computing performance metrics on held-out data, assessing calibration, running sensitivity analyses, performing external validation, and quantifying and visualising prediction uncertainty.
18
+
19
+ ---
20
+
21
+ ## When to Invoke
22
+
23
+ - After any model is fitted, before results are reported
24
+ - When the user asks about model performance, reliability, or uncertainty
25
+ - When preparing results for publication or decision-making
26
+
27
+ ---
28
+
29
+ ## Inputs
30
+
31
+ | Input | Format | Required |
32
+ |-------|--------|----------|
33
+ | Fitted model object | RData, pkl, ONNX | Yes |
34
+ | Validation dataset (independent) | CSV | Yes |
35
+ | Training/CV predictions | CSV | Yes |
36
+ | Prediction surface/map | GeoTIFF | Conditional |
37
+
38
+ ---
39
+
40
+ ## Outputs
41
+
42
+ | Output | Description |
43
+ |--------|-------------|
44
+ | `performance_metrics.csv` | Full metric table (train, CV, test) |
45
+ | `calibration_plot.png` | Observed vs predicted calibration curve |
46
+ | `roc_curve.png` | ROC curve with AUC (for classifiers) |
47
+ | `sensitivity_report.md` | Effect of parameter/predictor perturbation |
48
+ | `uncertainty_map.tif` | Spatial uncertainty (SD across ensemble or bootstrap) |
49
+ | `validation_report.md` | Comprehensive validation narrative |
50
+
51
+ ---
52
+
53
+ ## Steps
54
+
55
+ ### 1. Select Appropriate Metrics
56
+
57
+ | Task | Primary metrics | Secondary |
58
+ |------|----------------|-----------|
59
+ | Binary classification (SDM) | AUC-ROC, TSS, Boyce index | Sensitivity, specificity |
60
+ | Regression (abundance, biomass) | RMSE, MAE, R² | Bias, MAPE |
61
+ | Occupancy | AUC, WAIC, posterior predictive checks | |
62
+ | Community ordination | Stress (NMDS), R² (RDA/CCA) | Procrustes |
63
+ | Count / Poisson | RMSE, Pseudo-R², dispersion | |
64
+
65
+ ### 2. Compute Metrics on Train, CV, and Test Sets
66
+ - Report all three to diagnose overfitting (train >> CV/test gap)
67
+ - Report metric mean ± SD across CV folds
68
+
69
+ ### 3. Assess Calibration
70
+ - For classifiers: plot mean predicted probability vs. observed occurrence rate across bins
71
+ - For regression: plot predicted vs. observed scatter
72
+ - Compute calibration slope and intercept; values near 1 and 0 are ideal
73
+
74
+ ### 4. Threshold Selection (for binary predictions)
75
+ - Maximise TSS (Youden's J)
76
+ - Maximise Sensitivity + Specificity
77
+ - Fixed prevalence-based threshold
78
+ - Document chosen threshold and rationale
79
+
80
+ ### 5. Variable Importance and Response Curves
81
+ - Compute permutation importance for each predictor
82
+ - Plot partial dependence / marginal effect curves for top predictors
83
+ - Flag predictors with response curves showing ecologically implausible patterns
84
+
85
+ ### 6. Sensitivity Analysis
86
+ - Perturb each hyperparameter by ±10%; measure effect on primary metric
87
+ - Remove each predictor in turn; measure effect (jackknife importance)
88
+ - Assess sensitivity to background/pseudo-absence sampling strategy (for SDMs)
89
+
90
+ ### 7. External Validation
91
+ - Apply model to an independent dataset (different time period, different region)
92
+ - Report metric degradation; quantify transferability
93
+ - Flag models with poor transferability before applying to novel conditions
94
+
95
+ ### 8. Uncertainty Quantification
96
+ - Ensemble SD: standard deviation across ensemble members or bootstrap replicates
97
+ - Bayesian credible intervals: for Bayesian models, report posterior predictive intervals
98
+ - Map uncertainty spatially where relevant
99
+ - Report regions of high uncertainty explicitly
100
+
101
+ ---
102
+
103
+ ## Key Decisions to Document
104
+
105
+ - Primary and secondary metrics chosen and rationale
106
+ - Threshold selection method
107
+ - External validation dataset description
108
+ - Uncertainty quantification method
109
+
110
+ ---
111
+
112
+ ## Tools and Libraries
113
+
114
+ **R:** `ROCR`, `PresenceAbsence`, `dismo`, `gbm`, `boot`, `brms`
115
+ **Python:** `sklearn.metrics`, `scikit-learn`, `shap`, `pysdm`
116
+
117
+ ---
118
+
119
+ ## Resources
120
+
121
+ - `resources/metric-selection-guide.md` — which metrics for which task
122
+ - `resources/threshold-selection-guide.md` — threshold methods compared
123
+ - `examples/` — calibration plot and uncertainty map examples
124
+
125
+ ---
126
+
127
+ ## Notes
128
+
129
+ - Never report only training performance
130
+ - Boyce index is preferred over AUC for presence-only SDMs
131
+ - For small samples, report bootstrapped CIs around all metrics
@@ -0,0 +1,30 @@
1
+ # Example Invocation Prompts — model-validation-and-uncertainty
2
+
3
+ ## Full Validation Suite
4
+
5
+ ```
6
+ Load skill: model-validation-and-uncertainty
7
+ Task: Validate the SDM ensemble for Chrysocyon brachyurus.
8
+ Files:
9
+ - models/ensemble_predictions.csv (columns: site_id, observed, predicted_prob)
10
+ - models/cv_fold_predictions.csv (columns: fold, observed, predicted_prob)
11
+ - outputs/suitability_ensemble.tif
12
+ - outputs/suitability_sd.tif (uncertainty)
13
+
14
+ Run:
15
+ 1. AUC-ROC and TSS on CV folds and independent test set.
16
+ 2. Boyce index on test set.
17
+ 3. Calibration plot (10 bins).
18
+ 4. Threshold selection: MaxTSS and P10.
19
+ 5. Report: performance_metrics.csv, calibration_plot.png, validation_report.md
20
+ ```
21
+
22
+ ## Calibration Check Only
23
+
24
+ ```
25
+ Load skill: model-validation-and-uncertainty
26
+ Task: Calibration assessment only.
27
+ Predictions: outputs/glm_predictions.csv (obs: 0/1, pred: probability 0-1).
28
+ Generate calibration plot with 10 bins. Report calibration slope and intercept.
29
+ A well-calibrated model should have slope ≈ 1 and intercept ≈ 0.
30
+ ```