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,442 @@
|
|
|
1
|
+
# ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Population Viability Analysis using stage-structured matrix models.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python pva_analysis.py <vital_rates_csv> <output_dir>
|
|
9
|
+
[--n_init 500] [--t_max 100] [--n_sim 1000] [--quasi_ext 50]
|
|
10
|
+
|
|
11
|
+
Inputs:
|
|
12
|
+
vital_rates_csv — CSV with columns: year, a_i_j (matrix elements),
|
|
13
|
+
population_N (optional census count)
|
|
14
|
+
|
|
15
|
+
Implements:
|
|
16
|
+
- Deterministic analysis: λ, stable stage, sensitivity, elasticity
|
|
17
|
+
- Stochastic PVA: Monte Carlo with Beta/Lognormal vital rate sampling
|
|
18
|
+
- IUCN Criterion E classification
|
|
19
|
+
|
|
20
|
+
Outputs:
|
|
21
|
+
lambda_summary.csv — Eigenvalue analysis
|
|
22
|
+
stochastic_pva_results.csv — P(extinction), MTE, λ_s
|
|
23
|
+
extinction_curve.csv — P(ext) by year
|
|
24
|
+
iucn_criterion_e.csv — Category assessment
|
|
25
|
+
trajectory_plot.png — Stochastic trajectories
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
import sys
|
|
30
|
+
import csv
|
|
31
|
+
import math
|
|
32
|
+
import random
|
|
33
|
+
import argparse
|
|
34
|
+
import warnings
|
|
35
|
+
from datetime import datetime
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from collections import defaultdict
|
|
38
|
+
|
|
39
|
+
SKILL_NAME = "population-viability-analysis"
|
|
40
|
+
_LOG_DIR = Path("logs")
|
|
41
|
+
_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
_log_file = _LOG_DIR / f"skill_{SKILL_NAME}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
|
43
|
+
logging.basicConfig(
|
|
44
|
+
level=logging.INFO,
|
|
45
|
+
format="[%(asctime)s] [%(levelname)s] [" + SKILL_NAME + "] %(message)s",
|
|
46
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
47
|
+
handlers=[
|
|
48
|
+
logging.StreamHandler(sys.stdout),
|
|
49
|
+
logging.FileHandler(_log_file, encoding="utf-8"),
|
|
50
|
+
],
|
|
51
|
+
)
|
|
52
|
+
logger = logging.getLogger(SKILL_NAME)
|
|
53
|
+
|
|
54
|
+
def log_step(n: int, desc: str) -> None:
|
|
55
|
+
logger.info("-- STEP %d: %s", n, desc)
|
|
56
|
+
|
|
57
|
+
def log_decision(var: str, val, why: str) -> None:
|
|
58
|
+
logger.info("DECISION | %s = %s | %s", var, val, why)
|
|
59
|
+
|
|
60
|
+
import numpy as np
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
import numpy.linalg as la
|
|
64
|
+
except ImportError:
|
|
65
|
+
logger.error("numpy required. Install: pip install numpy")
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_args():
|
|
70
|
+
parser = argparse.ArgumentParser(description="Population Viability Analysis")
|
|
71
|
+
parser.add_argument("vital_rates_csv", help="CSV of vital rates over years")
|
|
72
|
+
parser.add_argument("output_dir", help="Output directory")
|
|
73
|
+
parser.add_argument("--n_init", type=int, default=None,
|
|
74
|
+
help="Initial population size (default: last year N in CSV)")
|
|
75
|
+
parser.add_argument("--t_max", type=int, default=100,
|
|
76
|
+
help="Projection years (default: 100)")
|
|
77
|
+
parser.add_argument("--n_sim", type=int, default=1000,
|
|
78
|
+
help="Monte Carlo simulations (default: 1000)")
|
|
79
|
+
parser.add_argument("--quasi_ext", type=float, default=50.0,
|
|
80
|
+
help="Quasi-extinction threshold (default: 50)")
|
|
81
|
+
return parser.parse_args()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def load_vital_rates(csv_path: Path) -> tuple[list[str], list[dict]]:
|
|
85
|
+
"""Load vital rates CSV; return (mat_cols, rows)."""
|
|
86
|
+
with open(csv_path, newline="", encoding="utf-8") as f:
|
|
87
|
+
reader = csv.DictReader(f)
|
|
88
|
+
rows = list(reader)
|
|
89
|
+
mat_cols = [k for k in rows[0].keys() if k.startswith("a_")]
|
|
90
|
+
return mat_cols, rows
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_mean_matrix(mat_cols: list[str], rows: list[dict]) -> np.ndarray:
|
|
94
|
+
"""Build mean matrix from vital rate rows."""
|
|
95
|
+
indices = []
|
|
96
|
+
for col in mat_cols:
|
|
97
|
+
parts = col.split("_")
|
|
98
|
+
indices.append((int(parts[1]) - 1, int(parts[2]) - 1))
|
|
99
|
+
k = max(max(i, j) for i, j in indices) + 1
|
|
100
|
+
A = np.zeros((k, k))
|
|
101
|
+
for col, (i, j) in zip(mat_cols, indices):
|
|
102
|
+
vals = [float(r[col]) for r in rows if r[col] != ""]
|
|
103
|
+
A[i, j] = sum(vals) / len(vals) if vals else 0.0
|
|
104
|
+
return A
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def compute_lambda(A: np.ndarray) -> float:
|
|
108
|
+
"""Dominant eigenvalue of A."""
|
|
109
|
+
evals = la.eigvals(A)
|
|
110
|
+
return float(max(evals.real))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def stable_stage(A: np.ndarray) -> np.ndarray:
|
|
114
|
+
"""Right eigenvector corresponding to dominant eigenvalue."""
|
|
115
|
+
evals, evecs = la.eig(A)
|
|
116
|
+
dom_idx = np.argmax(evals.real)
|
|
117
|
+
v = evecs[:, dom_idx].real
|
|
118
|
+
v = np.abs(v)
|
|
119
|
+
return v / v.sum()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def sensitivity_matrix(A: np.ndarray) -> np.ndarray:
|
|
123
|
+
"""Sensitivity matrix S_ij = ∂λ/∂a_ij = w_i * v_j / <w,v>."""
|
|
124
|
+
evals, evecs_right = la.eig(A)
|
|
125
|
+
dom_idx = np.argmax(evals.real)
|
|
126
|
+
w = evecs_right[:, dom_idx].real
|
|
127
|
+
evals_l, evecs_left = la.eig(A.T)
|
|
128
|
+
dom_idx_l = np.argmax(evals_l.real)
|
|
129
|
+
v = evecs_left[:, dom_idx_l].real
|
|
130
|
+
w, v = np.abs(w), np.abs(v)
|
|
131
|
+
inner = np.dot(v, w)
|
|
132
|
+
S = np.outer(w, v) / inner
|
|
133
|
+
return S
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def elasticity_matrix(A: np.ndarray, S: np.ndarray) -> np.ndarray:
|
|
137
|
+
lam = compute_lambda(A)
|
|
138
|
+
return (A / lam) * S
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def beta_draw(mu: float, var: float) -> float:
|
|
142
|
+
"""Draw from Beta distribution parameterised by mean and variance."""
|
|
143
|
+
if var <= 0 or mu <= 0 or mu >= 1:
|
|
144
|
+
return mu
|
|
145
|
+
max_var = mu * (1 - mu) - 1e-6
|
|
146
|
+
var_use = min(var, max_var * 0.95)
|
|
147
|
+
denom = mu * (1 - mu) / var_use - 1
|
|
148
|
+
a = mu * denom
|
|
149
|
+
b = (1 - mu) * denom
|
|
150
|
+
if a <= 0 or b <= 0:
|
|
151
|
+
return mu
|
|
152
|
+
return random.betavariate(a, b)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def lnorm_draw(mu: float, var: float) -> float:
|
|
156
|
+
"""Draw from lognormal parameterised by mean and variance."""
|
|
157
|
+
if var <= 0 or mu <= 0:
|
|
158
|
+
return mu
|
|
159
|
+
sigma2_ln = math.log(1 + var / mu**2)
|
|
160
|
+
mu_ln = math.log(mu) - sigma2_ln / 2
|
|
161
|
+
return math.exp(random.gauss(mu_ln, math.sqrt(sigma2_ln)))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def build_stoch_distributions(mat_cols, rows, indices):
|
|
165
|
+
"""Compute mean and variance per matrix element."""
|
|
166
|
+
dists = {}
|
|
167
|
+
for col, (i, j) in zip(mat_cols, indices):
|
|
168
|
+
vals = [float(r[col]) for r in rows if r.get(col, "") != ""]
|
|
169
|
+
if not vals:
|
|
170
|
+
dists[col] = {"mu": 0.0, "var": 0.0, "row": i, "col": j}
|
|
171
|
+
continue
|
|
172
|
+
mu = sum(vals) / len(vals)
|
|
173
|
+
var = sum((v - mu)**2 for v in vals) / max(len(vals) - 1, 1)
|
|
174
|
+
dists[col] = {"mu": mu, "var": var, "row": i, "col": j}
|
|
175
|
+
return dists
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def draw_random_matrix(dists: dict, k: int) -> np.ndarray:
|
|
179
|
+
A = np.zeros((k, k))
|
|
180
|
+
for col, d in dists.items():
|
|
181
|
+
i, j = d["row"], d["col"]
|
|
182
|
+
mu, var = d["mu"], d["var"]
|
|
183
|
+
if i == 0: # fecundity row
|
|
184
|
+
A[i, j] = max(0.0, lnorm_draw(mu, var))
|
|
185
|
+
else:
|
|
186
|
+
A[i, j] = beta_draw(mu, var)
|
|
187
|
+
return A
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def run_stochastic_pva(dists, k, n0, t_max, n_sim, quasi_ext, stable_stg):
|
|
191
|
+
all_N = np.full((n_sim, t_max + 1), np.nan)
|
|
192
|
+
ext_times = np.full(n_sim, np.nan)
|
|
193
|
+
|
|
194
|
+
for s in range(n_sim):
|
|
195
|
+
n_vec = np.round(n0 * stable_stg).astype(float)
|
|
196
|
+
all_N[s, 0] = n_vec.sum()
|
|
197
|
+
extinct = False
|
|
198
|
+
for t in range(1, t_max + 1):
|
|
199
|
+
if not extinct:
|
|
200
|
+
A_t = draw_random_matrix(dists, k)
|
|
201
|
+
n_vec = A_t @ n_vec
|
|
202
|
+
N_t = n_vec.sum()
|
|
203
|
+
all_N[s, t] = N_t
|
|
204
|
+
if N_t <= quasi_ext:
|
|
205
|
+
extinct = True
|
|
206
|
+
ext_times[s] = t
|
|
207
|
+
all_N[s, t + 1:] = 0.0
|
|
208
|
+
return all_N, ext_times
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def iucn_criterion_e(ext_curve, t_max, gen_time=20):
|
|
212
|
+
results = []
|
|
213
|
+
for cat, threshold, horiz_fn in [
|
|
214
|
+
("CR", 0.50, lambda g: min(100, max(10, 3 * g))),
|
|
215
|
+
("EN", 0.20, lambda g: min(100, max(20, 5 * g))),
|
|
216
|
+
("VU", 0.10, lambda g: 100),
|
|
217
|
+
]:
|
|
218
|
+
T = int(round(horiz_fn(gen_time)))
|
|
219
|
+
T_use = min(T, t_max) - 1
|
|
220
|
+
p_ext = ext_curve[T_use] if T_use >= 0 else 0.0
|
|
221
|
+
results.append({
|
|
222
|
+
"category": cat,
|
|
223
|
+
"threshold": threshold,
|
|
224
|
+
"time_horizon": T,
|
|
225
|
+
"p_extinction": round(p_ext, 4),
|
|
226
|
+
"qualifies": p_ext >= threshold,
|
|
227
|
+
})
|
|
228
|
+
return results
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def main():
|
|
232
|
+
args = parse_args()
|
|
233
|
+
output_dir = Path(args.output_dir)
|
|
234
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
235
|
+
|
|
236
|
+
log_decision("vital_rates_csv", args.vital_rates_csv,
|
|
237
|
+
"Input vital rates CSV with stage matrix elements over years")
|
|
238
|
+
log_decision("t_max", args.t_max, "Projection horizon in years for stochastic PVA")
|
|
239
|
+
log_decision("n_sim", args.n_sim, "Number of Monte Carlo simulation replicates")
|
|
240
|
+
log_decision("quasi_ext", args.quasi_ext,
|
|
241
|
+
"Quasi-extinction threshold N below which population is considered extinct")
|
|
242
|
+
|
|
243
|
+
if not Path(args.vital_rates_csv).exists():
|
|
244
|
+
logger.error(
|
|
245
|
+
"Input nao encontrado: %s\n"
|
|
246
|
+
" Causa provavel: passo anterior nao concluiu.\n"
|
|
247
|
+
" Skill anterior que deveria ter produzido este input: reproducible-ecology-pipeline",
|
|
248
|
+
args.vital_rates_csv
|
|
249
|
+
)
|
|
250
|
+
sys.exit(1)
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
log_step(1, "Loading vital rates and building mean matrix")
|
|
254
|
+
mat_cols, rows = load_vital_rates(Path(args.vital_rates_csv))
|
|
255
|
+
if not mat_cols:
|
|
256
|
+
logger.error("No a_i_j columns found in vital_rates_csv.")
|
|
257
|
+
sys.exit(1)
|
|
258
|
+
|
|
259
|
+
indices = [(int(c.split("_")[1]) - 1, int(c.split("_")[2]) - 1) for c in mat_cols]
|
|
260
|
+
k = max(max(i, j) for i, j in indices) + 1
|
|
261
|
+
logger.info("Matrix size: %dx%d", k, k)
|
|
262
|
+
|
|
263
|
+
log_step(2, "Deterministic analysis: lambda, stable stage, sensitivity, elasticity")
|
|
264
|
+
A = build_mean_matrix(mat_cols, rows)
|
|
265
|
+
lam = compute_lambda(A)
|
|
266
|
+
SS = stable_stage(A)
|
|
267
|
+
S = sensitivity_matrix(A)
|
|
268
|
+
E = elasticity_matrix(A, S)
|
|
269
|
+
|
|
270
|
+
logger.info("lambda = %.4f", lam)
|
|
271
|
+
if lam < 0.95:
|
|
272
|
+
logger.warning(
|
|
273
|
+
"lambda = %.4f < 0.95 — population declining rapidly. "
|
|
274
|
+
"Review vital rates and consider conservation interventions.",
|
|
275
|
+
lam
|
|
276
|
+
)
|
|
277
|
+
elif lam < 1.0:
|
|
278
|
+
logger.warning(
|
|
279
|
+
"lambda = %.4f < 1.0 — population is declining (sub-replacement).",
|
|
280
|
+
lam
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
log_step(3, "Resolving initial population size")
|
|
284
|
+
# Initial N
|
|
285
|
+
n0 = args.n_init
|
|
286
|
+
if n0 is None:
|
|
287
|
+
pop_vals = [float(r["population_N"]) for r in rows
|
|
288
|
+
if "population_N" in r and r["population_N"] != ""]
|
|
289
|
+
n0 = int(pop_vals[-1]) if pop_vals else 1000
|
|
290
|
+
log_decision("n0", n0,
|
|
291
|
+
"Taken from last population_N value in CSV (no --n_init provided)")
|
|
292
|
+
else:
|
|
293
|
+
log_decision("n0", n0, "User-specified initial population size via --n_init")
|
|
294
|
+
logger.info("N0 = %d, quasi-extinction threshold = %s", n0, args.quasi_ext)
|
|
295
|
+
|
|
296
|
+
log_step(4, "Writing lambda summary CSV")
|
|
297
|
+
# Lambda summary
|
|
298
|
+
lam_sum_path = output_dir / "lambda_summary.csv"
|
|
299
|
+
with open(lam_sum_path, "w", newline="", encoding="utf-8") as f:
|
|
300
|
+
writer = csv.writer(f)
|
|
301
|
+
writer.writerow(["metric", "value"])
|
|
302
|
+
writer.writerows([
|
|
303
|
+
["lambda", round(lam, 6)],
|
|
304
|
+
["log_lambda", round(math.log(lam), 6) if lam > 0 else "nan"],
|
|
305
|
+
["doubling_time_yr", round(math.log(2) / math.log(lam), 2) if lam > 1 else "Inf"],
|
|
306
|
+
["halving_time_yr", round(math.log(0.5) / math.log(lam), 2) if 0 < lam < 1 else "Inf"],
|
|
307
|
+
])
|
|
308
|
+
writer.writerow(["sum_elasticity", round(float(E.sum()), 4)])
|
|
309
|
+
|
|
310
|
+
logger.info("Lambda summary -> %s", lam_sum_path)
|
|
311
|
+
|
|
312
|
+
log_step(5, "Running stochastic Monte Carlo simulations")
|
|
313
|
+
logger.info(
|
|
314
|
+
"Running %d stochastic simulations (t=%d)...", args.n_sim, args.t_max
|
|
315
|
+
)
|
|
316
|
+
dists = build_stoch_distributions(mat_cols, rows, indices)
|
|
317
|
+
all_N, ext_times = run_stochastic_pva(
|
|
318
|
+
dists, k, n0, args.t_max, args.n_sim, args.quasi_ext, SS
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
log_step(6, "Computing extinction curve and IUCN Criterion E")
|
|
322
|
+
# Extinction curve
|
|
323
|
+
ext_curve = []
|
|
324
|
+
for t in range(1, args.t_max + 1):
|
|
325
|
+
p = float(np.sum(~np.isnan(ext_times) & (ext_times <= t))) / args.n_sim
|
|
326
|
+
ext_curve.append(p)
|
|
327
|
+
|
|
328
|
+
ext_path = output_dir / "extinction_curve.csv"
|
|
329
|
+
with open(ext_path, "w", newline="", encoding="utf-8") as f:
|
|
330
|
+
writer = csv.writer(f)
|
|
331
|
+
writer.writerow(["time", "p_extinction"])
|
|
332
|
+
for t, p in enumerate(ext_curve, 1):
|
|
333
|
+
writer.writerow([t, round(p, 4)])
|
|
334
|
+
logger.info("Extinction curve -> %s", ext_path)
|
|
335
|
+
|
|
336
|
+
# IUCN Criterion E
|
|
337
|
+
iucn_rows = iucn_criterion_e(ext_curve, args.t_max, gen_time=args.t_max // 5)
|
|
338
|
+
iucn_path = output_dir / "iucn_criterion_e.csv"
|
|
339
|
+
with open(iucn_path, "w", newline="", encoding="utf-8") as f:
|
|
340
|
+
writer = csv.DictWriter(f, fieldnames=list(iucn_rows[0].keys()))
|
|
341
|
+
writer.writeheader()
|
|
342
|
+
writer.writerows(iucn_rows)
|
|
343
|
+
|
|
344
|
+
# Determine category
|
|
345
|
+
risk_cat = "LC/NT"
|
|
346
|
+
for row in iucn_rows:
|
|
347
|
+
if row["qualifies"]:
|
|
348
|
+
risk_cat = row["category"]
|
|
349
|
+
break
|
|
350
|
+
|
|
351
|
+
log_step(7, "Computing MTE and stochastic growth rate")
|
|
352
|
+
# MTE
|
|
353
|
+
valid_ext = ext_times[~np.isnan(ext_times)]
|
|
354
|
+
mte_mean = float(np.mean(valid_ext)) if len(valid_ext) > 0 else float("inf")
|
|
355
|
+
mte_lo = float(np.percentile(valid_ext, 2.5)) if len(valid_ext) >= 10 else float("nan")
|
|
356
|
+
mte_hi = float(np.percentile(valid_ext, 97.5)) if len(valid_ext) >= 10 else float("nan")
|
|
357
|
+
|
|
358
|
+
# Stochastic growth rate
|
|
359
|
+
final_N = all_N[:, -1]
|
|
360
|
+
log_N = np.log(final_N[np.isfinite(final_N) & (final_N > 0)])
|
|
361
|
+
lam_s = float(np.exp((np.mean(log_N) - math.log(n0)) / args.t_max)) if len(log_N) > 0 else float("nan")
|
|
362
|
+
|
|
363
|
+
log_step(8, "Writing stochastic PVA results CSV")
|
|
364
|
+
results_path = output_dir / "stochastic_pva_results.csv"
|
|
365
|
+
with open(results_path, "w", newline="", encoding="utf-8") as f:
|
|
366
|
+
writer = csv.writer(f)
|
|
367
|
+
writer.writerow(["metric", "value"])
|
|
368
|
+
writer.writerows([
|
|
369
|
+
["n_simulations", args.n_sim],
|
|
370
|
+
["n_init", n0],
|
|
371
|
+
["quasi_ext_threshold", args.quasi_ext],
|
|
372
|
+
["t_max", args.t_max],
|
|
373
|
+
["p_extinction", round(ext_curve[-1], 4)],
|
|
374
|
+
["mte_mean_yr", round(mte_mean, 1)],
|
|
375
|
+
["mte_CI_2.5", round(mte_lo, 1)],
|
|
376
|
+
["mte_CI_97.5", round(mte_hi, 1)],
|
|
377
|
+
["lambda_s", round(lam_s, 4)],
|
|
378
|
+
["iucn_category", risk_cat],
|
|
379
|
+
])
|
|
380
|
+
logger.info("PVA results -> %s", results_path)
|
|
381
|
+
logger.info("P(extinction at t=%d) = %.4f", args.t_max, ext_curve[-1])
|
|
382
|
+
logger.info("IUCN Criterion E: %s", risk_cat)
|
|
383
|
+
if risk_cat in ("CR", "EN"):
|
|
384
|
+
logger.warning(
|
|
385
|
+
"Population qualifies as %s under IUCN Criterion E. "
|
|
386
|
+
"Immediate conservation action recommended.",
|
|
387
|
+
risk_cat
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
log_step(9, "Generating trajectory and extinction curve plots")
|
|
391
|
+
# Trajectory plot
|
|
392
|
+
try:
|
|
393
|
+
import matplotlib
|
|
394
|
+
matplotlib.use("Agg")
|
|
395
|
+
import matplotlib.pyplot as plt
|
|
396
|
+
|
|
397
|
+
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
|
|
398
|
+
t_axis = np.arange(args.t_max + 1)
|
|
399
|
+
sample_idx = np.random.choice(args.n_sim, min(200, args.n_sim), replace=False)
|
|
400
|
+
for s in sample_idx:
|
|
401
|
+
axes[0].plot(t_axis, all_N[s, :], alpha=0.05, color="steelblue", lw=0.5)
|
|
402
|
+
med_N = np.nanmedian(all_N, axis=0)
|
|
403
|
+
axes[0].plot(t_axis, med_N, color="darkblue", lw=2, label="Median N")
|
|
404
|
+
axes[0].axhline(args.quasi_ext, color="red", ls="--", label=f"Ne={args.quasi_ext}")
|
|
405
|
+
axes[0].set_xlabel("Time (years)"); axes[0].set_ylabel("N")
|
|
406
|
+
axes[0].set_title(f"Stochastic trajectories ({args.n_sim} sims, N0={n0})")
|
|
407
|
+
axes[0].legend(); axes[0].set_ylim(bottom=0)
|
|
408
|
+
|
|
409
|
+
axes[1].plot(range(1, args.t_max + 1), ext_curve, color="darkred", lw=2)
|
|
410
|
+
for pct, label, col in [(0.50, "CR >=50%", "red"),
|
|
411
|
+
(0.20, "EN >=20%", "orange"),
|
|
412
|
+
(0.10, "VU >=10%", "goldenrod")]:
|
|
413
|
+
axes[1].axhline(pct, color=col, ls="--", lw=1, label=label)
|
|
414
|
+
axes[1].set_xlabel("Time (years)"); axes[1].set_ylabel("P(quasi-extinction)")
|
|
415
|
+
axes[1].set_title(f"Extinction curve (Ne={args.quasi_ext})")
|
|
416
|
+
axes[1].legend(); axes[1].set_ylim(0, 1)
|
|
417
|
+
|
|
418
|
+
plt.suptitle(f"PVA — IUCN Category: {risk_cat} | lambda={lam:.4f} | lambda_s={lam_s:.4f}")
|
|
419
|
+
plt.tight_layout()
|
|
420
|
+
fig.savefig(output_dir / "trajectory_plot.png", dpi=150)
|
|
421
|
+
plt.close(fig)
|
|
422
|
+
logger.info("Trajectory plot -> %s", output_dir / "trajectory_plot.png")
|
|
423
|
+
except ImportError:
|
|
424
|
+
logger.warning("matplotlib not available; skipping trajectory plot.")
|
|
425
|
+
|
|
426
|
+
logger.info("PVA analysis complete.")
|
|
427
|
+
|
|
428
|
+
except FileNotFoundError as e:
|
|
429
|
+
logger.error(
|
|
430
|
+
"Input file not found: %s\n"
|
|
431
|
+
" Expected output from: reproducible-ecology-pipeline\n"
|
|
432
|
+
" Check that previous step completed.",
|
|
433
|
+
e
|
|
434
|
+
)
|
|
435
|
+
raise
|
|
436
|
+
except Exception as e:
|
|
437
|
+
logger.error("Unexpected error in PVA analysis: %s", e)
|
|
438
|
+
raise
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
if __name__ == "__main__":
|
|
442
|
+
main()
|