Skip to content

Commit c261b44

Browse files
committed
added a spectral reconstruction function
1 parent e8a5171 commit c261b44

File tree

4 files changed

+281
-0
lines changed

4 files changed

+281
-0
lines changed

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export(pulses_above_threshold)
6060
export(sc2interval)
6161
export(sleep_int2Brown)
6262
export(solar_noon)
63+
export(spectral_reconstruction)
6364
export(supported_devices)
6465
export(symlog_trans)
6566
export(threshold_for_duration)

R/spectral_reconstruction.R

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#' Reconstruct spectral irradiance from sensor counts
2+
#'
3+
#' This function takes sensor data in the form of (normalized) counts and
4+
#' reconstructs a spectral power distribution (SPD) through a calibration matrix.
5+
#' The matrix takes the form of `sensor channel x wavelength`, and the spectrum
6+
#' results form a linear combination of `counts x calibration-value` for any
7+
#' wavelength in the matrix. Handles multiple sensor readings by returning a list of spectra
8+
#'
9+
#' Please note that calibration matrices are not provided by LightLogR, but can
10+
#' be provided by a wearable device manufacturer. Counts can be normalized with
11+
#' the [normalize_counts()] function, provided that the output also contains a
12+
#' `gain` column.
13+
#'
14+
#' @param sensor_channels Named numeric vector or dataframe with
15+
#' sensor readings. Names must match calibration matrix columns.
16+
#' @param calibration_matrix Matrix or dataframe with sensor-named columns and
17+
#' wavelength-indexed rows
18+
#' @param format Output format: "long" (list of tibbles) or "wide" (dataframe)
19+
#'
20+
#' @return
21+
#' - "long": List of tibbles (wavelength, irradiance)
22+
#' - "wide": Dataframe with wavelength columns and one row per spectrum
23+
#'
24+
#' @export
25+
#'
26+
#' @examples
27+
#' # Calibration matrix example
28+
#' calib <- matrix(1:12, ncol=3, dimnames = list(400:403, c("R", "G", "B")))
29+
#'
30+
#' # Named vector input
31+
#' spectral_reconstruction(c(R=1, G=2, B=3), calib)
32+
#'
33+
#' # Dataframe input
34+
#' df <- data.frame(R=1, G=2, B=3, other_col=10)
35+
#' spectral_reconstruction(dplyr::select(df, R:B), calib)
36+
#'
37+
#' # Multiple spectra: as list columns
38+
#' df <- data.frame(Measurement = c(1,2), R=c(1,2), G=c(2,4), B=c(3,6))
39+
#' df <-
40+
#' df |>
41+
#' dplyr::mutate(
42+
#' Spectrum = spectral_reconstruction(dplyr::pick(R:B), calib)
43+
#' )
44+
#' df |> tidyr::unnest(Spectrum)
45+
#'
46+
#' # Multiple spectra: as extended dataframes
47+
#' df |>
48+
#' dplyr::mutate(
49+
#' Spectrum = spectral_reconstruction(dplyr::pick(R:B), calib, "wide"))
50+
51+
spectral_reconstruction <- function(sensor_channels,
52+
calibration_matrix,
53+
format = c("long", "wide")) {
54+
format <- match.arg(format)
55+
56+
# Convert calibration matrix to tibble
57+
calibration_df <- tryCatch({
58+
dplyr::as_tibble(
59+
as.data.frame(calibration_matrix),
60+
rownames = "wavelength"
61+
)
62+
}, error = function(e) stop("Invalid calibration matrix format"))
63+
64+
# Validate calibration structure
65+
if (!"wavelength" %in% colnames(calibration_df)) {
66+
stop("Calibration matrix must have wavelength row names")
67+
}
68+
cal_cols <- setdiff(colnames(calibration_df), "wavelength")
69+
if (length(cal_cols) == 0) stop("Calibration matrix needs sensor columns")
70+
71+
# Process sensor inputs
72+
if (is.data.frame(sensor_channels)) {
73+
sensor_data <- tryCatch(
74+
dplyr::select(sensor_channels, dplyr::all_of(cal_cols)),
75+
error = function(e) stop("Missing required sensor columns")
76+
)
77+
sensor_matrix <- as.matrix(sensor_data)
78+
} else if (is.numeric(sensor_channels) && !is.null(names(sensor_channels))) {
79+
if (!all(cal_cols %in% names(sensor_channels))) {
80+
stop("Sensor names mismatch with calibration columns")
81+
}
82+
sensor_matrix <- matrix(
83+
sensor_channels[cal_cols],
84+
nrow = 1,
85+
dimnames = list(NULL, cal_cols)
86+
)
87+
} else {
88+
stop("sensor_channels must be named vector or dataframe")
89+
}
90+
91+
# Matrix multiplication
92+
calib_values <- as.matrix(calibration_df[, cal_cols])
93+
irradiance_matrix <- calib_values %*% t(sensor_matrix)
94+
95+
# Format output
96+
wavelengths <- as.numeric(calibration_df$wavelength)
97+
98+
if (format == "wide") {
99+
wide_df <- as.data.frame(t(irradiance_matrix))
100+
colnames(wide_df) <- wavelengths
101+
return(wide_df)
102+
} else {
103+
result_list <- lapply(1:ncol(irradiance_matrix), function(i) {
104+
dplyr::tibble(
105+
wavelength = wavelengths,
106+
irradiance = irradiance_matrix[, i]
107+
)
108+
})
109+
if (ncol(irradiance_matrix) == 1) result_list[[1]] else result_list
110+
}
111+
}

man/spectral_reconstruction.Rd

Lines changed: 65 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# tests/testthat/test-spectral_reconstruction.R
2+
3+
test_that("Valid inputs produce correct outputs", {
4+
# Create 3-channel calibration matrix (R, G, B)
5+
calib <- matrix(
6+
c(0.1, 0.2, 0.3, 0.4, 0.5,
7+
0.6, 0.7, 0.8, 0.9, 1.0,
8+
1.1, 1.2, 1.3, 1.4, 1.5),
9+
ncol = 3,
10+
dimnames = list(400:404, c("R", "G", "B"))
11+
)
12+
13+
# Test named vector input
14+
sensor_vec <- c(R = 1, G = 2, B = 3)
15+
expect_silent(
16+
result_vec <- spectral_reconstruction(sensor_vec, calib)
17+
)
18+
expected <- calib[,"R"]*1 + calib[,"G"]*2 + calib[,"B"]*3
19+
expect_equal(result_vec$irradiance, as.numeric(expected))
20+
21+
# Test dataframe input with tidyselect
22+
df <- data.frame(R = 1, G = 2, B = 3, other = "x")
23+
expect_silent(
24+
result_df <- spectral_reconstruction(dplyr::select(df, R:B), calib)
25+
)
26+
expect_equal(result_df, result_vec)
27+
28+
# Test wide format
29+
wide_result <- spectral_reconstruction(df, calib, format = "wide")
30+
expect_equal(nrow(wide_result), 1)
31+
expect_equal(colnames(wide_result), as.character(400:404))
32+
expect_equal(as.numeric(wide_result[1,]), as.numeric(expected))
33+
})
34+
35+
test_that("Multi-row inputs return correct formats", {
36+
# 4 wavelengths x 2 sensors calibration
37+
calib <- matrix(
38+
1:8, ncol = 2,
39+
dimnames = list(400:403, c("S1", "S2"))
40+
)
41+
42+
multi_df <- data.frame(S1 = 1:3, S2 = 4:6)
43+
44+
# Long format (list of tibbles)
45+
long_result <- spectral_reconstruction(multi_df, calib)
46+
expect_type(long_result, "list")
47+
expect_length(long_result, 3)
48+
49+
# Verify second spectrum calculation
50+
expected_2 <- calib[,"S1"]*2 + calib[,"S2"]*5
51+
expect_equal(long_result[[2]]$irradiance, as.numeric(expected_2))
52+
53+
# Wide format (dataframe)
54+
wide_result <- spectral_reconstruction(multi_df, calib, "wide")
55+
expect_equal(nrow(wide_result), 3)
56+
expect_equal(
57+
as.numeric(wide_result[3,]),
58+
as.numeric(calib[,"S1"]*3 + calib[,"S2"]*6)
59+
)
60+
})
61+
62+
test_that("Input validation works correctly", {
63+
# Proper 3x3 calibration matrix
64+
good_calib <- matrix(
65+
1:9, ncol = 3,
66+
dimnames = list(400:402, c("R", "G", "B"))
67+
)
68+
69+
# Test calibration matrix without column names
70+
bad_calib <- matrix(1:6, ncol = 2, dimnames = list(NULL, NULL))
71+
expect_error(
72+
spectral_reconstruction(c(R=1), bad_calib),
73+
"Sensor names mismatch with calibration columns",
74+
fixed = TRUE
75+
)
76+
77+
# Test calibration matrix with wrong column names
78+
misnamed_calib <- matrix(1:6, ncol = 2, dimnames = list(NULL, c("X", "Y")))
79+
expect_error(
80+
spectral_reconstruction(c(R=1), misnamed_calib),
81+
"Sensor names mismatch with calibration columns",
82+
fixed = TRUE
83+
)
84+
})
85+
86+
87+
test_that("Edge cases are handled properly", {
88+
# Single wavelength calibration
89+
calib_single <- matrix(
90+
1:3, ncol = 3, nrow = 1,
91+
dimnames = list(400, c("R", "G", "B"))
92+
)
93+
expect_silent(
94+
spectral_reconstruction(c(R=1, G=2, B=3), calib_single)
95+
)
96+
97+
# Zero values
98+
calib_zero <- matrix(
99+
0, ncol = 3, nrow = 3,
100+
dimnames = list(400:402, c("R", "G", "B"))
101+
)
102+
result_zero <- spectral_reconstruction(c(R=1, G=2, B=3), calib_zero)
103+
expect_equal(result_zero$irradiance, rep(0,3))
104+
})

0 commit comments

Comments
 (0)