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,305 @@
1
+ # ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ # Usage: Rscript recovery_trajectory.R <timeseries.csv> <disturbance_date> <output_dir>
5
+ # Estimate post-disturbance vegetation recovery trajectory
6
+ # Usage: Rscript recovery_trajectory.R <timeseries_csv> <disturbance_date> <output_dir>
7
+ # Requires: dplyr, ggplot2, zoo, broom, lubridate
8
+
9
+ # ── Inline logger ─────────────────────────────────────────────────────────────
10
+ SKILL_NAME <- "environmental-time-series"
11
+ .log_ts <- function() format(Sys.time(), "[%Y-%m-%d %H:%M:%S]")
12
+ log_info <- function(...) message(.log_ts(), " [INFO] ", sprintf(...))
13
+ log_warn <- function(...) message(.log_ts(), " [WARN] ", sprintf(...))
14
+ log_error<- function(...) message(.log_ts(), " [ERROR] ", sprintf(...))
15
+ log_step <- function(n, d) log_info("-- STEP %d: %s", n, d)
16
+ log_decision <- function(v, val, why) log_info("DECISION | %s = %s | %s", v, val, why)
17
+ dir.create("logs", recursive=TRUE, showWarnings=FALSE)
18
+
19
+ suppressPackageStartupMessages({
20
+ library(dplyr)
21
+ library(ggplot2)
22
+ library(zoo)
23
+ library(broom)
24
+ })
25
+
26
+ args <- commandArgs(trailingOnly = TRUE)
27
+ ts_file <- ifelse(length(args) >= 1, args[1], "tests/data/ndvi_monthly_series.csv")
28
+ disturbance_date <- ifelse(length(args) >= 2, args[2], "2010-01-01")
29
+ output_dir <- ifelse(length(args) >= 3, args[3], "outputs/recovery")
30
+ dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
31
+
32
+ log_info("Skill: %s | ts_file=%s | disturbance_date=%s | output_dir=%s",
33
+ SKILL_NAME, ts_file, disturbance_date, output_dir)
34
+
35
+ # ── Input precondition check ──────────────────────────────────────────────────
36
+ if (!file.exists(ts_file)) {
37
+ log_error(
38
+ "Input nao encontrado: %s\nCausa provavel: serie temporal nao gerada pelo passo anterior ou caminho incorreto.\nVerifique: execute primeiro o script de extracao de NDVI ou forneca o CSV correto.\nSkill anterior: geoprocessing-for-ecology ou remote-sensing-analysis.",
39
+ ts_file
40
+ )
41
+ stop("Missing: ", ts_file)
42
+ }
43
+
44
+ # ── 1. Load and parse ──────────────────────────────────────────────────────────
45
+ log_step(1, "Carregar e parsear serie temporal")
46
+ dat <- tryCatch({
47
+ read.csv(ts_file)
48
+ }, error = function(e) {
49
+ log_error(
50
+ "Falha ao ler CSV de serie temporal: %s\nCausa provavel: arquivo corrompido, encoding incorreto ou separador diferente de virgula.\nVerifique: abra o arquivo em editor de texto e confira o formato.\nSkill anterior: geoprocessing-for-ecology.",
51
+ conditionMessage(e)
52
+ )
53
+ stop(e)
54
+ })
55
+
56
+ val_col <- if ("value" %in% names(dat)) "value" else names(dat)[ncol(dat)]
57
+ date_col <- if ("date" %in% names(dat)) "date" else names(dat)[1]
58
+
59
+ log_decision("val_col", val_col, "coluna 'value' usada se presente, caso contrario ultima coluna numerica")
60
+ log_decision("date_col", date_col, "coluna 'date' usada se presente, caso contrario primeira coluna")
61
+
62
+ dat[[date_col]] <- tryCatch({
63
+ as.Date(dat[[date_col]])
64
+ }, error = function(e) {
65
+ log_error(
66
+ "Falha ao converter coluna '%s' para Date: %s\nCausa provavel: formato de data nao reconhecido (esperado YYYY-MM-DD).\nVerifique: todos os valores da coluna de data devem seguir o formato ISO 8601.\nSkill anterior: geoprocessing-for-ecology.",
67
+ date_col, conditionMessage(e)
68
+ )
69
+ stop(e)
70
+ })
71
+
72
+ dist_date <- tryCatch({
73
+ as.Date(disturbance_date)
74
+ }, error = function(e) {
75
+ log_error(
76
+ "Falha ao parsear disturbance_date='%s': %s\nCausa provavel: formato invalido; use YYYY-MM-DD.\nVerifique: o segundo argumento do script.\nSkill anterior: nenhuma.",
77
+ disturbance_date, conditionMessage(e)
78
+ )
79
+ stop(e)
80
+ })
81
+
82
+ n_na_val <- sum(is.na(dat[[val_col]]))
83
+ if (n_na_val > 0) {
84
+ log_warn("Coluna '%s' contem %d valores NA — podem afetar calculo de baseline e minimo.", val_col, n_na_val)
85
+ }
86
+
87
+ log_info("Comprimento da serie: %d | Data de disturbio: %s", nrow(dat), format(dist_date))
88
+
89
+ # ── 2. Define periods ──────────────────────────────────────────────────────────
90
+ log_step(2, "Separar periodos pre e pos-disturbio")
91
+ pre <- dat[dat[[date_col]] < dist_date, ]
92
+ post <- dat[dat[[date_col]] >= dist_date, ]
93
+ log_info("Obs pre-disturbio: %d | Pos: %d", nrow(pre), nrow(post))
94
+ log_decision("period_split", format(dist_date),
95
+ "divisao estrita: pre < disturbance_date, pos >= disturbance_date")
96
+
97
+ if (nrow(pre) < 12) {
98
+ log_error(
99
+ "Observacoes pre-disturbio insuficientes: %d (minimo 12 necessario).\nCausa provavel: a data de disturbio e muito proxima ao inicio da serie.\nVerifique: a data de disturbio e os dados disponiveis antes dela.\nSkill anterior: geoprocessing-for-ecology.",
100
+ nrow(pre)
101
+ )
102
+ stop("Need at least 12 pre-disturbance observations for baseline.")
103
+ }
104
+
105
+ if (nrow(post) < 5) {
106
+ log_warn("Apenas %d observacoes pos-disturbio — ajuste de curva pode ser instavel.", nrow(post))
107
+ }
108
+
109
+ # ── 3. Baseline statistics ─────────────────────────────────────────────────────
110
+ log_step(3, "Calcular estatisticas de baseline pre-disturbio (ultimos 24 meses)")
111
+ recent_pre <- tail(pre, 24)
112
+ baseline_mean <- mean(recent_pre[[val_col]], na.rm = TRUE)
113
+ baseline_sd <- sd(recent_pre[[val_col]], na.rm = TRUE)
114
+ log_info("Baseline pre-disturbio: %.4f +/- %.4f (n=%d obs)", baseline_mean, baseline_sd, nrow(recent_pre))
115
+ log_decision("baseline_window", "24 observacoes mais recentes antes do disturbio",
116
+ "janela de 2 anos captura condicoes imediatamente anteriores ao impacto")
117
+
118
+ if (nrow(recent_pre) < 24) {
119
+ log_warn("Janela de baseline reduzida para %d obs (esperado 24) — serie pre-disturbio curta.", nrow(recent_pre))
120
+ }
121
+
122
+ # ── 4. Minimum post-disturbance value ─────────────────────────────────────────
123
+ log_step(4, "Identificar minimo pos-disturbio com suavizacao (janela 3)")
124
+ post_smooth <- tryCatch({
125
+ rollapply(post[[val_col]], width = 3, FUN = mean, align = "center", fill = NA)
126
+ }, error = function(e) {
127
+ log_error(
128
+ "Falha na suavizacao rolante: %s\nCausa provavel: serie pos-disturbio muito curta para janela de 3 observacoes.\nVerifique: numero de observacoes pos-disturbio.\nSkill anterior: nenhuma.",
129
+ conditionMessage(e)
130
+ )
131
+ stop(e)
132
+ })
133
+
134
+ min_val <- min(post_smooth, na.rm = TRUE)
135
+ min_idx <- which.min(post_smooth)
136
+ min_date <- post[[date_col]][min_idx]
137
+ log_info("Minimo pos-disturbio: %.4f em %s", min_val, format(min_date))
138
+ log_decision("smoothing_width", "3",
139
+ "suavizacao com janela de 3 reduz ruido de sensor sem mascarar dinamica de recuperacao")
140
+
141
+ if (baseline_mean <= min_val) {
142
+ log_warn("Minimo pos-disturbio (%.4f) nao e menor que a media baseline (%.4f) — possivel ausencia de disturbio detectavel.", min_val, baseline_mean)
143
+ }
144
+
145
+ # ── 5. Recovery Indicator (RI) ─────────────────────────────────────────────────
146
+ log_step(5, "Calcular Recovery Indicator (RI) e salvar serie")
147
+ # RI_t = (value_t - min_val) / (baseline_mean - min_val)
148
+ post$RI <- (post[[val_col]] - min_val) / (baseline_mean - min_val + 1e-10)
149
+
150
+ tryCatch({
151
+ write.csv(post[, c(date_col, val_col, "RI")],
152
+ file.path(output_dir, "recovery_indicator.csv"), row.names = FALSE)
153
+ log_info("recovery_indicator.csv salvo em: %s", output_dir)
154
+ }, error = function(e) {
155
+ log_error(
156
+ "Falha ao salvar recovery_indicator.csv: %s\nCausa provavel: permissao negada ou disco cheio.\nVerifique: permissoes do diretorio de saida.\nSkill anterior: nenhuma.",
157
+ conditionMessage(e)
158
+ )
159
+ stop(e)
160
+ })
161
+
162
+ log_decision("RI_formula", "RI = (value - min) / (baseline_mean - min + 1e-10)",
163
+ "normalizacao 0->1 onde 0=minimo pos-disturbio e 1=baseline pre-disturbio; epsilon evita divisao por zero")
164
+
165
+ # ── 6. Fit recovery curves ─────────────────────────────────────────────────────
166
+ log_step(6, "Ajustar curvas de recuperacao (linear e exponencial)")
167
+ # t = months since minimum
168
+ post$t_months <- as.numeric(difftime(post[[date_col]], min_date, units = "days")) / 30.44
169
+ post_fit <- post[post$t_months >= 0, ]
170
+
171
+ results_list <- list()
172
+
173
+ m_lin <- tryCatch({
174
+ lm(RI ~ t_months, data = post_fit)
175
+ }, error = function(e) {
176
+ log_error(
177
+ "Falha ao ajustar modelo linear de recuperacao: %s\nCausa provavel: dados pos-disturbio insuficientes ou sem variacao em t_months.\nVerifique: numero de observacoes pos-disturbio apos o minimo.\nSkill anterior: nenhuma.",
178
+ conditionMessage(e)
179
+ )
180
+ stop(e)
181
+ })
182
+
183
+ results_list$linear <- broom::glance(m_lin) |>
184
+ mutate(model = "linear", formula = "RI ~ t")
185
+ log_info("Modelo linear ajustado: R2=%.4f", summary(m_lin)$r.squared)
186
+
187
+ # Exponential (log-linear)
188
+ post_fit_pos <- post_fit[post_fit$RI > 0.01, ] # avoid log(0)
189
+ if (nrow(post_fit_pos) > 5) {
190
+ tryCatch({
191
+ m_exp <- lm(log(RI) ~ t_months, data = post_fit_pos)
192
+ results_list$exponential <- broom::glance(m_exp) |>
193
+ mutate(model = "exponential", formula = "log(RI) ~ t")
194
+ log_info("Modelo exponencial ajustado: R2=%.4f", summary(m_exp)$r.squared)
195
+ }, error = function(e) {
196
+ log_warn("Falha ao ajustar modelo exponencial: %s — continuando apenas com modelo linear.", conditionMessage(e))
197
+ })
198
+ } else {
199
+ log_warn("Apenas %d obs com RI > 0.01 — modelo exponencial nao ajustado (minimo 5 necessario).", nrow(post_fit_pos))
200
+ }
201
+
202
+ # ── 7. Estimate time to 80% and 100% recovery ─────────────────────────────────
203
+ log_step(7, "Estimar tempo para 80%% e 100%% de recuperacao (modelo linear)")
204
+ slope <- coef(m_lin)[["t_months"]]
205
+ intercept <- coef(m_lin)[["(Intercept)"]]
206
+ # RI = intercept + slope * t → t = (RI_target - intercept) / slope
207
+ t_80 <- if (slope > 0) round((0.80 - intercept) / slope, 1) else NA
208
+ t_100 <- if (slope > 0) round((1.00 - intercept) / slope, 1) else NA
209
+
210
+ log_info("Tempo estimado para 80%% de recuperacao: %s meses", ifelse(is.na(t_80), "NA (inclinacao negativa)", t_80))
211
+ log_info("Tempo estimado para 100%% de recuperacao: %s meses", ifelse(is.na(t_100), "NA (inclinacao negativa)", t_100))
212
+ log_decision("recovery_model", "linear",
213
+ "modelo linear usado para projecao de tempo de recuperacao por simplicidade e interpretabilidade")
214
+
215
+ if (slope <= 0) {
216
+ log_warn("Inclinacao linear negativa (%.6f) — sem recuperacao detectada no periodo pos-disturbio.", slope)
217
+ }
218
+
219
+ recovery_metrics <- data.frame(
220
+ baseline_mean = round(baseline_mean, 4),
221
+ baseline_sd = round(baseline_sd, 4),
222
+ disturbance_date = format(dist_date),
223
+ post_minimum_value = round(min_val, 4),
224
+ post_minimum_date = format(min_date),
225
+ magnitude_of_decline = round((baseline_mean - min_val) / baseline_mean * 100, 2),
226
+ RI_current = round(tail(post$RI, 1), 4),
227
+ slope_linear = round(slope, 6),
228
+ r2_linear = round(summary(m_lin)$r.squared, 4),
229
+ t_to_80pct_months = t_80,
230
+ t_to_100pct_months = t_100
231
+ )
232
+
233
+ tryCatch({
234
+ write.csv(recovery_metrics, file.path(output_dir, "recovery_metrics.csv"), row.names = FALSE)
235
+ log_info("recovery_metrics.csv salvo em: %s", output_dir)
236
+ }, error = function(e) {
237
+ log_error(
238
+ "Falha ao salvar recovery_metrics.csv: %s\nCausa provavel: permissao negada ou disco cheio.\nVerifique: permissoes do diretorio de saida.\nSkill anterior: nenhuma.",
239
+ conditionMessage(e)
240
+ )
241
+ stop(e)
242
+ })
243
+
244
+ # ── 8. Plot recovery trajectory ────────────────────────────────────────────────
245
+ log_step(8, "Gerar grafico de trajetoria de recuperacao")
246
+ tryCatch({
247
+ pred_df <- data.frame(t_months = seq(0, max(post_fit$t_months, na.rm=TRUE), by=1))
248
+ pred_df$RI_pred <- intercept + slope * pred_df$t_months
249
+
250
+ p <- ggplot() +
251
+ geom_hline(yintercept = 1.0, linetype = "dashed", colour = "forestgreen", alpha = 0.7) +
252
+ geom_hline(yintercept = 0.8, linetype = "dashed", colour = "orange", alpha = 0.7) +
253
+ geom_line(data = post_fit, aes(x = t_months, y = RI), colour = "grey50", linewidth = 0.8) +
254
+ geom_point(data = post_fit, aes(x = t_months, y = RI), size = 1.5, alpha = 0.7) +
255
+ geom_line(data = pred_df, aes(x = t_months, y = RI_pred),
256
+ colour = "#2166ac", linewidth = 1.1, linetype = "solid") +
257
+ annotate("text", x = max(post_fit$t_months)*0.05, y = 1.02, label = "100% recovery",
258
+ colour = "forestgreen", size = 3, hjust = 0) +
259
+ annotate("text", x = max(post_fit$t_months)*0.05, y = 0.82, label = "80% recovery",
260
+ colour = "orange", size = 3, hjust = 0) +
261
+ labs(x = "Months since post-disturbance minimum",
262
+ y = "Recovery Indicator (RI)",
263
+ title = "Post-Disturbance Recovery Trajectory",
264
+ subtitle = paste0("Disturbance: ", format(dist_date),
265
+ " | Linear model R\u00b2 = ", round(summary(m_lin)$r.squared, 3))) +
266
+ theme_bw()
267
+
268
+ ggsave(file.path(output_dir, "recovery_trajectory.png"), p, width = 8, height = 5, dpi = 150)
269
+ log_info("recovery_trajectory.png salvo em: %s", output_dir)
270
+ }, error = function(e) {
271
+ log_error(
272
+ "Falha ao gerar grafico de trajetoria de recuperacao: %s\nCausa provavel: dados insuficientes para projecao ou diretorio sem permissao de escrita.\nVerifique: se post_fit contem observacoes validas.\nSkill anterior: nenhuma.",
273
+ conditionMessage(e)
274
+ )
275
+ stop(e)
276
+ })
277
+
278
+ # ── 9. Full time series context plot ───────────────────────────────────────────
279
+ log_step(9, "Gerar grafico de contexto da serie temporal completa")
280
+ tryCatch({
281
+ p2 <- ggplot(dat, aes(x = .data[[date_col]], y = .data[[val_col]])) +
282
+ geom_line(colour = "grey60", linewidth = 0.6) +
283
+ geom_vline(xintercept = as.numeric(dist_date), linetype = "dashed",
284
+ colour = "red", linewidth = 0.8) +
285
+ geom_hline(yintercept = baseline_mean, linetype = "dotted",
286
+ colour = "forestgreen", linewidth = 0.8) +
287
+ annotate("text", x = dist_date, y = max(dat[[val_col]], na.rm=TRUE),
288
+ label = " Disturbance", hjust = 0, colour = "red", size = 3.2) +
289
+ annotate("text", x = min(dat[[date_col]]), y = baseline_mean + 0.005,
290
+ label = "Pre-disturbance baseline", hjust = 0, colour = "forestgreen", size = 3) +
291
+ labs(x = NULL, y = val_col, title = "Full NDVI Time Series with Disturbance Event") +
292
+ theme_bw()
293
+ ggsave(file.path(output_dir, "timeseries_context.png"), p2, width = 10, height = 4, dpi = 150)
294
+ log_info("timeseries_context.png salvo em: %s", output_dir)
295
+ }, error = function(e) {
296
+ log_error(
297
+ "Falha ao gerar grafico de contexto da serie temporal: %s\nCausa provavel: colunas de data ou valor invalidas ou diretorio sem permissao de escrita.\nVerifique: integridade do CSV de entrada.\nSkill anterior: geoprocessing-for-ecology.",
298
+ conditionMessage(e)
299
+ )
300
+ stop(e)
301
+ })
302
+
303
+ log_info("Analise de recuperacao concluida. Saidas em: %s", output_dir)
304
+ log_info("Metricas-chave:")
305
+ print(t(recovery_metrics))
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env python3
2
+ # ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ """
6
+ recovery_trajectory.py
7
+ Estimate post-disturbance vegetation recovery trajectory.
8
+ Usage: python recovery_trajectory.py <timeseries_csv> <disturbance_date> <output_dir>
9
+ Requires: pandas, numpy, scipy, matplotlib
10
+ """
11
+ import logging
12
+ import sys
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+
16
+ SKILL_NAME = "environmental-time-series"
17
+ _LOG_DIR = Path("logs")
18
+ _LOG_DIR.mkdir(parents=True, exist_ok=True)
19
+ _log_file = _LOG_DIR / f"skill_{SKILL_NAME}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
20
+ logging.basicConfig(
21
+ level=logging.INFO,
22
+ format="[%(asctime)s] [%(levelname)s] [" + SKILL_NAME + "] %(message)s",
23
+ datefmt="%Y-%m-%d %H:%M:%S",
24
+ handlers=[
25
+ logging.StreamHandler(sys.stdout),
26
+ logging.FileHandler(_log_file, encoding="utf-8"),
27
+ ],
28
+ )
29
+ logger = logging.getLogger(SKILL_NAME)
30
+
31
+ def log_step(n: int, desc: str) -> None:
32
+ logger.info("-- STEP %d: %s", n, desc)
33
+
34
+ def log_decision(var: str, val, why: str) -> None:
35
+ logger.info("DECISION | %s = %s | %s", var, val, why)
36
+
37
+ import numpy as np
38
+ import pandas as pd
39
+ import matplotlib.pyplot as plt
40
+ from scipy.stats import linregress
41
+ from scipy.ndimage import uniform_filter1d
42
+
43
+
44
+ def main():
45
+ ts_file = sys.argv[1] if len(sys.argv) > 1 else "tests/data/ndvi_monthly_series.csv"
46
+ dist_date = sys.argv[2] if len(sys.argv) > 2 else "2010-01-01"
47
+ output_dir = Path(sys.argv[3]) if len(sys.argv) > 3 else Path("outputs/recovery")
48
+ output_dir.mkdir(parents=True, exist_ok=True)
49
+
50
+ log_decision("ts_file", ts_file, "Input time series CSV")
51
+ log_decision("dist_date", dist_date, "Disturbance event date for pre/post split")
52
+ log_decision("output_dir", str(output_dir), "Directory for recovery outputs")
53
+
54
+ if not Path(ts_file).exists():
55
+ logger.error(
56
+ "Input nao encontrado: %s\n"
57
+ " Causa provavel: passo anterior nao concluiu.\n"
58
+ " Skill anterior que deveria ter produzido este input: geoprocessing-for-ecology",
59
+ ts_file
60
+ )
61
+ sys.exit(1)
62
+
63
+ try:
64
+ log_step(1, "Loading and splitting time series at disturbance date")
65
+ dat = pd.read_csv(ts_file, parse_dates=[0])
66
+ date_col = dat.columns[0]
67
+ val_col = "value" if "value" in dat.columns else dat.columns[-1]
68
+ dist_dt = pd.to_datetime(dist_date)
69
+
70
+ pre = dat[dat[date_col] < dist_dt].copy()
71
+ post = dat[dat[date_col] >= dist_dt].copy()
72
+ logger.info("Pre: %d obs | Post: %d obs | Disturbance: %s", len(pre), len(post), dist_date)
73
+ if len(pre) < 12:
74
+ raise ValueError("Need >= 12 pre-disturbance observations.")
75
+ if len(post) == 0:
76
+ logger.warning("No post-disturbance observations found. Recovery metrics will be empty.")
77
+
78
+ log_step(2, "Computing pre-disturbance baseline (last 24 months)")
79
+ # Baseline: last 24 pre-disturbance months
80
+ recent_pre = pre.tail(24)
81
+ baseline_mean = recent_pre[val_col].mean()
82
+ baseline_sd = recent_pre[val_col].std()
83
+ log_decision("baseline_window", 24,
84
+ "Last 24 pre-disturbance observations used as baseline reference period")
85
+ logger.info("Baseline: %.4f +/- %.4f", baseline_mean, baseline_sd)
86
+
87
+ log_step(3, "Detecting post-disturbance minimum via smoothed series")
88
+ # Smoothed minimum
89
+ smooth = uniform_filter1d(post[val_col].values, size=3)
90
+ min_idx = np.nanargmin(smooth)
91
+ min_val = smooth[min_idx]
92
+ min_date = post[date_col].iloc[min_idx]
93
+ logger.info("Post-disturbance minimum: %.4f at %s", min_val, min_date.date())
94
+
95
+ log_step(4, "Computing Recovery Indicator (RI) and linear fit")
96
+ # Recovery Indicator
97
+ post = post.copy()
98
+ post["RI"] = (post[val_col] - min_val) / (baseline_mean - min_val + 1e-10)
99
+ post["t_months"] = ((post[date_col] - min_date).dt.days / 30.44).clip(lower=0)
100
+ post.to_csv(output_dir / "recovery_indicator.csv", index=False)
101
+
102
+ # Linear fit
103
+ fit_df = post[post["t_months"] >= 0].dropna(subset=["RI"])
104
+ slope, intercept, r, p_val, se = linregress(fit_df["t_months"], fit_df["RI"])
105
+ r2 = r**2
106
+ logger.info("Linear fit: slope=%.5f/month | R2=%.3f", slope, r2)
107
+
108
+ t_80 = round((0.80 - intercept) / slope, 1) if slope > 0 else None
109
+ t_100 = round((1.00 - intercept) / slope, 1) if slope > 0 else None
110
+ logger.info("Estimated recovery: 80%% at %s months | 100%% at %s months", t_80, t_100)
111
+ if slope <= 0:
112
+ logger.warning(
113
+ "Recovery slope is non-positive (slope=%.5f). "
114
+ "Vegetation may not be recovering — review disturbance date or data quality.",
115
+ slope
116
+ )
117
+
118
+ log_step(5, "Saving recovery metrics CSV")
119
+ metrics = pd.DataFrame([{
120
+ "baseline_mean": round(baseline_mean, 4),
121
+ "baseline_sd": round(baseline_sd, 4),
122
+ "disturbance_date": dist_date,
123
+ "post_minimum_value": round(float(min_val), 4),
124
+ "post_minimum_date": str(min_date.date()),
125
+ "magnitude_decline_pct": round((baseline_mean - min_val)/baseline_mean*100, 2),
126
+ "RI_current": round(float(post["RI"].iloc[-1]), 4),
127
+ "slope_linear": round(slope, 6),
128
+ "r2_linear": round(r2, 4),
129
+ "t_to_80pct_months": t_80,
130
+ "t_to_100pct_months": t_100,
131
+ }])
132
+ metrics.to_csv(output_dir / "recovery_metrics.csv", index=False)
133
+
134
+ log_step(6, "Generating recovery trajectory and context plots")
135
+ # Recovery plot
136
+ t_range = np.linspace(0, fit_df["t_months"].max(), 200)
137
+ ri_pred = intercept + slope * t_range
138
+ fig, ax = plt.subplots(figsize=(8, 5))
139
+ ax.axhline(1.0, linestyle="--", color="forestgreen", alpha=0.7, label="100% recovery")
140
+ ax.axhline(0.8, linestyle="--", color="orange", alpha=0.7, label="80% recovery")
141
+ ax.scatter(fit_df["t_months"], fit_df["RI"], s=20, alpha=0.7, color="grey")
142
+ ax.plot(fit_df["t_months"], fit_df["RI"], color="grey", linewidth=0.7)
143
+ ax.plot(t_range, ri_pred, color="steelblue", linewidth=1.5,
144
+ label=f"Linear fit R2={r2:.3f}")
145
+ ax.set_xlabel("Months since post-disturbance minimum")
146
+ ax.set_ylabel("Recovery Indicator (RI)")
147
+ ax.set_title(f"Recovery Trajectory — disturbance: {dist_date}")
148
+ ax.legend(); plt.tight_layout()
149
+ plt.savefig(output_dir / "recovery_trajectory.png", dpi=150)
150
+ plt.close()
151
+
152
+ # Context plot
153
+ fig2, ax2 = plt.subplots(figsize=(10, 4))
154
+ ax2.plot(dat[date_col], dat[val_col], color="grey", linewidth=0.8)
155
+ ax2.axvline(dist_dt, color="red", linestyle="--", linewidth=1, label="Disturbance")
156
+ ax2.axhline(baseline_mean, color="forestgreen", linestyle=":", linewidth=1,
157
+ label=f"Baseline ({baseline_mean:.3f})")
158
+ ax2.set_xlabel("Date"); ax2.set_ylabel(val_col)
159
+ ax2.set_title("Full Time Series with Disturbance Event")
160
+ ax2.legend(); plt.tight_layout()
161
+ plt.savefig(output_dir / "timeseries_context.png", dpi=150)
162
+ plt.close()
163
+ logger.info("Outputs written to: %s", output_dir)
164
+
165
+ except FileNotFoundError as e:
166
+ logger.error(
167
+ "Input file not found: %s\n"
168
+ " Expected output from: geoprocessing-for-ecology\n"
169
+ " Check that previous step completed.",
170
+ e
171
+ )
172
+ raise
173
+ except Exception as e:
174
+ logger.error("Unexpected error in recovery trajectory analysis: %s", e)
175
+ raise
176
+
177
+ if __name__ == "__main__":
178
+ main()
@@ -0,0 +1,192 @@
1
+ # ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
2
+ # SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ # Usage: Rscript trend_analysis.R <timeseries.csv> <output_dir> [frequency] [baseline_end]
5
+ # Mann-Kendall trend + Sen's slope + BFAST breakpoints
6
+ # Usage: Rscript trend_analysis.R <timeseries_csv> <output_dir> [frequency]
7
+ # Requires: trend, bfast, zoo, ggplot2
8
+
9
+ # ── Inline logger ─────────────────────────────────────────────────────────────
10
+ SKILL_NAME <- "environmental-time-series"
11
+ .log_ts <- function() format(Sys.time(), "[%Y-%m-%d %H:%M:%S]")
12
+ log_info <- function(...) message(.log_ts(), " [INFO] ", sprintf(...))
13
+ log_warn <- function(...) message(.log_ts(), " [WARN] ", sprintf(...))
14
+ log_error<- function(...) message(.log_ts(), " [ERROR] ", sprintf(...))
15
+ log_step <- function(n, d) log_info("-- STEP %d: %s", n, d)
16
+ log_decision <- function(v, val, why) log_info("DECISION | %s = %s | %s", v, val, why)
17
+ dir.create("logs", recursive=TRUE, showWarnings=FALSE)
18
+
19
+ suppressPackageStartupMessages({
20
+ library(trend)
21
+ library(bfast)
22
+ library(zoo)
23
+ library(ggplot2)
24
+ })
25
+
26
+ args <- commandArgs(trailingOnly = TRUE)
27
+ ts_file <- ifelse(length(args) >= 1, args[1], "data/ndvi_series.csv")
28
+ output_dir <- ifelse(length(args) >= 2, args[2], "outputs/timeseries")
29
+ freq <- ifelse(length(args) >= 3, as.integer(args[3]), 12L)
30
+ dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
31
+
32
+ log_info("Skill: %s | ts_file=%s | output_dir=%s | freq=%d",
33
+ SKILL_NAME, ts_file, output_dir, freq)
34
+ log_decision("freq", as.character(freq),
35
+ "frequencia da serie temporal (observacoes por ano); padrao 12 para dados mensais")
36
+
37
+ # ── Input precondition check ──────────────────────────────────────────────────
38
+ if (!file.exists(ts_file)) {
39
+ log_error(
40
+ "Input nao encontrado: %s\nCausa provavel: serie temporal nao gerada ou caminho incorreto.\nVerifique: execute primeiro o script de extracao/exportacao da serie temporal.\nSkill anterior: geoprocessing-for-ecology ou remote-sensing-analysis.",
41
+ ts_file
42
+ )
43
+ stop("Missing: ", ts_file)
44
+ }
45
+
46
+ # ── Load ───────────────────────────────────────────────────────────────────────
47
+ log_step(1, "Carregar serie temporal e validar coluna 'value'")
48
+ dat <- tryCatch({
49
+ read.csv(ts_file)
50
+ }, error = function(e) {
51
+ log_error(
52
+ "Falha ao ler CSV de serie temporal: %s\nCausa provavel: arquivo corrompido, encoding incorreto ou separador diferente de virgula.\nVerifique: abra o arquivo em editor de texto e confira o formato.\nSkill anterior: geoprocessing-for-ecology.",
53
+ conditionMessage(e)
54
+ )
55
+ stop(e)
56
+ })
57
+
58
+ if (!"value" %in% names(dat)) {
59
+ log_error(
60
+ "Coluna 'value' nao encontrada no CSV (colunas presentes: %s).\nCausa provavel: CSV exportado com nome de coluna diferente.\nVerifique: renomeie a coluna de valores para 'value' ou ajuste o script.\nSkill anterior: geoprocessing-for-ecology.",
61
+ paste(names(dat), collapse = ", ")
62
+ )
63
+ stop("Missing column: value")
64
+ }
65
+
66
+ n_na <- sum(is.na(dat$value))
67
+ if (n_na > 0) {
68
+ log_warn("Coluna 'value' contem %d valores NA — Mann-Kendall pode ser afetado.", n_na)
69
+ }
70
+
71
+ log_info("Observacoes: %d | Frequencia: %d", nrow(dat), freq)
72
+
73
+ ts_obj <- ts(dat$value, frequency = freq)
74
+
75
+ # ── Mann-Kendall + Sen's slope ─────────────────────────────────────────────────
76
+ log_step(2, "Teste de Mann-Kendall e inclinacao de Sen")
77
+ mk <- tryCatch({
78
+ mk.test(dat$value)
79
+ }, error = function(e) {
80
+ log_error(
81
+ "Falha no teste de Mann-Kendall: %s\nCausa provavel: serie com todos os valores iguais ou NA excessivo.\nVerifique: variabilidade dos dados de entrada.\nSkill anterior: geoprocessing-for-ecology.",
82
+ conditionMessage(e)
83
+ )
84
+ stop(e)
85
+ })
86
+
87
+ sen <- tryCatch({
88
+ sens.slope(dat$value)
89
+ }, error = function(e) {
90
+ log_error(
91
+ "Falha ao calcular inclinacao de Sen: %s\nCausa provavel: serie insuficiente ou sem variacao.\nVerifique: numero de observacoes validas.\nSkill anterior: geoprocessing-for-ecology.",
92
+ conditionMessage(e)
93
+ )
94
+ stop(e)
95
+ })
96
+
97
+ log_info("Mann-Kendall: tau=%.4f | p=%.4f", mk$statistic, mk$p.value)
98
+ log_info("Inclinacao de Sen: %.6f (unidades/observacao)", sen$estimates)
99
+
100
+ if (mk$p.value < 0.05) {
101
+ trend_dir <- ifelse(mk$statistic > 0, "crescente", "decrescente")
102
+ log_info("Tendencia significativa detectada (p<0.05): %s", trend_dir)
103
+ } else {
104
+ log_info("Nenhuma tendencia significativa detectada (p=%.4f >= 0.05).", mk$p.value)
105
+ }
106
+
107
+ mk_results <- data.frame(
108
+ tau = mk$statistic, p_value = mk$p.value,
109
+ sens_slope = as.numeric(sen$estimates),
110
+ trend_direction = ifelse(mk$p.value < 0.05,
111
+ ifelse(mk$statistic > 0, "increasing", "decreasing"),
112
+ "no significant trend")
113
+ )
114
+
115
+ tryCatch({
116
+ write.csv(mk_results, file.path(output_dir, "trend_results.csv"), row.names = FALSE)
117
+ log_info("trend_results.csv salvo em: %s", output_dir)
118
+ }, error = function(e) {
119
+ log_error(
120
+ "Falha ao salvar trend_results.csv: %s\nCausa provavel: permissao negada ou disco cheio.\nVerifique: permissoes do diretorio de saida.\nSkill anterior: nenhuma.",
121
+ conditionMessage(e)
122
+ )
123
+ stop(e)
124
+ })
125
+
126
+ # ── BFAST breakpoints ──────────────────────────────────────────────────────────
127
+ log_step(3, "Detectar quebras estruturais com BFAST")
128
+ min_length_bfast <- 3 * freq
129
+ if (length(ts_obj) >= min_length_bfast) {
130
+ log_info("Serie suficiente para BFAST (%d obs >= %d minimo). Executando...", length(ts_obj), min_length_bfast)
131
+ log_decision("bfast_h", "0.15",
132
+ "h=0.15 requer pelo menos 15%% da serie entre quebras; equilibrio entre sensibilidade e estabilidade")
133
+ log_decision("bfast_season", "harmonic",
134
+ "modelo harmonico para sazonalidade adequado para series de vegetacao com ciclo anual")
135
+ tryCatch({
136
+ bf <- bfast(ts_obj, h = 0.15, season = "harmonic", max.iter = 20)
137
+ bp <- bf$output[[1]]$bp.Vt$breakpoints
138
+ if (length(bp) == 0 || all(is.na(bp))) {
139
+ log_info("BFAST: nenhuma quebra estrutural detectada.")
140
+ } else {
141
+ log_info("BFAST: quebras detectadas nas observacoes: %s", paste(bp, collapse = ", "))
142
+ }
143
+ write.csv(data.frame(breakpoint_obs = bp),
144
+ file.path(output_dir, "breakpoints.csv"), row.names = FALSE)
145
+ log_info("breakpoints.csv salvo em: %s", output_dir)
146
+ png(file.path(output_dir, "bfast_plot.png"), width = 1200, height = 600, res = 150)
147
+ plot(bf)
148
+ dev.off()
149
+ log_info("bfast_plot.png salvo em: %s", output_dir)
150
+ }, error = function(e) {
151
+ log_warn("BFAST falhou: %s — continuando sem deteccao de quebras.", conditionMessage(e))
152
+ })
153
+ } else {
154
+ log_warn("Serie muito curta para BFAST: %d obs < %d minimo (3 ciclos completos de frequencia %d).",
155
+ length(ts_obj), min_length_bfast, freq)
156
+ }
157
+
158
+ # ── Anomalies ──────────────────────────────────────────────────────────────────
159
+ log_step(4, "Calcular anomalias em relacao ao baseline")
160
+ baseline_n <- min(freq * 10, length(dat$value) %/% 2)
161
+ baseline_mean <- mean(dat$value[1:baseline_n], na.rm = TRUE)
162
+ baseline_sd <- sd(dat$value[1:baseline_n], na.rm = TRUE)
163
+
164
+ log_decision("baseline_n", as.character(baseline_n),
165
+ "minimo entre 10 anos de dados e metade da serie; evita que o baseline abranja o periodo de mudanca")
166
+ log_info("Baseline: %.4f +/- %.4f (primeiras %d observacoes)", baseline_mean, baseline_sd, baseline_n)
167
+
168
+ if (baseline_sd == 0) {
169
+ log_warn("Desvio padrao do baseline e zero — todas as anomalias serao infinitas ou NaN.")
170
+ }
171
+
172
+ dat$anomaly_z <- (dat$value - baseline_mean) / baseline_sd
173
+
174
+ n_extreme <- sum(abs(dat$anomaly_z) > 3, na.rm = TRUE)
175
+ if (n_extreme > 0) {
176
+ log_warn("%d observacao(oes) com anomalia |Z| > 3 detectada(s) — possiveis outliers ou eventos extremos.", n_extreme)
177
+ }
178
+
179
+ tryCatch({
180
+ write.csv(dat[, c(names(dat)[1], "value", "anomaly_z")],
181
+ file.path(output_dir, "anomaly_series.csv"), row.names = FALSE)
182
+ log_info("anomaly_series.csv salvo em: %s", output_dir)
183
+ }, error = function(e) {
184
+ log_error(
185
+ "Falha ao salvar anomaly_series.csv: %s\nCausa provavel: permissao negada ou disco cheio.\nVerifique: permissoes do diretorio de saida.\nSkill anterior: nenhuma.",
186
+ conditionMessage(e)
187
+ )
188
+ stop(e)
189
+ })
190
+
191
+ log_info("Anomalias calculadas em relacao as primeiras %d observacoes.", baseline_n)
192
+ log_info("Analise de tendencia concluida. Saidas em: %s", output_dir)