test_that("read_tga_txt parses a well-formed 3-column export", { tmp <- tempfile(fileext = ".txt") writeLines(c("Temp(C)\tTime(min)\tWeight(%)", "25\t0\t100.0", "50\t2\t99.8", "100\t6\t98.1"), tmp) out <- read_tga_txt(tmp) unlink(tmp) expect_equal(names(out), c("temperature_C", "time_min", "weight_pct")) expect_equal(out$temperature_C, c(25, 50, 100)) expect_equal(nrow(out), 3) }) test_that("read_tga_txt skips malformed lines and records them", { tmp <- tempfile(fileext = ".txt") # "header junk" is silently ignored (doesn't start with a digit, per # skip_pattern); "25 abc 100.0" starts with a digit so is attempted, but # fails to parse as 3 numeric fields, so it lands in skipped_lines. writeLines(c("header junk", "25 0 100.0", "25 abc 100.0", "50 2 99.8"), tmp) out <- read_tga_txt(tmp, col_names = c("temperature_C", "time_min", "weight_pct")) unlink(tmp) expect_equal(nrow(out), 2) expect_true(length(attr(out, "skipped_lines")) >= 1) }) test_that("read_tga_txt works with a 2-column file", { tmp <- tempfile(fileext = ".txt") writeLines(c("25 100.0", "100 98.1", "500 65.0"), tmp) out <- read_tga_txt(tmp, col_names = c("temperature_C", "weight_pct")) unlink(tmp) expect_equal(names(out), c("temperature_C", "weight_pct")) }) test_that("read_tga_txt errors on missing file and empty data", { expect_error(read_tga_txt("/no/such/file.txt"), "not found") tmp <- tempfile(fileext = ".txt") writeLines(c("not numeric at all", "still not numeric"), tmp) expect_error(read_tga_txt(tmp), "No parseable") unlink(tmp) }) test_that("tga_normalize rescales mass to percent of initial reading", { tga <- data.frame(temperature_C = c(25, 100, 500), weight_mg = c(10, 9.9, 6.5)) out <- tga_normalize(tga) expect_equal(out$weight_pct[1], 100) expect_equal(out$weight_pct[3], 65) }) test_that("tga_normalize errors on non-positive initial mass or missing column", { expect_error(tga_normalize(data.frame(weight_mg = c(0, 5))), "positive") expect_error(tga_normalize(data.frame(x = 1)), "not found") }) test_that("tga_dtg recovers a known single-step weight loss as one DTG peak", { set.seed(1) T <- seq(25, 800, by = 4) W <- 100 - 40 / (1 + exp(-(T - 350) / 15)) + rnorm(length(T), 0, 0.05) tga <- data.frame(temperature_C = T, weight_pct = W) dtg <- tga_dtg(tga) expect_equal(nrow(dtg), length(T)) peak_T <- dtg$temperature_C[which.max(dtg$dtg)] expect_equal(peak_T, 350, tolerance = 15) }) test_that("tga_dtg errors with too few distinct temperature points", { tga <- data.frame(temperature_C = c(25, 25, 26), weight_pct = c(100, 100, 99)) expect_error(tga_dtg(tga), "at least 10") }) test_that("find_dtg_peaks finds the single peak in a clean one-step curve", { set.seed(2) T <- seq(25, 800, by = 4) W <- 100 - 40 / (1 + exp(-(T - 350) / 15)) dtg <- tga_dtg(data.frame(temperature_C = T, weight_pct = W)) pk <- find_dtg_peaks(dtg, min_prominence = 0.05) expect_equal(nrow(pk), 1) expect_equal(pk$peak_temperature_C, 350, tolerance = 10) }) test_that("find_dtg_peaks returns an empty data frame when nothing qualifies", { dtg <- data.frame(temperature_C = 1:20, dtg = rep(-0.1, 20)) pk <- find_dtg_peaks(dtg) expect_equal(nrow(pk), 0) }) test_that("assign_dtg_peaks matches known ranges and returns NA outside them", { out <- assign_dtg_peaks(c(80, 350, 5000)) expect_equal(out$stage, c("Moisture", "Cellulose", NA_character_)) }) test_that("tga_stages feeds into proximate_analysis and sums correctly", { T <- seq(25, 800, by = 5) W <- ifelse(T <= 110, 100 - (5 / 110) * T, ifelse(T <= 650, 95 - (40 / (650 - 110)) * (T - 110), 55)) tga <- data.frame(temperature_C = T, weight_pct = W) out <- tga_stages(tga) expect_equal(out$moisture_pct, 5, tolerance = 0.5) expect_equal(out$VM_pct, 40, tolerance = 0.5) expect_equal(out$ash_pct, 55, tolerance = 0.5) expect_equal(out$moisture_pct + out$VM_pct + out$ash_pct + out$fixed_carbon_pct, 100, tolerance = 1e-6) }) test_that("tga_stages errors when breakpoints are out of order or out of range", { tga <- data.frame(temperature_C = seq(25, 500, by = 25), weight_pct = seq(100, 60, length.out = 20)) expect_error(tga_stages(tga, moisture_end = 600, vm_end = 700), "Require min") expect_error(tga_stages(tga, moisture_end = 300, vm_end = 200), "Require min") }) test_that("tga_stages warns when the curve is not normalized near 100%", { tga <- data.frame(temperature_C = seq(25, 800, by = 25), weight_pct = seq(40, 20, length.out = 32)) expect_warning(tga_stages(tga), "not close to 100") }) test_that("tga_stages_batch processes multiple samples and skips failures with a warning", { mk <- function(id, vm_center) { Tt <- seq(25, 800, by = 10) Ww <- 100 - 5 * (Tt > 110) - 40 / (1 + exp(-(Tt - vm_center) / 20)) data.frame(id = id, temperature_C = Tt, weight_pct = Ww) } good <- rbind(mk("A", 300), mk("B", 380)) bad <- data.frame(id = "C", temperature_C = c(25, 50), weight_pct = c(100, 99)) df <- rbind(good, bad) expect_warning(out <- tga_stages_batch(df, "id"), "could not be processed") expect_equal(sort(unique(out$id)), c("A", "B")) }) test_that("tga_stages_batch errors when required columns are missing", { df <- data.frame(id = "A", x = 1, y = 2) expect_error(tga_stages_batch(df, "id"), "not found") }) test_that("tga_kinetics_kissinger recovers a known activation energy", { # Simulate Kissinger-consistent (beta, Tp) pairs for a target Ea/A, then # check the fit recovers them (this is testing the regression math, not # asserting anything about real biochar decomposition kinetics). R_gas <- 8.314 Ea_true <- 150e3 # J/mol A_true <- 1e12 # 1/min beta <- c(5, 10, 15, 20, 25) # Solve ln(beta/Tp^2) = ln(A*R/Ea) - Ea/(R*Tp) for Tp numerically per beta Tp <- vapply(beta, function(b) { f <- function(Tp) log(b / Tp^2) - (log(A_true * R_gas / Ea_true) - Ea_true / (R_gas * Tp)) stats::uniroot(f, c(500, 900))$root }, numeric(1)) fit <- tga_kinetics_kissinger(beta, Tp - 273.15) expect_equal(fit$Ea_kJmol, Ea_true / 1000, tolerance = 0.5) expect_true(fit$R2 > 0.999) expect_false(is.null(fit$model)) }) test_that("tga_kinetics_kissinger errors with fewer than 3 valid points", { expect_error(tga_kinetics_kissinger(c(5, 10), c(320, 335)), "at least 3") }) test_that("fit_confint works directly on a Kissinger fit", { beta <- c(5, 10, 15, 20) Tp_C <- c(320, 335, 344, 351) fit <- tga_kinetics_kissinger(beta, Tp_C) ci <- fit_confint(fit) expect_true(all(c("parameter", "estimate", "lower", "upper") %in% names(ci))) expect_equal(nrow(ci), 2) }) test_that("plot_tga runs without error and accepts a precomputed dtg", { T <- seq(25, 800, by = 10) W <- 100 - 40 / (1 + exp(-(T - 350) / 15)) tga <- data.frame(temperature_C = T, weight_pct = W) pdf(NULL) on.exit(dev.off()) expect_silent(plot_tga(tga)) dtg <- tga_dtg(tga) expect_silent(plot_tga(tga, dtg = dtg)) })