-
Notifications
You must be signed in to change notification settings - Fork 320
feat: implement automated reporting and visualization layer #4040
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
dlebauer
merged 31 commits into
PecanProject:develop
from
ayushman1210:automated-reporting-and-visualization
Aug 14, 2026
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
b6d18a1
feat: implement automated reporting and visualization layer
ayushman1210 0a3b5e4
Merge branch 'develop' into automated-reporting-and-visualization
ayushman1210 5e01b22
Merge branch 'develop' into automated-reporting-and-visualization
ayushman1210 1f2609f
Fix: Serialize benchmark_results to RDS to prevent Quarto YAML parsin…
ayushman1210 4da8557
Fix CI: Add quarto dependency, update tests, and sync Roxygen docs
ayushman1210 8efa210
Feat: Add uncertainty ribbon and error bars to timeseries plot
ayushman1210 9635b36
chore: remove local scratch script from tracking
ayushman1210 4113e58
Merge branch 'develop' into automated-reporting-and-visualization
ayushman1210 c92ff29
Update modules/benchmark/tests/testthat/test-visualization.R
ayushman1210 958f37d
Refine validation toolkit based on maintainer feedback
ayushman1210 1ee57d5
Merge branch 'develop' into automated-reporting-and-visualization
ayushman1210 3278216
Merge branch 'develop' into automated-reporting-and-visualization
ayushman1210 99e6e19
fix(benchmark): update docs, dependencies, and unit tests for timeser…
ayushman1210 776c019
refactor(benchmark): delegate plot coverage to metric_Coverage, reuse…
ayushman1210 92604f8
Merge branch 'develop' into automated-reporting-and-visualization
ayushman1210 6788ce4
Merge branch 'develop' into automated-reporting-and-visualization
dlebauer 7990ebc
adressed review comments on metric coverage.PMU plot label and docs
ayushman1210 b53fd2c
Merge branch 'develop' into automated-reporting-and-visualization
ayushman1210 87dc809
document unit arg in timeseries plot
divine7022 8e6df46
document unit arg in residual plot
divine7022 a578ba4
document unit arg in scatter plot
divine7022 f2d6f0c
regenerate timeseries plot man page
divine7022 ca72148
regenerate residual plot man page
divine7022 12563ab
regenerate scatter plot man page
divine7022 6f43127
regenerate compute_metrics man page
divine7022 88fee32
use .data pronoun for label in timeseries plot annotation
divine7022 b77afa9
use .data pronoun for label in residual plot annotation
divine7022 73319c7
Merge branch 'develop' into automated-reporting-and-visualization
ayushman1210 3097520
Merge branch 'develop' into automated-reporting-and-visualization
dlebauer eeec791
Merge branch 'develop' into automated-reporting-and-visualization
ayushman1210 6db9bd6
Merge branch 'develop' into automated-reporting-and-visualization
ayushman1210 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| ##' Generate Validation Benchmark Report | ||
| ##' | ||
| ##' @param benchmark_results A list containing `metrics` (data.frame), `aligned_data` (data.frame), and `plots` (list of ggplot objects) returned by the validation pipeline. | ||
| ##' @param output_file The path where the compiled report should be saved (e.g., "validation_report.html"). | ||
| ##' @param template The path to the Quarto template. Defaults to the one provided in the package `inst/reports/Validation_report.qmd`. | ||
| ##' | ||
| ##' @author PEcAn Project | ||
| ##' @export | ||
| generate_validation_report <- function(benchmark_results, output_file = "Validation_report.html", template = NULL) { | ||
| PEcAn.logger::logger.info("Generating Validation Benchmark Report...") | ||
|
|
||
| if (is.null(template)) { | ||
| template <- system.file("reports", "Validation_report.qmd", package = "PEcAn.benchmark") | ||
| if (template == "") { | ||
| # Fallback for development mode | ||
| template <- file.path(getwd(), "inst", "reports", "Validation_report.qmd") | ||
| } | ||
| } | ||
|
|
||
| if (!file.exists(template)) { | ||
| PEcAn.logger::logger.severe("Template file not found:", template) | ||
| } | ||
|
|
||
| if (!requireNamespace("quarto", quietly = TRUE)) { | ||
| PEcAn.logger::logger.severe("The 'quarto' package is required to generate the report.") | ||
| } | ||
|
|
||
| # Ensure absolute paths | ||
| output_file <- normalizePath(output_file, mustWork = FALSE) | ||
| output_dir <- dirname(output_file) | ||
|
|
||
| if (!dir.exists(output_dir)) { | ||
| dir.create(output_dir, recursive = TRUE) | ||
| } | ||
|
|
||
| # Copy template to output directory to avoid permission issues in system folders | ||
| temp_qmd <- file.path(output_dir, basename(template)) | ||
| file.copy(template, temp_qmd, overwrite = TRUE) | ||
|
|
||
| # Quarto execute_params are converted to YAML. Complex R objects like ggplots | ||
| # cannot be passed via YAML. We must save them to an RDS and pass the path. | ||
| results_rds <- file.path(output_dir, "benchmark_results.rds") | ||
| saveRDS(benchmark_results, results_rds) | ||
|
|
||
| # Render the document | ||
| tryCatch({ | ||
| quarto::quarto_render( | ||
| input = temp_qmd, | ||
| output_file = basename(output_file), | ||
| execute_params = list(benchmark_results = results_rds) | ||
| ) | ||
|
|
||
| PEcAn.logger::logger.info("Validation report successfully generated at:", output_file) | ||
| }, error = function(e) { | ||
| PEcAn.logger::logger.severe("Failed to render validation report:", e$message) | ||
| }, finally = { | ||
| # Clean up the temporary template file | ||
| if (file.exists(temp_qmd)) { | ||
| file.remove(temp_qmd) | ||
| } | ||
| }) | ||
|
|
||
| return(invisible(output_file)) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,34 @@ | ||
| #' @name metric_Coverage | ||
| #' @title Prediction Interval Coverage | ||
| #' @export | ||
| #' @param dat dataframe with columns `model_q025` and `model_q975` | ||
| #' @param dat dataframe with columns `model_q05` and `model_q95` (or `model_q025` and `model_q975`) | ||
| #' @param ... ignored | ||
| #' @return A numeric value representing the fraction of observations that fall within the 95% prediction interval. | ||
| #' @return A numeric value representing the fraction of observations that fall within the prediction interval. | ||
| #' @details | ||
| #' Measures the fraction of observations that fall within the model's | ||
| #' stated 95% prediction interval. | ||
| #' stated prediction interval (defaults to 90% interval via `model_q05`/`model_q95`, or 95% via `model_q025`/`model_q975`). | ||
|
|
||
| metric_Coverage <- function(dat, ...) { | ||
| if (!"model_q025" %in% names(dat) || !"model_q975" %in% names(dat)) { | ||
| PEcAn.logger::logger.severe("Metric Coverage requires 'model_q025' and 'model_q975' columns in the dataset.") | ||
| q_low <- NULL | ||
| q_high <- NULL | ||
|
|
||
| if (all(c("model_q05", "model_q95") %in% names(dat))) { | ||
| q_low <- dat$model_q05 | ||
| q_high <- dat$model_q95 | ||
| } else if (all(c("model_q025", "model_q975") %in% names(dat))) { | ||
| q_low <- dat$model_q025 | ||
| q_high <- dat$model_q975 | ||
| } else { | ||
| PEcAn.logger::logger.severe("Metric Coverage requires quantile columns ('model_q05'/'model_q95' or 'model_q025'/'model_q975') in the dataset.") | ||
| } | ||
|
|
||
| PEcAn.logger::logger.info("Metric: Prediction Interval Coverage") | ||
|
|
||
| valid <- !is.na(dat$obvs) & !is.na(dat$model_q025) & !is.na(dat$model_q975) | ||
| covered <- dat$obvs[valid] >= dat$model_q025[valid] & dat$obvs[valid] <= dat$model_q975[valid] | ||
|
|
||
| valid <- !is.na(dat$obvs) & !is.na(q_low) & !is.na(q_high) | ||
| if (!any(valid)) { | ||
| return(NA_real_) | ||
| } | ||
|
|
||
| covered <- dat$obvs[valid] >= q_low[valid] & dat$obvs[valid] <= q_high[valid] | ||
| return(mean(covered)) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,29 +1,36 @@ | ||
| ##' Scatter Plot | ||
| ##' | ||
| ##' @param metric_dat dataframe to plot, with at least columns `model` and `obvs` | ||
| ##' @param var ignored | ||
| ##' @param var title for the plot | ||
| ##' @param unit measurement unit for the variable, added to the axis labels; NULL to omit | ||
| ##' @param filename path to save plot, or NA to not save | ||
| ##' @param draw.plot logical: Return the plot object? | ||
| ##' | ||
| ##' @author Betsy Cowdery | ||
| ##' @export | ||
|
|
||
| metric_scatter_plot <- function(metric_dat, var, filename = NA, draw.plot = is.na(filename)) { | ||
| metric_scatter_plot <- function(metric_dat, var, unit = NULL, filename = NA, draw.plot = is.na(filename)) { | ||
| PEcAn.logger::logger.info("Metric: Scatter Plot") | ||
|
|
||
| p <- ggplot2::ggplot(data = metric_dat) | ||
| p <- p + ggplot2::geom_point(ggplot2::aes(x = .data$model, y = .data$obvs), size = 4) | ||
| p <- p + ggplot2::geom_abline(slope = 1, intercept = 0, colour = "#666666", | ||
| size = 2, linetype = 2) | ||
| metric_dat <- as.data.frame(metric_dat) | ||
|
|
||
| xlab <- if (is.null(unit)) "modeled" else sprintf("modeled (%s)", unit) | ||
| ylab <- if (is.null(unit)) "observed" else sprintf("observed (%s)", unit) | ||
|
|
||
| p <- ggplot2::ggplot(data = metric_dat, ggplot2::aes(x = .data$model, y = .data$obvs)) + | ||
| ggplot2::geom_point(size = 2, alpha = 0.7, colour = "#619CFF") + | ||
| ggplot2::geom_abline(slope = 1, intercept = 0, colour = "#666666", | ||
| linewidth = 1, linetype = 2) + | ||
| ggplot2::labs(title = var, x = xlab, y = ylab, colour = NULL, fill = NULL) + | ||
| ggplot2::theme_minimal(base_size = 12) | ||
|
|
||
| if (!is.na(filename)) { | ||
| grDevices::pdf(filename, width = 10, height = 6) | ||
| plot(p) | ||
| print(p) | ||
| grDevices::dev.off() | ||
| } | ||
|
|
||
| if (draw.plot) { | ||
| return(p) | ||
| } | ||
|
|
||
| invisible(p) | ||
| } # metric_scatter_plot |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.