From 2c9bebc2f426985d5884c44bd159c81130487403 Mon Sep 17 00:00:00 2001 From: Daniel Nachun Date: Tue, 7 Jul 2026 14:50:08 -0700 Subject: [PATCH 1/2] add loader functions and update vignettes --- .github/recipe/recipe.yaml | 2 + DESCRIPTION | 10 +- NAMESPACE | 5 +- R/GenotypeHandle.R | 28 + R/ctwasPipeline.R | 2 +- R/exampleData.R | 53 -- R/genotypeIo.R | 12 +- R/manifestLoaders.R | 966 ++++++++++++++++++++ R/twasWeightsPipeline.R | 28 + data-raw/build_examples.R | 18 +- data/gwas_sumstats_s4_example.rda | Bin 7044 -> 7076 bytes data/multi_study_qtl_dataset_example.rda | Bin 6888 -> 6956 bytes data/qtl_dataset_example.rda | Bin 5316 -> 5348 bytes data/qtl_sumstats_example.rda | Bin 8016 -> 8036 bytes data/qtl_sumstats_multicontext_example.rda | Bin 16176 -> 16204 bytes man/fixupExampleGenotypePaths.Rd | 26 - man/loadGwasSumStatsFromManifest.Rd | 52 ++ man/loadMultiStudyQtlDatasetFromManifest.Rd | 49 + man/loadQtlDatasetFromManifest.Rd | 52 ++ man/loadQtlSumStatsFromManifest.Rd | 38 + pixi.toml | 1 + tests/testthat/test_ctwasPipeline.R | 8 +- tests/testthat/test_manifestLoaders.R | 807 ++++++++++++++++ tests/testthat/test_twasWeightsPipeline.R | 16 +- vignettes/coloc-pipeline.Rmd | 69 +- vignettes/colocboost-pipeline.Rmd | 35 +- vignettes/constructing-qtl-datasets.Rmd | 229 +++++ vignettes/constructing-sumstats.Rmd | 294 ++++++ vignettes/fine-mapping.Rmd | 44 +- vignettes/rss-qc.Rmd | 213 ++--- vignettes/twas-weights.Rmd | 65 +- vignettes/twas-zscore.Rmd | 30 +- 32 files changed, 2725 insertions(+), 427 deletions(-) create mode 100644 R/manifestLoaders.R delete mode 100644 man/fixupExampleGenotypePaths.Rd create mode 100644 man/loadGwasSumStatsFromManifest.Rd create mode 100644 man/loadMultiStudyQtlDatasetFromManifest.Rd create mode 100644 man/loadQtlDatasetFromManifest.Rd create mode 100644 man/loadQtlSumStatsFromManifest.Rd create mode 100644 tests/testthat/test_manifestLoaders.R create mode 100644 vignettes/constructing-qtl-datasets.Rmd create mode 100644 vignettes/constructing-sumstats.Rmd diff --git a/.github/recipe/recipe.yaml b/.github/recipe/recipe.yaml index dd574a80..21666705 100644 --- a/.github/recipe/recipe.yaml +++ b/.github/recipe/recipe.yaml @@ -84,6 +84,7 @@ requirements: - r-vctrs - r-vroom - r-xgboost + - r-yaml run: - bioconductor-biocparallel - bioconductor-genomicranges @@ -145,6 +146,7 @@ requirements: - r-vctrs - r-vroom - r-xgboost + - r-yaml tests: - script: diff --git a/DESCRIPTION b/DESCRIPTION index 55e8b870..4fda7654 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -39,7 +39,8 @@ Imports: tictoc, tidyr, vctrs, - vroom + vroom, + yaml Suggests: aSPU, arrow, @@ -124,19 +125,20 @@ Collate: 'jointEngine.R' 'jointSpecification.R' 'ld.R' + 'sumstatsQc.R' + 'variantId.R' + 'qtlSumStats.R' + 'manifestLoaders.R' 'mashPipeline.R' 'mashWrapper.R' 'pvalCombine.R' 'qtlEnrichmentPipeline.R' - 'qtlSumStats.R' 'regularizedRegressionWrappers.R' 'relatednessQc.R' 'sldscPostprocessingPipeline.R' 'sldscWrapper.R' - 'sumstatsQc.R' 'twasWeights.R' 'twasWeightsPipeline.R' - 'variantId.R' 'vcfWriter.R' Depends: R (>= 3.5) diff --git a/NAMESPACE b/NAMESPACE index 995c5163..93537aa6 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -82,7 +82,6 @@ export(fitMashContrast) export(fitMvsusie) export(fitMvsusieRss) export(fitSusieInfThenSusieRss) -export(fixupExampleGenotypePaths) export(formatFinemappingOutput) export(fsusieGetCs) export(fsusieWeights) @@ -185,11 +184,15 @@ export(ldMismatchQc) export(ldPruneByCorrelation) export(learnTwasWeights) export(loadGenotypeRegion) +export(loadGwasSumStatsFromManifest) export(loadLdMatrix) export(loadLdSketch) +export(loadMultiStudyQtlDatasetFromManifest) export(loadMultitaskRegionalData) export(loadMultitraitRSumstat) export(loadMultitraitTensorqtlSumstat) +export(loadQtlDatasetFromManifest) +export(loadQtlSumStatsFromManifest) export(loadRegionalAssociationData) export(loadRegionalFunctionalData) export(loadRegionalMultivariateData) diff --git a/R/GenotypeHandle.R b/R/GenotypeHandle.R index 7e882e16..8ec96674 100644 --- a/R/GenotypeHandle.R +++ b/R/GenotypeHandle.R @@ -424,6 +424,34 @@ setMethod("getFormat", "GenotypeHandle", function(x) x@format) #' @export setMethod("getPath", "GenotypeHandle", function(x) x@path) +# Resolve a portable bundled-resource reference of the form +# "pecotmr://extdata/" to a concrete filesystem path via +# `system.file()`; any ordinary path is returned unchanged. This lets the +# packaged example objects (which cannot bake an install-specific absolute +# path into their serialized GenotypeHandle) carry a portable reference that +# resolves at extraction time, on whatever machine the package is installed. +.resolveGenotypeResourcePath <- function(path) { + if (length(path) != 1L || is.na(path)) return(path) + m <- regmatches(path, regexec("^pecotmr://extdata/(.+)$", path))[[1L]] + if (length(m) != 2L) return(path) + stem <- m[[2L]] + bed <- system.file("extdata", paste0(stem, ".bed"), package = "pecotmr") + if (nzchar(bed)) return(sub("\\.bed$", "", bed)) + resolved <- system.file("extdata", stem, package = "pecotmr") + if (!nzchar(resolved)) + stop("cannot resolve bundled genotype resource '", stem, + "' under inst/extdata/ (package 'pecotmr').") + resolved +} + +# The concrete filesystem path/stem to open for a handle's data. Resolves a +# bundled-resource reference; ordinary paths pass through. Used by the block +# extractors and LD-panel keying, so `getPath()` keeps returning the stored +# (portable) value for display/provenance while file access resolves it. +.genotypeReadPath <- function(handle) { + .resolveGenotypeResourcePath(getPath(handle)) +} + #' @rdname getSampleIds #' @export setMethod("getSampleIds", "GenotypeHandle", function(x) x@sampleIds) diff --git a/R/ctwasPipeline.R b/R/ctwasPipeline.R index 2dab23ba..66f28270 100644 --- a/R/ctwasPipeline.R +++ b/R/ctwasPipeline.R @@ -1242,7 +1242,7 @@ mergeCtwasBoundaryRegions <- function(finemapResult, # @noRd .ctwasLdPanelKey <- function(handle) { fmt <- getFormat(handle) - stem <- getPath(handle) + stem <- .genotypeReadPath(handle) candidates <- switch(fmt, "plink2" = c(paste0(stem, ".pgen")), "plink1" = c(paste0(stem, ".bed")), diff --git a/R/exampleData.R b/R/exampleData.R index 6e38dc1e..bd841c9f 100644 --- a/R/exampleData.R +++ b/R/exampleData.R @@ -288,59 +288,6 @@ NULL NULL -#' @title Resolve bundled-example GenotypeHandle paths -#' -#' @description The bundled S4 example objects (\code{qtl_dataset_example}, -#' \code{qtl_sumstats_example}, \code{qtl_sumstats_multicontext_example}, -#' \code{gwas_sumstats_s4_example}, \code{multi_study_qtl_dataset_example}) -#' store a relative-style path to the bundled -#' \code{inst/extdata/toy_canonical} PLINK1 reference. -#' Call this helper once at the top of a vignette to re-point each -#' contained \code{GenotypeHandle} at the installed path resolved via -#' \code{system.file()}. -#' -#' @param x A bundled \code{QtlDataset}, \code{MultiStudyQtlDataset}, -#' \code{QtlSumStats}, or \code{GwasSumStats} example object. -#' @return The same object with \code{GenotypeHandle@@path} rewritten -#' to the resolved install path. -#' @export -fixupExampleGenotypePaths <- function(x) { - resolve <- function(handle) { - bn <- basename(handle@path) - # PLINK1 / PLINK2 are stems (no extension); resolve via the .bed - # (or .pgen) sidecar and strip the extension back off. - bedPath <- system.file("extdata", paste0(bn, ".bed"), - package = "pecotmr") - if (nzchar(bedPath)) { - handle@path <- sub("\\.bed$", "", bedPath) - return(handle) - } - new <- system.file("extdata", bn, package = "pecotmr") - if (!nzchar(new)) - stop("fixupExampleGenotypePaths: cannot resolve '", bn, - "' under inst/extdata/") - handle@path <- new - handle - } - if (methods::is(x, "QtlDataset")) { - x@genotypes <- resolve(x@genotypes) - } else if (methods::is(x, "MultiStudyQtlDataset")) { - for (nm in names(x@qtlDatasets)) - x@qtlDatasets[[nm]]@genotypes <- - resolve(x@qtlDatasets[[nm]]@genotypes) - if (!is.null(x@sumStats)) - x@sumStats@ldSketch <- resolve(x@sumStats@ldSketch) - } else if (methods::is(x, "QtlSumStats") || - methods::is(x, "GwasSumStats")) { - x@ldSketch <- resolve(x@ldSketch) - } else { - stop("fixupExampleGenotypePaths: unsupported class '", - class(x)[[1L]], "'.") - } - x -} - - #' @name multi_study_qtl_dataset_example #' #' @title Example MultiStudyQtlDataset (S4) diff --git a/R/genotypeIo.R b/R/genotypeIo.R index 8ca39f63..fdb8995c 100644 --- a/R/genotypeIo.R +++ b/R/genotypeIo.R @@ -323,7 +323,7 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { #' @keywords internal .extractBlockGds <- function(handle, snpIdx) { - .withGds(getPath(handle), allow.fork = TRUE, fn = function(gds) { + .withGds(.genotypeReadPath(handle), allow.fork = TRUE, fn = function(gds) { snpIds <- getSnpInfo(handle)$SNP[snpIdx] # Use snpgdsGetGeno for proper non-contiguous SNP selection geno <- SNPRelate::snpgdsGetGeno(gds, snp.id = snpIds, @@ -345,7 +345,7 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { ) param <- VariantAnnotation::ScanVcfParam(which = gr, fixed = NA, info = NA, geno = "GT") - vcf <- VariantAnnotation::readVcf(getPath(handle), genome = "", param = param) + vcf <- VariantAnnotation::readVcf(.genotypeReadPath(handle), genome = "", param = param) gt <- VariantAnnotation::geno(vcf)$GT # Convert GT strings to ALT dosage (A1 dosage) @@ -365,7 +365,7 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { #' @keywords internal .extractBlockPlink1 <- function(handle, snpIdx) { snpIds <- getSnpInfo(handle)$SNP[snpIdx] - pathStem <- getPath(handle) + pathStem <- .genotypeReadPath(handle) plinkData <- snpStats::read.plink( bed = paste0(pathStem, ".bed"), bim = paste0(pathStem, ".bim"), @@ -386,7 +386,7 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { # become stale), so we re-open from getPath() on the fly if the cached # pointer errors out. Opening is cheap relative to dosage extraction. ptr <- getPgenPtr(handle) - paths <- resolvePlink2Paths(getPath(handle)) + paths <- resolvePlink2Paths(.genotypeReadPath(handle)) # A sharded handle routes through a transient view with pgenPtr = NULL (one # pgen per chromosome), and a deserialized pointer is stale; open a fresh # pgen up front in those cases rather than provoking a caught read error. @@ -461,7 +461,7 @@ computeBlockLdCor <- function(handle, snpIdx, backend = "internal", #' @keywords internal .computeBlockLdGds <- function(handle, snpIdx) { - .withGds(getPath(handle), allow.fork = TRUE, fn = function(gds) { + .withGds(.genotypeReadPath(handle), allow.fork = TRUE, fn = function(gds) { snpIds <- getSnpInfo(handle)$SNP[snpIdx] ldMat <- SNPRelate::snpgdsLDMat( gds, snp.id = snpIds, method = "corr", @@ -1052,7 +1052,7 @@ loadGenotypeRegion <- function(genotype, region = NULL, keepIndel = TRUE, # --- Attach allele frequency from .afreq sidecar (plink2 only) --- if (getFormat(handle) == "plink2") { - afreq <- readAfreq(getPath(handle)) + afreq <- readAfreq(.genotypeReadPath(handle)) if (!is.null(afreq)) { afreqCols <- intersect(c("id", "alt_freq", "obs_ct"), colnames(afreq)) variantInfo <- merge(variantInfo, afreq[, afreqCols, drop = FALSE], diff --git a/R/manifestLoaders.R b/R/manifestLoaders.R new file mode 100644 index 00000000..a6bcca5f --- /dev/null +++ b/R/manifestLoaders.R @@ -0,0 +1,966 @@ +# ============================================================================= +# Manifest loaders +# ----------------------------------------------------------------------------- +# Reader functions that build the individual-level / summary-statistic S4 +# containers from an on-disk (or in-memory) manifest: +# +# loadQtlDatasetFromManifest -> QtlDataset +# loadQtlSumStatsFromManifest -> QtlSumStats +# loadGwasSumStatsFromManifest -> GwasSumStats +# loadMultiStudyQtlDatasetFromManifest -> MultiStudyQtlDataset +# +# These are pure readers: they never run summaryStatsQc() (the loaded +# sumstats objects carry qcInfo = list()) and never filter genotypes (the +# QtlDataset QC knobs are stored as lazy slots, applied at extraction time). +# +# A manifest is either a data.frame or a path to a delimited file. A ".csv" +# extension is read with readr::read_csv; anything else is read with +# readr::read_tsv. Both snake_case and camelCase column names are accepted at +# the parsing boundary and canonicalised to camelCase; snake_case is never +# propagated internally. +# ============================================================================= + +#' @include GenotypeHandle.R QtlDataset.R gwasSumStats.R qtlSumStats.R +#' @include MultiStudyQtlDataset.R variantId.R sumstatsQc.R +NULL + +# ============================================================================= +# Manifest ingestion + column canonicalisation +# ============================================================================= + +# Read a manifest into a base data.frame. A data.frame is passed through; a +# single path is read by extension (.csv -> read_csv, else read_tsv). +.readManifest <- function(manifest) { + if (is.data.frame(manifest)) { + return(as.data.frame(manifest, stringsAsFactors = FALSE, check.names = FALSE)) + } + if (!is.character(manifest) || length(manifest) != 1L) { + stop("`manifest` must be a data.frame or a single file path.") + } + if (!file.exists(manifest)) { + stop("manifest file not found: ", manifest) + } + parsed <- if (grepl("\\.csv$", manifest, ignore.case = TRUE)) { + readr::read_csv(manifest, show_col_types = FALSE, progress = FALSE) + } else { + readr::read_tsv(manifest, show_col_types = FALSE, progress = FALSE) + } + as.data.frame(parsed, stringsAsFactors = FALSE, check.names = FALSE) +} + +# Directory a manifest's relative paths resolve against: the file's own +# directory when the manifest was a path, otherwise NULL (paths used as-is). +.manifestBase <- function(manifest) { + if (is.character(manifest) && length(manifest) == 1L && file.exists(manifest)) { + dirname(normalizePath(manifest)) + } else { + NULL + } +} + +# Resolve a possibly-relative path against a manifest base directory. +.resolveRel <- function(p, base) { + if (is.null(p) || length(p) != 1L || is.na(p) || !nzchar(p)) return(p) + if (grepl("^(/|[A-Za-z]:|~)", p) || is.null(base) || file.exists(p)) return(p) + file.path(base, p) +} + +# Rename accepted aliases to the canonical camelCase name and validate that all +# `required` canonical columns are present. `aliases` is a named list keyed by +# canonical name; each value is the character vector of accepted source names +# (including the canonical name itself). The first alias present wins. +.canonManifestCols <- function(df, aliases, required, label) { + for (canon in names(aliases)) { + if (canon %in% names(df)) next + hit <- intersect(aliases[[canon]], names(df)) + if (length(hit) >= 1L) { + names(df)[match(hit[[1L]], names(df))] <- canon + } + } + missingCols <- setdiff(required, names(df)) + if (length(missingCols) > 0L) { + stop(label, " manifest is missing required column(s): ", + paste(missingCols, collapse = ", ")) + } + df +} + +# Resolve a single collection-level scalar (e.g. genome) that may be supplied +# both as a constant manifest column and as a function argument. The column (if +# present) must be constant; a supplied argument must agree with it. +.reconcileScalar <- function(colValues, arg, name, required = TRUE) { + colVal <- NULL + if (!is.null(colValues)) { + v <- unique(as.character(colValues[!is.na(colValues) & + nzchar(as.character(colValues))])) + if (length(v) > 1L) { + stop("`", name, "` column must be constant across the manifest; got: ", + paste(v, collapse = ", ")) + } + if (length(v) == 1L) colVal <- v + } + if (!is.null(arg) && !is.null(colVal)) { + if (!identical(as.character(arg), colVal)) { + stop("`", name, "` argument ('", arg, "') disagrees with the manifest ", + "column value ('", colVal, "').") + } + return(arg) + } + if (!is.null(arg)) return(arg) + if (!is.null(colVal)) return(colVal) + if (required) { + stop("`", name, "` must be provided as an argument or a manifest column.") + } + NULL +} + +# ============================================================================= +# Genotype-format detection + LD-sketch resolution +# ============================================================================= + +# Build a single-file / prefix GenotypeHandle, detecting the backend from the +# path. Recognised: .vcf[.gz|.bgz]/.bcf/.gds single files, .bed/.pgen single +# files, and bare PLINK1/PLINK2 prefixes (probed by sidecar existence). +.detectGenotypeFormat <- function(path) { + lower <- tolower(path) + if (grepl("\\.vcf(\\.b?gz)?$", lower) || endsWith(lower, ".bcf") || + endsWith(lower, ".gds")) { + return(GenotypeHandle(path = path)) + } + if (endsWith(lower, ".bed")) { + return(GenotypeHandle(plink1Prefix = sub("\\.bed$", "", path, ignore.case = TRUE))) + } + if (endsWith(lower, ".pgen")) { + return(GenotypeHandle(plink2Prefix = sub("\\.pgen$", "", path, ignore.case = TRUE))) + } + if (file.exists(paste0(path, ".bed"))) return(GenotypeHandle(plink1Prefix = path)) + if (file.exists(paste0(path, ".pgen"))) return(GenotypeHandle(plink2Prefix = path)) + stop("Could not determine genotype format for '", path, + "' (expected .bed/.pgen/.vcf[.gz]/.bcf/.gds or a PLINK prefix).") +} + +# Turn an ldSketch specification into a GenotypeHandle. Accepts a prebuilt +# handle, a named chrom -> path vector (genoMeta), or a single string that is +# either a genotype file/prefix or a chrom-sharded genoMeta meta file. +.resolveLdSketch <- function(spec) { + if (is.null(spec)) return(NULL) + if (methods::is(spec, "GenotypeHandle")) return(spec) + if (is.character(spec) && !is.null(names(spec))) { + return(GenotypeHandle(genoMeta = spec)) + } + if (is.character(spec) && length(spec) == 1L) { + lower <- tolower(spec) + looksLikeGenotype <- grepl("\\.vcf(\\.b?gz)?$", lower) || + endsWith(lower, ".bcf") || endsWith(lower, ".gds") || + endsWith(lower, ".bed") || endsWith(lower, ".pgen") || + file.exists(paste0(spec, ".bed")) || file.exists(paste0(spec, ".pgen")) + if (looksLikeGenotype) return(.detectGenotypeFormat(spec)) + # Otherwise treat it as a chrom-sharded genoMeta meta file. + return(GenotypeHandle(genoMeta = spec)) + } + stop("`ldSketch` must be a GenotypeHandle, a genotype path/prefix, or a ", + "genoMeta spec (named chrom->path vector or meta-file path).") +} + +# Resolve the LD sketch from the (argument, ldSketchPath column) pair. +.resolveLdSketchInput <- function(df, ldSketch, base) { + if (methods::is(ldSketch, "GenotypeHandle")) return(ldSketch) + colSpec <- NULL + if (!is.null(df$ldSketchPath)) { + v <- unique(as.character(df$ldSketchPath[!is.na(df$ldSketchPath) & + nzchar(as.character(df$ldSketchPath))])) + if (length(v) > 1L) stop("`ldSketchPath` column must be constant across the manifest.") + if (length(v) == 1L) colSpec <- .resolveRel(v, base) + } + spec <- ldSketch + if (!is.null(spec) && !is.null(colSpec) && + !identical(as.character(spec), as.character(colSpec))) { + stop("`ldSketch` argument disagrees with the manifest `ldSketchPath` column.") + } + spec <- spec %||% colSpec + if (is.null(spec)) { + stop("`ldSketch` must be provided as an argument or an `ldSketchPath` column.") + } + .resolveLdSketch(spec) +} + +# ============================================================================= +# LD-sketch containment check (validation only; no filtering) +# ============================================================================= + +# Canonical chromosomes present in an LD sketch: for a sharded handle these are +# the chromPaths names; for a single-file handle the unique snpInfo chromosomes. +.ldSketchChroms <- function(ldSketch) { + if (length(ldSketch@chromPaths) > 0L) { + canonChrom(names(ldSketch@chromPaths)) + } else { + unique(canonChrom(as.character(ldSketch@snpInfo$CHR))) + } +} + +# Two-tier containment check for one entry GRanges against the LD sketch: +# * chromosome tier (hard error): any entry chromosome absent from the sketch. +# * variant tier: match entry variants against the sketch's variants on the +# shared chromosomes. Zero overlap -> error; overlap fraction below +# `minLdOverlapWarn` -> warning. Never filters. +.checkLdContainment <- function(ldSketch, entryGr, minLdOverlapWarn, label) { + if (length(entryGr) == 0L) return(invisible(NULL)) + reqChrom <- unique(canonChrom(as.character(GenomicRanges::seqnames(entryGr)))) + availChrom <- .ldSketchChroms(ldSketch) + missingChrom <- setdiff(reqChrom, availChrom) + if (length(missingChrom) > 0L) { + stop(label, ": chromosome(s) ", paste(missingChrom, collapse = ", "), + " are absent from the LD sketch (available: ", + paste(availChrom, collapse = ", "), ").") + } + snpInfo <- ldSketch@snpInfo + if (is.null(snpInfo) || nrow(snpInfo) == 0L) { + warning(label, ": LD sketch carries no variant metadata; ", + "skipping the variant-overlap check.") + return(invisible(NULL)) + } + keepSketch <- canonChrom(as.character(snpInfo$CHR)) %in% reqChrom + snpInfo <- snpInfo[keepSketch, , drop = FALSE] + entryVid <- formatVariantId( + chrom = canonChrom(as.character(GenomicRanges::seqnames(entryGr))), + pos = GenomicRanges::start(entryGr), + A2 = as.character(entryGr$A2), + A1 = as.character(entryGr$A1)) + sketchVid <- formatVariantId( + chrom = canonChrom(as.character(snpInfo$CHR)), + pos = as.integer(snpInfo$BP), + A2 = as.character(snpInfo$A2), + A1 = as.character(snpInfo$A1)) + km <- matchVariants(entryVid, sketchVid) + nOverlap <- length(km$idxA) + if (nOverlap == 0L) { + stop(label, ": none of the ", length(entryGr), " summary-statistic ", + "variants are present in the LD sketch.") + } + frac <- nOverlap / length(entryGr) + if (frac < minLdOverlapWarn) { + warning(sprintf( + "%s: only %.1f%% of summary-statistic variants (%d/%d) are present in the LD sketch.", + label, 100 * frac, nOverlap, length(entryGr))) + } + invisible(NULL) +} + +# ============================================================================= +# Summary-statistic file readers +# ============================================================================= + +# Coerce a region argument (chr:start-end string, GRanges, or one-row +# data.frame with chrom/start/end) into a GRanges. +.asGRegion <- function(region) { + if (methods::is(region, "GRanges")) return(region) + if (is.data.frame(region)) { + return(GenomicRanges::GRanges( + seqnames = as.character(region$chrom), + ranges = IRanges::IRanges(start = as.integer(region$start), + end = as.integer(region$end)))) + } + if (is.character(region) && length(region) == 1L) { + m <- regmatches(region, regexec("^([^:]+):([0-9,]+)-([0-9,]+)$", region))[[1L]] + if (length(m) != 4L) { + stop("`region` must be a 'chr:start-end' string, a GRanges, or a ", + "one-row data.frame with chrom/start/end.") + } + return(GenomicRanges::GRanges( + seqnames = m[[2L]], + ranges = IRanges::IRanges(start = as.integer(gsub(",", "", m[[3L]])), + end = as.integer(gsub(",", "", m[[4L]]))))) + } + stop("`region` must be a 'chr:start-end' string, a GRanges, or a ", + "one-row data.frame with chrom/start/end.") +} + +# Remap a region's seqnames to the seqnames actually present in a tabix file +# (tolerant of chr-prefix differences). Returns NULL when no seqname matches. +.matchRegionSeqnames <- function(gr, available) { + want <- as.character(GenomicRanges::seqnames(gr)) + mapped <- vapply(want, function(w) { + hit <- available[canonChrom(available) == canonChrom(w)] + if (length(hit) >= 1L) hit[[1L]] else NA_character_ + }, character(1)) + if (anyNA(mapped)) return(NULL) + GenomicRanges::GRanges( + seqnames = mapped, + ranges = IRanges::IRanges(start = GenomicRanges::start(gr), + end = GenomicRanges::end(gr))) +} + +# Read the region of a bgzipped + tabix-indexed delimited file into a +# data.frame, using the file's last header (#...) line for column names. +.readTabixRegion <- function(path, region) { + tf <- Rsamtools::TabixFile(path) + hdr <- Rsamtools::headerTabix(tf)$header + colLine <- if (length(hdr) > 0L) { + sub("^#", "", hdr[[length(hdr)]]) + } else { + NULL + } + gr <- .matchRegionSeqnames(.asGRegion(region), Rsamtools::seqnamesTabix(tf)) + emptyDf <- if (!is.null(colLine)) { + as.data.frame(readr::read_tsv(I(colLine), show_col_types = FALSE, + progress = FALSE), check.names = FALSE)[0, , drop = FALSE] + } else { + data.frame() + } + if (is.null(gr)) return(emptyDf) + lines <- unlist(Rsamtools::scanTabix(tf, param = gr), use.names = FALSE) + if (length(lines) == 0L) return(emptyDf) + txt <- paste(c(colLine, lines), collapse = "\n") + as.data.frame(readr::read_tsv(I(txt), show_col_types = FALSE, progress = FALSE), + check.names = FALSE) +} + +# Default source-column aliases used when no explicit columnMapping is given +# (mirrors gwas_sumstats_construct.R). +.sumstatColumnAliases <- list( + chrom = c("chrom", "#chrom", "chr", "#chr"), + pos = c("pos", "position", "BP", "bp"), + variant_id = c("variant_id", "SNP", "snp", "rsid"), + A1 = c("A1", "a1"), + A2 = c("A2", "a2"), + Z = c("z", "Z"), + N = c("n_sample", "N", "n"), + BETA = c("beta", "BETA"), + SE = c("se", "SE"), + P = c("p", "P", "pvalue", "pval"), + MAF = c("maf", "MAF", "effect_allele_frequency"), + INFO = c("info", "INFO")) + +# Accepted columnMapping *key* spellings per canonical field. A mapping file +# uses the xqtl-protocol standard-key names (e.g. `z`, `n_sample`), which +# differ from the canonical mcol names (`Z`, `N`); this bridges the two. +.sumstatMappingKeys <- list( + chrom = c("chrom", "chr"), + pos = c("pos", "position"), + variant_id = c("variant_id", "snp", "SNP"), + A1 = c("A1", "a1"), + A2 = c("A2", "a2"), + Z = c("z", "Z"), + N = c("n_sample", "N", "n"), + BETA = c("beta", "BETA"), + SE = c("se", "SE"), + P = c("p", "P", "pvalue"), + MAF = c("maf", "MAF", "effect_allele_frequency"), + INFO = c("info", "INFO")) + +# Read a columnMapping specification into a named character vector mapping a +# standard key (chrom/pos/variant_id/...) to the source column name. Accepts a +# named list/vector, or a path to a YAML file of `standardName: sourceName` +# entries (the xqtl-protocol column-mapping format). +.readColumnMapping <- function(columnMapping) { + if (is.null(columnMapping)) return(NULL) + if (is.list(columnMapping) || (is.character(columnMapping) && + !is.null(names(columnMapping)))) { + return(vapply(columnMapping, as.character, character(1))) + } + if (is.character(columnMapping) && length(columnMapping) == 1L) { + if (!file.exists(columnMapping)) { + stop("columnMapping file not found: ", columnMapping) + } + mapping <- yaml::read_yaml(columnMapping) + if (!is.list(mapping) || is.null(names(mapping)) || + any(!nzchar(names(mapping)))) { + stop("columnMapping file '", columnMapping, + "' must be a YAML mapping of standardName: sourceName.") + } + vapply(mapping, as.character, character(1)) + } else { + stop("`columnMapping` must be a named list/vector or a path to a YAML ", + "mapping file.") + } +} + +# Standardise the columns of a raw sumstats data.frame into the canonical +# schema expected by .dfToEntryGranges: chrom, pos, SNP, A1, A2, Z, N (+ +# optional BETA, SE, P, MAF, INFO). `mapping` (named std -> source) overrides +# the default aliases per key. +.resolveSumstatCols <- function(df, columnMapping, label) { + # Strip a leading '#' from the first column name so a '#CHR'-style header + # (common in GWAS TSVs) resolves the same way on the plain-text and tabix + # read paths, and so explicit columnMapping values match the bare name. + if (ncol(df) > 0L) names(df)[1L] <- sub("^#", "", names(df)[1L]) + mapping <- .readColumnMapping(columnMapping) + resolveKey <- function(key) { + # Look the field up in the mapping under any accepted standard-key spelling + # (`z`/`Z`, `n_sample`/`N`, ...). `intersect` avoids the "subscript out of + # bounds" a named character vector throws for an absent `[[` key. + if (!is.null(mapping)) { + cand <- intersect(.sumstatMappingKeys[[key]], names(mapping)) + if (length(cand) >= 1L) { + src <- mapping[[cand[[1L]]]] + if (!(src %in% names(df))) { + stop(label, ": columnMapping['", cand[[1L]], "'] = '", src, + "' is not a column in the sumstats file.") + } + return(src) + } + } + intersect(.sumstatColumnAliases[[key]], names(df))[1L] + } + required <- c("chrom", "pos", "variant_id", "A1", "A2", "Z", "N") + resolved <- setNames(lapply(required, resolveKey), required) + missingKeys <- required[vapply(resolved, function(x) is.na(x) || is.null(x), logical(1))] + if (length(missingKeys) > 0L) { + stop(label, ": sumstats file is missing required field(s): ", + paste(missingKeys, collapse = ", "), + " (supply a columnMapping if the source names differ).") + } + out <- data.frame( + chrom = as.character(df[[resolved$chrom]]), + pos = as.integer(df[[resolved$pos]]), + SNP = as.character(df[[resolved$variant_id]]), + A1 = as.character(df[[resolved$A1]]), + A2 = as.character(df[[resolved$A2]]), + Z = as.numeric(df[[resolved$Z]]), + N = as.numeric(df[[resolved$N]]), + stringsAsFactors = FALSE) + for (key in c("BETA", "SE", "P", "MAF", "INFO")) { + src <- resolveKey(key) + if (!is.na(src) && !is.null(src)) out[[key]] <- as.numeric(df[[src]]) + } + out +} + +# Default GWAS-VCF FORMAT tag mapping (canonical stat -> FORMAT field). +.gwasVcfFormatDefaults <- list(BETA = "ES", SE = "SE", P = "LP", + N = "SS", MAF = "EAF") + +# Pick the FORMAT sample column of a GWAS-VCF to read. Defaults to the single +# sample when there is exactly one; otherwise `sampleSelect` must name it. +.pickVcfSample <- function(samples, sampleSelect, label) { + if (!is.null(sampleSelect)) { + if (!(sampleSelect %in% samples)) { + stop(label, ": sampleSelect '", sampleSelect, + "' is not a sample in the VCF (have: ", + paste(samples, collapse = ", "), ").") + } + return(sampleSelect) + } + if (length(samples) == 1L) return(samples[[1L]]) + stop(label, ": the VCF has ", length(samples), " samples (", + paste(samples, collapse = ", "), + "); pass `sampleSelect` to choose the study column.") +} + +# Convert a VCF (GWAS-VCF format) into the canonical sumstats data.frame. The +# effect allele (A1) is ALT; the other allele (A2) is REF. Stats come from the +# per-study FORMAT fields ES/SE/LP/SS/EAF, with Z = ES / SE. +.vcfToSumstatDf <- function(vcf, sampleSelect, formatMapping, label) { + fmap <- modifyList(.gwasVcfFormatDefaults, + if (is.null(formatMapping)) list() else as.list(formatMapping)) + rr <- SummarizedExperiment::rowRanges(vcf) + altList <- VariantAnnotation::alt(vcf) + alt <- vapply(as.list(altList), function(a) as.character(a)[[1L]], character(1)) + ref <- as.character(VariantAnnotation::ref(vcf)) + geno <- VariantAnnotation::geno(vcf) + sample <- .pickVcfSample(colnames(vcf), sampleSelect, label) + getField <- function(tag) { + if (!is.null(tag) && tag %in% names(geno)) { + as.numeric(geno[[tag]][, sample]) + } else { + NULL + } + } + es <- getField(fmap$BETA) + se <- getField(fmap$SE) + lp <- getField(fmap$P) + ss <- getField(fmap$N) + eaf <- getField(fmap$MAF) + if (is.null(es) || is.null(se)) { + stop(label, ": GWAS-VCF must carry ES and SE FORMAT fields to derive Z ", + "(configure via `formatMapping`).") + } + if (is.null(ss)) { + stop(label, ": GWAS-VCF must carry an SS (sample size) FORMAT field.") + } + out <- data.frame( + chrom = as.character(GenomicRanges::seqnames(rr)), + pos = as.integer(GenomicRanges::start(rr)), + SNP = names(rr), + A1 = alt, + A2 = ref, + Z = es / se, + N = ss, + BETA = es, + SE = se, + stringsAsFactors = FALSE) + if (!is.null(lp)) out$P <- 10^(-lp) + if (!is.null(eaf)) out$MAF <- pmin(eaf, 1 - eaf) + out +} + +# Read one GWAS-VCF sumstats file (region-restricted only when bgzipped + +# tabix-indexed) into the canonical sumstats data.frame. +.readSumStatsVcf <- function(path, region, sampleSelect, formatMapping, label) { + if (!requireNamespace("VariantAnnotation", quietly = TRUE)) { + stop(label, ": reading VCF sumstats requires the 'VariantAnnotation' ", + "package; please install it.") + } + hasTbi <- file.exists(paste0(path, ".tbi")) + if (!is.null(region) && hasTbi) { + tf <- Rsamtools::TabixFile(path) + gr <- .matchRegionSeqnames(.asGRegion(region), Rsamtools::seqnamesTabix(tf)) + vcf <- if (is.null(gr)) { + VariantAnnotation::readVcf(path)[0, ] + } else { + VariantAnnotation::readVcf(tf, param = gr) + } + } else { + if (!is.null(region) && !hasTbi) { + warning(label, ": VCF '", path, "' is not bgzipped + tabix-indexed; ", + "region ignored, reading the whole file.") + } + vcf <- VariantAnnotation::readVcf(path) + } + .vcfToSumstatDf(vcf, sampleSelect, formatMapping, label) +} + +# Read one delimited-text sumstats file. Region reads only when a .tbi +# sidecar exists; otherwise the whole file is read and any region is ignored. +.readSumStatsText <- function(path, region, columnMapping, label) { + hasTbi <- file.exists(paste0(path, ".tbi")) + raw <- if (!is.null(region) && hasTbi) { + .readTabixRegion(path, region) + } else { + if (!is.null(region) && !hasTbi) { + warning(label, ": '", path, "' is not tabix-indexed; ", + "region ignored, reading the whole file.") + } + as.data.frame(readr::read_tsv(path, show_col_types = FALSE, progress = FALSE), + check.names = FALSE) + } + .resolveSumstatCols(raw, columnMapping, label) +} + +# Dispatch a sumstats file to the text or GWAS-VCF reader. BCF is rejected. +.readSumStatsFile <- function(path, region, columnMapping, sampleSelect, + formatMapping, label) { + lower <- tolower(path) + if (endsWith(lower, ".bcf")) { + stop(label, ": BCF sumstats are not supported; convert '", path, + "' to a bgzipped, tabix-indexed VCF.") + } + if (grepl("\\.vcf(\\.b?gz)?$", lower)) { + .readSumStatsVcf(path, region, sampleSelect, formatMapping, label) + } else { + .readSumStatsText(path, region, columnMapping, label) + } +} + +# Read one sumstats source into an entry GRanges (canonical mcols). +.loadSumStatsEntry <- function(path, region, columnMapping, sampleSelect, + formatMapping, label) { + df <- .readSumStatsFile(path, region, columnMapping, sampleSelect, + formatMapping, label) + .dfToEntryGranges(df) +} + +# ============================================================================= +# Phenotype SummarizedExperiment builder (individual-level QTL) +# ============================================================================= + +# Read a covariate TSV into a samples-as-rows numeric matrix. The first column +# holds row identifiers; `transpose = TRUE` handles QTLtools-format inputs +# (covariates as rows, samples as columns). +.readCovariateMatrix <- function(path, transpose = FALSE) { + raw <- as.data.frame(readr::read_tsv(path, show_col_types = FALSE, + progress = FALSE), check.names = FALSE) + rn <- as.character(raw[[1L]]) + m <- as.matrix(raw[, -1L, drop = FALSE]) + rownames(m) <- rn + storage.mode(m) <- "double" + if (transpose) m <- t(m) + m +} + +# Build one per-context SummarizedExperiment from a bgzipped BED phenotype +# file (QTLtools layout: #chr/start/end/ID + sample columns) and an optional +# per-context covariate TSV (attached as colData). +.buildContextSe <- function(bedPath, covPath, transposeCov) { + bed <- as.data.frame(readr::read_tsv(bedPath, show_col_types = FALSE, + progress = FALSE, comment = ""), + check.names = FALSE) + chrCol <- intersect(c("#chr", "#chrom", "chrom", "chr"), names(bed))[1L] + startCol <- intersect(c("start", "Start"), names(bed))[1L] + endCol <- intersect(c("end", "End"), names(bed))[1L] + geneCol <- intersect(c("gene_id", "ID", "phenotype_id"), names(bed))[1L] + if (any(is.na(c(chrCol, startCol, endCol, geneCol)))) { + stop("phenotype BED is missing one of chrom/start/end/gene_id columns: ", + bedPath) + } + meta <- bed[, c(chrCol, startCol, endCol, geneCol)] + names(meta) <- c("chrom", "start", "end", "gene_id") + sampleCols <- setdiff(names(bed), + c("#chr", "#chrom", "chrom", "chr", "start", "Start", "end", "End", + "gene_id", "ID", "phenotype_id", "strand", "Strand")) + expr <- as.matrix(bed[, sampleCols, drop = FALSE]) + storage.mode(expr) <- "double" + rownames(expr) <- meta$gene_id + rr <- GenomicRanges::GRanges( + seqnames = meta$chrom, + ranges = IRanges::IRanges(start = meta$start + 1L, end = meta$end)) + names(rr) <- meta$gene_id + cd <- if (!is.null(covPath) && nzchar(covPath)) { + pcov <- .readCovariateMatrix(covPath, transposeCov) + common <- intersect(rownames(pcov), colnames(expr)) + if (length(common) == 0L) { + stop("No shared samples between phenotype and covariate file: ", bedPath) + } + expr <- expr[, common, drop = FALSE] + S4Vectors::DataFrame(pcov[common, , drop = FALSE], row.names = common) + } else { + S4Vectors::DataFrame(row.names = colnames(expr)) + } + SummarizedExperiment::SummarizedExperiment( + assays = list(expression = expr), rowRanges = rr, colData = cd) +} + +# Aliases for the QtlDataset (phenotype) manifest. +.qtlDatasetManifestAliases <- list( + context = c("context", "cond", "condition"), + phenotypePath = c("phenotypePath", "phenotype_path", "path", + "phenotypeFile", "phenotype_file"), + covariatePath = c("covariatePath", "covariate_path", "cov_path", + "covPath"), + study = c("study", "study_id", "studyId"), + genotypePath = c("genotypePath", "genotype_path", "genotypePrefix", + "genotype_prefix", "genotypeFile"), + genotypeCovariatePath = c("genotypeCovariatePath", "genotype_covariate_path", + "genotype_covariates", "genotypeCovariates")) + +# Build one QtlDataset from an already-canonicalised set of manifest rows for a +# single study. `genotypesOverride` / `genoCovOverride` short-circuit the +# genotypePath / genotypeCovariatePath columns when supplied as arguments. +.buildQtlDatasetFromRows <- function(rows, study, base, transposeCov, qc, + genotypesOverride = NULL, + genoCovOverride = NULL) { + contexts <- unique(as.character(rows$context)) + phenotypes <- setNames(lapply(contexts, function(cx) { + sub <- rows[as.character(rows$context) == cx, , drop = FALSE] + pths <- unique(sub$phenotypePath[!is.na(sub$phenotypePath)]) + if (length(pths) != 1L) { + stop("Context '", cx, "' (study '", study, + "') must reference exactly one phenotypePath; got: ", + paste(pths, collapse = ", ")) + } + covPath <- NULL + if ("covariatePath" %in% names(sub)) { + covs <- unique(sub$covariatePath[!is.na(sub$covariatePath) & + nzchar(as.character(sub$covariatePath))]) + if (length(covs) > 1L) { + stop("Context '", cx, "' (study '", study, + "') references multiple covariatePath values: ", + paste(covs, collapse = ", ")) + } + if (length(covs) == 1L) covPath <- .resolveRel(covs[[1L]], base) + } + .buildContextSe(.resolveRel(pths[[1L]], base), covPath, transposeCov) + }), contexts) + + genoHandle <- if (methods::is(genotypesOverride, "GenotypeHandle")) { + genotypesOverride + } else { + spec <- if (is.character(genotypesOverride)) { + genotypesOverride + } else { + v <- unique(as.character(rows$genotypePath[!is.na(rows$genotypePath)])) + if (length(v) != 1L) { + stop("study '", study, "' must reference exactly one genotypePath; got: ", + paste(v, collapse = ", ")) + } + v[[1L]] + } + .detectGenotypeFormat(.resolveRel(spec, base)) + } + + genoCov <- if (is.matrix(genoCovOverride)) { + genoCovOverride + } else { + spec <- if (is.character(genoCovOverride)) { + genoCovOverride + } else if ("genotypeCovariatePath" %in% names(rows)) { + v <- unique(as.character(rows$genotypeCovariatePath[ + !is.na(rows$genotypeCovariatePath) & + nzchar(as.character(rows$genotypeCovariatePath))])) + if (length(v) > 1L) { + stop("study '", study, "' references multiple genotypeCovariatePath values.") + } + if (length(v) == 1L) v[[1L]] else NULL + } else { + NULL + } + if (!is.null(spec)) { + .readCovariateMatrix(.resolveRel(spec, base), transposeCov) + } else { + matrix(numeric(0), nrow = 0, ncol = 0) + } + } + + do.call(QtlDataset, c(list( + study = study, genotypes = genoHandle, phenotypes = phenotypes, + genotypeCovariates = genoCov), qc)) +} + +# ============================================================================= +# Public loaders +# ============================================================================= + +#' @title Load a QtlDataset from a manifest +#' @description Build a single-study \code{\link{QtlDataset}} from a manifest +#' describing one row per QTL context (or per trait x context). The manifest +#' is a data.frame or a path to a delimited file (\code{.csv} -> CSV, else +#' TSV). Column names may be snake_case or camelCase. +#' @param manifest A data.frame or a path to a manifest file. Recognised +#' columns (canonical camelCase; snake_case aliases accepted): +#' \code{context} (required), \code{phenotypePath} (required, bgzipped BED), +#' \code{covariatePath} (optional, per-context covariates), and the +#' single-valued \code{study} / \code{genotypePath} / +#' \code{genotypeCovariatePath}. +#' @param study Study identifier; reconciled with a \code{study} column. +#' @param genotypes A \code{\link{GenotypeHandle}} or a genotype path/prefix; +#' reconciled with a \code{genotypePath} column. +#' @param genotypeCovariates A numeric matrix (samples x covariates) or a path +#' to a covariate TSV; reconciled with a \code{genotypeCovariatePath} column. +#' @param scaleResiduals,mafCutoff,macCutoff,xvarCutoff,imissCutoff,keepSamples,keepVariants,keepIndel +#' Pass-through \code{\link{QtlDataset}} arguments (stored as lazy QC slots). +#' @param transposeCovariates Transpose covariate TSVs (QTLtools layout) before +#' treating them as samples-as-rows. +#' @return A \code{QtlDataset} object. +#' @export +loadQtlDatasetFromManifest <- function(manifest, study = NULL, + genotypes = NULL, + genotypeCovariates = NULL, + scaleResiduals = TRUE, + mafCutoff = 0, macCutoff = 0, + xvarCutoff = 0, imissCutoff = 0, + keepSamples = character(0), + keepVariants = character(0), + keepIndel = TRUE, + transposeCovariates = FALSE) { + base <- .manifestBase(manifest) + df <- .canonManifestCols(.readManifest(manifest), + .qtlDatasetManifestAliases, + required = c("context", "phenotypePath"), + label = "QtlDataset") + study <- .reconcileScalar(df$study, study, "study") + qc <- list(scaleResiduals = scaleResiduals, mafCutoff = mafCutoff, + macCutoff = macCutoff, xvarCutoff = xvarCutoff, + imissCutoff = imissCutoff, keepSamples = keepSamples, + keepVariants = keepVariants, keepIndel = keepIndel) + .buildQtlDatasetFromRows(df, study, base, transposeCovariates, qc, + genotypesOverride = genotypes, + genoCovOverride = genotypeCovariates) +} + +# Aliases shared by the sumstats manifests. +.gwasSumStatsManifestAliases <- list( + study = c("study", "study_id", "studyId"), + sumStatsPath = c("sumStatsPath", "sumstats_path", "path", "file_path", + "gwas_tsv", "filePath"), + columnMapping = c("columnMapping", "column_mapping", "column_mapping_file", + "columnMappingFile"), + nCase = c("nCase", "n_case"), + nControl = c("nControl", "n_control"), + varY = c("varY", "var_y"), + genome = c("genome"), + ldSketchPath = c("ldSketchPath", "ld_sketch_path", "ld_sketch")) + +.qtlSumStatsManifestAliases <- list( + study = c("study", "study_id", "studyId"), + context = c("context", "cond", "condition"), + trait = c("trait", "gene_id", "geneId", "molecular_trait_id"), + sumStatsPath = c("sumStatsPath", "sumstats_path", "path", "file_path", + "filePath"), + columnMapping = c("columnMapping", "column_mapping", "column_mapping_file", + "columnMappingFile"), + varY = c("varY", "var_y"), + genome = c("genome"), + ldSketchPath = c("ldSketchPath", "ld_sketch_path", "ld_sketch")) + +#' @title Load a GwasSumStats collection from a manifest +#' @description Build a \code{\link{GwasSumStats}} from a manifest with one row +#' per study. No QC is run (the result carries \code{qcInfo = list()}). +#' @param manifest A data.frame or path. Columns (snake_case aliases accepted): +#' \code{study} (required, unique), \code{sumStatsPath} (required), +#' \code{columnMapping} (optional), \code{nCase} / \code{nControl} +#' (optional), \code{varY} (optional), and the single-valued \code{genome} / +#' \code{ldSketchPath}. +#' @param genome Genome build; reconciled with a \code{genome} column. +#' @param ldSketch A \code{\link{GenotypeHandle}} or a spec +#' (path/prefix/genoMeta); reconciled with an \code{ldSketchPath} column. +#' @param region Optional \code{chr:start-end} string, GRanges, or one-row +#' data.frame restricting the variants read (honoured only for tabix-indexed +#' text and bgzipped+tabixed VCF; ignored with a warning otherwise). +#' @param minLdOverlapWarn Warn when the fraction of sumstats variants present +#' in the LD sketch falls below this (default 0.5). +#' @param columnMapping Optional default column mapping (a named list/vector, +#' or a path to a YAML file of \code{standardName: sourceName} entries) +#' applied when a row has no \code{columnMapping}. +#' @param sampleSelect Optional GWAS-VCF FORMAT sample (study) column to read. +#' @param formatMapping Optional GWAS-VCF FORMAT tag mapping (canonical stat -> +#' FORMAT field), overriding ES/SE/LP/SS/EAF defaults. +#' @return A \code{GwasSumStats} object. +#' @export +loadGwasSumStatsFromManifest <- function(manifest, genome = NULL, + ldSketch = NULL, region = NULL, + minLdOverlapWarn = 0.5, + columnMapping = NULL, + sampleSelect = NULL, + formatMapping = NULL) { + base <- .manifestBase(manifest) + df <- .canonManifestCols(.readManifest(manifest), + .gwasSumStatsManifestAliases, + required = c("study", "sumStatsPath"), + label = "GwasSumStats") + if (anyDuplicated(as.character(df$study))) { + stop("GwasSumStats manifest `study` values must be unique.") + } + genome <- .reconcileScalar(df$genome, genome, "genome") + ldSketch <- .resolveLdSketchInput(df, ldSketch, base) + + entries <- lapply(seq_len(nrow(df)), function(i) { + label <- paste0("GwasSumStats[study=", df$study[[i]], "]") + mapping <- if ("columnMapping" %in% names(df) && + !is.na(df$columnMapping[[i]]) && + nzchar(as.character(df$columnMapping[[i]]))) { + .resolveRel(as.character(df$columnMapping[[i]]), base) + } else { + columnMapping + } + gr <- .loadSumStatsEntry(.resolveRel(as.character(df$sumStatsPath[[i]]), base), + region, mapping, sampleSelect, formatMapping, label) + .checkLdContainment(ldSketch, gr, minLdOverlapWarn, label) + gr + }) + + args <- list(study = as.character(df$study), entry = entries, + genome = genome, ldSketch = ldSketch) + if ("nCase" %in% names(df)) args$nCase <- as.numeric(df$nCase) + if ("nControl" %in% names(df)) args$nControl <- as.numeric(df$nControl) + if ("varY" %in% names(df)) args$varY <- as.numeric(df$varY) + do.call(GwasSumStats, args) +} + +# Build the entry list + tuple vectors for a QtlSumStats manifest. +.loadQtlSumStatsEntries <- function(df, base, region, ldSketch, + minLdOverlapWarn, columnMapping, + sampleSelect, formatMapping) { + lapply(seq_len(nrow(df)), function(i) { + label <- paste0("QtlSumStats[", df$study[[i]], "/", df$context[[i]], "/", + df$trait[[i]], "]") + mapping <- if ("columnMapping" %in% names(df) && + !is.na(df$columnMapping[[i]]) && + nzchar(as.character(df$columnMapping[[i]]))) { + .resolveRel(as.character(df$columnMapping[[i]]), base) + } else { + columnMapping + } + gr <- .loadSumStatsEntry(.resolveRel(as.character(df$sumStatsPath[[i]]), base), + region, mapping, sampleSelect, formatMapping, label) + if (!is.null(ldSketch)) { + .checkLdContainment(ldSketch, gr, minLdOverlapWarn, label) + } + gr + }) +} + +#' @title Load a QtlSumStats collection from a manifest +#' @description Build a \code{\link{QtlSumStats}} from a manifest with one row +#' per \code{(study, context, trait)} tuple. No QC is run. +#' @param manifest A data.frame or path. Columns (snake_case aliases accepted): +#' \code{study}, \code{context}, \code{trait} (required), \code{sumStatsPath} +#' (required), \code{columnMapping} (optional), \code{varY} (optional), and +#' the single-valued \code{genome} / \code{ldSketchPath}. +#' @param genome Genome build; reconciled with a \code{genome} column. +#' @param ldSketch A \code{\link{GenotypeHandle}} or spec; reconciled with an +#' \code{ldSketchPath} column. +#' @param region,minLdOverlapWarn,columnMapping,sampleSelect,formatMapping As +#' for \code{\link{loadGwasSumStatsFromManifest}}. +#' @return A \code{QtlSumStats} object. +#' @export +loadQtlSumStatsFromManifest <- function(manifest, genome = NULL, + ldSketch = NULL, region = NULL, + minLdOverlapWarn = 0.5, + columnMapping = NULL, + sampleSelect = NULL, + formatMapping = NULL) { + base <- .manifestBase(manifest) + df <- .canonManifestCols(.readManifest(manifest), + .qtlSumStatsManifestAliases, + required = c("study", "context", "trait", + "sumStatsPath"), + label = "QtlSumStats") + genome <- .reconcileScalar(df$genome, genome, "genome") + ldSketch <- .resolveLdSketchInput(df, ldSketch, base) + entries <- .loadQtlSumStatsEntries(df, base, region, ldSketch, + minLdOverlapWarn, columnMapping, + sampleSelect, formatMapping) + args <- list(study = as.character(df$study), + context = as.character(df$context), + trait = as.character(df$trait), + entry = entries, genome = genome, ldSketch = ldSketch) + if ("varY" %in% names(df)) args$varY <- as.numeric(df$varY) + do.call(QtlSumStats, args) +} + +#' @title Load a MultiStudyQtlDataset from manifests +#' @description Build a \code{\link{MultiStudyQtlDataset}} from a QtlDataset +#' manifest (individual-level studies) and, optionally, a QtlSumStats +#' manifest (summary-only studies). +#' @param qtlDatasetsManifest A data.frame or path. The QtlDataset schema with +#' \code{study} and \code{genotypePath} \strong{mandatory} (grouping key = +#' \code{study}); one \code{QtlDataset} is built per study group. +#' @param sumStatsManifest Optional QtlSumStats manifest (see +#' \code{\link{loadQtlSumStatsFromManifest}}). +#' @param genome,ldSketch,region,minLdOverlapWarn,columnMapping,sampleSelect,formatMapping +#' Passed to the QtlSumStats loader for \code{sumStatsManifest}. +#' @param transposeCovariates Transpose covariate TSVs (QTLtools layout). +#' @param scaleResiduals,mafCutoff,macCutoff,xvarCutoff,imissCutoff,keepSamples,keepVariants,keepIndel +#' Pass-through \code{\link{QtlDataset}} arguments applied to every study. +#' @return A \code{MultiStudyQtlDataset} object. +#' @export +loadMultiStudyQtlDatasetFromManifest <- function(qtlDatasetsManifest, + sumStatsManifest = NULL, + genome = NULL, ldSketch = NULL, + region = NULL, + minLdOverlapWarn = 0.5, + columnMapping = NULL, + sampleSelect = NULL, + formatMapping = NULL, + transposeCovariates = FALSE, + scaleResiduals = TRUE, + mafCutoff = 0, macCutoff = 0, + xvarCutoff = 0, imissCutoff = 0, + keepSamples = character(0), + keepVariants = character(0), + keepIndel = TRUE) { + base <- .manifestBase(qtlDatasetsManifest) + df <- .canonManifestCols(.readManifest(qtlDatasetsManifest), + .qtlDatasetManifestAliases, + required = c("study", "genotypePath", "context", + "phenotypePath"), + label = "MultiStudyQtlDataset (qtlDatasetsManifest)") + qc <- list(scaleResiduals = scaleResiduals, mafCutoff = mafCutoff, + macCutoff = macCutoff, xvarCutoff = xvarCutoff, + imissCutoff = imissCutoff, keepSamples = keepSamples, + keepVariants = keepVariants, keepIndel = keepIndel) + studies <- unique(as.character(df$study)) + qtlDatasets <- setNames(lapply(studies, function(s) { + .buildQtlDatasetFromRows(df[as.character(df$study) == s, , drop = FALSE], + s, base, transposeCovariates, qc) + }), studies) + + sumStats <- NULL + if (!is.null(sumStatsManifest)) { + sumStats <- loadQtlSumStatsFromManifest( + sumStatsManifest, genome = genome, ldSketch = ldSketch, region = region, + minLdOverlapWarn = minLdOverlapWarn, columnMapping = columnMapping, + sampleSelect = sampleSelect, formatMapping = formatMapping) + } + MultiStudyQtlDataset(qtlDatasets = qtlDatasets, sumStats = sumStats) +} diff --git a/R/twasWeightsPipeline.R b/R/twasWeightsPipeline.R index dc057bb8..5c9c9c01 100644 --- a/R/twasWeightsPipeline.R +++ b/R/twasWeightsPipeline.R @@ -424,6 +424,24 @@ names(.fineMappingMethodCapabilities) } +# Canonical fine-mapping tokens actually present as methods in a +# FineMappingResult, matched tolerantly across canonical / camelCase / +# snake_case spellings (mirrors the candidate logic in .twasFineMappingFits). +# @noRd +.twasFineMappingMethodsPresent <- function(fineMappingResult) { + if (is.null(fineMappingResult)) return(character(0)) + methods <- tolower(as.character(fineMappingResult$method)) + present <- character(0) + for (canonical in .twasFineMappingTokens()) { + candidates <- tolower(c( + canonical, + paste0(tolower(substring(canonical, 1L, 1L)), substring(canonical, 2L)), + gsub("([A-Z])", "_\\1", canonical))) + if (any(methods %in% candidates)) present <- c(present, canonical) + } + present +} + # Reject fine-mapping methods (susie / susieInf / susieAsh / mvsusie / # fsusie) when no FineMappingResult is supplied. twasWeightsPipeline is # not allowed to re-fit fine-mapping models from scratch; users must run @@ -459,6 +477,16 @@ if (!is(fineMappingResult, "FineMappingResultBase")) { stop("`fineMappingResult` must be a FineMappingResult or NULL.") } + # A supplied fineMappingResult must actually contain each requested + # fine-mapping method; twasWeightsPipeline never re-fits them from scratch. + missingMethods <- setdiff(fmTokens, + .twasFineMappingMethodsPresent(fineMappingResult)) + if (length(missingMethods) > 0L) { + stop(sprintf( + "twasWeightsPipeline: method(s) %s were requested but the supplied fineMappingResult contains no such fine-mapping fit. Run fineMappingPipeline() with method(s) %s first and pass the result via `fineMappingResult = `.", + paste(unique(missingMethods), collapse = ", "), + paste(unique(missingMethods), collapse = ", "))) + } invisible(NULL) } diff --git a/data-raw/build_examples.R b/data-raw/build_examples.R index e8d2ca06..40fd19a2 100644 --- a/data-raw/build_examples.R +++ b/data-raw/build_examples.R @@ -245,21 +245,23 @@ qtl_sumstats_multicontext_example <- QtlSumStats( qcInfo = list(prebuilt = "synthetic multi-context example; QC bypassed")) # ----------------------------------------------------------------------------- -# 7. Strip the GenotypeHandle paths down to bare basenames so the bundled -# .rda objects don't carry source-tree-absolute paths. Vignettes / -# users resolve them at use-time via fixupExampleGenotypePaths(). +# 7. Rewrite the GenotypeHandle paths to a portable bundled-resource reference +# ("pecotmr://extdata/") so the .rda objects carry no source-tree +# absolute path. The genotype readers resolve this via system.file() at +# extraction time, so no user-facing fixup step is required. # ----------------------------------------------------------------------------- -qtl_dataset_example@genotypes@path <- basename( +asResource <- function(p) paste0("pecotmr://extdata/", basename(p)) +qtl_dataset_example@genotypes@path <- asResource( qtl_dataset_example@genotypes@path) -qtl_sumstats_example@ldSketch@path <- basename( +qtl_sumstats_example@ldSketch@path <- asResource( qtl_sumstats_example@ldSketch@path) -qtl_sumstats_multicontext_example@ldSketch@path <- basename( +qtl_sumstats_multicontext_example@ldSketch@path <- asResource( qtl_sumstats_multicontext_example@ldSketch@path) -gwas_sumstats_s4_example@ldSketch@path <- basename( +gwas_sumstats_s4_example@ldSketch@path <- asResource( gwas_sumstats_s4_example@ldSketch@path) for (nm in names(multi_study_qtl_dataset_example@qtlDatasets)) multi_study_qtl_dataset_example@qtlDatasets[[nm]]@genotypes@path <- - basename(multi_study_qtl_dataset_example@qtlDatasets[[nm]]@genotypes@path) + asResource(multi_study_qtl_dataset_example@qtlDatasets[[nm]]@genotypes@path) # ----------------------------------------------------------------------------- # 8. Save. diff --git a/data/gwas_sumstats_s4_example.rda b/data/gwas_sumstats_s4_example.rda index d59338e923731149526698f8fe53c356f9613812..8232c7630d8a66dbf535c874c6a1e6c7dcd7a42c 100644 GIT binary patch delta 7033 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 X-K_!uXDkHuJ}?c70ssI200CKAg=VFm delta 7001 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 diff --git a/data/multi_study_qtl_dataset_example.rda b/data/multi_study_qtl_dataset_example.rda index c17c6f3e66c86b2c295718d208a838de38a8d403..73f52791cb82858f693884750b046ece59a96128 100644 GIT binary patch delta 6902 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 delta 6834 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

;0lq1_$N>P4)R1F7 OFb#_W000000a;oXWKcf< delta 5276 zcmV;N6l3e;Da0v|83beD6o!!<^?9*bLQIWy+lw4cbDw%pU-yC-aD|U zZa#|)a^2rv#sCyHQ;3@Fx@j(}U&{d|)|#%00O9D@e7tCJ4zJLl+8KgV?|(qtx!%c1 zM#Y;R;&=J>U!aJMPirc+Wz{t8DzD2FQmsVOmvkUY@!hcZN_*Ds2<)&xK;&G#+jkvZh47SXQ+WimkJnW!*$10TM zkvx@Qn75yz3FAL9UVEK}ayffczB@K&dQfaq>&*+<1(2p${OgzWa(eB&(}X=BkErXG zOoTPb;$fX-{3$penTIB*wg##uNhqvZ1>SxUcyU**AKlHV6Pv*%Y=5ro{-_5ocZ}6AE<#yza-<0u5!<&o z&h@pyPU|!dq!&=6R5mJiL$`pZ(RgTkf7|pGrpZ0jTnbeOyq|Q z?2sxo0|m~M!@Nire1W0ED$DSqSU67?#_N(MIf4?AY{?dHzt~lwT=@<*A<_ap$q=Jg z4F%n&P+M+vxqq6JA#FIC;94Em3GiJ0eg^4_?M+EM{;pv1veg{=X$oMwOAX1^Sp+}g z?$CB#9>Y&*)Qw|M_7U`)wV-^w*BfIS;@lwBj8@WIQ6~sVSSjR|=x1P;1sk_+(7lVo zet1IHK*Q{f*va2sCL7-PhxBnEHo~Tk^H1nvW88}R$$!hgtEN;e6|WzX_Jy+H2E1wq ziiaA_+e=Vsqt*Np%~voBPH2HV)&=^k2(#$+_6)uL4(UdUHCwn<;)?h8z9Nf`+1V(G z5zkz0mwc2PyG4f`DrBmO2yS}K3^ah7iYR}GXEXXwBiu}ZXAU`77H5Sqh^dLK-6pyH!U>-@2HyYc-l$!3oD}J8}y^AdLA>r z%~84rvQW%f9xznQ*fK(YMVzL3mU zHWlU<2ny0ZdF7|QPkVx)uGodwnWZkbag2#D=YKShPvxwM_S}PnBCtLox@5iX_mVj> zZPjj{m>$X(UItP0xw8nMfEwOVB=p=x^Vy7LJKUowje<*qd;9I1vNDC zfojq!NeVh;sVpYV0|K}v%uM8W^9=+nG=G`9t-zRA83j~p0)K{5$!rBHO9(3dyvObH zE7|EYRihu!LMzLQ#l>bZfq1xwkH@e;(a!Ylk`_TjSn>+d=Q==!n2A(*oK8h_GP3>- zS$qZ3sM~kNtSl+&a>XwiPTK%urv9i#RId!ogJhF8Gu}v$wi`j{g8qY294L2dr+>(d zAY5-%_ryHZ+hR%|Sg&2AB0d!S$og91|XS;|XyRj*yi_0~8_hN?db!Q10Jq&t4WBCx4Ps1*0ZK!x@1ioFeoRxhA8d8oH3kLsWS{-zqFn zeuWfVMNI_Ht(c5#pPmVk5rX=Jx}Yn7GERU1MwuadVmErU>=o@a*zanjR@4L>izbL^ zMQu2{q43y=jH@JkhuUdn7b$^YJ3A}9YtdCMw0lSyiqlIE$smTjLPKof9eP5I?jDQTHinCDcdvi&>ZX9rnF~%wZ%yT|8Tf{V>lF9TbPrVTc∋r=_af>{+>!%%K^ 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 K000000a;qRfpI(l delta 7988 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