Skip to content

Commit 5647e02

Browse files
new documentation
1 parent 04930ca commit 5647e02

12 files changed

+305
-23
lines changed

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export(cube_to_matrix)
99
export(median_scale)
1010
export(plot_cluster)
1111
export(plot_cluster_spectra)
12+
export(reconstruct_cluster_cube)
13+
export(reconstruct_flux_preserving_cube)
1214
export(segment)
1315
export(segment_approx)
1416
export(segment_blockward)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Released: 2026-03-18
88
- `build_starlet_mask()` and `segment_starlet()` for Sagui-style white-light starlet masking before clustering.
99
- `summarize_cluster_spectra()` for median, summed, and inverse-variance-weighted cluster spectra.
1010
- `choose_ncomp_by_snr()` for variance-aware component selection from an SNR threshold.
11+
- `reconstruct_cluster_cube()` and `reconstruct_flux_preserving_cube()` for representative and flux-preserving model cubes.
1112
- `scripts/run_manga_starlet_comparison.R` for a full-frame MaNGA comparison workflow.
1213

1314
## Changed

R/SpecMean.R

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
#' \item Generates a table linking each pixel's original spectrum to its assigned cluster.
2525
#' }
2626
#'
27+
#' This legacy helper is retained for compatibility. For new workflows, prefer
28+
#' \code{\link{reconstruct_cluster_cube}} or
29+
#' \code{\link{reconstruct_flux_preserving_cube}}.
30+
#'
2731
#' @return A list containing:
2832
#' \describe{
2933
#' \item{\code{MeanSpec}}{A data frame with the median spectrum for each cluster (rows = clusters, columns = wavelength).}
@@ -58,6 +62,13 @@ SpecMean <- function(cluster_result) {
5862
cluster_map <- cluster_result$cluster_map
5963
summary <- summarize_cluster_spectra(cluster_result)
6064
wavelength <- summary$wavelength
65+
recon <- reconstruct_cluster_cube(
66+
cluster_result = cluster_result,
67+
template = "median",
68+
preserve_mask = FALSE,
69+
fill_mode = "na",
70+
return_residual = FALSE
71+
)
6172

6273
IFU2D <- cube_to_matrix(cubedat)
6374
class2D <- as.vector(cluster_map)
@@ -72,19 +83,7 @@ SpecMean <- function(cluster_result) {
7283
check.names = FALSE
7384
)
7485

75-
median_cube <- matrix(NA_real_, nrow = nrow(IFU2D), ncol = ncol(IFU2D))
76-
for (i in seq_along(summary$cluster_ids)) {
77-
group <- summary$cluster_ids[i]
78-
group_indices <- which(class2D == group)
79-
median_cube[group_indices, ] <- matrix(
80-
summary$median_spectra[i, ],
81-
nrow = length(group_indices),
82-
ncol = ncol(IFU2D),
83-
byrow = TRUE
84-
)
85-
}
86-
87-
reshaped_cube <- array(median_cube, dim = dim(cubedat$imDat))
86+
reshaped_cube <- recon$model_cube$imDat
8887

8988
post_process_table <- as.data.frame(IFU2D)
9089
post_process_table$class <- class2D

R/reconstruct_cluster_cube.R

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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+
}

R/summarize_cluster_spectra.R

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
#'
33
#' This is the recommended post-segmentation summary layer for Capivara. It keeps
44
#' the segmentation step separate from downstream spectral products and can return
5-
#' median spectra, summed spectra, and inverse-variance-weighted mean spectra.
5+
#' median spectra, arithmetic mean spectra, summed spectra, and
6+
#' inverse-variance-weighted mean spectra.
67
#'
78
#' @param cluster_result A list returned by a segmentation function.
89
#' @param var_cube Optional variance cube matching the dimensions of the original
@@ -11,8 +12,9 @@
1112
#' variances. Use this to account for covariance if needed.
1213
#'
1314
#' @return A list with wavelength coordinates, cluster ids, cluster sizes,
14-
#' median spectra, summed spectra, and, when \code{var_cube} is supplied,
15-
#' propagated sum variances and inverse-variance-weighted means.
15+
#' per-wavelength finite counts, median spectra, mean spectra, summed spectra,
16+
#' and, when \code{var_cube} is supplied, propagated sum variances and
17+
#' inverse-variance-weighted means.
1618
#' @export
1719
summarize_cluster_spectra <- function(cluster_result,
1820
var_cube = NULL,
@@ -39,7 +41,14 @@ summarize_cluster_spectra <- function(cluster_result,
3941
ncol = ncol(flux_mat),
4042
dimnames = list(as.character(cluster_ids), wave_names)
4143
)
44+
mean_spectra <- median_spectra
4245
sum_spectra <- median_spectra
46+
finite_counts <- matrix(
47+
0L,
48+
nrow = length(cluster_ids),
49+
ncol = ncol(flux_mat),
50+
dimnames = list(as.character(cluster_ids), wave_names)
51+
)
4352
n_spaxels <- integer(length(cluster_ids))
4453

4554
has_var <- !is.null(var_cube)
@@ -63,6 +72,9 @@ summarize_cluster_spectra <- function(cluster_result,
6372
n_spaxels[i] <- length(idx)
6473
median_spectra[i, ] <- apply(X, 2, stats::median, na.rm = TRUE)
6574
sum_spectra[i, ] <- colSums(X, na.rm = TRUE)
75+
finite_counts[i, ] <- colSums(is.finite(X))
76+
mean_spectra[i, ] <- sum_spectra[i, ] / finite_counts[i, ]
77+
mean_spectra[i, finite_counts[i, ] == 0] <- NA_real_
6678

6779
if (has_var) {
6880
V <- var_mat[idx, , drop = FALSE]
@@ -88,7 +100,9 @@ summarize_cluster_spectra <- function(cluster_result,
88100
wavelength = wavelengths,
89101
cluster_ids = cluster_ids,
90102
n_spaxels = stats::setNames(n_spaxels, cluster_ids),
103+
finite_counts = finite_counts,
91104
median_spectra = median_spectra,
105+
mean_spectra = mean_spectra,
92106
sum_spectra = sum_spectra
93107
)
94108

README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ spec_summary <- summarize_cluster_spectra(res, var_cube = var_cube$imDat)
128128
inspection. For flux and SNR calculations, `sum_spectra` or the
129129
inverse-variance-weighted summary are usually better choices.
130130

131+
### Reconstructed Cubes
132+
133+
Use `reconstruct_cluster_cube()` to build a representative cube from
134+
cluster templates, or `reconstruct_flux_preserving_cube()` when you want
135+
a model cube that preserves the summed flux spectrum of the segmented
136+
data for later spectral fitting.
137+
138+
``` r
139+
rep_cube <- reconstruct_cluster_cube(res_star, template = "median")
140+
fit_cube <- reconstruct_flux_preserving_cube(res_star)
141+
```
142+
131143
## Release Notes
132144

133145
See [NEWS.md](NEWS.md) for the `0.2.0` release summary.
@@ -167,7 +179,6 @@ research. A BibTeX entry for the paper is:
167179
**539**(4), 3166–3179. <https://doi.org/10.1093/mnras/staf688>
168180
3. **Torch in R**: Paszke, Adam, et al. “PyTorch: An Imperative Style,
169181
High-Performance Deep Learning Library.” Advances in Neural
170-
Information Processing Systems 2019.
171-
------------------------------------------------------------------------
172-
173-
For more information, check the [Capivara GitHub webpage](https://rafaelsdesouza.github.io/capivara/).
182+
Information Processing Systems 2019. For more information, check the
183+
[Capivara GitHub
184+
webpage](https://rafaelsdesouza.github.io/capivara/).

README.qmd

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ spec_summary <- summarize_cluster_spectra(res, var_cube = var_cube$imDat)
121121
For flux and SNR calculations, `sum_spectra` or the inverse-variance-weighted
122122
summary are usually better choices.
123123

124+
### Reconstructed Cubes
125+
126+
Use `reconstruct_cluster_cube()` to build a representative cube from cluster
127+
templates, or `reconstruct_flux_preserving_cube()` when you want a model cube
128+
that preserves the summed flux spectrum of the segmented data for later
129+
spectral fitting.
130+
131+
```R
132+
rep_cube <- reconstruct_cluster_cube(res_star, template = "median")
133+
fit_cube <- reconstruct_flux_preserving_cube(res_star)
134+
```
135+
124136
## Release Notes
125137

126138
See [NEWS.md](NEWS.md) for the `0.2.0` release summary.

index.qmd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ res <- segment(cube, Ncomp = choice$Ncomp)
9898
spec_summary <- summarize_cluster_spectra(res, var_cube = var_cube$imDat)
9999
```
100100

101+
### Reconstructed Cubes
102+
103+
```R
104+
rep_cube <- reconstruct_cluster_cube(res_star, template = "median")
105+
fit_cube <- reconstruct_flux_preserving_cube(res_star)
106+
```
107+
101108
## Attribution
102109

103110
Please cite de Souza et al. if you find this code useful in your research.

man/SpecMean.Rd

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/reconstruct_cluster_cube.Rd

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)