From 08b91cff6c30405568742433e6d331acdb07467e Mon Sep 17 00:00:00 2001 From: Daniel Nachun Date: Thu, 15 Oct 2026 16:12:27 -0700 Subject: [PATCH 1/5] add readers and writers --- DESCRIPTION | 5 +- NAMESPACE | 16 +- R/AllClasses.R | 19 + R/AllGenerics.R | 248 +- R/GwasFineMappingResult.R | 28 +- R/QtlDataset.R | 41 +- R/QtlFineMappingResult.R | 44 +- R/RangedTupleList.R | 10 + R/causalInferencePipeline.R | 61 +- R/colocPipeline.R | 65 +- R/colocboostPipeline.R | 96 +- R/ctwasWrapper.R | 33 +- R/fineMappingPipeline.R | 387 ++- R/fineMappingRow.R | 190 +- R/fineMappingWrappers.R | 269 +- R/gwasSumStats.R | 1 + R/jointEngine.R | 92 +- R/jointSpecification.R | 7 +- R/manifestLoaders.R | 116 +- R/overlapTopLoci.R | 11 +- R/resultReaders.R | 1287 ++++++++ R/resultWriters.R | 2626 +++++++++++++++++ R/sumstatsQc.R | 8 +- R/tupleSelectors.R | 153 +- R/twasWeights.R | 12 +- R/twasWeightsPipeline.R | 29 +- R/variantId.R | 14 +- R/vcfWriter.R | 695 ----- _pkgdown.yml | 12 +- data/mvsusieFineMappingExample.rda | Bin 115012 -> 118008 bytes data/qtlFineMappingExample.rda | Bin 211120 -> 212452 bytes data/qtlFineMappingPairedExample.rda | Bin 19288 -> 19332 bytes man/GwasFineMappingResult.Rd | 27 + man/QtlFineMappingResult.Rd | 27 + man/ResidualizationParam.Rd | 10 + man/TwasWeights.Rd | 7 + man/blockId.Rd | 46 + man/colocboostPipeline.Rd | 13 + man/exportToBed.Rd | 138 + man/exportToVcf.Rd | 114 + man/fineMappingPipeline.Rd | 41 +- man/fsusieCredibleBand.Rd | 9 +- man/fsusieTraitEffects.Rd | 42 + man/genomicOrder.Rd | 37 + man/genomicRegion.Rd | 32 +- man/overlapTopLoci.Rd | 4 +- man/parseRegion.Rd | 1 + man/readFineMappingResult.Rd | 121 + man/readTwasWeights.Rd | 85 + man/twasWeightsPipeline.Rd | 23 + man/writeSumStatsVcf.Rd | 71 - tests/testthat/test_RangedTupleList.R | 50 + tests/testthat/test_causalInferencePipeline.R | 97 +- tests/testthat/test_colocPipeline.R | 53 +- tests/testthat/test_colocboostPipeline.R | 48 + tests/testthat/test_fineMappingPipeline.R | 320 +- tests/testthat/test_fineMappingRow.R | 192 +- tests/testthat/test_fineMappingWrappers.R | 75 +- tests/testthat/test_jointEngine.R | 19 + tests/testthat/test_manifestLoaders.R | 76 + tests/testthat/test_overlapTopLoci.R | 2 +- tests/testthat/test_resultReaders.R | 675 +++++ tests/testthat/test_resultWriters.R | 1721 +++++++++++ tests/testthat/test_tupleSelectors.R | 125 +- tests/testthat/test_twasWeightsPipeline.R | 92 + tests/testthat/test_variantId.R | 28 +- tests/testthat/test_vcfWriter.R | 747 ----- vignettes/finemapping-postprocessing.Rmd | 85 +- vignettes/twas-weights.Rmd | 62 +- 69 files changed, 9962 insertions(+), 1928 deletions(-) create mode 100644 R/resultReaders.R create mode 100644 R/resultWriters.R delete mode 100644 R/vcfWriter.R create mode 100644 man/blockId.Rd create mode 100644 man/exportToBed.Rd create mode 100644 man/exportToVcf.Rd create mode 100644 man/fsusieTraitEffects.Rd create mode 100644 man/genomicOrder.Rd create mode 100644 man/readFineMappingResult.Rd create mode 100644 man/readTwasWeights.Rd delete mode 100644 man/writeSumStatsVcf.Rd create mode 100644 tests/testthat/test_resultReaders.R create mode 100644 tests/testthat/test_resultWriters.R delete mode 100644 tests/testthat/test_vcfWriter.R diff --git a/DESCRIPTION b/DESCRIPTION index bf07385f..2aed4fa5 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -174,12 +174,13 @@ Collate: 'qtlEnrichmentPipeline.R' 'regularizedRegressionWrappers.R' 'relatednessQc.R' + 'twasWeights.R' + 'resultWriters.R' + 'resultReaders.R' 'sldscPostprocessingPipeline.R' 'sldscWrapper.R' - 'twasWeights.R' 'twasWeightsPipeline.R' 'twasWeightsRow.R' - 'vcfWriter.R' Depends: R (>= 4.5) LazyData: false diff --git a/NAMESPACE b/NAMESPACE index 47c4572d..57b01701 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -154,6 +154,7 @@ export(bayesCWeights) export(bayesLWeights) export(bayesNWeights) export(bayesRWeights) +export(blockId) export(blockMetadata) export(buildLdEigen) export(buildLdScore) @@ -210,6 +211,8 @@ export(ensembleWeights) export(estCtwasGroupPriors) export(estimateH2) export(estimateSparsity) +export(exportToBed) +export(exportToVcf) export(extractCsInfo) export(extractTopPipInfo) export(filterInvalidSummaryStat) @@ -233,8 +236,10 @@ export(frqData) export(fsusieAffectedRegions) export(fsusieCredibleBand) export(fsusieGetCs) +export(fsusieTraitEffects) export(fsusieWeights) export(fullFit) +export(genomicOrder) export(genomicRegion) export(genotypeCovariates) export(genotypes) @@ -343,10 +348,12 @@ export(qtlSumStatsFromZMatrix) export(raiss) export(readAfreq) export(readAnnotations) +export(readFineMappingResult) export(readGenotypes) export(readSldscAnnot) export(readSldscFrq) export(readSldscTrait) +export(readTwasWeights) export(refPanel) export(refVariantInfo) export(regionToDf) @@ -409,7 +416,6 @@ export(variantInfo) export(waldTestPval) export(weightStandardized) export(weightsDataType) -export(writeSumStatsVcf) export(xvarCutoff) export(z) exportClasses(AnnotationMatrix) @@ -486,6 +492,7 @@ exportMethods(as.data.frame) exportMethods(as.list) exportMethods(baselineAnnotations) exportMethods(bindROWS) +exportMethods(blockId) exportMethods(blockMetadata) exportMethods(coef) exportMethods(colnames) @@ -507,11 +514,14 @@ exportMethods(cvResult) exportMethods(eigenList) exportMethods(endoapply) exportMethods(estimateH2) +exportMethods(exportToBed) +exportMethods(exportToVcf) exportMethods(fineMappingPipeline) exportMethods(fineMappingResult) exportMethods(frqData) exportMethods(fsusieAffectedRegions) exportMethods(fsusieCredibleBand) +exportMethods(fsusieTraitEffects) exportMethods(fullFit) exportMethods(genomicRegion) exportMethods(genotypeCovariates) @@ -590,7 +600,6 @@ exportMethods(variantInfo) exportMethods(weightStandardized) exportMethods(weights) exportMethods(weightsDataType) -exportMethods(writeSumStatsVcf) exportMethods(xvarCutoff) exportMethods(z) importClassesFrom(GenomicRanges,GRanges) @@ -623,6 +632,7 @@ importFrom(GenomicRanges, start ) importFrom(IRanges, + CharacterList, DataFrameList, IRanges, findOverlaps @@ -632,7 +642,9 @@ importFrom(MultiAssayExperiment,longForm) importFrom(Rsamtools, TabixFile, asBcf, + bgzip, headerTabix, + indexTabix, scanTabix, seqnamesTabix ) diff --git a/R/AllClasses.R b/R/AllClasses.R index 80898645..153bd0f0 100644 --- a/R/AllClasses.R +++ b/R/AllClasses.R @@ -772,6 +772,7 @@ setMethod("methodNames", "FineMappingResultBase", function(x) { unique(as.character(x$method)) }) + #' @noRd setMethod( "adjustPips", @@ -1085,6 +1086,24 @@ setMethod("fsusieCredibleBand", "FineMappingResultBase", function(x) { .fmrAggregateView(x, perEntry = .fmrRowFsusieCredibleBand) }) +#' @rdname fsusieTraitEffects +#' @export +setMethod("fsusieTraitEffects", "FineMappingResultBase", function(x) { + # Not .fmrAggregateView(): the per-entry payload carries only the fit, and + # this view also needs the row's `jointTraits` column for the trait names. + meta <- .fmrRowMetadata(x) + parts <- compact(map( + seq_len(nrow(x)), + .fsusieEntryTraitEffects, + x = x, + meta = meta + )) + if (length(parts) == 0L) { + return(bind_cols(slice(meta, integer(0)), .emptyTraitEffects())) + } + .rbindAligned(parts) +}) + #' @rdname fsusieAffectedRegions #' @export setMethod("fsusieAffectedRegions", "FineMappingResultBase", function(x) { diff --git a/R/AllGenerics.R b/R/AllGenerics.R index 1b2f7233..6721f949 100644 --- a/R/AllGenerics.R +++ b/R/AllGenerics.R @@ -1381,25 +1381,50 @@ setGeneric("weightsDataType", function(x, ...) { # AlleleQcResult accessor generics # QcResult accessor generics # ============================================================================= -# VCF/BCF writer generic +# Published-output generics (VCF/BCF and BED) # ============================================================================= -#' Write summary statistics or fine-mapping results to VCF/BCF +#' Export a result to VCF/BCF #' -#' Creates a VCF object from GWAS summary statistics or fine-mapping results and -#' writes it to disk via VariantAnnotation and Rsamtools. Supports bgzipped -#' VCF (.vcf.gz/.vcf.bgz); BCF (.bcf) additionally needs a working -#' \code{Rsamtools::asBcf()}, which recent Rsamtools releases disable, so -#' prefer .vcf.bgz. +#' Writes GWAS summary statistics, fine-mapping results or TWAS weights to a +#' GWAS-VCF-style file via VariantAnnotation and Rsamtools: one record per +#' variant, one SAMPLE per study / fitted model, and the per-variant statistics +#' as FORMAT fields. Supports bgzipped VCF (.vcf.gz/.vcf.bgz); BCF (.bcf) +#' additionally needs a working \code{Rsamtools::asBcf()}, which recent +#' Rsamtools releases disable, so prefer .vcf.bgz. #' -#' @param x Input data: a \code{GwasSumStats} object, a \code{FineMappingResult} -#' object, or a data.frame with columns \code{chrom}, \code{pos}, \code{ref}, -#' \code{alt}. +#' Which FORMAT fields are written depends on the input: +#' \describe{ +#' \item{\code{GwasSumStats}}{\code{ES} / \code{SS} / \code{AF} for one +#' study.} +#' \item{\code{FineMappingResult}}{the marginal fields (\code{ES}, +#' \code{SE}, \code{LP}, \code{SS}, \code{AF}) plus the posterior ones +#' (\code{PIP}, \code{LBF}, \code{LFSR}, and a \code{CS}/\code{PUR} pair +#' per credible-set coverage present).} +#' \item{\code{TwasWeights}}{\code{WGT}, the prediction weight.} +#' } +#' +#' Every fit the selectors match becomes a SAMPLE of ONE file: the records are +#' the union of their variants, the FORMAT schema is the union of their fields, +#' and a fit that did not include a variant -- or does not report a field -- +#' leaves those cells empty. So two methods over a region, or a dozen weight +#' methods for one gene, are compared by reading one file rather than a dozen. +#' Each sample is described in its own \code{##SAMPLE} header line, carrying +#' the fit's identity tuple and how many variants it contributed. +#' Cross-validation results are not exported -- see \code{\link{exportToBed}} +#' for why -- and an export requires the full-data fit. +#' +#' \code{splitByContext} / \code{splitByTrait} opt out of that and write one +#' single-sample file per fit, at a path decorated with the axis value. +#' +#' @param x A \code{GwasSumStats}, \code{FineMappingResult} or +#' \code{TwasWeights} object, or a \code{list} of \code{FineMappingResult} +#' / \code{TwasWeights} objects to combine first. #' @param outputPath File path for output. Extension determines format: #' \code{.vcf.gz} or \code{.vcf.bgz} for bgzipped VCF, \code{.bcf} for BCF, #' \code{.vcf} for uncompressed VCF. -#' @param sampleName Name for the VCF sample column (default: trait name or -#' method name from the S4 object). +#' @param sampleName Name(s) for the VCF sample column(s). Default \code{NULL} +#' derives them from the object's identity tuple. #' @param studyName Character or \code{NULL}. Restrict the written records to #' this study; \code{NULL} includes all studies. #' @param context Character or \code{NULL}. Restrict the written records to this @@ -1413,14 +1438,109 @@ setGeneric("weightsDataType", function(x, ...) { #' @param splitByTrait Logical. If \code{TRUE}, write one VCF per trait. Default #' \code{FALSE}. #' @param ... Additional arguments passed to methods. -#' @return Invisible path to the written file. +#' @return Invisible path(s) to the written file(s). +#' @seealso \code{\link{exportToBed}} #' @examples #' data(gwasSumStatsS4Example) -#' writeSumStatsVcf( +#' exportToVcf( #' gwasSumStatsS4Example, outputPath = tempfile(fileext = ".vcf")) #' @export -setGeneric("writeSumStatsVcf", function(x, outputPath, sampleName = NULL, ...) { - standardGeneric("writeSumStatsVcf") +setGeneric("exportToVcf", function(x, outputPath, sampleName = NULL, ...) { + standardGeneric("exportToVcf") +}) + +#' Export a result to a published BED table +#' +#' Writes fine-mapping results or TWAS weights to a published FunGen-xQTL BED +#' table, bgzipping and tabix-indexing the output when the path ends in +#' \code{.gz} / \code{.bgz}. The tabular counterpart of +#' \code{\link{exportToVcf}}: the VCF is one record per variant with the models +#' as samples, while these tables are long-format, one row per published +#' observation. +#' +#' A \code{TwasWeights} writes one table, a row per (variant, weight column). +#' A \code{FineMappingResult} offers two, selected with \code{schema}: +#' \describe{ +#' \item{\code{"topLoci"}}{(default) the posterior view joined to the +#' marginal one, a row per (variant, context).} +#' \item{\code{"fsusie"}}{the functional view, a row per (context, +#' credible set), carrying the fitted curve at both resolutions +#' \code{levels} selects.} +#' } +#' +#' Every table identifies the fit a row came from -- \code{study_ID}, +#' \code{gene_ID}, \code{event_ID} and (where it spans methods) +#' \code{method} -- because these collections are keyed by that tuple and +#' several of them are routinely folded into one output. +#' +#' Cross-validation results are NOT exported, in any schema. They describe how +#' a model scored on held-out FOLDS; nothing about them is per-variant, and a +#' published file has no data-secure way to express anything fold-level, +#' because fold membership is sample-level information and sample names can be +#' protected. They remain on the object, where \code{\link{cvResult}} returns +#' them and the caller tabulates whatever they need. For the same reason an +#' export REQUIRES the full-data fit: a cross-validation-only collection (what +#' \code{twasWeightsPipeline(fitFullData = FALSE)} produces) is refused rather +#' than written out empty. +#' +#' @param x A \code{FineMappingResult} or \code{TwasWeights} object, or a +#' \code{list} of either, to combine first. +#' @param outputPath Output path. A \code{.gz} / \code{.bgz} extension selects +#' bgzip + tabix (via Rsamtools, so no htslib binary is needed); any other +#' extension writes a plain table. +#' @param schema \code{FineMappingResult} only: which published schema to +#' write, \code{"topLoci"} (default) or \code{"fsusie"}. +#' @param coverage Which credible-set coverage levels to publish, as +#' proportions (\code{c(0.95, 0.5)}). \code{NULL} (default) publishes every +#' level the input carries. Which levels exist is fixed at FIT time by +#' \code{\link{CredibleSetParam}} and the stored \code{cs_} columns are +#' read rather than recomputed, so this selects a subset and errors -- +#' naming what is available -- for a level the fit never computed. +#' @param levels \code{"fsusie"} schema only: which resolution(s) of the +#' fitted effect to write. fSuSiE carries the same effect twice -- +#' \code{"wavelet"}, the curve on the fit's own grid +#' (\code{\link{fsusieCredibleBand}}), and \code{"trait"}, that curve +#' reinterpolated onto the positions of the traits the fit ran over +#' (\code{\link{fsusieTraitEffects}}). They are complementary, so both are +#' written by default; naming one drops the other's columns, leaving a strict +#' subset of the full table. +#' @param signalCutoff PIP cutoff passed to \code{\link{topLoci}} +#' (\code{FineMappingResult}, \code{"topLoci"} schema only). Default 0.025. +#' @param minPurity Optional credible-set purity cutoff passed to +#' \code{\link{topLoci}} (\code{FineMappingResult}, \code{"topLoci"} schema +#' only). Default \code{NULL} for no purity filter. +#' @param columns Which of the SOURCE view's own variant-level columns to +#' append after the published schema. \code{NULL} (default) appends none; +#' \code{"all"} appends every column the view carries that the schema does +#' not already write; a character vector appends exactly those. The +#' published columns always lead, so adding to them never moves an existing +#' one. The fine-mapping views carry a long tail worth asking for +#' (\code{grange_start} / \code{grange_end}, the legacy \code{gene} / +#' \code{event} aliases, and whatever a custom build added); a +#' \code{TwasWeights} element carries only its alleles and weight, so there +#' is nothing further to add unless the collection was built by hand. Not +#' accepted by the \code{"performance"} schema, which has no variant axis. +#' @param extraColumns Optional NAMED list of trailing columns to append after +#' the published schema. Each element is either length 1 (recycled across +#' every row) or one value per row. For fields no S4 object can know -- the +#' legacy top-loci BED's \code{block_id}, parsed from the source filename, +#' is the motivating case. Names that collide with a published column are an +#' error rather than a silent overwrite. +#' @param ... Additional arguments passed to methods. +#' @return \code{outputPath}, invisibly. +#' @seealso \code{\link{exportToVcf}}, \code{\link{genomicOrder}} +#' @examples +#' data(qtlFineMappingExample) +#' exportToBed(qtlFineMappingExample, tempfile(fileext = ".bed")) +#' exportToBed( +#' qtlFineMappingExample, tempfile(fileext = ".bed"), +#' schema = "topLoci", columns = "all" +#' ) +#' data(twasWeightsExample) +#' exportToBed(twasWeightsExample, tempfile(fileext = ".bed")) +#' @export +setGeneric("exportToBed", function(x, outputPath, ...) { + standardGeneric("exportToBed") }) # ============================================================================= @@ -1460,19 +1580,64 @@ setGeneric("contexts", function(x) standardGeneric("contexts")) #' @export setGeneric("traitNames", function(x) standardGeneric("traitNames")) -#' @title Get Per-Row Genomic Regions -#' @description Return the genomic anchor of each row of a per-tuple collection -#' as a \code{GRanges} with one range per row -- the trait's own region for a -#' \code{TwasWeights}, or the fine-mapping window for a -#' \code{FineMappingResult}. This is location provenance (e.g. for cTWAS -#' LD-block placement). +#' @title Get Per-Row Analysis Windows +#' @description Return the genomic interval each row was analysed over, as a +#' \code{GRanges} with one range per row -- the trait's own region for a +#' \code{TwasWeights}, and for a \code{FineMappingResult} the window the fit +#' was run over (the requested region, or the trait's cis-window when the fit +#' was driven by \code{traitId} + \code{cisWindow}). This is location +#' provenance (e.g. for cTWAS LD-block placement). +#' +#' This is the window that was ANALYSED, not the span of the variants that +#' survived into the fit: QC and screening drop variants first, so a fit over +#' \code{chr22:10000000-18000000} may contain variants spanning only +#' \code{chr22:10414272-16916492}. The window is stored because it cannot be +#' recovered from the object; the span can, and is simply \code{range(x)}. +#' +#' Being a record of what was analysed, it deliberately does NOT change under +#' \code{subsetRegion()} or row subsetting -- a subset of a fit's variants was +#' still produced by a fit over the original window. Use \code{range(x)} when +#' you want the current content instead. +#' +#' Collections built before the window was recorded fall back to the derived +#' span, so the accessor always returns a range. #' @param x The object. #' @param ... Reserved for future use. #' @return A \code{GRanges} with one range per row of \code{x}, or an empty -#' \code{GRanges} when the collection carries no region provenance. +#' \code{GRanges} when the collection has no rows. +#' @seealso \code{\link{traitPosition}} for the trait's own coordinates. #' @export setGeneric("genomicRegion", function(x, ...) standardGeneric("genomicRegion")) +#' @title Get The LD Block Identifier +#' @description Return the LD-block identifier of each row of a collection +#' that keys its rows by block --- a \code{GwasSumStats}, or a +#' \code{GwasFineMappingResult} fine-mapped over one. +#' +#' Per ROW, like \code{\link{genomicRegion}} and unlike +#' \code{\link{studyName}} / \code{\link{methodNames}}: a block id is +#' provenance for a particular fit, not an axis the collection is indexed +#' on, so the returned vector is always the same length as \code{nrow(x)} +#' and can be used to label or join rows directly. +#' +#' It keys the external block manifest the blocks were defined in, and so +#' carries the true block BOUNDARIES --- unlike \code{range(x)}, which is +#' the span of the variants that survived QC. A collection with no block +#' axis (every QTL flavour) returns \code{NA} for every row rather than +#' erroring, so code that handles both flavours needs no class test. +#' @param x The object. +#' @param ... Reserved for future use. +#' @return Character, one value per row of \code{x} (\code{NA} where the row +#' records none). +#' @seealso \code{\link{genomicRegion}} for the window that was analysed. +#' @examples +#' data(gwasFineMappingExample, qtlFineMappingExample) +#' blockId(gwasFineMappingExample) +#' # A QTL result has no block axis, so one NA per row rather than an error. +#' blockId(qtlFineMappingExample) +#' @export +setGeneric("blockId", function(x, ...) standardGeneric("blockId")) + #' @title Get Per-Row Trait Positions #' @description Return the molecular feature's OWN genomic coordinates (the gene #' / peak range; TSS = \code{start()}) for each row of a per-tuple collection, @@ -2287,9 +2452,12 @@ setGeneric("credibleSetSummary", function(x, ...) { #' #' @param x A \code{FineMappingRow} or \code{FineMappingResultBase}. #' @param ... Ignored. -#' @return A long \code{tibble}: \code{cs, chrom, pos, effect, lower, upper} -#' (the collection method additionally carries the entry identity columns). -#' @seealso \code{\link{fsusieAffectedRegions}} +#' @return A long \code{tibble}: \code{cs, chrom, pos, effect, lower, upper, +#' halfwidth} -- \code{halfwidth} being \code{(upper - lower) / 2}, the +#' symmetric band half-width publication tables carry (the collection method +#' additionally carries the entry identity columns). +#' @seealso \code{\link{fsusieTraitEffects}}, +#' \code{\link{fsusieAffectedRegions}} #' @examples #' data(fsusieFineMappingExample) #' fsusieCredibleBand(fsusieFineMappingExample) @@ -2298,6 +2466,34 @@ setGeneric("fsusieCredibleBand", function(x, ...) { standardGeneric("fsusieCredibleBand") }) +#' fSuSiE effect at each joint trait +#' +#' The fitted effect curve evaluated AT the traits the functional fit was run +#' over, one row per (credible set, trait). The curve itself lives on a wavelet +#' grid (\code{\link{fsusieCredibleBand}}); the traits sit at arbitrary +#' positions between grid points, so the effect is linearly interpolated with +#' constant extrapolation past the ends. +#' +#' This is the view that pairs a trait's NAME with its position: the names come +#' from the row's \code{jointTraits} column and the positions from the fit, +#' index-aligned because both describe the same joint group. Returns zero rows +#' when the functional fields are absent. +#' +#' @param x A \code{FineMappingRow} or \code{FineMappingResultBase}. +#' @param ... Ignored. +#' @return A long \code{tibble}: \code{cs, chrom, jointTrait, traitPos, +#' effect} (the collection method additionally carries the entry identity +#' columns). +#' @seealso \code{\link{fsusieCredibleBand}}, +#' \code{\link{fsusieAffectedRegions}} +#' @examples +#' data(fsusieFineMappingExample) +#' fsusieTraitEffects(fsusieFineMappingExample) +#' @export +setGeneric("fsusieTraitEffects", function(x, ...) { + standardGeneric("fsusieTraitEffects") +}) + #' fSuSiE affected genomic regions #' #' The sub-intervals of the functional grid where a credible set's band excludes diff --git a/R/GwasFineMappingResult.R b/R/GwasFineMappingResult.R index d072008c..1a2aefd9 100644 --- a/R/GwasFineMappingResult.R +++ b/R/GwasFineMappingResult.R @@ -59,6 +59,7 @@ setClass( c( .gfmrCheckEntryLength(object), .validateTraitPosColumn(object), + .validateRegionColumn(object), .gfmrCheckTupleUniqueness(object) ) } @@ -126,11 +127,30 @@ setClass( #' not part of the identity key. Supply it when the block BOUNDARIES matter: #' those are the one thing not recoverable from the variants, since adjacent #' blocks' spans leave gaps. +#' @param dataType Optional assay / modality label ("bulk_rnaseq", +#' "methylation"), recorded as a per-row provenance column. Length 1 or +#' one per row; \code{NULL} (default) omits the column. +#' @param dataSource Optional per-row label for the kind of input the fit saw, +#' \code{"individual"} or \code{"sumstat"}. Recorded rather than derived: a +#' combined collection can hold rows of both kinds, and its single +#' \code{ldSketch} cannot say which row came from which. +#' @param weightStandardized Optional per-row flag: whether the posterior +#' effect is on the STANDARDIZED genotype scale. The posterior effect IS the +#' prediction weight, so it carries the same scale question as a TWAS +#' weight. \code{\link{fineMappingPipeline}} always passes \code{TRUE}: +#' it standardizes genotypes before fitting, and the posterior mean is +#' reported on the scale of the \code{X} the fit was given. The argument +#' exists for a collection assembled by hand, where only the caller knows. #' @param ldSketch An optional genotype panel (see #' \code{\link{readGenotypes}}). #' @param traitPos Optional per-row trait genomic anchor (a \code{GRanges} or #' \code{NULL}), carried forward as provenance; not part of the identity key. #' \code{NULL} (default) omits the column. +#' @param region Optional per-row analysis WINDOW (a \code{GRanges} or +#' \code{NULL}) -- the interval each row's fit was run over, carried +#' forward as provenance; not part of the identity key. \code{NULL} +#' (default) omits the column, and \code{\link{genomicRegion}} then +#' derives the window from the span of the variants present. #' @return A \code{GwasFineMappingResult} object. #' @examples #' tl <- data.frame(variant_id = paste0("chr1:", 100 * 1:3, ":A:G"), @@ -145,6 +165,10 @@ GwasFineMappingResult <- function( entry, blockId = NULL, traitPos = NULL, + region = NULL, + dataType = NULL, + dataSource = NULL, + weightStandardized = NULL, ldSketch = NULL ) { n <- length(studyName) @@ -160,7 +184,9 @@ GwasFineMappingResult <- function( cvResult = S4Vectors::SimpleList(map(entry, cvResult)) ) |> .appendBlockIdCol(blockId, n) |> - .appendTraitPosCol(traitPos, n) + .appendTraitPosCol(traitPos, n) |> + .appendRegionCol(region, n) |> + .appendFmProvenanceCols(dataType, dataSource, weightStandardized, n) dfArgs <- c(cols, list(check.names = FALSE)) # Each entry's variants become one ELEMENT, its topLoci that element's # inner mcols, and its fit/cv payload outer mcols. A multi-seqname entry diff --git a/R/QtlDataset.R b/R/QtlDataset.R index b2a5edc6..f3c7c1e5 100644 --- a/R/QtlDataset.R +++ b/R/QtlDataset.R @@ -1652,6 +1652,20 @@ setMethod("phenotypeCovariates", "QtlDataset", function(x, contexts) { set_names(map(contexts, .qtlContextColData, x = x), contexts) }) +# Whether to scale residual columns to unit variance: the per-call override +# when the bundle sets one, else the dataset's own slot. +# +# Unset (NULL) rather than FALSE means "not asked", the same three-state +# convention `residualizePhenotype` / `residualizeGenotype` use -- a flat +# formal defaulting to TRUE could not tell "FALSE" from "not given". +# @noRd +.qtlScaleResiduals <- function(x, residualizationArgs) { + if (is.null(residualizationArgs)) { + return(scaleResiduals(x)) + } + residualizationArgs$scaleResiduals %||% scaleResiduals(x) +} + # Internal: residualize a numeric matrix Y (n x k) against a covariate # matrix C (n x p) via pivoted QR decomposition. Adds an intercept column # to C. When C is rank-deficient (e.g., union of all contexts' phenotype @@ -2021,7 +2035,7 @@ setMethod("phenotypeCovariates", "QtlDataset", function(x, contexts) { .qtlResidualizeQr( aligned$G, aligned$C, - scaleResiduals = scaleResiduals(x) + scaleResiduals = .qtlScaleResiduals(x, residualizationArgs) ) } @@ -2149,7 +2163,8 @@ setMethod( x = x, outlierAction = outlierAction, outlierPvalThreshold = outlierPvalThreshold, - outlierArgs = outlierArgs + outlierArgs = outlierArgs, + scaleResiduals = .qtlScaleResiduals(x, residualizationArgs) ) } @@ -2203,7 +2218,8 @@ setMethod( x, outlierAction, outlierPvalThreshold, - outlierArgs + outlierArgs, + scaleResiduals ) { out <- set_names( map( @@ -2214,7 +2230,8 @@ setMethod( x = x, outlierAction = outlierAction, outlierPvalThreshold = outlierPvalThreshold, - outlierArgs = outlierArgs + outlierArgs = outlierArgs, + scaleResiduals = scaleResiduals ), contexts ) @@ -2303,7 +2320,8 @@ setMethod("show", "QtlDataset", function(object) { x, outlierAction, outlierPvalThreshold, - outlierArgs = list() + outlierArgs = list(), + scaleResiduals = scaleResiduals(x) ) { .qtlResidualizeContextPheno( Yraw[[ctx]], @@ -2311,7 +2329,7 @@ setMethod("show", "QtlDataset", function(object) { ctx, outlierAction, outlierPvalThreshold, - scaleResiduals(x), + scaleResiduals, outlierArgs ) } @@ -2332,6 +2350,7 @@ setClass( genotypeCovariates = "CovariateSelection", residualizePhenotype = "logical_OR_NULL", residualizeGenotype = "logical_OR_NULL", + scaleResiduals = "logical_OR_NULL", covariateNaAction = "character_OR_NULL" ) ) @@ -2374,6 +2393,14 @@ setClass( #' read as \code{TRUE}. #' @param residualizeGenotype Logical. Residualize on the genotype #' covariates. \code{NULL} (default) means unset, read as \code{TRUE}. +#' @param scaleResiduals Logical. Scale each residual column to unit +#' variance, which is what puts a fitted effect on the standardized +#' genotype scale. \code{NULL} (default) means unset, and the accessors +#' fall back to the dataset's own \code{\link{scaleResiduals}} slot +#' (itself \code{TRUE} by default) --- so this is the per-call override of +#' a per-object default, and the only knob the pipelines need: it reaches +#' \code{\link{residualizedGenotypes}} already, which is the one point +#' every individual-level fit draws its genotypes through. #' @param covariateNaAction How missing covariate values are handled when the #' design is assembled: \code{"impute"} or \code{"drop"}. \code{NULL} #' (default) leaves the accessor's own default in place. @@ -2386,6 +2413,7 @@ ResidualizationParam <- function( genotypeCovariates = NULL, residualizePhenotype = NULL, residualizeGenotype = NULL, + scaleResiduals = NULL, covariateNaAction = NULL ) { # NULL means "unset", which is why the accessors no longer need a @@ -2398,6 +2426,7 @@ ResidualizationParam <- function( genotypeCovariates = genotypeCovariates, residualizePhenotype = residualizePhenotype, residualizeGenotype = residualizeGenotype, + scaleResiduals = scaleResiduals, covariateNaAction = covariateNaAction ) } diff --git a/R/QtlFineMappingResult.R b/R/QtlFineMappingResult.R index 2ec638d2..f7dfe39a 100644 --- a/R/QtlFineMappingResult.R +++ b/R/QtlFineMappingResult.R @@ -71,6 +71,7 @@ setClass( c( .qfmrCheckEntryLength(object), .validateTraitPosColumn(object), + .validateRegionColumn(object), .qfmrCheckJointCols(object, jointCols), .qfmrCheckTupleUniqueness(object, jointCols) ) @@ -181,12 +182,31 @@ setClass( #' shape as \code{jointStudies}. #' @param jointTraits Optional character vector for cross-trait joints. Same #' shape as \code{jointStudies}. +#' @param dataType Optional assay / modality label ("bulk_rnaseq", +#' "methylation"), recorded as a per-row provenance column. Length 1 or +#' one per row; \code{NULL} (default) omits the column. +#' @param dataSource Optional per-row label for the kind of input the fit saw, +#' \code{"individual"} or \code{"sumstat"}. Recorded rather than derived: a +#' combined collection can hold rows of both kinds, and its single +#' \code{ldSketch} cannot say which row came from which. +#' @param weightStandardized Optional per-row flag: whether the posterior +#' effect is on the STANDARDIZED genotype scale. The posterior effect IS the +#' prediction weight, so it carries the same scale question as a TWAS +#' weight. \code{\link{fineMappingPipeline}} always passes \code{TRUE}: +#' it standardizes genotypes before fitting, and the posterior mean is +#' reported on the scale of the \code{X} the fit was given. The argument +#' exists for a collection assembled by hand, where only the caller knows. #' @param ldSketch An optional genotype panel (see \code{\link{readGenotypes}}) #' (the LD reference for RSS-derived fits), or \code{NULL} for #' individual-level fits. #' @param traitPos Optional per-row trait genomic anchor (a \code{GRanges} or #' \code{NULL}), carried forward as provenance; not part of the identity key. #' \code{NULL} (default) omits the column. +#' @param region Optional per-row analysis WINDOW (a \code{GRanges} or +#' \code{NULL}) -- the interval each row's fit was run over, carried +#' forward as provenance; not part of the identity key. \code{NULL} +#' (default) omits the column, and \code{\link{genomicRegion}} then +#' derives the window from the span of the variants present. #' @return A \code{QtlFineMappingResult} object. #' @examples #' tl <- data.frame(variant_id = paste0("chr1:", 100 * 1:3, ":A:G"), @@ -208,6 +228,10 @@ QtlFineMappingResult <- function( jointContexts = NULL, jointTraits = NULL, traitPos = NULL, + region = NULL, + dataType = NULL, + dataSource = NULL, + weightStandardized = NULL, ldSketch = NULL ) { .qfmrAssertArgs( @@ -234,6 +258,10 @@ QtlFineMappingResult <- function( jointContexts, jointTraits, traitPos, + region, + dataType, + dataSource, + weightStandardized, n ) obj <- new( @@ -286,6 +314,10 @@ QtlFineMappingResult <- function( jointContexts, jointTraits, traitPos, + region, + dataType, + dataSource, + weightStandardized, n ) { baseCols <- list( @@ -303,7 +335,17 @@ QtlFineMappingResult <- function( jointTraits, n ) - .appendTraitPosCol(withJoint, traitPos, n) + .appendFmProvenanceCols( + .appendRegionCol( + .appendTraitPosCol(withJoint, traitPos, n), + region, + n + ), + dataType, + dataSource, + weightStandardized, + n + ) } # Each joint-provenance column must be one value per row. diff --git a/R/RangedTupleList.R b/R/RangedTupleList.R index b0cb29bf..26cece45 100644 --- a/R/RangedTupleList.R +++ b/R/RangedTupleList.R @@ -230,6 +230,16 @@ methods::setValidity("RangedTupleList", function(object) { #' @export setMethod("nrow", "RangedTupleList", function(x) length(x)) +# One method on the shared base rather than one per flavour: every collection +# that keys rows by LD block stores the id in the same mcols column, and the +# ones that have no block axis want the same NA-padded answer. Serialized +# collections predating the column take that path too. +#' @rdname blockId +#' @export +setMethod("blockId", "RangedTupleList", function(x, ...) { + .fmrIdentityColumn(x, "blockId") +}) + #' @rdname RangedTupleList-methods #' @export setMethod("ncol", "RangedTupleList", function(x) { diff --git a/R/causalInferencePipeline.R b/R/causalInferencePipeline.R index 97e2f0dc..35512d90 100644 --- a/R/causalInferencePipeline.R +++ b/R/causalInferencePipeline.R @@ -767,6 +767,12 @@ causalInferencePipeline <- function( } wr <- resolveWeights(ent) if (length(wr$variantIds) == 0L) { + warn(glue( + "causalInferencePipeline: no weights resolved for ", + "(study={studyName}, context={context}, trait={trait}, ", + "method={method}); skipping. The row carries variants but no ", + "usable weight, or the two are not the same length." + )) return(NULL) } wr @@ -971,13 +977,7 @@ causalInferencePipeline <- function( return(NULL) } cols <- .cipTlCols(tl) - if ( - length(cols$pip) == 0L || - length(cols$beta) == 0L || - length(cols$se) == 0L - ) { - return(NULL) - } + .cipRequireMrCols(cols, c("pip", "beta", "se"), "the Wald-ratio path") keep <- !is.na(tl[[cols$pip[[1L]]]]) & tl[[cols$pip[[1L]]]] > pipCutoff ivVars <- as.character(tl$variant_id)[keep] if (length(ivVars) == 0L) { @@ -1067,14 +1067,11 @@ causalInferencePipeline <- function( return(NULL) } cols <- .cipTlCols(tl) - if ( - length(cols$pip) == 0L || - length(cols$beta) == 0L || - length(cols$se) == 0L || - length(cols$cs) == 0L - ) { - return(NULL) - } + .cipRequireMrCols( + cols, + c("pip", "beta", "se", "cs"), + "the credible-set-aware path" + ) raw <- .cipCsAwareRaw(tl, cols) if (is.null(raw)) { return(NULL) @@ -1270,6 +1267,30 @@ causalInferencePipeline <- function( # Resolve the topLoci column aliases used across the CIP MR helpers. Returns the # first matching column name for each role (character(0) if absent); `cs` also # falls back to any column whose name starts with "cs" (e.g. cs_0.95). +# Abort naming the columns Mendelian randomization needs but the exposure does +# not have. +# +# These used to return NULL, so a result lacking `se` -- routine for a table +# converted from another tool, or read back from a published file whose source +# recorded only z -- produced "no (qtl, gwas) tuples produced a result" with +# nothing pointing at the cause. A column that is ABSENT is a structural +# property of the input and identical for every tuple, so it is worth stopping +# over; a column that is present but empty for one tuple stays a skip. +# @noRd +.cipRequireMrCols <- function(cols, need, label) { + absent <- need[map_int(cols[need], length) == 0L] + if (length(absent) == 0L) { + return(invisible(NULL)) + } + abort(glue( + "causalInferencePipeline: {label} needs ", + "{str_flatten(need, ', ')} on the exposure, and this result has no ", + "{str_flatten(absent, ', ')}. A result read from a published table ", + "carries beta/se only if the source recorded them, or if af and n ", + "let .zToBetaSe() derive them." + )) +} + .cipTlCols <- function(tl) { cn <- colnames(tl) exact <- intersect("cs", cn) @@ -1284,8 +1305,14 @@ causalInferencePipeline <- function( } list( pip = intersect(c("pip", "PIP"), cn), - beta = intersect(c("betahat", "beta", "bhat_x"), cn), - se = intersect(c("sebetahat", "se", "sbhat_x"), cn), + beta = intersect( + c("betahat", "beta", "bhat_x", "posterior_effect"), + cn + ), + se = intersect( + c("sebetahat", "se", "sbhat_x", "posterior_se"), + cn + ), cs = cs ) } diff --git a/R/colocPipeline.R b/R/colocPipeline.R index a44f1d3c..f3e30184 100644 --- a/R/colocPipeline.R +++ b/R/colocPipeline.R @@ -1151,14 +1151,15 @@ colocPipeline <- function( ) { fit <- susieFit(parts) if (is.null(fit)) { - msg <- glue("colocPipeline: {label} has no trimmedFit; skipping.") - warn(msg) - return(NULL) + abort(glue( + "colocPipeline: {label} has no retained fit, so it carries no ", + "LBF matrix and cannot be colocalized. Refit with ", + "`keepLbf = TRUE`." + )) } + # Filtering down to nothing below is a legitimate "no signal here" and + # stays a skip; it is only an ABSENT LBF that is an error. allRows <- .colocLbfMatrix(fit, label) - if (is.null(allRows)) { - return(NULL) - } allMass <- .colocEffectRetainedMass(fit, nrow(allRows)) keep <- .colocSelectLbfRows(allRows, fit, lbfFilterArgs) kept <- allRows[keep, , drop = FALSE] @@ -1181,39 +1182,31 @@ colocPipeline <- function( ) } -# Extract the (effects x variants) LBF matrix from a fit: susie lbf_variable, or -# a stacked fSuSiE lBF (direct or nested), or NULL (with a warning) when absent -# / empty. +# The (effects x variants) LBF matrix of a fit, or an ABORT. +# +# Delegates to .asLbfMatrix(), which knows every shape the LBF arrives in. +# These two used to keep separate shape lists and had drifted: coloc did not +# recognise a bare `fit$lBF`, which is what an fSuSiE fit carries, so fSuSiE +# colocalization skipped silently. +# +# A missing LBF is an ERROR, not a skip. coloc.bf_bf scores per-effect Bayes +# factors; without them there is nothing to approximate from -- a single-SNP +# ABF reconstructed from z/se is a different object on a different (prior- +# dependent) scale, and substituting one would quietly change the analysis. +# Warning per tuple and carrying on produced an empty result that looked like +# "no colocalization" rather than "the input cannot be colocalized", which is +# the wrong failure for a genome-wide run. # @noRd .colocLbfMatrix <- function(fit, label) { - lbfMatrix <- if (!is.null(fit$lbf_variable)) { - as.matrix(fit$lbf_variable) - } else if ( - !is.null(fit$fsusie_result) && - is.list(fit$fsusie_result$lBF) - ) { - # fSuSiE path: stack per-trait lBF lists into a single matrix. - lbfList <- fit$fsusie_result$lBF - exec(rbind, !!!lbfList) - } else if ( - is.list(fit) && - length(fit) >= 1L && - !is.null(fit[[1L]]$fsusie_result$lBF) - ) { - lbfList <- fit[[1L]]$fsusie_result$lBF - exec(rbind, !!!lbfList) - } else { - msg <- glue( - "colocPipeline: {label} trimmedFit has no lbf_variable / fsusie ", - "lBF; skipping." - ) - warn(msg) - return(NULL) - } + lbfMatrix <- .asLbfMatrix(fit) if (is.null(lbfMatrix) || nrow(lbfMatrix) == 0L) { - msg <- glue("colocPipeline: {label} LBF matrix is empty.") - warn(msg) - return(NULL) + abort(glue( + "colocPipeline: {label} carries no per-effect LBF matrix ", + "(no lbf_variable / fSuSiE lBF), so it cannot be colocalized. ", + "Refit with `keepLbf = TRUE`, or -- for a result read back from ", + "a published table -- export it from a fit built with ", + "`CredibleSetParam(perCsColumns = \"full\")`." + )) } lbfMatrix } diff --git a/R/colocboostPipeline.R b/R/colocboostPipeline.R index 9cecffbd..592ad5c7 100644 --- a/R/colocboostPipeline.R +++ b/R/colocboostPipeline.R @@ -121,6 +121,16 @@ #' below its cutoffs is dropped before the LD matrix is built. The defaults #' filter nothing. See the Details section for how these relate to the #' individual-level cutoffs recorded on a \code{QtlDataset}. +#' @param residualizationArgs Covariate residualization settings for +#' individual-level input, built with \code{\link{ResidualizationParam}}. +#' Reaches both \code{\link{residualizedGenotypes}} and +#' \code{\link{residualizedPhenotypes}}, so it governs what is regressed +#' out of each side and --- through \code{scaleResiduals} --- whether the +#' columns are scaled to unit variance. Previously absent, which left this +#' pipeline on the dataset's own settings with no per-call say, unlike +#' \code{\link{fineMappingPipeline}} and +#' \code{\link{twasWeightsPipeline}}. \strong{Ignored} on +#' \code{QtlSumStats} input, which has nothing to residualize. #' @param alleleFlip Logical, default \code{TRUE}. When TRUE, harmonize variants #' across the individual X, sumstats, and LD by (chrom, pos) with ref/alt #' swaps recognized (flipping z / residualized dosage / LD to a shared @@ -297,7 +307,8 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { region = NULL, cisWindow = NULL, samples = NULL, - pipCutoffToSkip = 0 + pipCutoffToSkip = 0, + residualizationArgs = ResidualizationParam() ) { contexts <- .cbResolveBundleContexts(qd, contexts) p <- list( @@ -306,7 +317,8 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { region = region, cisWindow = cisWindow, samples = samples, - pipCutoffToSkip = pipCutoffToSkip + pipCutoffToSkip = pipCutoffToSkip, + residualizationArgs = residualizationArgs ) built <- compact(map(contexts, .cbBuildContextXY, p = p)) if (length(built) == 0L) { @@ -356,7 +368,13 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { # NULL when the context should be skipped. # @noRd .cbBuildContextXY <- function(ctx, p) { - rawY <- .cbResidualizedY(p$qd, ctx, p$traitId, p$region) + rawY <- .cbResidualizedY( + p$qd, + ctx, + p$traitId, + p$region, + p$residualizationArgs + ) if (is.null(rawY) || ncol(rawY) == 0L) { return(NULL) } @@ -366,7 +384,8 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { p$traitId, p$region, p$cisWindow, - p$samples + p$samples, + p$residualizationArgs ) if (is.null(rawX) || ncol(rawX) == 0L) { return(NULL) @@ -401,13 +420,20 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { # Residualized phenotypes for one context (message + NULL on failure). # @noRd -.cbResidualizedY <- function(qd, ctx, traitId, region) { +.cbResidualizedY <- function( + qd, + ctx, + traitId, + region, + residualizationArgs = ResidualizationParam() +) { try_fetch( residualizedPhenotypes( qd, contexts = ctx, traitId = traitId, - region = region + region = region, + residualizationArgs = residualizationArgs ), error = function(cnd) { msg <- glue( @@ -422,7 +448,15 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { # Residualized genotypes for one context (message + NULL on failure). # @noRd -.cbResidualizedX <- function(qd, ctx, traitId, region, cisWindow, samples) { +.cbResidualizedX <- function( + qd, + ctx, + traitId, + region, + cisWindow, + samples, + residualizationArgs = ResidualizationParam() +) { try_fetch( residualizedGenotypes( qd, @@ -430,7 +464,8 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { traitId = traitId, region = region, cisWindow = cisWindow, - samples = samples + samples = samples, + residualizationArgs = residualizationArgs ), error = function(cnd) { msg <- glue( @@ -933,7 +968,10 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { ) runJoint <- .cbCanRun(jointGwas, hasSs, "jointGwas", ssNeeded) runSeparate <- .cbCanRun( - separateGwas, hasGwasSs, "separateGwas", "GWAS summary statistics" + separateGwas, + hasGwasSs, + "separateGwas", + "GWAS summary statistics" ) compact(list( xqtl_coloc = if (runXqtl) { @@ -1534,8 +1572,12 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { # each QTL study as a focal GWAS in its own right. gwasKeys <- setdiff(names(harmonized$pairs), qtlKeys) gwasRunBundles <- set_names( - map(gwasKeys, .cbGwasRunBundle, pairs = harmonized$pairs, - qtlKeys = qtlKeys), + map( + gwasKeys, + .cbGwasRunBundle, + pairs = harmonized$pairs, + qtlKeys = qtlKeys + ), gwasKeys ) .cbRunVariants( @@ -1607,7 +1649,8 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { panelFilterParam, signalScreenArgs, alleleFlip, - methodArgs + methodArgs, + residualizationArgs ) { screenSpec <- .screenResolve(signalScreenArgs) indBundle <- .cbIndividualBundle( @@ -1617,7 +1660,8 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { region = region, cisWindow = cisWindow, samples = samples, - pipCutoffToSkip = screenSpec + pipCutoffToSkip = screenSpec, + residualizationArgs = residualizationArgs ) .cbDriver( indBundle, @@ -1656,6 +1700,7 @@ setMethod( panelFilterParam = PanelFilterParam(), signalScreenArgs = SignalScreenParam(), alleleFlip = TRUE, + residualizationArgs = ResidualizationParam(), # `list()`, not ColocboostOptions(): the formal shadows the # constructor, so a constructor-call default is a recursive # reference. An empty list is the "no options" spelling @@ -1679,7 +1724,8 @@ setMethod( panelFilterParam = panelFilterParam, signalScreenArgs = signalScreenArgs, alleleFlip = alleleFlip, - methodArgs = methodArgs + methodArgs = methodArgs, + residualizationArgs = residualizationArgs ) } ) @@ -1748,6 +1794,7 @@ setMethod( panelFilterParam = PanelFilterParam(), signalScreenArgs = SignalScreenParam(), alleleFlip = TRUE, + residualizationArgs = ResidualizationParam(), # `list()`, not ColocboostOptions(): the formal shadows the # constructor, so a constructor-call default is a recursive # reference. An empty list is the "no options" spelling @@ -1771,7 +1818,8 @@ setMethod( panelFilterParam = panelFilterParam, signalScreenArgs = signalScreenArgs, alleleFlip = alleleFlip, - methodArgs = methodArgs + methodArgs = methodArgs, + residualizationArgs = residualizationArgs ) } ) @@ -1794,7 +1842,8 @@ setMethod( panelFilterParam, signalScreenArgs, alleleFlip, - methodArgs + methodArgs, + residualizationArgs ) { screenSpec <- .screenResolve(signalScreenArgs) indBundle <- .cbMultiStudyIndBundle( @@ -1804,7 +1853,8 @@ setMethod( region = region, cisWindow = cisWindow, samples = samples, - screenSpec = screenSpec + screenSpec = screenSpec, + residualizationArgs = residualizationArgs ) cutoffs <- .panelCutoffs(panelFilterParam) ss <- .cbMultiStudySumstats( @@ -1844,7 +1894,8 @@ setMethod( region, cisWindow, samples, - screenSpec + screenSpec, + residualizationArgs = ResidualizationParam() ) { sub <- .cbIndividualBundle( qtlDatasets[[studyName]], @@ -1853,7 +1904,8 @@ setMethod( region = region, cisWindow = cisWindow, samples = samples, - pipCutoffToSkip = screenSpec + pipCutoffToSkip = screenSpec, + residualizationArgs = residualizationArgs ) if (is.null(sub)) { return(NULL) @@ -1886,7 +1938,8 @@ setMethod( region, cisWindow, samples, - screenSpec + screenSpec, + residualizationArgs = ResidualizationParam() ) { qtlDatasets <- qtlDatasets(qtlData) subs <- compact(map( @@ -1898,7 +1951,8 @@ setMethod( region = region, cisWindow = cisWindow, samples = samples, - screenSpec = screenSpec + screenSpec = screenSpec, + residualizationArgs = residualizationArgs )) if (length(subs) == 0L) { return(NULL) diff --git a/R/ctwasWrapper.R b/R/ctwasWrapper.R index e0c03ce4..7595b0a9 100644 --- a/R/ctwasWrapper.R +++ b/R/ctwasWrapper.R @@ -261,7 +261,10 @@ screenCtwasRegions <- function( #' @export CtwasGeneZOptions <- function(...) { .ctwasStepOptions( - list(...), .ctwasOwnedGeneZ(), "CtwasGeneZOptions", "ctwasGeneZ", + list(...), + .ctwasOwnedGeneZ(), + "CtwasGeneZOptions", + "ctwasGeneZ", "ctwas::compute_gene_z" ) } @@ -292,7 +295,8 @@ CtwasRegionDataOptions <- function(...) { thin = "CtwasPriorParam(thin =)", ncore = "the pipeline's own `numThreads`" ), - "CtwasRegionDataOptions", "ctwasRegionData", + "CtwasRegionDataOptions", + "ctwasRegionData", "ctwas::assemble_region_data" ) } @@ -321,7 +325,8 @@ CtwasEstParamOptions <- function(...) { group_prior_var_structure = "CtwasPriorParam(varStructure =)", ncore = "the pipeline's own `numThreads`" ), - "CtwasEstParamOptions", "ctwasEstParam", + "CtwasEstParamOptions", + "ctwasEstParam", c("ctwas::est_param", "ctwas::fit_EM") ) } @@ -343,7 +348,9 @@ CtwasScreenOptions <- function(...) { group_prior_var = "estimated by the EM step", ncore = "the pipeline's own `numThreads`" ), - "CtwasScreenOptions", "ctwasScreen", "ctwas::screen_regions" + "CtwasScreenOptions", + "ctwasScreen", + "ctwas::screen_regions" ) } @@ -365,7 +372,9 @@ CtwasExpandOptions <- function(...) { z_snp = "supplied from the data by the pipeline", ncore = "the pipeline's own `numThreads`" ), - "CtwasExpandOptions", "ctwasExpand", "ctwas::expand_region_data" + "CtwasExpandOptions", + "ctwasExpand", + "ctwas::expand_region_data" ) } @@ -392,7 +401,9 @@ CtwasFinemapOptions <- function(...) { snpinfo_loader_fun = "fixed by the pipeline's LD loaders", ncore = "the pipeline's own `numThreads`" ), - "CtwasFinemapOptions", "ctwasFinemap", "ctwas::finemap_regions" + "CtwasFinemapOptions", + "ctwasFinemap", + "ctwas::finemap_regions" ) } @@ -437,7 +448,8 @@ CtwasMergeOptions <- function(...) { snpinfo_loader_fun = "fixed by the pipeline's LD loaders", ncore = "the pipeline's own `numThreads`" ), - "CtwasMergeOptions", "ctwasMerge", + "CtwasMergeOptions", + "ctwasMerge", c( "ctwas::postprocess_region_merging", "ctwas::postprocess_region_merging_noLD", @@ -464,7 +476,8 @@ CtwasBoundaryGenesOptions <- function(...) { weights = "supplied from the data by the pipeline", ncore = "the pipeline's own `numThreads`" ), - "CtwasBoundaryGenesOptions", "ctwasBoundaryGenes", + "CtwasBoundaryGenesOptions", + "ctwasBoundaryGenes", "ctwas::get_boundary_genes" ) } @@ -523,7 +536,9 @@ CtwasOptions <- function( .assertMethodOptions(finemap, "CtwasFinemapOptions", "finemap") .assertMethodOptions(merge, "CtwasMergeOptions", "merge") .assertMethodOptions( - boundaryGenes, "CtwasBoundaryGenesOptions", "boundaryGenes" + boundaryGenes, + "CtwasBoundaryGenesOptions", + "boundaryGenes" ) .newMethodOptions( NULL, diff --git a/R/fineMappingPipeline.R b/R/fineMappingPipeline.R index 0a762fde..8b6ffcb5 100644 --- a/R/fineMappingPipeline.R +++ b/R/fineMappingPipeline.R @@ -321,8 +321,34 @@ #' which meant a misspelled option was dropped silently; the #' constructor checks the names against that function's live formals. #' } +#' @param dataType Optional assay / modality label for the phenotype the fit +#' was run on (\code{"bulk_rnaseq"}, \code{"methylation"}), recorded as a +#' per-row provenance column and published by \code{\link{exportToBed}} / +#' \code{\link{exportToVcf}}. \code{NULL} (default) omits the column. It +#' cannot be derived --- the input carries the values, not what they measure. #' @param ... Reserved for future per-method arguments. #' +#' @section The genotype scale: +#' Individual-level genotypes reach every fit centred and scaled to unit +#' variance, so a posterior effect --- which is the prediction weight --- is +#' on the standardized scale, and the published +#' \code{weight_standardized} records it. That scaling belongs to the +#' \code{QtlDataset} accessors, not to this pipeline: it is +#' \code{\link{residualizedGenotypes}} that applies it, governed by +#' \code{ResidualizationParam(scaleResiduals = )} per call with the +#' dataset's own \code{\link{scaleResiduals}} slot as the default. One +#' mechanism, one place, shared with \code{\link{twasWeightsPipeline}} and +#' \code{\link{colocboostPipeline}}. An RSS fit needs nothing done: its +#' likelihood is written in standardized units already. +#' +#' It is what makes weights comparable across methods. Measured through +#' pecotmr's own entry points, every TWAS method returns coefficients on the +#' scale of the \code{X} it was given, and the DPR samplers are not even +#' scale-equivariant --- a uniform rescale of \code{X} changes their fit +#' rather than just its units. One input scale removes the question, and it +#' is the scale the LD correlations and z-scores behind a TWAS Z are already +#' on. +#' #' @return A \code{FineMappingResult} collection keyed by \code{(study, context, #' trait, method)}. The \code{ldSketch} slot is set automatically: \code{NULL} #' for individual-level (QtlDataset / all-individual-level @@ -828,6 +854,38 @@ setGeneric("fineMappingPipeline", function(data, ...) { .fmrRowParts(fineMappingResult, idx[[1L]]) } +# Record the three provenance columns every published export reads off a row. +# +# `dataType` is an outside fact (which assay the phenotype came from) and can +# only be supplied. The other two the pipeline KNOWS from the branch it took: +# a summary-statistics fit saw summary statistics, and the RSS likelihood is +# written in standardized units, so its posterior effects -- the prediction +# weights -- are on the standardized genotype scale by construction. An +# individual-level fit uses the X it was handed, so there the flag is the +# caller's to declare. +# +# Applied to the ASSEMBLED result rather than threaded into the builders: +# joint-engine rows are constructed on their own path and would otherwise +# reach `.rbindFineMappingResult` with a different column set. +# @noRd +.fmRecordProvenance <- function(x, dataType, dataSource, standardized) { + if (is.null(x)) { + return(x) + } + n <- nrow(x) + if (!is.null(dataType)) { + mcols(x)$dataType <- rep(as.character(dataType), length.out = n) + } + mcols(x)$dataSource <- rep(dataSource, length.out = n) + # DERIVED, never declared: it records what the accessors actually did. + # `residualizedGenotypes()` scales each genotype column to unit variance + # unless told otherwise -- `ResidualizationParam(scaleResiduals = FALSE)` + # per call, or the dataset's own `scaleResiduals` slot -- and an RSS fit's + # likelihood is written in standardized units to begin with. + mcols(x)$standardized <- rep(as.logical(standardized), length.out = n) + x +} + # Build a QtlFineMappingResult collection from per-tuple parallel vectors. # `jointStudies`, `jointContexts`, `jointTraits` are optional character # vectors (length matches `studies`) describing semicolon-joined joint @@ -844,6 +902,7 @@ setGeneric("fineMappingPipeline", function(data, ...) { jointContexts = NULL, jointTraits = NULL, traitPos = NULL, + region = NULL, ldSketch = NULL, allowEmpty = FALSE ) { @@ -864,10 +923,55 @@ setGeneric("fineMappingPipeline", function(data, ...) { jointContexts = jointContexts, jointTraits = jointTraits, traitPos = traitPos, + region = region, ldSketch = ldSketch ) } +# The per-row ANALYSIS WINDOW for a set of (context, trait) rows: the explicit +# `region` when the caller named one, else each trait's own cis window +# (traitPos +/- cisWindow) -- which is the interval the cis path actually +# slices X over, so a per-gene fit records its window just as a region-mode +# fit does. NULL when nothing resolves, so the builder omits the column and +# genomicRegion() falls back to the derived span. +# The window ONE block was analysed over: the explicit range when the caller +# named one, else the cis window of the trait(s) the block covers -- the same +# interval .fmResidGenoBlock() slices X over when `rg` is NULL. Collapsed to a +# single range so a multi-trait block (a PC pseudo-trait over a context's +# traits) records the span it covered. +# @noRd +.fmBlockWindow <- function(rg, ctx, traits, cfg) { + if (!is.null(rg)) { + return(rg) + } + per <- try_fetch( + .anchorVector( + cfg$data, + rep(ctx, length(traits)), + traits, + "region", + cfg$cisWindow + ), + error = function(cnd) NULL + ) + if (is.null(per) || length(per) == 0L) { + return(NULL) + } + range(per)[1L] +} + +# @noRd +.fmRowWindows <- function(data, contexts, traits, region, cisWindow, n) { + explicit <- .analysisWindowForRows(region, n) + if (!is.null(explicit)) { + return(explicit) + } + try_fetch( + .anchorVector(data, contexts, traits, "region", cisWindow), + error = function(cnd) NULL + ) +} + # Build a GwasFineMappingResult collection from per-row vectors. `blockIds` is # optional provenance keying the external LD block manifest; row identity comes # from (study, method) plus the element's own range either way. @@ -877,6 +981,7 @@ setGeneric("fineMappingPipeline", function(data, ...) { methods, entries, blockIds = NULL, + region = NULL, ldSketch = NULL, allowEmpty = FALSE ) { @@ -892,6 +997,7 @@ setGeneric("fineMappingPipeline", function(data, ...) { method = methods, blockId = blockIds, entry = entries, + region = region, ldSketch = ldSketch ) } @@ -937,7 +1043,41 @@ setGeneric("fineMappingPipeline", function(data, ...) { # the identity of the row it ends up on agree by construction rather than by # two formatters happening to match. # @noRd -.fmGwasBlockId <- function(gr) { +# The block key for GWAS entry `i`: the one the caller SUPPLIED when the +# GwasSumStats carries it, else derived from the entry's variant span. +# +# GwasSumStats() always materializes a `blockId` column (from the entry names, +# a blockId metadata column, else coordinates), but it lives in the +# COLLECTION's mcols -- .collectionEntry() hands back the element alone, so +# deriving from that element silently discarded a supplied key and replaced a +# true block boundary (chr22_49355984_50799822) with the narrower span of the +# variants that survived QC (chr22_49363017_50598205). +# A supplied key is one that carries COORDINATES. GwasSumStats() always +# materializes a blockId, but with no `ldBlocks` manifest and no explicit +# `blockId` it falls back to the bare seqname ("chr22") -- which is the +# "nothing was specified" case, and is less informative than the span. Only a +# coordinate-bearing key (from a manifest or an explicit vector) overrides. +# @noRd +.fmGwasBlockId <- function(data, i, gr) { + supplied <- try_fetch( + as.character(.tupleColumn(data, "blockId")[[i]]), + error = function(cnd) NULL + ) + if ( + length(supplied) == 1L && + !is.na(supplied) && + !is.null(.fmBlockIdRange(supplied)) + ) { + return(supplied) + } + .fmGwasRangeKey(gr) +} + +# The entry's own range key. This is what the resume cache matches on -- it +# compares a cached row's DERIVED range key, so it must stay derived even when +# a block manifest supplies a wider boundary for the stored provenance. +# @noRd +.fmGwasRangeKey <- function(gr) { .rtlOneRangeKey(range(gr)) } @@ -955,6 +1095,63 @@ setGeneric("fineMappingPipeline", function(data, ...) { } } +# The window an RSS fit covered. +# +# A GWAS block's BOUNDARY is the one positional fact not recoverable from the +# variants -- adjacent blocks leave gaps, so the realized span is narrower than +# the block (a chr22_49355984_50799822 block whose variants span +# 49363017-50598205). The boundary survives only in `blockId`, so parse it when +# it carries coordinates. Failing that the fit's own variants define the +# window: a QtlSumStats entry has no separately-requested window at all. +# @noRd +.fmBlockIdRange <- function(blockId) { + if (is.null(blockId) || length(blockId) != 1L || is.na(blockId)) { + return(NULL) + } + # .asGRegion() owns both spellings ("chr1:100-200" and "chr1_100_200") + # and keeps the seqname verbatim, so the window matches the variants' + # chromosome naming. It aborts on anything that is not coordinates -- + # the "this blockId is an opaque label" case. + try_fetch(.asGRegion(as.character(blockId)), error = function(cnd) NULL) +} + +# Per-row windows for a GWAS collection, from each row's blockId. NULL when no +# row's blockId carries coordinates, so the column is omitted and +# genomicRegion() falls back to the derived span. +# @noRd +.fmGwasRowWindows <- function(rows) { + if (length(rows) == 0L) { + return(NULL) + } + gs <- map(map_chr(rows, "blockId"), .fmBlockIdRange) + if (all(map_lgl(gs, is.null))) { + return(NULL) + } + # A row whose blockId holds no coordinates still needs a range; the + # chrUn sentinel marks it, matching .anchorVector()'s convention. + sentinel <- GenomicRanges::GRanges("chrUn", IRanges::IRanges(1L, 1L)) + exec(c, !!!map(gs, function(g) if (is.null(g)) sentinel else g)) +} + +# @noRd +.fmRssFitWindow <- function(entry, variantIds, blockId = NULL) { + fromBlock <- .fmBlockIdRange(blockId) + if (!is.null(fromBlock)) { + return(fromBlock) + } + # isS4 guard, not just try_fetch: base::range(NULL) WARNS rather than + # erroring, so an unguarded call leaks "no non-missing arguments" noise. + rg <- if (isS4(entry)) { + try_fetch(range(entry), error = function(cnd) NULL) + } else { + NULL + } + if (methods::is(rg, "GRanges") && length(rg) > 0L) { + return(rg[1L]) + } + try_fetch(.csVariantRegion(variantIds), error = function(cnd) NULL) +} + # Fit the still-to-run RSS tokens for one GWAS region and return one row-record # per fitted token. # @noRd @@ -965,7 +1162,8 @@ setGeneric("fineMappingPipeline", function(data, ...) { toRun, glue("GWAS (study='{st}', region='{blockId}')"), .fmAfByVar(gr, zn$variantIds), - cfg + cfg, + region = .fmRssFitWindow(gr, zn$variantIds, blockId) ) map(names(ents), .fmGwasRowFor, st = st, blockId = blockId, ents = ents) } @@ -994,13 +1192,17 @@ setGeneric("fineMappingPipeline", function(data, ...) { ldSketch = cfg$ldSketch, cutoffs = .panelCutoffs(cfg$panelFilterParam) ) - blockId <- .fmGwasBlockId(gr) + # Two different things that used to be one: the key the resume cache + # matches on is always the entry's derived range, while the key stored as + # provenance is the manifest block boundary when one was supplied. + rangeKey <- .fmGwasRangeKey(gr) + blockId <- .fmGwasBlockId(cfg$data, i, gr) lookups <- map( tokens, .fmGwasLookup, fineMappingResult = cfg$fineMappingResult, st = st, - blockId = blockId + blockId = rangeKey ) cachedRows <- map( keep(lookups, .fmHasCached), @@ -1066,7 +1268,7 @@ setGeneric("fineMappingPipeline", function(data, ...) { # .fmFitRssBlock with the per-run knobs supplied from `cfg`; callers pass only # the block-specific arguments. The RSS analog of .fmFitXBlockP. # @noRd -.fmFitRssBlockP <- function(z, zn, toRun, label, af, cfg) { +.fmFitRssBlockP <- function(z, zn, toRun, label, af, cfg, region = NULL) { ldMat <- .ldFromSketch( cfg$ldSketch, zn$variantIds, @@ -1085,7 +1287,8 @@ setGeneric("fineMappingPipeline", function(data, ...) { nVar = zn$nVar, credibleSetParam = cfg$credibleSetParam, fitRetention = cfg$fitRetention, - susieRssParam = cfg$susieRssParam + susieRssParam = cfg$susieRssParam, + region = region ) } @@ -1109,7 +1312,8 @@ setGeneric("fineMappingPipeline", function(data, ...) { toRun, glue("(study='{st}', context='{ctx}', trait='{tr}')"), .fmAfByVar(entry, zn$variantIds), - cfg + cfg, + region = .fmRssFitWindow(entry, zn$variantIds) ) map(names(ents), .fmQtlRowFor, st = st, ctx = ctx, tr = tr, ents = ents) } @@ -1254,6 +1458,13 @@ combineFineMappingResults <- function(..., ldSketch = NULL) { ), is.null ) + # No scaling step here: `residualizedGenotypes()` already returns columns + # scaled to unit variance, governed by + # `ResidualizationParam(scaleResiduals=)` with the dataset's own slot as + # the default. Adding a second one here would have been a parallel + # mechanism for something the accessor layer owns -- and would have + # covered only the callers that happen to come through this function, + # not `colocboostPipeline`, which calls the accessor directly. exec( residualizedGenotypes, !!!c( @@ -1267,6 +1478,7 @@ combineFineMappingResults <- function(..., ldSketch = NULL) { ) } + # Directional effect-allele (A1) frequency for the variants in a fitted # genotype block `X` (samples x variants, post-residualization and post # sample-intersection). Re-extracts the allele frequency from the dataset @@ -2267,7 +2479,8 @@ combineFineMappingResults <- function(..., ldSketch = NULL) { ctx, label, afVec, - cfg + cfg, + region = NULL ) { .fmFitXBlock( X, @@ -2284,7 +2497,8 @@ combineFineMappingResults <- function(..., ldSketch = NULL) { af = afVec, credibleSetParam = cfg$credibleSetParam, fitRetention = cfg$fitRetention, - seed = cfg$seed + seed = cfg$seed, + region = region ) } @@ -2332,7 +2546,17 @@ combineFineMappingResults <- function(..., ldSketch = NULL) { region = rg, cisWindow = cfg$cisWindow ) - .fmFitXBlockP(X, y, toRun, cfg$initializeWithSusieInf, ctx, tid, afVec, cfg) + .fmFitXBlockP( + X, + y, + toRun, + cfg$initializeWithSusieInf, + ctx, + tid, + afVec, + cfg, + region = .fmBlockWindow(rg, ctx, tid, cfg) + ) } # All univariate row-records for one (context, trait): cache hits, then (when @@ -2409,7 +2633,17 @@ combineFineMappingResults <- function(..., ldSketch = NULL) { ) # The PC path never chains susieInf: a PC is not a trait whose fit the # susieInf stage is defined for. - .fmFitXBlockP(Xb, pcY[common], "susie", FALSE, ctx, pcName, afVec, cfg) + .fmFitXBlockP( + Xb, + pcY[common], + "susie", + FALSE, + ctx, + pcName, + afVec, + cfg, + region = .fmBlockWindow(rg, ctx, traits, cfg) + ) } # Row-records for one PC pseudo-trait: a cache hit, else per-window fits merged @@ -2817,7 +3051,13 @@ combineFineMappingResults <- function(..., ldSketch = NULL) { # trait-anchored cis span) combined with the joint result. Errors only when # neither path produced anything. # @noRd -.fmQdsAssemble <- function(data, rows, jointResult) { +.fmQdsAssemble <- function( + data, + rows, + jointResult, + region = NULL, + cisWindow = NULL +) { rowContext <- map_chr(rows, "context") rowTrait <- map_chr(rows, "trait") perTupleResult <- if (length(rows) > 0L) { @@ -2831,6 +3071,14 @@ combineFineMappingResults <- function(..., ldSketch = NULL) { .anchorVector(data, rowContext, rowTrait, "traitPos"), error = function(cnd) NULL ), + region = .fmRowWindows( + data, + rowContext, + rowTrait, + region, + cisWindow, + length(rows) + ), ldSketch = NULL ) } else { @@ -2895,6 +3143,8 @@ combineFineMappingResults <- function(..., ldSketch = NULL) { .fmQdsAssemble( data, rows, + region = region, + cisWindow = commonCfg$cisWindow, .fmQdsAutoJoint( jointCfg, mvTokens = split$mvTokens, @@ -2938,7 +3188,8 @@ combineFineMappingResults <- function(..., ldSketch = NULL) { dataDrivenPriorWeightsCutoff = 1e-10, naAction = c("drop", "impute"), verbose = 1, - fitRetention = c("slim", "full") + fitRetention = c("slim", "full"), + dataType = NULL ) { fitRetention <- arg_match(fitRetention) naAction <- arg_match(naAction, c("drop", "impute")) @@ -2991,9 +3242,14 @@ combineFineMappingResults <- function(..., ldSketch = NULL) { jointCfg ) if (rt$done) { - return(rt$result) + return(.fmRecordProvenance( + rt$result, + dataType, + "individual", + .qtlScaleResiduals(data, residualizationArgs) + )) } - .fmQdsRunAndAssemble( + out <- .fmQdsRunAndAssemble( data, commonCfg, jointCfg, @@ -3008,6 +3264,12 @@ combineFineMappingResults <- function(..., ldSketch = NULL) { naAction = naAction, usePCA = usePCA ) + .fmRecordProvenance( + out, + dataType, + "individual", + .qtlScaleResiduals(data, residualizationArgs) + ) } #' @rdname fineMappingPipeline @@ -3024,6 +3286,37 @@ setMethod( # MultiStudyQtlDataset method # ============================================================================= +# Whether a MultiStudyQtlDataset's JOINT rows are on the standardized scale. +# The per-call override settles it when given; otherwise the embedded studies' +# own slots do, and a collection whose studies disagree gets NA rather than a +# guess. A sumstats-only collection is in standardized units regardless. +# @noRd +.fmMsScaleResiduals <- function(data, residualizationArgs) { + if (!is.null(residualizationArgs$scaleResiduals)) { + return(residualizationArgs$scaleResiduals) + } + if (identical(.fmMsDataSource(data), "sumstat")) { + return(TRUE) + } + perStudy <- unique(map_lgl(qtlDatasets(data), scaleResiduals)) + if (length(perStudy) == 1L) perStudy else NA +} + +# The `data_source` a MultiStudyQtlDataset run can state for its JOINT rows. A +# joint fit spans the studies the specification selected, so when the +# collection holds both individual-level and summary-statistics studies there +# is no single answer and the column is left NA (and so dropped on export) +# rather than guessed. +# @noRd +.fmMsDataSource <- function(data) { + hasIndividual <- length(qtlDatasets(data)) > 0L + hasSumstats <- length(sumStats(data)) > 0L + if (hasIndividual && hasSumstats) { + return(NA_character_) + } + if (hasSumstats) "sumstat" else "individual" +} + # Per-embedded-study fine-mapping worker for .multiStudyPipelineDriver: recurse # fineMappingPipeline on one QtlDataset with the individual-capable methods. # `cfg` bundles the parent call's forwarded arguments. @@ -3063,7 +3356,8 @@ setMethod( nPCs = cfg$nPCs, fitRetention = cfg$fitRetention, mrmashPrior = cfg$mrmashPrior, - dataDrivenPriorWeightsCutoff = cfg$dataDrivenPriorWeightsCutoff + dataDrivenPriorWeightsCutoff = cfg$dataDrivenPriorWeightsCutoff, + dataType = cfg$dataType ) } @@ -3094,7 +3388,8 @@ setMethod( susieRssParam = cfg$susieRssParam, fitRetention = cfg$fitRetention, mrmashPrior = cfg$mrmashPrior, - dataDrivenPriorWeightsCutoff = cfg$dataDrivenPriorWeightsCutoff + dataDrivenPriorWeightsCutoff = cfg$dataDrivenPriorWeightsCutoff, + dataType = cfg$dataType ) } @@ -3261,9 +3556,12 @@ setMethod( seed = NULL, naAction = c("drop", "impute"), verbose = 1, - fitRetention = c("slim", "full") + fitRetention = c("slim", "full"), + dataType = NULL ) { fitRetention <- arg_match(fitRetention) + # The joint rows are recorded here; each per-study recursion records its + # own, so both sides of the driver's rbind carry the same columns. inputs <- .fmMsResolveInputs(naAction, region, cisWindow, jointRegions) naAction <- inputs$naAction # One record of the settings the joint phase and the shared dispatcher @@ -3286,13 +3584,21 @@ setMethod( credibleSetParam$maxNumSingleEffectsGreedy, jointCfg ) + jointResult <- .fmRecordProvenance( + rt$result, + dataType, + .fmMsDataSource(data), + # A sumstats-only collection is in standardized units whatever the + # hatch says; otherwise the hatch decides. + .fmMsScaleResiduals(data, residualizationArgs) + ) if (rt$done) { - return(rt$result) + return(jointResult) } methods <- rt$methods .multiStudyPipelineDriver( data, - rt$result, + jointResult, .fmPerStudy, .fmSumStats, list( @@ -3318,7 +3624,8 @@ setMethod( nPCs = nPCs, fitRetention = fitRetention, mrmashPrior = mrmashPrior, - dataDrivenPriorWeightsCutoff = dataDrivenPriorWeightsCutoff + dataDrivenPriorWeightsCutoff = dataDrivenPriorWeightsCutoff, + dataType = dataType ), .rbindFineMappingResult, QtlFineMappingResult, @@ -3846,6 +4153,14 @@ NULL .anchorVector(data, rowContext, rowTrait, "traitPos"), error = function(cnd) NULL ), + region = .fmRowWindows( + data, + rowContext, + rowTrait, + NULL, + NULL, + length(rows) + ), ldSketch = ldSketch ) } else { @@ -3929,7 +4244,8 @@ NULL verbose, fitRetention, susieRssParam, - panelFilterParam + panelFilterParam, + dataType = NULL ) { .fmAssertQcd(data) cfgs <- .fmQssConfigs( @@ -3954,9 +4270,9 @@ NULL jointCfg ) if (rt$done) { - return(rt$result) + return(.fmRecordProvenance(rt$result, dataType, "sumstat", TRUE)) } - .fmQssAfterJoint( + out <- .fmQssAfterJoint( data, rt = rt, commonCfg = commonCfg, @@ -3966,6 +4282,7 @@ NULL initializeWithSusieInf = initializeWithSusieInf, susieRssParam = susieRssParam ) + .fmRecordProvenance(out, dataType, "sumstat", TRUE) } # Everything after the joint phase has had its say: select the rows the @@ -4068,7 +4385,8 @@ setMethod( susieRssParam = SusieRssParam(), panelFilterParam = PanelFilterParam(), crossValidationArgs = CrossValidationParam(), - residualizationArgs = ResidualizationParam() + residualizationArgs = ResidualizationParam(), + dataType = NULL ) { fitRetention <- arg_match(fitRetention) .cvRefuseOnSumstats( @@ -4090,7 +4408,8 @@ setMethod( verbose = verbose, fitRetention = fitRetention, susieRssParam = susieRssParam, - panelFilterParam = panelFilterParam + panelFilterParam = panelFilterParam, + dataType = dataType ) } ) @@ -4149,7 +4468,8 @@ setMethod( verbose, fitRetention, susieRssParam, - panelFilterParam + panelFilterParam, + dataType = NULL ) { .fmAssertQcd(data) norm <- .fmNormalizeMethods( @@ -4173,7 +4493,7 @@ setMethod( verbose = verbose, panelFilterParam = panelFilterParam ) - .fmGwasRunEntries( + out <- .fmGwasRunEntries( data, studyCol = studyCol, tokens = tokens, @@ -4183,6 +4503,7 @@ setMethod( methodArgs = methodArgs, susieRssParam = susieRssParam ) + .fmRecordProvenance(out, dataType, "sumstat", TRUE) } # Fine-map every row of the GWAS collection and assemble the result. @@ -4219,6 +4540,10 @@ setMethod( map_chr(rows, "method"), map(rows, "entry"), blockIds = map_chr(rows, "blockId"), + # The block BOUNDARY where blockId carries one, else the entry's span: + # the same window .fmRssFitWindow() gives the per-row grange columns, + # so the two cannot disagree. + region = .fmGwasRowWindows(rows), ldSketch = ldSketch, allowEmpty = (nSkipped > 0L || nrow(data) == 0L) ) @@ -4240,7 +4565,8 @@ setMethod( susieRssParam = SusieRssParam(), panelFilterParam = PanelFilterParam(), crossValidationArgs = CrossValidationParam(), - residualizationArgs = ResidualizationParam() + residualizationArgs = ResidualizationParam(), + dataType = NULL ) { fitRetention <- arg_match(fitRetention) .cvRefuseOnSumstats( @@ -4257,7 +4583,8 @@ setMethod( verbose = verbose, fitRetention = fitRetention, susieRssParam = susieRssParam, - panelFilterParam = panelFilterParam + panelFilterParam = panelFilterParam, + dataType = dataType ) } ) diff --git a/R/fineMappingRow.R b/R/fineMappingRow.R index bc2abdd9..e70c541b 100644 --- a/R/fineMappingRow.R +++ b/R/fineMappingRow.R @@ -204,7 +204,7 @@ fineMappingRow <- function(variantIds, susieFit, topLoci, cvResult = NULL) { !is.null(fit$fitted_func) && !is.null(fit$outing_grid) && (!is.null(fit$cred_band) || - (!is.null(fit$fitted_wc2) && !is.null(fit$alpha))) + (!is.null(fit$fitted_wc2) && !is.null(fit$alpha))) } # Chromosome of the fit's variants (matches topLoci's `chrom`, no "chr"). @@ -224,10 +224,120 @@ fineMappingRow <- function(variantIds, susieFit, topLoci, cvResult = NULL) { pos = numeric(), effect = numeric(), lower = numeric(), - upper = numeric() + upper = numeric(), + halfwidth = numeric() ) } +# ---- fsusieTraitEffects helpers ------------------------------------- + +# The fit's effect curve lives on a wavelet GRID; the traits it was fit over sit +# at arbitrary positions between grid points. Evaluating the curve AT each trait +# is linear interpolation with constant extrapolation past the ends (rule = 2), +# which is what a per-trait effect means for a functional fit. +# @noRd +.fsusieTraitEffectRow <- function(l, fit, grid, pos, traitNames, chrom) { + eff <- fit$fitted_func[[l]] + if (is.null(eff)) { + return(NULL) + } + tibble( + cs = str_c("fsusie_", l), + chrom = chrom, + jointTrait = traitNames, + traitPos = pos, + effect = stats::approx( + grid, + as.numeric(eff), + xout = pos, + rule = 2, + ties = "ordered" + )$y + ) +} + +# @noRd +.emptyTraitEffects <- function() { + tibble( + cs = character(), + chrom = character(), + jointTrait = character(), + traitPos = numeric(), + effect = numeric() + ) +} + +# One entry's per-trait effects. `traitNames` is index-aligned with the fit's +# `trait_positions` -- both come from the same joint group, which is why the +# names can be carried in the row's `jointTraits` column while only the +# positions ride on the fit. +# @noRd +.fsusieTraitEffectsFit <- function(fit, traitNames) { + if (!.isFsusieFit(fit) || is.null(fit$trait_positions)) { + return(.emptyTraitEffects()) + } + grid <- as.numeric(fit$outing_grid) + pos <- as.numeric(fit$trait_positions) + if (length(traitNames) != length(pos)) { + msg <- glue( + "fsusieTraitEffects: the fit has {length(pos)} trait position(s) ", + "but {length(traitNames)} trait name(s). They index the same ", + "joint group, so a mismatch means the stored fit and ", + "`jointTraits` disagree." + ) + abort(msg) + } + chrom <- .fsusieChrom(fit) + parts <- compact(map( + seq_along(fit$fitted_func), + .fsusieTraitEffectRow, + fit = fit, + grid = grid, + pos = pos, + traitNames = traitNames, + chrom = chrom + )) + if (length(parts) == 0L) { + return(.emptyTraitEffects()) + } + bind_rows(parts) +} + +# The joint members of row `i`, index-aligned with the fit's trait_positions. +# A fit over a single trait has no `jointTraits` entry, so fall back to the +# row's own trait id. +# @noRd +.fmrJointTraitNames <- function(x, i) { + cols <- .tupleColumnNames(x) + jt <- if (is_in("jointTraits", cols)) { + as.character(x$jointTraits[[i]]) + } else { + NA_character_ + } + if (length(jt) != 1L || is.na(jt) || !nzchar(jt)) { + return( + if (is_in("trait", cols)) { + as.character(x$trait[[i]]) + } else { + character(0) + } + ) + } + str_split_1(jt, fixed(";")) +} + +# @noRd +.fsusieEntryTraitEffects <- function(i, x, meta) { + v <- .fsusieTraitEffectsFit( + susieFit(.fmrRowParts(x, i)), + .fmrJointTraitNames(x, i) + ) + if (nrow(v) == 0L) { + return(NULL) + } + bind_cols(slice(meta, rep(i, nrow(v))), v) +} + # @noRd .fsusieCredibleBandFit <- function(fit) { if (!.isFsusieFit(fit)) { @@ -328,7 +438,9 @@ fineMappingRow <- function(variantIds, susieFit, topLoci, cvResult = NULL) { # ---- map/apply helpers (lambda-free callbacks) --------------------------- # One credible band's variant-level data.frame (effect + lower/upper), or NULL -# when band `l` has no fitted band/effect. +# when band `l` has no fitted band/effect. `halfwidth` is the band's half-width +# at each grid point -- the symmetric +/- quantity publication tables carry, +# which every consumer otherwise re-derives as (upper - lower) / 2. # @noRd .fsusieBandRow <- function(l, fit, chrom, grid) { band <- fit$cred_band[[l]] @@ -336,13 +448,16 @@ fineMappingRow <- function(variantIds, susieFit, topLoci, cvResult = NULL) { if (is.null(band) || is.null(eff)) { return(NULL) } + lower <- as.numeric(band[2, ]) + upper <- as.numeric(band[1, ]) tibble( cs = str_c("fsusie_", l), chrom = chrom, pos = grid, effect = as.numeric(eff), - lower = as.numeric(band[2, ]), - upper = as.numeric(band[1, ]) + lower = lower, + upper = upper, + halfwidth = (upper - lower) / 2 ) } @@ -636,8 +751,8 @@ fineMappingRow <- function(variantIds, susieFit, topLoci, cvResult = NULL) { cs_log_bf = .csEffectLogBf(ctx$fit, Lidx), # cs_pip: total posterior inclusion mass captured by the CS. cs_pip = suppressWarnings(sum(as.numeric(m$pip), na.rm = TRUE)), - # cs_mean_effect: mean posterior conditional effect over the CS (NA when - # the entry has no conditional_effect column, e.g. univariate susie). + # cs_mean_effect: mean effect over the CS members CONDITIONAL on the + # variant being causal, i.e. the posterior mean over the PIP. cs_mean_effect = .csMeanEffect(m), lead_variant = as.character(m$variant_id[lead]), lead_pip = as.numeric(m$pip[lead]) @@ -683,21 +798,41 @@ fineMappingRow <- function(variantIds, susieFit, topLoci, cvResult = NULL) { if (length(v) == 0L) NA_real_ else max(v) } -# cs_mean_effect: mean conditional effect over the CS (NA when absent). +# cs_mean_effect: the mean over the CS members of the effect conditional on +# the variant being causal. +# +# Derived from `posterior_mean / pip` rather than read from a stored +# `conditional_effect` column. That column was `coef[, condition] / pip`, +# which IS this quotient now that `.btlPosterior()` puts the posterior mean on +# the same scale `coef()` uses -- verified exactly against a real mvsusie fit. +# Before that change the two differed by the per-variant standardization +# factor, so the column was carrying information the table lacked and could +# not be dropped. +# +# A knock-on improvement: this is now populated for univariate susie too, +# where the stored column only ever existed for multi-condition fits. # @noRd .csMeanEffect <- function(m) { - if (!is_in("conditional_effect", names(m))) { + effect <- suppressWarnings(as.numeric(.csColumn(m, "posterior_mean"))) + pip <- suppressWarnings(as.numeric(.csColumn(m, "pip"))) + if (length(effect) == 0L || length(pip) != length(effect)) { return(NA_real_) } # suppressWarnings covers only the coercion; the empty case is handled - # explicitly so it returns NA_real_ rather than NaN. + # explicitly so it returns NA_real_ rather than NaN. A zero PIP has no + # conditional effect to speak of and drops out with the non-finites. v <- keep( - suppressWarnings(as.numeric(m$conditional_effect)), + if_else(!is.na(pip) & pip > 0, effect / pip, NA_real_), is.finite ) if (length(v) == 0L) NA_real_ else mean(v) } +# @noRd +.csColumn <- function(m, nm) { + if (is_in(nm, names(m))) m[[nm]] else numeric(0) +} + # Apply the pip `signalCutoff` + optional purity filter, then project to the # posterior view. An empty topLoci passes through unchanged. @@ -1270,21 +1405,27 @@ fineMappingRow <- function(variantIds, susieFit, topLoci, cvResult = NULL) { # given type when the column is absent. Lets accessor projectors tolerate # skeletal entries that lack optional columns. # @noRd -.tlCol <- function(tl, name, type = c("character", "integer", "numeric")) { +.tlCol <- function( + tl, + name, + type = c("character", "integer", "numeric", "logical") +) { type <- arg_match(type) if (is_in(name, colnames(tl))) { return(switch( type, character = as.character(tl[[name]]), integer = as.integer(tl[[name]]), - numeric = as.numeric(tl[[name]]) + numeric = as.numeric(tl[[name]]), + logical = as.logical(tl[[name]]) )) } switch( type, character = rep(NA_character_, nrow(tl)), integer = rep(NA_integer_, nrow(tl)), - numeric = rep(NA_real_, nrow(tl)) + numeric = rep(NA_real_, nrow(tl)), + logical = rep(NA, nrow(tl)) ) } @@ -1307,8 +1448,11 @@ fineMappingRow <- function(variantIds, susieFit, topLoci, cvResult = NULL) { A2 = .tlCol(tl, "A2", "character"), N = .tlCol(tl, "N", "numeric"), af = .tlCol(tl, "af", "numeric"), - beta = .tlCol(tl, "posterior_mean", "numeric"), - se = .tlCol(tl, "posterior_sd", "numeric"), + # NOT `beta`/`se`: marginalEffects() publishes the MARGINAL estimate + # under those names, so using them here for the posterior mean made + # the two views silently joinable into a mix of the two quantities. + posterior_effect = .tlCol(tl, "posterior_mean", "numeric"), + posterior_se = .tlCol(tl, "posterior_sd", "numeric"), pip = .tlCol(tl, "pip", "numeric"), logBF = .tlCol(tl, "logBF", "numeric") ) @@ -1332,8 +1476,8 @@ fineMappingRow <- function(variantIds, susieFit, topLoci, cvResult = NULL) { A2 = character(0), N = numeric(0), af = numeric(0), - beta = numeric(0), - se = numeric(0), + posterior_effect = numeric(0), + posterior_se = numeric(0), pip = numeric(0), logBF = numeric(0) ) @@ -1342,9 +1486,11 @@ fineMappingRow <- function(variantIds, susieFit, topLoci, cvResult = NULL) { # Optional pass-through columns for the posterior view, in canonical order: the # dynamic CS columns (cs_ membership / cs__purity), the per-CS variant- # level fullFit columns (within_cs_pip[_] / cs_logbf_ / -# cs_effect[_var]_), the per-condition posterior quantities -# (conditional_effect, lfsr), and pipeline-added annotations -- whichever are -# present. +# cs_effect[_var]_), the per-condition `lfsr`, and pipeline-added +# annotations -- whichever are present. +# +# `conditional_effect` was one of these and is gone: it was +# `posterior_mean / pip` once the posterior mean moved onto `coef()`'s scale. # @noRd .projectPosteriorExtraCols <- function(tl) { csCols <- colnames(tl)[str_detect(colnames(tl), "^cs_[0-9.]+(_purity)?$")] @@ -1356,7 +1502,6 @@ fineMappingRow <- function(variantIds, susieFit, topLoci, cvResult = NULL) { c( csCols, fullFitCols, - "conditional_effect", "lfsr", "method", "gene", @@ -1400,6 +1545,7 @@ fineMappingRow <- function(variantIds, susieFit, topLoci, cvResult = NULL) { beta = .tlCol(tl, "marginal_beta", "numeric"), se = .tlCol(tl, "marginal_se", "numeric"), z = .tlCol(tl, "marginal_z", "numeric"), + z_supplied = .tlCol(tl, "marginal_z_supplied", "logical"), p = .tlCol(tl, "marginal_p", "numeric") ) } diff --git a/R/fineMappingWrappers.R b/R/fineMappingWrappers.R index 3912b1b4..e0aaf227 100644 --- a/R/fineMappingWrappers.R +++ b/R/fineMappingWrappers.R @@ -845,13 +845,40 @@ selectEffects <- function(fit, priorEffTol = 1e-9) { } .asLbfMatrix <- function(fit) { - if (!is.null(fit$lbf_variable)) { - return(.asEffectMatrix(fit$lbf_variable)) + # Every shape a fit's per-effect LBF is known to arrive in, in one place. + # `lbf()` and colocPipeline used to keep separate lists of these and had + # drifted apart -- coloc was blind to a bare `fit$lBF`, which is exactly + # what an fSuSiE fit carries, so fSuSiE colocalization silently skipped. + nested <- if (is.list(fit) && length(fit) >= 1L) fit[[1L]] else NULL + # compact() drops NULL and zero-length entries, so the first survivor is + # the first shape the fit actually carries. + candidates <- compact(list( + .fitField(fit, "lbf_variable"), + .fitField(fit, "lBF"), + .fitField(fit, "fsusie_result", "lBF"), + .fitField(nested, "fsusie_result", "lBF") + )) + if (length(candidates) == 0L) { + return(NULL) } - if (!is.null(fit$lBF)) { - return(.asEffectMatrix(fit$lBF)) + .asEffectMatrix(candidates[[1L]]) +} + +# Read a nested fit field by path, tolerating every way the path can fail to +# exist. Neither `$` nor `[[` is safe here on its own: `$` errors on an atomic +# vector (a fit's first element is often a matrix) and `[[` errors on a name a +# list does not have. +# @noRd +.fitField <- function(x, ...) { + reduce(c(...), .fitFieldStep, .init = x) +} + +# @noRd +.fitFieldStep <- function(x, nm) { + if (!is.list(x) || !is_in(nm, names(x))) { + return(NULL) } - NULL + x[[nm]] } #' Compute Credible-Set Tables From a Fine-Mapping Fit @@ -1107,12 +1134,13 @@ setMethod( # Per-effect (per credible set) variant-level columns from the susie fit. Always # returns `within_cs_pip` (the variant's alpha in the single effect of its # assigned primary-coverage CS; NA for non-CS variants -- alpha is a -# probability, -# no scaling). With fullFit = TRUE it also widens the per-effect matrices, one -# column set per CS: `within_cs_pip_` (alpha) and -- unless -# perCsColumns = "full" -- `cs_logbf_` (lbf_variable), -# `cs_effect_` (mu / -# X_column_scale_factors) and `cs_effect_var_` ((mu2 - mu^2) / scale^2). +# probability, no scaling). +# +# `perCsColumns` then decides how much of the per-effect matrices is widened, +# one column set per CS: "none" stops at the scalar above, "alpha" adds +# `within_cs_pip_` (alpha), and "full" adds `cs_logbf_` +# (lbf_variable), `cs_effect_` (mu / X_column_scale_factors) and +# `cs_effect_var_` ((mu2 - mu^2) / scale^2) on top of it. # includeAllCs = TRUE widens EVERY effect (label `L`), else only effects that # produced a passing CS (label `cs`, matching the cs_ columns). # alpha/mu/mu2/lbf are L x p per-effect matrices (mu/mu2 already @@ -1550,7 +1578,7 @@ buildTopLoci <- function( cov <- .btlCoverage(csTables) fc <- .btlFitConstants(dataY, otherQuantities, region) post <- .btlPosterior(fit, conditionIdx, nV) - marg <- .btlMarginal(sumstats, nV) + marg <- .btlMarginal(sumstats, nV, af, n) cs <- .btlCsMembership(cov, csTables, nV, variantNames) fullFitBlock <- .btlFullFitBlock( fit, @@ -1632,11 +1660,7 @@ buildTopLoci <- function( withCond <- if (is.null(conditionIdx)) { out } else { - mutate( - out, - conditional_effect = cond$condEffect, - lfsr = cond$condLfsr - ) + mutate(out, lfsr = cond$condLfsr) } if (is.null(signalCutoff) || signalCutoff <= 0) { return(withCond) @@ -1718,19 +1742,65 @@ buildTopLoci <- function( ) } + +# The per-variant standardization factors susie divides its coefficients by, +# or 1 where the fit records none -- an RSS fit has no X to scale, and a fit +# run with `standardize = FALSE` records all-1s. Read with `[[`: `$` on a list +# falls back to prefix matching. +# @noRd +.btlScaleFactors <- function(fit, nV) { + sf <- fit[["X_column_scale_factors"]] + if (is.null(sf) || !is.numeric(sf) || length(sf) != nV) { + return(rep(1, nV)) + } + # A zero factor would divide to Inf; it can only come from a constant + # column, which carries no effect anyway. + replace(as.numeric(sf), !is.finite(sf) | sf == 0, 1) +} + +# susieInf / susieAsh carry an infinitesimal effect alongside the sparse one, +# and `coef.susie()` adds it in. Dropping it would silently change what the +# posterior effect means for those two methods. +# @noRd +.btlInfinitesimal <- function(fit, nV) { + theta <- fit[["theta"]] + if (is.null(theta) || !is.numeric(theta) || length(theta) != nV) { + return(rep(0, nV)) + } + replace(as.numeric(theta), !is.finite(theta), 0) +} + # Per-variant posterior mean/SD (from alpha, mu, mu2) and the strongest # single-effect log Bayes factor. A conditionIdx slices 3-D mvsusie mu/mu2. +# +# The mean and SD are returned on the scale of the X the fit was GIVEN, which +# means dividing the internal `colSums(alpha * mu)` by +# `X_column_scale_factors` -- exactly what `susieR::coef.susie()` does, and +# what `susieWeights()`, `.ctwasRenormalizeSusieWeights()` and the whole TWAS +# layer already return. Without the division this one column was the only +# quantity in the package on the standardized scale: the same fit yielded two +# different "posterior effects" depending on whether you read `topLoci()` or +# `susieWeights()`, and `weight_standardized` could not describe both. .btlPosterior <- function(fit, conditionIdx, nV) { alpha <- as.matrix(fit$alpha) mu <- .fmSliceCond(fit$mu, conditionIdx) mu2 <- .fmSliceCond(fit$mu2, conditionIdx) - postMean <- if (!is.null(mu) && all(dim(alpha) == dim(mu))) { - colSums(alpha * mu) + scaleFactors <- .btlScaleFactors(fit, nV) + meanStd <- if (!is.null(mu) && all(dim(alpha) == dim(mu))) { + colSums(alpha * mu) + .btlInfinitesimal(fit, nV) } else { rep(NA_real_, nV) } + postMean <- meanStd / scaleFactors postSd <- if (!is.null(mu2) && all(dim(alpha) == dim(mu2))) { - sqrt(pmax(colSums(alpha * mu2) - postMean^2, 0)) + # From the STANDARDIZED mean, then rescaled: an SD rescales the same + # way its mean does, but the mu2 identity only holds in the units mu2 + # is stored in. + sdStd <- sqrt(pmax( + colSums(alpha * mu2) - colSums(alpha * mu)^2, + 0 + )) + sdStd / scaleFactors } else { rep(NA_real_, nV) } @@ -1783,7 +1853,7 @@ buildTopLoci <- function( # Marginal univariate effects (beta, se, z, p) from the sumstats list; z and p # are derived when not supplied directly. -.btlMarginal <- function(sumstats, nV) { +.btlMarginal <- function(sumstats, nV, af = NULL, n = NULL) { beta <- if (!is.null(sumstats$betahat)) { as.numeric(sumstats$betahat) } else { @@ -1794,7 +1864,12 @@ buildTopLoci <- function( } else { rep(NA_real_, nV) } - z <- if (!is.null(sumstats$z)) { + # Whether the SOURCE supplied z matters downstream, not just its value: + # the GWAS-VCF `EZ` field is defined as the z "provided if it was used to + # derive the ES and SE fields", so a z we computed from a recorded + # beta/se must not be published as one that was provided. + zSupplied <- !is.null(sumstats$z) + z <- if (zSupplied) { as.numeric(sumstats$z) } else if (any(!is.na(beta)) && any(!is.na(se))) { beta / se @@ -1808,7 +1883,38 @@ buildTopLoci <- function( } else { rep(NA_real_, nV) } - list(beta = beta, se = se, z = z, p = p) + filled <- .btlFillFromZ(beta, se, z, af, n, nV) + list( + beta = filled$beta, + se = filled$se, + z = z, + p = p, + zSupplied = zSupplied + ) +} + +# A z-only source still has an effect size. .zToBetaSe() inverts the model +# (se = 1/sqrt(2*p*q*(N + z^2)), beta = z*se) given allele frequency and N, so +# leaving beta/se NA made every downstream effect-size consumer silently lose a +# GWAS that reported only z -- and such sources are routine. +# +# Only when NOTHING was recorded: filling the gaps in a partly-recorded column +# would mix measured and model-derived values under one name. `af` rather than +# maf is fine, since the 2*p*q variance term is symmetric in p <-> q. +# @noRd +.btlFillFromZ <- function(beta, se, z, af, n, nV) { + asIs <- list(beta = beta, se = se) + if (any(!is.na(beta)) || all(is.na(z))) { + return(asIs) + } + if (is.null(af) || is.null(n) || anyNA(af) || anyNA(n)) { + return(asIs) + } + if (length(af) != nV || !is_in(length(n), c(1L, nV))) { + return(asIs) + } + bs <- .zToBetaSe(z, af, n) + list(beta = as.numeric(bs$beta), se = as.numeric(bs$se)) } # CS membership index + purity for every coverage present (high -> low), with @@ -1857,29 +1963,6 @@ buildTopLoci <- function( ) } -# Per-condition conditional effect (coef / pip) for a multi-condition fit. -# Accepts a trimmed fit's `$coef` or a raw mvsusie fit via coef.mvsusie(). -.btlCondEffect <- function(fit, method, conditionIdx, nV) { - coefMat <- if (!is.null(fit$coef)) { - as.matrix(fit$coef) - } else if ( - identical(method, "mvsusie") && - requireNamespace("mvsusieR", quietly = TRUE) - ) { - cm <- try_fetch(mvsusieR::coef.mvsusie(fit), error = function(cnd) NULL) - if (!is.null(cm)) as.matrix(cm)[-1L, , drop = FALSE] else NULL - } else { - NULL - } - if ( - is.null(coefMat) || nrow(coefMat) != nV || ncol(coefMat) < conditionIdx - ) { - return(rep(NA_real_, nV)) - } - pipVec <- as.numeric(fit$pip) - if_else(pipVec > 0, coefMat[, conditionIdx] / pipVec, NA_real_) -} - # Per-condition conditional lfsr: map each variant to its effect (L) via the # primary (highest-coverage) credible set, then read that effect's lfsr. .btlCondLfsr <- function( @@ -1890,7 +1973,9 @@ buildTopLoci <- function( csTables, nV ) { - clf <- if (!is.null(fit$clfsr)) fit$clfsr else fit$conditional_lfsr + # `%||%` not `if (!is.null(...))`: three sources in priority order, the + # last of which COMPUTES the quantity a univariate fit never reports. + clf <- fit$clfsr %||% fit$conditional_lfsr %||% .btlUnivariateClfsr(fit) condLfsr <- rep(NA_real_, nV) if ( is.null(clf) || @@ -1938,6 +2023,38 @@ buildTopLoci <- function( # One credible set's conditional-lfsr values, or NULL when its effect is out # of range or it covers no in-range variant. # @noRd +# The conditional lfsr of a univariate susie fit, which reports none. +# +# It has everything needed to compute one: the single-effect posterior for +# variant j in effect l is N(mu[l, j], mu2[l, j] - mu[l, j]^2), so the +# probability of a SIGN error conditional on that variant carrying the effect +# is pnorm(-|mu| / sd). That is the same quantity mvSuSiE reports in `clfsr`, +# for the one condition a univariate fit has -- so it comes back in the same +# [effect, variant, condition] shape and flows through the identical credible +# set assignment, rather than needing a parallel code path. +# +# NULL (hence NA downstream, as before) when the fit was trimmed of mu / mu2. +# @noRd +.btlUnivariateClfsr <- function(fit) { + mu <- fit$mu + mu2 <- fit$mu2 + if (is.null(mu) || is.null(mu2)) { + return(NULL) + } + mu <- as.matrix(mu) + mu2 <- as.matrix(mu2) + if (!identical(dim(mu), dim(mu2)) || length(dim(mu)) != 2L) { + return(NULL) + } + # Numerical slop can make mu2 - mu^2 very slightly negative. + sd <- sqrt(pmax(mu2 - mu^2, 0)) + lfsr <- stats::pnorm(-abs(mu) / sd) + # A zero-variance single effect is degenerate rather than sign-certain, so + # it reports nothing instead of a confident 0. + lfsr[sd <= 0 | !is.finite(lfsr)] <- NA_real_ + array(lfsr, dim = c(dim(mu), 1L)) +} + .btlCondLfsrForSet <- function( csPos, setsPrim, @@ -1969,13 +2086,9 @@ buildTopLoci <- function( nV ) { if (is.null(conditionIdx)) { - return(list( - condEffect = rep(NA_real_, nV), - condLfsr = rep(NA_real_, nV) - )) + return(list(condLfsr = rep(NA_real_, nV))) } list( - condEffect = .btlCondEffect(fit, method, conditionIdx, nV), condLfsr = .btlCondLfsr( fit, conditionIdx, @@ -2078,6 +2191,7 @@ buildTopLoci <- function( marginal_beta = unname(marg$beta), marginal_se = unname(marg$se), marginal_z = unname(marg$z), + marginal_z_supplied = isTRUE(marg$zSupplied), marginal_p = unname(marg$p), pip = as.numeric(fit$pip), posterior_mean = unname(post$postMean), @@ -2116,6 +2230,7 @@ buildTopLoci <- function( marginal_beta = numeric(), marginal_se = numeric(), marginal_z = numeric(), + marginal_z_supplied = logical(), marginal_p = numeric(), pip = numeric(), posterior_mean = numeric(), @@ -2137,6 +2252,18 @@ buildTopLoci <- function( } .parseGrange <- function(regionStr) { + # Every in-package source of a window is a GRanges (the pipeline's + # `region`, an X block's own range); only the documented user-facing form + # is a string. Normalize here so no caller has to. + if (methods::is(regionStr, "GRanges")) { + if (length(regionStr) == 0L) { + return(c(start = NA_integer_, end = NA_integer_)) + } + return(c( + start = as.integer(GenomicRanges::start(regionStr)[[1L]]), + end = as.integer(GenomicRanges::end(regionStr)[[1L]]) + )) + } if ( is.null(regionStr) || length(regionStr) == 0L || @@ -4314,7 +4441,8 @@ extractTopPipInfo <- function(fmRow, sumstats) { ctx, tid, af, - fitRetention + fitRetention, + region = NULL ) { fit <- .fmXFitOne( tk, @@ -4338,7 +4466,8 @@ extractTopPipInfo <- function(fmRow, sumstats) { y = y, credibleSetParam = credibleSetParam, af = af, - fitRetention = fitRetention + fitRetention = fitRetention, + region = region ) } @@ -4357,7 +4486,8 @@ extractTopPipInfo <- function(fmRow, sumstats) { ctx, tid, af, - fitRetention + fitRetention, + region = NULL ) { chainLocal <- .fmResolveSusieChain(toRun, initializeWithSusieInf) infFit <- .fmXInfFit( @@ -4384,7 +4514,8 @@ extractTopPipInfo <- function(fmRow, sumstats) { ctx = ctx, tid = tid, af = af, - fitRetention = fitRetention + fitRetention = fitRetention, + region = region ), toRun )) @@ -4405,7 +4536,8 @@ extractTopPipInfo <- function(fmRow, sumstats) { samplePartition = NULL, af = NULL, fitRetention = "slim", - seed = NULL + seed = NULL, + region = NULL ) { out <- .fmXFitAll( toRun, @@ -4418,7 +4550,8 @@ extractTopPipInfo <- function(fmRow, sumstats) { ctx = ctx, tid = tid, af = af, - fitRetention = fitRetention + fitRetention = fitRetention, + region = region ) .fmXCrossValidate( out, @@ -4516,7 +4649,8 @@ extractTopPipInfo <- function(fmRow, sumstats) { y, credibleSetParam, af, - fitRetention + fitRetention, + region = NULL ) { .fmPostprocessOne( fit = fit, @@ -4524,6 +4658,7 @@ extractTopPipInfo <- function(fmRow, sumstats) { dataX = X, dataY = y, af = af, + region = region, csInput = "X", credibleSetParam = credibleSetParam, fitRetention = fitRetention @@ -4596,7 +4731,8 @@ extractTopPipInfo <- function(fmRow, sumstats) { af = NULL, nVar = NULL, fitRetention = "slim", - susieRssParam + susieRssParam, + region = NULL ) { chainLocal <- .fmResolveSusieChain(toRun, initializeWithSusieInf) infFit <- .fmRssInfFit( @@ -4626,7 +4762,8 @@ extractTopPipInfo <- function(fmRow, sumstats) { label = label, af = af, nVar = nVar, - fitRetention = fitRetention + fitRetention = fitRetention, + region = region ), toRun )) @@ -4649,7 +4786,8 @@ extractTopPipInfo <- function(fmRow, sumstats) { label, af, nVar, - fitRetention + fitRetention, + region = NULL ) { f <- .fmRssFitOne( tk, @@ -4674,7 +4812,8 @@ extractTopPipInfo <- function(fmRow, sumstats) { credibleSetParam = credibleSetParam, af = af, nVar = nVar, - fitRetention = fitRetention + fitRetention = fitRetention, + region = region ) if (f$isStd && isTRUE(susieRssParam$serFallback)) { return(.fmRssRecordFallback(entry, f, susieRssParam$keepFullFit)) @@ -4839,7 +4978,8 @@ extractTopPipInfo <- function(fmRow, sumstats) { credibleSetParam, af, nVar, - fitRetention + fitRetention, + region = NULL ) { .fmPostprocessOne( fit = fit, @@ -4851,6 +4991,7 @@ extractTopPipInfo <- function(fmRow, sumstats) { # path that has no per-variant N -> buildTopLoci leaves N as NA, # never 1. This is NOT `n` (the scalar median the RSS fit consumes). n = nVar, + region = region, csInput = "Xcorr", credibleSetParam = credibleSetParam, fitRetention = fitRetention diff --git a/R/gwasSumStats.R b/R/gwasSumStats.R index e9a3694f..01377306 100644 --- a/R/gwasSumStats.R +++ b/R/gwasSumStats.R @@ -507,6 +507,7 @@ setMethod("varY", "GwasSumStats", function(x, studyName = NULL) { if (is.na(val)) NULL else val }) + # ============================================================================= # Coercion / converters # ============================================================================= diff --git a/R/jointEngine.R b/R/jointEngine.R index 2a589a22..fce1a4ea 100644 --- a/R/jointEngine.R +++ b/R/jointEngine.R @@ -252,6 +252,56 @@ NULL # ---- fitters (fitJointGroup) ------------------------------------------------ +# The window a joint group was analysed over. An explicit region wins; failing +# that the group's traits define it, as the union span of their own cis +# windows -- one joint fit over several traits covers all of them. +# @noRd +.jointGroupWindow <- function(data, cond, args) { + explicit <- .asAnalysisWindow(args$region) + if (!is.null(explicit)) { + return(explicit) + } + per <- try_fetch( + .anchorVector( + data, + as.character(cond$context), + as.character(cond$trait), + "region", + args$cisWindow + ), + error = function(cnd) NULL + ) + if (is.null(per) || length(per) == 0L) { + return(NULL) + } + range(per)[1L] +} + + +# Per-variant allele frequency for a joint group's design matrix, or NULL. +# +# The univariate/RSS paths pass `af` into postprocessFinemappingFits(); the +# joint ones did not, so every mvsusie / fsusie row exported af = NA (and +# therefore maf = NA) even though the genotypes were right there. The window is +# taken from the fit's OWN variants rather than a trait cis-window: a joint +# group spans several traits, so no single trait's window describes X. +# @noRd +.jointAfForGroup <- function(data, g) { + if (!is(g, "IndividualJointGroup")) { + return(NULL) + } + X <- .jgX(g) + if (is.null(X) || ncol(X) == 0L) { + return(NULL) + } + rg <- try_fetch(.csVariantRegion(colnames(X)), error = function(cnd) NULL) + if (is.null(rg)) { + return(NULL) + } + .fmAfForX(data, X, region = rg) +} + + # (individual, fine-mapping) -> mvSuSiE joint fit + honest per-fold CV prior. setMethod( "fitJointGroup", @@ -323,7 +373,9 @@ setMethod( fit = fit, cvM = cvM, Xc = Xc, - cfg = cfg + cfg = cfg, + af = args$af, + region = args$region ) # ONE entry for the whole joint fit, unlike mvsusie's one-per-condition. # mvsusie's posteriors carry a conditions axis, so .fmSliceCond() gives @@ -374,13 +426,17 @@ setMethod( Xc, cfg, credibleSetParam, - fitRetention + fitRetention, + af = NULL, + region = NULL ) { bare <- .fmPostprocessOne( fit = fit, method = "fsusie", dataX = Xc, dataY = NULL, + af = af, + region = region, conditionIdx = r, csInput = "fsusie", credibleSetParam = cfg$credibleSetParam, @@ -425,7 +481,9 @@ setMethod( Xc = Xc, keep = keep, survivors = survivors, - cfg = cfg + cfg = cfg, + af = args$af, + region = args$region ) } @@ -551,7 +609,9 @@ setMethod( survivors, cfg, credibleSetParam, - fitRetention + fitRetention, + af = NULL, + region = NULL ) { if (!keep[i]) { return(NULL) @@ -562,6 +622,8 @@ setMethod( method = "mvsusie", dataX = Xc, dataY = NULL, + af = af, + region = region, conditionIdx = r, csInput = "X", credibleSetParam = cfg$credibleSetParam, @@ -605,7 +667,8 @@ setMethod( .jointRssEntry, fit = fit, ldMat = ldMat, - cfg = cfg + cfg = cfg, + region = args$region ) } ) @@ -656,12 +719,13 @@ setMethod( # `credibleSet` / `fitRetention` come off `cfg`, which is what the caller # supplies; they were also declared as formals with no defaults, so nothing # could ever have passed them without erroring. `group` was unused too. -.jointRssEntry <- function(r, fit, ldMat, cfg) { +.jointRssEntry <- function(r, fit, ldMat, cfg, region = NULL) { .fmPostprocessOne( fit = fit, method = "mvsusie", dataX = ldMat, dataY = NULL, + region = region, conditionIdx = r, csInput = "Xcorr", credibleSetParam = cfg$credibleSetParam, @@ -1917,9 +1981,20 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { cond = cond, js = .jointAxisMembers(cond, "study"), jc = .jointAxisMembers(cond, "context"), - jt = .jointAxisMembers(cond, "trait") + jt = .jointAxisMembers(cond, "trait"), + # The window this group was analysed over: the caller's region when + # one was named, else the group's own cis window (the union span of + # its traits +/- cisWindow). Recorded because the fit only ever sees + # whichever variants survived QC; read back by genomicRegion(). + region = .jointGroupWindow(data, cond, args) ) fitArgs <- .twasGroupArgs(g, pipeline, args) + # Provenance the fitters cannot recover for themselves: `data` lives here + # but not inside fitJointGroup(), and the requested window is only ever + # known to the caller. Both are read back as args$af / args$region where + # the entries are post-processed. + fitArgs$af <- .jointAfForGroup(data, g) + fitArgs$region <- args$region perTokenEntries <- compact(set_names( map( tokens, @@ -2576,7 +2651,8 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { jointStudies = grp$js, jointContexts = grp$jc, jointTraits = grp$jt, - traitPos = tpos + traitPos = tpos, + region = grp$region ) } diff --git a/R/jointSpecification.R b/R/jointSpecification.R index 1f2ecb6c..3030c80e 100644 --- a/R/jointSpecification.R +++ b/R/jointSpecification.R @@ -2405,7 +2405,12 @@ validateMethodsVsJointSpec <- function(methodsParsed, jointSpecParsed) { dataType = weightsDataType, crossValidationArgs = .cvResolve(CrossValidationParam()), fitFullData = TRUE, - standardized = FALSE, + # What the accessor will actually do on this path. No + # residualization bundle reaches the TWAS joint dispatcher, so + # `residualizedGenotypes()` falls back to the dataset's own + # `scaleResiduals` slot -- which is therefore the honest answer + # here, and `NULL` says that explicitly rather than by omission. + standardized = .qtlScaleResiduals(data, NULL), seed = seed, ldSketch = NULL ) diff --git a/R/manifestLoaders.R b/R/manifestLoaders.R index 9ce30d67..f3f909f7 100644 --- a/R/manifestLoaders.R +++ b/R/manifestLoaders.R @@ -484,11 +484,21 @@ NULL )) } if (is.character(region) && length(region) == 1L) { + # "chr1_100_200" is the LD-block / region-id spelling of + # "chr1:100-200". Normalized here so one matcher below handles both + # and the seqname survives verbatim (parseRegion() canonicalizes the + # chromosome, which would drop a "chr" prefix the variants still use). + region <- str_replace( + region, + "^([^:]+)_([0-9,]+)_([0-9,]+)$", + "\\1:\\2-\\3" + ) m <- str_match(region, "^([^:]+):([0-9,]+)-([0-9,]+)$")[1L, ] if (is.na(m[[1L]])) { msg <- glue( - "`region` must be a 'chr:start-end' string, a GRanges, or a ", - "one-row data.frame with chrom/start/end." + "`region` must be a 'chr:start-end' or 'chr_start_end' ", + "string, a GRanges, or a one-row data.frame with ", + "chrom/start/end." ) abort(msg) } @@ -866,6 +876,11 @@ NULL } else { as.numeric(df[[z$betaSrc]]) / as.numeric(df[[z$seSrc]]) }, + # Whether the SOURCE supplied z, which is what GWAS-VCF's `EZ` means. + # Recorded here because this is the only place that still knows: past + # it, a supplied z and a Wald z computed from beta/se are both just + # `Z`. + z_supplied = z$hasZ, !!!.sumstatSampleSizeCols(df, n), !!!map(present, .sumstatNumericColumn, df = df) ) @@ -915,12 +930,37 @@ NULL } # Default GWAS-VCF FORMAT tag mapping (canonical stat -> FORMAT field). +# Candidate FORMAT keys per field, in priority order: the GWAS-VCF reserved +# key first, then spellings found in the wild. +# +# `N` used to be `"SS"` alone -- which is NOT a GWAS-VCF key; the spec's +# sample-size field is `NS`. The writer emitted `SS` too, so the two +# round-tripped with each other while neither conformed, and fixing the writer +# would have broken this reader on pecotmr's own output. `SS` stays as a +# fallback because files in the wild use it. +# @noRd .gwasVcfFormatDefaults <- list( BETA = "ES", SE = "SE", + # `EZ` is the z the SOURCE supplied -- the spec defines it as the z + # "provided if it was used to derive the ES and SE fields". Reading it was + # missing entirely, so a z-derived study round-tripped as though pecotmr + # had computed its z from ES/SE. + Z = "EZ", P = "LP", - N = "SS", - MAF = "EAF" + N = c("NS", "SS"), + # `AF` is the spec's ALTERNATIVE-allele frequency: DIRECTIONAL, and the + # one the posterior view publishes as `af`. It used to be read as a + # fallback for the MAF slot and then folded with pmin(af, 1 - af), so an + # AF of 0.8 came back as a MAF of 0.2 with no `AF` column at all -- + # pecotmr's own VCF output did not round-trip, and the direction the + # standardized-to-raw weight conversion needs was silently gone. The + # mirror of the released writer bug where `AF` held MAF. + AF = "AF", + # `EAF` stays with MAF deliberately, matching the delimited-table loader: + # a column calling itself an effect-allele frequency is not auto-trusted + # as directional, because the effect allele it refers to is not stated. + MAF = c("MAF", "EAF") ) # Pick the FORMAT sample column of a GWAS-VCF to read. Defaults to the single @@ -950,14 +990,18 @@ NULL # Read column `tag` from the GWAS-VCF geno matrix for one sample as numeric, or # NULL when the tag is absent/unmapped. # @noRd +# `tag` may name SEVERAL candidate keys in priority order (the spec's key then +# the spellings found in the wild), so the first one the file actually carries +# wins. A single string still works -- it is a length-1 candidate list. +# @noRd .vcfGetField <- function(tag, geno, sample) { - if (!is.null(tag) && is_in(tag, names(geno))) { - # unname: geno matrix columns carry variant-id element names that the - # canonical sumstat tibble must not keep (data.frame stripped them). - unname(as.numeric(geno[[tag]][, sample])) - } else { - NULL + hit <- intersect(as.character(tag %||% character(0)), names(geno)) + if (length(hit) == 0L) { + return(NULL) } + # unname: geno matrix columns carry variant-id element names that the + # canonical sumstat tibble must not keep (data.frame stripped them). + unname(as.numeric(geno[[hit[[1L]]]][, sample])) } # Convert a VCF (GWAS-VCF format) into the canonical sumstats data.frame. The @@ -978,42 +1022,76 @@ NULL sample <- .pickVcfSample(colnames(vcf), sampleSelect, label) es <- .vcfGetField(fmap$BETA, geno, sample) se <- .vcfGetField(fmap$SE, geno, sample) + ez <- .vcfGetField(fmap$Z, geno, sample) lp <- .vcfGetField(fmap$P, geno, sample) ss <- .vcfGetField(fmap$N, geno, sample) + af <- .vcfGetField(fmap$AF, geno, sample) eaf <- .vcfGetField(fmap$MAF, geno, sample) - if (is.null(es) || is.null(se)) { + if ((is.null(es) || is.null(se)) && is.null(ez)) { msg <- glue( - "{label}: GWAS-VCF must carry ES and SE FORMAT fields to derive ", - "Z (configure via `formatMapping`)." + "{label}: GWAS-VCF must carry either a supplied z (EZ) or ES and ", + "SE to derive one from (configure via `formatMapping`)." ) abort(msg) } if (is.null(ss)) { msg <- glue( - "{label}: GWAS-VCF must carry an SS (sample size) FORMAT field." + "{label}: GWAS-VCF must carry a sample-size FORMAT field ", + "(NS, or SS in older files; configure via `formatMapping`)." ) abort(msg) } + z <- .vcfSumstatZ(es, se, ez, length(rr)) out <- tibble( chrom = as.character(GenomicRanges::seqnames(rr)), pos = unname(as.integer(GenomicRanges::start(rr))), SNP = names(rr), A1 = alt, A2 = ref, - Z = es / se, - N = ss, - BETA = es, - SE = se + Z = z$z, + z_supplied = z$supplied, + N = ss ) mutate( out, !!!compact(list( + BETA = es, + SE = se, P = if (!is.null(lp)) 10^(-lp), - MAF = if (!is.null(eaf)) pmin(eaf, 1 - eaf) + AF = af, + # A directionless MAF of its own when the file has one; else + # folded from AF, which is QC-usable and keeps `AF` intact + # alongside it rather than consuming it. + MAF = if (!is.null(eaf)) { + pmin(eaf, 1 - eaf) + } else if (!is.null(af)) { + pmin(af, 1 - af) + } )) ) } +# The z and its provenance, per variant. A supplied `EZ` wins where it has a +# value; elsewhere the Wald z is derived from ES / SE. Per VARIANT rather than +# per file because a field declared in the header can be absent from +# individual records, and publishing a derived z as a supplied one would tell +# a reader that ES and SE came from it. +# @noRd +.vcfSumstatZ <- function(es, se, ez, n) { + derived <- if (is.null(es) || is.null(se)) { + rep(NA_real_, n) + } else { + es / se + } + if (is.null(ez)) { + return(list(z = derived, supplied = rep(FALSE, n))) + } + list( + z = if_else(is.na(ez), derived, ez), + supplied = !is.na(ez) + ) +} + # 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) { diff --git a/R/overlapTopLoci.R b/R/overlapTopLoci.R index 87bf9359..272dd66f 100644 --- a/R/overlapTopLoci.R +++ b/R/overlapTopLoci.R @@ -48,7 +48,6 @@ } - # The per-variant top-loci columns of one side as a tibble -- topLoci() # returns ranges, and mcols() is exactly the pre-conversion table. # @noRd @@ -64,8 +63,8 @@ #' \code{GwasFineMappingResult} on shared variants, matched with pecotmr's #' allele-aware \code{matchVariants} (handling strand flips / ref-alt swaps #' rather than naive id equality). The GWAS side is harmonized to the QTL -#' orientation: its signed effect columns (\code{beta}, \code{z}, -#' \code{conditional_effect}) are sign-flipped and its effect-allele frequency +#' orientation: its signed effect columns (\code{posterior_effect}, +#' \code{beta}, \code{z}) are sign-flipped and its effect-allele frequency #' (\code{af}) is complemented wherever a swap occurred. The result keeps the #' variant key columns once (from the QTL, the reference orientation) and #' prefixes every other column \code{qtl_} / \code{gwas_}. A variant shared @@ -167,7 +166,11 @@ setMethod( # @noRd .overlapRelabelGwas <- function(gwasTl, vmap, coordCols) { joined <- inner_join(gwasTl, vmap, by = c("variant_id" = "gwas_vid")) - signedCols <- c("beta", "z", "conditional_effect") + # `posterior_effect` FIRST: it is what topLoci() actually publishes, and + # `any_of()` below skips a name it does not find SILENTLY -- so omitting it + # left a swapped variant's posterior effect with the wrong sign and no + # error. `beta`/`z` stay for a marginal-view frame. + signedCols <- c("posterior_effect", "beta", "z") # Bound with partial() rather than across()'s deprecated `...`; the local # matters because partial() resolves it lazily (see .negateWhere et al). rowSign <- joined$.sign diff --git a/R/resultReaders.R b/R/resultReaders.R new file mode 100644 index 00000000..5a725865 --- /dev/null +++ b/R/resultReaders.R @@ -0,0 +1,1287 @@ +# ============================================================================= +# Published result readers +# ----------------------------------------------------------------------------- +# The inverse of R/resultWriters.R: take a file that was published -- by +# `exportToBed()` / `exportToVcf()`, or by another tool writing the same +# vocabulary -- and rebuild the S4 collection. +# +# readFineMappingResult(path, ...) -> Qtl / GwasFineMappingResult +# readTwasWeights(path, ...) -> TwasWeights +# +# Two rules shape everything here. +# +# PERMISSIVE about columns. Each consumer needs a different few: TWAS needs +# only the weight, Mendelian randomization beta/se/PIP, colocalization the LBF +# matrix on its non-enloc path. A file converted from another tool will carry +# some of those and not others, so a missing column is restored as absent, not +# as an error -- the consumers name what they lack when they are reached. +# What IS required is only what the class itself cannot exist without: a +# coordinate-encoding variant id, and for a fine-mapping result a PIP. +# +# STRICT about identity. A merged table holding several (study, context, +# trait, method) tuples reads back as a multi-row collection, one row per +# tuple. Where a per-row value is recorded per variant -- the analysis window, +# the provenance flags -- every variant of a tuple must agree, and a +# disagreement is an error: it means two different results were concatenated +# under one identity, and picking either value would silently publish one +# fit's window over another's. +# ============================================================================= + +#' @include AllGenerics.R AllClasses.R resultWriters.R +#' @importFrom dplyr slice mutate select bind_cols all_of +#' @importFrom tibble tibble as_tibble +#' @importFrom purrr map map_chr map_lgl list_rbind compact set_names +#' @importFrom stringr str_c str_detect str_remove str_to_lower +#' @importFrom stringr str_flatten str_split_1 +#' @importFrom magrittr is_in +#' @importFrom glue glue +#' @importFrom dplyr if_else +#' @importFrom rlang abort warn arg_match try_fetch .data +#' @importFrom checkmate assertString assertFlag +NULL + +# ---- the inverse of the published vocabulary -------------------------------- + +# Published column -> the CANONICAL WIDE `topLoci` column it was projected +# from. Only the pure renames live here. The four quantities that are +# TRANSFORMED on the way out are inverted explicitly below, each next to a +# comment naming the forward transform: `start`/`end` from `pos`, `LP` from +# `p`, `EZ` with its supplied flag, and `CS` from the credible-set label. +# +# `MAF` has no canonical slot -- the posterior view publishes `af`, the +# directional frequency -- so it is restored under its own name and travels as +# a passthrough column rather than being mistaken for `af`. +# @noRd +.PUB_RESTORE_VARIANT <- c( + `#chr` = "chrom", + ID = "variant_id", + REF = "A2", + ALT = "A1", + AF = "af", + MAF = "maf", + NS = "N", + ES = "marginal_beta", + SE = "marginal_se", + WGT = "posterior_mean", + WGTSE = "posterior_sd", + PIP = "pip", + LBF = "logBF", + LFSR = "lfsr" +) + +# Published column -> the collection mcols column it was read from. These are +# per-ROW, so they are lifted out of the variant table and reconciled across +# each tuple's variants rather than stored per variant. +# @noRd +.PUB_RESTORE_IDENTITY <- c( + study = "study", + context = "context", + trait = "trait", + method = "method", + block_id = "blockId", + data_type = "dataType", + data_source = "dataSource", + weight_standardized = "standardized", + joint_studies = "jointStudies", + joint_contexts = "jointContexts", + joint_traits = "jointTraits" +) + +# The published columns that are structural or per-row, and so are never +# carried through as a variant-level passthrough column. +# @noRd +.PUB_NON_PAYLOAD <- c( + "#chr", + "start", + "end", + "region_id", + "region_start", + "region_end", + names(.PUB_RESTORE_IDENTITY) +) + +# The identity axes a published table is grouped into collection rows by. +# @noRd +.PRD_GROUP_COLS <- c("study", "context", "trait", "method", "block_id") + +# ---- reading the file ------------------------------------------------------- + +# One published table as a tibble of character columns, region-restricted when +# the file is bgzipped + tabix-indexed. +# +# Every column is read as character and coerced afterwards, for the same +# reason `.readTabixRegion()` does it: an allele or id column that happens to +# be uniformly "T" within a region would otherwise be type-guessed as logical, +# silently corrupting the variant identity. +# @noRd +.prdReadTable <- function(path, region, label) { + if (!file.exists(path)) { + abort(glue("{label}: file '{path}' does not exist.")) + } + if (!is.null(region)) { + if (file.exists(str_c(path, ".tbi"))) { + return(.readTabixRegion(path, region)) + } + warn(glue( + "{label}: '{path}' is not bgzipped + tabix-indexed; `region` ", + "ignored, reading the whole file." + )) + } + readr::read_tsv( + path, + show_col_types = FALSE, + progress = FALSE, + col_types = readr::cols(.default = readr::col_character()) + ) +} + +# Which reader a path asks for. The writers choose the extension, so `"auto"` +# is unambiguous on pecotmr's own output; `format` is for a file named +# something else. +# @noRd +.prdFormat <- function(path, format) { + format <- arg_match(format, c("auto", "bed", "vcf")) + if (format != "auto") { + return(format) + } + stem <- str_remove(path, "\\.(gz|bgz|bz2)$") + if (str_detect(stem, "\\.(vcf|bcf)$")) "vcf" else "bed" +} + +# ---- reading a published VCF ------------------------------------------------ + +# One published GWAS-VCF as the SAME long table the BED reader produces: the +# structural columns, one column per FORMAT key, and the identity columns read +# off the `##SAMPLE` header block. Each sample is one fitted model, so each +# contributes its own block of rows. +# @noRd +.prdVcfTable <- function(path, region, sampleSelect, label) { + vcf <- .prdReadVcf(path, region, label) + samples <- colnames(vcf) + if (!is.null(sampleSelect)) { + absent <- setdiff(as.character(sampleSelect), samples) + if (length(absent) > 0L) { + abort(glue( + "{label}: sampleSelect names {str_flatten(absent, ', ')}, ", + "which the VCF does not have (have: ", + "{str_flatten(samples, ', ')})." + )) + } + samples <- as.character(sampleSelect) + } + if (length(samples) == 0L) { + return(list(table = tibble(), fits = list())) + } + meta <- .prdVcfSampleMeta(vcf) + pieces <- map(samples, .prdVcfSampleTable, vcf = vcf, meta = meta) + list( + table = list_rbind(pieces), + fits = compact(set_names( + map(samples, .prdVcfPartialFit, meta = meta), + map_chr(pieces, .prdFirstKey) + )) + ) +} + +# The identity key of a sample's block, so its header-recorded partial fit can +# be found again once every sample's rows are stacked into one table. +# @noRd +.prdFirstKey <- function(tbl) { + cols <- intersect(.PRD_GROUP_COLS, names(tbl)) + if (nrow(tbl) == 0L || length(cols) == 0L) { + return("") + } + .pubIdentityKey(tbl, cols)[[1L]] +} + +# @noRd +.prdReadVcf <- function(path, region, label) { + if (!requireNamespace("VariantAnnotation", quietly = TRUE)) { + abort(glue( + "{label}: reading a published VCF requires the ", + "'VariantAnnotation' package." + )) + } + if (!file.exists(path)) { + abort(glue("{label}: file '{path}' does not exist.")) + } + if (is.null(region) || !file.exists(str_c(path, ".tbi"))) { + if (!is.null(region)) { + warn(glue( + "{label}: '{path}' is not bgzipped + tabix-indexed; ", + "`region` ignored, reading the whole file." + )) + } + return(VariantAnnotation::readVcf(path)) + } + tf <- Rsamtools::TabixFile(path) + gr <- .matchRegionSeqnames( + .asGRegion(region), + Rsamtools::seqnamesTabix(tf) + ) + if (is.null(gr)) { + return(VariantAnnotation::readVcf(path)[0, ]) + } + VariantAnnotation::readVcf(tf, param = gr) +} + +# The per-fit `##SAMPLE` block as a plain data.frame keyed by sample id, or +# NULL when the file has none. This is where `exportToVcf()` records the +# identity, the analysis window and the two per-EFFECT fit quantities (`V`, +# `cs_index`) that no per-variant column could hold. +# @noRd +.prdVcfSampleMeta <- function(vcf) { + # `meta()` is VariantAnnotation's own accessor for the header's structured + # `##` blocks -- S4Vectors::metadata() is a different thing and returns + # nothing here. + md <- try_fetch( + VariantAnnotation::meta(VariantAnnotation::header(vcf))$SAMPLE, + error = function(cnd) NULL + ) + if (is.null(md) || nrow(md) == 0L) { + return(NULL) + } + df <- as.data.frame(md, stringsAsFactors = FALSE) + # Back to the shared vocabulary through the writer's own table, so the + # rest of the reader sees the same names a published BED carries. + known <- is_in(names(df), .PUB_SAMPLE_KEYS) + names(df)[known] <- names(.PUB_SAMPLE_KEYS)[ + match(names(df)[known], .PUB_SAMPLE_KEYS) + ] + df +} + +# One sample's rows: the fixed columns, its FORMAT values, and the identity +# the header recorded for it. +# @noRd +.prdVcfSampleTable <- function(sample, vcf, meta) { + rr <- SummarizedExperiment::rowRanges(vcf) + n <- length(rr) + geno <- VariantAnnotation::geno(vcf) + fixed <- tibble( + `#chr` = as.character(GenomicRanges::seqnames(rr)), + start = as.integer(GenomicRanges::start(rr)) - 1L, + end = as.integer(GenomicRanges::start(rr)), + ID = names(rr) %||% rep(NA_character_, n), + REF = unname(as.character(VariantAnnotation::ref(vcf))), + ALT = unname(map_chr(as.list(VariantAnnotation::alt(vcf)), .mlFirstAlt)) + ) + fmt <- set_names( + map(names(geno), .prdVcfFormatColumn, geno = geno, sample = sample), + map_chr(names(geno), .prdVcfKeyName) + ) + ident <- .prdVcfIdentity(meta, sample, n) + bind_cols(fixed, as_tibble(fmt, .name_repair = "minimal"), ident) +} + +# One FORMAT field's column for one sample. A field the sample does not report +# is all-NA, which is what an absent column means on the way back in. +# @noRd +.prdVcfFormatColumn <- function(key, geno, sample) { + m <- geno[[key]] + if (!is_in(sample, colnames(m))) { + return(rep(NA_character_, nrow(m))) + } + v <- as.character(unname(m[, sample])) + # A `Number=A` geno field comes back as a CHARACTER matrix whose missing + # cells are the literal strings "NA" (or VCF's own "."), not NA. Left in + # place they stop a numeric field from being recognised as numeric, which + # is how a published Float came back as a character column. + replace(v, !is.na(v) & is_in(v, c("NA", ".", "")), NA_character_) +} + +# A FORMAT key as the published column name. The fixed vocabulary is already +# shared between the two formats, so only the per-credible-set `fullFit` +# fields need undoing: `exportToVcf()` uppercases those column names because a +# FORMAT key is conventionally uppercase, and nothing else in the schema is +# case-ambiguous. +# @noRd +.prdVcfKeyName <- function(key) { + lower <- str_to_lower(key) + if (str_detect(lower, "^(within_cs_pip|cs_logbf_|cs_effect_)")) { + return(lower) + } + key +} + +# The identity columns for one sample, recycled over its variants. +# +# A file with no `##SAMPLE` block still has the sample NAME. Splitting it back +# into a tuple would be guesswork, but taking it whole is not: it becomes the +# `study`, which keeps each sample a result of its own rather than letting +# several merge into one identity-less row, and the reader's own arguments +# supply whatever else is wanted. +# @noRd +.prdVcfIdentity <- function(meta, sample, n) { + if (is.null(meta) || !is_in(sample, rownames(meta))) { + return(tibble(study = rep(sample, length.out = n))) + } + row <- meta[sample, , drop = FALSE] + cols <- intersect( + c( + names(.PUB_RESTORE_IDENTITY), + "region_id", + "region_start", + "region_end" + ), + colnames(row) + ) + if (length(cols) == 0L) { + return(tibble(study = rep(sample, length.out = n))) + } + out <- as_tibble( + set_names(map(cols, .prdMetaValue, row = row, n = n), cols), + .name_repair = "minimal" + ) + .prdVcfRegionStart(out) +} + +# A VCF header's `RegionStart` is 1-based, because VCF coordinates are; a +# published table's `region_start` is 0-based half-open, because BED +# coordinates are. Each format is right on its own terms, so the long form +# the readers share settles on the table's convention and the VCF value is +# shifted into it here -- once, rather than at every reader of the column. +# @noRd +.prdVcfRegionStart <- function(out) { + if (!is_in("region_start", names(out))) { + return(out) + } + mutate( + out, + region_start = as.character( + suppressWarnings(as.integer(.data$region_start)) - 1L + ) + ) +} + +# One header field's value, recycled over the sample's variants. "." is VCF's +# missing value and `.vcfHeaderValue()` writes it for a field one sample has +# and another does not, so it is restored as NA rather than as the string ".". +# @noRd +.prdMetaValue <- function(nm, row, n) { + v <- as.character(row[[nm]][[1L]]) + rep(if (!is.na(v) && v == ".") NA_character_ else v, length.out = n) +} + +# The per-effect fit quantities the `##SAMPLE` block carried, as the partial +# `susieFit` of one row -- `V` (the estimated prior variances) and the credible +# sets' `cs_index`. They are per EFFECT, not per variant, which is why they +# live in the header at all, and colocalization's credible-set filter reads +# them straight off the fit. Everything else a fit holds (alpha, lbf_variable, +# the posterior matrices) is not published, so this is deliberately partial: +# `NULL` when the header recorded neither. +# @noRd +.prdVcfPartialFit <- function(meta, sample) { + if (is.null(meta) || !is_in(sample, rownames(meta))) { + return(NULL) + } + row <- meta[sample, , drop = FALSE] + v <- .prdUnpackNumeric(row[["V"]]) + csIndex <- .prdUnpackNumeric(row[["cs_index"]]) + out <- compact(list( + V = v, + sets = if (!is.null(csIndex)) list(cs_index = as.integer(csIndex)) + )) + if (length(out) == 0L) NULL else out +} + +# A ";"-packed header value as a numeric vector, or NULL when it was NA. +# @noRd +.prdUnpackNumeric <- function(cell) { + if (is.null(cell) || length(cell) == 0L) { + return(NULL) + } + chr <- as.character(cell[[1L]]) + if (length(chr) == 0L || is.na(chr) || chr == "") { + return(NULL) + } + suppressWarnings(as.numeric(str_split_1(chr, ";"))) +} + +# ---- published table -> canonical per-variant table ------------------------- + +# The canonical wide `topLoci` table for one published table: the pure renames +# applied, the four transformed quantities inverted, and every other column +# carried through verbatim so a `columns = "all"` export and a third party's +# own annotations both survive the trip. +# @noRd +.prdCanonical <- function(df, method, priorVariance) { + pos <- .prdPos(df) + out <- tibble( + variant_id = .prdVariantIds(df, pos), + chrom = .prdChr(df), + pos = pos + ) + renamed <- intersect(names(.PUB_RESTORE_VARIANT), names(df)) + renamed <- setdiff(renamed, c("#chr", "ID")) + out <- bind_cols( + out, + as_tibble( + set_names( + map(renamed, .prdRestoreColumn, df = df), + .PUB_RESTORE_VARIANT[renamed] + ), + .name_repair = "minimal" + ) + ) + out <- bind_cols( + out, + .prdMarginalZ(df), + .prdMarginalP(df), + .prdCsColumns(df, method), + .prdPassthrough(df) + ) + .prdWakefieldLbf(out, priorVariance) +} + +# A published column restored to its canonical name, typed. Everything the +# renaming table covers is numeric except the four character columns, which +# are the alleles and the id-like fields. +# @noRd +.prdRestoreColumn <- function(nm, df) { + v <- df[[nm]] + if (is_in(nm, c("REF", "ALT"))) { + return(as.character(v)) + } + suppressWarnings(as.numeric(v)) +} + +# The 1-based position. A published range is 0-based half-open -- that is the +# BED format's requirement -- so `end` IS the 1-based position of a +# single-base record, and `start + 1` says the same thing. `end` is preferred +# because it is what the writer set from `pos` directly. +# @noRd +.prdPos <- function(df) { + if (is_in("end", names(df))) { + return(suppressWarnings(as.integer(df$end))) + } + if (is_in("start", names(df))) { + return(suppressWarnings(as.integer(df$start)) + 1L) + } + rep(NA_integer_, nrow(df)) +} + +# @noRd +.prdChr <- function(df) { + if (is_in("#chr", names(df))) { + return(as.character(df[["#chr"]])) + } + rep(NA_character_, nrow(df)) +} + +# The variant ids, which are the one thing a result cannot be built without: +# an id renders a range and its alleles, so `.variantIdsToGRanges()` refuses +# one that does not encode coordinates. A published `ID` normally does, having +# been written from exactly that. When it does not -- an rsid-keyed file from +# another tool -- the id is rebuilt from the coordinates and alleles that are +# published alongside it, which is the same string the writer would have +# produced. +# @noRd +.prdVariantIds <- function(df, pos) { + ids <- if (is_in("ID", names(df))) as.character(df$ID) else NULL + if (!is.null(ids) && !any(is.na(parseVariantId(ids)$pos))) { + return(ids) + } + parts <- map(c("#chr", "REF", "ALT"), .prdCol, df = df) + if (any(map_lgl(parts, is.null)) || anyNA(pos)) { + abort(str_c( + "A published table must identify its variants: either an `ID` ", + "column of chrom:pos:ref:alt ids, or `#chr`, `start`/`end`, ", + "`REF` and `ALT` to build them from. An id without coordinates ", + "has no variant identity to store." + )) + } + formatVariantId( + as.character(parts[[1L]]), + pos, + as.character(parts[[2L]]), + as.character(parts[[3L]]) + ) +} + +# @noRd +.prdCol <- function(nm, df) { + if (is_in(nm, names(df))) df[[nm]] else NULL +} + +# `EZ` is the z the SOURCE supplied, and the spec means it that way: the z +# "provided if it was used to derive the ES and SE fields". So a published EZ +# restores `marginal_z_supplied = TRUE`, and its absence leaves z derived from +# ES / SE with the flag FALSE -- which is what keeps a re-export from +# publishing an EZ that pecotmr computed itself. +# @noRd +.prdMarginalZ <- function(df) { + n <- nrow(df) + ez <- .prdNumericOrNull(df, "EZ") + if (!is.null(ez) && any(!is.na(ez))) { + return(tibble( + marginal_z = ez, + marginal_z_supplied = !is.na(ez) + )) + } + es <- .prdNumericOrNull(df, "ES") + se <- .prdNumericOrNull(df, "SE") + if (is.null(es) || is.null(se)) { + return(tibble(.rows = n)) + } + tibble( + marginal_z = if_else(is.na(se) | se <= 0, NA_real_, es / se), + marginal_z_supplied = rep(FALSE, n) + ) +} + +# `LP` is -log10 p, which is the better published form (invertible, and no +# underflow at tiny p); the canonical table stores the p itself. +# @noRd +.prdMarginalP <- function(df) { + lp <- .prdNumericOrNull(df, "LP") + if (is.null(lp)) { + return(tibble(.rows = nrow(df))) + } + tibble(marginal_p = 10^(-lp)) +} + +# @noRd +.prdNumericOrNull <- function(df, nm) { + if (!is_in(nm, names(df))) { + return(NULL) + } + suppressWarnings(as.numeric(df[[nm]])) +} + +# The credible-set columns. Both formats publish an integer INDEX, because the +# label's prefix is the fitting method, which is already its own field. The +# canonical table stores the label, so it is rebuilt as `_`; +# NA stays NA, which is "in no credible set at this coverage". +# @noRd +.prdCsColumns <- function(df, method) { + csNames <- names(df)[str_detect(names(df), "^CS[0-9.]+$")] + if (length(csNames) == 0L) { + return(tibble(.rows = nrow(df))) + } + levels <- str_remove(csNames, "^CS") + cols <- c( + set_names( + map(csNames, .prdCsLabels, df = df, method = method), + str_c("cs_", levels) + ), + compact(set_names( + map(str_c("PUR", levels), .prdNumericOrNull, df = df), + str_c("cs_", levels, "_purity") + )) + ) + as_tibble(cols, .name_repair = "minimal") +} + +# @noRd +.prdCsLabels <- function(nm, df, method) { + idx <- suppressWarnings(as.integer(df[[nm]])) + prefix <- if (is.null(method) || is.na(method)) "cs" else method + if_else( + is.na(idx) | idx == 0L, + NA_character_, + str_c(prefix, "_", idx) + ) +} + +# Every published column the schema does not already account for, carried +# through under its own name: the per-credible-set `fullFit` statistics, and +# whatever a `columns = "all"` export or another tool added. +# +# Each is numeric when every non-NA value parses as a number, and character +# otherwise. Guessing per column rather than per value is what keeps an allele +# or label column from being coerced to NA, and the file itself carries no +# types to consult. +# @noRd +.prdPassthrough <- function(df) { + known <- c( + names(.PUB_RESTORE_VARIANT), + .PUB_NON_PAYLOAD, + "ES", + "SE", + "EZ", + "LP", + names(df)[str_detect(names(df), "^(CS|PUR)[0-9.]+$")] + ) + extra <- setdiff(names(df), known) + if (length(extra) == 0L) { + return(tibble(.rows = nrow(df))) + } + as_tibble( + set_names(map(extra, .prdAutoType, df = df), extra), + .name_repair = "minimal" + ) +} + +# @noRd +.prdAutoType <- function(nm, df) { + v <- as.character(df[[nm]]) + num <- suppressWarnings(as.numeric(v)) + if (all(is.na(num) == is.na(v))) num else v +} + +# Wakefield's approximate Bayes factor, as an OPT-IN substitute for a `LBF` +# the file does not carry. Opt-in because it is an approximation with a prior +# the caller has to choose: `priorVariance` is Wakefield's W, and the +# resulting log Bayes factors are not what susie's `lbf_variable` would have +# been. Colocalization's non-enloc path needs a real LBF matrix and says so +# rather than accepting this. +# @noRd +.prdWakefieldLbf <- function(out, priorVariance) { + if (is.null(priorVariance)) { + return(out) + } + assertNumber(priorVariance, lower = 0) + if (is_in("logBF", names(out)) && any(!is.na(out$logBF))) { + abort(str_c( + "`priorVariance` asks for Wakefield log Bayes factors, but this ", + "file already carries LBF. Drop `priorVariance` to keep the ", + "published values." + )) + } + z <- .prdCol("marginal_z", out) + se <- .prdCol("marginal_se", out) + absent <- c("z (EZ, or ES/SE)", "standard error (SE)")[ + c(is.null(z), is.null(se)) + ] + if (length(absent) > 0L) { + abort(glue( + "`priorVariance` asks for Wakefield log Bayes factors, which ", + "need a z and a standard error. This file publishes no ", + "{str_flatten(absent, ' and no ')}." + )) + } + mutate(out, logBF = .approxBayesFactor(z, se, W = priorVariance)$lbf) +} + +# ---- published table -> collection rows ------------------------------------- + +# One published table read into the long form both readers work from. +# @noRd +.prdRead <- function(path, region, format, sampleSelect, label) { + assertString(path) + if (.prdFormat(path, format) == "vcf") { + return(.prdVcfTable(path, region, sampleSelect, label)) + } + # A published table has no header to carry a fit in -- only the VCF's + # `##SAMPLE` block does -- so a BED read restores no fit at all. + list(table = .prdReadTable(path, region, label), fits = list()) +} + +# The row groups of a published table, in first-appearance order: one per +# (study, context, trait, method, block_id) tuple the file carries. A table +# with none of those axes is a single result, which is what a bare +# single-entry export writes. +# @noRd +.prdGroups <- function(df) { + # A header-only file -- which is what an export of an empty result writes + # -- has no groups at all, not one empty group: the identity axes are + # column names there, with no values to key anything by. + if (nrow(df) == 0L) { + return(list()) + } + cols <- intersect(.PRD_GROUP_COLS, names(df)) + if (length(cols) == 0L) { + return(list(seq_len(nrow(df)))) + } + key <- .pubIdentityKey(df, cols) + split(seq_len(nrow(df)), factor(key, levels = unique(key))) +} + +# The single value a per-ROW column takes over one group's variants. +# +# Several distinct values mean two different results were concatenated under +# one identity tuple, and picking either would silently publish one fit's +# window or provenance over another's -- so it is an error naming the column +# and what it held. +# @noRd +.prdGroupValue <- function(df, nm, idx, label) { + if (!is_in(nm, names(df))) { + return(NA_character_) + } + vals <- unique(as.character(df[[nm]][idx])) + vals <- vals[!is.na(vals)] + if (length(vals) == 0L) { + return(NA_character_) + } + if (length(vals) > 1L) { + abort(glue( + "{label}: `{nm}` takes {length(vals)} different values ", + "({str_flatten(vals, ', ')}) within one ", + "(study, context, trait, method) group. It is a per-result ", + "value, so two results were concatenated under one identity." + )) + } + vals[[1L]] +} + +# Reconcile what the file recorded with what the caller supplied. The +# argument FILLS IN a column the file does not carry -- which is the point of +# having it, for a file a user assembled themselves -- and a disagreement is +# an error rather than a silent override, because one of the two is wrong and +# the reader cannot tell which. +# @noRd +.prdResolve <- function(fileValue, supplied, nm, label) { + if (is.null(supplied)) { + return(fileValue) + } + sup <- as.character(supplied)[[1L]] + if (!is.na(fileValue) && !identical(fileValue, sup)) { + abort(glue( + "{label}: `{nm}` was given as '{sup}' but the file records ", + "'{fileValue}'. Drop the argument to use the file's value." + )) + } + sup +} + +# The per-row identity and provenance of one group. +# @noRd +.prdRowFields <- function(df, idx, supplied, label) { + fromFile <- set_names( + map( + names(.PUB_RESTORE_IDENTITY), + .prdGroupValue, + df = df, + idx = idx, + label = label + ), + .PUB_RESTORE_IDENTITY + ) + for (nm in names(supplied)) { + fromFile[[nm]] <- .prdResolve( + fromFile[[nm]], + supplied[[nm]], + nm, + label + ) + } + fromFile +} + +# The analysis WINDOW of one group as a one-range GRanges, or NULL when the +# file did not publish it. `region_start` is 0-based half-open like every +# other published coordinate, so it is shifted back by one; `region_id` stays +# the 1-based `chrom_start_end` identifier and is not a coordinate source. +# @noRd +.prdRowRegion <- function(df, idx, label) { + start <- .prdGroupValue(df, "region_start", idx, label) + end <- .prdGroupValue(df, "region_end", idx, label) + chrom <- .prdRegionChrom(df, idx, label) + if (is.na(start) || is.na(end) || is.na(chrom)) { + return(NULL) + } + GenomicRanges::GRanges( + chrom, + IRanges::IRanges( + start = as.integer(start) + 1L, + end = as.integer(end) + ) + ) +} + +# The window's chromosome: from `region_id` when it has one (that is what the +# identifier encodes), else the variants' own, which cannot disagree with it +# because a result is fine-mapped within one block. +# @noRd +.prdRegionChrom <- function(df, idx, label) { + id <- .prdGroupValue(df, "region_id", idx, label) + if (!is.na(id) && str_detect(id, "_")) { + return(str_remove(id, "_[^_]*_[^_]*$")) + } + if (!is_in("#chr", names(df))) { + return(NA_character_) + } + chrom <- unique(as.character(df[["#chr"]][idx])) + if (length(chrom) == 1L) chrom else NA_character_ +} + +# Stack the per-group region ranges into the collections' `region` column: +# one range per row, or NULL when no row published one. A partial set would +# have to invent a window for the rest, so it is all or nothing. +# @noRd +.prdRegionColumn <- function(regions) { + if (any(map_lgl(regions, is.null))) { + return(NULL) + } + if (length(regions) == 0L) { + return(NULL) + } + # Through a GRangesList, not `c()`: `c()` on a list of S4 objects falls + # back to the list method and hands back a list, which the constructor + # then rejects as "not a GRanges". Names come from the group keys and + # would otherwise land on the stored ranges. + unname(unlist(GenomicRanges::GRangesList(regions))) +} + +# A per-row character column for a constructor argument, or NULL when no row +# recorded it -- which is how the constructors are told to omit the column. +# @noRd +.prdRowColumn <- function(rows, nm) { + v <- map_chr(rows, .prdRowField, nm = nm) + if (all(is.na(v))) NULL else v +} + +# @noRd +.prdRowField <- function(r, nm) { + v <- r[[nm]] + if (is.null(v) || length(v) == 0L) NA_character_ else as.character(v)[[1L]] +} + +# The identity axes a class cannot be built without, checked once with every +# absent one named along with the argument that supplies it. The constructors +# reject NA on these axes, and a published file is free not to have recorded +# them -- a user who assembled the table themselves then says what they are. +# @noRd +.prdRequireIdentity <- function(rows, axes, label) { + absent <- keep(axes, .prdAxisAbsent, rows = rows) + if (length(absent) == 0L) { + return(invisible(NULL)) + } + args <- c( + study = "studyName", + context = "context", + trait = "trait", + method = "method" + ) + abort(glue( + "{label}: the file does not record {str_flatten(absent, ', ')}, ", + "which this result is keyed by. Pass ", + "{str_flatten(str_c('`', args[absent], '`'), ', ')} to supply ", + "{if (length(absent) == 1L) 'it' else 'them'}." + )) +} + +# @noRd +.prdAxisAbsent <- function(axis, rows) { + is.null(.prdRowColumn(rows, axis)) +} + +# `weight_standardized` describes the SCALE of the published weight, and it +# cannot be recovered from the numbers: the standardized-to-raw conversion is +# a per-variant factor 1/sqrt(2*af*(1-af)), so the two scales differ by an +# af-dependent amount at every variant and neither is recognisable on its own. +# So when a file publishes a weight without the flag, the caller has to say. +# @noRd +.prdRequireStandardized <- function(df, supplied, label) { + if (!is.null(supplied)) { + assertFlag(supplied) + return(invisible(NULL)) + } + if (!is_in("WGT", names(df)) || all(is.na(df$WGT))) { + return(invisible(NULL)) + } + if (is_in("weight_standardized", names(df))) { + if (any(!is.na(df$weight_standardized))) { + return(invisible(NULL)) + } + } + abort(glue( + "{label}: this file publishes WGT but not `weight_standardized`, ", + "and the scale cannot be recovered from the weights -- converting ", + "between them rescales every variant by a different, allele-", + "frequency-dependent factor. Pass `weightStandardized` to say ", + "whether the genotypes behind these weights were standardized." + )) +} + +# @noRd +.prdFlag <- function(v) { + if (is.null(v) || length(v) == 0L || is.na(v)) NA else as.logical(v) +} + +# ---- entry point: fine-mapping results -------------------------------------- + +#' @title Read A Published Fine-Mapping Result +#' @description Rebuild a \code{QtlFineMappingResult} or +#' \code{GwasFineMappingResult} from a file \code{\link{exportToBed}} or +#' \code{\link{exportToVcf}} wrote -- or from any file in the same +#' vocabulary, which is what makes this the way into pecotmr for results +#' produced elsewhere. +#' +#' A merged table holding several \code{(study, context, trait, method)} +#' tuples reads back as a multi-row collection, one row per tuple. Which +#' class is returned follows the axes the file has: a result with a context +#' or a trait is a QTL result, one without is a GWAS result. +#' +#' @section What is required, and what is not: +#' Only what the class cannot exist without: variant ids that encode +#' coordinates (or the \code{#chr} / \code{start} / \code{end} / \code{REF} +#' / \code{ALT} columns to build them from) and a \code{PIP} column. +#' Everything else is optional, because each consumer needs a different few +#' columns -- Mendelian randomization the marginal effect and its standard +#' error, colocalization the log Bayes factors -- and a file converted from +#' another tool will have some and not others. A consumer reached without +#' what it needs says which column is missing. +#' +#' The fit itself is not published. A VCF's \code{##SAMPLE} header records +#' the two per-effect quantities that no per-variant column could hold -- +#' the prior variances \code{V} and the credible sets' \code{cs_index} -- +#' and those are restored as a partial \code{susieFit}; a BED carries +#' neither. Anything needing the posterior matrices (colocalization's LBF +#' path) wants the \code{LBF} columns, not the fit. +#' +#' @section What a round trip changes: +#' Every published quantity comes back identical, in either format. Three +#' things are not identities: +#' \itemize{ +#' \item \code{exportToBed()} publishes the \strong{filtered} top-loci +#' table, so a default export holds the variants that passed +#' \code{signalCutoff} / \code{minPurity} rather than every variant of +#' the fit. Export with \code{signalCutoff = 0} for the whole table. +#' \item Credible-set membership comes back as \code{NA} where the fit +#' recorded the \code{"_0"} not-captured sentinel. Both formats +#' publish an integer index, and \code{NA} is what every reader of the +#' column already treats as "in no credible set". +#' \item \code{data_source} and \code{weight_standardized} are +#' populated on the way back -- the writer derives the first and the +#' reader is told the second -- where a fit predating them left them +#' empty. +#' } +#' +#' @param path Path to the published file. Bgzipped and tabix-indexed files +#' can be read by region. +#' @param region Optional region to restrict the read to -- a +#' \code{"chr:start-end"} string, a \code{GRanges}, or a one-row +#' \code{data.frame} with chrom/start/end. Ignored with a warning when the +#' file is not tabix-indexed. +#' @param format \code{"auto"} (default) reads a \code{.vcf} / \code{.bcf} +#' path as a VCF and anything else as a published table; \code{"bed"} / +#' \code{"vcf"} say so explicitly for a file named otherwise. +#' @param studyName,context,trait,method Identity values for a file that did +#' not record them. Each FILLS IN the corresponding column when the file +#' lacks it; a value that disagrees with what the file does record is an +#' error rather than a silent override. +#' @param dataType Optional assay / modality label, same fill-in rule. +#' @param weightStandardized Whether the published posterior effect +#' (\code{WGT}) is on the standardized genotype scale. Required when the +#' file publishes \code{WGT} without \code{weight_standardized}: the scale +#' is not recoverable from the weights, since converting between the two +#' rescales every variant by its own allele-frequency-dependent factor. +#' @param sampleSelect For a multi-sample VCF, which samples to read. Each +#' sample is one fitted model. Defaults to all of them. +#' @param priorVariance Opt-in Wakefield prior variance (\code{W}) for +#' deriving per-variant log Bayes factors from z when the file has no +#' \code{LBF}. These are an approximation under a prior you chose, not +#' susie's \code{lbf_variable}, so nothing derives them unasked. +#' @return A \code{QtlFineMappingResult} or \code{GwasFineMappingResult}. +#' @seealso \code{\link{exportToBed}}, \code{\link{exportToVcf}}, +#' \code{\link{readTwasWeights}} +#' @examples +#' data(qtlFineMappingExample) +#' path <- file.path(tempdir(), "top_loci.bed") +#' exportToBed(qtlFineMappingExample, path) +#' # This fit predates the provenance columns, so its published table does +#' # not say which genotype scale its posterior effects are on -- the reader +#' # will not guess. +#' readFineMappingResult(path, weightStandardized = TRUE) +#' @importFrom purrr keep imap +#' @importFrom checkmate assertNumber +#' @export +readFineMappingResult <- function( + path, + region = NULL, + format = c("auto", "bed", "vcf"), + studyName = NULL, + context = NULL, + trait = NULL, + method = NULL, + dataType = NULL, + weightStandardized = NULL, + sampleSelect = NULL, + priorVariance = NULL +) { + label <- "readFineMappingResult" + src <- .prdRead(path, region, format, sampleSelect, label) + df <- src$table + .prdRequirePip(df, label) + .prdRequireStandardized(df, weightStandardized, label) + supplied <- compact(list( + study = studyName, + context = context, + trait = trait, + method = method, + dataType = dataType, + standardized = weightStandardized + )) + groups <- .prdGroups(df) + rows <- imap( + groups, + .prdFmRow, + df = df, + supplied = supplied, + label = label, + priorVariance = priorVariance, + fits = src$fits + ) + .prdBuildFineMapping(rows, df, context, trait, label) +} + +# A fine-mapping result is a posterior, and the PIP is the posterior. A table +# without one is some other kind of file -- a weights export, most likely -- +# and building a result from it would produce rows whose every accessor is +# empty. +# @noRd +.prdRequirePip <- function(df, label) { + if (is_in("PIP", names(df))) { + return(invisible(NULL)) + } + abort(glue( + "{label}: no `PIP` column. A fine-mapping result is a posterior ", + "over variants, so the inclusion probability is the one statistic ", + "it cannot be read without. For a weights file use readTwasWeights()." + )) +} + +# One collection row from one group of published rows. +# @noRd +.prdFmRow <- function(idx, key, df, supplied, label, priorVariance, fits) { + fields <- .prdRowFields(df, idx, supplied, label) + tl <- .prdCanonical(slice(df, idx), fields$method, priorVariance) + list( + fields = fields, + region = .prdRowRegion(df, idx, label), + entry = fineMappingRow( + variantIds = tl$variant_id, + susieFit = .prdFitFor(fits, key), + topLoci = tl + ) + ) +} + +# The partial fit the VCF header recorded for this group, if any. A table with +# no identity axes is one unnamed group, so the key is an index rather than a +# key -- which must not be allowed to pick the first fit by position. +# @noRd +.prdFitFor <- function(fits, key) { + if (!is.character(key) || !is_in(key, names(fits))) { + return(NULL) + } + fits[[key]] +} + +# Which class the file describes, and build it. A QTL result is keyed by +# (study, context, trait, method) and a GWAS result by (study, method) plus +# its block, so the presence of a context or a trait is what separates them. +# An empty file has no values to look at, only its header, so there the +# COLUMN decides. +# @noRd +.prdBuildFineMapping <- function(rows, df, context, trait, label) { + regions <- .prdRegionColumn(map(rows, "region")) + fields <- map(rows, "fields") + if (length(rows) == 0L) { + return(.prdEmptyFineMapping(df, context, trait)) + } + if (.prdIsQtl(df, fields, context, trait)) { + return(.prdBuildQtl(rows, fields, regions, label)) + } + .prdRequireIdentity(fields, c("study", "method"), label) + GwasFineMappingResult( + studyName = .prdRowColumn(fields, "study"), + method = .prdRowColumn(fields, "method"), + entry = map(rows, "entry"), + blockId = .prdRowColumn(fields, "blockId"), + region = regions, + dataType = .prdRowColumn(fields, "dataType"), + dataSource = .prdRowColumn(fields, "dataSource"), + weightStandardized = .prdStandardizedColumn(fields) + ) +} + + +# A header-only file reads back as an EMPTY collection of the class its +# columns describe, rather than as an error about identity axes it has no +# rows to record. An empty export is a legitimate output -- a region where +# every entry was screened out still writes its schema. +# @noRd +.prdEmptyFineMapping <- function(df, context, trait) { + if (.prdIsQtl(df, list(), context, trait)) { + return(QtlFineMappingResult( + studyName = character(0), + context = character(0), + trait = character(0), + method = character(0), + entry = list() + )) + } + GwasFineMappingResult( + studyName = character(0), + method = character(0), + entry = list() + ) +} + +# @noRd +.prdBuildQtl <- function(rows, fields, regions, label) { + .prdRequireIdentity( + fields, + c("study", "context", "trait", "method"), + label + ) + QtlFineMappingResult( + studyName = .prdRowColumn(fields, "study"), + context = .prdRowColumn(fields, "context"), + trait = .prdRowColumn(fields, "trait"), + method = .prdRowColumn(fields, "method"), + entry = map(rows, "entry"), + jointStudies = .prdRowColumn(fields, "jointStudies"), + jointContexts = .prdRowColumn(fields, "jointContexts"), + jointTraits = .prdRowColumn(fields, "jointTraits"), + region = regions, + dataType = .prdRowColumn(fields, "dataType"), + dataSource = .prdRowColumn(fields, "dataSource"), + weightStandardized = .prdStandardizedColumn(fields) + ) +} + +# @noRd +.prdIsQtl <- function(df, fields, context, trait) { + if (!is.null(context) || !is.null(trait)) { + return(TRUE) + } + if (nrow(df) == 0L) { + return(any(is_in(c("context", "trait"), names(df)))) + } + !is.null(.prdRowColumn(fields, "context")) || + !is.null(.prdRowColumn(fields, "trait")) +} + +# @noRd +.prdStandardizedColumn <- function(fields) { + v <- .prdRowColumn(fields, "standardized") + if (is.null(v)) NULL else as.logical(v) +} + +# ---- entry point: TWAS weights ---------------------------------------------- + +#' @title Read Published TWAS Weights +#' @description Rebuild a \code{TwasWeights} from a file +#' \code{\link{exportToBed}} or \code{\link{exportToVcf}} wrote, or from any +#' file in the same vocabulary. +#' +#' A merged table reads back as a multi-row collection, one row per +#' \code{(study, context, trait, method)} tuple. A fitted model whose +#' weights were a MATRIX -- a joint fit, one column per member -- was +#' published as one block per member with \code{trait} naming it, so it +#' reads back as one row per member with \code{joint_traits} keeping the +#' membership. The weights are identical either way; only the packing +#' differs. +#' +#' @section What is required, and what is not: +#' Variant ids that encode coordinates (or the columns to build them from) +#' and a \code{WGT} column -- a weight is the whole content of a +#' \code{TwasWeights}. The four identity axes must be recorded or supplied, +#' because the collection is keyed by them. Everything else is optional and +#' carried through if present. +#' +#' Cross-validation metrics are never published (they describe a model +#' scored on held-out folds, which is not per-variant and not +#' data-secure to express per sample), so a read-back collection has no +#' \code{cvResult} and no fitted models. +#' +#' @inheritParams readFineMappingResult +#' @param weightStandardized Whether the published weights are on the +#' standardized genotype scale. Required when the file does not record it: +#' the scale is not recoverable from the weights themselves. +#' @return A \code{TwasWeights}. +#' @seealso \code{\link{exportToBed}}, \code{\link{readFineMappingResult}} +#' @examples +#' data(twasWeightsExample) +#' path <- file.path(tempdir(), "weights.bed") +#' exportToBed(twasWeightsExample, path) +#' readTwasWeights(path) + +#' @export +readTwasWeights <- function( + path, + region = NULL, + format = c("auto", "bed", "vcf"), + studyName = NULL, + context = NULL, + trait = NULL, + method = NULL, + dataType = NULL, + weightStandardized = NULL, + sampleSelect = NULL +) { + label <- "readTwasWeights" + src <- .prdRead(path, region, format, sampleSelect, label) + df <- src$table + if (!is_in("WGT", names(df))) { + abort(glue( + "{label}: no `WGT` column. The weight is the whole content of a ", + "TwasWeights. For a fine-mapping table use ", + "readFineMappingResult()." + )) + } + .prdRequireStandardized(df, weightStandardized, label) + supplied <- compact(list( + study = studyName, + context = context, + trait = trait, + method = method, + dataType = dataType, + standardized = weightStandardized + )) + rows <- imap( + .prdGroups(df), + .prdTwRow, + df = df, + supplied = supplied, + label = label + ) + if (length(rows) == 0L) { + return(TwasWeights( + studyName = character(0), + context = character(0), + trait = character(0), + method = character(0), + entry = list() + )) + } + fields <- map(rows, "fields") + .prdRequireIdentity( + fields, + c("study", "context", "trait", "method"), + label + ) + TwasWeights( + studyName = .prdRowColumn(fields, "study"), + context = .prdRowColumn(fields, "context"), + trait = .prdRowColumn(fields, "trait"), + method = .prdRowColumn(fields, "method"), + entry = map(rows, "entry"), + jointStudies = .prdRowColumn(fields, "jointStudies"), + jointContexts = .prdRowColumn(fields, "jointContexts"), + jointTraits = .prdRowColumn(fields, "jointTraits"), + region = .prdRegionColumn(map(rows, "region")) + ) +} + +# One TwasWeightsRow from one group of published rows. +# +# The ids go through `.prdCanonical()` so they are derived exactly as the +# fine-mapping reader derives them. Variant-level columns BEYOND the weight +# are dropped: a TwasWeightsRow holds alleles and a weight, and the published +# `AF` came from the collection's LD panel, which is not in the file either. +# @noRd +.prdTwRow <- function(idx, key, df, supplied, label) { + fields <- .prdRowFields(df, idx, supplied, label) + sub <- slice(df, idx) + tl <- .prdCanonical(sub, fields$method, NULL) + list( + fields = fields, + region = .prdRowRegion(df, idx, label), + entry = twasWeightsRow( + variantIds = tl$variant_id, + weights = suppressWarnings(as.numeric(sub$WGT)), + weightStandardized = isTRUE(.prdFlag(fields$standardized)), + weightsDataType = .prdNaNull(fields$dataType) + ) + ) +} + +# @noRd +.prdNaNull <- function(v) { + if (is.null(v) || length(v) == 0L || is.na(v)) NULL else as.character(v) +} diff --git a/R/resultWriters.R b/R/resultWriters.R new file mode 100644 index 00000000..ed566cb1 --- /dev/null +++ b/R/resultWriters.R @@ -0,0 +1,2626 @@ +# ============================================================================= +# Published result writers +# ----------------------------------------------------------------------------- +# Every pecotmr output format that leaves the package as a FILE lives here, +# behind TWO generics rather than one entry point per class: +# +# exportToVcf(x, path, ...) GWAS-VCF-style: one record per variant, one +# SAMPLE per study / fitted model, the statistics +# as FORMAT fields. GwasSumStats, every +# FineMappingResult, TwasWeights. +# exportToBed(x, path, ...) the published long-format tables, one row per +# published observation, bgzipped + tabix-indexed +# on request. FineMappingResult, TwasWeights. +# +# One file because they are one problem -- project an S4 collection onto a +# FIXED published schema, join the posterior view to the marginal one, sort +# genomically, compress and index -- and while the VCF and the BED writers were +# separate each kept its own copy of the column pick, the row narrowing, the +# identity-key join and the fullFit column scan, which had already drifted +# apart. +# +# Five layers, in order: +# .pub* the shared layer: column pick, identity keys, row narrowing, the +# posterior/marginal join, genomic order, the table writer, +# output-path decoration +# .vcf* the VariantAnnotation-backed VCF / BCF writer +# .twv* the TWAS-weight VCF projection (weights as per-model samples) +# .fmb* the fine-mapping BED schemas (topLoci, fsusie) +# .twb* the TWAS-weight BED schemas (weights, performance) +# ============================================================================= + +#' @include AllGenerics.R AllClasses.R gwasSumStats.R twasWeights.R +#' @importFrom S4Vectors DataFrame SimpleList mcols +#' @importFrom GenomicRanges GRanges seqnames +#' @importFrom IRanges DataFrameList CharacterList +#' @importFrom Biostrings DNAStringSet DNAStringSetList +#' @importFrom Rsamtools asBcf bgzip indexTabix +#' @importFrom tools file_ext +#' @importFrom stringr str_sub +#' @importFrom purrr map map_chr keep set_names list_flatten +NULL + +# ---- shared: schema assembly ------------------------------------------------ + +# The identity axes a view is split on when several collection rows are folded +# into one published table. `blockId` is deliberately absent: the fSuSiE views +# do not carry it, and both sides of a join must key on the same axes. +# @noRd +.PUB_ROW_COLS <- c("study", "context", "trait", "method") + +# The identity axes a per-VARIANT join keys on -- the row identity plus the +# variant itself. +# @noRd +.PUB_VARIANT_COLS <- c( + "study", + "context", + "trait", + "blockId", + "method", + "variant_id" +) + +# Column `col` of `df`, or NA when the view does not carry it. Published +# schemas are fixed, so a column a particular input happens not to have becomes +# an NA cell rather than a missing column. `n` makes that NA a full-length +# vector, which the VCF FORMAT matrices need; the BED writers let the table +# recycle a scalar instead. +# @noRd +.pubCol <- function(df, col, n = NULL) { + if (!is.null(df) && is_in(col, names(df))) { + return(df[[col]]) + } + if (is.null(n)) NA else rep(NA, n) +} + +# The row-identity key a published view joins on: the identity columns the +# frame actually carries, "\r"-joined. A bare single-entry view carries none of +# them, so the key degenerates to whatever is present (the variant alone), +# which is exactly the right join for one entry. +# @noRd +.pubIdentityKey <- function(df, cols) { + present <- intersect(cols, names(df)) + if (length(present) == 0L) { + return(rep("", nrow(df))) + } + exec(str_c, !!!map(as.list(df[present]), .pubKeyPart), sep = "\r") +} + +# The same key read off a COLLECTION's tuple columns, so a long-format view's +# rows can be matched back to the collection row they came from. +# `.tupleColumn()` not `[[`: on a RangedTupleList `[[` extracts an ELEMENT, +# while the identity axes live in mcols. +# @noRd +.pubCollectionKey <- function(x, cols) { + present <- intersect(cols, .tupleColumnNames(x)) + exec( + str_c, + !!!map(map(present, .pubTupleColumn, x = x), .pubKeyPart), + sep = "\r" + ) +} + +# @noRd +.pubTupleColumn <- function(nm, x) as.character(.tupleColumn(x, nm)) + +# One component of an identity key. str_c() propagates NA, so a single absent +# axis (a GWAS result has no trait) would make the WHOLE key NA, losing the +# axes that ARE set -- and match() pairs NA with NA, so two unrelated rows +# would silently join. Spelling the absent value keeps NA matching NA within a +# column, which is what an absent axis means, without erasing the rest. +# @noRd +.pubKeyPart <- function(v) { + chr <- as.character(v) + if_else(is.na(chr), "", chr) +} + +# "chr22_10000000_18000000" -- the region_ID publication tables key on. +# @noRd +.pubRegionIds <- function(gr) { + if (!methods::is(gr, "GRanges") || length(gr) == 0L) { + return(character(0)) + } + str_c(as.character(seqnames(gr)), start(gr), end(gr), sep = "_") +} + +# A vector packed into one cell -- the published spelling for a column holding +# a curve, a grid or a set of names. +# @noRd +.pubPack <- function(z) str_flatten(as.character(z), collapse = ";") + +# The per-CS variant-level columns a fullFit topLoci build adds: the +# `within_cs_pip` scalar plus the wide within_cs_pip_ / cs_logbf_ / +# cs_effect_ sets. Data-driven in both the VCF and the BED writer, so the +# pattern lives once. +# @noRd +.pubFullFitColumns <- function(df) { + names(df)[str_detect(names(df), "^(within_cs_pip|cs_logbf_|cs_effect_)")] +} + +# Align a marginalEffects view to a posterior (topLoci) view, row for row, so +# the two can be read as one table. Both sides are keyed on (row identity, +# variant): across a whole collection that keeps two rows contributing the same +# variant apart, and for a single entry -- whose views carry no identity +# columns -- it degenerates to a variant_id join. +# +# `[` rather than slice(): a posterior variant with no marginal record must +# produce an NA row, and slice() rejects the NA index that expresses it. +# @noRd +.pubJoinMarginals <- function(base, marginals) { + idx <- match( + .pubIdentityKey(base, .PUB_VARIANT_COLS), + .pubIdentityKey(marginals, .PUB_VARIANT_COLS) + ) + marginals[idx, , drop = FALSE] +} + +# Fold a list of collections into one, choosing the class's own combiner. The +# `list` methods exist so a genome's worth of per-region RDSes can be exported +# in one call -- the combiners also handle the single-element case, so there is +# no separate concatenate-and-sort step. +# @noRd +.pubCombineList <- function(x) { + if (length(x) == 0L) { + abort("`x` must contain at least one result to export.") + } + if (methods::is(x[[1L]], "TwasWeights")) { + return(combineTwasWeights(x)) + } + if (methods::is(x[[1L]], "FineMappingResultBase")) { + return(combineFineMappingResults(x)) + } + abort(glue( + "`x` must be a TwasWeights or FineMappingResult object, or a list ", + "of one of those; got a list of {class(x[[1L]])[[1L]]}." + )) +} + +# Row indices whose identity axes equal every supplied selector. Axes the +# collection does not carry are ignored, so one call addresses a GWAS result +# (no context, no trait) and a QTL one alike. +# @noRd +.pubMatchingRows <- function(x, studyName, context, trait, method) { + cols <- .tupleColumnNames(x) + selectors <- compact(list( + study = studyName, + context = if (is_in("context", cols)) context, + trait = if (is_in("trait", cols)) trait, + method = method + )) + reduce( + names(selectors), + .pubNarrowRows, + x = x, + selectors = selectors, + .init = seq_len(nrow(x)) + ) +} + +# Narrow a row-index vector to those rows whose `axis` column equals the +# caller's requested value. `.tupleColumn()` not `[[`: on a RangedTupleList +# `[[` extracts an ELEMENT, while the identity axes live in mcols. +# @noRd +.pubNarrowRows <- function(rows, axis, x, selectors) { + rows[as.character(.tupleColumn(x, axis))[rows] == selectors[[axis]]] +} + +# Which of a source view's OWN columns to append after the published schema. +# The published columns always lead -- that is what makes the format +# publishable -- so this only ever ADDS: NULL adds nothing, "all" adds every +# column the view carries that the schema did not already map, and a character +# vector adds exactly those. +# +# `mapped` names the source columns the schema already publishes (under their +# published spellings) plus the GRanges range columns, which duplicate the +# position columns. Without that, "all" would write the same value twice under +# two names, and an explicitly named one would collide outright. +# @noRd +.pubViewColumns <- function(view, columns, mapped) { + if (is.null(columns)) { + return(character(0)) + } + available <- setdiff(names(view), mapped) + if (identical(columns, "all")) { + return(available) + } + assertCharacter(columns, any.missing = FALSE, .var.name = "columns") + .pubCheckViewColumns(columns, view, available, mapped) + columns +} + +# @noRd +.pubCheckViewColumns <- function(columns, view, available, mapped) { + clash <- intersect(columns, mapped) + if (length(clash) > 0L) { + abort(glue( + "`columns` names {str_flatten(clash, ', ')}, which the published ", + "schema already writes; drop them." + )) + } + unknown <- setdiff(columns, names(view)) + if (length(unknown) > 0L) { + abort(glue( + "`columns` names {str_flatten(unknown, ', ')}, which this view ", + "does not carry. Available: {str_flatten(available, ', ')}." + )) + } + invisible(NULL) +} + +# Refuse to export a collection whose rows lack a FULL-DATA fit. +# +# Cross-validation alone is not publishable. A CV-only run -- which is what +# `twasWeightsPipeline(fitFullData = FALSE)` produces, and which is legal only +# WITH cross-validation -- stores each row as an empty element with no weights +# and nothing but its `cvResult`. All it holds is fold-level results, and those +# cannot go in a published file: fold membership is sample-level information +# and sample names can be protected. So an export needs the full-data fit, and +# its absence is an ERROR rather than a silently empty output file. +# +# The check is on the OBJECT, not on the flag: `fitFullData` is a pipeline +# argument that may not have been the thing that emptied a row, and an export +# has to be safe whatever produced its input. +# +# It also keeps genomicRegion() honest. That derives a row's analysis window +# from range(x) when no `region` column was recorded, and a variant-less row +# contributes no range -- so the derived vector would be SHORTER than the +# collection and indexing it by row number would read a neighbour's window. +# @noRd +.pubRequireFullFit <- function(x, incomplete, what) { + if (length(incomplete) == 0L) { + return(invisible(x)) + } + abort(glue( + "{what}: {length(incomplete)} of {nrow(x)} row(s) carry no ", + "full-data fit, only cross-validation results -- ", + "{str_flatten(.pubRowLabels(x, incomplete), ', ')}. Fold-level ", + "results cannot be published (fold membership is sample-level ", + "information), so refit with `fitFullData = TRUE` and export that. ", + "The cross-validation record stays available through cvResult()." + )) +} + +# Rows of a TwasWeights with no full-data weights: a CV-only row has an empty +# element AND no `weight` metadata column (twasWeightsRow() drops the column +# when `weights` is NULL). +# @noRd +.twbIncompleteRows <- function(x) { + which( + lengths(x) == 0L | !map_lgl(seq_len(nrow(x)), .twbRowHasWeights, x = x) + ) +} + +# @noRd +.twbRowHasWeights <- function(i, x) !is.null(mcols(x[[i]])$weight) + +# Rows of a FineMappingResult with no fitted variants. Fine-mapping has no +# CV-only mode today -- .fmAttachCv() rebuilds the row around its existing +# susieFit and topLoci, so CV is strictly additive -- but the export checks +# rather than assumes it. +# @noRd +.fmbIncompleteRows <- function(x) { + which(lengths(x) == 0L) +} + +# Identity labels for the named rows, for an error message. +# @noRd +.pubRowLabels <- function(x, rows) { + keys <- .pubCollectionKey(x, .PUB_ROW_COLS) + str_replace_all(keys[rows], "\r", "|") +} + +# ---- shared: genomic order -------------------------------------------------- + +#' Order rows by genomic position, with chromosomes sorted naturally +#' +#' Returns the permutation that puts rows in genomic order: chromosomes +#' 1..22 numerically, then the non-numeric contigs (X, Y, MT, ...) +#' alphabetically after all of them, and positions numerically within a +#' chromosome. A plain character sort puts chr10 before chr2, which is why +#' published tables need this. +#' +#' Shared by every pecotmr writer and by the pipeline wrappers that merge +#' per-region tables, so one definition of "genomic order" covers both. +#' +#' @param chrom Chromosome labels. A leading \code{"chr"} is optional and is +#' ignored for ordering, so \code{"chr2"} and \code{"2"} sort alike. +#' @param start Start positions; coerced to numeric, non-numeric entries sort +#' last within their chromosome. +#' @param end Optional second position key, used only to break ties on +#' \code{start} (a half-open BED's end column). Ignored when it is not +#' numeric, so a table whose third column is not a position (an allele, say) +#' can pass it harmlessly. +#' @return An integer permutation, as from \code{order()}. +#' @examples +#' genomicOrder(c("chr10", "chr2", "chrX", "chr2"), c(5, 90, 1, 10)) +#' @export +genomicOrder <- function(chrom, start, end = NULL) { + rank <- .chromRank(chrom) + pos <- suppressWarnings(as.numeric(start)) + tie <- suppressWarnings(as.numeric(end)) + if (is.null(end) || all(is.na(tie))) { + return(order(rank, as.character(chrom), pos)) + } + order(rank, as.character(chrom), pos, tie) +} + +# The sort key behind genomicOrder(): numeric contigs keep their number, the +# rest sort alphabetically among themselves after every numeric one. A key for +# ONE vector, not a global scale -- the non-numeric contigs are ranked among +# themselves, so values are comparable only within a single call (which is all +# order() needs). +# @noRd +.chromRank <- function(chrom) { + bare <- str_remove(as.character(chrom), "^chr") + num <- suppressWarnings(as.numeric(bare)) + alpha <- sort(unique(bare[is.na(num)])) + if_else(is.na(num), 1000 + match(bare, alpha), num) +} + +# ---- shared: the table writer ----------------------------------------------- + +# Append caller-supplied trailing columns. Publication tables sometimes carry +# a field no S4 object can know -- the legacy top-loci BED's `block_id` is +# parsed from the RDS FILENAME -- so the writer takes them rather than forcing +# the caller to re-open and rewrite the file it just produced. +# @noRd +.pubAppendExtra <- function(out, extraColumns) { + if (is.null(extraColumns) || length(extraColumns) == 0L) { + return(out) + } + if (!is.list(extraColumns) || !is_named(extraColumns)) { + abort("`extraColumns` must be a NAMED list of columns.") + } + clash <- intersect(names(extraColumns), names(out)) + if (length(clash) > 0L) { + abort(glue( + "`extraColumns` would overwrite the published column(s) ", + "{str_flatten(clash, ', ')}; rename them." + )) + } + reduce( + names(extraColumns), + .pubAppendOneExtra, + extra = extraColumns, + .init = out + ) +} + +# @noRd +.pubAppendOneExtra <- function(out, nm, extra) { + v <- extra[[nm]] + if (!is_in(length(v), c(1L, nrow(out)))) { + abort(glue( + "`extraColumns${nm}` has length {length(v)}; it must be 1 ", + "(recycled) or {nrow(out)} (one value per row)." + )) + } + out[[nm]] <- if (nrow(out) == 0L) v[0L] else v + out +} + +# Write a table as a published BED: plain when `path` has no .gz/.bgz +# extension, bgzipped + tabix-indexed when it has. Rsamtools rather than a +# shell bgzip/tabix, so no pipeline needs an htslib binary on PATH. +# +# The builders already order their columns to the schema, so this takes the +# frame as given; it supplies the header for an empty result and drops the +# columns that had nothing to say. +# +# utils::write.table(), not vroom_write(): the two differ in how they format a +# float (1e-06 vs 1e-6) and in how many digits they keep. +# @noRd +# `emptySchema` is the header an empty result still documents -- the schema of +# whichever table was being written, since the fsusie table's vocabulary is not +# the per-variant one. +.pubWriteTable <- function( + out, + path, + naString, + index, + emptySchema = .pubSchema(character(0)), + keepColumns = character(0), + extraColumns = NULL +) { + if (is.null(out) || nrow(out) == 0L) { + # A header-only file documents the whole vocabulary rather than + # collapsing to the required core, which is what dropping empty + # columns would do when every column is vacuously absent. + out <- as_tibble( + set_names( + rep(list(character()), length(emptySchema)), + emptySchema + ), + .name_repair = "minimal" + ) + } else { + out <- .pubDropEmpty(out, keepColumns) + } + out <- .pubAppendExtra(out, extraColumns) + if (nrow(out) > 0L) { + out <- slice(out, genomicOrder(out[["#chr"]], out[["start"]])) + } + .pubEmitTable(out, path, naString, index) +} + +# Materialise the ordered table at `path`, compressing and indexing it when the +# extension asks for it. +# @noRd +.pubEmitTable <- function(out, path, naString, index) { + dir.create(dirname(path), showWarnings = FALSE, recursive = TRUE) + compress <- str_detect(path, "\\.(gz|bgz)$") + plain <- if (compress) str_remove(path, "\\.(gz|bgz)$") else path + utils::write.table( + out, + plain, + sep = "\t", + quote = FALSE, + row.names = FALSE, + na = naString + ) + if (!compress) { + return(invisible(path)) + } + bgzip(plain, dest = path, overwrite = TRUE) + file.remove(plain) + # An empty table has no records to index, and indexTabix() errors on one. + if (index && nrow(out) > 0L) { + indexTabix(path, format = "bed") + } + invisible(path) +} + +# ---- shared: output-path decoration ----------------------------------------- + +# Decorate `outputPath` with the spec's context / trait tags when split flags +# are set. Preserves the file extension. Examples: "out.vcf" + (context="brain") +# -> "out.brain.vcf" "out.vcf.bgz" + (context="brain", trait="ENSG1") -> +# "out.brain.ENSG1.vcf.bgz" +# @noRd +.decorateOutputPath <- function( + outputPath, + spec, + splitByContext, + splitByTrait +) { + if (!isTRUE(splitByContext) && !isTRUE(splitByTrait)) { + return(outputPath) + } + ext <- str_to_lower(tools::file_ext(outputPath)) + composite <- ext == "bgz" || ext == "gz" + base <- if (composite) { + str_remove( + outputPath, + regex("\\.[^.]+\\.(bgz|gz)$", ignore_case = TRUE) + ) + } else { + tools::file_path_sans_ext(outputPath) + } + ext_keep <- str_sub( + outputPath, + str_length(base) + 1L, + str_length(outputPath) + ) + tags <- c( + if (isTRUE(splitByContext)) .pubPathTag(spec$context), + if (isTRUE(splitByTrait)) .pubPathTag(spec$trait) + ) + if (length(tags) == 0L) { + return(outputPath) + } + str_c(base, ".", str_flatten(tags, "."), ext_keep) +} + +# One path tag, or NULL when the axis carries no usable value (a context-less +# result split by context keeps its undecorated path). +# @noRd +.pubPathTag <- function(v) { + if (is.null(v) || is.na(v) || str_length(v) == 0L) { + return(NULL) + } + v +} + +# ---- VCF: entry points ------------------------------------------------------ + +#' @rdname exportToVcf +#' @export +setMethod( + "exportToVcf", + signature("list"), + function(x, outputPath, sampleName = NULL, ...) { + exportToVcf( + .pubCombineList(x), + outputPath, + sampleName = sampleName, + ... + ) + } +) + +#' @rdname exportToVcf +#' @export +setMethod( + "exportToVcf", + signature("GwasSumStats"), + function(x, outputPath, sampleName = NULL, studyName = NULL) { + .vcfRequireVariantAnnotation() + studyName <- .vcfResolveStudy(x, studyName) + ss <- sumStats(x, studyName = studyName) + mc <- mcols(ss) + .writeVcfImpl( + chrom = as.character(seqnames(ss)), + pos = start(ss), + ref = mc$A2, + alt = mc$A1, + snpIds = mc$SNP, + geno = .vcfSumstatsGeno(mc, length(ss)), + genoHeader = .vcfSumstatsGenoHeader(), + sampleName = sampleName %||% studyName, + outputPath = outputPath + ) + } +) + +# VariantAnnotation is a Suggests, and every VCF path needs it. +# @noRd +.vcfRequireVariantAnnotation <- function() { + if (!requireNamespace("VariantAnnotation", quietly = TRUE)) { + abort("Package 'VariantAnnotation' is required for exportToVcf") + } + invisible(NULL) +} + +# Select which study to write (a GwasSumStats can hold many); a single-study +# collection defaults to its one study. +# @noRd +.vcfResolveStudy <- function(x, studyName) { + if (!is.null(studyName)) { + return(studyName) + } + if (nrow(x) != 1L) { + msg <- glue( + "This GwasSumStats has {nrow(x)} studies. ", + "Pass `study = ` to select one." + ) + abort(msg) + } + as.character(x$study[[1L]]) +} + +# Per-sample geno matrices for the GWAS-VCF fields the mcols can fill. +# +# Three released bugs are fixed here, all of them the same mistake -- a +# reserved key carrying the wrong quantity: +# * ES held the Z-SCORE. The spec defines ES as "effect size estimate +# relative to the alternative allele", and defines EZ for a z "provided if +# it was used to derive the ES and SE fields" -- which is exactly our case. +# * SS is not a GWAS-VCF key at all. The spec's sample-size field is NS. +# * AF held MAF. The spec's AF is the ALTERNATIVE allele frequency, and a +# minor-allele frequency cannot be converted to one -- it has lost which +# allele it refers to -- so it goes under our own MAF key instead of being +# mislabelled. +# @noRd +.vcfSumstatsGeno <- function(mc, nSnps) { + compact(list( + ES = .vcfSumstatsField(mc, "BETA", nSnps), + SE = .vcfSumstatsField(mc, "SE", nSnps), + EZ = .vcfSuppliedZ(mc, nSnps), + NS = .vcfSumstatsField(mc, "N", nSnps, as.integer), + AF = .vcfSumstatsField(mc, "AF", nSnps), + MAF = .vcfSumstatsField(mc, "MAF", nSnps) + )) +} + +# `EZ` only where the SOURCE supplied the z. The spec defines the field as the +# z "provided if it was used to derive the ES and SE fields", so publishing a +# Wald z that pecotmr computed from a recorded beta/se inverts its meaning -- +# a reader would then believe ES and SE were derived from it. The loaders +# record `z_supplied` for exactly this; a collection built by hand without it +# is treated as not having supplied one, which is the safe direction. +# @noRd +.vcfSuppliedZ <- function(mc, nSnps) { + z <- .vcfSumstatsField(mc, "Z", nSnps) + if (is.null(z)) { + return(NULL) + } + supplied <- as.logical(.pubCol(mc, "z_supplied", nSnps)) + if (!any(isTRUE(supplied) | (!is.na(supplied) & supplied))) { + return(NULL) + } + matrix(replace(as.numeric(z), !supplied | is.na(supplied), NA_real_), nSnps) +} + +# @noRd +.vcfSumstatsField <- function(mc, nm, nSnps, coerce = as.numeric) { + if (!is_in(nm, colnames(mc))) { + return(NULL) + } + matrix(coerce(mc[[nm]]), nSnps) +} + +# The FORMAT header for the sumstats geno fields, with the spec's own wording +# for the keys it defines. +# @noRd +.vcfSumstatsGenoHeader <- function() { + DataFrame( + Number = rep("A", 6L), + Type = c("Float", "Float", "Float", "Integer", "Float", "Float"), + Description = c( + "Effect size estimate relative to the alternative allele", + "Standard error of effect size estimate", + str_c( + "Z-score provided if it was used to derive the ES and SE ", + "fields" + ), + str_c( + "Variant-specific number of samples/individuals with called ", + "genotypes used to test association with specified trait" + ), + "Alternative allele frequency in trait subset", + "Minor allele frequency (not the spec's AF: allele unknown)" + ), + row.names = c("ES", "SE", "EZ", "NS", "AF", "MAF") + ) +} + +#' @rdname exportToVcf +#' @export +setMethod( + "exportToVcf", + signature("FineMappingResultBase"), + function( + x, + outputPath, + sampleName = NULL, + studyName = NULL, + context = NULL, + trait = NULL, + method = NULL, + splitByContext = FALSE, + splitByTrait = FALSE + ) { + .vcfRequireVariantAnnotation() + # Resolve what goes in each FILE. Without a split flag every matching + # fit is a SAMPLE of one file; with one, each fit gets its own file at + # a decorated path. + groups <- .resolveFineMappingGroups( + x, + outputPath = outputPath, + studyName = studyName, + context = context, + trait = trait, + method = method, + splitByContext = splitByContext, + splitByTrait = splitByTrait + ) + invisible(map_chr( + groups, + .fmvWriteGroup, + x = x, + sampleName = sampleName + )) + } +) + +# Resolve what each OUTPUT FILE holds. Without a split flag every matching fit +# becomes a SAMPLE of one multi-sample VCF -- which is GWAS-VCF's own shape, +# where the samples are the studies -- so comparing two methods over a region +# means reading one file rather than two. `splitByContext` / `splitByTrait` +# still emit one single-sample file per fit, at a decorated path. +# @noRd +.resolveFineMappingGroups <- function( + x, + outputPath, + studyName, + context, + trait, + method, + splitByContext, + splitByTrait +) { + rows <- .pubMatchingRows(x, studyName, context, trait, method) + if (length(rows) == 0L) { + abort("exportToVcf: no rows match the supplied selectors.") + } + specs <- map(rows, .rowSpec, x = x) + if (!isTRUE(splitByContext) && !isTRUE(splitByTrait)) { + return(list(list(path = outputPath, specs = specs))) + } + map( + specs, + .fmvSplitGroup, + outputPath = outputPath, + splitByContext = splitByContext, + splitByTrait = splitByTrait + ) +} + +# @noRd +.fmvSplitGroup <- function(spec, outputPath, splitByContext, splitByTrait) { + list( + path = .decorateOutputPath( + outputPath, + spec, + splitByContext, + splitByTrait + ), + specs = list(spec) + ) +} + +# Build a (study, context, trait, method) spec list for one row index. +# @noRd +.rowSpec <- function(r, x) { + list( + row = r, + study = as.character(x$study)[r], + context = if (is_in("context", .tupleColumnNames(x))) { + as.character(x$context)[r] + } else { + NA_character_ + }, + trait = if (is_in("trait", .tupleColumnNames(x))) { + as.character(x$trait)[r] + } else { + NA_character_ + }, + method = as.character(x$method)[r] + ) +} + +# ---- VCF: FORMAT field specs ------------------------------------------------ + +# One VCF FORMAT field spec: name, per-variant `values`, VCF `type` +# (Float/Integer), and header `desc`. Number is always "A" (per-allele). +# @noRd +.vcfSpec <- function(name, values, type, desc) { + list(name = name, values = values, type = type, desc = desc) +} + +# The per-variant FORMAT fields, named exactly as the BED columns are: the +# spec's keys for the quantities it defines, ours alongside them for the rest. +# +# `ES` is the MARGINAL effect and nothing else. It used to hold the posterior +# conditional effect when the fit reported one and the marginal beta otherwise, +# so a consumer could not tell which quantity it had. The posterior effect now +# has its own field -- `WGT`, which is also the prediction weight, because a +# susie posterior mean IS the TWAS coefficient. +# @noRd +.vcfCoreSpecs <- function(base, m, nSnps) { + p <- suppressWarnings(as.numeric(.pubCol(m, "p", nSnps))) + list( + .vcfSpec( + "ES", + .pubCol(m, "beta", nSnps), + "Float", + "Effect size estimate relative to the alternative allele" + ), + .vcfSpec( + "SE", + .pubCol(m, "se", nSnps), + "Float", + "Standard error of effect size estimate" + ), + .vcfSpec( + "EZ", + .pubEz(m, nSnps), + "Float", + str_c( + "Z-score provided if it was used to derive the ES and SE ", + "fields" + ) + ), + .vcfSpec( + "LP", + if_else(is.na(p) | p <= 0, NA_real_, -log10(p)), + "Float", + "-log10 p-value for effect estimate" + ), + .vcfSpec( + "NS", + as.integer(.pubCol(m, "N", nSnps)), + "Integer", + str_c( + "Variant-specific number of samples/individuals with called ", + "genotypes used to test association with specified trait" + ) + ), + .vcfSpec( + "AF", + .pubCol(base, "af", nSnps), + "Float", + "Alternative allele frequency in trait subset" + ), + .vcfSpec( + "WGT", + .pubCol(base, "posterior_effect", nSnps), + "Float", + "Posterior effect estimate, i.e. the prediction weight" + ), + .vcfSpec( + "WGTSE", + .pubCol(base, "posterior_se", nSnps), + "Float", + "Standard error of the posterior effect estimate" + ) + ) +} + +# Credible-set FORMAT fields. pecotmr emits CS (+ PUR when +# a purity column exists) for EVERY cs_ column present, so coverage +# levels are data-driven rather than the 50/70/95 xqtl-protocol default. +# @noRd +.vcfCsSpecs <- function(base) { + csCols <- names(base)[str_detect(names(base), "^cs_[0-9.]+$")] + perCol <- map(csCols, .vcfCsColSpecs, base = base) + list_flatten(perCol) +} + +# Per-CS variant-level fullFit FORMAT fields (within_cs_pip scalar plus the +# wide within_cs_pip_ / cs_logbf_ / cs_effect_ sets emitted when +# the topLoci was built with fullFit = TRUE). Each column becomes an uppercase +# field. +# @noRd +.vcfFullFitSpecs <- function(base) { + map(.pubFullFitColumns(base), .vcfFullFitColSpec, base = base) +} + +# Posterior FORMAT fields, only when a fine-mapping posterior table exists: +# PIP, per-variant log Bayes factor, local false sign rate, plus the dynamic +# credible-set and fullFit column sets. +# @noRd +.vcfPosteriorSpecs <- function(base, nSnps) { + fixed <- list( + .vcfSpec( + "PIP", + .pubCol(base, "pip", nSnps), + "Float", + "Posterior inclusion probability" + ), + .vcfSpec( + "LBF", + .pubCol(base, "logBF", nSnps), + "Float", + "Per-variant log Bayes factor (max single effect)" + ), + .vcfSpec( + "LFSR", + .pubCol(base, "lfsr", nSnps), + "Float", + "Local false sign rate (per-condition posterior)" + ) + ) + c(fixed, .vcfCsSpecs(base), .vcfFullFitSpecs(base)) +} + +# FORMAT specs for one cs_ column: a CS index field plus a PUR purity +# field when a matching *_purity column exists. +# @noRd +.vcfCsColSpecs <- function(cc, base) { + cov <- str_remove(cc, "^cs_") + csSpec <- .vcfSpec( + str_c("CS", cov), + .pubCsIndex(base[[cc]]), + "Integer", + glue("Credible-set index at {cov}% coverage") + ) + pc <- str_c(cc, "_purity") + if (!is_in(pc, names(base))) { + return(list(csSpec)) + } + pur <- suppressWarnings(as.numeric(base[[pc]])) + purSpec <- .vcfSpec( + str_c("PUR", cov), + pur, + "Float", + glue("Purity (min abs corr) of the {cov}% credible set") + ) + list(csSpec, purSpec) +} + +# The FORMAT spec for one fullFit posterior column (uppercased Float field). +# @noRd +.vcfFullFitColSpec <- function(cc, base) { + v <- suppressWarnings(as.numeric(base[[cc]])) + .vcfSpec( + str_to_upper(cc), + v, + "Float", + glue("Per-credible-set variant statistic ({cc})") + ) +} + +# TRUE when a FORMAT spec has at least one non-NA value (drop all-NA specs). +# @noRd +.vcfSpecHasValues <- function(s) { + any(!is.na(s$values)) +} + +# ---- VCF: fine-mapping assembly --------------------------------------------- + +# Resolve the per-variant body: the fine-mapping POSTERIOR (topLoci at +# signalCutoff 0 = every fitted variant, carrying pip / CS membership / +# conditional effect) joined to the MARGINAL univariate sumstats +# (marginalEffects) where those exist. mvSuSiE / fSuSiE have no marginal +# sumstats, so the posterior drives the variant set; univariate susie adds +# marginal ES=beta / SE / LP / AF on top. Returns list(base, m, hasPost). +# @noRd +.vcfResolveBody <- function(entry, sn) { + post <- try_fetch( + as_tibble(as.data.frame( + S4Vectors::mcols(topLoci(entry, signalCutoff = 0)) + )), + error = function(cnd) NULL + ) + marg <- try_fetch( + as_tibble(marginalEffects(entry)), + error = function(cnd) NULL + ) + hasPost <- !is.null(post) && nrow(post) > 0L + hasMarg <- !is.null(marg) && nrow(marg) > 0L + if (hasPost) { + base <- post + m <- if (hasMarg) .pubJoinMarginals(base, marg) else NULL + } else if (hasMarg) { + base <- marg + m <- marg + } else { + msg <- glue("exportToVcf: entry [{sn}] has no variants to write") + abort(msg) + } + list(base = base, m = m, hasPost = hasPost) +} + +# The geno header DataFrame for a set of FORMAT specs. +# @noRd +.vcfGenoHeader <- function(specs) { + DataFrame( + Number = rep("A", length(specs)), + Type = map_chr(specs, "type"), + Description = map_chr(specs, "desc"), + row.names = map_chr(specs, "name") + ) +} + +# Sample names: the caller's, or the derived ones. A caller-supplied vector has +# to name EVERY sample -- the samples are what the file is keyed on, so a +# partial naming would silently mislabel the rest. +# @noRd +.vcfSampleNames <- function(derived, sampleName) { + if (is.null(sampleName)) { + return(derived) + } + if (length(sampleName) != length(derived)) { + abort(glue( + "`sampleName` has length {length(sampleName)} but ", + "{length(derived)} model(s) will be written; pass one name per ", + "model or NULL to derive them." + )) + } + as.character(sampleName) +} + +# Write one file's worth of fine-mapping fits: the records are the UNION of +# their variants, each fit is a sample, and a fit that did not include a +# variant leaves its cells empty. +# @noRd +.fmvWriteGroup <- function(group, x, sampleName) { + rows <- map(group$specs, .fmvRowSpecs, x = x) + sn <- .vcfSampleNames(map_chr(rows, "sampleName"), sampleName) + variants <- .fmvVariantUnion(rows) + specs <- keep( + .fmvFormatSpecs(rows, variants$variant_id), + .vcfSpecHasValues + ) + .writeVcfImpl( + chrom = variants$chrom, + pos = variants$pos, + ref = variants$A2, + alt = variants$A1, + snpIds = variants$variant_id, + geno = set_names(map(specs, "values"), map_chr(specs, "name")), + genoHeader = .vcfGenoHeader(specs), + sampleName = sn, + outputPath = group$path, + sampleMeta = .fmvSampleMeta(rows, sn) + ) + group$path +} + +# One fit's body, its FORMAT specs, and the variants they are aligned to. +# @noRd +.fmvRowSpecs <- function(spec, x) { + entry <- fineMappingResult( + x, + spec$study, + spec$context, + spec$trait, + spec$method + ) + sn <- .fmvSampleLabel(spec) + body <- .vcfResolveBody(entry, sn) + nSnps <- nrow(body$base) + list( + sampleName = sn, + spec = spec, + perFit = .pubPerFitFields(x, spec$row, susieFit(entry)), + base = body$base, + nVariants = nSnps, + variantIds = as.character(.pubCol(body$base, "variant_id", nSnps)), + formats = c( + .vcfCoreSpecs(body$base, body$m, nSnps), + if (body$hasPost) .vcfPosteriorSpecs(body$base, nSnps) else list() + ) + ) +} + +# @noRd +.fmvSampleLabel <- function(spec) { + as.character(glue( + "{spec$study}|{spec$context %||% '_'}|", + "{spec$trait %||% '_'}|{spec$method}" + )) +} + +# The union of every sample's variants, in genomic order, carrying the alleles +# the first fit to report each one recorded. +# @noRd +.fmvVariantUnion <- function(rows) { + tbl <- bind_rows(map(rows, .fmvRowVariants)) + tbl <- slice(tbl, which(!duplicated(tbl$variant_id))) + slice(tbl, genomicOrder(tbl$chrom, tbl$pos)) +} + +# @noRd +.fmvRowVariants <- function(r) { + n <- r$nVariants + tibble( + variant_id = r$variantIds, + chrom = as.character(.pubCol(r$base, "chrom", n)), + pos = .pubCol(r$base, "pos", n), + A1 = as.character(.pubCol(r$base, "A1", n)), + A2 = as.character(.pubCol(r$base, "A2", n)) + ) +} + +# The multi-sample FORMAT specs: one per field name ANY sample reported, its +# values a nVariant x nSample matrix placed onto the union by variant id. The +# field set is the union because which posterior fields a fit reports depends +# on the method, and one file needs one FORMAT schema across its samples. +# @noRd +.fmvFormatSpecs <- function(rows, unionIds) { + fields <- unique(list_c(c( + list(character(0)), + map(rows, .fmvRowFormatNames) + ))) + map(fields, .fmvFormatSpec, rows = rows, unionIds = unionIds) +} + +# @noRd +.fmvRowFormatNames <- function(r) map_chr(r$formats, "name") + +# @noRd +.fmvFormatSpec <- function(nm, rows, unionIds) { + template <- compact(map(rows, .fmvFindFormat, nm = nm))[[1L]] + cols <- map( + rows, + .fmvFormatColumn, + nm = nm, + unionIds = unionIds, + type = template$type + ) + .vcfSpec(nm, exec(cbind, !!!cols), template$type, template$desc) +} + +# @noRd +.fmvFindFormat <- function(r, nm) detect(r$formats, .fmvFormatNamed, nm = nm) + +# @noRd +.fmvFormatNamed <- function(f, nm) identical(f$name, nm) + +# One sample's column of a FORMAT field. The NA fill matches the field's +# declared VCF type, so an Integer field does not become a Float the moment one +# sample is missing a variant. +# @noRd +.fmvFormatColumn <- function(r, nm, unionIds, type) { + fill <- if (identical(type, "Integer")) NA_integer_ else NA_real_ + out <- rep(fill, length(unionIds)) + f <- .fmvFindFormat(r, nm) + if (is.null(f)) { + return(out) + } + out[match(r$variantIds, unionIds)] <- f$values + out +} + +# The ##SAMPLE header block for a fine-mapping VCF: each fit's identity tuple +# and how many variants it contributed, so the file says which fits it holds +# without reparsing the sample names. +# @noRd +.fmvSampleMeta <- function(rows, sampleNames) { + tbl <- bind_rows(map(rows, .fmvSampleMetaRow)) + .pubSampleMetaFrame(tbl, sampleNames) +} + +# The `##SAMPLE` key for each per-fit field. +# +# The DATA vocabulary is shared between the two formats, but a structured VCF +# header key cannot be: Rsamtools' header parser splits the `<...>` on +# `,(?=[[:alnum:]]+=)`, so a key containing an underscore is not recognised as +# a key at all -- it glues onto the previous field's value, and everything +# after it on the line is lost. Six of these fields were unreadable for that +# reason. CamelCase is also what the GWAS-VCF spec's own SAMPLE keys use +# (`TotalVariants`, `StudyType`), so this follows the format's convention +# rather than departing from ours. This table is the only place the two +# spellings meet, and `.prdVcfSampleMeta()` reads them back through it. +# @noRd +.PUB_SAMPLE_KEYS <- c( + total_variants = "TotalVariants", + study = "Study", + context = "Context", + trait = "Trait", + method = "Method", + block_id = "BlockId", + data_type = "DataType", + data_source = "DataSource", + weight_standardized = "WeightStandardized", + joint_studies = "JointStudies", + joint_contexts = "JointContexts", + joint_traits = "JointTraits", + region_id = "RegionId", + region_start = "RegionStart", + region_end = "RegionEnd", + V = "V", + cs_index = "CsIndex" +) + +# The ##SAMPLE block as a DataFrame, with the fields that are empty for EVERY +# sample dropped -- the same rule the BED columns follow, so a header is not +# padded with `BlockId=.,JointStudies=.,V=.` for an input that has none. +# @noRd +.pubSampleMetaFrame <- function(tbl, sampleNames) { + kept <- names(tbl)[!map_lgl(tbl, .pubAllAbsent)] + df <- as.data.frame(mutate( + select(tbl, all_of(kept)), + across(everything(), .vcfHeaderValue) + )) + names(df) <- .pubSampleKeys(names(df)) + `rownames<-`(as(df, "DataFrame"), .vcfHeaderValue(sampleNames)) +} + +# Field names as `##SAMPLE` keys. An unmapped name is a new per-fit field that +# has not been given a key: letting it through would write a line that reads +# back short by that field and every field after it, so it stops here. +# @noRd +.pubSampleKeys <- function(nms) { + absent <- nms[!is_in(nms, names(.PUB_SAMPLE_KEYS))] + if (length(absent) > 0L) { + abort(glue( + "Per-fit field(s) {str_flatten(absent, ', ')} have no ", + "##SAMPLE key. Add them to .PUB_SAMPLE_KEYS: an underscored key ", + "is silently unreadable." + )) + } + unname(.PUB_SAMPLE_KEYS[nms]) +} + +# @noRd +.fmvSampleMetaRow <- function(r) { + bind_cols( + tibble(total_variants = r$nVariants), + as_tibble(r$perFit) + ) +} + +# The per-fit fields a `##SAMPLE` line carries, named exactly as the BED's +# per-fit COLUMNS are -- one vocabulary across both formats. The header line +# spells them in CamelCase, because a VCF structured-header key must be +# alphanumeric to be parseable; `.PUB_SAMPLE_KEYS` is that one translation. +# +# `V` and `cs_index` are the reason this block is load-bearing rather than +# decoration: they are per-EFFECT, so no per-variant column can hold them, and +# colocPipeline's row filter reads them off the fit -- `sets$cs_index` or `V`. +# Without them a round-tripped LBF matrix could be read but not filtered. +# @noRd +.pubPerFitFields <- function(x, row, fit) { + rg <- genomicRegion(x) + inRange <- !is.null(row) && length(rg) >= row + md <- mcols(x) + list( + study = .pubRowValue(md, "study", row), + context = .pubRowValue(md, "context", row), + trait = .pubRowValue(md, "trait", row), + method = .pubRowValue(md, "method", row), + block_id = .pubRowValue(md, "blockId", row), + data_type = .pubRowValue(md, "dataType", row), + data_source = .pubDataSource(x), + joint_studies = .pubRowValue(md, "jointStudies", row), + joint_contexts = .pubRowValue(md, "jointContexts", row), + joint_traits = .pubRowValue(md, "jointTraits", row), + weight_standardized = .pubRowValue(md, "standardized", row), + region_id = if (inRange) .pubRegionIds(rg)[[row]] else NA_character_, + region_start = if (inRange) start(rg)[[row]] else NA_integer_, + region_end = if (inRange) end(rg)[[row]] else NA_integer_, + V = .pubPackOrNa(.fitField(fit, "V")), + cs_index = .pubPackOrNa(.fitField(fit, "sets", "cs_index")) + ) +} + +# @noRd +# One mcols cell as a string. `[[row]]` then pack, rather than +# as.character() on the column: several of these are List columns (a +# TwasWeights `dataType` is one) and as.character() on a CompressedList +# errors. +# @noRd +.pubRowValue <- function(md, nm, row) { + if (is.null(row) || !is_in(nm, colnames(md))) { + return(NA_character_) + } + cell <- md[[nm]][[row]] + if (length(cell) == 0L) NA_character_ else .pubPack(cell) +} + +# @noRd +.pubPackOrNa <- function(v) { + if (length(v) == 0L) NA_character_ else .pubPack(v) +} + +# ---- VCF: TWAS weights ------------------------------------------------------ + +#' @rdname exportToVcf +#' @export +setMethod( + "exportToVcf", + signature("TwasWeights"), + function( + x, + outputPath, + sampleName = NULL, + studyName = NULL, + context = NULL, + trait = NULL, + method = NULL + ) { + .vcfRequireVariantAnnotation() + .pubRequireFullFit(x, .twbIncompleteRows(x), "exportToVcf") + rows <- .pubMatchingRows(x, studyName, context, trait, method) + if (length(rows) == 0L) { + abort("exportToVcf: no rows match the supplied selectors.") + } + .twvWriteSamples( + x, + .twvSampleSpecs(x, rows), + sampleName, + outputPath + ) + } +) + +# Project the selected models onto one GWAS-VCF-style file: the records are the +# UNION of their variants, each model is a sample, and a model that did not +# weight a variant leaves that cell empty. +# +# Multi-sample rather than one file per model because that is what GWAS-VCF +# does with several studies, and because a region's TwasWeights routinely holds +# a dozen methods for one gene -- a dozen near-identical files would be the +# wrong shape for the same information. +# @noRd +.twvWriteSamples <- function(x, samples, sampleName, outputPath) { + gr <- .twvVariantUnion(x, samples) + ids <- .grVariantIds(gr) + mc <- mcols(gr) + wgt <- .twvWeightMatrix(x, samples, ids) + meta <- .twbRowMetadata(x) + sn <- .twvSampleNames(samples, meta, sampleName) + .writeVcfImpl( + chrom = as.character(seqnames(gr)), + pos = start(gr), + ref = as.character(mc$A2), + alt = as.character(mc$A1), + snpIds = ids, + geno = list(WGT = wgt), + genoHeader = .twvGenoHeader(), + sampleName = sn, + outputPath = outputPath, + sampleMeta = .twvSampleMeta( + samples, + sn, + meta, + colSums(!is.na(wgt)), + x + ) + ) +} + +# The ##SAMPLE header block: what GWAS-VCF records per study, here per fitted +# MODEL -- its identity tuple, how many variants it actually weighted, and the +# scale its weights are on. +# +# Cross-validation metrics are deliberately NOT here. They describe how a model +# scored on held-out FOLDS, and a published file has no data-secure way to say +# anything fold-level: fold membership is sample-level information and sample +# names can be protected. The export carries the full-data fit and nothing +# else; callers who want the CV record read cvResult() and tabulate it +# themselves. +# @noRd +.twvSampleMeta <- function(samples, sampleNames, meta, nWeighted, x) { + tbl <- bind_rows(map( + seq_along(samples), + .twvSampleMetaRow, + samples = samples, + meta = meta, + nWeighted = nWeighted, + x = x + )) + .pubSampleMetaFrame(tbl, sampleNames) +} + +# @noRd +.twvSampleMetaRow <- function(j, samples, meta, nWeighted, x) { + s <- samples[[j]] + ident <- slice(meta, s$row) + fields <- .pubPerFitFields(x, s$row, NULL) + # A weight-matrix column is a member of a joint fit, so `trait` names the + # member while `joint_traits` keeps the membership. Assigned rather than + # bound, so there is one `trait` field and not a repaired duplicate. + fields$trait <- .twbTraitOf(s$label, ident$trait) + bind_cols(tibble(total_variants = nWeighted[[j]]), as_tibble(fields)) +} + +# One ##SAMPLE field value. These are written unquoted, so a comma, an equals +# sign, an angle bracket or whitespace would break the line's key=value +# structure; an absent value is spelled "." as elsewhere in VCF. +# @noRd +.vcfHeaderValue <- function(v) { + chr <- str_replace_all(as.character(v), "[,=<>[:space:]]+", "_") + if_else(is.na(chr), ".", chr) +} + +# @noRd +.twvGenoHeader <- function() { + DataFrame( + Number = "A", + Type = "Float", + Description = str_c( + "TWAS prediction weight for the effect (ALT) allele; empty where ", + "the model did not weight the variant" + ), + row.names = "WGT" + ) +} + +# One sample spec per (collection row, weight column): the row it reads, which +# weight column, and that column's label. A plain weight vector contributes one +# sample; a weight MATRIX (fSuSiE collapses a functional fit to variants x +# features) contributes one per column. +# @noRd +.twvSampleSpecs <- function(x, rows) { + list_flatten(map(rows, .twvRowSampleSpecs, x = x)) +} + +# @noRd +.twvRowSampleSpecs <- function(i, x) { + # .pubRequireFullFit() has already established that every row has weights. + labels <- .twbWeightLabels(mcols(x[[i]])$weight) + map(seq_along(labels), .twvOneSampleSpec, row = i, labels = labels) +} + +# @noRd +.twvOneSampleSpec <- function(k, row, labels) { + list(row = row, column = k, label = labels[[k]]) +} + +# The union of every selected model's variants, in genomic order. The weight +# mcols is dropped first: it is numeric on one row and a matrix on another, and +# c() on GRanges requires one mcols schema. +# @noRd +.twvVariantUnion <- function(x, samples) { + rows <- unique(map_int(samples, "row")) + all <- exec(c, !!!map(rows, .twvRowAlleles, x = x)) + gr <- all[!duplicated(.grVariantIds(all))] + gr[genomicOrder(as.character(seqnames(gr)), start(gr))] +} + +# @noRd +.twvRowAlleles <- function(i, x) { + gr <- x[[i]] + S4Vectors::`mcols<-`( + gr, + value = mcols(gr)[, c("A1", "A2"), drop = FALSE] + ) +} + +# The nVariant x nSample weight matrix: one column per model, placed onto the +# union by variant id. +# @noRd +.twvWeightMatrix <- function(x, samples, unionIds) { + exec( + cbind, + !!!map(samples, .twvSampleColumn, x = x, unionIds = unionIds) + ) +} + +# @noRd +.twvSampleColumn <- function(s, x, unionIds) { + gr <- x[[s$row]] + out <- rep(NA_real_, length(unionIds)) + out[match(.grVariantIds(gr), unionIds)] <- + .twbWeightColumn(s$column, mcols(gr)$weight) + out +} + +# Sample names: the caller's, or the model's identity tuple -- with the weight +# column's label appended when the row carries a weight matrix, since those +# columns are separate samples and must have separate names. +# @noRd +.twvSampleNames <- function(samples, meta, sampleName) { + .vcfSampleNames( + map_chr(samples, .twvSampleLabel, meta = meta), + sampleName + ) +} + +# @noRd +.twvSampleLabel <- function(s, meta) { + ident <- slice(meta, s$row) + tags <- c(ident$study, ident$context, ident$trait, ident$method) + if (!is.na(s$label)) { + tags <- c(tags, s$label) + } + str_flatten(if_else(is.na(tags), "_", tags), "|") +} + +# ---- VCF: the VariantAnnotation writer -------------------------------------- + +# Internal implementation shared by all methods. `sampleName` may name several +# samples, in which case every geno matrix is nVariant x nSample. +# `sampleMeta` is an optional per-sample DataFrame written as ##SAMPLE header +# lines (GWAS-VCF's per-study metadata block). +# @noRd +.writeVcfImpl <- function( + chrom, + pos, + ref, + alt, + snpIds, + geno, + genoHeader, + sampleName, + outputPath, + sampleMeta = NULL +) { + # Ensure chromosome names have the "chr" prefix. + if (!all(str_detect(chrom, "^chr"))) { + chrom <- str_c("chr", chrom) + } + gr <- .vcfBuildRanges(chrom, pos, ref, alt, snpIds) + hdr <- .vcfBuildHeader(sampleName, genoHeader, geno, sampleMeta) + vcf <- .vcfBuildObject(gr, sampleName, hdr, geno, ref, alt) + .vcfWriteByFormat(vcf, outputPath, chrom) + invisible(outputPath) +} + +# Per-variant rowRanges (end extended to the wider of ref / alt for indels). +# @noRd +.vcfBuildRanges <- function(chrom, pos, ref, alt, snpIds) { + GRanges( + chrom, + IRanges( + start = as.integer(pos), + end = as.integer(pos) + pmax(str_length(ref), str_length(alt)) - 1L, + names = snpIds + ) + ) +} + +# VCF header (VCFv4.2, the optional ##SAMPLE block, and the geno header subset +# to the fields actually present). +# @noRd +.vcfBuildHeader <- function(sampleName, genoHeader, geno, sampleMeta = NULL) { + sections <- compact(list( + fileformat = DataFrame(Value = "VCFv4.2", row.names = "fileformat"), + SAMPLE = sampleMeta + )) + hdr <- VariantAnnotation::VCFHeader( + header = exec(DataFrameList, !!!sections), + sample = sampleName + ) + VariantAnnotation::`geno<-`( + hdr, + value = genoHeader[ + is_in(rownames(genoHeader), names(geno)), + , + drop = FALSE + ] + ) +} + +# Assemble + finalize the VCF object (ref / alt / FILTER, sorted). +# @noRd +.vcfBuildObject <- function(gr, sampleName, hdr, geno, ref, alt) { + vcf <- VariantAnnotation::VCF( + rowRanges = gr, + colData = DataFrame(Samples = sampleName, row.names = sampleName), + exptData = list(header = hdr), + geno = SimpleList(geno) + ) + alleled <- VariantAnnotation::`alt<-`( + VariantAnnotation::`ref<-`(vcf, value = DNAStringSet(ref)), + value = .vcfAltList(alt) + ) + filtered <- VariantAnnotation::`fixed<-`( + alleled, + value = `[[<-`( + VariantAnnotation::fixed(alleled), + "FILTER", + value = "PASS" + ) + ) + sort(filtered) +} + +# The ALT column's container. A DNAStringSetList is the normal one, but ALT is +# the one allele field the VCF spec lets be non-DNA: "*" for a variant spanned +# by an upstream deletion, or a symbolic <...> allele. DNAStringSet rejects +# those outright ("key 42 (char '*') not in lookup table"), so such a file +# carries its ALT as a CharacterList. REF is DNA by the spec and unaffected. +# @noRd +.vcfAltList <- function(alt) { + if (isTRUE(all(str_detect(alt, "^[ACGTNacgtn]+$")))) { + return(DNAStringSetList(as.list(alt))) + } + CharacterList(as.list(alt)) +} + +# Write the VCF in the format implied by the output extension. writeVcf appends +# ".bgz" when index = TRUE, so paths are passed WITHOUT the .bgz/.gz suffix. +# @noRd +.vcfWriteByFormat <- function(vcf, outputPath, chrom) { + ext <- file_ext(outputPath) + if (ext == "bcf") { + .vcfWriteBcf(vcf, outputPath, chrom) + } else if (ext == "gz" || ext == "bgz") { + .vcfWriteBgz(vcf, outputPath) + } else { + VariantAnnotation::writeVcf(vcf, outputPath) + } + invisible(NULL) +} + +# BCF path: write a temporary bgzipped VCF, then convert to BCF via asBcf. +# @noRd +#' @importFrom rlang try_fetch +.vcfWriteBcf <- function(vcf, outputPath, chrom) { + tmpVcfStem <- tempfile(fileext = ".vcf") + tmpVcfBgz <- str_c(tmpVcfStem, ".bgz") + on.exit( + unlink(c(tmpVcfBgz, str_c(tmpVcfBgz, ".tbi")), force = TRUE), + add = TRUE + ) + VariantAnnotation::writeVcf(vcf, tmpVcfStem, index = TRUE) + # asBcf appends ".bcf" to destination, so strip the extension. + # Rsamtools disabled asBcf() (>= 2.26 raises "temporarily disabled"), so + # the bare upstream error is translated into something actionable rather + # than surfacing as an opaque failure from a documented output format. + try_fetch( + asBcf( + tmpVcfBgz, + dictionary = unique(chrom), + destination = str_remove(outputPath, "\\.bcf$") + ), + error = function(cnd) { + msg <- glue( + "exportToVcf: BCF output needs a working ", + "Rsamtools::asBcf(), which the installed Rsamtools does not ", + "provide. Write a bgzipped VCF instead by giving the output ", + "path a .vcf.bgz extension." + ) + abort(msg, parent = cnd) + } + ) +} + +# bgzip / gzip path: writeVcf always creates .bgz; rename to .gz on request. +# @noRd +.vcfWriteBgz <- function(vcf, outputPath) { + vcfStem <- str_remove(outputPath, "\\.(bgz|gz)$") + VariantAnnotation::writeVcf(vcf, vcfStem, index = TRUE) + actualPath <- str_c(vcfStem, ".bgz") + if (actualPath != outputPath && file.exists(actualPath)) { + file.rename(actualPath, outputPath) + tbiActual <- str_c(actualPath, ".tbi") + if (file.exists(tbiActual)) { + file.rename(tbiActual, str_c(outputPath, ".tbi")) + } + } +} + +# ---- BED: the fSuSiE schema ------------------------------------------------- + +# @noRd +.FMB_FSUSIE_COLUMNS <- c( + "#chr", + "start", + "end", + "ID", + "REF", + "ALT", + "AF", + "PIP", + ".cs.", + "cs_id", + "cs_root", + "grid_resolution", + "grid_positions", + "grid_effects", + "grid_band_halfwidth", + "trait_positions", + "trait_effects", + "joint_traits", + "study", + "context", + "trait", + "method", + "region_id", + "region_start", + "region_end" +) + +# fSuSiE carries the SAME effect twice, at two resolutions, and the published +# table holds both: the fitted curve on the wavelet grid, and that curve +# reinterpolated onto the positions of the traits the fit was run over +# (linearly between grid points, constant past the ends). They are +# complementary -- one is the model, the other is what it predicts where the +# data actually sit -- so both are written by default and `levels` drops a +# group when only one is wanted. +# +# Dropping a group REMOVES its columns from the published order rather than +# re-ordering what is left, so a single-level table is a strict subset of the +# full one. +# @noRd +.FMB_FSUSIE_WAVELET_COLUMNS <- c( + "grid_resolution", + "grid_positions", + "grid_effects", + "grid_band_halfwidth" +) + +# @noRd +# The trait-level group. `joint_traits` rather than a new `trait_names`: these +# ARE the joint members (fsusieTraitEffects() reads them from the row's +# `jointTraits` column), so they use the vocabulary the rest of the schema +# already has for joint membership. "epi_mark" also presumed epigenomic data, +# which a functional fit does not require. +.FMB_FSUSIE_TRAIT_COLUMNS <- c( + "trait_positions", + "trait_effects", + "joint_traits" +) + +# @noRd +.fmbFsusieColumns <- function(levels, csCols = character(0)) { + at <- match(".cs.", .FMB_FSUSIE_COLUMNS) + full <- c( + .FMB_FSUSIE_COLUMNS[seq_len(at - 1L)], + csCols, + .FMB_FSUSIE_COLUMNS[seq(at + 1L, length(.FMB_FSUSIE_COLUMNS))] + ) + setdiff( + full, + c( + if (!is_in("wavelet", levels)) .FMB_FSUSIE_WAVELET_COLUMNS, + if (!is_in("trait", levels)) .FMB_FSUSIE_TRAIT_COLUMNS + ) + ) +} + +# The coverage that KEYS the rows. A functional fit's table is one row per +# (context, credible set), so one level has to define the row; the caller's +# first requested level wins, else the first the fit carries. Membership at +# every other level rides along as its own CS/PUR pair, so a secondary +# coverage is represented rather than silently dropped. +# @noRd +.fmbPrimaryCsLevel <- function(view, coverage) { + levels <- .pubCsLevels(view, coverage) + if (length(levels) == 0L) { + abort(str_c( + "exportToBed(schema = 'fsusie'): the result carries no ", + "credible-set column, so there are no rows to key on." + )) + } + levels[[1L]] +} + +# The source columns .FMB_FSUSIE_COLUMNS already publishes, plus the range +# columns an as.data.frame() adds. The credibleSets view carries a good deal +# more per-variant detail (beta / se / N / logBF / the other coverages and +# their purities), which `columns = "all"` appends. +# @noRd +.FMB_FSUSIE_SOURCE <- c( + "seqnames", + "start", + "end", + "width", + "strand", + "chrom", + "pos", + "variant_id", + "A1", + "A2", + "af", + "pip", + "cs_95", + "study", + "context", + "trait", + "method" +) + +# One (row, credible set) block of the fSuSiE table. `v` is that CS's +# credibleSets() rows; `band` / `marks` are the matching curve views, already +# narrowed to the same row. +# @noRd +.fmbFsusieBlock <- function( + v, + band, + marks, + rowRegionIds, + rowRegions, + added, + levels, + primary, + csCols +) { + key <- .pubIdentityKey(v, .PUB_ROW_COLS)[[1L]] + cs <- v[[str_c("cs_", primary)]][[1L]] + context <- v$context[[1L]] + bKey <- .pubIdentityKey(band, .PUB_ROW_COLS) + mKey <- .pubIdentityKey(marks, .PUB_ROW_COLS) + b <- band[bKey == key & band$cs == cs, , drop = FALSE] + if (nrow(b) == 0L) { + return(NULL) + } + m <- marks[mKey == key & marks$cs == cs, , drop = FALSE] + i <- match(key, names(rowRegionIds)) + regionId <- rowRegionIds[[i]] + rg <- rowRegions[i] + csId <- str_c(context, regionId, str_remove(cs, "^fsusie_"), sep = ":") + out <- tibble( + # The "chr" prefix is kept, as in the other two schemas and in the + # protocol's own phenotype BEDs; stripping it here was unique to this + # table and made one reader rule impossible. + `#chr` = as.character(v$chrom), + start = v$pos - 1L, + end = v$pos, + ID = v$variant_id, + REF = .pubCol(v, "A2"), + ALT = .pubCol(v, "A1"), + # AF, not MAF: the same `af` the other table publishes. Writing + # pmin(af, 1 - af) here lost which allele the frequency referred to, + # so one export of an object was recoverable and the other was not. + AF = .pubCol(v, "af"), + PIP = v$pip, + cs_id = csId, + cs_root = csId, + grid_resolution = nrow(b), + grid_positions = .pubPack(b$pos), + grid_effects = .pubPack(b$effect), + grid_band_halfwidth = .pubPack(b$halfwidth), + trait_positions = .pubPack(m$traitPos), + trait_effects = .pubPack(m$effect), + joint_traits = .pubPack(m$jointTrait), + study = .pubCol(v, "study"), + context = context, + trait = .pubCol(v, "trait"), + method = .pubCol(v, "method"), + region_id = regionId, + region_start = start(rg) - 1L, + region_end = end(rg) + ) + # Membership at EVERY coverage the fit carries, not just the one keying + # the rows -- a secondary coverage would otherwise be silently dropped. + out <- bind_cols(out, .pubFrameCs(v, csCols)) + bind_cols( + select(out, all_of(.fmbFsusieColumns(levels, csCols))), + select(v, all_of(added)) + ) +} + +# @noRd +.fmbFsusieTable <- function(x, columns, levels, coverage) { + fs <- x[as.character(.tupleColumn(x, "method")) == "fsusie"] + if (nrow(fs) == 0L) { + return(NULL) + } + band <- as.data.frame(fsusieCredibleBand(fs)) + if (nrow(band) == 0L) { + abort(str_c( + "exportToBed(schema = 'fsusie'): the functional fields ", + "are absent from every row. Refit with a retention level that ", + "keeps the fSuSiE curves." + )) + } + csTbl <- as.data.frame(credibleSets(fs)) + primary <- .fmbPrimaryCsLevel(csTbl, coverage) + csCols <- .pubCsColumns(csTbl, coverage) + marks <- as.data.frame(fsusieTraitEffects(fs)) + # genomicRegion() is the window each row was ANALYSED over (the requested + # region, or the trait's cis window), which is what a publication table + # means by its region -- not the narrower span of whichever variants + # survived QC. + rowRegions <- genomicRegion(fs) + rowRegionIds <- set_names( + .pubRegionIds(rowRegions), + .pubCollectionKey(fs, .PUB_ROW_COLS) + ) + # Splitting the views on (context, cs) alone is only safe for a single + # region: once several regions are combined into one table, two regions can + # each contribute context "c1" / "fsusie_1", and they must stay separate + # rows with their own windows. + pieces <- compact(map( + split( + csTbl, + list( + .pubIdentityKey(csTbl, .PUB_ROW_COLS), + csTbl[[str_c("cs_", primary)]] + ), + drop = TRUE + ), + .fmbFsusieBlock, + band = band, + marks = marks, + rowRegionIds = rowRegionIds, + rowRegions = rowRegions, + added = .pubViewColumns(csTbl, columns, .FMB_FSUSIE_SOURCE), + levels = levels, + primary = primary, + csCols = csCols + )) + if (length(pieces) == 0L) NULL else list_rbind(pieces) +} + +# ---- BED: the topLoci schema ------------------------------------------------ + +# The ONE published vocabulary, shared by both formats. +# +# Fine-mapping results and TWAS weights are the same kind of record -- the +# fine-mapping posterior effect IS the prediction weight -- so they are one +# schema, not two. A file carries the subset of these columns it has anything +# to say about (see .pubDropEmpty), so a weights table is not padded with +# twenty empty posterior columns. +# +# Names are the GWAS-VCF FORMAT keys for every per-variant quantity the spec +# defines, our own uppercase keys alongside them for the quantities it does +# not, and snake_case for the per-fit fields -- which are BED columns here and +# `##SAMPLE` keys in the VCF, so they cannot inherit a spec name. One +# vocabulary across both formats; no translation layer to drift. +# +# The CS columns are NOT here: which coverages exist is fixed at FIT time by +# CredibleSetParam, so they are discovered per input (see .pubCsColumns) and +# spliced in at `.cs.` below. +# @noRd +.PUB_COLUMNS <- c( + # Structural. `ID`/`REF`/`ALT` mirror the VCF's fixed columns -- REF/ALT + # says which allele is which, where `a1`/`a2` left it to convention. + "#chr", + "start", + "end", + "ID", + "REF", + "ALT", + # Frequency and sample size. `AF` is the ALT-allele frequency the spec + # defines; `MAF` is ours, for a source that only recorded the minor + # frequency -- which is not derivable to AF, since it has lost which + # allele it refers to. `NS` is the spec's sample-size key (there is no + # `SS` in GWAS-VCF). + "AF", + "MAF", + "NS", + # Marginal association. `ES`/`SE` are the MARGINAL effect and its error -- + # never a z-score, never silently the posterior. `EZ` carries z, and the + # spec defines it as exactly that: the z "provided if it was used to + # derive the ES and SE fields". `LP` is -log10 p, which is also the better + # representation: invertible, and no underflow at tiny p. + "ES", + "SE", + "EZ", + "LP", + # Posterior effect and its error. This IS the prediction weight: a susie + # posterior mean is sum_l alpha_l * mu_l, which is the TWAS coefficient. + # `conditional_effect` is gone entirely: it was `WGT / PIP` once the + # posterior mean moved onto the scale `coef()` uses, so it duplicated two + # columns that are both here. + "WGT", + "WGTSE", + # Fine-mapping posterior. CS/PUR are spliced in after these. + "PIP", + "LBF", + "LFSR", + ".cs.", + # Identity: the tuple the collections are keyed by. + "study", + "context", + "trait", + "method", + "block_id", + # Provenance. `data_type` is the assay/modality; `data_source` is + # individual-level vs summary-statistics, which `data_type` was being + # mistaken for and which is derivable from whether an ldSketch is present. + "data_type", + "data_source", + "weight_standardized", + # Joint-fit membership. The identity column carries the "joint" sentinel + # and these list the members; the writers used to drop them entirely. + "joint_studies", + "joint_contexts", + "joint_traits", + # The window ANALYSED, without which a read-back result's genomicRegion() + # silently degrades to the span of the variants that survived QC. + "region_id", + "region_start", + "region_end" +) + +# Always written, so the output is always a valid BED even when every optional +# column is empty. +# @noRd +.PUB_REQUIRED_COLUMNS <- c("#chr", "start", "end", "ID") + +# The credible-set column pair for every coverage the input actually carries. +# Which coverages exist is a FIT-time decision (CredibleSetParam), and +# .fmrRowCs() READS a stored `cs_` column rather than recomputing, so a +# writer can restrict what it publishes but cannot invent a level the fit never +# computed. The VCF path already discovered these; the BED path hardcoded +# 95/70/50, which is the inconsistency this removes. +# @noRd +.pubCsColumns <- function(view, coverage = NULL) { + levels <- .pubCsLevels(view, coverage) + list_c(c( + list(character(0)), + map(levels, .pubCsPair) + )) +} + +# @noRd +.pubCsPair <- function(lv) c(str_c("CS", lv), str_c("PUR", lv)) + +# The coverage levels to publish: every `cs_` column the view carries, or +# the requested subset -- with a requested level the fit never computed an +# error that names what IS available, rather than a silently empty column. +# @noRd +.pubCsLevels <- function(view, coverage = NULL) { + have <- str_remove( + names(view)[str_detect(names(view), "^cs_[0-9.]+$")], + "^cs_" + ) + if (is.null(coverage)) { + return(have) + } + want <- as.character(as.numeric(coverage) * 100) + absent <- setdiff(want, have) + if (length(absent) > 0L) { + abort(glue( + "`coverage` asks for {str_flatten(absent, ', ')}, which this fit ", + "did not compute; credible sets are fixed at fit time by ", + "CredibleSetParam. Available: ", + "{str_flatten(have, ', ') %||% ''}." + )) + } + want +} + +# Splice the discovered CS pairs into the schema at the `.cs.` placeholder. +# @noRd +.pubSchema <- function(csCols) { + at <- match(".cs.", .PUB_COLUMNS) + c( + .PUB_COLUMNS[seq_len(at - 1L)], + csCols, + .PUB_COLUMNS[seq(at + 1L, length(.PUB_COLUMNS))] + ) +} + +# Build one record kind's rows with every schema column present -- the ones it +# carries, and NA for the ones its class cannot know. .pubDropEmpty() then +# removes whatever stayed entirely NA, so the file shows a user only columns +# that say something. +# @noRd +.pubFrame <- function(cols, schema) { + absent <- setdiff(schema, names(cols)) + full <- c(cols, set_names(rep(list(NA), length(absent)), absent)) + select(exec(tibble, !!!full), all_of(schema)) +} + +# Drop the columns that are entirely absent. A fixed schema padded with twenty +# empty columns is worse for a reader than a varying one, and nothing +# downstream breaks: `.pubCol()` already returns NA for a column a table does +# not have, so absent and all-NA are the same thing on the way back in. +# @noRd +.pubDropEmpty <- function(out, keepColumns = character(0)) { + keep <- names(out)[ + is_in(names(out), c(.PUB_REQUIRED_COLUMNS, keepColumns)) | + !map_lgl(out, .pubAllAbsent) + ] + select(out, all_of(keep)) +} + +# @noRd +.pubAllAbsent <- function(v) all(is.na(v)) + +# The columns the caller named explicitly. `"all"` is a request for whatever +# happens to be there, so those stay subject to the drop-empty rule; a named +# column is a request for that column, so it is written even if it is empty. +# @noRd +.pubNamedColumns <- function(columns) { + if (is.null(columns) || identical(columns, "all")) { + return(character(0)) + } + as.character(columns) +} + +# @noRd +.FMB_TOPLOCI_SOURCE <- c( + "seqnames", + "start", + "end", + "width", + "strand", + "chrom", + "pos", + "A1", + "A2", + "variant_id", + "af", + "beta", + "se", + "z", + "trait", + "context", + "cs_95", + "cs_70", + "cs_50", + "cs_95_purity", + "cs_70_purity", + "cs_50_purity", + "posterior_effect", + "posterior_se", + "pip", + "logBF", + "lfsr", + "blockId", + "study", + "method", + "N", + "p" +) + +# @noRd +.fmbTopLociTable <- function(x, signalCutoff, minPurity, columns, coverage) { + tl <- as.data.frame( + topLoci(x, signalCutoff = signalCutoff, minPurity = minPurity) + ) + if (nrow(tl) == 0L) { + return(NULL) + } + # `tl$beta`/`tl$se` are the POSTERIOR mean and sd -- topLoci() is the + # posterior view -- while `mm$beta`/`mm$se` are the MARGINAL estimates. + # Same column name, different quantity, which is why they land in + # different published fields. + mm <- .pubJoinMarginals(tl, as.data.frame(marginalEffects(x))) + win <- .fmbRowWindows(x, tl) + pos <- .pubCol(tl, "pos") + p <- suppressWarnings(as.numeric(.pubCol(mm, "p"))) + csCols <- .pubCsColumns(tl, coverage) + cols <- c( + list( + `#chr` = .pubCol(tl, "chrom"), + start = pos - 1L, + end = pos, + ID = .pubCol(tl, "variant_id"), + REF = .pubCol(tl, "A2"), + ALT = .pubCol(tl, "A1"), + AF = .pubCol(tl, "af"), + NS = .pubCol(tl, "N"), + ES = .pubCol(mm, "beta"), + SE = .pubCol(mm, "se"), + EZ = .pubEz(mm), + LP = if_else(is.na(p) | p <= 0, NA_real_, -log10(p)), + WGT = .pubCol(tl, "posterior_effect"), + WGTSE = .pubCol(tl, "posterior_se"), + PIP = .pubCol(tl, "pip"), + LBF = .pubCol(tl, "logBF"), + LFSR = .pubCol(tl, "lfsr") + ), + .fmbCsValues(tl, csCols), + .pubIdentityValues(x, tl), + list( + region_id = win$id, + region_start = win$start, + region_end = win$end + ) + ) + out <- .pubFrame(cols, .pubSchema(csCols)) + # Per-CS variant-level columns from a fullFit build ride after the fixed + # schema; `columns` adds whatever else the caller asked for after those. + added <- union( + .pubFullFitColumns(tl), + .pubViewColumns(tl, columns, .FMB_TOPLOCI_SOURCE) + ) + bind_cols(out, select(tl, all_of(added))) +} + +# The CS membership / purity values for the discovered column pairs. +# The CS membership / purity values as a tibble, for binding into a block. +# @noRd +.pubFrameCs <- function(view, csCols) { + if (length(csCols) == 0L) { + return(tibble(.rows = nrow(view))) + } + as_tibble( + .fmbCsValues(view, csCols), + .name_repair = "minimal" + ) +} + +# @noRd +.fmbCsValues <- function(tl, csCols) { + set_names(map(csCols, .fmbCsValue, tl = tl), csCols) +} + +# @noRd +.fmbCsValue <- function(nm, tl) { + lv <- str_remove(nm, "^(CS|PUR)") + if (str_starts(nm, "PUR")) { + return(.pubCol(tl, str_c("cs_", lv, "_purity"))) + } + .pubCsIndex(.pubCol(tl, str_c("cs_", lv))) +} + +# A credible-set label ("susie_1", "fsusie_2") as its integer index, which is +# what both formats publish: the prefix is the fitting method, already its own +# field, and an index is filterable in a table and representable in a VCF +# Integer FORMAT field. NA -- not 0 -- for "in no credible set at this +# coverage": NA is missing in both formats, where 0 is a sentinel that could +# collide with a real index. +# @noRd +.pubCsIndex <- function(v) { + idx <- suppressWarnings(as.integer(str_remove(as.character(v), ".*_"))) + replace(idx, is.na(idx) | idx == 0L, NA_integer_) +} + +# The identity and provenance values shared by both record kinds. `data_source` +# is derived rather than stored: a collection's ldSketch is the LD reference the +# fit was run against, so its absence means the fit saw individual-level data. +# @noRd +.pubIdentityValues <- function(x, view) { + list( + study = .pubCol(view, "study"), + context = .pubCol(view, "context"), + trait = .pubCol(view, "trait"), + method = .pubCol(view, "method"), + block_id = .pubCol(view, "blockId"), + data_type = .pubCol(view, "dataType"), + data_source = .pubRowOrDerived(view, "dataSource", .pubDataSource(x)), + weight_standardized = .pubCol(view, "standardized"), + joint_studies = .pubCol(view, "jointStudies"), + joint_contexts = .pubCol(view, "jointContexts"), + joint_traits = .pubCol(view, "jointTraits") + ) +} + +# The collection-level fallback for `data_source`: an ldSketch is the LD +# reference a fit was run against, so its absence means individual-level data. +# Only a fallback -- a row that recorded its own wins, because a combined +# collection can hold rows of both kinds and has only one ldSketch. +# @noRd +.pubDataSource <- function(x) { + if (is.null(ldSketch(x))) "individual" else "sumstat" +} + +# @noRd +# Absent OR entirely empty both mean "the rows did not record this", because +# .fmrRowMetadata() NA-fills a column the collection lacks -- so testing only +# for absence would return the NA column and never reach the fallback. +# `EZ` only when the SOURCE supplied z. The spec defines the field as the z +# "provided if it was used to derive the ES and SE fields", so publishing a z +# that pecotmr itself computed as beta/se would invert its meaning -- and a +# reader would then believe ES/SE were derived when they were recorded. +# @noRd +.pubEz <- function(view, n = NULL) { + supplied <- .pubCol(view, "z_supplied", n) + if (!any(isTRUE(as.logical(supplied)))) { + return(if (is.null(n)) NA_real_ else rep(NA_real_, n)) + } + .pubCol(view, "z", n) +} + +# @noRd +.pubRowOrDerived <- function(view, nm, fallback) { + v <- .pubCol(view, nm) + if (all(is.na(v))) { + return(fallback) + } + v +} + +# The analysis window of the collection row each long-view row came from, +# matched on the row identity so a folded multi-row table keeps each row's own +# window rather than the first one's. +# @noRd +.fmbRowWindows <- function(x, df) { + rg <- genomicRegion(x) + idx <- match( + .pubIdentityKey(df, .PUB_ROW_COLS), + .pubCollectionKey(x, .PUB_ROW_COLS) + ) + # 0-based half-open, like every other coordinate in a BED -- that is the + # format's requirement, not a pecotmr choice. `region_id` stays the + # canonical 1-based `chrom_start_end`, because it is an IDENTIFIER matched + # against upstream region lists rather than a coordinate pair. + list( + id = .pubRegionIds(rg)[idx], + start = start(rg)[idx] - 1L, + end = end(rg)[idx] + ) +} + +# The analysis window of the collection row each long-view row came from, +# matched on the row identity so a folded multi-row table keeps each row's own +# window rather than the first one's. +# @noRd + +# ---- BED: fine-mapping entry point ------------------------------------------ + +#' @rdname exportToBed +#' @export +setMethod( + "exportToBed", + signature("list"), + function(x, outputPath, ...) { + exportToBed(.pubCombineList(x), outputPath, ...) + } +) + +# `region_ID` / `region_start` / `region_end` come from genomicRegion() -- the +# window each row was ANALYSED over. A nominal TAD boundary is what a +# publication table means by its region, not the narrower span of whichever +# variants survived QC. +#' @rdname exportToBed +#' @export +setMethod( + "exportToBed", + signature("FineMappingResultBase"), + function( + x, + outputPath, + schema = c("topLoci", "fsusie"), + signalCutoff = 0.025, + minPurity = NULL, + coverage = NULL, + levels = c("wavelet", "trait"), + columns = NULL, + extraColumns = NULL + ) { + schema <- arg_match(schema) + levels <- arg_match(levels, multiple = TRUE) + assertString(outputPath) + .pubRequireFullFit(x, .fmbIncompleteRows(x), "exportToBed") + if (identical(schema, "fsusie")) { + return(.pubWriteTable( + .fmbFsusieTable(x, columns, levels, coverage), + outputPath, + naString = "NA", + index = TRUE, + emptySchema = .fmbFsusieColumns(levels), + extraColumns = extraColumns + )) + } + .pubWriteTable( + .fmbTopLociTable(x, signalCutoff, minPurity, columns, coverage), + outputPath, + naString = "NA", + index = TRUE, + keepColumns = .pubNamedColumns(columns), + extraColumns = extraColumns + ) + } +) + +# ---- BED: the TWAS-weight schemas ------------------------------------------- + +# The source columns the weights schema already publishes. Unlike the +# fine-mapping views, a TwasWeights element carries nothing else today -- its +# variants hold exactly A1 / A2 / weight -- so `columns = "all"` is a no-op on +# a pipeline-built collection and exists for the custom-built ones that +# annotate their variants further. +# @noRd +.TWB_WEIGHT_SOURCE <- c("A1", "A2", "weight") + +# A zero-row frame naming every variant-level column ANY row's variants carry. +# `columns` resolves against this rather than against one row, because the +# published schema must not vary row by row: a column one row has and another +# lacks has to become an NA cell, not a column that appears halfway down. +# @noRd +.twbVariantColumns <- function(x) { + nms <- unique(list_c(c( + list(character(0)), + map(seq_len(nrow(x)), .twbRowVariantColumns, x = x) + ))) + as_tibble( + set_names(rep(list(character()), length(nms)), nms), + .name_repair = "minimal" + ) +} + +# @noRd +.twbRowVariantColumns <- function(i, x) colnames(mcols(x[[i]])) + +# Per-row identity and payload provenance, as a tibble parallel to the +# collection's rows. The identity axes are mcols columns on a TwasWeights, not +# element fields, so they are read through `.tupleColumn()`. +# @noRd +.twbRowMetadata <- function(x) { + tibble( + study = .pubTupleColumn("study", x), + trait = .pubTupleColumn("trait", x), + context = .pubTupleColumn("context", x), + method = .pubTupleColumn("method", x), + standardized = .twbColumnOr(x, "standardized", as.logical, NA), + dataType = .twbListColumn(x, "dataType"), + dataSource = .pubDataSource(x), + jointStudies = .twbColumnOr( + x, + "jointStudies", + as.character, + NA_character_ + ), + jointContexts = .twbColumnOr( + x, + "jointContexts", + as.character, + NA_character_ + ), + jointTraits = .twbColumnOr( + x, + "jointTraits", + as.character, + NA_character_ + ) + ) +} + +# An mcols column read through `coerce`, or a recycled default when the +# collection does not carry it. +# @noRd +.twbColumnOr <- function(x, nm, coerce, default) { + if (!is_in(nm, .tupleColumnNames(x))) { + return(rep(default, nrow(x))) + } + coerce(.tupleColumn(x, nm)) +} + +# A per-row List column flattened to one string per row. +# @noRd +.twbListColumn <- function(x, nm) { + if (!is_in(nm, .tupleColumnNames(x))) { + return(rep(NA_character_, nrow(x))) + } + map_chr(as.list(.tupleColumn(x, nm)), .twbScalarChr) +} + +# One List cell as a single string: an absent value becomes NA, and a +# multi-value cell is ";"-packed rather than silently truncated. +# @noRd +.twbScalarChr <- function(v) { + if (length(v) == 0L) NA_character_ else .pubPack(v) +} + +# The weight-matrix column labels of one row. A plain numeric vector -- the +# univariate case, and what a multivariate fit is split into one row per +# context -- has no feature axis, so it gets a single NA label. A matrix keeps +# one label per column: its colname when it has them (fSuSiE's functional grid +# positions), else its index. +# @noRd +.twbWeightLabels <- function(w) { + if (is.null(dim(w))) { + return(NA_character_) + } + colnames(w) %||% as.character(seq_len(ncol(w))) +} + +# The k-th weight column of a row's weights, as a plain numeric vector. +# @noRd +.twbWeightColumn <- function(k, w) { + if (is.null(dim(w))) as.numeric(w) else as.numeric(w[, k]) +} + +# Fine-mapping methods have no business in a weights export: their "weights" +# are the fine-mapping posterior re-expressed, so the FineMappingResult says +# strictly more. Those rows are skipped with a message rather than written. +# @noRd +.TWB_FINE_MAPPING_METHODS <- c( + "susie", + "susie_inf", + "susie_ash", + "mvsusie", + "fsusie" +) + +# @noRd +.twbExportableRows <- function(x) { + methods <- .pubTupleColumn("method", x) + skip <- which(is_in(methods, .TWB_FINE_MAPPING_METHODS)) + if (length(skip) > 0L) { + inform(glue( + "exportToBed: skipping {length(skip)} fine-mapping-derived ", + "row(s) ({str_flatten(unique(methods[skip]), ', ')}). Those ", + "weights are the fine-mapping posterior re-expressed -- export ", + "the FineMappingResult instead, which carries the posterior, its ", + "standard error and the credible sets." + )) + } + setdiff(seq_len(nrow(x)), skip) +} + +# One (row, weight column) block of the weights table: every variant of the +# row, carrying that column's weight and the row's identity. A weight MATRIX +# column is a member of a joint fit, so `trait` names the member and +# `joint_traits` keeps the full membership. +# @noRd +.twbWeightBlock <- function(k, gr, w, labels, ident, win, af, schema, added) { + mc <- mcols(gr) + n <- length(gr) + cols <- c( + list( + `#chr` = as.character(seqnames(gr)), + start = start(gr) - 1L, + end = start(gr), + ID = .grVariantIds(gr), + REF = as.character(.pubCol(mc, "A2", n)), + ALT = as.character(.pubCol(mc, "A1", n)), + AF = af, + WGT = .twbWeightColumn(k, w), + study = ident$study, + context = ident$context, + trait = .twbTraitOf(labels[[k]], ident$trait), + method = ident$method, + data_type = ident$dataType, + data_source = ident$dataSource, + weight_standardized = ident$standardized, + joint_studies = ident$jointStudies, + joint_contexts = ident$jointContexts, + joint_traits = ident$jointTraits, + region_id = win$id, + region_start = win$start, + region_end = win$end + ) + ) + bind_cols(.pubFrame(cols, schema), .twbAddedColumns(mc, added, n)) +} + +# The caller-requested variant-level columns of one block, NA-filled for a row +# whose variants do not carry them. +# @noRd +.twbAddedColumns <- function(mc, added, n) { + # A zero-column tibble still has to declare its row count, or bind_cols + # refuses to recycle the block it is being joined to. + if (length(added) == 0L) { + return(tibble(.rows = n)) + } + as_tibble( + set_names(map(added, .pubCol, df = mc, n = n), added), + .name_repair = "minimal" + ) +} + +# The trait a weight column belongs to: the matrix column's label when it has +# one (a joint fit's member), else the row's own trait. `%||%` is no use here -- +# a plain weight vector's label is NA, not NULL. +# @noRd +.twbTraitOf <- function(label, trait) { + if (is.na(label)) trait else label +} + +# @noRd +.twbWeightRows <- function(i, x, meta, regions, regionIds, schema, added) { + gr <- x[[i]] + w <- mcols(gr)$weight + labels <- .twbWeightLabels(w) + list_rbind(map( + seq_along(labels), + .twbWeightBlock, + gr = gr, + w = w, + labels = labels, + ident = slice(meta, i), + win = list( + id = .pubRegionIds(regions)[[i]], + start = start(regions)[[i]] - 1L, + end = end(regions)[[i]] + ), + af = .twbVariantAf(x, gr), + schema = schema, + added = added + )) +} + +# Per-variant allele frequency, from the collection's LD panel when it has one. +# A TwasWeights element records only alleles and a weight, so without a panel +# there is no frequency to publish -- and `AF` is what the standardized-to-raw +# weight conversion needs, so its absence is worth seeing rather than faking. +# @noRd +.twbVariantAf <- function(x, gr) { + panel <- ldSketch(x) + if (is.null(panel)) { + return(NA_real_) + } + rr <- try_fetch( + SummarizedExperiment::rowRanges(panel), + error = function(cnd) NULL + ) + if (is.null(rr) || !is_in("af", colnames(mcols(rr)))) { + return(NA_real_) + } + as.numeric(mcols(rr)$af)[match(.grVariantIds(gr), .grVariantIds(rr))] +} + +# @noRd +.twbWeightsTable <- function(x, columns, coverage) { + rows <- .twbExportableRows(x) + if (length(rows) == 0L) { + abort(str_c( + "exportToBed: every row is fine-mapping-derived, so there is ", + "nothing to write as weights. Export the FineMappingResult." + )) + } + schema <- .pubSchema(.pubCsColumns(tibble(), coverage)) + regions <- genomicRegion(x) + pieces <- compact(map( + rows, + .twbWeightRows, + x = x, + meta = .twbRowMetadata(x), + regions = regions, + regionIds = .pubRegionIds(regions), + schema = schema, + added = .pubViewColumns( + .twbVariantColumns(x), + columns, + .TWB_WEIGHT_SOURCE + ) + )) + if (length(pieces) == 0L) NULL else list_rbind(pieces) +} + +# ---- BED: TWAS-weight entry point ------------------------------------------- + +# One table, one row per (variant, weight column). There is no companion +# performance table: cross-validation metrics describe a MODEL scored on +# held-out FOLDS, nothing about them is per-variant, and a published file has +# no data-secure way to express anything fold-level. They stay on the object, +# where cvResult() returns them and the caller tabulates whatever they need. +#' @rdname exportToBed +#' @export +setMethod( + "exportToBed", + signature("TwasWeights"), + function( + x, + outputPath, + coverage = NULL, + columns = NULL, + extraColumns = NULL + ) { + assertString(outputPath) + .pubRequireFullFit(x, .twbIncompleteRows(x), "exportToBed") + .pubWriteTable( + .twbWeightsTable(x, columns, coverage), + outputPath, + naString = "NA", + index = TRUE, + keepColumns = .pubNamedColumns(columns), + extraColumns = extraColumns + ) + } +) diff --git a/R/sumstatsQc.R b/R/sumstatsQc.R index e0f5450c..be92e164 100644 --- a/R/sumstatsQc.R +++ b/R/sumstatsQc.R @@ -2911,7 +2911,13 @@ effectiveN <- function(nCase, nControl) { "SE", "P", "N_CASE", - "N_CONTROL" + "N_CONTROL", + # Lower case, unlike its neighbours, because it is not a statistic: + # it is the per-variant flag saying whether `Z` was supplied by the + # source or derived by pecotmr from BETA / SE, and GWAS-VCF's `EZ` + # means only the former. Same name as the fine-mapping marginal + # view's, so the vocabulary is one. + "z_supplied" ) use <- intersect(c(baseCols, optCols), colnames(df)) S4Vectors::`mcols<-`( diff --git a/R/tupleSelectors.R b/R/tupleSelectors.R index fa9bae4a..094ef340 100644 --- a/R/tupleSelectors.R +++ b/R/tupleSelectors.R @@ -259,7 +259,7 @@ # GWAS rows carry study/method/blockId -- so the same call works on either. # `region` matches the `blockId` column. Returns all rows when nothing # constrains. Unlike the single-row selectors above this never errors on -# ambiguity; it is the aggregate counterpart used by getTopLoci. +# ambiguity; it is the aggregate counterpart used by topLoci(). .fmrRowsMatching <- function( x, studyName = NULL, @@ -296,7 +296,16 @@ # method). Columns the collection lacks are NA-filled so QTL and GWAS results # yield the same metadata shape. Safe for zero-row collections. .fmrRowMetadata <- function(x) { - cols <- c("study", "context", "trait", "blockId", "method") + cols <- c( + "study", + "context", + "trait", + "blockId", + "method", + "dataType", + "dataSource", + "standardized" + ) n <- nrow(x) as_tibble(set_names(map(cols, .fmrMetadataCol, x = x, n = n), cols)) } @@ -311,21 +320,34 @@ bind_rows(parts) } -# Internal: read the per-row `region` GRanges column of a collection, or an -# empty GRanges when the collection carries none. Shared by genomicRegion on -# TwasWeights and FineMappingResultBase (region is a uniform column across the -# family; no derivation from topLoci / blockId). -# The per-element span, DERIVED from the ranges rather than read from a stored -# `region` column (spec 4.4). range() is always in sync by construction, where -# a stored window had no correct update rule under subsetRegion() and would -# quietly go stale. `blockId` carries the nominal block identity instead. +# Internal: the per-row analysis WINDOW -- the genomic interval each row's fit +# was run over, as stored on the `region` column. Shared by genomicRegion() on +# TwasWeights and FineMappingResultBase. +# +# Stored rather than derived, because it is the half that cannot be recovered: +# QC and screening drop variants before the fit, so the realized span of the +# variants present is narrower than the window that was analysed (a fit over +# chr22:10,000,000-18,000,000 whose variants span 10,414,272-16,916,492). The +# span IS recoverable -- it is just range(x) -- so nothing is lost by deriving +# it on demand, and that is what callers wanting current content should use. +# +# Spec 4.4 removed a stored column on the grounds that it "would quietly go +# stale" under subsetRegion(). That argument holds for a SPAN, which is +# current state and must track subsetting; it does not hold for a WINDOW, +# which is provenance -- subsetting a fit's variants does not retroactively +# change which interval was analysed. +# +# Falls back to the derived span for collections built before the column +# existed (older saved objects, hand-built collections), so the accessor never +# returns nothing where it used to return a range. # @noRd .getRegionColumn <- function(x) { if (nrow(x) == 0L) { return(GenomicRanges::GRanges()) } - # No stored region column: nothing carries one any more, so the span is - # always derived from the elements' own ranges. + if (is_in("region", .tupleColumnNames(x))) { + return(.tupleColumn(x, "region")) + } unlist(range(x), use.names = FALSE) } @@ -349,13 +371,109 @@ c(cols, list(blockId = as.character(blockId))) } +# Internal: normalize a caller-supplied window into the single GRanges the +# `region` column stores, or NULL when there is none. Accepts whatever the +# pipeline's `region` argument accepts (a "chr:start-end" string or a +# GRanges); a multi-range window (jointRegions) is recorded as its overall +# span, which is the interval the fit covered. +.asAnalysisWindow <- function(region) { + if (is.null(region) || length(region) == 0L) { + return(NULL) + } + gr <- try_fetch(.asGRegion(region), error = function(cnd) NULL) + if (is.null(gr) || length(gr) == 0L) { + return(NULL) + } + if (length(gr) == 1L) gr else range(gr)[1L] +} + +# Internal: `.asAnalysisWindow()` replicated to one range per row, for the +# constructors that take a whole collection's worth of rows at once. +.analysisWindowForRows <- function(region, n) { + gr <- .asAnalysisWindow(region) + if (is.null(gr) || n == 0L) { + return(NULL) + } + rep(gr, n) +} + # Internal: append a validated `region` GRanges to a constructor's column list -# (no-op when region is NULL). Shared by the TwasWeights / FineMappingResult +# (no-op when NULL). Shared by the TwasWeights / FineMappingResult # constructors so the provenance column is added identically everywhere. +# Internal: an optional per-row provenance column. One helper for all three +# (`dataType`, `dataSource`, `standardized`) since they differ only in name and +# coercion. +# +# `dataSource` is per ROW, not per collection: it used to be derived from the +# collection's single `ldSketch` slot, so after combineFineMappingResults() +# folded individual-level and RSS-derived fits together every row got the +# combined object's answer. +# +# `standardized` gets the same treatment fine-mapping needed anyway: the +# posterior effect IS the prediction weight, so it carries the same genotype- +# scale ambiguity as a TWAS weight -- and the fit cannot answer it (susie +# records whether IT scaled, not whether the supplied X was already +# standardized), so it has to be recorded rather than derived. +# @noRd +.appendProvenanceCol <- function(cols, value, nm, n, coerce = as.character) { + if (is.null(value)) { + return(cols) + } + v <- coerce(value) + if (!is_in(length(v), c(1L, n))) { + abort(glue("`{nm}` must be length 1 or the same length as `study`.")) + } + c(cols, set_names(list(rep(v, length.out = n)), nm)) +} + +# @noRd +.appendDataTypeCol <- function(cols, dataType, n) { + .appendProvenanceCol(cols, dataType, "dataType", n) +} + +# @noRd +.appendFmProvenanceCols <- function( + cols, + dataType, + dataSource, + standardized, + n +) { + cols <- .appendProvenanceCol(cols, dataType, "dataType", n) + cols <- .appendProvenanceCol(cols, dataSource, "dataSource", n) + .appendProvenanceCol(cols, standardized, "standardized", n, as.logical) +} + +.appendRegionCol <- function(cols, region, n) { + if (is.null(region)) { + return(cols) + } + if (!methods::is(region, "GRanges")) { + abort("`region` must be a GRanges (one range per row) or NULL.") + } + if (length(region) != n) { + abort("`region` must have the same length as `study`.") + } + c(cols, list(region = region)) +} # Internal: validate the optional `region` GRanges column (one range per row), # shared by the TwasWeights / FineMappingResult validity methods. Returns a # character vector of errors (empty when valid or the column is absent). +.validateRegionColumn <- function(object) { + if (!is_in("region", .tupleColumnNames(object))) { + return(character(0)) + } + region <- .tupleColumn(object, "region") + res <- checkClass(region, "GRanges") + if (!isTRUE(res)) { + return(str_c("'region' column ", res)) + } + if (length(region) != nrow(object)) { + return("'region' column must have one range per row") + } + character(0) +} # Internal: trait-position provenance. One GRanges per trait carrying the # molecular feature's OWN genomic coordinates (gene/peak range; TSS = start()), @@ -842,18 +960,19 @@ # @noRd .fmrRowResolveWeights <- function(parts) { empty <- list(variantIds = character(0), weights = numeric(0)) - # The topLoci posterior view projects the effect to `beta`; use it as the - # per-variant weight, aligned with variant_id. + # The topLoci posterior view projects the effect to `posterior_effect`, + # and that effect IS the per-variant weight -- a susie posterior mean is + # the TWAS coefficient. Aligned with variant_id. tl <- .fmrRowTopLoci(parts) if ( is.null(tl) || nrow(tl) == 0L || - !all(is_in(c("variant_id", "beta"), names(tl))) + !all(is_in(c("variant_id", "posterior_effect"), names(tl))) ) { return(empty) } vids <- as.character(tl$variant_id) - w <- as.numeric(tl$beta) + w <- as.numeric(tl$posterior_effect) ok <- !is.na(vids) & !is.na(w) if (!any(ok)) { return(empty) diff --git a/R/twasWeights.R b/R/twasWeights.R index 44df146a..0fcc1731 100644 --- a/R/twasWeights.R +++ b/R/twasWeights.R @@ -97,6 +97,7 @@ setClass( c( .twasValidateEntries(object), .validateTraitPosColumn(object), + .validateRegionColumn(object), .twasValidateJointCols(object, jointCols), .twasValidateKeyUniqueness(object, jointCols) ) @@ -174,6 +175,11 @@ setClass( #' @param traitPos Optional per-row trait genomic anchor (a \code{GRanges} or #' \code{NULL}), carried forward as provenance; not part of the identity key. #' \code{NULL} (default) omits the column. +#' @param region Optional per-row analysis WINDOW (a \code{GRanges} or +#' \code{NULL}) -- the interval each row's fit was run over, carried +#' forward as provenance; not part of the identity key. \code{NULL} +#' (default) omits the column, and \code{\link{genomicRegion}} then +#' derives the window from the span of the variants present. #' @return A \code{TwasWeights} object. #' @examples #' twe <- twasWeightsRow(variantIds = sprintf("chr1:%d:A:G", 100L * (1:4)), @@ -195,6 +201,7 @@ TwasWeights <- function( jointContexts = NULL, jointTraits = NULL, traitPos = NULL, + region = NULL, ldSketch = NULL ) { .twasAssertArgs( @@ -220,6 +227,7 @@ TwasWeights <- function( jointContexts, jointTraits, traitPos, + region, n ) obj <- new( @@ -272,6 +280,7 @@ TwasWeights <- function( jointContexts, jointTraits, traitPos, + region, n ) { baseCols <- c( @@ -290,7 +299,7 @@ TwasWeights <- function( jointTraits, n ) - .appendTraitPosCol(withJoint, traitPos, n) + .appendRegionCol(.appendTraitPosCol(withJoint, traitPos, n), region, n) } # One entry -> its element: the variants as a GRanges, with the per-variant @@ -362,6 +371,7 @@ TwasWeights <- function( #' @export setMethod("genomicRegion", "TwasWeights", function(x) .getRegionColumn(x)) + #' @rdname traitPosition #' @export setMethod("traitPosition", "TwasWeights", function(x) { diff --git a/R/twasWeightsPipeline.R b/R/twasWeightsPipeline.R index 811f027e..3cc2440e 100644 --- a/R/twasWeightsPipeline.R +++ b/R/twasWeightsPipeline.R @@ -1238,6 +1238,26 @@ combineTwasWeights <- function(..., ldSketch = NULL) { #' something the input cannot do. #' @param weightsDataType Optional data-type label recorded on every #' \code{TwasWeightsRow$dataType} (e.g. \code{"expression"}). +#' @section The genotype scale: +#' Individual-level genotypes reach every method centred and scaled to unit +#' variance, so all the weights are on one scale and the recorded +#' \code{weight_standardized} says so. The scaling belongs to the +#' \code{QtlDataset} accessors rather than to this pipeline: +#' \code{\link{residualizedGenotypes}} applies it, governed by +#' \code{ResidualizationParam(scaleResiduals = )} per call with the +#' dataset's own \code{\link{scaleResiduals}} slot as the default. +#' +#' It is not cosmetic: measured through pecotmr's own entry points, every +#' method returns coefficients on the scale of the \code{X} it was given +#' --- exactly so for glmnet / ncvreg / L0Learn / mr.ash / the Bayesian +#' alphabet and for \code{susieWeights()} --- while the DPR samplers are +#' not scale-equivariant at all, so a uniform rescale of \code{X} changes +#' their fit and not merely its units. The chosen scale is also the one the +#' LD correlations and z-scores behind a TWAS Z already use. A +#' summary-statistics fit needs nothing done. +#' +#' \code{\link{learnTwasWeights}} still takes \code{weightStandardized}: +#' there the caller supplies \code{X} themselves, so only they can say. #' @param verbose Verbosity (0 silent, 1 default, 2 includes external package #' messages). #' @param seed Integer or \code{NULL}. When supplied (\code{QtlDataset} path), @@ -1505,6 +1525,7 @@ setGeneric("twasWeightsPipeline", function(data, ...) { ensembleArgs = .ensembleResolve(ensembleArgs), fitFullData = fitFullData, weightsDataType = weightsDataType, + standardized = .qtlScaleResiduals(data, residualizationArgs), fitRetention = fitRetention, estimatePi = estimatePi, verbose = verbose, @@ -1796,6 +1817,7 @@ setMethod( ensembleArgs, fitFullData, weightsDataType, + standardized, fitRetention, estimatePi, verbose, @@ -1812,7 +1834,12 @@ setMethod( fitFullData = fitFullData, dataType = weightsDataType, fitRetention = fitRetention, - standardized = FALSE, + # DERIVED from what the accessors actually do: genotypes reach + # the fitter scaled to unit variance unless + # `ResidualizationParam(scaleResiduals = FALSE)` or the dataset's + # own slot says otherwise. Resolved by the caller, which is where + # the dataset and the bundle are both in scope. + standardized = standardized, estimatePi = estimatePi, verbose = verbose, seed = seed, diff --git a/R/variantId.R b/R/variantId.R index a8f8e70e..b162df99 100644 --- a/R/variantId.R +++ b/R/variantId.R @@ -1191,16 +1191,24 @@ matchVariants <- function( #' @importFrom stringr str_split #' @examples #' parseRegion("chr1:1000000-2000000") +#' parseRegion("chr1_1000000_2000000") #' @export parseRegion <- function(region) { if (!is.character(region) || length(region) != 1) { return(region) } - if (!str_detect(region, "^chr[0-9XY]+:[0-9]+-[0-9]+$")) { - abort("Input string format must be 'chr:start-end'.") + # Both documented spellings: "chr1:100-200" and the underscore form + # "chr1_100_200" that LD-block / region ids use. MT and M are accepted + # alongside X/Y -- canonChrom() below exists precisely to normalize them, + # and rejecting them here made that unreachable. + pattern <- "^chr[0-9XYMTm]+(:[0-9]+-[0-9]+|_[0-9]+_[0-9]+)$" + if (!str_detect(region, pattern)) { + abort( + "Input string format must be 'chr:start-end' or 'chr_start_end'." + ) } - parts <- str_split_1(region, "[:-]") + parts <- str_split_1(region, "[:_-]") tibble( chrom = canonChrom(parts[1]), start = as.integer(parts[2]), diff --git a/R/vcfWriter.R b/R/vcfWriter.R deleted file mode 100644 index 7aecc39a..00000000 --- a/R/vcfWriter.R +++ /dev/null @@ -1,695 +0,0 @@ -#' @include AllGenerics.R -#' @importFrom S4Vectors DataFrame SimpleList mcols -#' @importFrom GenomicRanges GRanges seqnames -#' @importFrom IRanges DataFrameList -#' @importFrom Biostrings DNAStringSet DNAStringSetList -#' @importFrom Rsamtools asBcf -#' @importFrom tools file_ext -#' @importFrom stringr str_sub -#' @importFrom purrr map map_chr keep set_names list_flatten -NULL - -#' @rdname writeSumStatsVcf -#' @export -setMethod( - "writeSumStatsVcf", - signature("GwasSumStats"), - function(x, outputPath, sampleName = NULL, studyName = NULL) { - if (!requireNamespace("VariantAnnotation", quietly = TRUE)) { - abort( - "Package 'VariantAnnotation' is required for writeSumStatsVcf" - ) - } - studyName <- .vcfResolveStudy(x, studyName) - ss <- sumStats(x, studyName = studyName) - mc <- mcols(ss) - .writeVcfImpl( - chrom = as.character(seqnames(ss)), - pos = start(ss), - ref = mc$A2, - alt = mc$A1, - snpIds = mc$SNP, - geno = .vcfSumstatsGeno(mc, length(ss)), - genoHeader = .vcfSumstatsGenoHeader(), - sampleName = sampleName %||% studyName, - outputPath = outputPath - ) - } -) - -# Select which study to write (a GwasSumStats can hold many); a single-study -# collection defaults to its one study. -# @noRd -.vcfResolveStudy <- function(x, studyName) { - if (!is.null(studyName)) { - return(studyName) - } - if (nrow(x) != 1L) { - msg <- glue( - "This GwasSumStats has {nrow(x)} studies. ", - "Pass `study = ` to select one." - ) - abort(msg) - } - as.character(x$study[[1L]]) -} - -# Per-sample geno matrices (ES / SS / AF) for the fields present in the mcols. -# @noRd -.vcfSumstatsGeno <- function(mc, nSnps) { - compact(list( - ES = if (is_in("Z", colnames(mc))) matrix(mc$Z, nSnps), - SS = if (is_in("N", colnames(mc))) { - matrix(as.integer(mc$N), nSnps) - }, - AF = if (is_in("MAF", colnames(mc))) matrix(mc$MAF, nSnps) - )) -} - -# The fixed FORMAT header for the sumstats geno fields (ES / SS / AF). -# @noRd -.vcfSumstatsGenoHeader <- function() { - DataFrame( - Number = c("A", "A", "A"), - Type = c("Float", "Integer", "Float"), - Description = c( - "Z-score of effect size estimate", - "Sample size", - "Minor allele frequency" - ), - row.names = c("ES", "SS", "AF") - ) -} - -#' @rdname writeSumStatsVcf -#' @export -setMethod( - "writeSumStatsVcf", - signature("FineMappingResultBase"), - function( - x, - outputPath, - sampleName = NULL, - studyName = NULL, - context = NULL, - trait = NULL, - method = NULL, - splitByContext = FALSE, - splitByTrait = FALSE - ) { - if (!requireNamespace("VariantAnnotation", quietly = TRUE)) { - abort( - "Package 'VariantAnnotation' is required for writeSumStatsVcf" - ) - } - - # Resolve the set of rows to write. With both selectors NULL and no - # split flags, the collection must have exactly one row. Splitting - # iterates over the unique values of the requested axis. - rowSpecs <- .resolveFineMappingRows( - x, - studyName = studyName, - context = context, - trait = trait, - method = method, - splitByContext = splitByContext, - splitByTrait = splitByTrait - ) - invisible(map_chr( - rowSpecs, - .writeFineMappingVcfSpec, - x = x, - outputPath = outputPath, - sampleName = sampleName, - splitByContext = splitByContext, - splitByTrait = splitByTrait - )) - } -) - -# @noRd -.writeFineMappingVcfSpec <- function( - spec, - x, - outputPath, - sampleName, - splitByContext, - splitByTrait -) { - .writeFineMappingVcf( - x, - spec, - outputPath = outputPath, - sampleName = sampleName, - splitByContext = splitByContext, - splitByTrait = splitByTrait - ) -} - -# Narrow a row-index vector to those rows whose `axis` column equals the -# caller's requested value. `.tupleColumn()` not `[[`: on a RangedTupleList -# `[[` extracts an ELEMENT, while the identity axes live in mcols. -# @noRd -.vcfNarrowRows <- function(rows, axis, x, selectors) { - rows[as.character(.tupleColumn(x, axis))[rows] == selectors[[axis]]] -} - -# Resolve which (study, context, trait, method) rows to write. Without -# the split flags this returns a single spec; with `splitByContext` or -# `splitByTrait` the collection's rows are walked and one spec is emitted -# per row (after applying any explicit selector filters). -# @noRd -.resolveFineMappingRows <- function( - x, - studyName, - context, - trait, - method, - splitByContext, - splitByTrait -) { - cols <- .tupleColumnNames(x) - hasContextSlot <- is_in("context", cols) - hasTraitSlot <- is_in("trait", cols) - selectors <- compact(list( - study = studyName, - context = if (hasContextSlot) context, - trait = if (hasTraitSlot) trait, - method = method - )) - rows <- reduce( - names(selectors), - .vcfNarrowRows, - x = x, - selectors = selectors, - .init = seq_len(nrow(x)) - ) - if (length(rows) == 0L) { - abort("writeSumStatsVcf: no rows match the supplied selectors.") - } - if (!isTRUE(splitByContext) && !isTRUE(splitByTrait)) { - if (length(rows) != 1L) { - msg <- glue( - "This FineMappingResult has {length(rows)} matching rows. ", - "Pass `study`/`context`/`trait`/`method` to select one, or ", - "set `splitByContext = TRUE` / `splitByTrait = TRUE` to emit ", - "one file per row." - ) - abort(msg) - } - return(list(.rowSpec(rows[[1L]], x))) - } - map(rows, .rowSpec, x = x) -} - -# Build a (study, context, trait, method) spec list for one row index. -# @noRd -.rowSpec <- function(r, x) { - list( - study = as.character(x$study)[r], - context = if (is_in("context", .tupleColumnNames(x))) { - as.character(x$context)[r] - } else { - NA_character_ - }, - trait = if (is_in("trait", .tupleColumnNames(x))) { - as.character(x$trait)[r] - } else { - NA_character_ - }, - method = as.character(x$method)[r] - ) -} - -# Extract column `nm` from `df`, or an all-NA vector of length nSnps when -# absent. -# @noRd -.vcfCol <- function(df, nm, nSnps) { - if (!is.null(df) && is_in(nm, names(df))) df[[nm]] else rep(NA, nSnps) -} - -# One VCF FORMAT field spec: name, per-variant `values`, VCF `type` -# (Float/Integer), and header `desc`. Number is always "A" (per-allele). -# @noRd -.vcfSpec <- function(name, values, type, desc) { - list(name = name, values = values, type = type, desc = desc) -} - -# Core FORMAT fields shared by univariate + posterior VCFs: effect size -# (posterior conditional effect, else marginal beta), standard error, -log10 -# p-value, sample size, and effect-allele frequency. Returns a list of specs; -# empty (all-NA) ones are dropped downstream. -# @noRd -.vcfCoreSpecs <- function(base, m, nSnps) { - # The conditional effect when the fit reported one, else the marginal. - conditional <- .vcfCol(base, "conditional_effect", nSnps) - es <- if (all(is.na(conditional))) { - .vcfCol(m, "beta", nSnps) - } else { - conditional - } - baseAf <- .vcfCol(base, "af", nSnps) - af <- if (all(is.na(baseAf))) .vcfCol(m, "af", nSnps) else baseAf - p <- .vcfCol(m, "p", nSnps) - lp <- if_else(is.na(p) | p <= 0, NA_real_, -log10(p)) - list( - .vcfSpec( - "ES", - es, - "Float", - str_c( - "Effect size (posterior conditional effect, ", - "else marginal beta), effect allele" - ) - ), - .vcfSpec( - "SE", - .vcfCol(m, "se", nSnps), - "Float", - "Standard error of the marginal effect-size estimate" - ), - .vcfSpec( - "LP", - lp, - "Float", - "-log10 p-value of the marginal univariate effect" - ), - .vcfSpec( - "SS", - as.integer(.vcfCol(m, "N", nSnps)), - "Integer", - "Sample size" - ), - .vcfSpec("AF", af, "Float", "Allele frequency (effect allele)") - ) -} - -# Credible-set FORMAT fields. pecotmr emits CS (+ PUR when -# a purity column exists) for EVERY cs_ column present, so coverage -# levels are data-driven rather than the 50/70/95 xqtl-protocol default. -# @noRd -.vcfCsSpecs <- function(base) { - csCols <- names(base)[str_detect(names(base), "^cs_[0-9.]+$")] - perCol <- map(csCols, .vcfCsColSpecs, base = base) - list_flatten(perCol) -} - -# Per-CS variant-level fullFit FORMAT fields (within_cs_pip scalar plus the -# wide within_cs_pip_ / cs_logbf_ / cs_effect_ sets emitted when -# the topLoci was built with fullFit = TRUE). Each column becomes an uppercase -# field. -# @noRd -.vcfFullFitSpecs <- function(base) { - cols <- names(base)[str_detect( - names(base), - "^(within_cs_pip|cs_logbf_|cs_effect_)" - )] - map(cols, .vcfFullFitColSpec, base = base) -} - -# Posterior FORMAT fields, only when a fine-mapping posterior table exists: -# PIP, per-variant log Bayes factor, local false sign rate, plus the dynamic -# credible-set and fullFit column sets. -# @noRd -.vcfPosteriorSpecs <- function(base, nSnps) { - fixed <- list( - .vcfSpec( - "PIP", - .vcfCol(base, "pip", nSnps), - "Float", - "Posterior inclusion probability" - ), - .vcfSpec( - "LBF", - .vcfCol(base, "logBF", nSnps), - "Float", - "Per-variant log Bayes factor (max single effect)" - ), - .vcfSpec( - "LFSR", - .vcfCol(base, "lfsr", nSnps), - "Float", - "Local false sign rate (per-condition posterior)" - ) - ) - c(fixed, .vcfCsSpecs(base), .vcfFullFitSpecs(base)) -} - -# Resolve the per-variant body: the fine-mapping POSTERIOR (topLoci at -# signalCutoff 0 = every fitted variant, carrying pip / CS membership / -# conditional effect) joined to the MARGINAL univariate sumstats -# (marginalEffects) where those exist. mvSuSiE / fSuSiE have no marginal -# sumstats, so the posterior drives the variant set; univariate susie adds -# marginal ES=beta / SE / LP / AF on top. Returns list(base, m, hasPost). -# @noRd -.vcfResolveBody <- function(entry, sn) { - post <- try_fetch( - as_tibble(as.data.frame( - S4Vectors::mcols(topLoci(entry, signalCutoff = 0)) - )), - error = function(cnd) NULL - ) - marg <- try_fetch( - as_tibble(marginalEffects(entry)), - error = function(cnd) NULL - ) - hasPost <- !is.null(post) && nrow(post) > 0L - hasMarg <- !is.null(marg) && nrow(marg) > 0L - if (hasPost) { - base <- post - m <- if (hasMarg) { - slice(marg, match(base$variant_id, marg$variant_id)) - } else { - NULL - } - } else if (hasMarg) { - base <- marg - m <- marg - } else { - msg <- glue("writeSumStatsVcf: entry [{sn}] has no variants to write") - abort(msg) - } - list(base = base, m = m, hasPost = hasPost) -} - -# Materialise the collected FORMAT specs into the geno matrix list + header -# DataFrame (dropping all-NA specs) and write the VCF via .writeVcfImpl. -# @noRd -.vcfWriteSpecs <- function(base, specs, sn, finalPath, nSnps) { - specs <- keep(specs, .vcfSpecHasValues) - geno <- set_names( - map(specs, .vcfSpecMatrix, nSnps = nSnps), - map_chr(specs, "name") - ) - genoHeader <- DataFrame( - Number = rep("A", length(specs)), - Type = map_chr(specs, "type"), - Description = map_chr(specs, "desc"), - row.names = map_chr(specs, "name") - ) - .writeVcfImpl( - chrom = .vcfCol(base, "chrom", nSnps), - pos = .vcfCol(base, "pos", nSnps), - ref = .vcfCol(base, "A2", nSnps), - alt = .vcfCol(base, "A1", nSnps), - snpIds = .vcfCol(base, "variant_id", nSnps), - geno = geno, - genoHeader = genoHeader, - sampleName = sn, - outputPath = finalPath - ) -} - -# Internal worker: write one (study, context, trait, method) tuple to a -# single VCF. When `splitByContext` / `splitByTrait` is in play the -# output path is decorated with the corresponding tag(s) so multiple -# files don't collide. -# @noRd -.writeFineMappingVcf <- function( - x, - spec, - outputPath, - sampleName, - splitByContext, - splitByTrait -) { - entry <- fineMappingResult( - x, - spec$study, - spec$context, - spec$trait, - spec$method - ) - finalPath <- .decorateOutputPath( - outputPath, - spec, - splitByContext, - splitByTrait - ) - sn <- sampleName %||% - glue( - "{spec$study}|{spec$context %||% '_'}|", - "{spec$trait %||% '_'}|{spec$method}" - ) - body <- .vcfResolveBody(entry, sn) - nSnps <- nrow(body$base) - specs <- c( - .vcfCoreSpecs(body$base, body$m, nSnps), - if (body$hasPost) .vcfPosteriorSpecs(body$base, nSnps) else list() - ) - .vcfWriteSpecs(body$base, specs, sn, finalPath, nSnps) - finalPath -} - -# Decorate `outputPath` with the spec's context / trait tags when split flags -# are set. Preserves the file extension. Examples: "out.vcf" + (context="brain") -# -> "out.brain.vcf" "out.vcf.bgz" + (context="brain", trait="ENSG1") -> -# "out.brain.ENSG1.vcf.bgz" -# @noRd -.decorateOutputPath <- function( - outputPath, - spec, - splitByContext, - splitByTrait -) { - if (!isTRUE(splitByContext) && !isTRUE(splitByTrait)) { - return(outputPath) - } - ext <- str_to_lower(tools::file_ext(outputPath)) - composite <- ext == "bgz" || ext == "gz" - base <- if (composite) { - str_remove( - outputPath, - regex("\\.[^.]+\\.(bgz|gz)$", ignore_case = TRUE) - ) - } else { - tools::file_path_sans_ext(outputPath) - } - ext_keep <- str_sub( - outputPath, - str_length(base) + 1L, - str_length(outputPath) - ) - tags <- c( - if ( - isTRUE(splitByContext) && - !is.null(spec$context) && - !is.na(spec$context) && - str_length(spec$context) > 0L - ) { - spec$context - }, - if ( - isTRUE(splitByTrait) && - !is.null(spec$trait) && - !is.na(spec$trait) && - str_length(spec$trait) > 0L - ) { - spec$trait - } - ) - if (length(tags) == 0L) { - return(outputPath) - } - str_c(base, ".", str_flatten(tags, "."), ext_keep) -} - -# Internal implementation shared by all methods -# @noRd -.writeVcfImpl <- function( - chrom, - pos, - ref, - alt, - snpIds, - geno, - genoHeader, - sampleName, - outputPath -) { - # Ensure chromosome names have the "chr" prefix. - if (!all(str_detect(chrom, "^chr"))) { - chrom <- str_c("chr", chrom) - } - gr <- .vcfBuildRanges(chrom, pos, ref, alt, snpIds) - hdr <- .vcfBuildHeader(sampleName, genoHeader, geno) - vcf <- .vcfBuildObject(gr, sampleName, hdr, geno, ref, alt) - .vcfWriteByFormat(vcf, outputPath, chrom) - invisible(outputPath) -} - -# Per-variant rowRanges (end extended to the wider of ref / alt for indels). -# @noRd -.vcfBuildRanges <- function(chrom, pos, ref, alt, snpIds) { - GRanges( - chrom, - IRanges( - start = as.integer(pos), - end = as.integer(pos) + pmax(str_length(ref), str_length(alt)) - 1L, - names = snpIds - ) - ) -} - -# VCF header (VCFv4.2 + the geno header subset to the fields actually present). -# @noRd -.vcfBuildHeader <- function(sampleName, genoHeader, geno) { - hdr <- VariantAnnotation::VCFHeader( - header = DataFrameList( - fileformat = DataFrame(Value = "VCFv4.2", row.names = "fileformat") - ), - sample = sampleName - ) - VariantAnnotation::`geno<-`( - hdr, - value = genoHeader[ - is_in(rownames(genoHeader), names(geno)), - , - drop = FALSE - ] - ) -} - -# Assemble + finalize the VCF object (ref / alt / FILTER, sorted). -# @noRd -.vcfBuildObject <- function(gr, sampleName, hdr, geno, ref, alt) { - vcf <- VariantAnnotation::VCF( - rowRanges = gr, - colData = DataFrame(Samples = sampleName, row.names = sampleName), - exptData = list(header = hdr), - geno = SimpleList(geno) - ) - alleled <- VariantAnnotation::`alt<-`( - VariantAnnotation::`ref<-`(vcf, value = DNAStringSet(ref)), - value = DNAStringSetList(as.list(alt)) - ) - filtered <- VariantAnnotation::`fixed<-`( - alleled, - value = `[[<-`( - VariantAnnotation::fixed(alleled), - "FILTER", - value = "PASS" - ) - ) - sort(filtered) -} - -# Write the VCF in the format implied by the output extension. writeVcf appends -# ".bgz" when index = TRUE, so paths are passed WITHOUT the .bgz/.gz suffix. -# @noRd -.vcfWriteByFormat <- function(vcf, outputPath, chrom) { - ext <- file_ext(outputPath) - if (ext == "bcf") { - .vcfWriteBcf(vcf, outputPath, chrom) - } else if (ext == "gz" || ext == "bgz") { - .vcfWriteBgz(vcf, outputPath) - } else { - VariantAnnotation::writeVcf(vcf, outputPath) - } - invisible(NULL) -} - -# BCF path: write a temporary bgzipped VCF, then convert to BCF via asBcf. -# @noRd -#' @importFrom rlang try_fetch -.vcfWriteBcf <- function(vcf, outputPath, chrom) { - tmpVcfStem <- tempfile(fileext = ".vcf") - tmpVcfBgz <- str_c(tmpVcfStem, ".bgz") - on.exit( - unlink(c(tmpVcfBgz, str_c(tmpVcfBgz, ".tbi")), force = TRUE), - add = TRUE - ) - VariantAnnotation::writeVcf(vcf, tmpVcfStem, index = TRUE) - # asBcf appends ".bcf" to destination, so strip the extension. - # Rsamtools disabled asBcf() (>= 2.26 raises "temporarily disabled"), so - # the bare upstream error is translated into something actionable rather - # than surfacing as an opaque failure from a documented output format. - try_fetch( - asBcf( - tmpVcfBgz, - dictionary = unique(chrom), - destination = str_remove(outputPath, "\\.bcf$") - ), - error = function(cnd) { - msg <- glue( - "writeSumStatsVcf: BCF output needs a working ", - "Rsamtools::asBcf(), which the installed Rsamtools does not ", - "provide. Write a bgzipped VCF instead by giving the output ", - "path a .vcf.bgz extension." - ) - abort(msg, parent = cnd) - } - ) -} - -# bgzip / gzip path: writeVcf always creates .bgz; rename to .gz on request. -# @noRd -.vcfWriteBgz <- function(vcf, outputPath) { - vcfStem <- str_remove(outputPath, "\\.(bgz|gz)$") - VariantAnnotation::writeVcf(vcf, vcfStem, index = TRUE) - actualPath <- str_c(vcfStem, ".bgz") - if (actualPath != outputPath && file.exists(actualPath)) { - file.rename(actualPath, outputPath) - tbiActual <- str_c(actualPath, ".tbi") - if (file.exists(tbiActual)) { - file.rename(tbiActual, str_c(outputPath, ".tbi")) - } - } -} - -# ---- map/apply helpers (lambda-free callbacks) --------------------------- - -# FORMAT specs for one cs_ column: a CS index field plus a PUR purity -# field when a matching *_purity column exists. -# @noRd -.vcfCsColSpecs <- function(cc, base) { - cov <- str_remove(cc, "^cs_") - raw <- suppressWarnings(as.integer(str_remove( - as.character(base[[cc]]), - ".*_" - ))) - # 0 = the variant is in no credible set at this coverage. - idx <- replace(raw, is.na(raw), 0L) - csSpec <- .vcfSpec( - str_c("CS", cov), - idx, - "Integer", - glue("Credible-set index at {cov}% coverage (0 = not captured)") - ) - pc <- str_c(cc, "_purity") - if (!is_in(pc, names(base))) { - return(list(csSpec)) - } - pur <- suppressWarnings(as.numeric(base[[pc]])) - purSpec <- .vcfSpec( - str_c("PUR", cov), - pur, - "Float", - glue("Purity (min abs corr) of the {cov}% credible set") - ) - list(csSpec, purSpec) -} - -# The FORMAT spec for one fullFit posterior column (uppercased Float field). -# @noRd -.vcfFullFitColSpec <- function(cc, base) { - v <- suppressWarnings(as.numeric(base[[cc]])) - .vcfSpec( - str_to_upper(cc), - v, - "Float", - glue("Per-credible-set variant statistic ({cc})") - ) -} - -# TRUE when a FORMAT spec has at least one non-NA value (drop all-NA specs). -# @noRd -.vcfSpecHasValues <- function(s) { - any(!is.na(s$values)) -} - -# One FORMAT spec's values reshaped to the nSnps x nSamples geno matrix. -# @noRd -.vcfSpecMatrix <- function(s, nSnps) { - matrix(s$values, nSnps) -} diff --git a/_pkgdown.yml b/_pkgdown.yml index edfb8b7a..2957dc5d 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -297,6 +297,7 @@ reference: - computeCsCorrelation - fsusieAffectedRegions - fsusieCredibleBand + - fsusieTraitEffects - credibleSetSummary - credibleSets - lbf @@ -312,7 +313,10 @@ reference: - variantIds - intersectVariants - resolveWeights - - writeSumStatsVcf + - exportToVcf + - exportToBed + - readFineMappingResult + - blockId - subtitle: "QtlFineMappingResult" contents: @@ -369,7 +373,7 @@ reference: - sumStatsDataFrame - "sumStats,GwasSumStats-method" - varY - - writeSumStatsVcf + - exportToVcf - subtitle: "QtlSumStats" contents: @@ -400,6 +404,9 @@ reference: - variantIds - weights-methods - resolveWeights + - exportToVcf + - exportToBed + - readTwasWeights - subtitle: "TwasWeightsRow" contents: @@ -588,6 +595,7 @@ reference: - parseRegion - regionToDf - asGranges + - genomicOrder - title: "Fine-mapping" desc: > diff --git a/data/mvsusieFineMappingExample.rda b/data/mvsusieFineMappingExample.rda index 6dfbbefa10cff7df5b7e1ca1b172752bbfe8ca43..f635b77b99eb3917db520ac5566a8e78c232f52f 100644 GIT binary patch literal 118008 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%0AxyAM>&xl0AT5b>1&LM=n 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 }5>Hib% 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⪻70CZx+GBm4Yqs6BBFUBxw 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
A1z9)eYy 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
N1-&LUtSV`FH& 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.OqNGZ;S?xl*Zc(h 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 delta 12797 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 PJ}?c70ssI200CKAZAJa% diff --git a/man/GwasFineMappingResult.Rd b/man/GwasFineMappingResult.Rd index 99a3d45a..19af42fb 100644 --- a/man/GwasFineMappingResult.Rd +++ b/man/GwasFineMappingResult.Rd @@ -10,6 +10,10 @@ GwasFineMappingResult( entry, blockId = NULL, traitPos = NULL, + region = NULL, + dataType = NULL, + dataSource = NULL, + weightStandardized = NULL, ldSketch = NULL ) } @@ -30,6 +34,29 @@ blocks' spans leave gaps.} \code{NULL}), carried forward as provenance; not part of the identity key. \code{NULL} (default) omits the column.} +\item{region}{Optional per-row analysis WINDOW (a \code{GRanges} or +\code{NULL}) -- the interval each row's fit was run over, carried +forward as provenance; not part of the identity key. \code{NULL} +(default) omits the column, and \code{\link{genomicRegion}} then +derives the window from the span of the variants present.} + +\item{dataType}{Optional assay / modality label ("bulk_rnaseq", +"methylation"), recorded as a per-row provenance column. Length 1 or +one per row; \code{NULL} (default) omits the column.} + +\item{dataSource}{Optional per-row label for the kind of input the fit saw, +\code{"individual"} or \code{"sumstat"}. Recorded rather than derived: a +combined collection can hold rows of both kinds, and its single +\code{ldSketch} cannot say which row came from which.} + +\item{weightStandardized}{Optional per-row flag: whether the posterior +effect is on the STANDARDIZED genotype scale. The posterior effect IS the +prediction weight, so it carries the same scale question as a TWAS +weight. \code{\link{fineMappingPipeline}} always passes \code{TRUE}: +it standardizes genotypes before fitting, and the posterior mean is +reported on the scale of the \code{X} the fit was given. The argument +exists for a collection assembled by hand, where only the caller knows.} + \item{ldSketch}{An optional genotype panel (see \code{\link{readGenotypes}}).} } diff --git a/man/QtlFineMappingResult.Rd b/man/QtlFineMappingResult.Rd index 8736c3dc..ed2ccc4c 100644 --- a/man/QtlFineMappingResult.Rd +++ b/man/QtlFineMappingResult.Rd @@ -14,6 +14,10 @@ QtlFineMappingResult( jointContexts = NULL, jointTraits = NULL, traitPos = NULL, + region = NULL, + dataType = NULL, + dataSource = NULL, + weightStandardized = NULL, ldSketch = NULL ) } @@ -46,6 +50,29 @@ shape as \code{jointStudies}.} \code{NULL}), carried forward as provenance; not part of the identity key. \code{NULL} (default) omits the column.} +\item{region}{Optional per-row analysis WINDOW (a \code{GRanges} or +\code{NULL}) -- the interval each row's fit was run over, carried +forward as provenance; not part of the identity key. \code{NULL} +(default) omits the column, and \code{\link{genomicRegion}} then +derives the window from the span of the variants present.} + +\item{dataType}{Optional assay / modality label ("bulk_rnaseq", +"methylation"), recorded as a per-row provenance column. Length 1 or +one per row; \code{NULL} (default) omits the column.} + +\item{dataSource}{Optional per-row label for the kind of input the fit saw, +\code{"individual"} or \code{"sumstat"}. Recorded rather than derived: a +combined collection can hold rows of both kinds, and its single +\code{ldSketch} cannot say which row came from which.} + +\item{weightStandardized}{Optional per-row flag: whether the posterior +effect is on the STANDARDIZED genotype scale. The posterior effect IS the +prediction weight, so it carries the same scale question as a TWAS +weight. \code{\link{fineMappingPipeline}} always passes \code{TRUE}: +it standardizes genotypes before fitting, and the posterior mean is +reported on the scale of the \code{X} the fit was given. The argument +exists for a collection assembled by hand, where only the caller knows.} + \item{ldSketch}{An optional genotype panel (see \code{\link{readGenotypes}}) (the LD reference for RSS-derived fits), or \code{NULL} for individual-level fits.} diff --git a/man/ResidualizationParam.Rd b/man/ResidualizationParam.Rd index bfce5fa0..b07794e7 100644 --- a/man/ResidualizationParam.Rd +++ b/man/ResidualizationParam.Rd @@ -11,6 +11,7 @@ ResidualizationParam( genotypeCovariates = NULL, residualizePhenotype = NULL, residualizeGenotype = NULL, + scaleResiduals = NULL, covariateNaAction = NULL ) } @@ -28,6 +29,15 @@ read as \code{TRUE}.} \item{residualizeGenotype}{Logical. Residualize on the genotype covariates. \code{NULL} (default) means unset, read as \code{TRUE}.} +\item{scaleResiduals}{Logical. Scale each residual column to unit +variance, which is what puts a fitted effect on the standardized +genotype scale. \code{NULL} (default) means unset, and the accessors +fall back to the dataset's own \code{\link{scaleResiduals}} slot +(itself \code{TRUE} by default) --- so this is the per-call override of +a per-object default, and the only knob the pipelines need: it reaches +\code{\link{residualizedGenotypes}} already, which is the one point +every individual-level fit draws its genotypes through.} + \item{covariateNaAction}{How missing covariate values are handled when the design is assembled: \code{"impute"} or \code{"drop"}. \code{NULL} (default) leaves the accessor's own default in place.} diff --git a/man/TwasWeights.Rd b/man/TwasWeights.Rd index 338c0046..f6c03e75 100644 --- a/man/TwasWeights.Rd +++ b/man/TwasWeights.Rd @@ -14,6 +14,7 @@ TwasWeights( jointContexts = NULL, jointTraits = NULL, traitPos = NULL, + region = NULL, ldSketch = NULL ) } @@ -46,6 +47,12 @@ shape as \code{jointStudies}.} \code{NULL}), carried forward as provenance; not part of the identity key. \code{NULL} (default) omits the column.} +\item{region}{Optional per-row analysis WINDOW (a \code{GRanges} or +\code{NULL}) -- the interval each row's fit was run over, carried +forward as provenance; not part of the identity key. \code{NULL} +(default) omits the column, and \code{\link{genomicRegion}} then +derives the window from the span of the variants present.} + \item{ldSketch}{An optional genotype panel (see \code{\link{readGenotypes}}), or \code{NULL} for individual-level fits.} } diff --git a/man/blockId.Rd b/man/blockId.Rd new file mode 100644 index 00000000..a6d53181 --- /dev/null +++ b/man/blockId.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/AllGenerics.R, R/RangedTupleList.R +\name{blockId} +\alias{blockId} +\alias{blockId,RangedTupleList-method} +\title{Get The LD Block Identifier} +\usage{ +blockId(x, ...) + +\S4method{blockId}{RangedTupleList}(x, ...) +} +\arguments{ +\item{x}{The object.} + +\item{...}{Reserved for future use.} +} +\value{ +Character, one value per row of \code{x} (\code{NA} where the row + records none). +} +\description{ +Return the LD-block identifier of each row of a collection + that keys its rows by block --- a \code{GwasSumStats}, or a + \code{GwasFineMappingResult} fine-mapped over one. + + Per ROW, like \code{\link{genomicRegion}} and unlike + \code{\link{studyName}} / \code{\link{methodNames}}: a block id is + provenance for a particular fit, not an axis the collection is indexed + on, so the returned vector is always the same length as \code{nrow(x)} + and can be used to label or join rows directly. + + It keys the external block manifest the blocks were defined in, and so + carries the true block BOUNDARIES --- unlike \code{range(x)}, which is + the span of the variants that survived QC. A collection with no block + axis (every QTL flavour) returns \code{NA} for every row rather than + erroring, so code that handles both flavours needs no class test. +} +\examples{ +data(gwasFineMappingExample, qtlFineMappingExample) +blockId(gwasFineMappingExample) +# A QTL result has no block axis, so one NA per row rather than an error. +blockId(qtlFineMappingExample) +} +\seealso{ +\code{\link{genomicRegion}} for the window that was analysed. +} diff --git a/man/colocboostPipeline.Rd b/man/colocboostPipeline.Rd index d054c457..6b5a047e 100644 --- a/man/colocboostPipeline.Rd +++ b/man/colocboostPipeline.Rd @@ -24,6 +24,7 @@ colocboostPipeline(qtlData, gwasSumStats = NULL, ...) panelFilterParam = PanelFilterParam(), signalScreenArgs = SignalScreenParam(), alleleFlip = TRUE, + residualizationArgs = ResidualizationParam(), methodArgs = ColocboostOptions() ) @@ -56,6 +57,7 @@ colocboostPipeline(qtlData, gwasSumStats = NULL, ...) panelFilterParam = PanelFilterParam(), signalScreenArgs = SignalScreenParam(), alleleFlip = TRUE, + residualizationArgs = ResidualizationParam(), methodArgs = ColocboostOptions() ) } @@ -134,6 +136,17 @@ swaps recognized (flipping z / residualized dosage / LD to a shared coding); when FALSE, match on exact alleles only (names-only), so a ref/alt swap is treated as a distinct variant.} +\item{residualizationArgs}{Covariate residualization settings for +individual-level input, built with \code{\link{ResidualizationParam}}. +Reaches both \code{\link{residualizedGenotypes}} and +\code{\link{residualizedPhenotypes}}, so it governs what is regressed +out of each side and --- through \code{scaleResiduals} --- whether the +columns are scaled to unit variance. Previously absent, which left this +pipeline on the dataset's own settings with no per-call say, unlike +\code{\link{fineMappingPipeline}} and +\code{\link{twasWeightsPipeline}}. \strong{Ignored} on +\code{QtlSumStats} input, which has nothing to residualize.} + \item{methodArgs}{Additional arguments forwarded to \code{colocboost::colocboost}, built with \code{\link{ColocboostOptions}}. Names are checked against that diff --git a/man/exportToBed.Rd b/man/exportToBed.Rd new file mode 100644 index 00000000..37acdf76 --- /dev/null +++ b/man/exportToBed.Rd @@ -0,0 +1,138 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/AllGenerics.R, R/resultWriters.R +\name{exportToBed} +\alias{exportToBed} +\alias{exportToBed,list-method} +\alias{exportToBed,FineMappingResultBase-method} +\alias{exportToBed,TwasWeights-method} +\title{Export a result to a published BED table} +\usage{ +exportToBed(x, outputPath, ...) + +\S4method{exportToBed}{list}(x, outputPath, ...) + +\S4method{exportToBed}{FineMappingResultBase}( + x, + outputPath, + schema = c("topLoci", "fsusie"), + signalCutoff = 0.025, + minPurity = NULL, + coverage = NULL, + levels = c("wavelet", "trait"), + columns = NULL, + extraColumns = NULL +) + +\S4method{exportToBed}{TwasWeights}( + x, + outputPath, + coverage = NULL, + columns = NULL, + extraColumns = NULL +) +} +\arguments{ +\item{x}{A \code{FineMappingResult} or \code{TwasWeights} object, or a +\code{list} of either, to combine first.} + +\item{outputPath}{Output path. A \code{.gz} / \code{.bgz} extension selects +bgzip + tabix (via Rsamtools, so no htslib binary is needed); any other +extension writes a plain table.} + +\item{...}{Additional arguments passed to methods.} + +\item{schema}{\code{FineMappingResult} only: which published schema to +write, \code{"topLoci"} (default) or \code{"fsusie"}.} + +\item{signalCutoff}{PIP cutoff passed to \code{\link{topLoci}} +(\code{FineMappingResult}, \code{"topLoci"} schema only). Default 0.025.} + +\item{minPurity}{Optional credible-set purity cutoff passed to +\code{\link{topLoci}} (\code{FineMappingResult}, \code{"topLoci"} schema +only). Default \code{NULL} for no purity filter.} + +\item{coverage}{Which credible-set coverage levels to publish, as +proportions (\code{c(0.95, 0.5)}). \code{NULL} (default) publishes every +level the input carries. Which levels exist is fixed at FIT time by +\code{\link{CredibleSetParam}} and the stored \code{cs_} columns are +read rather than recomputed, so this selects a subset and errors -- +naming what is available -- for a level the fit never computed.} + +\item{levels}{\code{"fsusie"} schema only: which resolution(s) of the +fitted effect to write. fSuSiE carries the same effect twice -- +\code{"wavelet"}, the curve on the fit's own grid +(\code{\link{fsusieCredibleBand}}), and \code{"trait"}, that curve +reinterpolated onto the positions of the traits the fit ran over +(\code{\link{fsusieTraitEffects}}). They are complementary, so both are +written by default; naming one drops the other's columns, leaving a strict +subset of the full table.} + +\item{columns}{Which of the SOURCE view's own variant-level columns to +append after the published schema. \code{NULL} (default) appends none; +\code{"all"} appends every column the view carries that the schema does +not already write; a character vector appends exactly those. The +published columns always lead, so adding to them never moves an existing +one. The fine-mapping views carry a long tail worth asking for +(\code{grange_start} / \code{grange_end}, the legacy \code{gene} / +\code{event} aliases, and whatever a custom build added); a +\code{TwasWeights} element carries only its alleles and weight, so there +is nothing further to add unless the collection was built by hand. Not +accepted by the \code{"performance"} schema, which has no variant axis.} + +\item{extraColumns}{Optional NAMED list of trailing columns to append after +the published schema. Each element is either length 1 (recycled across +every row) or one value per row. For fields no S4 object can know -- the +legacy top-loci BED's \code{block_id}, parsed from the source filename, +is the motivating case. Names that collide with a published column are an +error rather than a silent overwrite.} +} +\value{ +\code{outputPath}, invisibly. +} +\description{ +Writes fine-mapping results or TWAS weights to a published FunGen-xQTL BED +table, bgzipping and tabix-indexing the output when the path ends in +\code{.gz} / \code{.bgz}. The tabular counterpart of +\code{\link{exportToVcf}}: the VCF is one record per variant with the models +as samples, while these tables are long-format, one row per published +observation. +} +\details{ +A \code{TwasWeights} writes one table, a row per (variant, weight column). +A \code{FineMappingResult} offers two, selected with \code{schema}: +\describe{ + \item{\code{"topLoci"}}{(default) the posterior view joined to the + marginal one, a row per (variant, context).} + \item{\code{"fsusie"}}{the functional view, a row per (context, + credible set), carrying the fitted curve at both resolutions + \code{levels} selects.} +} + +Every table identifies the fit a row came from -- \code{study_ID}, +\code{gene_ID}, \code{event_ID} and (where it spans methods) +\code{method} -- because these collections are keyed by that tuple and +several of them are routinely folded into one output. + +Cross-validation results are NOT exported, in any schema. They describe how +a model scored on held-out FOLDS; nothing about them is per-variant, and a +published file has no data-secure way to express anything fold-level, +because fold membership is sample-level information and sample names can be +protected. They remain on the object, where \code{\link{cvResult}} returns +them and the caller tabulates whatever they need. For the same reason an +export REQUIRES the full-data fit: a cross-validation-only collection (what +\code{twasWeightsPipeline(fitFullData = FALSE)} produces) is refused rather +than written out empty. +} +\examples{ +data(qtlFineMappingExample) +exportToBed(qtlFineMappingExample, tempfile(fileext = ".bed")) +exportToBed( + qtlFineMappingExample, tempfile(fileext = ".bed"), + schema = "topLoci", columns = "all" +) +data(twasWeightsExample) +exportToBed(twasWeightsExample, tempfile(fileext = ".bed")) +} +\seealso{ +\code{\link{exportToVcf}}, \code{\link{genomicOrder}} +} diff --git a/man/exportToVcf.Rd b/man/exportToVcf.Rd new file mode 100644 index 00000000..437eb79b --- /dev/null +++ b/man/exportToVcf.Rd @@ -0,0 +1,114 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/AllGenerics.R, R/resultWriters.R +\name{exportToVcf} +\alias{exportToVcf} +\alias{exportToVcf,list-method} +\alias{exportToVcf,GwasSumStats-method} +\alias{exportToVcf,FineMappingResultBase-method} +\alias{exportToVcf,TwasWeights-method} +\title{Export a result to VCF/BCF} +\usage{ +exportToVcf(x, outputPath, sampleName = NULL, ...) + +\S4method{exportToVcf}{list}(x, outputPath, sampleName = NULL, ...) + +\S4method{exportToVcf}{GwasSumStats}(x, outputPath, sampleName = NULL, studyName = NULL) + +\S4method{exportToVcf}{FineMappingResultBase}( + x, + outputPath, + sampleName = NULL, + studyName = NULL, + context = NULL, + trait = NULL, + method = NULL, + splitByContext = FALSE, + splitByTrait = FALSE +) + +\S4method{exportToVcf}{TwasWeights}( + x, + outputPath, + sampleName = NULL, + studyName = NULL, + context = NULL, + trait = NULL, + method = NULL +) +} +\arguments{ +\item{x}{A \code{GwasSumStats}, \code{FineMappingResult} or +\code{TwasWeights} object, or a \code{list} of \code{FineMappingResult} +/ \code{TwasWeights} objects to combine first.} + +\item{outputPath}{File path for output. Extension determines format: +\code{.vcf.gz} or \code{.vcf.bgz} for bgzipped VCF, \code{.bcf} for BCF, +\code{.vcf} for uncompressed VCF.} + +\item{sampleName}{Name(s) for the VCF sample column(s). Default \code{NULL} +derives them from the object's identity tuple.} + +\item{...}{Additional arguments passed to methods.} + +\item{studyName}{Character or \code{NULL}. Restrict the written records to +this study; \code{NULL} includes all studies.} + +\item{context}{Character or \code{NULL}. Restrict the written records to this +context; \code{NULL} includes all contexts.} + +\item{trait}{Character or \code{NULL}. Restrict the written records to this +trait; \code{NULL} includes all traits.} + +\item{method}{Character or \code{NULL}. Restrict the written records to this +method; \code{NULL} includes all methods.} + +\item{splitByContext}{Logical. If \code{TRUE}, write one VCF per context. +Default \code{FALSE}.} + +\item{splitByTrait}{Logical. If \code{TRUE}, write one VCF per trait. Default +\code{FALSE}.} +} +\value{ +Invisible path(s) to the written file(s). +} +\description{ +Writes GWAS summary statistics, fine-mapping results or TWAS weights to a +GWAS-VCF-style file via VariantAnnotation and Rsamtools: one record per +variant, one SAMPLE per study / fitted model, and the per-variant statistics +as FORMAT fields. Supports bgzipped VCF (.vcf.gz/.vcf.bgz); BCF (.bcf) +additionally needs a working \code{Rsamtools::asBcf()}, which recent +Rsamtools releases disable, so prefer .vcf.bgz. +} +\details{ +Which FORMAT fields are written depends on the input: +\describe{ + \item{\code{GwasSumStats}}{\code{ES} / \code{SS} / \code{AF} for one + study.} + \item{\code{FineMappingResult}}{the marginal fields (\code{ES}, + \code{SE}, \code{LP}, \code{SS}, \code{AF}) plus the posterior ones + (\code{PIP}, \code{LBF}, \code{LFSR}, and a \code{CS}/\code{PUR} pair + per credible-set coverage present).} + \item{\code{TwasWeights}}{\code{WGT}, the prediction weight.} +} + +Every fit the selectors match becomes a SAMPLE of ONE file: the records are +the union of their variants, the FORMAT schema is the union of their fields, +and a fit that did not include a variant -- or does not report a field -- +leaves those cells empty. So two methods over a region, or a dozen weight +methods for one gene, are compared by reading one file rather than a dozen. +Each sample is described in its own \code{##SAMPLE} header line, carrying +the fit's identity tuple and how many variants it contributed. +Cross-validation results are not exported -- see \code{\link{exportToBed}} +for why -- and an export requires the full-data fit. + +\code{splitByContext} / \code{splitByTrait} opt out of that and write one +single-sample file per fit, at a path decorated with the axis value. +} +\examples{ +data(gwasSumStatsS4Example) +exportToVcf( + gwasSumStatsS4Example, outputPath = tempfile(fileext = ".vcf")) +} +\seealso{ +\code{\link{exportToBed}} +} diff --git a/man/fineMappingPipeline.Rd b/man/fineMappingPipeline.Rd index aad0192a..02d29fbf 100644 --- a/man/fineMappingPipeline.Rd +++ b/man/fineMappingPipeline.Rd @@ -33,7 +33,8 @@ fineMappingPipeline(data, ...) dataDrivenPriorWeightsCutoff = 1e-10, naAction = c("drop", "impute"), verbose = 1, - fitRetention = c("slim", "full") + fitRetention = c("slim", "full"), + dataType = NULL ) \S4method{fineMappingPipeline}{MultiStudyQtlDataset}( @@ -61,7 +62,8 @@ fineMappingPipeline(data, ...) seed = NULL, naAction = c("drop", "impute"), verbose = 1, - fitRetention = c("slim", "full") + fitRetention = c("slim", "full"), + dataType = NULL ) \S4method{fineMappingPipeline}{QtlSumStats}( @@ -80,7 +82,8 @@ fineMappingPipeline(data, ...) susieRssParam = SusieRssParam(), panelFilterParam = PanelFilterParam(), crossValidationArgs = CrossValidationParam(), - residualizationArgs = ResidualizationParam() + residualizationArgs = ResidualizationParam(), + dataType = NULL ) \S4method{fineMappingPipeline}{GwasSumStats}( @@ -94,7 +97,8 @@ fineMappingPipeline(data, ...) susieRssParam = SusieRssParam(), panelFilterParam = PanelFilterParam(), crossValidationArgs = CrossValidationParam(), - residualizationArgs = ResidualizationParam() + residualizationArgs = ResidualizationParam(), + dataType = NULL ) } \arguments{ @@ -285,6 +289,12 @@ matrices. The per-variant \code{topLoci} table is fully populated either way --- its per-credible-set columns are governed by \code{credibleSet}'s \code{perCsColumns}, not by this.} +\item{dataType}{Optional assay / modality label for the phenotype the fit +was run on (\code{"bulk_rnaseq"}, \code{"methylation"}), recorded as a +per-row provenance column and published by \code{\link{exportToBed}} / +\code{\link{exportToVcf}}. \code{NULL} (default) omits the column. It +cannot be derived --- the input carries the values, not what they measure.} + \item{panelFilterParam}{LD-reference-panel filters for the summary-statistics and GWAS methods, built with \code{\link{PanelFilterParam}}. See \emph{Panel filters on the RSS path} above.} @@ -464,6 +474,29 @@ deliberately not ported here: rare variant would need. } +\section{The genotype scale}{ + + Individual-level genotypes reach every fit centred and scaled to unit + variance, so a posterior effect --- which is the prediction weight --- is + on the standardized scale, and the published + \code{weight_standardized} records it. That scaling belongs to the + \code{QtlDataset} accessors, not to this pipeline: it is + \code{\link{residualizedGenotypes}} that applies it, governed by + \code{ResidualizationParam(scaleResiduals = )} per call with the + dataset's own \code{\link{scaleResiduals}} slot as the default. One + mechanism, one place, shared with \code{\link{twasWeightsPipeline}} and + \code{\link{colocboostPipeline}}. An RSS fit needs nothing done: its + likelihood is written in standardized units already. + + It is what makes weights comparable across methods. Measured through + pecotmr's own entry points, every TWAS method returns coefficients on the + scale of the \code{X} it was given, and the DPR samplers are not even + scale-equivariant --- a uniform rescale of \code{X} changes their fit + rather than just its units. One input scale removes the question, and it + is the scale the LD correlations and z-scores behind a TWAS Z are already + on. +} + \examples{ data(qtlDatasetExample) fineMappingPipeline(qtlDatasetExample, methods = "susie", cisWindow = 1e6) diff --git a/man/fsusieCredibleBand.Rd b/man/fsusieCredibleBand.Rd index b1b371c8..a1e29d90 100644 --- a/man/fsusieCredibleBand.Rd +++ b/man/fsusieCredibleBand.Rd @@ -15,8 +15,10 @@ fsusieCredibleBand(x, ...) \item{...}{Ignored.} } \value{ -A long \code{tibble}: \code{cs, chrom, pos, effect, lower, upper} - (the collection method additionally carries the entry identity columns). +A long \code{tibble}: \code{cs, chrom, pos, effect, lower, upper, + halfwidth} -- \code{halfwidth} being \code{(upper - lower) / 2}, the + symmetric band half-width publication tables carry (the collection method + additionally carries the entry identity columns). } \description{ For a functional-SuSiE (\code{fsusieR::susiF}) fine-mapping entry, returns @@ -32,5 +34,6 @@ data(fsusieFineMappingExample) fsusieCredibleBand(fsusieFineMappingExample) } \seealso{ -\code{\link{fsusieAffectedRegions}} +\code{\link{fsusieTraitEffects}}, + \code{\link{fsusieAffectedRegions}} } diff --git a/man/fsusieTraitEffects.Rd b/man/fsusieTraitEffects.Rd new file mode 100644 index 00000000..c3fe3ac3 --- /dev/null +++ b/man/fsusieTraitEffects.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/AllGenerics.R, R/AllClasses.R +\name{fsusieTraitEffects} +\alias{fsusieTraitEffects} +\alias{fsusieTraitEffects,FineMappingResultBase-method} +\title{fSuSiE effect at each joint trait} +\usage{ +fsusieTraitEffects(x, ...) + +\S4method{fsusieTraitEffects}{FineMappingResultBase}(x) +} +\arguments{ +\item{x}{A \code{FineMappingRow} or \code{FineMappingResultBase}.} + +\item{...}{Ignored.} +} +\value{ +A long \code{tibble}: \code{cs, chrom, jointTrait, traitPos, + effect} (the collection method additionally carries the entry identity + columns). +} +\description{ +The fitted effect curve evaluated AT the traits the functional fit was run +over, one row per (credible set, trait). The curve itself lives on a wavelet +grid (\code{\link{fsusieCredibleBand}}); the traits sit at arbitrary +positions between grid points, so the effect is linearly interpolated with +constant extrapolation past the ends. +} +\details{ +This is the view that pairs a trait's NAME with its position: the names come +from the row's \code{jointTraits} column and the positions from the fit, +index-aligned because both describe the same joint group. Returns zero rows +when the functional fields are absent. +} +\examples{ +data(fsusieFineMappingExample) +fsusieTraitEffects(fsusieFineMappingExample) +} +\seealso{ +\code{\link{fsusieCredibleBand}}, + \code{\link{fsusieAffectedRegions}} +} diff --git a/man/genomicOrder.Rd b/man/genomicOrder.Rd new file mode 100644 index 00000000..a690f19c --- /dev/null +++ b/man/genomicOrder.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/resultWriters.R +\name{genomicOrder} +\alias{genomicOrder} +\title{Order rows by genomic position, with chromosomes sorted naturally} +\usage{ +genomicOrder(chrom, start, end = NULL) +} +\arguments{ +\item{chrom}{Chromosome labels. A leading \code{"chr"} is optional and is +ignored for ordering, so \code{"chr2"} and \code{"2"} sort alike.} + +\item{start}{Start positions; coerced to numeric, non-numeric entries sort +last within their chromosome.} + +\item{end}{Optional second position key, used only to break ties on +\code{start} (a half-open BED's end column). Ignored when it is not +numeric, so a table whose third column is not a position (an allele, say) +can pass it harmlessly.} +} +\value{ +An integer permutation, as from \code{order()}. +} +\description{ +Returns the permutation that puts rows in genomic order: chromosomes +1..22 numerically, then the non-numeric contigs (X, Y, MT, ...) +alphabetically after all of them, and positions numerically within a +chromosome. A plain character sort puts chr10 before chr2, which is why +published tables need this. +} +\details{ +Shared by every pecotmr writer and by the pipeline wrappers that merge +per-region tables, so one definition of "genomic order" covers both. +} +\examples{ +genomicOrder(c("chr10", "chr2", "chrX", "chr2"), c(5, 90, 1, 10)) +} diff --git a/man/genomicRegion.Rd b/man/genomicRegion.Rd index e8f89e0f..4d730306 100644 --- a/man/genomicRegion.Rd +++ b/man/genomicRegion.Rd @@ -4,7 +4,7 @@ \alias{genomicRegion} \alias{genomicRegion,FineMappingResultBase-method} \alias{genomicRegion,TwasWeights-method} -\title{Get Per-Row Genomic Regions} +\title{Get Per-Row Analysis Windows} \usage{ genomicRegion(x, ...) @@ -19,16 +19,34 @@ genomicRegion(x, ...) } \value{ A \code{GRanges} with one range per row of \code{x}, or an empty - \code{GRanges} when the collection carries no region provenance. + \code{GRanges} when the collection has no rows. } \description{ -Return the genomic anchor of each row of a per-tuple collection - as a \code{GRanges} with one range per row -- the trait's own region for a - \code{TwasWeights}, or the fine-mapping window for a - \code{FineMappingResult}. This is location provenance (e.g. for cTWAS - LD-block placement). +Return the genomic interval each row was analysed over, as a + \code{GRanges} with one range per row -- the trait's own region for a + \code{TwasWeights}, and for a \code{FineMappingResult} the window the fit + was run over (the requested region, or the trait's cis-window when the fit + was driven by \code{traitId} + \code{cisWindow}). This is location + provenance (e.g. for cTWAS LD-block placement). + + This is the window that was ANALYSED, not the span of the variants that + survived into the fit: QC and screening drop variants first, so a fit over + \code{chr22:10000000-18000000} may contain variants spanning only + \code{chr22:10414272-16916492}. The window is stored because it cannot be + recovered from the object; the span can, and is simply \code{range(x)}. + + Being a record of what was analysed, it deliberately does NOT change under + \code{subsetRegion()} or row subsetting -- a subset of a fit's variants was + still produced by a fit over the original window. Use \code{range(x)} when + you want the current content instead. + + Collections built before the window was recorded fall back to the derived + span, so the accessor always returns a range. } \examples{ data(qtlFineMappingExample) genomicRegion(qtlFineMappingExample) } +\seealso{ +\code{\link{traitPosition}} for the trait's own coordinates. +} diff --git a/man/overlapTopLoci.Rd b/man/overlapTopLoci.Rd index 82182ef0..e78c0e9b 100644 --- a/man/overlapTopLoci.Rd +++ b/man/overlapTopLoci.Rd @@ -30,8 +30,8 @@ Intersect the top-loci tables of a \code{QtlFineMappingResult} and a \code{GwasFineMappingResult} on shared variants, matched with pecotmr's allele-aware \code{matchVariants} (handling strand flips / ref-alt swaps rather than naive id equality). The GWAS side is harmonized to the QTL -orientation: its signed effect columns (\code{beta}, \code{z}, -\code{conditional_effect}) are sign-flipped and its effect-allele frequency +orientation: its signed effect columns (\code{posterior_effect}, +\code{beta}, \code{z}) are sign-flipped and its effect-allele frequency (\code{af}) is complemented wherever a swap occurred. The result keeps the variant key columns once (from the QTL, the reference orientation) and prefixes every other column \code{qtl_} / \code{gwas_}. A variant shared diff --git a/man/parseRegion.Rd b/man/parseRegion.Rd index 6814441b..b011a737 100644 --- a/man/parseRegion.Rd +++ b/man/parseRegion.Rd @@ -20,4 +20,5 @@ non-scalar input is returned unchanged. } \examples{ parseRegion("chr1:1000000-2000000") +parseRegion("chr1_1000000_2000000") } diff --git a/man/readFineMappingResult.Rd b/man/readFineMappingResult.Rd new file mode 100644 index 00000000..644c15c4 --- /dev/null +++ b/man/readFineMappingResult.Rd @@ -0,0 +1,121 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/resultReaders.R +\name{readFineMappingResult} +\alias{readFineMappingResult} +\title{Read A Published Fine-Mapping Result} +\usage{ +readFineMappingResult( + path, + region = NULL, + format = c("auto", "bed", "vcf"), + studyName = NULL, + context = NULL, + trait = NULL, + method = NULL, + dataType = NULL, + weightStandardized = NULL, + sampleSelect = NULL, + priorVariance = NULL +) +} +\arguments{ +\item{path}{Path to the published file. Bgzipped and tabix-indexed files +can be read by region.} + +\item{region}{Optional region to restrict the read to -- a +\code{"chr:start-end"} string, a \code{GRanges}, or a one-row +\code{data.frame} with chrom/start/end. Ignored with a warning when the +file is not tabix-indexed.} + +\item{format}{\code{"auto"} (default) reads a \code{.vcf} / \code{.bcf} +path as a VCF and anything else as a published table; \code{"bed"} / +\code{"vcf"} say so explicitly for a file named otherwise.} + +\item{studyName, context, trait, method}{Identity values for a file that did +not record them. Each FILLS IN the corresponding column when the file +lacks it; a value that disagrees with what the file does record is an +error rather than a silent override.} + +\item{dataType}{Optional assay / modality label, same fill-in rule.} + +\item{weightStandardized}{Whether the published posterior effect +(\code{WGT}) is on the standardized genotype scale. Required when the +file publishes \code{WGT} without \code{weight_standardized}: the scale +is not recoverable from the weights, since converting between the two +rescales every variant by its own allele-frequency-dependent factor.} + +\item{sampleSelect}{For a multi-sample VCF, which samples to read. Each +sample is one fitted model. Defaults to all of them.} + +\item{priorVariance}{Opt-in Wakefield prior variance (\code{W}) for +deriving per-variant log Bayes factors from z when the file has no +\code{LBF}. These are an approximation under a prior you chose, not +susie's \code{lbf_variable}, so nothing derives them unasked.} +} +\value{ +A \code{QtlFineMappingResult} or \code{GwasFineMappingResult}. +} +\description{ +Rebuild a \code{QtlFineMappingResult} or + \code{GwasFineMappingResult} from a file \code{\link{exportToBed}} or + \code{\link{exportToVcf}} wrote -- or from any file in the same + vocabulary, which is what makes this the way into pecotmr for results + produced elsewhere. + + A merged table holding several \code{(study, context, trait, method)} + tuples reads back as a multi-row collection, one row per tuple. Which + class is returned follows the axes the file has: a result with a context + or a trait is a QTL result, one without is a GWAS result. +} +\section{What is required, and what is not}{ + + Only what the class cannot exist without: variant ids that encode + coordinates (or the \code{#chr} / \code{start} / \code{end} / \code{REF} + / \code{ALT} columns to build them from) and a \code{PIP} column. + Everything else is optional, because each consumer needs a different few + columns -- Mendelian randomization the marginal effect and its standard + error, colocalization the log Bayes factors -- and a file converted from + another tool will have some and not others. A consumer reached without + what it needs says which column is missing. + + The fit itself is not published. A VCF's \code{##SAMPLE} header records + the two per-effect quantities that no per-variant column could hold -- + the prior variances \code{V} and the credible sets' \code{cs_index} -- + and those are restored as a partial \code{susieFit}; a BED carries + neither. Anything needing the posterior matrices (colocalization's LBF + path) wants the \code{LBF} columns, not the fit. +} + +\section{What a round trip changes}{ + + Every published quantity comes back identical, in either format. Three + things are not identities: + \itemize{ + \item \code{exportToBed()} publishes the \strong{filtered} top-loci + table, so a default export holds the variants that passed + \code{signalCutoff} / \code{minPurity} rather than every variant of + the fit. Export with \code{signalCutoff = 0} for the whole table. + \item Credible-set membership comes back as \code{NA} where the fit + recorded the \code{"_0"} not-captured sentinel. Both formats + publish an integer index, and \code{NA} is what every reader of the + column already treats as "in no credible set". + \item \code{data_source} and \code{weight_standardized} are + populated on the way back -- the writer derives the first and the + reader is told the second -- where a fit predating them left them + empty. + } +} + +\examples{ +data(qtlFineMappingExample) +path <- file.path(tempdir(), "top_loci.bed") +exportToBed(qtlFineMappingExample, path) +# This fit predates the provenance columns, so its published table does +# not say which genotype scale its posterior effects are on -- the reader +# will not guess. +readFineMappingResult(path, weightStandardized = TRUE) +} +\seealso{ +\code{\link{exportToBed}}, \code{\link{exportToVcf}}, + \code{\link{readTwasWeights}} +} diff --git a/man/readTwasWeights.Rd b/man/readTwasWeights.Rd new file mode 100644 index 00000000..54f845e8 --- /dev/null +++ b/man/readTwasWeights.Rd @@ -0,0 +1,85 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/resultReaders.R +\name{readTwasWeights} +\alias{readTwasWeights} +\title{Read Published TWAS Weights} +\usage{ +readTwasWeights( + path, + region = NULL, + format = c("auto", "bed", "vcf"), + studyName = NULL, + context = NULL, + trait = NULL, + method = NULL, + dataType = NULL, + weightStandardized = NULL, + sampleSelect = NULL +) +} +\arguments{ +\item{path}{Path to the published file. Bgzipped and tabix-indexed files +can be read by region.} + +\item{region}{Optional region to restrict the read to -- a +\code{"chr:start-end"} string, a \code{GRanges}, or a one-row +\code{data.frame} with chrom/start/end. Ignored with a warning when the +file is not tabix-indexed.} + +\item{format}{\code{"auto"} (default) reads a \code{.vcf} / \code{.bcf} +path as a VCF and anything else as a published table; \code{"bed"} / +\code{"vcf"} say so explicitly for a file named otherwise.} + +\item{studyName, context, trait, method}{Identity values for a file that did +not record them. Each FILLS IN the corresponding column when the file +lacks it; a value that disagrees with what the file does record is an +error rather than a silent override.} + +\item{dataType}{Optional assay / modality label, same fill-in rule.} + +\item{weightStandardized}{Whether the published weights are on the +standardized genotype scale. Required when the file does not record it: +the scale is not recoverable from the weights themselves.} + +\item{sampleSelect}{For a multi-sample VCF, which samples to read. Each +sample is one fitted model. Defaults to all of them.} +} +\value{ +A \code{TwasWeights}. +} +\description{ +Rebuild a \code{TwasWeights} from a file + \code{\link{exportToBed}} or \code{\link{exportToVcf}} wrote, or from any + file in the same vocabulary. + + A merged table reads back as a multi-row collection, one row per + \code{(study, context, trait, method)} tuple. A fitted model whose + weights were a MATRIX -- a joint fit, one column per member -- was + published as one block per member with \code{trait} naming it, so it + reads back as one row per member with \code{joint_traits} keeping the + membership. The weights are identical either way; only the packing + differs. +} +\section{What is required, and what is not}{ + + Variant ids that encode coordinates (or the columns to build them from) + and a \code{WGT} column -- a weight is the whole content of a + \code{TwasWeights}. The four identity axes must be recorded or supplied, + because the collection is keyed by them. Everything else is optional and + carried through if present. + + Cross-validation metrics are never published (they describe a model + scored on held-out folds, which is not per-variant and not + data-secure to express per sample), so a read-back collection has no + \code{cvResult} and no fitted models. +} + +\examples{ +data(twasWeightsExample) +path <- file.path(tempdir(), "weights.bed") +exportToBed(twasWeightsExample, path) +readTwasWeights(path) +} +\seealso{ +\code{\link{exportToBed}}, \code{\link{readFineMappingResult}} +} diff --git a/man/twasWeightsPipeline.Rd b/man/twasWeightsPipeline.Rd index 87b1fe72..a63e090d 100644 --- a/man/twasWeightsPipeline.Rd +++ b/man/twasWeightsPipeline.Rd @@ -311,6 +311,29 @@ same shared partition. RAISS will impute. } +\section{The genotype scale}{ + + Individual-level genotypes reach every method centred and scaled to unit + variance, so all the weights are on one scale and the recorded + \code{weight_standardized} says so. The scaling belongs to the + \code{QtlDataset} accessors rather than to this pipeline: + \code{\link{residualizedGenotypes}} applies it, governed by + \code{ResidualizationParam(scaleResiduals = )} per call with the + dataset's own \code{\link{scaleResiduals}} slot as the default. + + It is not cosmetic: measured through pecotmr's own entry points, every + method returns coefficients on the scale of the \code{X} it was given + --- exactly so for glmnet / ncvreg / L0Learn / mr.ash / the Bayesian + alphabet and for \code{susieWeights()} --- while the DPR samplers are + not scale-equivariant at all, so a uniform rescale of \code{X} changes + their fit and not merely its units. The chosen scale is also the one the + LD correlations and z-scores behind a TWAS Z already use. A + summary-statistics fit needs nothing done. + + \code{\link{learnTwasWeights}} still takes \code{weightStandardized}: + there the caller supplies \code{X} themselves, so only they can say. +} + \examples{ data(qtlDatasetExample) twasWeightsPipeline(qtlDatasetExample, methods = "lasso", cisWindow = 1e6) diff --git a/man/writeSumStatsVcf.Rd b/man/writeSumStatsVcf.Rd deleted file mode 100644 index 484012c5..00000000 --- a/man/writeSumStatsVcf.Rd +++ /dev/null @@ -1,71 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/AllGenerics.R, R/vcfWriter.R -\name{writeSumStatsVcf} -\alias{writeSumStatsVcf} -\alias{writeSumStatsVcf,GwasSumStats-method} -\alias{writeSumStatsVcf,FineMappingResultBase-method} -\title{Write summary statistics or fine-mapping results to VCF/BCF} -\usage{ -writeSumStatsVcf(x, outputPath, sampleName = NULL, ...) - -\S4method{writeSumStatsVcf}{GwasSumStats}(x, outputPath, sampleName = NULL, studyName = NULL) - -\S4method{writeSumStatsVcf}{FineMappingResultBase}( - x, - outputPath, - sampleName = NULL, - studyName = NULL, - context = NULL, - trait = NULL, - method = NULL, - splitByContext = FALSE, - splitByTrait = FALSE -) -} -\arguments{ -\item{x}{Input data: a \code{GwasSumStats} object, a \code{FineMappingResult} -object, or a data.frame with columns \code{chrom}, \code{pos}, \code{ref}, -\code{alt}.} - -\item{outputPath}{File path for output. Extension determines format: -\code{.vcf.gz} or \code{.vcf.bgz} for bgzipped VCF, \code{.bcf} for BCF, -\code{.vcf} for uncompressed VCF.} - -\item{sampleName}{Name for the VCF sample column (default: trait name or -method name from the S4 object).} - -\item{...}{Additional arguments passed to methods.} - -\item{studyName}{Character or \code{NULL}. Restrict the written records to -this study; \code{NULL} includes all studies.} - -\item{context}{Character or \code{NULL}. Restrict the written records to this -context; \code{NULL} includes all contexts.} - -\item{trait}{Character or \code{NULL}. Restrict the written records to this -trait; \code{NULL} includes all traits.} - -\item{method}{Character or \code{NULL}. Restrict the written records to this -method; \code{NULL} includes all methods.} - -\item{splitByContext}{Logical. If \code{TRUE}, write one VCF per context. -Default \code{FALSE}.} - -\item{splitByTrait}{Logical. If \code{TRUE}, write one VCF per trait. Default -\code{FALSE}.} -} -\value{ -Invisible path to the written file. -} -\description{ -Creates a VCF object from GWAS summary statistics or fine-mapping results and -writes it to disk via VariantAnnotation and Rsamtools. Supports bgzipped -VCF (.vcf.gz/.vcf.bgz); BCF (.bcf) additionally needs a working -\code{Rsamtools::asBcf()}, which recent Rsamtools releases disable, so -prefer .vcf.bgz. -} -\examples{ -data(gwasSumStatsS4Example) -writeSumStatsVcf( - gwasSumStatsS4Example, outputPath = tempfile(fileext = ".vcf")) -} diff --git a/tests/testthat/test_RangedTupleList.R b/tests/testthat/test_RangedTupleList.R index 047e2810..e687c5fe 100644 --- a/tests/testthat/test_RangedTupleList.R +++ b/tests/testthat/test_RangedTupleList.R @@ -848,3 +848,53 @@ test_that("seqinfo is left alone when the source carries none", { ) expect_identical(pecotmr:::.rtlRestoreSeqinfo(grl, empty), grl) }) + +# --------------------------------------------------------------------------- +# blockId(): per-ROW, on every collection flavour +# +# `blockId` was a constructor argument and a published column with no +# accessor, so reading it back meant reaching into mcols(). One method on the +# shared base rather than one per flavour: every collection that keys rows by +# LD block stores it in the same column, and the ones with no block axis want +# the same NA-padded answer. +# --------------------------------------------------------------------------- + +test_that("blockId is one value per row, NA where the row records none", { + data(gwasFineMappingExample, qtlFineMappingExample, envir = environment()) + gwas <- blockId(gwasFineMappingExample) + expect_length(gwas, nrow(gwasFineMappingExample)) + expect_false(anyNA(gwas)) + # Per ROW, not unique() like studyName/methodNames: a block id labels one + # fit, so the vector has to stay alignable with the rows. + expect_type(gwas, "character") + # A QTL result has no block axis at all, so NA per row rather than an + # error -- code handling both flavours needs no class test. + qtl <- blockId(qtlFineMappingExample) + expect_length(qtl, nrow(qtlFineMappingExample)) + expect_true(all(is.na(qtl))) +}) + +test_that("blockId holds the length contract on every flavour", { + data(gwasSumStatsS4Example, twasWeightsExample, envir = environment()) + for (x in list(gwasSumStatsS4Example, twasWeightsExample)) { + expect_length(blockId(x), nrow(x)) + } +}) + +test_that("blockId reads the recorded value, not the variants' span", { + # The point of storing it: the id keys the external block manifest, so it + # carries the true block BOUNDARIES, while range(x) is only the span of + # the variants that survived QC. + tl <- data.frame( + variant_id = c("chr1:100:A:G", "chr1:200:C:T"), + pip = c(0.9, 0.1) + ) + row <- fineMappingRow(tl$variant_id, susieFit = list(), topLoci = tl) + res <- GwasFineMappingResult( + studyName = "s", + method = "susie", + entry = list(row), + blockId = "chr1_1_1000000" + ) + expect_equal(blockId(res), "chr1_1_1000000") +}) diff --git a/tests/testthat/test_causalInferencePipeline.R b/tests/testthat/test_causalInferencePipeline.R index d7597f7d..65d738c8 100644 --- a/tests/testthat/test_causalInferencePipeline.R +++ b/tests/testthat/test_causalInferencePipeline.R @@ -1202,7 +1202,10 @@ test_that(".cipComputeMr: IVW Wald-ratio over PIP-passing instruments", { se = rep(0.05, 4), stringsAsFactors = FALSE ) - local_mocked_bindings(.cipTopLociTable = function(x) tl, .package = "pecotmr") + local_mocked_bindings( + .cipTopLociTable = function(x) tl, + .package = "pecotmr" + ) res <- pecotmr:::.cipComputeMr( NULL, .cip_gwasDf(sprintf("chr1:%d:A:G", 100L * (1:4)), c(2, -1.5, 1, 1.8)), @@ -1227,7 +1230,10 @@ test_that(".cipComputeMr: the exposure standardizes when maf/N are absent", { se = 0.01, stringsAsFactors = FALSE ) - local_mocked_bindings(.cipTopLociTable = function(x) tl, .package = "pecotmr") + local_mocked_bindings( + .cipTopLociTable = function(x) tl, + .package = "pecotmr" + ) g <- .cip_gwasDf("chr1:100:A:G", 2) noScale <- g[, setdiff(names(g), c("N", "maf"))] res <- pecotmr:::.cipComputeMr(NULL, noScale, pipCutoff = 0.5) @@ -1282,7 +1288,10 @@ test_that(".cipComputeMr matches across a chr-prefix difference (tuple match)", se = c(0.05, 0.05), stringsAsFactors = FALSE ) - local_mocked_bindings(.cipTopLociTable = function(x) tl, .package = "pecotmr") + local_mocked_bindings( + .cipTopLociTable = function(x) tl, + .package = "pecotmr" + ) # GWAS carries the same variants without the "chr" prefix. g <- .cip_gwasDf(c("1:100:A:G", "1:200:A:G"), z = c(2, 1.5)) res <- pecotmr:::.cipComputeMr(NULL, g, pipCutoff = 0.5) @@ -1298,7 +1307,10 @@ test_that(".cipComputeMr includes an allele-swapped GWAS variant and flips the s se = 0.05, stringsAsFactors = FALSE ) - local_mocked_bindings(.cipTopLociTable = function(x) tl, .package = "pecotmr") + local_mocked_bindings( + .cipTopLociTable = function(x) tl, + .package = "pecotmr" + ) rSame <- pecotmr:::.cipComputeMr( NULL, .cip_gwasDf("chr1:100:A:G", z = 2.0), @@ -1323,7 +1335,10 @@ test_that(".cipComputeMr with alleleFlip = FALSE drops an allele-swapped GWAS va se = 0.05, stringsAsFactors = FALSE ) - local_mocked_bindings(.cipTopLociTable = function(x) tl, .package = "pecotmr") + local_mocked_bindings( + .cipTopLociTable = function(x) tl, + .package = "pecotmr" + ) # GWAS variant is the ref/alt swap; with flipping disabled it is not matched. rSwap <- pecotmr:::.cipComputeMr( NULL, @@ -1344,7 +1359,10 @@ test_that(".cipComputeMrCsAware matches across a chr-prefix difference", { se = c(0.05, 0.05), stringsAsFactors = FALSE ) - local_mocked_bindings(.cipTopLociTable = function(x) tl, .package = "pecotmr") + local_mocked_bindings( + .cipTopLociTable = function(x) tl, + .package = "pecotmr" + ) g <- .cip_gwasDf(c("1:100:A:G", "1:200:A:G"), z = c(2, 1.5)) res <- pecotmr:::.cipComputeMrCsAware(NULL, g, cpipCutoff = 0.5) expect_equal(res$nCs, 1L) @@ -1368,7 +1386,7 @@ test_that(".cipComputeTwasZ reconciles a chr-prefix mismatch (previously dropped expect_true(is.finite(res$Z)) }) -test_that(".cipComputeMr: NA on empty / missing-col / no-IV / no-overlap / zero-beta", { +test_that(".cipComputeMr: NA on empty / no-IV / no-overlap / zero-beta", { g <- .cip_gwasDf(sprintf("chr1:%d:A:G", 100L * (1:4))) isNa <- function(r) { expect_true(is.na(r$waldRatio)) @@ -1379,17 +1397,6 @@ test_that(".cipComputeMr: NA on empty / missing-col / no-IV / no-overlap / zero- .package = "pecotmr" ) isNa(pecotmr:::.cipComputeMr(NULL, g, 0.5)) # empty - local_mocked_bindings( - .cipTopLociTable = function(x) { - # no beta/se - data.frame( - variant_id = sprintf("chr1:%d:A:G", 100L * (1:4)), - pip = rep(0.9, 4) - ) - }, - .package = "pecotmr" - ) - isNa(pecotmr:::.cipComputeMr(NULL, g, 0.5)) local_mocked_bindings( .cipTopLociTable = function(x) { # none pass @@ -1440,7 +1447,10 @@ test_that(".cipComputeMrCsAware: CS-aware composite Wald + heterogeneity", { se = rep(0.05, 4), stringsAsFactors = FALSE ) - local_mocked_bindings(.cipTopLociTable = function(x) tl, .package = "pecotmr") + local_mocked_bindings( + .cipTopLociTable = function(x) tl, + .package = "pecotmr" + ) res <- pecotmr:::.cipComputeMrCsAware( NULL, .cip_gwasDf( @@ -1463,7 +1473,10 @@ test_that(".cipComputeMrCsAware: credible-set column found via the ^cs fallback" se = rep(0.05, 2), stringsAsFactors = FALSE ) - local_mocked_bindings(.cipTopLociTable = function(x) tl, .package = "pecotmr") + local_mocked_bindings( + .cipTopLociTable = function(x) tl, + .package = "pecotmr" + ) res <- pecotmr:::.cipComputeMrCsAware( NULL, .cip_gwasDf(sprintf("chr1:%d:A:G", 100L * (1:2)), c(2, -1.5)), @@ -1472,27 +1485,55 @@ test_that(".cipComputeMrCsAware: credible-set column found via the ^cs fallback" expect_true(is.finite(res$waldRatio)) # cs_0.95 via grep("^cs") }) -test_that(".cipComputeMrCsAware: naResult on empty / missing-col / no-CS / no-overlap / low-cpip", { +test_that("the MR paths abort naming the exposure columns they lack", { g <- .cip_gwasDf(sprintf("chr1:%d:A:G", 100L * (1:4))) - isNa <- function(r) expect_true(is.na(r$waldRatio)) local_mocked_bindings( - .cipTopLociTable = function(x, ...) data.frame(), + .cipTopLociTable = function(x, ...) { + data.frame( + variant_id = sprintf("chr1:%d:A:G", 100L * (1:4)), + pip = rep(0.9, 4) + ) + }, .package = "pecotmr" ) - isNa(pecotmr:::.cipComputeMrCsAware(NULL, g, 0.5)) # empty + # Absent beta/se is a property of the whole table, so it would fail + # identically for every tuple -- it is named, not silently skipped. + expect_error( + pecotmr:::.cipComputeMr(NULL, g, 0.5), + "Wald-ratio path needs pip, beta, se.*has no beta, se" + ) + expect_error( + pecotmr:::.cipComputeMrCsAware(NULL, g, 0.5), + "credible-set-aware path needs.*has no beta, se, cs\\." + ) + # A table carrying everything but `cs` is usable for the Wald ratio and + # only the CS-aware path objects. local_mocked_bindings( - .cipTopLociTable = function(x) { - # no pip col + .cipTopLociTable = function(x, ...) { data.frame( variant_id = sprintf("chr1:%d:A:G", 100L * (1:4)), - cs = rep(1L, 4), + pip = rep(0.9, 4), beta = rep(0.2, 4), se = rep(0.05, 4) ) }, .package = "pecotmr" ) - isNa(pecotmr:::.cipComputeMrCsAware(NULL, g, 0.5)) + expect_true(is.finite(pecotmr:::.cipComputeMr(NULL, g, 0.5)$waldRatio)) + expect_error( + pecotmr:::.cipComputeMrCsAware(NULL, g, 0.5), + "this result has no cs\\." + ) +}) + +test_that(".cipComputeMrCsAware: naResult on empty / no-CS / no-overlap / low-cpip", { + g <- .cip_gwasDf(sprintf("chr1:%d:A:G", 100L * (1:4))) + isNa <- function(r) expect_true(is.na(r$waldRatio)) + local_mocked_bindings( + .cipTopLociTable = function(x, ...) data.frame(), + .package = "pecotmr" + ) + isNa(pecotmr:::.cipComputeMrCsAware(NULL, g, 0.5)) # empty local_mocked_bindings( .cipTopLociTable = function(x) { # cs all 0 diff --git a/tests/testthat/test_colocPipeline.R b/tests/testthat/test_colocPipeline.R index 837a508e..361e76ad 100644 --- a/tests/testthat/test_colocPipeline.R +++ b/tests/testthat/test_colocPipeline.R @@ -165,7 +165,11 @@ context("colocPipeline") # The enrichment lookup takes each side's identity list, as the scoring loop # builds it. -.cp_side <- function(studyName, context = NA_character_, trait = NA_character_) { +.cp_side <- function( + studyName, + context = NA_character_, + trait = NA_character_ +) { list(study = studyName, context = context, trait = trait) } @@ -517,14 +521,14 @@ test_that(".colocExtractLbfFromEntry: entry without trimmedFit returns NULL with stringsAsFactors = FALSE ) ) - expect_warning( - out <- pecotmr:::.colocExtractLbfFromEntry( - e, - ColocLbfFilterParam() - ), - "has no trimmedFit" + # An absent fit means an absent LBF, and coloc.bf_bf has nothing to score. + # That is a refusal, not a skip: warning per tuple produced an empty + # result that read as "no colocalization" rather than "this input cannot + # be colocalized". + expect_error( + pecotmr:::.colocExtractLbfFromEntry(e, ColocLbfFilterParam()), + "no retained fit" ) - expect_null(out) }) test_that(".colocExtractLbfFromEntry: filterLbfCs subsets by cs_index", { @@ -1080,15 +1084,15 @@ test_that(".colocExtractLbfFromEntry: stacks nested fSuSiE lBF (fit[[1]] path)", expect_equal(nrow(out$lbf), 2L) }) -test_that(".colocExtractLbfFromEntry: warns + NULL when fit carries no LBF slot", { - fit <- list(notLbf = list(a = 1)) # fit[[1]] is a list -> $ access is safe +test_that(".colocExtractLbfFromEntry: refuses a fit carrying no LBF slot", { + fit <- list(notLbf = list(a = 1)) e <- fineMappingRow( variantIds = "chr1:100:A:G", susieFit = fit, topLoci = .cp_tl("chr1:100:A:G") ) - expect_warning( - out <- pecotmr:::.colocExtractLbfFromEntry( + expect_error( + pecotmr:::.colocExtractLbfFromEntry( e, lbfFilterArgs = ColocLbfFilterParam( filterLbfCs = FALSE, @@ -1096,12 +1100,24 @@ test_that(".colocExtractLbfFromEntry: warns + NULL when fit carries no LBF slot" concentration = 0.5 ) ), - "no lbf_variable" + "no per-effect LBF matrix" ) - expect_null(out) }) -test_that(".colocExtractLbfFromEntry: warns + NULL on an empty LBF matrix", { +test_that(".colocExtractLbfFromEntry: reads a bare fSuSiE lBF list", { + # The shape an fSuSiE fit actually carries. coloc used to look only for + # lbf_variable / fsusie_result$lBF and skipped these silently, while + # lbf() read them fine -- the two extractors had drifted apart. + data(fsusieFineMappingExample) + fit <- susieFit(fsusieFineMappingExample) + expect_type(fit$lBF, "list") + m <- pecotmr:::.colocLbfMatrix(fit, "probe") + expect_true(is.matrix(m)) + expect_identical(nrow(m), length(fit$lBF)) + expect_identical(m, pecotmr:::.asLbfMatrix(fit)) +}) + +test_that(".colocExtractLbfFromEntry: refuses an empty LBF matrix", { fit <- list( lbf_variable = matrix( numeric(0), @@ -1115,8 +1131,8 @@ test_that(".colocExtractLbfFromEntry: warns + NULL on an empty LBF matrix", { susieFit = fit, topLoci = .cp_tl(sprintf("chr1:%d:A:G", 100L * (1:3))) ) - expect_warning( - out <- pecotmr:::.colocExtractLbfFromEntry( + expect_error( + pecotmr:::.colocExtractLbfFromEntry( e, lbfFilterArgs = ColocLbfFilterParam( filterLbfCs = FALSE, @@ -1124,9 +1140,8 @@ test_that(".colocExtractLbfFromEntry: warns + NULL on an empty LBF matrix", { concentration = 0.5 ) ), - "LBF matrix is empty" + "no per-effect LBF matrix" ) - expect_null(out) }) test_that(".colocExtractLbfFromEntry: secondary CS filter subsets rows", { diff --git a/tests/testthat/test_colocboostPipeline.R b/tests/testthat/test_colocboostPipeline.R index 20cae95c..e467dddb 100644 --- a/tests/testthat/test_colocboostPipeline.R +++ b/tests/testthat/test_colocboostPipeline.R @@ -1513,3 +1513,51 @@ test_that(".cbAppendGwasPairs disambiguates a colliding study key", { test_that(".cbConcat answers an empty list for no pieces", { expect_identical(pecotmr:::.cbConcat(list()), list()) }) + +test_that("colocboostPipeline forwards residualizationArgs to both sides", { + # It accepted no bundle at all, so it ran on the dataset's own settings + # with no per-call say -- unlike fineMappingPipeline() and + # twasWeightsPipeline(). Both SIDES matter here: colocboost consumes X and + # Y together, so a bundle that reached only the genotypes would + # residualize the two against different covariate sets. + data(qtlDatasetExample, envir = environment()) + origG <- residualizedGenotypes + origP <- residualizedPhenotypes + seen <- new.env(parent = emptyenv()) + note <- function(which, ra) { + assign( + which, + c( + get0(which, seen, ifnotfound = character(0)), + as.character(ra$scaleResiduals %||% "unset") + ), + envir = seen + ) + } + local_mocked_bindings( + residualizedGenotypes = function( + x, + ..., + residualizationArgs = ResidualizationParam() + ) { + note("geno", residualizationArgs) + origG(x, ..., residualizationArgs = residualizationArgs) + }, + residualizedPhenotypes = function( + x, + ..., + residualizationArgs = ResidualizationParam() + ) { + note("pheno", residualizationArgs) + origP(x, ..., residualizationArgs = residualizationArgs) + }, + .package = "pecotmr" + ) + suppressWarnings(suppressMessages(colocboostPipeline( + qtlDatasetExample, + cisWindow = 1e6, + residualizationArgs = ResidualizationParam(scaleResiduals = FALSE) + ))) + expect_equal(unique(get("geno", envir = seen)), "FALSE") + expect_equal(unique(get("pheno", envir = seen)), "FALSE") +}) diff --git a/tests/testthat/test_fineMappingPipeline.R b/tests/testthat/test_fineMappingPipeline.R index 48002ace..878667f1 100644 --- a/tests/testthat/test_fineMappingPipeline.R +++ b/tests/testthat/test_fineMappingPipeline.R @@ -474,7 +474,10 @@ test_that(".fmResolveSusieChain: keeps susieInf when explicitly requested", { }) test_that(".fmResolveSusieChain: no chain when addSusieInf=FALSE", { - res <- pecotmr:::.fmResolveSusieChain(c("susie"), initializeWithSusieInf = FALSE) + res <- pecotmr:::.fmResolveSusieChain( + c("susie"), + initializeWithSusieInf = FALSE + ) expect_false(res$chainSusie) expect_false(res$runInf) }) @@ -701,8 +704,16 @@ test_that(".rbindFineMappingResult: concatenates two GwasFineMappingResult colle stringsAsFactors = FALSE ) ) - a <- GwasFineMappingResult(studyName = "g1", method = "susie", entry = list(e)) - b <- GwasFineMappingResult(studyName = "g2", method = "susie", entry = list(e)) + a <- GwasFineMappingResult( + studyName = "g1", + method = "susie", + entry = list(e) + ) + b <- GwasFineMappingResult( + studyName = "g2", + method = "susie", + entry = list(e) + ) out <- pecotmr:::.rbindFineMappingResult(a, b) expect_s4_class(out, "GwasFineMappingResult") expect_equal(nrow(out), 2L) @@ -2143,7 +2154,10 @@ test_that("fineMappingPipeline(MSQD): L / Lgreedy reach the per-study fit", { methods = "susie", cisWindow = 1000L, initializeWithSusieInf = FALSE, - credibleSetParam = CredibleSetParam(maxNumSingleEffects = 4L, maxNumSingleEffectsGreedy = 2L) + credibleSetParam = CredibleSetParam( + maxNumSingleEffects = 4L, + maxNumSingleEffectsGreedy = 2L + ) ) ) # The multi-study method had no L / Lgreedy formals of its own before the @@ -2717,7 +2731,10 @@ test_that("fineMappingPipeline(QtlSumStats): top-level L / Lgreedy reach the sus ss, methods = "susie", initializeWithSusieInf = FALSE, - credibleSetParam = CredibleSetParam(maxNumSingleEffects = 4L, maxNumSingleEffectsGreedy = 2L) + credibleSetParam = CredibleSetParam( + maxNumSingleEffects = 4L, + maxNumSingleEffectsGreedy = 2L + ) ) ) # QtlDataset and GwasSumStats have always taken these; QtlSumStats did not, @@ -5200,7 +5217,10 @@ test_that("CredibleSetParam validity accepts every legitimate construction", { "CredibleSetParam" ) expect_s4_class( - CredibleSetParam(maxNumSingleEffects = 4L, maxNumSingleEffectsGreedy = 2L), + CredibleSetParam( + maxNumSingleEffects = 4L, + maxNumSingleEffectsGreedy = 2L + ), "CredibleSetParam" ) expect_s4_class( @@ -5228,8 +5248,14 @@ test_that("CredibleSetParam validity catches range and length errors", { CredibleSetParam(secondaryCoverage = c(0.7, 3)), "secondaryCoverage" ) - expect_error(CredibleSetParam(maxNumSingleEffects = 0), "maxNumSingleEffects") - expect_error(CredibleSetParam(maxNumSingleEffects = 2.5), "maxNumSingleEffects") + expect_error( + CredibleSetParam(maxNumSingleEffects = 0), + "maxNumSingleEffects" + ) + expect_error( + CredibleSetParam(maxNumSingleEffects = 2.5), + "maxNumSingleEffects" + ) expect_error( CredibleSetParam(maxNumSingleEffectsGreedy = 0), "maxNumSingleEffectsGreedy" @@ -5311,3 +5337,281 @@ test_that(".fmOwnedEngineArgs owns nothing for an unknown engine", { character(0) ) }) + +test_that(".fmBlockIdRange recovers a block boundary the variants cannot give", { + # Adjacent LD blocks leave gaps, so the realized variant span is narrower + # than the block; only blockId carries the boundary. + gr <- .fmBlockIdRange("chr22_49355984_50799822") + expect_s4_class(gr, "GRanges") + expect_identical(as.character(GenomicRanges::seqnames(gr)), "chr22") + expect_identical(GenomicRanges::start(gr), 49355984L) + expect_identical(GenomicRanges::end(gr), 50799822L) + # the chr:start-end spelling works too + expect_identical( + as.character(.fmBlockIdRange("chr1:100-200")), + as.character(GenomicRanges::GRanges("chr1:100-200")) + ) + # anything that is not coordinates is not guessed at + expect_null(.fmBlockIdRange("ENSG00000283047")) + expect_null(.fmBlockIdRange(NA_character_)) + expect_null(.fmBlockIdRange(NULL)) + expect_null(.fmBlockIdRange(c("chr1_1_2", "chr1_3_4"))) +}) + +test_that(".fmRssFitWindow prefers the block boundary over the variant span", { + ids <- c("chr22:49363017:A:T", "chr22:50598205:C:G") + withBlock <- .fmRssFitWindow(NULL, ids, "chr22_49355984_50799822") + expect_identical(GenomicRanges::start(withBlock), 49355984L) + # No parseable blockId -> the fit's own variants define the window. + noBlock <- .fmRssFitWindow(NULL, ids, NULL) + expect_identical(GenomicRanges::start(noBlock), 49363017L) + expect_identical(GenomicRanges::end(noBlock), 50598205L) +}) + +test_that(".fmBlockWindow falls back to the trait's own cis window", { + data(qtlDatasetExample) + explicit <- GenomicRanges::GRanges("chr1:1-999") + # An explicit block range wins untouched. + expect_identical( + .fmBlockWindow(explicit, "ctx", "t", list(data = qtlDatasetExample)), + explicit + ) + # Without one, the cis path's window is the trait's position +/- cisWindow + # -- the same interval .fmResidGenoBlock() slices X over. Widening the + # cis window must widen the recorded window. + cfgNarrow <- list(data = qtlDatasetExample, cisWindow = 1000) + cfgWide <- list(data = qtlDatasetExample, cisWindow = 500000) + ctx <- contexts(qtlDatasetExample)[[1L]] + tid <- rownames(molecularTraits(qtlDatasetExample, contexts = ctx))[[1L]] + narrow <- .fmBlockWindow(NULL, ctx, tid, cfgNarrow) + wide <- .fmBlockWindow(NULL, ctx, tid, cfgWide) + skip_if(is.null(narrow) || is.null(wide)) + expect_lt(GenomicRanges::start(wide), GenomicRanges::start(narrow)) + expect_gt(GenomicRanges::end(wide), GenomicRanges::end(narrow)) + expect_identical( + GenomicRanges::width(wide) - GenomicRanges::width(narrow), + 2L * (500000L - 1000L) + ) +}) + +test_that("a supplied block key survives into the result; a seqname fallback does not", { + data(gwasSumStatsS4Example) + gss <- gwasSumStatsS4Example + gr <- .collectionEntry(gss, 1L) + derived <- .fmGwasRangeKey(gr) + + # (a) No manifest and no explicit blockId: GwasSumStats() falls back to the + # bare seqname, which carries no coordinates and must NOT override the + # entry's own range key. + bare <- gss + S4Vectors::mcols(bare)$blockId <- rep("chr22", nrow(bare)) + expect_identical(.fmGwasBlockId(bare, 1L, gr), derived) + + # (b) A coordinate-bearing key -- from an ldBlocks manifest or an explicit + # vector -- IS the block boundary and wins. Adjacent blocks leave gaps, so + # it is wider than the variants that survived QC. + supplied <- gss + key <- "chr22_1_50000000" + S4Vectors::mcols(supplied)$blockId <- rep(key, nrow(supplied)) + expect_identical(.fmGwasBlockId(supplied, 1L, gr), key) + expect_false(identical(.fmGwasBlockId(supplied, 1L, gr), derived)) + + # (c) The resume cache keys on the DERIVED range either way, so supplying a + # wider boundary cannot make the cache miss. + expect_identical(.fmGwasRangeKey(gr), derived) +}) + +# --------------------------------------------------------------------------- +# Provenance columns (dataType / dataSource / standardized) +# +# These three are what exportToBed() / exportToVcf() publish as data_type, +# data_source and weight_standardized. Only the first is an outside fact; the +# other two follow from the branch the pipeline took, so they are derived and +# the summary-statistics methods refuse to be told. +# --------------------------------------------------------------------------- + +# @noRd +.fmprov_cols <- function(res) { + as.data.frame(S4Vectors::mcols(res))[, + intersect( + c("dataType", "dataSource", "standardized"), + colnames(S4Vectors::mcols(res)) + ), + drop = FALSE + ] +} + +test_that("fineMappingPipeline(QtlDataset) records what it is given", { + data(qtlDatasetExample, envir = environment()) + res <- suppressMessages(fineMappingPipeline( + qtlDatasetExample, + methods = "susie", + cisWindow = 1e6, + dataType = "bulk_rnaseq" + )) + cols <- .fmprov_cols(res) + expect_equal(unique(cols$dataType), "bulk_rnaseq") + # Derived, not supplied: this entry point fits individual-level genotypes. + expect_equal(unique(cols$dataSource), "individual") + # Also derived: the genotypes were standardized on the way in, so there + # is nothing for a caller to declare and no way for it to be FALSE. + expect_true(all(cols$standardized)) +}) + +test_that("fineMappingPipeline(QtlDataset) omits dataType when not supplied", { + data(qtlDatasetExample, envir = environment()) + res <- suppressMessages(fineMappingPipeline( + qtlDatasetExample, + methods = "susie", + cisWindow = 1e6 + )) + # An absent label leaves the column out rather than filling it with NA -- + # an all-NA column is dropped on export anyway, and its absence is what + # the reader sees either way. + expect_false("dataType" %in% colnames(S4Vectors::mcols(res))) + cols <- .fmprov_cols(res) + expect_equal(unique(cols$dataSource), "individual") + expect_true(all(cols$standardized)) +}) + +test_that("fineMappingPipeline(QtlSumStats) records sumstat / standardized", { + res <- suppressWarnings(suppressMessages(fineMappingPipeline( + .rssf_qcd(), + methods = "susie", + dataType = "bulk_rnaseq" + ))) + cols <- .fmprov_cols(res) + expect_equal(unique(cols$dataType), "bulk_rnaseq") + expect_equal(unique(cols$dataSource), "sumstat") + # The RSS likelihood is written in standardized units, so the posterior + # effects are on that scale whatever the caller believes. + expect_true(all(cols$standardized)) +}) + +test_that("no pipeline takes a weight-scale declaration any more", { + # It was an argument while the caller's genotypes went to the fitter + # untouched. They are standardized on the way in now, so the flag is + # derived and a declaration could only contradict it. + expect_error( + fineMappingPipeline( + .rssf_qcd(), + methods = "susie", + weightStandardized = FALSE + ), + "unused argument" + ) + data(qtlDatasetExample, envir = environment()) + expect_error( + fineMappingPipeline( + qtlDatasetExample, + methods = "susie", + cisWindow = 1e6, + weightStandardized = FALSE + ), + "unused argument" + ) +}) + +test_that(".fmMsDataSource leaves a mixed collection's joint rows NA", { + mixed <- structure(list(), class = "fmprov_mixed") + local_mocked_bindings( + qtlDatasets = function(x, ...) list(1), + sumStats = function(x, ...) list(1), + .package = "pecotmr" + ) + expect_true(is.na(pecotmr:::.fmMsDataSource(mixed))) +}) + +test_that(".fmMsDataSource derives a single-kind collection's joint rows", { + local_mocked_bindings( + qtlDatasets = function(x, ...) list(), + sumStats = function(x, ...) list(1), + .package = "pecotmr" + ) + expect_equal(pecotmr:::.fmMsDataSource(NULL), "sumstat") + local_mocked_bindings( + qtlDatasets = function(x, ...) list(1), + sumStats = function(x, ...) list(), + .package = "pecotmr" + ) + expect_equal(pecotmr:::.fmMsDataSource(NULL), "individual") +}) + +test_that("individual-level genotypes reach the fitter standardized", { + # Through residualizedGenotypes(), which owns the scaling. + # The single preparation point for every individual-level fit in the + # package: fine-mapping's per-tuple and joint paths, the TWAS joint + # engine, and the top-PC construction all come through it. + data(qtlDatasetExample, envir = environment()) + X <- pecotmr:::.fmResidGeno( + qtlDatasetExample, + ResidualizationParam(), + contexts = "brain", + traitId = "ENSG_example", + cisWindow = 1e6 + ) + expect_true(max(abs(colMeans(X))) < 1e-10) + sds <- matrixStats::colSds(X) + expect_true(all(abs(sds[sds > 0] - 1) < 1e-10)) +}) + +test_that("a constant genotype column comes back as zeros, not NaN", { + # `.qtlResidualizeQr()` treats anything below sqrt(.Machine$double.eps) as + # constant: lm.fit residuals of a constant column are ~1e-16 rather than + # 0, and dividing the (also tiny) residuals by that amplifies roundoff to + # unit scale. The column is zeroed instead, and .dropZeroVariance() + # removes it downstream. + Y <- cbind(a = c(1, 2, 3, 4), b = c(2, 2, 2, 2)) + out <- pecotmr:::.qtlResidualizeQr(Y, NULL, scaleResiduals = TRUE) + expect_equal(unname(out[, "b"]), rep(0, 4)) + expect_equal(sd(out[, "a"]), 1) + expect_false(anyNA(out)) +}) + +test_that("the genotype scale comes from the accessor, not the pipeline", { + # ONE knob: ResidualizationParam(scaleResiduals=) per call, with the + # QtlDataset's own slot as the default. The pipelines add no mechanism of + # their own -- they only RECORD what the accessor resolved, so a wrong + # record and a wrong scale cannot come apart. + data(qtlDatasetExample, envir = environment()) + recorded <- function(..., dataset = qtlDatasetExample) { + res <- suppressMessages(fineMappingPipeline( + dataset, + methods = "susie", + cisWindow = 1e6, + residualizationArgs = ResidualizationParam(...) + )) + unique(as.logical(S4Vectors::mcols(res)$standardized)) + } + expect_true(recorded()) + expect_true(recorded(scaleResiduals = TRUE)) + expect_false(recorded(scaleResiduals = FALSE)) + # Unset falls back to the object, which is where the default lives. + off <- methods::initialize(qtlDatasetExample, scaleResiduals = FALSE) + expect_false(recorded(dataset = off)) + # An explicit per-call value overrides the object either way. + expect_true(recorded(scaleResiduals = TRUE, dataset = off)) +}) + +test_that("the pipelines add no second standardization mechanism", { + # The accessor owns it. A parallel step inside .fmResidGeno() would have + # covered only its own callers -- colocboostPipeline() calls + # residualizedGenotypes() directly and would have been left out. + expect_false(exists( + ".fmStandardizeGenotypes", + envir = asNamespace("pecotmr") + )) + expect_false(is_in( + "standardizeGenotypes", + names(formals(pecotmr:::.fmPipelineQtlDataset)) + )) +}) + +test_that("the hatch does not reach a summary-statistics fit", { + # There are no genotypes to scale, and the RSS likelihood is in + # standardized units whatever anyone asks, so the flag stays TRUE. + res <- suppressWarnings(suppressMessages( + fineMappingPipeline(.rssf_qcd(), methods = "susie") + )) + expect_true(all(as.logical(S4Vectors::mcols(res)$standardized))) +}) diff --git a/tests/testthat/test_fineMappingRow.R b/tests/testthat/test_fineMappingRow.R index 6e9c3f36..4ccd746a 100644 --- a/tests/testthat/test_fineMappingRow.R +++ b/tests/testthat/test_fineMappingRow.R @@ -5,7 +5,11 @@ .makeAdjustEntry <- function(vids, maxNumSingleEffects = 2L) { p <- length(vids) set.seed(11L) - lbf <- matrix(rnorm(maxNumSingleEffects * p), nrow = maxNumSingleEffects, ncol = p) + lbf <- matrix( + rnorm(maxNumSingleEffects * p), + nrow = maxNumSingleEffects, + ncol = p + ) colnames(lbf) <- vids alpha <- lbfToAlpha(lbf) pip <- as.numeric(1 - apply(1 - alpha, 2, prod)) @@ -509,11 +513,22 @@ test_that("credibleSets / topLoci minPurity filters CS by purity, independent of # A fit whose alpha was built from a deliberately NON-uniform prior, so the # stored-alpha route and the rebuild-from-lbf route disagree. -.makeNonUniformPriorEntry <- function(vids, prior, maxNumSingleEffects = 2L, seed = 404L) { +.makeNonUniformPriorEntry <- function( + vids, + prior, + maxNumSingleEffects = 2L, + seed = 404L +) { p <- length(vids) set.seed(seed) - lbf <- matrix(rnorm(maxNumSingleEffects * p, sd = 2), maxNumSingleEffects, p, dimnames = list(NULL, vids)) - w <- exp(sweep(lbf, 1L, apply(lbf, 1L, max), `-`)) * rep(prior, each = maxNumSingleEffects) + lbf <- matrix( + rnorm(maxNumSingleEffects * p, sd = 2), + maxNumSingleEffects, + p, + dimnames = list(NULL, vids) + ) + w <- exp(sweep(lbf, 1L, apply(lbf, 1L, max), `-`)) * + rep(prior, each = maxNumSingleEffects) alpha <- w / rowSums(w) pip <- as.numeric(1 - apply(1 - alpha, 2, prod)) list( @@ -758,7 +773,11 @@ test_that("lbf is empty when the fit carries no lbf matrix", { test_that("lbf aggregates over a collection, carrying identity + NA-filling ragged effects", { vids <- c("chr1:100:A:G", "chr1:200:C:T", "chr1:300:G:A") mkEntry <- function(maxNumSingleEffects) { - lbf <- matrix(seq_len(maxNumSingleEffects * length(vids)), nrow = maxNumSingleEffects, byrow = TRUE) # L x p + lbf <- matrix( + seq_len(maxNumSingleEffects * length(vids)), + nrow = maxNumSingleEffects, + byrow = TRUE + ) # L x p fit <- structure( list(lbf_variable = lbf, pip = rep(0.3, length(vids))), class = "susie" @@ -856,8 +875,11 @@ test_that("credibleSetSummary returns one row per CS with size/purity/V/logBF/le expect_equal(s$cs_log_bf, c(1.9597, 1.2031), tolerance = 1e-3) # cs_pip = summed member PIP (inclusion mass captured) expect_equal(s$cs_pip, c(1.1, 0.8), tolerance = 1e-6) - # cs_mean_effect = NA for univariate susie (no conditional_effect column) - expect_true(all(is.na(s$cs_mean_effect))) + # cs_mean_effect = mean(posterior_mean / pip) over the members. It was NA + # for every univariate susie fit while it read a `conditional_effect` + # column that only multi-condition fits ever got -- an artefact of where + # the column came from, not of the quantity. + expect_true(all(is.finite(s$cs_mean_effect))) # lead = highest-PIP member expect_equal(s$lead_variant, c("chr1:100:A:G", "chr1:300:A:G")) }) @@ -1031,7 +1053,10 @@ test_that("fsusieCredibleBand returns a long effect + band table (lower <= effec skip_if_not_installed("fsusieR") skip_if_not_installed("wavethresh") cb <- .fmrRowFsusieCredibleBand(.fsa_entry(.fsa_makeFit())) - expect_named(cb, c("cs", "chrom", "pos", "effect", "lower", "upper")) + expect_named( + cb, + c("cs", "chrom", "pos", "effect", "lower", "upper", "halfwidth") + ) expect_gt(nrow(cb), 0) expect_true(all( cb$lower <= cb$effect + 1e-9 & cb$effect <= cb$upper + 1e-9 @@ -1047,7 +1072,10 @@ test_that("trimmed fSuSiE keeps curves, bands and coloc inputs", { expectedFit <- pecotmr:::.fsusiePopulateCredibleBand(fit) tables <- list(list(sets = list(cs = list()))) trimmed <- pecotmr:::trimFinemappingFit( - fit, seq_along(fit$alpha), "fsusie", tables + fit, + seq_along(fit$alpha), + "fsusie", + tables ) entry <- unserialize(serialize(.fsa_entry(trimmed), NULL)) saved <- entry@susieFit @@ -1117,6 +1145,103 @@ test_that("fsusie accessors degrade to empty for a non-fSuSiE / trimmed fit (no expect_equal(length(.fmrRowFsusieAffectedRegions(e)), 0L) }) +# ---- fsusieTraitEffects --------------------------------------------- + +# The pipeline stamps trait_positions onto the stored fit (jointEngine.R) and +# keeps the matching names in the row's jointTraits column; rebuild that pairing +# here, since .fsa_makeFit() returns the raw susiF fit. +.fsa_jointFit <- function(fit, pos) { + fit$trait_positions <- pos + fit +} + +test_that("fsusieTraitEffects evaluates the curve at each joint trait", { + skip_if_not_installed("fsusieR") + skip_if_not_installed("wavethresh") + fit <- .fsa_makeFit() + nTrait <- length(fit$outing_grid) + # Positions BETWEEN grid points, so the interpolation is doing real work. + pos <- as.numeric(seq_len(nTrait)) + 0.5 + names <- paste0("gene", seq_len(nTrait)) + res <- QtlFineMappingResult( + studyName = "s", + context = "brain", + trait = "g", + method = "fsusie", + entry = list(.fsa_entry(.fsa_jointFit(fit, pos))), + jointTraits = paste(names, collapse = ";") + ) + + te <- fsusieTraitEffects(res) + expect_true(all( + c( + "study", + "context", + "trait", + "method", + "cs", + "chrom", + "jointTrait", + "traitPos", + "effect" + ) %in% + names(te) + )) + nCs <- length(fit$fitted_func) + expect_equal(nrow(te), nCs * nTrait) + expect_equal(unique(te$jointTrait), names) + expect_equal(sort(unique(te$traitPos)), sort(pos)) + + # The effect is the fitted curve interpolated onto the trait positions, + # with constant extrapolation past the grid ends. + first <- te[te$cs == "fsusie_1", ] + expect_equal( + first$effect, + stats::approx( + as.numeric(fit$outing_grid), + as.numeric(fit$fitted_func[[1L]]), + xout = pos, + rule = 2, + ties = "ordered" + )$y + ) +}) + +test_that("fsusieTraitEffects refuses a fit whose positions and names disagree", { + skip_if_not_installed("fsusieR") + skip_if_not_installed("wavethresh") + fit <- .fsa_makeFit() + res <- QtlFineMappingResult( + studyName = "s", + context = "brain", + trait = "g", + method = "fsusie", + entry = list(.fsa_entry(.fsa_jointFit( + fit, + seq_along(fit$outing_grid) + ))), + jointTraits = "only;two" + ) + expect_error(fsusieTraitEffects(res), "trait position") +}) + +test_that("fsusieTraitEffects returns zero rows for a non-fsusie fit", { + res <- QtlFineMappingResult( + studyName = "s", + context = "brain", + trait = "g", + method = "susie", + entry = list(fineMappingRow( + variantIds = "chr1:100:A:G", + susieFit = list(pip = 0.5), + topLoci = data.frame(variant_id = "chr1:100:A:G", pip = 0.5) + )) + ) + te <- fsusieTraitEffects(res) + expect_equal(nrow(te), 0L) + expect_true(all(c("cs", "jointTrait", "traitPos", "effect") %in% names(te))) +}) + test_that("fsusieCredibleBand + fsusieAffectedRegions aggregate across a collection", { skip_if_not_installed("fsusieR") skip_if_not_installed("wavethresh") @@ -1367,7 +1492,11 @@ test_that("topLoci must list the variants in the same order", { .fmr_susieFit <- function(p = 5L, maxNumSingleEffects = 2L, seed = 7L) { set.seed(seed) - lbf <- matrix(rnorm(maxNumSingleEffects * p, sd = 3), maxNumSingleEffects, p) + lbf <- matrix( + rnorm(maxNumSingleEffects * p, sd = 3), + maxNumSingleEffects, + p + ) alpha <- lbfToAlpha(lbf) list( alpha = alpha, @@ -1590,12 +1719,24 @@ test_that("an untrimmed fit with no recorded coverage uses its primary sets", { expect_equal(pecotmr:::.csSetsForCoverage(fit, 0.95)$cs$L1, 1:2) }) -test_that("mean conditional effect is NA when the column is absent", { +test_that("mean conditional effect is the posterior mean over the PIP", { + # NA when either column the quotient needs is absent. expect_true(is.na(pecotmr:::.csMeanEffect(tibble(a = 1)))) + expect_true(is.na(pecotmr:::.csMeanEffect(tibble(pip = c(0.5, 0.5))))) expect_equal( - pecotmr:::.csMeanEffect(tibble(conditional_effect = c(1, 3))), + pecotmr:::.csMeanEffect( + tibble(posterior_mean = c(0.5, 1.5), pip = c(0.5, 0.5)) + ), 2 ) + # A zero PIP has no conditional effect to speak of, so it drops out + # rather than contributing an infinity. + expect_equal( + pecotmr:::.csMeanEffect( + tibble(posterior_mean = c(0.5, 1), pip = c(0.5, 0)) + ), + 1 + ) }) test_that("rebuilding an empty topLoci returns it unchanged", { @@ -1731,15 +1872,36 @@ test_that(".csLog10Bf and .csMeanEffect return NA, never -Inf or NaN", { expect_identical(pecotmr:::.csMeanEffect(list()), NA_real_) expect_identical( - pecotmr:::.csMeanEffect(list(conditional_effect = numeric(0))), + pecotmr:::.csMeanEffect( + list(posterior_mean = numeric(0), pip = numeric(0)) + ), NA_real_ ) expect_identical( - pecotmr:::.csMeanEffect(list(conditional_effect = c(NA, NA))), + pecotmr:::.csMeanEffect( + list(posterior_mean = c(NA, NA), pip = c(0.5, 0.5)) + ), NA_real_ ) expect_identical( - pecotmr:::.csMeanEffect(list(conditional_effect = c(1, 3))), + pecotmr:::.csMeanEffect( + list(posterior_mean = c(0.5, 1.5), pip = c(0.5, 0.5)) + ), 2 ) }) + +test_that("fsusieCredibleBand() carries the band half-width", { + data(fsusieFineMappingExample) + band <- as.data.frame(fsusieCredibleBand(fsusieFineMappingExample)) + expect_true(is_in("halfwidth", names(band))) + expect_gt(nrow(band), 0L) + expect_equal(band$halfwidth, (band$upper - band$lower) / 2) + # A band half-width is a magnitude, never negative. + expect_true(all(band$halfwidth >= 0)) +}) + +test_that("an empty credible band still declares halfwidth", { + expect_true(is_in("halfwidth", names(.emptyCredibleBand()))) + expect_identical(nrow(.emptyCredibleBand()), 0L) +}) diff --git a/tests/testthat/test_fineMappingWrappers.R b/tests/testthat/test_fineMappingWrappers.R index cbfe9044..4b2ee7b2 100644 --- a/tests/testthat/test_fineMappingWrappers.R +++ b/tests/testthat/test_fineMappingWrappers.R @@ -631,6 +631,9 @@ if (!exists(".makeUnivariateData", inherits = FALSE)) { "marginal_beta", "marginal_se", "marginal_z", + # Whether the SOURCE supplied z, which is what GWAS-VCF's `EZ` means: a + # Wald z pecotmr derived from beta/se must not be published as provided. + "marginal_z_supplied", "marginal_p", "pip", "posterior_mean", @@ -913,11 +916,12 @@ test_that("buildTopLoci: mvsusie per-condition lfsr + effect (long)", { method = "mvsusie", conditionIdx = 1L ) - expect_true(all(c("conditional_effect", "lfsr") %in% names(tl1))) - expect_true(is.numeric(tl1$conditional_effect) && is.numeric(tl1$lfsr)) - # conditional_effect = coef[, condition] / pip, for every variant - expect_equal(unname(tl1$conditional_effect), unname(coef[, 1] / pip)) - # lfsr populated ONLY for the 0.95 CS variants, pulled from clfsr[L, variant, condition] + expect_true(is_in("lfsr", names(tl1))) + expect_true(is.numeric(tl1$lfsr)) + # `conditional_effect` is gone: it was coef[, condition] / pip, and the + # posterior mean is on coef()'s scale now, so the table already held it. + expect_false(is_in("conditional_effect", names(tl1))) + # lfsr populated ONLY for the 0.95 CS variants, from clfsr[L, variant, condition] expect_equal(tl1$lfsr[1:2], c(0.001, 0.02)) expect_true(all(is.na(tl1$lfsr[3:4]))) # conditionIdx = 2 selects the other condition's coef + clfsr slice @@ -928,14 +932,13 @@ test_that("buildTopLoci: mvsusie per-condition lfsr + effect (long)", { method = "mvsusie", conditionIdx = 2L ) - expect_equal(unname(tl2$conditional_effect), unname(coef[, 2] / pip)) expect_equal(tl2$lfsr[1:2], c(0.30, 0.40)) # univariate fit (no conditionIdx) keeps the base schema, no extra columns tl0 <- buildTopLoci(fit, cst, variantNames = vids, method = "susie") expect_false(any(c("conditional_effect", "lfsr") %in% names(tl0))) }) -test_that("topLoci surfaces mvsusie conditional_effect + lfsr through the projector", { +test_that("topLoci surfaces the mvsusie lfsr through the projector", { tl <- data.frame( variant_id = c("chr1:100:A:G", "chr1:200:C:T"), chrom = "1", @@ -946,7 +949,6 @@ test_that("topLoci surfaces mvsusie conditional_effect + lfsr through the projec posterior_mean = c(0.1, 0.2), posterior_sd = c(0.05, 0.05), cs_95 = c("mvsusie_1", "mvsusie_1"), - conditional_effect = c(0.3, -0.2), lfsr = c(0.001, 0.02), stringsAsFactors = FALSE ) @@ -956,9 +958,8 @@ test_that("topLoci surfaces mvsusie conditional_effect + lfsr through the projec topLoci = tl ) out <- .fmrRowTopLoci(e, signalCutoff = 0) - expect_true(all(c("conditional_effect", "lfsr") %in% names(out))) + expect_true(is_in("lfsr", names(out))) expect_equal(out$lfsr, c(0.001, 0.02)) - expect_equal(out$conditional_effect, c(0.3, -0.2)) }) test_that("buildTopLoci derives CS columns from the ACTUAL coverages (no hardcoded 0.95/0.70/0.50)", { @@ -1694,7 +1695,10 @@ test_that("missing region produces NA grange columns rather than silent omission expect_equal(unique(out$event), "ctx_ENSG00000179403") }) -test_that("posterior_mean equals colSums(alpha*mu); posterior_sd equals sqrt(pmax(colSums(alpha*mu2) - mean^2, 0))", { +test_that("posterior_mean is colSums(alpha*mu) when the fit records no scale", { + # The no-scale-factors case: an RSS fit has no X to scale, and a fit run + # with standardize = FALSE records all-1s, so the quotient below is the + # identity. The division itself is pinned by the next test. variant_ids <- c("chr1:100:A:G", "chr1:200:C:T") cs_at_cov <- list( "0.95" = list(c(1L, 2L)), @@ -1726,6 +1730,47 @@ test_that("posterior_mean equals colSums(alpha*mu); posterior_sd equals sqrt(pma }) +test_that("posterior_mean and posterior_sd are on the SUPPLIED X scale", { + # `.btlPosterior()` reports what `susieR::coef.susie()` reports: + # (colSums(alpha*mu) + theta) / X_column_scale_factors. Without the + # division this column was the only quantity in the package on the + # standardized scale, so the same fit gave two different "posterior + # effects" depending on whether you read topLoci() or susieWeights(). + nV <- 3L + fit <- list( + alpha = matrix( + c(0.6, 0.3, 0.1, 0.2, 0.7, 0.1), + nrow = 2L, + byrow = TRUE + ), + mu = matrix(c(0.5, 1.0, 2.0, 0.4, 0.8, 1.6), nrow = 2L, byrow = TRUE), + mu2 = matrix(c(0.5, 1.2, 4.2, 0.4, 0.9, 3.0), nrow = 2L, byrow = TRUE), + X_column_scale_factors = c(0.5, 2, 4) + ) + meanStd <- colSums(fit$alpha * fit$mu) + sdStd <- sqrt(pmax(colSums(fit$alpha * fit$mu2) - meanStd^2, 0)) + post <- pecotmr:::.btlPosterior(fit, NULL, nV) + expect_equal(post$postMean, meanStd / fit$X_column_scale_factors) + # An SD rescales the way its mean does, but the mu2 identity only holds + # in the units mu2 is stored in -- so it is computed there, then divided. + expect_equal(post$postSd, sdStd / fit$X_column_scale_factors) + + # susieInf / susieAsh carry an infinitesimal term that coef.susie adds in; + # dropping it would silently change what the posterior effect means. + withTheta <- `$<-`(fit, "theta", c(0.1, -0.2, 0.05)) + expect_equal( + pecotmr:::.btlPosterior(withTheta, NULL, nV)$postMean, + (meanStd + withTheta$theta) / fit$X_column_scale_factors + ) + + # A zero factor can only come from a constant column, which carries no + # effect: treated as 1 rather than dividing to Inf. + zeroed <- `$<-`(fit, "X_column_scale_factors", c(0.5, 0, 4)) + expect_true(all(is.finite( + pecotmr:::.btlPosterior(zeroed, NULL, nV)$postMean + ))) +}) + # ---- cal_purity ---- test_that("cal_purity with min method and single element CS", { set.seed(42) @@ -3618,14 +3663,6 @@ test_that("the N column falls back to the fit N and recycles a scalar", { # buildTopLoci per-condition blocks # =========================================================================== -test_that("conditional effects are NA when the fit carries no coefficients", { - # susie without a coef matrix and no mvsusie fallback: one NA per variant - # rather than a shorter vector that would misalign the column. - out <- pecotmr:::.btlCondEffect(list(), "susie", 1L, 3L) - expect_length(out, 3L) - expect_true(all(is.na(out))) -}) - test_that("conditional lfsr passes through when no coverage level matches", { clf <- array(0.1, dim = c(2L, 3L, 1L)) out <- pecotmr:::.btlCondLfsr( diff --git a/tests/testthat/test_jointEngine.R b/tests/testthat/test_jointEngine.R index 554ebce6..84a1e201 100644 --- a/tests/testthat/test_jointEngine.R +++ b/tests/testthat/test_jointEngine.R @@ -3123,3 +3123,22 @@ test_that(".twasEnsembleCollect answers empty parts when no token contributes", expect_identical(out$wts, list()) expect_identical(out$rsq, c()) }) + +test_that(".jointAfForGroup resolves allele frequencies for an individual group", { + data(qtlDatasetExample) + X <- matrix( + c(0, 1, 2, 1, 2, 0), + nrow = 3, + dimnames = list(c("s1", "s2", "s3"), c("chr1:100:A:T", "chr1:200:C:G")) + ) + g <- new( + "IndividualJointGroup", + conditions = tibble(study = "s", context = "c", trait = "t"), + X = X, + Y = matrix(1:3, ncol = 1, dimnames = list(c("s1", "s2", "s3"), "c:t")) + ) + # Not a QtlDataset -> no af rather than an error. + expect_null(.jointAfForGroup(NULL, g)) + # A SumStats group has no design matrix to take frequencies from. + expect_null(.jointAfForGroup(qtlDatasetExample, "not a group")) +}) diff --git a/tests/testthat/test_manifestLoaders.R b/tests/testthat/test_manifestLoaders.R index 7c4772da..8ab98967 100644 --- a/tests/testthat/test_manifestLoaders.R +++ b/tests/testthat/test_manifestLoaders.R @@ -1816,3 +1816,79 @@ test_that("manifest loaders guard the QtlDataset pass-through arguments", { "xvarCutoff.*is not >= 0" ) }) + +# --------------------------------------------------------------------------- +# AF vs MAF on the GWAS-VCF path +# +# `AF` is the spec's ALTERNATIVE-allele frequency -- directional, and what the +# posterior view publishes as `af`. It used to be a fallback for the MAF slot +# and was then folded with pmin(af, 1 - af), so an AF of 0.8 came back as a +# MAF of 0.2 with no `AF` column at all: pecotmr's own VCF output did not +# round-trip, and the direction the standardized-to-raw weight conversion +# needs was gone. The mirror of the released writer bug where `AF` held MAF. +# --------------------------------------------------------------------------- + +# @noRd +.afvcf_sumstats <- function(af = c(0.8, 0.3)) { + gr <- GenomicRanges::GRanges( + "chr1", + IRanges::IRanges(c(100L, 200L), width = 1L) + ) + cols <- S4Vectors::DataFrame( + SNP = c("chr1:100:A:G", "chr1:200:C:T"), + A1 = c("G", "T"), + A2 = c("A", "C"), + Z = c(2.5, -1.2), + N = c(1000L, 1000L), + BETA = c(0.25, -0.12), + SE = c(0.1, 0.1), + z_supplied = c(TRUE, TRUE) + ) + if (!is.null(af)) { + cols$AF <- af + } + GwasSumStats( + studyName = "s", + entry = list(S4Vectors::`mcols<-`(gr, value = cols)), + genome = "hg38" + ) +} + +test_that("a directional AF survives the GWAS-VCF round trip", { + skip_if_not_installed("VariantAnnotation") + out <- withr::local_tempfile(fileext = ".vcf") + exportToVcf(.afvcf_sumstats(), out) + back <- .readSumStatsVcf(out, NULL, NULL, NULL, "x") + # Unfolded, and under its own name -- 0.8 is an ALT frequency above 0.5, + # which is exactly the case folding destroys. + expect_equal(as.numeric(back$AF), c(0.8, 0.3)) + # A directionless MAF is still derived for QC, alongside AF rather than + # instead of it. + expect_equal(as.numeric(back$MAF), c(0.2, 0.3)) +}) + +test_that("a GWAS-VCF with no AF has neither AF nor a folded one", { + skip_if_not_installed("VariantAnnotation") + out <- withr::local_tempfile(fileext = ".vcf") + exportToVcf(.afvcf_sumstats(af = NULL), out) + back <- .readSumStatsVcf(out, NULL, NULL, NULL, "x") + expect_false(is_in("AF", names(back))) + expect_false(is_in("MAF", names(back))) +}) + +test_that("an explicit formatMapping still redirects the AF slot", { + skip_if_not_installed("VariantAnnotation") + out <- withr::local_tempfile(fileext = ".vcf") + exportToVcf(.afvcf_sumstats(), out) + # Reading the spec's AF field into the MAF slot on purpose: the caller + # says this file's AF is not to be trusted as directional. + back <- .readSumStatsVcf( + out, + NULL, + NULL, + list(AF = "NOPE", MAF = "AF"), + "x" + ) + expect_false(is_in("AF", names(back))) + expect_equal(as.numeric(back$MAF), c(0.2, 0.3)) +}) diff --git a/tests/testthat/test_overlapTopLoci.R b/tests/testthat/test_overlapTopLoci.R index cd3c5e2b..73c43694 100644 --- a/tests/testthat/test_overlapTopLoci.R +++ b/tests/testthat/test_overlapTopLoci.R @@ -191,5 +191,5 @@ test_that("overlapTopLoci returns a GRanges of the shared variants", { gr <- overlapTopLoci(qtl, gwas, signalCutoff = 0) expect_s4_class(gr, "GRanges") expect_equal(length(gr), 1L) - expect_true("gwas_beta" %in% names(S4Vectors::mcols(gr))) + expect_true("gwas_posterior_effect" %in% names(S4Vectors::mcols(gr))) }) diff --git a/tests/testthat/test_resultReaders.R b/tests/testthat/test_resultReaders.R new file mode 100644 index 00000000..34e08312 --- /dev/null +++ b/tests/testthat/test_resultReaders.R @@ -0,0 +1,675 @@ +# --------------------------------------------------------------------------- +# Published result readers: readFineMappingResult() / readTwasWeights() +# +# The reader and the writer are each other's only real test. Each direction +# had only ever been checked against a stored fixture before, which is how the +# two came to disagree about a reserved key while both "passed" -- so most of +# what is here is a round trip, and the comparisons are against the ORIGINAL +# object rather than against a file. +# --------------------------------------------------------------------------- + +# @noRd +.rdr_tmp <- function(name) file.path(tempdir(), name) + +# The ways a round trip legitimately differs from its input, so a test can say +# "everything else is identical" and mean it: +# dataSource / standardized ADDED on the way back (one is derived by the +# writer, the other supplied to the reader) +# cs_ the published form is an integer index, and "susie_0" -- the +# not-captured sentinel -- publishes as NA, which is the canonical form +# every reader of the column already treats as "in no credible set" +# z_supplied resolved from NA to FALSE: a file publishing ES/SE and no EZ +# says the z was NOT supplied by the source, which is what the fixture's +# NA meant before the flag existed +# @noRd +.RDR_NORMALIZED <- c( + "dataSource", + "standardized", + "cs_95", + "cs_70", + "cs_50", + "z_supplied" +) + +# Which columns of two per-variant tables disagree, aligned on variant id. +# +# Aligned rather than compared in place because a published file is sorted +# genomically and the stored table is not, and restricted to the variants +# `b` carries because an export is topLoci-FILTERED by default -- the +# question is whether what was published came back intact, not whether a +# filtered file holds every variant of the fit. +# @noRd +.rdr_diff <- function(a, b, tolerance = 1e-6) { + a <- a[is_in(a$variant_id, b$variant_id), ] + b <- b[match(a$variant_id, b$variant_id), ] + shared <- intersect(names(a), names(b)) + bad <- character(0) + for (nm in shared) { + va <- a[[nm]] + vb <- b[[nm]] + ok <- if (is.numeric(va) && is.numeric(vb)) { + isTRUE(all.equal(va, vb, tolerance = tolerance)) + } else { + identical(as.character(va), as.character(vb)) + } + if (!ok) { + bad <- c(bad, nm) + } + } + setdiff(bad, .RDR_NORMALIZED) +} + +# A minimal published table written by hand, as a user converting another +# tool's output would produce it. +# @noRd +.rdr_writeTable <- function(df, name) { + path <- .rdr_tmp(name) + utils::write.table( + df, + path, + sep = "\t", + quote = FALSE, + row.names = FALSE, + na = "NA" + ) + path +} + +# ---- round trips ---------------------------------------------------------- + +test_that("a fine-mapping result round-trips through a published BED", { + data(qtlFineMappingExample, envir = environment()) + path <- .rdr_tmp("rt_fm.bed") + exportToBed(qtlFineMappingExample, path, columns = "all") + back <- readFineMappingResult(path, weightStandardized = TRUE) + expect_s4_class(back, "QtlFineMappingResult") + expect_equal(nrow(back), nrow(qtlFineMappingExample)) + expect_equal( + .rdr_diff( + as.data.frame(topLoci(qtlFineMappingExample)), + as.data.frame(topLoci(back)) + ), + character(0) + ) + # The marginal view is the other half of the same table and has its own + # published fields (ES/SE/EZ/LP), so it is checked separately. + expect_equal( + .rdr_diff( + as.data.frame(marginalEffects(qtlFineMappingExample)), + as.data.frame(marginalEffects(back)) + ), + character(0) + ) + # The analysis WINDOW, without which genomicRegion() silently degrades to + # the span of the variants that survived QC. + expect_equal( + genomicRegion(back), + genomicRegion(qtlFineMappingExample) + ) +}) + +test_that("a fine-mapping result round-trips through a published VCF", { + data(qtlFineMappingExample, envir = environment()) + path <- .rdr_tmp("rt_fm.vcf") + exportToVcf(qtlFineMappingExample, path, context = "context_1") + back <- readFineMappingResult(path, weightStandardized = TRUE) + expect_s4_class(back, "QtlFineMappingResult") + expect_equal( + .rdr_diff( + as.data.frame(topLoci(qtlFineMappingExample)), + as.data.frame(topLoci(back)), + tolerance = 1e-5 + ), + character(0) + ) + expect_equal(genomicRegion(back), genomicRegion(qtlFineMappingExample)) +}) + +test_that("the two formats agree with each other", { + # The point of one shared vocabulary: whichever format a result was + # published in, it reads back the same. A `region_start` that is 0-based + # in the table and 1-based in the VCF header was caught by exactly this. + data(qtlFineMappingExample, envir = environment()) + bed <- .rdr_tmp("agree.bed") + vcf <- .rdr_tmp("agree.vcf") + exportToBed(qtlFineMappingExample, bed, columns = "all") + exportToVcf(qtlFineMappingExample, vcf, context = "context_1") + fromBed <- readFineMappingResult(bed, weightStandardized = TRUE) + fromVcf <- readFineMappingResult(vcf, weightStandardized = TRUE) + expect_equal(genomicRegion(fromBed), genomicRegion(fromVcf)) + expect_equal( + .rdr_diff( + as.data.frame(topLoci(fromBed)), + as.data.frame(topLoci(fromVcf)), + tolerance = 1e-5 + ), + character(0) + ) +}) + +test_that("a published VCF restores the per-effect fit the header carried", { + # `V` and `sets$cs_index` are per EFFECT, so no per-variant column can + # hold them -- the `##SAMPLE` block is why they survive at all, and + # colocalization's credible-set filter reads them off the fit. + data(qtlFineMappingExample, envir = environment()) + path <- .rdr_tmp("fit.vcf") + exportToVcf(qtlFineMappingExample, path, context = "context_1") + fit <- susieFit(readFineMappingResult(path, weightStandardized = TRUE)) + original <- susieFit(qtlFineMappingExample) + expect_equal(as.numeric(fit$V), as.numeric(original$V), tolerance = 1e-8) + expect_equal( + as.integer(fit$sets$cs_index), + as.integer(original$sets$cs_index) + ) + # A BED has no header to carry them, so there the fit is honestly absent + # rather than half-invented. + bed <- .rdr_tmp("fit.bed") + exportToBed(qtlFineMappingExample, bed) + expect_null(susieFit(readFineMappingResult(bed, weightStandardized = TRUE))) +}) + +test_that("TWAS weights round-trip through a published BED", { + data(twasWeightsExample, envir = environment()) + path <- .rdr_tmp("rt_w.bed") + expect_message( + exportToBed(twasWeightsExample, path), + "fine-mapping-derived" + ) + back <- readTwasWeights(path) + # Fine-mapping-derived rows are skipped on export, so fewer rows come + # back than went out -- that is the writer's rule, not a reader loss. + expect_lt(nrow(back), nrow(twasWeightsExample)) + md <- S4Vectors::mcols(back) + for (i in seq_len(nrow(back))) { + key <- list( + studyName = as.character(md$study[[i]]), + context = as.character(md$context[[i]]), + trait = as.character(md$trait[[i]]), + method = as.character(md$method[[i]]) + ) + was <- exec(resolveWeights, twasWeightsExample, !!!key) + now <- exec(resolveWeights, back, !!!key) + expect_setequal(now$variantIds, was$variantIds) + expect_equal( + as.numeric(now$weights)[order(now$variantIds)], + as.numeric(was$weights)[order(was$variantIds)], + tolerance = 1e-10 + ) + } + expect_equal( + genomicRegion(back), + genomicRegion(twasWeightsExample)[ + match( + as.character(md$method), + as.character(S4Vectors::mcols(twasWeightsExample)$method) + ) + ] + ) +}) + +# ---- multi-row tables ----------------------------------------------------- + +test_that("a merged table reads back as a multi-row collection", { + data(qtlFineMappingExample, qtlFineMappingLbfExample, envir = environment()) + combined <- combineFineMappingResults( + qtlFineMappingExample, + qtlFineMappingLbfExample + ) + path <- .rdr_tmp("merged.bed") + exportToBed(combined, path) + back <- readFineMappingResult(path, weightStandardized = TRUE) + expect_equal(nrow(back), nrow(combined)) + md <- S4Vectors::mcols(back) + expect_setequal( + as.character(md$trait), + as.character(S4Vectors::mcols(combined)$trait) + ) +}) + +test_that("a per-result value that disagrees within one tuple is an error", { + # Two results concatenated under one identity. Picking either window + # would silently publish one fit's region as the other's. + df <- data.frame( + `#chr` = c("chr1", "chr1"), + start = c(99L, 199L), + end = c(100L, 200L), + ID = c("chr1:100:A:G", "chr1:200:C:T"), + PIP = c(0.9, 0.1), + study = c("s", "s"), + context = c("c", "c"), + trait = c("t", "t"), + method = c("susie", "susie"), + region_start = c(0L, 500L), + region_end = c(1000L, 1500L), + check.names = FALSE + ) + path <- .rdr_writeTable(df, "clash.bed") + expect_error( + readFineMappingResult(path), + "`region_start` takes 2 different values" + ) +}) + +# ---- supplying what the file does not record ------------------------------- + +test_that("identity arguments fill in a file that did not record them", { + df <- data.frame( + `#chr` = "chr1", + start = 99L, + end = 100L, + ID = "chr1:100:A:G", + PIP = 0.9, + check.names = FALSE + ) + path <- .rdr_writeTable(df, "bare.bed") + back <- readFineMappingResult( + path, + studyName = "s", + context = "c", + trait = "t", + method = "susie" + ) + expect_s4_class(back, "QtlFineMappingResult") + md <- S4Vectors::mcols(back) + expect_equal(as.character(md$study), "s") + expect_equal(as.character(md$method), "susie") + expect_equal(as.numeric(topLoci(back)$pip), 0.9) +}) + +test_that("an identity argument that disagrees with the file is an error", { + df <- data.frame( + `#chr` = "chr1", + start = 99L, + end = 100L, + ID = "chr1:100:A:G", + PIP = 0.9, + study = "recorded", + context = "c", + trait = "t", + method = "susie", + check.names = FALSE + ) + path <- .rdr_writeTable(df, "disagree.bed") + expect_error( + readFineMappingResult(path, studyName = "supplied"), + "was given as 'supplied' but the file records 'recorded'" + ) + # Dropping the argument uses the file's value. + expect_equal( + as.character(S4Vectors::mcols(readFineMappingResult(path))$study), + "recorded" + ) +}) + +test_that("a result keyed by axes the file lacks names them", { + df <- data.frame( + `#chr` = "chr1", + start = 99L, + end = 100L, + ID = "chr1:100:A:G", + PIP = 0.9, + context = "c", + check.names = FALSE + ) + path <- .rdr_writeTable(df, "noaxes.bed") + expect_error( + readFineMappingResult(path), + "does not record study, trait, method.*`studyName`, `trait`, `method`" + ) +}) + +# ---- permissiveness ------------------------------------------------------- + +test_that("a table carrying only PIP is enough for a fine-mapping result", { + # TWAS needs only the weight, MR beta/se/PIP, colocalization the LBF. A + # file converted from another tool has some and not others, so the reader + # requires only what the class cannot exist without. + df <- data.frame( + `#chr` = c("chr1", "chr1"), + start = c(99L, 199L), + end = c(100L, 200L), + ID = c("chr1:100:A:G", "chr1:200:C:T"), + PIP = c(0.9, 0.05), + study = "s", + context = "c", + trait = "t", + method = "susie", + check.names = FALSE + ) + path <- .rdr_writeTable(df, "pip_only.bed") + back <- readFineMappingResult(path) + tl <- topLoci(back, signalCutoff = 0) + expect_equal(length(tl), 2L) + expect_equal(as.numeric(S4Vectors::mcols(tl)$pip), c(0.9, 0.05)) + # Absent columns are absent, not NA-filled: nothing invents a beta. + expect_true(all(is.na(marginalEffects(back)$beta))) +}) + +test_that("a table carrying only a weight is enough for TwasWeights", { + df <- data.frame( + `#chr` = c("chr1", "chr1"), + start = c(99L, 199L), + end = c(100L, 200L), + ID = c("chr1:100:A:G", "chr1:200:C:T"), + WGT = c(0.3, -0.2), + study = "s", + context = "c", + trait = "t", + method = "lasso", + weight_standardized = TRUE, + check.names = FALSE + ) + path <- .rdr_writeTable(df, "wgt_only.bed") + back <- readTwasWeights(path) + w <- resolveWeights(back) + expect_equal(as.numeric(w$weights), c(0.3, -0.2)) + expect_true(weightStandardized(back)) +}) + +test_that("ids are rebuilt from coordinates when ID does not encode them", { + df <- data.frame( + `#chr` = "chr1", + start = 99L, + end = 100L, + ID = "rs123", + REF = "A", + ALT = "G", + PIP = 0.9, + study = "s", + context = "c", + trait = "t", + method = "susie", + check.names = FALSE + ) + path <- .rdr_writeTable(df, "rsid.bed") + back <- readFineMappingResult(path) + expect_equal(variantIds(back), "chr1:100:A:G") +}) + +test_that("a table with no way to identify its variants says so", { + df <- data.frame( + ID = "rs123", + PIP = 0.9, + check.names = FALSE + ) + path <- .rdr_writeTable(df, "noid.bed") + expect_error( + readFineMappingResult(path, studyName = "s"), + "must identify its variants" + ) +}) + +test_that("a weights file read as a fine-mapping result points back", { + data(twasWeightsExample, envir = environment()) + wt <- .rdr_tmp("x_wt.bed") + suppressMessages(exportToBed(twasWeightsExample, wt)) + expect_error( + readFineMappingResult(wt, weightStandardized = TRUE), + "no `PIP` column.*readTwasWeights" + ) +}) + +test_that("a fine-mapping table reads as weights, because WGT is the weight", { + # Not a category error in this direction: a susie posterior mean IS the + # TWAS coefficient, which is why it publishes as WGT in both schemas. + data(qtlFineMappingExample, envir = environment()) + fm <- .rdr_tmp("x_fm.bed") + exportToBed(qtlFineMappingExample, fm) + asWeights <- readTwasWeights(fm, weightStandardized = TRUE) + expect_s4_class(asWeights, "TwasWeights") + expect_equal( + as.numeric(resolveWeights(asWeights)$weights), + as.numeric( + S4Vectors::mcols( + topLoci(qtlFineMappingExample) + )$posterior_effect + )[order(variantIds( + readFineMappingResult(fm, weightStandardized = TRUE) + ))], + tolerance = 1e-8 + ) +}) + +# ---- the weight scale ----------------------------------------------------- + +test_that("a published weight without its scale has to be told", { + data(twasWeightsExample, envir = environment()) + path <- .rdr_tmp("scale.bed") + suppressMessages(exportToBed(twasWeightsExample, path)) + # This fixture records the flag, so it reads without one. + expect_s4_class(readTwasWeights(path), "TwasWeights") + df <- utils::read.delim(path, check.names = FALSE) + df$weight_standardized <- NA + stripped <- .rdr_writeTable(df, "scale_stripped.bed") + expect_error( + readTwasWeights(stripped), + "publishes WGT but not `weight_standardized`" + ) + expect_s4_class( + readTwasWeights(stripped, weightStandardized = FALSE), + "TwasWeights" + ) +}) + +# ---- opt-in Wakefield log Bayes factors ------------------------------------ + +test_that("priorVariance derives log Bayes factors only when asked", { + df <- data.frame( + `#chr` = c("chr1", "chr1"), + start = c(99L, 199L), + end = c(100L, 200L), + ID = c("chr1:100:A:G", "chr1:200:C:T"), + PIP = c(0.9, 0.05), + ES = c(0.3, -0.1), + SE = c(0.05, 0.05), + study = "s", + context = "c", + trait = "t", + method = "susie", + check.names = FALSE + ) + path <- .rdr_writeTable(df, "wakefield.bed") + plain <- readFineMappingResult(path) + expect_true(all(is.na( + S4Vectors::mcols( + topLoci(plain, signalCutoff = 0) + )$logBF + ))) + derived <- readFineMappingResult(path, priorVariance = 0.04) + lbf <- S4Vectors::mcols(topLoci(derived, signalCutoff = 0))$logBF + expect_equal( + lbf, + .approxBayesFactor(df$ES / df$SE, df$SE, W = 0.04)$lbf, + tolerance = 1e-10 + ) + # Never silently over published values. + df$LBF <- c(5, 0) + withLbf <- .rdr_writeTable(df, "wakefield_has_lbf.bed") + expect_error( + readFineMappingResult(withLbf, priorVariance = 0.04), + "already carries LBF" + ) +}) + +test_that("priorVariance without a z or a standard error says so", { + df <- data.frame( + `#chr` = "chr1", + start = 99L, + end = 100L, + ID = "chr1:100:A:G", + PIP = 0.9, + study = "s", + context = "c", + trait = "t", + method = "susie", + check.names = FALSE + ) + path <- .rdr_writeTable(df, "wakefield_bare.bed") + expect_error( + readFineMappingResult(path, priorVariance = 0.04), + "publishes no z \\(EZ, or ES/SE\\)" + ) +}) + +# ---- EZ, and what it means -------------------------------------------------- + +test_that("EZ round-trips as a SUPPLIED z and is not invented", { + # The spec defines EZ as the z "provided if it was used to derive ES and + # SE". A z pecotmr computed as ES/SE must not come back claiming to be + # the source's, or a reader would believe ES/SE were derived. + df <- data.frame( + `#chr` = "chr1", + start = 99L, + end = 100L, + ID = "chr1:100:A:G", + PIP = 0.9, + ES = 0.3, + SE = 0.05, + study = "s", + context = "c", + trait = "t", + method = "susie", + check.names = FALSE + ) + noEz <- readFineMappingResult(.rdr_writeTable(df, "ez_no.bed")) + me <- marginalEffects(noEz) + expect_equal(as.numeric(me$z), 6, tolerance = 1e-8) + expect_false(any(as.logical(me$z_supplied))) + df$EZ <- 5.5 + withEz <- readFineMappingResult(.rdr_writeTable(df, "ez_yes.bed")) + me <- marginalEffects(withEz) + expect_equal(as.numeric(me$z), 5.5) + expect_true(all(as.logical(me$z_supplied))) +}) + +# ---- class choice, format choice, region ------------------------------------ + +test_that("a result with no context or trait is a GWAS result", { + data(gwasFineMappingExample, envir = environment()) + path <- .rdr_tmp("gwas.bed") + exportToBed(gwasFineMappingExample, path) + back <- readFineMappingResult(path, weightStandardized = TRUE) + expect_s4_class(back, "GwasFineMappingResult") + expect_equal( + .rdr_diff( + as.data.frame(topLoci(gwasFineMappingExample)), + as.data.frame(topLoci(back)) + ), + character(0) + ) +}) + +test_that("the format follows the extension unless it is named", { + expect_equal(pecotmr:::.prdFormat("x/y.bed.gz", "auto"), "bed") + expect_equal(pecotmr:::.prdFormat("x/y.tsv", "auto"), "bed") + expect_equal(pecotmr:::.prdFormat("x/y.vcf.gz", "auto"), "vcf") + expect_equal(pecotmr:::.prdFormat("x/y.bcf", "auto"), "vcf") + # A file named something else is read as whatever it is said to be. + expect_equal(pecotmr:::.prdFormat("x/y.txt", "vcf"), "vcf") +}) + +test_that("a tabix-indexed table can be read by region", { + data(qtlFineMappingExample, envir = environment()) + path <- .rdr_tmp("region.bed.gz") + exportToBed(qtlFineMappingExample, path) + all <- readFineMappingResult(path, weightStandardized = TRUE) + full <- topLoci(all, signalCutoff = 0) + window <- GenomicRanges::GRanges( + as.character(GenomicRanges::seqnames(full))[[1L]], + IRanges::IRanges( + start = GenomicRanges::start(full)[[1L]], + end = GenomicRanges::start(full)[[3L]] + ) + ) + part <- readFineMappingResult( + path, + region = window, + weightStandardized = TRUE + ) + expect_lt(length(variantIds(part)), length(variantIds(all))) + expect_true(all(is_in(variantIds(part), variantIds(all)))) +}) + +test_that("an un-indexed file ignores a region loudly", { + df <- data.frame( + `#chr` = "chr1", + start = 99L, + end = 100L, + ID = "chr1:100:A:G", + PIP = 0.9, + study = "s", + context = "c", + trait = "t", + method = "susie", + check.names = FALSE + ) + path <- .rdr_writeTable(df, "noindex.bed") + expect_warning( + readFineMappingResult(path, region = "chr1:1-1000"), + "not bgzipped \\+ tabix-indexed" + ) +}) + +test_that("a missing file names itself", { + expect_error( + readFineMappingResult(.rdr_tmp("absent_xyz.bed")), + "does not exist" + ) +}) + +# ---- the ##SAMPLE key table ------------------------------------------------- + +test_that("every ##SAMPLE key is alphanumeric", { + # Rsamtools' header parser splits the `<...>` on `,(?=[[:alnum:]]+=)`, so + # an underscored key is not recognised as a key at all: it glues onto the + # previous value and every field after it on the line is lost. Six of + # these were unreadable for that reason. + keys <- pecotmr:::.PUB_SAMPLE_KEYS + expect_true(all(str_detect(keys, "^[A-Za-z0-9]+$"))) + expect_equal(length(unique(keys)), length(keys)) +}) + +test_that("a per-fit field with no ##SAMPLE key stops the writer", { + expect_error( + pecotmr:::.pubSampleKeys(c("study", "brand_new_field")), + "brand_new_field.*no\\s+##SAMPLE key" + ) +}) + +test_that("the ##SAMPLE block survives a VariantAnnotation read", { + data(qtlFineMappingExample, envir = environment()) + path <- .rdr_tmp("sample_block.vcf") + exportToVcf(qtlFineMappingExample, path, context = "context_1") + meta <- pecotmr:::.prdVcfSampleMeta(VariantAnnotation::readVcf(path)) + # Named in the shared vocabulary, not the header's CamelCase: the reader + # inverts the key table so everything downstream sees one set of names. + expect_true(all(is_in( + c("study", "context", "trait", "method", "region_start", "cs_index"), + colnames(meta) + ))) + expect_equal(meta$method, "susie") +}) + +test_that("a header-only file reads back as an empty collection", { + # An export of a result whose every entry was screened out writes its + # schema and no rows. That is a legitimate file, so it reads as an empty + # collection rather than erroring about identity axes it has no rows to + # record. + header <- str_flatten(pecotmr:::.pubSchema(character(0)), "\t") + path <- .rdr_tmp("empty.bed") + writeLines(header, path) + back <- readFineMappingResult(path) + expect_s4_class(back, "QtlFineMappingResult") + expect_equal(nrow(back), 0L) + weights <- readTwasWeights(path) + expect_s4_class(weights, "TwasWeights") + expect_equal(nrow(weights), 0L) +}) + +test_that("an empty result survives an export and a read", { + data(qtlFineMappingExample, envir = environment()) + path <- .rdr_tmp("empty_rt.bed") + # A signal cutoff above 1 retains nothing, so the table is vacuous. + exportToBed(qtlFineMappingExample, path, signalCutoff = 2) + expect_equal(nrow(readFineMappingResult(path)), 0L) +}) diff --git a/tests/testthat/test_resultWriters.R b/tests/testthat/test_resultWriters.R new file mode 100644 index 00000000..75814123 --- /dev/null +++ b/tests/testthat/test_resultWriters.R @@ -0,0 +1,1721 @@ +# Tests for the published result writers (resultWriters.R): exportToVcf, the +# GWAS-VCF-style writer, and exportToBed, the published long-format tables. + +# ============================================================================= +# Test data helpers +# ============================================================================= + +makeTestGenotypeHandle <- function() { + new( + "GenotypeHandle", + path = "/tmp/test.gds", + format = "gds", + snpInfo = data.frame(), + nSamples = 0L, + sampleIds = character(), + pgenPtr = NULL + ) +} + +makeTestGwasSumStats <- function(n = 5) { + gr <- GenomicRanges::GRanges( + "chr1", + IRanges::IRanges(start = seq(100, by = 100, length.out = n), width = 1) + ) + S4Vectors::mcols(gr) <- S4Vectors::DataFrame( + SNP = paste0("rs", seq_len(n)), + A1 = rep("A", n), + A2 = rep("T", n), + Z = seq(1.5, by = -0.5, length.out = n), + N = rep(1000L, n) + ) + GwasSumStats( + studyName = "test_trait", + entry = list(gr), + genome = "hg38", + ldSketch = makeTestGenotypeHandle() + ) +} + +makeTestFineMappingResult <- function(n = 5) { + beta <- seq(0.5, by = -0.1, length.out = n) + se <- rep(0.1, n) + zv <- seq(5.0, by = -1.0, length.out = n) + tl <- data.frame( + variant_id = paste0( + "chr1:", + seq(100, by = 100, length.out = n), + ":T:A" + ), + chrom = rep("1", n), + pos = as.integer(seq(100, by = 100, length.out = n)), + A1 = rep("A", n), + A2 = rep("T", n), + N = rep(1000, n), + MAF = rep(0.1, n), + marginal_beta = beta, + marginal_se = se, + marginal_z = zv, + marginal_p = 2 * pnorm(-abs(zv)), + pip = seq(0.9, by = -0.1, length.out = n), + posterior_mean = beta * 0.5, + posterior_sd = se * 0.5, + cs_95 = paste0("susie_", c(1L, 1L, 0L, 2L, 0L)[seq_len(n)]), + method = rep("susie", n), + stringsAsFactors = FALSE + ) + entry <- fineMappingRow( + variantIds = tl$variant_id, + susieFit = list(), + topLoci = tl + ) + GwasFineMappingResult( + studyName = "test_study", + method = "susie", + entry = list(entry) + ) +} + +# ============================================================================= +# GwasSumStats to VCF +# ============================================================================= + +test_that("exportToVcf writes GwasSumStats to uncompressed VCF", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + + ss <- makeTestGwasSumStats(5) + out <- tempfile(fileext = ".vcf") + on.exit(unlink(out), add = TRUE) + + result <- exportToVcf(ss, out) + expect_equal(result, out) + expect_true(file.exists(out)) + expect_gt(file.info(out)$size, 0) +}) + +# ============================================================================= +# GwasSumStats to bgzipped VCF +# ============================================================================= + +test_that("exportToVcf writes GwasSumStats to bgzipped VCF", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + + ss <- makeTestGwasSumStats(5) + out <- tempfile(fileext = ".vcf.bgz") + on.exit(unlink(c(out, paste0(out, ".tbi")), force = TRUE), add = TRUE) + + result <- exportToVcf(ss, out) + expect_equal(result, out) + expect_true(file.exists(out)) + expect_gt(file.info(out)$size, 0) +}) + +# ============================================================================= +# FineMappingResult to VCF +# ============================================================================= + +test_that("exportToVcf writes FineMappingResult to uncompressed VCF", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + + fm <- makeTestFineMappingResult(5) + out <- tempfile(fileext = ".vcf") + on.exit(unlink(out), add = TRUE) + + result <- exportToVcf(fm, out) + expect_equal(result, out) + expect_true(file.exists(out)) + expect_gt(file.info(out)$size, 0) +}) + +test_that("exportToVcf(FineMapping): emits PIP + CS FORMAT fields", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + + fm <- makeTestFineMappingResult(5) + out <- tempfile(fileext = ".vcf") + on.exit(unlink(out), add = TRUE) + exportToVcf(fm, out) + ln <- readLines(out) + # The posterior fields the legacy create_vcf carried must be present. The CS + # field is DYNAMIC (CS) -- the fixture carries cs_95 -> CS95. + expect_true(any(grepl("ID=PIP,", ln, fixed = TRUE))) + expect_true(any(grepl("ID=CS95,", ln, fixed = TRUE))) + data1 <- ln[!grepl("^#", ln)][[1L]] + fmtKeys <- strsplit(strsplit(data1, "\t")[[1L]][[9L]], ":")[[1L]] + expect_true("PIP" %in% fmtKeys) + expect_true("CS95" %in% fmtKeys) + # cs_95 "susie_" -> integer CS index; fixture row 1 is in CS 1. + vals <- strsplit(strsplit(data1, "\t")[[1L]][[10L]], ":")[[1L]] + expect_equal(vals[[which(fmtKeys == "CS95")]], "1") +}) + +# ============================================================================= +# FineMappingResult to BCF +# ============================================================================= + +test_that("exportToVcf writes FineMappingResult to BCF", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + skip_if_not_installed("Rsamtools") + + # asBcf may be temporarily disabled in some Rsamtools versions + asbcf_works <- tryCatch( + { + tmp_stem <- tempfile(fileext = ".vcf") + tmp_bgz <- paste0(tmp_stem, ".bgz") + gr <- GenomicRanges::GRanges( + "chr1", + IRanges::IRanges(start = 100, width = 1, names = "chr1:100:A:G") + ) + hdr <- VariantAnnotation::VCFHeader( + header = IRanges::DataFrameList( + fileformat = S4Vectors::DataFrame( + Value = "VCFv4.2", + row.names = "fileformat" + ) + ), + sample = "probe" + ) + cd <- S4Vectors::DataFrame(Samples = "probe", row.names = "probe") + v <- VariantAnnotation::VCF( + rowRanges = gr, + colData = cd, + exptData = list(header = hdr) + ) + VariantAnnotation::ref(v) <- Biostrings::DNAStringSet("A") + VariantAnnotation::alt(v) <- Biostrings::DNAStringSetList(list("T")) + VariantAnnotation::fixed(v)$FILTER <- "PASS" + VariantAnnotation::writeVcf(v, tmp_stem, index = TRUE) + bcf_stem <- tempfile() + Rsamtools::asBcf( + tmp_bgz, + dictionary = "chr1", + destination = bcf_stem + ) + TRUE + }, + error = function(e) FALSE + ) + skip_if(!asbcf_works, "Rsamtools::asBcf is not functional in this build") + + fm <- makeTestFineMappingResult(5) + out <- tempfile(fileext = ".bcf") + on.exit(unlink(out), add = TRUE) + + result <- exportToVcf(fm, out) + expect_equal(result, out) + expect_true(file.exists(out)) + expect_gt(file.info(out)$size, 0) +}) + +# ============================================================================= +# Empty FineMappingResult errors +# ============================================================================= + +test_that("exportToVcf errors on empty FineMappingResult", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + + empty_tl <- data.frame( + variant_id = character(0), + chrom = character(0), + pos = integer(0), + A1 = character(0), + A2 = character(0), + N = numeric(0), + MAF = numeric(0), + marginal_beta = numeric(0), + marginal_se = numeric(0), + marginal_z = numeric(0), + marginal_p = numeric(0), + pip = numeric(0), + posterior_mean = numeric(0), + posterior_sd = numeric(0), + stringsAsFactors = FALSE + ) + entry <- fineMappingRow( + variantIds = character(0), + susieFit = list(), + topLoci = empty_tl + ) + fm_empty <- GwasFineMappingResult( + studyName = "test_study", + method = "susie", + entry = list(entry) + ) + + out <- tempfile(fileext = ".vcf") + on.exit(unlink(out), add = TRUE) + + expect_error(exportToVcf(fm_empty, out), "no variants to write") +}) + +# ============================================================================= +# splitByContext / splitByTrait — one file per tuple +# ============================================================================= + +.makeMultiTupleQtlFmr <- function() { + contexts <- c("brain", "blood") + traits <- c("ENSG_A", "ENSG_B") + entries <- map(seq_along(contexts), function(i) { + ids <- paste0("chr1:", 100 * (1:3), ":T:A") + tl <- data.frame( + variant_id = ids, + chrom = rep("1", 3), + pos = c(100L, 200L, 300L), + A1 = rep("A", 3), + A2 = rep("T", 3), + N = rep(1000, 3), + MAF = rep(0.1, 3), + marginal_beta = c(0.3, 0.1, -0.2) + i / 100, + marginal_se = rep(0.05, 3), + marginal_z = c(6.0, 2.0, -4.0), + marginal_p = c(1e-9, 0.045, 6e-5), + pip = c(0.9, 0.5, 0.7), + posterior_mean = rep(0.05, 3), + posterior_sd = rep(0.02, 3), + cs_95 = paste0("susie_", c(1L, 1L, 0L)), + stringsAsFactors = FALSE + ) + fineMappingRow(variantIds = ids, susieFit = list(), topLoci = tl) + }) + QtlFineMappingResult( + studyName = rep("study1", 2), + context = contexts, + trait = traits, + method = rep("susie", 2), + entry = entries + ) +} + +test_that("exportToVcf(FineMapping): splitByContext emits one VCF each", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + fmr <- .makeMultiTupleQtlFmr() + baseOut <- tempfile(fileext = ".vcf") + on.exit( + unlink(list.files( + dirname(baseOut), + pattern = basename(tools::file_path_sans_ext(baseOut)), + full.names = TRUE + )), + add = TRUE + ) + paths <- exportToVcf(fmr, baseOut, splitByContext = TRUE) + expect_length(paths, 2L) + # Each path is decorated with the context tag. + expect_true(any(grepl("\\.brain\\.vcf$", paths))) + expect_true(any(grepl("\\.blood\\.vcf$", paths))) + for (p in paths) { + expect_true(file.exists(p)) + } +}) + +test_that("exportToVcf(FineMapping): splitByTrait emits one VCF each", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + fmr <- .makeMultiTupleQtlFmr() + baseOut <- tempfile(fileext = ".vcf") + on.exit( + unlink(list.files( + dirname(baseOut), + pattern = basename(tools::file_path_sans_ext(baseOut)), + full.names = TRUE + )), + add = TRUE + ) + paths <- exportToVcf(fmr, baseOut, splitByTrait = TRUE) + expect_length(paths, 2L) + expect_true(any(grepl("\\.ENSG_A\\.vcf$", paths))) + expect_true(any(grepl("\\.ENSG_B\\.vcf$", paths))) +}) + +test_that("exportToVcf(FineMapping): both split flags combine the tags", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + fmr <- .makeMultiTupleQtlFmr() + baseOut <- tempfile(fileext = ".vcf") + on.exit( + unlink(list.files( + dirname(baseOut), + pattern = basename(tools::file_path_sans_ext(baseOut)), + full.names = TRUE + )), + add = TRUE + ) + paths <- exportToVcf( + fmr, + baseOut, + splitByContext = TRUE, + splitByTrait = TRUE + ) + expect_length(paths, 2L) + expect_true(any(grepl("\\.brain\\.ENSG_A\\.vcf$", paths))) + expect_true(any(grepl("\\.blood\\.ENSG_B\\.vcf$", paths))) +}) + +test_that("exportToVcf(FineMapping): several fits make one multi-sample VCF", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + fmr <- .makeMultiTupleQtlFmr() + out <- tempfile(fileext = ".vcf") + on.exit(unlink(out), add = TRUE) + # No selector and no split flag: every matching fit is a SAMPLE of one + # file, which is what GWAS-VCF's per-sample design means. + expect_identical(exportToVcf(fmr, out), out) + lines <- readLines(out) + header <- str_split(lines[str_starts(lines, "#CHROM")][[1L]], "\t")[[1L]] + samples <- header[-seq_len(9L)] + expect_length(samples, 2L) + # each sample is named for the fit it came from, and described in a + # ##SAMPLE header line + expect_true(all(str_detect(samples, "\\|"))) + sampleLines <- keep(lines, str_starts, "##SAMPLE=") + expect_length(sampleLines, 2L) + expect_true(all(str_detect(sampleLines, "TotalVariants="))) + # one record per variant, with a cell per sample + body <- str_split(lines[!str_starts(lines, "#")][[1L]], "\t")[[1L]] + expect_length(body, 9L + 2L) +}) + +test_that("exportToVcf(FineMapping): a fit missing a variant leaves a gap", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + # Two fits over DISJOINT variants: the records are the union, and each + # sample is empty where its own fit had nothing. + mk <- function(positions, ctx) { + ids <- sprintf("chr1:%d:T:A", positions) + tl <- data.frame( + variant_id = ids, + chrom = rep("1", length(ids)), + pos = as.integer(positions), + A1 = "A", + A2 = "T", + pip = seq(0.9, by = -0.1, length.out = length(ids)), + cs_95 = "susie_1", + method = "susie", + stringsAsFactors = FALSE + ) + QtlFineMappingResult( + studyName = "s1", + context = ctx, + trait = "g1", + method = "susie", + entry = list(fineMappingRow( + variantIds = ids, + susieFit = list(), + topLoci = tl + )) + ) + } + fmr <- combineFineMappingResults(mk(c(100, 200), "a"), mk(c(300), "b")) + out <- tempfile(fileext = ".vcf") + on.exit(unlink(out), add = TRUE) + exportToVcf(fmr, out) + lines <- readLines(out) + cells <- str_split(keep(lines, str_starts, "chr1"), "\t") + expect_identical(map_chr(cells, 2L), c("100", "200", "300")) + keys <- str_split(cells[[1L]][[9L]], ":")[[1L]] + pipOf <- function(cell) str_split(cell, ":")[[1L]][[which(keys == "PIP")]] + # sample 1 covers 100/200, sample 2 only 300 + expect_identical(map_chr(map(cells, 10L), pipOf), c("0.9", "0.8", ".")) + expect_identical(map_chr(map(cells, 11L), pipOf), c(".", ".", "0.9")) +}) + +# ============================================================================= +# GwasSumStats: multi-study selection, MAF/AF field, .gz rename +# ============================================================================= + +.makeTwoStudyGwasSumStats <- function(n = 3, withMaf = FALSE) { + mkGr <- function() { + gr <- GenomicRanges::GRanges( + "chr1", + IRanges::IRanges( + start = seq(100, by = 100, length.out = n), + width = 1 + ) + ) + mc <- S4Vectors::DataFrame( + SNP = paste0("rs", seq_len(n)), + A1 = rep("A", n), + A2 = rep("T", n), + Z = seq(1.5, by = -0.5, length.out = n), + N = rep(1000L, n) + ) + if (withMaf) { + mc$MAF <- rep(0.2, n) + } + S4Vectors::mcols(gr) <- mc + gr + } + GwasSumStats( + studyName = c("studyA", "studyB"), + entry = list(mkGr(), mkGr()), + genome = "hg38", + ldSketch = makeTestGenotypeHandle() + ) +} + +test_that("exportToVcf(SumStats): multi-study needs a `study`", { + skip_if_not_installed("VariantAnnotation") + ss2 <- .makeTwoStudyGwasSumStats() + out <- tempfile(fileext = ".vcf") + expect_error(exportToVcf(ss2, out), "2 studies") +}) + +test_that("exportToVcf(SumStats): `study` picks the chosen study", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + ss2 <- .makeTwoStudyGwasSumStats() + out <- tempfile(fileext = ".vcf") + on.exit(unlink(out), add = TRUE) + res <- exportToVcf(ss2, out, studyName = "studyB") + expect_equal(res, out) + expect_true(file.exists(out)) +}) + +test_that("exportToVcf(SumStats): emits AF when MAF is present", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + ss <- GwasSumStats( + studyName = "t", + entry = list(local({ + gr <- GenomicRanges::GRanges( + "chr1", + IRanges::IRanges(start = c(100, 200, 300, 400), width = 1) + ) + S4Vectors::mcols(gr) <- S4Vectors::DataFrame( + SNP = paste0("rs", 1:4), + A1 = rep("A", 4), + A2 = rep("T", 4), + Z = c(1.5, 1.0, 0.5, 0.0), + N = rep(1000L, 4), + MAF = rep(0.2, 4) + ) + gr + })), + genome = "hg38", + ldSketch = makeTestGenotypeHandle() + ) + out <- tempfile(fileext = ".vcf") + on.exit(unlink(out), add = TRUE) + exportToVcf(ss, out) + expect_true(file.exists(out)) + # The AF FORMAT field is declared in the header when MAF is present. + # A source carrying only MAF gets the MAF key, not AF: the spec's AF is + # the ALTERNATIVE allele frequency, which a minor-allele frequency cannot + # be converted to -- it has lost which allele it refers to. + expect_true(any(grepl("ID=MAF", readLines(out)))) + expect_false(any(grepl("ID=AF,", readLines(out)))) +}) + +test_that("exportToVcf(SumStats): .vcf.gz is renamed from .bgz", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + ss <- makeTestGwasSumStats(5) + out <- tempfile(fileext = ".vcf.gz") + on.exit(unlink(c(out, paste0(out, ".tbi")), force = TRUE), add = TRUE) + res <- exportToVcf(ss, out) + expect_equal(res, out) + # writeVcf always emits .bgz; the writer renames it to the requested .gz. + expect_true(file.exists(out)) + expect_false(file.exists(sub("\\.gz$", ".bgz", out))) +}) + +# ============================================================================= +# FineMappingResult: explicit selectors, no-match error, composite/empty tags +# ============================================================================= + +test_that("exportToVcf(FineMapping): selectors pick a single row", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + fmr <- .makeMultiTupleQtlFmr() + out <- tempfile(fileext = ".vcf") + on.exit(unlink(out), add = TRUE) + res <- exportToVcf( + fmr, + out, + studyName = "study1", + context = "brain", + trait = "ENSG_A", + method = "susie" + ) + expect_equal(res, out) + expect_true(file.exists(out)) +}) + +test_that("exportToVcf(FineMappingResult): no matching rows errors", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + fmr <- .makeMultiTupleQtlFmr() + out <- tempfile(fileext = ".vcf") + expect_error( + exportToVcf(fmr, out, studyName = "does_not_exist"), + "no rows match" + ) +}) + +test_that("exportToVcf(FineMapping): split decorates a .vcf.bgz path", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + fmr <- .makeMultiTupleQtlFmr() + baseOut <- tempfile(fileext = ".vcf.bgz") + stem <- tools::file_path_sans_ext(tools::file_path_sans_ext(baseOut)) + on.exit( + unlink( + list.files( + dirname(baseOut), + pattern = basename(stem), + full.names = TRUE + ), + force = TRUE + ), + add = TRUE + ) + paths <- exportToVcf(fmr, baseOut, splitByContext = TRUE) + expect_length(paths, 2L) + # The composite extension survives; the tag is injected before it. + expect_true(all(grepl("\\.vcf\\.bgz$", paths))) + expect_true(any(grepl("\\.brain\\.vcf\\.bgz$", paths))) + expect_true(any(grepl("\\.blood\\.vcf\\.bgz$", paths))) +}) + +test_that("exportToVcf(FineMapping): a context-less split keeps the path", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + # GwasFineMappingResult has no context/trait axes, so the split tag set is + # empty and the original output path is used unchanged. + fm <- makeTestFineMappingResult(5) + out <- tempfile(fileext = ".vcf") + on.exit(unlink(out), add = TRUE) + paths <- exportToVcf(fm, out, splitByContext = TRUE) + expect_length(paths, 1L) + expect_equal(paths[[1L]], out) + expect_true(file.exists(out)) +}) + +test_that("exportToVcf(FineMapping): AF comes from topLoci `af`", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + # The marginal view projects allele frequency to a column named `af`, so a + # topLoci carrying `af` must surface as the AF genotype field. + n <- 3 + tl <- data.frame( + variant_id = paste0("chr1:", c(100, 200, 300), ":T:A"), + chrom = rep("1", n), + pos = c(100L, 200L, 300L), + A1 = rep("A", n), + A2 = rep("T", n), + N = rep(1000, n), + af = c(0.1, 0.2, 0.3), + marginal_beta = c(0.3, -0.2, 0.1), + marginal_se = rep(0.05, n), + marginal_z = c(6.0, -4.0, 2.0), + marginal_p = c(1e-9, 6e-5, 0.045), + pip = c(0.9, 0.7, 0.5), + posterior_mean = rep(0.05, n), + posterior_sd = rep(0.02, n), + cs_95 = paste0("susie_", c(1L, 0L, 2L)), + stringsAsFactors = FALSE + ) + entry <- fineMappingRow( + variantIds = tl$variant_id, + susieFit = list(), + topLoci = tl + ) + fm <- GwasFineMappingResult( + studyName = "s", + method = "susie", + entry = list(entry) + ) + out <- tempfile(fileext = ".vcf") + on.exit(unlink(out), add = TRUE) + exportToVcf(fm, out) + expect_true(file.exists(out)) + expect_true(any(grepl("ID=AF", readLines(out)))) +}) + +test_that("exportToVcf(FineMapping): LBF / LFSR / PUR / fullFit fields", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + # A rich topLoci exercises every posterior FORMAT field: per-variant + # logBF (LBF), local false sign rate (LFSR), per-CS purity (PUR), + # and the wide fullFit + # columns (within_cs_pip / cs_logbf_ / cs_effect_). + n <- 3 + tl <- data.frame( + variant_id = paste0("chr1:", c(100, 200, 300), ":T:A"), + chrom = rep("1", n), + pos = c(100L, 200L, 300L), + A1 = rep("A", n), + A2 = rep("T", n), + N = rep(1000, n), + marginal_beta = c(0.3, -0.2, 0.1), + marginal_se = rep(0.05, n), + marginal_z = c(6, -4, 2), + marginal_p = c(1e-9, 6e-5, 0.045), + pip = c(0.9, 0.7, 0.5), + posterior_mean = rep(0.05, n), + posterior_sd = rep(0.02, n), + logBF = c(3.1, 1.2, 0.4), + lfsr = c(0.01, 0.2, 0.4), + cs_95 = paste0("susie_", c(1L, 0L, 2L)), + cs_95_purity = c(0.95, 0, 0.88), + within_cs_pip = c(0.6, 0, 0.5), + cs_logbf_susie_1 = c(2.2, NA, NA), + cs_effect_susie_1 = c(0.4, NA, NA), + stringsAsFactors = FALSE + ) + entry <- fineMappingRow( + variantIds = tl$variant_id, + susieFit = list(), + topLoci = tl + ) + fm <- GwasFineMappingResult( + studyName = "s", + method = "susie", + entry = list(entry) + ) + out <- tempfile(fileext = ".vcf") + on.exit(unlink(out), add = TRUE) + exportToVcf(fm, out) + ln <- readLines(out) + for (id in c( + "ID=LBF", + "ID=LFSR", + "ID=PUR95", + "ID=WITHIN_CS_PIP", + "ID=CS_LOGBF_SUSIE_1", + "ID=CS_EFFECT_SUSIE_1" + )) { + expect_true(any(grepl(id, ln, fixed = TRUE)), info = id) + } +}) + +test_that("exportToVcf(FineMapping): falls back to marginal sumstats", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Biostrings") + fm <- makeTestFineMappingResult(5) + # Simulate an entry whose posterior table is unavailable (empty topLoci) so + # the marginal univariate sumstats alone drive the body (the else-if). + testthat::local_mocked_bindings( + topLoci = function(x, ...) GenomicRanges::GRanges(), + .package = "pecotmr" + ) + out <- tempfile(fileext = ".vcf") + on.exit(unlink(out), add = TRUE) + exportToVcf(fm, out) + expect_true(file.exists(out)) + # marginal beta -> ES + expect_true(any(grepl("ID=ES", readLines(out), fixed = TRUE))) +}) + + +# =========================================================================== +# Output formats +# =========================================================================== + +test_that("plain and bgzipped VCF are written, the latter with an index", { + skip_if_not_installed("VariantAnnotation") + data(gwasSumStatsS4Example) + d <- withr::local_tempdir() + exportToVcf(gwasSumStatsS4Example, outputPath = file.path(d, "a.vcf")) + expect_true(file.exists(file.path(d, "a.vcf"))) + exportToVcf( + gwasSumStatsS4Example, + outputPath = file.path(d, "b.vcf.bgz") + ) + expect_true(file.exists(file.path(d, "b.vcf.bgz"))) + # index = TRUE, so the tabix index is written alongside. + expect_true(file.exists(file.path(d, "b.vcf.bgz.tbi"))) +}) + +test_that("BCF output explains itself when Rsamtools cannot convert", { + # Rsamtools >= 2.26 raises "asBcf() is temporarily disabled". Left bare + # that surfaces as an opaque failure from a documented output format, so + # it is translated into the cause plus the workaround. + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("Rsamtools") + data(gwasSumStatsS4Example) + d <- withr::local_tempdir() + canConvert <- !inherits( + tryCatch( + Rsamtools::asBcf("nonexistent", "x", "y"), + error = function(e) e + ), + "error" + ) + skip_if(canConvert, "Rsamtools::asBcf() works here; nothing to translate") + expect_error( + exportToVcf( + gwasSumStatsS4Example, + outputPath = file.path(d, "c.bcf") + ), + "BCF output needs a working Rsamtools::asBcf" + ) +}) + +test_that(".vcfResolveBody uses posterior rows when no marginals exist", { + local_mocked_bindings( + topLoci = function(entry, signalCutoff) { + # topLoci() returns ranges carrying the per-variant columns in + # mcols(); these ids are deliberately not coordinate-shaped, so + # the ranges are built directly rather than parsed from them. + gr <- GenomicRanges::GRanges( + "chr1", + IRanges::IRanges(start = c(1L, 2L), width = 1L) + ) + S4Vectors::`mcols<-`( + gr, + value = S4Vectors::DataFrame( + variant_id = c("v1", "v2"), + pip = c(0.9, 0.1) + ) + ) + }, + marginalEffects = function(entry) stop("none"), + .package = "pecotmr" + ) + out <- pecotmr:::.vcfResolveBody("e", "S1") + expect_true(out$hasPost) + expect_equal(nrow(out$base), 2L) + # No marginal table to align against, so there is nothing to merge in. + expect_null(out$m) +}) + +test_that(".writeVcfImpl adds the chr prefix to bare chromosome names", { + skip_if_not_installed("VariantAnnotation") + out <- tempfile(fileext = ".vcf") + on.exit(unlink(out), add = TRUE) + geno <- matrix(c(1.5, -0.5), ncol = 1L, dimnames = list(NULL, "S1")) + header <- S4Vectors::DataFrame( + Number = "1", + Type = "Float", + Description = "Z score", + row.names = "Z" + ) + pecotmr:::.writeVcfImpl( + chrom = c("1", "1"), + pos = c(100L, 200L), + ref = c("A", "C"), + alt = c("G", "T"), + snpIds = c("rs1", "rs2"), + geno = list(Z = geno), + genoHeader = header, + sampleName = "S1", + outputPath = out + ) + body <- readLines(out) + body <- body[!startsWith(body, "#")] + expect_true(all(startsWith(body, "chr1\t"))) +}) + +# ============================================================================= +# exportToBed: the fine-mapping schemas +# ============================================================================= + +test_that("exportToBed writes the published top-loci schema", { + data(qtlFineMappingExample) + out <- withr::local_tempfile(fileext = ".bed") + expect_identical( + exportToBed(qtlFineMappingExample, out, signalCutoff = 0), + out + ) + tbl <- utils::read.delim(out, check.names = FALSE) + # The whole schema, in order -- `context` is gone, `event_ID` already + # carried it, as in the other two schemas. + expect_identical( + names(tbl)[1:6], + c("#chr", "start", "end", "ID", "REF", "ALT") + ) + # One vocabulary with the VCF: the spec's keys for what it defines, ours + # alongside, snake_case for the per-fit fields. + expect_true(all(c("ES", "SE", "LP", "NS") %in% names(tbl))) + # EZ only when the source SUPPLIED z -- the spec defines it as the z used + # to derive ES/SE, and this fixture recorded beta/se instead. + expect_false("EZ" %in% names(tbl)) + expect_true(all(c("WGT", "WGTSE", "PIP", "LBF") %in% names(tbl))) + # coverage levels are discovered, not hardcoded + expect_true(all(c("CS95", "PUR95", "CS70", "CS50") %in% names(tbl))) + expect_true(all(c("study", "context", "trait", "method") %in% names(tbl))) + expect_true(all( + c("region_id", "region_start", "region_end") %in% names(tbl) + )) + # and the columns this input has nothing to say about are absent, not + # padded with NAs + expect_false(any(c("MAF", "weight_standardized") %in% names(tbl))) + expect_gt(nrow(tbl), 0L) + # sorted by (chr, pos) + first <- tbl[[1L]][[1L]] + expect_false(is.unsorted(tbl$start[tbl[[1L]] == first])) +}) + +test_that("exportToBed bgzips and tabix-indexes a .gz fsusie table", { + data(fsusieFineMappingExample) + out <- withr::local_tempfile(fileext = ".bed.gz") + exportToBed(fsusieFineMappingExample, out, schema = "fsusie") + expect_true(file.exists(out)) + expect_true(file.exists(str_c(out, ".tbi"))) + hdr <- readLines(gzfile(out), n = 1L) + cols <- str_split(hdr, "\t")[[1L]] + expect_identical(cols[[1L]], "#chr") + # MAF is absent from this fixture, so the file carries the schema minus + # the columns that had nothing to say. + expect_identical( + cols, + setdiff( + .fmbFsusieColumns( + c("wavelet", "trait"), + c("CS95", "PUR95") + ), + "AF" + ) + ) + expect_true(all( + c( + "grid_positions", + "grid_effects", + "grid_band_halfwidth", + "joint_traits", + "region_start", + "region_end" + ) %in% + cols + )) +}) + +test_that("exportToBed emits the full schema for an empty result", { + data(qtlFineMappingExample) + out <- withr::local_tempfile(fileext = ".bed") + # A cutoff no variant clears -> 0 data rows, but the schema must not + # collapse. read.table() cannot read a header-only file, so read the line. + exportToBed(qtlFineMappingExample, out, signalCutoff = 2) + lines <- readLines(out) + expect_length(lines, 1L) + # A header-only file documents the whole vocabulary rather than + # collapsing to the required core. + hdr <- str_split(lines[[1L]], "\t")[[1L]] + expect_identical(hdr[1:6], c("#chr", "start", "end", "ID", "REF", "ALT")) + expect_true(all(c("ES", "WGT", "PIP", "region_id") %in% hdr)) +}) + +test_that("exportToBed combines several results into one table", { + data(qtlFineMappingExample) + data(mvsusieFineMappingExample) + one <- withr::local_tempfile(fileext = ".bed") + both <- withr::local_tempfile(fileext = ".bed") + exportToBed(qtlFineMappingExample, one, signalCutoff = 0) + exportToBed( + list(qtlFineMappingExample, mvsusieFineMappingExample), + both, + signalCutoff = 0 + ) + n1 <- length(readLines(one)) + n2 <- length(readLines(both)) + # One header either way, and the combined table carries strictly more rows. + expect_gt(n2, n1) + tbl <- utils::read.delim(both, check.names = FALSE) + expect_setequal( + unique(tbl$context), + unique(c( + as.character(contexts(qtlFineMappingExample)), + as.character(contexts(mvsusieFineMappingExample)) + )) + ) +}) + +test_that("exportToBed rejects a non-FineMappingResult", { + out <- withr::local_tempfile(fileext = ".bed") + expect_error(exportToBed(data.frame(a = 1), out), "FineMappingResult") + expect_error(exportToBed(list(), out), "at least one") +}) + +test_that("the fsusie schema is still complete when no fsusie rows exist", { + data(qtlFineMappingExample) + out <- withr::local_tempfile(fileext = ".bed") + # No fsusie rows at all -> a header-only table, not an error. + exportToBed(qtlFineMappingExample, out, schema = "fsusie") + lines <- readLines(out) + expect_length(lines, 1L) + # a header-only file documents the schema, with no coverage discovered + expect_identical( + str_split(lines[[1L]], "\t")[[1L]], + .fmbFsusieColumns(c("wavelet", "trait")) + ) +}) + +test_that("a merged multi-region fSuSiE table keeps each region's own window", { + data(fsusieFineMappingExample) + a <- fsusieFineMappingExample + b <- fsusieFineMappingExample + # Two regions whose (context, cs) labels collide -- the case a + # context+cs key would silently merge onto the first region's window. + S4Vectors::mcols(b)$study <- "studyB" + wantA <- GenomicRanges::GRanges("chr22:1-9000000") + wantB <- GenomicRanges::GRanges("chr22:2-8000000") + withRegion <- function(x, want) { + S4Vectors::mcols(x)$region <- rep(want, nrow(x)) + x + } + both <- combineFineMappingResults( + withRegion(a, wantA), + withRegion(b, wantB) + ) + out <- withr::local_tempfile(fileext = ".bed.gz") + exportToBed(both, out, schema = "fsusie") + tbl <- utils::read.delim(gzfile(out), check.names = FALSE) + expect_setequal( + unique(tbl$region_id), + c("chr22_1_9000000", "chr22_2_8000000") + ) + expect_setequal(unique(tbl$region_start), c(0, 1)) + expect_setequal(unique(tbl$region_end), c(9000000, 8000000)) +}) + +test_that("extraColumns appends trailing fields the object cannot know", { + data(qtlFineMappingExample) + out <- withr::local_tempfile(fileext = ".bed") + exportToBed( + qtlFineMappingExample, + out, + signalCutoff = 0, + extraColumns = list(source_file = "chr22_1_50000000") + ) + tbl <- utils::read.delim(out, check.names = FALSE) + expect_identical(names(tbl)[[ncol(tbl)]], "source_file") + expect_true(all(tbl$source_file == "chr22_1_50000000")) + + # one value per row is equally valid + perRow <- withr::local_tempfile(fileext = ".bed") + exportToBed( + qtlFineMappingExample, + perRow, + signalCutoff = 0, + extraColumns = list(tag = seq_len(nrow(tbl))) + ) + expect_identical( + utils::read.delim(perRow, check.names = FALSE)$tag, + seq_len(nrow(tbl)) + ) +}) + +test_that("extraColumns refuses a clash, a bad length, an unnamed list", { + data(qtlFineMappingExample) + out <- withr::local_tempfile(fileext = ".bed") + expect_error( + exportToBed( + qtlFineMappingExample, + out, + signalCutoff = 0, + extraColumns = list(PIP = 1) + ), + "would overwrite" + ) + expect_error( + exportToBed( + qtlFineMappingExample, + out, + signalCutoff = 0, + extraColumns = list(tag = c(1, 2)) + ), + "one value per row" + ) + expect_error( + exportToBed( + qtlFineMappingExample, + out, + signalCutoff = 0, + extraColumns = list(1) + ), + "NAMED list" + ) +}) + +test_that("extraColumns survive an empty table", { + data(qtlFineMappingExample) + out <- withr::local_tempfile(fileext = ".bed") + exportToBed( + qtlFineMappingExample, + out, + signalCutoff = 2, + extraColumns = list(source_file = "b1") + ) + lines <- readLines(out) + expect_length(lines, 1L) + expect_identical( + str_split(lines[[1L]], "\t")[[1L]], + c(str_split(readLines(out)[[1L]], "\t")[[1L]]) + ) +}) + +test_that("genomicOrder sorts chromosomes naturally, not as text", { + # 1..22 numerically, then the non-numeric contigs after all of them -- + # `sort -k1,1V` order, where 2 precedes 10. + expect_identical( + genomicOrder(c("10", "2", "X", "1"), c(1, 1, 1, 1)), + c(4L, 2L, 1L, 3L) + ) + expect_identical( + genomicOrder(c("chr10", "chrX", "chr2", "chrY", "chr22"), rep(1, 5)), + c(3L, 1L, 5L, 2L, 4L) + ) + # a leading "chr" is cosmetic + expect_identical( + genomicOrder(c("chr2", "10"), c(1, 1)), + genomicOrder(c("2", "chr10"), c(1, 1)) + ) + # positions sort numerically within a chromosome, not as text + expect_identical( + genomicOrder(rep("chr1", 3L), c("100", "9", "1000")), + c(2L, 1L, 3L) + ) + # a single chromosome is the common case and must be a no-op + expect_identical(genomicOrder(rep("chr22", 3L), c(1, 2, 3)), 1:3) +}) + +test_that("genomicOrder uses `end` only to break ties, and only if numeric", { + chrom <- rep("chr1", 3L) + start <- c(10, 10, 5) + expect_identical( + genomicOrder(chrom, start, end = c(90, 20, 1)), + c(3L, 2L, 1L) + ) + # A third column that is not a position (an allele) is ignored rather than + # coerced to NA and allowed to scramble the order. + expect_identical( + genomicOrder(chrom, start, end = c("A", "T", "G")), + genomicOrder(chrom, start) + ) + expect_identical( + genomicOrder(chrom, start, end = NULL), + genomicOrder(chrom, start) + ) +}) + +# ============================================================================= +# exportToBed: the TWAS-weight schemas +# ============================================================================= + +makeTestTwasWeights <- function(weights, method = "lasso", nVariants = 3L) { + ids <- sprintf("chr1:%d:A:G", 100L * seq_len(nVariants)) + TwasWeights( + studyName = "s1", + context = "brain", + trait = "g1", + method = method, + entry = list(twasWeightsRow( + variantIds = ids, + weights = weights, + cvResult = list(metrics = c(rsq = 0.5, pval = 0.01)), + weightStandardized = TRUE + )) + ) +} + +test_that("exportToBed writes the published TWAS-weight schema", { + data(twasWeightsExample) + out <- withr::local_tempfile(fileext = ".bed") + expect_identical(exportToBed(twasWeightsExample, out), out) + tbl <- utils::read.delim(out, check.names = FALSE) + expect_identical( + names(tbl)[1:6], + c("#chr", "start", "end", "ID", "REF", "ALT") + ) + expect_true(all( + c("WGT", "weight_standardized", "data_type") %in% names(tbl) + )) + # the posterior columns a weights table has nothing to say about are gone + expect_false(any(c("PIP", "LBF", "CS95") %in% names(tbl))) + # one row per (model, variant): 168 variants each + # 2 of the 11 rows are fine-mapping-derived and are skipped + expect_identical(nrow(tbl), 9L * 168L) + expect_setequal( + unique(tbl$method), + setdiff( + as.character(S4Vectors::mcols(twasWeightsExample)$method), + .TWB_FINE_MAPPING_METHODS + ) + ) + expect_true(all(tbl$end == tbl$start + 1L)) + expect_false(is.unsorted(tbl$start)) + expect_true(all(tbl$gene_ID == "ENSG00000130538")) + # the analysis window, not the per-variant position + expect_identical(unique(tbl$region_end), 18850784L) +}) + +test_that("exportToBed writes one TWAS-weight row per matrix column", { + w <- matrix(c(0.1, 0.2, 0.3, -0.4, -0.5, -0.6), nrow = 3L) + colnames(w) <- c("mark_a", "mark_b") + out <- withr::local_tempfile(fileext = ".bed") + exportToBed(makeTestTwasWeights(w), out) + tbl <- utils::read.delim(out, check.names = FALSE) + expect_identical(nrow(tbl), 6L) + expect_setequal(unique(tbl$trait), c("mark_a", "mark_b")) + expect_identical( + tbl$WGT[tbl$trait == "mark_b"], + c(-0.4, -0.5, -0.6) + ) + # a plain weight vector has no feature axis, so the column is empty + plain <- withr::local_tempfile(fileext = ".bed") + exportToBed(makeTestTwasWeights(c(0.1, 0.2, 0.3)), plain) + plainTbl <- utils::read.delim(plain, check.names = FALSE) + expect_identical(nrow(plainTbl), 3L) + # a plain weight vector has no feature axis, so `trait` stays the row's + expect_identical(unique(plainTbl$trait), "g1") +}) + +test_that("exportToBed bgzips and tabix-indexes a .gz TWAS-weight table", { + data(twasWeightsExample) + out <- withr::local_tempfile(fileext = ".bed.gz") + exportToBed(twasWeightsExample, out) + expect_true(file.exists(out)) + expect_true(file.exists(str_c(out, ".tbi"))) + cols <- str_split(readLines(gzfile(out), n = 1L), "\t")[[1L]] + expect_true(all(c("#chr", "start", "end", "ID", "WGT") %in% cols)) +}) + +test_that("exporting refuses a TwasWeights with no full-data fit", { + # twasWeightsPipeline(fitFullData = FALSE) leaves each row an empty + # element with no weights and only a cvResult. Fold-level results cannot + # be published, so that is an error rather than an empty output file. + cvOnly <- TwasWeights( + studyName = "s1", + context = "brain", + trait = "g1", + method = "susie", + entry = list(twasWeightsRow( + variantIds = character(0), + weights = NULL, + cvResult = list(metrics = c(rsq = 0.5)) + )) + ) + out <- withr::local_tempfile(fileext = ".bed") + expect_error(exportToBed(cvOnly, out), "no full-data fit") + expect_error(exportToBed(cvOnly, out), "fitFullData") + expect_error( + exportToVcf(cvOnly, withr::local_tempfile(fileext = ".vcf")), + "no full-data fit" + ) + # the offending row is named, so a genome-wide run says which one + expect_error(exportToBed(cvOnly, out), "s1\\|brain\\|g1\\|susie") + # and a weightless-but-populated row is refused for the same reason + expect_error( + exportToBed(makeTestTwasWeights(NULL), out), + "no full-data fit" + ) +}) + +test_that("exporting refuses a FineMappingResult row with no fitted variants", { + # Fine-mapping has no CV-only mode -- CV is attached on top of the fit -- + # but the export checks rather than assumes it. + fmr <- QtlFineMappingResult( + studyName = "s1", + context = "c1", + trait = "g1", + method = "susie", + entry = list(fineMappingRow( + variantIds = character(0), + susieFit = list(), + topLoci = data.frame() + )) + ) + out <- withr::local_tempfile(fileext = ".bed") + expect_error(exportToBed(fmr, out), "no full-data fit") +}) + +test_that("exportToBed combines a list of TwasWeights into one table", { + one <- withr::local_tempfile(fileext = ".bed") + both <- withr::local_tempfile(fileext = ".bed") + a <- makeTestTwasWeights(c(0.1, 0.2, 0.3), method = "lasso") + b <- makeTestTwasWeights(c(0.4, 0.5, 0.6), method = "enet") + exportToBed(a, one) + exportToBed(list(a, b), both) + expect_identical(length(readLines(both)), 2L * length(readLines(one)) - 1L) + tbl <- utils::read.delim(both, check.names = FALSE) + expect_setequal(unique(tbl$method), c("lasso", "enet")) +}) + +test_that("exportToBed rejects a list that holds neither result type", { + out <- withr::local_tempfile(fileext = ".bed") + expect_error(exportToBed(list(), out), "at least one") + expect_error( + exportToBed(list(1, 2), out), + "TwasWeights or FineMappingResult" + ) +}) + +# ============================================================================= +# exportToVcf: TWAS weights +# ============================================================================= + +test_that("exportToVcf writes TWAS weights with one sample per model", { + data(twasWeightsExample) + out <- withr::local_tempfile(fileext = ".vcf") + exportToVcf(twasWeightsExample, out) + lines <- readLines(out) + header <- str_split( + lines[str_starts(lines, "#CHROM")][[1L]], + "\t" + )[[1L]] + samples <- header[-seq_len(9L)] + expect_length(samples, 11L) + expect_true(all(str_detect(samples, "^s|protocol_example\\|bulk_rnaseq"))) + expect_true(any(str_detect(lines, "ID=WGT"))) + expect_identical(sum(!str_starts(lines, "#")), 168L) + # the per-variant FORMAT column is the weight, one value per sample + body <- str_split(lines[!str_starts(lines, "#")][[1L]], "\t")[[1L]] + expect_identical(body[[9L]], "WGT") + expect_length(body, 9L + 11L) +}) + +test_that("the TWAS-weight VCF describes each model in a ##SAMPLE line", { + data(twasWeightsExample) + out <- withr::local_tempfile(fileext = ".vcf") + exportToVcf(twasWeightsExample, out) + sampleLines <- keep(readLines(out), str_starts, "##SAMPLE=") + expect_length(sampleLines, 11L) + susie <- keep(sampleLines, str_detect, fixed("Method=lasso,"))[[1L]] + expect_true(str_detect(susie, "Trait=ENSG00000130538")) + expect_true(str_detect(susie, "TotalVariants=168")) + expect_true(str_detect(susie, "WeightStandardized=FALSE")) + expect_true(str_detect(susie, "DataType=bulk_rnaseq")) +}) + +test_that("exportToVcf narrows TWAS models with the identity selectors", { + data(twasWeightsExample) + out <- withr::local_tempfile(fileext = ".vcf") + exportToVcf(twasWeightsExample, out, method = "susie") + lines <- readLines(out) + header <- str_split( + lines[str_starts(lines, "#CHROM")][[1L]], + "\t" + )[[1L]] + expect_length(header[-seq_len(9L)], 1L) + expect_error( + exportToVcf(twasWeightsExample, out, method = "nope"), + "no rows match" + ) +}) + +test_that("exportToVcf names TWAS samples, or refuses a mismatched count", { + out <- withr::local_tempfile(fileext = ".vcf") + tw <- makeTestTwasWeights(c(0.1, 0.2, 0.3)) + exportToVcf(tw, out, sampleName = "my_model") + lines <- readLines(out) + header <- str_split( + lines[str_starts(lines, "#CHROM")][[1L]], + "\t" + )[[1L]] + expect_identical(header[[10L]], "my_model") + expect_error( + exportToVcf(tw, out, sampleName = c("a", "b")), + "one name per model" + ) +}) + +test_that("a TWAS-weight VCF unions the variants its models disagree on", { + a <- makeTestTwasWeights(c(0.1, 0.2, 0.3), method = "lasso") + b <- TwasWeights( + studyName = "s1", + context = "brain", + trait = "g1", + method = "enet", + entry = list(twasWeightsRow( + variantIds = c("chr1:300:A:G", "chr1:400:A:G"), + weights = c(0.7, 0.8) + )) + ) + out <- withr::local_tempfile(fileext = ".vcf") + exportToVcf(list(a, b), out) + body <- keep(readLines(out), str_starts, "chr1") + # union of {100,200,300} and {300,400} + expect_length(body, 4L) + cells <- str_split(body, "\t") + pos <- map_chr(cells, 2L) + expect_identical(pos, c("100", "200", "300", "400")) + # the model that skipped a variant leaves its cell empty + expect_identical(map_chr(cells, 10L), c("0.1", "0.2", "0.3", ".")) + expect_identical(map_chr(cells, 11L), c(".", ".", "0.7", "0.8")) +}) + +# ============================================================================= +# Shared layer +# ============================================================================= + +test_that(".vcfAltList keeps a non-DNA ALT allele out of a DNAStringSet", { + # VCF's "*" (a variant spanned by an upstream deletion) is a legal ALT but + # not a DNA letter, and DNAStringSet rejects it outright. + expect_s4_class(.vcfAltList(c("A", "GT")), "DNAStringSetList") + expect_s4_class(.vcfAltList(c("A", "*")), "CharacterList") + expect_s4_class(.vcfAltList(c("A", NA)), "CharacterList") +}) + +test_that(".pubIdentityKey keeps an absent axis from erasing the whole key", { + # str_c() propagates NA, so a GWAS row's missing trait would make the + # entire key NA -- and match() pairs NA with NA, joining unrelated rows. + df <- data.frame( + study = c("a", "b"), + trait = c(NA_character_, NA_character_), + variant_id = c("v1", "v1") + ) + keys <- .pubIdentityKey(df, .PUB_VARIANT_COLS) + expect_false(anyNA(keys)) + expect_false(keys[[1L]] == keys[[2L]]) +}) + +test_that(".pubJoinMarginals gives an NA row where no marginal exists", { + base <- data.frame(variant_id = c("v1", "v2")) + marg <- data.frame(variant_id = "v1", beta = 0.5) + joined <- .pubJoinMarginals(base, marg) + expect_identical(nrow(joined), 2L) + expect_identical(joined$beta, c(0.5, NA_real_)) +}) + +# ============================================================================= +# exportToBed: variant-level column completeness +# ============================================================================= + +test_that("the top-loci schema identifies the fit each row came from", { + data(qtlFineMappingExample) + data(mvsusieFineMappingExample) + out <- withr::local_tempfile(fileext = ".bed") + exportToBed( + list(qtlFineMappingExample, mvsusieFineMappingExample), + out, + signalCutoff = 0 + ) + tbl <- utils::read.delim(out, check.names = FALSE) + # Without study_ID / method a folded table cannot say which fit a row + # came from, which is the whole point of folding several in. + expect_true(all(c("study", "method") %in% names(tbl))) + expect_setequal( + unique(tbl$method), + c( + as.character(qtlFineMappingExample$method), + as.character(mvsusieFineMappingExample$method) + ) + ) + # the marginal statistics are complete: N and p, not only z, and they + # are populated rather than present-but-empty. Only for the univariate + # rows -- mvSuSiE has no marginal sumstats at all, so its rows carry no + # betahat / sebetahat / z / N / p either, which is why these columns are + # NA-able rather than required. + expect_true(all(c("NS", "LP") %in% names(tbl))) + univariate <- tbl[tbl$method == "susie", , drop = FALSE] + expect_gt(nrow(univariate), 0L) + expect_false(anyNA(univariate$NS)) + # LP is -log10 p of the marginal test, so it inverts to a probability + expect_true(all(10^(-univariate$LP) > 0 & 10^(-univariate$LP) <= 1)) + expect_equal( + 10^(-univariate$LP), + 2 * pnorm(-abs(univariate$ES / univariate$SE)), + tolerance = 1e-6 + ) + expect_true(all(is.na(tbl$LP[tbl$method == "mvsusie"]))) + # purity at every coverage the schema reports a credible set for + expect_true(all(c("PUR95", "PUR70", "PUR50") %in% names(tbl))) + # a true half-open BED like the other two schemas, so one reader rule + # covers everything + expect_identical(names(tbl)[1:3], c("#chr", "start", "end")) + expect_true(all(tbl$end == tbl$start + 1L)) + # the analysis window travels with the row, so a read-back result does not + # silently fall back to the span of the surviving variants + expect_false(anyNA(tbl$region_ID)) + expect_true(all(tbl$region_start <= tbl$end)) +}) + +test_that("columns = 'all' appends the rest of the source view", { + data(qtlFineMappingExample) + base <- withr::local_tempfile(fileext = ".bed") + full <- withr::local_tempfile(fileext = ".bed") + exportToBed(qtlFineMappingExample, base, signalCutoff = 0) + exportToBed(qtlFineMappingExample, full, signalCutoff = 0, columns = "all") + baseCols <- names(utils::read.delim(base, check.names = FALSE)) + fullCols <- names(utils::read.delim(full, check.names = FALSE)) + # additive: the published columns keep their place, the rest follow + expect_identical(fullCols[seq_along(baseCols)], baseCols) + # "all" is a request for whatever is there, so it stays subject to the + # drop-empty rule. Every remaining view column is empty in this fixture + # (`gene`, `event`, `grange_*`), so it appends nothing and never removes + # anything. An explicitly NAMED column is kept even when empty -- that is + # what the next test checks. + expect_identical(fullCols, baseCols) + # the range columns an as.data.frame() adds duplicate chr/pos, so they + # must not come back under a second name + expect_false(any(c("seqnames", "width", "strand") %in% fullCols)) +}) + +test_that("columns takes an explicit set, and refuses a bad one", { + data(qtlFineMappingExample) + out <- withr::local_tempfile(fileext = ".bed") + exportToBed( + qtlFineMappingExample, + out, + signalCutoff = 0, + columns = "grange_start" + ) + cols <- names(utils::read.delim(out, check.names = FALSE)) + expect_identical(cols[[length(cols)]], "grange_start") + expect_error( + exportToBed( + qtlFineMappingExample, + out, + signalCutoff = 0, + columns = "not_a_column" + ), + "does not carry" + ) + expect_error( + exportToBed( + qtlFineMappingExample, + out, + signalCutoff = 0, + columns = "pip" + ), + "already writes" + ) +}) + +test_that("the fsusie schema carries its study and takes columns = 'all'", { + data(fsusieFineMappingExample) + out <- withr::local_tempfile(fileext = ".bed") + exportToBed(fsusieFineMappingExample, out, schema = "fsusie") + tbl <- utils::read.delim(out, check.names = FALSE) + expect_identical( + names(tbl), + setdiff( + .fmbFsusieColumns( + c("wavelet", "trait"), + c("CS95", "PUR95") + ), + "AF" + ) + ) + expect_false(anyNA(tbl$study)) + + full <- withr::local_tempfile(fileext = ".bed") + exportToBed( + fsusieFineMappingExample, + full, + schema = "fsusie", + columns = "all" + ) + fullCols <- names(utils::read.delim(full, check.names = FALSE)) + # "all" is additive: the published columns keep their place and the + # non-empty remainder of the credible-set view follows. + expect_identical(fullCols[seq_along(names(tbl))], names(tbl)) + expect_true(all(names(tbl) %in% fullCols)) + # an explicitly named column is written even though it is empty here + named <- withr::local_tempfile(fileext = ".bed") + exportToBed( + fsusieFineMappingExample, + named, + schema = "fsusie", + columns = "cs_95_purity" + ) + namedCols <- names(utils::read.delim(named, check.names = FALSE)) + expect_identical(namedCols[[length(namedCols)]], "cs_95_purity") +}) + +test_that("a TwasWeights element has no variant columns left to add", { + data(twasWeightsExample) + base <- withr::local_tempfile(fileext = ".bed") + full <- withr::local_tempfile(fileext = ".bed") + exportToBed(twasWeightsExample, base) + exportToBed(twasWeightsExample, full, columns = "all") + expect_identical( + names(utils::read.delim(full, check.names = FALSE)), + names(utils::read.delim(base, check.names = FALSE)) + ) +}) + +test_that("the fsusie schema writes both resolutions by default", { + data(fsusieFineMappingExample) + out <- withr::local_tempfile(fileext = ".bed") + exportToBed(fsusieFineMappingExample, out, schema = "fsusie") + cols <- names(utils::read.delim(out, check.names = FALSE)) + # MAF is absent from this fixture, so the file carries the schema minus + # the columns that had nothing to say. + expect_identical( + cols, + setdiff( + .fmbFsusieColumns( + c("wavelet", "trait"), + c("CS95", "PUR95") + ), + "AF" + ) + ) + expect_true(all(.FMB_FSUSIE_WAVELET_COLUMNS %in% cols)) + expect_true(all(.FMB_FSUSIE_TRAIT_COLUMNS %in% cols)) +}) + +test_that("fsusie `levels` drops one resolution, leaving a strict subset", { + data(fsusieFineMappingExample) + read <- function(lv) { + out <- withr::local_tempfile( + fileext = ".bed", + .local_envir = parent.frame() + ) + exportToBed( + fsusieFineMappingExample, + out, + schema = "fsusie", + levels = lv + ) + utils::read.delim(out, check.names = FALSE) + } + wav <- read("wavelet") + tr <- read("trait") + both <- read(c("wavelet", "trait")) + + # each level keeps its own group and drops the other's + expect_true(all(.FMB_FSUSIE_WAVELET_COLUMNS %in% names(wav))) + expect_false(any(.FMB_FSUSIE_TRAIT_COLUMNS %in% names(wav))) + expect_true(all(.FMB_FSUSIE_TRAIT_COLUMNS %in% names(tr))) + expect_false(any( + .FMB_FSUSIE_WAVELET_COLUMNS %in% + names(wav) & + .FMB_FSUSIE_WAVELET_COLUMNS %in% names(tr) + )) + + # a strict subset: same rows, same order, columns removed not reordered + expect_identical(nrow(wav), nrow(both)) + expect_identical( + names(wav), + setdiff(names(both), .FMB_FSUSIE_TRAIT_COLUMNS) + ) + expect_identical( + names(tr), + setdiff(names(both), .FMB_FSUSIE_WAVELET_COLUMNS) + ) + expect_identical(wav$cs_id, both$cs_id) + expect_identical(wav$grid_effects, both$grid_effects) + + expect_error( + exportToBed( + fsusieFineMappingExample, + withr::local_tempfile(), + schema = "fsusie", + levels = "nope" + ), + "wavelet" + ) +}) + +test_that("the weight VCF header carries no cross-validation metrics", { + # CV describes held-out folds; a published file has no data-secure way to + # say anything fold-level, so the header stops at identity + counts. + data(twasWeightsExample) + out <- withr::local_tempfile(fileext = ".vcf") + exportToVcf(twasWeightsExample, out) + sampleLines <- keep(readLines(out), str_starts, "##SAMPLE=") + expect_length(sampleLines, 11L) + expect_true(all(str_detect(sampleLines, "TotalVariants="))) + expect_false(any(str_detect(sampleLines, "rsq=|corr=|pval=|CvRegions="))) +}) + +# --------------------------------------------------------------------------- +# EZ on the GwasSumStats path +# +# The spec defines EZ as the z "provided if it was used to derive the ES and +# SE fields". The fine-mapping writer already obeyed that through +# `.pubEz()`; this writer published any `Z` column it found, so a Wald z that +# pecotmr itself computed from a recorded beta/se went out as though the +# source had provided it -- which tells a reader the opposite of the truth +# about where ES and SE came from. +# --------------------------------------------------------------------------- + +# @noRd +.ezss <- function(supplied) { + gr <- GenomicRanges::GRanges( + "chr1", + IRanges::IRanges(c(100L, 200L), width = 1L) + ) + cols <- S4Vectors::DataFrame( + SNP = c("chr1:100:A:G", "chr1:200:C:T"), + A1 = c("G", "T"), + A2 = c("A", "C"), + Z = c(2.5, -1.2), + N = c(1000L, 1000L), + BETA = c(0.25, -0.12), + SE = c(0.1, 0.1) + ) + if (!is.null(supplied)) { + cols$z_supplied <- rep(supplied, 2L) + } + GwasSumStats( + studyName = "s", + entry = list(S4Vectors::`mcols<-`(gr, value = cols)), + genome = "hg38" + ) +} + +# @noRd +.ezFields <- function(x, name) { + skip_if_not_installed("VariantAnnotation") + out <- withr::local_tempfile(fileext = ".vcf") + exportToVcf(x, out) + lines <- readLines(out) + record <- keep(lines, str_starts, "chr1")[[1L]] + str_split(str_split(record, "\t")[[1L]][[9L]], ":")[[1L]] +} + +test_that("exportToVcf(GwasSumStats) publishes EZ only for a supplied z", { + expect_true(is_in("EZ", .ezFields(.ezss(TRUE)))) + expect_false(is_in("EZ", .ezFields(.ezss(FALSE)))) + # A collection built by hand that never recorded the provenance is + # treated as not having supplied one: that is the safe direction, since + # claiming ES/SE were derived from a z is the assertion that misleads. + expect_false(is_in("EZ", .ezFields(.ezss(NULL)))) +}) + +test_that("a supplied z survives the GwasSumStats VCF round trip", { + skip_if_not_installed("VariantAnnotation") + out <- withr::local_tempfile(fileext = ".vcf") + exportToVcf(.ezss(TRUE), out) + back <- .readSumStatsVcf(out, NULL, NULL, NULL, "x") + expect_equal(back$Z, c(2.5, -1.2)) + expect_true(all(back$z_supplied)) + # The derived case comes back derived, so a re-export does not promote it. + exportToVcf(.ezss(FALSE), out) + derived <- .readSumStatsVcf(out, NULL, NULL, NULL, "x") + expect_false(any(derived$z_supplied)) + expect_equal(derived$Z, c(2.5, -1.2)) +}) + +# Drop the ES and SE subfields from one record (or leave a header line as-is). +# @noRd +.ezDropEsSe <- function(line) { + if (!str_starts(line, "chr1")) { + return(line) + } + fields <- str_split(line, "\t")[[1L]] + keys <- str_split(fields[[9L]], ":")[[1L]] + values <- str_split(fields[[10L]], ":")[[1L]] + keep <- !is_in(keys, c("ES", "SE")) + fields[[9L]] <- str_flatten(keys[keep], ":") + fields[[10L]] <- str_flatten(values[keep], ":") + str_flatten(fields, "\t") +} + +test_that("a z-only GWAS-VCF reads without ES and SE", { + # Reading EZ was missing altogether, so a file whose ES/SE were derived + # upstream from z either lost that fact or, with no ES/SE at all, was + # refused outright. + skip_if_not_installed("VariantAnnotation") + out <- withr::local_tempfile(fileext = ".vcf") + exportToVcf(.ezss(TRUE), out) + lines <- readLines(out) + lines <- lines[!str_detect(lines, "^##FORMAT=) -- the fixture carries cs_95 -> CS95. - expect_true(any(grepl("ID=PIP,", ln, fixed = TRUE))) - expect_true(any(grepl("ID=CS95,", ln, fixed = TRUE))) - data1 <- ln[!grepl("^#", ln)][[1L]] - fmtKeys <- strsplit(strsplit(data1, "\t")[[1L]][[9L]], ":")[[1L]] - expect_true("PIP" %in% fmtKeys) - expect_true("CS95" %in% fmtKeys) - # cs_95 "susie_" -> integer CS index; fixture row 1 is in CS 1. - vals <- strsplit(strsplit(data1, "\t")[[1L]][[10L]], ":")[[1L]] - expect_equal(vals[[which(fmtKeys == "CS95")]], "1") -}) - -# ============================================================================= -# FineMappingResult to BCF -# ============================================================================= - -test_that("writeSumStatsVcf writes FineMappingResult to BCF", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - skip_if_not_installed("Rsamtools") - - # asBcf may be temporarily disabled in some Rsamtools versions - asbcf_works <- tryCatch( - { - tmp_stem <- tempfile(fileext = ".vcf") - tmp_bgz <- paste0(tmp_stem, ".bgz") - gr <- GenomicRanges::GRanges( - "chr1", - IRanges::IRanges(start = 100, width = 1, names = "chr1:100:A:G") - ) - hdr <- VariantAnnotation::VCFHeader( - header = IRanges::DataFrameList( - fileformat = S4Vectors::DataFrame( - Value = "VCFv4.2", - row.names = "fileformat" - ) - ), - sample = "probe" - ) - cd <- S4Vectors::DataFrame(Samples = "probe", row.names = "probe") - v <- VariantAnnotation::VCF( - rowRanges = gr, - colData = cd, - exptData = list(header = hdr) - ) - VariantAnnotation::ref(v) <- Biostrings::DNAStringSet("A") - VariantAnnotation::alt(v) <- Biostrings::DNAStringSetList(list("T")) - VariantAnnotation::fixed(v)$FILTER <- "PASS" - VariantAnnotation::writeVcf(v, tmp_stem, index = TRUE) - bcf_stem <- tempfile() - Rsamtools::asBcf( - tmp_bgz, - dictionary = "chr1", - destination = bcf_stem - ) - TRUE - }, - error = function(e) FALSE - ) - skip_if(!asbcf_works, "Rsamtools::asBcf is not functional in this build") - - fm <- makeTestFineMappingResult(5) - out <- tempfile(fileext = ".bcf") - on.exit(unlink(out), add = TRUE) - - result <- writeSumStatsVcf(fm, out) - expect_equal(result, out) - expect_true(file.exists(out)) - expect_gt(file.info(out)$size, 0) -}) - -# ============================================================================= -# Empty FineMappingResult errors -# ============================================================================= - -test_that("writeSumStatsVcf errors on empty FineMappingResult", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - - empty_tl <- data.frame( - variant_id = character(0), - chrom = character(0), - pos = integer(0), - A1 = character(0), - A2 = character(0), - N = numeric(0), - MAF = numeric(0), - marginal_beta = numeric(0), - marginal_se = numeric(0), - marginal_z = numeric(0), - marginal_p = numeric(0), - pip = numeric(0), - posterior_mean = numeric(0), - posterior_sd = numeric(0), - stringsAsFactors = FALSE - ) - entry <- fineMappingRow( - variantIds = character(0), - susieFit = list(), - topLoci = empty_tl - ) - fm_empty <- GwasFineMappingResult( - studyName = "test_study", - method = "susie", - entry = list(entry) - ) - - out <- tempfile(fileext = ".vcf") - on.exit(unlink(out), add = TRUE) - - expect_error(writeSumStatsVcf(fm_empty, out), "no variants to write") -}) - -# ============================================================================= -# splitByContext / splitByTrait — one file per tuple -# ============================================================================= - -.makeMultiTupleQtlFmr <- function() { - contexts <- c("brain", "blood") - traits <- c("ENSG_A", "ENSG_B") - entries <- map(seq_along(contexts), function(i) { - ids <- paste0("chr1:", 100 * (1:3), ":T:A") - tl <- data.frame( - variant_id = ids, - chrom = rep("1", 3), - pos = c(100L, 200L, 300L), - A1 = rep("A", 3), - A2 = rep("T", 3), - N = rep(1000, 3), - MAF = rep(0.1, 3), - marginal_beta = c(0.3, 0.1, -0.2) + i / 100, - marginal_se = rep(0.05, 3), - marginal_z = c(6.0, 2.0, -4.0), - marginal_p = c(1e-9, 0.045, 6e-5), - pip = c(0.9, 0.5, 0.7), - posterior_mean = rep(0.05, 3), - posterior_sd = rep(0.02, 3), - cs_95 = paste0("susie_", c(1L, 1L, 0L)), - stringsAsFactors = FALSE - ) - fineMappingRow(variantIds = ids, susieFit = list(), topLoci = tl) - }) - QtlFineMappingResult( - studyName = rep("study1", 2), - context = contexts, - trait = traits, - method = rep("susie", 2), - entry = entries - ) -} - -test_that("writeSumStatsVcf(FineMappingResult): splitByContext emits one VCF per context", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - fmr <- .makeMultiTupleQtlFmr() - baseOut <- tempfile(fileext = ".vcf") - on.exit( - unlink(list.files( - dirname(baseOut), - pattern = basename(tools::file_path_sans_ext(baseOut)), - full.names = TRUE - )), - add = TRUE - ) - paths <- writeSumStatsVcf(fmr, baseOut, splitByContext = TRUE) - expect_length(paths, 2L) - # Each path is decorated with the context tag. - expect_true(any(grepl("\\.brain\\.vcf$", paths))) - expect_true(any(grepl("\\.blood\\.vcf$", paths))) - for (p in paths) { - expect_true(file.exists(p)) - } -}) - -test_that("writeSumStatsVcf(FineMappingResult): splitByTrait emits one VCF per trait", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - fmr <- .makeMultiTupleQtlFmr() - baseOut <- tempfile(fileext = ".vcf") - on.exit( - unlink(list.files( - dirname(baseOut), - pattern = basename(tools::file_path_sans_ext(baseOut)), - full.names = TRUE - )), - add = TRUE - ) - paths <- writeSumStatsVcf(fmr, baseOut, splitByTrait = TRUE) - expect_length(paths, 2L) - expect_true(any(grepl("\\.ENSG_A\\.vcf$", paths))) - expect_true(any(grepl("\\.ENSG_B\\.vcf$", paths))) -}) - -test_that("writeSumStatsVcf(FineMappingResult): splitByContext + splitByTrait combines tags", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - fmr <- .makeMultiTupleQtlFmr() - baseOut <- tempfile(fileext = ".vcf") - on.exit( - unlink(list.files( - dirname(baseOut), - pattern = basename(tools::file_path_sans_ext(baseOut)), - full.names = TRUE - )), - add = TRUE - ) - paths <- writeSumStatsVcf( - fmr, - baseOut, - splitByContext = TRUE, - splitByTrait = TRUE - ) - expect_length(paths, 2L) - expect_true(any(grepl("\\.brain\\.ENSG_A\\.vcf$", paths))) - expect_true(any(grepl("\\.blood\\.ENSG_B\\.vcf$", paths))) -}) - -test_that("writeSumStatsVcf(FineMappingResult): multi-row without split flags requires selectors", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - fmr <- .makeMultiTupleQtlFmr() - out <- tempfile(fileext = ".vcf") - expect_error(writeSumStatsVcf(fmr, out), "2 matching rows") -}) - -# ============================================================================= -# GwasSumStats: multi-study selection, MAF/AF field, .gz rename -# ============================================================================= - -.makeTwoStudyGwasSumStats <- function(n = 3, withMaf = FALSE) { - mkGr <- function() { - gr <- GenomicRanges::GRanges( - "chr1", - IRanges::IRanges( - start = seq(100, by = 100, length.out = n), - width = 1 - ) - ) - mc <- S4Vectors::DataFrame( - SNP = paste0("rs", seq_len(n)), - A1 = rep("A", n), - A2 = rep("T", n), - Z = seq(1.5, by = -0.5, length.out = n), - N = rep(1000L, n) - ) - if (withMaf) { - mc$MAF <- rep(0.2, n) - } - S4Vectors::mcols(gr) <- mc - gr - } - GwasSumStats( - studyName = c("studyA", "studyB"), - entry = list(mkGr(), mkGr()), - genome = "hg38", - ldSketch = makeTestGenotypeHandle() - ) -} - -test_that("writeSumStatsVcf(GwasSumStats): multi-study without `study` selector errors", { - skip_if_not_installed("VariantAnnotation") - ss2 <- .makeTwoStudyGwasSumStats() - out <- tempfile(fileext = ".vcf") - expect_error(writeSumStatsVcf(ss2, out), "2 studies") -}) - -test_that("writeSumStatsVcf(GwasSumStats): `study` selector writes the chosen study", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - ss2 <- .makeTwoStudyGwasSumStats() - out <- tempfile(fileext = ".vcf") - on.exit(unlink(out), add = TRUE) - res <- writeSumStatsVcf(ss2, out, studyName = "studyB") - expect_equal(res, out) - expect_true(file.exists(out)) -}) - -test_that("writeSumStatsVcf(GwasSumStats): emits the AF genotype field when MAF present", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - ss <- GwasSumStats( - studyName = "t", - entry = list(local({ - gr <- GenomicRanges::GRanges( - "chr1", - IRanges::IRanges(start = c(100, 200, 300, 400), width = 1) - ) - S4Vectors::mcols(gr) <- S4Vectors::DataFrame( - SNP = paste0("rs", 1:4), - A1 = rep("A", 4), - A2 = rep("T", 4), - Z = c(1.5, 1.0, 0.5, 0.0), - N = rep(1000L, 4), - MAF = rep(0.2, 4) - ) - gr - })), - genome = "hg38", - ldSketch = makeTestGenotypeHandle() - ) - out <- tempfile(fileext = ".vcf") - on.exit(unlink(out), add = TRUE) - writeSumStatsVcf(ss, out) - expect_true(file.exists(out)) - # The AF FORMAT field is declared in the header when MAF is present. - expect_true(any(grepl("ID=AF", readLines(out)))) -}) - -test_that("writeSumStatsVcf(GwasSumStats): .vcf.gz output is renamed from writeVcf's .bgz", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - ss <- makeTestGwasSumStats(5) - out <- tempfile(fileext = ".vcf.gz") - on.exit(unlink(c(out, paste0(out, ".tbi")), force = TRUE), add = TRUE) - res <- writeSumStatsVcf(ss, out) - expect_equal(res, out) - # writeVcf always emits .bgz; the writer renames it to the requested .gz. - expect_true(file.exists(out)) - expect_false(file.exists(sub("\\.gz$", ".bgz", out))) -}) - -# ============================================================================= -# FineMappingResult: explicit selectors, no-match error, composite/empty tags -# ============================================================================= - -test_that("writeSumStatsVcf(FineMappingResult): explicit selectors pick a single row", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - fmr <- .makeMultiTupleQtlFmr() - out <- tempfile(fileext = ".vcf") - on.exit(unlink(out), add = TRUE) - res <- writeSumStatsVcf( - fmr, - out, - studyName = "study1", - context = "brain", - trait = "ENSG_A", - method = "susie" - ) - expect_equal(res, out) - expect_true(file.exists(out)) -}) - -test_that("writeSumStatsVcf(FineMappingResult): no matching rows errors", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - fmr <- .makeMultiTupleQtlFmr() - out <- tempfile(fileext = ".vcf") - expect_error( - writeSumStatsVcf(fmr, out, studyName = "does_not_exist"), - "no rows match" - ) -}) - -test_that("writeSumStatsVcf(FineMappingResult): splitByContext decorates composite .vcf.bgz paths", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - fmr <- .makeMultiTupleQtlFmr() - baseOut <- tempfile(fileext = ".vcf.bgz") - stem <- tools::file_path_sans_ext(tools::file_path_sans_ext(baseOut)) - on.exit( - unlink( - list.files( - dirname(baseOut), - pattern = basename(stem), - full.names = TRUE - ), - force = TRUE - ), - add = TRUE - ) - paths <- writeSumStatsVcf(fmr, baseOut, splitByContext = TRUE) - expect_length(paths, 2L) - # Composite extension is preserved while the context tag is injected before it. - expect_true(all(grepl("\\.vcf\\.bgz$", paths))) - expect_true(any(grepl("\\.brain\\.vcf\\.bgz$", paths))) - expect_true(any(grepl("\\.blood\\.vcf\\.bgz$", paths))) -}) - -test_that("writeSumStatsVcf(FineMappingResult): splitByContext on a context-less result keeps the path", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - # GwasFineMappingResult has no context/trait axes, so the split tag set is - # empty and the original output path is used unchanged. - fm <- makeTestFineMappingResult(5) - out <- tempfile(fileext = ".vcf") - on.exit(unlink(out), add = TRUE) - paths <- writeSumStatsVcf(fm, out, splitByContext = TRUE) - expect_length(paths, 1L) - expect_equal(paths[[1L]], out) - expect_true(file.exists(out)) -}) - -test_that("writeSumStatsVcf(FineMappingResult): emits AF from the topLoci `af` column", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - # The marginal view projects allele frequency to a column named `af`, so a - # topLoci carrying `af` must surface as the AF genotype field. - n <- 3 - tl <- data.frame( - variant_id = paste0("chr1:", c(100, 200, 300), ":T:A"), - chrom = rep("1", n), - pos = c(100L, 200L, 300L), - A1 = rep("A", n), - A2 = rep("T", n), - N = rep(1000, n), - af = c(0.1, 0.2, 0.3), - marginal_beta = c(0.3, -0.2, 0.1), - marginal_se = rep(0.05, n), - marginal_z = c(6.0, -4.0, 2.0), - marginal_p = c(1e-9, 6e-5, 0.045), - pip = c(0.9, 0.7, 0.5), - posterior_mean = rep(0.05, n), - posterior_sd = rep(0.02, n), - cs_95 = paste0("susie_", c(1L, 0L, 2L)), - stringsAsFactors = FALSE - ) - entry <- fineMappingRow( - variantIds = tl$variant_id, - susieFit = list(), - topLoci = tl - ) - fm <- GwasFineMappingResult( - studyName = "s", - method = "susie", - entry = list(entry) - ) - out <- tempfile(fileext = ".vcf") - on.exit(unlink(out), add = TRUE) - writeSumStatsVcf(fm, out) - expect_true(file.exists(out)) - expect_true(any(grepl("ID=AF", readLines(out)))) -}) - -test_that("writeSumStatsVcf(FineMappingResult): emits LBF / LFSR / PUR / fullFit FORMAT fields", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - # A rich topLoci exercises every posterior FORMAT field: per-variant logBF (LBF), - # local false sign rate (LFSR), per-CS purity (PUR), and the wide fullFit - # columns (within_cs_pip / cs_logbf_ / cs_effect_). - n <- 3 - tl <- data.frame( - variant_id = paste0("chr1:", c(100, 200, 300), ":T:A"), - chrom = rep("1", n), - pos = c(100L, 200L, 300L), - A1 = rep("A", n), - A2 = rep("T", n), - N = rep(1000, n), - marginal_beta = c(0.3, -0.2, 0.1), - marginal_se = rep(0.05, n), - marginal_z = c(6, -4, 2), - marginal_p = c(1e-9, 6e-5, 0.045), - pip = c(0.9, 0.7, 0.5), - posterior_mean = rep(0.05, n), - posterior_sd = rep(0.02, n), - logBF = c(3.1, 1.2, 0.4), - lfsr = c(0.01, 0.2, 0.4), - cs_95 = paste0("susie_", c(1L, 0L, 2L)), - cs_95_purity = c(0.95, 0, 0.88), - within_cs_pip = c(0.6, 0, 0.5), - cs_logbf_susie_1 = c(2.2, NA, NA), - cs_effect_susie_1 = c(0.4, NA, NA), - stringsAsFactors = FALSE - ) - entry <- fineMappingRow( - variantIds = tl$variant_id, - susieFit = list(), - topLoci = tl - ) - fm <- GwasFineMappingResult( - studyName = "s", - method = "susie", - entry = list(entry) - ) - out <- tempfile(fileext = ".vcf") - on.exit(unlink(out), add = TRUE) - writeSumStatsVcf(fm, out) - ln <- readLines(out) - for (id in c( - "ID=LBF", - "ID=LFSR", - "ID=PUR95", - "ID=WITHIN_CS_PIP", - "ID=CS_LOGBF_SUSIE_1", - "ID=CS_EFFECT_SUSIE_1" - )) { - expect_true(any(grepl(id, ln, fixed = TRUE)), info = id) - } -}) - -test_that("writeSumStatsVcf(FineMappingResult): falls back to marginal sumstats when no posterior", { - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Biostrings") - fm <- makeTestFineMappingResult(5) - # Simulate an entry whose posterior table is unavailable (empty topLoci) so - # the marginal univariate sumstats alone drive the VCF body (the else-if branch). - testthat::local_mocked_bindings( - topLoci = function(x, ...) GenomicRanges::GRanges(), - .package = "pecotmr" - ) - out <- tempfile(fileext = ".vcf") - on.exit(unlink(out), add = TRUE) - writeSumStatsVcf(fm, out) - expect_true(file.exists(out)) - expect_true(any(grepl("ID=ES", readLines(out), fixed = TRUE))) # marginal beta -> ES -}) - - -# =========================================================================== -# Output formats -# =========================================================================== - -test_that("plain and bgzipped VCF are written, the latter with an index", { - skip_if_not_installed("VariantAnnotation") - data(gwasSumStatsS4Example) - d <- withr::local_tempdir() - writeSumStatsVcf(gwasSumStatsS4Example, outputPath = file.path(d, "a.vcf")) - expect_true(file.exists(file.path(d, "a.vcf"))) - writeSumStatsVcf( - gwasSumStatsS4Example, - outputPath = file.path(d, "b.vcf.bgz") - ) - expect_true(file.exists(file.path(d, "b.vcf.bgz"))) - # index = TRUE, so the tabix index is written alongside. - expect_true(file.exists(file.path(d, "b.vcf.bgz.tbi"))) -}) - -test_that("BCF output explains itself when Rsamtools cannot convert", { - # Rsamtools >= 2.26 raises "asBcf() is temporarily disabled". Left bare - # that surfaces as an opaque failure from a documented output format, so - # it is translated into the cause plus the workaround. - skip_if_not_installed("VariantAnnotation") - skip_if_not_installed("Rsamtools") - data(gwasSumStatsS4Example) - d <- withr::local_tempdir() - canConvert <- !inherits( - tryCatch( - Rsamtools::asBcf("nonexistent", "x", "y"), - error = function(e) e - ), - "error" - ) - skip_if(canConvert, "Rsamtools::asBcf() works here; nothing to translate") - expect_error( - writeSumStatsVcf( - gwasSumStatsS4Example, - outputPath = file.path(d, "c.bcf") - ), - "BCF output needs a working Rsamtools::asBcf" - ) -}) - -test_that(".vcfResolveBody uses posterior rows when no marginals exist", { - local_mocked_bindings( - topLoci = function(entry, signalCutoff) { - # topLoci() returns ranges carrying the per-variant columns in - # mcols(); these ids are deliberately not coordinate-shaped, so - # the ranges are built directly rather than parsed from them. - gr <- GenomicRanges::GRanges( - "chr1", - IRanges::IRanges(start = c(1L, 2L), width = 1L) - ) - S4Vectors::`mcols<-`( - gr, - value = S4Vectors::DataFrame( - variant_id = c("v1", "v2"), - pip = c(0.9, 0.1) - ) - ) - }, - marginalEffects = function(entry) stop("none"), - .package = "pecotmr" - ) - out <- pecotmr:::.vcfResolveBody("e", "S1") - expect_true(out$hasPost) - expect_equal(nrow(out$base), 2L) - # No marginal table to align against, so there is nothing to merge in. - expect_null(out$m) -}) - -test_that(".writeVcfImpl adds the chr prefix to bare chromosome names", { - skip_if_not_installed("VariantAnnotation") - out <- tempfile(fileext = ".vcf") - on.exit(unlink(out), add = TRUE) - geno <- matrix(c(1.5, -0.5), ncol = 1L, dimnames = list(NULL, "S1")) - header <- S4Vectors::DataFrame( - Number = "1", - Type = "Float", - Description = "Z score", - row.names = "Z" - ) - pecotmr:::.writeVcfImpl( - chrom = c("1", "1"), - pos = c(100L, 200L), - ref = c("A", "C"), - alt = c("G", "T"), - snpIds = c("rs1", "rs2"), - geno = list(Z = geno), - genoHeader = header, - sampleName = "S1", - outputPath = out - ) - body <- readLines(out) - body <- body[!startsWith(body, "#")] - expect_true(all(startsWith(body, "chr1\t"))) -}) diff --git a/vignettes/finemapping-postprocessing.Rmd b/vignettes/finemapping-postprocessing.Rmd index b8ff2818..be86bfd9 100644 --- a/vignettes/finemapping-postprocessing.Rmd +++ b/vignettes/finemapping-postprocessing.Rmd @@ -54,11 +54,29 @@ head(as.data.frame(loci)[, c( "pip", "cs_95", "cs_95_purity", - "beta", - "se" + "posterior_effect", + "posterior_se" )], 4) ``` +`posterior_effect` is the posterior mean effect — the same quantity a TWAS +weight is, and literally what `susieWeights()` returns — and `posterior_se` +its standard error. They are deliberately *not* called `beta`/`se`: +[`marginalEffects()`](#) publishes the **marginal** single-variant estimate +under those names, so sharing them would let the two views be joined into a +silent mix of posterior and marginal effects. + +Both are on the standardized genotype scale, because the `QtlDataset` +accessors centre and scale every genotype column to unit variance before +fitting — `residualizedGenotypes()` does it, governed by +`ResidualizationParam(scaleResiduals = FALSE)` per call or the dataset's own +`scaleResiduals` slot. The published `weight_standardized` records whichever +way it resolved. +That is what makes a posterior effect comparable with a TWAS weight from any +other method — they are all on the scale of the `X` the fitter was given, and +on the same scale as the LD correlations and z-scores a TWAS Z is computed +from. The published `weight_standardized` column records it. + `cs_95` names the credible set the variant belongs to, or is `NA` when it belongs to none. The `cs_70` / `cs_50` columns give the same membership at looser coverage, so a variant can be in a 95% set but not a 50% one. @@ -266,20 +284,73 @@ object the other helpers expect. ## Writing results out -`writeSumStatsVcf()` serialises a fine-mapping result — or summary -statistics — to GWAS-VCF, the interchange format most external tools read: +Two generics cover every published format. `exportToVcf()` serialises a +fine-mapping result — or summary statistics, or TWAS weights — to GWAS-VCF, +the interchange format most external tools read: one record per variant, one +sample per study or fitted model, and the statistics as FORMAT fields. ```{r vcf, eval=FALSE} -writeSumStatsVcf( +exportToVcf( qtlFineMappingExample, - file = "finemapping.vcf.gz", - genome = "hg38" + outputPath = "finemapping.vcf.gz", + context = "context_1" ) ``` It needs the `VariantAnnotation` package, which is a suggested dependency rather than a hard one. +`exportToBed()` writes the published long-format tables instead, one row per +published observation, bgzipped and tabix-indexed when the path ends in `.gz`: + +```{r bed, eval=FALSE} +# the top-loci table: the posterior view joined to the marginal one +exportToBed(qtlFineMappingExample, "top_loci.bed.gz") +# every remaining column the view carries, after the published schema +exportToBed(qtlFineMappingExample, "top_loci_full.bed.gz", columns = "all") +# the fSuSiE table: one row per (context, credible set), carrying the fitted +# curve at both resolutions -- the wavelet grid and its reinterpolation onto +# the trait positions. `levels` keeps only one of the two. +exportToBed(fsusieFineMappingExample, "fsusie.bed.gz", schema = "fsusie") +exportToBed(fsusieFineMappingExample, "fsusie_grid.bed.gz", + schema = "fsusie", levels = "wavelet") +``` + +Both accept a `list` of results and fold it into one sorted, indexed output, +so a genome's worth of per-region RDSes needs no separate concatenate step. + +## Reading results back + +`readFineMappingResult()` is the inverse, and it is also the way into pecotmr +for a result produced somewhere else: it reads either format and rebuilds the +collection, keyed by whatever `(study, context, trait, method)` tuples the +file holds. + +```{r read, eval=FALSE} +res <- readFineMappingResult("top_loci.bed.gz") +# only the variants of one region, from a tabix-indexed file +res <- readFineMappingResult("top_loci.bed.gz", region = "chr22:32e6-33e6") +``` + +It requires almost nothing: coordinate-encoding variant ids and a `PIP`. +Every other column is optional, because each consumer needs a different few — +Mendelian randomization the marginal effect and its standard error, +colocalization the log Bayes factors — and a table converted from another tool +will carry some and not others. A consumer reached without what it needs names +the column that is missing rather than quietly producing nothing. + +Two arguments exist for files that were assembled by hand. `studyName`, +`context`, `trait` and `method` fill in identity columns the file does not +record (a value that *disagrees* with the file is an error, not an override), +and `weightStandardized` says which genotype scale a published `WGT` is on — +required when the file does not say, because the two scales differ by a +per-variant, allele-frequency-dependent factor and neither is recognisable on +its own. + +`readTwasWeights()` does the same for a weights file. Note that a fine-mapping +table reads as weights too: the posterior effect *is* the prediction weight, +which is why both schemas publish it as `WGT`. + ## Next steps - [Fine-mapping](fine-mapping.html) — producing the results this vignette diff --git a/vignettes/twas-weights.Rmd b/vignettes/twas-weights.Rmd index 4addaaeb..8c3a26bc 100644 --- a/vignettes/twas-weights.Rmd +++ b/vignettes/twas-weights.Rmd @@ -355,10 +355,24 @@ key <- list( ) w <- do.call(weights, c(list(tw), key)) length(w) -do.call(weightStandardized, c(list(tw), key)) # standardized or original scale +do.call(weightStandardized, c(list(tw), key)) # TRUE from the pipeline do.call(weightsDataType, c(list(tw), key)) # tag carried through for reporting ``` +`weightStandardized()` is `TRUE` for anything the pipeline fitted: genotypes +are centred and scaled to unit variance before every method runs, so all the +weights are on one scale. That scaling lives in the `QtlDataset` accessors, +not in the pipeline — `ResidualizationParam(scaleResiduals = FALSE)` opts out +per call, and the dataset's own `scaleResiduals` slot is the default — so the +recorded flag reports what was done rather than what you claimed. That is not bookkeeping. Every method here returns +coefficients on the scale of the `X` it was handed — exactly so for glmnet, +ncvreg, L0Learn, mr.ash, the Bayesian alphabet and `susieWeights()` — while +the DPR samplers are not even scale-equivariant, so rescaling `X` changes +their fit rather than just its units. Standardizing up front removes the +question, and lands on the scale the LD correlations and z-scores behind a +TWAS Z already use. `learnTwasWeights()` still takes `weightStandardized`, +because there you supply `X` yourself. + `weights()` returns a named numeric vector for univariate methods and a matrix for multivariate ones. The remaining payloads are keyed the same way: `variantIds()` for the aligned variant set, `cvResult()` for the @@ -367,6 +381,52 @@ the full fit object when the pipeline kept fits (`fitRetention != "none"`). `resolveWeights()` returns the weights and their variant IDs together, which is the form the downstream TWAS and MR steps consume. +## Publishing the weights + +The same two generics that export fine-mapping results export weights. +`exportToVcf()` writes a GWAS-VCF-style file: one record per variant, one +sample per fitted model, the weight in the `WGT` FORMAT field, and each +model's identity in its own `##SAMPLE` header line. `exportToBed()` writes the +same thing as a long table, one row per (variant, model). + +```{r tw-export, eval=FALSE} +exportToVcf(tw, "weights.vcf.gz") +exportToVcf(tw, "susie_weights.vcf.gz", method = "susie") # one model +exportToBed(tw, "weights.bed.gz") +``` + +Both export the **full-data fit only**, and refuse an object that has none — +`twasWeightsPipeline(fitFullData = FALSE)` produces rows holding nothing but +cross-validation results, and those cannot be published: they describe +held-out folds, and fold membership is sample-level information. For the same +reason neither exporter writes CV metrics. Those stay on the object: + +```{r tw-cv, eval=FALSE} +cv <- cvResult(tw, studyName = "s1", context = "brain", trait = "g1", + method = "susie") +cv$performance # per-method metric matrix: corr, rsq, adj_rsq, pval, RMSE, MAE +``` + +Fine-mapping-derived rows are skipped with a message, because their "weights" +are the fine-mapping posterior re-expressed — export the `FineMappingResult` +instead, which carries the posterior, its standard error and the credible +sets. + +`readTwasWeights()` reads either format back into a `TwasWeights`: + +```{r tw-read, eval=FALSE} +tw <- readTwasWeights("weights.bed.gz") +tw <- readTwasWeights("weights.bed.gz", region = "chr22:10e6-19e6") +``` + +A weight is all it requires, so a table converted from another tool reads with +nothing but variant ids and a `WGT` column — plus the identity axes the +collection is keyed by, which `studyName` / `context` / `trait` / `method` +supply when the file does not record them. `weightStandardized` is required +when the file does not say which genotype scale the weights are on: that is +not recoverable from the numbers, since converting between the two rescales +every variant by its own allele-frequency-dependent factor. + ## Next steps - Combine the learned weights with a GWAS via From 4847c263a40a5ebe16e4f131867cd810ff70a8f3 Mon Sep 17 00:00:00 2001 From: Daniel Nachun Date: Thu, 15 Oct 2026 19:17:05 -0700 Subject: [PATCH 2/5] add tests --- R/resultReaders.R | 17 +- R/resultWriters.R | 40 ++- tests/testthat/test_resultReaders.R | 301 ++++++++++++++++++++ tests/testthat/test_resultWriters.R | 411 +++++++++++++++++++++++++++- vignettes/twas-weights.Rmd | 12 +- 5 files changed, 759 insertions(+), 22 deletions(-) diff --git a/R/resultReaders.R b/R/resultReaders.R index 5a725865..b42ebca4 100644 --- a/R/resultReaders.R +++ b/R/resultReaders.R @@ -170,7 +170,16 @@ NULL samples <- as.character(sampleSelect) } if (length(samples) == 0L) { - return(list(table = tibble(), fits = list())) + # A sites-only VCF -- no FORMAT, no sample columns -- carries no + # per-variant statistics at all. Returning an empty table instead + # only defers the failure to whichever required column is missed + # first, which reported "no `PIP` column" for a file whose real + # problem is that it holds no results. + abort(glue( + "{label}: the VCF has no samples, so it carries no published ", + "results. A published file records each result as a sample ", + "with its own FORMAT fields." + )) } meta <- .prdVcfSampleMeta(vcf) pieces <- map(samples, .prdVcfSampleTable, vcf = vcf, meta = meta) @@ -720,11 +729,11 @@ NULL # having it, for a file a user assembled themselves -- and a disagreement is # an error rather than a silent override, because one of the two is wrong and # the reader cannot tell which. +# `supplied` is never NULL here: both callers build it with `compact()`, so +# only the arguments the caller actually gave reach this function, and a +# NULL-guard would be dead by construction. # @noRd .prdResolve <- function(fileValue, supplied, nm, label) { - if (is.null(supplied)) { - return(fileValue) - } sup <- as.character(supplied)[[1L]] if (!is.na(fileValue) && !identical(fileValue, sup)) { abort(glue( diff --git a/R/resultWriters.R b/R/resultWriters.R index ed566cb1..49f0ac8e 100644 --- a/R/resultWriters.R +++ b/R/resultWriters.R @@ -1288,6 +1288,19 @@ setMethod( if (length(rows) == 0L) { abort("exportToVcf: no rows match the supplied selectors.") } + # The same rule the BED path applies, and for the same reason: a + # fine-mapping method's "weights" are the posterior re-expressed, so + # the FineMappingResult says strictly more. This path used to publish + # them, which made the two formats disagree about what a weights file + # contains. + rows <- intersect(rows, .twbExportableRows(x, "exportToVcf")) + if (length(rows) == 0L) { + abort(str_c( + "exportToVcf: every selected row is fine-mapping-derived, so ", + "there is nothing to write as weights. Export the ", + "FineMappingResult." + )) + } .twvWriteSamples( x, .twvSampleSpecs(x, rows), @@ -1985,6 +1998,13 @@ setMethod( # @noRd .pubCsPair <- function(lv) c(str_c("CS", lv), str_c("PUR", lv)) +# The available-levels phrase. `str_flatten(character(0))` is "", not NULL, so +# a `%||%` fallback never fires and the message read "Available: ." +# @noRd +.pubLevelList <- function(have) { + if (length(have) == 0L) "" else str_flatten(have, ", ") +} + # The coverage levels to publish: every `cs_` column the view carries, or # the requested subset -- with a requested level the fit never computed an # error that names what IS available, rather than a silently empty column. @@ -2003,8 +2023,7 @@ setMethod( abort(glue( "`coverage` asks for {str_flatten(absent, ', ')}, which this fit ", "did not compute; credible sets are fixed at fit time by ", - "CredibleSetParam. Available: ", - "{str_flatten(have, ', ') %||% ''}." + "CredibleSetParam. Available: {.pubLevelList(have)}." )) } want @@ -2150,7 +2169,6 @@ setMethod( bind_cols(out, select(tl, all_of(added))) } -# The CS membership / purity values for the discovered column pairs. # The CS membership / purity values as a tibble, for binding into a block. # @noRd .pubFrameCs <- function(view, csCols) { @@ -2228,11 +2246,17 @@ setMethod( # reader would then believe ES/SE were derived when they were recorded. # @noRd .pubEz <- function(view, n = NULL) { - supplied <- .pubCol(view, "z_supplied", n) - if (!any(isTRUE(as.logical(supplied)))) { + # `isTRUE()` is length-1 only: `isTRUE(c(TRUE, TRUE))` is FALSE, so the + # all-or-nothing test this used never fired for a view with more than one + # variant and EZ was never published at all. Per ROW anyway, matching + # `.vcfSuppliedZ()`: a file can hold a mix, and a derived z must not + # borrow a neighbour's provenance. + supplied <- as.logical(.pubCol(view, "z_supplied", n)) + z <- as.numeric(.pubCol(view, "z", n)) + if (!any(!is.na(supplied) & supplied)) { return(if (is.null(n)) NA_real_ else rep(NA_real_, n)) } - .pubCol(view, "z", n) + replace(z, is.na(supplied) | !supplied, NA_real_) } # @noRd @@ -2448,12 +2472,12 @@ setMethod( ) # @noRd -.twbExportableRows <- function(x) { +.twbExportableRows <- function(x, label = "exportToBed") { methods <- .pubTupleColumn("method", x) skip <- which(is_in(methods, .TWB_FINE_MAPPING_METHODS)) if (length(skip) > 0L) { inform(glue( - "exportToBed: skipping {length(skip)} fine-mapping-derived ", + "{label}: skipping {length(skip)} fine-mapping-derived ", "row(s) ({str_flatten(unique(methods[skip]), ', ')}). Those ", "weights are the fine-mapping posterior re-expressed -- export ", "the FineMappingResult instead, which carries the posterior, its ", diff --git a/tests/testthat/test_resultReaders.R b/tests/testthat/test_resultReaders.R index 34e08312..d6dca5a6 100644 --- a/tests/testthat/test_resultReaders.R +++ b/tests/testthat/test_resultReaders.R @@ -673,3 +673,304 @@ test_that("an empty result survives an export and a read", { exportToBed(qtlFineMappingExample, path, signalCutoff = 2) expect_equal(nrow(readFineMappingResult(path)), 0L) }) + +# --------------------------------------------------------------------------- +# The VCF reader's edge paths +# --------------------------------------------------------------------------- + +# A published VCF, bgzipped and tabix-indexed so it can be read by region. +# @noRd +.rdr_indexedVcf <- function(x, name, ...) { + plain <- .rdr_tmp(name) + exportToVcf(x, plain, ...) + gz <- Rsamtools::bgzip(plain, dest = str_c(plain, ".gz"), overwrite = TRUE) + Rsamtools::indexTabix(gz, format = "vcf") + gz +} + +test_that("a tabix-indexed VCF can be read by region", { + skip_if_not_installed("VariantAnnotation") + data(qtlFineMappingExample, envir = environment()) + gz <- .rdr_indexedVcf( + qtlFineMappingExample, + "region.vcf", + context = "context_1" + ) + all <- readFineMappingResult(gz, weightStandardized = TRUE) + full <- topLoci(all, signalCutoff = 0) + window <- GenomicRanges::GRanges( + as.character(GenomicRanges::seqnames(full))[[1L]], + IRanges::IRanges( + start = GenomicRanges::start(full)[[1L]], + end = GenomicRanges::start(full)[[5L]] + ) + ) + part <- readFineMappingResult( + gz, + region = window, + weightStandardized = TRUE + ) + expect_lt(length(variantIds(part)), length(variantIds(all))) + expect_true(all(is_in(variantIds(part), variantIds(all)))) + # The window still carries the analysis region from the ##SAMPLE header, + # which is per-fit and does not shrink with the variants read. + expect_equal(genomicRegion(part), genomicRegion(all)) +}) + +test_that("a region naming no seqname in the VCF reads as empty", { + skip_if_not_installed("VariantAnnotation") + data(qtlFineMappingExample, envir = environment()) + gz <- .rdr_indexedVcf( + qtlFineMappingExample, + "noseq.vcf", + context = "context_1" + ) + back <- readFineMappingResult( + gz, + region = "chrZZ:1-1000", + weightStandardized = TRUE, + studyName = "s", + context = "c", + trait = "t", + method = "susie" + ) + expect_equal(nrow(back), 0L) +}) + +test_that("an un-indexed VCF ignores a region loudly", { + skip_if_not_installed("VariantAnnotation") + data(qtlFineMappingExample, envir = environment()) + path <- .rdr_tmp("plain.vcf") + exportToVcf(qtlFineMappingExample, path, context = "context_1") + expect_warning( + readFineMappingResult( + path, + region = "chr22:1-1000000", + weightStandardized = TRUE + ), + "not bgzipped \\+ tabix-indexed" + ) +}) + +test_that("a missing VCF names itself before VariantAnnotation is asked", { + skip_if_not_installed("VariantAnnotation") + expect_error( + readFineMappingResult(.rdr_tmp("absent_xyz.vcf")), + "does not exist" + ) +}) + +test_that("sampleSelect picks fits out of a multi-sample VCF", { + skip_if_not_installed("VariantAnnotation") + data(qtlFineMappingLbfExample, envir = environment()) + # Two rows -> two samples in one file, which is GWAS-VCF's own shape. + expect_equal(nrow(qtlFineMappingLbfExample), 2L) + path <- .rdr_tmp("multi.vcf") + exportToVcf(qtlFineMappingLbfExample, path) + both <- readFineMappingResult(path, weightStandardized = TRUE) + expect_equal(nrow(both), 2L) + samples <- VariantAnnotation::samples( + VariantAnnotation::scanVcfHeader(path) + ) + one <- readFineMappingResult( + path, + sampleSelect = samples[[1L]], + weightStandardized = TRUE + ) + expect_equal(nrow(one), 1L) + expect_error( + readFineMappingResult(path, sampleSelect = "not-a-sample"), + "sampleSelect names not-a-sample, which the VCF does not have" + ) +}) + +test_that("a VCF with no ##SAMPLE block falls back to the sample name", { + skip_if_not_installed("VariantAnnotation") + data(qtlFineMappingExample, envir = environment()) + path <- .rdr_tmp("nosample.vcf") + exportToVcf(qtlFineMappingExample, path, context = "context_1") + stripped <- .rdr_tmp("nosample_stripped.vcf") + lines <- readLines(path) + writeLines(lines[!str_starts(lines, "##SAMPLE=")], stripped) + # Without the header the file records no method either, so the reader asks + # for the one axis it cannot invent. + expect_error( + readFineMappingResult(stripped, weightStandardized = TRUE), + "does not record method" + ) + back <- readFineMappingResult( + stripped, + method = "susie", + weightStandardized = TRUE + ) + # Splitting the sample name into a tuple would be guesswork; taking it + # whole keeps each sample a result of its own. + expect_s4_class(back, "GwasFineMappingResult") + expect_match(studyName(back), "\\|") + # No header means no partial fit to restore either. + expect_null(susieFit(back)) +}) + +# --------------------------------------------------------------------------- +# Published-table edge paths +# --------------------------------------------------------------------------- + +test_that("a table with start but no end still locates its variants", { + # `end` is the 1-based position of a half-open single-base record, and + # `start + 1` says the same thing -- a table carrying only `start` is + # still readable. + df <- data.frame( + `#chr` = "chr1", + start = 99L, + ID = "chr1:100:A:G", + PIP = 0.9, + study = "s", + context = "c", + trait = "t", + method = "susie", + check.names = FALSE + ) + back <- readFineMappingResult(.rdr_writeTable(df, "startonly.bed")) + expect_equal(variantIds(back), "chr1:100:A:G") +}) + +test_that("an identity-less header-only file reads as an empty GWAS result", { + # No context or trait column at all, so the GWAS branch of the empty path. + writeLines( + str_flatten(c("#chr", "start", "end", "ID", "PIP"), "\t"), + .rdr_tmp("empty_gwas.bed") + ) + back <- readFineMappingResult(.rdr_tmp("empty_gwas.bed")) + expect_s4_class(back, "GwasFineMappingResult") + expect_equal(nrow(back), 0L) +}) + +test_that("a weights file read with no WGT column points at the other reader", { + df <- data.frame( + `#chr` = "chr1", + start = 99L, + end = 100L, + ID = "chr1:100:A:G", + PIP = 0.9, + check.names = FALSE + ) + expect_error( + readTwasWeights(.rdr_writeTable(df, "nowgt.bed")), + "no `WGT` column.*readFineMappingResult" + ) +}) + +test_that("a region id with no underscores falls back to the variants' chrom", { + expect_equal( + .prdRegionChrom( + data.frame( + region_id = "noUnderscores", + `#chr` = "chr7", + check.names = FALSE + ), + 1L, + "x" + ), + "chr7" + ) + # Nothing to go on at all. + expect_true(is.na(.prdRegionChrom(data.frame(a = 1), 1L, "x"))) +}) + +test_that("a packed header value unpacks, and an absent one is NULL", { + expect_equal(.prdUnpackNumeric(list("1;2;3")), c(1, 2, 3)) + expect_null(.prdUnpackNumeric(NULL)) + expect_null(.prdUnpackNumeric(list(NA_character_))) + expect_null(.prdUnpackNumeric(list(""))) +}) + +# --------------------------------------------------------------------------- +# Headers sparser than the writer's own +# --------------------------------------------------------------------------- + +# A published VCF whose `##SAMPLE` block is replaced wholesale, keeping the +# sample `ID` so the block still belongs to the sample. A file written by an +# older pecotmr -- or by anything else following the GWAS-VCF spec -- carries +# fewer keys than today's writer, and the reader has to work from whatever is +# there rather than requiring the full set. +# @noRd +.rdr_reheaderedVcf <- function(name, keys) { + data(qtlFineMappingExample, envir = environment()) + path <- .rdr_tmp(str_c(name, "_src.vcf")) + exportToVcf(qtlFineMappingExample, path, context = "context_1") + lines <- readLines(path) + at <- which(str_starts(lines, "##SAMPLE=")) + expect_length(at, 1L) + id <- str_match(lines[[at]], "ID=([^,>]+)")[, 2L] + lines[[at]] <- str_c("##SAMPLE=") + out <- .rdr_tmp(str_c(name, ".vcf")) + writeLines(lines, out) + list(path = out, sample = id) +} + +test_that("a ##SAMPLE block with no identity keys reads as one study", { + skip_if_not_installed("VariantAnnotation") + f <- .rdr_reheaderedVcf("noident", "TotalVariants=3") + # Nothing in the header maps to an identity axis, so the sample name is + # the only study there is -- and method, which cannot be invented, has to + # be supplied. + expect_error( + readFineMappingResult(f$path, weightStandardized = TRUE), + "does not record method" + ) + back <- readFineMappingResult( + f$path, + method = "susie", + weightStandardized = TRUE + ) + expect_s4_class(back, "GwasFineMappingResult") + expect_equal(studyName(back), f$sample) + # A header carrying neither V nor CsIndex restores no partial fit. + expect_null(susieFit(back)) +}) + +test_that("a ##SAMPLE block with no RegionStart still reads", { + skip_if_not_installed("VariantAnnotation") + f <- .rdr_reheaderedVcf( + "noregion", + "Study=s1,Context=c1,Trait=t1,Method=susie" + ) + back <- readFineMappingResult(f$path, weightStandardized = TRUE) + expect_equal(as.character(back$study), "s1") + expect_equal(as.character(back$method), "susie") + # The window is the span of the variants the file holds, because the + # header recorded no analysis region to prefer over it. + rg <- genomicRegion(back) + expect_equal(start(rg), min(IRanges::start(back[[1L]]))) + expect_null(susieFit(back)) +}) + +# A header or data line trimmed to the eight fixed VCF columns. +# @noRd +.rdr_dropSampleColumns <- function(line) { + if (str_starts(line, "##")) { + return(line) + } + str_flatten(str_split_1(line, "\t")[1:8], "\t") +} + +test_that("a VCF with no samples reads as an empty result", { + skip_if_not_installed("VariantAnnotation") + data(qtlFineMappingExample, envir = environment()) + path <- .rdr_tmp("sites_src.vcf") + exportToVcf(qtlFineMappingExample, path, context = "context_1") + lines <- readLines(path) + keep <- !str_starts(lines, "##SAMPLE=") & !str_starts(lines, "##FORMAT=") + # A sites-only VCF: the eight fixed columns and no sample at all, which + # is a legal VCF and carries no results. + sites <- map_chr(lines[keep], .rdr_dropSampleColumns) + out <- .rdr_tmp("sites.vcf") + writeLines(sites, out) + # Naming the real problem: the earlier behaviour was to hand back an + # empty table, which failed further in with "no `PIP` column". + expect_error( + readFineMappingResult(out, method = "susie"), + "the VCF has no samples" + ) + expect_error(readTwasWeights(out), "the VCF has no samples") +}) diff --git a/tests/testthat/test_resultWriters.R b/tests/testthat/test_resultWriters.R index 75814123..f42cd9cc 100644 --- a/tests/testthat/test_resultWriters.R +++ b/tests/testthat/test_resultWriters.R @@ -1261,14 +1261,16 @@ test_that("exportToVcf writes TWAS weights with one sample per model", { "\t" )[[1L]] samples <- header[-seq_len(9L)] - expect_length(samples, 11L) + # Nine of the eleven rows: the two fine-mapping-derived ones are skipped, + # the same as on the BED path. + expect_length(samples, 9L) expect_true(all(str_detect(samples, "^s|protocol_example\\|bulk_rnaseq"))) expect_true(any(str_detect(lines, "ID=WGT"))) expect_identical(sum(!str_starts(lines, "#")), 168L) # the per-variant FORMAT column is the weight, one value per sample body <- str_split(lines[!str_starts(lines, "#")][[1L]], "\t")[[1L]] expect_identical(body[[9L]], "WGT") - expect_length(body, 9L + 11L) + expect_length(body, 9L + 9L) }) test_that("the TWAS-weight VCF describes each model in a ##SAMPLE line", { @@ -1276,7 +1278,7 @@ test_that("the TWAS-weight VCF describes each model in a ##SAMPLE line", { out <- withr::local_tempfile(fileext = ".vcf") exportToVcf(twasWeightsExample, out) sampleLines <- keep(readLines(out), str_starts, "##SAMPLE=") - expect_length(sampleLines, 11L) + expect_length(sampleLines, 9L) susie <- keep(sampleLines, str_detect, fixed("Method=lasso,"))[[1L]] expect_true(str_detect(susie, "Trait=ENSG00000130538")) expect_true(str_detect(susie, "TotalVariants=168")) @@ -1287,7 +1289,7 @@ test_that("the TWAS-weight VCF describes each model in a ##SAMPLE line", { test_that("exportToVcf narrows TWAS models with the identity selectors", { data(twasWeightsExample) out <- withr::local_tempfile(fileext = ".vcf") - exportToVcf(twasWeightsExample, out, method = "susie") + exportToVcf(twasWeightsExample, out, method = "lasso") lines <- readLines(out) header <- str_split( lines[str_starts(lines, "#CHROM")][[1L]], @@ -1298,6 +1300,16 @@ test_that("exportToVcf narrows TWAS models with the identity selectors", { exportToVcf(twasWeightsExample, out, method = "nope"), "no rows match" ) + # Selecting a fine-mapping method leaves nothing exportable, which is a + # different refusal from "no rows match". + expect_error( + suppressMessages(exportToVcf( + twasWeightsExample, + out, + method = "susie" + )), + "every selected row is fine-mapping-derived" + ) }) test_that("exportToVcf names TWAS samples, or refuses a mismatched count", { @@ -1613,7 +1625,7 @@ test_that("the weight VCF header carries no cross-validation metrics", { out <- withr::local_tempfile(fileext = ".vcf") exportToVcf(twasWeightsExample, out) sampleLines <- keep(readLines(out), str_starts, "##SAMPLE=") - expect_length(sampleLines, 11L) + expect_length(sampleLines, 9L) expect_true(all(str_detect(sampleLines, "TotalVariants="))) expect_false(any(str_detect(sampleLines, "rsq=|corr=|pval=|CvRegions="))) }) @@ -1719,3 +1731,392 @@ test_that("a z-only GWAS-VCF reads without ES and SE", { expect_true(all(back$z_supplied)) expect_false(is_in("BETA", names(back))) }) + +# --------------------------------------------------------------------------- +# `coverage =`: the dynamic credible-set levels +# +# Which coverages exist is a FIT-time decision (CredibleSetParam), and the +# writers READ the stored cs_ columns rather than recomputing, so a writer +# can restrict what it publishes but can never invent a level. The error says +# what IS available instead of emitting a silently empty column. +# --------------------------------------------------------------------------- + +test_that("coverage = selects a subset of the levels the fit computed", { + data(qtlFineMappingExample, envir = environment()) + out <- withr::local_tempfile(fileext = ".bed") + exportToBed(qtlFineMappingExample, out, coverage = 0.95) + nms <- names(read.delim(out, check.names = FALSE)) + expect_true(is_in("CS95", nms)) + # The fit computed 70 and 50 as well; asking for 95 drops them. + expect_false(any(is_in(c("CS70", "CS50"), nms))) +}) + +test_that("coverage = names what the fit did compute, when asked for more", { + data(qtlFineMappingExample, envir = environment()) + out <- withr::local_tempfile(fileext = ".bed") + expect_error( + exportToBed(qtlFineMappingExample, out, coverage = c(0.95, 0.8)), + "asks for 80, which this fit did not compute.*Available: 95, 70, 50" + ) +}) + +test_that(".pubCsLevels reports the available set even when it is empty", { + # A view with no cs_ column at all: the message has to say "" + # rather than interpolate an empty vector away. + expect_error( + .pubCsLevels(tibble(pip = 0.5), coverage = 0.95), + "Available: " + ) + expect_equal(.pubCsLevels(tibble(cs_95 = "susie_1")), "95") +}) + +test_that("the fsusie schema refuses a result with no credible-set column", { + expect_error( + .fmbPrimaryCsLevel(tibble(pip = 0.5), NULL), + "carries no credible-set column, so there are no rows to key on" + ) +}) + +# --------------------------------------------------------------------------- +# EZ on the fine-mapping path, and AF from the LD panel +# --------------------------------------------------------------------------- + +test_that("a fine-mapping table publishes EZ when the source supplied z", { + # `.pubEz()` is the fine-mapping counterpart of the GwasSumStats rule: the + # z is published only where `marginal_z_supplied` says the source gave it, + # never where pecotmr derived it from beta/se. + tl <- data.frame( + variant_id = c("chr1:100:A:G", "chr1:200:C:T"), + chrom = "chr1", + pos = c(100L, 200L), + A1 = c("G", "T"), + A2 = c("A", "C"), + pip = c(0.9, 0.1), + posterior_mean = c(0.2, -0.1), + posterior_sd = c(0.05, 0.05), + marginal_beta = c(0.3, -0.2), + marginal_se = c(0.1, 0.1), + marginal_z = c(5.5, -2.5), + marginal_z_supplied = c(TRUE, TRUE), + stringsAsFactors = FALSE + ) + row <- fineMappingRow(tl$variant_id, susieFit = list(), topLoci = tl) + res <- QtlFineMappingResult( + studyName = "s", + context = "c", + trait = "t", + method = "susie", + entry = list(row) + ) + out <- withr::local_tempfile(fileext = ".bed") + exportToBed(res, out, signalCutoff = 0) + tbl <- read.delim(out, check.names = FALSE) + expect_equal(tbl$EZ, c(5.5, -2.5)) + # Flip the flag and the column goes away entirely rather than publishing a + # z pecotmr computed itself. + tl$marginal_z_supplied <- c(FALSE, FALSE) + row2 <- fineMappingRow(tl$variant_id, susieFit = list(), topLoci = tl) + res2 <- QtlFineMappingResult( + studyName = "s", + context = "c", + trait = "t", + method = "susie", + entry = list(row2) + ) + exportToBed(res2, out, signalCutoff = 0) + expect_false(is_in("EZ", names(read.delim(out, check.names = FALSE)))) +}) + +test_that("a weights export takes AF from the collection's LD panel", { + # A TwasWeights element records only alleles and a weight, so the only + # frequency available is the panel's -- and AF is what a + # standardized-to-raw conversion would need, so its absence is worth + # seeing rather than faking. + data(twasWeightsExample, envir = environment()) + expect_null(ldSketch(twasWeightsExample)) + out <- withr::local_tempfile(fileext = ".bed") + suppressMessages(exportToBed(twasWeightsExample, out)) + expect_false(is_in("AF", names(read.delim(out, check.names = FALSE)))) +}) + +test_that("every weight row being fine-mapping-derived is an error", { + # Those weights are the fine-mapping posterior re-expressed, so there is + # nothing left to write once they are skipped. + data(twasWeightsExample, envir = environment()) + fm <- twasWeightsExample[ + is_in( + as.character(twasWeightsExample$method), + .TWB_FINE_MAPPING_METHODS + ) + ] + expect_gt(nrow(fm), 0L) + out <- withr::local_tempfile(fileext = ".bed") + expect_error( + suppressMessages(exportToBed(fm, out)), + "every row is fine-mapping-derived" + ) +}) + +test_that("the output path is left alone when no split flag is set", { + expect_equal( + .decorateOutputPath( + "out.vcf.bgz", + list(context = "brain"), + FALSE, + FALSE + ), + "out.vcf.bgz" + ) +}) + +test_that("an identity-less view keys on nothing; an empty range has no id", { + # A bare single-entry view carries none of the identity axes, so the join + # key degenerates to one empty string per row -- which is the right join + # for one entry. + expect_equal( + .pubIdentityKey(tibble(pip = c(0.1, 0.2)), .PUB_ROW_COLS), + c("", "") + ) + expect_equal(.pubRegionIds(GenomicRanges::GRanges()), character(0)) + expect_equal(.pubRegionIds(NULL), character(0)) +}) + +test_that("both formats skip fine-mapping-derived weight rows alike", { + # The BED path skipped them and the VCF path published them, so the two + # disagreed about what a weights file contains. The rule is the same in + # both: those weights are the fine-mapping posterior re-expressed, and the + # FineMappingResult says strictly more. + skip_if_not_installed("VariantAnnotation") + data(twasWeightsExample, envir = environment()) + derived <- is_in( + as.character(twasWeightsExample$method), + .TWB_FINE_MAPPING_METHODS + ) + expect_gt(sum(derived), 0L) + out <- withr::local_tempfile(fileext = ".vcf") + expect_message( + exportToVcf(twasWeightsExample, out), + "exportToVcf: skipping \\d+ fine-mapping-derived" + ) + header <- keep(readLines(out), str_starts, "#CHROM")[[1L]] + samples <- str_split(header, "\t")[[1L]][-seq_len(9L)] + expect_length(samples, sum(!derived)) + # None of the samples names a fine-mapping method. + expect_false(any(str_detect( + samples, + str_c("\\|", .TWB_FINE_MAPPING_METHODS, "$", collapse = "|") + ))) +}) + +test_that("a VCF of only fine-mapping-derived weights is refused", { + skip_if_not_installed("VariantAnnotation") + data(twasWeightsExample, envir = environment()) + fm <- twasWeightsExample[is_in( + as.character(twasWeightsExample$method), + .TWB_FINE_MAPPING_METHODS + )] + out <- withr::local_tempfile(fileext = ".vcf") + expect_error( + suppressMessages(exportToVcf(fm, out)), + "every selected row is fine-mapping-derived" + ) +}) + +test_that("a weights export reads AF off the collection's LD panel", { + # A TwasWeights element records only alleles and a weight, so the panel is + # the only place a frequency can come from. This panel carries no `af`, so + # the column stays absent rather than being faked. + data(ctwasWeightsExample, twasWeightsExample, envir = environment()) + panel <- ldSketch(ctwasWeightsExample) + expect_false(is.null(panel)) + lasso <- twasWeightsExample[ + as.character(twasWeightsExample$method) == "lasso" + ] + expect_equal(nrow(lasso), 1L) + withPanel <- TwasWeights( + studyName = as.character(lasso$study), + context = as.character(lasso$context), + trait = as.character(lasso$trait), + method = as.character(lasso$method), + entry = list(.twrRowParts(lasso, 1L)), + ldSketch = panel + ) + out <- withr::local_tempfile(fileext = ".bed") + exportToBed(withPanel, out) + expect_false(is_in("AF", names(read.delim(out, check.names = FALSE)))) +}) + +test_that("a sumstats collection with no Z publishes no EZ", { + skip_if_not_installed("VariantAnnotation") + gr <- GenomicRanges::GRanges( + "chr1", + IRanges::IRanges(c(100L, 200L), width = 1L) + ) + gr <- S4Vectors::`mcols<-`( + gr, + value = S4Vectors::DataFrame( + SNP = c("chr1:100:A:G", "chr1:200:C:T"), + A1 = c("G", "T"), + A2 = c("A", "C"), + N = c(1000L, 1000L), + BETA = c(0.25, -0.12), + SE = c(0.1, 0.1) + ) + ) + ss <- GwasSumStats( + studyName = "s", + entry = list(gr), + genome = "hg38" + ) + out <- withr::local_tempfile(fileext = ".vcf") + exportToVcf(ss, out) + expect_false(any(str_detect(readLines(out), "ID=EZ,"))) +}) + +# --------------------------------------------------------------------------- +# A joint (matrix-weight) model, and AF from the LD panel +# --------------------------------------------------------------------------- + +# A multi-trait weight matrix on a panel that records allele frequency: the +# two shapes the single-trait fixtures never exercise. `mr_mash` because the +# matrix-weight fixture is a susie fit, which both exporters refuse as +# fine-mapping-derived -- a joint TWAS fit is the case that reaches the +# matrix paths at all. +# @noRd +.pbw_jointWeights <- function(nVariant = 6L) { + data(ctwasWeightsExample, envir = environment()) + panel <- ldSketch(ctwasWeightsExample) + vids <- .grVariantIds(SummarizedExperiment::rowRanges(panel)) + SummarizedExperiment::rowRanges(panel)$af <- seq( + 0.05, + 0.45, + length.out = length(vids) + ) + w <- matrix( + c(0.1, -0.2, 0.3, 0, 0.05, -0.4, 0.2, 0.1, -0.1, 0.3, 0, 0.15), + nrow = 6L, + dimnames = list(NULL, c("tA", "tB")) + ) + TwasWeights( + studyName = "S1", + context = "ctx", + trait = "tA", + method = "mr_mash", + entry = list(twasWeightsRow( + variantIds = vids[seq_len(nVariant)], + weights = w[seq_len(nVariant), , drop = FALSE] + )), + ldSketch = panel + ) +} + +test_that("a joint fit writes one BED row per (variant, trait)", { + tw <- .pbw_jointWeights() + out <- withr::local_tempfile(fileext = ".bed") + exportToBed(tw, out) + bed <- read.delim(out, check.names = FALSE) + # Six variants x two weight columns, and the matrix column label -- not + # the row's own trait -- is what each block is published under. + expect_equal(nrow(bed), 12L) + expect_equal(sort(unique(bed$trait)), c("tA", "tB")) + expect_equal(bed$WGT[bed$trait == "tB"][[1L]], 0.2) +}) + +test_that("AF is published from the LD panel when it records one", { + tw <- .pbw_jointWeights() + out <- withr::local_tempfile(fileext = ".bed") + exportToBed(tw, out) + bed <- read.delim(out, check.names = FALSE) + panel <- ldSketch(tw) + rr <- SummarizedExperiment::rowRanges(panel) + expected <- mcols(rr)$af[match(bed$ID, .grVariantIds(rr))] + expect_equal(bed$AF, expected) +}) + +test_that("a panel with no AF column publishes no AF at all", { + data(twasWeightsExample, envir = environment()) + out <- withr::local_tempfile(fileext = ".bed") + exportToBed(twasWeightsExample[5], out) + # An all-NA column is dropped rather than written as a column of ".", + # so the absence of a frequency is visible in the file's shape. + expect_false(is_in("AF", names(read.delim(out, check.names = FALSE)))) +}) + +test_that("a joint fit's VCF samples carry the weight-column label", { + skip_if_not_installed("VariantAnnotation") + tw <- .pbw_jointWeights() + out <- withr::local_tempfile(fileext = ".vcf") + exportToVcf(tw, out) + hdr <- readLines(out) + chrom <- hdr[str_starts(hdr, "#CHROM")] + # The label is appended to the row's identity, so two weight columns of + # one row stay two distinguishable samples. + expect_match(chrom, "S1\\|ctx\\|tA\\|mr_mash\\|tA") + expect_match(chrom, "S1\\|ctx\\|tA\\|mr_mash\\|tB") +}) + +test_that("a recorded data_source wins over the derived one", { + data(qtlFineMappingExample, envir = environment()) + x <- qtlFineMappingExample + # These rows record where they came from. The collection carries no LD + # panel, so the value the writer would DERIVE is "individual" -- and a + # published file must report what the rows recorded instead of + # re-deriving it from the object it happens to be held in. + recorded <- QtlFineMappingResult( + studyName = as.character(x$study), + context = as.character(x$context), + trait = as.character(x$trait), + method = as.character(x$method), + entry = map(seq_len(nrow(x)), .fmrRowParts, x = x), + dataSource = rep("sumstat", nrow(x)) + ) + expect_equal(.pubDataSource(recorded), "individual") + out <- withr::local_tempfile(fileext = ".bed") + exportToBed(recorded, out) + bed <- read.delim(out, check.names = FALSE) + expect_equal(unique(bed$data_source), "sumstat") +}) + +test_that("columns= appends a hand-built row's own variant columns", { + # `twasWeightsRow()` writes exactly A1 / A2 / weight, which is the whole + # published source set -- so for a constructor-built collection there is + # nothing left for `columns` to add, as ?exportToBed says. A collection + # assembled by hand is the case the argument exists for: its variants + # carry fields pecotmr knows nothing about, and they travel into the file + # AFTER the published schema rather than displacing any of it. + vids <- sprintf("chr1:%d:A:G", 100L * (1:4)) + gr <- .variantIdsToGRanges(vids, "variantIds") + mcols(gr)$weight <- c(0.1, -0.2, 0.3, 0) + mcols(gr)$n_fold <- rep(5L, 4L) + mcols(gr)$qc_note <- c("ok", "ok", "flipped", "ok") + row <- methods::new( + "TwasWeightsRow", + variants = gr, + weights = c(0.1, -0.2, 0.3, 0), + weightStandardized = FALSE + ) + tw <- TwasWeights( + studyName = "S", + context = "c", + trait = "t", + method = "lasso", + entry = list(row) + ) + out <- withr::local_tempfile(fileext = ".bed") + exportToBed(tw, out, columns = "all") + bed <- read.delim(out, check.names = FALSE) + expect_equal(bed$qc_note, c("ok", "ok", "flipped", "ok")) + expect_equal(bed$n_fold, rep(5L, 4L)) + # The published columns still lead, and the weight is published as WGT + # rather than appearing a second time under its source name. + expect_equal(names(bed)[1:4], c("#chr", "start", "end", "ID")) + expect_false(is_in("weight", names(bed))) + # Naming one explicitly works the same way; naming a published one is an + # error rather than a duplicate column. + exportToBed(tw, out, columns = "qc_note") + expect_false(is_in("n_fold", names(read.delim(out, check.names = FALSE)))) + expect_error( + exportToBed(tw, out, columns = "weight"), + "already writes" + ) +}) diff --git a/vignettes/twas-weights.Rmd b/vignettes/twas-weights.Rmd index 8c3a26bc..54fe5be3 100644 --- a/vignettes/twas-weights.Rmd +++ b/vignettes/twas-weights.Rmd @@ -391,7 +391,7 @@ same thing as a long table, one row per (variant, model). ```{r tw-export, eval=FALSE} exportToVcf(tw, "weights.vcf.gz") -exportToVcf(tw, "susie_weights.vcf.gz", method = "susie") # one model +exportToVcf(tw, "lasso_weights.vcf.gz", method = "lasso") # one model exportToBed(tw, "weights.bed.gz") ``` @@ -407,10 +407,12 @@ cv <- cvResult(tw, studyName = "s1", context = "brain", trait = "g1", cv$performance # per-method metric matrix: corr, rsq, adj_rsq, pval, RMSE, MAE ``` -Fine-mapping-derived rows are skipped with a message, because their "weights" -are the fine-mapping posterior re-expressed — export the `FineMappingResult` -instead, which carries the posterior, its standard error and the credible -sets. +Fine-mapping-derived rows (`susie`, `susie_inf`, `susie_ash`, `mvsusie`, +`fsusie`) are skipped with a message by **both** exporters, because their +"weights" are the fine-mapping posterior re-expressed — export the +`FineMappingResult` instead, which carries the posterior, its standard error +and the credible sets. Selecting only such a method, as in +`method = "susie"`, is therefore an error rather than an empty file. `readTwasWeights()` reads either format back into a `TwasWeights`: From 5e02edeac2d4488f452a595f2c6369d071e5b4ff Mon Sep 17 00:00:00 2001 From: Daniel Nachun Date: Thu, 15 Oct 2026 20:38:52 -0700 Subject: [PATCH 3/5] fix a few tesst --- R/resultWriters.R | 17 ++++++++----- tests/testthat/test_resultWriters.R | 38 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/R/resultWriters.R b/R/resultWriters.R index 49f0ac8e..e8b5e6e0 100644 --- a/R/resultWriters.R +++ b/R/resultWriters.R @@ -2170,11 +2170,14 @@ setMethod( } # The CS membership / purity values as a tibble, for binding into a block. +# +# `csCols` is never empty here. The only caller runs downstream of +# `.fmbPrimaryCsLevel()`, which aborts outright for a table carrying no +# `cs_` column, and both columns of every level that IS present come back +# from `.pubCsPair()` unconditionally -- so an empty-`csCols` branch would be +# dead by construction. # @noRd .pubFrameCs <- function(view, csCols) { - if (length(csCols) == 0L) { - return(tibble(.rows = nrow(view))) - } as_tibble( .fmbCsValues(view, csCols), .name_repair = "minimal" @@ -2425,11 +2428,13 @@ setMethod( } # A per-row List column flattened to one string per row. +# +# No absent-column branch, unlike `.twbColumnOr()` above: this is only ever +# asked for `dataType`, which `TwasWeights()` materializes for every row even +# when no row declares one -- and that constructor is the only way to build +# the class, so the column is always there. # @noRd .twbListColumn <- function(x, nm) { - if (!is_in(nm, .tupleColumnNames(x))) { - return(rep(NA_character_, nrow(x))) - } map_chr(as.list(.tupleColumn(x, nm)), .twbScalarChr) } diff --git a/tests/testthat/test_resultWriters.R b/tests/testthat/test_resultWriters.R index f42cd9cc..a263d443 100644 --- a/tests/testthat/test_resultWriters.R +++ b/tests/testthat/test_resultWriters.R @@ -2120,3 +2120,41 @@ test_that("columns= appends a hand-built row's own variant columns", { "already writes" ) }) + +test_that("a weight matrix with no columns publishes a header-only file", { + # A joint fit that retained no conditions leaves a 0-column weight + # matrix, so the row contributes no (variant, trait) block at all. The + # published file is then the schema with no rows -- the same shape an + # empty result takes everywhere else -- rather than a file with no header + # a reader could not parse. + gr <- .variantIdsToGRanges(sprintf("chr1:%d:A:G", 100L * (1:4)), "v") + w <- matrix(numeric(0), nrow = 4L, ncol = 0L) + mcols(gr)$weight <- w + tw <- TwasWeights( + studyName = "S", + context = "c", + trait = "t", + method = "lasso", + entry = list(methods::new( + "TwasWeightsRow", + variants = gr, + weights = w, + weightStandardized = FALSE + )) + ) + expect_length(.twbWeightLabels(mcols(tw[[1L]])$weight), 0L) + expect_null(.twbWeightsTable(tw, NULL, NULL)) + out <- withr::local_tempfile(fileext = ".bed") + exportToBed(tw, out) + lines <- readLines(out) + expect_length(lines, 1L) + expect_equal( + str_split_1(lines[[1L]], "\t")[1:4], + c( + "#chr", + "start", + "end", + "ID" + ) + ) +}) From 284b3ac0ebf592a36a354b72f48ea71e9280c6ae Mon Sep 17 00:00:00 2001 From: Daniel Nachun Date: Fri, 16 Oct 2026 11:33:07 -0700 Subject: [PATCH 4/5] add more coverage --- tests/testthat/test_fineMappingRow.R | 142 ++++++++++++++++ tests/testthat/test_fineMappingWrappers.R | 187 ++++++++++++++++++++++ 2 files changed, 329 insertions(+) diff --git a/tests/testthat/test_fineMappingRow.R b/tests/testthat/test_fineMappingRow.R index 4ccd746a..568d81d0 100644 --- a/tests/testthat/test_fineMappingRow.R +++ b/tests/testthat/test_fineMappingRow.R @@ -1905,3 +1905,145 @@ test_that("an empty credible band still declares halfwidth", { expect_true(is_in("halfwidth", names(.emptyCredibleBand()))) expect_identical(nrow(.emptyCredibleBand()), 0L) }) + +# --------------------------------------------------------------------------- +# fsusie trait-effect edge paths, and the two projections either side of them +# --------------------------------------------------------------------------- + +# The minimum a fit needs to pass `.isFsusieFit()` and carry trait positions: +# one curve per effect over `outing_grid`, plus the two fields that identify a +# susiF fit. A `NULL` element of `fitted_func` is what an effect that produced +# no curve looks like. +# @noRd +.fte_stubFit <- function(curves, pos) { + list( + fitted_func = curves, + outing_grid = c(1, 2, 3), + fitted_wc2 = 1, + alpha = 1, + trait_positions = pos + ) +} + +# @noRd +.fte_row <- function(fit) { + fineMappingRow( + variantIds = "chr1:100:A:G", + susieFit = fit, + topLoci = data.frame( + variant_id = "chr1:100:A:G", + pip = 0.5, + stringsAsFactors = FALSE + ) + ) +} + +test_that("an effect with no fitted curve contributes no trait effects", { + res <- QtlFineMappingResult( + studyName = "s", + context = "brain", + trait = "g", + method = "fsusie", + entry = list(.fte_row(.fte_stubFit(list(NULL, NULL), 1.5))), + jointTraits = "g1" + ) + te <- fsusieTraitEffects(res) + # Every effect skipped, so there is nothing to report -- but the empty + # table keeps the schema, so a caller can bind or filter it exactly as it + # would a populated one. + expect_equal(nrow(te), 0L) + expect_true(all( + c("cs", "chrom", "jointTrait", "traitPos", "effect") %in% names(te) + )) +}) + +test_that("a fit with no jointTraits names its curves after the row", { + res <- QtlFineMappingResult( + studyName = "s", + context = "brain", + trait = "g", + method = "fsusie", + entry = list(.fte_row(.fte_stubFit(list(c(1, 2, 3)), 1.5))) + ) + te <- fsusieTraitEffects(res) + # The joint member names ride on `jointTraits`; a fit over a single trait + # has none, so the row's own trait is the name of its curve. + expect_equal(nrow(te), 1L) + expect_equal(te$jointTrait, "g") + # The curve is linear on the grid, so the midpoint interpolates exactly. + expect_equal(te$effect, 1.5) +}) + +test_that("a GWAS fsusie fit has no trait axis to name its curves", { + res <- GwasFineMappingResult( + studyName = "s", + method = "fsusie", + entry = list(.fte_row(.fte_stubFit(list(c(1, 2, 3)), 1.5))) + ) + # A GWAS result is not keyed by trait at all, so there is no name to fall + # back to and the fit's one trait position has nothing to pair with. + expect_error( + fsusieTraitEffects(res), + "1 trait position\\(s\\) but 0 trait name\\(s\\)" + ) +}) + +test_that("a credible-set label matching no fit effect claims none", { + tl <- data.frame( + variant_id = "chr1:100:A:G", + cs_95 = "fsusie_1", + stringsAsFactors = FALSE + ) + # The label's variants are not any effect's membership, so the label has + # no effect to attach to -- NA rather than silently taking the first. + expect_true(is.na(.fsusieFirstMatchingEffect( + "fsusie_1", + fitSets = list("chr1:200:A:G", "chr1:300:A:G"), + topLoci = tl + ))) + expect_equal( + .fsusieFirstMatchingEffect( + "fsusie_1", + fitSets = list("chr1:300:A:G", "chr1:100:A:G"), + topLoci = tl + ), + 2L + ) +}) + +test_that("a NULL posterior table projects to an empty GRanges", { + gr <- .fmeTopLociGRanges(NULL) + expect_s4_class(gr, "GRanges") + expect_length(gr, 0L) +}) + +test_that("a fit whose prior variances all vanished labels nothing", { + vids <- str_c("chr1:", (1:4) * 100, ":A:G") + p <- length(vids) + L <- 2L + set.seed(3L) + lbf <- matrix(stats::rnorm(L * p), nrow = L, ncol = p) + colnames(lbf) <- vids + alpha <- lbfToAlpha(lbf) + pip <- as.numeric(1 - apply(1 - alpha, 2, prod)) + entry <- fineMappingRow( + variantIds = vids, + susieFit = list( + pip = pip, + alpha = alpha, + lbf_variable = lbf, + # Every prior variance driven to zero: no effect clears the + # tolerance, so there is no credible set left to label with. + V = rep(0, L) + ), + topLoci = data.frame( + variant_id = vids, + pip = pip, + method = "susie", + cs_95 = c("susie_1", rep("susie_0", p - 1L)), + stringsAsFactors = FALSE + ) + ) + adj <- adjustPips(entry, vids[1:3]) + expect_equal(.fmrRowTopLoci(adj, raw = TRUE)$cs_95, rep("susie_0", 3L)) +}) diff --git a/tests/testthat/test_fineMappingWrappers.R b/tests/testthat/test_fineMappingWrappers.R index 4b2ee7b2..08be938e 100644 --- a/tests/testthat/test_fineMappingWrappers.R +++ b/tests/testthat/test_fineMappingWrappers.R @@ -760,6 +760,11 @@ if (!exists(".makeUnivariateData", inherits = FALSE)) { otherQuantities = NULL, region = NULL ) { + # `signalCutoff` has to go to buildTopLoci's OWN formal: that is the one + # the PIP filter reads, and it is what the pipeline unpacks from + # CredibleSetParam before calling here. Setting it only on the param -- + # which this helper used to do -- left every caller's cutoff inert, so a + # test asking for 0.5 was silently testing no filtering at all. buildTopLoci( fit = inp$fit, csTables = inp$cs_tables, @@ -767,6 +772,7 @@ if (!exists(".makeUnivariateData", inherits = FALSE)) { sumstats = sumstats, af = af, method = method, + signalCutoff = signalCutoff, dataY = inp$data_y, otherQuantities = otherQuantities, region = region, @@ -5277,3 +5283,184 @@ test_that("krigingOutlierQc forwards methodArgs to susieR::kriging_rss", { # Fixtures moved here with the tests that use them. # Fixtures moved here with the tests that use them. + +# --------------------------------------------------------------------------- +# buildTopLoci: the PIP filter and the z-only source +# --------------------------------------------------------------------------- + +test_that("signalCutoff drops rows at or below the cutoff", { + vids <- c("chr1:100:A:G", "chr1:200:C:T", "chr1:300:G:T") + noCs <- list("0.95" = list(), "0.7" = list(), "0.5" = list()) + inp <- .fakeFitAndCs(vids, noCs, pip = c(0.02, 0.4, 0.95)) + # 0 (and NULL) keeps everything: the cutoff is a publication filter, not + # part of the fit. + expect_equal(nrow(.runBuildTopLoci(inp, signalCutoff = 0)), 3L) + # Strictly greater than the cutoff survives, so 0.4 goes at 0.5 and + # stays at 0.3. + expect_equal( + .runBuildTopLoci(inp, signalCutoff = 0.3)$variant_id, + vids[2:3] + ) + expect_equal( + .runBuildTopLoci(inp, signalCutoff = 0.5)$variant_id, + vids[[3L]] + ) +}) + +test_that("a z-only source still reports an effect size", { + vids <- c("chr1:100:A:G", "chr1:200:C:T") + noCs <- list("0.95" = list(), "0.7" = list(), "0.5" = list()) + inp <- .fakeFitAndCs(vids, noCs, pip = c(0.9, 0.8)) + af <- c(0.2, 0.35) + n <- 1000 + z <- c(3.5, -2.1) + out <- buildTopLoci( + fit = inp$fit, + csTables = inp$cs_tables, + variantNames = inp$variantNames, + # A GWAS that published only z, which is routine: with af and N the + # model inverts to an effect size, and leaving the columns NA would + # lose the study for every effect-size consumer downstream. + sumstats = list(z = z), + af = af, + n = n, + method = "susie", + dataY = inp$data_y + ) + se <- 1 / sqrt(2 * af * (1 - af) * (n + z^2)) + expect_equal(out$marginal_se, se) + expect_equal(out$marginal_beta, z * se) + # The z was the source's own, which is what the published EZ field means. + expect_true(all(out$marginal_z_supplied)) +}) + +test_that("a z-only source with no af or N leaves the effect unfilled", { + vids <- c("chr1:100:A:G", "chr1:200:C:T") + noCs <- list("0.95" = list(), "0.7" = list(), "0.5" = list()) + inp <- .fakeFitAndCs(vids, noCs, pip = c(0.9, 0.8)) + out <- buildTopLoci( + fit = inp$fit, + csTables = inp$cs_tables, + variantNames = inp$variantNames, + sumstats = list(z = c(3.5, -2.1)), + method = "susie", + dataY = inp$data_y + ) + # Without the frequency and sample size the model cannot be inverted, so + # the columns stay empty rather than being filled with a guess. + expect_true(all(is.na(out$marginal_beta))) + expect_true(all(is.na(out$marginal_se))) + expect_equal(out$marginal_z, c(3.5, -2.1)) +}) + +test_that("the RSS weight extractors refuse to fine-map themselves", { + # These read weights off an existing fit; without one there is nothing to + # extract, and the message says where the fit comes from. + expect_error( + susieRssWeights(stat = list(z = 1), LD = diag(2)), + "susieRssWeights: no 'susie' fit supplied" + ) + expect_error( + susieInfRssWeights(stat = list(z = 1), LD = diag(2)), + "susieInfRssWeights: no 'susieInf' fit supplied" + ) + expect_error( + susieAshRssWeights(stat = list(z = 1), LD = diag(2)), + "susieAshRssWeights: no 'susieAsh' fit supplied" + ) +}) + +# --------------------------------------------------------------------------- +# Degenerate inputs the fine-mapping helpers have to absorb +# --------------------------------------------------------------------------- + +test_that("an out-of-range credible-set position yields no within-CS PIP", { + # The primary CS position points at effect 5 of a one-effect fit, so + # there is no row of alpha to read the within-CS share from. + expect_equal( + .ffcWithinPip( + alpha = matrix(0.5, nrow = 1L, ncol = 3L), + primaryCsPos = c(1L, 1L, 1L), + effectOf = 5L, + nV = 3L, + hasAlpha = TRUE + ), + rep(NA_real_, 3L) + ) +}) + +test_that("a credible set with no members assigns no variant", { + # The coverage matches and the set list is non-empty, but the set itself + # holds no variant, so every variant stays unassigned (0). + expect_equal( + .fmCsIdxAtCoverage( + targetCov = 0.95, + coverageValues = 0.95, + csTables = list(list(sets = list(cs = list(L1 = integer(0))))), + nV = 3L + ), + integer(3L) + ) +}) + +test_that("mu and mu2 of different shapes yield no conditional lfsr", { + # mu2 is the second moment of mu, so a shape disagreement means the two + # describe different fits -- no lfsr rather than a recycled one. + expect_null(.btlUnivariateClfsr(list( + mu = matrix(0, 2L, 3L), + mu2 = matrix(1, 2L, 4L) + ))) +}) + +test_that("an empty window parses to no coordinates", { + expect_equal( + .parseGrange(GenomicRanges::GRanges()), + c(start = NA_integer_, end = NA_integer_) + ) +}) + +test_that("merging two already-merged blocks changes nothing", { + parent <- list(a = "a", b = "b") + once <- .ufUnion("a", "b", parent) + expect_equal(once[["b"]], "a") + # Both now have the same root, so the second merge is a no-op rather than + # re-pointing a root at itself. + expect_identical(.ufUnion("a", "b", once), once) + expect_identical(.ufUnion("b", "a", once), once) +}) + +test_that("a purity calculation that fails leaves the sets untouched", { + skip_if_not_installed("fsusieR") + sets <- list(cs = list(L1 = 1:2)) + # cal_purity needs the genotype matrix the sets index into; given + # something else it errors, and purity is simply absent rather than the + # credible sets being lost with it. + expect_identical(.fsusieSetsWithPurity(sets, "not-a-matrix"), sets) +}) + +test_that("a frequency vector that does not match the variants is not used", { + # af has one value for two variants. Recycling it would attach one + # variant's frequency to the other and derive an effect size from it, so + # the fill-in declines instead and the columns stay empty. + out <- .btlFillFromZ( + beta = rep(NA_real_, 2L), + se = rep(NA_real_, 2L), + z = c(1.5, -2.5), + af = 0.2, + n = 1000, + nV = 2L + ) + expect_true(all(is.na(out$beta))) + expect_true(all(is.na(out$se))) + # A single N for every variant IS allowed -- one sample size can describe + # the whole study, where one frequency cannot. + ok <- .btlFillFromZ( + beta = rep(NA_real_, 2L), + se = rep(NA_real_, 2L), + z = c(1.5, -2.5), + af = c(0.2, 0.3), + n = 1000, + nV = 2L + ) + expect_false(anyNA(ok$beta)) +}) From d2d38a840781957704bb567128f078a001863fda Mon Sep 17 00:00:00 2001 From: Daniel Nachun Date: Fri, 16 Oct 2026 11:58:25 -0700 Subject: [PATCH 5/5] test cleanup --- R/fineMappingWrappers.R | 18 +++++--------- tests/testthat/test_fineMappingWrappers.R | 29 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/R/fineMappingWrappers.R b/R/fineMappingWrappers.R index e0aaf227..9c01a6f9 100644 --- a/R/fineMappingWrappers.R +++ b/R/fineMappingWrappers.R @@ -759,22 +759,16 @@ postprocessFinemappingFit.susiF <- function( ) } +# No try_fetch around the normalisation: `normalizeVariantId()` refuses only a +# non-CHARACTER `ids`, and all three sources here -- names(), colnames() and +# str_c() -- are character or NULL by construction. For character input it +# never throws; an id it cannot parse (an rsID, say) is returned unchanged, +# which is the behaviour the fallback was there to approximate. extractVariantNames <- function(fit) { variantNames <- names(fit$pip) %||% colnames(fit$alpha) %||% str_c("variant_", seq_along(fit$pip)) - try_fetch( - normalizeVariantId(variantNames), - error = function(cnd) { - msg <- glue( - "variant ids could not be normalised; using them as given. ", - "Downstream joins that assume the canonical form may not ", - "match." - ) - warn(msg, parent = cnd) - variantNames - } - ) + normalizeVariantId(variantNames) } extractSumstats <- function( diff --git a/tests/testthat/test_fineMappingWrappers.R b/tests/testthat/test_fineMappingWrappers.R index 08be938e..0300aef2 100644 --- a/tests/testthat/test_fineMappingWrappers.R +++ b/tests/testthat/test_fineMappingWrappers.R @@ -5464,3 +5464,32 @@ test_that("a frequency vector that does not match the variants is not used", { ) expect_false(anyNA(ok$beta)) }) + +test_that("an unnormalisable id set is returned as given", { + # `normalizeVariantId()` leaves an id it cannot parse alone rather than + # emitting "chrNA:..." garbage, so a fit named by rsID keeps its names + # and only the parseable ones are rewritten. + out <- extractVariantNames(list( + pip = set_names( + c(0.1, 0.2, 0.3), + c( + "rs12345", + "chr1_200_C_T", + "not an id" + ) + ) + )) + expect_equal(out, c("rs12345", "chr1:200:C:T", "not an id")) +}) + +test_that("a screen whose marginal z cannot be computed falls back", { + # univariate_regression refuses a non-finite X, and the screen is + # advisory: a block it cannot score is kept (the fineMapping default) + # rather than silently discarded. + X <- matrix(c(Inf, stats::rnorm(49L)), nrow = 10L, ncol = 5L) + y <- stats::rnorm(10L) + scr <- list(metric = "absZ", cutoff = 2) + expect_true(.fmSerScreen(X, y, scr, fallback = TRUE)) + # colocboost passes FALSE: it drops an outcome it cannot screen. + expect_false(.fmSerScreen(X, y, scr, fallback = FALSE)) +})

B;pNGt1rG-nu3W$U8S((JW4+{Th0^SBk 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