From 16a593e5763058e876a56a3861fcc9f9349d098e Mon Sep 17 00:00:00 2001 From: Yining97 Date: Sat, 29 Aug 2026 21:56:58 -0400 Subject: [PATCH 1/2] Store the LD-sketch sample axis once and drop it on emptied sketches genotypeDelayedArray copied handle@sampleIds into GhSeed@dn[[2]], read by nothing but the dimnames() method. The sample axis is an anonymous projection dimension (LD is a variant x variant statistic), so the copy is dead weight in every serialized sketch. Store only the variant axis; derive the sample axis from getSampleIds(handle) in dimnames(). Also empty sampleIds and nSamples in .emptyGenotypeHandle (reached only via .emptySketch), so a skipped-region sketch is a consistent 0 x 0 instead of 0 x 10000. --- R/GenotypeHandle.R | 14 +++++++++-- R/genotypeIo.R | 13 +++++++---- tests/testthat/test_genotypeHandle.R | 35 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/R/GenotypeHandle.R b/R/GenotypeHandle.R index 5d6369bc..22b7147d 100644 --- a/R/GenotypeHandle.R +++ b/R/GenotypeHandle.R @@ -812,7 +812,14 @@ setClass( setMethod("dim", "GhSeed", function(x) x@dm) #' @rdname GhSeed-class -setMethod("dimnames", "GhSeed", function(x) x@dn) +setMethod("dimnames", "GhSeed", function(x) { + # The sample axis is derived from the handle, not stored a second time. + # handle@sampleIds is the single copy; storing dn[[2]] as well would + # duplicate ~10k anonymous projection-dimension names in every serialised + # sketch (see genotypeDelayedArray()). getSampleIds() is character(0) on an + # emptied handle, so this stays consistent with dm = c(nVar, 0). + list(x@dn[[1L]], as.character(getSampleIds(x@handle))) +}) # The handle a seed reads through. Internal accessor: GhSeed is not exported, # so this adds no public surface, and it keeps the `@` next to the class. @@ -866,9 +873,12 @@ genotypeDelayedArray <- function(handle) { "GhSeed", handle = handle, dm = c(nrow(si), as.integer(getNSamples(handle))), + # Only the variant axis is stored; the sample axis is derived from the + # handle in the dimnames() method above, so the ~10k sample ids are not + # duplicated between dn[[2]] and handle@sampleIds. dn = list( normalizeVariantId(as.character(si$SNP)), - as.character(getSampleIds(handle)) + NULL ) ) DelayedArray::DelayedArray(seed) diff --git a/R/genotypeIo.R b/R/genotypeIo.R index e4b1837e..423db2ef 100644 --- a/R/genotypeIo.R +++ b/R/genotypeIo.R @@ -102,11 +102,14 @@ setMethod( if (is.null(handle)) { return(NULL) } - si <- getSnpInfo(handle) - if (nrow(si) == 0L) { - return(handle) - } - handle@snpInfo <- slice(si, integer(0)) + # Empty both axes. An emptied sketch references no LD, so its sample axis is + # dead weight (~10k anonymous names, the bulk of a skipped-region file). + # nSamples must go to 0 alongside sampleIds, or dm (variants x nSamples) + # would disagree with the now-empty derived sample dimnames. Reached only via + # .emptySketch (the empty / PIP-skip path), never on a surviving region. + handle@snpInfo <- slice(getSnpInfo(handle), integer(0)) + handle@sampleIds <- character(0) + handle@nSamples <- 0L handle } diff --git a/tests/testthat/test_genotypeHandle.R b/tests/testthat/test_genotypeHandle.R index b81a0359..899247fe 100644 --- a/tests/testthat/test_genotypeHandle.R +++ b/tests/testthat/test_genotypeHandle.R @@ -814,6 +814,41 @@ test_that("the seed answers an empty extraction on either axis", { ) }) +test_that("genotypeDelayedArray stores the sample axis once, derives it on read", { + # The sample ids are an anonymous projection dimension and would otherwise be + # duplicated between GhSeed@dn[[2]] and handle@sampleIds. The seed stores only + # the variant axis; dimnames() derives the sample axis from the handle. + data(qtlDatasetExample, envir = environment()) + gh <- getGenotypeHandle(qtlDatasetExample) + seed <- genotypeDelayedArray(gh)@seed + expect_null(seed@dn[[2L]]) # not stored + expect_identical(dimnames(seed)[[2L]], as.character(getSampleIds(gh))) # derived + expect_identical(dimnames(seed)[[1L]], seed@dn[[1L]]) # variant axis stored + expect_equal(dim(seed)[[2L]], length(dimnames(seed)[[2L]])) # dm consistent +}) + +test_that(".emptyGenotypeHandle empties the sample axis as well as the variants", { + data(qtlDatasetExample, envir = environment()) + gh <- getGenotypeHandle(qtlDatasetExample) + e <- pecotmr:::.emptyGenotypeHandle(gh) + expect_equal(nrow(getSnpInfo(e)), 0L) + expect_equal(length(getSampleIds(e)), 0L) + expect_equal(getNSamples(e), 0L) + # the rebuilt seed is a consistent 0 x 0 (no 0 x nSamples mismatch) + seed <- genotypeDelayedArray(e)@seed + expect_equal(dim(seed), c(0L, 0L)) + expect_equal(lengths(dimnames(seed)), c(0L, 0L)) +}) + +test_that(".emptySketch drops the sample axis on an RSE sketch", { + data(qtlDatasetExample, envir = environment()) + gh <- getGenotypeHandle(qtlDatasetExample) + empty <- pecotmr:::.emptySketch(pecotmr:::.genotypeExperiment(gh)) + h <- pecotmr:::.ldSketchHandle(empty) + expect_equal(length(getSampleIds(h)), 0L) + expect_equal(nrow(getSnpInfo(h)), 0L) +}) + test_that("nothing is read until a block is actually touched", { # The point of the seed: a panel can be described without being read. data(qtlDatasetExample, envir = environment()) From ff6e74fcf09e1173f68716101f159c313e6aa2b7 Mon Sep 17 00:00:00 2001 From: Yining97 Date: Sat, 29 Aug 2026 23:03:52 -0400 Subject: [PATCH 2/2] Update the empty-sketch test for the dropped sample axis The dedup change makes .emptyGenotypeHandle empty the sample axis too, so an emptied sketch is 0 x 0 rather than 0 x nSamples. The test that pinned emptying as keeping the sample columns no longer holds; assert the new contract instead (both axes empty, panel identity preserved). --- tests/testthat/test_sumstatsQc.R | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/testthat/test_sumstatsQc.R b/tests/testthat/test_sumstatsQc.R index 19209009..8ec77b36 100644 --- a/tests/testthat/test_sumstatsQc.R +++ b/tests/testthat/test_sumstatsQc.R @@ -7907,11 +7907,19 @@ test_that("both trimmers empty the sketch for zero-variant entries", { } }) -test_that("emptying keeps every other property of the panel", { +test_that("emptying drops both axes but keeps the panel's identity", { h <- .sk_panel() e <- .subsetSketchToRange(h, list()) - expect_equal(ncol(e), ncol(h)) - expect_equal(colnames(e), colnames(h)) + # The sample axis is shed along with the variants: an emptied sketch + # references no LD, so its anonymous sample names are dead weight (the bulk + # of a skipped-region file). What is kept is the panel's identity -- where + # it came from -- not its dimensions. + expect_equal(nrow(e), 0L) + expect_equal(ncol(e), 0L) + hh <- .ldSketchHandle(h) + eh <- .ldSketchHandle(e) + expect_equal(getFormat(eh), getFormat(hh)) + expect_equal(getPath(eh), getPath(hh)) }) test_that(".emptySketch drops the seed's variants, not just the rows", {