# # YOUNG_FIELD_ANALYSIS.R # ====================== # Specialized analysis for young sugarcane fields (0-12 months) # Focuses on: # 1. Germination issues (gap detection, uneven emergence) # 2. Weed pressure detection (both patchy and uniform weeds) # 3. Age-specific intervention recommendations # # Usage: Rscript young_field_analysis.R [current_week] [previous_week] [project_dir] # # 1. Load required packages # ----------------------- suppressPackageStartupMessages({ library(sf) library(terra) library(tidyverse) library(lubridate) library(here) library(readxl) # For reading harvest_data.xlsx library(spdep) # For spatial statistics }) # 2. Young field analysis configuration # ----------------------------------- # Age thresholds (months) GERMINATION_PHASE_MAX <- 4 # 0-4 months: critical germination period WEED_CRITICAL_PHASE_MAX <- 8 # 4-8 months: weeds most competitive YOUNG_FIELD_MAX <- 12 # 0-12 months: all young field analysis # Detection thresholds WEED_CI_CHANGE_THRESHOLD <- 1.5 # Weekly CI increase indicating possible weeds SEVERE_WEED_CI_CHANGE <- 2.0 # Severe weed pressure GERMINATION_GAP_CV_THRESHOLD <- 0.30 # High CV in young fields = poor germination LOW_CI_GERMINATION_THRESHOLD <- 0.5 # Very low CI = poor emergence # 3. Enhanced spectral indices for young crop detection # --------------------------------------------------- #' Calculate enhanced vegetation indices from RGBNIR bands #' @param red Red band raster #' @param green Green band raster #' @param blue Blue band raster #' @param nir Near-infrared band raster #' @return List of vegetation index rasters calculate_enhanced_indices <- function(red, green, blue, nir) { cat("Calculating enhanced vegetation indices...\n") # 1. Standard NDVI (baseline) ndvi <- (nir - red) / (nir + red) names(ndvi) <- "NDVI" # 2. Green NDVI (more sensitive to early vegetation) gndvi <- (nir - green) / (nir + green) names(gndvi) <- "GNDVI" # 3. Excess Green Index (early vegetation detection) exg <- 2*green - red - blue names(exg) <- "ExG" # 4. Visible Atmospherically Resistant Index vari <- (green - red) / (green + red + blue) names(vari) <- "VARI" # 5. Green Ratio Vegetation Index grvi <- green / red names(grvi) <- "GRVI" # 6. Chlorophyll Index (CI = NIR / Green - 1, NOT NIR/Red) # *** CRITICAL: Correct formula uses GREEN band, not RED *** ci <- nir / green - 1 names(ci) <- "CI" return(list( NDVI = ndvi, GNDVI = gndvi, ExG = exg, VARI = vari, GRVI = grvi, CI = ci )) } #' Create visual maps of different indices for comparison #' @param indices_list List of index rasters #' @param field_boundaries SF object with field boundaries #' @param output_dir Directory to save maps #' @param week_num Week number for filename create_index_comparison_maps <- function(indices_list, field_boundaries, output_dir, week_num) { if (!dir.exists(output_dir)) { dir.create(output_dir, recursive = TRUE) } cat("Creating visual comparison maps and TIF exports...\n") # Create individual maps for each index for (index_name in names(indices_list)) { tryCatch({ index_raster <- indices_list[[index_name]] # Create filenames map_filename <- file.path(output_dir, paste0("week_", sprintf("%02d", week_num), "_", index_name, "_map.png")) tif_filename <- file.path(output_dir, paste0("week_", sprintf("%02d", week_num), "_", index_name, ".tif")) # Export TIF file for QGIS terra::writeRaster(index_raster, tif_filename, overwrite = TRUE) cat("- Exported TIF:", tif_filename, "\n") # Create PNG map without field boundaries png(map_filename, width = 1200, height = 800, res = 150) # Plot raster only (no field boundaries) plot(index_raster, main = paste("Week", week_num, "-", index_name)) dev.off() cat("- Saved map:", map_filename, "\n") }, error = function(e) { warning(paste("Error creating outputs for", index_name, ":", e$message)) }) } cat("✓ Index comparison maps and TIF files created\n") } #' Load field age data from existing harvest_data object (loaded in parameters_project.R) #' @param harvest_data Data frame with harvest/field information #' @param field_boundaries SF object with field boundaries #' @return Field boundaries with age information added #' # harvest_data = harvesting_data #' # field_boundaries = field_boundaries_sf load_field_ages_from_existing <- function(harvest_data, field_boundaries) { cat("Processing field age data from existing harvest_data object...\n") tryCatch({ # Display structure of harvest data cat("- Harvest data columns:", paste(colnames(harvest_data), collapse = ", "), "\n") cat("- Harvest data rows:", nrow(harvest_data), "\n") # Display field boundaries structure cat("- Field boundaries columns:", paste(colnames(field_boundaries), collapse = ", "), "\n") cat("- Field boundaries rows:", nrow(field_boundaries), "\n") # Join harvest data with field boundaries on field and sub_field # Convert field and sub_field to character to ensure consistent matching # Filter for current season only (where season_end is today's date) current_date <- Sys.Date() harvest_data_clean <- harvest_data %>% mutate( field = as.character(field), sub_field = as.character(sub_field) ) %>% # Filter for current season only filter(season_end == current_date | (season_end >= current_date - 7 & season_end <= current_date)) %>% select(field, sub_field, age, season_start, season_end, tonnage_ha) cat("- Harvest data after filtering for current season:", nrow(harvest_data_clean), "rows\n") if (nrow(harvest_data_clean) == 0) { cat("⚠️ No current season data found in harvest data\n") cat("- Looking for season_end near:", current_date, "\n") # Show available season_end dates to help debug available_dates <- harvest_data %>% select(season_end) %>% distinct() %>% arrange(season_end) cat("- Available season_end dates in harvest data:\n") print(available_dates) return(field_boundaries) } field_boundaries_clean <- field_boundaries %>% mutate( field = as.character(field), sub_field = as.character(sub_field) ) # Perform the join field_boundaries_with_age <- field_boundaries_clean %>% left_join(harvest_data_clean, by = c("field", "sub_field")) # Check join success matched_fields <- sum(!is.na(field_boundaries_with_age$age)) total_fields <- nrow(field_boundaries_with_age) cat("✓ Successfully joined harvest data\n") cat("- Fields with age data:", matched_fields, "out of", total_fields, "\n") if (matched_fields > 0) { age_summary <- field_boundaries_with_age %>% filter(!is.na(age)) %>% pull(age) %>% summary() cat("- Age range (weeks):", paste(names(age_summary), "=", round(age_summary, 1), collapse = ", "), "\n") # Convert age from weeks to months for analysis field_boundaries_with_age <- field_boundaries_with_age %>% mutate(age_months = round(age / 4.33, 1)) # 4.33 weeks per month average cat("- Age range (months):", paste(round(range(field_boundaries_with_age$age_months, na.rm = TRUE), 1), collapse = " to "), "\n") } return(field_boundaries_with_age) }, error = function(e) { warning(paste("Error processing harvest data:", e$message)) cat("Returning original field boundaries without age information\n") return(field_boundaries) }) } #' Detect germination issues in young fields #' @param indices_list List of vegetation index rasters #' @param field_boundaries SF object with field boundaries (with age info) #' @param young_fields_only Logical: analyze only young fields? #' @return List with germination analysis results detect_germination_issues <- function(indices_list, field_boundaries, young_fields_only = TRUE) { cat("=== GERMINATION ANALYSIS ===\n") germination_results <- list() field_boundaries_vect <- terra::vect(field_boundaries) for (i in seq_len(nrow(field_boundaries))) { field_name <- field_boundaries$field[i] sub_field_name <- field_boundaries$sub_field[i] field_id <- paste0(field_name, "_", sub_field_name) # Get field age if available field_age_months <- if ("age_months" %in% colnames(field_boundaries) && !is.na(field_boundaries$age_months[i])) { field_boundaries$age_months[i] } else { NA } cat("Analyzing germination for field:", field_id) if (!is.na(field_age_months)) { cat(" (Age:", field_age_months, "months)") } cat("\n") # Extract field boundary field_vect <- field_boundaries_vect[i] # Analyze each index for germination patterns field_analysis <- list() for (index_name in names(indices_list)) { index_raster <- indices_list[[index_name]] # Extract values for this field field_values <- terra::extract(index_raster, field_vect, fun = NULL) valid_values <- unlist(field_values) valid_values <- valid_values[!is.na(valid_values) & is.finite(valid_values)] if (length(valid_values) > 10) { # Calculate germination-relevant metrics mean_val <- mean(valid_values) cv_val <- sd(valid_values) / mean_val min_val <- min(valid_values) # Only check for germination issues in fields ≤ 4 months old check_germination <- is.na(field_age_months) || field_age_months <= GERMINATION_PHASE_MAX if (check_germination) { # Detect potential germination issues high_variation <- cv_val > GERMINATION_GAP_CV_THRESHOLD low_values <- mean_val < LOW_CI_GERMINATION_THRESHOLD very_low_areas <- sum(valid_values < (mean_val - 2*sd(valid_values))) / length(valid_values) * 100 } else { # Field too old for germination analysis high_variation <- FALSE low_values <- FALSE very_low_areas <- 0 } field_analysis[[index_name]] <- list( mean = mean_val, cv = cv_val, min = min_val, high_variation = high_variation, low_values = low_values, very_low_areas_pct = very_low_areas, n_pixels = length(valid_values), age_months = field_age_months, germination_analysis_applied = check_germination ) } } germination_results[[field_id]] <- field_analysis } return(germination_results) } #' Detect weed pressure using change analysis #' @param current_indices List of current week indices #' @param previous_indices List of previous week indices #' @param field_boundaries SF object with field boundaries #' @return List with weed detection results detect_weed_pressure <- function(current_indices, previous_indices, field_boundaries) { cat("=== WEED PRESSURE DETECTION ===\n") weed_results <- list() field_boundaries_vect <- terra::vect(field_boundaries) for (i in seq_len(nrow(field_boundaries))) { field_name <- field_boundaries$field[i] sub_field_name <- field_boundaries$sub_field[i] field_id <- paste0(field_name, "_", sub_field_name) cat("Analyzing weed pressure for field:", field_id, "\n") field_vect <- field_boundaries_vect[i] field_weed_analysis <- list() # Analyze change in each index for (index_name in names(current_indices)) { if (index_name %in% names(previous_indices)) { current_raster <- current_indices[[index_name]] previous_raster <- previous_indices[[index_name]] # Extract values for both weeks current_values <- unlist(terra::extract(current_raster, field_vect, fun = NULL)) previous_values <- unlist(terra::extract(previous_raster, field_vect, fun = NULL)) # Clean values valid_idx <- !is.na(current_values) & !is.na(previous_values) & is.finite(current_values) & is.finite(previous_values) current_clean <- current_values[valid_idx] previous_clean <- previous_values[valid_idx] if (length(current_clean) > 10) { # Calculate change metrics change_values <- current_clean - previous_clean mean_change <- mean(change_values) change_cv <- sd(change_values) / abs(mean(current_clean)) # Weed detection criteria # 1. Significant positive change (weeds growing faster than cane) rapid_increase <- mean_change >= WEED_CI_CHANGE_THRESHOLD severe_increase <- mean_change >= SEVERE_WEED_CI_CHANGE # 2. Percentage of field with rapid increase rapid_increase_pct <- sum(change_values >= WEED_CI_CHANGE_THRESHOLD) / length(change_values) * 100 # 3. Patchy vs uniform weed patterns patchy_weeds <- change_cv > 0.5 && rapid_increase_pct > 10 && rapid_increase_pct < 80 uniform_weeds <- change_cv < 0.3 && rapid_increase_pct > 60 # Whole field weeds # 4. Current vegetation level (high CI + rapid change = likely weeds) current_mean <- mean(current_clean) high_current_ci <- current_mean > 2.0 # Adjust threshold as needed field_weed_analysis[[index_name]] <- list( mean_change = mean_change, change_cv = change_cv, current_mean = current_mean, rapid_increase = rapid_increase, severe_increase = severe_increase, rapid_increase_pct = rapid_increase_pct, patchy_weeds = patchy_weeds, uniform_weeds = uniform_weeds, high_current_ci = high_current_ci, n_pixels = length(current_clean) ) } } } weed_results[[field_id]] <- field_weed_analysis } return(weed_results) } #' Generate intervention recommendations based on detected issues #' @param field_id Field identifier #' @param germination_analysis Germination analysis results #' @param weed_analysis Weed detection results #' @param field_age_months Field age in months (if available) #' @return List with recommendations generate_young_field_recommendations <- function(field_id, germination_analysis, weed_analysis, field_age_months = NA) { recommendations <- list() # Priority level: 1=urgent, 2=high, 3=moderate, 4=monitoring priority <- 4 messages <- c() interventions <- c() # Check for germination issues (if CI analysis available and field is young enough) if ("CI" %in% names(germination_analysis)) { ci_analysis <- germination_analysis[["CI"]] # Only apply germination analysis for fields ≤ 4 months old if (!is.null(ci_analysis$germination_analysis_applied) && ci_analysis$germination_analysis_applied) { if (ci_analysis$high_variation && ci_analysis$low_values) { priority <- min(priority, 1) messages <- c(messages, "🚨 URGENT: Poor germination detected - high variation with low CI values") interventions <- c(interventions, "Immediate replanting in gap areas", "Check seed quality and planting conditions") } else if (ci_analysis$high_variation) { priority <- min(priority, 2) messages <- c(messages, "⚠️ Alert: Uneven emergence detected - moderate intervention needed") interventions <- c(interventions, "Monitor gap areas for potential replanting") } if (ci_analysis$very_low_areas_pct > 15) { priority <- min(priority, 2) messages <- c(messages, paste0("⚠️ Alert: ", round(ci_analysis$very_low_areas_pct, 1), "% of field has very low vegetation")) interventions <- c(interventions, "Investigate cause of low-performing areas") } } else { # Field too old for germination analysis - check for other issues if (ci_analysis$cv > 0.5) { priority <- min(priority, 3) messages <- c(messages, "📊 Info: Uneven crop growth detected (field too old for replanting)") interventions <- c(interventions, "Monitor for potential management issues") } } } # Check for weed pressure (if CI analysis available) if ("CI" %in% names(weed_analysis)) { ci_weed <- weed_analysis[["CI"]] if (ci_weed$severe_increase) { priority <- min(priority, 1) messages <- c(messages, paste0("🚨 CRITICAL: Severe weed pressure detected - CI increased by ", round(ci_weed$mean_change, 2))) interventions <- c(interventions, "Immediate weed control required", "Consider herbicide application") } else if (ci_weed$rapid_increase) { priority <- min(priority, 2) messages <- c(messages, paste0("⚠️ Alert: Weed pressure detected - CI increased by ", round(ci_weed$mean_change, 2))) interventions <- c(interventions, "Schedule weed control within 1-2 weeks") } if (ci_weed$uniform_weeds) { priority <- min(priority, 2) messages <- c(messages, paste0("🔶 Pattern: Uniform weed emergence across ", round(ci_weed$rapid_increase_pct, 1), "% of field")) interventions <- c(interventions, "Broad-scale weed management needed") } else if (ci_weed$patchy_weeds) { priority <- min(priority, 3) messages <- c(messages, paste0("🔶 Pattern: Patchy weed emergence in ", round(ci_weed$rapid_increase_pct, 1), "% of field")) interventions <- c(interventions, "Targeted spot treatment recommended") } } # Age-specific recommendations if (!is.na(field_age_months)) { if (field_age_months <= GERMINATION_PHASE_MAX) { interventions <- c(interventions, "Critical germination phase - maintain optimal moisture", "Monitor for pest damage") } else if (field_age_months <= WEED_CRITICAL_PHASE_MAX) { interventions <- c(interventions, "Peak weed competition period - prioritize weed control") } } # Default monitoring if no issues if (length(messages) == 0) { messages <- c("✅ No significant issues detected") interventions <- c("Continue routine monitoring") } return(list( field_id = field_id, priority = priority, messages = messages, interventions = interventions, requires_action = priority <= 3 )) } # 4. Main analysis function # ----------------------- main_young_field_analysis <- function() { # Get command line arguments args <- commandArgs(trailingOnly = TRUE) current_week <- if (length(args) >= 1) as.numeric(args[1]) else 30 previous_week <- if (length(args) >= 2) as.numeric(args[2]) else 29 project_dir <- if (length(args) >= 3) args[3] else "simba" cat("=== YOUNG FIELD ANALYSIS SYSTEM ===\n") cat("Project:", project_dir, "\n") cat("Analyzing weeks:", previous_week, "→", current_week, "\n\n") # Load project configuration assign("project_dir", project_dir, envir = .GlobalEnv) tryCatch({ source("parameters_project.R") cat("✓ Project configuration loaded\n") }, error = function(e) { tryCatch({ source(here::here("r_app", "parameters_project.R")) cat("✓ Project configuration loaded from r_app directory\n") }, error = function(e) { stop("Failed to load project configuration") }) }) # Verify required variables if (!exists("weekly_CI_mosaic") || !exists("field_boundaries_sf")) { stop("Required project variables not found") } # Check if harvest data is available from parameters_project.R if (exists("harvesting_data") && !is.null(harvesting_data)) { cat("✓ Harvest data loaded from parameters_project.R\n") field_boundaries_with_age <- load_field_ages_from_existing(harvesting_data, field_boundaries_sf) } else { cat("⚠️ No harvest data found in parameters_project.R\n") field_boundaries_with_age <- field_boundaries_sf } # Construct mosaic file paths year <- 2025 current_week_file <- sprintf("week_%02d_%d.tif", current_week, year) previous_week_file <- sprintf("week_%02d_%d.tif", previous_week, year) current_mosaic_path <- file.path(weekly_CI_mosaic, current_week_file) previous_mosaic_path <- file.path(weekly_CI_mosaic, previous_week_file) cat("Current week mosaic:", current_mosaic_path, "\n") cat("Previous week mosaic:", previous_mosaic_path, "\n") # Check if files exist if (!file.exists(current_mosaic_path)) { stop("Current week mosaic not found: ", current_mosaic_path) } if (!file.exists(previous_mosaic_path)) { stop("Previous week mosaic not found: ", previous_mosaic_path) } # Load mosaics and extract bands cat("\nLoading mosaics and extracting RGBNIR bands...\n") current_mosaic <- terra::rast(current_mosaic_path) previous_mosaic <- terra::rast(previous_mosaic_path) cat("Current mosaic bands:", nlyr(current_mosaic), "\n") cat("Previous mosaic bands:", nlyr(previous_mosaic), "\n") # Extract RGBNIR bands (assuming standard order: R=1, G=2, B=3, NIR=4) current_red <- current_mosaic[[1]] current_green <- current_mosaic[[2]] current_blue <- current_mosaic[[3]] current_nir <- current_mosaic[[4]] previous_red <- previous_mosaic[[1]] previous_green <- previous_mosaic[[2]] previous_blue <- previous_mosaic[[3]] previous_nir <- previous_mosaic[[4]] # Calculate enhanced indices for both weeks cat("\nCalculating vegetation indices...\n") current_indices <- calculate_enhanced_indices(current_red, current_green, current_blue, current_nir) previous_indices <- calculate_enhanced_indices(previous_red, previous_green, previous_blue, previous_nir) # Create output directory for maps output_dir <- file.path(dirname(weekly_CI_mosaic), "young_field_analysis") # Create visual comparison maps cat("\nCreating visual maps...\n") # Check if maps already exist before creating them current_week_maps_exist <- all(sapply(names(current_indices), function(index_name) { map_file <- file.path(output_dir, paste0("week_", sprintf("%02d", current_week), "_", index_name, "_map.png")) tif_file <- file.path(output_dir, paste0("week_", sprintf("%02d", current_week), "_", index_name, ".tif")) file.exists(map_file) && file.exists(tif_file) })) previous_week_maps_exist <- all(sapply(names(previous_indices), function(index_name) { map_file <- file.path(output_dir, paste0("week_", sprintf("%02d", previous_week), "_", index_name, "_map.png")) tif_file <- file.path(output_dir, paste0("week_", sprintf("%02d", previous_week), "_", index_name, ".tif")) file.exists(map_file) && file.exists(tif_file) })) if (!current_week_maps_exist) { cat("Creating current week maps (week", current_week, ")...\n") create_index_comparison_maps(current_indices, field_boundaries_sf, output_dir, current_week) } else { cat("Current week maps already exist, skipping creation...\n") } if (!previous_week_maps_exist) { cat("Creating previous week maps (week", previous_week, ")...\n") create_index_comparison_maps(previous_indices, field_boundaries_sf, output_dir, previous_week) } else { cat("Previous week maps already exist, skipping creation...\n") } # Perform germination analysis cat("\nPerforming germination analysis...\n") germination_results <- detect_germination_issues(current_indices, field_boundaries_with_age) # Perform weed detection analysis cat("\nPerforming weed pressure analysis...\n") weed_results <- detect_weed_pressure(current_indices, previous_indices, field_boundaries_with_age) # Generate recommendations for each field cat("\n=== FIELD RECOMMENDATIONS ===\n") field_recommendations <- list() action_needed_fields <- 0 for (field_id in names(germination_results)) { germination_analysis <- germination_results[[field_id]] weed_analysis <- if (field_id %in% names(weed_results)) weed_results[[field_id]] else list() # Get field age if available field_row <- field_boundaries_with_age[field_boundaries_with_age$field == strsplit(field_id, "_")[[1]][1] & field_boundaries_with_age$sub_field == strsplit(field_id, "_")[[1]][2], ] field_age_months <- if (nrow(field_row) > 0 && !is.na(field_row$age_months[1])) field_row$age_months[1] else NA recommendations <- generate_young_field_recommendations( field_id, germination_analysis, weed_analysis, field_age_months ) field_recommendations[[field_id]] <- recommendations # Print recommendations cat("\nFIELD:", field_id, "\n") if (!is.na(field_age_months)) { cat("Age:", field_age_months, "months\n") } cat("Priority Level:", recommendations$priority, "\n") for (message in recommendations$messages) { cat("-", message, "\n") } if (length(recommendations$interventions) > 0) { cat("Recommended Actions:\n") for (intervention in recommendations$interventions) { cat(" •", intervention, "\n") } } if (recommendations$requires_action) { action_needed_fields <- action_needed_fields + 1 } # Show index comparison for this field if ("CI" %in% names(germination_analysis)) { ci_stats <- germination_analysis[["CI"]] cat("CI Stats: Mean =", round(ci_stats$mean, 3), ", CV =", round(ci_stats$cv, 3), ", Low areas =", round(ci_stats$very_low_areas_pct, 1), "%\n") } if ("CI" %in% names(weed_analysis)) { weed_stats <- weed_analysis[["CI"]] cat("Change Stats: CI Δ =", round(weed_stats$mean_change, 3), ", Rapid increase =", round(weed_stats$rapid_increase_pct, 1), "%\n") } } # Summary cat("\n=== ANALYSIS SUMMARY ===\n") cat("Total fields analyzed:", length(field_recommendations), "\n") cat("Fields requiring action:", action_needed_fields, "\n") cat("Maps saved to:", output_dir, "\n") cat("\nIndex comparison maps created for visual inspection:\n") cat("- NDVI: Standard vegetation index\n") cat("- GNDVI: Green NDVI (sensitive to early vegetation)\n") cat("- ExG: Excess Green (early detection)\n") cat("- VARI: Atmospherically resistant\n") cat("- GRVI: Green ratio\n") cat("- CI: Chlorophyll Index (current standard)\n") cat("\n✓ Young field analysis complete\n") return(list( germination_results = germination_results, weed_results = weed_results, recommendations = field_recommendations, indices_calculated = names(current_indices), maps_created = file.path(output_dir) )) } # Run analysis if script called directly if (sys.nframe() == 0) { main_young_field_analysis() }