--- params: ref: "word-styles-reference-var1.docx" output_file: CI_report.docx report_date: "2024-08-28" data_dir: "Chemba" mail_day: "Wednesday" borders: TRUE use_breaks: FALSE output: # html_document: # toc: yes # df_print: paged word_document: reference_docx: !expr file.path("word-styles-reference-var1.docx") toc: yes editor_options: chunk_output_type: console --- ```{r setup_parameters, include=FALSE} # Set up basic report parameters from input values report_date <- params$report_date mail_day <- params$mail_day borders <- params$borders use_breaks <- params$use_breaks # Whether to use breaks or continuous spectrum in visualizations # Environment setup notes (commented out) # # Activeer de renv omgeving # renv::activate() # renv::deactivate() # # Optioneel: Herstel de omgeving als dat nodig is # # Je kunt dit commentaar geven als je het normaal niet wilt uitvoeren # renv::restore() ``` ```{r load_libraries, message=FALSE, warning=FALSE, include=FALSE} # Configure knitr options knitr::opts_chunk$set(warning = FALSE, message = FALSE) # Path management library(here) # Spatial data libraries library(sf) library(terra) library(exactextractr) # library(raster) - Removed as it's no longer maintained # Data manipulation and visualization library(tidyverse) # Includes dplyr, ggplot2, etc. library(tmap) library(lubridate) library(zoo) # Machine learning library(rsample) library(caret) library(randomForest) library(CAST) # Load custom utility functions tryCatch({ source("report_utils.R") }, error = function(e) { message(paste("Error loading report_utils.R:", e$message)) # Try alternative path if the first one fails tryCatch({ source(here::here("r_app", "report_utils.R")) }, error = function(e) { stop("Could not load report_utils.R from either location: ", e$message) }) }) ``` ```{r initialize_project_config, message=FALSE, warning=FALSE, include=FALSE} # Set the project directory from parameters project_dir <- params$data_dir # Source project parameters with error handling tryCatch({ source(here::here("r_app", "parameters_project.R")) }, error = function(e) { stop("Error loading parameters_project.R: ", e$message) }) # Log initial configuration safe_log("Starting the R Markdown script") safe_log(paste("mail_day params:", params$mail_day)) safe_log(paste("report_date params:", params$report_date)) safe_log(paste("mail_day variable:", mail_day)) ``` ```{r calculate_dates_and_weeks, message=FALSE, warning=FALSE, include=FALSE} # Set locale for consistent date formatting Sys.setlocale("LC_TIME", "C") # Initialize date variables from parameters today <- as.character(report_date) mail_day_as_character <- as.character(mail_day) # Calculate week days report_date_as_week_day <- weekdays(lubridate::ymd(today)) days_of_week <- c("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday") # Calculate initial week number week <- lubridate::week(today) safe_log(paste("Initial week calculation:", week, "today:", today)) # Calculate previous dates for comparisons today_minus_1 <- as.character(lubridate::ymd(today) - 7) today_minus_2 <- as.character(lubridate::ymd(today) - 14) today_minus_3 <- as.character(lubridate::ymd(today) - 21) # Log the weekday calculations for debugging safe_log(paste("Report date weekday:", report_date_as_week_day)) safe_log(paste("Weekday index:", which(days_of_week == report_date_as_week_day))) safe_log(paste("Mail day:", mail_day_as_character)) safe_log(paste("Mail day index:", which(days_of_week == mail_day_as_character))) # Adjust week calculation based on mail day if (which(days_of_week == report_date_as_week_day) > which(days_of_week == mail_day_as_character)) { safe_log("Adjusting weeks because of mail day") week <- lubridate::week(today) + 1 today_minus_1 <- as.character(lubridate::ymd(today)) today_minus_2 <- as.character(lubridate::ymd(today) - 7) today_minus_3 <- as.character(lubridate::ymd(today) - 14) } # Generate subtitle for report subtitle_var <- paste("Report generated on", Sys.Date()) # Calculate week numbers for previous weeks week_minus_1 <- week - 1 week_minus_2 <- week - 2 week_minus_3 <- week - 3 # Format current week with leading zeros week <- sprintf("%02d", week) # Get years for each date year <- lubridate::year(today) year_1 <- lubridate::year(today_minus_1) year_2 <- lubridate::year(today_minus_2) year_3 <- lubridate::year(today_minus_3) ``` ```{r data, message=TRUE, warning=TRUE, include=FALSE} # Load CI index data with error handling tryCatch({ CI_quadrant <- readRDS(here::here(cumulative_CI_vals_dir, "All_pivots_Cumulative_CI_quadrant_year_v2.rds")) safe_log("Successfully loaded CI quadrant data") }, error = function(e) { stop("Error loading CI quadrant data: ", e$message) }) # Get file paths for different weeks using the utility function tryCatch({ path_to_week_current = get_week_path(weekly_CI_mosaic, today, 0) path_to_week_minus_1 = get_week_path(weekly_CI_mosaic, today, -1) path_to_week_minus_2 = get_week_path(weekly_CI_mosaic, today, -2) path_to_week_minus_3 = get_week_path(weekly_CI_mosaic, today, -3) # Log the calculated paths safe_log("Required mosaic paths:") safe_log(paste("Path to current week:", path_to_week_current)) safe_log(paste("Path to week minus 1:", path_to_week_minus_1)) safe_log(paste("Path to week minus 2:", path_to_week_minus_2)) safe_log(paste("Path to week minus 3:", path_to_week_minus_3)) # Validate that files exist if (!file.exists(path_to_week_current)) warning("Current week mosaic file does not exist: ", path_to_week_current) if (!file.exists(path_to_week_minus_1)) warning("Week minus 1 mosaic file does not exist: ", path_to_week_minus_1) if (!file.exists(path_to_week_minus_2)) warning("Week minus 2 mosaic file does not exist: ", path_to_week_minus_2) if (!file.exists(path_to_week_minus_3)) warning("Week minus 3 mosaic file does not exist: ", path_to_week_minus_3) # Load raster data with terra functions CI <- terra::rast(path_to_week_current)$CI CI_m1 <- terra::rast(path_to_week_minus_1)$CI CI_m2 <- terra::rast(path_to_week_minus_2)$CI CI_m3 <- terra::rast(path_to_week_minus_3)$CI }, error = function(e) { stop("Error loading raster data: ", e$message) }) ``` ```{r calculate_difference_rasters, message=TRUE, warning=TRUE, include=FALSE} # Calculate difference rasters for comparisons tryCatch({ # Calculate weekly difference last_week_dif_raster_abs <- (CI - CI_m1) safe_log("Calculated weekly difference raster") # Calculate three-week difference three_week_dif_raster_abs <- (CI - CI_m3) safe_log("Calculated three-week difference raster") }, error = function(e) { safe_log(paste("Error calculating difference rasters:", e$message), "ERROR") # Create placeholder rasters if calculations fail if (!exists("last_week_dif_raster_abs")) { last_week_dif_raster_abs <- CI * 0 } if (!exists("three_week_dif_raster_abs")) { three_week_dif_raster_abs <- CI * 0 } }) ``` ```{r load_field_boundaries, message=TRUE, warning=TRUE, include=FALSE} # Load field boundaries from parameters tryCatch({ AllPivots0 <- field_boundaries_sf safe_log("Successfully loaded field boundaries") }, error = function(e) { stop("Error loading field boundaries: ", e$message) }) ``` `r subtitle_var` \pagebreak # Explanation of the Report This report provides a detailed analysis of your sugarcane fields based on satellite imagery, helping you monitor crop health and development throughout the growing season. The data is processed weekly to give you timely insights for optimal farm management decisions. ## What is the Chlorophyll Index (CI)? The **Chlorophyll Index (CI)** is a vegetation index that measures the relative amount of chlorophyll in plant leaves. Chlorophyll is the green pigment responsible for photosynthesis in plants. Higher CI values indicate: * Greater photosynthetic activity * Healthier plant tissue * Better nitrogen uptake * More vigorous crop growth CI values typically range from 0 (bare soil or severely stressed vegetation) to 7+ (very healthy, dense vegetation). For sugarcane, values between 3-7 generally indicate good crop health, depending on the growth stage. ## What You'll Find in This Report: 1. **Chlorophyll Index Overview Map**: A comprehensive view of all your fields showing current CI values. This helps identify which fields are performing well and which might need attention. 2. **Weekly Difference Map**: Shows changes in CI values over the past week. Positive values (green) indicate improving crop health, while negative values (red) may signal stress or decline. 3. **Field-by-Field Analysis**: Detailed maps for each field showing: * CI values for the current week and two previous weeks * Week-to-week changes in CI values * Three-week change in CI values to track longer-term trends 4. **Growth Trend Graphs**: Time-series visualizations showing how CI values have changed throughout the growing season for each section of your fields. 5. **Yield Prediction**: For mature crops (over 300 days), we provide estimated yield predictions based on historical data and current CI measurements. Use these insights to identify areas that may need irrigation, fertilization, or other interventions, and to track the effectiveness of your management practices over time. \pagebreak # Chlorophyll Index (CI) Overview Map - Current Week ```{r render_ci_overview_map, echo=FALSE, fig.height=6.8, fig.width=9, message=FALSE, warning=FALSE} # Create overview chlorophyll index map tryCatch({ # Base shape map <- tmap::tm_shape(CI, unit = "m") # Add raster layer with either breaks or continuous spectrum based on parameter if (use_breaks) { map <- map + tmap::tm_raster(breaks = c(0,0.5,1,2,3,4,5,6,7,Inf), palette = "RdYlGn", midpoint = NA, legend.is.portrait = FALSE, title = "Chlorophyll Index (CI)") } else { map <- map + tmap::tm_raster(palette = "RdYlGn", style = "cont", midpoint = NA, legend.is.portrait = FALSE, title = "Chlorophyll Index (CI)") } # Complete the map with layout and other elements map <- map + tmap::tm_layout(legend.outside = TRUE, legend.outside.position = "bottom", legend.show = TRUE) + tmap::tm_scale_bar(position = tm_pos_out("right", "bottom"), text.color = "black") + tmap::tm_compass(position = tm_pos_out("right", "bottom"), text.color = "black") + tmap::tm_shape(AllPivots0) + tmap::tm_borders(col = "black") + tmap::tm_text("sub_field", size = 0.6, col = "black") # Print the map print(map) }, error = function(e) { safe_log(paste("Error creating CI overview map:", e$message), "ERROR") plot(1, type="n", axes=FALSE, xlab="", ylab="") text(1, 1, "Error creating CI overview map", cex=1.5) }) ``` \newpage # Weekly Chlorophyll Index Difference Map ```{r render_ci_difference_map, echo=FALSE, fig.height=6.8, fig.width=9, message=FALSE, warning=FALSE} # Create chlorophyll index difference map tryCatch({ # Base shape map <- tmap::tm_shape(last_week_dif_raster_abs, unit = "m") # Add raster layer with either breaks or continuous spectrum based on parameter if (use_breaks) { map <- map + tmap::tm_raster(breaks = c(-3,-2,-1,0,1,2,3), palette = "RdYlGn", midpoint = 0, legend.is.portrait = FALSE, title = "Chlorophyll Index (CI) Change") } else { map <- map + tmap::tm_raster(palette = "RdYlGn", style = "cont", midpoint = 0, legend.is.portrait = FALSE, title = "Chlorophyll Index (CI) Change") } # Complete the map with layout and other elements map <- map + tmap::tm_layout(legend.outside = TRUE, legend.outside.position = "bottom", legend.show = TRUE) + tmap::tm_scale_bar(position = tm_pos_out("right", "bottom"), text.color = "black") + tmap::tm_compass(position = tm_pos_out("right", "bottom"), text.color = "black") + tmap::tm_shape(AllPivots0) + tmap::tm_borders(col = "black") + tmap::tm_text("sub_field", size = 0.6, col = "black") # Print the map print(map) }, error = function(e) { safe_log(paste("Error creating CI difference map:", e$message), "ERROR") plot(1, type="n", axes=FALSE, xlab="", ylab="") text(1, 1, "Error creating CI difference map", cex=1.5) }) ``` \newpage \newpage ```{r generate_field_visualizations, eval=TRUE, fig.height=3.8, fig.width=10, message=FALSE,echo=FALSE, warning=FALSE, include=TRUE, results='asis'} # Generate detailed visualizations for each field tryCatch({ # Merge field polygons for processing AllPivots_merged <- AllPivots0 %>% dplyr::group_by(field) %>% dplyr::summarise(.groups = 'drop') # Generate plots for each field purrr::walk(AllPivots_merged$field, function(field_name) { tryCatch({ cat("\n") # Add an empty line for better spacing # Call ci_plot with explicit parameters ci_plot( pivotName = field_name, field_boundaries = AllPivots0, current_ci = CI, ci_minus_1 = CI_m1, ci_minus_2 = CI_m2, last_week_diff = last_week_dif_raster_abs, three_week_diff = three_week_dif_raster_abs, harvesting_data = harvesting_data, week = week, week_minus_1 = week_minus_1, week_minus_2 = week_minus_2, week_minus_3 = week_minus_3, use_breaks = use_breaks, borders = borders ) cat("\n") # Call cum_ci_plot with explicit parameters cum_ci_plot( pivotName = field_name, ci_quadrant_data = CI_quadrant, plot_type = "value", facet_on = FALSE ) }, error = function(e) { safe_log(paste("Error generating plots for field", field_name, ":", e$message), "ERROR") cat(paste("## Error generating plots for field", field_name, "\n")) cat(paste(e$message, "\n")) }) }) }, error = function(e) { safe_log(paste("Error in field visualization section:", e$message), "ERROR") cat("Error generating field plots. See log for details.\n") }) ``` ```{r generate_subarea_visualizations, echo=FALSE, fig.height=3.8, fig.width=10, message=FALSE, warning=FALSE, results='asis', eval=FALSE} # Alternative visualization grouped by sub-area (disabled by default) tryCatch({ # Group pivots by sub-area pivots_grouped <- AllPivots0 # Iterate over each subgroup for (subgroup in unique(pivots_grouped$sub_area)) { # Add subgroup heading cat("\n") cat("## Subgroup: ", subgroup, "\n") # Filter data for current subgroup subset_data <- dplyr::filter(pivots_grouped, sub_area == subgroup) # Generate visualizations for each field in the subgroup purrr::walk(subset_data$field, function(field_name) { cat("\n") ci_plot(field_name) cat("\n") cum_ci_plot(field_name) cat("\n") }) # Add page break after each subgroup cat("\\pagebreak\n") } }, error = function(e) { safe_log(paste("Error in subarea visualization section:", e$message), "ERROR") cat("Error generating subarea plots. See log for details.\n") }) ``` # Yield prediction The below table shows estimates of the biomass if you would harvest them now. ```{r yield_data_training, message=FALSE, warning=FALSE, include=FALSE} # Load and prepare yield prediction data with error handling tryCatch({ # Load CI quadrant data and fill missing values CI_quadrant <- readRDS(here::here(cumulative_CI_vals_dir, "All_pivots_Cumulative_CI_quadrant_year_v2.rds")) %>% dplyr::group_by(model) %>% tidyr::fill(field, sub_field, .direction = "downup") %>% dplyr::ungroup() # Check if tonnage_ha is empty if (all(is.na(CI_quadrant$tonnage_ha))) { safe_log("Lacking historic harvest data, please provide for yield prediction calculation", "WARNING") knitr::knit_exit() # Exit the chunk if tonnage_ha is empty } # Rename year column to season for consistency harvesting_data <- harvesting_data %>% dplyr::rename(season = year) # Join CI and yield data CI_and_yield <- dplyr::left_join(CI_quadrant, harvesting_data, by = c("field", "sub_field", "season")) %>% dplyr::group_by(sub_field, season) %>% dplyr::slice(which.max(DOY)) %>% dplyr::select(field, sub_field, tonnage_ha, cumulative_CI, DOY, season, sub_area) %>% dplyr::mutate(CI_per_day = cumulative_CI / DOY) # Define predictors and response variables predictors <- c("cumulative_CI", "DOY", "CI_per_day") response <- "tonnage_ha" # Prepare test and validation datasets CI_and_yield_test <- CI_and_yield %>% as.data.frame() %>% dplyr::filter(!is.na(tonnage_ha)) CI_and_yield_validation <- CI_and_yield_test # Prepare prediction dataset (fields without harvest data) prediction_yields <- CI_and_yield %>% as.data.frame() %>% dplyr::filter(is.na(tonnage_ha)) # Configure model training parameters ctrl <- caret::trainControl( method = "cv", savePredictions = TRUE, allowParallel = TRUE, number = 5, verboseIter = TRUE ) # Train the model with feature selection set.seed(202) # For reproducibility model_ffs_rf <- CAST::ffs( CI_and_yield_test[, predictors], CI_and_yield_test[, response], method = "rf", trControl = ctrl, importance = TRUE, withinSE = TRUE, tuneLength = 5, na.rm = TRUE ) # Function to prepare predictions with consistent naming and formatting prepare_predictions <- function(predictions, newdata) { return(predictions %>% as.data.frame() %>% dplyr::rename(predicted_Tcha = ".") %>% dplyr::mutate( sub_field = newdata$sub_field, field = newdata$field, Age_days = newdata$DOY, total_CI = round(newdata$cumulative_CI, 0), predicted_Tcha = round(predicted_Tcha, 0), season = newdata$season ) %>% dplyr::select(field, sub_field, Age_days, total_CI, predicted_Tcha, season) %>% dplyr::left_join(., newdata, by = c("field", "sub_field", "season")) ) } # Predict yields for the validation dataset pred_ffs_rf <- prepare_predictions(stats::predict(model_ffs_rf, newdata = CI_and_yield_validation), CI_and_yield_validation) # Predict yields for the current season (focus on mature fields over 300 days) pred_rf_current_season <- prepare_predictions(stats::predict(model_ffs_rf, newdata = prediction_yields), prediction_yields) %>% dplyr::filter(Age_days > 300) %>% dplyr::mutate(CI_per_day = round(total_CI / Age_days, 1)) safe_log("Successfully completed yield prediction calculations") }, error = function(e) { safe_log(paste("Error in yield prediction:", e$message), "ERROR") # Create empty dataframes to prevent errors in subsequent chunks pred_ffs_rf <- data.frame() pred_rf_current_season <- data.frame() }) ``` ```{r plotting_yield_data, echo=FALSE, fig.height=5, fig.width=8, message=FALSE, warning=FALSE} # Display yield prediction visualizations with error handling tryCatch({ if (nrow(pred_ffs_rf) > 0) { # Plot model performance (predicted vs actual) ggplot2::ggplot(pred_ffs_rf, ggplot2::aes(y = predicted_Tcha, x = tonnage_ha)) + ggplot2::geom_point(size = 2, alpha = 0.6) + ggplot2::geom_abline(intercept = 0, slope = 1, linetype = "dashed", color = "red") + ggplot2::scale_x_continuous(limits = c(0, 200)) + ggplot2::scale_y_continuous(limits = c(0, 200)) + ggplot2::labs(title = "Model Performance: \nPredicted vs Actual Tonnage/ha", x = "Actual tonnage/ha (Tcha)", y = "Predicted tonnage/ha (Tcha)") + ggplot2::theme_minimal() } if (nrow(pred_rf_current_season) > 0) { # Plot predicted yields by age ggplot2::ggplot(pred_rf_current_season, ggplot2::aes(x = Age_days, y = predicted_Tcha)) + ggplot2::geom_point(size = 2, alpha = 0.6) + ggplot2::labs(title = "Predicted Yields for Fields Over 300 Days \nOld Yet to Be Harvested", x = "Age (days)", y = "Predicted tonnage/ha (Tcha)") + ggplot2::scale_y_continuous(limits = c(0, 200)) + ggplot2::theme_minimal() # Display prediction table knitr::kable(pred_rf_current_season, digits = 0, caption = "Predicted Tonnage/ha for Fields Over 300 Days Old") } else { cat("No fields over 300 days old without harvest data available for yield prediction.") } }, error = function(e) { safe_log(paste("Error in yield prediction visualization:", e$message), "ERROR") cat("Error generating yield prediction visualizations. See log for details.") }) ```