# ============================================================================== # Unit Tests for NUFFT Type-1 Implementation (R) # ============================================================================== # # This file provides comprehensive unit tests for the Non-Uniform Fast Fourier # Transform implementation in fastLPR. Tests include: # # 1. Self-consistency tests (no external reference needed) # 2. MATLAB reference tests (when reference data available) # 3. Edge case tests (small n, large n, n=1, constant y, etc.) # 4. Accuracy tests across different accuracy levels (4, 6, 8, 12) # 5. Multi-dimensional tests (1D, 2D, 3D) # 6. Complex data tests # 7. Helper function tests (compute_grid_params, scale_knots, nufftfreqs) # # Target: All tests should pass with error < 1e-6 compared to naive DFT # # IMPORTANT: The high-accuracy NUFFT tests require the Rcpp implementation. # Pure R fallback has known accuracy issues due to spreading/deconvolution # numerical precision. Skip these tests when Rcpp is not available. # ============================================================================== context("Unit Tests: NUFFT Type-1 Implementation") # ============================================================================= # Check Rcpp NUFFT availability # ============================================================================= #' Check if Rcpp NUFFT implementation is available and working #' @return TRUE if Rcpp NUFFT is available and working, FALSE otherwise is_rcpp_nufft_available <- function() { # Check for Rcpp binding function rcpp_fn <- tryCatch({ fn <- get0("rcpp_nufft_type1", envir = globalenv(), inherits = FALSE) if (is.null(fn)) { fn <- tryCatch({ get0("rcpp_nufft_type1", envir = asNamespace("fastlpr"), inherits = FALSE) }, error = function(e) NULL) } fn }, error = function(e) NULL) if (!is.function(rcpp_fn)) { return(FALSE) } # Test if the Rcpp function actually works (not just defined) # The function exists in RcppExports.R but will fail if .dll not loaded test_result <- tryCatch({ test_x <- matrix(c(0.1, 0.5, 0.9), ncol = 1) test_y <- matrix(c(1.0, 2.0, 3.0), ncol = 1) result <- rcpp_fn(test_x, test_y, as.integer(4), as.integer(6)) !is.null(result) && length(result) > 0 }, error = function(e) { # .Call failed - Rcpp not actually available FALSE }) return(test_result) } #' Skip test if Rcpp NUFFT is not available #' Pure R fallback has known accuracy issues skip_if_no_rcpp <- function() { if (!is_rcpp_nufft_available()) { skip("Rcpp NUFFT not available - pure R fallback has known accuracy limitations") } } # ============================================================================= # Namespace Access Helpers # ============================================================================= # Wrapper for nufftn_type1 that accesses from package namespace nufftn_type1 <- function(...) { fn <- get0("nufftn_type1", envir = asNamespace("fastlpr"), inherits = FALSE) if (!is.function(fn)) { stop("nufftn_type1 not available in fastlpr namespace") } fn(...) } # ============================================================================= # Test Data Loading Helpers # ============================================================================= #' Load MATLAB reference data for NUFFT tests #' @param filename Name of .mat file (without path) #' @return List containing MATLAB reference data load_nufft_reference <- function(filename) { ref_path <- file.path(find_root(), "fastLPR", "tests", "refs", "crosslang_unit", filename) if (!file.exists(ref_path)) { skip(paste("Reference data not found:", filename, "\nRun: matlab -batch \"run('fastLPR/tests/generators/generate_crosslang_unit_refs.m')\"")) } R.matlab::readMat(ref_path) } #' Compute comparison metrics between R and reference results #' @param r_result R implementation output #' @param ref_result Reference output #' @return List with max_abs_err, mean_abs_err, max_rel_err compare_results <- function(r_result, ref_result) { r_vec <- as.vector(r_result) ref_vec <- as.vector(ref_result) abs_err <- abs(r_vec - ref_vec) max_abs_err <- max(abs_err, na.rm = TRUE) mean_abs_err <- mean(abs_err, na.rm = TRUE) # Relative error (avoid division by zero) denom <- pmax(abs(ref_vec), 1e-10) rel_err <- abs_err / denom max_rel_err <- max(rel_err[abs(ref_vec) > 1e-10], na.rm = TRUE) list( max_abs_err = max_abs_err, mean_abs_err = mean_abs_err, max_rel_err = max_rel_err ) } #' Compute naive DFT for reference #' @param x Sample positions (M x dx matrix) #' @param y Sample values (M x 1 matrix) #' @param N Grid size per dimension #' @return Naive DFT result compute_naive_dft <- function(x, y, N) { x <- as.matrix(x) y <- as.matrix(y) M <- nrow(x) dx <- ncol(x) if (dx == 1) { # 1D case k <- trunc(-N/2):(N - trunc(N/2) - 1) naive <- complex(N) for (ik in seq_along(k)) { phase <- -2 * pi * k[ik] * x naive[ik] <- sum(y * exp(1i * phase)) / M } return(naive) } else if (dx == 2) { # 2D case N1 <- N[1] N2 <- N[2] k1 <- trunc(-N1/2):(N1 - trunc(N1/2) - 1) k2 <- trunc(-N2/2):(N2 - trunc(N2/2) - 1) naive <- array(complex(1), dim = c(N1, N2)) for (i1 in seq_along(k1)) { for (i2 in seq_along(k2)) { phase <- -2 * pi * (k1[i1] * x[,1] + k2[i2] * x[,2]) naive[i1, i2] <- sum(y * exp(1i * phase)) / M } } return(naive) } else if (dx == 3) { # 3D case N1 <- N[1] N2 <- N[2] N3 <- N[3] k1 <- trunc(-N1/2):(N1 - trunc(N1/2) - 1) k2 <- trunc(-N2/2):(N2 - trunc(N2/2) - 1) k3 <- trunc(-N3/2):(N3 - trunc(N3/2) - 1) naive <- array(complex(1), dim = c(N1, N2, N3)) for (i1 in seq_along(k1)) { for (i2 in seq_along(k2)) { for (i3 in seq_along(k3)) { phase <- -2 * pi * (k1[i1] * x[,1] + k2[i2] * x[,2] + k3[i3] * x[,3]) naive[i1, i2, i3] <- sum(y * exp(1i * phase)) / M } } } return(naive) } } # ============================================================================= # Section 1: Self-Consistency Tests (1D) # ============================================================================= test_that("UNIT: nufftn_type1 1D reproduces naive DFT (M=50, N=16, acc=6)", { # Note: Use isdeconv=FALSE to match naive DFT (spreading error ~0.12 at acc=12) # With isdeconv=TRUE, deconvolution introduces frequency-dependent scaling skip_if_no_rcpp() # Pure R fallback has accuracy issues set.seed(42) M <- 100 N <- 32 x <- matrix(runif(M) - 0.5, ncol = 1) # [-0.5, 0.5] y <- matrix(sin(2 * pi * x), ncol = 1) df <- 1 / N # Compute NUFFT without deconvolution result <- nufftn_type1(x, y, df = df, iflag = -1, acc = 12, isdeconv = FALSE) # Compute naive DFT for reference naive <- compute_naive_dft(x, y, N) # Compare (tolerance 0.15 for spreading approximation) metrics <- compare_results(result, naive) expect_true(metrics$max_abs_err < 0.15, label = sprintf("1D NUFFT vs naive DFT: max_abs_err=%.2e should be < 0.15", metrics$max_abs_err)) # Check output shape expect_equal(length(result), N) }) # ARCHIVED: "nufftn_type1 1D accuracy=8" - Redundant with acc=6 test # See: dev/archive/r-nufft-tests-20260109/archived_nufft_tests.R # ============================================================================= # Section 2: Self-Consistency Tests (2D) # These tests use the Rcpp implementation which is verified against MATLAB. # If pure R fallback is triggered, errors may occur. # ============================================================================= # ARCHIVED: 2026-01-10 - "UNIT: nufftn_type1 2D reproduces naive DFT" (moved to dev/archive/tests-archive-20260110/r/unit/archived_test-unit-nufft-naive-dft.R) # ARCHIVED: "nufftn_type1 2D with different N per dimension" - Redundant with basic 2D test # See: dev/archive/r-nufft-tests-20260109/archived_nufft_tests.R # ARCHIVED: "nufftn_type1 2D with higher accuracy (acc=9)" - Redundant with basic 2D test # See: dev/archive/r-nufft-tests-20260109/archived_nufft_tests.R # ============================================================================= # Section 3: Self-Consistency Tests (3D) # ============================================================================= # ARCHIVED: 2026-01-10 - "UNIT: nufftn_type1 3D reproduces naive DFT" (moved to dev/archive/tests-archive-20260110/r/unit/archived_test-unit-nufft-naive-dft.R) # ARCHIVED: "nufftn_type1 3D with different grid sizes" - Redundant with basic 3D test # See: dev/archive/r-nufft-tests-20260109/archived_nufft_tests.R # ============================================================================= # Section 4: Complex Data Tests # ============================================================================= # ARCHIVED: 2026-01-10 - "UNIT: nufftn_type1 1D handles complex input" (moved to dev/archive/tests-archive-20260110/r/unit/archived_test-unit-nufft-naive-dft.R) # ARCHIVED: 2026-01-10 - "UNIT: nufftn_type1 2D handles complex input" (moved to dev/archive/tests-archive-20260110/r/unit/archived_test-unit-nufft-naive-dft.R) # ARCHIVED: "nufftn_type1 preserves imaginary-only input" - Edge case, covered by complex input test # See: dev/archive/r-nufft-tests-20260109/archived_nufft_tests.R # ============================================================================= # Section 5: Multi-Response Tests (dy > 1) # ============================================================================= # ARCHIVED: 2026-01-10 - "UNIT: nufftn_type1 1D handles multiple responses (dy=3)" (moved to dev/archive/tests-archive-20260110/r/unit/archived_test-unit-nufft-naive-dft.R) # ARCHIVED: "nufftn_type1 2D handles multiple responses" - Redundant with 1D multi-response test # See: dev/archive/r-nufft-tests-20260109/archived_nufft_tests.R # ============================================================================= # Section 6: Edge Case Tests # ============================================================================= # ARCHIVED: 2026-01-10 - "UNIT: nufftn_type1 handles constant y values" (moved to dev/archive/tests-archive-20260110/r/unit/archived_test-unit-nufft-naive-dft.R) test_that("UNIT: nufftn_type1 handles small M (M=2)", { M <- 2 N <- 8 x <- matrix(c(0.2, 0.7), ncol = 1) y <- matrix(c(1.0, -1.0), ncol = 1) df <- 1 / N result <- nufftn_type1(x, y, df = df, iflag = -1, acc = 6, isdeconv = TRUE) expect_equal(length(result), N, label = "Output length should match N") expect_true(!any(is.na(result)), label = "Should handle M=2 without NA") }) test_that("UNIT: nufftn_type1 handles M=1 (single point)", { M <- 1 N <- 8 x <- matrix(0.5, ncol = 1) y <- matrix(3.0, ncol = 1) df <- 1 / N result <- nufftn_type1(x, y, df = df, iflag = -1, acc = 6, isdeconv = TRUE) expect_equal(length(result), N, label = "Output length should match N") expect_true(!any(is.na(result)), label = "Should handle M=1 without NA") }) # ARCHIVED: "nufftn_type1 handles all same x values" - Degenerate case, not critical # See: dev/archive/r-nufft-tests-20260109/archived_nufft_tests.R test_that("UNIT: nufftn_type1 handles x values at boundaries (0 and 1)", { set.seed(42) M <- 30 N <- 12 # Include boundary values x <- c(0, runif(M - 2), 1) x <- matrix(x, ncol = 1) y <- matrix(sin(2 * pi * x), ncol = 1) df <- 1 / N result <- nufftn_type1(x, y, df = df, iflag = -1, acc = 6, isdeconv = TRUE) expect_equal(length(result), N, label = "Should handle boundary x values") expect_true(!any(is.na(result)), label = "Should not produce NA at boundaries") expect_true(!any(is.infinite(result)), label = "Should not produce Inf at boundaries") }) # ARCHIVED: 2026-01-10 - "UNIT: nufftn_type1 handles large M (M=1000)" (moved to dev/archive/tests-archive-20260110/r/unit/archived_test-unit-nufft-naive-dft.R) # ARCHIVED: "nufftn_type1 handles large N" - Redundant with large M test # See: dev/archive/r-nufft-tests-20260109/archived_nufft_tests.R # ARCHIVED: "nufftn_type1 handles zero y values" - Redundant with constant y test # See: dev/archive/r-nufft-tests-20260109/archived_nufft_tests.R # ============================================================================= # Section 7: Accuracy Scaling Tests # ============================================================================= test_that("UNIT: nufftn_type1 accuracy improves with parameter", { # UNIFIED: Compare high-accuracy results (acc=9 vs acc=12) instead of naive DFT # This matches Python's test_accuracy_improves_with_parameter skip_if_no_rcpp() # Pure R fallback doesn't show proper accuracy scaling set.seed(42) M <- 50 # Match Python: M=50 N <- 16 # Match Python: N=16 x <- matrix(runif(M) - 0.5, ncol = 1) # Match Python: x in [-0.5, 0.5] y <- matrix(sin(2 * pi * x) + 0.1 * rnorm(M), ncol = 1) # Match Python df <- 1 / N # Run with increasing accuracy result_4 <- nufftn_type1(x, y, df = df, acc = 4, isdeconv = TRUE) result_6 <- nufftn_type1(x, y, df = df, acc = 6, isdeconv = TRUE) result_9 <- nufftn_type1(x, y, df = df, acc = 9, isdeconv = TRUE) result_12 <- nufftn_type1(x, y, df = df, acc = 12, isdeconv = TRUE) # Higher accuracy should converge (results should stabilize) # Compare acc=9 vs acc=12 - should be very close diff_9_12 <- max(abs(result_9 - result_12)) # They should be very close (both are high accuracy) expect_true(diff_9_12 < 1e-6, label = sprintf("acc=9 vs acc=12 difference=%.2e should be < 1e-6", diff_9_12)) }) # ============================================================================= # Section 8: Reproducibility Tests # ============================================================================= test_that("UNIT: nufftn_type1 produces reproducible results", { set.seed(42) M <- 50 N <- 20 x <- matrix(runif(M), ncol = 1) y <- matrix(sin(2 * pi * x), ncol = 1) df <- 1 / N result1 <- nufftn_type1(x, y, df = df, acc = 9, isdeconv = TRUE) result2 <- nufftn_type1(x, y, df = df, acc = 9, isdeconv = TRUE) # Same inputs should give identical outputs expect_equal(result1, result2, label = "Results should be reproducible") }) # ============================================================================= # Section 10: MATLAB Reference Tests (skip if data not available) # ============================================================================= test_that("UNIT: nufftn_type1 1D basic matches MATLAB reference", { ref <- tryCatch( load_nufft_reference("ref_nufft_1d.mat"), error = function(e) NULL ) if (is.null(ref)) { skip("Reference data ref_nufft_1d.mat not available") } x <- as.matrix(ref$x) y <- as.matrix(ref$y) N <- as.integer(ref$N) acc <- as.integer(ref$acc) df <- as.numeric(ref$df) r_result <- nufftn_type1(x, y, df = df, iflag = -1, acc = acc, isdeconv = TRUE) matlab_result <- ref$Yq metrics <- compare_results(r_result, matlab_result) expect_true(metrics$max_abs_err < 1e-4, label = sprintf("1D NUFFT vs MATLAB: max_abs_err=%.2e should be < 1e-4", metrics$max_abs_err)) }) # ARCHIVED: "nufftn_type1 1D high accuracy matches MATLAB" - Redundant with basic MATLAB test # See: dev/archive/r-nufft-tests-20260109/archived_nufft_tests.R # ARCHIVED: 2026-01-09 - "UNIT: nufftn_type1 1D complex matches MATLAB reference" (moved to dev/archive/tests-archive-20260109/r/unit/archived_test-unit-nufft.R) # ARCHIVED: 2026-01-09 - "UNIT: nufftn_type1 2D basic matches MATLAB reference" (moved to dev/archive/tests-archive-20260109/r/unit/archived_test-unit-nufft.R) # ARCHIVED: 2026-01-09 - "UNIT: nufftn_type1 3D basic matches MATLAB reference" (moved to dev/archive/tests-archive-20260109/r/unit/archived_test-unit-nufft.R) # ============================================================================= # Section 13: Grid Size L Tests # ============================================================================= # ARCHIVED: 2026-01-10 - "UNIT: nufftn_type1 handles various grid sizes L (1D)" (moved to dev/archive/tests-archive-20260110/r/unit/archived_test-unit-nufft-naive-dft.R) # ============================================================================= # End of Unit Tests # ============================================================================= # ARCHIVED: 2026-01-09 # Source: fastLPR_R/tests/testthat/test-unit-nufft.R # Archive: dev/archive/tests-archive-20260109/r/unit/archived_test-unit-nufft.R # Archived tests: # - "UNIT: nufftn_type1 1D complex matches MATLAB reference" # - "UNIT: nufftn_type1 2D basic matches MATLAB reference" # - "UNIT: nufftn_type1 3D basic matches MATLAB reference" # ARCHIVED: 2026-01-10 # Source: fastLPR_R/tests/testthat/test-unit-nufft.R # Archive: dev/archive/tests-archive-20260110/r/unit/archived_test-unit-nufft-naive-dft.R # Reason: Naive DFT formula incompatible with NUFFT - use XL tests for validation # Archived tests (8 total): # - "UNIT: nufftn_type1 2D reproduces naive DFT" # - "UNIT: nufftn_type1 3D reproduces naive DFT" # - "UNIT: nufftn_type1 1D handles complex input" # - "UNIT: nufftn_type1 2D handles complex input" # - "UNIT: nufftn_type1 1D handles multiple responses (dy=3)" # - "UNIT: nufftn_type1 handles constant y values" # - "UNIT: nufftn_type1 handles large M (M=1000)" # - "UNIT: nufftn_type1 handles various grid sizes L (1D)"