|
| 1 | +#' Reconstruct a Model Cube from Cluster Representative Spectra |
| 2 | +#' |
| 3 | +#' This function builds a model cube from a segmentation result by assigning one |
| 4 | +#' representative spectrum to every pixel in each cluster. It is useful for |
| 5 | +#' visualization, denoising, and downstream spectral analysis where a |
| 6 | +#' cluster-level spectral template is desired. |
| 7 | +#' |
| 8 | +#' @param cluster_result A list returned by a segmentation function. |
| 9 | +#' @param template Representative spectrum to assign to each cluster. |
| 10 | +#' \code{"median"} is robust, \code{"mean"} is the arithmetic cluster mean, |
| 11 | +#' and \code{"weighted_mean"} uses inverse-variance weighting. |
| 12 | +#' @param var_cube Optional variance cube. Required when |
| 13 | +#' \code{template = "weighted_mean"}. |
| 14 | +#' @param variance_inflation Multiplicative factor applied to propagated |
| 15 | +#' variances when \code{var_cube} is supplied. |
| 16 | +#' @param preserve_mask Logical; if \code{TRUE}, channels that were non-finite in |
| 17 | +#' the original cube remain masked in the reconstructed cube. |
| 18 | +#' @param fill_mode Either \code{"na"} or \code{"zero"} for unassigned pixels and, |
| 19 | +#' when \code{preserve_mask = TRUE}, for masked spectral channels. |
| 20 | +#' @param return_residual Logical; if \code{TRUE}, also return the residual cube. |
| 21 | +#' |
| 22 | +#' @return A list with the reconstructed cube, optional residual cube, the |
| 23 | +#' template spectra, a cluster summary object, and a global flux comparison |
| 24 | +#' between the original and reconstructed cubes. |
| 25 | +#' @export |
| 26 | +reconstruct_cluster_cube <- function(cluster_result, |
| 27 | + template = c("median", "mean", "weighted_mean"), |
| 28 | + var_cube = NULL, |
| 29 | + variance_inflation = 1, |
| 30 | + preserve_mask = TRUE, |
| 31 | + fill_mode = c("na", "zero"), |
| 32 | + return_residual = TRUE) { |
| 33 | + template <- match.arg(template) |
| 34 | + fill_mode <- match.arg(fill_mode) |
| 35 | + |
| 36 | + if (identical(template, "weighted_mean") && is.null(var_cube)) { |
| 37 | + stop("`var_cube` must be supplied when `template = \"weighted_mean\"`.") |
| 38 | + } |
| 39 | + |
| 40 | + cubedat <- .as_cubedat(cluster_result$original_cube) |
| 41 | + flux_mat <- cube_to_matrix(cubedat) |
| 42 | + cluster_vec <- as.vector(cluster_result$cluster_map) |
| 43 | + |
| 44 | + if (length(cluster_vec) != nrow(flux_mat)) { |
| 45 | + stop("Cluster map dimensions do not match the input cube dimensions.") |
| 46 | + } |
| 47 | + |
| 48 | + summary <- summarize_cluster_spectra( |
| 49 | + cluster_result = cluster_result, |
| 50 | + var_cube = var_cube, |
| 51 | + variance_inflation = variance_inflation |
| 52 | + ) |
| 53 | + |
| 54 | + template_spectra <- switch( |
| 55 | + template, |
| 56 | + median = summary$median_spectra, |
| 57 | + mean = summary$mean_spectra, |
| 58 | + weighted_mean = summary$weighted_mean_spectra |
| 59 | + ) |
| 60 | + |
| 61 | + model_mat <- matrix(NA_real_, nrow = nrow(flux_mat), ncol = ncol(flux_mat)) |
| 62 | + |
| 63 | + for (i in seq_along(summary$cluster_ids)) { |
| 64 | + idx <- which(cluster_vec == summary$cluster_ids[i]) |
| 65 | + if (!length(idx)) { |
| 66 | + next |
| 67 | + } |
| 68 | + |
| 69 | + model_mat[idx, ] <- matrix( |
| 70 | + template_spectra[i, ], |
| 71 | + nrow = length(idx), |
| 72 | + ncol = ncol(flux_mat), |
| 73 | + byrow = TRUE |
| 74 | + ) |
| 75 | + } |
| 76 | + |
| 77 | + if (isTRUE(preserve_mask)) { |
| 78 | + orig_finite <- is.finite(flux_mat) |
| 79 | + if (fill_mode == "na") { |
| 80 | + model_mat[!orig_finite & !is.na(cluster_vec)] <- NA_real_ |
| 81 | + } else { |
| 82 | + model_mat[!orig_finite & !is.na(cluster_vec)] <- 0 |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + unassigned <- is.na(cluster_vec) |
| 87 | + if (fill_mode == "zero") { |
| 88 | + model_mat[unassigned, ] <- 0 |
| 89 | + } |
| 90 | + |
| 91 | + original_sum_spectrum <- colSums(flux_mat[!unassigned, , drop = FALSE], na.rm = TRUE) |
| 92 | + model_sum_spectrum <- colSums(model_mat[!unassigned, , drop = FALSE], na.rm = TRUE) |
| 93 | + flux_difference <- model_sum_spectrum - original_sum_spectrum |
| 94 | + flux_check <- list( |
| 95 | + original_sum_spectrum = original_sum_spectrum, |
| 96 | + model_sum_spectrum = model_sum_spectrum, |
| 97 | + difference = flux_difference, |
| 98 | + max_abs_difference = max(abs(flux_difference), na.rm = TRUE) |
| 99 | + ) |
| 100 | + |
| 101 | + model_cube <- cubedat |
| 102 | + model_cube$imDat <- array(model_mat, dim = dim(cubedat$imDat)) |
| 103 | + |
| 104 | + template_df <- data.frame( |
| 105 | + cluster = summary$cluster_ids, |
| 106 | + template_spectra, |
| 107 | + check.names = FALSE |
| 108 | + ) |
| 109 | + |
| 110 | + out <- list( |
| 111 | + model_cube = model_cube, |
| 112 | + template = template, |
| 113 | + template_spectra = template_df, |
| 114 | + cluster_summary = summary, |
| 115 | + flux_check = flux_check |
| 116 | + ) |
| 117 | + |
| 118 | + if (isTRUE(return_residual)) { |
| 119 | + residual_cube <- cubedat |
| 120 | + residual_cube$imDat <- array(flux_mat - model_mat, dim = dim(cubedat$imDat)) |
| 121 | + out$residual_cube <- residual_cube |
| 122 | + } |
| 123 | + |
| 124 | + out |
| 125 | +} |
| 126 | + |
| 127 | +#' Reconstruct a Flux-preserving Model Cube from Cluster Spectra |
| 128 | +#' |
| 129 | +#' This is a convenience wrapper around \code{\link{reconstruct_cluster_cube}} |
| 130 | +#' that uses the arithmetic cluster mean while preserving the original spectral |
| 131 | +#' mask. In this mode, the reconstructed cube preserves the summed flux spectrum |
| 132 | +#' of the segmented cube on the observed support, which makes it a sensible |
| 133 | +#' default for later spectral fitting. |
| 134 | +#' |
| 135 | +#' @param cluster_result A list returned by a segmentation function. |
| 136 | +#' @param fill_mode Either \code{"na"} or \code{"zero"} for unassigned pixels |
| 137 | +#' and masked channels. |
| 138 | +#' @param return_residual Logical; if \code{TRUE}, also return the residual cube. |
| 139 | +#' |
| 140 | +#' @return The same structure returned by \code{\link{reconstruct_cluster_cube}}. |
| 141 | +#' @export |
| 142 | +reconstruct_flux_preserving_cube <- function(cluster_result, |
| 143 | + fill_mode = c("na", "zero"), |
| 144 | + return_residual = TRUE) { |
| 145 | + reconstruct_cluster_cube( |
| 146 | + cluster_result = cluster_result, |
| 147 | + template = "mean", |
| 148 | + preserve_mask = TRUE, |
| 149 | + fill_mode = fill_mode, |
| 150 | + return_residual = return_residual |
| 151 | + ) |
| 152 | +} |
0 commit comments