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,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()