---
title: "Compositions with inspectable spectral quantities"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Compositions with inspectable spectral quantities}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
resource_files:
- figures/logo.svg
- figures/logo-dark.svg
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(fig.width = 7.2, fig.height = 5.4, dpi = 100,
collapse = TRUE, comment = "#>")
library(hyperspectaculR)
```
The 0.2.0.9001 development candidate creates compositions and descriptive
summaries from an `hsi_cube` or real numeric `(rows, cols, bands)` array. Reading recordings and
calibration happen upstream. The default label is **input value**: a range
such as 0–1 does not establish calibrated reflectance. These plots do not
support physiological inference.
## Inputs and missing observations
```{r input}
cube <- hsa_demo_cube(rows = 24, cols = 32, bands = 12, seed = 42)
cube$mask <- matrix(TRUE, 24, 32)
cube$mask[1:3, ] <- FALSE
cube$data[10, 10, 2] <- Inf
cube$data[11, 10, 3] <- NA_real_
```
A mask must be a logical spatial matrix without missing entries. `FALSE`
excludes a pixel; nonfinite observations are excluded separately. Wavelength
metadata must be a numeric vector containing one strictly increasing finite
wavelength in nm per band. An `hsi_cube` uses the exact field names `data`,
`wavelengths` and `mask`; abbreviated names are not accepted. Arrays can carry
`mask` and `wavelengths` attributes. Without wavelengths the
coordinates are **band indices**; physical selection and slopes require nm.
```{r index-density, fig.height=4.6, fig.alt="Histogram of finite synthetic pixel values across 12 band indices, with binned means and pixel envelopes."}
hsa_spectral_density(cube$data, nbins = 32)
```
## Selection and display domains
```{r mandala, fig.alt="Eight-ring synthetic mandala with wavelength-based band selection; masked and nonfinite selected pixels are excluded."}
p <- hsa_mandala(cube, n_rings = 8, sampling = "wavelength")
p
hsa_provenance(p)$selection$ring_band_indices
```
Default mandala targets are linear in band index. Physical targets are equally
spaced in nm and select nearest measured bands, with ties toward shorter
wavelengths. Repeated selections and empty rings preserve geometry; the full
selection is in provenance. Unequal ring areas are a spatial composition,
not a spectral distribution estimate. One ring selects the first band.
```{r fusion, fig.alt="Synthetic RGB fusion from three contiguous band groups, using finite available contributors and a shared spatial mask."}
strict <- hsa_fusion(cube)
available <- hsa_fusion(cube, red = 9:12, green = 5:8, blue = 1:4,
missing = "available")
available
hsa_provenance(available)$missingness$contributors
```
All default RGB groups are disjoint contiguous index thirds, high bands to red,
even with `by = "wavelength"`. Explicit groups must all be supplied. They may
overlap between channels; duplicates within a group are errors. A one-band
explicit fusion is supported. Wavelength targets must be in range and resolve
to distinct bands within each group:
```{r wavelength-selection}
hsa_provenance(hsa_fusion(cube, red = c(545, 555), green = c(525, 535),
blue = c(500, 510), by = "wavelength"))$selection
```
`missing = "propagate"` requires every selected band. `"available"` averages
finite contributors separately by channel, still requiring all three channels
to have a contribution. It changes the contributing population and records
each channel's selected-band count, minimum/maximum contributors,
complete-pixel count and no-contributor-pixel count. Per-pixel contributor
counts are not retained. Missing RGB values are transparent.
```{r raw-domain}
p <- hsa_mandala(cube, n_rings = 8, stretch = "none",
display_limits = c(-1, 1))
hsa_provenance(p)$enhancement
```
Range and type-7 percentile stretches map to `[0, 1]`. A collapsed percentile
interval falls back to the finite range; constants map to the dark endpoint.
`"none"` preserves raw values and requires them to fit the fixed domain.
The record includes requested/effective method, fallback, raw endpoints and
clipping/exclusion counts. Plot data keep `raw_value` (or raw RGB channels).
```{r provenance}
changed <- p + ggplot2::labs(title = "Selected spatial scene") +
ggplot2::theme(plot.title = ggplot2::element_text(size = 16))
identical(hsa_provenance(p), hsa_provenance(changed))
```
Provenance describes the original rendering. Ordinary theme/lab additions
preserve it; arbitrary later plot edits are not recalculated. Inspect each
component before patchwork composition. The accessor rejects a combined
patchwork. Records contain no source cube or arbitrary input metadata.
## Composing publication panels
A caption that fits a standalone figure can overlap its neighbour when the
figure becomes a narrow panel. Choose the final output size before composing,
and rewrap the displayed labels for the available width. Keep the full
original-rendering records on the individual components. This two-column
example uses the optional `patchwork` package and a 7.2 by 5.4 inch figure:
```{r composition, fig.width=7.2, fig.height=5.4, eval=requireNamespace("patchwork", quietly=TRUE), fig.alt="Side-by-side synthetic mandala and RGB fusion with separately wrapped subtitles and captions for a 7.2 by 5.4 inch layout."}
wrap_label <- function(label, width) {
paste(strwrap(gsub("\n", " ", label, fixed = TRUE), width = width),
collapse = "\n")
}
originals <- list(
hsa_mandala(cube, n_rings = 8, interpolate = FALSE),
hsa_fusion(cube, interpolate = FALSE)
)
components <- lapply(originals, function(plot) {
plot + ggplot2::labs(
subtitle = wrap_label(plot$labels$subtitle, 43),
caption = wrap_label(plot$labels$caption, 53)
) + ggplot2::theme(plot.caption = ggplot2::element_text(hjust = 0))
})
if (!identical(lapply(components, hsa_provenance),
lapply(originals, hsa_provenance))) {
cli::cli_abort("Rewrapping changed original provenance.",
class = "hsa_example_provenance_error")
}
combined <- patchwork::wrap_plots(components, ncol = 2) +
patchwork::plot_annotation(theme = hsa_theme())
combined
```
Export with the same dimensions, for example
`ggplot2::ggsave("spectral-panels.png", combined, width = 7.2, height = 5.4, dpi = 300)`.
The character widths above suit this example; longer labels, another font or a
different panel width require a fresh visual check. Inspect
`hsa_provenance(components[[1]])` or the second component for calculation
details; the combined patchwork intentionally has no single provenance record.
## Exact density populations
```{r density, fig.height=4.6, fig.alt="Per-band retained shares for synthetic values from 0.1 to 0.6, with binned means, pixel envelopes and global raw-value percentile references."}
density <- hsa_spectral_density(cube, nbins = 32, limits = c(.1, .6),
normalise = "band", show_limits = TRUE)
density
accounting <- hsa_provenance(density)$missingness
with(accounting, all(finite == below + kept + above))
with(accounting, all(eligible == finite + nonfinite))
```
Limits include both endpoints, with right-closed interior bins. Outside values
are dropped, not clipped. Band shares divide by retained counts. Empty bands
have missing shares and gaps in the overlays. White binned means and 5–95%
pixel envelopes describe retained populations, not confidence intervals. Gold
lines use global raw finite eligible observations, including those outside
histogram limits; they are separate from image enhancement limits. Automatic
limits use type-7 0.1–99.9% quantiles, with a disclosed finite-range or constant
padding fallback. Irregular wavelengths have midpoint rectangle boundaries;
single-band density discloses a width-one fallback. Raw `H`, breaks, references
and accounting remain inspectable in the enhancement record.
## Sampling and experimental summaries
For adjacent measured values, total variation is `V = sum(abs(diff(x)))`;
default mean step is `V/(B-1)` in input-value units per measured step.
`normalization = "wavelength_span"` computes `V/L` in input value per nm.
A rise from 0 to 1 across 400 nm has mean steps 0.5 and 0.2 at three and six
samples, while both physical slopes equal 0.0025 per nm. Physical units define
the quantity; they do not correct missing peaks, noise or unequal sampling.
```{r slopes, fig.alt="Nonnegative RMS spectral slope of the synthetic cube in input value per nm; masked and incomplete spectra are transparent."}
example <- structure(list(data = array(c(0, 2, 2), c(1, 1, 3)),
wavelengths = c(500, 501, 505)), class = "hsi_cube")
hsa_spectral_flux(example, normalization = "wavelength_span")$data$raw_value
hsa_spectral_gradient(example)$data$raw_value
hsa_spectral_gradient(cube)
```
The independently verified values are 0.4 and approximately 0.894427 per nm.
Experimental RMS slope is `G = sqrt(sum(h * (diff(x)/h)^2)/L)` for wavelength
intervals `h`. It is nonnegative, loses derivative sign, and is not a spatial
gradient or uncertainty. Short intervals amplify noise. On a flat signal with
independent noise SD `sigma`, `E[G^2] = 2 * sigma^2 / L * sum(1/h)`; increasing
band count may increase this quantity. No smoothing or noise correction is
applied. Both physical slopes bridge unobserved intervals with straight lines;
interpolating extra points does not reconstruct absent information.
```{r quartiles, fig.width=9, fig.height=4.2, fig.alt="Synthetic within-pixel 25th percentile, median and 75th percentile panels with one shared display scale; complete spectra are required."}
hsa_spectral_quartiles(cube)
quartile_example <- array(c(0, 1, 4, 9), c(1, 1, 4))
hsa_spectral_quartiles(quartile_example)$data$raw_value
```
Type-7 quartiles are `(0.75, 2.5, 5.25)`. Permuting the bands preserves them
but can change flux. These experimental panels weight measured bands equally,
discard order and depend on sampling density. They are not uncertainty
intervals. All three panels share complete-spectrum validity and one pooled
stretch/domain/legend. A single band gives identical panels. Experimental
summaries offer descriptive comparisons, without universal instrument or
sampling comparability.
## Migration from 0.1.0
Masked/nonfinite pixels, dark endpoints, fusion thirds, raw domains, density
geometry and retained counts can change visible output. Missing wavelengths
are now labelled as indices. New exports include provenance, RMS slope and
quartiles. Existing argument order and `normalise` remain supported; conflicting
explicit `normalise`/`normalization` values error. Malformed metadata, fractional
or out-of-range indices, duplicate within-group selections, incomplete RGB
requests, invalid counts/probabilities and raw values outside the display domain
now fail explicitly. Entirely invalid outputs fail instead of producing an
apparently measured blank image.