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,141 @@
1
+ ---
2
+ resource_id: resistance-surface-guide
3
+ skill_id: landscape-connectivity
4
+ ---
5
+
6
+ # Resistance Surface Construction Guide
7
+
8
+ ## Conceptual Framework
9
+
10
+ A **resistance surface** (also called a cost surface) assigns a movement cost to each raster cell based on the permeability of that land cover or habitat feature to the focal species. Least-cost path and Circuitscape analyses use resistance surfaces to model functionally realistic movement corridors.
11
+
12
+ **Resistance ≠ inverse of habitat suitability.** Resistance should reflect movement cost, not habitat preference. A species may move through unsuitable habitat quickly (low resistance) or slowly (high resistance).
13
+
14
+ ---
15
+
16
+ ## Resistance Assignment Methods
17
+
18
+ | Method | Data requirement | Strengths | Weaknesses |
19
+ |--------|-----------------|-----------|------------|
20
+ | **Expert opinion** | Land cover map + expert | Fast, applicable when no telemetry | Subjective; high uncertainty |
21
+ | **Inverse SDM suitability** | Occurrence + env layers | Empirically grounded | Conflates habitat use with movement |
22
+ | **Empirical movement model** | GPS telemetry, step-selection | Most ecologically valid | Requires large GPS datasets |
23
+ | **Gene flow (IBR)** | Population genetics + landscape | Tests functional connectivity | Requires population genetics |
24
+
25
+ ---
26
+
27
+ ## Resistance Value Tables by Land Cover
28
+
29
+ ### Example: Forest carnivore (jaguar-like)
30
+
31
+ | Land cover class | Resistance | Rationale |
32
+ |-----------------|-----------|-----------|
33
+ | Dense tropical forest | 1 (minimum) | Preferred habitat |
34
+ | Secondary forest | 3 | Suboptimal but permeable |
35
+ | Cerrado/savanna | 5 | Used for movement |
36
+ | Shrubland | 8 | Low canopy; avoidance |
37
+ | Pasture (low density) | 20 | Crosses at night |
38
+ | Pasture (high density cattle) | 40 | Crossing risk; human presence |
39
+ | Roads (unpaved) | 30 | Crossing risk |
40
+ | Roads (paved, 2-lane) | 80 | High mortality risk |
41
+ | Highways (4-lane) | 200 | Effective barrier |
42
+ | Urban | 500 | Near-absolute barrier |
43
+ | Water (rivers > 200 m) | 100 | Species-dependent |
44
+
45
+ **Note:** Resistance values are relative. Scale (1–10 vs 1–1000) does not affect relative results but affects absolute cost distances.
46
+
47
+ ---
48
+
49
+ ## Building a Resistance Raster in R
50
+
51
+ ```r
52
+ # Usage: source this block or call from resistance_surface.R
53
+ suppressPackageStartupMessages(library(terra))
54
+ suppressPackageStartupMessages(library(dplyr))
55
+
56
+ # Inputs:
57
+ # lc_raster: SpatRaster with integer land cover codes
58
+ # resistance_table: data.frame with columns 'lc_code' and 'resistance'
59
+
60
+ build_resistance_raster <- function(lc_raster, resistance_table) {
61
+ # Create reclassification matrix: from, to, becomes
62
+ rcl_mat <- as.matrix(
63
+ resistance_table %>%
64
+ arrange(lc_code) %>%
65
+ mutate(from = lc_code - 0.5, to = lc_code + 0.5) %>%
66
+ select(from, to, resistance)
67
+ )
68
+ res_raster <- classify(lc_raster, rcl_mat, include.lowest = TRUE)
69
+ return(res_raster)
70
+ }
71
+
72
+ # Example usage
73
+ lc <- rast("data/landcover_2024.tif")
74
+ rt <- read.csv("data/resistance_values.csv") # columns: lc_code, resistance
75
+ resistance <- build_resistance_raster(lc, rt)
76
+ writeRaster(resistance, "outputs/resistance_surface.tif", overwrite = TRUE)
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Multi-Layer Resistance (Additive/Multiplicative)
82
+
83
+ When multiple landscape features contribute to resistance (land cover + road proximity + slope):
84
+
85
+ ```r
86
+ suppressPackageStartupMessages(library(terra))
87
+
88
+ lc_res <- rast("outputs/lc_resistance.tif")
89
+ road_res <- rast("outputs/road_resistance.tif") # distance-weighted road effect
90
+ slope_res <- rast("outputs/slope_resistance.tif") # penalise steep terrain
91
+
92
+ # Multiplicative combination (common for Circuitscape)
93
+ combined <- lc_res * road_res * slope_res
94
+ combined <- combined / global(combined, "min")[[1]] # rescale min to 1
95
+
96
+ writeRaster(combined, "outputs/resistance_combined.tif", overwrite = TRUE)
97
+ ```
98
+
99
+ **Caution:** Multiplicative combinations can generate extreme values. Cap at a maximum (e.g., 1000) to avoid numerical instability in Circuitscape.
100
+
101
+ ---
102
+
103
+ ## Slope-Based Resistance (Terrain)
104
+
105
+ ```r
106
+ dem <- rast("data/dem_30m.tif")
107
+ slope_deg <- terrain(dem, v = "slope", unit = "degrees")
108
+
109
+ # Non-linear cost: steep slopes exponentially costly
110
+ slope_res <- exp(slope_deg / 15) # doubles every ~10°
111
+ slope_res <- slope_res / global(slope_res, "min")[[1]]
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Validation Approaches
117
+
118
+ | Method | What it tests |
119
+ |--------|--------------|
120
+ | Least-cost path overlay with GPS tracks | Are predicted corridors used by GPS-tracked individuals? |
121
+ | Isolation by resistance (IBR) vs IBD | Does resistance predict genetic differentiation better than Euclidean distance? |
122
+ | Leave-one-out cross-validation | Do alternative resistance surfaces predict held-out movement events? |
123
+ | Expert review | Are corridor predictions ecologically sensible? |
124
+
125
+ ---
126
+
127
+ ## Pitfalls
128
+
129
+ - **Uniform resistance for entire land cover class:** Resistance often varies within a class (e.g., pasture near roads vs interior pasture). Consider adding a road-proximity effect layer.
130
+ - **Resistance values assigned without uncertainty bounds:** Always report the sensitivity of corridor predictions to ±50% changes in key resistance values.
131
+ - **Ignoring patch boundaries in cost surface:** Resistance inside source/destination patches should be set to minimum (1) to avoid spuriously increasing corridor cost.
132
+ - **Using suitability maps directly as inverse resistance:** SDM suitability predicts habitat use, not movement cost. A species may traverse unsuitable habitat rapidly, warranting low resistance.
133
+ - **Resolution mismatch:** Resistance raster cell size should be ≤ 1/10 of the species' mean step length to capture fine-scale barriers.
134
+
135
+ ---
136
+
137
+ ## References
138
+
139
+ - Zeller, K.A., McGarigal, K. & Whiteley, A.R. (2012). Estimating landscape resistance to movement: a review. *Landscape Ecology*, 27(6), 777–797. DOI: 10.1007/s10980-012-9737-0
140
+ - Pullinger, M.G. & Johnson, C.J. (2010). Maintaining or restoring connectivity of modified landscapes. *Biological Conservation*, 143(6), 1483–1493. DOI: 10.1016/j.biocon.2010.03.019
141
+ - McRae, B.H. & Beier, P. (2007). Circuit theory predicts gene flow in plant and animal populations. *PNAS*, 104(50), 19885–19890. DOI: 10.1073/pnas.0706568104
@@ -0,0 +1,387 @@
1
+ # ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ """
5
+ Graph-based landscape connectivity analysis using networkx and scikit-image.
6
+
7
+ Usage:
8
+ python connectivity_analysis.py <patches_csv> <output_dir>
9
+ [--dmax 1000] [--area_col area_ha]
10
+
11
+ Inputs:
12
+ patches_csv — CSV with columns: patch_id, x (centroid easting),
13
+ y (centroid northing), <area_col>
14
+ (produced by GIS export of patch centroids)
15
+
16
+ Outputs:
17
+ patch_metrics.csv — IIC, dIIC, PC, dPC, betweenness centrality
18
+ landscape_summary.csv — Landscape-level metrics
19
+ connectivity_graph.png — Network plot coloured by dPC
20
+ least_cost_paths.csv — Pairwise effective resistance and path length
21
+
22
+ Notes:
23
+ For resistance-surface least-cost paths, provide a resistance raster via
24
+ --resistance_tif. Requires rasterio and scikit-image.
25
+ """
26
+
27
+ import logging
28
+ import sys
29
+ import csv
30
+ import math
31
+ import argparse
32
+ import warnings
33
+ from datetime import datetime
34
+ from pathlib import Path
35
+ from itertools import combinations
36
+
37
+ SKILL_NAME = "landscape-connectivity"
38
+ _LOG_DIR = Path("logs")
39
+ _LOG_DIR.mkdir(parents=True, exist_ok=True)
40
+ _log_file = _LOG_DIR / f"skill_{SKILL_NAME}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
41
+ logging.basicConfig(
42
+ level=logging.INFO,
43
+ format="[%(asctime)s] [%(levelname)s] [" + SKILL_NAME + "] %(message)s",
44
+ datefmt="%Y-%m-%d %H:%M:%S",
45
+ handlers=[
46
+ logging.StreamHandler(sys.stdout),
47
+ logging.FileHandler(_log_file, encoding="utf-8"),
48
+ ],
49
+ )
50
+ logger = logging.getLogger(SKILL_NAME)
51
+
52
+ def log_step(n: int, desc: str) -> None:
53
+ logger.info("-- STEP %d: %s", n, desc)
54
+
55
+ def log_decision(var: str, val, why: str) -> None:
56
+ logger.info("DECISION | %s = %s | %s", var, val, why)
57
+
58
+ import numpy as np
59
+
60
+ try:
61
+ import networkx as nx
62
+ except ImportError:
63
+ logger.error("networkx not installed. Run: pip install networkx")
64
+ sys.exit(1)
65
+
66
+
67
+ def parse_args():
68
+ parser = argparse.ArgumentParser(
69
+ description="Graph-based landscape connectivity metrics"
70
+ )
71
+ parser.add_argument("patches_csv", help="CSV of patch centroids (id, x, y, area_ha)")
72
+ parser.add_argument("output_dir", help="Output directory")
73
+ parser.add_argument("--dmax", type=float, default=1000.0,
74
+ help="Max dispersal distance in map units (default: 1000)")
75
+ parser.add_argument("--area_col", default="area_ha",
76
+ help="Column name for patch area (default: area_ha)")
77
+ parser.add_argument("--resistance_tif", default=None,
78
+ help="Optional resistance raster for least-cost paths")
79
+ return parser.parse_args()
80
+
81
+
82
+ def load_patches(csv_path: Path, area_col: str) -> list[dict]:
83
+ """Load patch data from CSV."""
84
+ patches = []
85
+ with open(csv_path, newline="", encoding="utf-8") as f:
86
+ reader = csv.DictReader(f)
87
+ for row in reader:
88
+ try:
89
+ patches.append({
90
+ "id": row.get("patch_id", row.get("id", str(len(patches)))),
91
+ "x": float(row["x"]),
92
+ "y": float(row["y"]),
93
+ "area": float(row[area_col]),
94
+ })
95
+ except (KeyError, ValueError) as e:
96
+ logger.warning("Skipping row %s: %s", row, e)
97
+ warnings.warn(f"Skipping row {row}: {e}")
98
+ return patches
99
+
100
+
101
+ def euclidean_distance(p1: dict, p2: dict) -> float:
102
+ return math.sqrt((p1["x"] - p2["x"]) ** 2 + (p1["y"] - p2["y"]) ** 2)
103
+
104
+
105
+ def build_graph(patches: list[dict], dmax: float) -> nx.Graph:
106
+ """Build undirected graph; edges for pairs within dmax."""
107
+ G = nx.Graph()
108
+ for p in patches:
109
+ G.add_node(p["id"], x=p["x"], y=p["y"], area=p["area"])
110
+ for p1, p2 in combinations(patches, 2):
111
+ d = euclidean_distance(p1, p2)
112
+ if d < dmax:
113
+ prob = math.exp(-d / dmax) # negative exponential kernel
114
+ G.add_edge(p1["id"], p2["id"], weight=d, prob=prob)
115
+ return G
116
+
117
+
118
+ def compute_iic(G: nx.Graph, total_area: float) -> float:
119
+ """Integral Index of Connectivity (binary graph)."""
120
+ nodes = list(G.nodes())
121
+ n = len(nodes)
122
+ numerator = 0.0
123
+ # Convert to unweighted graph for hop-count shortest paths
124
+ G_uw = nx.Graph(G)
125
+ for u, v in G_uw.edges():
126
+ G_uw[u][v]["weight"] = 1
127
+
128
+ for i, ni in enumerate(nodes):
129
+ for j, nj in enumerate(nodes):
130
+ if i <= j:
131
+ try:
132
+ nij = nx.shortest_path_length(G_uw, ni, nj)
133
+ except nx.NetworkXNoPath:
134
+ continue
135
+ ai = G.nodes[ni]["area"]
136
+ aj = G.nodes[nj]["area"]
137
+ val = (ai * aj) / (1 + nij)
138
+ numerator += val if i == j else 2 * val
139
+
140
+ return numerator / (total_area ** 2)
141
+
142
+
143
+ def compute_pc(G: nx.Graph, total_area: float) -> float:
144
+ """Probability of Connectivity using shortest probabilistic paths."""
145
+ nodes = list(G.nodes())
146
+ numerator = 0.0
147
+ # Edge weight for dijkstra = -log(prob)
148
+ G_prob = nx.Graph()
149
+ G_prob.add_nodes_from(G.nodes(data=True))
150
+ for u, v, data in G.edges(data=True):
151
+ neg_log_p = -math.log(data.get("prob", 1e-9) + 1e-12)
152
+ G_prob.add_edge(u, v, weight=neg_log_p)
153
+
154
+ for i, ni in enumerate(nodes):
155
+ for j, nj in enumerate(nodes):
156
+ if i <= j:
157
+ try:
158
+ path_cost = nx.shortest_path_length(G_prob, ni, nj, weight="weight")
159
+ pij = math.exp(-path_cost)
160
+ except nx.NetworkXNoPath:
161
+ pij = 0.0
162
+ ai = G.nodes[ni]["area"]
163
+ aj = G.nodes[nj]["area"]
164
+ val = pij * ai * aj
165
+ numerator += val if i == j else 2 * val
166
+
167
+ return numerator / (total_area ** 2)
168
+
169
+
170
+ def patch_importance(patches: list[dict], dmax: float,
171
+ iic_full: float, pc_full: float,
172
+ total_area: float) -> list[dict]:
173
+ """Compute dIIC and dPC for each patch by leave-one-out."""
174
+ results = []
175
+ for i, target in enumerate(patches):
176
+ remaining = [p for j, p in enumerate(patches) if j != i]
177
+ ta_i = sum(p["area"] for p in remaining)
178
+ G_i = build_graph(remaining, dmax)
179
+ iic_i = compute_iic(G_i, ta_i)
180
+ pc_i = compute_pc(G_i, ta_i)
181
+ diic = (iic_full - iic_i) / iic_full * 100 if iic_full > 0 else 0
182
+ dpc = (pc_full - pc_i) / pc_full * 100 if pc_full > 0 else 0
183
+ results.append({"patch_id": target["id"],
184
+ "dIIC_pct": round(diic, 4),
185
+ "dPC_pct": round(dpc, 4)})
186
+ return results
187
+
188
+
189
+ def main():
190
+ args = parse_args()
191
+ patches_csv = Path(args.patches_csv)
192
+ output_dir = Path(args.output_dir)
193
+ output_dir.mkdir(parents=True, exist_ok=True)
194
+
195
+ log_decision("patches_csv", str(patches_csv),
196
+ "Input CSV of patch centroids with area values")
197
+ log_decision("dmax", args.dmax,
198
+ "Maximum dispersal distance threshold for edge creation (map units)")
199
+ log_decision("area_col", args.area_col,
200
+ "Column name for patch area in the input CSV")
201
+
202
+ if not patches_csv.exists():
203
+ logger.error(
204
+ "Input nao encontrado: %s\n"
205
+ " Causa provavel: passo anterior nao concluiu.\n"
206
+ " Skill anterior que deveria ter produzido este input: ecological-impact-assessment",
207
+ patches_csv
208
+ )
209
+ sys.exit(1)
210
+
211
+ try:
212
+ log_step(1, "Loading patch data from CSV")
213
+ patches = load_patches(patches_csv, args.area_col)
214
+ if len(patches) < 3:
215
+ logger.error("Need at least 3 patches, found %d", len(patches))
216
+ sys.exit(1)
217
+
218
+ total_area = sum(p["area"] for p in patches)
219
+ logger.info("Loaded %d patches. Total area: %.1f ha", len(patches), total_area)
220
+ logger.info("Dispersal distance threshold (dmax): %s m", args.dmax)
221
+
222
+ log_step(2, "Building landscape connectivity graph")
223
+ G = build_graph(patches, args.dmax)
224
+ logger.info("Graph: %d nodes, %d edges", G.number_of_nodes(), G.number_of_edges())
225
+ if G.number_of_edges() == 0:
226
+ logger.warning(
227
+ "No edges in graph — all patch pairs exceed dmax=%.1f. "
228
+ "Consider increasing --dmax.",
229
+ args.dmax
230
+ )
231
+
232
+ log_step(3, "Computing landscape-level IIC and PC metrics")
233
+ # Landscape metrics
234
+ iic_full = compute_iic(G, total_area)
235
+ pc_full = compute_pc(G, total_area)
236
+ comps = list(nx.connected_components(G))
237
+ largest = max(len(c) for c in comps)
238
+ logger.info("IIC = %.6f | PC = %.6f", iic_full, pc_full)
239
+ logger.info("Components: %d, largest: %d patches", len(comps), largest)
240
+ if len(comps) > 1:
241
+ logger.warning(
242
+ "Landscape is fragmented into %d disconnected components. "
243
+ "dmax may be too small or habitat too sparse.",
244
+ len(comps)
245
+ )
246
+
247
+ log_step(4, "Computing betweenness centrality")
248
+ # Betweenness centrality (unweighted)
249
+ bc_dict = nx.betweenness_centrality(G, normalized=True)
250
+
251
+ log_step(5, "Computing patch importance (dIIC, dPC) via leave-one-out")
252
+ logger.info("Computing patch importance (dIIC, dPC)...")
253
+ importance = patch_importance(patches, args.dmax, iic_full, pc_full, total_area)
254
+
255
+ log_step(6, "Assembling and writing patch metrics CSV")
256
+ # Assemble patch metrics
257
+ comp_membership = {}
258
+ for c_id, comp in enumerate(comps):
259
+ for node in comp:
260
+ comp_membership[node] = c_id + 1
261
+
262
+ patch_rows = []
263
+ imp_dict = {r["patch_id"]: r for r in importance}
264
+ for p in patches:
265
+ pid = p["id"]
266
+ row = {
267
+ "patch_id": pid,
268
+ "area_ha": p["area"],
269
+ "dIIC_pct": imp_dict.get(pid, {}).get("dIIC_pct", 0),
270
+ "dPC_pct": imp_dict.get(pid, {}).get("dPC_pct", 0),
271
+ "BC_norm": round(bc_dict.get(pid, 0), 4),
272
+ "component": comp_membership.get(pid, -1),
273
+ }
274
+ patch_rows.append(row)
275
+
276
+ patch_rows.sort(key=lambda r: -r["dPC_pct"])
277
+ patch_path = output_dir / "patch_metrics.csv"
278
+ with open(patch_path, "w", newline="", encoding="utf-8") as f:
279
+ writer = csv.DictWriter(f, fieldnames=list(patch_rows[0].keys()))
280
+ writer.writeheader()
281
+ writer.writerows(patch_rows)
282
+ logger.info("Patch metrics -> %s", patch_path)
283
+
284
+ log_step(7, "Writing landscape summary CSV")
285
+ # Landscape summary
286
+ summary_path = output_dir / "landscape_summary.csv"
287
+ with open(summary_path, "w", newline="", encoding="utf-8") as f:
288
+ writer = csv.writer(f)
289
+ writer.writerow(["metric", "value"])
290
+ writer.writerows([
291
+ ["IIC", round(iic_full, 6)],
292
+ ["PC", round(pc_full, 6)],
293
+ ["n_patches", len(patches)],
294
+ ["n_components", len(comps)],
295
+ ["largest_component_size", largest],
296
+ ["dmax_m", args.dmax],
297
+ ["total_patch_area_ha", round(total_area, 2)],
298
+ ])
299
+ logger.info("Landscape summary -> %s", summary_path)
300
+
301
+ log_step(8, "Computing pairwise effective resistance / least-cost paths")
302
+ # Pairwise effective resistance (from PC path costs)
303
+ lcp_rows = []
304
+ G_prob = nx.Graph()
305
+ G_prob.add_nodes_from(G.nodes(data=True))
306
+ for u, v, data in G.edges(data=True):
307
+ neg_log_p = -math.log(data.get("prob", 1e-9) + 1e-12)
308
+ G_prob.add_edge(u, v, weight=neg_log_p)
309
+
310
+ for p1, p2 in combinations(patches, 2):
311
+ try:
312
+ cost = nx.shortest_path_length(G_prob, p1["id"], p2["id"],
313
+ weight="weight")
314
+ dist = euclidean_distance(p1, p2)
315
+ lcp_rows.append({"from": p1["id"], "to": p2["id"],
316
+ "euclidean_dist_m": round(dist, 1),
317
+ "effective_cost": round(cost, 4),
318
+ "pij_star": round(math.exp(-cost), 4)})
319
+ except nx.NetworkXNoPath:
320
+ lcp_rows.append({"from": p1["id"], "to": p2["id"],
321
+ "euclidean_dist_m": round(euclidean_distance(p1, p2), 1),
322
+ "effective_cost": float("inf"),
323
+ "pij_star": 0.0})
324
+
325
+ lcp_path = output_dir / "least_cost_paths.csv"
326
+ if lcp_rows:
327
+ with open(lcp_path, "w", newline="", encoding="utf-8") as f:
328
+ writer = csv.DictWriter(f, fieldnames=list(lcp_rows[0].keys()))
329
+ writer.writeheader()
330
+ writer.writerows(lcp_rows)
331
+ logger.info("Pairwise costs -> %s", lcp_path)
332
+
333
+ log_step(9, "Generating network visualisation plot")
334
+ # Network visualisation
335
+ try:
336
+ import matplotlib
337
+ matplotlib.use("Agg")
338
+ import matplotlib.pyplot as plt
339
+ import matplotlib.cm as cm
340
+
341
+ fig, ax = plt.subplots(figsize=(8, 7))
342
+ pos = {p["id"]: (p["x"], p["y"]) for p in patches}
343
+ dpc_vals = np.array([r["dPC_pct"] for r in patch_rows])
344
+ node_order = [p["id"] for p in patches]
345
+ dpc_map = {r["patch_id"]: r["dPC_pct"] for r in patch_rows}
346
+ node_colors = [dpc_map.get(n, 0) for n in G.nodes()]
347
+ area_map = {p["id"]: p["area"] for p in patches}
348
+ max_area = max(area_map.values())
349
+ node_sizes = [300 * area_map.get(n, 1) / max_area + 50 for n in G.nodes()]
350
+
351
+ nx.draw_networkx_edges(G, pos, ax=ax, alpha=0.4, edge_color="grey")
352
+ sc = nx.draw_networkx_nodes(G, pos, ax=ax,
353
+ node_color=node_colors,
354
+ node_size=node_sizes,
355
+ cmap=plt.cm.plasma)
356
+ plt.colorbar(sc, ax=ax, label="dPC (%)")
357
+ ax.set_title(f"Connectivity graph (dmax={args.dmax}m, IIC={iic_full:.4f})")
358
+ ax.set_xlabel("Easting"); ax.set_ylabel("Northing")
359
+ ax.axis("equal")
360
+ plt.tight_layout()
361
+ fig.savefig(output_dir / "connectivity_graph.png", dpi=150)
362
+ plt.close(fig)
363
+ logger.info("Graph plot -> %s", output_dir / "connectivity_graph.png")
364
+ except ImportError:
365
+ logger.warning("matplotlib not available; skipping network plot.")
366
+
367
+ logger.info("Connectivity analysis complete.")
368
+ logger.info(
369
+ "Top patch by dPC: %s (dPC = %.2f%%)",
370
+ patch_rows[0]["patch_id"], patch_rows[0]["dPC_pct"]
371
+ )
372
+
373
+ except FileNotFoundError as e:
374
+ logger.error(
375
+ "Input file not found: %s\n"
376
+ " Expected output from: ecological-impact-assessment\n"
377
+ " Check that previous step completed.",
378
+ e
379
+ )
380
+ raise
381
+ except Exception as e:
382
+ logger.error("Unexpected error in connectivity analysis: %s", e)
383
+ raise
384
+
385
+
386
+ if __name__ == "__main__":
387
+ main()