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,304 @@
1
+ # ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ """Download occurrence records from iNaturalist via pyinaturalist.
5
+
6
+ Usage: python download_from_inat.py <species_name_or_list_csv> <output_dir> [year_from] [year_to] [quality_grade]
7
+
8
+ Arguments:
9
+ species_name_or_list_csv : Species name (e.g., "Panthera onca") or path to CSV
10
+ with column 'scientificName'
11
+ output_dir : Directory for outputs (created if absent)
12
+ year_from : Minimum observation year (default: 2000)
13
+ year_to : Maximum observation year (default: current year)
14
+ quality_grade : 'research' or 'any' (default: 'research')
15
+
16
+ Outputs (per species):
17
+ occurrences_raw_iNat_{species}_{date}.csv — standardised occurrence records
18
+ download_metadata_iNat_{species}.txt — download provenance and citation
19
+
20
+ Standard output schema:
21
+ species, decimalLatitude, decimalLongitude, eventDate, countryCode,
22
+ basisOfRecord, coordinateUncertaintyInMeters, datasetName, occurrenceID,
23
+ source, download_doi
24
+ """
25
+
26
+ import logging
27
+ import sys
28
+ from datetime import datetime
29
+ from pathlib import Path
30
+
31
+ SKILL_NAME = "ecological-data-foundation"
32
+ _LOG_DIR = Path("logs")
33
+ _LOG_DIR.mkdir(parents=True, exist_ok=True)
34
+ _log_file = _LOG_DIR / f"skill_{SKILL_NAME}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
35
+ logging.basicConfig(
36
+ level=logging.INFO,
37
+ format="[%(asctime)s] [%(levelname)s] [" + SKILL_NAME + "] %(message)s",
38
+ datefmt="%Y-%m-%d %H:%M:%S",
39
+ handlers=[
40
+ logging.StreamHandler(sys.stdout),
41
+ logging.FileHandler(_log_file, encoding="utf-8"),
42
+ ],
43
+ )
44
+ logger = logging.getLogger(SKILL_NAME)
45
+
46
+ def log_step(n: int, desc: str) -> None:
47
+ logger.info("-- STEP %d: %s", n, desc)
48
+
49
+ def log_decision(var: str, val, why: str) -> None:
50
+ logger.info("DECISION | %s = %s | %s", var, val, why)
51
+
52
+
53
+ from datetime import date
54
+
55
+ try:
56
+ import pandas as pd
57
+ import pyinaturalist
58
+ from pyinaturalist import get_observations
59
+ except ImportError as e:
60
+ logger.error(
61
+ "Dependencia ausente: %s\n Instale com: pip install pyinaturalist pandas\n Skill anterior: ecological-data-foundation",
62
+ e,
63
+ )
64
+ sys.exit(1)
65
+
66
+
67
+ # ── Constants ─────────────────────────────────────────────────────────────────
68
+ MAX_RESULTS_PER_PAGE = 200 # iNaturalist API page size limit
69
+ MAX_TOTAL_RESULTS = 10000 # practical cap for single-species query
70
+
71
+
72
+ # ── Helper functions ──────────────────────────────────────────────────────────
73
+
74
+ def fetch_observations(taxon_name: str, year_from: int, year_to: int,
75
+ quality_grade: str) -> list[dict]:
76
+ """Fetch all research-grade observations for a taxon, handling pagination."""
77
+ all_results = []
78
+ page = 1
79
+ while True:
80
+ try:
81
+ response = get_observations(
82
+ taxon_name=taxon_name,
83
+ quality_grade=quality_grade,
84
+ geo=True,
85
+ captive=False,
86
+ d1=f"{year_from}-01-01",
87
+ d2=f"{year_to}-12-31",
88
+ per_page=MAX_RESULTS_PER_PAGE,
89
+ page=page,
90
+ order="desc",
91
+ order_by="created_at",
92
+ )
93
+ except Exception as e:
94
+ logger.error(
95
+ "Falha em get_observations (pagina=%d, taxon='%s'): %s\n Causa provavel: sem conexao com a internet ou API iNaturalist indisponivel.\n Skill anterior: ecological-data-foundation",
96
+ page, taxon_name, e,
97
+ )
98
+ raise
99
+
100
+ results = response.get("results", [])
101
+ if not results:
102
+ break
103
+ all_results.extend(results)
104
+ logger.info("Pagina %d: %d registros recuperados (total acumulado: %d)",
105
+ page, len(results), len(all_results))
106
+
107
+ total_results = response.get("total_results", 0)
108
+ if len(all_results) >= min(total_results, MAX_TOTAL_RESULTS):
109
+ break
110
+ if len(results) < MAX_RESULTS_PER_PAGE:
111
+ break
112
+ page += 1
113
+
114
+ return all_results
115
+
116
+
117
+ def standardise_records(observations: list[dict], species_name: str) -> "pd.DataFrame":
118
+ """Convert iNaturalist observation dicts to the standard occurrence schema."""
119
+ rows = []
120
+ for obs in observations:
121
+ loc = obs.get("location", "")
122
+ lat, lon = None, None
123
+ if loc and "," in str(loc):
124
+ parts = str(loc).split(",")
125
+ try:
126
+ lat, lon = float(parts[0]), float(parts[1])
127
+ except ValueError:
128
+ pass
129
+
130
+ taxon = obs.get("taxon", {}) or {}
131
+ place = obs.get("place_guess", "")
132
+
133
+ rows.append({
134
+ "species": species_name,
135
+ "decimalLatitude": lat,
136
+ "decimalLongitude": lon,
137
+ "eventDate": obs.get("observed_on", ""),
138
+ "countryCode": place,
139
+ "basisOfRecord": "HUMAN_OBSERVATION",
140
+ "coordinateUncertaintyInMeters": obs.get("positional_accuracy"),
141
+ "datasetName": "iNaturalist",
142
+ "occurrenceID": str(obs.get("id", "")),
143
+ "source": "iNaturalist",
144
+ "download_doi": None,
145
+ })
146
+ df = pd.DataFrame(rows)
147
+ # Drop records missing coordinates
148
+ df = df.dropna(subset=["decimalLatitude", "decimalLongitude"])
149
+ return df
150
+
151
+
152
+ def save_metadata(output_dir: Path, species_name: str, n_records: int,
153
+ year_from: int, year_to: int, quality_grade: str) -> None:
154
+ safe_name = species_name.replace(" ", "_")
155
+ today = date.today().isoformat()
156
+ year = date.today().year
157
+ lines = [
158
+ f"Species: {species_name}",
159
+ f"Source: iNaturalist (https://www.inaturalist.org)",
160
+ f"Quality grade: {quality_grade}",
161
+ f"Year range: {year_from} - {year_to}",
162
+ f"Captive excluded: True",
163
+ f"Geo-referenced only: True",
164
+ f"n_records: {n_records}",
165
+ f"Download date: {today}",
166
+ (f"Citation: iNaturalist contributors and the California Academy of Sciences ({year}). "
167
+ f"iNaturalist Research-grade Observations. iNaturalist.org. Accessed {today}."),
168
+ "License: CC BY-NC (individual records may vary; see iNaturalist for details)",
169
+ "Note: iNaturalist does not issue download DOIs; record the access date for reproducibility.",
170
+ ]
171
+ meta_path = output_dir / f"download_metadata_iNat_{safe_name}.txt"
172
+ try:
173
+ meta_path.write_text("\n".join(lines), encoding="utf-8")
174
+ logger.info("Metadados gravados: %s", meta_path)
175
+ except OSError as e:
176
+ logger.error(
177
+ "Falha ao gravar metadados em '%s': %s\n Causa provavel: sem permissao de escrita.\n Skill anterior: ecological-data-foundation",
178
+ meta_path, e,
179
+ )
180
+ raise
181
+
182
+
183
+ # ── Main download logic ────────────────────────────────────────────────────────
184
+
185
+ def download_species(species_name: str, output_dir: Path,
186
+ year_from: int, year_to: int, quality_grade: str) -> None:
187
+ logger.info("--- Iniciando download iNaturalist: %s ---", species_name)
188
+ today_str = date.today().strftime("%Y%m%d")
189
+ safe_name = species_name.replace(" ", "_")
190
+
191
+ log_step(1, f"Buscar observacoes iNaturalist para '{species_name}'")
192
+ observations = fetch_observations(species_name, year_from, year_to, quality_grade)
193
+ logger.info("Total de observacoes recuperadas: %d", len(observations))
194
+
195
+ if not observations:
196
+ logger.warning("Nenhum registro encontrado para '%s'.", species_name)
197
+ return
198
+
199
+ log_step(2, "Padronizar registros para schema de saida")
200
+ df = standardise_records(observations, species_name)
201
+ n_final = len(df)
202
+ logger.info("Registros com coordenadas validas: %d", n_final)
203
+
204
+ if n_final < 30:
205
+ logger.warning(
206
+ "Registros insuficientes para SDM confiavel (n = %d). Considere: (1) ampliar periodo, (2) usar quality='any', (3) combinar com GBIF.",
207
+ n_final,
208
+ )
209
+
210
+ log_step(3, "Gravar CSV de ocorrencias")
211
+ csv_path = output_dir / f"occurrences_raw_iNat_{safe_name}_{today_str}.csv"
212
+ try:
213
+ df.to_csv(csv_path, index=False)
214
+ logger.info("Gravado: %s (%d registros)", csv_path, n_final)
215
+ except OSError as e:
216
+ logger.error(
217
+ "Falha ao gravar CSV para '%s': %s\n Causa provavel: sem permissao de escrita em '%s'.\n Skill anterior: ecological-data-foundation",
218
+ species_name, e, output_dir,
219
+ )
220
+ raise
221
+
222
+ log_step(4, "Gravar metadados do download")
223
+ save_metadata(output_dir, species_name, n_final, year_from, year_to, quality_grade)
224
+
225
+
226
+ # ── Entry point ────────────────────────────────────────────────────────────────
227
+
228
+ def main():
229
+ logger.info("Script: download_from_inat.py | Skill: %s", SKILL_NAME)
230
+
231
+ argv = sys.argv[1:]
232
+
233
+ if len(argv) < 2:
234
+ species_input = "Panthera onca"
235
+ output_dir = Path("output/inat")
236
+ year_from = 2000
237
+ year_to = date.today().year
238
+ quality_grade = "research"
239
+ logger.warning("Menos de 2 argumentos fornecidos. Usando valores padrao para teste.")
240
+ else:
241
+ species_input = argv[0]
242
+ output_dir = Path(argv[1])
243
+ year_from = int(argv[2]) if len(argv) >= 3 else 2000
244
+ year_to = int(argv[3]) if len(argv) >= 4 else date.today().year
245
+ quality_grade = argv[4] if len(argv) >= 5 else "research"
246
+
247
+ logger.info("Species input : %s", species_input)
248
+ logger.info("Output dir : %s", output_dir)
249
+ logger.info("Year range : %d - %d", year_from, year_to)
250
+ logger.info("Quality grade : %s", quality_grade)
251
+
252
+ log_decision("quality_grade", quality_grade,
253
+ "research = comunidade validou ID + geo; recomendado para SDM")
254
+ log_decision("captive", False,
255
+ "excluir organismos em cativeiro/cultivados")
256
+ log_decision("year_from", year_from,
257
+ "filtro temporal; 2000 equilibra dataset e qualidade de GPS")
258
+
259
+ output_dir.mkdir(parents=True, exist_ok=True)
260
+
261
+ # Build species list
262
+ log_step(0, "Construir lista de especies")
263
+ if species_input.endswith(".csv") and Path(species_input).exists():
264
+ try:
265
+ df_sp = pd.read_csv(species_input)
266
+ if "scientificName" not in df_sp.columns:
267
+ logger.error(
268
+ "Coluna 'scientificName' nao encontrada em: %s\n Causa provavel: CSV mal formatado.\n Skill anterior: ecological-data-foundation",
269
+ species_input,
270
+ )
271
+ sys.exit(1)
272
+ species_list = df_sp["scientificName"].dropna().unique().tolist()
273
+ logger.info("Modo batch: %d especies carregadas", len(species_list))
274
+ log_decision("mode", "batch", "CSV valido com coluna scientificName")
275
+ except Exception as e:
276
+ logger.error(
277
+ "Falha ao ler lista de especies '%s': %s\n Skill anterior: ecological-data-foundation",
278
+ species_input, e,
279
+ )
280
+ sys.exit(1)
281
+ else:
282
+ species_list = [species_input.strip()]
283
+ logger.info("Modo especie unica: %s", species_list[0])
284
+ log_decision("mode", "single_species", "argumento nao e arquivo CSV")
285
+
286
+ for sp in species_list:
287
+ try:
288
+ download_species(sp, output_dir, year_from, year_to, quality_grade)
289
+ except FileNotFoundError as e:
290
+ logger.error(
291
+ "Arquivo de entrada nao encontrado ao processar '%s': %s\n Esperado como saida de: ecological-data-foundation\n Skill anterior: ecological-data-foundation",
292
+ sp, e,
293
+ )
294
+ except Exception as e:
295
+ logger.error(
296
+ "Falha ao baixar '%s' do iNaturalist: %s\n Causa provavel: problema de rede ou especie nao encontrada.\n Skill anterior: ecological-data-foundation",
297
+ sp, e,
298
+ )
299
+
300
+ logger.info("Todos os downloads iNaturalist concluidos. Verifique: %s", output_dir)
301
+
302
+
303
+ if __name__ == "__main__":
304
+ main()
@@ -0,0 +1,273 @@
1
+ # ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ # Usage: Rscript download_from_iucn.R <species_name_or_list_csv> <output_dir> [include_range_maps]
5
+
6
+ # ── Inline logger ─────────────────────────────────────────────────────────────
7
+ SKILL_NAME <- "ecological-data-foundation"
8
+ .log_ts <- function() format(Sys.time(), "[%Y-%m-%d %H:%M:%S]")
9
+ log_info <- function(...) message(.log_ts(), " [INFO] ", sprintf(...))
10
+ log_warn <- function(...) message(.log_ts(), " [WARN] ", sprintf(...))
11
+ log_error<- function(...) message(.log_ts(), " [ERROR] ", sprintf(...))
12
+ log_step <- function(n, d) log_info("-- STEP %d: %s", n, d)
13
+ log_decision <- function(v, val, why) log_info("DECISION | %s = %s | %s", v, val, why)
14
+ dir.create("logs", recursive=TRUE, showWarnings=FALSE)
15
+
16
+ #
17
+ # Arguments:
18
+ # species_name_or_list_csv : Species name (e.g., "Panthera onca") or path to CSV
19
+ # with column "scientificName"
20
+ # output_dir : Directory to write outputs (created if absent)
21
+ # include_range_maps : "TRUE" or "FALSE" — download range map data (default: FALSE)
22
+ #
23
+ # Requires:
24
+ # IUCN_REDLIST_KEY environment variable — obtain at: https://apiv3.iucnredlist.org/
25
+ #
26
+ # Outputs (per species):
27
+ # iucn_status_{species}.csv — Red List category, criteria, population trend, threats
28
+ # iucn_habitats_{species}.csv — suitable habitats by species
29
+ # download_metadata_IUCN_{species}.txt — provenance and citation
30
+ # If include_range_maps=TRUE:
31
+ # range_maps/{species}_range.gpkg — range polygons (if available via API)
32
+ #
33
+ # Standard output schema (iucn_status CSV):
34
+ # species, decimalLatitude, decimalLongitude, eventDate, countryCode,
35
+ # basisOfRecord, coordinateUncertaintyInMeters, datasetName, occurrenceID,
36
+ # source, download_doi
37
+ # + IUCN-specific: rl_category, rl_criteria, population_trend, assessment_year
38
+
39
+ suppressPackageStartupMessages(library(rredlist))
40
+ suppressPackageStartupMessages(library(dplyr))
41
+ suppressPackageStartupMessages(library(readr))
42
+
43
+ # ── 1. Parse arguments ───────────────────────────────────────────────────────
44
+ log_step(1, "Analisar argumentos da linha de comando")
45
+ args <- commandArgs(trailingOnly = TRUE)
46
+
47
+ if (length(args) < 2) {
48
+ species_input <- "Panthera onca"
49
+ output_dir <- "output/iucn"
50
+ include_range_maps <- FALSE
51
+ log_warn("Menos de 2 argumentos fornecidos. Usando valores padrao para teste.")
52
+ } else {
53
+ species_input <- args[1]
54
+ output_dir <- args[2]
55
+ include_range_maps <- ifelse(
56
+ length(args) >= 3 && toupper(args[3]) == "TRUE", TRUE, FALSE
57
+ )
58
+ }
59
+
60
+ log_info("Script: download_from_iucn.R | Skill: %s", SKILL_NAME)
61
+ log_info("Species input : %s", species_input)
62
+ log_info("Output dir : %s", output_dir)
63
+ log_info("Include range maps : %s", include_range_maps)
64
+
65
+ log_decision("api_version", "v3", "IUCN Red List API v3 — requer chave em IUCN_REDLIST_KEY")
66
+
67
+ # ── 2. Check API key ─────────────────────────────────────────────────────────
68
+ log_step(2, "Verificar chave da API IUCN")
69
+ iucn_key <- Sys.getenv("IUCN_REDLIST_KEY")
70
+ if (is.null(iucn_key) || iucn_key == "") {
71
+ log_error(
72
+ "Falha em verificar chave API IUCN: variavel IUCN_REDLIST_KEY nao definida.\nCausa provavel: chave nao configurada no ambiente.\nVerifique: adicione IUCN_REDLIST_KEY ao seu .Renviron via usethis::edit_r_environ()\nSkill anterior: ecological-data-foundation"
73
+ )
74
+ stop("IUCN_REDLIST_KEY environment variable not set.")
75
+ }
76
+ log_info("Chave IUCN detectada (primeiros 4 chars): %s...", substr(iucn_key, 1, 4))
77
+
78
+ # ── 3. Create output directory ───────────────────────────────────────────────
79
+ log_step(3, "Criar diretorio de saida")
80
+ dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
81
+
82
+ # ── 4. Build species list ────────────────────────────────────────────────────
83
+ log_step(4, "Construir lista de especies")
84
+ if (grepl("\\.csv$", species_input, ignore.case = TRUE) && file.exists(species_input)) {
85
+ tryCatch({
86
+ species_df <- read_csv(species_input, show_col_types = FALSE)
87
+ if (!"scientificName" %in% names(species_df)) {
88
+ log_error(
89
+ "Coluna 'scientificName' nao encontrada em: %s\nCausa provavel: CSV mal formatado.\nSkill anterior: ecological-data-foundation",
90
+ species_input
91
+ )
92
+ stop("Missing column 'scientificName'")
93
+ }
94
+ species_list <- unique(trimws(species_df$scientificName))
95
+ log_info("Modo batch: %d especies carregadas", length(species_list))
96
+ log_decision("mode", "batch", "CSV valido com coluna scientificName")
97
+ }, error = function(e) {
98
+ log_error(
99
+ "Falha ao ler lista de especies: %s\nSkill anterior: ecological-data-foundation",
100
+ conditionMessage(e)
101
+ )
102
+ stop(e)
103
+ })
104
+ } else {
105
+ species_list <- trimws(species_input)
106
+ log_info("Modo especie unica: %s", species_list)
107
+ log_decision("mode", "single_species", "argumento nao e arquivo CSV")
108
+ }
109
+
110
+ # ── 5. Download function ─────────────────────────────────────────────────────
111
+ download_iucn_species <- function(sp_name) {
112
+ log_info("--- Iniciando download IUCN: %s ---", sp_name)
113
+ safe_name <- gsub(" ", "_", sp_name)
114
+
115
+ # ── Fetch species assessment ───────────────────────────────────────────────
116
+ assessment <- tryCatch({
117
+ rredlist::rl_search(sp_name, key = iucn_key)
118
+ }, error = function(e) {
119
+ log_error(
120
+ "Falha em rl_search para '%s': %s\nCausa provavel: chave IUCN invalida ou API indisponivel.\nVerifique: https://apiv3.iucnredlist.org/\nSkill anterior: ecological-data-foundation",
121
+ sp_name, conditionMessage(e)
122
+ )
123
+ stop(e)
124
+ })
125
+
126
+ if (is.null(assessment$result) || length(assessment$result) == 0) {
127
+ log_warn("Nenhum resultado IUCN para '%s'. Especie pode nao estar avaliada.", sp_name)
128
+ return(invisible(NULL))
129
+ }
130
+
131
+ res <- assessment$result[[1]]
132
+ category <- res$category %||% NA_character_
133
+ criteria <- res$criteria %||% NA_character_
134
+ pop_trend<- res$population_trend %||% NA_character_
135
+ assess_yr<- res$assessment_year %||% NA_integer_
136
+ taxon_id <- res$taxonid %||% NA_integer_
137
+
138
+ log_info("Categoria IUCN para '%s': %s (criterios: %s, tendencia: %s, ano: %s)",
139
+ sp_name, category, criteria, pop_trend, assess_yr)
140
+
141
+ if (category %in% c("CR", "EN")) {
142
+ log_warn("Especie '%s' e %s — dados de distribuicao podem ser restritos por razoes de seguranca.",
143
+ sp_name, category)
144
+ }
145
+
146
+ # ── Fetch country occurrences ──────────────────────────────────────────────
147
+ country_occ <- tryCatch({
148
+ rredlist::rl_occ_country(sp_name, key = iucn_key)
149
+ }, error = function(e) {
150
+ log_warn("Falha ao buscar paises de ocorrencia para '%s': %s", sp_name, conditionMessage(e))
151
+ list(result = NULL)
152
+ })
153
+
154
+ # ── Fetch habitats ─────────────────────────────────────────────────────────
155
+ habitats <- tryCatch({
156
+ rredlist::rl_habitats(sp_name, key = iucn_key)
157
+ }, error = function(e) {
158
+ log_warn("Falha ao buscar habitats para '%s': %s", sp_name, conditionMessage(e))
159
+ list(result = NULL)
160
+ })
161
+
162
+ # ── Build standardised occurrence records (one row per country) ────────────
163
+ if (!is.null(country_occ$result) && length(country_occ$result) > 0) {
164
+ country_df <- as.data.frame(country_occ$result)
165
+ std <- data.frame(
166
+ species = sp_name,
167
+ decimalLatitude = NA_real_,
168
+ decimalLongitude = NA_real_,
169
+ eventDate = NA_character_,
170
+ countryCode = as.character(country_df$code),
171
+ basisOfRecord = "LITERATURE",
172
+ coordinateUncertaintyInMeters = NA_real_,
173
+ datasetName = "IUCN Red List",
174
+ occurrenceID = paste0("IUCN:", taxon_id, ":", as.character(country_df$code)),
175
+ source = "IUCN",
176
+ download_doi = NA_character_,
177
+ rl_category = category,
178
+ rl_criteria = criteria,
179
+ population_trend = pop_trend,
180
+ assessment_year = assess_yr,
181
+ stringsAsFactors = FALSE
182
+ )
183
+ } else {
184
+ log_warn("Sem dados de ocorrencia por pais para '%s'. Criando registro sumario.", sp_name)
185
+ std <- data.frame(
186
+ species = sp_name,
187
+ decimalLatitude = NA_real_,
188
+ decimalLongitude = NA_real_,
189
+ eventDate = NA_character_,
190
+ countryCode = NA_character_,
191
+ basisOfRecord = "LITERATURE",
192
+ coordinateUncertaintyInMeters = NA_real_,
193
+ datasetName = "IUCN Red List",
194
+ occurrenceID = paste0("IUCN:", taxon_id),
195
+ source = "IUCN",
196
+ download_doi = NA_character_,
197
+ rl_category = category,
198
+ rl_criteria = criteria,
199
+ population_trend = pop_trend,
200
+ assessment_year = assess_yr,
201
+ stringsAsFactors = FALSE
202
+ )
203
+ }
204
+
205
+ # Save status CSV
206
+ csv_path <- file.path(output_dir, paste0("iucn_status_", safe_name, ".csv"))
207
+ tryCatch({
208
+ write_csv(std, csv_path)
209
+ log_info("Gravado: %s (%d linhas)", csv_path, nrow(std))
210
+ }, error = function(e) {
211
+ log_error(
212
+ "Falha ao gravar CSV para '%s': %s\nSkill anterior: ecological-data-foundation",
213
+ sp_name, conditionMessage(e)
214
+ )
215
+ stop(e)
216
+ })
217
+
218
+ # Save habitats CSV
219
+ if (!is.null(habitats$result) && length(habitats$result) > 0) {
220
+ hab_df <- as.data.frame(habitats$result)
221
+ hab_df$species <- sp_name
222
+ hab_path <- file.path(output_dir, paste0("iucn_habitats_", safe_name, ".csv"))
223
+ tryCatch({
224
+ write_csv(hab_df, hab_path)
225
+ log_info("Habitats gravados: %s", hab_path)
226
+ }, error = function(e) {
227
+ log_warn("Falha ao gravar habitats para '%s': %s", sp_name, conditionMessage(e))
228
+ })
229
+ }
230
+
231
+ # Save metadata
232
+ meta_lines <- c(
233
+ paste("Species:", sp_name),
234
+ paste("IUCN Taxon ID:", taxon_id),
235
+ paste("Source: IUCN Red List (https://www.iucnredlist.org)"),
236
+ paste("API version: v3 (https://apiv3.iucnredlist.org)"),
237
+ paste("Red List category:", category),
238
+ paste("Assessment year:", assess_yr),
239
+ paste("Population trend:", pop_trend),
240
+ paste("Download date:", Sys.Date()),
241
+ paste("Citation: IUCN", format(Sys.Date(), "%Y"), ". The IUCN Red List of Threatened Species. Version",
242
+ format(Sys.Date(), "%Y-%m"), ". https://www.iucnredlist.org Accessed on", Sys.Date()),
243
+ paste("License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)")
244
+ )
245
+ meta_path <- file.path(output_dir, paste0("download_metadata_IUCN_", safe_name, ".txt"))
246
+ tryCatch({
247
+ writeLines(meta_lines, meta_path)
248
+ log_info("Gravado: %s", meta_path)
249
+ }, error = function(e) {
250
+ log_warn("Falha ao gravar metadados para '%s': %s", sp_name, conditionMessage(e))
251
+ })
252
+
253
+ return(invisible(csv_path))
254
+ }
255
+
256
+ # Null-coalescing operator
257
+ `%||%` <- function(a, b) if (!is.null(a) && !is.na(a) && length(a) > 0) a else b
258
+
259
+ # ── 6. Run for all species ───────────────────────────────────────────────────
260
+ log_step(5, "Executar download IUCN para todas as especies")
261
+ for (sp in species_list) {
262
+ tryCatch(
263
+ download_iucn_species(sp),
264
+ error = function(e) {
265
+ log_error(
266
+ "Falha ao baixar '%s' do IUCN: %s\nCausa provavel: chave invalida, especie nao avaliada ou API indisponivel.\nSkill anterior: ecological-data-foundation",
267
+ sp, conditionMessage(e)
268
+ )
269
+ }
270
+ )
271
+ }
272
+
273
+ log_info("Todos os downloads IUCN concluidos. Verifique: %s", output_dir)