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.
- package/AGENT_CONTEXT.md +191 -0
- package/CATALOG.md +329 -0
- package/LICENSE +692 -0
- package/README.md +347 -0
- package/bin/install.mjs +168 -0
- package/docs/comparison-with-alternatives.md +38 -0
- package/docs/global-examples-index.md +103 -0
- package/docs/repository-statistics.md +101 -0
- package/docs/theoretical-foundations.md +188 -0
- package/environment.yaml +106 -0
- package/examples/community/arctic_tundra_vegetation_example.md +247 -0
- package/examples/community/bird_landuse_example.md +63 -0
- package/examples/community/phytoplankton_reservoir_example.md +60 -0
- package/examples/community/reef_fish_indopacific_example.md +221 -0
- package/examples/impact/baci_road_example.md +57 -0
- package/examples/impact/ecosystem_services_atlantic_forest.md +83 -0
- package/examples/impact/forest_loss_borneo_timeseries_example.md +225 -0
- package/examples/occupancy/puma_camera_example.md +61 -0
- package/examples/occupancy/snow_leopard_himalayas_example.md +204 -0
- package/examples/reproducible/whittaker_biome_sdm_example.md +406 -0
- package/examples/sdm/anteater_cerrado_example.md +69 -0
- package/examples/sdm/jaguar_amazon_example.md +80 -0
- package/examples/sdm/koala_climate_change_example.md +170 -0
- package/examples/sdm/wolf_recolonization_europe_example.md +193 -0
- package/package.json +43 -0
- package/renv.lock +194 -0
- package/skills/SKILL_INDEX.json +1020 -0
- package/skills/acoustic-monitoring/SKILL.md +163 -0
- package/skills/acoustic-monitoring/examples/example-prompts.md +100 -0
- package/skills/acoustic-monitoring/examples/temperate_forest_birds_example.md +285 -0
- package/skills/acoustic-monitoring/resources/acoustic-indices-reference.md +93 -0
- package/skills/acoustic-monitoring/resources/soundscape-ecology-guide.md +90 -0
- package/skills/acoustic-monitoring/resources/species-id-tools-comparison.md +89 -0
- package/skills/acoustic-monitoring/scripts/batch_species_detection.py +360 -0
- package/skills/acoustic-monitoring/scripts/compute_acoustic_indices.R +235 -0
- package/skills/acoustic-monitoring/scripts/compute_acoustic_indices.py +374 -0
- package/skills/biostatistics-workbench/SKILL.md +140 -0
- package/skills/biostatistics-workbench/examples/example-prompts.md +39 -0
- package/skills/biostatistics-workbench/resources/effect-size-reference.md +81 -0
- package/skills/biostatistics-workbench/resources/glm-family-link-reference.md +47 -0
- package/skills/biostatistics-workbench/resources/test-selection-guide.md +93 -0
- package/skills/biostatistics-workbench/scripts/glm_pipeline.R +78 -0
- package/skills/biostatistics-workbench/scripts/glm_pipeline.py +210 -0
- package/skills/camera-trap-processing/SKILL.md +159 -0
- package/skills/camera-trap-processing/examples/example-prompts.md +103 -0
- package/skills/camera-trap-processing/examples/leopard_serengeti_example.md +231 -0
- package/skills/camera-trap-processing/resources/activity-patterns-reference.md +113 -0
- package/skills/camera-trap-processing/resources/camtrapR-workflow-guide.md +130 -0
- package/skills/camera-trap-processing/resources/detection-event-definition-guide.md +89 -0
- package/skills/camera-trap-processing/scripts/estimate_activity.R +169 -0
- package/skills/camera-trap-processing/scripts/process_camtrap_data.R +179 -0
- package/skills/camera-trap-processing/scripts/process_camtrap_data.py +192 -0
- package/skills/community-ecology-ordination/SKILL.md +133 -0
- package/skills/community-ecology-ordination/examples/example-prompts.md +35 -0
- package/skills/community-ecology-ordination/resources/dissimilarity-metric-guide.md +53 -0
- package/skills/community-ecology-ordination/resources/nmds-interpretation-guide.md +104 -0
- package/skills/community-ecology-ordination/scripts/__pycache__/community_analysis.cpython-311.pyc +0 -0
- package/skills/community-ecology-ordination/scripts/community_analysis.R +143 -0
- package/skills/community-ecology-ordination/scripts/community_analysis.py +231 -0
- package/skills/ecological-data-foundation/SKILL.md +129 -0
- package/skills/ecological-data-foundation/examples/example-prompts.md +40 -0
- package/skills/ecological-data-foundation/resources/coordinate-cleaning-flags.md +66 -0
- package/skills/ecological-data-foundation/resources/darwin-core-glossary.md +91 -0
- package/skills/ecological-data-foundation/resources/data-citation-guide.md +265 -0
- package/skills/ecological-data-foundation/resources/gbif-data-citation-guide.md +193 -0
- package/skills/ecological-data-foundation/resources/qa-checklist.md +83 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/clean_occurrences.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/download_from_ebird.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/download_from_inat.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/download_from_iucn.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/download_from_obis.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/clean_occurrences.R +230 -0
- package/skills/ecological-data-foundation/scripts/clean_occurrences.py +268 -0
- package/skills/ecological-data-foundation/scripts/download_from_ebird.R +251 -0
- package/skills/ecological-data-foundation/scripts/download_from_ebird.py +364 -0
- package/skills/ecological-data-foundation/scripts/download_from_gbif.R +315 -0
- package/skills/ecological-data-foundation/scripts/download_from_gbif.py +407 -0
- package/skills/ecological-data-foundation/scripts/download_from_inat.R +238 -0
- package/skills/ecological-data-foundation/scripts/download_from_inat.py +304 -0
- package/skills/ecological-data-foundation/scripts/download_from_iucn.R +273 -0
- package/skills/ecological-data-foundation/scripts/download_from_iucn.py +344 -0
- package/skills/ecological-data-foundation/scripts/download_from_obis.R +248 -0
- package/skills/ecological-data-foundation/scripts/download_from_obis.py +318 -0
- package/skills/ecological-impact-assessment/SKILL.md +123 -0
- package/skills/ecological-impact-assessment/examples/example-prompts.md +32 -0
- package/skills/ecological-impact-assessment/resources/baci-design-guide.md +55 -0
- package/skills/ecological-impact-assessment/resources/fragmentation-metrics-reference.md +86 -0
- package/skills/ecological-impact-assessment/resources/pressure-index-template.md +78 -0
- package/skills/ecological-impact-assessment/resources/study-design-guide.md +168 -0
- package/skills/ecological-impact-assessment/scripts/baci_analysis.R +161 -0
- package/skills/ecological-impact-assessment/scripts/fragmentation_analysis.py +141 -0
- package/skills/ecological-impact-assessment/scripts/power_analysis_baci.R +274 -0
- package/skills/ecosystem-services-assessment/SKILL.md +125 -0
- package/skills/ecosystem-services-assessment/examples/example-prompts.md +24 -0
- package/skills/ecosystem-services-assessment/resources/es-indicator-reference.md +45 -0
- package/skills/ecosystem-services-assessment/resources/invest-parameter-guide.md +86 -0
- package/skills/ecosystem-services-assessment/resources/rusle-coefficients.md +88 -0
- package/skills/ecosystem-services-assessment/scripts/__pycache__/compute_es.cpython-311.pyc +0 -0
- package/skills/ecosystem-services-assessment/scripts/compute_es.py +189 -0
- package/skills/ecosystem-services-assessment/scripts/tradeoff_analysis.R +161 -0
- package/skills/environmental-time-series/SKILL.md +125 -0
- package/skills/environmental-time-series/examples/example-prompts.md +33 -0
- package/skills/environmental-time-series/resources/anomaly-indices-reference.md +88 -0
- package/skills/environmental-time-series/resources/bfast-parameter-guide.md +69 -0
- package/skills/environmental-time-series/scripts/__pycache__/recovery_trajectory.cpython-311.pyc +0 -0
- package/skills/environmental-time-series/scripts/__pycache__/trend_analysis.cpython-311.pyc +0 -0
- package/skills/environmental-time-series/scripts/recovery_trajectory.R +305 -0
- package/skills/environmental-time-series/scripts/recovery_trajectory.py +178 -0
- package/skills/environmental-time-series/scripts/trend_analysis.R +192 -0
- package/skills/environmental-time-series/scripts/trend_analysis.py +184 -0
- package/skills/geoprocessing-for-ecology/SKILL.md +123 -0
- package/skills/geoprocessing-for-ecology/examples/example-prompts.md +32 -0
- package/skills/geoprocessing-for-ecology/resources/crs-reference.md +62 -0
- package/skills/geoprocessing-for-ecology/resources/global-predictor-sources.md +331 -0
- package/skills/geoprocessing-for-ecology/resources/resampling-methods.md +57 -0
- package/skills/geoprocessing-for-ecology/scripts/__pycache__/download_predictors.cpython-311.pyc +0 -0
- package/skills/geoprocessing-for-ecology/scripts/download_predictors.R +239 -0
- package/skills/geoprocessing-for-ecology/scripts/download_predictors.py +379 -0
- package/skills/geoprocessing-for-ecology/scripts/stack_and_extract.R +224 -0
- package/skills/geoprocessing-for-ecology/scripts/stack_and_extract.py +172 -0
- package/skills/landscape-connectivity/SKILL.md +170 -0
- package/skills/landscape-connectivity/examples/example-prompts.md +96 -0
- package/skills/landscape-connectivity/examples/jaguar_mesoamerica_corridor_example.md +271 -0
- package/skills/landscape-connectivity/resources/circuitscape-parameter-guide.md +155 -0
- package/skills/landscape-connectivity/resources/graph-theory-for-ecology.md +134 -0
- package/skills/landscape-connectivity/resources/resistance-surface-guide.md +141 -0
- package/skills/landscape-connectivity/scripts/connectivity_analysis.py +387 -0
- package/skills/landscape-connectivity/scripts/connectivity_metrics.R +274 -0
- package/skills/landscape-connectivity/scripts/resistance_surface.R +239 -0
- package/skills/model-validation-and-uncertainty/SKILL.md +131 -0
- package/skills/model-validation-and-uncertainty/examples/example-prompts.md +30 -0
- package/skills/model-validation-and-uncertainty/resources/extrapolation-risk-guide.md +236 -0
- package/skills/model-validation-and-uncertainty/resources/metric-selection-guide.md +52 -0
- package/skills/model-validation-and-uncertainty/resources/threshold-selection-guide.md +64 -0
- package/skills/model-validation-and-uncertainty/scripts/__pycache__/validate_model.cpython-311.pyc +0 -0
- package/skills/model-validation-and-uncertainty/scripts/extrapolation_risk.R +315 -0
- package/skills/model-validation-and-uncertainty/scripts/validate_model.py +226 -0
- package/skills/model-validation-and-uncertainty/scripts/validate_sdm.R +162 -0
- package/skills/occupancy-and-detection/SKILL.md +126 -0
- package/skills/occupancy-and-detection/examples/example-prompts.md +33 -0
- package/skills/occupancy-and-detection/resources/detection-history-format.md +100 -0
- package/skills/occupancy-and-detection/resources/occupancy-study-design.md +47 -0
- package/skills/occupancy-and-detection/scripts/__pycache__/occupancy_analysis.cpython-311.pyc +0 -0
- package/skills/occupancy-and-detection/scripts/occupancy_analysis.R +160 -0
- package/skills/occupancy-and-detection/scripts/occupancy_analysis.py +159 -0
- package/skills/population-viability-analysis/SKILL.md +161 -0
- package/skills/population-viability-analysis/examples/african_elephant_pva_example.md +266 -0
- package/skills/population-viability-analysis/examples/example-prompts.md +95 -0
- package/skills/population-viability-analysis/resources/extinction-risk-thresholds.md +128 -0
- package/skills/population-viability-analysis/resources/matrix-model-guide.md +139 -0
- package/skills/population-viability-analysis/resources/sensitivity-elasticity-reference.md +182 -0
- package/skills/population-viability-analysis/scripts/matrix_pva.R +258 -0
- package/skills/population-viability-analysis/scripts/pva_analysis.py +442 -0
- package/skills/population-viability-analysis/scripts/stochastic_pva.R +353 -0
- package/skills/predictive-modeling-best-practices/SKILL.md +136 -0
- package/skills/predictive-modeling-best-practices/examples/example-prompts.md +58 -0
- package/skills/predictive-modeling-best-practices/resources/collinearity-decision-tree.md +65 -0
- package/skills/predictive-modeling-best-practices/resources/sampling-bias-correction.md +267 -0
- package/skills/predictive-modeling-best-practices/resources/spatial-cv-guide.md +73 -0
- package/skills/predictive-modeling-best-practices/scripts/__pycache__/spatial_cv.cpython-311.pyc +0 -0
- package/skills/predictive-modeling-best-practices/scripts/collinearity_check.R +112 -0
- package/skills/predictive-modeling-best-practices/scripts/spatial_cv.py +182 -0
- package/skills/reproducible-ecology-pipeline/SKILL.md +139 -0
- package/skills/reproducible-ecology-pipeline/examples/example-prompts.md +35 -0
- package/skills/reproducible-ecology-pipeline/resources/directory-structure-template.md +94 -0
- package/skills/reproducible-ecology-pipeline/resources/params-yaml-template.yaml +84 -0
- package/skills/reproducible-ecology-pipeline/resources/reproducibility-checklist-template.md +66 -0
- package/skills/reproducible-ecology-pipeline/scripts/generate_file_manifest.py +110 -0
- package/skills/reproducible-ecology-pipeline/scripts/init_project.sh +53 -0
- package/skills/spatial-prioritization/SKILL.md +162 -0
- package/skills/spatial-prioritization/examples/biodiversity_hotspot_prioritization_example.md +289 -0
- package/skills/spatial-prioritization/examples/example-prompts.md +93 -0
- package/skills/spatial-prioritization/resources/cost-surface-reference.md +130 -0
- package/skills/spatial-prioritization/resources/marxan-vs-prioritizr-comparison.md +125 -0
- package/skills/spatial-prioritization/resources/prioritizr-formulation-guide.md +188 -0
- package/skills/spatial-prioritization/resources/representation-targets-guide.md +186 -0
- package/skills/spatial-prioritization/scripts/prioritization_sensitivity.R +320 -0
- package/skills/spatial-prioritization/scripts/run_prioritization.R +336 -0
- package/skills/species-distribution-modeling/SKILL.md +139 -0
- package/skills/species-distribution-modeling/examples/example-prompts.md +36 -0
- package/skills/species-distribution-modeling/resources/algorithm-comparison.md +25 -0
- package/skills/species-distribution-modeling/resources/calibration-area-guide.md +71 -0
- package/skills/species-distribution-modeling/resources/climate-scenario-preparation.md +170 -0
- package/skills/species-distribution-modeling/resources/maxent-calibration-guide.md +211 -0
- package/skills/species-distribution-modeling/resources/sdm-checklist.md +37 -0
- package/skills/species-distribution-modeling/scripts/predict_distribution.R +236 -0
- package/skills/species-distribution-modeling/scripts/predict_distribution.py +286 -0
- package/skills/species-distribution-modeling/scripts/prepare_future_layers.R +351 -0
- package/skills/species-distribution-modeling/scripts/project_scenarios.R +220 -0
- package/skills/species-distribution-modeling/scripts/run_ensemble_sdm.R +99 -0
- package/skills/species-distribution-modeling/scripts/sdm_pipeline.py +318 -0
- package/skills/species-distribution-modeling/scripts/tune_maxnet.R +344 -0
- package/templates/SKILL_TEMPLATE.md +225 -0
- package/templates/checklists/data-submission-checklist.md +38 -0
- package/templates/checklists/post-analysis-checklist.md +55 -0
- package/templates/checklists/pre-analysis-checklist.md +31 -0
- package/templates/prompts/debug-skill.md +47 -0
- package/templates/prompts/invoke-skill.md +34 -0
- package/templates/prompts/invoke-workflow.md +45 -0
- package/templates/reports/technical-report-template.md +80 -0
- package/templates/scripts/logger_setup.R +79 -0
- package/templates/scripts/logger_setup.py +119 -0
- package/templates/scripts/params_loader.R +28 -0
- package/templates/scripts/params_loader.py +38 -0
- package/workflows/analyze-community-structure/WORKFLOW.md +72 -0
- package/workflows/analyze-environmental-change/WORKFLOW.md +73 -0
- package/workflows/assess-ecological-impact/WORKFLOW.md +75 -0
- package/workflows/assess-ecosystem-services/WORKFLOW.md +68 -0
- package/workflows/assess-landscape-connectivity/WORKFLOW.md +84 -0
- package/workflows/build-fire-risk-map/WORKFLOW.md +79 -0
- package/workflows/produce-technical-report/WORKFLOW.md +113 -0
- package/workflows/run-camera-trap-occupancy/WORKFLOW.md +87 -0
- package/workflows/run-conservation-prioritization/WORKFLOW.md +89 -0
- package/workflows/run-multispecies-screening/WORKFLOW.md +197 -0
- package/workflows/run-occupancy-analysis/WORKFLOW.md +74 -0
- package/workflows/run-population-viability/WORKFLOW.md +90 -0
- 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()
|