From b0328f64062b7ade63748520cc905b15421f51a3 Mon Sep 17 00:00:00 2001 From: Jenny Anak Empawi Date: Wed, 28 Oct 2026 12:23:26 -0400 Subject: [PATCH 1/3] loadGenotypeRegion: apply keepIndel and keepVariantsPath to the variant index before reading dosages, so only the requested variants are loaded --- R/genotypeIo.R | 46 +++++++++++++------------------- tests/testthat/test_genotypeIo.R | 21 +++++++++++++++ 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/R/genotypeIo.R b/R/genotypeIo.R index 9a419fb3f..a2845f2c9 100644 --- a/R/genotypeIo.R +++ b/R/genotypeIo.R @@ -1656,6 +1656,9 @@ loadGenotypeRegion <- function( ) variants <- .rangesSnpInfo(SummarizedExperiment::rowRanges(panel)) snpIdx <- .loadGenoSnpIdx(variants, which) + # Apply the indel and whitelist filters to the variant index before any + # dosage is read, so only the requested variants are loaded. + snpIdx <- .loadGenoKeepIdx(variants, snpIdx, keepIndel, keepVariantsPath) # Samples x variants matrix (pecotmr convention), its columns named by # the file's own variant ids; callers handle missing. extracted <- list( @@ -1668,9 +1671,8 @@ loadGenotypeRegion <- function( .snpInfoToVariantInfo(slice(variants, snpIdx)) ) ) - filtered <- .loadGenoPostFilter(extracted, keepIndel, keepVariantsPath) result <- .loadGenoInvertStochastic( - filtered, + extracted, genotype, stochasticMetaPath, stochasticMetaFormat @@ -1715,35 +1717,23 @@ loadGenotypeRegion <- function( ) } -# Apply the indel-drop and variant-whitelist post-filters to (X, variant_info). +# Restrict the SNP indices to the variants kept by the indel and whitelist +# filters. Runs before the dosage read, so filtered-out variants are never +# loaded. # @noRd -.loadGenoPostFilter <- function(result, keepIndel, keepVariantsPath) { - snpOnly <- if (keepIndel) { - result - } else { - .loadGenoSubsetVariants( - result, - isSnpAlleles(result$variant_info$A1, result$variant_info$A2) - ) +.loadGenoKeepIdx <- function(variants, snpIdx, keepIndel, keepVariantsPath) { + if (keepIndel && is.null(keepVariantsPath)) { + return(snpIdx) } - if (is.null(keepVariantsPath)) { - return(snpOnly) + info <- .snpInfoToVariantInfo(slice(variants, snpIdx)) + keep <- rep(TRUE, length(snpIdx)) + if (!keepIndel) { + keep <- keep & isSnpAlleles(info$A1, info$A2) } - .loadGenoSubsetVariants( - snpOnly, - matchVariantsToKeep(snpOnly$variant_info, keepVariantsPath) - ) -} - -# Subset a loaded (X, variant_info) pair along the variant axis, keeping the -# dosage columns and the metadata rows in step. -# @noRd -.loadGenoSubsetVariants <- function(result, keep) { - list_assign( - result, - X = result$X[, keep, drop = FALSE], - variant_info = result$variant_info[keep, , drop = FALSE] - ) + if (!is.null(keepVariantsPath)) { + keep <- keep & matchVariantsToKeep(info, keepVariantsPath) + } + snpIdx[keep] } # Restore a stochastic sketch's original scale from its metadata sidecar. diff --git a/tests/testthat/test_genotypeIo.R b/tests/testthat/test_genotypeIo.R index 61b63cefe..1cd53e1c0 100644 --- a/tests/testthat/test_genotypeIo.R +++ b/tests/testthat/test_genotypeIo.R @@ -624,6 +624,27 @@ test_that("loadGenotypeRegion filters by keep_variants_path for plink2", { expect_equal(ncol(result), 3L) }) +test_that("loadGenotypeRegion with keepVariantsPath reads the same dosages as a full load", { + skip_if_not_installed("pgenlibr") + td <- test_path("test_data") + full <- loadGenotypeRegion( + file.path(td, "test_variants"), + returnVariantInfo = TRUE + ) + keep_file <- tempfile(fileext = ".tsv") + on.exit(unlink(keep_file), add = TRUE) + keep_df <- full$variant_info[c(2, 5, 9), c("chrom", "pos", "A2", "A1")] + vroom::vroom_write(keep_df, keep_file, delim = "\t") + kept <- loadGenotypeRegion( + file.path(td, "test_variants"), + keepVariantsPath = keep_file, + returnVariantInfo = TRUE + ) + expect_equal(kept$X, full$X[, c(2, 5, 9), drop = FALSE]) + expect_equal(kept$variant_info, full$variant_info[c(2, 5, 9), , drop = FALSE], + ignore_attr = TRUE) +}) + test_that("loadGenotypeRegion attaches afreq info for plink2", { skip_if_not_installed("pgenlibr") td <- test_path("test_data") From d56f5f726368168ead3cbd5cdb45da0f429e8ab7 Mon Sep 17 00:00:00 2001 From: Daniel Nachun Date: Wed, 4 Nov 2026 01:58:28 -0800 Subject: [PATCH 2/3] fix additional bugs in CTWAS --- DESCRIPTION | 1 - NAMESPACE | 12 +- R/AllClasses.R | 11 +- R/AllGenerics.R | 135 +- R/ColocBoostResult.R | 12 +- R/CtwasResult.R | 38 +- R/GenotypeHandle.R | 13 +- R/GwasFineMappingResult.R | 4 +- R/H2Estimate.R | 28 +- R/QtlDataset.R | 764 ++++++++- R/QtlFineMappingResult.R | 4 +- R/colocPipeline.R | 147 +- R/colocboostPipeline.R | 121 +- R/cpp11.R | 4 +- R/crossValidation.R | 11 +- R/ctwasPipeline.R | 1075 +++++++------ R/ctwasWrapper.R | 80 + R/enloc.R | 191 --- R/exampleData.R | 6 +- R/fineMappingPipeline.R | 614 ++++---- R/fineMappingRow.R | 14 +- R/fineMappingWrappers.R | 12 +- R/genotypeIo.R | 231 ++- R/h2Annotations.R | 36 +- R/h2EstimationWrappers.R | 538 +++++-- R/jointEngine.R | 571 ++++--- R/jointSpecification.R | 434 ++--- R/ld.R | 27 +- R/manifestLoaders.R | 53 +- R/qtlEnrichmentPipeline.R | 16 +- R/qtlSumStats.R | 10 +- R/regularizedRegressionWrappers.R | 273 ++-- R/resultReaders.R | 2 +- R/resultWriters.R | 2 +- R/sldscWrapper.R | 5 +- R/sumstatsQc.R | 28 +- R/tupleSelectors.R | 101 ++ R/twasWeights.R | 126 +- R/twasWeightsPipeline.R | 715 +++++---- R/twasWeightsRow.R | 9 +- R/variantId.R | 79 +- _pkgdown.yml | 9 +- data/ctwasEstExample.rda | Bin 105532 -> 106708 bytes data/ctwasFinemapExample.rda | Bin 106348 -> 109632 bytes data/ctwasInputsExample.rda | Bin 104956 -> 106132 bytes data/ctwasWeightsExample.rda | Bin 6624 -> 6636 bytes data/h2EstimateExample.rda | Bin 1092 -> 1104 bytes data/twasWeightsExample.rda | Bin 8940 -> 8944 bytes inst/COPYRIGHTS | 3 - inst/scripts/build_ctwas_examples.R | 39 +- man/BoundaryMergeParam.Rd | 15 +- man/CtwasPriorParam.Rd | 14 +- man/GenotypeFilterParam.Rd | 11 +- man/GwasFineMappingResult.Rd | 4 +- man/H2Estimate-class.Rd | 3 + man/PhenotypeFilterParam.Rd | 6 +- man/QtlDataset.Rd | 14 +- man/QtlEnrichmentParam.Rd | 7 +- man/QtlFineMappingResult.Rd | 4 +- man/QtlSumStats.Rd | 10 +- man/SumstatsFilterParam.Rd | 5 +- man/TwasWeightsRow-class.Rd | 2 +- man/TwasWeightsRow.Rd | 3 +- man/asCtwasResult.Rd | 2 +- man/assembleCtwasInputs.Rd | 24 +- man/bayesARssWeights.Rd | 6 +- man/bayesAlphabetRssWeights.Rd | 4 +- man/bayesBRssWeights.Rd | 6 +- man/bayesCRssWeights.Rd | 6 +- man/bayesLRssWeights.Rd | 6 +- man/bayesNRssWeights.Rd | 6 +- man/bayesRRssWeights.Rd | 6 +- man/colocPipeline.Rd | 4 +- man/colocboostPipeline.Rd | 9 +- man/ctwasCombinedPips.Rd | 37 + man/ctwasGroupPriors.Rd | 16 +- man/ctwasPipeline.Rd | 50 +- man/ctwasWeightsExample.Rd | 3 +- man/dataType.Rd | 64 + man/dprWeights.Rd | 13 +- man/enetRssWeights.Rd | 4 +- man/enetWeights.Rd | 8 +- man/enlocClusterProbabilities.Rd | 54 - man/estCtwasGroupPriors.Rd | 6 +- man/estimateH2.Rd | 70 +- man/filterVariantsByLdReference.Rd | 4 +- man/fineMappingAnnotations.Rd | 11 +- man/fineMappingPipeline.Rd | 21 +- man/genotypes.Rd | 4 +- man/glmnetWeights.Rd | 8 +- man/l0learnRssWeights.Rd | 4 +- man/lassoWeights.Rd | 8 +- man/lassosumRssWeights.Rd | 25 +- man/ldpred2AutoWeights.Rd | 4 +- man/ldpred2GridWeights.Rd | 4 +- man/learnTwasWeights.Rd | 19 +- man/loadGenotypeRegion.Rd | 11 +- man/loadQtlDatasetFromManifest.Rd | 4 +- man/loadQtlSumStatsFromManifest.Rd | 4 +- man/mcpRssWeights.Rd | 4 +- man/mcpWeights.Rd | 6 +- man/mergeCtwasBoundaryRegions.Rd | 14 +- man/mrashWeights.Rd | 8 +- man/ncvregWeights.Rd | 6 +- man/prsCsWeights.Rd | 4 +- man/qtlEnrichment.Rd | 7 +- man/readFineMappingResult.Rd | 2 +- man/readTwasWeights.Rd | 2 +- man/ridgeRssWeights.Rd | 4 +- man/ridgeWeights.Rd | 8 +- man/scadRssWeights.Rd | 4 +- man/scadWeights.Rd | 6 +- man/sdprWeights.Rd | 4 +- man/stratifiedHeritabilityEnrichment.Rd | 7 +- man/summaryStatsQc.Rd | 5 +- man/twasWeightsCv.Rd | 4 +- man/twasWeightsExample.Rd | 3 +- man/twasWeightsPipeline.Rd | 52 +- man/weightsDataType.Rd | 50 - src/bgen.cpp | 59 +- src/cpp11.cpp | 8 +- tests/testthat/helper-cfgForwarding.R | 4 +- tests/testthat/helper-fitSamples.R | 117 ++ tests/testthat/helper-h2Classes.R | 3 +- tests/testthat/helper-savedObjects.R | 2 +- tests/testthat/test_AllClasses.R | 42 +- tests/testthat/test_ColocBoostResult.R | 18 + tests/testthat/test_CtwasResult.R | 13 +- tests/testthat/test_GenotypeDosageSeed.R | 44 + tests/testthat/test_H2Estimate.R | 19 + tests/testthat/test_MethodOptions.R | 8 +- tests/testthat/test_QtlDataset.R | 614 +++++++- tests/testthat/test_causalInferencePipeline.R | 4 +- tests/testthat/test_colocPipeline.R | 182 ++- tests/testthat/test_colocboostPipeline.R | 163 +- tests/testthat/test_crossValidation.R | 16 + tests/testthat/test_ctwasPipeline.R | 1402 +++++++++++++---- tests/testthat/test_ctwasWrapper.R | 71 + tests/testthat/test_enloc.R | 121 -- tests/testthat/test_fineMappingPipeline.R | 440 +++++- tests/testthat/test_fineMappingRow.R | 21 + tests/testthat/test_fineMappingWrappers.R | 30 +- tests/testthat/test_genotypeHandle.R | 18 +- tests/testthat/test_genotypeIo.R | 291 +++- tests/testthat/test_h2Annotations.R | 53 + tests/testthat/test_h2EstimationWrappers.R | 527 ++++++- tests/testthat/test_jointEngine.R | 385 +++-- tests/testthat/test_jointSpecification.R | 414 ++++- tests/testthat/test_ld.R | 34 +- tests/testthat/test_manifestLoaders.R | 84 + tests/testthat/test_qtlEnrichmentPipeline.R | 8 +- tests/testthat/test_qtlSumStats.R | 32 + .../test_regularizedRegressionWrappers.R | 172 +- tests/testthat/test_resultWriters.R | 2 +- tests/testthat/test_rrBayesAlphabet.R | 14 +- tests/testthat/test_rrDpr.R | 14 +- tests/testthat/test_rrGlmnet.R | 27 +- tests/testthat/test_rrLassosum.R | 2 +- tests/testthat/test_rrLdpred2.R | 6 +- tests/testthat/test_rrMrash.R | 12 +- tests/testthat/test_rrNcvreg.R | 6 + tests/testthat/test_rrPenalizedRss.R | 8 +- tests/testthat/test_sumstatsQc.R | 28 +- tests/testthat/test_tupleSelectors.R | 96 ++ tests/testthat/test_twasWeights.R | 60 +- tests/testthat/test_twasWeightsPipeline.R | 733 ++++++++- tests/testthat/test_twasWeightsRow.R | 2 +- tests/testthat/test_variantId.R | 63 + vignettes/coloc-pipeline.Rmd | 26 +- vignettes/constructing-qtl-datasets.Rmd | 1 + vignettes/constructing-sumstats.Rmd | 1 + vignettes/ctwas-pipeline.Rmd | 82 +- vignettes/heritability.Rmd | 46 +- vignettes/twas-weights.Rmd | 15 +- vignettes/xqtl-enrichment.Rmd | 7 +- 175 files changed, 10629 insertions(+), 3478 deletions(-) delete mode 100644 R/enloc.R create mode 100644 man/ctwasCombinedPips.Rd create mode 100644 man/dataType.Rd delete mode 100644 man/enlocClusterProbabilities.Rd delete mode 100644 man/weightsDataType.Rd create mode 100644 tests/testthat/helper-fitSamples.R delete mode 100644 tests/testthat/test_enloc.R diff --git a/DESCRIPTION b/DESCRIPTION index 78677a385..ca9688a0b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -165,7 +165,6 @@ Collate: 'crossValidation.R' 'ctwasPipeline.R' 'ctwasWrapper.R' - 'enloc.R' 'exampleData.R' 'fineMappingRow.R' 'fineMappingWrappers.R' diff --git a/NAMESPACE b/NAMESPACE index e25866c44..bd60e583b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -191,11 +191,13 @@ export(credibleSetParam) export(credibleSetSummary) export(credibleSets) export(csCoverage) +export(ctwasCombinedPips) export(ctwasGroupPriors) export(ctwasPipeline) export(ctwasPosteriors) export(cvFits) export(cvResult) +export(dataType) export(dentist) export(dentistSingleWindow) export(dprAdaptiveGibbsWeights) @@ -207,7 +209,6 @@ export(eigenList) export(enetRssWeights) export(enetWeights) export(enforceDesignFullRank) -export(enlocClusterProbabilities) export(ensembleWeights) export(estCtwasGroupPriors) export(estimateH2) @@ -400,7 +401,6 @@ export(varY) export(variantIds) export(waldTestPval) export(weightStandardized) -export(weightsDataType) export(writeBed) export(writeGwasVcf) export(z) @@ -501,6 +501,7 @@ exportMethods(ctwasGroupPriors) exportMethods(ctwasPosteriors) exportMethods(cvFits) exportMethods(cvResult) +exportMethods(dataType) exportMethods(eigenList) exportMethods(endoapply) exportMethods(estimateH2) @@ -581,7 +582,6 @@ exportMethods(varY) exportMethods(variantIds) exportMethods(weightStandardized) exportMethods(weights) -exportMethods(weightsDataType) exportMethods(writeBed) exportMethods(writeGwasVcf) exportMethods(z) @@ -735,17 +735,20 @@ importFrom(dplyr, filter, full_join, group_by, + group_split, if_else, inner_join, left_join, matches, mutate, n_distinct, + na_if, pull, rename, row_number, select, slice, + slice_max, summarise ) importFrom(glue,glue) @@ -839,9 +842,9 @@ importFrom(stats, na.omit, optim, optimize, + pbeta, pcauchy, pchisq, - pf, pnorm, predict, pt, @@ -908,6 +911,7 @@ importFrom(vctrs, vec_chop, vec_duplicate_detect, vec_group_id, + vec_group_loc, vec_match, vec_unique_loc ) diff --git a/R/AllClasses.R b/R/AllClasses.R index 2cf21c934..16e218918 100644 --- a/R/AllClasses.R +++ b/R/AllClasses.R @@ -1361,14 +1361,10 @@ setMethod( } # TRUE when an entry shares at least one variant with `keepVariants`, matched -# the same (chrom, pos, allele) way adjustPips() itself matches them. +# the same way adjustPips() itself matches them. # @noRd .fmrEntryOverlaps <- function(entry, keepVariants) { - matched <- matchVariants( - .fmrPartsVariantIds(entry), - as.character(keepVariants) - ) - length(matched$idxA) > 0L + any(.keepIdsNamed(.fmrPartsVariantIds(entry), as.character(keepVariants))) } # ---- Trimming a collection to what its role reads --------------------------- @@ -1835,7 +1831,8 @@ setMethod("fsusieAffectedRegions", "FineMappingResultBase", function(x) { if (length(grs) == 0L) { return(GenomicRanges::GRanges()) } - .withGenome(exec(c, !!!grs), .oneBuild(x)) + # Each row knows only its own chromosome, which c() warns of. + .withGenome(suppressWarnings(exec(c, !!!grs)), .oneBuild(x)) }) #' @rdname topLoci diff --git a/R/AllGenerics.R b/R/AllGenerics.R index 45d66a9e4..b77dfb81e 100644 --- a/R/AllGenerics.R +++ b/R/AllGenerics.R @@ -32,25 +32,49 @@ NULL #' one estimate per selected (study, context, trait) row, each from the #' row's own statistics and LD reference. #' @details Each per-trait method is written from its authors' published -#' estimator and reports what they report: +#' estimator and reports what they report. All three read a row's \code{z} +#' as the t statistic of a marginal regression, the correlation being +#' \eqn{r = z / \sqrt{z^2 + n - 2}}: \eqn{z / \sqrt{n}}, which HDL-L's and +#' FastHer's own code read instead, overstates \eqn{r^2} by a factor of +#' \eqn{1 / (1 - r^2)}, about 1.4 for a strong QTL at \eqn{r^2 = 0.3}. #' \describe{ #' \item{\code{"lava"}}{(default) LAVA's univariate estimator (Werme et -#' al. 2022): the adjusted \eqn{R^2} of the trait on the standardized -#' LD components that hold 99\% of the LD variance (at least 2, at -#' most 0.75 times the sample size), with the F test of no -#' heritability. The estimate is scaled by \eqn{(N_{ref} - K - 1) / -#' (N_{ref} - 1)} for a reference of \eqn{N_{ref}} samples and -#' \eqn{K} components, a correction LAVA describes but leaves off. +#' al. 2022), on the \eqn{K} standardized LD components that hold 99\% +#' of the LD variance (at least 2, at most 0.75 times the sample size). +#' Whether LD is the study's own is not a yes or no: how far a reference +#' of \eqn{N} samples, holding a share \eqn{\rho} of the \eqn{n} study +#' samples (\code{referenceOverlap}), lies from the study's LD in units +#' of its sampling noise is \eqn{\phi = 1 + (1 - 2\rho) n / N}: 0 for +#' the study's own samples, about 1 for a panel of thousands. The +#' estimate is \eqn{(\delta'\delta - m) / (1 + m (\phi - 1))} with +#' \eqn{m = K / (n - 1)}, which at \eqn{\phi = 0} is exactly LAVA's +#' adjusted \eqn{R^2} of the trait on the components. LAVA's own noise +#' term holds only there: with a panel of 3000 on real genotypes it gave +#' 0.55-0.63 at a true 0.5 with \eqn{K / n} of 0.1-0.2, 0.69 at 0.3, and +#' \code{NA} for 70\% of traits at 0.6; this estimate stayed at +#' 0.47-0.52 across references of every kind. Assuming \eqn{\rho = 0} +#' costs about \eqn{2 \rho K / N} of the estimate, under 10\% for a +#' whole cis window with a panel of thousands holding all the study's +#' samples, but LD from the study's own samples at that default comes out +#' at half or less, hence a warning when \eqn{N \le 2 n}. Divergence +#' between populations of one ancestry raises \eqn{\phi}, unmodelled: +#' about 0.54 at a true 0.5 at Fst 0.001, and 0.62 at 0.003. The p-value +#' tests no heritability, with \eqn{\phi} in its variance. It is the +#' exact F test at \eqn{\phi = 0}, but ran at about 0.09 at a nominal +#' 0.05 with a panel, as real genotypes' components have heavier tails +#' than it assumes; HDL-L's held at 0.03-0.05, so prefer it for testing. #' LAVA itself withholds the estimate where \eqn{K} exceeds a tenth of #' the sample size, which a cis window's components usually do at QTL -#' sample sizes; it is reported here at any \eqn{K}. On real genotypes -#' its error past that point was about twice that below it, still -#' below HDL-L's. The estimate can be negative.} +#' sample sizes; it is reported here at any \eqn{K}. The estimate can be +#' negative or exceed 1.} #' \item{\code{"hdlL"}}{HDL-L (Li et al. 2025): the likelihood of the #' statistics rotated onto the LD eigenvectors that cover 99\% of the #' trace, in the heritability and an intercept, with a term for the #' reference's sampling noise. Its p-value is the likelihood ratio -#' against no heritability; \code{intercept} is the fitted intercept. +#' against no heritability, referred to the equal mixture of a +#' chi-square on one degree of freedom and a point mass at 0 since that +#' null is on the boundary of the heritability's range, half of what +#' HDL-L reports; \code{intercept} is the fitted intercept. #' \code{NA} where no fit converges and beats that null.} #' \item{\code{"fastHer"}}{FastHer (Svishcheva 2025): the maximum #' likelihood of an infinitesimal model for the study samples, @@ -58,11 +82,13 @@ NULL #' expected information; \code{scale} is the fitted phenotypic #' variance relative to its standardized value, about 1 when the model #' fits. HEELS (Li, Mazumder and Lin 2023) maximizes the same -#' likelihood by iteration. The likelihood assumes in-sample LD and is +#' likelihood by iteration. The standard error holds under that model: +#' with few causal variants it is too small, 0.77 of the estimates' +#' spread at a heritability of 0.2 with one causal variant, and 0.59 at +#' 0.5, on real genotypes. The likelihood assumes in-sample LD and is #' undefined when the reference's rank, or the statistics' squared -#' norm in the LD's metric, reaches the sample size, as it can with an -#' external reference larger than the study; such traits are -#' \code{NA}, with a warning.} +#' norm in the LD's metric, reaches the sample size, as it can with LD +#' from outside the study; such traits are \code{NA}, with a warning.} #' } #' Every per-trait method assumes the LD reference matches the population #' of the statistics, as the QC step (\code{\link{summaryStatsQc}}) @@ -142,7 +168,7 @@ NULL #' #' # Per-trait cis heritability, each row against its own LD reference. #' data(qtlSumStatsExample) -#' estimateH2(qtlSumStatsExample) +#' estimateH2(qtlSumStatsExample, referenceOverlap = 1) #' @export setGeneric( "estimateH2", @@ -321,7 +347,12 @@ setGeneric( #' \item{partitioned}{tau-based enrichment of the baseline annotations, #' with columns \code{annotation}, \code{tau}, \code{tauSe}, #' \code{enrichment}, \code{enrichmentSe}, \code{enrichmentP}, -#' \code{propH2}, \code{propSnps}.} +#' \code{propH2}, \code{propSnps}. \code{enrichmentP} is ldsc's t test +#' of the per-variant heritability inside the annotation against +#' outside it. The last five are \code{NA} for an annotation with +#' values outside [0, 1], where they are not shares of the variants and +#' the heritability and depend on where the annotation's zero is; read +#' \code{tau} there.} #' \item{score test}{the score test over the tested (candidate) #' annotations, with columns \code{annotation}, \code{scoreZ}, #' \code{scoreP}.} @@ -848,7 +879,9 @@ setGeneric("ldMatrix", function(x) standardGeneric("ldMatrix")) #' @description Extract a genotype matrix from an object that carries genotype #' data. For a \code{QtlDataset}, the whole dataset's dosages (samples x #' variants), with no QC applied: to read part of it, narrow the dataset -#' first, e.g. \code{genotypes(x[window, samples, ])}. +#' first, e.g. \code{genotypes(x[window, samples, ])}. Its columns carry the +#' labels the genotype file gives its variants; a pipeline's results name +#' them by the canonical id \code{\link{variantIds}} describes. #' @param x The object to extract from. #' @param ... Passed on to methods. #' @return A numeric matrix, a list of matrices, or \code{NULL}. @@ -967,7 +1000,9 @@ setGeneric( #' #' @param x A \code{FineMappingRow} or \code{FineMappingResultBase}. #' @param keepVariants Character vector of variant IDs to keep. Intersected with -#' the entry's own \code{variantIds}; an empty intersection raises an error. +#' the entry's own \code{variantIds}, by position and alleles in either order +#' and on either strand, or by label for an rsID; an empty intersection raises +#' an error. #' @param ... Future expansion. #' @return The same flavour of object with PIPs renormalized on the kept subset. #' @noRd @@ -1279,10 +1314,18 @@ setGeneric("susieAlpha", function(x, ...) standardGeneric("susieAlpha")) #' (typically ctwas's \code{param} list), or \code{NULL}. On a #' \code{CtwasResult}, a tibble with one row per run and group, led by the #' run identity (\code{gwasStudy}, \code{study}, \code{context}, -#' \code{method}, and the joint columns when present), then \code{group}, -#' \code{group_size}, \code{group_prior} and \code{group_prior_var}. A -#' value the run did not record is \code{NA}, a run with no priors has no -#' rows, and each context of a joint run lists the priors they share. +#' \code{method}, and the joint columns when present). Then \code{group}, +#' named \code{{study}_{context}_{type}}, with \code{group_study}, +#' \code{group_context} and \code{group_type} since those underscores cannot +#' be split back, \code{group_size}, \code{group_prior} and +#' \code{group_prior_var}. Then cTWAS's summary of them +#' (\code{ctwas::summarize_param()}): \code{log_enrichment}, +#' \code{log_enrichment_se} and \code{enrichment_p}, each gene group against +#' the SNP group by a one-sided Fisher test, and \code{group_pve} and +#' \code{prop_heritability}, the heritability the group explains and its share +#' of what all groups explain, at the GWAS's median sample size. A value the +#' run did not record is \code{NA}, a run with no priors has no rows, and each +#' context of a joint run lists the priors they share. #' @examples #' priors <- list(group_prior = c(SNP = 1e-4, brain = 0.01), #' group_prior_var = c(SNP = 10, brain = 20)) @@ -1378,27 +1421,41 @@ setGeneric("cvFits", function(x, ...) standardGeneric("cvFits")) #' @export setGeneric("methodNames", function(x) standardGeneric("methodNames")) -#' @title Get Data Type -#' @description Extract the data-type tag. -#' @param x A \code{TwasWeightsRow} or \code{TwasWeights}. +#' @title Get the Molecular Type +#' @description What a dataset's phenotypes or summary statistics measure +#' (\code{"expression"}, \code{"splicing"}, \code{"methylation"}), as +#' \code{\link{QtlDataset}} and \code{\link{QtlSumStats}} record it, and as +#' the pipelines carry it onto what they fit. Free text: the label means what +#' it is given to mean, and \code{\link{ctwasPipeline}}'s \code{varStructure = +#' "shared_type"} pools the contexts that share one. +#' @param x A \code{\link{QtlDataset}}, a collection such as a +#' \code{\link{QtlSumStats}} or fine-mapping result, a \code{TwasWeights} or a +#' \code{TwasWeightsRow}. #' @param ... Class-specific selection arguments. -#' @param studyName Character (length 1) or \code{NULL}. Restrict the selection -#' to this study; \code{NULL} matches all studies. -#' @param context Character (length 1) or \code{NULL}. Restrict the selection to -#' this context; \code{NULL} matches all contexts. -#' @param trait Character (length 1) or \code{NULL}. Restrict the selection to -#' this trait; \code{NULL} matches all traits. -#' @param method Character (length 1) or \code{NULL}. Restrict the selection to -#' this fine-mapping / weight method; \code{NULL} matches all methods. -#' @return A character vector or NULL. +#' @param studyName For a \code{TwasWeights}: character (length 1) or +#' \code{NULL}. Restrict the selection to this study; \code{NULL} matches all +#' studies. +#' @param context For a \code{TwasWeights}: character (length 1) or \code{NULL}. +#' Restrict the selection to this context; \code{NULL} matches all contexts. +#' @param trait For a \code{TwasWeights}: character (length 1) or \code{NULL}. +#' Restrict the selection to this trait; \code{NULL} matches all traits. +#' @param method For a \code{TwasWeights}: character (length 1) or \code{NULL}. +#' Restrict the selection to this weight method; \code{NULL} matches all +#' methods. +#' @return On a \code{QtlDataset}, a character vector named by context; on a +#' collection, one value per row. \code{NA} where nothing is recorded. On a +#' \code{TwasWeights} or \code{TwasWeightsRow}, the selected entry's label or +#' \code{NULL}. #' @examples #' twe <- TwasWeightsRow(variantIds = sprintf("chr1:%d:A:G", 100L * (1:4)), #' weights = rep(0.1, 4), cvResult = list(rsq = 0.5), -#' weightStandardized = FALSE) -#' weightsDataType(twe) +#' weightStandardized = FALSE, weightsDataType = "expression") +#' dataType(twe) +#' data(qtlDatasetExample) +#' dataType(qtlDatasetExample) #' @export -setGeneric("weightsDataType", function(x, ...) { - standardGeneric("weightsDataType") +setGeneric("dataType", function(x, ...) { + standardGeneric("dataType") }) # ============================================================================= diff --git a/R/ColocBoostResult.R b/R/ColocBoostResult.R index 374778425..0d1cf100a 100644 --- a/R/ColocBoostResult.R +++ b/R/ColocBoostResult.R @@ -511,13 +511,19 @@ ColocBoostResult <- function( } # Top variable of one set. Both cos_top_variables and ucos_top_variables are -# data frames whose ROWNAMES are the set ids, not per-id lists. +# data frames whose ROWNAMES are the set ids, not per-id lists. Variables tied +# for the top get a row each, named id.1, id.2, ...; the first is the one +# colocboost's own summary reports. # @noRd .cbrTopVariable <- function(tv, id) { - if (is.null(tv) || is.null(rownames(tv)) || !is_in(id, rownames(tv))) { + if (is.null(tv) || is.null(rownames(tv))) { return(NA_character_) } - as.character(tv[id, "top_variables"]) + ofSet <- rownames(tv) == id | startsWith(rownames(tv), str_c(id, ".")) + if (!any(ofSet)) { + return(NA_character_) + } + as.character(tv[which(ofSet)[[1L]], "top_variables"]) } # @noRd diff --git a/R/CtwasResult.R b/R/CtwasResult.R index eea7bebbb..d46430b29 100644 --- a/R/CtwasResult.R +++ b/R/CtwasResult.R @@ -494,24 +494,56 @@ setMethod("ctwasGroupPriors", "CtwasResult", function(x) { # ctwas's EM output (its `param` list) as one row per group. ctwas names # group_prior, group_prior_var and group_size by group, so the groups are -# group_prior's and the other two are matched to them by name: NA where the -# run recorded none. +# group_prior's and everything else is matched to them by name: NA where the +# run recorded none. .ctwasScoredParam() added each group's study, context and +# type, and cTWAS's summary of the priors, whose enrichment reads the gene +# groups against the SNP group, which so has none of its own. # @noRd #' @importFrom purrr pluck .ctwasPriorTable <- function(param) { prior <- pluck(param, "group_prior") groups <- names(prior) %||% rep(NA_character_, length(prior)) + info <- pluck(param, "group_info") + summarized <- pluck(param, "group_summary") tibble( - group = groups, + group = .ctwasGroupLabel(groups), + group_study = .ctwasGroupField(info, "study", groups), + group_context = .ctwasGroupField(info, "context", groups), + group_type = .ctwasGroupField(info, "type", groups), group_size = .ctwasPriorByGroup(pluck(param, "group_size"), groups), group_prior = as.numeric(unname(prior)), group_prior_var = .ctwasPriorByGroup( pluck(param, "group_prior_var"), groups + ), + log_enrichment = .ctwasPriorByGroup( + pluck(summarized, "log_enrichment"), + groups + ), + log_enrichment_se = .ctwasPriorByGroup( + pluck(summarized, "log_enrichment_se"), + groups + ), + enrichment_p = .ctwasPriorByGroup( + pluck(summarized, "enrichment_pval"), + groups + ), + group_pve = .ctwasPriorByGroup(pluck(summarized, "group_pve"), groups), + prop_heritability = .ctwasPriorByGroup( + pluck(summarized, "prop_heritability"), + groups ) ) } +# @noRd +.ctwasGroupField <- function(info, field, groups) { + if (is.null(info)) { + return(rep(NA_character_, length(groups))) + } + info[[field]][match(groups, info$group)] +} + # @noRd .ctwasPriorByGroup <- function(values, groups) { if (is.null(names(values))) { diff --git a/R/GenotypeHandle.R b/R/GenotypeHandle.R index 02d0580a8..884c9fc98 100644 --- a/R/GenotypeHandle.R +++ b/R/GenotypeHandle.R @@ -888,8 +888,17 @@ setMethod("extract_array", "GhSeed", function(x, index) { return(matrix(numeric(0), length(i), length(j))) } # Read as stored, unlabelled: a panel's rowRanges, not its seed, describe - # its variants. - t(.handleDosage(.ghSeedHandle(x), i))[, j, drop = FALSE] + # its variants. Each distinct sample is read once, in file order, and the + # block then laid out as asked: DelayedArray may ask in any order, and + # repeat. + handle <- .ghSeedHandle(x) + samples <- as.integer(sort(unique(j))) + dosage <- if (length(samples) == dm[[2L]]) { + .handleDosage(handle, i) + } else { + .handleDosage(handle, i, samples) + } + t(dosage)[, match(j, samples), drop = FALSE] }) #' Genotype dosages as a lazily-read DelayedArray diff --git a/R/GwasFineMappingResult.R b/R/GwasFineMappingResult.R index 0c556f21a..51b759b0e 100644 --- a/R/GwasFineMappingResult.R +++ b/R/GwasFineMappingResult.R @@ -130,8 +130,8 @@ setClass( #' \code{region}. Where it is \code{NULL}, or \code{NA} for a row, the #' row's \code{blockId} is its own span, as \code{"chrom_start_end"}, so #' every row is named by its block. -#' @param dataType Optional assay / modality label ("bulk_rnaseq", -#' "methylation"), recorded as a per-row provenance column. Length 1 or +#' @param dataType Optional molecular type ("expression", "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"}. Where it is absent, the result diff --git a/R/H2Estimate.R b/R/H2Estimate.R index 1b726735d..b0e29b5a0 100644 --- a/R/H2Estimate.R +++ b/R/H2Estimate.R @@ -34,6 +34,9 @@ NULL #' estimate used.} #' \item{\code{scale}}{FastHer's fitted phenotypic variance relative to #' its standardized value.} +#' \item{\code{reference_overlap}}{The share of the study's samples LAVA +#' took to be in the LD reference (\code{referenceOverlap} in +#' \code{\link{estimateH2}}).} #' \item{\code{enrichment}}{The per-annotation enrichment table of a #' stratified estimate (\code{\link{stratifiedHeritabilityEnrichment}}).} #' \item{\code{jackknife}}{The per-block jackknife matrix of the @@ -66,7 +69,8 @@ setClass( p_value = "double", n_snps = "integer", n_components = "integer", - scale = "double" + scale = "double", + reference_overlap = "double" ) # The objects an estimate carries, each NULL when it has none. @@ -277,7 +281,8 @@ setMethod("stratifiedHeritabilityEnrichment", "H2Estimate", function(object) { ) # An H2Estimate saved when it held one estimate in slots is rebuilt as a -# collection of one. +# collection of one; one saved before estimates recorded the reference overlap +# has none to record. #' @rdname updateObject-methods #' @export setMethod( @@ -290,10 +295,26 @@ setMethod( if (.h2SavedInSlots(object)) { object <- .h2FromSlots(.renameSavedSlots(object, .H2_RENAMED_SLOTS)) } + object <- .h2WithOverlapColumn(object) methods::callNextMethod(object, ..., verbose = verbose) } ) +# @noRd +.h2WithOverlapColumn <- function(object) { + md <- mcols(object, use.names = FALSE) + if (is_in("reference_overlap", colnames(md))) { + return(object) + } + S4Vectors::`mcols<-`( + object, + value = cbind( + md, + S4Vectors::DataFrame(reference_overlap = rep(NA_real_, nrow(md))) + ) + ) +} + # @noRd .h2SavedInSlots <- function(object) { !is.null(attr(object, "estimate", exact = TRUE)) || @@ -326,7 +347,8 @@ setMethod( p_value = NA_real_, n_snps = methods::slot(object, "nSnps"), n_components = NA_integer_, - scale = NA_real_ + scale = NA_real_, + reference_overlap = NA_real_ ), list( enrichment = list(methods::slot(object, "enrichment")), diff --git a/R/QtlDataset.R b/R/QtlDataset.R index ecf0095db..f316c9573 100644 --- a/R/QtlDataset.R +++ b/R/QtlDataset.R @@ -346,6 +346,13 @@ setClass( #' @param genotypeCovariates Numeric matrix of genotype-derived covariates #' (e.g., ancestry PCs); rows are samples. Becomes the \code{colData} of the #' genotype experiment. +#' @param dataType Optional molecular type of each context (\code{"expression"}, +#' \code{"splicing"}, \code{"methylation"}): free text, one value for every +#' context or a vector named by context. It cannot be derived, the phenotypes +#' being only numbers, so pipelines read it from here and record it on what +#' they fit, and \code{\link{ctwasPipeline}} models each type as its own. Kept +#' in the context's \code{metadata()} as \code{dataType}, and read by +#' \code{\link{dataType}}. \code{NULL} (default) leaves a context's as it is. #' @details A \code{QtlDataset} holds data only. Which variants and samples #' an analysis keeps, and whether residuals are scaled, are the pipeline's #' settings (\code{\link{GenotypeFilterParam}} and @@ -371,14 +378,16 @@ setClass( #' rowRanges = rng #' ) #' QtlDataset( -#' studyName = "s1", genotypes = panel, phenotypes = list(brain = se) +#' studyName = "s1", genotypes = panel, phenotypes = list(brain = se), +#' dataType = "expression" #' ) #' @export QtlDataset <- function( studyName, genotypes, phenotypes, - genotypeCovariates = matrix(numeric(0), nrow = 0, ncol = 0) + genotypeCovariates = matrix(numeric(0), nrow = 0, ncol = 0), + dataType = NULL ) { handle <- .qtlValidateInputs(phenotypes, genotypes) # Every experiment's rowRanges in UCSC style, and the genotypes' alleles @@ -390,7 +399,7 @@ QtlDataset <- function( list(.genotypeExperiment(genotypes, genotypeCovariates)), .QTL_GENO_EXPERIMENT ), - as.list(phenotypes) + .qtlWithDataTypes(as.list(phenotypes), dataType) ), .canonExperiment ) @@ -612,6 +621,69 @@ setMethod("longForm", "QtlDataset", function(object, ..., genotype = FALSE) { ) } +# The phenotypes, each carrying its context's molecular type in its metadata. +# @noRd +.qtlWithDataTypes <- function(phenotypes, dataType) { + if (is.null(dataType)) { + return(phenotypes) + } + assertCharacter(dataType, any.missing = FALSE, min.len = 1L) + typed <- names(dataType) %||% names(phenotypes) + if (length(dataType) != length(typed) && length(dataType) != 1L) { + abort(c( + glue( + "`dataType` has {length(dataType)} values for ", + "{length(phenotypes)} context(s)." + ), + i = "Give one value, or a vector named by context." + )) + } + unknown <- setdiff(typed, names(phenotypes)) + if (length(unknown) > 0L) { + abort(glue( + "`dataType` names no such context: ", + "{str_flatten(unknown, ', ')}." + )) + } + values <- set_names( + rep(unname(dataType), length.out = length(typed)), + typed + ) + imap(phenotypes, .qtlContextWithType, values = values) +} + +# @noRd +.qtlContextWithType <- function(se, context, values) { + if (!is_in(context, names(values))) { + return(se) + } + S4Vectors::`metadata<-`( + se, + value = list_assign( + S4Vectors::metadata(se), + dataType = values[[context]] + ) + ) +} + +#' @rdname dataType +#' @export +setMethod("dataType", "QtlDataset", function(x) { + ids <- contexts(x) + set_names( + map_chr( + as.list(MultiAssayExperiment::experiments(x))[ids], + .seDataType + ), + ids + ) +}) + +# @noRd +.seDataType <- function(se) { + S4Vectors::metadata(se)$dataType %||% NA_character_ +} + #' @rdname studyName #' @export setMethod("studyName", "QtlDataset", function(x) x@studyName) @@ -741,7 +813,8 @@ setMethod("genotypeHandle", "QtlDataset", function(x) { if (length(perTraitRanges) == 0L) { abort("None of the requested traitId values were found in any context.") } - allRanges <- exec(c, !!!perTraitRanges) + # Contexts need not hold the same chromosomes, which c() warns of. + allRanges <- suppressWarnings(exec(c, !!!perTraitRanges)) chrs <- unique(as.character(GenomicRanges::seqnames(allRanges))) if (length(chrs) != 1L) { msg <- glue( @@ -936,7 +1009,8 @@ setMethod("traitPosition", "QtlDataset", function(x, trait = NULL) { # # Returns a list: # geno : numeric matrix (kept samples x kept variants) -# variantIds : character vector of kept variant IDs (= colnames(geno)) +# variantIds : character vector of kept variant IDs (= colnames(geno)), the +# canonical ids .qtlReadBlock() labels a read with # sampleIds : character vector of kept sample IDs (= rownames(geno)) # maf : numeric vector of per-variant MAF for kept variants # af : numeric vector of per-variant effect-allele (A1) frequency @@ -944,11 +1018,15 @@ setMethod("traitPosition", "QtlDataset", function(x, trait = NULL) { # allele): the frequency of the dosage-counted allele, which # is A1 by the same convention the marginal betas use. `maf` # is `pmin(af, 1 - af)`. +# +# `read` is the window's dosages when a caller already holds them +# (.qtlReadBlock()); nothing is read again. It must span `window` and `samples`. .qtlExtractBlock <- function( x, window = NULL, samples = NULL, - filter = .qtlResolveFilter() + filter = .qtlResolveFilter(), + read = NULL ) { # A pipeline threads its resolved filter to every extraction; a NULL one # means a call site was missed, which must not pass for "no filter". @@ -962,19 +1040,19 @@ setMethod("traitPosition", "QtlDataset", function(x, trait = NULL) { if (length(inRegion) == 0L) { return(.qtlEmptyBlockAllSamples(x)) } - # Apply keepVariants + indel restrictions before materialization so we do - # not extract dosage we would immediately drop. - snpIdx <- .qtlNarrowSnpIdx(x, inRegion, filter) + held <- read %||% .qtlReadBlock(x, window, samples, filter) + snpIdx <- inRegion[is_in(inRegion, held$snpIdx)] if (length(snpIdx) == 0L) { return(.qtlEmptyBlock()) } - se <- .qtlGenotypeSe(x) - # Labelled with the panel's own variant ids, as the rows a block reports. - dosage <- `colnames<-`( - .ldRefDosage(se, snpIdx, meanImpute = FALSE), - .ldRefVariantIds(se)[snpIdx] - ) - keep <- .qtlResolveSamples(dosage, x, samples) + dosage <- .qtlHeldDosage(held, snpIdx) + keep <- .qtlResolveSamples(rownames(dosage), x, samples) + if (length(.qtlUnreadSamples(x, keep, samples)) > 0L) { + abort( + "A held genotype read does not cover the samples asked of it.", + .internal = TRUE + ) + } if (length(keep) == 0L) { return(.qtlEmptyBlockNoSamples(dosage)) } @@ -993,6 +1071,534 @@ setMethod("traitPosition", "QtlDataset", function(x, trait = NULL) { ) } +# The dosages of one window, read once, for .qtlExtractBlock() to filter on the +# samples each fit uses: the variants that survive keepVariants and the indel +# rule (narrowed before materialization so nothing is read only to be dropped) +# and the samples asked for, or every sample of the dataset. No read at all when +# no variant survives. Returns list(snpIdx, dosage), the dosage NULL in that +# case and otherwise labelled with each variant's canonical id, rendered from +# its range and alleles: a fit's ids are the ones every result class holds and +# locates, whatever the file calls the variant (an rsID names no position). +# @noRd +.qtlReadBlock <- function(x, window, samples, filter) { + inRegion <- .qtlVariantIndices(x, window) + snpIdx <- if (length(inRegion) > 0L) { + .qtlNarrowSnpIdx(x, inRegion, filter) + } else { + integer(0) + } + if (length(snpIdx) == 0L) { + return(list(snpIdx = integer(0), dosage = NULL)) + } + se <- .qtlGenotypeSe(x) + panelSamples <- as.character(colnames(se)) + wanted <- .qtlResolveSamples(panelSamples, x, samples) + list( + snpIdx = snpIdx, + dosage = `colnames<-`( + .ldRefDosage( + se, + snpIdx, + meanImpute = FALSE, + sampleIdx = if (length(wanted) < length(panelSamples)) { + which(is_in(panelSamples, wanted)) + } + ), + .ldRefMatchIds(se, snpIdx) + ) + ) +} + +# The file's own labels of the variants with canonical ids `ids`, which the +# accessors report: they hand the data back as stored. +# @noRd +.qtlStoredLabels <- function(x, ids) { + se <- .qtlGenotypeSe(x) + .ldRefVariantIds(se)[match(ids, .ldRefMatchIds(se))] +} + +# The whole dataset's block under its file's labels. +# @noRd +.qtlStoredBlock <- function(x) { + block <- .qtlExtractBlock(x) + labels <- .qtlStoredLabels(x, block$variantIds) + list_assign( + block, + geno = `colnames<-`(block$geno, labels), + variantIds = labels + ) +} + +# The columns of a held read at `snpIdx`, without a copy when that is all of it. +# @noRd +.qtlHeldDosage <- function(held, snpIdx) { + if (identical(snpIdx, held$snpIdx)) { + return(held$dosage) + } + held$dosage[, match(snpIdx, held$snpIdx), drop = FALSE] +} + +# The samples a request wants from the panel that a held read left out. A read +# narrower than its requests would silently lose samples from a fit. +# @noRd +.qtlUnreadSamples <- function(x, kept, samples) { + setdiff( + .qtlResolveSamples( + as.character(colnames(.qtlGenotypeSe(x))), + x, + samples + ), + kept + ) +} + +# The rows of a matrix at `samples`, without a copy when that is all of them in +# order, so fits sharing an X go on sharing the one matrix. +# @noRd +.qtlRowsOf <- function(X, samples) { + if (identical(samples, rownames(X))) { + return(X) + } + X[samples, , drop = FALSE] +} + +# The samples of the given contexts' phenotypes: all a fit on them can use. +# @noRd +.qtlContextSamples <- function(x, contexts) { + unique(list_c(map(contexts, .qtlOneContextSamples, x = x))) +} + +# @noRd +.qtlOneContextSamples <- function(ctx, x) { + colnames(.qtlContextSe(x, context = ctx)) +} + +# A window as a key to group fits by. +# @noRd +.qtlWindowKey <- function(window) { + if (is.null(window)) "" else str_flatten(as.character(window), "|") +} + +# The fits that can share one residualized X: the same window, covariates and +# samples. `contexts` labels the covariates (fits that may not share an X get a +# label of their own). A list of groups, each list(at, window, context, +# samples), `at` the group's positions among the fits, in order of first +# appearance. +# @noRd +#' @importFrom vctrs vec_group_loc +.qtlXGroups <- function(windows, contexts, samples) { + keys <- tibble( + window = map_chr(windows, .qtlWindowKey), + context = contexts, + samples = map_chr(samples, rlang::hash) + ) + map( + vec_group_loc(keys)$loc, + .qtlXGroup, + windows = windows, + contexts = contexts, + samples = samples + ) +} + +# @noRd +.qtlXGroup <- function(at, windows, contexts, samples) { + first <- at[[1L]] + list( + at = at, + window = windows[[first]], + context = contexts[[first]], + samples = samples[[first]] + ) +} + +# Results laid out group by group, put back in the order of the items the groups +# were formed from. `at` holds each group's positions among those items. +# @noRd +.qtlInItemOrder <- function(perGroup, at) { + flat <- exec(c, list(), !!!perGroup) + flat[order(as.integer(list_c(at)))] +} + +# The items one read serves, as groups of their positions: the items of a +# window, and with them those of any window inside it, in order of first +# appearance. An item with no window stays apart, as it may read nothing at all. +# @noRd +.qtlWindowGroups <- function(windows) { + exact <- vec_group_loc(map_chr(windows, .qtlWindowKey))$loc + widest <- .qtlWidestHolder(windows[map_int(exact, 1L)]) + map( + unname(split(exact, factor(widest, levels = unique(widest)))), + .qtlSortedPositions + ) +} + +# @noRd +.qtlSortedPositions <- function(groups) { + sort(list_c(groups)) +} + +# For each of these distinct windows, the position of the widest one that holds +# all of it: itself, when no wider one does. A window with several ranges is +# held when each of its ranges lies inside one of the holder's. +# @noRd +#' @importFrom dplyr count arrange desc distinct +.qtlWidestHolder <- function(windows) { + isWindow <- !map_lgl(windows, is.null) + if (sum(isWindow) < 2L) { + return(seq_along(windows)) + } + sizes <- map_int(windows, length) + owner <- rep(seq_along(windows), sizes) + # Each window knows only its own chromosomes, which c() warns of. + ranges <- suppressWarnings(exec(c, !!!unname(windows[isWindow]))) + inside <- GenomicRanges::findOverlaps(ranges, ranges, type = "within") + spans <- map_dbl(windows, .qtlWindowSpan) + holders <- tibble( + window = owner[S4Vectors::queryHits(inside)], + range = S4Vectors::queryHits(inside), + holder = owner[S4Vectors::subjectHits(inside)] + ) |> + distinct() |> + count(.data$window, .data$holder) |> + filter(.data$n == sizes[.data$window]) |> + arrange(desc(spans[.data$holder]), .data$holder) |> + distinct(.data$window, .keep_all = TRUE) + replace(seq_along(windows), holders$window, holders$holder) +} + +# How many bases a window covers; none for no window. +# @noRd +.qtlWindowSpan <- function(window) { + if (is.null(window)) 0 else sum(as.numeric(GenomicRanges::width(window))) +} + +# The widest of these windows, which .qtlWindowGroups() found holds the others. +# @noRd +.qtlWidestWindow <- function(windows) { + windows[[which.max(map_dbl(windows, .qtlWindowSpan))]] +} + +# ---- Planned fits ----------------------------------------------------------- +# +# A pipeline's paths (univariate, top PCs, joint) each plan their fits before +# any genotype is read, and only then do all of them run together: +# +# * one read of a window serves every path, and is let go as soon as the last +# block it is needed for is built, before that block's fits run; +# * one residualized X (a block) serves every fit on the same contexts and +# samples, whichever path it is on, and is let go when its last fit is done. +# +# A plan is a `.qtlWork()` of fits, or a `.qtlWorkOf()` joining plans. Each has +# a `finish` that makes its result: of its fits, or of its parts' results. +# Nothing runs until `.qtlRunWork()`. + +# The genotypes a fit is on: the variants of `window`, residualized on the +# covariates of `contexts`, filtered and scaled on `samples`. +# @noRd +.qtlFitSpec <- function(window, contexts, samples) { + list(window = window, contexts = contexts, samples = samples) +} + +# Fits to run: `fit(item, !!!fitArgs, block = )` for each of `items`, `block` +# being the residualized genotypes of the item's `.qtlFitSpec()` in `specs`, as +# list(X, af, window). An item with no spec reads no genotypes and gets none. +# @noRd +.qtlWork <- function( + items, + specs, + fit, + fitArgs = list(), + finish = identity, + finishArgs = list() +) { + list( + items = items, + specs = specs, + fit = fit, + fitArgs = fitArgs, + finish = finish, + finishArgs = finishArgs + ) +} + +# Plans run together, `finish` given the list of their results. +# @noRd +.qtlWorkOf <- function(parts, finish = identity, finishArgs = list()) { + list(parts = as.list(parts), finish = finish, finishArgs = finishArgs) +} + +# A plan with nothing to fit, and `value` for its result. +# @noRd +.qtlWorkValue <- function(value) { + .qtlWorkOf(list(), .qtlWorkConstant, list(value = value)) +} + +# @noRd +.qtlWorkConstant <- function(results, value) { + value +} + +# A plan's fits, and then its result. `withAf` has each block carry its +# variants' allele frequencies, which fine-mapping reports and TWAS never reads. +# @noRd +.qtlRunWork <- function( + work, + dataset, + filter, + residualizationParam = NULL, + withAf = FALSE +) { + leaves <- .qtlWorkLeaves(work) + leafOf <- rep(seq_along(leaves), map_int(leaves, .qtlWorkItemCount)) + specs <- list_flatten(map(leaves, "specs")) + tagged <- map2(leafOf, list_flatten(map(leaves, "items")), .qtlWorkTag) + run <- list( + dataset = dataset, + filter = filter, + residualizationParam = residualizationParam, + withAf = withAf, + leaves = leaves + ) + at <- .qtlWindowGroups(map(specs, "window")) + fits <- .qtlInItemOrder( + map(at, .qtlFitWindow, tagged = tagged, specs = specs, run = run), + at + ) + perLeaf <- split(fits, factor(leafOf, seq_along(leaves))) + .qtlWorkFinish(work, unname(perLeaf)) +} + +# The plans that hold fits, in order. +# @noRd +.qtlWorkLeaves <- function(work) { + if (is.null(work$parts)) { + return(list(work)) + } + list_flatten(map(work$parts, .qtlWorkLeaves)) +} + +# @noRd +.qtlWorkItemCount <- function(leaf) { + length(leaf$items) +} + +# @noRd +.qtlWorkTag <- function(leaf, item) { + list(leaf = leaf, item = item) +} + +# One read's fits. The last block's fits run after this function's read has +# gone out of scope (.qtlFitUnderRead()), so that fits never sit beside a raw +# block no one needs any more. +# @noRd +.qtlFitWindow <- function(at, tagged, specs, run) { + groups <- .qtlBlockGroups(specs[at], run$filter) + under <- .qtlFitUnderRead(groups, tagged[at], specs[at], run) + lastFits <- .qtlFitGroup( + groups[[length(groups)]], + tagged[at], + specs[at], + run, + under$block + ) + .qtlInItemOrder(c(under$fits, list(lastFits)), map(groups, "at")) +} + +# What needs the window's raw dosages: every block's fits but the last's, and +# the last block itself. `read` is a lazily evaluated argument, so a window +# whose items read no genotypes is never read, and it is gone when this returns. +# @noRd +.qtlFitUnderRead <- function( + groups, + tagged, + specs, + run, + read = .qtlReadBlock( + run$dataset, + .qtlWidestWindow(map(compact(specs), "window")), + .qtlUnionSamples(map(compact(specs), "samples")), + run$filter + ) +) { + earlier <- groups[-length(groups)] + list( + fits = map( + earlier, + .qtlFitGroupUnderRead, + tagged = tagged, + specs = specs, + run = run, + read = read + ), + block = .qtlFitBlock(groups[[length(groups)]]$spec, run, read) + ) +} + +# @noRd +.qtlFitGroupUnderRead <- function(group, tagged, specs, run, read) { + .qtlFitGroup(group, tagged, specs, run, .qtlFitBlock(group$spec, run, read)) +} + +# Every fit on one block. The block is let go when this returns. +# @noRd +.qtlFitGroup <- function(group, tagged, specs, run, block) { + map2(tagged[group$at], specs[group$at], .qtlWorkFitItem, run, block) +} + +# One item's fit, on its own window's part of the block. +# @noRd +.qtlWorkFitItem <- function(tagged, spec, run, block) { + leaf <- run$leaves[[tagged$leaf]] + fitArgs <- leaf$fitArgs + exec( + leaf$fit, + tagged$item, + !!!fitArgs, + block = .qtlBlockFor(block, spec, run$dataset) + ) +} + +# The fits that share one block, as groups of their positions: the same contexts +# and samples, each group with the spec its block is built to. A residualized +# column depends on no other, so a block over the widest window serves narrower +# windows too. The exception is a per-sample missingness filter, which looks +# across a block's variants: with one on, a block serves only its own window. +# @noRd +.qtlBlockGroups <- function(specs, filter) { + windows <- map(specs, "window") + keys <- if (filter$imissCutoff %||% 0 > 0) { + windows + } else { + rep(list(NULL), length(windows)) + } + map( + .qtlXGroups( + keys, + map_chr(specs, .qtlSpecContexts), + map(specs, "samples") + ), + .qtlBlockGroup, + specs = specs + ) +} + +# A spec's contexts as a key; the order matters to the covariate design. +# @noRd +.qtlSpecContexts <- function(spec) { + str_flatten(spec$contexts %||% character(0), "|") +} + +# @noRd +.qtlBlockGroup <- function(group, specs) { + first <- specs[[group$at[[1L]]]] + list( + at = group$at, + spec = if (!is.null(first)) { + .qtlFitSpec( + .qtlWidestWindow(map(specs[group$at], "window")), + first$contexts, + first$samples + ) + } + ) +} + +# The residualized genotypes of a spec, with their allele frequencies when the +# run reports them; both come from `read`, which nothing here keeps. +# @noRd +.qtlFitBlock <- function(spec, run, read) { + if (is.null(spec)) { + return(NULL) + } + X <- .fmResidGeno( + run$dataset, + residualizationParam = run$residualizationParam, + genotypeFilter = run$filter, + contexts = spec$contexts, + region = spec$window, + samples = spec$samples, + read = read + ) + list( + X = X, + af = if (run$withAf) { + .fmAfForX( + run$dataset, + X, + genotypeFilter = run$filter, + region = spec$window, + read = read + ) + }, + window = spec$window + ) +} + +# A block as one fit sees it: the variants of the fit's own window. +# @noRd +.qtlBlockFor <- function(block, spec, dataset) { + if ( + is.null(block) || + .qtlWindowKey(spec$window) == .qtlWindowKey(block$window) + ) { + return(block) + } + kept <- is_in( + colnames(block$X), + .ldRefMatchIds( + .qtlGenotypeSe(dataset), + .qtlVariantIndices(dataset, spec$window) + ) + ) + list( + X = block$X[, kept, drop = FALSE], + af = block$af[kept], + window = spec$window + ) +} + +# A plan's result, from each of its leaves' fits in order. +# @noRd +.qtlWorkFinish <- function(work, fits) { + finishArgs <- work$finishArgs + if (is.null(work$parts)) { + return(exec(work$finish, fits[[1L]], !!!finishArgs)) + } + counts <- map_int(work$parts, .qtlWorkLeafCount) + ends <- cumsum(counts) + results <- map( + seq_along(work$parts), + .qtlWorkFinishPart, + parts = work$parts, + ends = ends, + counts = counts, + fits = fits + ) + exec(work$finish, results, !!!finishArgs) +} + +# @noRd +.qtlWorkFinishPart <- function(k, parts, ends, counts, fits) { + start <- ends[[k]] - counts[[k]] + .qtlWorkFinish(parts[[k]], fits[start + seq_len(counts[[k]])]) +} + +# @noRd +.qtlWorkLeafCount <- function(work) { + length(.qtlWorkLeaves(work)) +} + +# Every sample any of these requests uses; NULL (all of them) if any has no +# limit. +# @noRd +.qtlUnionSamples <- function(samples) { + if (any(map_lgl(samples, is.null))) { + return(NULL) + } + unique(list_c(samples)) +} + # Empty block preserving the full panel sample set (no variants selected). # @noRd .qtlEmptyBlockAllSamples <- function(x) { @@ -1040,35 +1646,33 @@ setMethod("traitPosition", "QtlDataset", function(x, trait = NULL) { ) } -# Narrow the selected variant indices by keepVariants (matched by chrom/pos/ -# allele) and, unless kept, by dropping indels. Done before materialization. +# Narrow the selected variant indices by keepVariants (.keepByIds(), as a +# keep-list file is matched) and, unless kept, by dropping indels. Done before +# materialization. # @noRd .qtlNarrowSnpIdx <- function(x, snpIdx, filter) { si <- .qtlGenotypeVariants(x) kept <- if (length(filter$keepVariants) == 0L) { snpIdx } else { - snpAll <- si$SNP[snpIdx] - km <- matchVariants(snpAll, as.character(filter$keepVariants)) - snpIdx[replace(logical(length(snpAll)), km$idxA, TRUE)] + snpIdx[.keepByIds( + .snpInfoToVariantInfo(slice(si, snpIdx)), + as.character(filter$keepVariants) + )] } if (length(kept) == 0L || isTRUE(filter$keepIndel)) { return(kept) } - # which() (not the mask) so an NA mask drops the variant rather than - # injecting an NA index. - snpMask <- str_length(as.character(si$A1[kept])) == 1L & - str_length(as.character(si$A2[kept])) == 1L - kept[which(snpMask)] + kept[isSnpAlleles(si$A1[kept], si$A2[kept])] } # Resolve the sample set: panel samples intersected with the dataset's # primary colData -- which is what narrows a subset dataset -- and with the # per-call `samples` arg. # @noRd -.qtlResolveSamples <- function(dosage, x, samples) { +.qtlResolveSamples <- function(panelSamples, x, samples) { inDataset <- intersect( - rownames(dosage), + panelSamples, rownames(MultiAssayExperiment::colData(x)) ) keep <- if (is.null(samples)) { @@ -1132,7 +1736,7 @@ setMethod("traitPosition", "QtlDataset", function(x, trait = NULL) { # An all-NA column means NaN either way, matching mean(numeric(0)). means <- colMeans(dosage, na.rm = TRUE) # One fill over the whole matrix instead of a copy per column: dosage is - # variants x samples, so rebuilding it column by column is the expensive + # samples x variants, so rebuilding it column by column is the expensive # way to say this. replace(dosage, naMask, means[col(dosage)[naMask]]) } @@ -1141,20 +1745,20 @@ setMethod("traitPosition", "QtlDataset", function(x, trait = NULL) { #' @export setMethod("genotypes", "QtlDataset", function(x) { # The whole dataset, unfiltered: narrow it first with x[i, j, ]. - .qtlExtractBlock(x)$geno + .qtlStoredBlock(x)$geno }) #' @rdname maf #' @export setMethod("maf", "QtlDataset", function(x) { - block <- .qtlExtractBlock(x) + block <- .qtlStoredBlock(x) set_names(block$maf, block$variantIds) }) #' @rdname af #' @export setMethod("af", "QtlDataset", function(x) { - block <- .qtlExtractBlock(x) + block <- .qtlStoredBlock(x) set_names(block$af, block$variantIds) }) @@ -1432,13 +2036,33 @@ CovMcdOptions <- function(...) { # # Returns all-TRUE (no-op) when there are too few samples to support # a covariance estimate (n < p + 2). -#' @importFrom rlang try_fetch +# +# A sample with a missing value has no distance, so it is kept unjudged, and the +# centre and covariance come from the samples with every trait. .qtlOutlierKeepMask <- function(Y, pvalThreshold, outlierArgs = list()) { - Y <- as.matrix(Y) + values <- as.matrix(Y) + if (ncol(values) == 0L) { + return(rep(TRUE, nrow(values))) + } + complete <- stats::complete.cases(values) + replace( + rep(TRUE, nrow(values)), + complete, + .qtlOutlierKeepComplete( + values[complete, , drop = FALSE], + pvalThreshold, + outlierArgs + ) + ) +} + +# .qtlOutlierKeepMask() of samples with no missing value. +#' @importFrom rlang try_fetch +.qtlOutlierKeepComplete <- function(Y, pvalThreshold, outlierArgs) { n <- nrow(Y) p <- ncol(Y) - if (n == 0L || p == 0L) { - return(rep(TRUE, n)) + if (n == 0L) { + return(logical(0)) } if (n < p + 2L) { msg <- glue( @@ -1546,10 +2170,7 @@ setMethod("phenotypeCovariates", "QtlDataset", function(x, context) { # `qr.resid` does not support LAPACK pivoted QR, so use `lm.fit`. It # handles rank-deficient designs gracefully via base-R's pivoted QR # internally -- same effect the LAPACK path was meant to deliver. - res <- `dimnames<-`( - as.matrix(stats::lm.fit(x = X, y = Y)$residuals), - list(rownames(Y), colnames(Y)) - ) + res <- `dimnames<-`(as.matrix(.qtlResiduals(X, Y)), dimnames(Y)) if (isTRUE(scaleResiduals)) { # `sds == 0` exact-zero test is unreliable for residuals coming out of # lm.fit on a constant Y: roundoff gives sd ~ 1e-16 instead of 0, and @@ -1565,6 +2186,44 @@ setMethod("phenotypeCovariates", "QtlDataset", function(x, context) { res } +# The residuals of Y on X. A column with missing values is fit on the rows it +# has and stays missing on the others, and columns missing the same rows are fit +# together. +# @noRd +.qtlResiduals <- function(X, Y) { + if (!anyNA(Y)) { + return(stats::lm.fit(x = X, y = Y)$residuals) + } + groups <- .qtlMissingnessGroups(Y) + columns <- map(groups, .qtlResidualsOnObserved, X = X, Y = Y) + exec(cbind, !!!columns)[, order(list_c(groups)), drop = FALSE] +} + +# The columns of a matrix missing the same rows, as groups of their positions. +# @noRd +.qtlMissingnessGroups <- function(Y) { + vec_group_loc(t(is.na(Y)))$loc +} + +# The residuals of the columns at `at`, which are missing the same rows. +# @noRd +.qtlResidualsOnObserved <- function(at, X, Y) { + observed <- !is.na(Y[, at[[1L]]]) + missingAll <- matrix(NA_real_, nrow(Y), length(at)) + if (!any(observed)) { + return(missingAll) + } + `[<-`( + missingAll, + observed, + , + value = stats::lm.fit( + x = X[observed, , drop = FALSE], + y = Y[observed, at, drop = FALSE] + )$residuals + ) +} + # Resolve the phenotype-covariate selection for one context: NULL requested -> # all available covariates; otherwise validate the requested names are present. # @noRd @@ -1861,7 +2520,8 @@ setMethod("phenotypeCovariates", "QtlDataset", function(x, context) { window = NULL, samples = NULL, filter = .qtlResolveFilter(), - residualizationParam = ResidualizationParam() + residualizationParam = ResidualizationParam(), + read = NULL ) { if (missing(contexts) || is.null(contexts) || length(contexts) == 0L) { msg <- glue( @@ -1882,7 +2542,7 @@ setMethod("phenotypeCovariates", "QtlDataset", function(x, context) { msg <- glue("Unknown context(s): {str_flatten(bad, \', \')}") abort(msg) } - G <- .qtlExtractBlock(x, window, samples, filter)$geno + G <- .qtlExtractBlock(x, window, samples, filter, read)$geno if (ncol(G) == 0L) { return(G) } @@ -1901,11 +2561,12 @@ setMethod( "residualizedGenotypes", "QtlDataset", function(x, context, residualizationParam = ResidualizationParam()) { - .qtlResidualizedGenotypes( + residualized <- .qtlResidualizedGenotypes( x, context, residualizationParam = residualizationParam ) + `colnames<-`(residualized, .qtlStoredLabels(x, colnames(residualized))) } ) @@ -2378,7 +3039,7 @@ setClass( #' or \code{\link{colocboostPipeline}} on a \code{\link{QtlDataset}}. #' A \code{QtlDataset} records no filter of its own; \code{keepSamples} #' narrows it once at the start of the run, and the other fields apply to -#' each extracted block, against that block's samples. +#' each extracted block, against the samples being fit. #' @section Unset fields: #' Every field defaults to \code{NULL}, meaning \strong{not set}: an unset #' field is absent from the result rather than carried as \code{NULL}, and @@ -2394,9 +3055,12 @@ setClass( #' it. #' @param keepSamples Sample ids to restrict to; \code{character(0)} keeps #' all samples. -#' @param keepVariants Variant ids to restrict to; \code{character(0)} keeps -#' all variants. -#' @param keepIndel Logical. Retain insertions and deletions. +#' @param keepVariants Variant ids or rsIDs to restrict to; \code{character(0)} +#' keeps all variants. Alleles match in either order and on either strand, +#' and an rsID is matched against the panel's own variant ids. +#' @param keepIndel Logical. Retain variants other than SNPs. A SNP has exactly +#' one of A, C, G, T for each allele, so \code{I}/\code{D} codes, \code{*} and +#' \code{N} count as indels. #' @return A \code{GenotypeFilterParam} object, a \code{\link{MethodParam}}. #' @seealso \code{\link{PanelFilterParam}}, \code{\link{SumstatsFilterParam}} #' @examples @@ -2478,11 +3142,13 @@ setClass( #' its default when the run resolves the bundle. \code{naAction} resolves #' to the pipeline's own policy: fine-mapping and TWAS fits cannot take a #' missing phenotype, so they drop the affected samples (and refuse -#' \code{"keep"}), while colocboost receives the phenotypes as they are. +#' \code{"keep"}), while colocboost keeps them, fitting each trait on the +#' samples that have a value for it. #' Outliers are kept unless \code{outlierAction = "drop"}. #' @param naAction How missing phenotype values are handled: \code{"drop"} #' the samples with any missing value among the selected traits, -#' \code{"impute"} each trait's mean, or \code{"keep"} them as they are. +#' \code{"impute"} each trait's mean, or \code{"keep"} them, each trait +#' fit on the samples it has (colocboost only). #' @param outlierAction \code{"keep"} or \code{"drop"} samples flagged as #' multivariate outliers by a robust (MCD) Mahalanobis distance, computed #' on the residualized phenotypes. diff --git a/R/QtlFineMappingResult.R b/R/QtlFineMappingResult.R index ba4664c20..ec18eaa2f 100644 --- a/R/QtlFineMappingResult.R +++ b/R/QtlFineMappingResult.R @@ -178,8 +178,8 @@ 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 +#' @param dataType Optional molecular type ("expression", "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"}. Where it is absent, the result diff --git a/R/colocPipeline.R b/R/colocPipeline.R index ff1c8dc16..d69e0d1a5 100644 --- a/R/colocPipeline.R +++ b/R/colocPipeline.R @@ -373,7 +373,7 @@ GwasFineMappingParam <- function( #' present; \code{\link{qtlEnrichmentPipeline}} emits all four, the context #' and trait set for a QTL outcome side). Output of #' \code{\link{qtlEnrichmentPipeline}}. When non-\code{NULL} the pair is -#' scored by enloc rather than coloc: the priors come from that table's +#' scored with enloc's priors: they come from that table's #' \code{colocP1} / \code{colocP2} / \code{colocP12} columns, and the #' report gains \code{RCP} and \code{LCP}. Pairs without a matching #' enrichment row fall back to the baseline priors with a warning. Default @@ -417,7 +417,7 @@ GwasFineMappingParam <- function( #' \code{hit1}, \code{hit2}, \code{PP.H0.abf} \ldots \code{PP.H4.abf}) and #' the reconciliation diagnostics \code{qtlRetainedMass} / #' \code{gwasRetainedMass}. When \code{enrichment} is supplied the pair is -#' scored by enloc instead, and four further columns appear: +#' scored with enloc's priors, and four further columns appear: #' \code{enrichment} and \code{p12Used} report the per-pair factor and the #' shared-signal prior enloc derived, while \code{RCP} and \code{LCP} are #' enloc's regional and locus colocalisation probabilities. Those two are @@ -1087,32 +1087,6 @@ colocPipeline <- function( ) } -# With an enrichment table this is an enloc run: the prior depends on -# whether the GWAS hit is itself the causal eQTL, which coloc.bf_bf cannot -# express, so the scoring is ours. Without one it is plain coloc. -# @noRd -.colocRunPairEither <- function( - aligned, - p12Info, - q, - gInfo, - useEnrichment, - ColocOptions -) { - if (useEnrichment) { - return(.colocRunPairEnloc(aligned, p12Info)) - } - .colocRunPair( - aligned, - p12Info$p12Used, - q, - gInfo, - p1 = p12Info$p1, - p2 = p12Info$p2, - ColocOptions = ColocOptions - ) -} - # Score one (QTL, GWAS) pair via coloc.bf_bf -> a summary row, or NULL when the # variants don't align or coloc fails / returns no summary. # @noRd @@ -1126,8 +1100,7 @@ colocPipeline <- function( alleleFlip, ColocOptions ) { - # Align variants between the QTL and GWAS LBF matrices by (chrom, pos, - # allele) tuple via matchVariants (see .colocAlignLbf). + # Align the LBF matrices by (chrom, pos, allele); see .colocAlignLbf. aligned <- .colocAlignLbf(qLbf, gInfo$lbf, alleleFlip = alleleFlip) if (is.null(aligned)) { return(NULL) @@ -1139,12 +1112,13 @@ colocPipeline <- function( enrichment = enrichment, priors = priors ) - pairRes <- .colocRunPairEither( + pairRes <- .colocRunPair( aligned, - p12Info, + p12Info$p12Used, q, gInfo, - useEnrichment = useEnrichment, + p1 = p12Info$p1, + p2 = p12Info$p2, ColocOptions = ColocOptions ) if (is.null(pairRes) || is.null(pairRes$summary)) { @@ -1170,6 +1144,9 @@ colocPipeline <- function( # The priors for one pair. Without an enrichment table these are the caller's # own; with one they are enloc's, taken from the table rather than derived # from a default -- which is the whole point of running in enrichment mode. +# The table's colocP1 is fastenloc's p1, the prior of a GWAS-only variant, and +# its colocP2 the QTL-only one, while coloc's p1 goes with its first trait, +# which here is the QTL side. # @noRd .colocResolvePriors <- function(gInfo, q, useEnrichment, enrichment, priors) { if (!useEnrichment) { @@ -1199,90 +1176,35 @@ colocPipeline <- function( en <- .colocEnrichmentPriors(enrichment, idx) list( enRow = en$enrichment, - p1 = en$p1, - p2 = en$p2, + p1 = en$p2, + p2 = en$p1, p12Used = en$p12 ) } -# Score every (QTL effect, GWAS effect) pair with the enloc configuration -# probabilities, in the shape coloc.bf_bf returns so the two modes' rows line -# up. RCP and LCP are reported alongside the hypothesis probabilities they -# are: RCP is PP.H4 and LCP is PP.H3 + PP.H4. +# The enrichment columns of the summary `sm`, present only when the pipeline +# used an enrichment table: RCP and LCP are enloc's names for PP.H4 and +# PP.H3 + PP.H4. A NULL `p12Info` carries the zero-row schema an empty result +# needs. # @noRd -.colocRunPairEnloc <- function(aligned, p12Info) { - priors <- list( - p1 = p12Info$p1, - p2 = p12Info$p2, - p12 = p12Info$p12Used - ) - qtlRows <- seq_len(nrow(aligned$qtl)) - gwasRows <- seq_len(nrow(aligned$gwas)) - grid <- expand.grid(idx1 = qtlRows, idx2 = gwasRows) - scored <- map2( - grid$idx1, - grid$idx2, - .colocEnlocOneEffectPair, - aligned = aligned, - priors = priors - ) - summary <- data.frame( - idx1 = grid$idx1, - idx2 = grid$idx2, - nSnps = ncol(aligned$qtl), - PP.H0.abf = map_dbl(scored, "ppH0"), - PP.H1.abf = map_dbl(scored, "ppH1"), - PP.H2.abf = map_dbl(scored, "ppH2"), - PP.H3.abf = map_dbl(scored, "ppH3"), - PP.H4.abf = map_dbl(scored, "ppH4"), - RCP = map_dbl(scored, "rcp"), - LCP = map_dbl(scored, "lcp"), - stringsAsFactors = FALSE - ) - list(summary = summary, results = .colocEnlocResults(scored, aligned)) -} - -# One (QTL effect, GWAS effect) pair, which is enloc's signal cluster. The -# LBF matrices hold SuSiE's natural-log Bayes factors and enloc reads log10 -# ones, so they are converted here; read as log10 unconverted, every cluster's -# PIPs came out sharper than SuSiE's own alpha. -# @noRd -.colocEnlocOneEffectPair <- function(i, j, aligned, priors) { - .enlocScoreEffectPair( - qtlLog10Bf = aligned$qtl[i, ] / log(10), - gwasLog10Bf = aligned$gwas[j, ] / log(10), - priors = priors - ) -} - -# The per-variant layer, matching what coloc.bf_bf's `results` carries: one -# column per scored pair, holding that pair's SNP-level colocalisation -# probabilities (enloc's SCP, which sums to RCP). -# @noRd -.colocEnlocResults <- function(scored, aligned) { - scp <- map(scored, "scp") - # A data.frame rather than a tibble: this stands in for what - # coloc.bf_bf's `results` carries, and .crPivotColocResults consumes both - # paths' output through the same code. - out <- as.data.frame( - exec(cbind, !!!scp), - stringsAsFactors = FALSE - ) - colnames(out) <- sprintf("SNP.PP.H4.row%d", seq_along(scp)) - mutate(out, snp = colnames(aligned$qtl)) -} - -# The enrichment columns, present only when the pipeline used an enrichment -# table. A NULL `p12Info` carries the zero-row schema an empty result needs. -# @noRd -.colocEnrichmentCols <- function(useEnrichment, p12Info) { +.colocEnrichmentCols <- function(useEnrichment, p12Info, sm = NULL) { if (!useEnrichment) { return(list()) } if (is.null(p12Info)) { - return(list(enrichment = numeric(0), p12Used = numeric(0))) + return(list( + enrichment = numeric(0), + p12Used = numeric(0), + RCP = numeric(0), + LCP = numeric(0) + )) } - list(enrichment = p12Info$enRow, p12Used = p12Info$p12Used) + list( + enrichment = p12Info$enRow, + p12Used = p12Info$p12Used, + RCP = sm[["PP.H4.abf"]], + LCP = sm[["PP.H3.abf"]] + sm[["PP.H4.abf"]] + ) } # Which QTL tuple and which GWAS tuple a summary row belongs to. @@ -1313,7 +1235,7 @@ colocPipeline <- function( as.data.frame(pairRes$summary, stringsAsFactors = FALSE) ) identityCols <- .colocRowIdentity(q, gInfo) - enrichmentCols <- .colocEnrichmentCols(useEnrichment, p12Info) + enrichmentCols <- .colocEnrichmentCols(useEnrichment, p12Info, sm) mutate( sm, !!!identityCols, @@ -1863,7 +1785,8 @@ colocPipeline <- function( # A blank result data frame for the no-pair case so callers downstream # do not have to special-case a NULL return. When `enriched = TRUE` the -# enrichment + p12Used columns are appended (the enloc-mode schema). +# enrichment, p12Used, RCP and LCP columns are appended (the enloc-mode +# schema). # @noRd .colocEmptyResult <- function(enriched = FALSE) { base <- tibble( @@ -1920,8 +1843,10 @@ colocPipeline <- function( # The enloc priors for one pair, read from the enrichment table rather than # derived here. qtlEnrichmentPipeline already computes them the way fastenloc # does -- p1 = (1 - P_eqtl) * expit(a0), p2 = P_eqtl / (1 + exp(a0 + a1)), -# p12 = P_eqtl * expit(a0 + a1) -- from the genome-wide QTL results, so using -# them is what makes this an enloc run rather than an approximation of one. +# p12 = P_eqtl * expit(a0 + a1) -- from the genome-wide QTL results. Given +# Bayes factors for both traits, fastenloc's configuration sum is coloc's up to +# a factor of 1 - p1 - p2 - p12 on H0 and on H3's inverse, so scoring these +# priors with coloc.bf_bf is the enloc run. # @noRd .colocEnrichmentPriors <- function(enrichment, idx) { needed <- c("colocP1", "colocP2", "colocP12") diff --git a/R/colocboostPipeline.R b/R/colocboostPipeline.R index c4e0d8600..284b9857d 100644 --- a/R/colocboostPipeline.R +++ b/R/colocboostPipeline.R @@ -155,10 +155,11 @@ #' \code{\link{fineMappingPipeline}}. The default filters nothing. #' \code{QtlDataset} and \code{MultiStudyQtlDataset} input only. #' @param phenotypeFilterParam The run's phenotype QC, built with -#' \code{\link{PhenotypeFilterParam}}. colocboost takes missing phenotype -#' values as they are, so unset keeps them; \code{"drop"} or -#' \code{"impute"} applies that instead, and \code{outlierAction = -#' "drop"} removes outlier samples. \code{QtlDataset} and +#' \code{\link{PhenotypeFilterParam}}. Unset fits each trait on the samples +#' that have a value for it, with genotypes filtered and residualized on just +#' those; \code{"drop"} or \code{"impute"} applies that instead, and +#' \code{outlierAction = "drop"} removes outlier samples, judging only those +#' with every trait. \code{QtlDataset} and #' \code{MultiStudyQtlDataset} input only. #' @param residualizationParam Covariate residualization settings for #' individual-level input, built with \code{\link{ResidualizationParam}}. @@ -456,11 +457,12 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { # Materialise an individual-level QtlDataset into the colocboost -# (X, Y, dict_YX, outcome_names) bundle. Each context becomes one X / -# Y pair; the YA matrices are split into single-trait columns and -# dict_YX maps each split column back to its X. Returns NULL when no -# context survives selection. pipCutoffToSkip (scalar or context-named -# vector) optionally drops weak-signal outcomes / contexts up front. +# (X, Y, dict_YX, outcome_names) bundle. Each context becomes one X / Y pair +# per set of samples its traits are observed on; the YA matrices are split into +# single-trait columns and dict_YX maps each split column back to its X. Returns +# NULL when no context survives selection. pipCutoffToSkip (scalar or +# context-named vector) optionally drops weak-signal outcomes / contexts up +# front. .cbIndividualBundle <- function( qd, contexts = NULL, @@ -485,7 +487,7 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { pipCutoffToSkip = pipCutoffToSkip, residualizationParam = residualizationParam ) - built <- compact(map(contexts, .cbBuildContextXY, p = p)) + built <- .cbBuildContexts(contexts, p) if (length(built) == 0L) { return(NULL) } @@ -597,10 +599,31 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { any(is_in(traitId, rownames(.qtlContextSe(qd, context = ctx)))) } -# Residualized (X, Y) for one context (sample-aligned + signal-screened), or -# NULL when the context should be skipped. +# Every context's (X, Y). Their genotypes all come from one read of the bundle's +# window, made when the first context with phenotypes asks for it (`read` is a +# lazily evaluated argument), over the samples any of the contexts can fit. # @noRd -.cbBuildContextXY <- function(ctx, p) { +.cbBuildContexts <- function( + contexts, + p, + read = .qtlReadBlock( + p$qd, + .qtlResolveVariantRegion(p$qd, p$traitId, p$region, p$cisWindow), + .cbFitSamplesOf(.qtlContextSamples(p$qd, contexts), p$samples), + p$genotypeFilter + ) +) { + list_flatten(map(contexts, .cbBuildContextXY, p = p, read = read)) +} + +# Residualized (X, Y) records for one context, sample-aligned and +# signal-screened: one per set of samples its traits are observed on, so usually +# one, and none when the context should be skipped. Each trait goes to +# colocboost free of missing values, with an X of just its samples. colocboost's +# own handling of a missing value pairs the outcomes of a shared X with the +# wrong X (1.0.9). +# @noRd +.cbBuildContextXY <- function(ctx, p, read = NULL) { rawY <- .cbResidualizedY( p$qd, ctx, @@ -610,22 +633,69 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { p$phenotypeFilter ) if (is.null(rawY) || ncol(rawY) == 0L) { - return(NULL) + return(list()) } + compact(map( + .cbObservedGroups(rawY, ctx), + .cbBuildObservedXY, + rawY = rawY, + ctx = ctx, + p = p, + read = read + )) +} + +# The traits of a context that can share an X, as groups of Y's columns: those +# observed on the same samples. A trait observed on fewer than two is left out, +# with a message. +# @noRd +.cbObservedGroups <- function(Y, ctx) { + groups <- .qtlMissingnessGroups(Y) + enough <- map_lgl(groups, .cbGroupHasSamples, Y = Y) + if (!all(enough)) { + few <- colnames(Y)[list_c(groups[!enough])] + inform(glue( + "colocboostPipeline: leaving out {str_flatten(few, ', ')} of ", + "context '{ctx}' (observed on fewer than two samples)." + )) + } + groups[enough] +} + +# @noRd +.cbGroupHasSamples <- function(at, Y) { + sum(!is.na(Y[, at[[1L]]])) >= 2L +} + +# The (X, Y) of the traits at `at`, on the samples they are observed on; NULL +# when there are no genotypes to fit them to. +# @noRd +.cbBuildObservedXY <- function(at, rawY, ctx, p, read) { + Y <- rawY[!is.na(rawY[, at[[1L]]]), at, drop = FALSE] rawX <- .cbResidualizedX( p$qd, ctx, p$traitId, p$region, p$cisWindow, - p$samples, + .cbFitSamplesOf(rownames(Y), p$samples), p$residualizationParam, - p$genotypeFilter + p$genotypeFilter, + read ) if (is.null(rawX) || ncol(rawX) == 0L) { return(NULL) } - .cbAlignContextXY(ctx, rawX, rawY, p$pipCutoffToSkip) + .cbAlignContextXY(ctx, rawX, Y, p$pipCutoffToSkip) +} + +# The samples a fit is on: its phenotypes', narrowed by the run's `samples`. +# @noRd +.cbFitSamplesOf <- function(observed, samples) { + if (is.null(samples)) { + return(observed) + } + intersect(observed, samples) } # One context's X and Y on the samples they share, Y signal-screened; NULL, @@ -655,8 +725,9 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { list(ctx = ctx, X = X, Y = Y) } -# colocboost's phenotype QC: colocboost takes missing phenotypes as they are, -# so an unset naAction keeps them, and every choice is available. +# colocboost's phenotype QC: an unset naAction keeps missing phenotypes, each +# trait fit on the samples it has (.cbBuildContextXY()), and every choice is +# available. # @noRd .cbResolvePhenotypeFilter <- function(phenotypeFilterParam) { .qtlResolvePhenotypeFilter( @@ -697,7 +768,10 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { ) } -# Residualized genotypes for one context (message + NULL on failure). +# Residualized genotypes for one context (message + NULL on failure). The +# window's read is forced first, outside the handler: every context shares it, +# so its failure is no one context's to skip, and skipping them all would end +# the run with no rows and no error. # @noRd .cbResidualizedX <- function( qd, @@ -707,8 +781,10 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { cisWindow, samples, residualizationParam = ResidualizationParam(), - genotypeFilter + genotypeFilter, + read = NULL ) { + force(read) try_fetch( .qtlResidualizedGenotypes( qd, @@ -716,7 +792,8 @@ setGeneric("colocboostPipeline", function(qtlData, gwasSumStats = NULL, ...) { window = .qtlResolveVariantRegion(qd, traitId, region, cisWindow), samples = samples, filter = genotypeFilter, - residualizationParam = residualizationParam + residualizationParam = residualizationParam, + read = read ), error = function(cnd) { msg <- glue( diff --git a/R/cpp11.R b/R/cpp11.R index 93f5da152..742808f93 100644 --- a/R/cpp11.R +++ b/R/cpp11.R @@ -1,7 +1,7 @@ # Generated by cpp11: do not edit by hand -bgenDosageCpp <- function(data, nSamples, alleles) { - .Call(`_pecotmr_bgenDosageCpp`, data, nSamples, alleles) +bgenDosageCpp <- function(data, nSamples, alleles, samples) { + .Call(`_pecotmr_bgenDosageCpp`, data, nSamples, alleles, samples) } bgenSampleIdsCpp <- function(ids, nSamples) { diff --git a/R/crossValidation.R b/R/crossValidation.R index 7b9967f24..b169cc046 100644 --- a/R/crossValidation.R +++ b/R/crossValidation.R @@ -379,9 +379,9 @@ NULL # bplapply() on a copy of the caller's BPPARAM seeded by .seededBpparam(). A # worker's error comes back as BiocParallel's report on every element; the -# first one is raised again with its own message, as a serial map would have -# raised it. The extra arguments reach `.f` through bplapply(), whose own -# formals include X and FUN, so pass an argument of either name by position. +# first one is raised again with its own message and cause, as a serial map +# would have raised it. The extra arguments reach `.f` through bplapply(), whose +# own formals include X and FUN, so pass an argument of either name by position. # @noRd #' @importFrom BiocParallel bplapply .bpMap <- function(.x, .f, ..., BPPARAM, seed = NULL) { @@ -397,7 +397,10 @@ NULL if (length(failed) == 0L) { cnd_signal(cnd) } - abort(conditionMessage(failed[[1L]])) + # Reclassed by BiocParallel, its message stops at its own line: a purrr + # error says only "In index: 1", and what it wraps is in `parent`. + first <- failed[[1L]] + abort(conditionMessage(first), parent = first$parent, call = first$call) } # @noRd diff --git a/R/ctwasPipeline.R b/R/ctwasPipeline.R index e5e99a66d..265238ed7 100644 --- a/R/ctwasPipeline.R +++ b/R/ctwasPipeline.R @@ -28,9 +28,15 @@ setClass( #' @param niterPrefit Integer. Iterations for the prefit EM. Default #' \code{3}. #' @param niter Integer. Iterations for the accurate EM. Default \code{30}. -#' @param varStructure How the prior variance is shared across groups; one of -#' \code{"shared_type"} (default), \code{"shared_context"}, -#' \code{"shared_nonSNP"}, \code{"shared_all"} or \code{"independent"}. +#' @param varStructure How the prior variance is shared across groups, a group +#' being a study, context and molecular type (see +#' \code{\link{ctwasPipeline}}). \code{"shared_nonSNP"} (default) pools it +#' across the gene groups, so a group with few causal genes borrows from the +#' others, and keeps the variants' apart. \code{"shared_all"} pools the +#' variants in as well, \code{"shared_type"} pools the groups of each +#' molecular type, \code{"shared_context"} those of each study and context, +#' and \code{"independent"} gives each group its own. With one type per study +#' and context, \code{"shared_context"} is \code{"independent"}. #' @param fallbackToPrefit Logical. When the accurate EM returns NaN on #' underpowered data, fall back to the prefit estimates rather than failing. #' Default \code{FALSE}. A pecotmr recovery step, not a cTWAS argument. @@ -43,10 +49,10 @@ CtwasPriorParam <- function( niterPrefit = 3L, niter = 30L, varStructure = c( - "shared_type", - "shared_context", "shared_nonSNP", "shared_all", + "shared_type", + "shared_context", "independent" ), fallbackToPrefit = FALSE @@ -125,10 +131,13 @@ setClass( ) #' @title Boundary-Region Merge Settings -#' @description Whether a high-PIP gene whose cis window straddles an -#' LD-block boundary has its adjacent regions merged and re-fine-mapped -#' after each run (\code{\link{mergeCtwasBoundaryRegions}}). -#' @param enabled Logical. Run the merge step. Default \code{FALSE}; the +#' @description Whether a high-PIP gene whose weights cross an LD-block +#' boundary has its regions merged and re-fine-mapped after each run +#' (\code{\link{mergeCtwasBoundaryRegions}}). A first pass fine-maps such a +#' gene with the LD of one block only, whatever its z-score drew from the +#' other, and can call a gene whose real signal is a variant in that other +#' block; merging is what removes those calls. +#' @param enabled Logical. Run the merge step. Default \code{TRUE}; the #' other three fields are read only when this is \code{TRUE}. #' @param pipThreshold Numeric. PIP threshold for selecting which boundary #' genes to merge. Default \code{0.5}. @@ -137,10 +146,10 @@ setClass( #' @param maxSnp Numeric. Per-merged-region SNP cap. Default \code{Inf}. #' @return A \code{BoundaryMergeParam} object, a \code{\link{MethodParam}}. #' @examples -#' BoundaryMergeParam(enabled = TRUE, pipThreshold = 0.8) +#' BoundaryMergeParam(pipThreshold = 0.8) #' @export BoundaryMergeParam <- function( - enabled = FALSE, + enabled = TRUE, pipThreshold = 0.5, filterCs = FALSE, maxSnp = Inf @@ -194,6 +203,25 @@ BoundaryMergeParam <- function( #' cannot converge on a single region, so callers must supply at least two #' blocks. #' +#' @section Boundary genes: A gene is modelled as cTWAS models it. Its z-score +#' reads all its weights, whichever blocks they fall in, its weight variance +#' treating the blocks as independent; it is fine-mapped in the block holding +#' the largest share of its absolute weights, with the part of its weights in +#' that block. Such a first pass can call a gene whose real signal is a +#' variant across the boundary, so by default a high-PIP gene crossing one has +#' its regions merged and fine-mapped again (\code{boundaryMergeParam}). +#' +#' @section Groups and molecular types: cTWAS estimates one prior per group, and +#' a group here is a study, context and molecular type, so two studies with +#' one context name stay apart. The type is the \code{dataType} the weights +#' record (\code{"QTL"} where they record none, all such contexts being one +#' type), and it matters to the fit under \code{varStructure = "shared_type"}, +#' or where one study and context holds two types. Results name a group +#' \code{{study}_{context}_{type}}, with the three also as columns of their +#' own, and a gene \code{{trait}|{study}_{context}_{type}_{method}}, the form +#' cTWAS reads the trait from. Groups whose names would coincide are refused, +#' as is a name holding \code{"|"}, on which cTWAS's EM splits a group. +#' #' @section cTWAS's log: cTWAS narrates each of its steps through the #' \code{logging} package, which writes to the console with \code{cat()}, #' out of reach of \code{suppressMessages()}. The pipeline keeps that @@ -220,16 +248,15 @@ BoundaryMergeParam <- function( #' through \code{\link{summaryStatsQc}}. #' @param twasWeights The per-gene weight source. Either (a) a FLAT #' \code{\link{TwasWeights}} / \code{QtlFineMappingResult} (or a homogeneous -#' list of them) carrying \code{region} provenance -- each gene is placed into -#' its home LD block internally by \code{start(region)} (matching cTWAS's -#' \code{p0} assignment rule); or (b) a pre-bucketed NAMED LIST keyed by -#' \code{region_id} (keys a SUBSET of \code{gwasSumStats}'s), used as-is. -#' Blocks without any TWAS weights still contribute their SNP-level signal to -#' ctwas's joint group prior estimate (the legacy whole-chromosome pattern -#' where only a few of many LD blocks carry gene weights). A gene whose cis -#' span straddles a block boundary is homed by its single anchor; the -#' cross-block signal is cTWAS's boundary-gene concern -#' (\code{\link{mergeCtwasBoundaryRegions}}), not placement. A +#' list of them), each gene read with the LD block holding most of its +#' weight; or (b) a pre-bucketed NAMED LIST keyed by \code{region_id} (keys a +#' SUBSET of \code{gwasSumStats}'s), used as-is. A gene's \code{traitPos}, if +#' it has one, gives its coordinates in the results, which are otherwise the +#' span of its weight variants. +#' Blocks without any TWAS weights do not feed the group priors by default: +#' cTWAS skips regions with fewer genes than its \code{min_gene}, 1. To let +#' their SNP-level signal in, set \code{min_gene = 0} in +#' \code{\link{CtwasEstParamOptions}}. A #' \code{TwasWeights} row of the run's method that carries a SuSiE fit and #' was narrowed since that fit warns (condition class #' \code{pecotmrFitCoverage}): the fit no longer lines up with the row's @@ -281,7 +308,7 @@ BoundaryMergeParam <- function( #' @param boundaryMergeParam Whether a high-PIP gene straddling an LD-block #' boundary has its regions merged and re-fine-mapped, built with #' \code{\link{BoundaryMergeParam}}: \code{enabled} (default -#' \code{FALSE}), \code{pipThreshold}, \code{filterCs} and +#' \code{TRUE}), \code{pipThreshold}, \code{filterCs} and #' \code{maxSnp}. #' @param keepSnps Logical (length 1). When \code{TRUE}, retain the #' context-agnostic SNP background of each run as extra \code{CtwasResult} @@ -297,7 +324,11 @@ BoundaryMergeParam <- function( #' \code{(gwasStudy, study, context, method)} run and cTWAS region. A #' single-context run gives each method its own rows; a multi-context #' (joint) run emits per-context rows sharing the same \code{jointContexts} -#' set and the jointly-estimated group priors. Each row holds that region's +#' set and the jointly-estimated group priors, and a multi-study run tags +#' \code{jointStudies} the same way. The contexts of a joint run +#' need not hold the same genes: each context's prior comes from its own +#' genes, and a context too small for cTWAS's \code{min_group_size} is left +#' out of the fit with a warning. Each row holds that region's #' per-gene fine-mapping posteriors as ranges, with the region's per-effect #' susie alpha table (\code{susieAlpha}), its \code{regionInfo} and the #' run's \code{groupPriors} in \code{mcols()}. Its \code{seqinfo()} names @@ -396,8 +427,8 @@ ctwasPipeline <- function( } # The cTWAS run proper: estimate the group priors, screen regions against -# them, fine-map what survived, and optionally re-fine-map the merged -# boundary regions. Each stage takes the ctwas option list as one named +# them, fine-map what survived, and re-fine-map the merged boundary regions +# unless told not to. Each stage takes the ctwas option list as one named # argument, so it is passed rather than spliced in as loose top-level args. # @noRd .ctwasRunStages <- function( @@ -438,7 +469,7 @@ ctwasPipeline <- function( } # One cTWAS run for method `m`: assemble inputs -> estimate params -> screen -> -# fine-map (optionally boundary-merge) -> per-context row-specs. +# fine-map -> boundary-merge -> per-context row-specs. # @noRd .ctwasRunMethod <- function( m, @@ -482,7 +513,7 @@ ctwasPipeline <- function( ) } -# Boundary-gene region merging: split a high-PIP straddling gene's adjacent +# Boundary-gene region merging: merge a high-PIP boundary gene's adjacent # regions and re-fine-map. Merge-transparent downstream (keyed by gene id). # @noRd .ctwasMaybeMerge <- function( @@ -527,8 +558,9 @@ ctwasPipeline <- function( #' @inheritParams ctwasPipeline #' @return A list with elements \code{z_snp}, \code{z_gene} (NULL when no #' \code{twasZ}), \code{weights}, \code{region_info}, \code{snp_map}, -#' \code{LD_map}, \code{LD_loader_fun}, \code{snpinfo_loader_fun}, and -#' \code{resolvedMethod}. +#' \code{LD_map}, \code{LD_loader_fun}, \code{snpinfo_loader_fun}, +#' \code{resolvedMethod}, and \code{gwas_n}, the GWAS's median sample size +#' (\code{NA} when it records none). #' @examples #' data(ctwasWeightsExample) #' ldDir <- system.file("extdata", "ld_reference", "chr22", @@ -636,7 +668,7 @@ assembleCtwasInputs <- function( ) } -# The bundle's terminal: .ctwasBuildWeights and below take scalars, under the +# The bundle's terminal: .ctwasGeneContext() and below read scalars, under the # names they have always had there. # @noRd .ctwasPruningCutoffs <- function(variantPruningParam) { @@ -737,9 +769,9 @@ assembleCtwasInputs <- function( .withRowLdRefs(block, .subsetLdRefToIds(refs[[1L]], as.list(block))) } -# Resolve a flat weight source into per-region buckets (cTWAS's p0 start-of- -# region rule; a pre-bucketed named list passes through), then validate it is a -# named list of TwasWeights / QtlFineMappingResult with no stray region keys. +# Resolve a flat weight source into per-region buckets (.ctwasBucketWeights(); a +# pre-bucketed named list passes through), then validate it is a named list of +# TwasWeights / QtlFineMappingResult with no stray region keys. # Returns the resolved twasWeights. # @noRd .ctwasResolveAndValidateWeights <- function(twasWeights, gwasSumStats) { @@ -968,11 +1000,9 @@ assembleCtwasInputs <- function( ) } -# Second pass: build per-block weight lists. The GLOBAL gwasSnpIds bounds each -# gene's cis SPAN, so a gene whose cis-window straddles block boundaries is -# still recognised as one; the per-region snpMap bounds the weight vector that -# is actually fitted, so susie_rss never sees a variant the region has no LD -# for. Weight names are prefixed with the region id. +# Second pass: the genes' weight entries, keyed by gene id. Each gene is +# prepared from the block its weights were placed in, whichever blocks they +# reach, and the variances then read each block's LD once, over all the genes. # @noRd .ctwasSecondPass <- function( regionIds, @@ -983,17 +1013,24 @@ assembleCtwasInputs <- function( cutoffs, globalGwasSnpIds ) { + ctx <- .ctwasGeneContext( + fp$globalPanelInfo, + fp$globalVariance, + globalGwasSnpIds, + fineMappingResult, + cutoffs + ) perRegion <- map( regionIds, - .ctwasRegionBlockWeights, + .ctwasRegionGenes, twasWeights = twasWeights, - fp = fp, resolvedMethod = resolvedMethod, - fineMappingResult = fineMappingResult, - cutoffs = cutoffs, - globalGwasSnpIds = globalGwasSnpIds + ctx = ctx + ) + .ctwasGeneEntries( + .ctwasConcat(compact(perRegion)), + unname(fp$ldPanelsByRegion) ) - .ctwasConcat(compact(perRegion)) } # Concatenate per-region lists, empty-safe. @@ -1005,18 +1042,10 @@ assembleCtwasInputs <- function( list_c(pieces) } -# One region's block weights, region-qualified, or NULL when the region has no -# weights for the resolved method. +# The genes placed in one region, prepared up to their weight variances, or NULL +# when the region has no weights for the resolved method. # @noRd -.ctwasRegionBlockWeights <- function( - rid, - twasWeights, - fp, - resolvedMethod, - fineMappingResult, - cutoffs, - globalGwasSnpIds -) { +.ctwasRegionGenes <- function(rid, twasWeights, resolvedMethod, ctx) { tw <- twasWeights[[rid]] if (is.null(tw)) { return(NULL) @@ -1025,23 +1054,7 @@ assembleCtwasInputs <- function( if (is.null(twMethod)) { return(NULL) } - blockWeights <- .ctwasBuildWeights( - twMethod, - fp$ldPanelsByRegion[[fp$ldFileByRegion[[rid]]]], - fineMappingResult = fineMappingResult, - twasWeightCutoff = cutoffs$twasWeightCutoff, - csMinCor = cutoffs$csMinCor, - minPipCutoff = cutoffs$minPipCutoff, - maxNumVariants = cutoffs$maxNumVariants, - gwasSnpIds = globalGwasSnpIds, - regionSnpIds = fp$snpMap[[rid]]$id, - globalPanelInfo = fp$globalPanelInfo, - globalVariance = fp$globalVariance - ) - if (length(blockWeights) == 0L) { - return(NULL) - } - set_names(blockWeights, str_c(rid, "|", names(blockWeights))) + .ctwasPreparedGenes(twMethod, ctx) } # Concatenate the per-region pieces into the ctwas-shape input list. @@ -1080,10 +1093,21 @@ assembleCtwasInputs <- function( LD_map = ldMap, LD_loader_fun = .ctwasMultiBlockLdLoader(fp$ldPanelsByRegion), snpinfo_loader_fun = .ctwasMultiBlockSnpInfoLoader(fp$ldPanelsByRegion), - resolvedMethod = resolvedMethod + resolvedMethod = resolvedMethod, + gwas_n = .ctwasGwasN(zSnp) ) } +# The GWAS's sample size, one number as cTWAS's summaries read it: the median +# over its variants, NA when it records none. +# @noRd +.ctwasGwasN <- function(zSnp) { + if (is.null(zSnp$N) || all(is.na(zSnp$N))) { + return(NA_real_) + } + median(zSnp$N, na.rm = TRUE) +} + # estCtwasGroupPriors(): CtwasPriorParam is the bundle's terminal; the EM # helpers take its fields as plain scalars. assemble_region_data does not echo # z_gene back, so the precomputed z_gene passed in is propagated onto the @@ -1095,7 +1119,11 @@ assembleCtwasInputs <- function( #' \code{region_data} from the inputs and runs \code{ctwas::est_param} (prefit #' EM + accurate EM) to estimate the group prior probabilities and prior #' variances. Returns the input state plus \code{region_data}, -#' \code{boundary_genes}, \code{z_gene}, and \code{param}. +#' \code{boundary_genes}, \code{z_gene}, and \code{param}. A context with +#' fewer genes than \code{min_group_size} gets no prior: cTWAS leaves it out +#' of the fit, and a warning names it. To keep it, lower +#' \code{min_group_size} in both \code{\link{CtwasRegionDataOptions}} and +#' \code{\link{CtwasEstParamOptions}}. #' #' @param inputs A list returned by \code{\link{assembleCtwasInputs}}. #' @param ctwasPriorParam The EM settings, built with @@ -1148,6 +1176,7 @@ estCtwasGroupPriors <- function( threads, methodArgs$regionData ) + .ctwasWarnDroppedGroups(zGene, regionData) boundaryGenes <- .ctwasBoundaryGenes( inputs, threads, @@ -1173,6 +1202,30 @@ estCtwasGroupPriors <- function( ) } +# cTWAS leaves a group with too few genes (`min_group_size`) out of the fit and +# says so only in a log pecotmr quiets, so its context would come back with no +# priors and no posteriors, and nothing to say why. Contexts holding different +# genes make this reachable: one can fall short while another clears it. The +# advice names both options because `est_param` holds its own limit, and stops +# on a group under it where `assemble_region_data` drops one. +# @noRd +.ctwasWarnDroppedGroups <- function(zGene, regionData) { + dropped <- !is_in(zGene$group, list_c(map(regionData, "groups"))) + if (!any(dropped)) { + return(invisible()) + } + counts <- lengths(split(zGene$id[dropped], zGene$context[dropped])) + who <- str_c(names(counts), " (", counts, " gene(s))") + warn(glue( + "estCtwasGroupPriors: cTWAS left context(s) ", + "{str_flatten(who, ', ')} out of the fit, so they have no priors or ", + "posteriors: it drops a group with fewer genes than `min_group_size`. ", + "To keep them, lower `min_group_size` in both ", + "CtwasRegionDataOptions() and CtwasEstParamOptions(): the second ", + "stops on a group under its own limit." + )) +} + # est_param (accurate EM), falling back to a converged prefit EM on ANY accurate # error when fallbackToPrefit is set. The accurate EM fails on degenerate inputs # in several version-dependent ways (NAs / "No regions selected!" / NaN @@ -1298,13 +1351,18 @@ finemapCtwasRegions <- function( threads, methodArgs ) - # Repair cTWAS's molecular_id mislabel (first-"|" split of our composite - # id). list( z_gene = screenResult$z_gene, param = screenResult$param, - finemap_res = .ctwasFixMolecularId(fmRes$finemap_res), - susie_alpha_res = .ctwasFixMolecularId(fmRes$susie_alpha_res), + gwas_n = screenResult$gwas_n, + finemap_res = .ctwasFixMolecularId( + fmRes$finemap_res, + screenResult$weights + ), + susie_alpha_res = .ctwasFixMolecularId( + fmRes$susie_alpha_res, + screenResult$weights + ), region_data = screenResult$region_data, boundary_genes = screenResult$boundary_genes, screen_res = screenResult$screen_res, @@ -1322,13 +1380,13 @@ finemapCtwasRegions <- function( #' Merge boundary cTWAS regions and re-fine-map #' -#' @description Optional step 4 of the cTWAS pipeline (default-off region -#' merging). A gene whose cis window straddles an LD-block boundary (a -#' \code{boundary_genes} member) is split across two regions in the first-pass -#' fine-mapping. This step selects the high-PIP boundary genes, merges each -#' one's adjacent regions into a single region, re-runs fine-mapping on the -#' merged regions, and splices the updated results back into the -#' \code{\link{finemapCtwasRegions}} output. Thin wrapper over +#' @description Step 4 of the cTWAS pipeline, on by default in +#' \code{\link{ctwasPipeline}}. A gene whose weights cross an LD-block +#' boundary (a \code{boundary_genes} member) is fine-mapped in one block in +#' the first pass, with only that block's LD. This step selects the high-PIP +#' boundary genes, merges each one's adjacent regions into a single region, +#' re-runs fine-mapping on the merged regions, and splices the updated results +#' back into the \code{\link{finemapCtwasRegions}} output. Thin wrapper over #' \code{ctwas::postprocess_region_merging()} (or #' \code{ctwas::postprocess_region_merging_noLD()} when the inputs carry no LD #' loaders). @@ -1437,7 +1495,8 @@ mergeCtwasBoundaryRegions <- function( } # Write region-merging outputs back onto the finemap result (only components -# ctwas actually returned). +# ctwas actually returned). Rows fine-mapped again in a merged region have their +# molecular_id read off the id again, as the first pass's do. # @noRd .ctwasApplyMergeResult <- function(finemapResult, res) { regionUpdates <- compact(list( @@ -1448,8 +1507,14 @@ mergeCtwasBoundaryRegions <- function( )) list_assign( finemapResult, - finemap_res = res$updated_finemap_res, - susie_alpha_res = res$updated_susie_alpha_res, + finemap_res = .ctwasFixMolecularId( + res$updated_finemap_res, + finemapResult$weights + ), + susie_alpha_res = .ctwasFixMolecularId( + res$updated_susie_alpha_res, + finemapResult$weights + ), merge_res = res, !!!regionUpdates ) @@ -1710,7 +1775,7 @@ mergeCtwasBoundaryRegions <- function( if (!okTw) { msg <- glue( "`twasWeights` must be a TwasWeights / QtlFineMappingResult ", - "weight source (placed into LD blocks internally by region), or ", + "weight source (placed into LD blocks by its weights), or ", "a per-region named list keyed by region_id ", "(got {class(twasWeights)[[1L]]})." ) @@ -1722,7 +1787,7 @@ mergeCtwasBoundaryRegions <- function( # a NAMED plain list (not an S4 collection) of weight collections. Its keys are # validated against the block grid downstream (the `extra_tw_keys` check). A # flat collection or an UNNAMED list is instead treated as a flat weight source -# to place internally by region. +# to place by its weights. # @noRd .ctwasIsPreBucketed <- function(tw) { is.list(tw) && @@ -1767,34 +1832,68 @@ mergeCtwasBoundaryRegions <- function( abort(msg) } -# Place each gene (row) of a flat weight source into its home LD block. The -# anchor is start(region) -- matching cTWAS's own `assign_region_data` rule, -# which homes a gene by its p0 (single point) into the block where -# p0 in [start, stop), each block's [start, stop) being the one its -# region_info carries (.ctwasBuildSingleRegionInfo()). Returns a region_id per -# row (NA when the anchor falls in no block). A gene whose cis SPAN straddles -# a boundary is still homed by its single anchor here; the cross-block signal -# is cTWAS's boundary-gene concern (get_boundary_genes / -# postprocess_region_merging), not placement. +# Place each gene (row) of a flat weight source into the LD block its weights +# are read with, from those weights: the block holding the largest sum of +# |weight| over the row's methods, the rule cTWAS homes a gene crossing blocks +# by (adjust_boundary_gene_region_assignment). Returns a region_id per row, NA +# when no weight falls in a block. This only divides the work: the block a gene +# is fine-mapped in is cTWAS's choice, from its weights (assemble_region_data). # @noRd -.ctwasPlaceByAnchor <- function(region, gwasSumStats) { +#' @importFrom dplyr slice_max +.ctwasPlaceByWeights <- function(combined, gwasSumStats) { ids <- names(gwasSumStats) - blockGr <- .ctwasBlockWindows(gwasSumStats, ids) - aChr <- as.character(GenomicRanges::seqnames(region)) - aPos <- GenomicRanges::start(region) - bChr <- as.character(GenomicRanges::seqnames(blockGr)) - bS <- GenomicRanges::start(blockGr) - # The half-open stop region_info hands cTWAS: one past the window's end. - bE <- GenomicRanges::end(blockGr) + 1L - map_chr( - seq_along(aPos), - .ctwasBlockIdForVariant, - aPos = aPos, - aChr = aChr, - bChr = bChr, - bS = bS, - bE = bE, - ids = ids + windows <- .ctwasBlockWindows(gwasSumStats, ids) + sizes <- list_rbind( + map(.ctwasWeightUnits(combined), .ctwasUnitWeightSizes, x = combined), + ptype = tibble( + row = integer(), + chrom = character(), + pos = integer(), + size = double() + ) + ) + placed <- sizes |> + mutate(block = .ctwasBlockAt(.data$chrom, .data$pos, windows)) |> + filter(!is.na(.data$block)) |> + summarise(size = sum(.data$size), .by = all_of(c("row", "block"))) |> + slice_max(.data$size, n = 1L, with_ties = FALSE, by = all_of("row")) + ids[placed$block[match(seq_len(nrow(combined)), placed$row)]] +} + +# Where one unit's weights lie and how large they are, a variant of several +# columns' weights counting them all. NULL when it holds none. +# @noRd +.ctwasUnitWeightSizes <- function(unit, x) { + wr <- .rowResolveWeights(.ctwasUnitParts(x, unit)) + if (length(wr$variantIds) == 0L) { + return(NULL) + } + parsed <- parseVariantId(wr$variantIds) + tibble( + row = unit$row, + chrom = parsed$chrom, + pos = as.integer(parsed$pos), + size = rowSums(abs(as.matrix(wr$weights))) + ) +} + +# The index of the block window holding each position, NA where none does: a +# window's stop is the half-open one region_info hands cTWAS, one past its end. +# Both sides take every chromosome either has, since one only a side has warns. +# @noRd +.ctwasBlockAt <- function(chrom, pos, windows) { + windowChrom <- as.character(GenomicRanges::seqnames(windows)) + chroms <- union(windowChrom, chrom) + GenomicRanges::findOverlaps( + GenomicRanges::GRanges( + factor(chrom, levels = chroms), + IRanges::IRanges(pos, width = 1L) + ), + GenomicRanges::GRanges( + factor(windowChrom, levels = chroms), + IRanges::ranges(windows) + ), + select = "first" ) } @@ -1838,7 +1937,7 @@ mergeCtwasBoundaryRegions <- function( window } -# The genes homed into region `rid`, every row on that region's LD sketch, or +# The genes placed in region `rid`, every row on that region's LD sketch, or # NULL when nothing landed there. # @noRd .ctwasBucketForRegion <- function(rid, combined, home, gwasSumStats) { @@ -1850,36 +1949,18 @@ mergeCtwasBoundaryRegions <- function( } # Bucket a flat weight source into a per-region named list keyed to the block -# grid, homing each gene by start(region). Each per-block sub-collection carries +# grid, each gene where its weights lie. Each per-block sub-collection carries # that block's GWAS LD sketch (the panel its weights are harmonized against, and # what the downstream match-check expects). # @noRd .ctwasBucketWeights <- function(weights, gwasSumStats) { combined <- .ctwasCombineWeightSources(weights) - # Placement anchors on the GENE's own position, not on a stored analysis - # window and not on the span of its weight variants. Two genes at - # different loci can legitimately share a weight variant set, so the - # variant span cannot tell them apart; traitPos can (spec 4.4, which keeps - # gene coordinates in mcols for exactly this). - if (!is_in("traitPos", .tupleColumnNames(combined))) { - msg <- glue( - "assembleCtwasInputs: the weight source carries no `traitPos` ", - "provenance, which internal LD-block placement requires ", - "(produced by twasWeightsPipeline / fineMappingPipeline). Supply ", - "a pre-bucketed per-region named list if placement was done ", - "upstream." - ) - abort(msg) - } - home <- .ctwasPlaceByAnchor( - traitPosition(combined), - gwasSumStats - ) + home <- .ctwasPlaceByWeights(combined, gwasSumStats) unplaced <- sum(is.na(home)) if (unplaced > 0L) { msg <- glue( - "assembleCtwasInputs: {unplaced} gene(s) whose traitPos anchor ", - "fell in no LD block were dropped." + "assembleCtwasInputs: {unplaced} gene(s) with no weight in any LD ", + "block were dropped." ) warn(msg) } @@ -1896,7 +1977,7 @@ mergeCtwasBoundaryRegions <- function( if (length(out) == 0L) { msg <- glue( "assembleCtwasInputs: no gene placed into any LD block. ", - "Check that the genes' trait positions fall inside the GWAS ", + "Check that the genes' weight variants fall inside the GWAS ", "blocks' windows (their `region`), on the same genome build." ) abort(msg) @@ -1905,7 +1986,7 @@ mergeCtwasBoundaryRegions <- function( } # Resolve `twasWeights` to a per-region named list: pass a pre-bucketed list -# through, otherwise place a flat weight source by region. +# through, otherwise place a flat weight source by its weights. # @noRd .ctwasResolveWeightBuckets <- function(twasWeights, gwasSumStats) { if (.ctwasIsPreBucketed(twasWeights)) { @@ -1941,45 +2022,25 @@ mergeCtwasBoundaryRegions <- function( studies } -# Extract field `i` (as character) from each `region|study|context|trait|method` -# split in `parts`. -# @noRd -.ctwasPickField <- function(i, parts) { - map_chr(parts, i) -} - -# Parse the cTWAS gene ids (`region|study|context|trait|method`) that name the -# assembled weights list into their identity components. `method` is the LAST -# field and `trait` everything between context and method, so a trait that -# itself contains "|" is preserved. +# The genes of a run, from their weight records: what cTWAS's strings do not +# carry apart, or carry only as one label. No id is parsed for this. # @noRd -.ctwasParseGeneIds <- function(ids) { - parts <- str_split(ids, "\\|") - n <- lengths(parts) - if (any(n < 5L)) { - msg <- glue( - "ctwasPipeline: malformed cTWAS gene id(s): ", - "{str_flatten(ids[n < 5L], ', ')} ", - "(expected 'region|study|context|trait|method')." - ) - abort(msg) - } +.ctwasGeneTable <- function(weights) { + records <- unname(weights) tibble( - id = ids, - rid = .ctwasPickField(1L, parts), - study = .ctwasPickField(2L, parts), - context = .ctwasPickField(3L, parts), - trait = map_chr(parts, .ctwasTraitField), - method = map_chr(parts, .ctwasMethodField) + id = names(weights) %||% character(0), + study = map_chr(records, "study"), + context = map_chr(records, "qtl_context"), + trait = map_chr(records, "gene_name"), + method = map_chr(records, "method") ) } -# cTWAS's finemap_regions derives `molecular_id` by splitting the gene id on the -# FIRST "|", which mislabels our composite `region|study|context|trait|method` -# id (it takes the region as the molecular_id). Restore the true trait for gene -# rows; SNP-background rows (variant ids, no "|") are left untouched. +# cTWAS derives `molecular_id` from a gene id's first "|" field, which cuts a +# trait whose own id holds a "|" short. Restored from the gene's record; +# SNP-background rows are left untouched. # @noRd -.ctwasFixMolecularId <- function(df) { +.ctwasFixMolecularId <- function(df, weights) { if ( is.null(df) || !is.data.frame(df) || @@ -1988,46 +2049,19 @@ mergeCtwasBoundaryRegions <- function( ) { return(df) } - isGene <- lengths(str_split(as.character(df$id), "\\|")) >= 5L - if (any(isGene)) { - return(mutate( - df, - molecular_id = replace( - .data$molecular_id, - isGene, - .ctwasParseGeneIds(as.character(df$id)[isGene])$trait - ) - )) - } - df -} - -# Enforce the multi-context joint-model invariant: every context in a run must -# carry the SAME set of genes (traits). A cTWAS joint fit couples the contexts -# through shared group priors; contexts with disjoint gene sets make the joint -# model meaningless. No-op for a single-context run. -# @noRd -.ctwasAssertSharedGenes <- function(parsed) { - byCtx <- split(parsed$trait, parsed$context) - if (length(byCtx) < 2L) { - return(invisible()) + ids <- as.character(df$id) + isGene <- is_in(ids, names(weights)) + if (!any(isGene)) { + return(df) } - geneSets <- map(byCtx, .ctwasSortUnique) - ref <- geneSets[[1L]] - mismatch <- names(geneSets)[ - !map_lgl(geneSets, identical, ref) - ] - if (length(mismatch) > 0L) { - msg <- glue( - "ctwasPipeline: multi-context cTWAS requires the SAME gene set in ", - "every context (the joint model is only meaningful when genes are ", - "shared across contexts). Context(s) differing from the ", - "reference '{names(geneSets)[[1L]]}' ({length(ref)} gene(s)): ", - "{str_flatten(mismatch, ', ')}." + mutate( + df, + molecular_id = replace( + .data$molecular_id, + isGene, + map_chr(unname(weights[ids[isGene]]), "molecular_id") ) - abort(msg) - } - invisible() + ) } # Subset a ctwas result frame (finemap_res / susie_alpha_res) to the rows whose @@ -2060,9 +2094,8 @@ mergeCtwasBoundaryRegions <- function( # The coordinates are recoverable in-package without ctwas's mapping_table: # # SNP rows the `id` IS a variant id, so it renders its own range (4.1a) -# gene rows the id is region|study|context|trait|method, and the run's -# weights carry that gene's trait_pos (falling back to the -# weight-variant span chrom/p0/p1) +# gene rows the run's weights carry that gene's trait_pos under its id +# (falling back to the weight-variant span chrom/p0/p1) # # Without this a CtwasResult cannot answer "which genes did cTWAS implicate in # this window" without the caller redoing the join by hand. @@ -2079,11 +2112,6 @@ mergeCtwasBoundaryRegions <- function( # @noRd .ctwasOneGeneCoord <- function(w) { - # A weights element that is not a list carries no gene metadata (some - # callers pass a bare weight vector), so there is nothing to place it by. - if (!is.list(w)) { - return(NULL) - } tp <- w$trait_pos if (methods::is(tp, "GRanges") && length(tp) == 1L) { return(list( @@ -2189,13 +2217,31 @@ mergeCtwasBoundaryRegions <- function( .ctwasMkEntry <- function(fm, sa, runResult) { geneCoords <- .ctwasGeneCoords(runResult$weights) CtwasResultEntry( - posteriors = .ctwasRangePayload(fm, geneCoords), - susieAlpha = .ctwasRangePayload(sa, geneCoords), + posteriors = .ctwasRangePayload(.ctwasReportGroups(fm), geneCoords), + susieAlpha = .ctwasRangePayload(.ctwasReportGroups(sa), geneCoords), groupPriors = runResult$param, regionInfo = .ctwasRangeRegionInfo(runResult$region_info) ) } +# A result table with its groups named as results name them +# (.ctwasGroupLabel()). +# @noRd +.ctwasReportGroups <- function(df) { + if (is.null(df) || !is_in("group", names(df))) { + return(df) + } + mutate(df, group = .ctwasGroupLabel(.data$group)) +} + +# cTWAS's name for a group, `context|type`, as results name it. The "|" is the +# one cTWAS's EM needs (.ctwasGeneEntry()), of no use to anyone reading a +# result. +# @noRd +.ctwasGroupLabel <- function(groups) { + str_replace(groups, fixed("|"), "_") +} + # region_info as a GRanges. pecotmr builds this table itself, each block's # window as cTWAS's half-open [start, stop), so the range ends one base before # `stop` -- the window again (a merged region, the span of the windows it @@ -2236,38 +2282,76 @@ mergeCtwasBoundaryRegions <- function( # ctwas run is reconstructable from `ctwasPosteriors()` / `susieAlpha()`. # @noRd .ctwasRunToRows <- function(runResult, gwasStudy, method, keepSnps = FALSE) { - geneIds <- names(runResult$weights) - if (is.null(geneIds) || length(geneIds) == 0L) { + genes <- .ctwasGeneTable(runResult$weights) + if (nrow(genes) == 0L) { return(list()) } - parsed <- .ctwasParseGeneIds(geneIds) - contexts <- unique(parsed$context) - .ctwasAssertSharedGenes(parsed) - jointStr <- if (length(contexts) > 1L) { - str_flatten(sort(unique(contexts)), ",") - } else { - NA_character_ - } + runs <- distinct(genes, .data$study, .data$context) + joint <- list( + studies = .ctwasJointLabel(runs$study), + contexts = .ctwasJointLabel(runs$context) + ) fmDf <- .ctwasAsDf(runResult$finemap_res) saDf <- .ctwasAsDf(runResult$susie_alpha_res) - contextRows <- map( - contexts, + scored <- list_assign(runResult, param = .ctwasScoredParam(runResult)) + contextRows <- map2( + runs$study, + runs$context, .ctwasContextRow, - parsed = parsed, + genes = genes, gwasStudy = gwasStudy, method = method, - jointStr = jointStr, + joint = joint, fmDf = fmDf, saDf = saDf, - runResult = runResult + runResult = scored ) snpRow <- if (!keepSnps) { NULL } else { - .ctwasSnpRow(gwasStudy, method, jointStr, fmDf, saDf, runResult) + .ctwasSnpRow(gwasStudy, method, joint, fmDf, saDf, scored) } - rows <- c(contextRows, compact(list(snpRow))) - rows + c(contextRows, compact(list(snpRow))) +} + +# The studies or contexts a joint run spans, NA for a run over one. +# @noRd +.ctwasJointLabel <- function(values) { + members <- sort(unique(values)) + if (length(members) > 1L) str_flatten(members, ",") else NA_character_ +} + +# The run's priors with what results report beside them: each group's study, +# context and type, and cTWAS's summary of the priors. +# @noRd +.ctwasScoredParam <- function(runResult) { + param <- runResult$param + if (is.null(param$group_prior)) { + return(param) + } + c( + param, + list( + group_info = .ctwasGroupInfo(runResult$weights), + group_summary = .ctwasGroupSummary(param, runResult$gwas_n) + ) + ) +} + +# Each group's study, context and molecular type, under cTWAS's name for it. +# @noRd +.ctwasGroupInfo <- function(weights) { + records <- unname(weights) + genes <- tibble( + group = map_chr(records, .ctwasEntryGroup), + study = map_chr(records, "study"), + context = map_chr(records, "qtl_context"), + type = map_chr(records, "type") + ) + bind_rows( + distinct(genes), + tibble(group = "SNP", study = "SNP", context = "SNP", type = "SNP") + ) } # as.data.frame(), passing NULL through. @@ -2276,38 +2360,30 @@ mergeCtwasBoundaryRegions <- function( if (is.null(x)) NULL else as.data.frame(x) } -# One (gwasStudy, study, context, method) row-spec for a context. Errors if a -# context mixes multiple QTL studies (one study per context). +# One (gwasStudy, study, context, method) row-spec. # @noRd .ctwasContextRow <- function( - cx, - parsed, + study, + context, + genes, gwasStudy, method, - jointStr, + joint, fmDf, saDf, runResult ) { - inCx <- parsed$context == cx - studyCx <- unique(parsed$study[inCx]) - if (length(studyCx) != 1L) { - msg <- glue( - "ctwasPipeline: context '{cx}' mixes multiple QTL studies ", - "({str_flatten(studyCx, ', ')}); one study per context." - ) - abort(msg) - } - idsCx <- parsed$id[inCx] + ids <- genes$id[genes$study == study & genes$context == context] list( gwasStudy = gwasStudy, - study = studyCx, - context = cx, + study = study, + context = context, method = method, - jointContexts = jointStr, + jointStudies = joint$studies, + jointContexts = joint$contexts, entry = .ctwasMkEntry( - .ctwasSubsetById(fmDf, idsCx), - .ctwasSubsetById(saDf, idsCx), + .ctwasSubsetById(fmDf, ids), + .ctwasSubsetById(saDf, ids), runResult ) ) @@ -2316,7 +2392,7 @@ mergeCtwasBoundaryRegions <- function( # The SNP-level row-spec (study = context = "SNP"), or NULL when no SNP-level # finemap / susie-alpha rows exist. # @noRd -.ctwasSnpRow <- function(gwasStudy, method, jointStr, fmDf, saDf, runResult) { +.ctwasSnpRow <- function(gwasStudy, method, joint, fmDf, saDf, runResult) { snpFm <- .ctwasSubsetSnp(fmDf) snpSa <- .ctwasSubsetSnp(saDf) if (is.null(snpFm) && is.null(snpSa)) { @@ -2327,14 +2403,15 @@ mergeCtwasBoundaryRegions <- function( study = "SNP", context = "SNP", method = method, - jointContexts = jointStr, + jointStudies = joint$studies, + jointContexts = joint$contexts, entry = .ctwasMkEntry(snpFm, snpSa, runResult) ) } # Assemble accumulated per-run row-specs (from .ctwasRunToRows) into a single -# CtwasResult; the jointContexts column is omitted when every row is -# single-context. Shared by ctwasPipeline (across methods) and asCtwasResult. +# CtwasResult; a joint column is omitted when no row is joint on it. Shared by +# ctwasPipeline (across methods) and asCtwasResult. # @noRd .ctwasRowsToResult <- function(rows) { if (length(rows) == 0L) { @@ -2344,6 +2421,7 @@ mergeCtwasBoundaryRegions <- function( ) abort(msg) } + jointStudies <- map_chr(rows, "jointStudies") jointContexts <- map_chr(rows, "jointContexts") CtwasResult( gwasStudy = map_chr(rows, "gwasStudy"), @@ -2351,6 +2429,7 @@ mergeCtwasBoundaryRegions <- function( context = map_chr(rows, "context"), method = map_chr(rows, "method"), entry = map(rows, "entry"), + jointStudies = if (any(!is.na(jointStudies))) jointStudies else NULL, jointContexts = if (any(!is.na(jointContexts))) jointContexts else NULL ) } @@ -2378,7 +2457,7 @@ mergeCtwasBoundaryRegions <- function( } # The single weight method a finemap result was built for, read from the -# assembled weight ids. Errors on a mix (the granular path is one method/run). +# assembled weights. Errors on a mix (the granular path is one method/run). # @noRd .ctwasMethodFromWeights <- function(weights) { if (is.null(weights) || length(weights) == 0L) { @@ -2388,7 +2467,7 @@ mergeCtwasBoundaryRegions <- function( ) abort(msg) } - m <- unique(.ctwasParseGeneIds(names(weights))$method) + m <- unique(.ctwasGeneTable(weights)$method) if (length(m) != 1L) { msg <- glue( "asCtwasResult: the finemap result mixes weight methods ", @@ -2409,7 +2488,7 @@ mergeCtwasBoundaryRegions <- function( #' granular \code{assembleCtwasInputs} \eqn{\to} \code{estCtwasGroupPriors} #' \eqn{\to} \code{screenCtwasRegions} \eqn{\to} \code{finemapCtwasRegions} #' path. The GWAS study is read from \code{z_snp$study} and the (single) -#' weight method from the gene ids. +#' weight method from the weights. #' @param finemapResult A list from \code{\link{finemapCtwasRegions}} or #' \code{\link{mergeCtwasBoundaryRegions}}. #' @param keepSnps Logical (length 1). Retain the context-agnostic SNP @@ -2516,7 +2595,8 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { A1 = df$A1, A2 = df$A2, z = df$z, - study = as.character(gwasSumStats$study)[[i]] + study = as.character(gwasSumStats$study)[[i]], + N = df$N ) } @@ -2822,74 +2902,98 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { panelSnps[sort(m$idxA)] } -# The ctwas per-gene weight entries for one block. -# -# Two variant sets, because they answer different questions. `gwasSnpIds` is -# the GLOBAL set: it bounds each gene's cis SPAN, which has to cover every -# block the gene reaches for boundary detection to work. `regionSnpIds` is -# this block's own set: it bounds the weight vector actually FITTED, because -# ctwas fine-maps one region at a time. -# -# So anything feeding the SPAN is read off the whole panel rather than off -# `ldPanel`, whose sketch is narrowed to this block -- `globalPanelInfo` for -# the coordinates, `globalVariance` for the scaling, and the same table for -# allele harmonization, since an out-of-block weight variant has to survive -# matching to be counted in p0/p1 at all. Reading any of them off the block -# would clip a boundary gene back to its home region, which is exactly what -# p0/p1 exist to prevent. The allele columns agree with the block's own -# table wherever the two overlap, so nothing about the in-block result -# changes. -# -# Genes are prepared in one sweep and given their weight variances in a -# second, so the block's LD is read once, over every gene's fitted variants -# together (.ctwasWeightVariances()). Variants absent from the panel are -# dropped from that gene's row set. +# What preparing a gene reads, built once for the run since it is the same for +# every gene. A gene is read whole, whichever blocks its weights reach, so +# anything about it comes off the whole panel rather than off one block's -- +# `spanInfo` for the coordinates and allele harmonization, `spanVariance` for +# the scaling -- and `gwasSnpIds` is the GLOBAL set of GWAS variants. `cutoffs` +# are .ctwasPruningCutoffs()'s. # @noRd -.ctwasBuildWeights <- function( - twasWeights, - ldPanel, - fineMappingResult = NULL, - twasWeightCutoff = 0, - csMinCor = 0.8, - minPipCutoff = 0, - maxNumVariants = Inf, - gwasSnpIds = NULL, - regionSnpIds = NULL, - globalPanelInfo = NULL, - globalVariance = NULL +.ctwasGeneContext <- function( + spanInfo, + spanVariance, + gwasSnpIds, + fineMappingResult, + cutoffs ) { - spanInfo <- globalPanelInfo %||% ldPanel$snpInfo - panelSnps <- .ctwasPanelSnpsForGwas(spanInfo$id, gwasSnpIds) - ctx <- list( - ldPanel = ldPanel, + list( spanInfo = spanInfo, - panelSnps = panelSnps, - regionSnps = regionSnpIds, + inGwas = is_in( + spanInfo$id, + .ctwasPanelSnpsForGwas(spanInfo$id, gwasSnpIds) + ), refVariants = .ctwasRefVariants(spanInfo), - spanVariance = globalVariance %||% ldPanel$variance, + spanVariance = spanVariance, + varianceRows = match(spanInfo$id, names(spanVariance)), + positions = .ctwasPositionIndex(spanInfo$pos), fineMappingResult = fineMappingResult, - cutoffs = list( - twasWeightCutoff = twasWeightCutoff, - csMinCor = csMinCor, - minPipCutoff = minPipCutoff, - maxNumVariants = maxNumVariants - ) + cutoffs = cutoffs + ) +} + +# The panel's rows by position, for finding a gene's by searching the sorted +# positions rather than passing over the whole panel. +# @noRd +.ctwasPositionIndex <- function(pos) { + rows <- order(pos) + list(rows = rows, pos = pos[rows]) +} + +# The panel rows at these positions, in panel order. +# @noRd +.ctwasRowsAtPositions <- function(index, pos) { + wanted <- unique(pos[!is.na(pos)]) + first <- findInterval(wanted, index$pos, left.open = TRUE) + 1L + last <- findInterval(wanted, index$pos) + sort(index$rows[sequence(last - first + 1L, first)]) +} + +# The part of the panel a gene reads: its rows at the positions of the gene's +# variants, the only ones those can be. Nothing about preparing a gene then +# depends on the size of the panel. +# @noRd +.ctwasPanelNear <- function(parts, ctx) { + fields <- .variantIdFields(.rowResolveWeights(parts)$variantIds) + rows <- .ctwasRowsAtPositions(ctx$positions, as.integer(fields[, 3L])) + list( + spanInfo = ctx$spanInfo[rows, , drop = FALSE], + panelSnps = ctx$spanInfo$id[rows][ctx$inGwas[rows]], + refVariants = ctx$refVariants[rows, ], + spanVariance = ctx$spanVariance[ctx$varianceRows[rows]] ) - prepared <- compact(map( +} + +# The genes of one weight source, prepared up to their weight variances from +# `ctx` (.ctwasGeneContext()). Variants absent from the panel are dropped from +# that gene's row set. +# +# Genes are prepared here and given their weight variances afterwards, across +# all blocks (.ctwasGeneEntries()), so each block's LD is read once. +# @noRd +.ctwasPreparedGenes <- function(twasWeights, ctx) { + compact(map( .ctwasWeightUnits(twasWeights), .ctwasGenePrepared, twasWeights = twasWeights, ctx = ctx )) - genes <- map2( - prepared, - .ctwasWeightVariances(ldPanel, prepared), - .ctwasGeneEntryFor, - spanInfo = spanInfo - ) - keys <- map_chr(genes, "key") +} + +# The prepared genes' ctwas weight entries, keyed by gene id, with their weight +# variances from `panels`, the blocks' LD panels. +# @noRd +.ctwasGeneEntries <- function(prepared, panels) { + keys <- map_chr(prepared, "key") .ctwasRequireUniqueGenes(keys) - set_names(map(genes, "entry"), keys) + .ctwasRequireDistinctGroups(prepared) + set_names( + map2( + prepared, + .ctwasWeightVariances(panels, prepared), + .ctwasGeneEntry + ), + keys + ) } # One unit per weight vector a source holds: each (row, method) entry of a @@ -2917,7 +3021,7 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { .collectionEntry(x, unit$row) } -# Two weight vectors for one (study, context, trait, method) -- rows that +# Two weight vectors for one (study, context, type, trait, method) -- rows that # differ only in their joint group -- would both be modeled under one gene # id. Refuse, rather than keep whichever came last. # @noRd @@ -2929,8 +3033,41 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { abort(glue( "ctwasPipeline: the weights hold more than one fit of ", "{str_flatten(dup, ', ')} (rows that differ only in their joint ", - "group). cTWAS models one weight vector per gene, so keep one." + "group, or one gene placed in several blocks). cTWAS models one ", + "weight vector per gene, so keep one." + )) +} + +# cTWAS tells groups apart by their names, so distinct (study, context, type) +# must not read the same: the underscores of a result's label cannot be split +# back. And its EM finds a group's context and type by splitting the name on the +# "|" between them (.ctwasEntryGroup()), so none of the three may hold one. +# @noRd +.ctwasRequireDistinctGroups <- function(prepared) { + metas <- map(prepared, "meta") + groups <- distinct(tibble( + study = map_chr(metas, "study"), + context = map_chr(metas, "context"), + type = map_chr(metas, "type") )) + labels <- str_c(groups$study, "_", groups$context, "_", groups$type) + repeated <- unique(labels[duplicated(labels)]) + if (length(repeated) > 0L) { + abort(glue( + "ctwasPipeline: distinct studies, contexts and data types read ", + "the same once joined: {str_flatten(repeated, ', ')}. cTWAS would ", + "pool them or results could not tell them apart, so rename one." + )) + } + piped <- labels[str_detect(labels, fixed("|"))] + if (length(piped) > 0L) { + abort(glue( + "ctwasPipeline: a study, context or data type holds a '|': ", + "{str_flatten(piped, ', ')}. cTWAS splits its group names on it, ", + "so rename it." + )) + } + invisible(NULL) } # Panel variant info in the (chrom/pos/A2/A1/variant_id) frame harmonizeAlleles @@ -2946,8 +3083,15 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { ) } -# study/context/trait/method identity for one unit (.ctwasWeightUnits()) + its -# collection key. +# The molecular type of a gene whose weights record none: every such context is +# one type. +# @noRd +.CTWAS_NO_TYPE <- "QTL" + +# One unit's identity (.ctwasWeightUnits()) and the names cTWAS knows it by: its +# context, keeping two studies' contexts of one name apart as cTWAS's own FAQ +# suggests, its weight_name, as cTWAS's default forms it but for the study and +# the method, and its id, whose first "|" field cTWAS reads as the molecular id. # @noRd .ctwasGeneMeta <- function(twasWeights, unit) { i <- unit$row @@ -2955,16 +3099,41 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { context <- as.character(twasWeights$context)[[i]] trait <- as.character(twasWeights$trait)[[i]] method <- unit$method + type <- .ctwasUnitType(twasWeights, unit) + engineContext <- str_c(studyName, "_", context) + weightName <- str_c(engineContext, "_", type, "_", method) list( study = studyName, context = context, trait = trait, method = method, + type = type, + engineContext = engineContext, + weightName = weightName, traitPos = .ctwasTraitPosAt(twasWeights, i), - key = as.character(glue("{studyName}|{context}|{trait}|{method}")) + key = str_c(trait, "|", weightName) ) } +# The molecular type one unit's weights record, or .CTWAS_NO_TYPE. +# @noRd +.ctwasUnitType <- function(x, unit) { + recorded <- .ctwasRecordedType(x, unit) + if (length(recorded) == 0L || is.na(recorded)) { + return(.CTWAS_NO_TYPE) + } + as.character(recorded) +} + +# @noRd +.ctwasRecordedType <- function(x, unit) { + md <- mcols(x, use.names = FALSE) + if (methods::is(x, "TwasWeights")) { + return(md$dataType[[unit$row]][[unit$method]]) + } + md$dataType[unit$row] +} + # Steps 1-2: allele-harmonize a gene's (variantIds, weights) against the LD # panel (matching by chrom/pos with sign/strand-flip detection; weights # sign-flipped for swapped frames), then restrict to the panel variant set. @@ -3100,7 +3269,7 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { varLookup <- spanVariance[aligned$vids] if (anyNA(varLookup)) { msg <- glue( - ".ctwasBuildWeights: missing genotype variance for ", + "ctwasPipeline: missing genotype variance for ", "{sum(is.na(varLookup))} variant(s) in the LD panel; an LdMatrix ", "needs an `AF` column to scale non-standardized weights." ) @@ -3109,46 +3278,31 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { renormalized * sqrt(varLookup) } -# The ctwas per-gene weight entry (weight matrix, weight variance w'Rw, +# One prepared gene's ctwas weight entry (weight matrix, weight variance w'Rw, # chrom/BP span, plus identity metadata). ctwas would read the weights' LD # submatrix (R_wgt) only to compute the gene z (compute_gene_z); the pipeline # supplies that z itself (.ctwasGeneZ()), so the entry carries the scalar -# w'Rw instead. -# -# `spanVids` is the variant set the chrom/p0/p1 span is measured over, and it is -# deliberately allowed to be WIDER than the fitted `vids`: ctwas fine-maps one -# region at a time, so `wgt` must stay inside that region, while -# `ctwas::get_boundary_genes` reads p0/p1 to decide which genes straddle a -# region boundary. Measuring the span over the fitted subset would clip a -# boundary gene down to its home region and hide it from merge_regions. -# @noRd -.ctwasGeneEntry <- function( - vids, - w, - wgtVar, - meta, - spanVids = vids, - spanInfo -) { - # Span coordinates come from the whole-panel table, while `wgtVar` was - # computed on the block's own panel: ctwas fine-maps one region at a - # time, but a boundary gene's span has to reach past it. - panelInfo <- spanInfo - rowIdx <- match(spanVids, panelInfo$id) +# w'Rw instead. The weights are all the gene's, wherever they fall: ctwas cuts +# them to a region's variants itself when it fine-maps (compute_region_cor), and +# reads the span to find the genes that cross a boundary (get_boundary_genes). +# @noRd +.ctwasGeneEntry <- function(g, wgtVar) { + meta <- g$meta list( - wgt = matrix(w, ncol = 1L, dimnames = list(vids, "wgt")), + wgt = matrix(g$w, ncol = 1L, dimnames = list(g$vids, "wgt")), wgt_var = wgtVar, - type = meta$context, - context = meta$context, + type = meta$type, + context = meta$engineContext, gene_name = meta$trait, study = meta$study, + qtl_context = meta$context, method = meta$method, - n_wgt = length(vids), - chrom = as.integer(panelInfo$chrom[[rowIdx[1L]]]), - p0 = min(as.integer(panelInfo$pos[rowIdx])), - p1 = max(as.integer(panelInfo$pos[rowIdx])), + n_wgt = length(g$vids), + chrom = g$span$chrom, + p0 = g$span$p0, + p1 = g$span$p1, molecular_id = meta$trait, - weight_name = str_c(meta$context, meta$context, sep = "_"), + weight_name = meta$weightName, # The GENE's own coordinate, carried alongside the weight-variant span # (chrom/p0/p1) so the result payloads can be ranged on the gene rather # than on wherever its weight variants happen to fall. @@ -3156,6 +3310,24 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { ) } +# The chromosome and span of a gene's weight variants, from the whole-panel +# table: ctwas fine-maps one region at a time, but a gene can reach past it. +# @noRd +.ctwasGeneSpan <- function(vids, spanInfo) { + rowIdx <- match(vids, spanInfo$id) + list( + chrom = as.integer(spanInfo$chrom[[rowIdx[1L]]]), + p0 = min(as.integer(spanInfo$pos[rowIdx])), + p1 = max(as.integer(spanInfo$pos[rowIdx])) + ) +} + +# cTWAS's name for a gene's group, from the labels its entry carries. +# @noRd +.ctwasEntryGroup <- function(entry) { + str_c(entry$context, "|", entry$type) +} + # The gene's own position for row i, or NULL when the weight source carries no # traitPos provenance. # @noRd @@ -3178,7 +3350,8 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { # Polymorphic: the weight source may be a TwasWeights or a # fine-mapping result, so the payload comes from the class-aware bridge. parts <- .ctwasUnitParts(twasWeights, unit) - aligned <- .ctwasAlignGeneWeights(parts, ctx$refVariants, ctx$panelSnps) + near <- .ctwasPanelNear(parts, ctx) + aligned <- .ctwasAlignGeneWeights(parts, near$refVariants, near$panelSnps) if (is.null(aligned)) { return(NULL) } @@ -3186,30 +3359,20 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { w <- .ctwasAdjustGeneWeights( parts, aligned, - ctx$spanVariance, + near$spanVariance, .ctwasRenormFit(parts, ctx$fineMappingResult, meta) ) kept <- .ctwasGeneKept(aligned$vids, w, meta, ctx) if (length(kept) < 1L) { return(NULL) } - # The weight vector ctwas fits must live inside the region being - # fine-mapped: susie_rss gets that region's LD, and a gene reaching - # outside it yields a non-finite ELBO (and a non-finite fit_EM - # log-likelihood upstream). The SPAN stays the gene's full cis extent -- - # see .ctwasGeneEntry -- so a boundary gene is still detected and can be - # recovered by merge_regions. - inRegion <- .ctwasInRegion(kept$vids, ctx$regionSnps) - if (!any(inRegion)) { - return(NULL) - } # No LD yet: the weight variances come in a second sweep, from one read - # of the block's LD over every gene's fitted variants together. + # of each block's LD over every gene's variants in it together. list( key = meta$key, - vids = kept$vids[inRegion], - w = kept$w[inRegion], - spanVids = kept$vids, + vids = kept$vids, + w = kept$w, + span = .ctwasGeneSpan(kept$vids, near$spanInfo), meta = meta ) } @@ -3236,35 +3399,69 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { ) } -# One prepared gene's ctwas entry, given its weight variance w'Rw. +# Each prepared gene's weight variance w'Rw. As in ctwas's own weights' LD +# (compute_weight_LD_from_ref), the blocks a gene crosses count as independent: +# the variance is the sum of its pieces' w'Rw, each from its own block's LD, +# read once over that block's variants of all the genes. ctwas reads a gene's +# weight LD only for this quadratic form (compute_gene_z), so the pipeline +# computes the gene z itself (.ctwasGeneZ()) and no per-gene or block-wide LD +# matrix is ever formed for it. # @noRd -.ctwasGeneEntryFor <- function(g, wgtVar, spanInfo) { - list( - key = g$key, - entry = .ctwasGeneEntry( - g$vids, - g$w, - wgtVar, - g$meta, - spanVids = g$spanVids, - spanInfo = spanInfo - ) - ) +.ctwasWeightVariances <- function(panels, prepared) { + if (length(prepared) == 0L) { + return(numeric(0)) + } + pieces <- .ctwasWeightPieces(panels, prepared) + byBlock <- split(pieces, pieces$block) + variances <- list_rbind(map2( + byBlock, + panels[as.integer(names(byBlock))], + .ctwasBlockVariances + )) + as.numeric(tapply(variances$variance, variances$gene, sum)) } -# Each prepared gene's weight variance w'Rw, from one read of the block's LD -# over the union of the genes' weight variants. ctwas reads a gene's weight -# LD only for this quadratic form (compute_gene_z), so the pipeline computes -# the gene z itself (.ctwasGeneZ()) and no per-gene or block-wide LD matrix -# is ever formed for it. +# Every weight of every gene, with the panel its variant is in. # @noRd -.ctwasWeightVariances <- function(ldPanel, prepared) { - if (length(prepared) == 0L) { - return(numeric(0)) +.ctwasWeightPieces <- function(panels, prepared) { + vids <- map(prepared, "vids") + panelIds <- map(panels, .ctwasPanelVariantIds) + id <- list_c(vids) + at <- match(id, list_c(panelIds)) + if (anyNA(at)) { + abort(glue( + "ctwasPipeline: {sum(is.na(at))} weight variant(s) absent from ", + "the blocks' LD panels." + )) } - vids <- unique(list_c(map(prepared, "vids"))) - ld <- .ctwasPanelWeightLd(ldPanel, vids) - map_dbl(prepared, .ctwasWeightVariance, ld = ld) + tibble( + gene = rep(seq_along(vids), lengths(vids)), + id = id, + w = list_c(map(prepared, "w")), + block = rep(seq_along(panelIds), lengths(panelIds))[at] + ) +} + +# @noRd +.ctwasPanelVariantIds <- function(panel) { + panel$snpInfo$id +} + +# Each gene's w'Rw over its variants in one block, from one read of the block's +# LD. `pieces` are the block's rows of .ctwasWeightPieces(). +# @noRd +.ctwasBlockVariances <- function(pieces, panel) { + ld <- .ctwasPanelWeightLd(panel, unique(pieces$id)) + genes <- split(pieces, pieces$gene) + tibble( + gene = as.integer(names(genes)), + variance = map_dbl(genes, .ctwasPieceVariance, ld = ld) + ) +} + +# @noRd +.ctwasPieceVariance <- function(piece, ld) { + .ctwasWeightVariance(list(vids = piece$id, w = piece$w), ld) } # The block's LD over `vids`, for weight variances: list(ldFactor = ) holding a @@ -3311,11 +3508,13 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { if (length(weightsList) == 0L) { return(NULL) } - zGene <- list_rbind(map2( - weightsList, - names(weightsList), - .ctwasGeneZRow, - zSnp = zSnp + zGene <- list_rbind(pmap( + list( + weightsList, + names(weightsList), + .ctwasGeneSnpZ(weightsList, zSnp) + ), + .ctwasGeneZRow )) infinite <- is.infinite(zGene$z) if (any(infinite)) { @@ -3327,12 +3526,27 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { as.data.frame(filter(zGene, !infinite)) } +# Each gene's weight variants' GWAS z-scores, from one match over all of them. +# @noRd +.ctwasGeneSnpZ <- function(weightsList, zSnp) { + ids <- map(unname(weightsList), .ctwasWeightIds) + gene <- rep(seq_along(ids), lengths(ids)) + unname(split( + zSnp$z[match(list_c(ids), zSnp$id)], + factor(gene, levels = seq_along(ids)) + )) +} + +# @noRd +.ctwasWeightIds <- function(entry) { + rownames(entry$wgt) +} + # One gene's z row. Every weight variant must carry a GWAS z, as # compute_gene_z() requires. # @noRd -.ctwasGeneZRow <- function(entry, id, zSnp) { +.ctwasGeneZRow <- function(entry, id, zs) { wgt <- entry$wgt - zs <- zSnp$z[match(rownames(wgt), zSnp$id)] if (anyNA(zs)) { abort(glue( "ctwasPipeline: {sum(is.na(zs))} weight variant(s) of gene ", @@ -3344,7 +3558,7 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { } # A gene's z_gene row as ctwas reads it: its weight id, its z, and the type, -# context and group its weights carry. +# context and group its entry carries. # @noRd .ctwasZGeneRow <- function(entry, id, z) { tibble( @@ -3352,20 +3566,10 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { z = z, type = entry$type, context = entry$context, - group = str_c(entry$context, "|", entry$type) + group = .ctwasEntryGroup(entry) ) } -# Which of a gene's weight variants lie in the region being fine-mapped. A NULL -# region set means the caller supplied none, so nothing is restricted. -# @noRd -.ctwasInRegion <- function(vids, regionSnps) { - if (is.null(regionSnps)) { - return(rep(TRUE, length(vids))) - } - is_in(vids, as.character(regionSnps)) -} - # Look up one gene's PIP vector and 95% credible-set membership / purity in # the rescues' fine-mapping result, which .ctwasRescueSource() narrowed to the # one method they read, so the gene's (study, context, trait) picks its fit. @@ -3647,7 +3851,7 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { str_c( entry$study, "|", - entry$context, + entry$qtl_context, "|", entry$gene_name, "|", @@ -3990,32 +4194,3 @@ asCtwasResult <- function(finemapResult, keepSnps = FALSE) { .ctwasStudyChr <- function(g) { as.character(g$study) } - -# The block id whose window contains anchor variant `i`, or NA when unplaced. -# @noRd -.ctwasBlockIdForVariant <- function(i, aPos, aChr, bChr, bS, bE, ids) { - if (is.na(aPos[[i]])) { - return(NA_character_) - } - hit <- which(bChr == aChr[[i]] & aPos[[i]] >= bS & aPos[[i]] < bE) - if (length(hit) > 0L) ids[[hit[[1L]]]] else NA_character_ -} - -# The trait field of a split gene id: everything between context and the final -# method field, rejoined on "|" so a "|"-bearing trait survives. -# @noRd -.ctwasTraitField <- function(p) { - str_flatten(p[4:(length(p) - 1L)], "|") -} - -# The method field (last component) of a split gene id. -# @noRd -.ctwasMethodField <- function(p) { - p[[length(p)]] -} - -# The sorted unique gene (trait) set of one context. -# @noRd -.ctwasSortUnique <- function(g) { - sort(unique(g)) -} diff --git a/R/ctwasWrapper.R b/R/ctwasWrapper.R index 7f89c1960..88daf9bcb 100644 --- a/R/ctwasWrapper.R +++ b/R/ctwasWrapper.R @@ -121,6 +121,86 @@ ) } +# cTWAS's summary of a run's priors, each gene group's enrichment over the SNP +# group and each group's share of the heritability, at the GWAS's sample size. +# NULL where there is nothing to summarize: no group sizes, or a prior the EM +# left non-finite, on which cTWAS's enrichment test stops. +# @noRd +.ctwasGroupSummary <- function(param, gwasN) { + numbers <- c(param$group_prior, param$group_prior_var, param$group_size) + if ( + is.null(param$group_size) || + !is_in("SNP", names(param$group_prior)) || + !all(is.finite(numbers)) || + !requireNamespace("ctwas", quietly = TRUE) + ) { + return(NULL) + } + ctwas::summarize_param(param, gwas_n = gwasN %||% NA_real_) +} + +#' Combine cTWAS PIPs across a gene's molecular traits +#' +#' @description One row per gene with cTWAS's combined PIP, the probability that +#' the gene is causal through any of the molecular traits and contexts +#' modelled for it, from \code{ctwas::combine_gene_pips()}. The combination +#' needs the per-effect probabilities of a run, so it reads +#' \code{\link{susieAlpha}}, and a run is combined on its own: results of +#' several GWAS studies or methods are not mixed. +#' @param x A \code{\link{CtwasResult}}. +#' @param mapping \code{NULL}, or a \code{data.frame} mapping molecular traits +#' to genes, with columns \code{molecular_id} and \code{gene_name}. Without +#' it the rows are the molecular ids, so a gene's expression and splicing +#' traits, whose ids differ, stay apart. With it the rows are the genes, and +#' traits it does not map are dropped. +#' @return A tibble led by the run (\code{gwasStudy}, \code{method}) and the +#' gene (\code{molecular_id}, or \code{gene_name} with a \code{mapping}), then +#' \code{combined_pip}, \code{combined_cs_id}, and for each study and context +#' \code{{study}_{context}_pip} and \code{{study}_{context}_cs_id}, \code{NA} +#' where that context holds no trait of the gene. Highest PIP first within a +#' run. +#' @examples +#' data(ctwasFinemapExample) +#' ctwasCombinedPips(asCtwasResult(ctwasFinemapExample)) +#' @importFrom dplyr group_split +#' @export +ctwasCombinedPips <- function(x, mapping = NULL) { + if (!methods::is(x, "CtwasResult")) { + abort("`x` must be a CtwasResult.") + } + if (!requireNamespace("ctwas", quietly = TRUE)) { + abort("Package 'ctwas' is required for ctwasCombinedPips.") + } + runs <- group_split( + .ctwasRowTable(susieAlpha(x)), + .data$gwasStudy, + .data$method + ) + list_rbind(map(runs, .ctwasRunCombinedPips, mapping = mapping)) +} + +# One run's combined PIPs. cTWAS names its per-context columns after the +# `context` column, so that column names the study too. +# @noRd +.ctwasRunCombinedPips <- function(run, mapping) { + combined <- .ctwasQuietly( + ctwas::combine_gene_pips, + list( + susie_alpha_res = as.data.frame(mutate( + run, + context = str_c(.data$study, "_", .data$context) + )), + mapping_table = mapping, + group_by = if (is.null(mapping)) "molecular_id" else "gene_name", + by = "context" + ) + ) + bind_cols( + tibble(gwasStudy = run$gwasStudy[[1L]], method = run$method[[1L]]), + as_tibble(combined) + ) +} + #' Screen cTWAS regions #' #' @description Step 3 of the three-step \code{\link{ctwasPipeline}}: runs diff --git a/R/enloc.R b/R/enloc.R deleted file mode 100644 index 5dd1c5c74..000000000 --- a/R/enloc.R +++ /dev/null @@ -1,191 +0,0 @@ -# ============================================================================= -# enloc scoring -# ----------------------------------------------------------------------------- -# The colocalisation probabilities fastenloc computes, for one eQTL signal -# cluster, under the DAP-1 approximation. -# -# fastenloc enumerates a p x p grid over (GWAS hit m, causal eQTL n) and sums -# it (controller.cc, "case 4"). That is quadratic in the cluster size, but the -# per-cell prior takes only two values -- one on the diagonal, one off it -- -# so the off-diagonal total factorises and the whole grid collapses to a -# handful of vector operations: -# -# diagSum = priD * sum(bf * pip) -# offSum = priO * (sum(bf) * sum(pip) - sum(bf * pip)) -# -# The two agree exactly; this file computes the second. -# -# The four configurations fastenloc enumerates are coloc's hypotheses, so both -# parameterisations come out of one pass: RCP is PP.H4 (both traits causal at -# the same variant) and LCP is PP.H3 + PP.H4 (both causal, same variant or -# not). What makes this enloc rather than coloc is the prior, which depends on -# whether the GWAS hit IS the eQTL -- pi1_e = expit(a0 + a1) on the diagonal -# against pi1_ne = expit(a0) off it. coloc applies one p12 to every shared -# configuration and so cannot express that dependence. -# ============================================================================= - -#' @include AllGenerics.R -NULL - -# The GWAS Bayes factors fastenloc recovers from fine-mapped PIPs, contrasting -# against "every variant in the cluster has no effect" (controller.cc): -# log10bf = log10(pip) - log10(P_gwas) + log10(1 - P_gwas) - log10(1 - cpip) -# A zero PIP is floored, as the original fastenloc does, so the log is finite. -# @noRd -.enlocGwasLog10Bf <- function(gwasPip, pGwas, clusterPip) { - floored <- replace(gwasPip, gwasPip == 0, 1e-10) - log10(floored) - log10(pGwas) + (log10(1 - pGwas) - log10(1 - clusterPip)) -} - -# The two priors a configuration can carry. `pi1e` applies when the GWAS hit -# is itself the causal eQTL, `pi1ne` when it is not; the enrichment is -# precisely the gap between them. -# @noRd -.enlocConfigPriors <- function(pi1e, pi1ne, p) { - list( - diagonal = pi1e * (1 - pi1ne)^(p - 1), - offDiagonal = pi1ne * (1 - pi1e) * (1 - pi1ne)^(p - 2) - ) -} - -#' @title enloc Colocalisation Probabilities For One Signal Cluster -#' @description The quantities fastenloc reports for a single eQTL signal -#' cluster, given the GWAS Bayes factors and the eQTL PIPs over the same -#' variants. -#' -#' Both parameterisations are returned because they come from one -#' computation: \code{rcp} is \code{ppH4} and \code{lcp} is -#' \code{ppH3 + ppH4}. -#' @param gwasLog10Bf Numeric vector of per-variant GWAS log10 Bayes factors. -#' @param qtlPip Numeric vector of per-variant eQTL PIPs, same length and -#' order. -#' @param pi1e Prior probability of GWAS causality for a variant that is a -#' causal eQTL, \code{expit(a0 + a1)}. -#' @param pi1ne The same for a variant that is not, \code{expit(a0)}. -#' @param p12 Prior probability of colocalisation, used only for the -#' small-probability floor fastenloc applies to each SNP-level term. -#' @param applyFloor Logical. Apply that floor. fastenloc skips it on the -#' summary-statistics path, where not all variants are present. -#' @return A list with \code{ppH0} to \code{ppH4}, \code{rcp}, \code{lcp} and -#' \code{scp} (the per-variant colocalisation probabilities whose sum is -#' \code{rcp}). -#' @examples -#' enlocClusterProbabilities( -#' gwasLog10Bf = c(2, 0.1, 0.1), -#' qtlPip = c(0.7, 0.2, 0.05), -#' pi1e = 0.02, -#' pi1ne = 0.001 -#' ) -#' @export -enlocClusterProbabilities <- function( - gwasLog10Bf, - qtlPip, - pi1e, - pi1ne, - p12 = 0, - applyFloor = FALSE -) { - p <- length(gwasLog10Bf) - if (p != length(qtlPip)) { - abort(glue( - "enlocClusterProbabilities: `gwasLog10Bf` and `qtlPip` must ", - "describe the same variants ({p} vs {length(qtlPip)})." - )) - } - bf <- 10^gwasLog10Bf - # The cluster's total eQTL posterior, capped as the original fastenloc - # does so that log(1 - clusterPip) stays finite. - clusterPip <- min(sum(qtlPip), 1 - 1e-8) - pri <- .enlocConfigPriors(pi1e, pi1ne, p) - # case 4, split by whether the GWAS hit is the causal eQTL - perVariant <- pri$diagonal * bf * qtlPip - if (applyFloor) { - perVariant <- pmax(perVariant, p12) - } - diagSum <- sum(perVariant) - offSum <- pri$offDiagonal * - (sum(bf) * sum(qtlPip) - sum(bf * qtlPip)) - # cases 1-3: neither, GWAS only, eQTL only - nullBoth <- (1 - pi1ne)^p * (1 - clusterPip) - gwasOnly <- pi1ne * (1 - pi1ne)^(p - 1) * sum(bf) * (1 - clusterPip) - qtlOnly <- (1 - pi1e) * (1 - pi1ne)^(p - 1) * sum(qtlPip) - nc <- nullBoth + gwasOnly + qtlOnly + diagSum + offSum - list( - ppH0 = nullBoth / nc, - ppH1 = gwasOnly / nc, - ppH2 = qtlOnly / nc, - ppH3 = offSum / nc, - ppH4 = diagSum / nc, - rcp = diagSum / nc, - lcp = (offSum + diagSum) / nc, - scp = perVariant / nc - ) -} - -# enloc's priors from the enrichment parameters, as -# fastenloc's set_enrich_params(a0, a1) computes them. `a1` is capped first, -# matching the original fastenloc's --cap_a1 option. -# @noRd -.enlocPriorsFromEnrichment <- function(a0, a1, pQtl, capA1 = Inf) { - a1 <- min(a1, capA1) - pi1e <- stats::plogis(a0 + a1) - pi1ne <- stats::plogis(a0) - list( - pi1e = pi1e, - pi1ne = pi1ne, - p1 = (1 - pQtl) * pi1ne, - p2 = pQtl * (1 - pi1e), - p12 = pQtl * pi1e - ) -} - -# The inverse: the enrichment parameters implied by a p1/p2/p12 triple, as -# fastenloc's set_enrich_params(p1, p2, p12) computes them. Used to accept an -# enrichment table that carries the priors rather than a0/a1. -# @noRd -.enlocEnrichmentFromPriors <- function(p1, p2, p12) { - rest <- 1 - p1 - p2 - p12 - list( - a0 = log(p1 / rest), - a1 = log(p12 * rest / (p1 * p2)) - ) -} - -# Per-variant posteriors within one signal cluster. A SuSiE effect's log Bayes -# factors become posterior inclusion probabilities by softmax under the -# uniform prior over variants -- which is what SuSiE's own `alpha` is -- so a -# single effect row is exactly enloc's notion of a signal cluster. -# @noRd -.enlocClusterPip <- function(log10Bf) { - shifted <- log10Bf - max(log10Bf) - w <- 10^shifted - w / sum(w) -} - -# Score one (QTL effect, GWAS effect) pair the way fastenloc scores a signal -# cluster. Everything the scorer needs is implied by the priors: the two -# marginals are P_gwas = p1 + p12 and P_eqtl = p2 + p12, and (a0, a1) invert -# out of the triple, so an enrichment table carrying p1/p2/p12 is sufficient. -# @noRd -.enlocScoreEffectPair <- function(qtlLog10Bf, gwasLog10Bf, priors) { - pGwas <- priors$p1 + priors$p12 - pQtl <- priors$p2 + priors$p12 - en <- .enlocEnrichmentFromPriors(priors$p1, priors$p2, priors$p12) - pr <- .enlocPriorsFromEnrichment(en$a0, en$a1, pQtl) - qtlPip <- .enlocClusterPip(qtlLog10Bf) - gwasPip <- .enlocClusterPip(gwasLog10Bf) - # A SuSiE effect's alpha is conditional on that effect being present, so - # it sums to exactly 1 -- the cluster is certain to hold a causal variant. - # fastenloc's own cap on locus_epip is what keeps log(1 - cpip) finite - # there, and it has to be applied before the Bayes factors are recovered, - # not only inside the scorer. - clusterPip <- min(sum(qtlPip), 1 - 1e-8) - enlocClusterProbabilities( - gwasLog10Bf = .enlocGwasLog10Bf(gwasPip, pGwas, clusterPip), - qtlPip = qtlPip, - pi1e = pr$pi1e, - pi1ne = pr$pi1ne, - p12 = priors$p12, - applyFloor = FALSE - ) -} diff --git a/R/exampleData.R b/R/exampleData.R index 890648f6f..a3b7f23a3 100644 --- a/R/exampleData.R +++ b/R/exampleData.R @@ -446,7 +446,8 @@ NULL #' @format A \code{TwasWeights} collection with one row, the gene, holding #' 11 methods: \code{mrash}, \code{susie}, \code{susieInf}, \code{enet}, #' \code{lasso}, \code{mcp}, \code{scad}, \code{l0learn}, \code{bayesR}, -#' \code{bayesC} and \code{ensemble}. +#' \code{bayesC} and \code{ensemble}. Its \code{\link{dataType}} is +#' \code{"expression"}. #' #' @keywords data #' @@ -496,7 +497,8 @@ NULL #' placement). Derived from the de-identified \code{protocol_example} toy data #' (chr22); all identifiers are synthetic. #' -#' @format A \code{TwasWeights} collection with one row, method \code{susie}. +#' @format A \code{TwasWeights} collection with one row, method \code{susie}, +#' its \code{\link{dataType}} \code{"expression"}. #' #' @keywords data #' diff --git a/R/fineMappingPipeline.R b/R/fineMappingPipeline.R index 696eea3ee..af148fa9d 100644 --- a/R/fineMappingPipeline.R +++ b/R/fineMappingPipeline.R @@ -285,7 +285,7 @@ #' with \code{\link{GenotypeFilterParam}}: its \code{keepSamples} narrows #' the dataset once, and its MAF, MAC, variance, missingness, #' \code{keepVariants} and \code{keepIndel} settings apply to each -#' extracted block, against that block's samples. A field left unset +#' extracted block, against the samples being fit. A field left unset #' filters nothing. A \code{QtlDataset} records no filter of its own, so #' this is the only one. \code{QtlDataset} and #' \code{MultiStudyQtlDataset} input only. @@ -363,12 +363,6 @@ #' 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{writeBed}} / -#' \code{\link{writeGwasVcf}}. \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: @@ -907,25 +901,26 @@ setGeneric("fineMappingPipeline", function(data, ...) { # 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. +# `dataType` is an outside fact (what the phenotype measures), so it comes from +# the data the fit read (.rowDataTypes()), and from nowhere for a GWAS. 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) { +.fmRecordProvenance <- function(x, data, 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) + labels <- .rowDataTypes(x, data) + if (any(!is.na(labels))) { + mcols(x)$dataType <- labels } mcols(x)$dataSource <- rep(dataSource, length.out = n) # DERIVED, never declared: it records what the accessors actually did. @@ -981,7 +976,7 @@ setGeneric("fineMappingPipeline", function(data, ...) { # 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 +# interval .fmGenoWindow() gives X 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. The traits are the dataset's own, so # every one has a position to anchor on. @@ -1506,7 +1501,8 @@ combineFineMappingResults <- function(..., ldReference = NULL) { traitId = NULL, region = NULL, cisWindow = NULL, - samples = NULL + samples = NULL, + read = NULL ) { # No scaling step here: the residualizer already returns columns scaled # to unit variance, governed by `ResidualizationParam(scaleResiduals=)`. @@ -1518,15 +1514,16 @@ combineFineMappingResults <- function(..., ldReference = NULL) { window = .qtlResolveVariantRegion(x, traitId, region, cisWindow), samples = samples, filter = genotypeFilter, - residualizationParam = residualizationParam %||% ResidualizationParam() + residualizationParam = residualizationParam %||% ResidualizationParam(), + read = read ) } # 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 -# `data` over the SAME selection used to build `X` and aligns it to -# `colnames(X)`; variants `af` does not return (e.g. dropped by a +# sample-intersection). Filters the window's dosages (`read`, read here when +# not already held) over the SAME selection used to build `X` and aligns the +# frequencies to `colnames(X)`; variants `af` does not return (e.g. dropped by a # borderline MAF re-check on the final sample set) come back as NA. Returns # NULL when `X` is empty or the dataset exposes no `af` (non-QtlDataset # sources whose entries already carry `af`). The branch mirrors the @@ -1539,7 +1536,8 @@ combineFineMappingResults <- function(..., ldReference = NULL) { genotypeFilter, traitId = NULL, region = NULL, - cisWindow = NULL + cisWindow = NULL, + read = NULL ) { if (is.null(X) || ncol(X) == 0L || nrow(X) == 0L) { return(NULL) @@ -1550,9 +1548,10 @@ combineFineMappingResults <- function(..., ldReference = NULL) { block <- try_fetch( .qtlExtractBlock( data, - window = .fmAfWindow(data, traitId, region, cisWindow), + window = .fmGenoWindow(data, traitId, region, cisWindow), samples = rownames(X), - filter = genotypeFilter + filter = genotypeFilter, + read = read ), error = function(cnd) NULL ) @@ -1562,10 +1561,11 @@ combineFineMappingResults <- function(..., ldReference = NULL) { unname(set_names(block$af, block$variantIds)[colnames(X)]) } -# The window .fmAfForX() re-extracts: the explicit `region` when given (the -# trait is then only what the block was fit for), else the trait's cis window. +# The window a block's genotypes are read over: the explicit `region` when given +# (the trait is then only what the block was fit for), else the trait's cis +# window. # @noRd -.fmAfWindow <- function(data, traitId, region, cisWindow) { +.fmGenoWindow <- function(data, traitId, region, cisWindow) { if (is.null(region)) { return(.qtlResolveVariantRegion( data, @@ -2463,41 +2463,6 @@ combineFineMappingResults <- function(..., ldReference = NULL) { } # ============================================================================= -# Residualized genotype for one X window: the trait-derived cis block when -# `rg` is NULL, else the explicit region. Shared by the univariate and PCA -# dispatch paths. -# @noRd -.fmResidGenoBlock <- function( - ctx, - traitId, - rg, - samples, - data, - cisWindow, - residualizationParam, - genotypeFilter -) { - if (is.null(rg)) { - .fmResidGeno( - data, - contexts = ctx, - traitId = traitId, - cisWindow = cisWindow, - samples = samples, - residualizationParam = residualizationParam, - genotypeFilter = genotypeFilter - ) - } else { - .fmResidGeno( - data, - contexts = ctx, - region = rg, - samples = samples, - residualizationParam = residualizationParam, - genotypeFilter = genotypeFilter - ) - } -} # Merge per-window block entries into one row-record per token: a token is # dropped when any window failed to fit it (NULL), otherwise the windows are @@ -2551,51 +2516,52 @@ combineFineMappingResults <- function(..., ldReference = NULL) { ) } -# Fit one univariate X window for (ctx, trait): residualize genotype, align to -# Y, SER pre-screen, then .fmFitXBlock. Errors when too few shared samples -# (a hard data problem), returns list() when the window screens out. +# Fit one trait on its shared block: align to Y, SER pre-screen, then +# .fmFitXBlock. Errors when too few shared samples (a hard data problem), +# returns list() when the window screens out. # @noRd -.fmUnivBlockFit <- function(rg, ctx, tid, Y, toRun, cfg) { - allX <- .fmResidGenoBlock( - ctx, - tid, - rg, - rownames(Y), - data = cfg$data, - cisWindow = cfg$cisWindow, - residualizationParam = cfg$residualizationParam, - genotypeFilter = cfg$genotypeFilter - ) - xy <- .fmAlignXY(allX, Y, ctx, tid) - X <- xy$X - y <- xy$y - if (!.fmSerScreen(X, y, cfg$screen)) { +.fmUnivBlockFit <- function(plan, rg, cfg, block) { + xy <- .fmAlignXY(block$X, plan$Y, plan$ctx, plan$tid) + if (!.fmSerScreen(xy$X, xy$y, cfg$screen)) { if (cfg$verbose >= 1) { inform(glue( - "Skipping (context='{ctx}', trait='{tid}'): SER ", + "Skipping (context='{plan$ctx}', trait='{plan$tid}'): SER ", "pre-screen found no signal above the cutoff." )) } return(list()) } - afVec <- .fmAfForX( - cfg$data, - X, - genotypeFilter = cfg$genotypeFilter, - traitId = tid, - region = rg, - cisWindow = cfg$cisWindow - ) .fmFitXBlockP( - X, - y, - toRun, + xy$X, + xy$y, + plan$toRun, cfg$initializeWithSusieInf, - ctx, - tid, - afVec, + plan$ctx, + plan$tid, + block$af, cfg, - region = .fmBlockWindow(rg, ctx, tid, cfg) + region = .fmBlockWindow(rg, plan$ctx, plan$tid, cfg) + ) +} + +# One region's fits for every planned (context, trait), in the order of `plans`. +# @noRd +.fmUnivRegionWork <- function(rg, plans, cfg) { + .qtlWork( + plans, + map(plans, .fmPlanSpec, rg = rg, cfg = cfg), + .fmUnivBlockFit, + list(rg = rg, cfg = cfg) + ) +} + +# The genotypes one planned fit is on, in region `rg`. +# @noRd +.fmPlanSpec <- function(plan, rg, cfg) { + .qtlFitSpec( + .fmGenoWindow(cfg$data, plan$tid, rg, cfg$cisWindow), + plan$ctx, + rownames(plan$Y) ) } @@ -2611,15 +2577,15 @@ combineFineMappingResults <- function(..., ldReference = NULL) { )) } list( - X = allX[common, , drop = FALSE], + X = .qtlRowsOf(allX, common), y = drop(Y[common, , drop = FALSE]) ) } -# All univariate row-records for one (context, trait): cache hits, then (when -# tokens remain) per-window fits merged per token. +# One (context, trait) before any genotype is read: its cache hits, the tokens +# left to fit, and the phenotype they are fit to, when there are any. # @noRd -.fmUnivTraitRows <- function(tid, ctx, cfg) { +.fmUnivPlan <- function(tid, ctx, cfg) { lookups <- map( cfg$univTokens, .fmUnivLookup, @@ -2636,27 +2602,73 @@ combineFineMappingResults <- function(..., ldReference = NULL) { tid = tid ) toRun <- map_chr(keep(lookups, .fmNotCached), "tk") - if (length(toRun) == 0L) { - return(cachedRows) - } - Y <- .fmResidPheno( - cfg$data, - contexts = ctx, - traitId = tid, - phenotypeFilter = cfg$phenotypeFilter, - residualizationParam = cfg$residualizationParam - ) - blockEntries <- map( - cfg$xRegions, - .fmUnivBlockFit, + list( ctx = ctx, tid = tid, - Y = Y, + cachedRows = cachedRows, toRun = toRun, - cfg = cfg + Y = if (length(toRun) > 0L) { + .fmResidPheno( + cfg$data, + contexts = ctx, + traitId = tid, + phenotypeFilter = cfg$phenotypeFilter, + residualizationParam = cfg$residualizationParam + ) + } + ) +} + +# Whether a planned (context, trait) has anything to fit. +# @noRd +.fmPlanHasFits <- function(plan) { + length(plan$toRun) > 0L +} + +# One fitted plan's rows: its per-region fits merged per token. +# @noRd +.fmUnivPlanRows <- function(k, plans, perRegion, cfg) { + plan <- plans[[k]] + .fmMergeTokenRows( + cfg$study, + plan$ctx, + plan$tid, + plan$toRun, + map(perRegion, pluck, k) + ) +} + +# Every univariate fit of a run, for the row-records of .fmUnivRows(). Every fit +# is planned before any genotype is read, which is what lets the fits of a +# window share one read and the traits of a context share one X. +# @noRd +.fmUnivWork <- function(cfg) { + plans <- list_flatten(map(cfg$useCtx, .fmUnivContextPlans, cfg = cfg)) + fitting <- map_lgl(plans, .fmPlanHasFits) + .qtlWorkOf( + map(cfg$xRegions, .fmUnivRegionWork, plans = plans[fitting], cfg = cfg), + .fmUnivRows, + list(plans = plans, fitting = fitting, cfg = cfg) ) - computed <- .fmMergeTokenRows(cfg$study, ctx, tid, toRun, blockEntries) - c(cachedRows, computed) +} + +# Every univariate row-record of a run: each (context, trait)'s cache hits, then +# its per-window fits merged per token. +# @noRd +.fmUnivRows <- function(perRegion, plans, fitting, cfg) { + toFit <- plans[fitting] + computed <- replace( + rep(list(list()), length(plans)), + which(fitting), + map( + seq_along(toFit), + .fmUnivPlanRows, + plans = toFit, + perRegion = perRegion, + cfg = cfg + ) + ) + list_flatten(map2(map(plans, "cachedRows"), computed, c)) } # One window's fits for every top PC of a context, as pcName -> block entry @@ -2664,19 +2676,13 @@ combineFineMappingResults <- function(..., ldReference = NULL) { # same for each PC -- they differ only in the response -- so it is built, and # its allele frequencies read, once rather than once per PC. # @noRd -.fmPcaRegionFits <- function(rg, ctx, traits, scores, cfg) { +.fmPcaRegionFits <- function(plan, rg, cfg, block) { + ctx <- plan$ctx + traits <- plan$traits + scores <- plan$scores[, plan$toFit, drop = FALSE] pcNames <- colnames(scores) skipped <- set_names(rep(list(list()), length(pcNames)), pcNames) - X <- .fmResidGenoBlock( - ctx, - traits, - rg, - rownames(scores), - data = cfg$data, - cisWindow = cfg$cisWindow, - residualizationParam = cfg$residualizationParam, - genotypeFilter = cfg$genotypeFilter - ) + X <- block$X common <- intersect(rownames(X), rownames(scores)) if (length(common) < 2L) { return(skipped) @@ -2689,14 +2695,7 @@ combineFineMappingResults <- function(..., ldReference = NULL) { } blk <- list( X = Xb, - af = .fmAfForX( - cfg$data, - Xb, - genotypeFilter = cfg$genotypeFilter, - traitId = traits, - region = rg, - cisWindow = cfg$cisWindow - ), + af = block$af, window = .fmBlockWindow(rg, ctx, traits, cfg) ) fits <- map(pcNames[pass], .fmPcaBlockFit, blk, shared, ctx, cfg) @@ -2747,14 +2746,15 @@ combineFineMappingResults <- function(..., ldReference = NULL) { .fmCacheLookup(cfg$fineMappingResult, cfg$study, ctx, pcName, "susie") } -# All usePCA row-records for one context: PCA-reduce the multi-trait phenotype -# and fine-map each top PC. A single-trait context (or one with no usable PC -# scores) contributes nothing. +# One context's usePCA work before any genotype is read: PCA-reduce the +# multi-trait phenotype, and find which top PCs the resume cache leaves to fit. +# NULL for a single-trait context, or one with no usable PC scores, which +# contributes nothing. # @noRd -.fmPcaContextRows <- function(ctx, cfg) { +.fmPcaContextPlan <- function(ctx, cfg) { traits <- cfg$perCtxTraits[[ctx]] if (length(traits) < 2L) { - return(list()) + return(NULL) } Yctx <- .fmResidPheno( cfg$data, @@ -2765,7 +2765,7 @@ combineFineMappingResults <- function(..., ldReference = NULL) { ) scores <- .fmTopPcScores(Yctx, cfg$nPCs) if (is.null(scores)) { - return(list()) + return(NULL) } if (cfg$verbose >= 1) { nPc <- ncol(scores) @@ -2775,38 +2775,86 @@ combineFineMappingResults <- function(..., ldReference = NULL) { "({nTr} traits) ..." )) } - .fmPcaRows(ctx, traits, scores, cfg) -} - -# A context's top-PC rows: a PC in the resume cache as cached, the rest fit -# window by window. -# @noRd -.fmPcaRows <- function(ctx, traits, scores, cfg) { pcNames <- colnames(scores) cached <- set_names( map(pcNames, .fmPcaCacheLookup, ctx = ctx, cfg = cfg), pcNames ) - toFit <- pcNames[map_lgl(cached, is.null)] - # Windows outermost, so each window's block is built once for all the - # PCs and released before the next window's. - perRegion <- if (length(toFit) == 0L) { - list() - } else { - map( - cfg$xRegions, - .fmPcaRegionFits, - ctx = ctx, - traits = traits, - scores = scores[, toFit, drop = FALSE], - cfg = cfg - ) - } - list_flatten(map( - pcNames, - .fmPcaPcRows, + list( ctx = ctx, + traits = traits, + scores = scores, cached = cached, + toFit = pcNames[map_lgl(cached, is.null)] + ) +} + +# Every context's fits in one region, to run in the order of `plans`. +# @noRd +.fmPcaRegionWork <- function(rg, plans, cfg) { + .qtlWork( + plans, + map(plans, .fmPcaPlanSpec, rg = rg, cfg = cfg), + .fmPcaRegionFits, + list(rg = rg, cfg = cfg) + ) +} + +# The genotypes a context's top PCs are fit on, in region `rg`. +# @noRd +.fmPcaPlanSpec <- function(plan, rg, cfg) { + .qtlFitSpec( + .fmGenoWindow(cfg$data, plan$traits, rg, cfg$cisWindow), + plan$ctx, + rownames(plan$scores) + ) +} + +# Whether a context has any top PC to fit. +# @noRd +.fmPcaPlanHasFits <- function(plan) { + length(plan$toFit) > 0L +} + +# Every usePCA fit of a run, for the row-records of .fmPcaAllRows(). Windows +# outermost, so each window's block is built once per context for all the PCs +# and released before the next window's. +# @noRd +.fmPcaWork <- function(cfg) { + plans <- compact(map(cfg$useCtx, .fmPcaContextPlan, cfg = cfg)) + fitting <- map_lgl(plans, .fmPcaPlanHasFits) + .qtlWorkOf( + map(cfg$xRegions, .fmPcaRegionWork, plans = plans[fitting], cfg = cfg), + .fmPcaAllRows, + list(plans = plans, fitting = fitting, cfg = cfg) + ) +} + +# Every usePCA row-record of a run: a PC in the resume cache as cached, the rest +# from their fits. +# @noRd +.fmPcaAllRows <- function(perRegion, plans, fitting, cfg) { + planFits <- replace( + rep(list(list()), length(plans)), + which(fitting), + map(seq_len(sum(fitting)), .fmPcaPlanFits, perRegion = perRegion) + ) + list_flatten(map2(plans, planFits, .fmPcaPlanRows, cfg = cfg)) +} + +# One fitting context's per-region fits. +# @noRd +.fmPcaPlanFits <- function(k, perRegion) { + map(perRegion, pluck, k) +} + +# @noRd +.fmPcaPlanRows <- function(plan, perRegion, cfg) { + list_flatten(map( + colnames(plan$scores), + .fmPcaPcRows, + ctx = plan$ctx, + cached = plan$cached, perRegion = perRegion, cfg = cfg )) @@ -2899,10 +2947,11 @@ combineFineMappingResults <- function(..., ldReference = NULL) { .screenResolve(signalScreenParam) } -# The joint-specification phase for the QtlDataset path: dispatch whatever the -# spec covers, narrow the tokens the joint fits consumed, and report whether -# anything is left for the per-tuple loop. `done = TRUE` means the joint fits -# are the whole answer. +# The joint-specification phase for the QtlDataset path: plan whatever the spec +# covers, narrow the tokens the joint fits consume, and report whether anything +# is left for the per-tuple loop. `done = TRUE` means the joint fits are the +# whole answer. Planned, not run: they share their reads with the per-tuple +# fits. # # Deliberately mirrors .twasQdsJointPhase -- same responsibilities, same # done / result shape -- so the two pipelines' joint handling reads as one @@ -2913,38 +2962,48 @@ combineFineMappingResults <- function(..., ldReference = NULL) { if (length(parsedJointSpec) == 0L) { return(list( done = FALSE, - result = NULL, + work = .qtlWorkValue(NULL), tokens = norm$tokens, methodArgs = norm$methodArgs )) } - jointResult <- .fmDispatchJointSpecsQtlDataset( + work <- .fmJointSpecsWork( parsedJointSpec, intersect(norm$tokens, c("mvsusie", "fsusie")), norm$methodArgs, cfg ) - # The joint dispatch consumed the multivariate tokens; the univariate + # The joint dispatch consumes the multivariate tokens; the univariate # phase runs on whatever is left. narrowed <- .fmNarrowAfterJoint(norm$tokens, norm$methodArgs) - if (length(narrowed$tokens) == 0L) { - if (is.null(jointResult)) { - abort(glue( - "fineMappingPipeline(QtlDataset): no joint fits produced. ", - "Check that the jointSpecification scope intersects the ", - "available studies / contexts / traits." - )) - } - return(list(done = TRUE, result = jointResult)) - } list( - done = FALSE, - result = jointResult, + done = length(narrowed$tokens) == 0L, + work = work, tokens = narrowed$tokens, methodArgs = narrowed$methodArgs ) } +# A run whose joint fits are the whole answer. +# @noRd +.fmQdsJointOnly <- function(work, cfg) { + jointResult <- .qtlRunWork( + work, + cfg$data, + cfg$genotypeFilter, + cfg$residualizationParam, + withAf = TRUE + ) + if (is.null(jointResult)) { + abort(glue( + "fineMappingPipeline(QtlDataset): no joint fits produced. ", + "Check that the jointSpecification scope intersects the ", + "available studies / contexts / traits." + )) + } + jointResult +} + # Normalize methods, capability-check them, then hand off to the joint phase. # @noRd .fmQdsResolveTokens <- function( @@ -3071,39 +3130,35 @@ combineFineMappingResults <- function(..., ldReference = NULL) { # Univariate + usePCA dispatch: each (context, trait) -> merged-per-token # row-records; each multi-trait context's top PCs -> pseudo-trait rows. # @noRd -.fmQdsDispatchRows <- function(cfg) { - univRows <- if (length(cfg$univTokens) > 0L) { - list_flatten(map(cfg$useCtx, .fmUnivContextRows, cfg = cfg)) - } else { - list() - } - pcaRows <- if (isTRUE(cfg$usePCA)) { - list_flatten(map(cfg$useCtx, .fmPcaContextRows, cfg = cfg)) - } else { - list() - } - c(univRows, pcaRows) +.fmQdsRowsWork <- function(cfg) { + .qtlWorkOf( + c( + if (length(cfg$univTokens) > 0L) list(.fmUnivWork(cfg)), + if (isTRUE(cfg$usePCA)) list(.fmPcaWork(cfg)) + ), + list_flatten + ) } # Multivariate dispatch via the joint engine (auto-detected shape) for # mvsusie / fsusie WITHOUT an explicit jointSpecification. An explicit spec # consumes those tokens, so they reach here only when there was none: the -# explicit result, `jointResult`, is returned as it stands when no -# multivariate token is left, and is NULL otherwise. +# explicit plan, `jointWork`, stands as it is when no multivariate token is +# left, and has no fits otherwise. # @noRd -.fmQdsAutoJoint <- function( +.fmQdsAutoJointWork <- function( cfg, mvTokens, fsTokens, - jointResult, + jointWork, methodArgs, nCtx, nTraits ) { if (length(mvTokens) == 0L && length(fsTokens) == 0L) { - return(jointResult) + return(jointWork) } - .fmDispatchJointSpecsQtlDataset( + .fmJointSpecsWork( .fmSynthesizeJointSpec(nCtx, nTraits), c(mvTokens, fsTokens), methodArgs, @@ -3179,7 +3234,7 @@ combineFineMappingResults <- function(..., ldReference = NULL) { region, tokens, methodArgs, - jointResult, + jointWork, initializeWithSusieInf, nPCs, phenotypeFilter, @@ -3192,7 +3247,7 @@ combineFineMappingResults <- function(..., ldReference = NULL) { region = region ) split <- .fmQdsSplitTokens(tokens, ctxInfo$nCtx, ctxInfo$nTraits) - rows <- .fmQdsDispatchRows(.fmQdsRunCfg( + rowsWork <- .fmQdsRowsWork(.fmQdsRunCfg( cfg, initializeWithSusieInf = initializeWithSusieInf, methodArgs = methodArgs, @@ -3204,20 +3259,43 @@ combineFineMappingResults <- function(..., ldReference = NULL) { useCtx = ctxInfo$useCtx, usePCA = usePCA )) - .fmQdsAssemble( + autoJointWork <- .fmQdsAutoJointWork( + cfg, + mvTokens = split$mvTokens, + fsTokens = split$fsTokens, + jointWork = jointWork, + methodArgs = methodArgs, + nCtx = ctxInfo$nCtx, + nTraits = ctxInfo$nTraits + ) + .fmQdsRunPaths(list(rowsWork, autoJointWork), cfg, region) +} + +# A run's paths, all planned, so one read of a window serves them all. +# @noRd +.fmQdsRunPaths <- function(paths, cfg, region) { + .qtlRunWork( + .qtlWorkOf( + paths, + .fmQdsAssembleResults, + list(data = cfg$data, region = region, cisWindow = cfg$cisWindow) + ), cfg$data, - rows, + cfg$genotypeFilter, + cfg$residualizationParam, + withAf = TRUE + ) +} + +# The result of a run's paths: the per-tuple row-records, then the joint result. +# @noRd +.fmQdsAssembleResults <- function(results, data, region, cisWindow) { + .fmQdsAssemble( + data, + results[[1L]], + results[[2L]], region = region, - cisWindow = cfg$cisWindow, - .fmQdsAutoJoint( - cfg, - mvTokens = split$mvTokens, - fsTokens = split$fsTokens, - jointResult = jointResult, - methodArgs = methodArgs, - nCtx = ctxInfo$nCtx, - nTraits = ctxInfo$nTraits - ) + cisWindow = cisWindow ) } @@ -3257,8 +3335,7 @@ combineFineMappingResults <- function(..., ldReference = NULL) { dataDrivenPriorWeightsCutoff = 1e-10, phenotypeFilterParam = PhenotypeFilterParam(), verbose = 1, - fitRetention = c("slim", "full"), - dataType = NULL + fitRetention = c("slim", "full") ) { fitRetention <- arg_match(fitRetention) phenotypeFilter <- .fmResolvePhenotypeFilter(phenotypeFilterParam) @@ -3306,7 +3383,7 @@ combineFineMappingResults <- function(..., ldReference = NULL) { cfg ) out <- if (rt$done) { - rt$result + .fmQdsJointOnly(rt$work, cfg) } else { .fmQdsRunAndAssemble( cfg, @@ -3315,7 +3392,7 @@ combineFineMappingResults <- function(..., ldReference = NULL) { region = region, tokens = rt$tokens, methodArgs = rt$methodArgs, - jointResult = rt$result, + jointWork = rt$work, initializeWithSusieInf = initializeWithSusieInf, nPCs = nPCs, phenotypeFilter = phenotypeFilter, @@ -3325,7 +3402,7 @@ combineFineMappingResults <- function(..., ldReference = NULL) { .withGenome( .fmRecordProvenance( out, - dataType, + data, "individual", .qtlScaleResiduals(residualizationParam) ), @@ -3362,7 +3439,11 @@ setMethod( # `cfg` bundles the parent call's forwarded arguments. # @noRd .fmPerStudy <- function(qd, cfg) { - m <- .fmFilterMethodsForKind(cfg$methods, "individualImpl") + specs <- .msStudySpecs(cfg$studySpecs, qd) + m <- .fmFilterMethodsForKind( + if (length(specs) > 0L) cfg$studyMethods else cfg$methods, + "individualImpl" + ) if (length(m) == 0L) { return(NULL) } @@ -3383,7 +3464,7 @@ setMethod( fineMappingPipeline( data = qd, methods = m, - jointSpecification = NULL, + jointSpecification = if (length(specs) > 0L) specs, contexts = .selectedContexts(cfg$contexts, s), traitId = .studyTraitIdArg(cfg$traitId, s), region = cfg$region, @@ -3403,8 +3484,7 @@ setMethod( nPCs = cfg$nPCs, fitRetention = cfg$fitRetention, mrmashPrior = cfg$mrmashPrior, - dataDrivenPriorWeightsCutoff = cfg$dataDrivenPriorWeightsCutoff, - dataType = cfg$dataType + dataDrivenPriorWeightsCutoff = cfg$dataDrivenPriorWeightsCutoff ) } @@ -3443,8 +3523,7 @@ setMethod( susieRssParam = cfg$susieRssParam, fitRetention = cfg$fitRetention, mrmashPrior = cfg$mrmashPrior, - dataDrivenPriorWeightsCutoff = cfg$dataDrivenPriorWeightsCutoff, - dataType = cfg$dataType + dataDrivenPriorWeightsCutoff = cfg$dataDrivenPriorWeightsCutoff ) } @@ -3465,15 +3544,19 @@ setMethod( methodArgs = norm$methodArgs )) } + # The joint dispatch consumes the multivariate tokens; the univariate + # phase runs on whatever is left. A component with univariate fits to come + # fits its joint specs along with them, on one read of each window. + narrowed <- .fmNarrowAfterJoint(norm$tokens, norm$methodArgs) + jointTokens <- intersect(norm$tokens, c("mvsusie", "fsusie")) + withComponents <- length(narrowed$tokens) > 0L && length(jointTokens) > 0L jointResult <- .fmDispatchJointSpecsMultiStudy( parsedJointSpec, - intersect(norm$tokens, c("mvsusie", "fsusie")), + jointTokens, norm$methodArgs, - cfg + cfg, + components = !withComponents ) - # The joint dispatch consumed the multivariate tokens; the univariate - # phase runs on whatever is left. - narrowed <- .fmNarrowAfterJoint(norm$tokens, norm$methodArgs) if (length(narrowed$tokens) == 0L) { if (is.null(jointResult)) { abort(glue( @@ -3488,7 +3571,11 @@ setMethod( done = FALSE, result = jointResult, tokens = narrowed$tokens, - methodArgs = narrowed$methodArgs + methodArgs = narrowed$methodArgs, + studySpecs = if (withComponents) { + .fmSplitStudyAxisSpecs(parsedJointSpec)$nonStudy + }, + studyMethods = norm$methodArgs ) } @@ -3539,7 +3626,9 @@ setMethod( methods } else { phase$methodArgs - } + }, + studySpecs = phase$studySpecs, + studyMethods = phase$studyMethods ) } @@ -3578,8 +3667,7 @@ setMethod( seed = NULL, phenotypeFilterParam = PhenotypeFilterParam(), verbose = 1, - fitRetention = c("slim", "full"), - dataType = NULL + fitRetention = c("slim", "full") ) { fitRetention <- arg_match(fitRetention) # Resolved once here so a bad choice fails before any study runs; the @@ -3621,8 +3709,7 @@ setMethod( nPCs = nPCs, fitRetention = fitRetention, mrmashPrior = mrmashPrior, - dataDrivenPriorWeightsCutoff = dataDrivenPriorWeightsCutoff, - dataType = dataType + dataDrivenPriorWeightsCutoff = dataDrivenPriorWeightsCutoff ) .msAssertTraitsSelected(data, cfg, "fineMappingPipeline") jointCfg <- .fmMsJointCfg(cfg, data, inputs$xRegions) @@ -3635,7 +3722,7 @@ setMethod( ) jointResult <- .fmRecordProvenance( rt$result, - dataType, + data, .fmMsDataSource(data), .qtlScaleResiduals(residualizationParam) ) @@ -3648,9 +3735,13 @@ setMethod( jointResult, .fmPerStudy, .fmSumStats, - list_assign(cfg, methods = rt$methods), + list_assign( + cfg, + methods = rt$methods, + studySpecs = rt$studySpecs, + studyMethods = rt$studyMethods + ), .rbindFineMappingResult, - QtlFineMappingResult, "fineMappingPipeline" ), build @@ -4246,8 +4337,7 @@ NULL verbose, fitRetention, susieRssParam, - panelFilterParam, - dataType = NULL + panelFilterParam ) { inputs <- .fmQssInputs(data, contexts, traitId, cisWindow) cfgs <- .fmQssConfigs( @@ -4278,7 +4368,7 @@ NULL initializeWithSusieInf = initializeWithSusieInf, susieRssParam = susieRssParam ) - .fmRecordProvenance(out, dataType, "sumstat", TRUE) + .fmRecordProvenance(out, data, "sumstat", TRUE) } # Everything after the joint phase has had its say: select the rows the @@ -4380,8 +4470,7 @@ setMethod( susieRssParam = SusieRssParam(), panelFilterParam = PanelFilterParam(), crossValidationParam = CrossValidationParam(), - residualizationParam = ResidualizationParam(), - dataType = NULL + residualizationParam = ResidualizationParam() ) { fitRetention <- arg_match(fitRetention) build <- .fmSumstatsBuild(crossValidationParam, data, fineMappingResult) @@ -4400,8 +4489,7 @@ setMethod( verbose = verbose, fitRetention = fitRetention, susieRssParam = susieRssParam, - panelFilterParam = panelFilterParam, - dataType = dataType + panelFilterParam = panelFilterParam ) .withGenome(res, build) } @@ -4469,8 +4557,7 @@ setMethod( verbose, fitRetention, susieRssParam, - panelFilterParam, - dataType = NULL + panelFilterParam ) { .fmAssertQcd(data) .fmWarnChromosomeRows(data) @@ -4502,7 +4589,7 @@ setMethod( methodArgs = methodArgs, susieRssParam = susieRssParam ) - .fmRecordProvenance(out, dataType, "sumstat", TRUE) + .fmRecordProvenance(out, NULL, "sumstat", TRUE) } # The widest stretch a GWAS row can span and still read as an LD block. LD @@ -4602,8 +4689,7 @@ setMethod( susieRssParam = SusieRssParam(), panelFilterParam = PanelFilterParam(), crossValidationParam = CrossValidationParam(), - residualizationParam = ResidualizationParam(), - dataType = NULL + residualizationParam = ResidualizationParam() ) { fitRetention <- arg_match(fitRetention) build <- .fmSumstatsBuild(crossValidationParam, data, fineMappingResult) @@ -4616,8 +4702,7 @@ setMethod( verbose = verbose, fitRetention = fitRetention, susieRssParam = susieRssParam, - panelFilterParam = panelFilterParam, - dataType = dataType + panelFilterParam = panelFilterParam ) .withGenome(res, build) } @@ -4756,15 +4841,10 @@ setMethod( .fmQtlRow(studyName, ctx, tid, l$tk, l$cached) } -# All univariate row-records for one context (over its per-context traits). +# The plans of one context's traits. # @noRd -.fmUnivContextRows <- function(ctx, cfg) { - list_flatten(map( - cfg$perCtxTraits[[ctx]], - .fmUnivTraitRows, - ctx = ctx, - cfg = cfg - )) +.fmUnivContextPlans <- function(ctx, cfg) { + map(cfg$perCtxTraits[[ctx]], .fmUnivPlan, ctx = ctx, cfg = cfg) } # ============================================================================= diff --git a/R/fineMappingRow.R b/R/fineMappingRow.R index 6e9e89912..b0f95d9de 100644 --- a/R/fineMappingRow.R +++ b/R/fineMappingRow.R @@ -1292,21 +1292,21 @@ FineMappingRow <- function(variantIds, susieFit, topLoci, cvResult = NULL) { } -# Match the entry's variants to `keepVariants` by (chrom, pos, allele) so a -# chr-prefix / separator difference does not read as no-overlap. PIP / LBF are -# coding-invariant (no sign). The index is sorted to keep the entry's variant -# order so fit-matrix subsetting is unchanged. +# Match the entry's variants to `keepVariants` as every keep list is matched +# (.keepByIds()), so a chr-prefix / separator / allele-order difference does not +# read as no-overlap. PIP / LBF are coding-invariant (no sign). The index keeps +# the entry's variant order so fit-matrix subsetting is unchanged. # @noRd .adjustPipsKeepIdx <- function(variantIds, keepVariants) { - m <- matchVariants(variantIds, as.character(keepVariants)) - if (!length(m$idxA)) { + kept <- which(.keepIdsNamed(variantIds, as.character(keepVariants))) + if (length(kept) == 0L) { msg <- glue( "adjustPips: intersection of entry variants with `keepVariants` ", "is empty." ) abort(msg) } - sort(m$idxA) + kept } # Where each of `common` sits among the variables `fitted` the fit was made diff --git a/R/fineMappingWrappers.R b/R/fineMappingWrappers.R index fd31a2a14..2f87cdaa1 100644 --- a/R/fineMappingWrappers.R +++ b/R/fineMappingWrappers.R @@ -1104,20 +1104,22 @@ computeCsTable <- function( max(R[sets[[l]], sets[[k]]]) } -# GRanges spanning the fit's variants (parsed from chrom:pos in the ids). +# GRanges spanning the fit's variants, read from their ids. .csVariantRegion <- function(variantIds) { - parts <- str_split(variantIds, ":", simplify = TRUE) - chrom <- unique(parts[, 1L]) + parsed <- parseVariantId(variantIds) + chrom <- unique(parsed$chrom) if (length(chrom) != 1L) { abort(glue( "computeCsCorrelation(): the fit variants span multiple ", "chromosomes ({str_flatten(chrom, ', ')})." )) } - pos <- as.integer(parts[, 2L]) GenomicRanges::GRanges( seqnames = chrom, - ranges = IRanges::IRanges(start = min(pos), end = max(pos)) + ranges = IRanges::IRanges( + start = min(parsed$pos), + end = max(parsed$pos) + ) ) } diff --git a/R/genotypeIo.R b/R/genotypeIo.R index a2845f2c9..b753e57bb 100644 --- a/R/genotypeIo.R +++ b/R/genotypeIo.R @@ -339,7 +339,7 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { } geno <- .handleDosage(handle, snpIdx) .blockGenotypesToSe( - if (meanImpute) .meanImputeGeno(geno) else geno, + if (meanImpute) .qtlMeanImpute(geno) else geno, handle, snpIdx ) @@ -348,19 +348,23 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { # The samples x variants dosages of a handle's variants at `snpIdx` (never # an empty request), as read: neither imputed nor labelled. It reads only # the columns of the handle's variant table that find each variant in its -# file (.HANDLE_READ_COLUMNS), which are all a panel's seed keeps. +# file (.HANDLE_READ_COLUMNS), which are all a panel's seed keeps. `sampleIdx`, +# ascending positions, reads only those samples; NULL reads every one. # @noRd -.handleDosage <- function(handle, snpIdx) { +.handleDosage <- function(handle, snpIdx, sampleIdx = NULL) { # One-file-per-chromosome handle: route by chromosome to the right file. # `.genotypeChromPaths` tolerates handles deserialized before the slot # existed (treated as single-file). if (length(.genotypeChromPaths(handle)) > 0L) { - return(.handleDosageSharded(handle, snpIdx)) + return(.handleDosageSharded(handle, snpIdx, sampleIdx)) } # Read ascending, then put the columns back in the requested order. See # .restoreRequestedOrder() for why this is not merely tidiness. ord <- order(.genotypeFilePos(handle, snpIdx)) - .restoreRequestedOrder(.extractBlockByFormat(handle, snpIdx[ord]), ord) + .restoreRequestedOrder( + .extractBlockByFormat(handle, snpIdx[ord], sampleIdx), + ord + ) } # Position of each requested variant in the underlying file. snpInfo rows are @@ -768,12 +772,12 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { # Dispatch block extraction to the format-specific backend (samples x variants). # @noRd -.extractBlockByFormat <- function(handle, snpIdx) { +.extractBlockByFormat <- function(handle, snpIdx, sampleIdx = NULL) { fmt <- genotypeFormat(handle) switch( fmt, - "plink2" = .extractBlockPlink2(handle, snpIdx), - "bgen" = .extractBlockBgen(handle, snpIdx), + "plink2" = .extractBlockPlink2(handle, snpIdx, sampleIdx), + "bgen" = .extractBlockBgen(handle, snpIdx, sampleIdx), .abortUnsupportedFormat(fmt, "extractBlockGenotypes") ) } @@ -805,7 +809,7 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { # group read from its chromosome's file (.chromDosage()), and the columns # put back in the requested order. # @noRd -.handleDosageSharded <- function(handle, snpIdx) { +.handleDosageSharded <- function(handle, snpIdx, sampleIdx = NULL) { unifiedChr <- snpInfo(handle)$CHR groups <- split(seq_along(snpIdx), unifiedChr[snpIdx]) blocks <- map2( @@ -814,7 +818,8 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { .chromDosage, handle = handle, snpIdx = snpIdx, - unifiedChr = unifiedChr + unifiedChr = unifiedChr, + sampleIdx = sampleIdx ) dosageBlocks <- unname(blocks) exec(cbind, !!!dosageBlocks)[, @@ -827,7 +832,14 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { # single-file view of that chromosome's file holding the chromosome's rows of # the unified table, so positional backends like PLINK2 stay valid. # @noRd -.chromDosage <- function(chrom, posInReq, handle, snpIdx, unifiedChr) { +.chromDosage <- function( + chrom, + posInReq, + handle, + snpIdx, + unifiedChr, + sampleIdx = NULL +) { if (!is_in(chrom, names(chromPaths(handle)))) { msg <- glue( "extractBlockGenotypes: no per-chromosome file for chromosome ", @@ -845,7 +857,7 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { # Treated as single-file: the view addresses one chromosome's payload. chromPaths = character(0) ) - .handleDosage(view, match(snpIdx[posInReq], blockGlobal)) + .handleDosage(view, match(snpIdx[posInReq], blockGlobal), sampleIdx) } # Empty-block SE (no variants) carrying the handle's sample dimension. @@ -866,7 +878,7 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { #' @keywords internal #' @importFrom rlang try_fetch -.extractBlockPlink2 <- function(handle, snpIdx) { +.extractBlockPlink2 <- function(handle, snpIdx, sampleIdx = NULL) { # pgenlibr::ReadList returns ALT dosage = A1 dosage in pecotmr convention. # The cached @pgenPtr does not survive saveRDS/readRDS (external pointers # become stale), so we re-open from path() on the fly if the cached @@ -882,7 +894,10 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { # A sharded handle routes through a transient view with pgenPtr = NULL (one # pgen per chromosome), and a deserialized pointer is stale; open a fresh # pgen up front in those cases rather than provoking a caught read error. - ptr <- pgenPtr(handle) %||% pgenlibr::NewPgen(paths$pgen) + # So does a read of some samples: a pgen is restricted when it is opened, + # and the cached one was opened for all of them. + cached <- if (is.null(sampleIdx)) pgenPtr(handle) + ptr <- cached %||% pgenlibr::NewPgen(paths$pgen, sample_subset = sampleIdx) geno <- try_fetch( pgenlibr::ReadList( ptr, @@ -890,7 +905,7 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { meanimpute = FALSE ), error = function(cnd) { - reopened <- pgenlibr::NewPgen(paths$pgen) + reopened <- pgenlibr::NewPgen(paths$pgen, sample_subset = sampleIdx) pgenlibr::ReadList( reopened, variant_subset = variantSubset, @@ -997,23 +1012,6 @@ extractBlockGenotypes <- function(handle, snpIdx, meanImpute = TRUE) { ) } -# ============================================================================= -# Helpers -# ============================================================================= - -#' @keywords internal -.meanImputeGeno <- function(geno) { - naMask <- is.na(geno) - if (!any(naMask)) { - return(geno) - } - # One fill over the whole matrix rather than a copy per column: geno is - # samples x variants, so rebuilding it column by column is the expensive - # way to say this. - means <- colMeans(geno, na.rm = TRUE) - replace(geno, naMask, means[col(geno)[naMask]]) -} - # ============================================================================= # Genotype format detection # ----------------------------------------------------------------------------- @@ -1561,8 +1559,9 @@ refVariantInfo <- function(source, which = NULL) { pos <= parsed$end[r] } -#' Match variant_info against a whitelist file, returning logical index. Uses -#' parse_variant_id() from misc.R to handle all variant ID formats. +#' Match variant_info against a whitelist file, returning logical index. The +#' file is a table with chrom and pos columns, or one variant id per line in +#' any format parseVariantId() reads, or one label (rsID) per line. #' @importFrom vroom vroom #' @importFrom readr read_lines #' @noRd @@ -1582,30 +1581,80 @@ matchVariantsToKeep <- function(variantInfo, keepVariantsPath) { is_in("chrom", names(keepRaw)) && is_in("pos", names(keepRaw)) ) { - keepVariants <- parseVariantId(keepRaw) - } else { - # Fall back to reading as single-column variant IDs - ids <- read_lines(keepVariantsPath) - keepVariants <- parseVariantId(ids) + return(.keepByVariant(variantInfo, parseVariantId(keepRaw))) + } + # Fall back to reading as single-column variant IDs + .keepByIds(variantInfo, read_lines(keepVariantsPath)) +} + +# The panel variants a list of ids names. An id that gives a position and real +# alleles is matched by them; any other is matched against the panel's labels, +# which is all an rsID, or an id with a tag where an allele belongs, can be +# matched by. A header line matches nothing. Every keep list decides through +# this: a file, and the keepVariants of a genotype filter, of summaryStatsQc() +# and of adjustPips(). +# @noRd +.keepByIds <- function(variantInfo, ids) { + keepVariants <- parseVariantId(ids) + isLabel <- is.na(keepVariants$pos) | + !.isDnaAllele(keepVariants$A1) | + !.isDnaAllele(keepVariants$A2) + .keepByVariant(variantInfo, keepVariants) | + .keepByLabel(variantInfo, ids[isLabel]) +} + +# .keepByIds() for variants known only by their ids, as a fit's are: where each +# is and its alleles are read off the id. +# @noRd +.keepIdsNamed <- function(variantIds, ids) { + .keepByIds(mutate(parseVariantId(variantIds), id = variantIds), ids) +} + +# The panel variants a parsed keep list names: by position and alleles, or by +# position alone when the list does not give every variant's alleles. +# @noRd +.keepByVariant <- function(variantInfo, keepVariants) { + located <- keepVariants[ + !is.na(keepVariants$chrom) & !is.na(keepVariants$pos), + , + drop = FALSE + ] + hasAlleles <- is_in("A1", names(located)) && + is_in("A2", names(located)) && + !anyNA(located$A1) && + !anyNA(located$A2) + if (!hasAlleles) { + return(is_in( + str_c(variantInfo$chrom, variantInfo$pos, sep = ":"), + str_c(located$chrom, located$pos, sep = ":") + )) } - hasAlleles <- is_in("A1", names(keepVariants)) && - is_in("A2", names(keepVariants)) && - !any(is.na(keepVariants$A1)) && - !any(is.na(keepVariants$A2)) - is_in( - .keepVariantKey(variantInfo, hasAlleles), - .keepVariantKey(keepVariants, hasAlleles) + # A keep list asks only whether a variant is named, and no sign is applied: + # a variant listed in both allele orders or held twice is no conflict, and + # an A/T variant spelled T/A is the same variant. + matched <- matchVariants( + variantInfo, + located, + removeStrandAmbiguous = FALSE, + allPairs = TRUE ) + is_in(seq_len(nrow(variantInfo)), matched$idxA) } -# A variant table's keep-list match key: chrom:pos:A2:A1 when the keep list -# names alleles, chrom:pos when it names positions only. +# The panel variants a list of labels names, by the file's own variant ids. # @noRd -.keepVariantKey <- function(tbl, withAlleles) { - if (withAlleles) { - return(str_c(tbl$chrom, tbl$pos, tbl$A2, tbl$A1, sep = ":")) +.keepByLabel <- function(variantInfo, labels) { + # Most lists hold none, and matching parses every label of the panel. + if (length(labels) == 0L) { + return(rep(FALSE, nrow(variantInfo))) } - str_c(tbl$chrom, tbl$pos, sep = ":") + matched <- matchVariants( + variantInfo$id, + labels, + removeStrandAmbiguous = FALSE, + allPairs = TRUE + ) + is_in(seq_len(nrow(variantInfo)), matched$idxA) } #' Load genotype data for a specific region @@ -1620,8 +1669,15 @@ matchVariantsToKeep <- function(variantInfo, keepVariantsPath) { #' @param genotype Path to the genotype data file, or a PLINK prefix. #' @param which Optional \code{GRanges}: load only the variants that overlap #' it. If NULL, loads every variant. -#' @param keepIndel Whether to keep indel SNPs. -#' @param keepVariantsPath Path to a file listing variants to keep. +#' @param keepIndel Whether to keep variants other than SNPs. A SNP has exactly +#' one of A, C, G, T for each allele, so \code{I}/\code{D} codes, \code{*} +#' and \code{N} count as indels. +#' @param keepVariantsPath Path to a file listing variants to keep: a table +#' with \code{chrom} and \code{pos} columns, with their alleles in \code{A2} +#' and \code{A1} or without any, or one variant id or rsID per line. Alleles +#' match in either order and on either strand; a table without alleles keeps +#' every variant at each position, and an rsID is matched against the +#' file's own variant ids. #' @param returnVariantInfo If TRUE, return a list with X (dosage matrix) and #' variant_info (data.frame). If FALSE (default), return only the dosage #' matrix. @@ -1655,10 +1711,10 @@ loadGenotypeRegion <- function( format = .requireGenotypeFormat(genotype, "loadGenotypeRegion") ) variants <- .rangesSnpInfo(SummarizedExperiment::rowRanges(panel)) - snpIdx <- .loadGenoSnpIdx(variants, which) + inRegion <- .loadGenoSnpIdx(variants, which) # Apply the indel and whitelist filters to the variant index before any # dosage is read, so only the requested variants are loaded. - snpIdx <- .loadGenoKeepIdx(variants, snpIdx, keepIndel, keepVariantsPath) + snpIdx <- .loadGenoKeepIdx(variants, inRegion, keepIndel, keepVariantsPath) # Samples x variants matrix (pecotmr convention), its columns named by # the file's own variant ids; callers handle missing. extracted <- list( @@ -1726,14 +1782,13 @@ loadGenotypeRegion <- function( return(snpIdx) } info <- .snpInfoToVariantInfo(slice(variants, snpIdx)) - keep <- rep(TRUE, length(snpIdx)) - if (!keepIndel) { - keep <- keep & isSnpAlleles(info$A1, info$A2) - } - if (!is.null(keepVariantsPath)) { - keep <- keep & matchVariantsToKeep(info, keepVariantsPath) + passesIndel <- keepIndel | isSnpAlleles(info$A1, info$A2) + onKeepList <- if (is.null(keepVariantsPath)) { + TRUE + } else { + matchVariantsToKeep(info, keepVariantsPath) } - snpIdx[keep] + snpIdx[passesIndel & onKeepList] } # Restore a stochastic sketch's original scale from its metadata sidecar. @@ -2179,9 +2234,9 @@ loadGenotypeRegion <- function( # The samples x variants dosages of the handle's variants at `snpIdx`: each # record read once, its probabilities decompressed and decoded for the -# alleles its rows ask about. +# alleles its rows ask about and the samples at `sampleIdx` (NULL: all of them). # @noRd -.extractBlockBgen <- function(handle, snpIdx) { +.extractBlockBgen <- function(handle, snpIdx, sampleIdx = NULL) { si <- slice(snpInfo(handle), snpIdx) path <- .genotypeReadPath(handle) header <- .bgenReadHeader(path) @@ -2194,7 +2249,8 @@ loadGenotypeRegion <- function( si = si, con = con, header = header, - nSamples = handle@nSamples + nSamples = handle@nSamples, + sampleIdx = sampleIdx ) out <- exec(cbind, !!!blocks) # The blocks hold the rows grouped by record; put them back in `si`'s @@ -2210,26 +2266,45 @@ loadGenotypeRegion <- function( # One record's dosage columns, for the rows of `si` that read it. # @noRd -.bgenRecordDosage <- function(offset, si, con, header, nSamples) { +.bgenRecordDosage <- function( + offset, + si, + con, + header, + nSamples, + sampleIdx = NULL +) { rows <- .bgenRowsOf(offset, si) record <- .bgenReadRecord(con, offset, si$bgen_size[[rows[[1L]]]]) if (header$layout == 1L) { - return(.bgenLayout1Dosage(record, header$compression, nSamples)) + return(.bgenLayout1Dosage( + record, + header$compression, + nSamples, + sampleIdx + )) } probabilities <- .bgenProbabilityData(record, header$compression) bgenDosageCpp( probabilities, as.integer(nSamples), - as.integer(si$bgen_allele[rows]) + as.integer(si$bgen_allele[rows]), + as.integer(sampleIdx) ) } # A layout-1 record's dosage column: per sample three 16-bit probabilities, # of the genotypes AA, AB and BB, each out of 32768, zlib-compressed or # stored as they are. The second allele's expected count is P(AB) + 2 P(BB); -# a sample whose three are all zero is missing. -# @noRd -.bgenLayout1Dosage <- function(record, compression, nSamples) { +# a sample whose three are all zero is missing. Every sample's six bytes sit at +# a known place, so only those at `sampleIdx` (NULL: all of them) are converted. +# @noRd +.bgenLayout1Dosage <- function( + record, + compression, + nSamples, + sampleIdx = NULL +) { at <- .bgenParseIdentifying(record, layout = 1L)$genotypeStart nBytes <- 6 * nSamples data <- if (compression == 0L) { @@ -2241,11 +2316,19 @@ loadGenotypeRegion <- function( if (length(data) != nBytes) { abort("A layout-1 BGEN record holds the wrong amount of data.") } + wanted <- if (is.null(sampleIdx)) { + data + } else { + data[sequence( + rep(6L, length(sampleIdx)), + from = 6L * (sampleIdx - 1L) + 1L + )] + } p <- matrix( readBin( - data, + wanted, "integer", - n = 3 * nSamples, + n = length(wanted) / 2L, size = 2L, signed = FALSE, endian = "little" diff --git a/R/h2Annotations.R b/R/h2Annotations.R index 128dc459d..d8ecd64df 100644 --- a/R/h2Annotations.R +++ b/R/h2Annotations.R @@ -249,6 +249,13 @@ setMethod( #' trait at \code{coverage} (and \code{minPurity}), else 0.} #' \item{\code{"highPip"}}{1 when its largest PIP is at least #' \code{pipThreshold}, else 0.} +#' \item{\code{"maxCppCs"}, \code{"sumPipCs"}}{\code{"maxCpp"} and +#' \code{"sumPip"} from the effects that produced a credible set +#' alone: each variant's \code{within_cs_pip}, its posterior in the +#' effect of its credible set at the fit's primary coverage and +#' purity, 0 outside any (continuous). Every effect a fit keeps spreads +#' a whole unit of posterior over its variants, however weak its +#' evidence, and these leave that low-confidence mass out.} #' } #' #' Variants are matched to the reference allele-aware (id format, ref/alt @@ -265,7 +272,9 @@ setMethod( #' \code{\link{GwasFineMappingResult}}. #' @param ldRef The \code{\linkS4class{LdStatistic}} the annotations will be #' used with. -#' @param types Which annotation types to build; see above. Default all four. +#' @param types Which annotation types to build; see above. Default +#' \code{"maxCpp"}, \code{"sumPip"}, \code{"credibleSet"} and +#' \code{"highPip"}. #' @param coverage Credible-set coverage for \code{"credibleSet"}. Default #' \code{0.95}. #' @param minPurity Minimum credible-set purity for \code{"credibleSet"}; @@ -319,7 +328,7 @@ fineMappingAnnotations <- function( ) { assertMultiClass(x, c("QtlFineMappingResult", "GwasFineMappingResult")) assertClass(ldRef, "LdStatistic") - chosenTypes <- arg_match(types, multiple = TRUE) + chosenTypes <- arg_match(types, .fmaTypes, multiple = TRUE) assertNumber(coverage, lower = 0, upper = 1) assertNumber(minPurity, lower = 0, upper = 1, null.ok = TRUE) assertNumber(pipThreshold, lower = 0, upper = 1) @@ -344,7 +353,7 @@ fineMappingAnnotations <- function( name = colnames(mat), tier = rep(chosenTier, ncol(mat)), type = if_else( - is_in(colTypes, c("maxCpp", "sumPip")), + is_in(colTypes, c("maxCpp", "sumPip", "maxCppCs", "sumPipCs")), "continuous", "binary" ) @@ -357,6 +366,18 @@ fineMappingAnnotations <- function( ) } +# Every annotation type fineMappingAnnotations() builds; its default leaves the +# credible-set PIP columns to be asked for. +# @noRd +.fmaTypes <- c( + "maxCpp", + "sumPip", + "credibleSet", + "highPip", + "maxCppCs", + "sumPipCs" +) + # One unit's columns over the reference's variants: its traits' PIPs and # credible-set members, its variants aligned to the reference in one pass by # the matching engine (.alignTablesToPanel()). @@ -379,13 +400,17 @@ fineMappingAnnotations <- function( hit <- !is.na(at) & !is.na(values) M <- .variantTableSize(refTable) maxPip <- .fmaScatter(M, at[hit], values[hit], "max") + withinCs <- as.numeric(md$within_cs_pip %||% rep(NA_real_, length(gr))) + inCs <- !is.na(at) & !is.na(withinCs) out <- list( maxCpp = maxPip, sumPip = .fmaScatter(M, at[hit], values[hit], "sum"), credibleSet = if (is_in("credibleSet", types)) { .fmaCredibleSetColumn(md, at, M, coverage, minPurity) }, - highPip = as.numeric(maxPip >= pipThreshold) + highPip = as.numeric(maxPip >= pipThreshold), + maxCppCs = .fmaScatter(M, at[inCs], withinCs[inCs], "max"), + sumPipCs = .fmaScatter(M, at[inCs], withinCs[inCs], "sum") )[types] named <- set_names(out, str_c(.fmaUnitName(tuples, k), "|", types)) exec(cbind, !!!named) @@ -396,6 +421,9 @@ fineMappingAnnotations <- function( #' @importFrom dplyr summarise #' @noRd .fmaScatter <- function(M, at, values, f) { + if (length(at) == 0L) { + return(numeric(M)) + } combine <- switch(f, max = max, sum = sum) byIndex <- tibble(at = at, value = values) |> summarise(value = combine(.data$value), .by = "at") diff --git a/R/h2EstimationWrappers.R b/R/h2EstimationWrappers.R index 848c52b78..cf362ab74 100644 --- a/R/h2EstimationWrappers.R +++ b/R/h2EstimationWrappers.R @@ -395,30 +395,57 @@ NULL ) } -# TRUE when the all-ones vector lies in the column span of `B`, by least -# squares on B'B (pivoted, so overlapping or duplicated columns do no harm), -# which keeps a genome-wide B from being copied into a QR. +# TRUE when the all-ones vector lies in the column span of `B`. # @noRd .h2SpansOnes <- function(B) { - decomp <- qr(crossprod(B)) - beta <- coalesce(qr.coef(decomp, colSums(B)), 0) - max(abs(1 - as.vector(B %*% beta))) < 1e-6 + max(abs(1 - as.vector(B %*% .h2OnesCoef(B)))) < 1e-6 +} + +# The least-squares coefficients of the all-ones vector on `B`, from B'B +# (pivoted, so overlapping or duplicated columns do no harm), which keeps a +# genome-wide B from being copied into a QR. +# @noRd +.h2OnesCoef <- function(B) { + unname(coalesce(qr.coef(qr(crossprod(B)), colSums(B)), 0)) } # The partitioned heritabilities, enrichment and per-annotation coefficients # of a stratified fit, reported for the user's baseline annotations alone, # from its per-variant-effect coefficients `tau` over the design `A` and their -# delete-one-block values `tauBlocks` (folds x columns). Annotations overlap -# in general -- the base overlaps every one -- so an annotation's -# heritability is (A'A tau)_a, not tau_a M_a, and the total is 1'A tau. +# delete-one-block values `tauBlocks` (folds x columns). # @noRd .h2StratifiedReport <- function(A, tau, tauBlocks) { user <- attr(A, "userCols") - AtA <- crossprod(A) + tauSe <- apply(tauBlocks, 2L, .jackknifeSeFromLoo)[user] + list( + tau = tau[user], + tauSe = tauSe, + annotationJackknifeCoefs = tauBlocks[, user, drop = FALSE], + enrichment = bind_cols( + tibble( + annotation = colnames(A)[user], + tau = unname(tau[user]), + tauSe = unname(tauSe) + ), + .h2EnrichmentColumns(A, tau, tauBlocks) + ) + ) +} + +# The enrichment of each of the user's annotations and the two shares it is +# the ratio of. Annotations overlap in general -- the base overlaps every one +# -- so an annotation's heritability is (A'A tau)_a, not tau_a M_a, and the +# total is 1'A tau. These are shares of variants and of heritability only for +# values in [0, 1], the domain Dey et al. (2020) define enrichment on; outside +# it they depend on where the column's zero happens to be and are NA, while +# tau stays reported. +# @noRd +.h2EnrichmentColumns <- function(A, tau, tauBlocks) { + user <- attr(A, "userCols") + AtA <- as.matrix(crossprod(A)) size <- colSums(A) propSnps <- size[user] / nrow(A) propH2 <- as.vector(AtA %*% tau)[user] / sum(size * tau) - enrichment <- propH2 / propSnps blockH2 <- (tauBlocks %*% AtA)[, user, drop = FALSE] blockEnrichment <- sweep( blockH2 / as.vector(tauBlocks %*% size), @@ -426,26 +453,32 @@ NULL propSnps, "/" ) - enrichmentSe <- apply(blockEnrichment, 2L, .jackknifeSeFromLoo) - tauSe <- apply(tauBlocks, 2L, .jackknifeSeFromLoo)[user] - list( - tau = tau[user], - tauSe = tauSe, - annotationJackknifeCoefs = tauBlocks[, user, drop = FALSE], - enrichment = tibble( - annotation = colnames(A)[user], - tau = unname(tau[user]), - tauSe = unname(tauSe), - enrichment = unname(enrichment), - enrichmentSe = unname(enrichmentSe), - enrichmentP = unname( - pnorm(abs(enrichment - 1) / enrichmentSe, lower.tail = FALSE) * - 2 - ), - propH2 = unname(propH2), - propSnps = unname(propSnps) - ) + userCols <- A[, user, drop = FALSE] + outside <- which(colSums(userCols < 0 | userCols > 1) > 0) + columns <- list( + enrichment = unname(propH2 / propSnps), + enrichmentSe = unname(apply(blockEnrichment, 2L, .jackknifeSeFromLoo)), + enrichmentP = .h2EnrichmentP(AtA, size, nrow(A), tau, tauBlocks)[user], + propH2 = unname(propH2), + propSnps = unname(propSnps) ) + as_tibble(map(columns, replace, list = outside, values = NA_real_)) +} + +# ldsc's test of enrichment (regressions.py, _overlap_output): a t test, on as +# many degrees of freedom as blocks, of the per-variant heritability inside each +# column against outside it, h2_a / M_a - (h2 - h2_a) / (M - M_a). That is +# linear in tau and 0 exactly when the enrichment is 1, and unlike the +# enrichment it has no denominator to take a block's noise from. NA where it is +# undefined: a column covering every variant has no outside. +# @noRd +#' @importFrom stats pt +.h2EnrichmentP <- function(AtA, size, M, tau, tauBlocks) { + difference <- AtA / size - sweep(-AtA, 2L, size, "+") / (M - size) + estimate <- as.vector(difference %*% tau) + se <- apply(tauBlocks %*% t(difference), 2L, .jackknifeSeFromLoo) + p <- 2 * pt(abs(estimate / se), nrow(tauBlocks), lower.tail = FALSE) + unname(if_else(se > 0, p, NA_real_)) } # The result of a stratified or unstratified fit: `tau` and `tauBlocks` over @@ -723,11 +756,13 @@ lderUnivariate <- function(z, n, eigenRef, annotations = NULL) { TRUE ) jk <- .lderJackknife(blockStats, baselineMat, n, M, design$M_a, rough, TRUE) + # The fit's inflation a, on the scale the other estimators report: the + # intercept of the chi-square regression, 1 + N a, about 1 without any. .h2FitResult( h2 = fit$h2, h2Se = jk$h2Se, - intercept = fit$a, - interceptSe = jk$aSe, + intercept = 1 + n * fit$a, + interceptSe = n * jk$aSe, A = baselineMat, tau = fit$tau, tauBlocks = jk$annotationJackknifeCoefs, @@ -857,9 +892,12 @@ NULL # Omega = S o S, with S = rawNtau * R Da R + R and Da = diag(rowSums(A)), # reduced to its leading eigenpairs carrying `cutpoint` of the eigenvalue # mass (eigen.cut in the original gLDSC implementation): Omega^-1 is applied -# as V diag(1 / values) V' and never formed. +# as V diag(1 / values) V' and never formed. A variant's entry of Da is its +# plug-in variance, which a column taking negative values can drive below 0 +# and the square root in .gldscS() cannot take, so it is floored there; Omega +# only weights the fit, whose mean model this leaves alone. .gldscOmegaEigen <- function(ld, Ab, rawNtau, cutpoint = 0.99) { - S <- .gldscS(ld, rowSums(Ab), rawNtau) + S <- .gldscS(ld, pmax(rowSums(Ab), 0), rawNtau) ei <- eigen(S^2, symmetric = TRUE) k <- which(cumsum(ei$values) / sum(ei$values) >= cutpoint)[1] list( @@ -1130,9 +1168,10 @@ NULL # Unified HDL negative log-likelihood in b* = V'(z/sqrt(N)) units. Modeled # variance of rotated coordinate i is # lamh2_i = s_i lam_i^2 - lam_i h2Total / nRef + int lam_i / n, -# s_i = (ldAnnotScaled %*% h2a)_i and h2Total = sum(h2a). With a single 1/M -# score column this is exactly llfun in the original HDL implementation -# (Ning et al. 2020). param = c(h2_1, .., h2_nTau, int). +# s_i = (ldAnnotScaled %*% h2a)_i and h2Total = sum(h2a * h2Share), `h2Share` +# being what a unit of each parameter adds to the total (.hdlFitBasis()). With +# a single 1/M score column this is exactly llfun in the original HDL +# implementation (Ning et al. 2020). param = c(h2_1, .., h2_nTau, int). # `lim` is the original HDL's floor on the modelled per-direction variance # (llfun.R uses exp(-10)). It has to match: with 40% of a real reference's # eigen-directions near zero the floor is active for most of them, and the @@ -1146,7 +1185,8 @@ NULL lam, bstar, ldAnnotScaled, - lim = exp(-10) + lim = exp(-10), + h2Share = 1 ) { nTau <- ncol(ldAnnotScaled) h2a <- param[seq_len(nTau)] @@ -1154,7 +1194,7 @@ NULL lamh2 <- pmax( as.vector(ldAnnotScaled %*% h2a) * lam^2 - - lam * sum(h2a) / nSamples + + lam * sum(h2a * h2Share) / nSamples + int * lam / n, lim ) @@ -1190,56 +1230,102 @@ NULL rep(sqrt(max(sum(zb^2) - sum(bstar^2), 0) / nNull), nNull) } -# Genome-wide design: eigenvalues, rotated effects, block ids, the M_a-scaled -# eigenvalue-score matrix, and M_a. Univariate uses a single all-ones score -# column (l_{i,base} = 1) scaled by total M. -.hdlDesign <- function(blockData, M, baselineMat) { +# Genome-wide design: eigenvalues, rotated effects, block ids and the scaled +# eigenvalue-score matrix, with the parametrization `basis` (.hdlFitBasis()). +# Univariate uses a single all-ones score column (l_{i,base} = 1) scaled by +# total M. +.hdlDesign <- function(blockData, M, basis) { lam <- list_c(map(blockData, "lam")) - bstar <- list_c(map(blockData, "bstar")) - blockId <- rep( - seq_along(blockData), - lengths(map(blockData, "lam")) - ) - if (is.null(baselineMat)) { - return(list( - lam = lam, - bstar = bstar, - blockId = blockId, - ldAnnotScaled = matrix(1 / M, length(lam), 1), - M_a = M - )) + fit <- basis %||% + list(transform = matrix(1), size = M, share = 1, lower = 0, upper = 1) + ldAnnot <- if (is.null(basis)) { + matrix(1, length(lam), 1) + } else { + ldAnnotList <- map(blockData, "ldAnnot") + exec(rbind, !!!ldAnnotList) } - M_a <- colSums(baselineMat) - ldAnnotList <- map(blockData, "ldAnnot") - ldAnnot <- exec(rbind, !!!ldAnnotList) - list( + list_assign( + fit, lam = lam, - bstar = bstar, - blockId = blockId, - ldAnnotScaled = sweep(ldAnnot, 2, M_a, "/"), - M_a = M_a, - baseAdded = !is_in(1L, attr(baselineMat, "userCols")) + bstar = list_c(map(blockData, "bstar")), + blockId = rep(seq_along(blockData), lengths(map(blockData, "lam"))), + ldAnnotScaled = sweep(ldAnnot %*% fit$transform, 2L, fit$size, "/") + ) +} + +# How a stratified fit parametrizes the design `A`, whose columns need not read +# as heritabilities: the variance of a direction depends on a column only +# through tau_a l_ia, and a column whose zero its values never reach, like a +# z-score or x + 3, trades its coefficient against the base's without bound. +# Such a column is centered, so the base absorbs its location, except where it +# is part of what spans the all-ones vector: A* = A T with T = I - b c', for +# Ab = 1 and c the columns' means. Then each parameter is tau*_a size_a, with +# size_a the column's total absolute value, so it is on a scale near 1 even at +# a mean of 0; it adds tau*_a sum(A*_a) = parameter * share_a to the total, and +# tau = T tau* recovers the coefficients of `A`. Columns reaching 0 and never +# going below it, binary ones and PIPs among them, stay as they are: the +# parameter is the column's heritability, share 1 and T = I. +# +# Bounds: a parameter is bounded below by 0 only if some variant carries that +# column alone among those left uncentered, since then tau_a is that variant's +# own rate, as for the base or a partition's levels. Otherwise it is an excess +# over another column's rate, in [-1, 1]. A centered column's parameter is in +# [-2, 2]: its positive and negative parts cancel, and with non-negative +# variant rates each is at most the total. +# @noRd +.hdlFitBasis <- function(A) { + ones <- .h2OnesCoef(A) + anchored <- colSums(A != 0) < nrow(A) & colSums(A < 0) == 0 + centered <- !anchored & abs(ones) < 1e-8 + means <- colSums(A) / nrow(A) + shift <- replace(numeric(ncol(A)), centered, means[centered]) + size <- replace( + colSums(abs(A)), + centered, + .hdlCenteredSize(A[, centered, drop = FALSE], means[centered]) + ) + carried <- .hdlCarriedColumns(A[, !centered, drop = FALSE]) + alone <- replace(logical(ncol(A)), !centered, carried) + list( + transform = diag(ncol(A)) - tcrossprod(ones, shift), + size = unname(size), + share = unname(replace(colSums(A) / size, centered, 0)), + lower = unname(if_else(centered, -2, if_else(alone, 0, -1))), + upper = unname(if_else(centered, 2, 1)) ) } -# Fit HDL by L-BFGS-B over (h2_1..h2_nTau, int); bounds h2_a in [0,1], int in -# [0,10] (Ning et al. 2020). Parametrizing by per-annotation h2 (scale ~1) -# keeps the L-BFGS-B finite differences well conditioned. +# The total absolute value of each column of `A` about its mean. +# @noRd +.hdlCenteredSize <- function(A, means) { + colSums(abs(as.matrix(A) - rep(means, each = nrow(A)))) +} + +# Whether some variant is non-zero on each column of `A` and on no other. +# @noRd +.hdlCarriedColumns <- function(A) { + nonZero <- A != 0 + alone <- rowSums(nonZero) == 1L + colSums(nonZero[alone, , drop = FALSE]) > 0 +} + +# The coefficients of the design's columns from its parameters, one set per row +# of `h2a`. +# @noRd +.hdlTau <- function(h2a, design) { + sweep(h2a, 2L, design$size, "/") %*% t(design$transform) +} + +# Fit HDL by L-BFGS-B over (h2_1..h2_nTau, int), with each h2_a in the design's +# bounds (.hdlFitBasis(); [0, 1] unstratified) and int in [0, 10] (Ning et al. +# 2020). Parametrizing by per-annotation h2 (scale ~1) keeps the L-BFGS-B +# finite differences well conditioned. The unstratified fit is upstream's, +# optim()'s defaults included. A stratified one runs to a tighter tolerance, as +# at the defaults its jackknife folds stop short of agreeing, which reads as +# sampling noise in the standard errors. .hdlFit <- function(design, n, nSamples, warnOnBound = TRUE) { nTau <- ncol(design$ldAnnotScaled) - # Beside an added base column an annotation's parameter is its excess - # over the genome-wide rate, which a depleted annotation makes negative; - # only the base, and the columns of a baseline that spans the genome on - # its own, are heritabilities bounded below by 0. - lower <- c( - if (isTRUE(design$baseAdded)) { - c(0, rep(-1, nTau - 1L)) - } else { - rep(0, nTau) - }, - 0 - ) - upper <- c(rep(1, nTau), 10) + upper <- c(design$upper, 10) opt <- optim( c(rep(0.1, nTau), 1), .hdlNll, @@ -1248,15 +1334,17 @@ NULL lam = design$lam, bstar = design$bstar, ldAnnotScaled = design$ldAnnotScaled, + h2Share = design$share, method = "L-BFGS-B", - lower = lower, - upper = upper + lower = c(design$lower, 0), + upper = upper, + control = if (nTau > 1L) list(factr = 1e3, maxit = 5000L) else list() ) if (warnOnBound) { .hdlWarnIfAtBound(opt$par, upper, n, nSamples) } h2a <- opt$par[seq_len(nTau)] - list(h2a = h2a, int = opt$par[nTau + 1L], h2 = sum(h2a)) + list(h2a = h2a, int = opt$par[nTau + 1L], h2 = sum(h2a * design$share)) } # HDL's likelihood carries a finite-reference term, -lam * h2 / nRef, that @@ -1319,14 +1407,14 @@ NULL # Delete-one-block jackknife (Ning et al. 2020): refit leaving each block out, # SE = sqrt(mean((jack - mean)^2) * (nBlocks - 1)). Returns per-annotation and # total-h2 SEs, the intercept SE, and the LOO per-annotation blocks. -.hdlJackknife <- function(blockData, M, baselineMat, n, nSamples) { +.hdlJackknife <- function(blockData, M, basis, n, nSamples) { nBlocks <- length(blockData) loo <- map( seq_len(nBlocks), .hdlLooFit, blockData = blockData, M = M, - baselineMat = baselineMat, + basis = basis, n = n, nSamples = nSamples ) @@ -1335,7 +1423,7 @@ NULL intLoo <- map_dbl(loo, "int") list( h2aSe = apply(h2aBlocks, 2, .jackknifeSeFromLoo), - h2Se = .jackknifeSeFromLoo(rowSums(h2aBlocks)), + h2Se = .jackknifeSeFromLoo(map_dbl(loo, "h2")), intSe = .jackknifeSeFromLoo(intLoo), h2aBlocks = h2aBlocks ) @@ -1346,7 +1434,7 @@ NULL # ============================================================================= # HDL-L's fit of one locus (HDL's HDL.L()), from the standardized marginal -# effects `corInput` (z / sqrt(n)) and the locus' LD source `ld` from a +# effects `corInput` (correlations) and the locus' LD source `ld` from a # reference of `nRef` samples: the rotated effects bstar = V' corInput over the # eigenvalues that cover 99% of the trace, with modelled variance h2 / M lam^2 # - h2 lam / nRef + intercept lam / n (.hdlLocusVar()), h2 and the intercept @@ -1534,7 +1622,8 @@ NULL h2Se = NA_real_, p = NA_real_, intercept = NA_real_, - scale = NA_real_ + scale = NA_real_, + smallReference = FALSE ) { list( h2 = h2, @@ -1542,7 +1631,8 @@ NULL p = p, nComponents = as.integer(nComponents), intercept = intercept, - scale = scale + scale = scale, + smallReference = smallReference ) } @@ -1566,36 +1656,71 @@ NULL # Werme et al. (2022, Methods) and LAVA's documentation describe it: LAVA's # code is not licensed for reuse, and none of it is used here. The marginal # correlations r_j = z_j / sqrt(z_j^2 + n_j - 2) are projected onto the K -# standardized LD components of .lavaComponents() (no estimate with fewer -# than 2): delta = Lambda_K^-1/2 Q_K' r. h2 is the adjusted R^2, -# delta'delta - K sigma with sigma = (1 - delta'delta) / (n - K - 1) for the -# mean sample size n, times the reference correction (nRef - K - 1) / -# (nRef - 1) that LAVA's release notes describe; p is the F test of -# delta = 0 on the corrected statistic. LAVA withholds h2 where K / n -# exceeds 0.1; it is reported here at any K, since a cis window's K usually -# does. Where delta'delta reaches 1, the LD contradicts the statistics and -# both are NA. -# @noRd -#' @importFrom stats pf +# standardized LD components of .lavaComponents(): delta = Lambda_K^-1/2 Q_K' r, +# for the mean sample size n, with no estimate from fewer than 2 components or +# with no degrees of freedom left. With phi how far the reference's LD lies +# from the study's own (.lavaPhi()), the noise per component is +# (1 - h2 + h2 phi) / (n - 1), which gives +# h2 = (delta'delta - m) / v, m = K / (n - 1), v = 1 + m (phi - 1). +# At phi = 0 that is LAVA's adjusted R^2 exactly; for a reference sharing no +# samples it matches, to first order, the correction (nRef - K - 1) / (nRef - 1) +# that LAVA's release notes describe. LAVA's own noise term, +# (1 - delta'delta) / (n - K - 1), holds at phi = 0 alone: past it, it leaves +# h2 m in the estimate, and it has no answer once delta'delta reaches 1, as a +# reference's components can with K / n large. p is .lavaP()'s. LAVA withholds +# h2 where K / n exceeds 0.1; it is reported here at any K, since a cis window's +# K usually does. +# @noRd .cisH2Lava <- function(z, nVariant, ld) { + .lavaFit(.zToCor(z, nVariant), mean(nVariant), ld) +} + +# LAVA's estimate (.cisH2Lava()) from the correlations `r`. +# @noRd +.lavaFit <- function(r, n, ld) { dec <- .cisH2Eigen(ld) - n <- mean(nVariant) K <- .lavaComponents(dec$values, n) - if (K < 2L) { + if (K < 2L || n <= K + 1L) { return(.cisH2Result(K)) } - r <- z / sqrt(z^2 + nVariant - 2) top <- seq_len(K) delta <- crossprod(dec$vectors[, top, drop = FALSE], r) / sqrt(dec$values[top]) dtd <- sum(delta^2) - if (dtd >= 1) { - return(.cisH2Result(K)) + overlap <- ld$referenceOverlap %||% 0 + m <- K / (n - 1) + v <- 1 + m * (.lavaPhi(n, ld$nSamples, overlap) - 1) + .cisH2Result( + K, + h2 = (dtd - m) / v, + p = .lavaP(dtd, m, v, n), + smallReference = overlap == 0 && ld$nSamples <= 2 * n + ) +} + +# How far a reference's LD lies from the study's own, in units of its sampling +# noise: n Var(r_study - r_reference), for n study samples, a reference of nRef +# and the share `overlap` of the study's samples in it. 0 when the reference is +# the study, about 1 for a panel of thousands, whatever the two share. +# Divergence between the populations adds to it, unmodelled: measuring that +# takes the study's own LD. +# @noRd +.lavaPhi <- function(n, nRef, overlap) { + 1 + (1 - 2 * overlap) * n / nRef +} + +# The test of delta = 0. Under it delta'delta = y'Ay / y'y, a ratio of quadratic +# forms in the trait, with mean m and variance about 2 m v / (n + 1): a Beta +# with those moments, at phi = 0 exactly the F test of the adjusted R^2. +# NA where they fit no Beta, at a very small n. +# @noRd +#' @importFrom stats pbeta +.lavaP <- function(dtd, m, v, n) { + s <- (1 - m) * (n + 1) / (2 * v) - 1 + if (s <= 0) { + return(NA_real_) } - sigma <- (1 - dtd) / (n - K - 1) - scale <- (ld$nSamples - K - 1) / (ld$nSamples - 1) - p <- pf(dtd / sigma * scale / K, K, n - K - 1, lower.tail = FALSE) - .cisH2Result(K, h2 = (dtd - K * sigma) * scale, p = p) + pbeta(dtd, m * s, (1 - m) * s, lower.tail = FALSE) } # LAVA's number of LD components for a mean sample size n, as its @@ -1607,12 +1732,18 @@ NULL as.integer(min(which(held >= 0.99)[[1L]], max(floor(0.75 * n), 2L))) } -# HDL-L's estimate (.hdlLocusFit()) from the z and per-variant sample sizes: -# HDL.L()'s effects z_j / sqrt(n_j) and its median sample size. +# HDL-L's estimate (.hdlLocusFit()) from the z and per-variant sample sizes, +# with HDL.L()'s median sample size, and two departures from it, both of which +# matter for QTLs. HDL.L() reads z_j / sqrt(n_j) as the correlation, but a QTL's +# z is a t statistic: t^2 / n = r^2 / (1 - r^2), which is 1.43 r^2 at r^2 = 0.3. +# And it refers its likelihood ratio to a chi-square on 1 degree of freedom +# though no heritability is on the boundary of h2's range, where the null +# distribution is the equal mixture of that and a point mass at 0, so p is half +# of .hdlLocusFit()'s. # @noRd .cisH2HdlL <- function(z, nVariant, ld) { fit <- .hdlLocusFit( - z / sqrt(nVariant), + .zToCor(z, nVariant), ld, median(nVariant), ld$nSamples @@ -1620,7 +1751,7 @@ NULL .cisH2Result( fit$nComponents, h2 = fit$h2, - p = fit$p, + p = fit$p / 2, intercept = fit$intercept ) } @@ -1631,20 +1762,24 @@ NULL # and the standard error, with d profiled out of the likelihood instead of # FastHer's joint optimization: the n study samples' y ~ N(0, d (h2 X X' / m + # (1 - h2) I)) over the window's m variants, in the eigenbasis of the LD with -# X'X = n R, X'y = sqrt(n) z and y'y = n. As FastHer does, z is rescaled to -# the harmonic mean n of the per-variant sizes and the eigenpairs are kept -# down to the one that reaches 99.999% of the trace (r of them). With a_k = -# (Q'z)_k^2 / lambda_k and S their sum, the likelihood needs r < n and S < n, -# which in-sample LD guarantees; the estimate is NA otherwise. d is profiled -# out, h2 maximized over [0, 1), and its standard error is FastHer's, from -# the expected information in (h2, d). +# X'X = n R, X'y = n r and y'y = n. Departing from FastHer, which reads z_j / +# sqrt(n_j) as the correlation r_j (see .cisH2HdlL()), r is the exact one: with +# in-sample LD, strong QTLs otherwise push S to n and the estimate to NA. As +# FastHer does, the harmonic mean n of the per-variant sizes is used and the +# eigenpairs are kept down to the one that reaches 99.999% of the trace (r of +# them). With a_k = n (Q'r)_k^2 / lambda_k and S their sum, the likelihood needs +# r < n and S < n, which in-sample LD guarantees; the estimate is NA otherwise. +# d is profiled out, h2 maximized over [0, 1), and its standard error is +# FastHer's, from the expected information in (h2, d), and so holds under the +# infinitesimal model that likelihood assumes. # @noRd #' @importFrom stats optimize .cisH2FastHer <- function(z, nVariant, ld) { n <- 1 / mean(1 / nVariant) dec <- .fastHerEigen(.cisH2Eigen(ld)) r <- length(dec$values) - a <- as.vector(crossprod(dec$vectors, z * sqrt(n / nVariant)))^2 / + a <- n * + as.vector(crossprod(dec$vectors, .zToCor(z, nVariant)))^2 / dec$values if (r >= n || sum(a) >= n) { return(.cisH2Result(r)) @@ -1715,10 +1850,11 @@ NULL # sHDL candidate-annotation score statistics. Each eigen-direction's rotated # effect has modelled variance v_i = lamh2_i, and the likelihood's score for # a candidate's heritability is sum_i g_ic (bstar_i^2 / v_i^2 - 1 / v_i), -# with g_ic = dv_i/dh2_c = l_ic lam_i^2 / M_c - lam_i / nPanel and +# with g_ic = dv_i/dh2_c = l_ic lam_i^2 / size_c - share_c lam_i / nPanel, for +# a column's total absolute value size_c and share_c = M_c / size_c, and # information weight 2 / v_i^2. The baseline heritabilities and the intercept -# are the nuisance (.h2EfficientScoreBlocks()), with derivatives -# l_ia lam_i^2 / M_a - lam_i / nPanel and lam_i / n. +# are the nuisance (.h2EfficientScoreBlocks()), with derivatives from the fit's +# own parameters (.hdlFitBasis()) and lam_i / n. .shdlStratified <- function(blockData, nPanel, annotations, design, lamh2, n) { candidate <- testedAnnotations(annotations) candMat <- assay(candidate, "annotations") @@ -1728,8 +1864,17 @@ NULL lam <- design$lam blockCands <- map(blockData, "ldCand") ldCand <- exec(rbind, !!!blockCands) - gC <- .shdlGradient(sweep(ldCand, 2L, colSums(candMat), "/"), lam, nPanel) - G <- cbind(.shdlGradient(design$ldAnnotScaled, lam, nPanel), lam / n) + candSize <- colSums(abs(candMat)) + gC <- .shdlGradient( + sweep(ldCand, 2L, candSize, "/"), + lam, + nPanel, + colSums(candMat) / candSize + ) + G <- cbind( + .shdlGradient(design$ldAnnotScaled, lam, nPanel, design$share), + lam / n + ) Ub <- .h2EfficientScoreBlocks( gC, G, @@ -1745,12 +1890,12 @@ NULL ) } -# The derivative of each eigen-direction's modelled variance in an -# annotation's heritability, from the annotation's LD scores scaled by its -# size: l_ia lam_i^2 / M_a - lam_i / nPanel. +# The derivative of each eigen-direction's modelled variance in a column's +# parameter, from the column's LD scores scaled by its size: l_ia lam_i^2 / +# size_a - share_a lam_i / nPanel. # @noRd -.shdlGradient <- function(scaled, lam, nPanel) { - scaled * lam^2 - lam / nPanel +.shdlGradient <- function(scaled, lam, nPanel, share) { + scaled * lam^2 - tcrossprod(lam, share) / nPanel } # ============================================================================= @@ -1771,17 +1916,18 @@ hdlUnivariate <- function(z, n, eigenRef, annotations = NULL) { baselineMat = baselineMat, candMat = .h2CandidateMat(annotations) ) - design <- .hdlDesign(blockData, M, baselineMat) + basis <- if (!is.null(baselineMat)) .hdlFitBasis(baselineMat) + design <- .hdlDesign(blockData, M, basis) ft <- .hdlFit(design, n, nPanel) - jk <- .hdlJackknife(blockData, M, baselineMat, n, nPanel) + jk <- .hdlJackknife(blockData, M, basis, n, nPanel) .h2FitResult( h2 = ft$h2, h2Se = jk$h2Se, intercept = ft$int, interceptSe = jk$intSe, A = baselineMat, - tau = ft$h2a / design$M_a, - tauBlocks = sweep(jk$h2aBlocks, 2L, design$M_a, "/"), + tau = as.vector(.hdlTau(matrix(ft$h2a, 1L), design)), + tauBlocks = .hdlTau(jk$h2aBlocks, design), scoreStats = .hdlCandidateScores( blockData, nPanel, @@ -2422,7 +2568,8 @@ sldscUnivariate <- function(z, n, ldRef, annotations = NULL, nIter = 2L) { p_value = NA_real_, n_snps = as.integer(M), n_components = NA_integer_, - scale = NA_real_ + scale = NA_real_, + reference_overlap = NA_real_ ), list( enrichment = list(result$enrichment), @@ -2505,6 +2652,14 @@ setMethod( #' @param BPPARAM For a \code{QtlSumStats}: a \code{BiocParallelParam} over #' the selected rows, each row's estimate on one worker. Default #' \code{SerialParam()}. +#' @param referenceOverlap For a \code{QtlSumStats}: the share of the study's +#' samples that are in the LD reference, from 0 to 1, one value or one per +#' selected row. Default 0. Usually unknown, and with a reference of thousands +#' it hardly matters (see Details): set it when the LD came from the study's +#' own samples or a small cohort holding them, 1 if it holds them all. Only +#' \code{"lava"} reads it, and its estimates record it in +#' \code{reference_overlap}: HDL-L's intercept absorbs what it measures, and +#' FastHer assumes the study's own LD. #' @export #' @importFrom BiocParallel SerialParam setMethod( @@ -2518,7 +2673,8 @@ setMethod( contexts = NULL, traitId = NULL, cisWindow = NULL, - BPPARAM = SerialParam() + BPPARAM = SerialParam(), + referenceOverlap = 0 ) { method <- arg_match(method, names(.cisH2Estimators)) .cisH2AssertInput(sumstats, annotations) @@ -2529,11 +2685,17 @@ setMethod( cisWindow, "estimateH2" ) + rows <- .cisH2SelectedRows(selection) fits <- .bpMap( - .cisH2SelectedRows(selection), + rows, .cisH2Row, data = selection$data, method = method, + referenceOverlap = .cisH2RowOverlaps( + referenceOverlap, + rows, + length(selection$data) + ), BPPARAM = BPPARAM ) out <- .h2EstimateNew( @@ -2544,6 +2706,7 @@ setMethod( ) .cisH2WarnEmpty(out) .cisH2WarnUndefined(out, method) + .cisH2WarnSmallReference(out, map_lgl(fits, "smallReference")) out } ) @@ -2591,10 +2754,32 @@ setMethod( rows } +# Each row of the data's reference overlap, from one value or one per selected +# row. +# @noRd +.cisH2RowOverlaps <- function(referenceOverlap, rows, nRows) { + assertNumeric( + referenceOverlap, + lower = 0, + upper = 1, + any.missing = FALSE, + min.len = 1L + ) + if (!is_in(length(referenceOverlap), c(1L, length(rows)))) { + abort(glue( + "estimateH2: `referenceOverlap` has {length(referenceOverlap)} ", + "values for {length(rows)} selected row(s); give one, or one per ", + "selected row." + )) + } + replace(numeric(nRows), rows, referenceOverlap) +} + # One row's estimate, from its z and per-variant sample sizes against its LD -# reference's block for those variants: the window it covers, and its values. +# reference's block for those variants: the window it covers, its values, and +# whether LAVA's estimate rested on assuming no overlap with a small reference. # @noRd -.cisH2Row <- function(i, data, method) { +.cisH2Row <- function(i, data, method, referenceOverlap) { st <- as.character(data$study[[i]]) ctx <- as.character(data$context[[i]]) tr <- as.character(data$trait[[i]]) @@ -2607,9 +2792,16 @@ setMethod( require = c("Z", "N"), derive = "zFromBetaSe" ) - fit <- .cisH2Fit(df, .rowLdRef(data, i), method, label) + fit <- .cisH2Fit( + df, + .rowLdRef(data, i), + method, + label, + referenceOverlap[[i]] + ) list( window = .rowRegionAt(data, i), + smallReference = fit$smallReference, values = tibble( study = st, context = ctx, @@ -2622,7 +2814,12 @@ setMethod( p_value = fit$p, n_snps = nrow(df), n_components = fit$nComponents, - scale = fit$scale + scale = fit$scale, + reference_overlap = if (identical(method, "lava")) { + referenceOverlap[[i]] + } else { + NA_real_ + } ) ) } @@ -2630,12 +2827,32 @@ setMethod( # One row's fit by `method`; NA when its window holds no variants, as there # is nothing to estimate from. # @noRd -.cisH2Fit <- function(df, ldReference, method, label) { +.cisH2Fit <- function(df, ldReference, method, label, referenceOverlap) { if (nrow(df) == 0L) { return(.cisH2Result(NA_integer_)) } ld <- .cisH2LdSource(df$variant_id, ldReference, method, label) - exec(.cisH2Estimators[[method]], z = df$z, nVariant = df$N, ld = ld) + if (identical(method, "lava")) { + .cisH2AssertOverlap(mean(df$N), ld$nSamples, referenceOverlap, label) + } + exec( + .cisH2Estimators[[method]], + z = df$z, + nVariant = df$N, + ld = list_assign(ld, referenceOverlap = referenceOverlap) + ) +} + +# A reference cannot hold more of the study's samples than it has samples. +# @noRd +.cisH2AssertOverlap <- function(n, nRef, overlap, label) { + if (overlap * n <= nRef) { + return(invisible(NULL)) + } + abort(glue( + "{label}: `referenceOverlap` = {overlap} puts {overlap * n} of the ", + "{n} study samples in an LD reference of {nRef}." + )) } # The row's LD source (.rssLdSource()) for its variants, refusing a missing @@ -2685,12 +2902,35 @@ setMethod( warn(glue( "estimateH2: FastHer's likelihood is undefined for ", "{sum(undefined)} of {nrow(out)} gene(s), whose LD reference's rank ", - "or statistics reach the sample size (an external reference larger ", - "than the study); their h2 is NA: ", + "or statistics reach the sample size, as they can when the LD is not ", + "the study's own; their h2 is NA: ", "{str_flatten(out$trait[undefined], ', ')}." )) } +# Where the reference is no bigger than about the study, LAVA's estimate turns +# on the samples they share, and it assumed none: LD from the study's own +# samples comes out far too low. One warning lists the genes. +# @noRd +.cisH2WarnSmallReference <- function(out, small) { + if (!any(small)) { + return(invisible(NULL)) + } + warn(c( + glue( + "estimateH2: the LD reference of {sum(small)} of {length(small)} ", + "gene(s) has at most twice the study's samples, so LAVA's ", + "estimate depends on how many they share, and ", + "`referenceOverlap = 0` assumes none: ", + "{str_flatten(out$trait[small], ', ')}." + ), + i = str_c( + "If the reference holds the study's samples, set ", + "`referenceOverlap`: 1 if it holds them all." + ) + )) +} + # ============================================================================= # computeLdScores -- LD score computation # ============================================================================= @@ -2952,11 +3192,11 @@ h2EstimateToSldscTrait <- function(h2Est) { # @noRd # One HDL delete-one-block jackknife fold: refit leaving block `b` out. # @noRd -.hdlLooFit <- function(b, blockData, M, baselineMat, n, nSamples) { +.hdlLooFit <- function(b, blockData, M, basis, n, nSamples) { # The full-data fit already reported any bound it hit; repeating that # once per left-out block would bury it in noise. .hdlFit( - .hdlDesign(blockData[-b], M, baselineMat), + .hdlDesign(blockData[-b], M, basis), n, nSamples, warnOnBound = FALSE diff --git a/R/jointEngine.R b/R/jointEngine.R index 92c82b473..ea419f533 100644 --- a/R/jointEngine.R +++ b/R/jointEngine.R @@ -299,19 +299,11 @@ NULL # 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, genotypeFilter) { - if (!is(g, "IndividualJointGroup")) { +.jointAfForGroup <- function(g, block) { + if (!is(g, "IndividualJointGroup") || is.null(block$af)) { 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, genotypeFilter = genotypeFilter, region = rg) + block$af[match(colnames(.jgX(g)), colnames(block$X))] } @@ -998,7 +990,6 @@ setMethod( trait = as.character(cond$trait[1L]), fitRetention = "slim", weightStandardized = stdz, - weightsDataType = cfg$dataType, verbose = 0, seed = cfg$seed ) @@ -1016,6 +1007,14 @@ setMethod( .twasRowRetention(token, if (identical(level, "none")) "slim" else level) } +# Whether a joint fit keeps its copy of X: asked for, by a method whose fit has +# one. The run was refused at any level but "full", and a fine-mapping token, +# whose rows keep no fit, is not such a method. +# @noRd +.jointRetainX <- function(cfg, token) { + isTRUE(cfg$retainX) && is_in(token, .twasTokensKeepingX()) +} + # The mr.mash RSS fit of one summary-statistics joint group, with the caller's # per-method options split across the wrapper's own formals and the engine's # arguments exactly as the per-tuple path splits them (.twasWeightCallArgs()). @@ -1066,8 +1065,8 @@ setMethod( trait = as.character(cond$trait[1L]), fittedModels = fittedModels, fitRetention = retention, + retainX = .jointRetainX(cfg, token), weightStandardized = stdz, - weightsDataType = cfg$dataType, verbose = verbose, seed = cfg$seed ) @@ -1221,8 +1220,7 @@ setMethod( variantIds = character(0), weights = NULL, cvResult = cvR, - weightStandardized = stdz, - weightsDataType = cfg$dataType + weightStandardized = stdz )) } TwasWeightsRow( @@ -1230,8 +1228,7 @@ setMethod( weights = full$W[, r], methodFits = full$fitParts, cvResult = cvR, - weightStandardized = stdz, - weightsDataType = cfg$dataType + weightStandardized = stdz ) } @@ -1313,21 +1310,22 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { return(list()) } verbose <- if (is.null(args$verbose)) 1 else args$verbose - map( + compact(map( scopedTraits, - .deferGroups, - builder = .enumCrossContextGroupFor, + .deferCrossContext, data = data, studyName = studyName, scopedContexts = scopedContexts, args = args, verbose = verbose - ) + )) } -# One trait's cross-context group, or NULL when it has no usable (X, Y). +# One trait's deferred cross-context unit, or NULL when it cannot be fit. Its +# phenotypes are built here: the samples a fit is on decide what it can share an +# X with, and whether it needs one at all. # @noRd -.enumCrossContextGroupFor <- function( +.deferCrossContext <- function( tid, data, studyName, @@ -1335,17 +1333,38 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { args, verbose ) { - xy <- .buildIndividualCrossContextXy( + planned <- .crossContextPlan( data, tid, scopedContexts, - args$cisWindow, verbose, - label = "jointCrossContext", - region = args$region, - residualizationParam = args$residualizationParam, - genotypeFilter = args$genotypeFilter, - phenotypeFilter = args$phenotypeFilter + "jointCrossContext", + args$residualizationParam, + args$phenotypeFilter + ) + if (is.null(planned)) { + return(NULL) + } + .deferGroups( + tid, + .enumCrossContextGroupFor, + xTraits = tid, + xContexts = planned$perTraitContexts, + samples = planned$fitSamples, + studyName = studyName, + planned = planned + ) +} + +# One trait's cross-context group on `X`, or NULL when it has no usable (X, Y). +# @noRd +.enumCrossContextGroupFor <- function(tid, studyName, planned, X) { + xy <- .crossContextAssemble( + X, + planned$Yres, + planned$perTraitContexts, + planned$fitSamples, + planned$report ) if (is.null(xy)) { return(NULL) @@ -1494,21 +1513,21 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { scopedContexts <- scope$contexts[[studyName]] scopedTraits <- scope$traits[[studyName]] verbose <- if (is.null(args$verbose)) 1 else args$verbose - map( + compact(map( scopedContexts, - .deferGroups, - builder = .enumCrossTraitGroupFor, + .deferCrossTrait, data = data, studyName = studyName, scopedTraits = scopedTraits, args = args, verbose = verbose - ) + )) } -# One context's cross-trait group, or NULL when it has no usable (X, Y). +# One context's deferred cross-trait unit, or NULL when it cannot be fit. Its +# window spans the scoped traits the context carries. # @noRd -.enumCrossTraitGroupFor <- function( +.deferCrossTrait <- function( cx, data, studyName, @@ -1516,19 +1535,34 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { args, verbose ) { - xy <- .buildIndividualCrossTraitXy( + planned <- .crossTraitPlan( data, cx, scopedTraits, - args$cisWindow, + studyName, verbose, - label = "jointCrossTrait", + "jointCrossTrait", + args$residualizationParam, + args$phenotypeFilter + ) + if (is.null(planned)) { + return(NULL) + } + .deferGroups( + cx, + .enumCrossTraitGroupFor, + xTraits = planned$traitsHere, + xContexts = cx, + samples = planned$fitSamples, studyName = studyName, - region = args$region, - residualizationParam = args$residualizationParam, - genotypeFilter = args$genotypeFilter, - phenotypeFilter = args$phenotypeFilter + planned = planned ) +} + +# One context's cross-trait group on `X`, or NULL when it has no usable (X, Y). +# @noRd +.enumCrossTraitGroupFor <- function(cx, studyName, planned, X) { + xy <- .crossTraitAssemble(X, planned$allY, planned$traitsHere, planned$se) if (is.null(xy)) { return(NULL) } @@ -1735,21 +1769,38 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { return(list()) } verbose <- if (is.null(args$verbose)) 1 else args$verbose - xy <- .buildComposedIndividualXy( + planned <- .composedPlan( data, scope, studyName, - args$cisWindow, verbose, - label = "composed", - region = args$region, - residualizationParam = args$residualizationParam, - genotypeFilter = args$genotypeFilter, - phenotypeFilter = args$phenotypeFilter + "composed", + args$residualizationParam, + args$phenotypeFilter ) - if (is.null(xy)) { + if (is.null(planned)) { return(list()) } + list(.deferGroups( + studyName, + .enumComposedIndividualGroup, + xTraits = planned$allTraits, + xContexts = planned$allContexts, + samples = planned$fitSamples, + planned = planned + )) +} + +# The composed group on `X`, or NULL when it has no usable (X, Y). +# @noRd +.enumComposedIndividualGroup <- function(studyName, planned, X) { + xy <- .composedAlignXy( + list(X = X, YresList = planned$YresList), + planned$tuples + ) + if (is.null(xy)) { + return(NULL) + } # Conditions follow the fitted Y columns ("context:trait"), so dropped # tuples don't desync conditions from Y. Split on the first ":" (contexts # are simple labels; trait ids may themselves contain ":"). @@ -1759,7 +1810,7 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { context = str_remove(labs, ":.*$"), trait = str_remove(labs, "^[^:]*:") ) - list(new("IndividualJointGroup", conditions = conds, X = xy$X, Y = xy$Y)) + new("IndividualJointGroup", conditions = conds, X = xy$X, Y = xy$Y) } # The run's resolved phenotype QC, or the univariate fits' own default -- drop @@ -1797,7 +1848,22 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { )) } -# One context's univariate groups, one per trait it carries. +# The traits of context `cx` that a study's scope selects. +# @noRd +.enumContextTraits <- function(cx, data, scope, studyName) { + se <- .qtlContextSe(data, context = cx) + keep( + intersect(scope$traits[[studyName]], rownames(se)), + .traitSelectedAt, + traitId = scope$traitSelection, + s = studyName, + cx = cx + ) +} + +# One context's univariate units, one per trait it carries. Their phenotypes are +# built here, before any genotype is read: the samples a fit is on are what +# decide which traits can share a residualized X. # @noRd .enumUnivariateForContext <- function( cx, @@ -1807,18 +1873,10 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { studyName, phenotypeFilter ) { - se <- .qtlContextSe(data, context = cx) - traits <- keep( - intersect(scope$traits[[studyName]], rownames(se)), - .traitSelectedAt, - traitId = scope$traitSelection, - s = studyName, - cx = cx - ) + traits <- .enumContextTraits(cx, data, scope, studyName) map( traits, - .deferGroups, - builder = .enumUnivariateGroupFor, + .deferUnivariate, data = data, args = args, studyName = studyName, @@ -1827,10 +1885,9 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { ) } -# One (context, trait) univariate group, or NULL when fewer than two samples -# are shared between its genotypes and its phenotype. +# One trait's deferred univariate unit. # @noRd -.enumUnivariateGroupFor <- function( +.deferUnivariate <- function( tid, data, args, @@ -1845,24 +1902,22 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { phenotypeFilter = phenotypeFilter, residualizationParam = args$residualizationParam ) - X <- if (is.null(args$region)) { - .fmResidGeno( - data, - contexts = cx, - traitId = tid, - cisWindow = args$cisWindow, - residualizationParam = args$residualizationParam, - genotypeFilter = args$genotypeFilter - ) - } else { - .fmResidGeno( - data, - contexts = cx, - region = args$region, - residualizationParam = args$residualizationParam, - genotypeFilter = args$genotypeFilter - ) - } + .deferGroups( + tid, + .enumUnivariateGroupFor, + xTraits = tid, + xContexts = cx, + samples = rownames(Y), + studyName = studyName, + cx = cx, + Y = Y + ) +} + +# One (context, trait) univariate group on `X`, or NULL when fewer than two +# samples are shared between its genotypes and its phenotype. +# @noRd +.enumUnivariateGroupFor <- function(tid, studyName, cx, Y, X) { common <- intersect(rownames(X), rownames(Y)) if (length(common) < 2L) { return(NULL) @@ -1870,7 +1925,7 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { new( "IndividualJointGroup", conditions = tibble(study = studyName, context = cx, trait = tid), - X = X[common, , drop = FALSE], + X = .qtlRowsOf(X, common), Y = Y[common, , drop = FALSE] ) } @@ -2158,6 +2213,18 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { # -- one method -> per-condition entries; the SR-TWAS ensemble is a layer ON TOP # of that. .runJointCell <- function(cell, pipeline, data, scope, tokens, args = list()) { + .qtlRunWork( + .jointCellWork(cell, pipeline, data, scope, tokens, args), + data, + args$genotypeFilter, + args$residualizationParam, + is(pipeline, "FmJointPipeline") + ) +} + +# One joint cell's fits, planned: its units enumerated, none of them built yet. +# @noRd +.jointCellWork <- function(cell, pipeline, data, scope, tokens, args = list()) { # PC groups are labelled by component, not by trait: their enumerator # applies the selection itself, to the traits the components come from. selection <- if (identical(.jcPattern(cell), "topPc")) { @@ -2167,18 +2234,29 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { } doEnsemble <- is(pipeline, "TwasJointPipeline") && isTRUE(.jpConfig(pipeline)$ensembleParam$enabled) - perUnit <- map( - .jcEnumerate(cell)(data, scope, args), + units <- .jcEnumerate(cell)(data, scope, args) + .qtlWork( + units, + map(units, .unitSpec, data = data, args = args), .jointCellUnit, - selection = selection, - minGroup = .jcMinGroup(cell), - pipeline = pipeline, - data = data, - tokens = tokens, - args = args, - doEnsemble = doEnsemble + list( + selection = selection, + minGroup = .jcMinGroup(cell), + pipeline = pipeline, + data = data, + tokens = tokens, + args = args, + doEnsemble = doEnsemble + ), + .jointCellResult, + list(pipeline = pipeline) ) - fitted <- discard(perUnit, is.null) +} + +# A joint cell's result, from its units' fits. +# @noRd +.jointCellResult <- function(fits, pipeline) { + fitted <- discard(fits, is.null) if (length(fitted) == 0L) { return(NULL) } @@ -2187,8 +2265,8 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { # One enumerated unit built, filtered and fitted: the records of its groups, # or NULL when none survives the selection and the minimum size. The unit is -# built here, inside the loop, so its residualized X is let go before the next -# unit's is built. +# built here, inside the loop, so a residualized X of its own is let go before +# the next unit's is built. # @noRd .jointCellUnit <- function( unit, @@ -2198,9 +2276,10 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { data, tokens, args, - doEnsemble + doEnsemble, + block = NULL ) { - groups <- .unitGroups(unit) |> + groups <- .unitGroups(unit, block) |> map(.jointGroupSelect, traitId = selection) |> compact() |> keep(.jointGroupMeetsMin, minGroup = minGroup) @@ -2214,29 +2293,53 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { data = data, tokens = tokens, args = args, - doEnsemble = doEnsemble + doEnsemble = doEnsemble, + block = block )) } -# A unit of joint groups not built yet: the function that builds it, and its -# arguments, `x` first. The individual-level enumerators return these, so a -# unit's residualized X and Y exist only while its groups are fitted rather -# than every unit's being held at once. A unit is one group, or a context's -# principal components, which share one X. +# A unit of joint groups not built yet: the function that builds it from a +# residualized X, its arguments, `x` first, and the genotypes it will be built +# on (the traits that name their window, the contexts of their covariates and +# the samples it may fit). The individual-level enumerators return these: their +# phenotypes are in hand, so what can share an X is known before any genotype is +# read, and a unit's X exists only while its groups are fitted. A unit is one +# group, or a context's principal components, which share one X. +# @noRd +.deferGroups <- function(x, builder, xTraits, xContexts, samples, ...) { + list( + builder = builder, + args = list(x, ...), + xTraits = xTraits, + xContexts = xContexts, + samples = samples + ) +} + +# The genotypes a unit's fits are on; none for a group already built, which +# holds its own. # @noRd -.deferGroups <- function(x, builder, ...) { - list(builder = builder, args = list(x, ...)) +.unitSpec <- function(unit, data, args) { + if (is(unit, "JointGroup")) { + return(NULL) + } + .qtlFitSpec( + .fmGenoWindow(data, unit$xTraits, args$region, args$cisWindow), + unit$xContexts, + unit$samples + ) } -# The groups of an enumerated unit: built when it was deferred, as given when -# it was enumerated built (the summary-statistics groups, which hold no X). +# The groups of an enumerated unit: built on `block` when it was deferred, as +# given when it was enumerated built (the summary-statistics groups, which hold +# no X). # @noRd -.unitGroups <- function(unit) { +.unitGroups <- function(unit, block = NULL) { built <- if (is(unit, "JointGroup")) { unit } else { unitArgs <- unit$args - exec(unit$builder, !!!unitArgs) + exec(unit$builder, !!!unitArgs, X = block$X) } if (is(built, "JointGroup")) { return(list(built)) @@ -2278,36 +2381,18 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { # Fit every token for one joint group -> records, plus the optional SR-TWAS # ensemble layer (built ON TOP of the shared per-method fits, when >= 2 methods # produced entries). -# -# `grp` carries provenance: the ";"-joined members of each varying axis -# (identical on every per-context row of this group), and 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). The -# window is recorded because the fit only ever sees whichever variants -# survived QC; it is read back by genomicRegion(). -# -# `fitArgs$af` / `fitArgs$region` are 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. # @noRd -.jointCellGroup <- function(g, pipeline, data, tokens, args, doEnsemble) { - cond <- .jgConditions(g) - grp <- list( - cond = cond, - js = .jointAxisMembers(cond, "study"), - jc = .jointAxisMembers(cond, "context"), - jt = .jointAxisMembers(cond, "trait"), - region = .jointGroupWindow(data, cond, args), - ldReference = .jgLdRefOf(g) - ) - fitArgs <- .twasGroupArgs(g, pipeline, args) - # Allele frequencies go into fine-mapping's topLoci; TWAS never reads - # them, and computing them reads the group's genotypes a second time. - fitArgs$af <- if (is(pipeline, "FmJointPipeline")) { - .jointAfForGroup(data, g, args$genotypeFilter) - } - fitArgs$region <- args$region +.jointCellGroup <- function( + g, + pipeline, + data, + tokens, + args, + doEnsemble, + block = NULL +) { + grp <- .jointGroupProvenance(g, data, args) + fitArgs <- .jointGroupFitArgs(g, pipeline, args, block) perTokenEntries <- compact(set_names( map( tokens, @@ -2315,7 +2400,7 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { g = g, pipeline = pipeline, fitArgs = fitArgs, - cond = cond, + cond = grp$cond, args = args ), tokens @@ -2341,6 +2426,39 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { ) } +# A group's provenance: the ";"-joined members of each varying axis (identical +# on every per-context row of this group), and 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). The window is +# recorded because the fit only ever sees whichever variants survived QC; it is +# read back by genomicRegion(). +# @noRd +.jointGroupProvenance <- function(g, data, args) { + cond <- .jgConditions(g) + list( + cond = cond, + js = .jointAxisMembers(cond, "study"), + jc = .jointAxisMembers(cond, "context"), + jt = .jointAxisMembers(cond, "trait"), + region = .jointGroupWindow(data, cond, args), + ldReference = .jgLdRefOf(g) + ) +} + +# The arguments a joint group's fitters take, with what they cannot recover for +# themselves: allele frequencies live on the block, 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. +# @noRd +.jointGroupFitArgs <- function(g, pipeline, args, block) { + af <- .jointAfForGroup(g, block) + list_assign( + .twasGroupArgs(g, pipeline, args), + af = af, + region = args$region + ) +} + # One token's entries, or NULL when it produced none. # @noRd .jointTokenEntriesOrNull <- function(token, g, pipeline, fitArgs, cond, args) { @@ -2544,8 +2662,7 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { methodCoef = ens$methodCoef, methodPerformance = ens$methodPerformance ), - weightStandardized = stdz, - weightsDataType = cfg$dataType + weightStandardized = stdz ) } @@ -2554,7 +2671,7 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { # Structurally identical to .enumUnivariateIndividual -- same residualization # helpers, same IndividualJointGroup -- so everything downstream (CV, # ensemble, retention) applies unchanged. It is the TWAS peer of -# fineMappingPipeline's .fmPcaContextRows, sharing .fmTopPcScores(). +# fineMappingPipeline's .fmPcaContextPlan, sharing .fmTopPcScores(). # @noRd .enumTopPcIndividual <- function(data, scope, args = list()) { studyName <- studyName(data) @@ -2563,38 +2680,25 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { } phenotypeFilter <- .jointPhenotypeFilter(args) # One unit per context: its components share the context's one X. - map( + compact(map( scope$contexts[[studyName]], - .deferGroups, - builder = .enumTopPcForContext, + .deferTopPc, data = data, scope = scope, args = args, studyName = studyName, phenotypeFilter = phenotypeFilter - ) + )) } +# One context's deferred top-PC unit: its window spans the traits it selects. +# NULL when PCA has nothing to work on: it is undefined for a single trait, +# matching the fine-mapping rule. # @noRd -.enumTopPcForContext <- function( - cx, - data, - scope, - args, - studyName, - phenotypeFilter -) { - se <- .qtlContextSe(data, context = cx) - traits <- keep( - intersect(scope$traits[[studyName]], rownames(se)), - .traitSelectedAt, - traitId = scope$traitSelection, - s = studyName, - cx = cx - ) - # PCA is undefined for a single trait, matching the fine-mapping rule. +.deferTopPc <- function(cx, data, scope, args, studyName, phenotypeFilter) { + traits <- .enumContextTraits(cx, data, scope, studyName) if (length(traits) < 2L) { - return(list()) + return(NULL) } Y <- .fmResidPheno( data, @@ -2605,9 +2709,22 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { ) scores <- .fmTopPcScores(Y, args$nPCs %||% 10L) if (is.null(scores)) { - return(list()) + return(NULL) } - X <- .enumTopPcX(data, cx, traits, args) + .deferGroups( + cx, + .enumTopPcForContext, + xTraits = traits, + xContexts = cx, + samples = rownames(scores), + studyName = studyName, + scores = scores + ) +} + +# One context's top-PC groups on `X`. +# @noRd +.enumTopPcForContext <- function(cx, studyName, scores, X) { common <- intersect(rownames(X), rownames(scores)) if (length(common) < 2L) { return(list()) @@ -2622,27 +2739,6 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { )) } -# @noRd -.enumTopPcX <- function(data, cx, traits, args) { - if (is.null(args$region)) { - return(.fmResidGeno( - data, - contexts = cx, - traitId = traits, - cisWindow = args$cisWindow, - residualizationParam = args$residualizationParam, - genotypeFilter = args$genotypeFilter - )) - } - .fmResidGeno( - data, - contexts = cx, - region = args$region, - residualizationParam = args$residualizationParam, - genotypeFilter = args$genotypeFilter - ) -} - # @noRd .enumTopPcGroup <- function(pcName, X, scores, studyName, cx) { new( @@ -2753,58 +2849,74 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { traitIds, args = list() ) { - if (length(jointMethods) == 0L || length(parsedJointSpec) == 0L) { - return(NULL) - } - isFm <- is(pipeline, "FmJointPipeline") - results <- compact(map( - parsedJointSpec, - .runOneJointSpecFor, - data = data, - dataForm = dataForm, - pipeline = pipeline, - jointMethods = jointMethods, - contexts = contexts, - traitIds = traitIds, - args = args - )) - if (length(results) == 0L) { - return(NULL) - } - # Every row keeps the LD reference its group was fitted against. - reduce(results, if (isFm) .rbindFineMappingResult else .rbindTwasWeights) + .qtlRunWork( + .jointSpecsWork( + parsedJointSpec, + data, + dataForm, + pipeline, + jointMethods, + contexts, + traitIds, + args + ), + data, + args$genotypeFilter, + args$residualizationParam, + is(pipeline, "FmJointPipeline") + ) } -# `map()` hands the spec first, which is also where .runOneJointSpec wants it. +# .runJointSpecs()' fits, planned, for a caller with others to run alongside. # @noRd -.runOneJointSpecFor <- function( - spec, +.jointSpecsWork <- function( + parsedJointSpec, data, dataForm, pipeline, jointMethods, contexts, traitIds, - args + args = list() ) { - .runOneJointSpec( - spec, - data, - dataForm, - pipeline, - jointMethods, - contexts, - traitIds, - args + if (length(jointMethods) == 0L || length(parsedJointSpec) == 0L) { + return(.qtlWorkValue(NULL)) + } + .qtlWorkOf( + map( + parsedJointSpec, + .jointSpecWork, + data = data, + dataForm = dataForm, + pipeline = pipeline, + jointMethods = jointMethods, + contexts = contexts, + traitIds = traitIds, + args = args + ), + .jointSpecsResult, + list(pipeline = pipeline) ) } -# Run all joint methods for ONE spec: resolve its scope (optionally -# region-restricting the traits), pick the joint cell, and dispatch to -# .runJointCell (one call with ALL methods so the twas ensemble layer can -# combine a group's per-method fits). +# The specs' results rbound. Every row keeps the LD reference its group was +# fitted against. +# @noRd +.jointSpecsResult <- function(perSpec, pipeline) { + results <- compact(perSpec) + if (length(results) == 0L) { + return(NULL) + } + isFm <- is(pipeline, "FmJointPipeline") + reduce(results, if (isFm) .rbindFineMappingResult else .rbindTwasWeights) +} + +# Plan all joint methods for ONE spec: resolve its scope (optionally +# region-restricting the traits), pick the joint cell, and plan it +# (.jointCellWork(), with ALL methods so the twas ensemble layer can combine a +# group's per-method fits). # @noRd -.runOneJointSpec <- function( +.jointSpecWork <- function( spec, data, dataForm, @@ -2831,7 +2943,7 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { pattern <- if (length(spec$axes) > 1L) "composed" else spec$axes[[1L]] cell <- .lookupJointCell(pattern, dataForm) spArgs <- c(args, list(axes = spec$axes)) - .runJointCell(cell, pipeline, data, scope, jointMethods, spArgs) + .jointCellWork(cell, pipeline, data, scope, jointMethods, spArgs) } # One study's traits narrowed to those inside `region`; left alone when the @@ -2914,8 +3026,7 @@ setMethod("construct", "TwasJointPipeline", function(pipeline, records) { variantIds = vids, weights = weights[, r], methodFits = fitParts, - weightStandardized = TRUE, - weightsDataType = cfg$dataType + weightStandardized = TRUE ) } diff --git a/R/jointSpecification.R b/R/jointSpecification.R index 784371485..9928390bb 100644 --- a/R/jointSpecification.R +++ b/R/jointSpecification.R @@ -983,7 +983,7 @@ parseTraitIds <- function(traitId, data) { } -# --- .buildIndividualCrossContextXy helpers --------------------------------- +# --- .crossContextPlan helpers ---------------------------------------------- # Does context `cx` carry trait `tid`? # @noRd @@ -1025,62 +1025,68 @@ parseTraitIds <- function(traitId, data) { exec(cbind, !!!yCols) } -# Intersect samples, build the response matrix, and drop incomplete rows. +# Skip a trait with too few of something to fit: NULL, and a message saying so. +# `report` holds the caller's verbose, label and tid. +# @noRd +.crossContextSkip <- function(tooFew, report) { + if (report$verbose >= 1) { + inform(glue( + "{report$label}: trait '{report$tid}' has too few {tooFew}; ", + "skipping." + )) + } + NULL +} + +# The samples a cross-context fit is on: every context's Y holds them and none +# is missing a value. NULL, with a message, when fewer than two remain. +# @noRd +.crossContextFitSamples <- function(Yres, perTraitContexts, report) { + shared <- reduce(map(Yres, rownames), intersect) + if (length(shared) < 2L) { + return(.crossContextSkip("shared samples across contexts", report)) + } + Y <- .crossContextY(Yres, perTraitContexts, shared) + keep <- stats::complete.cases(Y) + if (sum(keep) < 2L) { + return(.crossContextSkip("complete-Y subjects", report)) + } + shared[keep] +} + +# X and the response matrix on the fit's samples that X holds. +# @noRd .crossContextAssemble <- function( X, Yres, perTraitContexts, - verbose, - label, - tid + fitSamples, + report ) { - commonSamples <- reduce( - c(list(rownames(X)), map(Yres, rownames)), - intersect - ) - if (length(commonSamples) < 2L) { - if (verbose >= 1) { - inform(glue( - "{label}: trait '{tid}' has too few shared samples across ", - "contexts; skipping." - )) - } - return(NULL) - } - shared <- X[commonSamples, , drop = FALSE] - Y <- .crossContextY(Yres, perTraitContexts, commonSamples) - keep <- stats::complete.cases(Y) - if (sum(keep) < 2L) { - if (verbose >= 1) { - inform(glue( - "{label}: trait '{tid}' has too few complete-Y subjects; ", - "skipping." - )) - } - return(NULL) + common <- intersect(rownames(X), fitSamples) + if (length(common) < 2L) { + return(.crossContextSkip("shared samples across contexts", report)) } list( - X = shared[keep, , drop = FALSE], - Y = Y[keep, , drop = FALSE], + X = X[common, , drop = FALSE], + Y = .crossContextY(Yres, perTraitContexts, common), perTraitContexts = perTraitContexts ) } # Build a multi-context Y matrix for a single (study, trait) from an -# individual-level QtlDataset. Returns list(X, Y, perTraitContexts) or -# NULL when fewer than 2 contexts carry `tid` or the sample / complete-Y -# subset is too small to fit. +# individual-level QtlDataset, before any genotype is read. Returns the trait's +# contexts, their residualized phenotypes and the samples the fit is on, or NULL +# when fewer than 2 contexts carry `tid` or the sample / complete-Y subset is +# too small to fit. # @noRd -.buildIndividualCrossContextXy <- function( +.crossContextPlan <- function( data, tid, scopedContexts, - cisWindow, verbose, label, - region = NULL, residualizationParam, - genotypeFilter, phenotypeFilter = NULL ) { perTraitContexts <- .crossContextPerTrait( @@ -1093,15 +1099,6 @@ parseTraitIds <- function(traitId, data) { if (is.null(perTraitContexts)) { return(NULL) } - X <- .buildResidGeno( - data, - perTraitContexts, - tid, - cisWindow, - region, - residualizationParam, - genotypeFilter - ) Yres <- .fmResidPheno( data, contexts = perTraitContexts, @@ -1109,7 +1106,17 @@ parseTraitIds <- function(traitId, data) { phenotypeFilter = phenotypeFilter, residualizationParam = residualizationParam ) - .crossContextAssemble(X, Yres, perTraitContexts, verbose, label, tid) + report <- list(verbose = verbose, label = label, tid = tid) + fitSamples <- .crossContextFitSamples(Yres, perTraitContexts, report) + if (is.null(fitSamples)) { + return(NULL) + } + list( + perTraitContexts = perTraitContexts, + Yres = Yres, + fitSamples = fitSamples, + report = report + ) } @@ -1147,19 +1154,16 @@ parseTraitIds <- function(traitId, data) { # Build a multi-trait Y matrix for a single (study, context) from an # individual-level QtlDataset. Returns list(X, Y, traitsHere, se) or NULL # when fewer than 2 traits live in the context or the sample / complete-Y -# subset is too small. +# subset is too small. Before any genotype is read. # @noRd -.buildIndividualCrossTraitXy <- function( +.crossTraitPlan <- function( data, cx, scopedTraits, - cisWindow, + studyName, verbose, label, - studyName, - region = NULL, residualizationParam, - genotypeFilter, phenotypeFilter = NULL ) { se <- .qtlContextSe(data, context = cx) @@ -1169,15 +1173,6 @@ parseTraitIds <- function(traitId, data) { if (.crossTraitTooFew(traitsHere, cx, studyName, verbose, label)) { return(NULL) } - allX <- .buildResidGeno( - data, - cx, - traitsHere, - cisWindow, - region, - residualizationParam, - genotypeFilter - ) allY <- .fmResidPheno( data, contexts = cx, @@ -1185,7 +1180,17 @@ parseTraitIds <- function(traitId, data) { phenotypeFilter = phenotypeFilter, residualizationParam = residualizationParam ) - .crossTraitAssemble(allX, allY, traitsHere, se) + # The samples observed for every trait, which are the ones the fit is on. + fitSamples <- rownames(allY)[stats::complete.cases(allY)] + if (length(fitSamples) < 2L) { + return(NULL) + } + list( + se = se, + traitsHere = traitsHere, + allY = allY, + fitSamples = fitSamples + ) } # The cross-trait X / Y on the samples they share, keeping only the samples @@ -1211,39 +1216,7 @@ parseTraitIds <- function(traitId, data) { } -# Residualized genotype for the cross-* / composed builders (cis-window or an -# explicit region). Shared by the individual multi-axis Xy builders. -# @noRd -.buildResidGeno <- function( - data, - contexts, - traitId, - cisWindow, - region, - residualizationParam, - genotypeFilter -) { - if (is.null(region)) { - .fmResidGeno( - data, - contexts = contexts, - traitId = traitId, - cisWindow = cisWindow, - residualizationParam = residualizationParam, - genotypeFilter = genotypeFilter - ) - } else { - .fmResidGeno( - data, - contexts = contexts, - region = region, - residualizationParam = residualizationParam, - genotypeFilter = genotypeFilter - ) - } -} - -# --- .buildComposedIndividualXy helpers ------------------------------------- +# --- .composedPlan helpers --------------------------------------------------- # Enumerate the in-scope (context, trait) tuples for a study; NULL if < 2. # @noRd @@ -1311,62 +1284,32 @@ parseTraitIds <- function(traitId, data) { exec(cbind, !!!yCols) } -# The residualized genotypes and the per-context residualized phenotypes -# for one composed block, both over the same context/trait span. +# The samples a composed fit is on: every context's Y holds them and no tuple's +# column misses them. Empty when the tuples cannot be fit together. # @noRd -.composedResidualized <- function( - data, - allContexts, - allTraits, - cisWindow, - region, - residualizationParam, - genotypeFilter, - phenotypeFilter = NULL -) { - # Genotypes first, as the inline version did: reading them is what - # fails on a missing panel file, and that error must stay the one the - # caller sees rather than a downstream lm.fit() complaint. - allX <- .buildResidGeno( - data, - allContexts, - allTraits, - cisWindow, - region, - residualizationParam, - genotypeFilter - ) - resid <- .fmResidPheno( - data, - contexts = allContexts, - traitId = allTraits, - phenotypeFilter = phenotypeFilter, - residualizationParam = residualizationParam - ) - list( - X = allX, - # A single context returns the bare matrix rather than a named list. - YresList = if (length(allContexts) == 1L) { - set_names(list(resid), allContexts) - } else { - resid - } - ) +.composedFitSamples <- function(YresList, tuples) { + shared <- reduce(map(YresList, rownames), intersect) + Y <- if (length(shared) >= 2L) { + .composedYCols(YresList, tuples, shared) + } + if (is.null(Y)) { + return(character(0)) + } + shared[stats::complete.cases(Y)] } -# Build a composed-axes (context, trait) X/Y for individual-level -# QtlDataset. Returns list(X, Y, tuples) or NULL. +# A composed-axes (context, trait) fit on an individual-level QtlDataset, before +# any genotype is read: its tuples, the per-context residualized phenotypes and +# the samples it is on, or NULL when too few samples could be fit to read any +# genotypes for them. # @noRd -.buildComposedIndividualXy <- function( +.composedPlan <- function( data, scope, studyName, - cisWindow, verbose, label, - region = NULL, residualizationParam, - genotypeFilter, phenotypeFilter = NULL ) { tuples <- .composedTuples(data, scope, studyName, verbose, label) @@ -1375,17 +1318,30 @@ parseTraitIds <- function(traitId, data) { } allContexts <- unique(map_chr(tuples, "context")) allTraits <- unique(map_chr(tuples, "trait")) - sides <- .composedResidualized( + resid <- .fmResidPheno( data, - allContexts, - allTraits, - cisWindow, - region, - residualizationParam, - genotypeFilter, - phenotypeFilter + contexts = allContexts, + traitId = allTraits, + phenotypeFilter = phenotypeFilter, + residualizationParam = residualizationParam + ) + # A single context returns the bare matrix rather than a named list. + YresList <- if (length(allContexts) == 1L) { + set_names(list(resid), allContexts) + } else { + resid + } + fitSamples <- .composedFitSamples(YresList, tuples) + if (length(fitSamples) < 2L) { + return(NULL) + } + list( + tuples = tuples, + allContexts = allContexts, + allTraits = allTraits, + YresList = YresList, + fitSamples = fitSamples ) - .composedAlignXy(sides, tuples) } # X and the tuples' Y columns on the samples both sides share, without the @@ -1519,31 +1475,33 @@ parseTraitIds <- function(traitId, data) { # One passthrough column of a joint result row as a character vector (the joint- # key columns), or NULL when absent. # @noRd -.jointStrCol <- function(nm, df) { - if (is_in(nm, names(df))) as.character(df[[nm]]) else NULL +.jointStrCol <- function(nm, rows) { + if (is_in(nm, colnames(mcols(rows)))) as.character(mcols(rows)[[nm]]) } # One passthrough column carried through UNCOERCED (GRanges provenance columns # region / traitPos), or NULL when absent. # @noRd -.jointRawCol <- function(nm, df) if (is_in(nm, names(df))) df[[nm]] else NULL +.jointRawCol <- function(nm, rows) { + if (is_in(nm, colnames(mcols(rows)))) mcols(rows)[[nm]] +} # The optional passthrough columns of a per-tuple result row, as a named list # (NULL for any absent column): the three joint-key columns (jointStudies / # jointContexts / jointTraits) plus the GRanges provenance columns (region / # traitPos). Spliced into the QtlFineMappingResult / TwasWeights constructors so # by-key / cross-study rebuilds preserve them. -.jointCols <- function(df) { +.jointCols <- function(rows) { # traitPos is a GRanges provenance column: carry it through uncoerced so # by-key / cross-study rebuilds keep the trait position instead of silently # dropping it. There is no `region` counterpart -- the fine-mapping window # retired (section 4.4) because the element's own span is the region and a # stored window had no correct update rule when variants are narrowed. list( - jointStudies = .jointStrCol("jointStudies", df), - jointContexts = .jointStrCol("jointContexts", df), - jointTraits = .jointStrCol("jointTraits", df), - traitPos = .jointRawCol("traitPos", df) + jointStudies = .jointStrCol("jointStudies", rows), + jointContexts = .jointStrCol("jointContexts", rows), + jointTraits = .jointStrCol("jointTraits", rows), + traitPos = .jointRawCol("traitPos", rows) ) } @@ -1591,34 +1549,12 @@ parseTraitIds <- function(traitId, data) { rbindFn(fromStudies, ssRes) } -# Reconstruct the per-tuple result object from the accumulated rows, each on -# the LD reference it had. A TwasWeights is already that: row-binding -# regroups its entries by gene, and every row keeps its window, trait -# position and LD reference. -.msDriverPerTuple <- function(out, resultCtor) { - if (methods::is(out, "TwasWeights")) { - return(out) - } - ctorArgs <- c( - list( - studyName = as.character(out$study), - context = as.character(out$context), - trait = as.character(out$trait), - method = as.character(out$method), - entry = .collectionEntries(out) - ), - .jointCols(out), - list(ldReference = .rowLdRefs(out)) - ) - exec(resultCtor, !!!ctorArgs) -} - # Shared tail of the MultiStudyQtlDataset fineMapping / twasWeights pipeline # methods: recurse into each embedded QtlDataset then the embedded QtlSumStats, -# row-bind the per-study results, build the per-tuple result object, and combine -# it with any joint-specification result. `perStudyFn` / `sumStatsFn` are the -# (method-specific) single-study recursions; `rbindFn` / `resultCtor` / -# `pipelineName` supply the per-pipeline pieces. Each method keeps its own +# row-bind the per-study results, and combine them with any +# joint-specification result. `perStudyFn` / `sumStatsFn` are the +# (method-specific) single-study recursions; `rbindFn` / `pipelineName` supply +# the per-pipeline pieces. Each method keeps its own # (divergent) method-gating / joint-dispatch preamble and passes the computed # `jointResult` in. # @noRd @@ -1629,11 +1565,10 @@ parseTraitIds <- function(traitId, data) { sumStatsFn, cfg, rbindFn, - resultCtor, pipelineName, noun = "a result" ) { - out <- .msDriverAccumulate( + perTupleResult <- .msDriverAccumulate( as.list(data), sumStats(data), perStudyFn, @@ -1641,11 +1576,6 @@ parseTraitIds <- function(traitId, data) { cfg, rbindFn ) - perTupleResult <- if (!is.null(out)) { - .msDriverPerTuple(out, resultCtor) - } else { - NULL - } if (is.null(jointResult)) { if (is.null(perTupleResult)) { msg <- glue( @@ -1703,28 +1633,48 @@ parseTraitIds <- function(traitId, data) { methodArgs, cfg ) { + .qtlRunWork( + .fmJointSpecsWork(parsedJointSpec, tokens, methodArgs, cfg), + cfg$data, + cfg$genotypeFilter, + cfg$residualizationParam, + withAf = TRUE + ) +} + +# .fmDispatchJointSpecsQtlDataset()'s fits, planned, for a run with others to +# fit alongside. +# @noRd +.fmJointSpecsWork <- function(parsedJointSpec, tokens, methodArgs, cfg) { .jointRejectStudyOnIndividual(parsedJointSpec) - perRegion <- compact(map( - cfg$xRegions, - .fmDispatchJointSpecRegion, - parsedJointSpec = parsedJointSpec, - tokens = tokens, - methodArgs = methodArgs, - cfg = cfg - )) - .fmCombineRegionResults(perRegion) + .qtlWorkOf( + map( + cfg$xRegions, + .fmJointSpecRegionWork, + parsedJointSpec = parsedJointSpec, + tokens = tokens, + methodArgs = methodArgs, + cfg = cfg + ), + .fmJointRegionsResult + ) +} + +# @noRd +.fmJointRegionsResult <- function(perRegion) { + .fmCombineRegionResults(compact(perRegion)) } -# One region block of .fmDispatchJointSpecsQtlDataset(). +# One region block of .fmJointSpecsWork(). # @noRd -.fmDispatchJointSpecRegion <- function( +.fmJointSpecRegionWork <- function( region, parsedJointSpec, tokens, methodArgs, cfg ) { - .runJointSpecs( + .jointSpecsWork( parsedJointSpec, cfg$data, dataForm = "individual", @@ -1814,17 +1764,20 @@ parseTraitIds <- function(traitId, data) { # applied, as the per-study recursion applies them) -- and on the summary # statistics too. `cfg` is the individual-level joint record plus # `genotypeFilterParam` and `panelFilterParam`, with the collection as data. +# `components = FALSE` leaves the components' fits to their own runs +# (.fmPerStudy()), which have other fits to share their reads with. # @noRd .fmDispatchJointSpecsMultiStudy <- function( parsedJointSpec, tokens, methodArgs, - cfg + cfg, + components = TRUE ) { qtlDatasets <- as.list(cfg$data) specs <- .fmSplitStudyAxisSpecs(parsedJointSpec) .fmMultiStudyWarnExcluded(specs$study, qtlDatasets, cfg$verbose) - out <- if (length(specs$nonStudy) == 0L) { + out <- if (length(specs$nonStudy) == 0L || !components) { NULL } else { .msFoldStudyResults( @@ -1928,6 +1881,39 @@ parseTraitIds <- function(traitId, data) { # --- .fmDispatchJointSpecsMultiStudy helpers -------------------------------- +# The joint specs a component's own run fits, each scope narrowed to what the +# component holds, as a scope only names what some component does. A spec that +# leaves it nothing does not apply to it. +# @noRd +.msStudySpecs <- function(specs, qd) { + compact(map(specs, .msStudySpec, qd = qd)) +} + +# @noRd +.msStudySpec <- function(spec, qd) { + if (is.null(spec$scope)) { + return(spec) + } + scope <- imap(spec$scope, .msScopeValuesIn, qd = qd) + if (any(lengths(scope) == 0L)) { + return(NULL) + } + list_assign(spec, scope = scope) +} + +# @noRd +.msScopeValuesIn <- function(values, key, qd) { + intersect( + values, + switch( + key, + study = .spListStudies(qd), + context = .spListContexts(qd), + trait = .spListTraits(qd) + ) + ) +} + # Partition joint specs into those with a `study` axis and the rest. .fmSplitStudyAxisSpecs <- function(parsedJointSpec) { hasStudy <- map_lgl(parsedJointSpec, .jsHasStudyAxis) @@ -2072,9 +2058,35 @@ parseTraitIds <- function(traitId, data) { ctx, contexts, traitIds +) { + .qtlRunWork( + .twasJointSpecsWork( + parsedJointSpec, + tokens, + marker, + ctx, + contexts, + traitIds + ), + ctx$data, + ctx$genotypeFilter, + ctx$residualizationParam + ) +} + +# .twasDispatchJointSpecsQtlDataset()'s fits, planned, for a run with others to +# fit alongside. +# @noRd +.twasJointSpecsWork <- function( + parsedJointSpec, + tokens, + marker, + ctx, + contexts, + traitIds ) { .jointRejectStudyOnIndividual(parsedJointSpec) - .twasRunJointSpecGrid( + .twasJointSpecGridWork( parsedJointSpec, intersect(tokens, "mrmash"), contexts, @@ -2098,7 +2110,6 @@ parseTraitIds <- function(traitId, data) { pipeline <- new( "TwasJointPipeline", config = list( - dataType = cfg$weightsDataType, crossValidationParam = .cvResolve(CrossValidationParam()), fitRetention = cfg$fitRetention, standardized = TRUE, @@ -2135,12 +2146,13 @@ parseTraitIds <- function(traitId, data) { parsedJointSpec, tokens, methods, - cfg + cfg, + components = TRUE ) { qtlDatasets <- as.list(cfg$data) specs <- .fmSplitStudyAxisSpecs(parsedJointSpec) .twasMultiStudyWarnExcluded(specs$study, qtlDatasets, cfg$verbose) - out <- if (length(specs$nonStudy) == 0L) { + out <- if (length(specs$nonStudy) == 0L || !components) { NULL } else { .msFoldStudyResults( diff --git a/R/ld.R b/R/ld.R index 223ff912c..c1372fc25 100644 --- a/R/ld.R +++ b/R/ld.R @@ -967,9 +967,13 @@ resolveGenotypePathForRegion <- function(metaPath, region) { # discarded: harmonization re-keys a surviving variant to the reference-allele # id, so the panel side has to speak the same form or every later id lookup # misses the variant it just harmonized. +# +# `idx` renders only those variants: a block read needs its own ids, not a whole +# panel's. # @noRd -.ldRefMatchIds <- function(x) { - gr <- .ldRefRanges(x) +.ldRefMatchIds <- function(x, idx = NULL) { + whole <- .ldRefRanges(x) + gr <- if (is.null(idx)) whole else whole[idx] mc <- S4Vectors::mcols(gr) # Rendered from the reference's own ranges and alleles, in the canonical # form harmonization gives every id (UCSC chromosome, colons, upper-case @@ -978,7 +982,8 @@ resolveGenotypePathForRegion <- function(metaPath, region) { # names no position to match on. A reference without alleles has only # its labels. if (is.null(mc$A1) || is.null(mc$A2)) { - return(.ldRefLabels(x)) + labels <- .ldRefLabels(x) + return(if (is.null(idx)) labels else labels[idx]) } .grVariantIds(gr) } @@ -1232,9 +1237,10 @@ resolveGenotypePathForRegion <- function(metaPath, region) { # ordering, the sharded routing and the empty-request guard in # `extractBlockGenotypes()` -- stays the single reader. `extract_array()` # delegates to that same function with `meanImpute = FALSE`, so imputation is -# layered on here rather than pushed down. +# layered on here rather than pushed down. `sampleIdx`, positions among the +# panel's samples, reads only those samples; NULL reads every one. # @noRd -.ldRefDosage <- function(x, snpIdx, meanImpute = TRUE) { +.ldRefDosage <- function(x, snpIdx, meanImpute = TRUE, sampleIdx = NULL) { # The other primitive that does not pass through `.ldRefRanges()`. .ldFromRefValidate(x, "LD reference") if (.ldIsMatrixRef(x) || .ldIsMixtureRef(x)) { @@ -1246,8 +1252,13 @@ resolveGenotypePathForRegion <- function(metaPath, region) { if (!methods::is(x, "RangedSummarizedExperiment")) { return(.dosageMatrix(x, snpIdx, meanImpute = meanImpute)) } + panelDosage <- SummarizedExperiment::assay(x, "dosage") dosage <- t(as.matrix( - SummarizedExperiment::assay(x, "dosage")[snpIdx, , drop = FALSE] + if (is.null(sampleIdx)) { + panelDosage[snpIdx, , drop = FALSE] + } else { + panelDosage[snpIdx, sampleIdx, drop = FALSE] + } )) if (meanImpute) .qtlMeanImpute(dosage) else dosage } @@ -2589,7 +2600,9 @@ resolveGenotypePathForRegion <- function(metaPath, region) { #' #' @param variantIds variant names in the format chr:pos:ref:alt. #' @param ldReferenceMetaFile Path to LD metadata file or PLINK prefix. -#' @param keepIndel Whether to keep indel variants. Default TRUE. +#' @param keepIndel Whether to keep variants other than SNPs. A SNP has exactly +#' one of A, C, G, T for each allele, so \code{I}/\code{D} codes, \code{*} +#' and \code{N} count as indels. Default TRUE. #' @return A list with: #' \item{data}{Character vector of filtered variant IDs.} #' \item{idx}{Integer vector of indices into the original variantIds.} diff --git a/R/manifestLoaders.R b/R/manifestLoaders.R index 9892e624c..a4269293f 100644 --- a/R/manifestLoaders.R +++ b/R/manifestLoaders.R @@ -1511,6 +1511,7 @@ NULL "phenotype_file" ), covariatePath = c("covariatePath", "covariate_path", "cov_path", "covPath"), + dataType = c("dataType", "data_type"), study = c("study", "study_id", "studyId"), genotypePath = c( "genotypePath", @@ -1562,10 +1563,46 @@ NULL studyName = studyName, genotypes = genotypes, phenotypes = phenotypes, - genotypeCovariates = genoCov + genotypeCovariates = genoCov, + dataType = .manifestContextTypes(rows, studyName) ) } +# The molecular type each context declares, named by context, or NULL when +# none does. A context's rows may leave it blank, but cannot disagree. +# @noRd +#' @importFrom dplyr distinct filter +.manifestContextTypes <- function(rows, studyName) { + if (!is_in("dataType", names(rows))) { + return(NULL) + } + typed <- filter( + distinct(tibble( + context = as.character(rows$context), + dataType = .manifestDataType(rows$dataType) + )), + !is.na(.data$dataType) + ) + clash <- unique(typed$context[duplicated(typed$context)]) + if (length(clash) > 0L) { + abort(glue( + "Context(s) {str_flatten(clash, ', ')} (study '{studyName}') ", + "declare several dataType values." + )) + } + if (nrow(typed) == 0L) { + return(NULL) + } + set_names(typed$dataType, typed$context) +} + +# A manifest's dataType column, a blank cell recording none. +# @noRd +#' @importFrom dplyr na_if +.manifestDataType <- function(values) { + na_if(as.character(values), "") +} + # The SummarizedExperiment for one context: exactly one phenotypePath (+ at most # one covariatePath) across its manifest rows. # @noRd @@ -1681,7 +1718,9 @@ NULL #' @param manifest A data.frame or a path to a manifest file. Recognised columns #' (canonical camelCase; snake_case aliases accepted): \code{context} #' (required), \code{phenotypePath} (required, bgzipped BED), -#' \code{covariatePath} (optional, per-context covariates), and the +#' \code{covariatePath} (optional, per-context covariates), +#' \code{dataType} (optional, each context's molecular type, as for +#' \code{\link{QtlDataset}}; a blank cell records none), and the #' single-valued \code{study} / \code{genotypePath} / #' \code{genotypeCovariatePath}. #' @param studyName Study identifier; reconciled with a \code{study} column. @@ -1785,6 +1824,7 @@ loadQtlDatasetFromManifest <- function( ), nSample = c("nSample", "n_sample"), varY = c("varY", "var_y"), + dataType = c("dataType", "data_type"), genome = c("genome"), ldReferencePath = c("ldReferencePath", "ld_reference_path", "ld_reference"), traitStart = c("traitStart", "trait_start"), @@ -2148,7 +2188,9 @@ loadGwasSumStatsFromManifest <- function( #' @param manifest A data.frame or path. Columns (snake_case aliases accepted): #' \code{study}, \code{context}, \code{trait} (required), \code{sumStatsPath} #' (required), \code{columnMapping} (optional), \code{nSample} (optional -#' tuple-level total N), \code{varY} (optional), the single-valued +#' tuple-level total N), \code{varY} (optional), \code{dataType} (optional, +#' each tuple's molecular type, as for \code{\link{QtlSumStats}}; a blank +#' cell records none), the single-valued #' \code{genome}, and \code{ldReferencePath}, which may differ between rows #' (each distinct reference is read once, for the chromosomes its rows #' cover, and stored once). When a row supplies \code{nSample}, @@ -2352,7 +2394,10 @@ loadQtlSumStatsFromManifest <- function( args, compact(list( nSample = nSampleCol, - varY = if (is_in("varY", names(df))) as.numeric(df$varY) + varY = if (is_in("varY", names(df))) as.numeric(df$varY), + dataType = if (is_in("dataType", names(df))) { + .manifestDataType(df$dataType) + } )) ) } diff --git a/R/qtlEnrichmentPipeline.R b/R/qtlEnrichmentPipeline.R index da7848c6a..a0f5a5d0d 100644 --- a/R/qtlEnrichmentPipeline.R +++ b/R/qtlEnrichmentPipeline.R @@ -36,8 +36,9 @@ setClass( #' Default \code{1.0}. #' @param impN Rounds of multiple imputation to draw QTL from. Default #' \code{25}. -#' @param doubleShrinkage Logical. Apply the double-shrinkage correction to -#' the enrichment estimate. Default \code{FALSE}. +#' @param doubleShrinkage Logical. Shrink each imputation round's estimate and +#' then the combined one, as fastenloc does; \code{FALSE} shrinks only the +#' combined one. Default \code{TRUE}. #' @param besselCorrection Logical. Apply Bessel's correction when estimating #' the sampling variance. Default \code{TRUE}. #' @return A \code{QtlEnrichmentParam} object, a \code{\link{MethodParam}}. @@ -49,7 +50,7 @@ QtlEnrichmentParam <- function( piQtl = NULL, lambda = 1.0, impN = 25, - doubleShrinkage = FALSE, + doubleShrinkage = TRUE, besselCorrection = TRUE ) { new( @@ -1114,8 +1115,9 @@ qtlEnrichmentPipeline <- function( #' (e.g. \code{qtlEnrichmentPipeline} aligns each QTL tuple once against #' the union GWAS panel rather than re-aligning per GWAS study); the names #' are then matched to \code{gwasPip} exactly as given. -#' @param doubleShrinkage Logical. Apply the double-shrinkage correction to the -#' enrichment estimate. Default \code{FALSE}. +#' @param doubleShrinkage Logical. Shrink each imputation round's estimate and +#' then the combined one, as fastenloc does; \code{FALSE} shrinks only the +#' combined one. Default \code{TRUE}. #' @param besselCorrection Logical. Apply Bessel's correction when estimating #' the sampling variance. Default \code{TRUE}. #' @param verbose Logical. Print progress messages. Default \code{TRUE}. @@ -1182,7 +1184,7 @@ qtlEnrichment <- function( piQtl = NULL, lambda = 1.0, impN = 25, - doubleShrinkage = FALSE, + doubleShrinkage = TRUE, besselCorrection = TRUE, BPPARAM = SerialParam(), verbose = TRUE, @@ -1228,7 +1230,7 @@ qtlEnrichment <- function( piQtl = NULL, lambda = 1.0, impN = 25, - doubleShrinkage = FALSE, + doubleShrinkage = TRUE, besselCorrection = TRUE, BPPARAM = SerialParam(), verbose = TRUE, diff --git a/R/qtlSumStats.R b/R/qtlSumStats.R index d76360050..b48302999 100644 --- a/R/qtlSumStats.R +++ b/R/qtlSumStats.R @@ -160,6 +160,12 @@ NULL #' @param metadata A list of free-form metadata about the collection, #' as \code{metadata()} holds on any Bioconductor vector. Default #' \code{list()}. +#' @param dataType Optional molecular type of each tuple (\code{"expression"}, +#' \code{"splicing"}, \code{"methylation"}): free text, length 1 or one per +#' tuple. The statistics cannot say what was measured, so pipelines read it +#' from here and record it on what they fit, and \code{\link{ctwasPipeline}} +#' models each type as its own. Read by \code{\link{dataType}}. \code{NULL} +#' (default) omits the column. #' @return A \code{QtlSumStats} object. #' @examples #' panel <- readGenotypes( @@ -183,7 +189,8 @@ QtlSumStats <- function( traitPos = NULL, region = NULL, extraCols = list(), - metadata = list() + metadata = list(), + dataType = NULL ) { if ( missing(studyName) || @@ -206,6 +213,7 @@ QtlSumStats <- function( cols <- .qssBaseCols(studyName, context, trait, varY) |> .qssAppendNSample(nSample, n) |> .appendTraitPosCol(traitPos, n) |> + .appendProvenanceCol(dataType, "dataType", n) |> .qssAppendExtras(extraCols) dict <- .ldRefDict(.ldRefPerTuple(ldReference, n)) grl <- .qssElements(entry, cols, dict$keys, windows) diff --git a/R/regularizedRegressionWrappers.R b/R/regularizedRegressionWrappers.R index ca70e5d7a..20c28dfc0 100644 --- a/R/regularizedRegressionWrappers.R +++ b/R/regularizedRegressionWrappers.R @@ -280,8 +280,8 @@ prsCs <- function( #' @param stat A list of summary statistics: \code{n} (per-variant sample #' sizes) and the marginal effects as \code{cor} (standardized), \code{z}, #' \code{b} with \code{seb}, or \code{b} alone, taken as already -#' standardized. The engine receives the standardized effects z / sqrt(n), -#' with n the median of \code{n}. +#' standardized. The engine receives the correlation each z stands for, +#' z / sqrt(z^2 + n - 2), with n the median of \code{n}. #' @param ldReference The LD reference the statistics' variants are matched #' to by \code{stat$variantNames}: a genotype panel (see #' \code{\link{readGenotypes}}), a \code{\link{GenotypeMixture}} or an @@ -568,8 +568,8 @@ sdpr <- function( #' @param stat A list of summary statistics: \code{n} (per-variant sample #' sizes) and the marginal effects as \code{cor} (standardized), \code{z}, #' \code{b} with \code{seb}, or \code{b} alone, taken as already -#' standardized. The engine receives the standardized effects z / sqrt(n), -#' with n the median of \code{n}. +#' standardized. The engine receives the correlation each z stands for, +#' z / sqrt(z^2 + n - 2), with n the median of \code{n}. #' @param ldReference The LD reference the statistics' variants are matched #' to by \code{stat$variantNames}: a genotype panel (see #' \code{\link{readGenotypes}}), a \code{\link{GenotypeMixture}} or an @@ -1757,8 +1757,10 @@ fitGlmnetCv <- function( #' (\code{coef}, over the variants fitted), \code{intercept}, #' \code{lambda} and cross-validated error there (\code{cvm}, #' \code{cvsd}); \code{"full"} the whole \code{cv.glmnet} object, with -#' every \code{lambda}'s coefficients. Either names the variants it was -#' fitted on. +#' every \code{lambda}'s coefficients but without the copy of \code{X} its +#' stored calls would hold. Each names the variants it was fitted on. +#' @param retainX Logical. Keep that copy of \code{X} in a \code{"full"} fit +#' (default \code{FALSE}). An error at any other \code{fitRetention}. #' @param methodArgs Additional arguments forwarded to #' \code{glmnet::cv.glmnet}, built with \code{\link{GlmnetOptions}}. A bare #' list is refused, since it cannot be checked against the engine's formals. @@ -1777,9 +1779,11 @@ glmnetWeights <- function( y, alpha, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = GlmnetOptions() ) { fitRetention <- arg_match(fitRetention) + .assertRetainX(retainX, fitRetention) eff.wgt <- matrix(0, ncol = 1, nrow = ncol(X)) keep <- .dropZeroVariance(X, "glmnetWeights") # `alpha` is the wrapper's own dial and is refused by GlmnetOptions() @@ -1800,14 +1804,46 @@ glmnetWeights <- function( if (identical(fitRetention, "none")) { return(weights) } - fit <- if (identical(fitRetention, "full")) { - enet - } else { - .glmnetSlimFit(enet, coefs) - } + fit <- switch( + fitRetention, + slim = .glmnetSlimFit(enet, coefs), + full = if (retainX) enet else .glmnetWithoutData(enet) + ) `attr<-`(weights, "fit", .withFitVariantIds(fit, colnames(X)[keep])) } +# cv.glmnet's whole object without the data it was given: called with values +# rather than names, its stored calls hold them inline. +# @noRd +.glmnetWithoutData <- function(cvFit) { + inner <- cvFit$glmnet.fit + `[[<-`( + `[[<-`(cvFit, "call", value = .callWithoutData(cvFit$call)), + "glmnet.fit", + value = `[[<-`(inner, "call", value = .callWithoutData(inner$call)) + ) +} + +# A stored call naming its data rather than holding it. +# @noRd +.callWithoutData <- function(call) { + held <- intersect(c("x", "y"), names(call)) + as.call(replace(as.list(call), held, map(held, as.name))) +} + +# `retainX` keeps a fit's copy of X, which only a "full" fit can hold. +# @noRd +.assertRetainX <- function(retainX, fitRetention) { + assertFlag(retainX) + if (retainX && !identical(fitRetention, "full")) { + abort(glue( + "`retainX = TRUE` requires `fitRetention = \"full\"`, not ", + "\"{fitRetention}\"." + )) + } + invisible(NULL) +} + # The fit cv.glmnet selected: its coefficients over the variants fitted, the # intercept, its lambda and the cross-validated error there. # @noRd @@ -1846,6 +1882,7 @@ enetWeights <- function( y, alpha = 0.5, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = GlmnetOptions() ) { .assertMethodOptions(methodArgs, "GlmnetOptions", "methodArgs") @@ -1855,6 +1892,7 @@ enetWeights <- function( y, alpha, fitRetention = arg_match(fitRetention), + retainX = retainX, methodArgs = methodArgs ) } @@ -1876,6 +1914,7 @@ ridgeWeights <- function( X, y, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = GlmnetOptions() ) { .assertMethodOptions(methodArgs, "GlmnetOptions", "methodArgs") @@ -1884,6 +1923,7 @@ ridgeWeights <- function( y, 0, fitRetention = arg_match(fitRetention), + retainX = retainX, methodArgs = methodArgs ) } @@ -1905,6 +1945,7 @@ lassoWeights <- function( X, y, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = GlmnetOptions() ) { .assertMethodOptions(methodArgs, "GlmnetOptions", "methodArgs") @@ -1913,6 +1954,7 @@ lassoWeights <- function( y, 1, fitRetention = arg_match(fitRetention), + retainX = retainX, methodArgs = methodArgs ) } @@ -1938,8 +1980,10 @@ lassoWeights <- function( #' attribute of the returned weights: \code{"none"} (default) attaches #' nothing, \code{"slim"} the fitted model without its copy of the data #' (\code{data}) or the order it updated the variants in each iteration -#' (\code{update.order}), \code{"full"} the whole fitted model. Either -#' names the variants it was fitted on. +#' (\code{update.order}), \code{"full"} the whole fitted model but for its +#' copy of \code{X}. Each names the variants it was fitted on. +#' @param retainX Logical. Keep that copy of \code{X} in a \code{"full"} fit +#' (default \code{FALSE}). An error at any other \code{fitRetention}. #' @param methodArgs Options forwarded to \code{susieR::mr.ash}, built #' with \code{\link{MrashOptions}}. A bare list is refused: it cannot be #' checked against the engine, so a misspelled option would be @@ -1953,10 +1997,12 @@ mrashWeights <- function( X, y, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = MrashOptions() ) { .assertMethodOptions(methodArgs, "MrashOptions", "methodArgs") fitRetention <- arg_match(fitRetention) + .assertRetainX(retainX, fitRetention) keep <- .dropZeroVariance(X, "mrashWeights") XKeep <- X[, keep, drop = FALSE] # as.list(): list_assign() rejects a MethodOptions outright as `.x`, the @@ -1973,11 +2019,19 @@ mrashWeights <- function( if (identical(fitRetention, "none")) { return(eff.wgt) } - kept <- if (identical(fitRetention, "full")) { - fit.mr.ash - } else { - .withoutFields(fit.mr.ash, c("data", "update.order")) - } + kept <- switch( + fitRetention, + slim = .withoutFields(fit.mr.ash, c("data", "update.order")), + full = if (retainX) { + fit.mr.ash + } else { + `[[<-`( + fit.mr.ash, + "data", + value = .withoutFields(fit.mr.ash$data, "X") + ) + } + ) `attr<-`(eff.wgt, "fit", .withFitVariantIds(kept, colnames(XKeep))) } # --- Bayesian alphabet ------------------------------------------------------ @@ -2712,8 +2766,8 @@ bayesRWeights <- function( #' @param stat A list of summary statistics: \code{n} (per-variant sample #' sizes) and the marginal effects as \code{cor} (standardized), \code{z}, #' \code{b} with \code{seb}, or \code{b} alone, taken as already -#' standardized. The engines receive the standardized effects -#' z / sqrt(n), with n the median of \code{n}. +#' standardized. The engines receive the correlation each z stands for, +#' z / sqrt(z^2 + n - 2), with n the median of \code{n}. #' @param ldReference The LD reference the statistics' variants are matched #' to by \code{stat$variantNames}: a genotype panel (see #' \code{\link{readGenotypes}}), a \code{\link{GenotypeMixture}} or an @@ -3289,7 +3343,7 @@ bayesAlphabetRss <- function( #' data(eqtlRegionExample) #' X <- eqtlRegionExample$X[, 1:30] #' stat <- list( -#' z = as.vector(cor(X, eqtlRegionExample$yRes)) * sqrt(nrow(X)), +#' cor = as.vector(cor(X, eqtlRegionExample$yRes)), #' n = rep(nrow(X), ncol(X)), #' variantNames = colnames(X) #' ) @@ -3333,7 +3387,7 @@ bayesARssWeights <- function( #' data(eqtlRegionExample) #' X <- eqtlRegionExample$X[, 1:30] #' stat <- list( -#' z = as.vector(cor(X, eqtlRegionExample$yRes)) * sqrt(nrow(X)), +#' cor = as.vector(cor(X, eqtlRegionExample$yRes)), #' n = rep(nrow(X), ncol(X)), #' variantNames = colnames(X) #' ) @@ -3378,7 +3432,7 @@ bayesBRssWeights <- function( #' data(eqtlRegionExample) #' X <- eqtlRegionExample$X[, 1:30] #' stat <- list( -#' z = as.vector(cor(X, eqtlRegionExample$yRes)) * sqrt(nrow(X)), +#' cor = as.vector(cor(X, eqtlRegionExample$yRes)), #' n = rep(nrow(X), ncol(X)), #' variantNames = colnames(X) #' ) @@ -3423,7 +3477,7 @@ bayesCRssWeights <- function( #' data(eqtlRegionExample) #' X <- eqtlRegionExample$X[, 1:30] #' stat <- list( -#' z = as.vector(cor(X, eqtlRegionExample$yRes)) * sqrt(nrow(X)), +#' cor = as.vector(cor(X, eqtlRegionExample$yRes)), #' n = rep(nrow(X), ncol(X)), #' variantNames = colnames(X) #' ) @@ -3465,7 +3519,7 @@ bayesLRssWeights <- function( #' data(eqtlRegionExample) #' X <- eqtlRegionExample$X[, 1:30] #' stat <- list( -#' z = as.vector(cor(X, eqtlRegionExample$yRes)) * sqrt(nrow(X)), +#' cor = as.vector(cor(X, eqtlRegionExample$yRes)), #' n = rep(nrow(X), ncol(X)), #' variantNames = colnames(X) #' ) @@ -3507,7 +3561,7 @@ bayesNRssWeights <- function( #' data(eqtlRegionExample) #' X <- eqtlRegionExample$X[, 1:30] #' stat <- list( -#' z = as.vector(cor(X, eqtlRegionExample$yRes)) * sqrt(nrow(X)), +#' cor = as.vector(cor(X, eqtlRegionExample$yRes)), #' n = rep(nrow(X), ncol(X)), #' variantNames = colnames(X) #' ) @@ -3544,7 +3598,7 @@ bayesRRssWeights <- function( # LDpred2-auto and LDpred2-grid run on bigsnpr, on the LD matrix written to a # temporary file, or on pecotmr's port of its samplers (ldpred2FactorCpp()), # on a genotype reference's factor of the LD. Both take the statistics in -# standardized units, z / sqrt(n_j) for each variant's own sample size, and +# standardized units, the correlations for each variant's own sample size, and # return weights in the same units. #' LDpred2-auto Weights from Summary Statistics @@ -3575,8 +3629,8 @@ bayesRRssWeights <- function( #' @param stat A list of summary statistics: \code{n} (per-variant sample #' sizes) and the marginal effects as \code{cor} (standardized), \code{z}, #' \code{b} with \code{seb}, or \code{b} alone, taken as already -#' standardized. The sampler reads z / sqrt(n) for each variant's own -#' sample size. +#' standardized. The sampler reads the correlation each z stands for, +#' z / sqrt(z^2 + n - 2), for each variant's own sample size. #' @param ldReference The LD reference the statistics' variants are matched #' to by \code{stat$variantNames}: a genotype panel (see #' \code{\link{readGenotypes}}), a \code{\link{GenotypeMixture}} or an @@ -3814,7 +3868,7 @@ ldpred2GridWeights <- function( } # The statistics bigsnpr's engines and pecotmr's LDpred2 read: the LD source -# (.rssLdSource()), the standardized effects z / sqrt(n_j) and each +# (.rssLdSource()), the correlations (.rssMarginalCor()) and each # variant's sample size. # @noRd .rssPerVariantInput <- function(stat, ldReference, label) { @@ -3849,8 +3903,9 @@ ldpred2GridWeights <- function( } # The window's heritability by LAVA (.cisH2Lava(), estimateH2()'s default -# for QTL summary statistics), which reads the reference's sample size, -# floored at bigsnpr's 0.001: the start too where LAVA gives no estimate. +# for QTL summary statistics), which reads the reference's sample size, taken to +# share no samples with the study, and floored at bigsnpr's 0.001: the start too +# where LAVA gives no estimate. # @noRd .ldpred2LavaH2 <- function(inp, label) { if (is.na(inp$ld$nSamples)) { @@ -3862,7 +3917,7 @@ ldpred2GridWeights <- function( i = "Build the LdMatrix with `nSamples`, or give the heritability." )) } - h2 <- .cisH2Lava(inp$bhat * sqrt(inp$n), inp$n, inp$ld)$h2 + h2 <- .lavaFit(inp$bhat, mean(inp$n), inp$ld)$h2 if (is.na(h2)) 1e-3 else max(h2, 1e-3) } @@ -4349,17 +4404,26 @@ lassosumRss <- function( map(fn_fmls(fn)[fmlNames], eval_bare, env = fn_env(fn)) } -# The standardized marginal effects, z / sqrt(n): the scale lassosum, PRS-CS -# and SDPR all fit. Read from `cor` as given, else from `z`, else from -# `b / seb`, which is a z-score whatever scale `b` is on, else from `b` alone, -# taken as already standardized. +# The correlation behind a t statistic z on n samples, the marginal effect of a +# standardized variant on a standardized trait. z / sqrt(n) is only close to it +# while z^2 is small against n, which a GWAS can assume and a strong QTL cannot. +# @noRd +.zToCor <- function(z, n) { + z / sqrt(z^2 + n - 2) +} + +# The standardized marginal effects, the scale lassosum, PRS-CS and SDPR all +# fit: the correlations (.zToCor()) rather than their usual stand-in z / +# sqrt(n), which past r^2 = 0.5 exceeds 1 and fails a strong QTL's fit. Read +# from `cor` as given, else from `z`, else from `b / seb`, which is a z-score +# whatever scale `b` is on, else from `b` alone, taken as already standardized. .rssMarginalCor <- function(stat, n, p) { corInput <- if (!is.null(stat$cor)) { as.numeric(stat$cor) } else if (!is.null(stat$z)) { - as.numeric(stat$z) / sqrt(n) + .zToCor(as.numeric(stat$z), n) } else if (!is.null(stat$b) && !is.null(stat$seb)) { - as.numeric(stat$b) / as.numeric(stat$seb) / sqrt(n) + .zToCor(as.numeric(stat$b) / as.numeric(stat$seb), n) } else if (!is.null(stat$b)) { as.numeric(stat$b) } else { @@ -4458,9 +4522,11 @@ lassosumRss <- function( .rssShrunkLdMoments <- function(candidateBeta, corInput, ld, w) { list( bxy = as.numeric(crossprod(corInput, candidateBeta)), - bxxb = (1 - w) * - .ldQuadForms(ld, candidateBeta) + - w * colSums(candidateBeta^2) + bxxb = as.numeric( + (1 - w) * + .ldQuadForms(ld, candidateBeta) + + w * colSums(candidateBeta^2) + ) ) } @@ -4484,7 +4550,7 @@ lassosumRss <- function( # @noRd .rssHeldOutTrainShare <- function() 0.75 -# Selection on held-out summary statistics, as PUMAS does it (Zhao et al. +# Selection on held-out summary statistics, with PUMAS's splits (Zhao et al. # 2021). For the marginal correlations c of n samples, a training part of # nTrain of them has c_t ~ N(c, (n - nTrain) / (n nTrain) Sigma), and its # held-out remainder c_h = (n c - nTrain c_t) / (n - nTrain). Sigma is the @@ -4493,7 +4559,9 @@ lassosumRss <- function( # directions of the true LD, the least-penalized fits put weight exactly # there, and draws and scores from R alone never see it. Each split refits # the whole (s, lambda) grid on c_t and scores it on c_h; the full-data -# candidate with the best mean score is returned (.rssHeldOutPick()). +# candidate with the best mean score is returned (.rssHeldOutPick()). One +# whose full-data fit is empty has nothing to return, so it counts as having +# no correlation. # @noRd #' @importFrom stats rnorm .rssSelectHeldOut <- function(candidateBeta, corInput, ld, n, refit) { @@ -4515,19 +4583,23 @@ lassosumRss <- function( corScores <- map(scores, "cor") explainedScores <- map(scores, "explained") idx <- .rssHeldOutPick( - rowMeans(exec(cbind, !!!corScores)), + replace( + rowMeans(exec(cbind, !!!corScores)), + colSums(candidateBeta != 0, na.rm = TRUE) == 0, + -Inf + ), rowMeans(exec(cbind, !!!explainedScores)) ) list(beta = candidateBeta[, idx], index = idx, mode = "heldOut") } # Split `k`: the candidates refit on its training part and scored on its -# held-out part, by their correlation with it and by the variance of it they -# explain, 2 c_h' b - b' Sigma b, which unlike the correlation depends on b's -# scale. `refit` names the refitting function (`fn`) and its other -# arguments (`args`); it is given the training part's correlations and the -# share of the samples that part keeps, and returns the candidates in the -# full data's order. +# held-out part, by the variance of it they explain, 2 c_h' b - b' Sigma b, +# and by their correlation with it, which is -Inf for a fit empty there. +# `refit` names the refitting function (`fn`) and its other arguments +# (`args`); it is given the training part's correlations and the share of the +# samples that part keeps, and returns the candidates in the full data's +# order. # @noRd .rssHeldOutScore <- function(k, noise, corInput, ld, refit, w) { share <- .rssHeldOutTrainShare() @@ -4542,14 +4614,18 @@ lassosumRss <- function( ) } -# The candidate with the best mean held-out correlation. Candidates that -# differ only in scale -- the ridge's at s = 1, where each fit is -# c / (1 + lambda) -- tie on it, and the one explaining the most held-out -# variance wins; the first in the grid where that ties too. +# The candidate explaining the most held-out variance on average. PUMAS picks +# on the correlation instead, but that ignores scale: at s near 1 the fit nearly +# ignores the LD, every tag of a signal keeps its marginal weight, and it can +# win with a predicted variance thousands of times the trait's. A correlation +# of -Inf marks a fit empty in some split or on the full data. Such a candidate +# is out while any other is left, or its score of 0 would beat every fit that +# overshoots, so a fit is returned wherever one exists. A tie in the variance +# goes to the better correlation, then to the first in the grid. # @noRd .rssHeldOutPick <- function(cor, explained) { - ties <- .rssScoreTies(cor) - ties[.lassosumFirstMax(explained[ties])] + ties <- .rssScoreTies(replace(explained, !is.finite(cor), -Inf)) + ties[.lassosumFirstMax(cor[ties])] } # The shrinkage grid refit on a held-out split's training part. @@ -4945,8 +5021,8 @@ lassosumRss <- function( #' training part, 75\% of the samples, and its held-out remainder, drawing #' each split from the statistics and the LD as PUMAS does (Zhao et al. #' 2021). The path is refit on each training part, every candidate is scored -#' on the held-out part by -#' \deqn{\frac{c_h^T \beta}{\sqrt{\beta^T \hat\Sigma \beta}}, \quad +#' on the held-out part by the variance of it that it explains, +#' \deqn{2 c_h^T \beta - \beta^T \hat\Sigma \beta, \quad #' \hat\Sigma = (1 - w) R + w I,} #' and the full-data fit at the (\code{s}, lambda) with the best mean score #' is returned. \eqn{w} is the Schafer-Strimmer intensity for a correlation @@ -4954,13 +5030,16 @@ lassosumRss <- function( #' number of variants misses directions of the true LD, and the #' least-penalized fits put weight exactly there, so scoring against #' \eqn{R} itself would keep choosing them. The splits use R's random number -#' generator, so \code{set.seed()} makes the weights reproducible. The score -#' does not change with \eqn{\beta}'s scale, so candidates that differ only -#' in scale tie on it, as the ridge's do at \eqn{s = 1}, where each fit is -#' \eqn{c / (1 + \lambda)}. Of tied candidates, the one explaining the most -#' held-out variance, \eqn{2 c_h^T \beta - \beta^T \hat\Sigma \beta} averaged -#' over the splits, is returned, and the first in the grid where that ties -#' too. +#' generator, so \code{set.seed()} makes the weights reproducible. +#' +#' PUMAS scores by the correlation with the held-out part, +#' \eqn{c_h^T \beta / \sqrt{\beta^T \hat\Sigma \beta}}, which here only breaks +#' ties. It does not change with \eqn{\beta}'s scale, so it lets a fit win at +#' \eqn{s} near 1, where the LD is nearly ignored and every tag of a signal +#' keeps its marginal weight, with a predicted variance thousands of times the +#' trait's. A candidate whose fit is empty in a split or on the full data is +#' returned only if every candidate's is, and the first in the grid wins a tie +#' on both scores. #' #' \code{selection = "ldQuadratic"} is lassosum's pseudovalidation (Mak et #' al. 2017): the in-sample score @@ -4973,8 +5052,8 @@ lassosumRss <- function( #' @param stat A list of summary statistics: \code{n} (per-variant sample #' sizes) and the marginal effects as \code{cor} (standardized), \code{z}, #' \code{b} with \code{seb}, or \code{b} alone, taken as already -#' standardized. The engine receives the standardized effects z / sqrt(n), -#' with n the median of \code{n}. +#' standardized. The engine receives the correlation each z stands for, +#' z / sqrt(z^2 + n - 2), with n the median of \code{n}. #' @param ldReference The LD reference the statistics' variants are matched #' to by \code{stat$variantNames}: a genotype panel (see #' \code{\link{readGenotypes}}), a \code{\link{GenotypeMixture}} or an @@ -5475,8 +5554,8 @@ penalizedRss <- function( #' @param stat A list of summary statistics: \code{n} (per-variant sample #' sizes) and the marginal effects as \code{cor} (standardized), \code{z}, #' \code{b} with \code{seb}, or \code{b} alone, taken as already -#' standardized. The engine receives the standardized effects z / sqrt(n), -#' with n the median of \code{n}. +#' standardized. The engine receives the correlation each z stands for, +#' z / sqrt(z^2 + n - 2), with n the median of \code{n}. #' @param ldReference The LD reference the statistics' variants are matched #' to by \code{stat$variantNames}: a genotype panel (see #' \code{\link{readGenotypes}}), a \code{\link{GenotypeMixture}} or an @@ -5548,8 +5627,8 @@ scadRssWeights <- function( #' @param stat A list of summary statistics: \code{n} (per-variant sample #' sizes) and the marginal effects as \code{cor} (standardized), \code{z}, #' \code{b} with \code{seb}, or \code{b} alone, taken as already -#' standardized. The engine receives the standardized effects z / sqrt(n), -#' with n the median of \code{n}. +#' standardized. The engine receives the correlation each z stands for, +#' z / sqrt(z^2 + n - 2), with n the median of \code{n}. #' @param ldReference The LD reference the statistics' variants are matched #' to by \code{stat$variantNames}: a genotype panel (see #' \code{\link{readGenotypes}}), a \code{\link{GenotypeMixture}} or an @@ -5758,8 +5837,8 @@ ridgeRssWeights <- function( #' @param stat A list of summary statistics: \code{n} (per-variant sample #' sizes) and the marginal effects as \code{cor} (standardized), \code{z}, #' \code{b} with \code{seb}, or \code{b} alone, taken as already -#' standardized. The engine receives the standardized effects z / sqrt(n), -#' with n the median of \code{n}. +#' standardized. The engine receives the correlation each z stands for, +#' z / sqrt(z^2 + n - 2), with n the median of \code{n}. #' @param ldReference The LD reference the statistics' variants are matched #' to by \code{stat$variantNames}: a genotype panel (see #' \code{\link{readGenotypes}}), a \code{\link{GenotypeMixture}} or an @@ -5883,7 +5962,9 @@ l0learnRssWeights <- function( #' \code{lambda} and cross-validated error there (\code{cve}, #' \code{cvse}); \code{"full"} the whole \code{cv.ncvreg} object, with #' every \code{lambda}'s coefficients but without its copy of \code{X}. -#' Either names the variants it was fitted on. +#' Each names the variants it was fitted on. +#' @param retainX Logical. Keep that copy of \code{X} in a \code{"full"} fit +#' (default \code{FALSE}). An error at any other \code{fitRetention}. #' @param methodArgs Options forwarded to \code{ncvreg::cv.ncvreg}, built #' with \code{\link{NcvregOptions}}. A bare list is refused: it cannot be #' checked against the engine, so a misspelled option would be @@ -5900,11 +5981,13 @@ ncvregWeights <- function( penalty, nfolds = 5, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = NcvregOptions() ) { .assertMethodOptions(methodArgs, "NcvregOptions", "methodArgs") assertCount(nfolds, positive = TRUE) fitRetention <- arg_match(fitRetention) + .assertRetainX(retainX, fitRetention) if (!requireNamespace("ncvreg", quietly = TRUE)) { abort("Package 'ncvreg' is required for this function.") } @@ -5926,11 +6009,15 @@ ncvregWeights <- function( if (identical(fitRetention, "none")) { return(weights) } - kept <- if (identical(fitRetention, "full")) { - `[[<-`(fit, "fit", .withoutFields(fit$fit, "X")) - } else { - .ncvregSlimFit(fit, coefs) - } + kept <- switch( + fitRetention, + slim = .ncvregSlimFit(fit, coefs), + full = if (retainX) { + fit + } else { + `[[<-`(fit, "fit", value = .withoutFields(fit$fit, "X")) + } + ) `attr<-`(weights, "fit", .withFitVariantIds(kept, colnames(X)[keep])) } @@ -5981,6 +6068,7 @@ scadWeights <- function( y, nfolds = 5, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = NcvregOptions() ) { .assertMethodOptions(methodArgs, "NcvregOptions", "methodArgs") @@ -5990,6 +6078,7 @@ scadWeights <- function( penalty = "SCAD", nfolds = nfolds, fitRetention = arg_match(fitRetention), + retainX = retainX, methodArgs = methodArgs ) } @@ -6021,6 +6110,7 @@ mcpWeights <- function( y, nfolds = 5, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = NcvregOptions() ) { .assertMethodOptions(methodArgs, "NcvregOptions", "methodArgs") @@ -6030,6 +6120,7 @@ mcpWeights <- function( penalty = "MCP", nfolds = nfolds, fitRetention = arg_match(fitRetention), + retainX = retainX, methodArgs = methodArgs ) } @@ -6227,9 +6318,11 @@ bglrWeights <- function( #' silently ignored. #' @param fitRetention How much of the fit is kept as the \code{"fit"} #' attribute of the returned weights: \code{"none"} (default) attaches -#' nothing, \code{"slim"} the fitted model without its copy of the -#' genotypes (\code{x}), \code{"full"} the whole fitted model. Either -#' names the variants it was fitted on. +#' nothing, \code{"slim"} and \code{"full"} the fitted model without its +#' copy of the genotypes (\code{x}). Each names the variants it was fitted +#' on. +#' @param retainX Logical. Keep that copy of the genotypes in a \code{"full"} +#' fit (default \code{FALSE}). An error at any other \code{fitRetention}. #' @param nK Integer. Number of variational mixture components for the VB fit #' (\code{dprVbWeights}). Default \code{8}. #' @param sStep Integer. Number of Gibbs sampling steps for the Gibbs fit @@ -6246,9 +6339,11 @@ dprWeights <- function( y, fittingMethod = "VB", fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = DprOptions() ) { fitRetention <- arg_match(fitRetention) + .assertRetainX(retainX, fitRetention) .assertMethodOptions(methodArgs, "DprOptions", "methodArgs") if (!requireNamespace("RcppDPR", quietly = TRUE)) { abort("Package 'RcppDPR' is required for this function.") @@ -6272,11 +6367,7 @@ dprWeights <- function( if (identical(fitRetention, "none")) { return(eff.wgt) } - kept <- if (identical(fitRetention, "full")) { - fit - } else { - .withoutFields(fit, "x") - } + kept <- if (retainX) fit else .withoutFields(fit, "x") `attr<-`(eff.wgt, "fit", .withFitVariantIds(kept, colnames(X)[keep])) } @@ -6292,6 +6383,7 @@ dprVbWeights <- function( y, nK = 8, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = DprOptions() ) { fitRetention <- arg_match(fitRetention) @@ -6301,6 +6393,7 @@ dprVbWeights <- function( y, fittingMethod = "VB", fitRetention = fitRetention, + retainX = retainX, methodArgs = .methodOptionsWith(methodArgs, "DprOptions", n_k = nK) ) } @@ -6321,6 +6414,7 @@ dprGibbsWeights <- function( y, sStep = 5000, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = DprOptions() ) { .assertMethodOptions(methodArgs, "DprOptions", "methodArgs") @@ -6331,6 +6425,7 @@ dprGibbsWeights <- function( y, fittingMethod = "Gibbs", fitRetention = fitRetention, + retainX = retainX, methodArgs = .methodOptionsWith( methodArgs, "DprOptions", @@ -6354,6 +6449,7 @@ dprAdaptiveGibbsWeights <- function( X, y, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = DprOptions() ) { .assertMethodOptions(methodArgs, "DprOptions", "methodArgs") @@ -6363,6 +6459,7 @@ dprAdaptiveGibbsWeights <- function( y, fittingMethod = "Adaptive_Gibbs", fitRetention = fitRetention, + retainX = retainX, methodArgs = methodArgs ) } diff --git a/R/resultReaders.R b/R/resultReaders.R index e0bc54af9..209e49f50 100644 --- a/R/resultReaders.R +++ b/R/resultReaders.R @@ -1102,7 +1102,7 @@ NULL #' 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 dataType Optional molecular type, 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 diff --git a/R/resultWriters.R b/R/resultWriters.R index df622a589..6fd8d37be 100644 --- a/R/resultWriters.R +++ b/R/resultWriters.R @@ -2805,7 +2805,7 @@ setMethod( # are ";"-packed rather than silently truncated. # @noRd .twbCellDataType <- function(entry) { - v <- weightsDataType(entry) + v <- dataType(entry) if (length(v) == 0L) NA_character_ else .pubPack(v) } diff --git a/R/sldscWrapper.R b/R/sldscWrapper.R index c29089e04..18e1acf3f 100644 --- a/R/sldscWrapper.R +++ b/R/sldscWrapper.R @@ -16,8 +16,9 @@ # # Cross-type comparison: tau* (Gazal et al. 2017 standardization) is the # cross-type comparable statistic. Use tau* to rank or meta-analyze annotations -# that mix binary and continuous types. E (proportion-based enrichment) is -# scale-dependent for continuous annotations and is only comparable within type. +# that mix binary and continuous types. E (proportion-based enrichment) moves +# with where a continuous annotation's zero is, though not with its scale, and +# is only comparable within type. # ---- internal helpers ---- diff --git a/R/sumstatsQc.R b/R/sumstatsQc.R index 7d17cf629..6106bbc91 100644 --- a/R/sumstatsQc.R +++ b/R/sumstatsQc.R @@ -4893,7 +4893,7 @@ effectiveN <- function(nCase, nControl) { .qcStepKeepSkip <- function(df, opts) { nIn <- nrow(df) kept <- if (length(opts$keepVariants) > 0L) { - filter(df, is_in(.data$SNP, opts$keepVariants)) + slice(df, which(.qcKeepListed(df, opts$keepVariants))) } else { df } @@ -4918,6 +4918,22 @@ effectiveN <- function(nCase, nControl) { ) } +# Which of an entry's variants a keep list names, as every keep list is matched +# (.keepByIds()): by position and alleles, or by the SNP label for an rsID. +# @noRd +.qcKeepListed <- function(df, keepVariants) { + .keepByIds( + tibble( + chrom = df$chrom, + id = df$SNP, + pos = df$pos, + A2 = df$A2, + A1 = df$A1 + ), + keepVariants + ) +} + # Optional post-harmonization signal screen (PIP / |Z| / BF / logBF). .qcStepScreen <- function(df, opts) { if (is.null(opts$screen)) { @@ -5797,8 +5813,9 @@ effectiveN <- function(nCase, nControl) { #' \code{AF} / \code{MAF} / \code{FRQ} column when one is present #' \emph{and} against the panel. It has always been a single knob, so it #' lives here rather than being split into two that could disagree. -#' @param keepVariants Optional character vector of variant IDs (SNP column) to -#' retain prior to harmonization. +#' @param keepVariants Optional character vector of variant IDs or rsIDs to +#' retain prior to harmonization. Alleles match in either order and on either +#' strand, and an rsID is matched against the \code{SNP} column. #' @param skipRegion Optional \code{GRanges} of regions to drop. #' @param signalScreenParam Whether to skip an entry before fitting it, #' built with @@ -6155,8 +6172,9 @@ setClass( #' checked bundle. These act on the sumstats themselves, not on any genotype #' matrix, which is why they are separate from #' \code{\link{GenotypeFilterParam}}. -#' @param removeIndels Logical. Drop insertions and deletions. Default -#' \code{FALSE}. +#' @param removeIndels Logical. Drop variants other than SNPs. A SNP has exactly +#' one of A, C, G, T for each allele, so \code{I}/\code{D} codes, \code{*} and +#' \code{N} count as indels. Default \code{FALSE}. #' @param removeStrandAmbiguous Logical. Drop strand-ambiguous variants (A/T #' and C/G). Default \code{TRUE}. #' @param infoCutoff Imputation-INFO floor. Default \code{0}. diff --git a/R/tupleSelectors.R b/R/tupleSelectors.R index fbc8a1b17..018cc9100 100644 --- a/R/tupleSelectors.R +++ b/R/tupleSelectors.R @@ -454,6 +454,107 @@ rep(gr, n) } +#' @rdname dataType +#' @export +setMethod("dataType", "RangedTupleList", function(x) { + md <- mcols(x, use.names = FALSE) + if (is_in("dataType", colnames(md))) { + return(as.character(md$dataType)) + } + rep(NA_character_, nrow(md)) +}) + +# The molecular type each row of a pipeline's result takes from the data it was +# fitted on: QtlDataset per context, QtlSumStats per row. A joint fit's members +# are its ";"-joined joint columns; it takes their one type, and NA if they +# differ. NA where the data records none. +# @noRd +.rowDataTypes <- function(x, data) { + table <- .dataTypeTable(data) + if (nrow(table) == 0L || nrow(x) == 0L) { + return(rep(NA_character_, nrow(x))) + } + members <- .rowMemberKeys(mcols(x, use.names = FALSE)) + found <- table$dataType[match(list_c(members), table$key)] + perRow <- split(found, rep(seq_along(members), lengths(members))) + map_chr(unname(perRow), .oneDataType) +} + +# What `data` records, keyed by (study, context) for a QtlDataset and by (study, +# context, trait) for a QtlSumStats, as .rowMemberKeys() reads a row. +# @noRd +.dataTypeTable <- function(data) { + if (methods::is(data, "MultiStudyQtlDataset")) { + return(list_rbind(c( + map(unname(as.list(data)), .dataTypeTable), + list(.dataTypeTable(data@sumStats)) + ))) + } + if (methods::is(data, "QtlDataset")) { + types <- dataType(data) + return( + tibble( + key = str_c(studyName(data), "\r", names(types)), + dataType = unname(types) + ) |> + filter(!is.na(.data$dataType)) + ) + } + if (methods::is(data, "QtlSumStats")) { + return( + tibble( + key = str_c(data$study, "\r", data$context, "\r", data$trait), + dataType = dataType(data) + ) |> + filter(!is.na(.data$dataType)) + ) + } + tibble(key = character(0), dataType = character(0)) +} + +# Each row's keys into .dataTypeTable(), over its joint members. +# @noRd +.rowMemberKeys <- function(md) { + pmap( + list( + .axisMembers(md, "study", "jointStudies"), + .axisMembers(md, "context", "jointContexts"), + .axisMembers(md, "trait", "jointTraits") + ), + .memberKeys + ) +} + +# @noRd +.memberKeys <- function(studies, contexts, traits) { + pairs <- as.character(outer(studies, contexts, str_c, sep = "\r")) + c(pairs, as.character(outer(pairs, traits, str_c, sep = "\r"))) +} + +# One axis's members per row: its value, or on a joint axis what its joint +# column lists. +# @noRd +.axisMembers <- function(md, axis, jointAxis) { + values <- as.character(md[[axis]]) + joint <- if (is_in(jointAxis, colnames(md))) { + as.character(md[[jointAxis]]) + } else { + rep(NA_character_, length(values)) + } + map2(values, joint, .oneAxisMembers) +} + +# @noRd +.oneAxisMembers <- function(value, joint) { + if (is.na(joint)) value else str_split_1(joint, fixed(";")) +} + +# @noRd +.oneDataType <- function(found) { + labels <- unique(found[!is.na(found)]) + if (length(labels) == 1L) labels else NA_character_ +} + # Internal: an optional per-row provenance column. One helper for all three # (`dataType`, `dataSource`, `standardized`) since they differ only in name and # coercion. diff --git a/R/twasWeights.R b/R/twasWeights.R index 912575fc3..8fb06bc71 100644 --- a/R/twasWeights.R +++ b/R/twasWeights.R @@ -549,7 +549,7 @@ TwasWeights <- function( variants = union, tensor = .twasTensor(placed, length(ids)), standardized = map_lgl(rowEntries, weightStandardized), - dataType = map(rowEntries, weightsDataType) + dataType = map(rowEntries, dataType) ) } @@ -1256,7 +1256,7 @@ TwasWeights <- function( methodFits = methodFits(entry), cvResult = cvResult(entry), weightStandardized = weightStandardized(entry), - weightsDataType = weightsDataType(entry) + weightsDataType = dataType(entry) ) } @@ -1614,10 +1614,10 @@ setMethod( } ) -#' @rdname weightsDataType +#' @rdname dataType #' @export setMethod( - "weightsDataType", + "dataType", "TwasWeights", function( x, @@ -1626,12 +1626,43 @@ setMethod( trait = NULL, method = NULL ) { - weightsDataType( - .twrSelectRowParts(x, studyName, context, trait, method) - ) + dataType(.twrSelectRowParts(x, studyName, context, trait, method)) } ) +# The assembled weights, each row taking the molecular type of the data it was +# fitted on (.rowDataTypes()). Applied to the assembled result, as a +# fine-mapping result's is (.fmRecordProvenance()), since joint rows are built +# on their own path. +# @noRd +.twasRecordDataTypes <- function(x, data) { + if (is.null(x)) { + return(x) + } + labels <- .rowDataTypes(x, data) + if (all(is.na(labels))) { + return(x) + } + `$<-`( + x, + "dataType", + S4Vectors::SimpleList(map2( + as.list(x$dataType), + labels, + .twasRowDataType + )) + ) +} + +# One row's per-method labels under one label, as they were if it has none. +# @noRd +.twasRowDataType <- function(row, label) { + if (is.na(label)) { + return(row) + } + set_names(rep(list(label), length(row)), names(row)) +} + #' @rdname variantIds #' @export setMethod( @@ -2961,8 +2992,8 @@ setMethod("show", "TwasWeights", function(object) { #' Every method keeps a fit, which names the variants it was fitted on. #' \code{"slim"} and \code{"full"} differ for mr.mash, glmnet #' (\code{lasso}, \code{enet}, \code{ridge}), ncvreg (\code{scad}, -#' \code{mcp}), \code{l0learn}, the Bayesian alphabet, \code{mrash} -#' and the DPR methods; for the others the two behave alike. +#' \code{mcp}), \code{l0learn}, the Bayesian alphabet and \code{mrash}; +#' for the others the two behave alike. #' @param seed Integer or \code{NULL}. When supplied, seeds both the #' main-process RNG (fold partitioning, variant sub-sampling) and the #' streams the folds draw from (see \code{BPPARAM}). The main-process seed @@ -3248,7 +3279,12 @@ twasWeightsCv <- function( } userArgs <- weightMethods[[methodName]] fnName <- .resolveMethodFunction(methodName, userArgs) - fitArgs <- .twasApplyRetainFit(userArgs, ctx$fitRetention) + fitArgs <- .twasApplyRetainFit( + userArgs, + ctx$fitRetention, + ctx$retainX, + fnName + ) fit <- .twasFitWeightsMatrix(fnName, fitArgs, ctx, methodName) embedded <- .embedWeights( fit$weights, @@ -3285,13 +3321,40 @@ twasWeightsCv <- function( ) # Pass the retention level down to the target weight function when the caller -# is keeping fits at all; every weight method takes `fitRetention`. +# is keeping fits at all; every weight method takes `fitRetention`. `retainX` +# goes only where it can apply: a method's own level may be lower than the +# run's, and most methods' fits hold no copy of X. # @noRd -.twasApplyRetainFit <- function(args, fitRetention) { +.twasApplyRetainFit <- function(args, fitRetention, retainX, fnName) { if (identical(fitRetention, "none")) { return(args) } - list_assign(args, fitRetention = args$fitRetention %||% fitRetention) + level <- args$fitRetention %||% fitRetention + keepingX <- retainX && identical(level, "full") && .twasKeepsX(fnName) + list_assign( + args, + fitRetention = level, + !!!compact(list(retainX = if (keepingX) TRUE)) + ) +} + +# Whether a weight function's fit can hold a copy of X, as its own formal says. +# @noRd +.twasKeepsX <- function(fnName) { + is_in("retainX", names(formals(get0(fnName, mode = "function")))) +} + +# `retainX = TRUE` with no method whose fit has a copy of X would keep nothing. +# @noRd +.twasAssertKeepsX <- function(weightMethods) { + fns <- map2_chr(names(weightMethods), weightMethods, .resolveMethodFunction) + if (!any(map_lgl(fns, .twasKeepsX))) { + abort(glue( + "learnTwasWeights: `retainX = TRUE` needs a method whose fit has ", + "a copy of X; the fits of {str_flatten(fns, ', ')} hold none." + )) + } + invisible(NULL) } # Dispatch weight fitting to the multivariate or per-column univariate path; @@ -3486,12 +3549,19 @@ twasWeightsCv <- function( #' produce the fits. #' @param fitRetention How much of each per-method fit is kept on the result: #' \code{"none"} (default) keeps none, \code{"slim"} keeps the trimmed -#' payload downstream needs, \code{"full"} keeps the whole fitted object. +#' payload downstream needs, \code{"full"} keeps the whole fitted object but +#' for any copy of \code{X}. #' Every method keeps a fit, which names the variants it was fitted on. #' \code{"slim"} and \code{"full"} differ for mr.mash, glmnet #' (\code{lasso}, \code{enet}, \code{ridge}), ncvreg (\code{scad}, -#' \code{mcp}), \code{l0learn}, the Bayesian alphabet, \code{mrash} -#' and the DPR methods; for the others the two behave alike. +#' \code{mcp}), \code{l0learn}, the Bayesian alphabet and \code{mrash}; +#' for the others the two behave alike. +#' @param retainX Logical. Keep a fit's copy of \code{X}, for the methods whose +#' fit has one (\code{lasso}, \code{enet}, \code{ridge}, \code{scad}, +#' \code{mcp}, \code{mrash} and the DPR methods), at +#' \code{fitRetention = "full"} (default \code{FALSE}). An error at any +#' other \code{fitRetention}, and when no method in \code{weightMethods} has +#' such a copy. #' @param verbose Integer controlling verbosity level: 0 = suppress all #' messages, 1 = suppress external package messages (default), 2 = show all #' messages including those from external packages. @@ -3503,8 +3573,8 @@ twasWeightsCv <- function( #' weights. #' @param weightStandardized Logical. Whether the supplied \code{X} / \code{Y} #' are already standardized. Default \code{FALSE}. -#' @param weightsDataType Character or \code{NULL}. Data-type label recorded on -#' the weights (e.g. \code{"individual"}). +#' @param weightsDataType Character or \code{NULL}. The molecular type recorded +#' on the weights (e.g. \code{"expression"}). #' @param ldReference An LD reference (a genotype panel, see #' \code{\link{readGenotypes}}, a \code{GenotypeMixture} or an #' \code{LdMatrix}) to record on the weights, or \code{NULL}. @@ -3544,17 +3614,17 @@ learnTwasWeights <- function( BPPARAM = SerialParam(), fittedModels = NULL, fitRetention = c("none", "slim", "full"), + retainX = FALSE, weightStandardized = FALSE, weightsDataType = NULL, ldReference = NULL, verbose = 1, seed = NULL ) { - assertString(studyName) - assertString(context) - assertString(trait) + .twasAssertRowLabels(studyName, context, trait) .assertBpparam(BPPARAM) fitRetention <- arg_match(fitRetention) + .assertRetainX(retainX, fitRetention) assertFlag(weightStandardized) assertString(weightsDataType, null.ok = TRUE) assertCount(verbose) @@ -3575,6 +3645,7 @@ learnTwasWeights <- function( BPPARAM = BPPARAM, fittedModels = fittedModels, fitRetention = fitRetention, + retainX = retainX, weightStandardized = weightStandardized, weightsDataType = weightsDataType, ldReference = ldReference, @@ -3583,6 +3654,14 @@ learnTwasWeights <- function( ) } +# The labels learnTwasWeights() gives its rows. +# @noRd +.twasAssertRowLabels <- function(studyName, context, trait) { + assertString(studyName) + assertString(context) + assertString(trait) +} + # Validate X/Y shapes; coerce a vector Y to a one-column matrix. Returns Y. # @noRd #' @importFrom checkmate assert assertMatrix checkAtomicVector checkMatrix @@ -3674,6 +3753,7 @@ learnTwasWeights <- function( BPPARAM, fittedModels, fitRetention, + retainX, weightStandardized, weightsDataType, ldReference, @@ -3686,6 +3766,9 @@ learnTwasWeights <- function( Y <- .twasValidateXY(X, Y) resolvedMethods <- .twasAsMethodRecord(weightMethods) .twasRequireLearnFits(resolvedMethods, fittedModels) + if (retainX) { + .twasAssertKeepsX(resolvedMethods) + } validColumns <- .nonzeroVarColumns(X) Xfiltered <- as.matrix(X[, validColumns, drop = FALSE]) prepared <- .prepareSusieWeightMethods(resolvedMethods, fittedModels) @@ -3698,6 +3781,7 @@ learnTwasWeights <- function( context = context, trait = trait, fitRetention = fitRetention, + retainX = retainX, standardized = weightStandardized, dataType = weightsDataType, verbose = verbose, diff --git a/R/twasWeightsPipeline.R b/R/twasWeightsPipeline.R index 6bada1568..38ebc82c8 100644 --- a/R/twasWeightsPipeline.R +++ b/R/twasWeightsPipeline.R @@ -139,8 +139,7 @@ combineTwasWeights <- function(..., ldReference = NULL) { weights = stacked, methodFits = set_names(map(payloads, methodFits), fittedLabels), cvResult = .twasRegionCvDf(payloads, fittedLabels), - weightStandardized = weightStandardized(payloads[[1L]]), - weightsDataType = weightsDataType(payloads[[1L]]) + weightStandardized = weightStandardized(payloads[[1L]]) ) } @@ -351,11 +350,12 @@ combineTwasWeights <- function(..., ldReference = NULL) { # the generic "unknown method token(s)". # @noRd .twasNormalizeCharMethods <- function(methods) { + expanded <- .twasExpandPresets(methods) fmExtra <- setdiff( - intersect(methods, .twasFineMappingTokens()), + intersect(expanded, .twasFineMappingTokens()), .twasKnownMethodLookupNames() ) - regular <- setdiff(methods, fmExtra) + regular <- setdiff(expanded, fmExtra) # Fine-mapping tokens carry no learner arguments of their own, so each # gets an empty stub entry under its `_weights` key. methodList <- c( @@ -364,7 +364,7 @@ combineTwasWeights <- function(..., ldReference = NULL) { ) # Tokens come from the user input (canonical camelCase) -- the snake keys in # methodList are an internal detail of learnTwasWeights. - list(tokens = unique(methods), methodList = methodList) + list(tokens = unique(expanded), methodList = methodList) } # One `methods` entry re-keyed to its canonical name, with the caller's @@ -1153,10 +1153,21 @@ combineTwasWeights <- function(..., ldReference = NULL) { #' embedded \code{QtlSumStats}, then rbinds the results. #' @param methods A character vector of short method names, a preset string #' (\code{"default"} or \code{"fastDefault"}), or a named list of -#' \code{_weights = args} entries. For QtlSumStats input the -#' default is \code{c("lasso", "prsCs", "dprGibbs")}, which there run -#' lassosum, PRS-CS and SDPR; the fine-mapping methods are not in it, and -#' are requested by name together with a \code{fineMappingResult}. +#' \code{_weights = args} entries. On individual-level input the +#' default is \code{"default"}, which is \code{"fastDefault"} (\code{susie}, +#' \code{susieInf}, \code{mrash}, \code{enet}, \code{lasso}, \code{mcp}, +#' \code{scad} and \code{l0learn}) with \code{bayesR} and \code{bayesC}. +#' \code{susie} and \code{susieInf} read their fits from a +#' \code{fineMappingResult}, so either preset skips them, with a warning, +#' where no \code{fineMappingResult} holds them; naming them outside a preset +#' without one is an error. For QtlSumStats input +#' the default, which either preset also names, is +#' \code{c("lasso", "prsCs", "dprGibbs")}, which there run lassosum, PRS-CS +#' and SDPR; the fine-mapping methods are not in it, and are requested by +#' name together with a \code{fineMappingResult}. A +#' multivariate method (\code{mrmash}, \code{mvsusie}, \code{fsusie}) fits +#' the selected contexts and traits together and any other method each +#' (context, trait) alone, so the two kinds can be requested at once. #' @param backend The engine a method with several runs on, as a named #' character vector keyed by method, e.g. #' \code{c(bayesR = "hibayes", bayesL = "bglr")}; \code{NULL} (the @@ -1287,14 +1298,22 @@ combineTwasWeights <- function(..., ldReference = NULL) { #' \code{ridge}, \code{scad}, \code{mcp}, \code{l0learn}, the Bayesian #' alphabet, \code{mrash} and the DPR methods, whose full fit is the #' engine's whole object where the slim one keeps the selected fit or the -#' posterior summaries, without any copy of the data. Every other method -#' keeps its fit whole or not at all, except the fine-mapping methods -#' (\code{susie}, \code{susieInf}, \code{susieAsh}, \code{mvsusie}, -#' \code{fsusie}), whose rows keep none: their weights come from the -#' \code{fineMappingResult}'s fit, which is where \code{ctwasPipeline()} -#' reads it. Requesting \code{"full"} for a run where no method can honour -#' it is an error, and a mixed run warns rather than dropping it in -#' silence. +#' posterior summaries, without any copy of \code{X} unless \code{retainX} +#' asks for it. Every other method keeps its fit whole or not at all, except +#' the fine-mapping methods (\code{susie}, \code{susieInf}, +#' \code{susieAsh}, \code{mvsusie}, \code{fsusie}), whose rows keep none: +#' their weights come from the \code{fineMappingResult}'s fit, which is where +#' \code{ctwasPipeline()} reads it. Requesting \code{"full"} for a run where +#' no method can honour it is an error, and a mixed run warns rather than +#' dropping it in silence. +#' @param retainX Logical. Keep a fit's copy of \code{X}, which can be its +#' largest part, at \code{fitRetention = "full"} (default \code{FALSE}). An +#' error at any other \code{fitRetention}, and on a run where no method's fit +#' has a copy: only \code{lasso}, \code{enet}, \code{ridge}, \code{scad}, +#' \code{mcp}, \code{mrash} and the DPR methods do. No fine-mapping fit holds +#' one, so \code{\link{fineMappingPipeline}} has no such argument, and summary +#' statistics have no \code{X}: on a \code{MultiStudyQtlDataset} it reaches +#' the individual-level studies. #' @param phenotypeFilterParam The run's phenotype QC, built with #' \code{\link{PhenotypeFilterParam}}: how missing phenotype values are #' handled (unset drops the affected samples; \code{"impute"} fills each @@ -1342,8 +1361,6 @@ combineTwasWeights <- function(..., ldReference = NULL) { #' so refusing a non-default value would reject the default bundle; CV is #' off by default, so a non-default value there is an explicit request for #' 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 @@ -1413,17 +1430,44 @@ setGeneric("twasWeightsPipeline", function(data, ...) { # `marker` and the same engine `args`. `ctx` is .twasJointSetup()'s record. # @noRd .twasRunJointSpecGrid <- function(spec, tokens, contexts, traits, marker, ctx) { - allLabs <- map_chr(ctx$xRegions, .twasRegionLabel) - allRegions <- map( - seq_along(ctx$xRegions), - .twasJointSpecRegion, - spec = spec, - tokens = tokens, - contexts = contexts, - traits = traits, - marker = marker, - ctx = ctx + .qtlRunWork( + .twasJointSpecGridWork(spec, tokens, contexts, traits, marker, ctx), + ctx$data, + ctx$genotypeFilter, + ctx$residualizationParam ) +} + +# .twasRunJointSpecGrid()'s fits, planned, for a run with others to fit +# alongside. +# @noRd +.twasJointSpecGridWork <- function( + spec, + tokens, + contexts, + traits, + marker, + ctx +) { + .qtlWorkOf( + map( + seq_along(ctx$xRegions), + .twasJointSpecRegionWork, + spec = spec, + tokens = tokens, + contexts = contexts, + traits = traits, + marker = marker, + ctx = ctx + ), + .twasJointGridResult, + list(allLabs = map_chr(ctx$xRegions, .twasRegionLabel)) + ) +} + +# The regions' joint results merged. +# @noRd +.twasJointGridResult <- function(allRegions, allLabs) { keep <- !map_lgl(allRegions, is.null) perRegion <- allRegions[keep] labs <- allLabs[keep] @@ -1438,109 +1482,101 @@ setGeneric("twasWeightsPipeline", function(data, ...) { # ---- QtlDataset pipeline worker + phase helpers ---------------------------- -# The multivariate half of .twasQdsRunGrid: one fit across all traits, -# against the shared grid context. +# Plan the resolved grid: the methods that fit its contexts and traits together +# on the multivariate engine, the others one tuple at a time on the univariate +# one, so that both kinds can be asked for at once, as on summary statistics. +# `grid` carries the axes (.twasQdsResolveGrid), `marker` the per-run fit +# settings and `settings` what the paths read. +# @noRd +.twasQdsGridWork <- function(grid, marker, norm, settings) { + isMv <- map_lgl(norm$tokens, .twasIsMultivariateToken) + if (!any(isMv)) { + return(exec( + .twasQdsUnivariateWork, + grid, + marker = marker, + norm = norm, + !!!settings + )) + } + if (all(isMv)) { + return(.twasQdsMultivariateWork(grid, marker, norm, settings)) + } + .qtlWorkOf( + list( + exec( + .twasQdsUnivariateWork, + grid, + marker = marker, + norm = .twasNormKeep(norm, norm$tokens[!isMv]), + !!!settings + ), + .twasQdsMultivariateWork( + grid, + marker, + .twasNormKeep(norm, norm$tokens[isMv]), + settings + ) + ), + .twasQdsGridResult + ) +} + +# The multivariate methods' fits over every (context, trait) of the grid. A +# per-context selection goes in whole: the joint scope then takes each study's +# union of it and drops the (context, trait) pairs it leaves out. # @noRd -.twasQdsRunMultivariate <- function( - grid, - marker, - data, - xRegions, - norm, - fineMappingResult, - dataDrivenPriorMatricesCv, - cisWindow, - residualizationParam, - genotypeFilter, - verbose -) { - # A per-context selection goes in whole: the joint scope then takes each - # study's union of it and drops the (context, trait) pairs it leaves out. - .twasRunJointSpecGrid( +.twasQdsMultivariateWork <- function(grid, marker, norm, settings) { + .twasJointSpecGridWork( list(list(axes = c("context", "trait"), scope = NULL)), norm$tokens, grid$useCtx, grid$traitSelection %||% grid$allTraits, marker, list( - xRegions = xRegions, - data = data, + xRegions = settings$xRegions, + data = settings$data, norm = norm, - fineMappingResult = fineMappingResult, - dataDrivenPriorMatricesCv = dataDrivenPriorMatricesCv, - cisWindow = cisWindow, - residualizationParam = residualizationParam, - genotypeFilter = genotypeFilter, - verbose = verbose + fineMappingResult = settings$fineMappingResult, + dataDrivenPriorMatricesCv = settings$dataDrivenPriorMatricesCv, + cisWindow = settings$cisWindow, + residualizationParam = settings$residualizationParam, + genotypeFilter = settings$genotypeFilter, + verbose = settings$verbose ) ) } -# Run the resolved grid: the multivariate engine when the grid says so, the -# per-tuple univariate engine otherwise. `grid` carries the axes -# (.twasQdsResolveGrid) and `marker` the per-run fit settings. +# The result of a grid's two paths, either of which may have fitted nothing. # @noRd -.twasQdsRunGrid <- function( - grid, - marker, - data, - xRegions, - norm, - fineMappingResult, - twasWeights, - dataDrivenPriorMatricesCv, - cisWindow, - phenotypeFilter, - residualizationParam, - genotypeFilter, - usePCA, - nPCs, - verbose -) { - if (grid$multivariate) { - return(.twasQdsRunMultivariate( - grid, - marker, - data = data, - xRegions = xRegions, - norm = norm, - fineMappingResult = fineMappingResult, - dataDrivenPriorMatricesCv = dataDrivenPriorMatricesCv, - cisWindow = cisWindow, - residualizationParam = residualizationParam, - genotypeFilter = genotypeFilter, - verbose = verbose - )) +.twasQdsGridResult <- function(results) { + fitted <- compact(results) + if (length(fitted) < 2L) { + return(pluck(fitted, 1L)) } - .twasQdsUnivariateEngine( - grid, - marker = marker, - data = data, - xRegions = xRegions, - norm = norm, - fineMappingResult = fineMappingResult, - twasWeights = twasWeights, - dataDrivenPriorMatricesCv = dataDrivenPriorMatricesCv, - cisWindow = cisWindow, - phenotypeFilter = phenotypeFilter, - residualizationParam = residualizationParam, - genotypeFilter = genotypeFilter, - usePCA = usePCA, - nPCs = nPCs, - verbose = verbose - ) + .rbindTwasWeights(fitted[[1L]], fitted[[2L]], ldReference = NULL) +} + +# `norm` narrowed to the methods among `keep`. +# @noRd +.twasNormKeep <- function(norm, keep) { + held <- is_in(.twasTokensFromMethodList(norm$methodList), keep) + list_assign(norm, tokens = keep, methodList = norm$methodList[held]) } + # The QtlDataset worker's argument checks, grouped so its entry point names # the validation phase once instead of listing four calls inline. # @noRd .twasQdsAssertParams <- function( methods, fitRetention, + retainX, crossValidationParam, ensembleParam ) { .twasWarnUnretainableDetail(fitRetention, methods, "QtlDataset") + .twasCheckRetainX(retainX, fitRetention, methods) .assertMethodParam( crossValidationParam, "CrossValidationParam" @@ -1578,8 +1614,8 @@ setGeneric("twasWeightsPipeline", function(data, ...) { usePCA = FALSE, nPCs = 10L, fitRetention = c("slim", "none", "full"), + retainX = FALSE, residualizationParam = ResidualizationParam(), - weightsDataType = NULL, phenotypeFilterParam = PhenotypeFilterParam(), verbose = 1, seed = NULL @@ -1596,6 +1632,7 @@ setGeneric("twasWeightsPipeline", function(data, ...) { .twasQdsAssertParams( methods, fitRetention, + retainX, crossValidationParam, ensembleParam ) @@ -1620,8 +1657,8 @@ setGeneric("twasWeightsPipeline", function(data, ...) { ensembleParam = ensembleParam, estimatePi = estimatePi, fitRetention = fitRetention, + retainX = retainX, residualizationParam = residualizationParam, - weightsDataType = weightsDataType, verbose = verbose, seed = seed ) @@ -1642,7 +1679,18 @@ setGeneric("twasWeightsPipeline", function(data, ...) { traitId ) if (joint$done) { - return(.withGenome(joint$result, build)) + return(.withGenome( + .twasRecordDataTypes( + .twasQdsJointOnly( + joint$work, + data, + resolved$genotypeFilter, + residualizationParam + ), + data + ), + build + )) } grid <- .twasQdsResolveGrid( data, @@ -1651,24 +1699,44 @@ setGeneric("twasWeightsPipeline", function(data, ...) { region, joint$norm$tokens ) - tw <- .twasQdsRunGrid( + gridWork <- .twasQdsGridWork( grid, setup$marker, - data = data, - xRegions = resolved$xRegions, - norm = joint$norm, - fineMappingResult = fineMappingResult, - twasWeights = twasWeights, - dataDrivenPriorMatricesCv = setup$ctx$dataDrivenPriorMatricesCv, - cisWindow = cisWindow, - phenotypeFilter = phenotypeFilter, - residualizationParam = residualizationParam, - genotypeFilter = resolved$genotypeFilter, - usePCA = usePCA, - nPCs = nPCs, - verbose = verbose + joint$norm, + list( + data = data, + xRegions = resolved$xRegions, + fineMappingResult = fineMappingResult, + twasWeights = twasWeights, + dataDrivenPriorMatricesCv = setup$ctx$dataDrivenPriorMatricesCv, + cisWindow = cisWindow, + phenotypeFilter = phenotypeFilter, + residualizationParam = residualizationParam, + genotypeFilter = resolved$genotypeFilter, + usePCA = usePCA, + nPCs = nPCs, + verbose = verbose + ) + ) + # Every path planned, so one read of a window serves them all. + .withGenome( + .twasRecordDataTypes( + .qtlRunWork( + .qtlWorkOf(list(gridWork, joint$work), .twasQdsAssembleResults), + data, + resolved$genotypeFilter, + residualizationParam + ), + data + ), + build ) - .withGenome(.twasQdsAssemble(tw, joint$result), build) +} + +# The result of a run's paths: the grid's fits, then the joint specs'. +# @noRd +.twasQdsAssembleResults <- function(results) { + .twasQdsAssemble(results[[1L]], results[[2L]]) } #' @rdname twasWeightsPipeline @@ -1751,10 +1819,11 @@ setMethod( } -# Explicit jointSpecification path: run the per-spec axis dispatcher for -# mr.mash. Returns list(done, result, norm): `done` requests an early return -# with `result`; otherwise `norm` is the mrmash-stripped normalization for the -# per-tuple loop below. +# Explicit jointSpecification path: plan the per-spec axis dispatcher for +# mr.mash. Returns list(done, work, norm): `done` means its fits are the whole +# answer; otherwise `norm` is the mrmash-stripped normalization for the +# per-tuple loop below. Planned, not run: they share their reads with the +# per-tuple fits. # @noRd .twasQdsJointPhase <- function( parsedJointSpec, @@ -1765,9 +1834,9 @@ setMethod( ) { norm <- ctx$norm if (length(parsedJointSpec) == 0L) { - return(list(done = FALSE, result = NULL, norm = norm)) + return(list(done = FALSE, work = .qtlWorkValue(NULL), norm = norm)) } - jointResult <- .twasDispatchJointSpecsQtlDataset( + work <- .twasJointSpecsWork( parsedJointSpec, norm$tokens, marker, @@ -1777,28 +1846,9 @@ setMethod( ) keep <- setdiff(norm$tokens, intersect(norm$tokens, "mrmash")) if (length(keep) == 0L) { - if (is.null(jointResult)) { - msg <- glue( - "twasWeightsPipeline(QtlDataset): no joint fits produced. ", - "Check that the jointSpecification scope intersects the ", - "available studies / contexts / traits." - ) - abort(msg) - } - return(list(done = TRUE, result = jointResult)) + return(list(done = TRUE, work = work)) } - keepKeys <- which( - is_in(str_remove(names(norm$methodList), "(_weights|Weights)$"), keep) - ) - list( - done = FALSE, - result = jointResult, - norm = list_assign( - norm, - tokens = keep, - methodList = norm$methodList[keepKeys] - ) - ) + list(done = FALSE, work = work, norm = .twasNormKeep(norm, keep)) } # Parse the joint spec and normalize the methods, then run the capability, @@ -1815,7 +1865,7 @@ setMethod( data ) { parsedJointSpec <- parseJointSpecification(jointSpecification, data) - norm <- .twasNormalizeMethods(methods) + norm <- .twasNormalizeMethods(.twasRunMethods(methods, fineMappingResult)) .twasCheckMethodCapabilities(norm$tokens, "QtlDataset") .twasCheckMethodArgsForInput( .twasMethodListArgs(norm$methodList), @@ -1929,8 +1979,8 @@ setMethod( ensembleParam, estimatePi, fitRetention, + retainX, residualizationParam, - weightsDataType, verbose, seed ) { @@ -1947,8 +1997,8 @@ setMethod( ensembleParam = ensembleParam, estimatePi = estimatePi, fitRetention = fitRetention, + retainX = retainX, residualizationParam = residualizationParam, - weightsDataType = weightsDataType, verbose = verbose, seed = seed ) @@ -1968,9 +2018,9 @@ setMethod( samplePartition = mash$samplePartition ), ensembleParam = .ensembleResolve(cfg$ensembleParam), - weightsDataType = cfg$weightsDataType, standardized = .qtlScaleResiduals(cfg$residualizationParam), fitRetention = cfg$fitRetention, + retainX = cfg$retainX, estimatePi = cfg$estimatePi, verbose = cfg$verbose, seed = cfg$seed @@ -1992,8 +2042,8 @@ setMethod( ) } -# Resolve the (context, trait) grid + multivariate flag and build the joint- -# pipeline marker + shared grid context. Returns the updated parameter bundle. +# Resolve the (context, trait) grid and build the joint-pipeline marker + shared +# grid context. Returns the updated parameter bundle. # @noRd .twasQdsResolveGrid <- function( data, @@ -2013,8 +2063,7 @@ setMethod( # Set only when traits are chosen context by context: the grid spans # every (context, trait) pair of allTraits, and this drops the pairs # the selection leaves out. - traitSelection = if (.hasNestedTraitSelection(traitId)) traitId, - multivariate = any(map_lgl(tokens, .twasIsMultivariateToken)) + traitSelection = if (.hasNestedTraitSelection(traitId)) traitId ) } @@ -2047,9 +2096,9 @@ setMethod( .twasQdsMarker <- function( crossValidationParam, ensembleParam, - weightsDataType, standardized, fitRetention, + retainX, estimatePi, verbose, seed @@ -2062,8 +2111,8 @@ setMethod( # into its signature would name each one twice for nothing. crossValidationParam = crossValidationParam, ensembleParam = ensembleParam, - dataType = weightsDataType, fitRetention = fitRetention, + retainX = retainX, # DERIVED from what the accessors actually do: genotypes reach # the fitter scaled to unit variance unless # `ResidualizationParam(scaleResiduals = FALSE)` says otherwise. @@ -2109,7 +2158,7 @@ setMethod( # for >= 2 methods + resume cache) as the joint paths, merged across regions. # `grid` is the dispatch row naming the study, contexts and traits. # @noRd -.twasQdsUnivariateEngine <- function( +.twasQdsUnivariateWork <- function( grid, marker, data, @@ -2133,28 +2182,33 @@ setMethod( grid$allTraits, grid$traitSelection ) - cells <- plan$cells - scope <- plan$scope - labs <- map_chr(xRegions, .twasRegionLabel) - perRegion <- map( - seq_along(xRegions), - .twasQdsUnivRegion, - cells = cells, - scope = scope, - marker = marker, - data = data, - xRegions = xRegions, - norm = norm, - fineMappingResult = fineMappingResult, - twasWeights = twasWeights, - dataDrivenPriorMatricesCv = dataDrivenPriorMatricesCv, - cisWindow = cisWindow, - phenotypeFilter = phenotypeFilter, - residualizationParam = residualizationParam, - genotypeFilter = genotypeFilter, - nPCs = nPCs, - verbose = verbose + .qtlWorkOf( + map( + seq_along(xRegions), + .twasQdsUnivRegionWork, + cells = plan$cells, + scope = plan$scope, + marker = marker, + data = data, + xRegions = xRegions, + norm = norm, + fineMappingResult = fineMappingResult, + twasWeights = twasWeights, + dataDrivenPriorMatricesCv = dataDrivenPriorMatricesCv, + cisWindow = cisWindow, + phenotypeFilter = phenotypeFilter, + residualizationParam = residualizationParam, + genotypeFilter = genotypeFilter, + nPCs = nPCs, + verbose = verbose + ), + .twasUnivRegionsResult, + list(labs = map_chr(xRegions, .twasRegionLabel)) ) +} + +# @noRd +.twasUnivRegionsResult <- function(perRegion, labs) { keep <- !map_lgl(perRegion, is.null) .twasMergeRegionResults(perRegion[keep], labs[keep]) } @@ -2239,13 +2293,13 @@ setMethod( fineMappingResult = NULL, twasWeights = NULL, fitRetention = c("slim", "none", "full"), - weightsDataType = NULL, verbose = 1L, panelFilterParam = PanelFilterParam(), crossValidationParam = CrossValidationParam() ) { fitRetention <- arg_match(fitRetention) - .twasWarnUnretainableDetail(fitRetention, methods, "QtlSumStats") + chosen <- .twasSumStatsPreset(methods) + .twasWarnUnretainableDetail(fitRetention, chosen, "QtlSumStats") .cvRefuseOnSumstats( crossValidationParam, "twasWeightsPipeline", @@ -2258,7 +2312,7 @@ setMethod( .fitCoverageCheckedRows(fineMappingResult, "twasWeightsPipeline()") res <- .twasPipelineQtlSumStats( data = data, - methods = methods, + methods = chosen, backend = backend, contexts = contexts, traitId = traitId, @@ -2267,11 +2321,10 @@ setMethod( fineMappingResult = fineMappingResult, twasWeights = twasWeights, fitRetention = fitRetention, - weightsDataType = weightsDataType, verbose = verbose, panelFilterParam = panelFilterParam ) - .withGenome(res, build) + .withGenome(.twasRecordDataTypes(res, data), build) } ) @@ -2296,7 +2349,6 @@ setMethod( univariateTokens = part$univariateTokens, data = cfg$data, twasWeights = cfg$twasWeights, - weightsDataType = cfg$dataType, fitRetention = cfg$fitRetention, fineMappingResult = cfg$fineMappingResult, panelFilterParam = cfg$panelFilterParam @@ -2311,7 +2363,6 @@ setMethod( multivariateTokens = part$multivariateTokens, twasWeights = cfg$twasWeights, data = cfg$data, - weightsDataType = cfg$dataType, fitRetention = cfg$fitRetention, fineMappingResult = cfg$fineMappingResult, panelFilterParam = cfg$panelFilterParam @@ -2339,7 +2390,6 @@ setMethod( fineMappingResult, twasWeights, fitRetention, - weightsDataType, verbose, panelFilterParam ) { @@ -2351,7 +2401,6 @@ setMethod( backend = backend, contexts = inputs$contexts, traitId = inputs$traitId, - weightsDataType = weightsDataType, verbose = verbose, fitRetention = fitRetention, panelFilterParam = panelFilterParam @@ -2371,7 +2420,6 @@ setMethod( contexts = inputs$contexts, traitId = inputs$traitId, twasWeights = twasWeights, - weightsDataType = weightsDataType, fineMappingResult = fineMappingResult, fitRetention = fitRetention, panelFilterParam = panelFilterParam @@ -2388,7 +2436,6 @@ setMethod( contexts, traitId, twasWeights, - weightsDataType, fineMappingResult, fitRetention, panelFilterParam @@ -2405,7 +2452,6 @@ setMethod( list( data = data, twasWeights = twasWeights, - dataType = weightsDataType, fineMappingResult = fineMappingResult, fitRetention = fitRetention, panelFilterParam = panelFilterParam @@ -2420,7 +2466,7 @@ setMethod( # @noRd .twasSumStatsMethodTokens <- function(methods) { if (is.null(methods)) { - tokens <- c("lasso", "prsCs", "dprGibbs") + tokens <- .twasSumStatsDefaults() return(list(tokens = tokens, methodArgs = .twasEmptyMethodArgs(tokens))) } if (is.character(methods)) { @@ -2449,6 +2495,80 @@ setMethod( abort(msg) } +# The methods summary statistics run by default. +# @noRd +.twasSumStatsDefaults <- function() { + c("lasso", "prsCs", "dprGibbs") +} + +# Whether `methods` is a preset string (.twasExpandPresets()). +# @noRd +.twasIsPreset <- function(methods) { + is.character(methods) && + length(methods) == 1L && + is_in(methods, c("default", "fastDefault")) +} + +# The methods an individual-level run fits: those it names, a preset (no +# `methods` being the "default" one) for what it can run. +# @noRd +.twasRunMethods <- function(methods, fineMappingResult) { + chosen <- methods %||% "default" + if (.twasIsPreset(chosen)) { + return(.twasPresetMethods(chosen, fineMappingResult)) + } + chosen +} + +# A preset's methods. Its fine-mapping methods read their fits from a +# fine-mapping result, so where none holds them they are skipped, which asking +# for them by name is not: that stops (.twasRequireFmResult()). +# @noRd +.twasPresetMethods <- function(preset, fineMappingResult) { + expanded <- .twasExpandPresets(preset) + skipped <- setdiff( + intersect(expanded, .twasFineMappingTokens()), + .twasFineMappingMethodsPresent(fineMappingResult) + ) + if (length(skipped) > 0L) { + warn(c( + glue( + "twasWeightsPipeline: the \"{preset}\" preset is skipping ", + "{str_flatten(skipped, ', ')}, because no ", + "`fineMappingResult` holds their fits." + ), + i = str_c( + "To include them, run fineMappingPipeline() with these ", + "methods and pass its result as `fineMappingResult`." + ) + )) + } + setdiff(expanded, skipped) +} + +# Summary statistics have one list of defaults, the one +# .twasSumStatsMethodTokens() takes NULL for, so a preset names that list. +# @noRd +.twasSumStatsPreset <- function(methods) { + if (.twasIsPreset(methods)) NULL else methods +} + +# A preset over a collection: each kind of component runs its own list. +# @noRd +.twasMsPreset <- function(methods, hasSumStats, fineMappingResult) { + if (!.twasIsPreset(methods)) { + return(methods) + } + forDatasets <- .twasPresetMethods(methods, fineMappingResult) + if (!hasSumStats) { + return(forDatasets) + } + TwasWeightsMethodsParam( + qtlDatasetMethods = forDatasets, + qtlSumStatsMethods = .twasSumStatsDefaults() + ) +} + # Empty-args map keyed by method token. # @noRd .twasEmptyMethodArgs <- function(tokens) { @@ -2757,7 +2877,6 @@ setMethod( traitCol, data, twasWeights, - weightsDataType, fitRetention, fineMappingResult, methodArgs, @@ -2775,7 +2894,6 @@ setMethod( traitCol = traitCol, data = data, twasWeights = twasWeights, - weightsDataType = weightsDataType, fitRetention = fitRetention, fineMappingResult = fineMappingResult, methodArgs = methodArgs, @@ -2817,7 +2935,6 @@ setMethod( traitCol, data, twasWeights, - weightsDataType, fitRetention, fineMappingResult, methodArgs, @@ -2850,8 +2967,7 @@ setMethod( fitCtx = fitCtx, methodArgs = methodArgs, fitRetention = fitRetention, - fineMappingResult = fineMappingResult, - weightsDataType = weightsDataType + fineMappingResult = fineMappingResult )) c(cachedRows, .recordsWithLdRef(fitted, ldReference)) } @@ -3013,8 +3129,7 @@ setMethod( fitCtx, methodArgs, fitRetention, - fineMappingResult, - weightsDataType + fineMappingResult ) { spec <- .twasResolveWeightFn(tk) userArgs <- .twasQssUnivariateArgs( @@ -3042,7 +3157,7 @@ setMethod( if (is.null(weights)) { return(NULL) } - .twasQssRowFromWeights(weights, st, ctx, tr, tk, fitCtx, weightsDataType) + .twasQssRowFromWeights(weights, st, ctx, tr, tk, fitCtx) } # The weights come back with the fit hung off them as an attribute; the row @@ -3054,8 +3169,7 @@ setMethod( ctx, tr, tk, - fitCtx, - weightsDataType + fitCtx ) { .twasRowRecord( st, @@ -3067,8 +3181,7 @@ setMethod( weights = as.numeric(`attr<-`(weights, "fit", NULL)), methodFits = attr(weights, "fit"), cvResult = NULL, - weightStandardized = TRUE, - weightsDataType = weightsDataType + weightStandardized = TRUE ) ) } @@ -3105,7 +3218,6 @@ setMethod( methodArgs, fitRetention, fineMappingResult, - weightsDataType, panelFilterParam, twasWeights = NULL ) { @@ -3130,7 +3242,6 @@ setMethod( methodArgs = methodArgs, fitRetention = fitRetention, fineMappingResult = fineMappingResult, - weightsDataType = weightsDataType, panelFilterParam = panelFilterParam )) } @@ -3147,7 +3258,6 @@ setMethod( methodArgs, fitRetention, fineMappingResult, - weightsDataType, panelFilterParam, twasWeights = NULL ) { @@ -3173,8 +3283,7 @@ setMethod( blockReference = grp$blockReference, methodArgs = methodArgs, fitRetention = fitRetention, - fineMappingResult = fineMappingResult, - weightsDataType = weightsDataType + fineMappingResult = fineMappingResult )) .recordsWithLdRef(rows, grp$ldReference) } @@ -3371,7 +3480,6 @@ setMethod( methodArgs, fitRetention, fineMappingResult, - weightsDataType, twasWeights = NULL ) { cached <- .twasMvCacheHits(twasWeights, st, tr, ctxNames, tk) @@ -3405,8 +3513,7 @@ setMethod( mvStat, st, tr, - tk, - weightsDataType + tk ) } @@ -3420,8 +3527,7 @@ setMethod( mvStat, st, tr, - tk, - weightsDataType + tk ) { if (is.null(weights)) { return(list()) @@ -3436,8 +3542,7 @@ setMethod( mvStat, st, tr, - tk, - weightsDataType + tk ) } @@ -3498,8 +3603,7 @@ setMethod( mvStat, st, tr, - tk, - weightsDataType + tk ) { map( seq_along(ctxNames), @@ -3510,8 +3614,7 @@ setMethod( mvStat = mvStat, st = st, tr = tr, - tk = tk, - weightsDataType = weightsDataType + tk = tk ) } @@ -3562,11 +3665,13 @@ setMethod( return(NULL) } s <- studyName(qd) + specs <- .msStudySpecs(cfg$studySpecs, qd) .muffleFitCoverage(twasWeightsPipeline( data = qd, - jointSpecification = NULL, + jointSpecification = if (length(specs) > 0L) specs, fitRetention = cfg$fitRetention, - methods = cfg$methods, + retainX = cfg$retainX, + methods = if (length(specs) > 0L) cfg$studyMethods else cfg$methods, backend = cfg$backend, contexts = .selectedContexts(cfg$contexts, s), traitId = .studyTraitIdArg(cfg$traitId, s), @@ -3585,7 +3690,6 @@ setMethod( estimatePi = cfg$estimatePi, usePCA = cfg$usePCA, nPCs = cfg$nPCs, - weightsDataType = cfg$weightsDataType, residualizationParam = cfg$residualizationParam )) } @@ -3618,8 +3722,7 @@ setMethod( fineMappingResult = cfg$fineMappingResult, twasWeights = cfg$twasWeights, verbose = cfg$verbose, - panelFilterParam = cfg$panelFilterParam, - weightsDataType = cfg$weightsDataType + panelFilterParam = cfg$panelFilterParam )) } @@ -3657,13 +3760,43 @@ setMethod( invisible(NULL) } +# `retainX` is rejected unless the fit is "full", and when no requested method's +# fit has a copy of X to keep, as a "full" no method can honour is. A mixed run +# says nothing: a method with no copy loses nothing. +# @noRd +.twasCheckRetainX <- function(retainX, fitRetention, methods) { + .assertRetainX(retainX, fitRetention) + if (!retainX) { + return(invisible(NULL)) + } + tokens <- .twasMethodTokensFromArg(methods) + holding <- .twasTokensKeepingX() + if (length(tokens) > 0L && !any(is_in(tokens, holding))) { + abort(glue( + "twasWeightsPipeline: `retainX = TRUE` applies only to ", + "{str_flatten(holding, ', ')}; the fits of ", + "{str_flatten(tokens, ', ')} hold no copy of X." + )) + } + invisible(NULL) +} + +# The methods whose fit can hold a copy of X, by their weight functions' own +# formals. +# @noRd +.twasTokensKeepingX <- function() { + fns <- compact(map(.twasMethodCapabilities, "individualImpl")) + names(fns)[map_lgl(fns, .twasKeepsX)] +} + # The methods whose "full" fit keeps more than their "slim" one, on one input # path. On summary statistics: mr.mash's whole fit and the shrinkage-grid # methods' candidate coefficients (lassosumRssWeights() and the penalized # RSS weights). On individual-level data: the engines' whole fitted objects, # where "slim" keeps the selected fit or the posterior summaries (glmnet, -# ncvreg, L0Learn, the Bayesian alphabet), or drops a copy of the data -# (mr.ash, DPR); and mr.mash's whole fit. +# ncvreg, L0Learn, the Bayesian alphabet), or drops more of the data (mr.ash); +# and mr.mash's whole fit. DPR holds nothing to drop but its copy of X, which +# goes at either level, so "full" loses it nothing. # @noRd .twasFullDetailTokens <- function(inputKind) { if (identical(inputKind, "QtlSumStats")) { @@ -3700,7 +3833,7 @@ setMethod( return(.methodsParamTokens(methods)) } if (is.character(methods)) { - methods + .twasExpandPresets(methods) } else if (is.list(methods)) { str_remove(names(methods), "(_weights|Weights)$") } else { @@ -3768,7 +3901,7 @@ setMethod( usePCA = FALSE, nPCs = 10L, fitRetention = c("slim", "none", "full"), - weightsDataType = NULL, + retainX = FALSE, phenotypeFilterParam = PhenotypeFilterParam(), verbose = 1, residualizationParam = ResidualizationParam(), @@ -3780,6 +3913,7 @@ setMethod( # Every collection holds individual-level studies, so their rule applies # here; a summary-statistic study is checked again by its own route. .twasWarnUnretainableDetail(fitRetention, methods, "QtlDataset") + .twasCheckRetainX(retainX, fitRetention, methods) # Resolved once here so a bad choice fails before any study runs; the # record carries the bundle itself, as each component resolves its own. .twasResolvePhenotypeFilter(phenotypeFilterParam) @@ -3817,7 +3951,7 @@ setMethod( usePCA = usePCA, nPCs = nPCs, fitRetention = fitRetention, - weightsDataType = weightsDataType, + retainX = retainX, residualizationParam = residualizationParam ) .msAssertTraitsSelected(data, cfg, "twasWeightsPipeline") @@ -3829,13 +3963,21 @@ setMethod( jointCfg ) if (joint$done) { - return(.withGenome(joint$result, build)) + return(.withGenome(.twasRecordDataTypes(joint$result, data), build)) } .withGenome( - .twasMsDriver( - data, - joint$result, - list_assign(cfg, methods = joint$methods) + .twasRecordDataTypes( + .twasMsDriver( + data, + joint$result, + list_assign( + cfg, + methods = joint$methods, + studySpecs = joint$studySpecs, + studyMethods = joint$studyMethods + ) + ), + data ), build ) @@ -3866,8 +4008,8 @@ setMethod( ensembleParam = cfg$ensembleParam, estimatePi = cfg$estimatePi, fitRetention = cfg$fitRetention, + retainX = cfg$retainX, residualizationParam = cfg$residualizationParam, - weightsDataType = cfg$weightsDataType, verbose = cfg$verbose, seed = cfg$seed ), @@ -3900,13 +4042,18 @@ setMethod( ) return(list(done = FALSE, result = NULL, methods = methods)) } + # A component with per-tuple fits to come fits its joint specs along with + # them, on one read of each window. + stripped <- .twasMsStripMrmash(methods) + tokens <- .twasMethodTokensFromArg(methods) + withComponents <- !.twasMethodsEmpty(stripped) && is_in("mrmash", tokens) jointResult <- .twasDispatchJointSpecsMultiStudy( parsedJointSpec, - .twasMethodTokensFromArg(methods), + tokens, methods, - cfg + cfg, + components = !withComponents ) - stripped <- .twasMsStripMrmash(methods) if (.twasMethodsEmpty(stripped)) { if (is.null(jointResult)) { msg <- glue( @@ -3923,7 +4070,15 @@ setMethod( .twasMethodTokensFromArg(stripped), cfg$data ) - list(done = FALSE, result = jointResult, methods = stripped) + list( + done = FALSE, + result = jointResult, + methods = stripped, + studySpecs = if (withComponents) { + .fmSplitStudyAxisSpecs(parsedJointSpec)$nonStudy + }, + studyMethods = methods + ) } # Gate fine-mapping methods early so the recursion into the embedded @@ -3940,11 +4095,12 @@ setMethod( # Translated here, where the dataset is in hand, so the Param is what # travels into the per-component recursion; each component reads the # slot for its own input class. + hasSumStats <- !is.null(sumStats(cfg$data)) methods <- .methodsParamForMulti( - methods, + .twasMsPreset(methods, hasSumStats, fineMappingResult), "TwasWeightsMethodsParam", "twasWeightsPipeline", - !is.null(sumStats(cfg$data)) + hasSumStats ) .twasCheckFineMappingMethods( .twasMethodTokensFromArg(methods), @@ -3968,7 +4124,6 @@ setMethod( .twasSumStats, cfg, .rbindTwasWeights, - TwasWeights, "twasWeightsPipeline", noun = "weights" ) @@ -4820,9 +4975,9 @@ ensembleWeights <- function( .twasCapabilityViolation(caps[[tk]], tk, inputKind) } -# One region block of .twasRunJointSpecGrid(). +# One region block of .twasJointSpecGridWork(). # @noRd -.twasJointSpecRegion <- function( +.twasJointSpecRegionWork <- function( bi, spec, tokens, @@ -4831,7 +4986,7 @@ ensembleWeights <- function( marker, ctx ) { - .runJointSpecs( + .jointSpecsWork( spec, ctx$data, dataForm = "individual", @@ -4863,9 +5018,9 @@ ensembleWeights <- function( ) } -# One region's univariate joint-cell fit (region `bi` of `xRegions`). +# One region's univariate joint-cell fits (region `bi` of `xRegions`). # @noRd -.twasQdsUnivRegion <- function( +.twasQdsUnivRegionWork <- function( bi, cells, scope, @@ -4899,16 +5054,18 @@ ensembleWeights <- function( nPCs = nPCs, verbose = verbose ) - out <- compact(map( - cells, - .runJointCell, - pipeline = marker, - data = data, - scope = scope, - tokens = norm$tokens, - args = args - )) - .twasCombineCellRows(out) + .qtlWorkOf( + map( + cells, + .jointCellWork, + pipeline = marker, + data = data, + scope = scope, + tokens = norm$tokens, + args = args + ), + .twasCombineCellRows + ) } # Combine the per-cell results of one region. rbind, NOT @@ -4916,7 +5073,8 @@ ensembleWeights <- function( # keeping results[[1]]'s row set. The top-PC rows are different rows, so # merging would silently drop them. # @noRd -.twasCombineCellRows <- function(out) { +.twasCombineCellRows <- function(perCell) { + out <- compact(perCell) if (length(out) == 0L) { return(NULL) } @@ -4926,6 +5084,31 @@ ensembleWeights <- function( reduce(out, .rbindTwasWeights) } +# A run whose joint fits are the whole answer. +# @noRd +.twasQdsJointOnly <- function( + work, + data, + genotypeFilter, + residualizationParam +) { + jointResult <- .qtlRunWork( + work, + data, + genotypeFilter, + residualizationParam + ) + if (is.null(jointResult)) { + msg <- glue( + "twasWeightsPipeline(QtlDataset): no joint fits produced. ", + "Check that the jointSpecification scope intersects the ", + "available studies / contexts / traits." + ) + abort(msg) + } + jointResult +} + # One cached row record from an imap over (entry, token) cache hits. # @noRd .twasCachedRowRecord <- function(entry, tk, st, ctx, tr) { @@ -4964,8 +5147,7 @@ ensembleWeights <- function( mvStat, st, tr, - tk, - weightsDataType + tk ) { .twasRowRecord( st, @@ -4977,8 +5159,7 @@ ensembleWeights <- function( weights = as.numeric(weights[, kk]), methodFits = fitAttr, cvResult = NULL, - weightStandardized = TRUE, - weightsDataType = weightsDataType + weightStandardized = TRUE ) ) } diff --git a/R/twasWeightsRow.R b/R/twasWeightsRow.R index 58f37563d..e5bd4c235 100644 --- a/R/twasWeightsRow.R +++ b/R/twasWeightsRow.R @@ -18,7 +18,7 @@ NULL #' @slot methodFits Per-method fit payload, or \code{NULL}. #' @slot cvResult Cross-validation payload, or \code{NULL}. #' @slot weightStandardized Whether the weights are on the standardized scale. -#' @slot weightsDataType Optional data-type label. +#' @slot weightsDataType Optional molecular type. #' @section Coercion: \code{as(x, "GRanges")} gives the row's variants with #' their \code{weight} column: the element a collection stores for it. #' @seealso \code{\link{TwasWeightsRow}}, @@ -80,7 +80,8 @@ methods::setValidity("TwasWeightsRow", function(object) { #' @param cvResult Optional cross-validation payload. #' @param weightStandardized Whether the weights are already on the standardized #' scale. -#' @param weightsDataType Optional data-type label. +#' @param weightsDataType Optional molecular type (\code{"expression"}, +#' \code{"splicing"}). #' @return A \code{\linkS4class{TwasWeightsRow}}. #' @seealso \code{\link{FineMappingRow}} #' @examples @@ -278,9 +279,9 @@ setMethod("weightStandardized", "TwasWeightsRow", function(x) { isTRUE(x@weightStandardized) }) -#' @rdname weightsDataType +#' @rdname dataType #' @export -setMethod("weightsDataType", "TwasWeightsRow", function(x) x@weightsDataType) +setMethod("dataType", "TwasWeightsRow", function(x) x@weightsDataType) # @noRd setMethod("variants", "TwasWeightsRow", function(x) x@variants) diff --git a/R/variantId.R b/R/variantId.R index fe46cf96f..d01bbd50d 100644 --- a/R/variantId.R +++ b/R/variantId.R @@ -222,20 +222,28 @@ stripBuildSuffix <- function(x) { #' Test whether allele pairs are single-nucleotide (SNP, not indel). #' #' Returns TRUE for each pair where both alleles are exactly one of A, T, C, G. +#' This is the package's one definition: every \code{keepIndel} and +#' \code{removeIndels} treats anything else as an indel, including \code{I}/ +#' \code{D} codes, \code{*} and \code{N}. #' @param a1 Character vector of first alleles. #' @param a2 Character vector of second alleles. #' @return Logical vector, TRUE if the variant is a SNP. #' @noRd isSnpAlleles <- function(a1, a2) { - isSnp <- str_length(a1) == 1L & - str_length(a2) == 1L & - str_detect(a1, "^[ATCG]$") & - str_detect(a2, "^[ATCG]$") - # str_length(NA)/str_detect(NA) yield NA where base nchar/grepl gave FALSE; - # preserve the base "NA allele is not a SNP" behavior. + isSnp <- str_detect(a1, "^[ATCG]$") & str_detect(a2, "^[ATCG]$") + # str_detect(NA) yields NA where base grepl gave FALSE; preserve the base + # "NA allele is not a SNP" behavior. replace_na(isSnp, FALSE) } +# Whether each allele is written in DNA bases, in either case: not a tag where +# an allele belongs, and not missing. +# @noRd +.isDnaAllele <- function(alleles) { + isDna <- str_detect(alleles, regex("^[ACGT]+$", ignore_case = TRUE)) + replace_na(isDna, FALSE) +} + #' Re-render variant ids whose allele slot holds a tag instead of an allele. #' #' Some LD panels (e.g. ADSP R5) name a variant with a tag where an allele @@ -269,10 +277,9 @@ isSnpAlleles <- function(a1, a2) { return(ids) } p <- parseVariantId(ids) - dna <- regex("^[ACGT]+$", ignore_case = TRUE) tagged <- !is.na(p$A1) & !is.na(p$A2) & - !replace_na(str_detect(p$A1, dna) & str_detect(p$A2, dna), FALSE) + !(.isDnaAllele(p$A1) & .isDnaAllele(p$A2)) repair <- tagged & !is.na(A2) & !is.na(A1) & A2 != "" & A1 != "" if (!any(repair)) { return(ids) @@ -1290,9 +1297,10 @@ harmonizeAlleles <- function( # The allele relationship of each pair of a target variant (alleles `a1t`, # `a2t`) with a reference variant (`a1r`, `a2r`): exact; a ref/alt swap, or a -# swap of the complements (`signFlip`); a strand flip; an indel; an I/D-coded -# target meeting a reference indel (`idMatch`); and whether the target's -# alleles leave the strand undecidable (A/T, C/G). Case-insensitive. +# swap of the complements (`signFlip`); a strand flip; an indel (a target that +# isSnpAlleles() does not call a SNP); an I/D-coded target meeting a reference +# indel (`idMatch`); and whether the target's alleles leave the strand +# undecidable (A/T, C/G). Case-insensitive. # @noRd .variantPairFlags <- function(a1t, a2t, a1r, a2r) { t1 <- .upperAlleles(a1t) @@ -1314,10 +1322,7 @@ harmonizeAlleles <- function( strandFlip = ((t1 == flip1 & t2 == flip2) | (t1 == flip2 & t2 == flip1)) & differ, - indel = t2 == "I" | - t2 == "D" | - str_length(t2) > 1L | - str_length(t1) > 1L, + indel = !is.na(t1) & !is.na(t2) & !isSnpAlleles(t1, t2), idMatch = (t2 == "D" | t2 == "I") & (str_length(r1) > 1L | str_length(r2) > 1L) ) @@ -1363,16 +1368,16 @@ harmonizeAlleles <- function( } # The matched (a, b) pairs of variant tables `a` and `b`: paired within -# `maxDistance`, decided by their alleles, each target settled on one -# reference variant, and each reference variant (its alleles as given) -# claimed by the first target only. list(idxA, idxB, sign), by idxA. +# `maxDistance` and decided by their alleles, then made one-to-one +# (.variantPairsSettled()) unless `allPairs`. list(idxA, idxB, sign), by idxA. # @noRd .matchVariantTables <- function( a, b, maxDistance = 0L, removeStrandAmbiguous = TRUE, - removeIndels = FALSE + removeIndels = FALSE, + allPairs = FALSE ) { pairs <- .variantPairs(a, b, maxDistance) q <- pairs$query @@ -1382,17 +1387,27 @@ harmonizeAlleles <- function( } flags <- .variantPairFlags(a$A1[q], a$A2[q], b$A1[sbj], b$A2[sbj]) keep <- .variantPairKeep(flags, removeStrandAmbiguous, removeIndels) - kept <- which(is_in(.variantPairResolve(q, keep, flags$signFlip), TRUE)) - claimed <- kept[vec_unique_loc( - .variantKeyFrame(.variantTableRows(b, sbj[kept])) - )] + chosen <- if (allPairs) { + which(is_in(keep, TRUE)) + } else { + .variantPairsSettled(q, sbj, keep, flags$signFlip, b) + } list( - idxA = q[claimed], - idxB = sbj[claimed], - sign = if_else(flags$signFlip[claimed], -1, 1) + idxA = q[chosen], + idxB = sbj[chosen], + sign = if_else(flags$signFlip[chosen], -1, 1) ) } +# The pairs `keep` marks that leave each target settled on one reference +# variant and each reference variant (its alleles as given) claimed by the +# first target only. +# @noRd +.variantPairsSettled <- function(q, sbj, keep, signFlip, b) { + kept <- which(is_in(.variantPairResolve(q, keep, signFlip), TRUE)) + kept[vec_unique_loc(.variantKeyFrame(.variantTableRows(b, sbj[kept])))] +} + #' Allele-aware variant matcher (match by chrom/pos/ref/alt, not id string) #' #' The single matching primitive for the package: given two sets of variant @@ -1425,10 +1440,14 @@ harmonizeAlleles <- function( #' @param maxDistance Largest distance in bp between the positions of two #' variants that may match; their alleles must still agree. Default 0: the #' same position. +#' @param allPairs With \code{allowFlip}, return every pair whose alleles +#' match rather than a one-to-one join: for asking only whether a variant +#' is in a set, where a variant listed in both allele orders, or held +#' twice, is no conflict. Default FALSE. #' @return A list of three equal-length vectors describing the matched pairs: #' \code{idxA} / \code{idxB} (indices into \code{idsA} / \code{idsB}) and #' \code{sign} (+1 exact, -1 allele-swap). At most one pair per \code{idsA} -#' entry; unmatched entries are omitted. +#' entry unless \code{allPairs}; unmatched entries are omitted. #' @noRd matchVariants <- function( idsA, @@ -1436,7 +1455,8 @@ matchVariants <- function( allowFlip = TRUE, removeStrandAmbiguous = TRUE, removeIndels = FALSE, - maxDistance = 0L + maxDistance = 0L, + allPairs = FALSE ) { ranged <- methods::is(idsA, "GRanges") || methods::is(idsB, "GRanges") if (!allowFlip && !ranged && maxDistance == 0L) { @@ -1466,7 +1486,8 @@ matchVariants <- function( tabB, maxDistance, removeStrandAmbiguous, - removeIndels + removeIndels, + allPairs ) } diff --git a/_pkgdown.yml b/_pkgdown.yml index 128ff156d..c4968baf2 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -324,6 +324,7 @@ reference: - subtitle: "QtlFineMappingResult" contents: + - dataType - contexts - cvResult - fineMappingResult @@ -380,6 +381,7 @@ reference: - subtitle: "QtlSumStats" contents: + - dataType - combineQtlSumStats - contexts - significantQtls @@ -393,7 +395,7 @@ reference: contents: - contexts - cvResult - - weightsDataType + - dataType - methodFits - ldReference - methodNames @@ -413,7 +415,7 @@ reference: - subtitle: "TwasWeightsRow" contents: - cvResult - - weightsDataType + - dataType - methodFits - weightStandardized - variantIds @@ -430,6 +432,7 @@ reference: contents: - contexts - ctwasGroupPriors + - ctwasCombinedPips - ctwasPosteriors - methodNames - studyName @@ -482,6 +485,7 @@ reference: - subtitle: "QtlDataset" contents: + - dataType - contexts - genotypeCovariates - genotypes @@ -753,7 +757,6 @@ reference: - metaSldscRandom - sldscSubsetMeta - qtlEnrichment - - enlocClusterProbabilities - title: "Bundled example datasets" desc: > diff --git a/data/ctwasEstExample.rda b/data/ctwasEstExample.rda index 0eaf6bf7ea826204be3e4b466400fffbeac4ed0e..c4bb120fa6741e376e7763c34f9099be9d69f759 100644 GIT binary patch literal 106708 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%0DZoH=xw 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 literal 105532 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

7=JJ6!KGE9Z95WBlcSWo^Vl*9E1hRh^3 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%0Awjh%= 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%0Ah-b8 zsLlGiC`wCLdTQXp@;`X!xaL-P!8ZFxkU(zmTh+Zt|UlC z+o_zI%~6WLI%dWF)3Xbo^qDN@1yY+Y8eE$<*OwuKSm#m1gkw0VUlkMe8>+lI>E_am z+1x@$G1TjC?^o-lTX(od9K+p#FYz!Wf;@%&z*@MQMRTz3YGV#P*fHV$1VN6MkuO>F zq3x(3rveR@LUbPhgk}CIaYOod6>$di*y~tXfmGZ>OZGgcwG9&FdK7#4cPYb?T+*R$ zVl+1!78gGQLto7W@=`D)Z>$%=Ii|?d>v$fp!dAD&2I-#+pgq+S$2PebgvOHdHHlDl z5`aIt8WDUL`$-w*1LI5(1N-kCFurYYsanc4TIWvO*ge(&-hWh&+5z7fFuAP53QN8i z@%K~3Z#>l54;e^~Dul5W6U^RY1smz$zf}%eC0BnvcG`RBqTiJ|362ZRJgCm=wSh}4 z@>q{J5tXu)bWUNR=(RySDVfb0|AzU~i`b!Ng_c4YPfSapg1`rY zcGE%!fVa|hxBpx8hBJHY%a&t!Ne4kHeonjUkT~2zbJuITq&rH{nKs(u(-i;+f}x`( z{JWZt6QznB{#&bR4D*1T&EY;G({F7oCnVQUCi9$|pm8%bx{P|20)39XjkCoA{^?I3GkJ^Q5Ab5iW%{7@go>KTlm4x6!?2#Ye;HGf3Z;1Ntc z*LTMb5;J*20XW}w`Nf6!#_6)8=}A)utS!9X6u$l4#Mz^`6ikgPtIN>YYj{Zv4R^F6jwTL9YI*OT zK%n}$8QhUdM_7?+1vj`8RMvzv2EIHY_w?>1$ts|S_mqL*UY#UemghlD?obEz**T`l zCHN9Sy!CzJ2ehUm$xX^rnrC-dGPh1#KJ8C_&-9o@dgj+DBr2#f8Jz@(VuiBqM24DV zy~Ad)5_-FNpC=g0xI_|cADl*Zbl4P1ABNIQo<7gtU~h&X(9D;#S}g`x=4(23vLr>D z30l(uL!I*p4jfV3ou6@$sA1lXfN>&T?@1;3lDS>3T35U7Q=7*|ldZ3@9x@R{6#vE= zUPyIoB(G`zjsl9Z1{~reGrMsQquWo+ZSUt%WX` z+`|%T09Tif4X`tRYc3!v_lSE<#^h49Al(4~OfHqRDqtWVZG=GvZ9WWpY2Y9$Zd1z? zlocA$h@w?f7(T=@$9^pJm`W8X_@?fnt>;-k2Zudo2^-}D&M;YkEMt`rk#o3ZK~IOG+5EaM4inHv*P@$kaJrG|l*Ro1@p}QaAxla11K-!u{L9YKU3DQazkb7TA*5vEfJ7R`=3mr4hLvI&1$}Y{IDV95Mtqq64j*&M-uZMZsq2kLdfN zurg+CqXq8S+wPDAj#iIZQkU5MjRN#GBFhQr(cC}eB0vVZy{jIuw7hE_$M4tQDKopW z*Wo&3i`OIq0p-Sag+v`rcbaLhelCXBws}^WpCtL-zD0g^Bc^uTu_4ivR-_ks6oTN_ zhZmGU{LY|OP2~|q9p~o*bvbeOx1390#G)WsarRC%DU@slyxn@qIuMK7vcv+sz(kzR zW=y*MaQBmphOnHmK3xViFFiQ5%to83lM!&7gqx3;`jodacbUl%JZ8i*fT-v^-LrTy(gI?ipmOk) z$X?sXp-u%%%W}r3VpBU{uF(%G0T6rWRp@o|LYEs+E-qiRW7qyDJ@QS&>kz zt58c%{zK(vP?9{0o<3kSzcm0}r{4c7oN~qR=ghkKgbMai_Yb~WoM_S&5RsrcL4)|i zp_e@8fP)@!cB=&K$&$CxP1cHr89~0ra@p8OVxv^p(!`EekA*#G3ASxi9Ezo{XPD8J zZ)%2zN-eQA)NpYN{Hi-E>9Jy!PoTB5CyxH8C+gxzL0mv7jxWOv3V>c*j>U2(kg+O~ z33z`h&91VI)()v!;vST)F}Ln&7?h9l2@8P&jA)7M{P7L#{d5<-g7wtn)Od~oFKhtO z=PTcPb{PP}^=-+@f3v``TvPV`#PQn-%l4!+O7%5Gwk{|3v6~9Ijd0@C6P+7)_-=ZM z9^I2vdg2X~q)Nmn0+k#5nbcku?cvRb@w3aFN<8qOLR(>}7Jg&5q3@I6Ix_K92PY@M z#4>OJP3GbkB3M!45gkwMwU^#0n{r=+jg^WjzRiC@wfK&!(|DvXg&~G}I{gZ~8j)Xy zKbq^mcJ&%9+`dyVi^@us?ak;sURVjk#)bbgUIH(D9iYI> z^;a&=KH8E{qwX#^-h*?rz#3|Ju#6=dD7IV|Q_K%Kh(%;UuDdcXu2Lbf->W_Fa|Whk zJ2R~SG%OkY{9#W7u4TLE(}1(S-U%Trt=B~8q(k;)F}zy}*y!m!&P=RddR%#maLF_m zvI`f|nGCOlie{#(ez@)SfV%+osWk~13ZuAzu%X8?`HYXzsAJIJm(&fBUdK0s$w_Ad zl53(zcnK#qRah<}XB{Y?2+{hAfl)EWvX2>a^z_f!kzBl>|<#qvw#?9m- z)Z#f!w8daC*5+RnOBR&gXtO_&H_(Ro?jCXl&xp;6f&7zOdD+lHSJv_8DZ80|JdU^m z`B`LJW4JY?MA^;rc3MQ}yA2F>;Ns$CoL;5Gny>v{gNST#-0k>`vW=}~zdFqfNL*m0@OnJme`@&_{) zVK9^V{0nG=et-_RwV$rlVWK(gCK!C!E9Z;PB(MTv91vXl!2cdGRAB^4%%WH#h{_s^ z2^*cF1PoxR{@n=uM>;?CMsPrxtJ_3l;*FO>Xfcf6aWW3XisYL{9C~#V=b8_Jfk)7Y z2oEA7575QxK!)xz<0Gpp2{=vx2U=>XDZ>P>Ii*vzFS-Y|@3B~M+>c|hFuAy>!lN_` z-aPl(t~UawBj3mr_qd_Xv~FHKSVgL%8xEwW8o2xxfCkD?ho0O^SM2nej)!LPUz8l> zOy#85XKD$+yEfA^0b9(DK0gOSfh6$l@(2R-RvNDQfP`bfn;_78XH8?L01T$cR@pV- z&y}!9P*A&LpK1AC7vxE<99qc4o@jp(>bT#_{7dB4S!-7?9vGeZ;lA#EA=WiPmh)*R z#aDl7Hd;B`^?~Ap{|@gwDtitjj7SZl$R>yX3B@{|`p)h+f>E!{TfnSkMe1 z9GJjQ^BJ$gjZ1IkD!Pty(f}!=r8RnV+>sG4wLE7oPy2AGjkfk;wLYc9%|Xq7NEehJCuJa3XF~ ztIFjpoNtMcvn4zcW24)cHFI-)wV7iEBl9i@TQn)&G%oKF?P7@&*9cO5!C{p9s20nQ zai%+UV)~uG3Z3+CIp`>KF8-85wx|AnvRL1}Y zYtoH@dZWO06NEs!iUcew6Ny?5_#+@)%f8o0{_IAA^QuoIE6D!lo_$a|S}uJ`@&C8c zf)c#g(%n7bB7I9$8tGwBT2yNh-_vfq2ULv%Xq&U-ace`k{7X_M=*F(F{)g{cgb-9o zzJ#KL_nLCSDs!3vH#rbR#%WuJ7UDVXebGi=JdWxWYKBA5l;!#^S;F#tg*f(lm?rF{ zu%^D0z(~cgFo#G7#v0N4s0>vdVL~|56t5eNQc_oE;dh9p!&XsO9mn0}_Yx@)`*n4&+x^>xvNS2U zm6_@faT*c=_ML=`0ooRf(v}| zq6fM^!kLL0)0Ma&;8rEVRQWuP5aH<&_OO6`-@|Ne#4^FC#4i-Y1B|Hf5Hm@8tBCT* zgL=C&HE6R(3zJ`jQ+7hlzP!f6rsR>O*4vZ3(aB=)YLl{YAcnU-A#%LQns<0%bs8ik zJyGR2lB~?*X8r(nFgO1@lQy~0Y@T!pyRe>eF3&_GKMj+Myc~KX_hEv(BPYIpp;eXK zA353?@RtQMG8~*F_AVB9(S*p)9G`Wu&^c6!s;@au1w~~}25;n|*Hk97WAF-_kT47$ zewF`2>#2Xm9k%s(^u(yLk~lJlhyC>2GTLRZipd2%3}~U5cOT`h41I7yN}S`KpElX9 zw|ALb=!AAGhSzGHiro8aS5P8qyw=dH8MNfByK-*{R-W28^u}ibr&@F^$0hu@NB%L| zqp*Ny5yWz3mgr6}Tq9X%`jSt$Ixj5X7va{Om7;Js+Iw;F)&smZ2a-c^^O*T$;WF{S zTct+EwPtD_-uzw%o`Njm5+uQMfKrKAWc?A`TT#Nz zRemz5i!4KUlmn|2kx~1b5S-8^B7K60ch|>2PZ_NwfMVqmBlR+*cn=40jVkf+Q*&^% zwu=6!0y%#^Hq+}Se9NiJat91x(5|G{A1{Zd4MiN#vKL&cPIsy^YRBoplR2?~xwC-u z0)ZEZ?;j1KNDH)rSi2H8V)v9QRNPJ4GQ7h3$yCxIZ!%!1vo0E{rkPs)Gk_WIeR4Nd z+j+KGV&>8Z++%$yDRjPh@xOO*Qe5RY5RrHA7M%`=}w|;$f2U4>Dv3})p3O4v6r>I#C>TpaX z(w)$i(;Plk3A38je6xYvC9^MFsKp%}SyiTH1?I(>u#6rAG7|W2Z=nIv^iSu#_+&km z|Cwgoaz!PrquAR&c}~)z#$BPs0^F`|bUE&S5IG!kTF=OxH7ORWyg=;-5eEm|{p9Al zMy$o7)Fu0Fj8-LR=z|fEmg2dKaHdyC=U!y- zLu?+2WNPZ-;{+cJXcK%qpW#EPb#R;3;b~s)lnKzu#!+X6br)z!mW%5uHa1+X_i3&j z7>Z=XKeN<|vA%VO?8r1BY+b57XObJ7NdZ0!8lCfKHHNhxk|nup7*@2+2YTxZoQUv-JSTS2V#2*P#O&A6#$t)SU_OZ2#C^GQX&9)`ghg}e$ zGFGGZHRP}y;)UlXOkrTI{3I@UVKn$ab`T^EzM9;*!|li>M$?OEevN+MM8!v9LqIaP zX#@2sQK+W?2Cn?M0GrPxBVn7ZPySVW2QKvJG<9~F5rD>Um%%nG zl2)SE=&a-_~#N6d{F>)(Ce<@-zK#sO!HDD0*YP8PJpnNSqPWOVDC ztzmOD1zi9Q4AU&v6!JCs;2(Pqn zA!p+6!gAsPh>r?WbL12Ci$S~zjw=@Ii9?0;!${w}>d7NIJ8E{W-*~$g>Q&U8wuJQj zwkBKGBi^3H#0ogy|8wC!e>v-aMZ`d#_-hUvqLD0K&0~e7G87)qK;_2%qjFR3xXRo^ z%z~Y$+BRQi&r?_~Zi&~Lu>us^!zTmp2I6QCZlo)W8g*#chmfe7mznxr{JcmLXXa_I zGrqFr88TYa)b9^tWk|;3ESphXSmqP-;owYP(w8~ikIV2F>RdMW=tf07LnG0I(q$l& z0zs{#AdFi)LoquTsANP4Ep0R3n93w-l_d7-5Rz7l1L1Gac zKE!S10PjZN#36S7*h49{jxtVLKB|G&X*e#v#%NA(9zqW*7?R+{RAU6))P;cu#ws2R zDpTWgoHOo)g#g?PsxP258w&a`WxZL;$A`F<8Up&*5nRDNdR=zLH`*THyIVCb;s4p* zH$LCX3xnDHLpCzaR*lzqax~^I|9DdOs~TFNHOmZBgv*>wcF9-gjNkn$h+SNBQ&S7}DRP z0W5`}STwtlVp~r-C>@waE*h$AOD<>NkEq`CtL%IWy zZ^ht3Y4mEHtTyP_NUi9ZTWc4iBu47oK*EioY3#@Te)d9^YCS(s{Gc;Vzj18_>(M`z zaFfn-G21@v1mw-=RdHqKTV|70jygK{vqf4Rar`OX$eidTVbM6qJ?NPQq_2FXmoDmw zU;k@2taA#*T%dqonS)Tu+a-btx5#!0;Ej}S)_9T4RVvYeNuCIN=0WBzngqXGZ+`f4 z&{8XO2Y#1>jXVk%tAev{o`@Do5}chR9&0>?E`}4#dQXKKbfK=*1f*fNA8fyv@b5Zl zLR<8s7txorEvHImW7)#wwlwa^`{!6f;H)BvWXL?U2ar*f)B^%h-EHJbsP4&~wefY<*Wt)txuP5Hlxb~KeY4qLr^A3>>rZbrXXuV%_}$xTspoN zM+aa~e7o|f^l(q(VIgLZp7bwdjBjJ5#_nbYV55!3NwDCHY$Z8OW#lO0*~~K%v0(0# zeh7S%G{Nz(QscnvO#AjW*9P5OSZ4Xc#wXVydtq&2Lkwos?$fP$=k%1C#puPs+{ z(#oz*rw)eyhO>M<63mw(nj@wW2?w|GP9WX%xH25!UlLbp1)>p$#Js(smqRr1lrh#E zW|Y}}*am<$b{fpf4SJg{ zkE{0piH;6^YPi+lyH@pUz$>vyxzt_6nWdkr841vv4qIUsdJxd}~TT zhtI{`9&7sZ={1(akK-xr^omG0CI}4FK_;AA6r0htiv@rm`9emBABdoK#8`4>*Z=>) z%w@9|vy-E6R=gMQYq<|njy?j2Ung}XjbN~RTF(C44?Kgh;_3{elu16oBxS?O6bPBD zgYNg%W(3@rR)z0zXHFXzP?c?xh!t_Kl|!?S#?X3AOZ14}j!F9+*K3C`>E+ zX*N-ZW|@;67Ney`$OC&H=m1XV&YOsPZE-B1<3kS56tj_^W+hOMuti7~G2;BC*6yyp z%#cxWignKUfNCGxoTWhB&16qU)(UYoA(7J=(%D|3gZ$={5M~o1pR9lRV z0nblG`Fi*iIok99R@xR%b#GeUq5@p%am0S_Kni^G)V8M@ku~{@e`(M~XNb~vIfizD z)IaYsIrbjHZ`jS@L%#(x8%L|$jEYk7= zo+!e(v{}8|YkCueg-V7{O+T7(XZ`!50eKZ(bi_7(tdtu>EXh!xA(#d?`izU=t_IeL zA-v2Zr?8)_@SEZ$o-rS9Y?s`Z;$gBxVpD`-*s2(Z6Lg`nWx;i!6!({X#3RVJriNLZ zI$<<~4rOGVLsPd1^>roJ3z*)iD0gBBsvCg2no6YYX&a1uJTU*jk-V#z;d-D0xg*a;aJB_vRYmP$zTl#hzIdf7ys( zFC1(XnG2PMSV?kkd;W1TN=%(O!ui$o@^TIwNUAWEd-I)QuRMO)B(C zHe4By@!ea?ey*Mg&oJT?yV`FCxL2P-)oh@!>i{uLj&Jj=hX=}zjqE(Ncnnpq1gC3t znUh+g+2vp1o7aDi0RB~#N#>s1c65&~zC|;=))vkk|J{JbYq+%)yrv#)K(ovZ|JEna zVSR-M0AN@Ns!@H99I7kba3*wmTaF|h7Hf&3om;KCRiWx~3IsMShfb`r@P6`cpPP<`mjZBA5&KUt|H`xixaQWLpiRkp6xC zlG!QfpF2FB22?Y;$c#53?)ArV#xW-2iG6|e1{-yvkUowc>ap?63WIcZgOg&1KIbI) z|0lgEP*;)Am=_TDMEq?0dT0&T&G;*El)z^1z=&?giP=&zYVrj+u)YNP?_tJRB-y$x zFAS)G{=+~pCtwB0nZ_EP6(lc{k4-Xg@@kwC1N5y=Tnfbq$AazR z@rsON?&w^Z`+$dkWA%$hisptIE(-wcGX$yA%mSDn0126mQS zG7UQ!9gW@+Ryhl{2rn9Do$q#+d7#!qVr~9&{(S-2UUtk04UO<2>$EfU>Wbw*s?AV1 zT-+apx?r^#=`v{t;J+*yAj2OkBx@k^7~=clIo_>6;^)ViW6ygOH(UfukpCsf;e&>V z52ZyrS6n)ZiY9R|Hst(eDE%$S2J6RJwM2fhRNO#QPm>NJAKxAv3uH_akzMLKXeHF%xLa9Oa?OZS74$gvrq&PaFeR2 zEOO)w)&L$Nvkz$U`R16Kw(FpYDPb_yVE6><^L#-7tjC3d6H||z4|~Pd-@zhZdc+BZ z3+w`EA`5syZu)PI-D^m+w)H`BPi1IF97+RYhRFpnB__brg#bhjeX{;W>jLYPpU{In zH&Mn*F6W)(<(nKgC>#2gsgP?7;*ivVj%w>IL94H%_X_@3595b zU%*z}21+73Wl?^87}|ymmg7}(4JP%|V>oVD+cqrOcI1TnSXlR@sG*w`W(L28XbXzs ztdqJZR*H-^7*oS4G}=Q-!J``H4E(P`cp~L|TwcViv!@v4WJhsTuP7JeTrFy@qY9V< zEv945aXK$b(u` z;*Z4KLajt$=ZyBDWjLvK(P1C1U+={{#N)YxNLt=)BI`3thBtAtzn8!`EjrhJ;M3D+ zAY-C+>>xbW&OZ$KMlAd^ZVUT^$_&NZ20;+1CYB?L-c4QDz)B>cXuZQ(DVMVM{IB!B z-&99U>@*MzpGMO#0AJ^xs#dI2qjL*jG`-SRlSBxh=)Qt{LW5khWA(Brt&y>6y>R+iztSnJ@U6JrKI{GL~vo9pCw-rZSIc&H1X@ z)weV_mYSq{)3jHhewAYFE_)}oW_G)&^i}xrKN88OUrv*&HM&^1>cqT@1FIV=XY#Ra zH5`|`HVE=`N4y}=Xj@`ZeKX>D#h0qkl;*96OMADF@|zH5R_fNa{oFmTPvmoXJ24&v ziAzXpiFfiwW5|hY(?M(R?gHnL*brT+00?g;f&CrRM#5{2+tNoBsPs7bPrQB z7GBx>X?@j48OTheV)If9x<}u7`u5$SPB-%jNc~Pe7wal$gV&?NHjZGWZf_tWFghTc zgXe?Z+taINzLl(pYcOv$4|ib?YKJ)DKZ&@_T)wSPKDCt@irAo^8w&qZGTD+B4)Z=F z^S#cXc&YNOEg;yTh7c%zjB!|eS&Q>LtY>*FRqB}jSwW)5^ueyl+IItll;Ke-_MRm} zO!%7rNG|Gy8>zgCq~_ZjHylqm`k3k87uydn5{CYj1asL37;c)k&yc+Ygwq@M;0mz> z`iSv`#ATWV1!s0gE7%Ht<-3BUt1mtAw%k_g4+^ySeb-$a5-)nbpqhH7Z5;sjBgTJ% z=AzAfaFFWKVQpf@ZH;?l&F&&7A```~UgoHBFkZz)XU$Uvb~O%KrFV$q4&F$P-LHg> z`@TP3Hr@t3U;LM$^u$v4E=ca%>n78n`~YJeVKq9+WgvfnMPpbgx&H$tilDSUXKS84 zOu&zC*{btB*d)B-l>GR_$BA!`3zV6m${%XWlUr0B)wr?Ojf0`%)^&j-gptfH;wQeD z3rT^{~o&)-E<&gVN+x@<2SsIC$g!{ClT74Hm!nvdd8k&vL0yePBB>*@7K z3J)IsB#RPYtS%c(l)vaN^L*~odC!DoVexYHHU}Y9G1T7ZC4sB-IPsxO@U~O-#d!o3 z=tVb*O(&VgjSimK^YPsSaoElAJZ(#Cl?{1+9i%VxBj%ya9)PEtheqr$z-3$NmmW-7 zPlfhK!Z>g(FF=rpZD;h($XI&KCn_C{j`R$$YMWjL{JUV;!7Te)5~Ux?0NuRY8yMlB zQ5H#Uta;|FOp|k7@hFY`P6XxDoP*si7wA8Rn2gmHzIW94+a^B*5+7?3ItNe%g8xRg z!id+3xkC47rPpv%Hvql~bG73vm&5DHeR|)5#euW#^rb@h7VIc5C4{3$BS zdu!WB_4xiZI#-8Wf$cPER^RyULw}->Xl7Rge%v6~x9s3!^gYo+U3k-0+X~!!4ligF z$af=xP^`H}N8FnGGzKG)^WY*ENDnPOizj_v>nJdxt~4~$Yw-G4W1YDW&ldS9>c;-b z)a3MIr_DQ-4N9Sr$DwUcE|-8IqDBfm81(b2x1oY`GIj-Q-I@i7k*<-qAsD;23OQ^A z&0auQJ_z_aekU|AF9k%D+TXak4EC-ytZFmwpUh?url=xSoxyh}FOfBhtwX}imt@%% zbBEu<9j8h0HUmTJP5?qpW>p)h+C_%Wg_2fE%}bynWG0}i_#+XN0(5Untv?U@5GdB% zhX+Cfn4jbUJt;Iu%4?M`UvHtY63{*qET86H{y{YaVp^nCZ5ofAB=kxgP<0 zWmWx;ygUVZss(k@X21!(SPBwQ*5Q2Vd_VD^he47G4Uz5oQ}UW}kh>Fkt&3XzCN9N# zDNgP|*rLZ)5`?71T0Wm?h;d()0=GBzG%{Js+1srmFl$c$MNMMnyo=@c_B$_u7Ia=o zdD^Tjn&^g2wtY7b`@kPmZS;f_TI~KAOBmiVFH}Wf0AyTEV_A+9?7fjN^^xfJFxbYC z+>Od<&=FF(lzz`e+)~-PCR@J6FHVLV^A>#--Rs_rI`34|7=c>=vLz$6wOx~B0JeZI zkb{r|4809#)JVw)7Cq$Tlc=yhL(I{}NY2X>C7bvl)K!Uw8G(PPD0URZplk={w-8xMuG^KU~f@x+(^`;SNEl;KiLXaE2J06X}eOaK9uxCYLy6W1guq&_eW Mivj=u00045T89%68UO$Q diff --git a/data/ctwasFinemapExample.rda b/data/ctwasFinemapExample.rda index b4fce8eaca8fccc612eaeb18253684d47ef7bd4e..5dbef5ab9cad31e985d171cc7c08c240c4929627 100644 GIT binary patch literal 109632 zcmV(lK=i-;H+ooF0004LBHlIv03iV!0000G&sfak9AEGMT>vQ&2UKVgRpfklJ z?$kMYTNN2WFvDQa3Knb9m$qXf;ze8Z%oj@lQ# zX&a-vSUKh~z^PomUMEqC4(gKrFarQSGDQ>Vw(=W~F71R-3VLYFAf(X}Z@((1h0MX- z(q2_qCD;E9;_Lwe#g5bMR5zxZpcw{4O6Wdf?L^)8hVLDWQP|dxU`DMcdNkIpj)%rN>f-k|eV#(OL#>gzMha>g8kc z55mPQ=RK|8NLW@!qt{|ayhG<`S#P0wOfcH{umX_oF6S3W7NjbADURwX+s?a=+0mDZiFNw;EvQ6W*6s-108FK-cm6kv5 z(u@+%H_}GjyDKG3-sNuX^*Mp7=ottu^KjCp=QP}EaGcA_WycYq05Esd1|1rgy&^DO z8O2oWuEm80gGmLLlpTQ9um8#_$r{#<5(*nESw2lbLkHLO4&k)_bPj&70$~J+eIJV< zBskf#gzmnN)GjtQ_c8xb4xSp_Ul5k5sb+n$zp#X7tIsA-lh`RMaBTq6!5uw!Eeryn z$Q;@RFZTT+VUx1JT>TDv1iBJzoN!`a`edCEIyF>lXgM=Lh;qPnx%lI%qc~Sr+z-X% zveIU&8V3ImAeEQOw>H0+rw+EywAVNENx{o)v!Yh2Qt81ZNQdV_8pgS)d+Codx2A@AYZK-h_a8KLrygt>N(uHAifj`~>;-Z^2FUkh(SGVaz0weuM>DPD_HkPJqc}zAXRfiW4Joq$zeVPl|$tZx>7A9 z+y#!mjy%5Bin8NmwzT?s#mJT9ka>D5V#9q9MNSBpdyNT_gETLjnM^6TX&qXl)F>*A z+3GtIN#5$S1>Hm_MjPh+X({5*ay1pc^^fH&dNXKGSv#Vgk8Fk7KU%wRFUIy5W?XH$ z;yeP%JOIA%iSyG!Nc0=WFQwX2VHeO>CQ(Ul;;|)3x|_Z-Sj=i%VyoI*fD)Lg6<)LX zMrYK$aP%Y+6l^!I%B6m0rw|`?kH7sKr>(H4)B^1esZ>CHf%f%TC`Lu>6VxHiJmYFT zcCOPUwC6xlQ(H!>IU`20&$IYw;8j5%FOEor*;2jn3$|*v6hR30Q1<_-buzreJLbqF z@N59?XqqkrIgPi$71BrS*Lky#BEC-jaaxAO1mjz4u}Ad9{h}FeG>aHwV9p<#UZP*9 zdE=Rn1L@?mXimM>G&5!l00YtQUHIN#F^8du5M%7;Zw-2c(=tpK9TN#b#19ho%Qy~a zQNs)x>nw?3ck%P7?t*;y9G?;XHpTGpnHAT3y%`&KxW?y#!y=sS4tA^Zv0uIM-jaNz zxqqQjnu9ksb{V6gWA)<%)0S{dMt?)jFys?ei@>?)nvBxxwGn+ZRI=pREfEGITH(&Xok=4mu>u+EW><|-np)`+ zI5de0-=F_VlZ3(tu%iE1eeF(&ihhT>HgBmM-DL<{NvVBvjWH??y35HlIyaSNR*02>uiNM`8;c+%BfJe=7>7{cm2qH z==5N&K*$D+Y}Rr$2RQP-R~7l7x?!?bWcJtD$rMvnoet=G#3ZqCyTh6PO@_>JF3W@y~+z~1wk9s-_${Zc_@EOR}g^Jt0o z#aeq0^pVR1F8#Z#=F54G?UC*{>0*fK+m5N>%9 zkt)g#pKq@^567@hMQkY37V>R9Ns6o^xq4b#=Bu#e#MmxKAeZ3NOkVB7+_AGG#5&KO zV+aKD8q$1YAKwU{)cRYg;;Xr-H1tMG$p;brnhq%X9WcXd)Juqz=JuMRoM!sC@zltA z>7ZK)j?AvZI#x>cX2C)rb?d^twxq|hW!x~?AsVQizJUXCdU>>QXMLsRQY6_Oeywu! zS5GYBh%F!SBk<5IYrTlbeBio zA8)uH+G{nvnDGam@B+mC)K#V19O)3)DcauvLAW3gXtp#oNPx4gi!8tR@8MMMndSd| zQrTenVvUob9a3JP{+WdMJjX%)$cIwQbvBC}pEsNzk&JW{oDb*Wkyd`Y+yqhp&pq`7CM}!`FWv7J;_>C1Y$6 zt*oI$gFu817*TzOS5TcI?v8a2gjSd@!U~zu#b;%HHlPSMxEdR-2!`~CaED@`Pz7?& z{}D8;8pe!}_dj-AMGUg3+7fMSOfPvgj)!KGrtS0BdAPCkUReJ$KM~ut_YfV{})iHqrh}S zHWrGaYF7Y{Ik`Rt8&YlhVMU7HO%)^)bY{U;Md_~Hm`p1K*Wc@$xH0r|uc#$t_6t+y z?EFeq0989dLBjsb-mKTGJmRrOii22 zAL7cT0?#IDZ0&q5;K6I{f8qmU`UPNAL|^KAY1u>SMrI;ul3Yj9o8Yt^VApTx^L*&4 zJ%{4p8-Z*nUN+locR|qgTP!A#NkODYWrvtAaH+Eh9rfZDtr38QS;KC{-)sTMCbLNx zyXO3i&oR@k%m#4upOh3*9d9`-zi9pGW*#ld1$oLUrNo2l>dW zYu%K64-!oW2hfX19H4)$P5lX2RZ9MA^_G?AZFCqNBIfD4-LNnswbw;A3r50&gBo8$ zU16b*Hydh1-2I^8s!6F3Fw458u1D|aKd#~K3s464=~=3j_fph>xmWXYuxngiVoy%} z-)GF#f!{@6akjG_s=;1|_J{{xir9igl{--Q-_k2>6NRdWdAh9YBKm%v97d{)HkMVj z-8wJDu?GClQElaLU8|nYngksvz)Bhij}%o~Fal3cUs?#sj%13RWO1A@4{Rq>w%(AU zLXKeYaK?RE52FW7NIg_&5;(lkH#=_AZGtH$-4%1;j&o~eVG==RWE7SyEBe)JhoT`i zzn%DoW15v8aFjQ2^Qia+mb0>=v5sjhL+^>D98gXmtR{uR)N9DXSprbhQn|qZ*!@f(1LW- z1vS}?i16AHxWUDU_i`Ff5S0~9xxLU#$VYw4NIEq%GQc#>6;(a4>z#JL2n*yE_+vu4eU7!I_T0IJ%>6vN&GQiT<(Z>v4PZetPDTi0StRi zYObLyS1?_BsAzt#JwRNw6l~r*0|c{{clQ}YRtWDezt81RM=Z9fa-|uLVx{44jNPO0 zGtQXD1V(o;5ug9y4QVI68W~?xKkj_dL@P*Orv?rN1y)zM#K9=s_M zoli+5YnqykN6^LR1M+1{e?W0Fa|Wnt(Yz3YVF78OW5Cex>zY;!3&D)EP}4k6qdk5l zJCDg$Fe`M&1`J7ACh3fVC@R)uT4u|!<^`WpUwiII5g{C((yG)^ggwR4RqfUbOU4b* zkcS5&@VA|*iCWzssBLU4R6snnpR4*A7;nWk;CKP_vQ+giYhGT0N~5f}pU(Qs5M_$@ zZMs5+Bk0r&U?9#Rx}m3OV$BM9hQ@w#Jwi1Ma5Shq*d=WoU57L_`-M#>OVvGoYXMM zXD>{!mhi;*L!CMPO6R~W)1s~+mRJmT)DjRb!Y@3U%XJ{hjScIH`~3Z7Yp}aJlesel zD2o{2xJ9ShwfrFFx5Js-fXws5cDuFQ1IpEp*@p8YJ3cT5ws?`Q4%$1ooDVpCYRM#< zB-1`C2z28|$4r_ea)Nc#Mq;N;aTM5lOSp5ZP8-=n`KWKwwUGN6P3`fz7=kXrd4x;m zji7@CW8UKo^GniQ`8?#5rp)z=>Dr1wyRpb{@hpQE(?P*AhrJUbu(}66`^9znP@JA! zQt4Y@8g-5B!q5GaAEY^`aa|5(P$Oltk!iDv|5Lux6(kSiQuu;r=6pj0?DmOnw?bT= z6h2<0V~Cm{t&M_Fj@e=V9LP#-)lVF)*7L8{nT1y3renwTM@$&7uXXmqrRH4!0TJD^ zU344h%m=+sH144mi@hHyLhkOL(1D&`INW>%jfU}l5#seax2!{WulbaMh$kHz;(d^Gdt-%UicyxX`~JxV6Mv@zww|ggQ@5}v7S~hQ%Z;(XEHp@R zMPX+KQ?!7Z2KG)1#+^ajylk7i_!6(PMR=Ka<1ck%j|YpN)aq7tXAq|xcn?^?>+b$3 z^oH9u2exJ3?}~*nTZeaTrUUR|Y)t1rz>2R+pvH3(OdEaeo1`XNV+e@9UL5rd31xf% zi*-M-eOYP5HR5k|iuRfdfLy%~^%iYOI{}zFjlclZuRp3gKgE=xLq<2PLN@}4(caF0 zJ3q%z1GK+rr7DtWe9HLP-6PaGsjsuf1N6fN_AwXep#9%3r%6TqK2G1oRNDoqg*cq0 zxoiKIe8f}~K)uA68Epw0WNb@O1s1(8q5Z-y_T$PW&_!RR#b*x;Vq>Rwo{#pS3$7>d zlyBBTH*zGA=Q5WYSJgRQw7BX8G{@o<1-JLB1;F+1Z0v?->`4TU=RvBeAWTau1xTFa zH?(Fx)DID~NMU)}Ksyt=E_!%`@DLfnJud%SCOdlPZ6f=gLBjN$L90vDV_>iVFH=h~ zcl3kF$v|E=EcUw_onRr`-6vTb4MAt6#d6zOp58Mw6pizUjwXAdLzri$c@d?6*=HI_ z9X7MiI}bf>?K3*3{8&*UGU65rzjO5C$l$`#2;DgYh8LJwIy5*4HXb9oav$|@uU^DSF26BuGjch&Tj0tO#0P`2Klb|l8h zSOyPk%@JNRy1`q470e4u+@ERoIvvAecXDL6pIVtLWW=rDyYaM{0vn>qn|kn9xF|}J zvNdW74BlGk*>)$JPGF58j1+%{zOu}F64#qE7{}z8vXm^7vY?FemgVp2)k$Rj7XRWl zR|eMXzDx@us=J?{tTy0VCoWw&Cx*(p9nDuS)kL!KQ!T(#&4|uycw-lWBP2 z&Z5@15?RWF8f*eq?;YAsqShg)%J2QNlDR#b#VJ+;iO#J~^cc2?#AcJIyPgFnXhO=3 zZ(R6a<^ssVC`Z3I)d1}G{Ij`A6&ubo#wXN_r(*3l#q^NAUE_2sr{1&v$AiJ77dP5I zjoGkD&B*g>$SUy_vCbFJhcs5(CMR5wS)e0Y7U8NRqlc_)q3Yf=u+jY+SdS4m4Ackj zF1F3?wHIz26eU`FT4i7$;XVUceKMW%eebMkXdKX<_mt3Lcq098Pvh;vwk$>( z=yOqp$GK8jSuj=_#nck@GecgK7}b*e7bAk#P1{DRE#9i4v76t z)53P-W6tEcH1SmZEYYzPqSt0Qpyb?yAquTkQ^xzZ;51Ty)4Zhlr^98%v}(?{fsKbW zukWxMa}?;2gJ!Mp1*k$4a76e>F<{VQJhH0q(LwQ#P(p3nlg&m*mn!$TF)y!MJ{@zj zAvzEZ)W)Vc_!-uMXHTlz%HNp@WkjI}njE9%yn*32fJ*Zxb!)3?hp)*GB4W?)>DCAF zL`3uG;s0DA3*0W}(k5y1fz9r20s`zH{({JLbRnM&dau?BODyzl6AR->nJ?#vVvL~U zRBw79+s;vFyFG;6%?vfmqVk&;sIM89%1Hcr zls&l_nnIW%b+|pMhC}vxvdV2m$cU^Fw*LRrJG-prf@UQQWf4IQMtBjJeq(8j)m)To{h$6)fmw;4N#=iJd1T=_->c-HObRM@4?FnjobUAA*uq9{D|YqEabmMM6ibV-v+d4zmeAlr}a7Me8aw*Cb)A%?@;9Naq=w z8-aR1{aQz}La=TM4&$U4&)>mZ22me#Dk|R~NVj9l%l>{e#BqJcaNtOqG{_m>uD&n( z7(YF=+xKkM_gn(4W-&z4i?fdP?voQXcE7=U5`NPB>An#M!aft%U&`#$eh;pjyU+AA z1Mx-N!p5wZ`%#}PeRS==i&int@3gYyaPj5U&Z4cKNA6M<_sbs!i27&HqFupDQ3LWJ zN3u|+x2$iXJ+6$Ao7qBa&Ze;PPAn1$mzwQG7eS@4fQPPrXM~_`9H}O3I*(F%(WP1E zQI2Q=jRCrh0{A~uxMG(-$73Nk~riO+lcji#3Fh<$=i@g)S)UR zCf1fC$d_g2Dd55Gjgmytl|WSzIBvi6PwseoF6UOy8x|+%_;h z@koS6n_k8si@W<7@hYu)YQB}48Ogl3K*ZSe9;Sv8>M9Q^2?^(Kd@e`~(Lu!W9QNst z0%oE6OQ|5`;*^{yH;ba=v;qVPGQfj1W3fM)gCvo=%UFVw`R`^xc!bsgt?u4XdWv*!ii^oFFemUYMk9^{u!) zL?=bC8nybogKm<6CXj3ntJIcI+c$X2xQx#qoZxL6>4atF|Dka7UzQdrpjBCi;;HaLY(81|a;=;8 z0-BCd6b~bDW!#SBdLyFHgOYh^QI0<0OsSJlu}Hm2>TT`NP^k6 zCM&8q(}yzxL3?A#!~dC1;h8S%=@iSOqk$*d4riK!jn$n<%)L}SIgD_NU^TuS-&dYb zA*~=>cOWBZ8>O4^Mp`j`z!S|Vy@jwppSY(^or|hWiz0pD%-&>8H7wowU-adrG)_Ur zaKGVv#jx?2MxFB}q1Fm&zlo7 z|IEs2H1koDNMpC64I_m>WyIj;BXGHN($xPDCx!0KT=_fCh-_~l#r#!U{y~g%L>i=X z$*qIwJfh6wc32$g=0w@?n18nwTR`-$B1Ow@>;lAi#f@ExS?o4NbFX-xD0m{77&8@W{ zIxZ@!3t!xktU&~9wHb;?us%;|7;S#^hv((p7lUXmT_p4(UmpdKZ*MUM{F{WHD75G- z)5S2eK(df0m}{{RmIdn2popRaYVN60!0;NZ}G#H#$9hzy5&?iU@-5|sQy-L#{$)FCpr@?IA!ReeF$aJ;MYR(Be-`p2WFU55luHMO|+h#Zl@R;6#~maYE< zGsiFv>@={KT-HIslF+Yj!sx&@|d4o=bG|8=$v9OV_-r;v>Aj4d1`}nH%BmLY0u-@Uko34A=hAJE5*-^DvH+Ch3XIb!%q{G>yB5%yI_oe|}NM2PMJN~w4a&!%U?o}m{-P!93evXZ{UXA4gYykCwPSlsPWi6n} z)X`RQcj|xJAJimURL}mM6y{2}Q6dQ&6;NR)#J0f^;Qrjk^ddsWR>9%(_u&NkAYOTq zH=!2IaaREF!a07QP(NcY5$)PM2~a)xlAfQHDX+7=u4S5qS^-A|&+2pdtRamx2Q_6+ zwS%@AY)ZCb$d3*V^s63G0Pw=fV_cmY&v&{4c69HG-EATS@Sppq=ZHq{eX)48$U2Mk zl*qmFcY3Q9E_XcWuY9580g3}sc7d>(N>;Fk-~=GmTA5-c$9u6;PURZ;dI{DLeswbG znnYW}GLNCy=h>uiu!4T)x3ozQn5b6h>CraPf+rDOZ=aa%H@H8P$NAiKJ1JpVRnh+! z=x1S#KnLxSqZIYgdSu?5{4(7}c76&? zV!zumAaT3CJR~*q1vJC8WUEcEiB8p(6 zK`r6pslsCM6M6&@xcG zd;F^_3MowI@gYj{ZZPlYeF1aZJicMVm`PYn^|=RSHv*YjKA+c$yeJIAjS10RN7I}z zDS8ejmHnP6B5nvG9Kt#w;;%rO%{b0o6oc04+78~s2n2$k--Xr?4#aOyH!1A#eciL9 znW)0bGZwzM^8pfQB+Vi;g(|d<{o_wgQvFlp?=aTg4Iahp^rMB@}wZ>@V- zc|J0?xRac-STk=n7-4j417}srTYPg9?p1O-h4T&#S97;2cIK|coCpJQH>x3`+MGFl z6%?L@vr(-}JUUzV@^>FZbq(^q4Rn~1&ZVNiJP@n8&bZ7)eo%JyJA6901O#!o-g~%z zId=empED*Q_;QXM7FlN|d=&X%Q#TdZ9B)K7j5KCRBi3P%w9&wgr##aOO3Dgtt*~k2 zBR&x4lb%uJfg*#G)z5`w;_8(D@0xfc%T6(oA+R(7LhBTbQ&G$DnSaNON1?A#$gm~M zV*#H^C?VgBkTHMl+BZ&1#p@z^PEKmD@(O&5&c%~kP@@}o!7-1M=%R14!{(Q9P(HZX zM+oNH%KFfE_5{!mcQo?ZZ~6GnKgnSX%Rjv|j8z=||5pp0G%Sqk>vV(P!X3|*4ob~2 zyI21BXC8O11DCPbn$Kq0&@g^mlm4 znaP_SuF23yFO&^#b%0TJ@oQ*P!(@$UTYml$qXt>nPL^9SulmlPlGQ(Gp#^}8w+J$M zrgbmF3Hclf^WbcZ7tqfN`B0Hafp#Y66@bCzisZmVt_F5=XBK3io+-Tfln)e&EgrBz5 zfh0BjdV0B80wPK&q|Mw_0Ebp)IRZsPB$#v1GTrK-J&Cwcs)L?xYGpbMC>#zTr^~Oz z#WUJ1Gt7d7+EA>mnm3(+Y6_U(Zf~Jecdgj))IDNIbKxqufF+d(w|wmBC3ezpcDf!$ z2XAQ&W&sl)w#4EUNQk{U0$QtyZw1lby+H{h5DBh>xH9ujnFQ4ys4) z>4iwa`=F57UbU^m4=0%@q)zGSq%}rIdhP_RSd(h3(YkEE0Yj@%&SE~~J}jop$@n&c zD|Mw3DpY`(4rt^;_s!yT(pzBQUvg*B#IJL=VjOY^+i6NsMykt_G{J#qfd|VTf$%%O zH0xUP0sO=jj)sB8D?k51T^@If(>^?*_7u9d@!yU>s_3unauU7aL7jN1=}07q-cpy9 zj=n&|?(SL>gFaE9y_DE;`-T&#a-hNx0QRKDuUOQ(FFE+p{?@}khAXHLZE}?`6q(15 zl9AdowgyIYEs#afb<`_wDr+KbJ%Y4uS9YvrEeBb`( zb7`|ZhTOM2bRToBZPfxhDJrCZZ6)6yjL}xw9Jlr4R!Lg~zqq4Yk*Cicb8bQ7St9$G z;l||+4M`qNP%?D?`v21%KrA!CN!h47%&#?>w~`c2Fp%7;+4METF+z3*);eq|c6y}m z)<7U6iVKM zhZrkxKeylT%c4$@l2j#@^Zp|W;C=-mJprf^JfIYKl&c<7t>;C7Q?dT>Z^+_=xcdB= zBtxak<;yvGk}CI7l|uVfVvYn#h-3zf&=}w+gxIhh2CDW-OfsX`x|F)FHN?(5w_OH& z@6q1ZY$!iTM^L85D8`bF-%K#4=DtSLftVQ(kBF9THILcgikNl`wEehbJmL@nfh=C( z&H$rZfwQ_0+{edFdvmTLZlbz~$QRgPGj4iiUV$W@&%+Ny<}LL-T}tzmQAZwmFN=YC zc*;Z1g+tG(1i$#TWU$z)?PG7p2pu~eAO&h!o=7bdvrxWqp9y4U_5_yIFP92nbS4*xeJy(Afjb6uAZ;)Qsfz#gRgcN@ zah(5QvGWHC=PC^apIKwO3572gJ9J?E#1GLKX|R7)*u`q?>6w|z5sAbS%lR57AcV!X z=%zb}VRpsW#>PhUVuJIl;bO!v7Q(w;^6Qbq{i)@}=@%?P4UOAhHhzgMY#+dNXZ?w_ ze`X1|+?h<_GGOZNn=|IZYP&?7V45N-*u$Zr@H-q0d) zB6bKRUV}L@>Z!o6^2PS>Uy(EG5@1t-P4@@AeV*eu*pF!XnFz?^ z-@MDZ>#jm%y_C?7)r6a3`6DX*fH?6^BuXl+=aNFX1T_{RwdYvSzV93JqwAN7zlcgl z3F&yRd+xcFZ1by4gc%Mpo28L|k&sCE8$BhTPK0>a(&Y&+b^D`SP`cexIG?FNz~ywb zS~Rw(q1{+;LjW*5C&Y3?t9&2%n2>}dg(Cu1#fdFR^2RoE7;q%+M`+%a!*WU zo24=EII9`(B;^L0w^@zE;S2%8o;n1CtB9b=iaGE z1b(0V_11w(7CWJWLXLxzZ#2J4o3@QebN|m=Wgr?Qa1g?w4!)j3Kn?ppZI(qC#P5V8 zJtzx#Uy@*pdX{T-peSi@;AfOuw;~W8Gi_zp@-;%vJU8(tpe^`3 z{J+_v^MI0zo$S92ZzM`Rhcb!qrQw>EX*YBnt{||W)Rp)i29t%PyqkaVgX5d)wyph< zREc&*q@(7KU@u$`&>*b_LO&i#Akg2bsybU)BF3RKtweKp=h3sT<3L1io`|T4+cM6T z&Kn@?%9RiW1sVbnsm9PzSATp{qk#%^+|TXBREETVSWSpWpgb7rmgN;cET#u2OfL{-wJL?0Y$hn4 zXn?g%`E25f`^=mNRk%WiGGkD~qW-{^Myu}HugwU<_b2Gy@~F;qkHt}n!(Su4BJt@f{c59Rbh04oXzgvup5w}-xI)5>Y3 zsikvC^frOMMMi`}f?LQ#0Uy}7^?JG~Ij>0l8oB+f$w!pL@$&CNM&VZiwoWNtsT>7I zQyV4I#FAcc0oL6-2?YbfGbg(D`eHh)4>o7dy?Qim+9jXe)8$ip<(0IJ7vA_Zk-MBD z)Y!Y8*$R_?*6U8b`XAt-G5QM%8+`}G;vXL9;mOb6T)rrIbV%^nz>83PY zLN9JF|3NI=d+7NLud^VXeO=xa<9#9tdN<|xh(@0^L z)8k52$S0ldz1eEKkjIvz2z66-l3z}rg3cX#!bkzwDELSo2K|q{{~uR$8WDlgI7q;a zboSFk#NRqf|D-%9RomFY8c5L>0oV53IYm)2PD;#{GlBmGyRf8s)8!t)l61|_rcn8dGdY6HxSq>7l2^&D5?T9nX zl0<@a&6l00_c`bKX}Avo98IoPat+r@5B>={uZvA_!C&*4-IM>D_HZMSgHI@%fG;I| z+v+O40JD#ra*oeI1*bz6;L@2z9p*{qG$0f>It{%@KY5aXa(P(F(g>q964o>e5|LuE zini8X4VCGh=irFXx3Lzd$D*16GgDgzn<_e2Jd`4<7LxJbRZDm24wlj4+PN|v={u5Q zDzKU~4gQJT!YiQI{Wu(ACRTz!7C7oY9XXFYC*b6AFE)v{^Lb*&*mxZqeX9N$+6UrZ zUf0l2nikgVwp7KPk>($4moIfYf8>E%FM3*tJ!J=B>E2{+_f$H9` zQfuzsfz5rO+ftV~GbBB0iwbt*Bx9+y)PY-rfS5M$?q0AIznm7dG?pLY*WjTec|y&1>T-?mdFP z=`UaSDg0nZp7x;7yjhmp^Irauh_enDGtl{=Bl|1NTQ)rh3Yq?;XwL(i8+p@hs<*aT z79D&!(8{ObM~_(jkhq?OKE6Ww2((V^ZTTcVMf6YsQkiRzYtJU~v8h9H&uT`uF^^$I zOj_MmuyT>`IERD5DeLdIYTiLP&nYTuqnwrs9#D&jh9Yw$jdm_)Z&|`f%ELi!i53k> zVOAp~VOP53m;f~JTNcMYvPZk$cS{fE)9|rYEOTqXAbU`#Ze`gmkKe*L7~X-dVkgq_ z8&=^IqUnQz>Hnyx>s2*L)Srwf4}rRrTPOLm$d zN6&)Bxn`*Yc#uJ=Upaq&rCH5K)vX>3=DUR5pg*|wd-J&Z9uF^_J2WbUakP)U zfosrR#t?YNTnb}EQIs&HsQXv&81hKa28a_ z5&&v9^GE=SOM(B^&8HmtbK0MyRs%$~XGVOXQPGaq3^sv>x?^my46)mxMc;i&cFe@?3 zQ8kflUPpigb@Z3XHHI+UnH08(mv4J^g#rY@D!k6b-pf$}2JU7g9nE|@va);vGULn{ z;e8gyh9%={{;*fz7IkkVsY{g)vz70~5TVn2p5_EWuW6ZC&=s6n9E~DYyI{XSX46%p_ zA}NFtkGW~oLnZP9%R!l(^iiSLCt^P%)PH|2MZ?9*@{YS#wFWHUjTLiaoBe zfsoc>TkvAuBrs@hsR41tr0%jYGfwv5Cb%8Po%N(%2l$E{T<+F za^ITgn?F4dpk2nG%+{Ap=?D%Vx1IzD)j$r^diMt(9>P&u0$VS>slkQ7uxSs>s^zWp zpMZUX6^8}rHGzNaU4n3vJ)I*9K%DQNG*r-+J5WuZKNBjO*M~+C!9%mnIg&2NQP1d4 zkB!r%H1de!E_TY8|B++4y9)~qnlF}6@nH*f3 z$wGdR9LBZRc9p{WnWn7eRpkLuLC{yI8}p`qR!duIOFfV6{>_ zRCfM2g{-m{x2pIcTq+NW%1jQ6{0mn40T8jv(|aSve)<4ZZToLQos&` zMpxw4)#6s^H39@-g3$0IaNtFcI_v>LJ?VQ;lDj~?j^p=+?vTj|Tr+Zg#jQ#W2T+vu zOo9gC-q-;T?tByM{c@vcwv=r*$XhKFdcDqRlRa?lJ);nUf65d?G3KK`jOnV=09N`0 z-0)-KF%rlFLVQL=fdRU5BMz~(1-B=OeB5eoZ%}J@!*XR%;vc>erslvK2G1*R z*+>3vX{`$md?$q|@A&$XoNPNp7q)%D+Yg$E6iZLHW^RPno71p8K6mi1;u5Pve)8TO zs>seG_2ceo1oAIN`FtR+x#O52TRYn#K1`;1tx|?)8Ypv|H*+-(@+_%)br)%TT^~AKq|qBTqGSr;Ro0P) zTffCsIdPA(C#G6SQtqTW4p>hNCt`GDttOQ&?JMG`6@SDY9{rraf79ngABg|hppFTK%!8lX8^7I*c@@unh-!sp}2BQzuV39lKp;9N8AZ_qHH~ z)$B7Rf0Qr;oz$H3|4+W$`=A%cVvfbdE8V5_a8wBIuiQI(4|j5avjMm_K#y6eSva>m zw!zt~c^n{aoLeOoW))M8Ov1^prr!v@YNMPlf~^^^H7Xtq)0roquAo`|vdlF-Z2!*3 z>SziJlBlhdw4zS}!L%KpR|s=yZ^vgUN4ptg>AF3#Oi`zXKl5hzKMMt8Ki$x3(CLBD zP{_r>-pK9cQHK8YStNTLzmb?Wvo_ju8MW@7RYm{F4NIXxK@jRSQrg0hNZRc{4OQ8Q zaP7Wnv|K^MrR#!U3rZQ6fuh7ck=_sYNdAL3ekJ?UO-f`Y^DBa3w6aKA<4;(t7hlU^$2?!M&SzMT?BNkn5*_y)2Pvw6V!n@BAPIe8bv%fwR^bH4 ztRqaeFC5D9S1~5OYu?tPdVp_k?Vbro7><1Ue7pc*&k;f7!#q7Hjea~o*h8fkDt_|Y z7}x&!hrLm>r588x3>{9c4NCpWV(hO++P`Zc=l zv4r#>5%HQDfoo0xLM>{e+b7$OQHmi!ODfck?A54Y%`BI5^0EJz@fPbWc^zc5vA|!` z4rvJA#wIh%0hkv^Mf#31WQLU#>$PZgq-!;IJ3)24z!!}!7aQ0n!z_0w#^lgT5jRI0 z9r#-!WT?HHsiK}pFkNjs%tGDE6#=r3vPEd~A6;3im;&p|G-tur2~yA#`VTb|aBtp-!^-}| zu7Eu%@@YYDzNO*mpkyq)&$`P?0MM_VDl6xh=+4288hlZdHW)AUyu~-Dp4&oXVvM0r+8Ihvk-QNZ>_J$)_|MbfUj#2nOfAG1g%eY7HULo zr3`^|C%ii_VO<82PWE3??H&_x+{m$O4N_5=DXRhYVj~^oOeJJFY$6P&Au(A)2T6s8 zMso>t2d>w^|5xV01O%(mHh-bYesDa~R<^^$e1iIa-_5V8j}dL)Jn^ zp})a{g~TIHx%2G(L|oysjf>0vh95@4Q|Wlw@U1jBrJl2_VZs280S11wrcE8Ikm5a# zgJO-epu>3GT8nqkD3VsN^KXk;(wwdBqds_%o?4G{1wtG(g~MERj#6~~@R?2cHa!Nd zXF-ob6n??Xc8W6nDH?LQ99=h|Eo#Tn61i6xipVV5=!dx_rz?9sT!=UjwffwTrO@Bi zl5~w}f5rQu{<&~9$4}pC=nIazEim^qKnpJ%J{waO2x~fp)Io9uCQ*^}Qw!Nu!P7vb zUZ%B0s0e`I?Ngm7=)jxg?d(BTT=tE>N^hG6#4SIDsgEWWms^kZBqV0Uoz#T z>cptK2NgNk2#6PFO8X#C#iv9ke{9Hfr~qGJgJ^T9zt$8)6;|8xOtWYo4%d96@HkQN}=DbkdOVFtLO@-E&F#Awjre+yGj;g}e{i ztLw}p#fdGTKGKS4C*a!6 zkM ze)qo|lZ!_1+;quQ8LHt0q)SMztP`J9gl$Y=+&Vlv@H(2(H#&Ccmv+X83W05um#HX+ z!JFXnRSUJ3r~nwD{6rg`cjUQ)L8pmn)w=V@O)a`_+#Lsv*rB6=qP}&6dvkwD^K`Q{ zXi-$Od^;?6w#BF>ik*gvcGAmE9J7JNe>+^|kNDp{W>MClA2u(XLK(_x5iX;}kh)-FSTNB)*J2y zFhf#AHw*RMDe|l?_XV${4dQoFnG`kzpcP@@`{a?G!k4wN2Fx}FEyb~VcGk%Ckf?j* z-GgH2?R_+1m3<#h3bVPkUTaLby~*=CL?Sqjn0V;Vf z=lnx}*kp}~_AvNWT^3bPD28P?AJz&0=~=2lbq+x~jkyLJqnu4j62Gh|4|43Ny!wdI zU>nw)z`I;yS%g|LCBR^tyPzIh?7Kn#jGNPMXN^jh{Uwp&PkI=(bD+P{9z$?c^t&B~iSckR`)PwHdK zFn&Ajg?rC0 z6ad{*0+JnjltWVyu^Sz`@%umLa_2U?9^(l&v&W|zV@2S8UXu%I8#zDg`xc%4{B z&BdAptJXj%2^QTVvh&F(ZX(+Zv{Vou6jj39qg&4Nkgms%$hNLwYW+2z z#-C_MKA+ipV^{5^vaDwLG|SQnMbiJvCxpql*Ra<(I!mko>$>L;b?jy&GLe3}s0~FR zbbZ!%mcBc{@xnad8ZtSeCot0@sznL^qdG*=D`92%+)u;L`3Z+v|9WfNc&Z{-2zt#> zjyj4F^@|~fa>X3b-x5%y_GYBiZS8PF_sORU)hYG=zRih|_>d1TPSZPkHQ5B-Lw)64 z>2vCDXc}0ED=(B%4&9q|E;ZaFVxuX%&hxsGD^_n{9A8t6JbRX+&&MK}DG)b9iOSRy~U)&VE#?1SnZOW%lBlmBd zCH2%~TXf)=PPB_6jf~n0E*5i_7<1XZ26QAhwfc4RNp&+v zo~SFth>pD7o0$9lhrIwKg#4|62T2#^l7odc!UyTS0i3qPJ{0k4m6gy9ZS|57_As^J|QkY zj(@x^1YEN~I^Pva5FEK%jaF+DLK)Q)4U*jfLHZHvwTi1%PSDM{gTd0ZdK!QI0e_q+ zfmVxL)|Z|{EuC)ZUvO5FcKq;<{N@&#FS4|a{K^}t%hLP+%$UJ-66Ea~@Od@_#Dz=R zPS2%s2d-i67>K@Vd-CbV@}@`;kZNDtGgPwf%)*>fCr_^&=k^vvHsdjZks1)}+~bn1 zsdw~%FRjYY*Ox2ojH;Zt#dHRAiuS?RI2F?5j_>lPGL`P)Jr0C2xkwTcnFz5V1nw zNgM*NOCe~_W84$$m*XQziV${OjgSnS$4*PiQW%&q0G&KIOU{P{pzWPv@ zs+~uRy_|h9b|+c|Ym2{*E$|K38Bk7^7=z&UgzDRv2HXSm8nnG1oJLib%>WuKl+Ein zK(0%}ZRTs%ZPZZdyL#J0NT@4#Lo)-8-FYe++@H?mK)VXDzU0X{FD0oC?EDt?_=`83 z*GO>;iUoCkOC*bEOp`0^lr|x0Nd!ox@`g?w)#^k1=P}zo{m&6tU@*+VZkWMpZnZ{n zqu%mTHW5Q@Rh)tcJ;kv8VDd}xv`I@@%#nKP0-lpq!g-Ku8@9Q&MumFMe92qQyvqw= zoP$xbab%IlQY4yC$4tShnia*|UnqSkz(&L5n;qD%hJs4n37l0alb8!5E5PBO-m`P| zA)SWGv)FRngSZ7d<2>2Y6Gah%PtH$*%b;Yfglj>f1(ya0_W3?pX#U>Yx>H@wQq^J~ zeG_(#MYmYpF^TvhVkW;amb`bA#kJ{CRA^b<&a3R7c@U|oau*C<8o%SV`vT^ZC(MLE z(y|2RT4v1&t>0-!vK0J8~>x$>I&9FV^68Amo1MzI^TVlz7(D)vUi*j7v z^6H7RFeRm8ICrnT`zbqlbM|o#BJJGlCH}2BVJ1gCwivGyvG~f9yHn^c;zuoVn?=mT z7a!~jhHtop8dIv4Kf)NtPd+BC&riAn(4zU2xcX>~Dwpg19bkVXTVUi~{6SK&(dws$ zGjZgdx|_auX_w~-te}X>Ka-jT=a#FtwZ;OrokHJ#Q=6V(>ixIH0d z&X1*Qe3}h@_zC9{fbD{WMT~feQ>yC<1(6nuNZn-CT*0(2S>N^zF=Kz{RFzY(!Vs8 z_#;XNmrns?|K?Rp7GNI|yN(QV8KXE^T!sG6wx#iR`$TSoC?2p8_+$`8inp2{=-d>% z0p1hzRbYChJzD6WK9+_-1(Eu39^*UcE9bM2(6~QRgiW(no8~KV1)xyq)JET=czX|@ zBeSM#?1&|;DzAYV8(?+s6u!6w@XYVZykB(|C6B_K!U2@S*zwv+pJK@^mAFQJS0UvI zs43gVmSu#j82zmn`Qfi-6v}$$i8vRcP1}xD{S1^8p#!dD-|O!Dx4{TnroxiKOFM_z z+dnfcB6#NFc4%*WL_yZReS=htkOkDn-nQ0#cvmd4u9=1my^dzpaLbOMpmlYqt|Q<> zr*qE$9!g{LvF?*)(E{~jdFDalF9+Wd)yQYbUK+^v&OJUP&@%S+ja??~xSA#}67*=ST~?Rx9U@W__n7ceg_FP+2!r)i(cM34YTUWTMWAAHw9232ktN@qZQmz$dI*a=@OYf(Bm ze6kw#SJ2w$Y0o}}Ca98H4Fh7l{A}MsOSK(+EFwp5v9Rs(k*pK^`htMu)(v@zT-2lG z>1{RIc37U=T!cGEzk14mI%l_PL+!? zk!bW1521)^d$_0;B3x(QksGKM<38i-%xePaEgAbHSuA$SZh%iHjUb@ZyvoFUd|i8a za&o`ryOo8BB|ziVy4TL{Qu<8L!oxfH9x`b{)}$dYGd5+Xf?Z!9V}#e6JYXlL)5dz! z@@&Dr)Xum*8Xso=M$q)e^{aj7VKJ;63hh@CoCuf|WzUT9<6Q6Zfpk|^#9QZcGdHuz zcZrcXF%!d`rS+ena>&Rz(P2pm4OwUelpgsb4j(a3LuJ@^p&EIoKM&$S8>|3_mO6;5 z+0!nKHjyzk>ssX0dq|VfU}?w%USg={u0khUo937+X!u*Nav`XkmKZ27 zLqM5kTx=*@yVf$tMCYD7{grZA80!yg=`WA-2m(kJ@~gc1$2lq#9JHYa>JV(~J7}a=KQs5gJ4v(XCi(E(1eGMc zvuANjeV6ki=LK8acocH<>NQI}DzB`gtv)TZ2ghJ%BQ$Muf>reW z;xs7fu7yHu0Es?Hs}aT=}#?L(Jpyw!pI?Bk#J>LOwF->8O8FGKGVhOd3)UiR-2 zVyq6vI(STkc;9%la}a$y>7GQ5WNCDhw_YA;Kwo6g!b3(0HW&8V1d5j#Q*)h=72A$N zl2Z}=qyf}?zHu|9RnlUk5~Zwl>Xr(WjsfB>(G>@G(n*i7l$+Z6fvm7Sk-NGuLj0D- z5gbn#`ybVZO@&YL?pozHaEH%4jRZ^`ewGZ%s__d&S598BDS!p9U7N5f zj3qL81Y>Z6UPKC_1g?hF@Kg&SNvFoNA1FW1#*KQEeB*U$J1?D;Op{x=-;h_Yo+x;U)Oq+F_ zHlwl&3zIt$%W^1Hv6D4hQKuJGzfa@h9L-_u&p7U$DH$I6fml7lmsVRHy~Z)0vXb7- zkyn|P$Q`{#0k`Udf6@YuX> zBw*X)B-op-MH?t69dS1D5-I}v9$L8S4EZ`Ot@MgDuM!UynNv1<95%=7$Tr{Z06Zuc z6?s9z$^{$12bOV0c|}uEXUQBcYGc|Z z?zN>PN1}?$^!ib*`j7QSxo=wqQJHjciLyia(Kfm85$uKDIM_$BP+r52pTMG|Sw4jU zvqdTav=?OI{iT%vX7ouP^}PC>$;ow0#jk(CbuNpw&Y4KS=htcD#gYYYrxy6YWjaX3 zV4eCSIzY*NB_NYjVo2z1#NC2;-aX3b(?*fOz3CP^iHv)6=tx53k4FXgIgC?|^=x7{ zIvsPz;UOpTE}E?8MU01YJT4{Er-3uIE#klYJVSIkqS;MIQ&~Tl4Ws^r-_y0{B;gyA zuE6GW*IRnYWJqTg`eK@{*G!9sXIX*Sc?GR?MektyK2&|5c51Jh$-MR(MpvkY3&2*t zh50SOt7pe-#IEV*O3O7oOFHbKYi0krFf9t7czV(lu})(GcsEFQTV(^V@%Gy_CDZYf;`w5Vurp8ysKH=Vg6-_p2omOV4*;l<9WK4}g@9)~Sf}|Pp!$v{4w?c)3m@z8`#<0Z^Rs?3w z2-%fdriUQNz5g8sR*V5tKk-^gT@Y}$Uy1@?6ueIdt3dKzyl#?0ZgNDgO_53(g}8wA zYDE`!m({dzxh_%h{f;(QYV>*@z(EDQjs=t{ybTw&f5Sv05+^--uXr?bd;MD2GVXkW zwHzy3oERx~2=X3ppPZhfjLrBBBA25sLM~^?JU~r@&>hy`P!gvp!sNZRr^hGV|0rIs`j zLqn|FZb6le?eCGB8NSSTZzNg7=8rm&8Op^t>U4#6J8U6$Qk$;K$A)r3*=S_ z=&U5Bdc{0GjpkqD>e-Ye&(+5F8vjBGA2F=3t&#>XQ8$!>oZj}0Fl=L~3oW*1&%lpV zsJwt=``x93j|X6N)U3;YRKtQ6q`}wXk@2w4ZEqm7bT?h!9fAQwsP1cD(;B`gkHOl{oKCZFyukvoT!{Dw4ek?f%vYT6QKCH|}s^|3jH zFg@5l7+5BlIuCmC%LX@Lv{yahXA4-hk(-7A!-VXs-633&P9vX?Xk+=lb~YWPw!S$CpkFS=omtt$0r)swpr zZC(2rVij`rD5?bpNp!dA@YUSsmU@>RaYmpJf^>4HHGBXqHMZxMgKxKlroC|(CW7y# ziE}b6_i|5`9bjc8n{}oU{bs+in%4`+L>3Q85@3D})0|t~5%emBU1UhnvuLi0bq`pe zdW0c*V-jl+d!!>|MjxKG^=e!%q+-LAwu~QRl z2^9MeuY{RdcSX$cmdH8SeIz+gMXvDW91c`0Tqzd41#MPBh$~`N|Fl{ZX@eOP1uY%W z|A5DeKSzT5&?yvlRn_mmT3NIWJXnvT_h)-2u_ph;StNpixt~bb!CBohw|4Ur@c)D2 z;zZVQ8tnR;N**}}m^6uroYt|q!rI}pv+;cGzEsSFW#f(zPKC_KqJL-0+00_TnFKCM z44%nX-W5(zKp3X}GVC_A!GHYJ@qx(;yNMEiz-Z^a@$CFG{HCHqXP>4WB1$`Ds)SQ7 zL37lnX!2@nm9~4VQOnnI!d*#SKPQlq@yrHr2>BAa^UR-Nd{SLVPfP40@=-S~@>_V; zcbcl+a+|wpA+WgTd;a~8J!G0LAbc|d_cr~LRM1oMX+>t7!mFL2U_ZKHg#Gw!-X5E4 zU>ZY}&R<{c0&QBpP1s018q~jG+nO`icB=ZDE@(uR3xDG4vui*oCnI~2szv{Ll1O$m zxiva-v(p?+G;TvALr|Qtd7I(?X(S7e?aYKYM*K@Frou^e|1 zCyCMWry$h|Y$3^_;^kD=`ly5*^SJACCk4c34MrHnQu~ncj$uE0aLql1B_7*n<_cqe zr*=EU85QqyDb<{lUaVN<0FE-+3mO!HknFjpAa)KM&~QtfGP0^ znslE-M5md*6@xgAW%s8;;;RzMOvwX@gt)zXT0uNOoA-HT$j^VYq#Yi{auex6=0D~A zM?7AOD)5|%^S@xwwFXayHrpGBqe$fOT;QyTknUZ3t37_a;Xwt39 zr27gR)-Z4I`F=9@C9Lw>EEThLXnyP0ofzFHb0BcX-m2L56eUVF?Mv3IeI?SLXmcr zgc^7$#-Yt|yyDpP?k|&OZG~-zeA@E`suXzYl%2~5L_vUAe`n?P4|m3&B$h+MX{Nk; z7dBE4lF=!EFr1ZXKSTu@ETW2)vm@F0Nk1XFM&JQ+wV_zD>NU?Id#KvA3P~e;3v)Prv;gS+)oP|G8WQaR$}i~LXrt$MO!3Q zEFv9xv4Igb)29%jwgw_29nD=;#RAX<)X%^DrX|f_5w38&dGFf;DiXvtkL!CT67vd^ zvv+jPCL-5sbjAQK7qSba6!|8XUEcC)`!>WC#MVJ3smx~rX=_=Z9xkhEQ?e`z{Hg*_ z3o~BGWE9Kq(A#zs2s{r@izmBuX4oy(x${-Ru}dj6b%VcPtfe2bb&8Y(CiNS3yI@F5Ns;aiRWn`gy336&nMJ|W8gTt(x^KftrSlC zB~NA5U1?mk#2vC`cOa*UPY|_sUR$r8GSn(c&)SqsQd{>k)n zo0^i?06_3Z6po6&lVc76ky2a-04KmHmY=Y55a0|}Rpm>+RK72JA_69JG;`dQrT)JA z>ODzNL%A?Y5x5C3nX0X{q)4eB#;4ada5t1_`!sa`!W{Il2vO7H-%Zk3 zhC*m)ii;qGt!yrU-~LP44koN4Nd2a)ldr4TbB25oB^nq(4lE3aOFWH#QF6o822OT! zbGDKE6~|nPBIwpa;fHs_!o)9>b6cj=DaaqKd~Qf%B1IIn)!$ksGiRs>w<~(z%t@jU zYkWg308y0X9wJc+T!gnWawLk{@qEy+@oeb(RBu80=A-Q%yU)T*8v_`OPzWP)Ngj^L(cglb-?JG#nE|DB`|=TNXPoHavm0h*YsF$4Kw z#kN*c{||%ufb#^rL?jfWE*{zk^r?x%MIs#b(~Pf~GJ;B^UfqYl5_RSGtpW7yMBOt$ zgz}ept`-2Yp$9pjdD+UML-78FXs(BJEO=qg;5Wlk1Mk2Y;s1X%7c&@l}(+0><*W7zlmHBfu+ZFCQZ%ZhKV6oBG?u+Ew

&b#h1Y$~-Dr^HSsavb;xsE1X0tnO;$lwPxm~tDDTt&99 z_$Tx_%GKExc4(mAg&v`t{VS~B5FI+@?7f!P<(rRyg)ExGB&7lq6q zmGQet8!D+puv159O4HWI07h*&IGfsG_Y=!l!f%}~k?=H$VDCm;7!sXRVzY6CCbheRc4A3IFAYs52yZTCL!4EMhXlAn8mxagSFnDkg&z1ybx5dRL@oW%GRZP^QE=6K~$= zbHdz+^l=t3W2;?W>c~17v6eD56(2GaO{a}fVhP6NR;#LIP2E@D*ens4P&em_Y%3Ze zkLn%^g#OpLP29rI&KlL137+7X?yq#t6+&%j=9UKluUNSo*89yHe8DwPm%vv0%0E9D z=@B`x*!*o6Lnk%8l(Fsj6MMW)Q_gEp7oH{6)2)Yf08FgHBDSw2tbQQcffy;YPqWNz zSp98L5zQne32{gQ^fhDSg~}_j6tw`_No;CkZob)=-)?Rh>tT6u#J67=(Xl??e$C>J}dPUc;@!)bf!a!nS$t>t`^~a*4Cx{uw7kJd1NLD#_8sC^7 ze7iP!03jygrF*!XAFw`g%*Swv*Z9Nesi&=Y(T*=wACejon_sR+Ecd?c7Hh>s6s^Y# zkgnk*A`c(oBc0py#x&kWI6`{*LU$qAW{?W#GZ%`2nX9KjWAQf34@G5dOvyiXnXZPY zOIKaftr5Z7=HZ_MaK;B4Sk3z9QJLm?@wz0*OD~VO!$&tu{?w0C|JpCkU5CPQaVWEE z+TI9!`%b9D9{#y;Cw=s_a^fj%XNHj`R8f6;In6F|)w9Mn7h8aoGN{B4YHgO7cZTG$ z28nL0c>2F#tJ!p&xb0C_UHn-XE#^9UgND$C_Nnk|D;$B4ILI#X)EBjFhQb<{8NOlD z3Fofq_SuR+l6b)yfE*Dgmxk&@t)GbGJ$U8yfxOi9G%mZXs@&ebhiNt?f7|!%%>&Ox z^l1xHodXA@R)K6VN_`G2UoPK0zQJ|Gjhdzr5z)hTL`hzct3ChjDGY=J zP1-q${nRy-wE|Qp1S!W(l`Q{?k>bxtZSm>HF|B(=?`*=zvwf!LPTIVQ6daO#g2^?VC z(u3MdW-*I}_-=@I#?S%_h^9fxYliEVGEsi93(CxDDHX?1?aH=Ky+7BV=P!25!@(mQ0RGdjxg8^DaUW?%e(hz)!YOA+uYLw{A&z^hqvzc zLdSQ36I!FQ@FmPKiXe0dESIegm7D55qNLGm;ZF*P-VE!Ufeg&b^h=@|yR??3h-B2; zsD0=<+V$RlvD*ZEs{$v6!b!VHL%COgi2JbSXXG|`zcEo`+0F9OIq&Z)aVgZ<&xx7{ z1TzQ)z{r)4gr}P8wDc2+mmVJ8_;~Rj+{4)Pwqh{E#LE97m z;}Dh;9P1as5A=;?N@o2rIzrKr-AsMB$0WPU)GbgRq?w9Hy`vg7Dk^t;612UwiHtsCW3UPcHhz(x^{Hi-M9lQ4t-IaLqO zW(&@Bu~h6f5_l&Z22?9NU@%#zYh?9sh@({|vIJ>blk6%eceum!TKDE0XOZq6_9$Mo z%ixX_J;muXfK4u<2zm^Bh!Q$2Pg(a}#owlF_0W*KK?FB+p4<^+c;9R z(PWVU;v$2diD2(V8M4&Hb;HF_LZJYAcEbK%e%rmD*X_@*4*$^!zuMnOPBej3-5u$|vqd$<17EWtk@?M$N^kbvZXEYl7iJ+*M zJ~%kX4sGp}4>Hf9VL!q3U;u%q3w&!?q2IIaaZ3;sweOeR+!>Q?8f5F}Q#*}KMf|4q zq8ih;qyvHtJa#!=@>qZ3EQ`(C78HY6ov_svKs9dF<409{qq?~}62O}6>dE<8DtgSD zK+qmhHl~_$lTCE!a1KbS9!tc2P5>&!d~2_aN0wD6(+L7k4eUC-RvRJFMHvNN`OuPY`n2v$1*p=a`08On2c-Gv zX>lAEpy4B$6nqJSMEP)KlHguB*;BfXRq2d}jo6L&g=HjzjBN|Ftyl+a|Gf

{5uEaZk8xU+M(t3{n*~ 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 ETFBMbssI20 literal 106348 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

ut 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 diff --git a/data/ctwasInputsExample.rda b/data/ctwasInputsExample.rda index 7553d0c65f4cb22a0eb734a371b0fa2bdbc8e8ea..33a77bcd445b29c8a3dec2e154a25bef2cb42114 100644 GIT binary patch literal 106132 zcmV(rK<>Z&H+ooF0004LBHlIv03iV!0000G&sfaj2j}npT>vQ&2UKVgRpfklJ z?$kMYTNN2WFvDQa3Knb9m$qXf;ze8p-Jcf_r$zZ3|gRC`r6WiBjIuPJ*ZGMGDmD-b@M%cGG+Sr5thBv5^w*f$GsD1WN|UDrFCQsyc7$7$mQ}Z9 zo^DWU?E=gO_BcOK@=Tgad1nowSXWkbQ5D9U#6%kC#gUeFum-h z|CT=dVSQR6IeoU)*4RAG=v-Uq#lA!*-U1-QaQJ2QbJ~TQ4wm!bS6T+>DKMDTN9)(1 zB@t@K>ASQ@mJ;olxJo+}rf}&Vmj%o7Qlp0rE}wOBWoT48lsT?CUK08^lGSXyvqmBG zj#tEY?9)-c%;g^Pe-BP0xAk?8Ob$7}E#(I+TE8Wy%~@4<`2Yk@3pZ7(mep1H(ZhrJ z?|ukWwH5rThbbFTEnQ%KlxP`x z=Um7BYgi*MCw$N1YXtOq#Cqt1rN==^3G5xgpjdJ-Op$|9l!3{N|0VVEC`mfm;*+n| z5Q(|+{tR!e1#&p};NGTie-Vu{OP%njvdOQR@DDMLhnvG+CvTPR8~l7C*2y%E$BW`==Bsirxj7fkbzq^E^;qhqD)?6ZPsE@cd<{*jy}U!UILh6eaet~`C|Q;G*!}lrqERe_$)Oa7HJG5 zHw6*U_+Q9~R2_`Zkg3?}BJyb#^Vx}K4?|~Wz%n>x{=(Yy5HKEiDaK@0=HF5b;Kjc+ z9M&cPXG2AnDYX$>h&imZ*V5s1yj2pt%QF`uw|r{07X^{0i_ zfm#GEZUF)_)qb9Z2vbxsHwz;J8;uLvLtS*W*;FkrPfG_VZW<%8HZk9%^9M=azk3%P zQ+|*BBX}(b_?qfBlaSL%7J>^1^+9M=1G)WIpr^9Ys>>at@1iK9Wp~nJVIJ&_72LOF zvRV5)ZvLQuN@7v>4Nj+Z1x(d_#TsIlYqgYx%l@OkYE>BEzrg{qOq}@2$ARm6WXGdtGlM@ELagN7%CCf zH&Oy6$y9oVNuI`4pLH#H7V)aPmA#b0@|kkL+R`hj!GCl6# zqI*6zhKX&hfDPYTD6WQ!My1MQ_yLl+h;>R1yMWmXg9OD1JMp>gV$?r;6G;AE3yxAH zOhW|)ljo$4oSTRJS3q{);v4^66Vr54tG0pm*3jtrYCEhuhWp;yI`q)0yf^7l%R`gB zaof9H8D$QHWZRrk5_^tY0)iqu9U@qyQhQl>?}%#R8*Gp2j=oCb#|n0>ulAJ!`oyq} zv$v8n(}>C4ginLEH;(rWDL~FhW0VYcVG_3W+?UW@Ip;eY0+RVdjdUuju^)ML=`;=OhjZ3mrX3dV>6~H74_Ko8Y|=)h_Nc9G+Bd(Xvz5| zO^N#li`o|z@ODU!-5)kYsPRA5Sq@&PQ&)DUFzYvrh(Z?*mNvvo6ep23f_;qvg>ElM zA~U&-4;I)OV=yv2C^rQWK@8|r z`=>Xo=h4+Ny~>oW+m5ZY@$SK%3bM_`ua(UJ)0DAI^7Ca6?Sh~>4w%!cz1FmIUUTrJ zIRaO8dq2USmkE;;rzgJDCTaAuJ-gULy4^RwDvR`NbU~VWU-1^lsMM5hs-VV-J2mxr zMNQa+85x);2UQV49S2cJ2T^MQJDw{WIxPT@`Lr2@4;upx;|X=*P{7`V5`e-Pb9q+B z+12o6uXHzyn96zf^~ZWb``BMfoNo#Ug$>8`UwBYb6V@eviH#mMSLko?J zO-$i znOgVhqImWB2q?L_r&=XATYn}1I!I3%Ct%Ek;X zqv&4)&s45&+j?KG5IPJNVo}{b>F7@Be5e0QK~^V2BS2}Olrp8zu%wu1yC)}O0=MIt z6EUfoQ&~6{!$)+{6mWiYLnLln?bYbdZ0J;vO1A?%dS?}c2>eGhp6j^Mubyudy(3{5 z*3A}d;EmC0!bP2fBr(-t^et3@Xj4{=%+%xZjYRIH`2>+ey))4)D*3b-f%NNp}CajB@HL`%p?xo*`T+DVfnq2^{H?wwQ>##2nnAaJ=d;J2>FnmWgEh-{6o@!WJ;YQXAKm%WR<(=eyFNUbTyYH<{w*4(`%A z8>@LJ(53IS4ESGC*a<6*;ear>zwFIWZHMIA;~u_bz;Z>IdzLFL%W%s)?r3k4+Xb`jehh{L0#=uo(yJ~=)MwHa_rFw*`Ryv zQe<0Z)PvwKwNM1)n-v6>z@gtjJSb5eqYbfV$6%!cI9?x%b0?z%n?wr-PXZ18w&{)@ z$n%2lIT|04B79rNyf78<-@BGF7UK*s8e?QOxkZ^{;kUZdR}yrW0f{uXcI44D2%f$# z6ALsr7+MreK@_!GX{r`SEjLzYc2z$%<&xHP&9O=}NdxtH*uK~}GmH*?L&18iH_`f$ zlBC_6JOEV5>@Kd3>FT0ouM_jY8uazqWsx96U(dOhRKVF}5y7%R(F4Z&TpI-_ecKUP=lA6g7&v#~ z$?2Phjey(Sc#U1hPVR{(EdBUlji#^uB__Ne|3|_?XL`KWVD(-7G2|#|IJ#AwmZKwg z*oCWh{B_eLD9oukw5dzTA%Y(PM$;74*vrpNvb3t@g!1pIY?Z?-mzoftpxY|<8JiHC zGa1-P<+<{fgPzs}3~pX=f0fYNFs=D-sJH;uJ0FVA=@F0aA;OAZ-V8eViajj4y7r5` zL~Sg)w&yT!t@r7>MH2haTC8v6OX8VYFNEwQIq&mJ-fWL)vM}62isv0H^(3+=dRl1wIt!ilT;|X9^Fm z6tp~<%ze(EiFZi+9rThP6J8u&MPyB{`{T#NXQPK9!~ypn^YlE;XwE`}UzZ$s(|EBp z%_Pm}1_|aJCyMX4S{9Loxt;I&KXc^T@6Rh~){<+GbGSADI6RK=UjV@I-?+UDyqfi7 z*bQ_r$=Yn|dSa>{A=g=3I|mr>!9gLy_P2LsBbdWy&&kZ*zi-DqIIw=emGVVioHGM( zR~Z<92G`JOclYIVspRs)3*0?$2F%x?7!ofb`Mg>2fF@2Rd;B8obl4}eEkc{2Q@G_A zSV90Ga^WxdUF)_5+UcS}Bcdtasjk87*j1GJ4W}~bxX0Cm4Y{x1Sr>I4cO-|#>Dcgm z{2cV|<6rBf((!mTcu;+BL-rEx{Mb^}L@wtI74UB4kW^uKZiqmPmE6mO{cIc5jZ*eSpRf(rN)m&9_}(cYB26oo4gw{%>=?3Oe?ze% zm&u}BWNGxig_CT+2r2990E?gL)*P)_NUJ^Y&Gs%6WDeE@@->n}EDmyp>Sy6Uk8+~m_xjiGi6CqBHS^uk0m`C^-PhvgPMT2PD9acAH zEIbtT`XA9JlE?GNy?C$b+Dyk5{h>*gyDlv<*JfQCG3O2y!{|Zgnd`irOWpA!7Zk@f zG)0qF=>Z8=eUJ8L7-uoXfCRW$!fiN`8B4q~AYVy!f zr|q{38C^(Ch88SaO*GJrEN^X6X0Ef3?CNe&2}|=4<-z%}lI!NpQg74A z7`%I(E*jHtP8@s=a)>6JUFoVX(JN(e{ae*&rxXwK4XzOpbkgPVbGF>MV%Y&BSQ>2R zOWl`p-W+3a9KrA@{h?AsI6_7=%-Fc0A9l|8eBAiZg=eH%;@RRZ#eSv?7CREe6Vm;f zKFh_rJFS1jp5%ae;mVCm8G;MLla zpyz#BX}4jtEgd)Gy!Q&(wsh?+03F7=*^!{d#pl@@FWuo9!os)7&Ld*0n^ZGj~K4}nrk)1EK5yc?KLcWk{q z=uxkB^FpzQ_9v5QaBwx_Xm;H>sELp_{3fR+Y^u*uF65XA$FqAF&DpT*7~2gK-!a~h z;Q}0J!Qgv&xpT?aFUFzIpPc)u62a2uiMx$!&VmzJQCOzEWgK0>h#cpsB^9_=mC}8O zJ1GUWsHtoBB|&f#-?G)%GJk~6e5I0t9Ft(64k?#-w+?MYyS&G;J{u`6V)^gZ%S!|k zC?<(GA`sUIjXCHPO!lIZ1ny&nR3D={{5Duo67=m2e!N(nfFw@eZ6y%ECSPqGL!~(~ z7!C6GQk?sXG2pjO@V1DOdGKAHb_XCFhIoN(zFg7|3Q2{8E^y&+gV9-}m$CwZL_f@@ zQVW(+#Tdb=vk^2iF)zFwhUg6w!3Gx-}m~pE2TwbXh4< zkcyV8=WixHW4C#>9|2O3$kDK&FTxhnLvT+t5~u`I6m0l0g5<2`g6{48YOvuXNU<}0 zYa0DRtKts<0=G@Q&@z$Ca}~u zp^d7RhlrDQVg}|#v>L|0jzI;T8<_EEcM*EzH<8XDq0#imJ~HBTmV_R?_ymmj6Zqwp zMh1gLBl)D~wa*)geZCl&GmL_}VouzW$5O}T%S>oJTF+A*!fKq?ENu&6nBMS(6(RpA-h@kC+UjwB_cp1kZr=OYwhFL(^`bB=y4pLJcc4y95h3>6h z!i$xM5}`(y5i^u(S5K#)^>bh%m5Pssu^Fb0RkWoRqu?~}XDD*-jZKeU6?(3|4$6dZ zgzCrIQnEGr`P90Q!FafPd=m^3x+Hgg_X<1FFWSQ|!UrK@EdY;0eYEKb7u-cLiVr}0 zod}cDaD}Yu1C_qVBKh|Z=2N~}`W4wEiwQNdIz8J(?$+2^6a)y}oxH8h3=W<`ndn6$T4TBJOL2( zx!Vp^c52P()dLt?Z~Mi-vrflG5NBUcr!_lm@QL$q*e)ep&7u+zh&=X^ISKdqFieWiS#8&kSa$oqTR?lTRG zb{u5LA%20+>YRaP%Wf`^wjh4%_w1&Jm60hT30+m`^L+YJ*@2fU6VPPwf^$64JWPRX zGHi3W$_9ll-b`|&e*X+%&FTyjQp(90{U<#a=-{0ycox}e-72NwWWJl%qGK-J4BT?3 zm8Cu*$hZPvybzqfgQNvzm*!(TVA}C1?<~3#3y=X|<^$Xb4VxNLlOupk@PciN2#qQ6 z0?gx76@RjEc%xNlgr!|_B+|TyXb|ylgy%VePa2{~=pZ0HpP7pzDk1*sYdj0)35ba^ zy%#@8-+8n`oFJyT%doKVwTjlT7x%kU@>)5GQV_x5e4SLztb2)>9qZ%sZ&dF>oP;rDwO^xX9$Z{ql zUi*BTkf~2W;x>?vDl$ZEVujFb@nhXVOQ628Lb;0tQ`eHS^s&!XWs_AxV19R!W&pip zHv5YEFl0`-nE(d960#lz^Wm=!wD@w9=s5a2 zWDGb<15-v0{2fS8m@sI|%Yw|*zSbckTdbel4UiX*K=E+&ky+=AUeoxxnppadOHfZ{ z%DOPclLLp3kj|mGBnSA@nyN8_y5zk@D!79K*T~T}H5w79vt>InpZB^Va9foXH-$;P$Xd8{Akidsosyh z8A6-up#d_v+uqBLE(XGW2x;Z@(Qb61a>fDvPklE$s%b?u`62T~Bx_(?kK+SMgc*KA zBmft0+PYRHrcNxRsQK@k_pc7WgXxZbz3467@pDWq0@1geRvhm%0#(adr=6;M)zF25 zb5pUCc88C<8XXkh>Y_5!a8lNpZx>?+4K@S^VUVr&%(8FfJg2hQN4nSdh|XC9I?M?B zw1&&gR8_ft8YCd6mrq;YGeN1o+q;3@8%F12PIl=@9Rid`R~=OP^n#~@eAJWznCx_H zV9;}l{~LW~syH{xCHou0rDNmwoY)UlRx@`iobc1;B+ zt#yX=U_bXgnV@&i3Ryy)vDM`KaGZ*(OsEmWx%&G2JB`Xf#IhfefiyWcKZ3+BXctW1 zu}X<4pU+5x>kqXnyw5h$UHoWaYJ4~o9s5=&e z1;Xb2&K{x>D}2mU$! z+hN=xC18ANj2y2WUO$rcYc8Ef4EljzVG{{sN|2wlIi!t}FUa}nm#vD0Zle3tN9mp- z*634-p_u$S7Hzcoe=H!T~2<2NEw<2QOgH}eG`&4TYXR5qxd$(ZUdO3bG z^7!BDE4vQIm$8CQ0?7OVBGqHvZ~MPl-jy$eonGSZg<<67FD`gW_Zd)@mKpXGrJ`s7 zY}jI2EPpgTJOTcCHnlTnfALlZofoJBUjqU6w_N zDuJc0!a^E)Jx&vxlquRe#uMg4mmR^@3nvmFHW70#IDzF0aQla{#Tbf%#zAQ8z0x_= z0$)^tIqF&;DQ{`$`u>}nQe;3RQvnZ)Q}Er;TWwvRwX<-9GAf54Gs7Ld=`-1PFwkwL z3d;9bz4`~XnSbN4roulWfsPu!$Ujh1BLOwibMwq+g9E@U>6ec1BWBEvXsf7Gub^Jy zQ-NNpdWD9{jB&x!;^qiqvHrRge)t+aiY$SkCg+U)&7c1I6zhRF9}kTsd+W)s9x2HP zK7xB~kx?~genkj>kms<<_+~NS*fG_M>=nKgpk-ps=tx<&3AEtqk)!61;W3t50(^Yz zF4={7IjUOU^&6(W6nN3#K~wKX2fJ;hY7SWT+>>;;+_-sQL!cO{Z^7;NzoE{-ir<5G zVip=fmt(ix-_%d+f)}1P{qpTbggWZQ3R0Fxeah@3H_ZLxIF{Nw}Z5! zC)BzZ5e?-xt)EK>DF!+0P`zBrJoTB&0@d{`W?C)vXk*LGZ6>sVmq>^xsm74RTxtxV zAz&E0krJ_e&Y0gOyRxC|Os%7VIRt+!w0&kU@V5to9DyvANd+%D3;~^@2kzBw=c$^S zd7P!;XCC`1r;$E(~k~+(W zqV4hZq}gx#w^-5hxRWgGDW-K821)StSeeY*ZkZ792ceyG>$sRT+S)`$bWdk<^W*1N z#){@-_q$MUwBZ1%Jk>;anmISgWAn!nN2uT(r#^65kJ=}e@!Uo_ZJLPH2T=#P%>-Br zi*}J^NIXuF{B25W$Ovq*q-jAFyxn~*p^a%_c6(So%*(WNUCcud60CK%I_VBq4Kz|W%oc|U>h*z-SRl3E!U{#`hEZfiy^QAJ zrUP>jK=_-89IiGY?b3Mg2=OeoQBNbP9`;CKL-|8fc1EHhMH!yX#%{fB8$^ ztB!NNOp#oP~<^ynY!E79dQ zwpFwsAh8Kd5hEfMkg6g2s1x9(jbdTD??7b-G%RAn@7WbMW5b)ihqkNUZ8cROv#&Da zD$26GQLeF+$>MhB6EDiNom0x*IZwoQiUe!Ja49@AA1*w*wFVXe$LFwA4OU{bwV=r- z(`BE=-B*AeuYl+&of-`TM~^bWx+Pw!R)~^-KxMZseX0PQp$PvU!@`}l8fFDK0yp`F zb`2vIgQCtH`THc0)C1-@RJVpBHGc$;1Fbyf0?tRLTZ37%3-!lc!mle{9A}R|NrkN@K(c{-|AL7`SBY_*a_t z(^SG)?{1NcIYn)qU8r0&^Ue;~@3VZNt(%=$NNR3ab%G+A+w}qCaNs3hRDSt<)_6gT zcv-Zh{N`e~7`Jro#E?U@7b|d>%(}BZ%bb)AW~Jsn+m>CqolDFLgm#N~biM2=N?H(b zf$7P^6~CW^j=kK1O?@*$P^^GA8ieeh2;uyV8%E7N8aV|AF*Q*~Yg{~beP`g4-lH>M z?}tG5U?n0f)*!Pv6avX7E-+Jx=-c zsm5*romOn3xLS?*v*VXXh_nC`a)JAXe$4FFQUMpV-nR!PkVOgKn#BsJl)su9is>+E zb>vg)+`MkMl!%IzkDj5_PfAw8pAae~Rf52VLU5AMA;=pkMQdkNS{bVsO9RXiqy6)n zAjH|rqs;K?<^!lxVt0F<4lsD+0%tgkvbv&Nkc_HwP(_|>YlTlOtOaMs>1x(*;~om2O>+h0&NRhirYVkz!fE5 z$ix2K${zHJnZqxZG4i!{kxpxAT`fVh^eVDlKN@T2NrB3h#1YVzFPU!V*(<;!W9v%F zJ=(G#noyPFw2h3)SVuP4z>kSTB+q5$m&>~)##IGkl6K;U{3%WZMtkg2Y250@H}XT& zs<-O8hPjh8s`QOhriBhONiMbYUj5N8gFnuZvTXS!mpSM?RB*~~=;(h24Ve}4ED*#e zoC@p{yrtHx4Qq};C2uH-s5^8;JBq z8AYSWauT}~;$fb4I=weG-^_4J3L_(Cp@on{P7aj0Pe8KV8)?OGHdJH|+JU~R#IAF%;SE?o7olB<^=>~Z^N!uJ_ zWHsz8d3kxT7N`uTauhkm*(_K=TV>5s`;37tIG1H4j7h|{O(yo1zro_UdT<}4zmqY3 znU=u_T}Pqrr;4m8_VQCZ*osuNIKD13HGh@0#k4G4}Jm4xg8BMA! zPzpIs9_6OV`q=Vcw66g#82l40B3k}(3K_6{lAuG~7Z{P;4*Q0VH8}9&rF#V-5E$ql8k{G(!B4rxG3yX*zg4pOND!RfxWNaR!y+<=lOT)IsKE0X zCcc2HJooPCdl`^SVlQj=`>d;+`ctx5L-u3)K%}Jsj51^tM$nNjR~^zg&&`Ne3RR%H zZrWG5rH_QaP`L)TxzhDUTI@qOMVN5GW+=E&yY>lt)0J06dUdwsrhIP&>=i(|kg1b1 z{2U$uYBm<2s^qH+b(|E=fj1T^4cE<{0j-JOE_YcXIv{djoq!XpmSo zBaB;MQBcjQ|I|u9_a$PNEU1s1NY+1cT+Rr}cX#^oM&+omTX{tk3DXdkOt^dK%2>0h zsTeUbMDxo-DK>F+L0Smre4}~hsM?)EE(F8d5I+W|%RAC_OZGF{PcX>L+Y!LZ=M}&l zEY+s6j`6BI-x#4t2W%5I>j9GR7(+(^w>4rdx7{+ai2SwZE_%_oTR4pDL`^SYSPb6C zVeT08B9{+U+FRb(GWB(P8m*0~$8)5A8P|Od7N5bRl9s%}KH4W1>7AqIEQswwtHlV! zg`mElNv6P(dy!IBxaStFOdNC+#JnOC>MiGDD?V=C1PT7H^005%G~L$W%F{RJzIx2k z#}s2gp%ZUvu3SSAmxT4svM!pp7tK3`23~+pJq?M@{2ueAl8Dj+DJ)9&Vw2aC$-!zL zewgMfvu1TD`S096<*ZxHeWS9#( z-q|}I!i1H|h_efYjHIu2zekmnnB|NFxn!y&_Hp{sNsU=Pj}0mWo;^?VJk^E_-I(*2 zgb48Cv*61TPXge`^!n(m%IDE^o*eW)^UiA|>6)h_);HEynpHN+Ef ze9)xP6}F6uz;*8AR4pMEdzG@#2AFN+a{)r@p^9^Ac<0D)*zmq9xL(IJuZScvf>WB@ z+{ICToXExP_e+5T-~hY-JjPNYWFV|Ap1of%(F(DxI<1tds5;Sb%lG*39Hvb&6(tXs zUqOw#y;@gIeO#e7*KXoSxSQ4^BDB$jzOkQ7&F~9IP~1{g<9ODYQS)q}0;PAec60-G z(PN*ieMtLG*z@+9yuu|5zp}@Y6L{{2JltstFMV06%(yD=b;w0PLvM)}EJ}itWl{@i zy4QzeVUuX`>YyL=oTmTXCHn}S5XX71GV?T)W<=9@DG0zHk3avpy<*Gg!IG2b8end9xJpHC;t`yfcF!Zxa z9ww^JSh1K;_rXvikJzAQ1fvOSGADMIe$)V_uoh&`29w(~i2LcU-!O8L-|8cR;bmhx zK37eA6g3iNM~4uEy<2F&jF;w7jR~YuNS01MA|XPe0H2WO7ub?&p~bK(YhpyQAC;XD z(gpCPGh&Lttqs|F{`P@LvdocWJxio-($}@yiGClBN#A)~n~KDO;egjO!PGCPeQjO8p{J5*=2b3A%Gy$J+$^DwNKrm)~C`?%I^2yP!FUrovfx7l3Tz8a(5 zA){-FLCl}Aj8a{hBq_CpD7LC-Kxb#$wFOQ)etSnj~+o090sygxkVfI5&Fds!`AT znqK%RyNIN81cM~&?rV!Z5zeE9UY2##P4clUo&$`28WH^0D1IfY6EIW6?t86SnW7e9i?`dDwStl-(icL(a71a>}%WKQ+!x1 zOQj`N9|6sTUBUiJGyUBFD0(_VH(BJfH+30(C$zj4%SzcCVUqUBUVv*UA#EFftrpA9f28M;E!9200r`iqgd&knL$uaM=N4c1 z)d2vXonSZzGw$^q`gNcFS+dD5lk=maAFT)?)@i@toI1K^BA%9~$`#27L}{yb;b+vx zI#R+Ps6xIdxOZt@ltv$j(j4(XXtBjtP#9omyko5RYT?F%qS@cQJH4d@ZPd#Cm+#rn z{`H>y!#Yh-{Ndj|FdV(4H0w@TwMf1%K|GwFl@7V&3{qN7_&>bzzFzXlIj_`y&k zIy6BkK%eVcE2)heHbWqGjs%jg>kM(a++KEYV>h^Q9Y&LQHFf|A#dSpu^ZJJJuN(c( zRgRf*9x0%6pFLWa8!wuVIQ<{$dFR01X6c?-UEKAEsvx!NiWBi}9IJVdvENcsg>(|T zWfr>&0b-vIRdl+h5>}f$3N4JecK~iNBA)q555rd?}RhlzVs7b=git`dt zBiH>U6TI~~Iy9Tb5XoQp1tLaJOm$Q-kVYNfN&NdDa+>Gptn8)Q9j&yI?GmJ5SNs03RRxnk< z+wrMxf5T4y%~1dK{7C@Ht%gj_=oI_7{JK`nRwcUtEX&mfyD+{T0&^+cM)iPY18Y0E zslU=F=8MS$a)u0#fKwg0B!p_M!k*sB@5ONm({v$r+ze4xScw6?44R1+*b!8$m&Hk7 z`!^3c`(8U9Hs)SRLTyV*DMl)szuRsp=i;%)BW zxKVle-C_IT#Vq1je5a^Z_(5)2urgjn6$IAx=Qz`-Oa^(EChg zdrmh2D@bM00U9gMB%!ywam|3iRX7}O9M^lL2{W0EzNet3vDRXn6Z35)!9{1ZxooUF z)?g#euwDEIrJXnA2|#*!4t?{C+_^jxE0ljPVR8D<#><0-A! zL8AR^0kl2fMavOnurV2py&qLEPh5qh9y7JHHlTqgtA4t5^IsZYVKj80E0ZoY<(_(k zSUe?r0roTo3`6CyNDJ+OKDs%2UAo#E8sCYB$ufX-*c0kb)c>&~>(1a;FOv_7RT46@ zWMZHSB?Nv%vJiF|B$_6a2NT=d5eTTJB5y1e9e8?6NkNA+v)1vAHBT?C=y>F=4z)4k zsK*HdksrX{*FYjPYv(<8w!uUN z$byah?PTc(v=w-yOGFd5Tt7{bg^lp@$FQ)el5NQD5X%L`2eckD}&Xh#j%wIREYBEb$bMlR`X)OMI)_ODNF7}hTud*cQ_{{UtIxdiFn@4R~OCgLpWe;53j04 z=|R;lFDr;ip;H};T7-p^`j)0%gNsuxJO$iM0ZQJ;0s7wrCL2pPxI(?d3r@3ENY+ss zwhS)6c-P_b89f%lI0^TUJ<9@QdITTBwrz8^`4MCklb0L_H|tpdRuA<6P4N+Izr}4Z z`NpYP7`Y4YaYq>cgo1EnH0BnA0u!6fQQ*9H#m~T6h(Joxop^eUq~;c8zikcHdmXLA zBsoFEJrEmn(M3F+DZ>FE96;c5vB=*uRAyqw#}MQrI3lf*ZZTuwwt132eQb6+j;VE>RiEC%nJu@9JC!c zwt{gC0jIR+I8w*)KGh+r-wa725ipW&oG*&ztJmxxM?SF%wHjBg z5yI)|T;}RYY4ANDcvWXnaRbdL_<&{9F!JHe5E1!jR-?t$A^V8qV^LyNuRtt~8Ed!~ zs&}N=N3lq37zuJlQNA8YQP2$@&^EmyKU^zzltmSjT3k%8l)GAkMYRo3*ckA-FrRu+ zHRP%|ZmHfcBx*ufq6MRHm{FR<+8OfCg0)^tdS@-q#4fd70Rx~zl)OOU4Ng0-E%&Dm zl8KJz#ll&f1#=xmOpqHEe#e8LZH815#4VLioAUxow15u3k?mjW`&R8Fe->Yu48_Zx zif_A^(X2g(_#1fI9z1o8bHC$z8fKKczj6*JJ;pj0?%;t*>V@yP@e01`3x1WwW!;$7)e*lZeN)U(uY+1w|L}!L$R>y zdGGZ!R&^w2s8i6hH5!O2TKc4;r}TAgfMoQBGUOuziM z8GniiJzl49Vw@P`WfF`N?E(6wDc7N97o7>4Llcz(A_`Z?;oIdyR=ylMhpF9df1=Tpc>+VgV3ueQVv;#eNpwLck>U&tfhX6kYj&N>mi7WYaSuaMr zPQb2GKc-*HKVx0W&^Z}F&v9Ej^}wpAq-xS6L_Cmk9kX({c0Bohji-;$rG8f8bO4XX zw2N3w377Cy_ODp;pb6sk@Rb2tVUt3!iR%4?HG$yX3f4Yg(I2CD>{nV>gJHDZFrFMf zGWnWyVmKa7&|HxMJm_;?7rmR!2Yx$w;5+!GOXfRM@=gaj#om;!{^N|>6~zh_UPcdOC$sd>+!O07Qk;RF!g_CSh7cHa9!TUIaF`0(;B)&vwa=mvSC zHO}D$%<@X?{7^Ks?J0C~jgF@38Z=1P|1*3&EVbIPtyf%i5%PB%^laa3i(`T9> z*J!7;TUQ-;?bYo@1=RIaz+#e#m{-&F#paz={{oURP`8>pO^ zH#R!*R)iu(Do7wt2)(P=b+dCv9I&mkmZ*MOkRlU%Ml$I`I4W&tV~v$GVictPF1C+3 z*ECLvNK2m`JH6sPjn;%w3j5Cym_~4z9E7=DvS%oJS-Ig9A3tO#s76D=2%kS(%G?~> zH&0Osx@G)kaLk!k1OGV+DqxERj^?0DnH@u!m#8 zi12Wcb4P% z6jFfR$r@vV0cw8)@PzCE0we)auGvRfv?$rpBD=^oemEplblbrP;DO+K2o;(%dxJ4o z7UgDeW$VZy>W+khklCp@BD~+kz^|Ei;>n*NeyddO$57qBecFnp%bA1?AD-Er`-*#h z1%5}Uff%=*Wd|r>SxM=!CdAVj?=`P)ZOjlEu2Ud+b9FJCvkOHoPbMCy<4Jx&P50R- zz=_h`6%pDxy;=ZXfoD0%rFIm#TUw)5KArQ|6?|v3C8edy3;m2^UG_-59-I%g&o;w? zt@?;yELsL6-3V@X?^Z%>psd=2EdjbMoRq^AsZP)Y_W4edB&7F=I4#BaCeeVW^a{g) zX0f~E_XD%v#rJ<{l}!iBv+ipjT_eR z)8?)3(O`?OcfVV@{ARWhdP>mf%}fh)hNXkJgbg3Di1f_ujm~kSd^u=@u==S=GPgVSyo!lr4J=SGGRafD3`{e9X zWoQJJ7PW9|JgRY!rpWXVd@s}jb1jFXM}`)AxbcAG4kQ|WCO8K`TwGizY=iaY|FxeR zjAa1r5%d~zrwB|k|8Ml*)V+T=dw#oj&?7nfc@|&ok;x1f3aQZKphN$d(NJSWfKiZJ zU#B#ZN~zhMs#&bTdM11|xtb$iG--J-YF8I50}87$nmrTWLaupVY2yIMKbLA$!mNAr z48W{Bwc_667x}9Jn8a+B%riUM(l>16I%7Bh+T0aR0T_!CSV_xbkkz3;x1|hKi|Y~u z;+Gl}3K4acZ>g*uN;2rCyZnr8z@Opbd?tpYg1p35xlQxsk4lL&CChaf^!Wa~=-n z1D;5eAOPNtaKUWCUv%XS#KnVxn=In%L-d9#F#yx_rHWRR{#&Vj^nomj94UsHW53=Z zCd#rO+orN%z>mu?Id!S}vI~0>*g@QU$}Xq08?h5yPduJ7w#QSNsG(N{A$nmb9S=_< z*3+CRst(U3fn&odsuxParSxsp{vsjX(%HBUSqnnOLJsHVUOrsGkqa(=ncNtY%OAKd z7J$7moyUc(n8D-TS&bvwSR~bKln(|@?8#?J-<}L&2}%k9!Xk~ZCXFyivxYrvhXSZE zD--#AvF&Vh4}px{_72g^D5*w;h5r&?!qF6K&|}?abKoYxju;B~IyFBjZ>mE=F7Lii zAp39c&h6(qDqfN9S=Blt<5vMm zGw$O}`M*RlQ0-M0+3)@P;IHU3^KAC@#m ze(aIkK|Y4RN$W`mxO)J63222Om_$uR2V!EwuZHzs=ct&n;BV=(ur{_YHrno&7(K01 zmcDN_#}ssF*c5xze->i|SXqL0&HS==NMII&p}nFy7b1d(AvO6aFq$m-u>Ow{#t{8T z@H^XcBx>($w0=jqH{gj}(U95BG>?+i{PxRWaTBmGoSDxX;i?Sj6>U+A=Z%8SR z;gb-D9=v^&(4v{QKiWW=1Qfmo%A`_s_BJ<=Y{EL2zLur?jxGPgO7uLNn&EPo*nrFw z2(6Ci35@@?n;#vw*%gV2^9i6}k#9VFB0Uc|{`I|Qa_E=;&dw_$Ij@lu2)DSfzB6X) zUZc9l-#Lx2whyf$k*6=@MekC_p>gvWBe68+yb$O8Z?WB))A7l8Z|bWioh>Xc;~z&a zn-Eqd3}9s*J-Y4hlkMliLCGe#7|qlr*CQ7xQ%9Rxw+b zh~Wx8VwE0eJ@1rI5l4ZOMLHQh8RbYTl(6FNP|Ag6jT%-gLOxZ-5~A>m!C6E1k zc99;IVmj1_#XgOHF|Rl^uMEE3uX~O(4h$>uh++5`iK|6`Diwaet?rXz9cyk;DuqJK zQHs@A(29nW1bW&%4%_uBtR?SsWaGpJc6wTeqqQc;Rvx<;&Pzh~n*V3A=Y)-|HG0&e zIzY(-C;b#b9OAtb{`>tdyEUSC^TL}P@7}Wr(cG{SkPV_lD4IRiLQ1K&R})(yrtcrd zD8RfKK=P{-ccJ6o7v&CI2Ij)G`5bh@Kc?oMwN|#xc?EP#QQml>SNV&y5-@I zr8bF_q8aCuS|Y{b&WX3dWw9+JXiaBz}(>sXr2A%>+m+v1#CqUcF~ z&8!HEkqiDLFLYZkCOxVsrt$sVR&?SjTePnaiBz&TI^;b*TZi<9!sDh?Sx++o*iGt$ zqOwLgxGh8JeN-%DDBM_sna2)+IYo1U*Rs%Xa*IYT+}HQHdXS;cW2h~u5-3+{rwgyx zMAMLyw3`I;{kGO%*sj8c7aPW7AIXkC4S`o=x)@%8aK6l_QllFFadP=a#&rV`7@qQ@{4$ zH!L#KASlroinR_Ro9t1H1WWh1NM}Pg^5!LG3| zOmXygIksGWsJb7IkdHsBB&ygn*LGjH!;KoOCSiaACXVi<4P#USp`K-zg<~m&DY$0% zqVFetCO5vCfP(Edgq5LfOnAit8!lY5#@AaMTMY^)DNAVVpDTmT4vJ9n;Y>Pkhu9iL z=QohyvTzt)jNU#@!;P59q<(3RwnRC%M@UBN>)CaXEP4i%Q+v9ePUAKUMZk#!P8?Xh z8~_`%%C`f6^yfb;YH7?1+Fr`%T0rB1?q+hiFlfk&w-gaqlotGWJ+(0+w%Y!4LBhc> zWIxP8I>HvXqV9naFz4{A|0~(`)r^#Cxdm|gm#`YkpE9tK@Yd#T=a+LHiU4(Fo9Dqv{z1 zIQtL2ip&mCjs1i2rD&!g1glb?cYacFQF4WvKET_4Ng*KWR`$C#i>UnML}`J+-F%t+ zB<1UwVG{E~V#X;`42r1WNejbp*v9A_8>qe z?O6rwdq2W3F?CUtCz#aOfk%TA@W4VZElp8PVrNI&Urk!626Rr_B=EaN&ez!&jg{@tHZ!=MbU0UBaLbt@&t)t@tbKH*;k}Z zzk_#!bfAmBRs)~l8tj5ldC=-Kdx6GH!7b9MfIa_*KqOf587o56F0xHzS7WciRt3^& zoZA^R_$m<{Our-8l(NSZm{VnXjWC?y$(!QQWu}I!xf#uXCt(JD>d%WtOUIE_1H2L?O+4x4{95C1@Hi+^uc( z2NZ54D9@nKin_J+H8tJX;Fef|k=2iE^I2l`>+aPVj+tOZzC$<}*rjvyMf+~iNT^)r za+~u7-M2tiBGB`owGDhW*zR{cTnIb#yE-^uphNl;Y4uie8eST>gu`elmdAsaMXKKY zk|SZ1;-`@;#>fFMX%Mc)Fc@6NYXZ2{N*3{QnW?ad#G{2SB@a5Kp3_zOcpu-ST^sW? z8nq(>kkQ(4dcZCU9JJom>@SK3OmZP`Y6sl#goExnWfYSoGBNXh0Z};8HFph;jX&h` z0EX*&1wjYLt(bI#jMQ;AinY~ztnB_t=@N%-A}yeU1f_9`Gq_1)(Ty*{Df;0)*osKV z=@L*N<#Yk*?+mn5HePCYvx3;pbMAxjnR$01>mHDoF zV?QVbrx2y4NtjYXOgPENGMT}lrH%5fG%1x<>?YmR+#;1SAw56)>7it21mhK;LWq|ACu;=fcWj_QT>t>he%`HtHAx|74_6Ck;=lDCc|4sI ze3b2NJNnMxn^?ApacgTbGzTD31Hp2IRLZ1)Z6N-N>_FQV19i<@UupAkeSU-RjdbCS zCr|IIT0l%=m3Fx_$KpY6AJFDr=D%A+HA>2MYXi9hM{@3|G~F2&$eW6ry)|~lh{5(i z%d9OHv94&V&;1YOAWXun084BD+Hy@n+S2J6KmNrL z-~YsTz>xr>bFj3!pO~h5?*Z_8#b*8Of8bJ1Q{DmqvP1{6T80z8FbZCK?q50GQ>J=x zgKX(0sB4p+TbwfCYQAP*Mv`zd>4f@;{niG&>E_)jbeA`U*;EDNAmwMYU`N@L@~U^} z5h|Sbtn(?hRNkm2yfj1}H;URhf0jDX>)gPwb8=h_a|pjs+pgYr^u5@A#Xzj7i6Y%ja9>>LFjZFg-=?vjn=~voP5LXRi)sKDf!eG9#V^WBcD_*rj4 zT1*v-iO^R?@9o80a|4Mk3q$mvxT0fS617fy3^u*F4Kh*2R?TYDT`=g6p&|{L`K0wM zrInOIMZ@`Pn(3aaa%Ni5{=h^OM%iiXz{`?@x(Jj&X|IOpo3e?I#uJ>qO%T&C%lDng zJf}I7`9~;=)OEXGP5+q83RyuSoh*Z+EwO>+hB^f?QX zenK(e;jVfBRnwDz4hvU$X<{-l5IyKqOw3Th#oY%THj~gSH?`vi8#G=$>16X_JY;^A ztLU$Df6qn+Z>dRo4<#(U&kNWXL!a{>yA_pY8uL#*kMuIh+-l1d8mCu%cD%goOKo`w zB*vH(a7N~IV8dkL&CTrUz;)FcaTHianZo?tBNz2F2b8elBRU>)(sK>c8MgzxbBDT4 ztd{wO`)ul?NX(o%PGo&|8{Cx-G0C7yx_!7D!-)p0Nv&vVE6+#3Jcg7cItCnKLIVk- z?#P>e9P;4f9HF-vuSgdJZv-h=V^D61JIfG$Exe|tsl!Bbr=iUdz{yz?5TCAkHfx-P zCdq=Kf-AD}tha+ULe_8+A0p6D0`T=Z?DPL;`~A=+bdGJksjfQOOK}1c2NoVy*Lu$s zkZyiT!3qSD=bo<%cd##R3}cdiOuK|T$f(=3UT@xyAqS$iT|y9K8uzF7AT@{q(c5X+ z#B)8)x_9aZF1WpCG3+wdm(||YAx6{AVPW4(@*WELUcV1J)#EP&vBJTcwN^f0)9>+2 zQ;f$K$Uu1_%zanGm6S@%(zhLyzw5X|-k9yYiy&_GA%LrgGidmZy##ax3G0P3XDnw4JD;`(PAIYtQc{rF1(!PDZGoU>#@VsLB zkKCn4O%80Q#^{Dvt!^ zFQv2zV$r$E1bwfb1Vhvs_B-G)YHm#_(D_!0UNr)ITfT3f77fSO^peZ1~H1v!M@A5+U0caviB>{ET`efn+p=1TU?mG zQJ!hNBzq^cg{?TCYOieH5J=BU>d4E>d4~FK>B~zrx=nZ~Bg99FNt1>=QSgjo(CM4q z|1deq&)zM9k*s7zEk??>LKYOhF>iD-m&)?MyV6c;M9T((O4ZkVBjz9D1G;_?MN zp%}bo&64KwVVFbn9CYdp+XKylw9xa-(=d)`d4tm#3P;aQu=GFeJ1A+Q05Ib{9dhWSc59% zMzI;QdCi}uJ%<)jmrlwo6mV%-YEwD2{O-Ai(?*;NYFS;gKK#B^maGp-hvO2MrdfeN zB>(>9B)b6|>slvZ9ijUy&tU04L*r5^Ur@s83rbQaf|>s$%+IlsG-tuH*Wzrm0gQb3 z@&$ETsEwm_he!2gL`^g@jcN9a3vvzX!niPZ8K&o1+bCcpxT~j(`X#O$ulM@|ANOh` zXN-)`+O?lb9;SY1sz}fMyntRO(~B1Ge350BoIT4Yg}|JE{x@%sr8lBkQVwi{I|}!h z$E+=4?2%-|rw9d8Wk{R5_5zVCoZRJ&=Z9F=X=B6E$;0g5@v^NKPyK`0R~>3gR^L8Z9> zhuGflP3*G^qt;V9!pVJA_Hz!Rt(s$ut~)8Sy5poALtN0AZFYx4=Mm9wRYm->SQMUo zWfp3anXoJ{1qf0Ifpp6J*0c}OU}=i@&Vj?-|5N^gXO?}N41_z6U-m?Aco|;UDZaZU zzzY_>ksn#tbOsgSce4P4>)e@&U;mKOM#CYIo|R@l8fPgH-sR5v8kF!JeKox~o;K%=fr^d|^ z42TWk_@|G9I#>nOfrTg#igsYnbL4+|i*dLFT!b}qo*A?DT`E!~^2kkPDV#OddEAwx zs<(MU8{Qf|*^wB?=mLZ5Cn?E_;=^a1G@W5ioYH1}_x``$Cjt>aL6O=I&i3Zq zHmNWmJaG9{a0oT+!kCNx0H;@ofCtXr;UuoJC-#ETB!GFylBn+iRv#KOzUCcbqf=@c z_vL_%vQMiD?UbxaP_0VVv3KJ(k|a zhM)T}&k8%a&q3w%&Ov-?jiAiF-fzH65SJ6HBk}+`XMc44yzX^LXoR$s!<(1>f6vYH z)Lq;2mLdc1P;{o8S=gcGv3cEN%c`&3UJs-P8EP_9f9%>`d_ESU5Z zNe4k=sc;eEwi=RE;v#eh0Z2gxEw^;ImoS|hQb&xvUyW2M;-PAYvmzCxJ$>arOb2rF zXTzqN+BKfhtfO=6V=f9kaXh#-A%1;^LhD&l4DtCCM$t8;%)c&3&(dK4iX-(HP!5c52|L6G|lPGZgu0PwHU^{Ag&WqMTEzVfFi> zKPYlc`}HFB`Y=i5F;BXZxx;)tc9dm)`WFi=p0u>he=DLIE8|HZu%2jz8{uFJB2RPm zBPSh~RZyW7o|X8^9jIN9p2nUE_kr^|iJ#wF#NMT=GX=t;XHH$Rnjq<7mQTKL%{CPJ zDuaH1b;EOnF>rg_gel_t3K=1HK@aYxR{t#Gl3R@`qqlzpK!wJu!@#?7mjQY4m;(~c zNiaT??VuUsyt*A@yq4;f-aT=CH!Y1h^rUH97PXAcj!`-AarXt{6lQ#_Q7glM1lhQP zF(9AnlH?-2im2#jq2=a+!P8l03E2=UVorM5qmkOpuxehJRiezUr1GlH`SaTGFTc8? zb~u<4dEa|b#x{n!Q~zXyMkyy=^drQoL*{(QF(Ii}AyO~&tllgrwrcJ?L0(97a}{_2 z{CD9r_gI;+Ol(b{{3=@tTmJAX9Nxr-FFP>x)^K8%{o|fuCe$DQ2IGyO+2f-KU4#Q~IR4o=7bWzM=K}FaCb+XaXa9>&e9G#8h57~5oVU_Unb&f=u%{<$!7pV}x~G+IPNmPpue((g_)}aMUGJ@Y)nHf9|?) z>D+7)cq&WU`ed9|qw3yP8Uw-u(cf=VO~BqZ_Z3!5(#E3W=m&?U-5wt#whRC&EuLfQ z1R193x>#$HiNvOhYw`>X{^EsjK3$rDZ+pNtX;*4@R`lfLP#4<@0{OHna~h>0o;^y9gn7!P`tgb7XtKsp5<2j9+=X&7?x*4FAWJh6UzvVxjnL zI1`Mgb7ebeH=f+p@acGox5yv@75r8rq~02%bD_ukO6O(Kiwfb7mZ;r6Ap5lJj(sPc z4&Vd}VO$U&3MF)6_c-+Z?k5{DjL5k2NTtewj-8G==*5bv3nUU(VfKIt2GvR8397&z z!PL*wnPKYm?^mf4&)Q2!HNcR8u~{Osp~v8fiuqVrF>>Yq#*AKw^6~bis)a zb4SEhI>xn9gfqI5+A>dutrrV$BMX%1o+8NGWRq2QLv5wh3%3T9JSEG8;}earlvw4@ zF@1AL#4QIZo(?uTbH4+VO!ugF--HeFC<^oJB%%qdh^hFVY8% z*U(#;q{~7F$Tlggvuv3tS7`*az1HIHJjvIi(%SuLS#F>l$hBpYWphvJ zK?xjH9TU|?0b5SCO$JOX-l}Mg4Hy~)+l6;FrAbLbK?ke2(Bt;nqE%`wHVyE_AVJCZ z3A=+*%eeVkSC7a>=9(|JBalKCK6eKxYS85=?`Msx-^logMeU+Y76SWuw_OA)f*A@h ztFDzU(2l+(9FWs2fwOIK^AKeg;j<-3)KUwqZkS>cQ9Nkz+GVGm0Z={p_*L+eU2)? zVa8YTES>0g0np{TQ8$iEn+D}1?B#ua5I50MpYxI>XXg&fP&oYPIFkORHh>tZ$K8%m zYoVW|huQT1a^+>E5Q%Y z)QIW|#O8XmV?wN+EdW`N>6*|jrJ3_TJSfxTEiF#OqqE5flfaD*s)>k$FK12!Ri1tI zru%Y{L=oK}&#}&0G~?Y{?{|+@ymD1!49a=h=yK-Reu_m5iC!a=SZl3G#s-ntF*J(} zf2dy_^1K5Ju7n~F51BWYCWw+^cn-X^41ZfHSp%mByAs};-(y{i(%D1YE6bXk`scp7 zs_jU%Dqh5Rv(nTxgqMbEOdjO+L#i(s0Dmk{n;O<%kP@<2Fa*qrO@}l6hxABZAs*YyalpSJr_e1s? zWH`Yaxi6Q+8yw;PDyvtZQYI!z(Pg)#S?Fv@A5A+?QRY3gH4ffpImJA9AD_&H1$gd@ zwK@bZIMes1{1e}wXJ#ruWo_eOMLIKGM}sWJcCfNK1nBD!hS!>DpGUeZli>ra+kQEv zErppUuA3{6e&=-0Aj4Gp^c1eb99!ja0&$$$#VJi<&ffKmk6#gll=9K=twkCW9*`2& z6lR~gB@V578Dr(PENp{OhjRP#|0A*-yv5ct!o=PHs}khYSk(W&?NH7skH8KrS&QHw z4dDE6ny~E8sCx9uq_nU%&CCdSu?wm@Fl9d?EfmuRCC_7kd}4cC zqFCxawr`3Jr#wJHN_7OSx$0Id4&Vd(#kw}W3W=)h?d2rGT9DohS-hF#gzjt{TtWy@ z5Vw0kZ+NXR`KG(C_(o=`fj^nR>FlLXD`OQy12QoGl;Nr#>0^6sFI{DJjQ78Pmdssd zUIYHCBe=P<&r5W z;$NjGE+1cBuZ5znM5$dZjxlCe>W_~5P+biV+L!DIYF{p{WVDW-?S*kon?2w)T&XfI zairFSD>SU)u0rnWb>+eU`F-IGHj;5Z{&QuhO39D653`O;drFO{yrxX|@!2g-UR6p{ z8|vHW^WiljmwH-TUG~W!4mJWUTvnFQPXG5e5L4+NKa^(^u;(>0O!}gn0C5i<*?In>p%1{trqvgv?M#|xp};0?S%l~9!T`uIfIHb z9UP z<(hrj!-AY2)#DP~nONr}YX>Z|L8AJAv=uPY@c&XDkCv}&6Cq=K7tEQ7#3vV~X-+<5 z;Ut|v7ATEPFpNq4Cehh2tsldz!As2a4m37w^WN~yQUs*c*c3fdWHe5-(DQFN(#Vvt zNN?*jwUCTJg)A_1TWU?!>C%9sT}{yctCBe+|1uGdg+rp0<(@cd-;g<>O18r z#OxWEPG_Z@nakrEyPvq!Mw)L75}E`W&)|`{?O0~+>`iTG@0*@O!l@V9UQUmNmSIAI z=^WpLho3vMkntQH(3cP&?>s@Fu#DKw{FSdAw@Ujj_!Z;pOh!CKqPLTS?;XdV%&^W&FLXRgFpr_isH5XHFEV9^MX2@9X@dHgb-j-%RoXT)MR?Jh@#U& z+jAhqsN#1Cq~^Ej;k3yYfak4#Z}Oqk4pq5)S%2w3gd3_Tsb@HTzNj)L%nJS=RN=BW zsvsBq_1^0vlQ|K7;NsXQIW6C8bD}CWhl)L;!F3A{si8|yli4qTR5#JMT~8S}9M5{! zENqx28Dz(G+S`{%&yG)0Y-q)Gol9SlWUh^WMCUu;XWN0_SSrU|s9~>ib5W9SDKS${ zF4eN(G!0ES4L=tNbNF`_2sxcVdvJzJ&h~xo$ErML$T-bTNVD=$(5hv5r=1T=%-pG9 zk)i27%-0z?4TUy=4qZ>x#J2;LZSTB9U)?9f2wOsMQZVZA|Jy-7ZTYlAA7SIp9CsmC zaayU}ZZe-k=JMWJ;P2AM)tq_W;27T`sq&DovpP<#z|~PF-Z8+RMYuoMlZBr51@2;? z5bjO?KQ*p+=lP``J5D{P2pfe*sPH5Yx#j+NM>o|nMvg|k^+PSBAfi5pS>r3!SxLAT zlGDj3!4hmf?g(sFfa6(e`Ogm42h>V#F58G&9VnxRjNFMi_fBDR9Mg!`pjj5AM6Y}V zdKtZ$lo3w1xfCZGpy+NVc+Kk12mk|W0*9!@P`|L}-MgAwwZQ4R9WY{fbr2V-_=h6J z*g|?xY;!|R`!DcY@U+gdhjYU?0*Eq;BrQ^3rB(Jf@V5AY@Q+eh;;-2PE;M`Zo}wk~ zE(!=Ny0D(IJH29*5|J$UE)pV(I$&$*()j6b5jy`3eV;~%1u3ZaR3TZnk^j6;>&?qK z-r)h)lI9_Et}jQz2xdYeEuQKDX5yOZcw|Mmg9)Xz7uj6p%i2FB;QX;|nFW-gsXqn6 z#D#;yI+WHlUNf`xO`craR9wN)RPtFy6Br6$zoa%sp_3 zQ~QP`3X!e7qE-facBRlbPAR$IMQAEu$!eb)(iW>mx7}guHK$R|FkDg*i1I|j!vYR#<;af0X?T>6;JLlJcoxN;hHC=iNhduUc>pMMFpG0 zo}u^p)N+!RO^##krRc3|OlYVG*gMP-Kc=`3D?F)3gy3P-%@x1GU2J1u0H$G4eQsgg7nS3g4A2eI6#A2#3!<7pA6=s$ndTl5lEas7#qLk)C z%gtgwUZJ+5q)P$Uy{Svn7_SSLR+q&lH5iq|jzJMs&?cO%2zlY|CG1GQd92r>#u4x% z0XUXO)VGHSm@lff<$6VfMBW~Txwl44pa(@UZ*F4;-MYQ_7YjbT57rJvD#ezCrMYm4 zQIDhq*8ihPuLFguyaG}bnCFuP#)*j-$-{e^*Y{bYSx;##DXWr2@8u0h2UR1)mWz8v zm3ak2i0Erf(ImJ=CTafp)^~+YY0#qK{p32Q#f< zI+mnZH1_4{<3ChRAdsb>8xwNR@OG&k0|c|?O!dz6Rwn2G7La3ti@w8!lt?hke7OH= za2Lfcw24!M*<#-ZWr>ZGin+V|GFR7{Qoq*nR5!jK_piQmyUo`ETZ-evb#pnI^l%Qy z_9oY(X6k$MWWK{RCWkX2&`vqB>~^5|j&3OjkJV8-9~x6u ze?uG%up7N{RqiZHSS`F)8~%ky-yc!d2A^s+0VqjXVmCIHwmZW5(DzDSzCQ?pq68Dk zAu4P1MH*x4^RT9={8GrrF0X;Z##U2#)etQ<*ax3p++PQnK)pv+fwXe#`fL>J5)5Yl zH*TrK=4=3!Xr^Vt$^y0z2K6DIR5-p2m97h}r2AbMv>eT$+|d>9K>jopX#7Sr1p z8aAhuWZ$}a)mVbEPy>G@QDtu$SK%N65t6u`9V}%?S+(_1r4^xlT6x|PIU(e}KI~kS z=4$pRK|2OM{RM0WdhpJdx7OO64-k)rp!;!C`i;5aZ`X^~AG~h(0otkC+r^IUT`-B) zd^MGre+PF`?F~}^Qzm!54Z_m@a4JDb5#J3_7>Z=xC|_>K>$PpAl;L+_W`Hl;E%XZi z-OW>fKeO*$k~a{w8`tl@S!g6j$(1&dK4DM=HpChBw+dDZl-qowal3p&nAZ$0OxO-tA(q5n**f6x1S4w>p@U0eR-YWQx(Pxvh*5oVINXwfZ`a zuy$+D^e9?-Vvhr1I3blqQ9Qll5HhogQugFQj`d^iXZnI4V30p89{4jBNZ|LZ+XsOwoblQxTH3;vsu#>5me!Ge; ztUkXEAJog$7@pw54*Qv_f$GnNgELJeWi7d+*Ek2CQxiyAP?i`RI`?|y%s6vqGqvJI$m~TGz z@CR<}KI7VcWwLf#pWxx2H9ZrWz$C|p$@(tkJ}_o>Eog2zEUZJtT`$B4?GUi+JSv28 zL_aGN`kVZLKbmBv*#~UkRitHNL@Wsr+Z%sl6e(w4?v>}~lJHdnu{C37A%^-*$Bij0 zK|Gx@Qn!kA5UazU9j8guo`GzPuyzs00Ue3TfWf_SU(l9>FhS4Iy^Cb#{rrAg4)GL6 z?0!n@=f*V8z7AIuD;XO{Hv+UoN8MLnA-R z!?U4PA{inJ;(alGVcV0(0vCQ!3<+^Zr`lqVd_%S^299R415Z1^1W}XQBzXlK=MWxT zb2177?wFoE*a=|dkKx3hQd&%-RZKLJvQ=2-s73+70%h6PGot7~K@?68>vO?q#tBBx zP-T7Lv8Ml`arG{m7e5Oz+)ZpsG&2}s`O;c4x^cG(0>`fJHvFUg%v`j;eM|d2UwLxuF2A4&Z^ z1Ny4Jf4<{)sSENcNfWV7aV;j(^l0N_{khlMok0__hM|fiPLw5L8eU++@`CL#mUo zhjqZE;ndRbi(%!RyfsnKNtSXlT)Zu{czTqCXYZ3uPAEDNV!^>sJ8sq~=!NxVmM^jp z==SG8CGokvo=T^AU2mSMk)B_)%%I+_@h?RzJ9FlZi8x6kQ(-5K39Ch%b*+Iek68qLEr9Er@bW6^Mf#zBmQGP_ z>sZ!5S_Nl9v1>tq3^?OkJ+sJ34S=yS!p`FO+&2fM)u4}KzeNi{Yqt4Y|KMh;G;8Ub zIEvi5IB2=Ad%exDj>W6}&#%yw+^r>Bap(ojDSQCB^|F2(a*CQAGnL8NEUazy4GaY} z>}&r|(*nmOF(k{RB3TNg91kT%+c!JQz^bo^3nQ|dLp#lR=`0fX0zG+GOx##3XvjzO z7D8{7@c(R7#MB-=H!lY*%bIAXOg@GlX7>J%ubP$3n$ z779BrtphTrD#o`E7n{xt&ZD5=9zjzseylMPuC=h^l5Ofu<3s@>QG4|eFD0f8>Q1NX zRMVmBe907Pi$j7;J7#=T$PnH!m5#%QvYKC7#!;UEfGd@0y1lJT@KDaruxRQzu}csT zi*`P1**e2sj3XT*Z?V6p0vweBH_wD#?%eDbQ|}B(TgR=DH3bcUTC}8l)hax1=!J-s zXH&5b^XU@k(IQEg<@wX$sRhiapK(SSdkUkw=pBvA_RB6cRnm{@DKSw-I1|Rn+}8#$ zH0j;vbnX_-Nr!pLDUXj8+JO{V%@{zTuDvaLD%@7zs|`Qu2^Kq z<^}wb$8F@HfFPwb`XrMlIS~L5GuVNOi-yHK`aPh)Fm$b9~9r1{bn}rDsdfCt>V;*i&R7?g9(mdTinSc{+sz(=#GGU z!lO)Q(!|6lHP=e@Ge4>>6CoMT*iJ5{%fS!oekdqcQ;c3-DQHO>i)By?7P)Y(tw%H5 z98`_N!-stoOBI`lEjvoqk2FKfo9-OKG;dB-yV~HEPor_mv)pi>`Sd5cyWGb35$hnE zUOChm9|J$1tRgU_Bm-JE_>EhQTf}ZC_;ihuE3_;RrfwP-l_Zr8fPAw4B}3!fJX;UF zdC$vx2!pya7kG$#H&MEj%O581BJX_?+6cCTmc)i$-W${W0;t}UP*GW)g)n%prsvOO zO%wd1f9S2hG;k@jlDt$c=f7WvI^ZYZXpi^Mpk1ktUWcyEr>qwW>KtdI;S_4zir*$? zbsZ^81;dk?F908$3(aZ2t%u#@Me2olN7&(|UP;^{g@9idsFU*J{_@NCmsK zIyJoaW=vrt#=Jl$CLp?nu(O^Z+XeB8F3(nyJnTxSgdHq)H+gY&qC3s;4KL#L{;OcM zj;14%U%)~oaKKbax#~+X)k%eJFOGZ_3^WhL+KFay;v$ksT8srLKmYLq9>A_52JQt#rv=6G2%GGy zNC7qRW-S-MWayv7|0XD;8z$@HOh_ZJ0HI7^x4fcrG7yzV*5(Zu+CL>505?F$zx2o} z#ThXsKrCO(4BK4sgvmug=H4G{lLQ|4h}{S0=e3jGEO*nuNu^N=UW3 zcjDkU5Eltv`qSio2LxOMdidvWiw^em2T>B&$6YjloqX50l+<_j^|(!L{5JQv0kW8X z5-}V+n*`uM30^lF0sSrzsnpMZ#Nbfn+rsHytBF(Eg}gx_Cw$b=+>~n%OrhHUWBse~ zScc9x2~rsJ`K08VHK@`AJU;468^5jNqA3^v{G=Uhs_a8rZpuNpAOXb7~PSPR6|^d(kb35WL#F}0WT1HPv9r<`4rg25US_GI(8&?kPzF>UA&u zr8SmYr%T;;oMVr1gqt=pHgb#HKw3{QWrO-asdx3AOTqH%|C}0zuJ_c%35CZ*4=Fw~Wb?twk1Wn|PI_LXmJY274M8L8SfgeLl%=D04*6q}X zy7){&KN(^mH!gb!7S{J!NXz|(?rm_tKQJMCPO;n=im3-rmfWhD&##Q8E+^%N;?6S} z<-J@slrQlYWZ+o7WDrAz})Fol&vb9qZ!37VJTsSEp=MfjL0q$AA|kt5Kcnb zN&s&q-j}#n(0Yn8MSQM3XGTFfv9V!ChM0i?v9a-nNk>}HzCmdfwWwMWb_swhYZj1B zv{`yDh@|c=h=>#er8WdL!-howT7ga{QAX`{y|5bBr?8|RHUT`eX!ZoS*{(E_$8J=f zLOGxGrih9qZ^PI&UwTHJoKV;_`d5o&i-?tTT=h$aA?(RyK;nU0*j_`a0Jcp#6od#PF^H58uWQ-8iw z(H^|OAtdQ@Wm3f|zjHh_I>s zhw{ncbnV98QSa^sprdpny*1!st8aM%-YD>a$Z&1fP1WeU%0{<`BtfC+mREVi3Do38 zP_Sa1vG|d`u^8@Yts$E3VLbXm=7#XK*6~j^4=3w!d$a%u8xqnspl|U zC4*&w^YdyghOXsCVdncDxmA~Chz>pL8Vxf3Wf<*h}BOv$@@tZK}yHKFj~%% z2}8;0Xu%Xk=F}2#vC`kiM`~wH(MIr>Vu21(1^MQZ;d|$&5xO6TH-+mdg4%v1IeVExcgF7TZ3hb zCR5qytvL{LqF?w%M{=iS5QUl7|4lDc{1X^T(84HPxu9HHU|5NOgK?{B+Tp!d;oG%Z zcvFG2#Bb?=EnIZ{1!M7j({jsKwd4(y?&~iv7f#JjtKJyOB{Raz$Bn{Q?gm-|TPjA1X%md|F7hFYm z!FY9hD9r;GiDZGklu3DkMDRM~PU(T6=I%%;$v>S`i=8hV@J>B2)>85bMQyK|jM?9b z#;86C)91gN%G;{z=IIL~n8{qd6_u+b4Cy#xNTfq&iU7B+lkrw0ARjFk1#zC&*dQ`Q zb|5`+S(w_s3wh)xLY^PFxFrh+-?>N-ZZ;<9g)`8nDmPojCwW^ENuy`6$1HS_X+L& z$U_bm&LJbWV08*!Q_rkkZ$AgSu=kBYiYW|WV7ax+b;;80T_bi?p?cYq>a#CiEaEbd z#cg~n=+Z>FHHSq?=8ZA1_Z>WTiY8z$IW_@xfCy|u3#{A7i2pX;nRW8Jof3XQPn-Ww z=c7Z=`!cEmC(3>A@~S`LUn;wWRNl+t3+}8fwG!O}yBIcIB*Qp02R+abF~C476Nd=8 z^H3Wp#HVwA&|fK0pidS>grMzYX0HzIJtvv3Irt)3__tC!U-Dp~9*A=M%J^i&Ucl9r zzp-8VO-`{WYnkX?uvA}QDA1uF)xjxZ_Q&+LQ0y;G`mm^F`ALT!__o@!Tt~;DUY63o zqG%*jNsUcN)0ye`_e$@ zC#?vXQ+$Dr<{@Mu0&39r4Br6FuR&)Z>s)Z9Jbe!_EejfldA-Z|Or`N)1Qqvrqo#;Q%yfjANw|z4HhlD?PPOId45Xm z`{W$sdp}@bG0Jt4kyC0^`Jr7- zNCqT$7t-3ZiBHboQ{mgv0QUvEZQ4ZN{)zhZrt1fFHi*v4D^Z=G2%~4E82z5;x#Dgn z_xw#fTw(D|(~glpalB$CiYM5H`0I4C6kqJNGBX8h0;v<)-`qb`U3(DClJf_uB+-oV zMoqmgj_#DG;XH__Y0#P70*Vc!pHWa2f;cskN1|Ac#EM za+{O9H9OV9}?@&8dPS ztn1Mrgby;x$aKzL=f^%z092M}J)htA38Y4EXY_J$y$1mRlI`y=rxOUMEDFN<=+=xD zg^P3=u`^CDzgB@$L;vE2L8rhRk=N6kSuJRt*)0bPB|C2!oScJIAi!*>URixyE7$#< zd`4)dAgkECCqDH3ibAXT&3Pq;6yD%!KW|fJtwl141tE4$E;A1M7t#e2l_F*(osQZHMla(L^1^vX6 z{in%pw{=4rE%BLTikw&N5hMQNfV1=8cW*T{Z7pF0ud=PS`UqVEScK zvh00?8`Z8FnUTt%&nLeOm{MycLabbtS#oyvL-0*^wWQ6dj_6prb34mNLw3f^{#dN{VW*(|{L zxWQJom^1G%cFVQ9MQ>D97Ui}ZuZy;GSJy!D{8#VqkE`sSsqa;uN6YkTqE)05P+zzq zUH2c0Y!)fugBLx=_|MC*v}X^+a)HsOO6l`U(;na0Ju47pe!lBI)x4q0WExK#%`AYZ(z!G%&HarB_8yGLJvV{(dY}opr`>5Wz6kB zfIhODkX1{M_Qs)zun?8YTZO`KWcp_nU%#i{pI=F?mJQuAONANFeAYn;y>cwvnC$ zHL~KpBQdw}8gdfqA%Ou5 zx;d{7$T&b%sM$!AKvq32l~?*L8@@_pu!*-$UCw2_I?PGhc~TZV(F>-9+wGjy%rSv1H8^V0sux@V4$)!)gU=x`kJB>1YZ@`I3Ui z*6SYEz{ak6O$elu)F0CqOyw`jjr%Q-R6)%&wD>wJ!3jOnW%#x*Ei&C_H!wZve!Ebv z7&$zVB5oR3Ah>XFvs+C7?m`LSvDJB{p@UJKd_y--V3vM+xYSEzqrQtis9|?ADMOAC zD^X5%m>^ewsQarMRq-zjMS%LJ_$#gF)AapB;rB1sRk5)EC_8yL>&i8C2Xu0k4Yh_> zoc&>#dz?bqr5#eua*VS$Z^Td6|!7P;xi2!kQDOOUQ<3*~gmVb4+z z8omW#O6&gRCTSYf=})PCRr~FRl!~klL}IzsM&daa-M|iUiMzq3xcg%W13M7k5V%|oAP6ckoyhSCo@oq z-_xAW@e0b3$FgAnsQVf zoF3D9NBhoL!gV{ajMM7;UmA|9s&6o%T-Gq1zB%pRj75qK#o@C-nJkxexCZAJ11k^Eu3dkxjuH*CQ>I@b#Ar zOg@<#YMLeyGXd9HYmFllWBO1p#Z0(pss3d>$3hZx7m>O)sJ@XzQ3Dgg#uSLtR*v@@ zzBvqqBZ`(q$S3{4#n?m6U}8_8v=Ai?N25uvj%VFd48o3lrrT(8>y@4&Ew0$mGQAa! zQdk zs>?B0p|!(Oy$?g=rO+U}oq>V%)W<)xMKz*=mPjXXwl(i7k-lXpW4~gl34lj!7ilv+ z{@kly-#}(jY=(AVe^y0jat*&6meWK~F?5#)%83FOt`KM^?d&-GAcl6IrW0Hr~$~QT-<)HAQ*T>1IAS|2-bwzyNaVT zqOD#n;vE{fFrtUFS4T0|<8)N)F_9sIPi3kv-r0|v`4NcQ)BE81lCS~Vwx9$sL|9+9 z{l)9xMqxyRaO*=C-TzVY>U_x2KFC$nV#iXAIz#&S3H00a-~+D06sVb*?sc>DO3!{p zA$qq+DoCLda0dKI3UQpep9)Ak@U3*pbp85SdHb5bMv&X>=189x<4|Qd`GjcVvui!H zU2%MT2c>X^T7@2ZkVg7hpx#dGv8F=V; z4sn;c+VKcku)mvUF>GGw6i*+o%h)Q4bLR`kBi|Ak@1i)|xQw0Kvp%GXM^c}UW7_~D zEd5f;_{BJ>!X0mUgT~p#!^Q|_sY+729>@wa5o5Q7FRy=yY$DdXPG5Q>+4Nwsuv^le z-9tvlLr>1Gqo}us zMjGIE+Lu(68!f1-yXm?}AjopE)6^!a^JR=)JYSG5E2ah(LymTOd@moC;xXx9ygUmc zV#CQ@h<~nso5SE-3RjA1dfEut_-ZuA+h8fuaPuwGQJcSC?;?#9|2r-euR}&_O4AO+ zDK7T{1Hbx}bzZF{=>%fRgDw?VF0=&46WYLX78wp3zNEvX!L}`3Y-6FicTLRJ$=-by zR?sa24TE;-GL3X>c`shpx4T#C32&2a9|G<}4-ACXl^q~@-VWL2)Q3<@!Dggqe)+tx zmUlBwn1m;x#EBCN(>krGc4*pwg$9y*SU~Sy01EYgkb37xA8g96I+|V88SJptH2MPN zwdCDNQ=n)6m)N&^5N*Y3i-5t1IsW8}0baNe$(BmPzx^&hZ$e_Shx=tw^_|p2b^@5ZGc~6mAETr^ukSK+ptK)1ibp({(XfHbpPt|Ctly-AiF-CTI$8hS9p zPQhP0NT?5C%WNvy@tG&Y|5H`9SUQ45Ga(;~w;6sijaME$)a{?=1?aUc%amsG<0CJL z!xNtSQcM1HmRgCFvz+FDl&o#V*SF?MKnW=9HvqdEkvywOr65{m$3FfN2K9cM7J( zh=ms2yyzXzO~nyi;qJctB)ODg!(PRy4Fbu0BBpHyo@Bi662KWoA-gAPUo6)}e= z$Fg?<6Qjfd__5kzJF~-3jinSy9w1NmyhfygN6XNWbB9l!K&Q_{-*OCJHdm{7l7~$y z4oe5pV3wOtRthu8neZgHt<IB^!QvuZ zN77WXV$fmD5Obzex_q-0cgV3AE|kBf(AmjkzlT^@a!RaSYl`we7RZvY!W7shQTXf) zC&UIC=eZng}%aq($WHNafwv&p#xC|oLSDtHU2sPc}l6}d~) zZprt0enr4$RC@!a^5@%Var&{pMTGa*ypoVZn3B89CZgt9qgMT4?CC3n^^-d^_Yklb zXh_K*WJ8*70O;G^!GCuIzn$=|_HU%65YzM8>R4xRITc%EPMS^n^OOqJ@|_E z3TG*iWW|2~0}6k!@GuV)n5qkG2ZPR{F6V^1sXL2uZ+-vUq9&Q(#+xBXz4DxjNfolY zrD1Gk8ieA)w!~D#&=X_xg7AK~o;&6-z(F9ersGKz^nJ{}Cdo&FM|q9SxRU%R_E)d^ z-Q=)W!fKI8%E!WIO;h7K{Y11Yd^j75Z^~|zT>IMN+BUlRC?SlmEJ}D9z25#ZQbYP* z{LGx^sQy>wS(J_~y*kJ%9aWkHkijn>lREW*Tkv%NMufzOVbYaCbxPnVuMvpDc`%p+ z%t2GUOX!K&;G?Ai#_Fcr(`W`_MSyn=#_Ff7*qmjZ%mX6H(eedYii{W#x>GV&f}1Le zoeQk1>kBRq_oYzsWZsjEMB3?@JR+2ATp$-lzrimJgTK#GwZJ{A4B#$|z zk{hRl{DAJHQ#1(B{9UbsA{oqoHb+}*AZ?@R7@FvzOsa%UJ1?8Lj)7bz#%J-dXhUU9 z(i(3uX+GX<27ze3M#7gORbB-^PNywTE_rxh5Lip3_4INV@F+Two8;tij(bm#bObRD z#SaoqR>#;Wo_^7xW06-QPvW{P0sO4V z4_vAJ6ZN^M;?lmBlT;G@;P^I^#MCv17W~##Z4rl?AkAcy0AaZF2XbzP4Djj7!p_yp^ZsTbl+kbO>-n;z195A}L<7O5*qmT|LN<=>?ixn}*RK;Fym5!ntz z+qxE!QJQ&T^!H42)#7{kW!Q?JzGH-bBu`zY*thw49$@_y^b!AIjI(`8oI{Y-^j`sS zIaLHa3b{U^co76L8?kcIZ#iDzZ4V?htC_8}T%~1E?t;I+kKQsxVXbFF@u1Fh*38p0 zoxhI6fAt20Y?r6XqCu0nBnM~p?92-okJ{il_HO9h;_1Pb%<5YJE-pfc=ULc{PNjtv zLWeTt$R@T?4YZ=xam_KfHnV(L%EwacDOL6nS2+QX@KftC8_gKBr%>EZJ*-}<`Xp8OvfIO!Y`VM6-h~=u zJmcB>4DqciR1sskz=yrP=o*G7R>c5f(t>kNWWr8YjcYa@a4`IK#aY7Qz=AvvkC{H1 zKq>bYU6w&v7XZI9k_-R54Sku5-qJ?~1u(FwTP7 zgWJ*HC0M@i_vAuAx%xu)d4wXFYQ{xt-)Q)A2gh9;a`ER8M3VrXVZU1`A$!ASSmMX@ zM+3Tnw$JIwzb<|36+3qvq8xGoN-LCt&C*34yTc`AgS&87+O>x!zhEkAV3R2w7Dpg> z&X6)%(~wznD+{@Lj*I|VwaUF<^1CyE?VoVIcE0Z2F%iuEeP!kdEgn*}lbD$;>7tvo z!Ftc+5XL_QxBdw6i+$$7;_e@Gq-S>K%qEN3VrZoJf|agR=!yC%0nOdrqT* z0rJBZuLqh{@J#F0g6mwlSqLgn-XCR~-lKf;43E`j&FOQ1FuxMhZ|AGk-iar8^`Afk z*Ir;@qU7m;;Vnt`1@nF=@nj~+rc7@)$loJH#O(r#9ofW=;R$SaPgjremR!}*qc zQgnMag@y=#2iifTdJdAtGgS{NNWgetC@_dUiRRw6wFEr@cOZ^^&j(LdrqIx@wmk%lQBv zv)Ak}DQKlbgt=&YdYw6)rx|k&oRJkqZ3zSM&){iR+=jJG)`Xv zg7`Weh&W$)(3}&m_>J4AE^L7<+*4oqXmAJ;DvpqU`#_3*gwnS{&!TN=^rJs`FSe=H ziil#uNid2p??reuendO9S-M2VPCAL*8S7k^f!qkz7R4GS=z|4DS zoUX)eU$BT7RHt*Phu2bg{IZRL)@#8)}df*{G+>wE@21TY}h8bl)M4+a$~%ZP&vx-jxdsk*-a*#u6(kZGiky=hQg7? zyVEoN((`Ev1C?awp3oM!2ExCr#JYN3`AkQhgiJJ9RA%ToVQK_ggeoMSFTdg9w-Sky zGueWi3mC4!*QeIqNH)R->YJmYMMYcsruE2RS0v6Padj)9D9ttK!mEfvz>r=88I4*4 zcfP5wLwL?S_(G@|SuJExzncI;gi|7dT^4-tq1IPm1UD z@>qd&a^tOrBIO>B?;(FkyrQsB4xfDAc9HxXxj1=oQINgIQ!iVfV#UoMLIYrGT_ zDASc?@|}y|22z(vkZS>GbRT__skNUy?_#-_GiZz$UX`HLo_4|VSbId*faw=wcLO9* z@R@2g;_ZIz`=h4;r$q^ZMr0Z75^84%3%Z_}IFE2QeLM z25ruz_*ly`?cY+*EhIIQ3gZx0Mq%(Ss6Qxs@&T_)G0*X=fD_qR2!Mb*z3aJ?$p;5B zu((x)@8ZUeRlw!(jTsT>jyS`Q{r5k0rZI$t<5Cba8YBoZPa3n^oa&Ni)c?c$d*-y)=D= zI~b*}v_p)t$<70vwmpfQ6e9#;F(=OM0m@QQb83?36XbzdF-WkFHZ&U8mvq!WQ}z*0 zpCMG#0auu@-bvq<>EVxiG&G9x_Vl;m7&_b~qC1^*wXimM@s+aoi2dv-lu&G)CpLH9 z^rTm%R978`WBMSvHix4aju!ng+@m-NJ;vRGnxJFqR5XB!G1m+-HROgQhkOWe1hs>62u&&&jC^M$oX)ci z;a{!x{O|oqDGutoLA{G@;+2fH5Hcir^LMW>0vHzfPom4GH!TP5=ZJ1BxEiONI=IKq zbu6xQO7cSBGk>eY9FiPqPk7f6NR^Jc?gbh>o!;qTdv63UYGb;x5gf=Uw^gzf!^_X# zKI1`|nX2C91b+vk!oGZpKIrJIOcvdw$;gNt5xEu%gScR@C`EZ%^71P2cRt7>gy++9(SL+EGN z_kqHXamOk&z4C51!+!pp3%jA-aHJy^WyFj&^=*}U#GAp{or(_24$CF#6>_Fl@9Mss zybad#E+g$SzPf8oLbX7tX1_~}q*OFJk|Q3s4fz&-O3ept>WtTK0g{^P@MbisJqW=A zDFhpfyeCl!^~Tte5Dc0|M>HXrf=|g@d{$@;@2Y*;du{}zefFRo@tzK4roQ|xa>Z#r zYLSWowvA>y#463V;Ky}URnuT6YujCZCdF((C3SB3;C%o^#iAm?(O)=~nZ`?%zAv1; zXhiQ8@FkXs6;V-CS2kDz7tPTdHh* zOreEnhM_mkR7(!DSxCCgY5b-ciQ9G%XynZ_y&6jEc-S7U4f(e{t!(%5GFsl6AgY5( z*CR_s!5L_P!ysYvVg{rKDFNkDGp-6MLS(_c@RE|{wic=9>BFY*qlldoYT6k>Cnzr^ zUtSxgkGNRB2N{4hQ5(OAz}WHJovphN`;Vv}hy(+=msj^6GuFm_d?K!f4PjOIv&Sr_ zPsUE)?6u9;kI@?s*zR+Wmz2fmrm39Ng*%h{4W~0>Y9!pPfw>w9W>JIrgZ7j~Mg-RT z^5p>|->bzFnbik#HYZeeeo3y%!MpUSV!S{j6{A*vz_y;B@L;lb!%nuY1K7S2Het)f z2yk&uK|}&S??1Kw15myLuvE(gg3Lxkxzc07t)RW{dlMeTX)1udJV(CNi`xH^DixGf z1T;QedLa~k1(>qVq)(YO3xFbf#I*PZ(fBWeng&1^@4TqNV!$JJoX7|;3u%fgX(xL( z&$Q#r%hX0JxL5zdG6%@gz{qF>hQ43ba2aH0uiZcQg~vnK^%KIP*m?PeJpJ~q!_l_I zQt7acOS|72TGzkN?Pf z##>dbySoCuL7X0s>ck+m!}Q?~BePAYeY`gV6;J-SytNEt_)=K96IrsusHeH*q5*Wd zLrdev6s5k0vw$FakR>r{uibG9JFNfHH4+j8qN55`cPwPMM4{=%v%TpQG7J09MC11) zQ+AObSH#0T@Z*-Z>Khyhv@ZfI&ha)TR*471&Js!1f$3TkHN~zIMw4@D*sW_%kh>e< zNWb92v^R6xm{V_%ejm;CtLITbAUYl%A8T>0YS945)mY-DccH_u-NVeq7-kRtMNq&9 z2rpL~1#6Q|i#)S_>R@rLer!tL@bt0(hluqxI5}sMs(=eSW{xN+InRWp@XUcbNG%PO z*g+wM@drh&?^o>YWPRD5+^!Q?#o7mqnG zQ`LB=myXb?y~EU}pU-qq?O|)(z_V|UhIEUl4;cM<6Inpuy1XKBRQHY}JLqTUiN(T2 z1C4DZTNuJH{AyODM}Qd7!hX6ucw}m4$k=Gq;EwpGA??+PIW?N6SIvWhQ(J&wPH{;7 zb&~QqlW{NBv6Cu+h7d)3Ul6Ab+ciher9AO_dmFN34Wxx>0Rasrj@63*<5es zD%a1F7Dg>&i0wMq7dgyVNkgjGAb$6_LV=B4GDofloefb{1vV-<`&n9r0oQgr*Z_Pf z40i$1>qVLaOAdd*oA!O!Jh(MpNKHy8f3KVGc7tAi--ov5WB@q{gL1lax3QkK>P&U%yMv`T(Rzi39Gs+%RGjkd$NEmy_H7|EUu zZV)jrjC88b1tVh2Z%Zwgv!-#Q#eif}TISeP6@sfLiuJuEFL_Py5cCwB@hPoU5=RmJ zUM&AZX)2&W*7AhzsVIzxa=~P7gQ?aseeoU?4>7YPon#rJ?o&&iHqTy9=&(BqM2!`$ zB>o@`1|j1ttr(A7D6xq_Agj$GqlLAmR{WR z0>`rTKk0h%cDUAR^hzr%@|4C0;r^vQgtupn8Vx9#CJFCsb}Uy36?E)S!2a>vPYrNE znvTa$%Po8;`3$yj(j}cA_`>LJ^&Q;c)y5qm5T4a&VpV#jH5YHNPC znd7kn%N)COos?6tMc$H4YoLG)ycL0w4)Dl~P=25lX|Lol72N!5!{h}<2_j<6wqapn z(1BXsjl!xFY&z`!s_;;Xv~ld-uAT+TKd0mGrBAM-5xHEdQJ6UAiN-qCEn{bR`sj2p z;-+%PJx`g{FUi}4bygdUsIzYD+Vf`p0I9rg`_&hc#1b=_Vy$9<*B+;X#u;Os_vo0k zmT2YT=<>Dns7!US`O)jd**qLS?{ID6i^qEHruMJEmGW=noEY%j*QY!07g0LbEgCk?0~{F;uJEmKD zGo4coF~6fWe+G)@$S#{rUVi#l;9AinR7)E#~wY{*5|i zD9!GQQ+UxwdW`OH2CAVdCPKn`)|oh-SVZU#wRwp{43t~_rm8J|f+=9yf#w3d?w~=f zksp3cSdJR@_;gml7!XS%E(8uui=LN={A=yLvq2Zr9WdYVN;p`I-2-Wo{VjZ}S{xfiSTUB3CP5+%xaqRPIa+PZ#*t>h?|6R(B2>@m7Nr0XAY-J6}MrV(VuPX-07J|<7 zsdPje<&}|SQlQt9TT52-7{_LDdM(l9ASV=9rc~`D4IhLK_+eX(09!`_lQ!tPk9xt$ z6tabB2DOh3%3kWkPpwDI5ou;_CK1!uQu*a7>O7fEfMMyfa;%6^{TXW~OiZEz-NkJFPi67dv1ZPWpwoS={dT}9`UE*!O>wdPX=M#d+Zj^d! zmZqRvD2VpSH#|6;!%f=jk)da%`+rQNwnO2BObEmaUo7sgOoDq zM@9x7&rF6a&wl{snSRK!Rm5xW3Z=O2UpW!db^YRvYS6Zqc=XERi{Fm(wEz+w&H$@H zN&k*1qKt$9YBlx)WC;R<*Mzy=|6XQCsnbMd09+Vt`9Pa+)8@v%Ufk0B%U<9g)+3?)ES8ywf65-iCRWu+!pp{ z$ZgC*xiL_GnFo2@m#mmq%+OF|+EcM+n-*%+Km#GPwh+#=NsCDMh`E(fj$2#0-7nC? zTIzRI29%#{TG0SMt%|ZVYkh*G2%I1rq*ZB!bh7-;vkBU6MU=WUjvU3Yxv^t^=1$+R$s^TkYs44tt8Tyqae?~xe`0mosRCjfIkRMf)a6}UKyy7 zp*T=RZ+n0s2-)}`gu4-UtSr~Wyuipcl^5h+QZ*E$Tdn>u8&_rO%Y_ok!8b#}irUcY ztrJBLe^d>%GGoCYi!!`Fi8G7P97+82O_~f)8)|1|=U}J&4BA-ZIV^QihL>9WEC2a}Vtnf{)@mGKF8F1<XOK zXtLjDye|)EA%DH$OkbA356p_6&@u}&K0Emm>F@h@FA3ix8d`P6`^V|K zpD>c_z}rNdMH!5__|W>Yiq(Fx$d%L>NhhIq>SFi4d^TyWQtTQ?92Hf*Cqu6YhW>HRNKWuPxy0IxW?K#y@6o|6T!p zZpxa##2JKt=$1bJ$GZ7e9<9Uia;2-kRDgF9ym%Pl>;{Alu8PGHNKlo&J;FrXoai-VEd=FNmMdn6Ap^RF6kv)2lx2^dII9h#k6 z>#tyAIcRTrScXpE4$EWOr%|N{>1aHh4C&-r)+6sy!shotR)1cBN1MYy0^wS7l3bw5 zE&rlNFB5;kB2ofRsI1|Yr+aBN4J~nsq#D;gpD<0Tph5Mmd(wRr1QRLV3PNGSR)Xhv z#A?T}L3@>{bh)336a=I6Btz1hqErczyMhz(`26-}=~q04DjPvA!-MoC=600AKfQ8m z`Hpvn2VA$EMCn^F9aH;3nR0rgdU~I`D}E)p3HqAosP($Qr5r-k^!j;hD7ha96 z)y^{WR7mq&6YYn!Xctanh)1uG=EzZ4>PwC9&`m?0twvw-<2;4lzS$HzNT2@dS;u-p-~zf-sBcFGQkNF9~x0>(i_1(2FVCBQ>U7m5l&6>~Tm-F=mqvJ_TUvkI+BXOv})kSrc-v~Kb zz-Oc#C7tXi{m2`x*KWS$qoAkvp!;+QTuLP5CU>DH#7_|2u-g@y z-U2wLnNT){OcK1%GdQQTSR7+ea1o6=*9e3&G5r{VhOoVqNA^F`#~tt)*DqP@VZ>w7 zavqf^l?)sdeanz+H6mK{z^2lgWX{3I5aw(T@*u?lu7~1UKRBQLu%5m$sv$$-pyJ98 z&W*iK19anS3l^kx8%R6;UIs+*F^9e8o3w1{7)i^Mzw!QYwbB6Uhe1v!|J(M5LCjt5 zXy&mq6ZAcIVGEHkERDE;OXJIK38)#imJ^H@7gLctxJhO!_iq9#49Rhhc4+^lxouL%fUedSsx zj}?CoW-#O9TdV@W3j)1Gi10v0gnq#tj-;PMGyx$buBSLbAG?p+seJ zvpE1ecK8B`<(*pj{HGDSZY>R?lxYi;a8+~~?t>J$EC-D$d z_B^lLPRjmPm87pV|5g{r1!B8ifZI}Pr1GqmNqqmoVE8H;mncS|jWr$)-RlhV^bl7c z2evp!4tpsy^`NFgeP&S>Qe%1YKf}=f!_2Lf^QVO)IizfJ9sT4)V5pXwGDIb6`+6~8 z@iRrp07XE$zgQd7^Ctk3tlcv=WPBMLY5mIRdA}q9%{UmK^l%lQQH6YRTP0t7I7@Tv zsLU_kXIfVaCh*dKbKa=C*sJ`Io6ApD0${O~`SRE%fs;yy&C>TE(_BgS^9oy)eq0p< z32Dg%ov6TqSY*Z94JYE8CXz3U?jn2U(gth9N?F8T9H9>zNV0`9u#G}=Ew2+4nj%DU z*%ZgmQRJacff8tJu@243VUArZIl+*}JqQ@^h7MB)&PRg4cwUE=cXY8%qtEOU)aibR z2if*1JTQ-;CP<3*6ohnkV1M8Rn0J!IC&$?IXx92alg8>TMiIsDT=_78g>G|&YhoyC zwQ@PX6anTW0XvWlzN^5rr`f4(MJ0(IOZ}x@8k2))rpWO9-=_JlIWTKVFY;CHW$w+N z#C_4L|BNVrBVzz-G^JJPo4sMa7mGS{(29i=ED`YXf{Ig9DXn4n(0&+EqK}(u^RUlj zs?(rz68_iJNQvk_4aivHaJc7%m%|sB0?DBjo_ql6x2^`;bh4k~4NoSrC5;q|H)ISM zp1J5-nsCaypd!?m)!9tTh$SGXwpl9c+bEHu=YD^9>?KWQWM(+O+)e=~ZeCeZjy?-U z&b;9rAlri~nQ5(#eo-{<;A|T(&oeM{q27{h@MvDgHH`iI=hp=p3n9o%&p*2y8H9ew z6an;Q1?#0iNBOVsg_^QG-LV=Vf0zDSqb)a(n1hZognXN)TcvD}u>5WkFgII32w~W^ z?k1j02kjrms%$wze$%bUZI=q&S7u&PQs9v^epoBtjaoBo;gdGmrw|B)TZ9+=`77>e z@lSXsO?Gn`C0Kzn5LU__S(nszip{f*HYD*=~Wy(=h`Pc!XCfqD)(4ALBS) zF3_>=dC+`#!)=rtJtomzc*8SJp74un>h?p%`v~?F=O|U-FP**bq|U zxfaryyYX>QcL6Wo|2G(Ly?K}ZKNN8w&WdgFrOBCT(?k+7`K1wvLwAcF__H zx!rI0kp?9bAb%=HbrZj84S7O_{Hp6Z$tK|KnE%Z}-HQ66yl83EUO~!tv&$zF55j6q zvjVHFC0v!}6dv@h*Umf8L;P1`loAsg^w%k1uKj7Bmy~by{+q1XZ-=-h{mTsd~Q)Em0PGCkoo&kjR!e$)T4A5WH;=>qh-0ZsMPEJCT z5k6vA;OlXE3XsP>5Rt%P_3>xrbP0{zyFX6#skm9h{P+Q@cG)z$9T4Q{W14FT{F?1z*eRX&9Mj6m+?1KpWDe#0S-|$bly>Uyo ziWK}L#2wz#+}S(z&erI(F%Bh;SQ@0FAVx<%4hEzIeDx|X4KhD1$)h!j;@0*uJ@wM< z%H)!*<`@$1@`swujzVi8G5^hN604KFPa(|f@6{e}G~sEwqgf}q9gxvribpAa-%GcJ zGt*zbyEKVq(mlE*}l*%%je0bztdeHpF?4WY$jU9vUOc3lz{oP*iK%ZLG- zHYupruPX(rnC7b4YxhM%T3xYIBUq+tK=Fk!!j=ow`+jHL9h%W5GV>ibmcfEY=9C}E}w+Hn&sKF%!HQa z@O&|-x}gwl*nnm~LO{VX*Na7?^1Jty_ylM|cW)O~G8POGXb6_<35+eHv)c<|`Ho1AEV4&>~7rpdb$cA{8but1+N~b;#zi{NVdtHP= zjI3q?q#3<5v4ToIb6V1N!|pGx z^v*gz>X}EJm_D|k92^!5+|m_R0XO7bFeFqee)>;(IY4+*s*^W9md(46YD~fUEal&? zD}j7wcJ=GM#71S?F)G2xTBo@^x1=7V0eSyEZp^L4Sm__^qiTYWi~1CjSd@xuHqMJcWFo*`qq!-*FezWB`CQ?eB*JuT65!+A6LZH7Z&a!LYY z0ht5#&?d;hfquNv=8q9_c)J-7F=vyqM9U4mn@89*xUHrHy9$0%430i z#Uf|}@GGmZ%b*(=+af#zG?>!QT=fCSK#r|DhM5m>MNG7<>!IYFYP3YJfCMU)T{m9U zT#Jq(nU{R~+SM4wbO5=(l@*{p%jS2Y8gBO8hP1;yshmW(S(xJ_4tCddWF^ zOroA?Rr;p;YAK@ikAo1P`z)XAZ`;dZCIsjHB`r)_S1oHGc--M zhnr3NpIxqsxv*Q530x7+H~j3GHhOhg%6-d9QuwKSTCvZ~MbBA{E*D!subfN)Jzdi< z7a}ptTO>RZ_OVXsx&(b%`q+0XyDkpJ?iQjd>|Jh)%7~J%qvOORX(I;Yz-`kK*z8|6 zDtOQWcIb;;6(;re2%Ev$4FDk!=!9mOh&9=j#M-n8z}92k7uTn{o!7g5AE$2?-xptN zN9z7*peNAse>ADSS<-x;=Heke%SwB9&XTmg?CiE-zaP^rSgGef0JszzVsxa2se^j* zN3?{E5VctO8Hp8*T$+)GEBGXz$aGFvY@{@iFGiwiLPU16)Zid1PDC^(ZkAG(qu-D~ zx+z}iu})KJs(d8T-N^Kp>*HFKxzWJ8s4#|tGmV53h;Xl*Iz#b06Dokn;N&N+thubM z(S>xNF}pv^UvD1#8J|(WZ9$)PC#0>9bKmOxlarHcaXL}wcJ)!gw+T|ke-A(-^72F3 zL`+g>+c#O&C{BL-tF8!tKrC zrq1lAj38OU3yp8Tt`N?{5K{i+)SWm5Lbk`r4?@pbET zYSZ>b)ng7sF8e1T_yur&1;$>J}$}=mZ;K1v@R%WD^Gs9Gb3cv!}=iXu(m4SWyoa(g`ObxB%>GU-RmP$_Q{5k9h2iP!GCc+y6dL6B&pwA8udYXF|u_iiu_;_N$#T4 zv*w#`YCrjfpg@OiL_W( zyw_ixq=HwlFUjAv!bllS?-A9e#UIj1d5u@Xg)~Mna>5Y6e+MK9UHCrD@mOe6YZTF+ zB#dSaPaXnvWJu^K3owOF8C)NXHw^kGISAcvBL6q)fJoJu?-h7q4*3QTuqb?1Z5{uY zwmA29)DpuS+_$fI%>L9af9KnWpT$;j%SCu^WQ(rMLlyUIjOcRBA@?qfqsBIfdGZP9 zKzTkk_bG&INsG~Mdl)gFgR?Owl*AgZZ^=dJKoTxspTG|PfQiLX2(oI30J=c2~L01HcHKvje4DF zB7!EQzvCrPrZ#l0F)D+0bh8~;Bn~W)IWdf17_vmo5=daEmF|v3CY$& zIM_p%$<5L{ol#7z^E$HZ*IKB;T6(Z@!NG?;$!fF45ID1xlyir?&YjeSm2pf`U$$~E zXF{iVxx=dCjl_F1>2LaD-tNuIITvfBf=E|0*2B!~j&}zEgkd@sj>YZjNo%Wz48eci zIsOg5S#^Aq{W_%XmV#>Hv34e08J5S2x^bo}=NQF{ujIZtNMly07<(pB>K~B1BJL0A zjvX5R^=s_YL{Ko{0&CjOGRf}M@||vr0zkEWGs%uWlHi$xPQfRf>2*Z*wU-a4uS397 zbKXgl#H`oD8O_>$VpYGVv@;(lr!Q{Q6#=!i#i&Iq*_`82f1H1x!^03JgQnkX@&KOS zOxyacJR0FkJ0~bPwK#9i>?y5qIW7Gv6tT+1w(#THURcAO; z<8XRjtc7YVg1vz`N9859`M8^lnvAXW=(pog5K2b>|qs-=MkisJTXRH@xb0;2>8H=V4 za4Wc;9BA>#dDi3WA=PW&{sf375Kh>w^m$gQfgL`w?`s{@r6T83jXHj|7?sZ$MgsrTp0gHL(!ba$eJqMO3 z$f$zD=AFoh_Uws_a8j;t&d#~RiOC9GQz3&hUyroPVlH*hE@^+`+(FfP1$0GxkY>4T zr!GEf2i6ggncCwzTS;MFt4a&|)MJe4(ovVp2ku!xWjAYt$@K|{rg(Wkg}0+X;1q=) zqjvjt{j}!=g_rwsQly4(cC=$-U_+b#c$bJoR>cM755?FT`|OTHbpFyJ28ix4N~?2` zh}wC_5RG+rOY^xo*U>g$UC_Xy-KP7s;TyMjRcof9aVWz|h^xWG`+dHJn0EU;FBJWTm~LOwZq9wjzh$b^6JT+fHNsex^K@AE)X}lbJr*CI0ehEy z-RX?GkYG!JF-L4u96VuxJ3Le)0z;zPf?G)*v8%1E*1DDZCECY8tQ_i6B9;4m5j;fG z(O|&P>3p)$smx$HqBU$Dtm+as$i4WYPx)fgCp==)nWk z%*3-caO}j2yvI2c%i&JAv#m#}D*->a=lw_`^hnzF%%(jhb^07ZSJ1>3`90`g))Uz2 zMBhN)b`~v@8}7q-@MT~@M~AS%bQr2m{NT!@Z?v{-Z%iBvAZ~A1FozdUr7u~i@}Le( z^DT~ES_cN3bt-<6t?vv#GUzn5;g$NVwuZewfbkndZq(V9 z5#3)hIEKN9==4DQdFBvtmGz15z|+~t^P(v(a=8Hhz2QukllixLxqf$^jx7-PM|4ov zh^~;W7@q|gk_>)N7|0`=`$vvDtKgNfg#*o!v|gDz)&89E6u)GWUSRbbvXiD%M00pw%iQ8OE~y*%AP9 z>5k5mTIocDl?_!LphD%=VeTbCCI|e_+v?+Sjj{96ot9Yj$dVl=!1ct+gMHzgSs7Dg zm5LK;FXr5OBtPq;F^B!bczXO+ z@D^(Zv2I5&d0=M}{y?8oP&i6BK=C#Jy!ipFO#u=-NqW#g3P z7)fJaD#7c#&u5!nMrTUQIY2{ik4AA_$!30Rd2R7L@FpKYo-is+e-+khtEx{)Dd%Fx z>CtlBx>f0SGqm9QkQ}zqp;|oi&JbXsS{D4(Xc2FWnPkF6v2N-PE7NpoA67>a76*U! zZj*2Im^wVPf+@g0@R88I%Ma2X*fe+tRUu51_TZ$e_u{CE{;1m!vt7xBr32q2k?8k=Io$~sZE;V_{hm=d3~`a&I$1I{ zY-TD7wfD*?@jE%s=~<<)FJKRD)wcct+t0|t(m945X<#_c7m+%;q2C2m5_}q!9|$c7 zz%0E7#D7K^;7vohZCXZQ|2%Gu9`kS%5%M`Z$RajDZkPNKN8~R1^m5|QPSOnKu~Fpr zy!{b2(bhRg%g1Cb5>@_YmczrmRTLBvV>g*|C-EAPntg4GGGE8%=#gCmrixBDM&dm< zmDfS!E;@|1kfGx?=qW7MV(D`SMt5)Mp15jrE(+P-(mdtQYE#yEn|2`TA=m71aY9C1 z;uCJDni_l`Ns#NW72e8SxRBz35D;R(eZSYE+a5>VAlM#0=_TROpdiOdbD+F8GHX1_ z64NIlq7^Q!BD#A2q#qT(E*1ox8xq|)Xa-t>**i}&NRikORI#xOuiWD~oNctmp?wNa zU%|Ms?;e#A;tMg#`wqjTu@bMyk4&<~A1A^CKqMouzGpI}aS|cZw1zZ!osCswi&500 zkKL6{>h;&T@UdI`snC`T7=UyB{7^QD9PM1XN7$dn};$$5bX=P;Zj%FC2enGbC%C@xd({wG^>UFH@(MVPvAxI)SDSZV z+Jdq0F>qKA^lj3)V<1KR*ja-QtISVxKu>hQ@3f|Jz;*@3Ep~zymS2!8qUk(UD(Z zG(`TTjN|Cs*Sz4Cmp2{hyQ#ei7CB;o@FjrJNdV{ysK0MZN#}E5_dOEWGT%)cKenhj zW%?oSHty}XYshxP%{KFo@;$taO%Sl&92s-5f`i}|$TQJ0xuFo%Q?qYLODb&%n;OIM zkvYbt8fH}D+*akV54HKS^(wSOcR*_6J+eV~WNt?1p=K5o`_jr_ShcK_s+t^of=92o z(~eMUH(`#E%q(L(sDQZU=%S0IS0;GA@O~3^IwarYpxhkr_mpP^boY@^D-c`hCpqZk z9nBO(D+#$B-Iz%H=AP*9J4~;<9;?~Lx&+v4rBNQehH|sblr-Iat%~=#(9-~xTG^4> z`1kT*Ft-8pjF87Mm2(wyQ0oX*J)Ur{x&i_R7}h`6aKRmGV3@X2GJqugzd-WbAHU`l zLd5e09cDiqlT6@lC1uuF@wrU@jAGjMn z<=dqZO}${vTe;0TiEwWcd{tBUAw*C9Re<{bei|+3NF#R*$T1wh1_P-7lA53~?hHya zAWPRzjOiCO(|~Q(3i@s(}9cC=19c zVWz2{oyJ(vuTw(dH?$BB{W$-knYkd8PM5D82H0c!6+#6oXA0!l*AI@I+14z;rE0YM zY)$7~?N=79xHTX^yIzOo7yW}+TInmMrJAc+=D4PtSo|mR67g5J`}ud9!Q-KU%N)j= zht1SMGkjkBco84Ih>@1>7WyA0fYIz_RlvZD%SEz~VzQUKQ}1EyM?jVgS$ejl4BEl9 zoz$L{y(6AXUF3W^Ek~m9u86mTF0S&G=I2%>^${)S630W}>usrd0&MWssk~#jtZfKX zH}@1i4d2JRN#|b@AMoc15!j*8yZ9!aqIodH@`8V?Aa7@krg1!dDw^|qP9xBy_$Ew1 zTnt^I1s-dBc$}}Z=S&}$=t0D%Fe|%W$Yb|eW1y;A4GUAQjuPIKo9DxJ{>MfLROnP^ zh=jJJ5Mjj!_dw=Lw~H1)$+}u;^Z`9q;Dgz2O;6L{ZP(Ez*JH)H=#w&yDs=jXF>s1* zA#2zV2KUGtokP~PqQKD71Ji}kQTBQEmE}LiE9*Tw|9|rE{m`~D2Eb*VopKOWJp;L= za+mcApLKeS3pIAYeMs1M6qjIN?M5DAGcjf_r*J0sFS6u?XiOBFnNjS{-~xNDCeafbFuLTwqxOqo~F zPpEoSa$&Z3+)?zxW2R0PPKd?;-MHoKClA&NKRP#T}b=Gxn z5S{7vLlvHw<`1;-{>iS=!#!51M+zt>y4$44dZWb`|(#D4mR4FV2_^_!s2Ff2Vh|UUhm3|*z2(w~?4H24giiPF;FjtL^ zEho4-nAc>|e3@MZ!%B(vPc)6`qH$8?3Rk{Wy)%Sw8}I53UU*zZ=@(F8%s!k6{7YP$ zOLaR}dN{9&9l{@mjDfg@qPTi-X{8gCtJIAMA0c-233*h5!OY*G8p*C@Xii13xj{fK zmT_f@Y8TV$bd?)|ov!zEKoO>}ij^Anxdn@d-6S&Rxu_?)_`tfzs)};pwd3Xp61144 z7T=HP?#;!uTovHP9|n{t3JN{Ki+Q0EX+1re+(A1A6Zpz zhVH}-gyheUh~fnkRj2oqm54`Ul_p8P;q=k8nlHX5$&fjl0xk^r{Ob!hU*r%xxKjj6 zj)QI}EpT5*2obp5`9?Hr>jjfWpT8RMlfsyWwcJtJvs}byLO9;CWw5njl3GH1%!xV$ z3^5_QuC9>zt;zo=x>qz*f$tfzH^Y8Wu-uf#qiT9z6&huERW%jf&YW5Aa-8}(JwlFPsOLd^KoJ4*t@9gSV zcS#ok6=Y&6z_iea?=x_k&pwjf+CL~nkd)hFc$jZd09r5f%p^v3vq_-ylr67scRxC7 zdR3?+a0rT3GEAo5MISPmBrqaV<>R;wT(q~04j|^c#-B?HB04u`b1ZpTEn`PrZJT*C-(=Fd&OTte9fmzCMG6&pKJ`EuZEz8AGu(-g4m5&V zmZFH#-v(O*& zKXyy^$|hJ3!&VKCxS$fJ;&`BrhqmRfI5}ZpUQ##^zX>e$EuCIFly)zrtgxiD`Dc73 z)u*N0duIsDT21}v;A+ql2m91sRz82x6^oofWT6UT2{j1Xc$+IlA*+l zH$PdEgN;y{*hl)rLEju-E4rrKYy!^l?>PKnb*L}r$mw>AM0*__8qEHyX zgCIqygbq5xfQq{UeG~6;oE9!islx@Uwr?w0t`cpv!8g{&Vg9$|HEDge!cgA8k*Na1 z0vsF0+h`6QEOH7+`T==6iAjuKe_{GIpQ?u#PtTZ6yQquaVoJSVYf`sLHe;c8{ugA? zC;lb*>Q+!(7wAy^J^=rSz!0!m{=P+{*hXiH_AAinGm+|;$-l+HXXU>SC49uO>m*WEG(>LSIaDDAB)vml0w?hB9}Y?zubII^wqkf}B=>yy`b{-7KpH zRgjb42yZ=i=yFA8##CmjssvVNzF?_69Pe;FGVDxeDA|Wg*{pl1z8cCPUa|E^CLaK$;ZQOwOH?`FYjjGgxDr$E@vK|UZ7TOip%URr z6wh`c`Vo7XiVp1LXK7-|9E&jIQO+iEB_Xy~*5<#HG}Mwy7hZ$pX~@MbBc>=Xmv~c( z_&Sm9+DsXU+WE06C?N;d7qeo?P`;D@cAL?bSQDBa8ckVZ>%!-!>+4EzW_F=nAc0K6 zM9N=9fNDzscT`Hw-o|zadu^%@d%f0@iP0`^7zL+v^~y@e3`SFjM>uhHstZefBfyYc zYoUb`Q2dqQxZ0&<1p36w_h2R)Ba{CDrUAKh8R57*))of>*2yK!l8zT2sNF z!MA)J;w^yd;Y_%Wi!bZE=hh5!E}w~&dw1ZeF}nsjQ@2Q|rBXJFKbT_>7&FmNbJzW` zA*rRd@!;(0zD%?RE~=e+jzwUF1Y*Z4qZDb5$0==`Sc-%eA?6@v95)V+NIArz zyW~j+fV~K?PX%Qsrs5Dkh9QA$sL z;?ujQ4xT1a-ryJ(_8Wy55V{LpcYfoErQH&mg zTOB}v!YFbckzAIXd!Lf_{E+_)skJNSdSCJ}orr7(m*orb5*HY+UI-R8&p6RJuG;P; zF2#ekK~;JrhV?W6Srl8Gf*|pq)RMu;nQ2~p6Hj8NwKdH+Z&;jL66pPrikqcip7v4s zbcuIiz4LT-stksB#6zxXDx6V2<6=9e90gJQwt!C&_G?4(vwcmcG8r>fkFCP`P1o|*^=o7Y9M+hX)@Oe58wb>*j@fl(hZb*9}yN2 z9zOB9OK=FDzO)Q8(vM-MtNOvXOww_A-#lukb|n+W_yi;*O@3U@E~A-&YEa@Sc~{5R zqyzaaY3C$kJD$Gu`oE_2@n{0h2fRWRm6Q%nE!6i(i)5Ptm z7{h2Wi6AOduF3-laRw8_GHMCftt&NntpYfPVkJ_&Fv~a7hb8z}ONY$iSkdOp7Ls<37b6b_&;}!3x}1*~4xfB;gKF~{HGfYe{X)#}O2Zdl}c z4FJfhEU9NsL>9B>W_q8(O?Nf&bKw03sl#B!b@(O5Ki+xdmn};FXAJ5P9~EfI=JX7a zdmD9e&Vl_V=GM`IOch>jn7ZV$YGT_eg))=X-os<~CzLmJ7=c zT!MrOsxuveFw*!p5Ifc`rJO~b$YodY1PlEcb{teRUqW-5 z>kJ~JRt$m%`+~Vvy!XG#O7|b%sAKY=vj;ofRK2Ovaq>B?d)jiKGN8A+t6K^)87HH? zJ|>XsE>c#_%o{-8>65ajHH%uD8_8G~ef;%6l96VBcCK$h|Mcl}p^;0l1W;g!-{%$F zYJ-qLm_oOF8XRFfMwP*{h&kBZhGlLoz(D^LMS?F+CFCD3s0MaEn{s z1paFu!W<3w9}LyFXGi$^RGWF2hY{&f6^yfeVFKkzGz z5V?ya4PLC^di?wnHmIu?`}}EZ)>nz8AsMgqu*tBYn=_NzKn+5pfY$U8_H_Lif}7NjPcfV;m~{H zr?C*xw)o=C9*}}0sv(%Tua?vx^DWakSHi_-az${*0b=sa#Ke7~G=2(Q8x5|;`XOXIpXI%v5!nY{N@Cwi}VBq9ZImJ60KGcl0QN2~L)Rk(a_qR2yT^F?D-ZZ*1ds ze^Jnp2cDGQL;8F{9erfEk-~x@?Ap9F`MS-k>{a|~*2n}QYfe4%^>#1mi6PNMitEy6 zBCin+Dl_en$lGH~T_~>39Gv z>-E*Su}ujfA28}f^cl>SI{DSEM_7#;xe?Q9?Q=@bF_JVPR7kL#L#y{9!d=psZlbPT zi8HG$@E+&dUxOZ6+_FejN*>%=5CO+Y1JaP;-XvEB5{;}Nk^)2W0>NRA^|d2f-NRh# zOx9gxjc`8;xH$1^2_r?k6zW2&kUe}M2k6jnI!!!dlA$81|EsKM2_a92C7o)#(oF(9 zn4sfe|I^#_ZYxL}^On|2^6nBH;EM_L|7`$3D~ZLOwgVk66dr|U{;P*hQ5i+$t7lKC z-c2Tk6KN{YkY{!E6B#EM$P8uZkZLSfx5LkJl%Y{A5squEBNIu&iy_XB7R{^c$7JR| zsXjV$m(c?QP{WA=66}ZdzGUR}4v5a&1`D@iBK3BHjDT04b`8c;J0e}Un9+HeVpwY+9%6_ z-m8+#|H5Bv)bo*g{1^R4>Yq(!Zvp<2n4s^~Id&TAB8gfcBoE#r8P_uR5h^3TdE3|= zt**(Y$^lX)9kD!X%_HaS1b_7Gch=qUFbrd=2$=4E{Y+=t%{$=@XvU52eeLR{)+;j) z_@{H0KCtOx!q7FEG?9b!-pNp~+Mu%tA#dzJ&cm;Mru{exx4CCkhl5K@AYG>beP{c7 zS6n%gWJ1I3-Ap&P* z-jzr)0bwk{u$3-iZoo|t`d!LV6$o$SOZ2gRI(E- z_40yYsnONTW*p~1Opn_G)JGvAxhv9UCCW-hv+9>HcUnpjSEF*zCpyBvz^&;~Ik)iF z25mV~!Pru`h{@bYnBR(W zzMDMXf%Dc4kOcL6mn^UmSu}&Qhl!q$>J>TpA6d9*SNCdvo6cI0c@4nyG%%PX^qYuy zI8S9-?Hund{cJ`({K0Q=BHKxFSn%?Pk^{9)dgmJqnr7J$G~PZyWG<#-E6DXm6d6rp(fV6eMFh2j)@Q4k)7vz}9N8z5d2=%H}bOFeY+Ri(dY~@p-!|D zK13uH(x1(6$dMI?(HRg8t`&D(-g_1FVf+sQvu&K2&OeQ66WYJroX5@j;-jmkFAYzj zAED_ln&>E~&He;N-m^8_S@ff%fK)`U1!T}zCCAOMq$Yws;oW&cj~-ckJUJ8}rW&th zUxd-dMzo|(%Z1yMsA&5(TVSxrMw?8r6h=ht#Y@ikqk$Y&n4?smrA$gM+1w)fj+dGs z`&O{Q;8DwFLYX$Hw40ZEH3n?jz<;zFH@CU}H{w)!rpq9qz z-=RR4e{BJ%nJ$~b(&BX6Zt8?`UTca66$QvhShUq?K9P9Ng#4Mu(=ja>#zy}Um*{go z4nI>XgJSVIv@A=pAyppYa89QrP9rWU29XTl_p-@&u(E6ZMLD7fBf|v$=C79 zjiGALt&NRD7DwM-hKlmC^b$eVqEr>sjRx8?Moh_uT6O~0j%F-GWux4b)OG#nHohtm zA{PRZBV1(Eqw+dM$l6zPVv4wy6-ic za<#Fif8y9Pm~tURROx_{<#oSHFGq6hyu!H=#r>a$8-{LfI`Vzk6Y*G$6zkRrNWH;V z2w_h!lk)xY-%H{SxWrGm0CaQy*|PygNf0rF*Dp2i7H_Hhu%J>iWv@0b@7A|6lp=FP z2mFzrbC*78`1YS{(c|&!Suyo9Ml}dwC33Sf!S5gg6(y?#WH28mes-= zO`Xq02e2qqUT2u9*GQ`NVA6bjQY{7Wj)oty&SB=9qCKuD>}-a19(+#`!i#f68Np(k zWXr<43LcP4_D#_gSE27UeU6?6&i^t6n)km?r8Z7HX zLO(ed>K2}W`MR{d)-f~2r>kQNXm->Ur#94D_;XL*3%#PXiJ06YD4f%%6>ouu!eB{I z;t9NzTHKgcA;u~i|N1>uQeYM^3w+-4wf0ktoIh3MVgBPK%25HjNC;*ZZMkwrQ7zF^ z;igRJXW%UF;pASKL=tijXQ4+8UO}|AoFuPv#LQT-GudTMcOvXC5s(Ib7n`xaaeCzzALRt^j3 zKT4WKl!mbpyxsD$ZN{lLrb_3r5@ON)~G?zJDt(nF;jxC zorLflhp?JbkiwJ_e1!T3vzq8jXI~@(Qs%E*=LS*bfd1fch1hF4P+@#@ztGUFg=0F- zX>?(;M24Z}G2-qthit;x+3kKU5}C*3L{x^%+h911l?F1SiPmrX`%$^Zg@rJUGWzZk zAQ7{w$M`Fx8T8;H<{_nUec=fI^MNJnk?Won%#zH>1iesl%*2p(KzFw`p6{Q;(lb@t z;51~lPhxWq(&hlpv6eFjJBi|A0W?CRNmFon>1si@ku1= zMJ|f1s+DLh&jTjLR`d7r+^pkA87s3?AG3sW+KSVZKB1xiO+p=Wf_d(7%Kat_;_h*7 zp-r;!`FjV(P${->#wEGTPY<5V)F#S3I7TfoXixgH7{(u(k^_Y}qQT_~x?S$qSOhL4 zUCWlRtdxIOQmgcXx-?@$&Q&%T@F)cRTNA|e5b*hG9HR2td0xpXc{If(s`z+Vbu4s8 z>IEP@fMCt~Gs&^~5(}(x&-nS9_#2O#=2ncI4%Ui`nenG1bQkOcDKnR40LX9zeEMGh zl}Z4=c`Mzn1$HKe66d`wRr5yiqN9r}yQCZ1a8xo#H!tN3e^GNoi};Fzch(}mFWk{# zZL0*+J9gvNyZz5(lz-3;!H2Mh7jVSi4fl(Ig}&1P|CHO!SLVJ(quNrh6+QoHB!Vt2 zDA?yWsdj`QOu7BTNS~rS4$p~pKQ>M`n=L(K+QxxLVt5&r<*}v$ZVX$M(Tkf_mBjUS zb@S&=1?S@RKuSQGXNmKaa@yX8;Mj@tdAtrJanR+d_huZXVPGH;I$5 zkiG)b*UOQ>^0A%%54I^etdBBZmGxmKc>0!K`M$?n^tn~9HntfS#Q@hlhQ`y`$a_&J z6$|;}8w7Bu5j%WW!fuUsonG`2MD&a=m`gK%l(tpGc)3* zLo2#N%%e9mjsFb*PcJE>8P;s#_gi}_1VzD6GkaKQwhjbhlIb`YZGbD&9fn>L{PxzT zMy)$kRq}zsf~0x_UuNi__kuzjrSH>ut702>pnpBN+8@aCVr%$9OL5#*iyx~paHbrC zXMR0I_>RS3y9<6pypJ!pYG~R74Xo;SLlB~8z#Lt~6?H+`H9Rp;gd(8K=FPpMueUX{ zVi-!@0=R>`mo-3mD}S_ub%ii@O$! z7TB9Hq*+4_T?PY`Ncm@pKyGQX?eNbzF-OcrE_GawhGnyr>3OWEu)a9}vof@$VVq`n zvkE~3wUcO+{L4}QYydE6bAFmK(8*AJqMSR7^{<@1lWbL=44|r~@4=GgQ z7cHU`B^D{bI_4$H0_1NF+bZg(152|=HdfKyGZM1g+GkDkB(o4ZEA207P2KFBUE6s8 zM{ufiObRKJ&4E^jUc-;g!sm;9=%=uHdghW@BQ!Byi+lh*HAxGzFr2QugkEpkpfi^M z_piDm#3LS5wPx@omeXcV+OOcPev{d9SK6=<5I_|yn&Q6PieSidpOQliTUx!ZZQlpWe$#qhbf1Yok0WRXLCiqSranQJ>PUlUv?0 z?S70y9{bM>z#1eU&(6vVxf5SGEPJNtJ>{Q=pa+W&WxURCLY7n^1h*D&2qO}qd86D! zhuk5UVZk*PRfH)ry~6d2q?)Ykc${knhNjPD+c0CC=!`EJ<}5vMET6f`#~+z>t98|3 zlrf^*(j&S%zz-x0N!z~pIDl-^dMhax1<+yGS#KNW`=kzMOgVxr%8sU7=JF&P{bC*( zjS9u%13u$}&m#p$3^*8RTED@O)ekh$Q>0ZtJg(Tvdz5Hbe;L$I;91)ApGWPI`EiQo zldH$Dli*IIDwxf3j8o@^nkE@0RXnP%JG%vq5C0#T&Q^j(>$Yom-K z%Tk>CcQ(+1L=EwQvk!YYSr2vI5S|K;-4Py{MKjQ&Gp($>MV2@i6W`}&u5PU^*u@!i zs#^t;c#=66AR&s>TCCZp%R!p)7Qks9rL64&ib%OfJ~bakQV9`2FV1Z{o-X>}Hh_59_IX9&1zbuCzXVI{#A$&4qTohT4RBJW1H9wLX zXh4{fbi*1$BCx9l=e4S#-%17nJ$|ZN6+O*LzQWWi1f?_F_AD+Zj28_!dqLG^p-#zU+lM4RRi+fUOlorMXHHxIiIll!-Pe#8QyCgJYsUgU(@hYvZPR9-H6%Ol zdK*DKV0HRB(P?}nEW8pIXj>Mt3cJ*_;$tDw)a3C9=}NMq&WI$OMkCwZOZ!GwI+#*R zq+VGLFwPMI*7{xK6UMG6+!QjZo2Zbfb({NxkAo;PDxicMQstN3J=aWxD^^UHw!Tc0 zL^2K5f{W+HnNIL;{&X0e&%o-_U4`7G8-3fDz=G@`#PInNeeqXk{q9Wm+t+zo--HuSsdBl;n$`NK)O;aUlbt-}|KwRzkm$jyFj4 z*;gN3B93*<;bXO}FSKo2hOipPzO`#hSJf?`{OVE81@r{OVJ2CYeA}lK9LJ@x{>XX%D;Qwg!cc1j$gKG$m`tT{<+Vk0*Is8`7B!kjHe!da+w z5tl~XObzoh4GG%5hMIE>Z#3uIsAkHU0H#1^biXh`V3U+^e713dnQ+Sj0ha9}p&NoC ze4X`KGmvZ_is{XuK-*PQp=TgZM&Yaz;VdHsqjU4lh4H6mZL4GBC~1N-uSVh^;h|pE zb-MtYISJk2PZW#p!r;}3yXWu*e~c_IXh9-rc+t7AK$g5n8n&v}yPClChZ2CD-LsfO zXaJ}BwYO-6lFssWCB{(KO#%D(+O_mF$=)y^>%n_-g9vx9D0HmPoF3Ys(!V9y#pI0m zuN7V727=9&qR=Y#jPXrU;`{*`6>f)_%%Iy1Aqsu5@n_l>plgkM>{MJ;eS^m&ckI0_D-k@Xaig+W2 zPT|WY$&UK=J>MD4pf8gOR|$0KgWfEgyM8tK(wFz!)k5?791Q0G&miZ~ol<_eE-Fm+ zSOY|lB0;jFJ5;y#=;mq)S?GtB1Zqx*OJuc1n_@H~qY6rJhXQ_McM56|j4M&( z@Lz?Ln`?WxjZPLC#egMJg7R%M!zwnYc#Au*5&-(3yB>n=`?zVa%*M1bJ|;m{nbPo{ z&^O{~)r$*po+}&g^JkGdAl&$b+zo*GWx1&6_US)NM_G|>+|6IBQt9m!W|}h+N^p_R zDShZT5OhtRL(ZWN_Uibb?su0H1TC2G#e_?q~k+4+?BOKDX-wiE3((D|kJ#jL{vyBJ_ zsIGVesE2Z;OEk`%UgF;JQDl^LTPI_jvI>#4FPz8vY170p$Oq8r~vGA@Ns?_jJCATjtpLj-+s zAbnTB;~Fem^@ZELZX5Q-ZBbOiLU%ZxNN;K0q6*-eW)apmK?j10okuFCpF##^ zY$7w#ZN%@fW8f_=85|Ljk5B8757n~eCf44p_VB=VbW;swlC1|?#Py!Nr>H*7KS1}m z#u0ds+Wt~)qG@=4R5yf)Pprc>YJ{Sa zwy#ojOA!Xwe|r}c__EKrW*G5BwUAGbx?8Cp?Vm-Y^`QW$Wo?@2Fh@s`hhsu~c&x}^ zM2HhQ)uiYEESe4`a(jN|#e#kI**abiLsU<@;U9fyCV$rmen)y zDxYfWTvd*)A5*X!GNza`U(c!%To1vs+xg&KYe{plOv}MnNOcv7} z&I&yPi>X&hmWcxYc2(VoS)!+ZSIF-S@^J_+NIJwqI_Y{{+)m0n_`!drqDFUNAzm1A zfR+-tH7kqNquG>ju<)F3a2}|R0Md7fI3Zh14$xUyGbc{e)SR4V%+V_14Tf!;F)`m8 z7l}IRt`hMB3Wye2z^TEMd^N!F4uRT5YhXZkygsq<_ckAgv!En24+Lbo^)Vt*M9aTc z#YzWUjRWNjW?c1&ivH>k$$k$l6c#nck2WtWeD~`1J}o#s`)z>~ZPn`^#*AxWC&0gY zKb4)16=WW4Zo+YRQXp*Ul*?iyMcrO-Qcb9{qz;F2rk zHh1V2b+dn^8f;p2g_#M+DYJuyP7N_$@Nk6kSCJt`5N;7Gsk`JPF?1%g#=B6j}#I4!2CiPr=lag5I zbr0V9jmdz&w2eP5_|_JGvL{ah1q^f94t=Y^sM0WfbCUjBcL8Rutq|}qr5w6pb5wGm z^8@q-lT*iHcPsWaxFNR%jTF>Ke7lM45q}yNNP+eZ1*2*ynp>ODMC~>ma<-*F_8K}v zj|lKpzWgt0IMnCzmQAnpL_WV5D&?NXwXrD1{)`XKf$EPckkGiX)BciM1B9j_0B5(B zBdyH3>XD->6hD72s|}3xUfwS6wficYov(hI!f1=y>r5%8n!gv~(r+6Z5toEja1}Gj z?`9gHP;XZYCQ87OSpEwupyWq3r(TcV^5ZqLNz-NOdl`Ud#B*k`3(9&>I9S;L3MaWa z$IAe6C^q~bs05|-pfwL^xeVFnH2sAyUJoPRm)nF33k$HpWOWxnJBaib)&aa%hFb9P zr^+P^4enE+e3z6{;aKJuM^|O3ERz#@j+_d+6QIW4!HFisnRY-GYh7P4Ss9T{0os4b zNRIhY3;ziCU6W!W1O^w@HtdDA;>+bsD~+jW2l6OhH=`f_E*k12T>ALBO*ZxTpx+B@Mjo(iIcnGJJ^-xBATxi+(DN!~T$Q-%N zJ_!r!^{(v<);cVJSWkokHq_FlX=c0Sw=;bh3aBEyH8W|kq z`2_#=l&lNlietjML4qRho{)+oIw4O9ZXceJC+4Z8Q%6qDDzT628Pc$?VH9uWATHjF zIdn5w_`Zfgi=PW{7rlgG!b0z4t~DWcG4~?bt#;G3-L09`h>efQVC&m}`q{Fr52W3n zPZo#y^8b43h!H6uDn1D}f* zN=cCVWbj9G1J03&shPl| z68|@4ozAwB!`s|rkP>n_VR0s&zG6$7gm)FZ2jL9N{$U8|5xQoR_Z0tBP1#4tuM84$0Xhhi#7P+Y>etti_;R)v>(3X)@;Ry1;kFU=64|H>##a}ri={7ciMLh|V zytCjHO%)o=O2{>r*z^rr$Uf|6(}kZqoYo+*zBxY*EC+u#y3DgAU7*G;#l zzqiVr@N(BgQ|W z07?OTHX`~s3F$;H-6U0K)^4kNg}u<5ZUCR8E#d%nV$4y`J455;ELI~afgU!pG3=(( zPN2Z_nC0GPw1(=@0>On&`DnXQvT~3wqfxVFK5PI8?EXXtTpqi*AsZFn^-NEHnOT8H zmy1i23m0d6e;kcXow!GXPyVkmBKhKK( z3^_~SyOEZj^nYl03T@@1e$!Ft6SbrT+`AY4fME6(V8!ZY8=^|x1f&^&CFcd@#*Rk> zoObsRiToDK8I%&kd%Z}n1tl)`B1L0xSyB>43LD!zlIM{uN`At5{SJK@6-gbt+{+vWAi6!Zrp^MKE5iX& z(Oy+xgB3~7oe$Ed%A6hf`2k#AZ|nf@bOrV_ou)qf%m@2fudo=!*q0U{lWleP&qb`Q6W|>xkM8 zr!0yU68bys5Q_m0Mg!A%E>4A}GbL6Hua3U+K5vsPR`7w>Ycu|6BKVB+jyPf|Bf_LF z8_PA0OcI_FkXX$R1+oMjZ6Sf%vQXhI=Ip=TLv2A)9I`HP^;)reNFjM+Q9p?%XQ7~~ zhXU*7n5^yC8QQ%|(i?Y9;ka z)*l$_RqwY-I;ZCmD?bF&XoxEw6J|}(X_1`uQu3!Sh~Vh)6)-?!pXR6W%B!dyFXd`r zycLx|D%UIX!&Og-_|hm#tt&43kVWy}gjth2@n-sb*-v)ehy+Qh&1)!0C6fLhi&MH? z(p2+`dFDt?7&n7xa7%7y@g(}D1>uBQz@wFF-E?l&%xUuCuJ}pa8iP&kQp1^knI2Rs z2zyDaNkhJQWz-$edB+j3_zUrP^0GppJm6Ce>)@zl#)rbaXS{rpU>1$0^^!p@ph{N& zA6m>)&w4pi^g;)$=hSyJh5I3wDmADBPl9DaUEW;i^kbyuo~yQWFx^iv+F!hv9d3GB zr+?ZV0Z9`>@=FIO0!5=N>*|?o`1ha?%jFAURX4VCw_e7O+46T=g||~HOyfdZ1WlzU zgEUE}c2040jno)Q$u`o$N*WPp0<}7&?3Gr&> zSbPhw%fJHas+SfH&Rx2)Hd9l?s=gPPvHS+KGL76|TKTY1S1B^}{3J#3M(~&vMhD+W zOYUiEJ=OhJ?OfGPC?}FD$pzhvsVsj_+e_&Xa1-_x5`ZA$?+vnnt;%(Mpf5NNcSWp? zoZ1AxJmhkCNrxb7WWEH&H1u;d(2hOtJZIJ}K=Ug72>AI>bvDs+-XQfV2B=*ZULVq0 zf!!F^r-nJ1*9>BGz}1VG=-P=%lvWe}(eToGr5k9k;(9xZ!lu3F0tJbYkKwASyVn>c zeB>guRt1~RdmXD`MemO%p4H=sY;i2jmOP13atKdX*Db7Xz5M_Y0e@Qb`wyDtO$$aj z5-;Ut2INC-D6=_$aMX^GDJaVr#Fik4v{mnQ|i1b}{&)UX_>BBXmj31U7ZwwKTA^I@ABeX)EyjtR0K%j)wYTsV*44Q@?#SAOD ztRP`K?$QZZNP<4|(PQJ{7`)i+eKGD0y|>%_Ntt8$u0|=ZZpAFKJ|QY^XI?nT_^D!C zmr5!uIh45uZG$FI&jN-vZz!h}ACi?0tIUC~QO&bf%3VHS1azBT+dB82(6PFN8qt*q zm)FPi4kz-g6?x(ar^d>h4NSfiqeK=z{Jm=>3-COn5Flyd8=P8#5OP2bGuNVW){th9 z$0srFUI;L&*5oQs6Xm>|%d{Z+5rXUd8CR@q|M~5b9eV&ys2OEF(wWl={zeJa8gbD_ zA#Fm2=_=t9feW83NXDH>uPM^kto8fu4n}>vkf{HkBtvPYXq%f$^*>l13pg)C<#Ys* z>OO?pAH~RgdCZBG3W7G};=8&C_J>Od3pom3Sxe!z(A(knrhsD`%5rBQEWRWJfE;cn zRlN)-5m4H85xvr9%={2IlLIggVW92Z@~lM^gomWw?MR^|O!|h@c?wENxt{13=IV-i z0L_v?6@ZYr;+=oz&|&dnCrES_2soV#fQ0~(`A#@u}?FRc} zTZ>I8Mmn^49)wqi!wHAMRIByaby}rFL#Y240yk$7Jfg#)lcRT}-L?Ww0^?NfFc6Ru zWnww+J5W}A)=L9V%j}NE2$hp1+#tYb51f`WX!DhE@TpS?F1 zOqAWPx$?rSCR7TW>||&l?J@{O5V!gzu2SoLYJ)SBC-Puikd%Oh{;SMZwS~WkLB}g| z_@YCuu$;K(zVwU0I3^P4nCF?A=u{qg5ce~4h;vu zxe}|q=Lv&o1_y|wKPR0WT#qkJML{t2oOfB|S%KL~!KGzBJ#yT)EHPgV^kcZ38Rw;l4C9*yHQ z6w+y!C?PY{$MSo?0q$%;_BpPT%sjTK2wwl;iO5Qn=i-MGz-qT2{1qX!w}Fn| zW)=ccv7f8{agP&ntaGxjxZOa?&+oAB#7Y#x17o?Jq$fp=!xE(^nAAt8&1I;rbVpF$Lz|1KvYW+1-xS*=m+sPZ0kq;vQKw!x%D*F{U}RF zve+g9L*kctkGwLm$9}hRC}SJ_WwfJCZWm}`WZPYRWmxZyxc*#AF;8CV;^~!i2oq3b zK5izMU$k@V>&L~VHP7P-+&0k?=qJy2U77duX>B8!R=Gm6%`!S93K;rzMgEVAMB=hZ$-$7uG{XGU3orONIV6> zw@N!rAf`0@xpE5GY4rJ~lQ@&qOy!>LBWY3_C{<(`zEzI`cocbR$=56(x zQLIfIamKoU0fa>6$`dY7jAU|xCiO+qLB1CWr&ium?fame2yc@^d5Jpc8l(K*vD=&G zm-TWVH7GV3`~uo5UGwZ~s5iSxGv zq;4%EWWC#cEj2GUxN(CE3A`UnSVyh{j>g6M04Yc1oaZT&DXJk;VlMrWMK+lFu z$pykm2Qt@e0jMgK6I|(}^Pa+*+p#G2dsWk}ppGV6KJykCEqTc{{A}#`6InTiAC(ZEC7`6TmSUS%)S*Pw(Yq+>4zAGZz#4lt+(Fg~RJxRBsr zW@{wb8w!~ChMDkKBGSvZ?Ui0iNGDe2+=b#8DQh@j4Xdu_-7V zFhHj{aB8cBCB!s1KWYX+J{#o7fR*3(*G(+=Gn%7{u4cn+0$`9{Gm3@+9 zbsRfvT9sD6!(gOSt}eOrW1Ek&72Qn25{9)h6XPnE^Wa#d`ruW zj#WVCbDHAH8Jr$rkSbBm6huLYbgg7&?a(9j7W3LD@EjOo6bzy@susImBDhO!Fy+!Z zk}UZZNK#=1ZA47lS2|hfJ2V}N7MEP&D*`Z|1jUzj7s5oD;>mcE>3K7QS^~BqxTCT; zORqS85HrqZKe*esC;}0ZF_}At69qz=bgrR3f-kTMEH5{?|DxJDrYt|85pt6Sfyt(% ztPqgYFa>*up6Gv}Cz7O%U=PfA_wlL{lLNan5p8=X?2;c5IOtYbeC}QjEQ;TpNqFUI zFbi=h>Sr#=V-oA{(I@l`AjB)ACTdb1@*L9p#>0xms}XHidn3n;I2r2=N1ofwK?No8 zH60c%jFDy*uzRq@sI?h%+6W+lH_+R}8CmcB4)F#>h=LLLl0^&nm|ya0$OzOjIfW1HL9DXfb(yu0Qr;qB-IK| z8K0XS1iUrp4tcc9U3%Fb%jN7wB2c!v0u9%K{O~FNGAmAAvb3Y|IA^{`;vQ~-m_4jb zrwK%~-V=w+;~HsB@HDWonZpB0;19?XpLm*K&9SckMX;a+t}>SGmtPf>1faeRzb4@! z{UIOysfM=x%xNs&pnHGI&oEsU62P>e%0QHW*dhL&SDZZA0|MZ#@M=MFE3&(tk9=2R zEkh{SJ_R<2&tQSa%o)IJ1WPJ~gZqrKAE>p=VyF~cy$mXWX6Yv24~USlt~(FRidC(z z)O$=y^=b$C@|dfDQ+6B#e{Sz=wpuyYvGS_Oh%03u%T(!~KS(r(`S1x$&L=Iw^=|R& z#%_5eSUoEUiDM$9HHU>4hq^uwIcnT$-4yf*Oe)DZZ*-wUL@#=2_ngIyxgDo3@M2Bb z0QfRJ(O2hPIM%3|$Y|6nlzXebNL3^DlU!vVe=(Xu59N`tRQFT%1hoA1D6TyWDfrP~ z(DBki*%xqXByt@X%ID5?Vk`Ifop@%nu|6U90~}V|=nsnhmwrC}mo5Y=a5<3J2rZ&<>=dF6J9&4MYPUj6eoHFKGGd_kRjc>H4J6 z;x!E@Lu)5ivA#DUU#voPZo1S?p^eofrBPZILBS7{-zhdw%?a1_z14Iee--f71`HGn z4lfS}*Zf!t9S!?6fIUS@36B562FWex#|oz&E+{ct<&HfDAqU-?Rgg_>F~;zUuq-W~ zMQAd`n$lgL;`(8;>r$MlDvC@fpZ$&X@LIsncR78$Z~6&|laSdP0*)zZz#D|oHcEX( zQ}|E`F(CZn_elo3G=caph3{{BbPM+a{21oFJS#0@BbwTI zZ+=!;>s(SW2)B2??YeRRrkj&RF7{P9ftPu%S&siBqhWQu>a&+R-H3ip=jN274I*~e zej7kMk=0VzbartxyaK%ZD%SkG^xkqVijJ22tmg7YW9@2SaJm zr2MV->FoW|Y-JFSMb$OoGdyo6%!#M7w=#lycM=cADpOsv0&X6s@ewe{z^WA3tj@y@ z1Bo)Lu0<@5HV%+f&N@0e6&;#eD5V%r`*1c=kqlRZR81ft^E;7N=;~Cb3=eP(%bO(F zCCDG$=o4($ce$AnGbbraGbs0Xul?*qa}Z%{3Z2vJ*2oZ!Q7V1nu}28}t3%Ta4fAMr z6AXNW!;-h3wKJPOC||sb?D4*l{`t8Kq|dHn5I9IhRqb}}?%I>@W3{v{{(s`jWbeS7 ztkYuRwr+J$=)YqcjHr*e885V~Ew?RfOCLx}k*-Q@EjOmk%g2xtyfTbux&B^}Y~DI^ zb~L4^9oQopbRy&7PRy(K?Kapp4GkY22K1+m-omB~;|vs!J7OK@95GwVd|?&HcP`S=7YR4rSb53QG*{C-*}pcKjA8u~AW> zsO|NAaLq%XE6jz`#2)?%0{5wx*C$Y60ntf96?czJDsqy6jUv|CrB5MVX>x@lLl_~y zjSlU%hX8c|Y#j46G<`w`Pc{g}9Wt|PEtx6sci9)B+3w1jdAYL!*-sV!>Q>LL3@AL0H0qRPIZ>!5hAVFDq95z+-BVqJG^v(c(-F2P*VO3w! zJ~ToF8tirS?azyXS!c^UDD>r>1n!*6G@AP9TZQLEpn@jn8w1)REX;N&(L>f~ix$^I66oIbW~ zxWg4GGHNu^s=hH>ct_1|aMA|{ka2ck&1F3tQcD?+yX|61U)T+?D|dSxf2{5(9{Z{a zOUf33EI8~OtkA<|%d@u(9`nb!2b#)%N!G*^Ixz-LbZAH;L-bt);>jsOfR*4 zjC0T^xPd35BntOwxlS7uCgv2+r*g>^k&|nh#~i->LuvjXZM~4-gr141b5hT1eBBw)4xwEdVklkVc83ObbdM8` zDr=FV2J_EIN5EGpIOUp1LDdZJVYf2rptd7VD{-!~Z6;D~U>`zJaT<|%6bi4u;`lmn z*OHNb=J$o9$t#hzJ0l;m$U8-sg2sVI^%nHrjrkfr>crQdFwro;qxdM;un=6D`tOY| z#JW}Ri`5R@P}aQ!bEWcx+6?A@dFVDmbhO|^$+otbP-;!CdJVxs7P4`HjPFDZ52h}- zqQH{q1uy}hxa`#-H#2IBb9^L~XwPCnv#*FmJ$Lv6yD8KvjE-S}7wFp!*r|6vd#*Dq zrHt^8q4y>}Bq8kJSc#}DtwrvzAs_J^mB&J4hUxIx{$eHw`pMaKA;|nhj}PUb>Wwye zkQCMu6O)m)Hm;XggT=SI(%*>cK!Ffc`!mF>Mtq0uK>>KG-eV|4e)>6?fN91Q)~;=_ zMu@>A6BIts-1}f*vi})+Qe;eE4!WHhUyOO33-A@3OGWzLx1q?Q*;=CHtep1u<%%%KT#SW@Tto@~?zp3hgQ< z@7R4tct<>+q2+frp`wef((!h3hyXE#_|^(HF+DSW`Z`?Xoc>huck}QK3xGm|br0%G z_n4Y1qrPl$rMJUFIFJiygIAwHEg3cT&N1Qw*SJxyfZ7hYA!7x~a)$R_X^3?E6oheA z^iO5IB54GZHHnQw9j_dSkY#%q+u`{i-k(s4>F2Wk7QX*sl!>i3Ll3B@<%H$)*)MQw`7z@anl=Mq!m^FHNzZ=4O9CGvqMf_exYD;Zo1>*%zU z)JF%PK$Ke-1$OqD?_W}sIc={XA`_v`p`mRMRE4)vuii~WkDRC}_3TaFCZc0m&^2pW zD(0#FbNhQ8?#0wL#kcY-9%SZebV75z&fTBX=C6Q8L}StSxd4*wMBwC0iBvW}{z(wz z1l){lv^D`j3$e+RnMd#tlHE0pwf_5w&Lx-%;NJ3$!vk4SSabklRn&Wdl3~n8IO#qd zM8X|Ax1q1=u)|OP`vG!Q6;rKpxe);+0hu83i77 zv_vfFYP&%gNPIQ@x0qIGByaicp@fay+c2Z(Yte40S|JD}u%P}*IDiC80&RrlKbvmf z@9Jh%N7D0fAjx@q31EK-9M5_u3dMrSiR9S!q|)TqcT7==iPr1|RkN;e8qhAx;IBli zj)RbHT5^jaZ+?)Y&=5Ua#Y;jDrf23JR5>OM(gP;*6U8S$0e!x ze?rq+zE-dG*H-YAQxClXsUE;H`RsT*NC$CGjce0Ym<0^}`xo!6=9G`tlyh}`A~cNw zZKV}EHDHa^7)6W11%3(Ni>beM!~Z0oqc+pa$hRegytqvaWch`RJrsx;F>@7&XeGX+ zqXD!5&KUurp<@XHa$h(t5|{~AizkGg+;<=V0k3qqkPa-~Ns84G3WX7uyAzQ~*9CP7 zj%t$^hr44wD9ePrDKL7ThPhYDKB}9%28VU7fIp|vooiW=$#a1dQvh$4eAdFVkVD}!O9Z%fv){7mF z60;78_A@a^8l&mk3y-?`4xNDL(s1%Y(#gNlXB_oHK}J$+DI7wI6X4%M{u^9y1Tp&O zV@)(j6Qq1QM0eE~+?ZVF5|;AK3d&nA6_MZe=qv##7;W4@wF|w5I>p0PbTuJ)cHAuh185MHN%XI^Dn2CX;LcS3rrR&?G*Dh_ z@w<9YSpU7_^9flP>)#5$U5B&2Wk=s=XqlELa-RU(4Nn39{B< zP0tx-e1WjG=}CGiw~Wi7O0TXPiv=9c z6zuAo*?_5gs-k z$-~G{mG!TP_1Fx^ZKMKe2oCb6@V6l7Ujpft>y)j7{<~0g(Fl?)F1F)NZy#@0+NLB; zq^|VoRgtIE?`iu3G-vme6x|ZEszNs*)H9{l3*4}h;p`7N^38&~&lcj1*Lq;FBb`Hp z^mL4M?gF1wWTMbdgaP*77mBNrKeq&o$S-?L!N(duQl(h3Sdp?+sWcVSy;6jlzOe+E zgQ)xbUYfAP{(z(ubw^CuZ)mbf*2Jwjyn>=gSIwXa8`RWRPf zNX%Zy+9>NeYNMQVBnCdy5lr?sbOSw(M5sB{12MQ2CqiArZD{p)N;l}Hro3RJ9`zrI z@ks5lQzT+)jB~mcyyrPQrEZwbpOLzl&Wj|5eFL+4$gtr7{UiypyDQm0_yUDVAjgV` ztM`-L6Mrzbzo*9ndZ3vu6!0<;ewm=9_(fVABLZs4!^uO5}PxK-t z+YV0q89hM~jN)Gq3cMFFT-OdEp7$CeP&m zT2j^+{_zX`gQp3eZgK;o7foMx~(JE@NE0VAZ|o(M1SoX5>44{>6uK>UZtok`e_*CjRQ`kmX!W;Am>o zn_-U+n-+d&I`sUuN>M9lqX9;;26Nqw$A28#k4`A&(g7eW<1X$GzhFbbeH#Rp{ zJD1^g2Po@N=-=LnG+^){{#LA22v*QqKG(RT-y~f7LX1~q1M5F~#kBFHoePVdyz{Gs z$#=k;gr(`iQfZq${nHG6=M@`_hF7fP9J0toM!9su(BJ(<*{;+#55T;Uhywn@ITH@A#4XG)=#<%M_mHw#1yh4?^w;9IotRrnz}ZW{umi7C7sWU0P-u2*fI z^<2AAYykw91EbUJ{#P5ruiGyzB}*@2q4lseLs3QAD1}RHVt?!VKf~7Qnk&+JPl4|U zrrWb~PjttwdWWy?F6lDFhFX*Zkj5fbAF&jvpeCKvQ!w&8S5yfBpkNHFKkt!-fiorO zA+tC2d}me~mbGNS#(+FB+lX%8;t`Jze2){Eb@I`gu(rcDh7dd6-rI@`L(km9wO2uo zR`^9aIOHjdl4I_E2kv1{2WoqPA=u{9Z{l-3Y|O(X_i4~V$9&@w8ZWEol5ZU$CjZy~ zp~y9K^h|r150=Ig|9z?!#vJNFN#wmZ7{2`!;}WeTczX7E=Q$-KBrY8f1dkErKW@*c zjYZLvy_V{B|F{?H5Ou?Sx2|$bhTCox(LC8F@@<(XaM|K+wIuoHW3VAQ!0%FbmJToX z-z4v$1L5RVB8RnIZY0(r?^yM-c@!$YpK4O^ydk&1ooG~BfzvMQg&*}%iCCB}T8M6th< z`+k{)Rd+;CX+$yFoNX-j-mI~xW(pX7ZG0o3#RGot>FX`f?9u;^ZyD&lzCC!YmI(5Y zK8vjfw}uG%WNf4#hYQMY6#rmVZj^k5eu3L~RV-pIl8u{DcAsgi=Cj@FD1Gx~Fj1ca z88Xx`h5!y$JDYh;t+vcl7L5vyP|Y=gdThb5FhQ=26ND%tEpN|$WnZSP*a!F_6H?g9PN9++Fr!2ea2r03G zM#C?AF%ow`(E+1(8-Yy+)||-FG8@4Z3~l-<*18;pSgG`2ts@nCrZDozslQX2opS>S z+93_44U-oXJpTo}&8TIi224koG{KX3nk5B|uU#VqFzNV;y=Q{Z%YH`X;9cxfX^fxaQccv0Tbu zdU;)UTo*%*d>7~xNSF`qLGZ-`blnoCKbZQ`3!ihWpIgBEpR7lgfs%=VREub3U7e|S zQQzyD4tFZ3^uVCD+ob4cQ@HpRBDEoyrjA2P2Zb%7YqgjiK6&icoieYcUzq7zl=u^l zNR}z#+9jhLUm8cD!D*I3DYP=gMMp6$Qi5*+Q~3F&8pGKrDR8JqUc-#eJzbZ#(R4VZ z)JZo=(y6n0`bq2M$J`}b z8s@Ojo3#otbS+Z#$7VgLgYAJl%A9aS$MzT++mei4GPH0AL^J+aHhc`hH+ehVlp6Sn)+>4SrwuP>{yu+Y&gIiT zv7k;#zs#(z=9QWCLql!}-#fa!x^YLiyyv#&Fcv;1)ZsosaD+A&Jz@PeRUw#OTy(3S z>K^X9Z+>*F;B`cBa}c>1#szv`Jv@MOGMI1c6Fvhw$N0T|kIy{v!1B8uxso_56tCuyU+<=l=?vW_S#@~ zBMmMdk}BHZMO>-j_V~X{FgHE&%)eW8ufb0w9=kBp#h@jTOn>`mBn2dy>~rvT zo<{^^YX)vZz18ET0d8`T2ZsNVG483?xtw6CO%wZfH8$)6A=#1%a4WSoJfT|66Dqpn z(M>AUlxYr1%A;YjbYm+KFY47{V$FjaD&BO z{V#}CeBq9`0`@O5{j#3bjMl1MwW2!&qPCCRI@Xs}Zv0%%$nnAvnN8g1tpO?5<|e|>^5P6 z((68yukW3K2rU7+Vp!W!B$(9b1lpgwY=217scZzmfs3MCSsUK|)h_g>Wr9IcvoPBC zIO4IhHB>LxJb#+MVG=g0qbOx0Ox@WV$q7jMu5kDRhF8^--q_Fn=6iERt@4Q1#}p``2%Z-%Xw(MQqe? zflWeKIE1j_037uwyJ+o;{I>fR*G4b2X*82vhHG`dEoewmq+BB&ly>fhFt9JbUHE7oU8A68HK7gW za+0f#Ah8W=c=F+{OKg}~_9dVv6&fEHgv@qqH^DL!?UOl}@BT1;mYL$~D;c$6C6t`w zmh~Gi4o4px5cVvooNe7r{lU*r375fZjAO#<3!jIWrCP zU0}zSM@L9JW3I;Pjc;u^Xc0Je4OE_gB&jqA7veYRq|CN_O&idLFJiz3Y~R*fQ6>7L zhd4c~c*I=}*QoqjF8#Z=#fo4&w@+(qc||t57;*LIdg$-u1<#-2gSxb*^!nqFJ5}(F z*Aod8nkcyMT_ZXG<=4ZgwoxJZRHfHhLye%}ziHf|2b9D6tRQ|!!c6y&m=tRE2XFbuxw^Mx-DL8Jy0=uvM2Ny6k_i297Qel`V;Y2 z%`*i-APO~IUf>rtYbYg7`l-QYDg9V5DXO3AnF!cTU-=gi1~tKyWiClh_Efqb6hi_q z-UJoQ=rbfN+btcXvTop@rDRb_>uK%W>G`r3kjBn)v(+(Unjo)&9lcY>FUD4#p`k}t z30cv`rwldLmsh!{P{2X}@Rf-6`VV5oa|@0KV@zCQTyE2h9eBJs_0TlRrB1+XSg&dc z#6v$^JI7V=A`iF{Cq;Ja*dz<_hR9dw3zFuhW^oIJ=T$1HKib-MPHIv?IPQVeZWdYE zlbtoeM%)xNySRnFa2a` zu0oSnrkA+K1Eu4r=e@VwkS9xDu%6t-c#I@S`^P$6kKk&Gh1j-P&^I&F^0$U0lW`~tA-TSABT?7H7WW}+aNNogzdR!^i~CNM7qWNvyr~2T$ys_iA&edK0o&UY9LOI2z}XT5t^i6I2L4EcU*~n z+bm`SC2u)NW#3fK$G=&YCF)o_vg~@J$yjeb7mp$~ou9lKsyOz{8+uv|HZ7M99iiX8X_zzdT^SJ~0m(56){JMK5+QwuH7){U#N z<*PuPE!CU59;pR6FGy7jeGFKL^NjAxI|I_Fy?loG(Mvk+NTXc(1(agS6b!g-ArL|jx6jG(@Gs8Z|83tEBx^g@6J0M(@EgJjm*-dCd}16`~JG=$eKBV>_-HD zUu=!1eNA?lGb!?vel=Ke& zCP+;|ZwXhxrnL!iTcch^gX2LS)3xKf`Mcw@maQAmqL<}n)(WWa?zv>qz{Z}7F3@Bd z-}-;jnhPk}T=0vjKTstPgKj8Gq)vMK;;_I z2os{*++Z@6IYmlBc7v)VwTIXb*qyvNxXBOYa!mQR9CbmnX#GL%I*@jqA z^@hO2ML3Zj7Su<(N^i@??`Jlz7Y>dFn65U(EFV>s9v|yzy67!svd zl8g!}AyN^vf8i8>ZF{B}J`(>a2|5qX`bE_xH>*3-%dE$6*hEb_6N*{`bs;jcW@%l) z53zmiK$Hp^V0ViJW6s!z$zj?b(_gAElznbDLT(V0mJOLn4O4C5YmI5BF$=|skK0I} z_~vTx4DR6^31iNkMMCm-G5c@|tum@?9dhBgwjwyjNlvNP}efss%eqEP;k9E23VZ-OArS$Gx!q(V*>=nURHSwwQ{w9lL zn+O``JitH)hh13v^yPN09(jVRicTb;lpk1N@aXy3?6d-~*>OpXt0s@F`H6+${2`!W z`j%mWxry>s8F8h19m|%ORkv1%#zM$M>X^~uKRzeC4X39imnL|UVdj}rQ1E%ECFit0 zOy;TkJp9m5U5^V4B%2#RL{v3J=m$qDZ1UBTR>>hwuZE9&bly}X;yzn_z##hIKG%MfpwcZNi0 z^cLSYgdgGY1)!8C=0;P%Q70S&al66UZn;xu$`(J{sO6BNq`$(lXxD9TI5qejcAzuQql}kx=S0t5u4>jcR>W`Z!j1jIY66Re(!p zbSuMU_(dj(p znU5>xzM9J4(00sFq`8Ud0+qO9SQJw$HzWCbhXDPreHcv`k{1G&WA^nicI;hSz)T~d zzr|JKnPYTGq<%b*I<8Wi0Ar0?AFO04yIT(5y~+f2_k;bA-%a%4Q!ZqI0<%%dHc@^Z z!4j>4353KQZ1`&^EZxr6t(7RNt)0`He&}@hFgkG^d1aUF<3qJOiJAr_Cu978PQf-;G#gzw z*4PZYuQDI>7Z|BFpoN<$w~D(8M)CY=Uly9Zx{*f114r(XvB@M(e!x9DyD%ZtviG_G z$|0RrkYyg_jpa{lP>hVS^<4To8p4A7B2D?>%%@_r^baO9v*gYi;G(Hv(LA^BR?g7g z)TpBc$R#*SN`?$mQ_52Vf68~Qb16JuA01gzD0dQNZz+E#fYAFWzk8)R{s`(<-#=1* zLE|GFn^yu#yok(J4Q4;KMSR-$y4K8MbVok1EW{=<%r;%4eYZ6DtqI+jIg@F52JE3`}NU7qH}?liQ29O}E~gg764XiRLUu z0ZdB9jB7RW$5_%TJX>BPsQDO(y^Em4;fwVRe;aOvin{Psf2+R#M|c6fe@`g!2aNd0 zaeK?9%K~@6-#oaR837XQIA?V$^$nwAvg6ycW=aug0QD7^-s4Yp2j=AAvxYK;wohq0kmdo%c<- zo6P9z@hzos!EYrE^Y+EaZlNmFsNQFbU6U2Qs$6c};EfMA#xq%6nx%KR;GLV@6vTX% zIPxT9+hkr|@h4|e(f;y7DimVQ3w_{$YoyVy2P`##{Q-Ppp_t`kZ9QFJ#otG))-ZE& zIo-)#pk)1NGz^z9oXu5A7Y>Z63vdg;<|a9_p`@UFU(5ymFt5=p;*o)>6V>&Jh@ROEhR-Yd;;UiGqa}M z2grg>WP{0NoS18iifeae{C3UE0+B&6!|Xt#C%CGKb}A^63Ef~K#POq{n|>W$*6qOWta3QPo?!ae`l`S+&Uw9d zZUzs6#rnu2)SO%h-k7a-xYZ_W=v}|6avEiBO;6B(|BCFI(HGxwT2V>B@`aH2e>5uf z2%3csY5j0NkCVj^FQW7RoeQL5!>997Dwjipel+rr_Csk*8FI{x5#7)eaX9tU*-GS$ zjts=#ATnxYk3q-1>9nz{1FX7$INsMLanD9KD85#GUc|(|CKRoigrqWXuJxR5g|Xn& zy=7hvrY zek6?$3(u?^5rX8ih=c8d3|DmO#Cf{eB|}*Ehp2D5qE@jx$4Pk@v(DbZA=9d_^hIL= zbP^NA-C$QtkMR2Zy+t3rw19jTsH4pmC93?;OODaaLpQ(AX`#vm>-3H{zt5R`0F+T} z9M~H7RK?DQD||jU@pdZwn8KED119!8u9S)~Ob1d_gJ0jaNHPegF$B;V z*5)YQxOobiyV4`~uGYRpjUru-U1OoU54%`_=pwc`rc}}J*3-R``xamN6412$)Kr{+ z-rs;Ug1<*c7@nMMJPODjVb=?UXCwyEtph4aSy#QO*Kw=exE|b&1q4FEWypkhr9sVM z=?EP^wbR7jW!|MeTv{ju`p7)Ih&$Fp|^u5gVIDIh_j7|mc;sxpi zS{Rt#hF3DU_#%{+))qzv15<5kN&J#9_(H-D{+RKOQ9<$PB|XRzjC7kpF0L;Z+pa3P zudMf7rN4ph10%UH<|ew!Jz12mzR;J3UH}p`Xky-`QC==u-#Nd!{ue3ThKBZDFVk=oi z@434EyjWoc2OMV0lkpZJ^r~fqF(LT!>st6{=Dz^CdQ%6|X6;FA&n2}UW*sEV9CFtt zI|uzgX&I*Grz!&aji#jVRCH-sjy3u&6<&%Dtsb*3a|dpouMRF`12<>;bvBk&~# zU*_o>W(KVRlMU(!dNQHj`6PX8@5`8wmf#>fzZsk?^8a)4j0H}~?f!npZ>F4CtV^)( zd*6&}lc7}|Ao7geLnWU0NM`LLH2@E4o(*jgbXXYYvE!u(?gF<%{J$XSOpNxmvxYgZ zY&aMR#sCu+W5D}_(aj)CTMeFn1Jo=qYX2**)$zYD=G*5>SuMB2@1RAz>V`%s68IJN z6u)rvGa9I!JVXUPL`V)oh{AE#-2ZA4ECiHoXuAWBjWlam1I(wHbF8pG<2**f@n18} z!$&{>o?v5!`NN%p_WQIe8}RJ6fS4vMa%32F`x~0n0VXwxrwmO-+Q8$lWghthU^10K zsMp9&L+t%y`Za;Mnl)%GmrQmKu%P^8VueApN8QQ2slPxp0f0RViEhh%;LKIatFo|Nm8v zRJfAX*VqkYXYTWb+|{W*#f#st=68!BCCD20s%TlRwd0|=2lT^j=jv1Tv{UC%G`W5K z2A_~1G|Ws3Nvq<^LcpcSwv zEK#Nx1c0fcxHNmw{E@FxHuk{G%zqk)s~(ef#`lL%;#`BG@=>0G8C@w1hARSfT8?s& zR&uZ;7-|C9r(X(gF-l4~wt&b8ldS|3DoDVXxF-)1u}`HQ+Ad;bEsBfGm7m!C^Nh9b z3_KdSCZZq-dbZUl#?BGfA;>xIybTCLsXw^}&b!pl3t^;_J+ec|B``E}29h&ngCUmW zBw9+gkzyYhO#F2|lDq#T8BGLq3m=;{;(eSz2WBB_WQqbDmvShj_D)Edxi8y~w5vFX z$)OAKoOAtmplzO0LMDx48{Xs~%fn5}3QpidM213>i2(H8p;ABBLu75T*VAc)ulcF16d^#YpWar;n#H0tQ45q_6z23?47=n zN>v*!!f^_#H`Rl^Z^4QHs$pq>+4-D2teFbTrLZIBE>XTz*y!{eF`ei_5Bkoyd}^!a z(5+>w&adVpI(1$JwOwByK{uMr?-^r0J{%B?b_>6TO;>p7pnSN1s~+$`>%usF22Y^` zAro@%m&^NUO4P47!H}fimA;_}S0Uq7S5VUb@tZf2aERZHxkQXJSFj6a zh(UBBp)&fry59nv9YPbJG&)vi>Il#Bz{kY*?OzS>H-VpPU(sKzZG|mi;ER!i!H(TR_g<6p4qX~q;n!XoE*KS&V)h|Vn4tOqZQUo zJO`ETU81JGjdmFm8t^+PQRA`EN??U$Z&jXww>p7wQ_GLTJ%pPqbF&53O>%-#Zz05? zqm}fV6dM`k^~140#itk=6<|{QEW|N&;K;&?Bl8LW_&>p-moFW9xe3r%z=7LE%eut| zGJ}Ru;@_CaB5n}q!b>;#rBMI+E&xL?m+#5|Qb}@`@s+rkJfvVV0{9U?WcC}AfU4`> zw3G3DxM3g58>^h;=Iry=Z!?5PeQKa$S;US}(n|J@qrKBd^@SQD<9o+DUe51v7X| zfDcPDJ%~tgj6qe*aVIkR>`NHxM>w?!Of`ipKKwTd1sl`du4 z6jQvg)IKJ`hmT7YQIJm(f_wKtD0z zZ&hn}rHytA2u+PA^556eO=)XpF)0i;DD38&)<A6DoSB{1N5S@ z9o9k&oMR?E!$RV(xu@tSFgy`H$NlH~(v(!--+#1JKBx^Qktf@E!LTPW0S-GbVp?b- zDQkg(BvT=SRkb%@l;Z2i04{fCHDGh~XjcFS$Ly z1c^AEfu2qLWh`$6&Wmf@@gfm()LBMKA71jrjXv$4Bx~OS!NNF3~sWJPJtqo*LF2iftKW))1*TFiVE%ljUrUR0~=KariwX- zlPTnzPQB;?mBf2X5Ao#2oY~STB<8)w!#Qpw!%7Cz&Yk=T?SLnP2*%J8Hl7GcQN>%T zDfT`gVxLB!U9)K1V=Hy@mz^E+u1HX}=?N?JGJ|2);JI%>eCRQRy${TOZ0L6c0$sq9 z5|09clx*K|!)NQtNelujP_;doMPJ}6VSrH66<-B+=}IP=No3ZI^9z&L9We~EKoVqg zR+^b?U3L>a>CpY<+Xtwg)r<*M>8;g6W6EI+PQ#HmJO_>J%-<9XQ9rr-um4A z3VhjKwrQLxP(&)w#DEUO?po2}3M|(+%er~#zm;&twpeSz?Chq)|Ge;WR14=ccVo8x zOkknf{5t2TwaLl}cg@zsLtGRc$SufX+E#Npb0pG2;uYSLpKlYAB%!b*6e_~iEXj8A z%0v(O3P&zkOE1dnk+ctO=f{G0$_=s{(0dhtunFZdC(g$?pyBI6QZqK>Jy*WrD6a?m z^$H(pmyjpbc?2{Wd*q=uVh-J!vR(SA?`kcmYr=Bn^r_(J%WA{PRHMrXlo=wJ`n1Wr zv*EQGy4v)ItAb0d@ol_dg&=&T&770izwJT^TEf)hru-XKN^q)^XT|o{k%3 zK+36ZyL0N9lNn>IvCsB}n6ytBk{8Z5dG^X?m2YsPBG9@#YyVXV?6V>xtsX-q;gt+x zGa7K;xRW}c{}iT|wK!wmF5OL!{G#8p3Sj#_y#@zj4;%onY9a>CD=mb)HEC#ao7Zfn z(@2@*G%Q`X9I#NL+}*zl!5>Q({GI`NE*ZH~n|n2GV!?J=++}XNoXA)v<|Oj+?=2`- zJQwf_&T4A}3>uOB_6LOHn}9|W(>O|rtRrABwQ8sX`~bj}XH++R9#B;zg{6pcf3A|4 za+98J;4IE!d_Q~oPRM;OB)WJyl`)L4q#J4yS@M*rR%zYh%w3)ba~`d`7QkbX4p?BR z$R!Nc9zl>_3Axi};l1i^eOPO$2UnqbXkt3bIO_P_@Lmhm!PaXP$@~wfsf7I6vgwx? z9mfhAlqW|4pjo1gag7>br0w5KfMYH9+#TQ`?*9EpT$YG}*p9YS#(UlM@^4V_4@WPe zZMKZoU3Xmu5Q-|fV8SRv3Rp7Ld^MdP+!42978DGwet0p5Sl+9E?(uaVm19R=q`-+<0Qr;Je%_GTOI*I38gDY$N9o+P` zP9;q_w~^i7FT;>lfz-$V3Hz^%DJ?GH1j{oNkvFD9S7GP{l(Nm!Pf$;3b zz9be)-$&L1kiOH%4Cmsg(X+x3& ztHCPDc>=hAF{R2n+e{q^)!n)ix<;(`UTRMaiJl)3egIZWY*>+Yz-{!>r4l~}pLGUX zo@9&8$G3~`ZlmhE$=0U5F}1}jmm->?>}vBJQY|4w1oEmRGVPF?h38Z{J!w23>iK8GuLDDkpgFhe9|l0x@F z0%@nnU66ZmUM2U{7zbl4V8CNutyCJ#(dkIpY;mJCp2}58vRgJlQi4WS&B6u2Ucs<{&UNgrWC&9dN>U zlqC38YDD^=Dpjhh4tV-LT;2JR{~OH~{q93?n6SNV$o#YcbJ&%Co#Xd2`8-7MeODi5 zKB`CTFXD6lK#dblTH@6j@W3kimRql`n)>6>X_UhB)*Z}`Az4nc`$J25W{BMS*7$6b zKVA4|qWHsuJe2qeR10!M$#X8KSY*Ka9lG3hFL~GES+A6A7huW^Kr;V8V$TlZWO{S0 z5d|8ixR^k*Q^0v(wa5t0gr%ajc^23Uzsudf?4ph3Y}RS8PkIYgi;9qyX`6_7vy*q@ zE_iwiO|VO;w!Xvb?0OyMlg@us0*2jN-B+W^>MG1w5p{4uMnRNdv)19)Yg5* z%XP~_pl6*O?Be6FRAc#~?)p2*fxs|Fu*g?!Nx7nItb!EHKwX696ZTQ~vOss3)6P}H z`9mf&9PHp3wz`iNA?mW9?oR*Ppp5`&pr%JbMD&7=in5Wziy0w!OalFYzX^w4U`l)D zCm<@gFqIXq<%LBLBNP6-1!wZ+=atESU>w6V+N3i$#$g-W9#LfnzcJZ*uV`g^w(VYc zaTN~_Xwc^82!!YWq?plui3)i})n=5GohQtg0hbJCqs>n4PQ+EkFsw+vyTD z=ao?@9)pv{)jAVdUTNmSe73!9ZBnoZx!bk@aRY2oTQh1k*Vg7Pux_{j{Nh$u7{g#% zZXePATW(8HDn2cFb> zH0YCB=k^ot=%nGhE#Dpmodfw1Ge?(uiU*pnTjlLE)fsEA%cNmPd$o=}FFV*6l{4Yi zYP5oBCtb&YXs~sdih>w?emjeh?^HlO(YCLD;TKJF;xE6A*Vqcez=79-T}Essf5$kc z;epf|tgKRLqlG)No({K7q;<9&P>?;W_y7vqEs0L%Xsg6rC|ju@7J!aJNz7yut575@ zM+JHcv#LFBzK)CgtTo0{OcgK2^X&C=JN<_yX@vTlA2n14y2nP);wXI?twEzG-Uy6s6p`SOKB zvaz`~^;zf?JS+&+zK$(4XJlOYYwkT93v-ZIp|ld-e{7Ds1&t7D8E1HLS*#n>y-PE;*`;2=I;@^3fRL8gO5$H)rm z7^Q|X`v7KR!zh2fB!L`L^6TGCViq?!UNM}3I3`89|B+K>-%*^!LLBV>r+D5>N`!;N zp|k!lKPmeg=W(I4{5RTOtb7d|7|zn zEpAUtv-audT{TI!D~4*HC0F=5nrx;0P0|PBoD0c&RVL$|uUud%dmDOn@H=8LuE%Be zhUE+ZRyt9JmG+e*U>V6lznl&(su$z1;7d}%BwdszpAu9tBoTibv;DrDnT|lNF=~LS za9HUe`gkQW10i0w`6*-zorld}jAYjgy_HA%Oho^DyP1JH4Xa5y&G8<5#?KFKi6O(v zYj4ssXqzJuX@E7N_B9Nn=~rk=L@Y zZH=`scvRQ<7~$92Cz36+vA;U)X@91K6S;#XAc6*OJfwzWS>V}3o$_F_By8bSxtEbgLajpvhV;{| z`WW&gpWe-XMNw%;ws9K2W@e9D0VoLZ7UTzF{FM&bfFU*{A1wETSY^a70L-V|fra%j zP>Olpv*g^72lH)g;7j5a%a`+Vw2)FCcyi`vIvO;<=FrR4-yo-4?UzQQ)#QUIV=#_~ zTDXf$n{;0n*l-^Oh4pV}$8_6ah)eSy%x@;WAF3s&)KpkKdok+vRC><1YNvD zhxNknS6y3e)W3_aaX;K`IJZU;j=nqoyWwmt*Z5Kf0{PYZX#+*4OK3R%&tCc*iJLsFOuTkdz+L-1yGI*YQDq zdC=tSR|qn3eezkM%2y6N3ez7=4U=@jtW>7%_#@iu1<$=Ln$Md7)}dRz;WR|CZQ-5) z9fT8gM-Q)y+oSoftxFXvsbKQt`r#8BBm144%K zJi2Za-6e-sraI}AIIR69Iydt&YhgEd?%{S}@=`jB(xF_3{|D>g{QuxL(&D0T(_Tcl z%oIXfRo^r^FjtXIaJG2xLwvl~e@@D0QQB(;$2AQ)XBcC?Lcqi&%({&u^wU76`M5@- zVT5!LD&#l)K2F1gvaS+pKAloOTB9^^QK^bKGfR#Pr8s!=mD}kekuaG}t^ohL*m)8B zIg)W~Bt6zxWwv)UG3wkQu?~iV^!zTu##%{P9zNc)YZbN?-gq;;J2LxX00}EO`dVrE zy|SVsv|XF(C{h2tYAU$&<$xvt&I3db?>jpv)tz9elt|~q#HO1UA5{tz#tg8#H=L)V z^jJSLG_a6veAGsf$7vxS@L+hmV| zKvZ?M=oHv_uu&{obWfJ^SOzLHYcou4{xs~E)~BNJs&7#i`(DPI==mL;^Yt6UL?sGcCGd%yfXacdJVor;qMOvrJChI3d4RS%F)N04mHi!e!#6 z=)L57yc%FfinPl*cwTTFRyGqYLpHes4Kr@K!}mOQAcMUJSZI3(?BG}X=J^tZ&UOZy zwX6Kkh@|5mi2|j#93VGh`*DDzg(I{>zT(XlYETD$J z2QO;ttPPI(r9xUd8j8b#nynmCA%qwqiW548d+3ae66qnDHZQOzXX~1_g=~6#&L)=c zYPTtRHD6vEJG6P?4{mVG`I^CEg*(z{gfjvN7|sER4jp{>EbW~7JcSQ*C}i))OSV{s zG_+q@I=R1`T+h&do*ftbmNZ=qtG7^($`5s^r2oWi5$-~pLzQ5neIWh>;zJsSZJy$s z&EA+1B51Sij$>ar;{^CyCenn#EZS>6!5FJGE|fIP3r+9=A#TYTcXOzNCLE_3gAf1W z9=658d>*j=Z7llyWSCNAP1_~BHXGCJTWn?Y|LL+B583!L$?ajk6vgSFh0FJTQHL~V@YYkbr8SEkg53>%Lf9un@Ll|WxIOoTJO+? zb6B*SyxMz7h46IQZNyX5AeN@V?f(>6k0!-JW=$H}!$3v=#uJ{5Y8Hk>%J7&)BP-dK zBVz(|y0Y<| zWNhU*C>r-d+dR78IiKj*!ZPk6LNkh#J&``TwV;#T>j8a6ej@!Gp;=-0se_itBawUh zwWKXHPRGc*V(jZijeiVfJ=TqBSIPYUJT!p;6NqEF(rbr*Lq-x{lfch3gklb70glTS z5RLUJbF|jzZ@9>5Ef(CL4P}E|9zFD;jhGclN7Dqm5d9iR&NT;sG!^ zG1co!GDEH%%jhuqHgr(IXCI+!TBKDtZ*^#dWy(oeL<6{lbILs=T)7uRnr2h1&sx<{z48Eu9V8()S;~ z1V=W4WfDT1hU;GN#KV7r?)k8X+S4D-qq~d&CYk2BYCTLiuyFWWXswyOHQl}LcaWB^ z7nt1Tp$Fx&IemZQ2vs7B;ZOJ(XudiuZxS+ylpv#I2f^}2CSh;bUW%|WZl-&}ghSVl zVSMp;foB>Bd6C}-2zoF@L4z*Z_ualN=6GW6Nod%xI;SUE!0>tRT>E^RlYi4sGVko( z4I1=2e>h16$Gkq3Vg|6BeQn7UMLFB_WFn$bwV_CT()M~+ zega}a@<1wpf|nDz`aOmgXlgP{T;EA~tC3^20J zOGP@0mk-^;PF*|43UXy(M0b7XE0-|feap>7cvI7SExD0OJx2pr9MXwy@p~#~K;;@7-p(8xpDN{T5Gsh+ne1BV zePy+<|JNotJk+ba2IGxmdxFd3X!pn9)m@U3K)Cd>D3?LdJtR%gB1E9}_4GKh5&Oay z8wrSsKJ|B3;)FRcLs=ZgP5;50edc6iG%IDA@5A=bS!XkPabjjqE*cFG_lP!uSti|a^b zmd^A+E?1XZ;W|=s^W_K@!Wjw~YjD>QVxf(YO&KjE&l9`gX^xjM_ubgO2&voB?WB)L z5}Db;3ET8U$y|hzp^UFwgpDn3L-z@amFC0NxqQM7mO)?n?52e#hEgHHhpf`o-!;@> zBgFOyN|Y}7(j+nPeJ>1yW4i$#?jKnF_{1+~oSd<+7@Xj^dB%`j*%N&Y`qe$0qM~f0c0JU z;stvPfPW9Vf#9^#ipDJ8F_V07La53-%U!^dwQA57C(qg0ZTI1Q+pH$@uAn06W`bE| zZko|onLzo4yXmiSdCHe>h zZTTCX4>#tU4_|Q^=MP_y9G7oouM=W?ZICH_!%P4xhPNyU@s&G{#Ew##uChSLqNjLO z<{qPIhIn$%)c94g^JV`>nwtIzC)`anEEa~<<(mD8L(FqCI1aM-rPa<W-S)&U$(jUmc`A0 z#P(16(=PEe*Ew9LNZvdyUL7*8zAM`N^%RxnH>}>EhAH6MeYd2R8ENj{`cm!?ux9%3 zXYd612utc&W%JXF@8L0$#FOsyYk19yF_wor|9PG&V5o{0CWv?;`Mw$|0f;!_74%Ec z?Z;F46ERqqVFRvGO)lHr^=P=?GA3Xp=CN<65D;ejX*X^pWVl0CCs8xz0`~bb1Lf1k z@R>_>lrI|_*Mv~7yS}go+h7mSqbJXCy_3MaEy4QR!J$#d4!dJgWT;&kDNm+!i)q@c zFEH2-v!(m~MZYgPYGf4tIjJfuoq5oo_PiE5O>!ITi~=z0L@$;H)xWNwubJ8DB85N+ z_!1yJ-CyBQ&h%=Q1Afrmq%ASCO~@2ShCiAje+9yQH#~UTW(!?07k;uCd4CR;atWA- ztB4OSv0Bh}iN#X?a0YoTIE;V+fQxIHMSk9Bs-zefiMUb`EAg&&`1i2BQcR~~?MNT{ zWi4v>uy*V-c#sT0>*6423AJ@HKsHg$;gliYtG*jrmPs# z3Ph4NEc15+9e$+{wl80XL}X8{;+a38eXqu1BikN!W+{r{EJoj5bu(x-dWTw3CwZ{& zjk~H+VbsqRzkj8876?$F&M>`ol~q9d$b9FpSgMazTsivc1&NewOQ#x;zDIf^NR$^7 z1=%2_w4Q20VUH?hH9BrTr0A5Pi_V%ElZ|JSvt85!)4&o{YOD=$@QknK2O>QzvGt20K-l$~_D@M)WYXwTNN%%5nHqJaw(SZxCAzT+ zon4qfarIyIec#Fs&`WVteid-&wz-TrdI1L~5tb_ao4$#aH=p>}#t5>&rKr(~A15nv zvG7?L%^+BWSI}RB4LqQrrHAa{lIo>8nou1>GgH7G>hixD`5Yo(=e_M)nVY4Ov8&2$ zU~}h{5JIUA9iD(EZ9rX+f`J3e;Qcc}_#K}LH-cDj%fwDCgX69`@a)YeND4TUP=V3W zAr{ZZrM{X7ZlYjNY_vATTLnP>T1a5A1Qquq$ZO!@GRA#ESV}(Pu%w&En`jR>f2VGm zN)6T2LWKRJC&X00n*socl>LE&4W1T+s1f==8k zcCaY6P_aBM7Pi6<2_zF{FFA&XwE|Hy)|o49Wr^ds>TV7AwxvJipT`_8zq2P0FOobh zLCsn~a?qS_&{lAaju^V#TMg2NX2wT!RVo|Ng4vM5Ph^N;q7V~Xwp zYHDOveFw#%eHZh_`WoKcgf~ZFiU@!k{HwC)0gLzbl?2zS91)Lg=JMYIbCGiKUu4|F zCNX{}`FxXr-a%y?0G|qYM^E;H=G$b0Lq#A)GJGh`90G<&6C%jg5&)6VQ#;3Tl&Q&C zi|&yZUaq`gg?*Q|SVJa4g}M?^(#L%H?J0Ap)mgKM_4mo0LO|S*PsYl01$O-Ptfziv zFW@2_p&32(DD0+lu}qNsh>;GwQN4@gkLk{OK+}J0rOUM1Xx7`8==)+dB2C&D`w^jk z+SP~kgbMSIoY9dO2tLR2=eRPv2{FzqocuOuS61Ci$1OfV%N!(LU2@YhaLfSQH8GtE zoM$5^Yr|G-sePf_ZRV=5ZOZfo|nDtpO%E7P8N4N1hP0O9~W@ z6YHKDZNQix7hOdQQ?nW54Nhlv8|p^D0<<|83BQc;Jf*&bpMt;m{k7s@l@b~^1}^F>CD^x)`&Fc> zrek)v)CLcXV=>Mr&N)QZkWr{ljonPyZ1Pi1C_<89N1=^e@s{Ry<0kt_hW~hX@Qbao zDKE(DdQt^=Q(X%FiRuxJEq-8YYlc8yI(YEC#FFi4L*-~^S-m{~wKJC0G>Pd!jgi`U&ird=TFcp>9i8S6OJchh()dS{dxtbxA) zXB*%cZ2L@h3aI;!Dle!i05WG7N;4xfP49>;H?EbXuuk+N5kz`=yRhULj%jDl5$1U`;u=QY0zx zA5GT(6wR)_N1yIl-Wg>(Wx z3zKFY(V#HGN#ObuEM*uBmZk}36a4n$C}C%y^U&-9$vsca=zYzUWTM`8Qi8MBeSFgt zHYO7}{b`d3=;tCQ-biJ?Dnok~2OHEU;PeL?hsIf`*mhvpGU|*C=$@F8w9=-l_}~{X zvydhiLAwu}#eju`KN!<>O5^TUt+?kgVftCV3i{woeeX*`actv1`wW`>l&{&4k@8>v zYhhWy6dnC*N73ZA09y+AFbkLbY)r#b7po466NR_uPB-*f3B5Pon}<{DdOQF|Ucg~< zt*m`?aZ+A=3$=1?Ockg zvGo-(#3rRRsNL;V^HkT%Yr`bZHblG^nI))Mm=ddTQrvF%T$=U2I*mh-ULE%0fr0H) zP3gX!<8+t!Du%d%_CGQ2g0P`~1-AHabmF9!IQ5(!8?^%Wh#&%|Gmu~q?QJr;89tub zaHI7T!8gkouzJMEuPFTU&_irykMaP0LA_|ugD&ZFjY;Wu{#YDps;UO{QrnYwM{WWj z&|fO}&4(DXOXNjspbda%r}2g(G2P)??d|hR1#9-@JKlFK?epI(CqNE+;!j+gYQI4P z!Y}j#{ft;v`I#1iO1CfDiD(%I1qU4G3~# z%#W`+z?D{SKdG!P54d)X4l^EEIzMUZM_?s`ve2HUDICdjNAPsry!a7j6cN4zZDzsR zKEBl)dJBu3H&YsHsj6u-u?n$qJo|?3ZuOm)3(-nR^h@p_QIxRg+zG;uOT;{3p0-#y7(k7v{rB% zc>Z8+*+4wzBI6)no>cjB>xl~hRC5XxaQ+6S?*Jb>p;Us*=D=9NQM6|V!wmN~^Z=DB zAiAlK-8ynDeK|D2Fs#ljJ8MfXyIHuLSC*NZ#!$#{&lVKAN4tm2&YX2{+?Y2F9j@wE%%>Mdj)TvC$o?$;GoH`M5>Kg znpXrc5IG_PkkO}(Aa6yqd{ViMxDGWn{B|M%ZRiR-4w59~5w(Pc^=}|i`t&Qz4Rf=G z38@bZch_+jCxnPU2Hkow$7%n`8Y9=CQ}ZJxNhJ zw7&H+cal=mMH{8$%bX*9j!78EgMZFQKgY{Pwk?YXSan$EbnRHak9k}$GYV-N)iVy+*Lr>X{jP_k7y-o8?lCsj4ES_K*P3%QIDcjiD(P)BOi@@yaqsdlb#1;_N z9m~~(y?BC^_O)~PUbrQ4+;tJ#%fb4yYMK$ENy1R;oLrb18HU?FQnhxSZ4VF*?{&rw zTnWZ`gx^hcwTa!y_m)YuniQy)?xwW0)$hjm-OI0otpTJd?orS+`hH``&gIy&n6A&CB&T}%$I(>2-5^s?d{X@)rhwxn_dH( zkYschr2UYvhk&G**I45?{;qz;W?&MmFJtQyi|fBe-#EtQVkMgOLnBk~3)}_XA4a?0 z|MgfOAe7@u<8R{_(!MGwPeO5J2p}N*u1D;by_EbFsrg>!)(8L9k8u^?h7Om^2cn<)h-cXlmKmbXK5wmy9*n95!8q%# zl-ibaV-0K{5s84Iv7u#5*z1xgdR?qv9df5UbvS&9CcHCoPR$jV8-q4++XUxEhlzee zCImIX4cZ$7SfNxa+k{;f1Rlui*HhdEr)tawhy751!_N%ZpY4%u#UI(^JCam+^ zIMl4^$N|==_1d!-3U{-@0fiRl@&u7KT~k-^5H+sm22$hPP||^RhHS0WzEv_qb*T%H z1hyKEGC7Ot=^0=n?<`4|2_gr*cHY7e{DFk%qx78gfkbTyc3rx|mhiA*%p>rZ!hs+C zqP0q57SK`)BaCiYdQ4tXWStLA@&yxfB~g=ssPXHjiVqD^ajb95#YG~$|4DBq5h<#A zo$ahyn;+%NydWK@oB&g836U zolo3R+5DVKEsDJ29mIjS3(tFyFd>`}+bIkej4PDl!H!;VX?Q68fnwQRDG zGD4(h=ekN?N#O4?vrs?!yUkKpMR;>8;zLOFIzWsm_BDB2Ut51r!nX_UW z>;nJ#@Egdb50v|>d;F~hTT9Ux+PU+a!NwQJQ!69tcJUOV_=p{Nwm9%P0vm%Xb2mC- zqZ$$M7bPb1s;)o!41~BP%+lUWexEhY3O`goJg9ANr&zO!&lTDOlO^O+*^#@vrmm@` zQ`sOg91GP6l*oR^6zbv)XgAry(-G9`8u@Mo=TfxU0Fm1Z2JZkdbA~OTh^J%Bfhdg3 zUX3G1NOs$LI|Ommf227x^aAZvx9U#%Vd9m^opVx&9f%v9S!P}%n&Rh~OE(MYsfKPk z#d&v`hHto1kL8C`8gNY{=@r1?2Lr|IMnOb7JF$Fx8`~`X<7uMA-??>;$3v}laf}^{ z#9X_fEZ8;)8-pdhXQ3X6sqNzemeNrq&aaRr$^2QmklH~l98I`~glTZ|;X8s31mGaj z|2|x%h#-`>y2lu(;!OcZW(xpv$TmxpZhWZXDe)`VOiJ{kL1(b#rs3UbETseG++9^< z-9WV3q~@D<5cVDiX#*r(I~{nv8?jUHqpb32RcS~IEY&tFU{ziE6cVA3B5^+DKWCKE z!MGxjK-t3y+Ny<8y)ahE8R9JtRmm?i;(%|u?ST)7heK*tOz?Af#XQ8@2w6JrEWbZ@ ztJzO>Nk~xa&-RyMemw=fHJp(go}?c-r)dGW&>eDLy$141uJ(wj1_nZaB0! z9toMrrW>#UgBS+WM{Wa?9}QX?o%YkDPK@Q$cBKYFcPIB7I+IEx3dO$QK)!c4T@75P z4Q6p>ml*mvSw_6w8F&oo3gMr5<=Z*{jaGgxy+ccxocqL2`!@9LOoT|_^*NXpt|&+z zD*F%c6|=VFWYvUSyW2D&k(>w0Ui+Orym`~_q znw>K{1Slxh``68||FhsZ#RKM})RLO*#x>mdb)2hgtu0Jfq=HvA%?py4SRwT#PxGmt zjKvfXiK3I@ckBq&NmTUK#?uV=-?6&2wX--n5Kkjuet#~M$_*DEYVlHT0Vk0DnKUg} z@BIskhyhe^+l!jy**@6r#DjB- ztGPEqJx<8g?sB-5LRw}eCY0AzvK7&?>B&Hd5Q=^jQE=dv(c@I)qqbclyW-M6C4jm8 z*ah>R`?(CtS$&_Zcxb}Q&z%8$C3PIZK7dH^%aMrFMdxW^2l`k0bFH$YwNdWN1gc$o zco_0@Uu@Ga(wA-QkrAYFYxos#A340EAzoiatlMz_IYuG|PFT8t-VO5y?(TNc*#s%> zj;}Kvk&rU6JJ}L&{*27(YO~_?rd#xhv;gfVvoWk$1h`Pj|QD=)+Ik7BC+`~W5AGR<9Wb!ie*&_C5PJqq zFr9s)OmV;DRJ(QaKY`rcM}gT=IUSx3B4N3u4eFvMHXzgBByU9F7U>=|rMe0mLW&uA zrYlZ---kvnG*wRwcj(l(gi^H-s)&R+L7sUzr1&lI+Sawgc+&cOhQBb;O8i^{CjlZ$ zjCiU-A3cLrM^YRi>!1_k^>gL@g_OmqAW%Z#QAvwtm}WId>Zz$e%nAIGNHc`}D9hyZ zeBzTaeNbOB(lO>EdHxg6LkAu`@w6^AriLXz_SKMxTNlFBb{7O#nCAW)innIMo;sKc znF^T=-*q-XK#>ff$FkR-BmggorCQb@VpLAHls@QeU~$v}&TG`N8P~X06t};yg~;Asbz)5-9cK-9C#NqH z8`_8F2v-OfevFrVAvxlqO-|)15Y3t|z84puB@4QgNV`f3hx)-<&9) zJE3^Y)@))wzv-726sz9}PGolA@G^fkW}O6fBHQ!(X(t|DYj{Y5$mvE?SlfIUwumC+ zWlR*{JnD-UtmX4PV&t5SE8gmbMXWTf4g&rl>oj_uOJ z^*%pHg~{bgbKgskAKYeq;82-<;0jY06J!A_u1XMe|&~9 zYJa*08q|Q@><+E9*sA`5IY_`PGI@&VshJ()^4V7SlnmUaYkRa?_Xgb=Xi#B$^(mce z;P9aq;2k>%4t#6RkxRyNFT#5RiWp8ou=MJoOxW=;u*kit?j_e-MA?f5qkG%43b`ai zEZU5@lLEuQW|E7!s`lJIVA3y)a{R_Y^4-qaJ``0~<@}8R1OJfEORB}M2SA?2jrkM1 zl^6}~!qDVhvHPJ{|F@48MOchcR-Hjqa*(@O4pp;`ZQph5&Ylx4^j8t+I;w`Ycu8|| oXU5szJIvSs0Cm<7hyVfbyat@G5^3q~#Xc|%ivj=u00045TEwg)#sB~S literal 104956 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�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 I00045TH^^*i~s-t diff --git a/data/ctwasWeightsExample.rda b/data/ctwasWeightsExample.rda index cd39124579e3a56f8b6b587cb3c9d015eb65744e..39381327a751e201f5e2192a4ce819f118ce05eb 100644 GIT binary patch delta 247 zcmVi9}I!QyvZ2$&pNRw)&WqLh(^4O<$w_nrE`L zEG^h4)Gd9e--bVgCS2D;ZPA zt>M_~w_!vO7ufz>f167RiZerkEWCC6?^#gaH*rC3y1q#U;@;ua;=Vw9C}^cA*R1^w za0v;@l}(V2BDGqLSE0|55qvcx_x4T}N*00001Sz2>7b;kez delta 235 zcmV|C;zv6 z33*grlE6Y@tRd3ny$2-ZN`bQ>Rn+FIFgswV%XrurcK1(<7-+U4YcE^s8jhFmals_E z%qK{5Ilux3@!c*{4*qxo13{aVdtRN|=pLqg?Z@EWU^~vmu!qX8U)X)HWs1ugkcM*gmB<7~ zIUEuSvkOq4-e5(+u=VUlm9%U~Dep}KM)X`xi&Dpj((zCmy*7N}Eq~6o1{qLa5Q%F2 zIZCi84QG=y3|*U2$DeE~UYiua#ho#=B-og()Ag+Ok4N4w>ia%_AVvk^&Qm_Jjl`V; z&Mg6*%>a4C5J7=E z%jr44f(NVjEP#f@E_;l<&e>gS5jhi}y#;k$Rj00qv)$PCwo{~MWvm#PT0|W9Dq)qV zb}h5#|EO&3>k@{u_wAHYnMFImKN)oPbKS=2TLOI!@T6C2j)^%H2=&?v!Fr1zb&F7bAX~m1RNla0*po|eJ?iDReIl$|`4l{oJV^tRF`cgI%e$F> zX)d2;KWCqT%$h?c{XM?r1%D4$QUz@#1`nQ6kUptqxX7rMKKDK_ByE)HRTyT{E7OvB z!&ZoPhEj&jlUT5?2pN4s-5dld(bER^z8Cbty~3|cNvt;_1I81^-*kbh3<+!KYFiWo z9N4d4wHvNj3R|P51zDN{9lw6|0Rw}fyFDf{T0<3tIsCx4MnY2kI^xXH6> z7JMV1o&t%If6yc)e}v@EEskC~pY@?BZMEqXnPAl?_l)TjVmkg)Ss2N?BHr)}+B{tb zX{rtE)7HQAbU+D=BRU4l9nK6WFnb&?QxrVF0;WlBXe%6yyr`NnESq?WxTd4@sQOo} zvE5%JZL^fA24iDy8E;tJrf~{cG9%R@I%vCW4cZ>SbdKRc?JJf7L;;OBm}=2)I{71o zE71zoKS>>PPe}sWNc4`!1|j(*(iq~gwRbZfh7%{azd;moV$2eN000000Cr({jsO9j Y2;&(50I2NXg+4G1ivj=u00045TB(;Q6951J diff --git a/data/twasWeightsExample.rda b/data/twasWeightsExample.rda index 80e9463a235bce2c1b201014ce60f3f7650195ac..41648a6f4987215eac0ccc9c940ed4410eac3ba6 100644 GIT binary patch delta 212 zcmV;_04x9OMes$C8UsEew6PrzB7YX1bl$n1NxfPN8#nm^#Sy-1Htt)_;5YLhTQV?F zwVx0Oz2LWX5rO*C^$F>{@QCb5(ywAukf16>U1h)+CMJ-UOT zrS?x8M%%C>#j_VyY~+NUM-Prv>BGcLD-l|lqiq2Gh?V~zfpkA{8EjP_l|?Fb%{R*D z9XEp9nEbPgf9{>R1Gjmk{0nA0evH<|tn7pq( OFb#_W000000a;oV%3Tlu delta 208 zcmV;>05AXWMeIe88Uslpt+5>sB7YYFPDFVoho7}{>mac8S5yhoo-9eXu?~uc&PKoG z1M5J`NvH|&o+zZdVjm0lokBjB!|s&&;79wvezv~5g%o?g!xgkS7C1ur)>|X&ykJ-g z85z7XJyw1yS3;r$d9hhh-nkS7Z$3&8c)OB35~vjz@X" form afterwards -- the same two-step # every other bundled example uses. .resolveGenotypeResourcePath() turns it # back into a real path at read time, on whatever machine has the package. ctwasWeightsExample <- pecotmr:::.withRowLdRefs( ctwasWeightsExample, - .portableHandle(ldStem) + .portablePanel(ldStem) ) stopifnot( @@ -111,6 +121,13 @@ stopifnot( asBundledResource(ldStem) ) +# These are expression weights, and cTWAS's groups name the type a gene records. +ctwasWeightsExample$dataType <- S4Vectors::SimpleList(purrr::map( + as.list(ctwasWeightsExample$dataType), + pecotmr:::.twasRowDataType, + label = "expression" +)) + # ----------------------------------------------------------------------------- # 2. Run the chain, capturing each payload. # ----------------------------------------------------------------------------- diff --git a/man/BoundaryMergeParam.Rd b/man/BoundaryMergeParam.Rd index 0344f36ae..d23d8ce7d 100644 --- a/man/BoundaryMergeParam.Rd +++ b/man/BoundaryMergeParam.Rd @@ -7,14 +7,14 @@ \title{Boundary-Region Merge Settings} \usage{ BoundaryMergeParam( - enabled = FALSE, + enabled = TRUE, pipThreshold = 0.5, filterCs = FALSE, maxSnp = Inf ) } \arguments{ -\item{enabled}{Logical. Run the merge step. Default \code{FALSE}; the +\item{enabled}{Logical. Run the merge step. Default \code{TRUE}; the other three fields are read only when this is \code{TRUE}.} \item{pipThreshold}{Numeric. PIP threshold for selecting which boundary @@ -29,10 +29,13 @@ to be selected. Default \code{FALSE}.} A \code{BoundaryMergeParam} object, a \code{\link{MethodParam}}. } \description{ -Whether a high-PIP gene whose cis window straddles an - LD-block boundary has its adjacent regions merged and re-fine-mapped - after each run (\code{\link{mergeCtwasBoundaryRegions}}). +Whether a high-PIP gene whose weights cross an LD-block + boundary has its regions merged and re-fine-mapped after each run + (\code{\link{mergeCtwasBoundaryRegions}}). A first pass fine-maps such a + gene with the LD of one block only, whatever its z-score drew from the + other, and can call a gene whose real signal is a variant in that other + block; merging is what removes those calls. } \examples{ -BoundaryMergeParam(enabled = TRUE, pipThreshold = 0.8) +BoundaryMergeParam(pipThreshold = 0.8) } diff --git a/man/CtwasPriorParam.Rd b/man/CtwasPriorParam.Rd index 77a91996e..a2dfef3de 100644 --- a/man/CtwasPriorParam.Rd +++ b/man/CtwasPriorParam.Rd @@ -10,7 +10,7 @@ CtwasPriorParam( thin = 0.1, niterPrefit = 3L, niter = 30L, - varStructure = c("shared_type", "shared_context", "shared_nonSNP", "shared_all", + varStructure = c("shared_nonSNP", "shared_all", "shared_type", "shared_context", "independent"), fallbackToPrefit = FALSE ) @@ -26,9 +26,15 @@ which variants enter a gene's weight matrix.} \item{niter}{Integer. Iterations for the accurate EM. Default \code{30}.} -\item{varStructure}{How the prior variance is shared across groups; one of -\code{"shared_type"} (default), \code{"shared_context"}, -\code{"shared_nonSNP"}, \code{"shared_all"} or \code{"independent"}.} +\item{varStructure}{How the prior variance is shared across groups, a group +being a study, context and molecular type (see +\code{\link{ctwasPipeline}}). \code{"shared_nonSNP"} (default) pools it +across the gene groups, so a group with few causal genes borrows from the +others, and keeps the variants' apart. \code{"shared_all"} pools the +variants in as well, \code{"shared_type"} pools the groups of each +molecular type, \code{"shared_context"} those of each study and context, +and \code{"independent"} gives each group its own. With one type per study +and context, \code{"shared_context"} is \code{"independent"}.} \item{fallbackToPrefit}{Logical. When the accurate EM returns NaN on underpowered data, fall back to the prefit estimates rather than failing. diff --git a/man/GenotypeFilterParam.Rd b/man/GenotypeFilterParam.Rd index a887a35b9..716f89483 100644 --- a/man/GenotypeFilterParam.Rd +++ b/man/GenotypeFilterParam.Rd @@ -31,10 +31,13 @@ it.} \item{keepSamples}{Sample ids to restrict to; \code{character(0)} keeps all samples.} -\item{keepVariants}{Variant ids to restrict to; \code{character(0)} keeps -all variants.} +\item{keepVariants}{Variant ids or rsIDs to restrict to; \code{character(0)} +keeps all variants. Alleles match in either order and on either strand, +and an rsID is matched against the panel's own variant ids.} -\item{keepIndel}{Logical. Retain insertions and deletions.} +\item{keepIndel}{Logical. Retain variants other than SNPs. A SNP has exactly +one of A, C, G, T for each allele, so \code{I}/\code{D} codes, \code{*} and +\code{N} count as indels.} } \value{ A \code{GenotypeFilterParam} object, a \code{\link{MethodParam}}. @@ -46,7 +49,7 @@ Which variants and samples survive when a genotype matrix is or \code{\link{colocboostPipeline}} on a \code{\link{QtlDataset}}. A \code{QtlDataset} records no filter of its own; \code{keepSamples} narrows it once at the start of the run, and the other fields apply to - each extracted block, against that block's samples. + each extracted block, against the samples being fit. } \section{Unset fields}{ diff --git a/man/GwasFineMappingResult.Rd b/man/GwasFineMappingResult.Rd index f5d01ed83..dbc93380c 100644 --- a/man/GwasFineMappingResult.Rd +++ b/man/GwasFineMappingResult.Rd @@ -46,8 +46,8 @@ input row. \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 +\item{dataType}{Optional molecular type ("expression", "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, diff --git a/man/H2Estimate-class.Rd b/man/H2Estimate-class.Rd index 51ddebaa0..b288aee19 100644 --- a/man/H2Estimate-class.Rd +++ b/man/H2Estimate-class.Rd @@ -27,6 +27,9 @@ Besides the identity, \code{mcols()} hold each estimate's values: estimate used.} \item{\code{scale}}{FastHer's fitted phenotypic variance relative to its standardized value.} + \item{\code{reference_overlap}}{The share of the study's samples LAVA + took to be in the LD reference (\code{referenceOverlap} in + \code{\link{estimateH2}}).} \item{\code{enrichment}}{The per-annotation enrichment table of a stratified estimate (\code{\link{stratifiedHeritabilityEnrichment}}).} \item{\code{jackknife}}{The per-block jackknife matrix of the diff --git a/man/PhenotypeFilterParam.Rd b/man/PhenotypeFilterParam.Rd index e891f2ac9..b199e4be9 100644 --- a/man/PhenotypeFilterParam.Rd +++ b/man/PhenotypeFilterParam.Rd @@ -16,7 +16,8 @@ PhenotypeFilterParam( \arguments{ \item{naAction}{How missing phenotype values are handled: \code{"drop"} the samples with any missing value among the selected traits, -\code{"impute"} each trait's mean, or \code{"keep"} them as they are.} +\code{"impute"} each trait's mean, or \code{"keep"} them, each trait +fit on the samples it has (colocboost only).} \item{outlierAction}{\code{"keep"} or \code{"drop"} samples flagged as multivariate outliers by a robust (MCD) Mahalanobis distance, computed @@ -46,7 +47,8 @@ How a run of \code{\link{fineMappingPipeline}}, its default when the run resolves the bundle. \code{naAction} resolves to the pipeline's own policy: fine-mapping and TWAS fits cannot take a missing phenotype, so they drop the affected samples (and refuse - \code{"keep"}), while colocboost receives the phenotypes as they are. + \code{"keep"}), while colocboost keeps them, fitting each trait on the + samples that have a value for it. Outliers are kept unless \code{outlierAction = "drop"}. } diff --git a/man/QtlDataset.Rd b/man/QtlDataset.Rd index eb5be46aa..808a5b5b5 100644 --- a/man/QtlDataset.Rd +++ b/man/QtlDataset.Rd @@ -8,7 +8,8 @@ QtlDataset( studyName, genotypes, phenotypes, - genotypeCovariates = matrix(numeric(0), nrow = 0, ncol = 0) + genotypeCovariates = matrix(numeric(0), nrow = 0, ncol = 0), + dataType = NULL ) } \arguments{ @@ -24,6 +25,14 @@ context. Each SE must have \code{rowRanges} carrying trait positions and \item{genotypeCovariates}{Numeric matrix of genotype-derived covariates (e.g., ancestry PCs); rows are samples. Becomes the \code{colData} of the genotype experiment.} + +\item{dataType}{Optional molecular type of each context (\code{"expression"}, +\code{"splicing"}, \code{"methylation"}): free text, one value for every +context or a vector named by context. It cannot be derived, the phenotypes +being only numbers, so pipelines read it from here and record it on what +they fit, and \code{\link{ctwasPipeline}} models each type as its own. Kept +in the context's \code{metadata()} as \code{dataType}, and read by +\code{\link{dataType}}. \code{NULL} (default) leaves a context's as it is.} } \value{ A \code{QtlDataset} object. @@ -63,6 +72,7 @@ se <- SummarizedExperiment::SummarizedExperiment( rowRanges = rng ) QtlDataset( - studyName = "s1", genotypes = panel, phenotypes = list(brain = se) + studyName = "s1", genotypes = panel, phenotypes = list(brain = se), + dataType = "expression" ) } diff --git a/man/QtlEnrichmentParam.Rd b/man/QtlEnrichmentParam.Rd index 1240ede13..303ff686c 100644 --- a/man/QtlEnrichmentParam.Rd +++ b/man/QtlEnrichmentParam.Rd @@ -11,7 +11,7 @@ QtlEnrichmentParam( piQtl = NULL, lambda = 1, impN = 25, - doubleShrinkage = FALSE, + doubleShrinkage = TRUE, besselCorrection = TRUE ) } @@ -33,8 +33,9 @@ Default \code{1.0}.} \item{impN}{Rounds of multiple imputation to draw QTL from. Default \code{25}.} -\item{doubleShrinkage}{Logical. Apply the double-shrinkage correction to -the enrichment estimate. Default \code{FALSE}.} +\item{doubleShrinkage}{Logical. Shrink each imputation round's estimate and +then the combined one, as fastenloc does; \code{FALSE} shrinks only the +combined one. Default \code{TRUE}.} \item{besselCorrection}{Logical. Apply Bessel's correction when estimating the sampling variance. Default \code{TRUE}.} diff --git a/man/QtlFineMappingResult.Rd b/man/QtlFineMappingResult.Rd index 117635cca..3ad16256a 100644 --- a/man/QtlFineMappingResult.Rd +++ b/man/QtlFineMappingResult.Rd @@ -57,8 +57,8 @@ 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 +\item{dataType}{Optional molecular type ("expression", "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, diff --git a/man/QtlSumStats.Rd b/man/QtlSumStats.Rd index 3240aa330..4e8008467 100644 --- a/man/QtlSumStats.Rd +++ b/man/QtlSumStats.Rd @@ -17,7 +17,8 @@ QtlSumStats( traitPos = NULL, region = NULL, extraCols = list(), - metadata = list() + metadata = list(), + dataType = NULL ) } \arguments{ @@ -78,6 +79,13 @@ as adding an entry rather than a metadata column.} \item{metadata}{A list of free-form metadata about the collection, as \code{metadata()} holds on any Bioconductor vector. Default \code{list()}.} + +\item{dataType}{Optional molecular type of each tuple (\code{"expression"}, +\code{"splicing"}, \code{"methylation"}): free text, length 1 or one per +tuple. The statistics cannot say what was measured, so pipelines read it +from here and record it on what they fit, and \code{\link{ctwasPipeline}} +models each type as its own. Read by \code{\link{dataType}}. \code{NULL} +(default) omits the column.} } \value{ A \code{QtlSumStats} object. diff --git a/man/SumstatsFilterParam.Rd b/man/SumstatsFilterParam.Rd index d95b27a9f..7225e5abc 100644 --- a/man/SumstatsFilterParam.Rd +++ b/man/SumstatsFilterParam.Rd @@ -14,8 +14,9 @@ SumstatsFilterParam( ) } \arguments{ -\item{removeIndels}{Logical. Drop insertions and deletions. Default -\code{FALSE}.} +\item{removeIndels}{Logical. Drop variants other than SNPs. A SNP has exactly +one of A, C, G, T for each allele, so \code{I}/\code{D} codes, \code{*} and +\code{N} count as indels. Default \code{FALSE}.} \item{removeStrandAmbiguous}{Logical. Drop strand-ambiguous variants (A/T and C/G). Default \code{TRUE}.} diff --git a/man/TwasWeightsRow-class.Rd b/man/TwasWeightsRow-class.Rd index 55c8a67cf..a72ffaba3 100644 --- a/man/TwasWeightsRow-class.Rd +++ b/man/TwasWeightsRow-class.Rd @@ -23,7 +23,7 @@ One row's worth of TWAS weights. Build one with \item{\code{weightStandardized}}{Whether the weights are on the standardized scale.} -\item{\code{weightsDataType}}{Optional data-type label.} +\item{\code{weightsDataType}}{Optional molecular type.} }} \section{Coercion}{ diff --git a/man/TwasWeightsRow.Rd b/man/TwasWeightsRow.Rd index 2e27d197b..35f71a43d 100644 --- a/man/TwasWeightsRow.Rd +++ b/man/TwasWeightsRow.Rd @@ -27,7 +27,8 @@ or a matrix with one column per condition).} \item{weightStandardized}{Whether the weights are already on the standardized scale.} -\item{weightsDataType}{Optional data-type label.} +\item{weightsDataType}{Optional molecular type (\code{"expression"}, +\code{"splicing"}).} } \value{ A \code{\linkS4class{TwasWeightsRow}}. diff --git a/man/asCtwasResult.Rd b/man/asCtwasResult.Rd index 0db925ff2..ea0ebbd0c 100644 --- a/man/asCtwasResult.Rd +++ b/man/asCtwasResult.Rd @@ -26,7 +26,7 @@ Decompose the raw list returned by granular \code{assembleCtwasInputs} \eqn{\to} \code{estCtwasGroupPriors} \eqn{\to} \code{screenCtwasRegions} \eqn{\to} \code{finemapCtwasRegions} path. The GWAS study is read from \code{z_snp$study} and the (single) - weight method from the gene ids. + weight method from the weights. } \examples{ data(ctwasFinemapExample) diff --git a/man/assembleCtwasInputs.Rd b/man/assembleCtwasInputs.Rd index b61cbd07a..4b927d09a 100644 --- a/man/assembleCtwasInputs.Rd +++ b/man/assembleCtwasInputs.Rd @@ -23,16 +23,15 @@ through \code{\link{summaryStatsQc}}.} \item{twasWeights}{The per-gene weight source. Either (a) a FLAT \code{\link{TwasWeights}} / \code{QtlFineMappingResult} (or a homogeneous -list of them) carrying \code{region} provenance -- each gene is placed into -its home LD block internally by \code{start(region)} (matching cTWAS's -\code{p0} assignment rule); or (b) a pre-bucketed NAMED LIST keyed by -\code{region_id} (keys a SUBSET of \code{gwasSumStats}'s), used as-is. -Blocks without any TWAS weights still contribute their SNP-level signal to -ctwas's joint group prior estimate (the legacy whole-chromosome pattern -where only a few of many LD blocks carry gene weights). A gene whose cis -span straddles a block boundary is homed by its single anchor; the -cross-block signal is cTWAS's boundary-gene concern -(\code{\link{mergeCtwasBoundaryRegions}}), not placement. A +list of them), each gene read with the LD block holding most of its +weight; or (b) a pre-bucketed NAMED LIST keyed by \code{region_id} (keys a +SUBSET of \code{gwasSumStats}'s), used as-is. A gene's \code{traitPos}, if +it has one, gives its coordinates in the results, which are otherwise the +span of its weight variants. +Blocks without any TWAS weights do not feed the group priors by default: +cTWAS skips regions with fewer genes than its \code{min_gene}, 1. To let +their SNP-level signal in, set \code{min_gene = 0} in +\code{\link{CtwasEstParamOptions}}. A \code{TwasWeights} row of the run's method that carries a SuSiE fit and was narrowed since that fit warns (condition class \code{pecotmrFitCoverage}): the fit no longer lines up with the row's @@ -78,8 +77,9 @@ matrix, built with \code{\link{VariantPruningParam}}: \value{ A list with elements \code{z_snp}, \code{z_gene} (NULL when no \code{twasZ}), \code{weights}, \code{region_info}, \code{snp_map}, - \code{LD_map}, \code{LD_loader_fun}, \code{snpinfo_loader_fun}, and - \code{resolvedMethod}. + \code{LD_map}, \code{LD_loader_fun}, \code{snpinfo_loader_fun}, + \code{resolvedMethod}, and \code{gwas_n}, the GWAS's median sample size + (\code{NA} when it records none). } \description{ Builds the per-block ctwas-shape input set (\code{z_snp}, diff --git a/man/bayesARssWeights.Rd b/man/bayesARssWeights.Rd index f975939e3..9aa736488 100644 --- a/man/bayesARssWeights.Rd +++ b/man/bayesARssWeights.Rd @@ -20,8 +20,8 @@ bayesARssWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engines receive the standardized effects -z / sqrt(n), with n the median of \code{n}.} +standardized. The engines receive the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see @@ -77,7 +77,7 @@ A numeric vector of the posterior mean effects, one per variant. data(eqtlRegionExample) X <- eqtlRegionExample$X[, 1:30] stat <- list( - z = as.vector(cor(X, eqtlRegionExample$yRes)) * sqrt(nrow(X)), + cor = as.vector(cor(X, eqtlRegionExample$yRes)), n = rep(nrow(X), ncol(X)), variantNames = colnames(X) ) diff --git a/man/bayesAlphabetRssWeights.Rd b/man/bayesAlphabetRssWeights.Rd index 54fbffe64..252401418 100644 --- a/man/bayesAlphabetRssWeights.Rd +++ b/man/bayesAlphabetRssWeights.Rd @@ -22,8 +22,8 @@ bayesAlphabetRssWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engines receive the standardized effects -z / sqrt(n), with n the median of \code{n}.} +standardized. The engines receive the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see diff --git a/man/bayesBRssWeights.Rd b/man/bayesBRssWeights.Rd index 8ec2fbf32..accbe17e3 100644 --- a/man/bayesBRssWeights.Rd +++ b/man/bayesBRssWeights.Rd @@ -21,8 +21,8 @@ bayesBRssWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engines receive the standardized effects -z / sqrt(n), with n the median of \code{n}.} +standardized. The engines receive the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see @@ -81,7 +81,7 @@ A numeric vector of the posterior mean effects, one per variant. data(eqtlRegionExample) X <- eqtlRegionExample$X[, 1:30] stat <- list( - z = as.vector(cor(X, eqtlRegionExample$yRes)) * sqrt(nrow(X)), + cor = as.vector(cor(X, eqtlRegionExample$yRes)), n = rep(nrow(X), ncol(X)), variantNames = colnames(X) ) diff --git a/man/bayesCRssWeights.Rd b/man/bayesCRssWeights.Rd index bfe2ef452..2b0165677 100644 --- a/man/bayesCRssWeights.Rd +++ b/man/bayesCRssWeights.Rd @@ -21,8 +21,8 @@ bayesCRssWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engines receive the standardized effects -z / sqrt(n), with n the median of \code{n}.} +standardized. The engines receive the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see @@ -81,7 +81,7 @@ A numeric vector of the posterior mean effects, one per variant. data(eqtlRegionExample) X <- eqtlRegionExample$X[, 1:30] stat <- list( - z = as.vector(cor(X, eqtlRegionExample$yRes)) * sqrt(nrow(X)), + cor = as.vector(cor(X, eqtlRegionExample$yRes)), n = rep(nrow(X), ncol(X)), variantNames = colnames(X) ) diff --git a/man/bayesLRssWeights.Rd b/man/bayesLRssWeights.Rd index 062e061e8..13b90c247 100644 --- a/man/bayesLRssWeights.Rd +++ b/man/bayesLRssWeights.Rd @@ -20,8 +20,8 @@ bayesLRssWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engines receive the standardized effects -z / sqrt(n), with n the median of \code{n}.} +standardized. The engines receive the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see @@ -78,7 +78,7 @@ A numeric vector of the posterior mean effects, one per variant. data(eqtlRegionExample) X <- eqtlRegionExample$X[, 1:30] stat <- list( - z = as.vector(cor(X, eqtlRegionExample$yRes)) * sqrt(nrow(X)), + cor = as.vector(cor(X, eqtlRegionExample$yRes)), n = rep(nrow(X), ncol(X)), variantNames = colnames(X) ) diff --git a/man/bayesNRssWeights.Rd b/man/bayesNRssWeights.Rd index 3df40e1a5..7156f575d 100644 --- a/man/bayesNRssWeights.Rd +++ b/man/bayesNRssWeights.Rd @@ -20,8 +20,8 @@ bayesNRssWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engines receive the standardized effects -z / sqrt(n), with n the median of \code{n}.} +standardized. The engines receive the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see @@ -78,7 +78,7 @@ A numeric vector of the posterior mean effects, one per variant. data(eqtlRegionExample) X <- eqtlRegionExample$X[, 1:30] stat <- list( - z = as.vector(cor(X, eqtlRegionExample$yRes)) * sqrt(nrow(X)), + cor = as.vector(cor(X, eqtlRegionExample$yRes)), n = rep(nrow(X), ncol(X)), variantNames = colnames(X) ) diff --git a/man/bayesRRssWeights.Rd b/man/bayesRRssWeights.Rd index bf413f5d6..42cca0fb8 100644 --- a/man/bayesRRssWeights.Rd +++ b/man/bayesRRssWeights.Rd @@ -20,8 +20,8 @@ bayesRRssWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engines receive the standardized effects -z / sqrt(n), with n the median of \code{n}.} +standardized. The engines receive the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see @@ -77,7 +77,7 @@ A numeric vector of the posterior mean effects, one per variant. data(eqtlRegionExample) X <- eqtlRegionExample$X[, 1:30] stat <- list( - z = as.vector(cor(X, eqtlRegionExample$yRes)) * sqrt(nrow(X)), + cor = as.vector(cor(X, eqtlRegionExample$yRes)), n = rep(nrow(X), ncol(X)), variantNames = colnames(X) ) diff --git a/man/colocPipeline.Rd b/man/colocPipeline.Rd index a8b120e6b..cc141b557 100644 --- a/man/colocPipeline.Rd +++ b/man/colocPipeline.Rd @@ -68,7 +68,7 @@ columns \code{gwasStudy}, \code{qtlStudy}, \code{qtlContext}, present; \code{\link{qtlEnrichmentPipeline}} emits all four, the context and trait set for a QTL outcome side). Output of \code{\link{qtlEnrichmentPipeline}}. When non-\code{NULL} the pair is -scored by enloc rather than coloc: the priors come from that table's +scored with enloc's priors: they come from that table's \code{colocP1} / \code{colocP2} / \code{colocP12} columns, and the report gains \code{RCP} and \code{LCP}. Pairs without a matching enrichment row fall back to the baseline priors with a warning. Default @@ -117,7 +117,7 @@ A \code{\linkS4class{ColocResult}}: one element per scored group \code{hit1}, \code{hit2}, \code{PP.H0.abf} \ldots \code{PP.H4.abf}) and the reconciliation diagnostics \code{qtlRetainedMass} / \code{gwasRetainedMass}. When \code{enrichment} is supplied the pair is - scored by enloc instead, and four further columns appear: + scored with enloc's priors, and four further columns appear: \code{enrichment} and \code{p12Used} report the per-pair factor and the shared-signal prior enloc derived, while \code{RCP} and \code{LCP} are enloc's regional and locus colocalisation probabilities. Those two are diff --git a/man/colocboostPipeline.Rd b/man/colocboostPipeline.Rd index 530f185a8..2f383ed84 100644 --- a/man/colocboostPipeline.Rd +++ b/man/colocboostPipeline.Rd @@ -173,10 +173,11 @@ settings apply to each extracted block, as in \code{QtlDataset} and \code{MultiStudyQtlDataset} input only.} \item{phenotypeFilterParam}{The run's phenotype QC, built with -\code{\link{PhenotypeFilterParam}}. colocboost takes missing phenotype -values as they are, so unset keeps them; \code{"drop"} or -\code{"impute"} applies that instead, and \code{outlierAction = -"drop"} removes outlier samples. \code{QtlDataset} and +\code{\link{PhenotypeFilterParam}}. Unset fits each trait on the samples +that have a value for it, with genotypes filtered and residualized on just +those; \code{"drop"} or \code{"impute"} applies that instead, and +\code{outlierAction = "drop"} removes outlier samples, judging only those +with every trait. \code{QtlDataset} and \code{MultiStudyQtlDataset} input only.} \item{residualizationParam}{Covariate residualization settings for diff --git a/man/ctwasCombinedPips.Rd b/man/ctwasCombinedPips.Rd new file mode 100644 index 000000000..71f5efe16 --- /dev/null +++ b/man/ctwasCombinedPips.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ctwasWrapper.R +\name{ctwasCombinedPips} +\alias{ctwasCombinedPips} +\title{Combine cTWAS PIPs across a gene's molecular traits} +\usage{ +ctwasCombinedPips(x, mapping = NULL) +} +\arguments{ +\item{x}{A \code{\link{CtwasResult}}.} + +\item{mapping}{\code{NULL}, or a \code{data.frame} mapping molecular traits +to genes, with columns \code{molecular_id} and \code{gene_name}. Without +it the rows are the molecular ids, so a gene's expression and splicing +traits, whose ids differ, stay apart. With it the rows are the genes, and +traits it does not map are dropped.} +} +\value{ +A tibble led by the run (\code{gwasStudy}, \code{method}) and the + gene (\code{molecular_id}, or \code{gene_name} with a \code{mapping}), then + \code{combined_pip}, \code{combined_cs_id}, and for each study and context + \code{{study}_{context}_pip} and \code{{study}_{context}_cs_id}, \code{NA} + where that context holds no trait of the gene. Highest PIP first within a + run. +} +\description{ +One row per gene with cTWAS's combined PIP, the probability that + the gene is causal through any of the molecular traits and contexts + modelled for it, from \code{ctwas::combine_gene_pips()}. The combination + needs the per-effect probabilities of a run, so it reads + \code{\link{susieAlpha}}, and a run is combined on its own: results of + several GWAS studies or methods are not mixed. +} +\examples{ +data(ctwasFinemapExample) +ctwasCombinedPips(asCtwasResult(ctwasFinemapExample)) +} diff --git a/man/ctwasGroupPriors.Rd b/man/ctwasGroupPriors.Rd index 0b5b0654f..1bd5ad751 100644 --- a/man/ctwasGroupPriors.Rd +++ b/man/ctwasGroupPriors.Rd @@ -23,10 +23,18 @@ On a \code{CtwasResultEntry}, the stored parameter object (typically ctwas's \code{param} list), or \code{NULL}. On a \code{CtwasResult}, a tibble with one row per run and group, led by the run identity (\code{gwasStudy}, \code{study}, \code{context}, - \code{method}, and the joint columns when present), then \code{group}, - \code{group_size}, \code{group_prior} and \code{group_prior_var}. A - value the run did not record is \code{NA}, a run with no priors has no - rows, and each context of a joint run lists the priors they share. + \code{method}, and the joint columns when present). Then \code{group}, + named \code{{study}_{context}_{type}}, with \code{group_study}, + \code{group_context} and \code{group_type} since those underscores cannot + be split back, \code{group_size}, \code{group_prior} and + \code{group_prior_var}. Then cTWAS's summary of them + (\code{ctwas::summarize_param()}): \code{log_enrichment}, + \code{log_enrichment_se} and \code{enrichment_p}, each gene group against + the SNP group by a one-sided Fisher test, and \code{group_pve} and + \code{prop_heritability}, the heritability the group explains and its share + of what all groups explain, at the GWAS's median sample size. A value the + run did not record is \code{NA}, a run with no priors has no rows, and each + context of a joint run lists the priors they share. } \description{ Return the group priors a cTWAS run's EM step estimated diff --git a/man/ctwasPipeline.Rd b/man/ctwasPipeline.Rd index da8f01d61..5dd9630ed 100644 --- a/man/ctwasPipeline.Rd +++ b/man/ctwasPipeline.Rd @@ -29,16 +29,15 @@ through \code{\link{summaryStatsQc}}.} \item{twasWeights}{The per-gene weight source. Either (a) a FLAT \code{\link{TwasWeights}} / \code{QtlFineMappingResult} (or a homogeneous -list of them) carrying \code{region} provenance -- each gene is placed into -its home LD block internally by \code{start(region)} (matching cTWAS's -\code{p0} assignment rule); or (b) a pre-bucketed NAMED LIST keyed by -\code{region_id} (keys a SUBSET of \code{gwasSumStats}'s), used as-is. -Blocks without any TWAS weights still contribute their SNP-level signal to -ctwas's joint group prior estimate (the legacy whole-chromosome pattern -where only a few of many LD blocks carry gene weights). A gene whose cis -span straddles a block boundary is homed by its single anchor; the -cross-block signal is cTWAS's boundary-gene concern -(\code{\link{mergeCtwasBoundaryRegions}}), not placement. A +list of them), each gene read with the LD block holding most of its +weight; or (b) a pre-bucketed NAMED LIST keyed by \code{region_id} (keys a +SUBSET of \code{gwasSumStats}'s), used as-is. A gene's \code{traitPos}, if +it has one, gives its coordinates in the results, which are otherwise the +span of its weight variants. +Blocks without any TWAS weights do not feed the group priors by default: +cTWAS skips regions with fewer genes than its \code{min_gene}, 1. To let +their SNP-level signal in, set \code{min_gene = 0} in +\code{\link{CtwasEstParamOptions}}. A \code{TwasWeights} row of the run's method that carries a SuSiE fit and was narrowed since that fit warns (condition class \code{pecotmrFitCoverage}): the fit no longer lines up with the row's @@ -99,7 +98,7 @@ matrix, built with \code{\link{VariantPruningParam}}: \item{boundaryMergeParam}{Whether a high-PIP gene straddling an LD-block boundary has its regions merged and re-fine-mapped, built with \code{\link{BoundaryMergeParam}}: \code{enabled} (default -\code{FALSE}), \code{pipThreshold}, \code{filterCs} and +\code{TRUE}), \code{pipThreshold}, \code{filterCs} and \code{maxSnp}.} \item{keepSnps}{Logical (length 1). When \code{TRUE}, retain the @@ -119,7 +118,11 @@ A \code{\link{CtwasResult}} collection: one row per \code{(gwasStudy, study, context, method)} run and cTWAS region. A single-context run gives each method its own rows; a multi-context (joint) run emits per-context rows sharing the same \code{jointContexts} - set and the jointly-estimated group priors. Each row holds that region's + set and the jointly-estimated group priors, and a multi-study run tags + \code{jointStudies} the same way. The contexts of a joint run + need not hold the same genes: each context's prior comes from its own + genes, and a context too small for cTWAS's \code{min_group_size} is left + out of the fit with a warning. Each row holds that region's per-gene fine-mapping posteriors as ranges, with the region's per-effect susie alpha table (\code{susieAlpha}), its \code{regionInfo} and the run's \code{groupPriors} in \code{mcols()}. Its \code{seqinfo()} names @@ -152,6 +155,29 @@ Pipeline that hands a per-block set of blocks. } +\section{Boundary genes}{ + A gene is modelled as cTWAS models it. Its z-score + reads all its weights, whichever blocks they fall in, its weight variance + treating the blocks as independent; it is fine-mapped in the block holding + the largest share of its absolute weights, with the part of its weights in + that block. Such a first pass can call a gene whose real signal is a + variant across the boundary, so by default a high-PIP gene crossing one has + its regions merged and fine-mapped again (\code{boundaryMergeParam}). +} + +\section{Groups and molecular types}{ + cTWAS estimates one prior per group, and + a group here is a study, context and molecular type, so two studies with + one context name stay apart. The type is the \code{dataType} the weights + record (\code{"QTL"} where they record none, all such contexts being one + type), and it matters to the fit under \code{varStructure = "shared_type"}, + or where one study and context holds two types. Results name a group + \code{{study}_{context}_{type}}, with the three also as columns of their + own, and a gene \code{{trait}|{study}_{context}_{type}_{method}}, the form + cTWAS reads the trait from. Groups whose names would coincide are refused, + as is a name holding \code{"|"}, on which cTWAS's EM splits a group. +} + \section{cTWAS's log}{ cTWAS narrates each of its steps through the \code{logging} package, which writes to the console with \code{cat()}, diff --git a/man/ctwasWeightsExample.Rd b/man/ctwasWeightsExample.Rd index cd04aad90..179569ead 100644 --- a/man/ctwasWeightsExample.Rd +++ b/man/ctwasWeightsExample.Rd @@ -5,7 +5,8 @@ \alias{ctwasWeightsExample} \title{Example cTWAS-formatted TWAS Weights} \format{ -A \code{TwasWeights} collection with one row, method \code{susie}. +A \code{TwasWeights} collection with one row, method \code{susie}, + its \code{\link{dataType}} \code{"expression"}. } \description{ A \code{\link{TwasWeights}} collection formatted for the cTWAS diff --git a/man/dataType.Rd b/man/dataType.Rd new file mode 100644 index 000000000..767c7e3ea --- /dev/null +++ b/man/dataType.Rd @@ -0,0 +1,64 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/AllGenerics.R, R/tupleSelectors.R, +% R/QtlDataset.R, R/twasWeights.R, R/twasWeightsRow.R +\name{dataType} +\alias{dataType} +\alias{dataType,RangedTupleList-method} +\alias{dataType,QtlDataset-method} +\alias{dataType,TwasWeights-method} +\alias{dataType,TwasWeightsRow-method} +\title{Get the Molecular Type} +\usage{ +dataType(x, ...) + +\S4method{dataType}{RangedTupleList}(x) + +\S4method{dataType}{QtlDataset}(x) + +\S4method{dataType}{TwasWeights}(x, studyName = NULL, context = NULL, trait = NULL, method = NULL) + +\S4method{dataType}{TwasWeightsRow}(x) +} +\arguments{ +\item{x}{A \code{\link{QtlDataset}}, a collection such as a +\code{\link{QtlSumStats}} or fine-mapping result, a \code{TwasWeights} or a +\code{TwasWeightsRow}.} + +\item{...}{Class-specific selection arguments.} + +\item{studyName}{For a \code{TwasWeights}: character (length 1) or +\code{NULL}. Restrict the selection to this study; \code{NULL} matches all +studies.} + +\item{context}{For a \code{TwasWeights}: character (length 1) or \code{NULL}. +Restrict the selection to this context; \code{NULL} matches all contexts.} + +\item{trait}{For a \code{TwasWeights}: character (length 1) or \code{NULL}. +Restrict the selection to this trait; \code{NULL} matches all traits.} + +\item{method}{For a \code{TwasWeights}: character (length 1) or \code{NULL}. +Restrict the selection to this weight method; \code{NULL} matches all +methods.} +} +\value{ +On a \code{QtlDataset}, a character vector named by context; on a + collection, one value per row. \code{NA} where nothing is recorded. On a + \code{TwasWeights} or \code{TwasWeightsRow}, the selected entry's label or + \code{NULL}. +} +\description{ +What a dataset's phenotypes or summary statistics measure + (\code{"expression"}, \code{"splicing"}, \code{"methylation"}), as + \code{\link{QtlDataset}} and \code{\link{QtlSumStats}} record it, and as + the pipelines carry it onto what they fit. Free text: the label means what + it is given to mean, and \code{\link{ctwasPipeline}}'s \code{varStructure = + "shared_type"} pools the contexts that share one. +} +\examples{ +twe <- TwasWeightsRow(variantIds = sprintf("chr1:\%d:A:G", 100L * (1:4)), + weights = rep(0.1, 4), cvResult = list(rsq = 0.5), + weightStandardized = FALSE, weightsDataType = "expression") +dataType(twe) +data(qtlDatasetExample) +dataType(qtlDatasetExample) +} diff --git a/man/dprWeights.Rd b/man/dprWeights.Rd index bc0ea60af..98abc2d44 100644 --- a/man/dprWeights.Rd +++ b/man/dprWeights.Rd @@ -12,6 +12,7 @@ dprWeights( y, fittingMethod = "VB", fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = DprOptions() ) @@ -20,6 +21,7 @@ dprVbWeights( y, nK = 8, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = DprOptions() ) @@ -28,6 +30,7 @@ dprGibbsWeights( y, sStep = 5000, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = DprOptions() ) @@ -35,6 +38,7 @@ dprAdaptiveGibbsWeights( X, y, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = DprOptions() ) } @@ -48,9 +52,12 @@ dprAdaptiveGibbsWeights( \item{fitRetention}{How much of the fit is kept as the \code{"fit"} attribute of the returned weights: \code{"none"} (default) attaches -nothing, \code{"slim"} the fitted model without its copy of the -genotypes (\code{x}), \code{"full"} the whole fitted model. Either -names the variants it was fitted on.} +nothing, \code{"slim"} and \code{"full"} the fitted model without its +copy of the genotypes (\code{x}). Each names the variants it was fitted +on.} + +\item{retainX}{Logical. Keep that copy of the genotypes in a \code{"full"} +fit (default \code{FALSE}). An error at any other \code{fitRetention}.} \item{methodArgs}{Options forwarded to \code{RcppDPR::fit_model}, built with \code{\link{DprOptions}}. A bare list is refused: it cannot be diff --git a/man/enetRssWeights.Rd b/man/enetRssWeights.Rd index 7f1c30340..66bbb0e69 100644 --- a/man/enetRssWeights.Rd +++ b/man/enetRssWeights.Rd @@ -18,8 +18,8 @@ enetRssWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engine receives the standardized effects z / sqrt(n), -with n the median of \code{n}.} +standardized. The engine receives the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see diff --git a/man/enetWeights.Rd b/man/enetWeights.Rd index 2f6c54f3c..459df7b90 100644 --- a/man/enetWeights.Rd +++ b/man/enetWeights.Rd @@ -9,6 +9,7 @@ enetWeights( y, alpha = 0.5, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = GlmnetOptions() ) } @@ -26,8 +27,11 @@ nothing; \code{"slim"} the selected fit, as a list of its coefficients (\code{coef}, over the variants fitted), \code{intercept}, \code{lambda} and cross-validated error there (\code{cvm}, \code{cvsd}); \code{"full"} the whole \code{cv.glmnet} object, with -every \code{lambda}'s coefficients. Either names the variants it was -fitted on.} +every \code{lambda}'s coefficients but without the copy of \code{X} its +stored calls would hold. Each names the variants it was fitted on.} + +\item{retainX}{Logical. Keep that copy of \code{X} in a \code{"full"} fit +(default \code{FALSE}). An error at any other \code{fitRetention}.} \item{methodArgs}{Additional arguments forwarded to \code{glmnet::cv.glmnet}, built with \code{\link{GlmnetOptions}}. A bare diff --git a/man/enlocClusterProbabilities.Rd b/man/enlocClusterProbabilities.Rd deleted file mode 100644 index 17b0e1e66..000000000 --- a/man/enlocClusterProbabilities.Rd +++ /dev/null @@ -1,54 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/enloc.R -\name{enlocClusterProbabilities} -\alias{enlocClusterProbabilities} -\title{enloc Colocalisation Probabilities For One Signal Cluster} -\usage{ -enlocClusterProbabilities( - gwasLog10Bf, - qtlPip, - pi1e, - pi1ne, - p12 = 0, - applyFloor = FALSE -) -} -\arguments{ -\item{gwasLog10Bf}{Numeric vector of per-variant GWAS log10 Bayes factors.} - -\item{qtlPip}{Numeric vector of per-variant eQTL PIPs, same length and -order.} - -\item{pi1e}{Prior probability of GWAS causality for a variant that is a -causal eQTL, \code{expit(a0 + a1)}.} - -\item{pi1ne}{The same for a variant that is not, \code{expit(a0)}.} - -\item{p12}{Prior probability of colocalisation, used only for the -small-probability floor fastenloc applies to each SNP-level term.} - -\item{applyFloor}{Logical. Apply that floor. fastenloc skips it on the -summary-statistics path, where not all variants are present.} -} -\value{ -A list with \code{ppH0} to \code{ppH4}, \code{rcp}, \code{lcp} and - \code{scp} (the per-variant colocalisation probabilities whose sum is - \code{rcp}). -} -\description{ -The quantities fastenloc reports for a single eQTL signal - cluster, given the GWAS Bayes factors and the eQTL PIPs over the same - variants. - - Both parameterisations are returned because they come from one - computation: \code{rcp} is \code{ppH4} and \code{lcp} is - \code{ppH3 + ppH4}. -} -\examples{ -enlocClusterProbabilities( - gwasLog10Bf = c(2, 0.1, 0.1), - qtlPip = c(0.7, 0.2, 0.05), - pi1e = 0.02, - pi1ne = 0.001 -) -} diff --git a/man/estCtwasGroupPriors.Rd b/man/estCtwasGroupPriors.Rd index 2118f5870..7f74ac489 100644 --- a/man/estCtwasGroupPriors.Rd +++ b/man/estCtwasGroupPriors.Rd @@ -40,7 +40,11 @@ Step 2 of the three-step \code{\link{ctwasPipeline}}: assembles \code{region_data} from the inputs and runs \code{ctwas::est_param} (prefit EM + accurate EM) to estimate the group prior probabilities and prior variances. Returns the input state plus \code{region_data}, - \code{boundary_genes}, \code{z_gene}, and \code{param}. + \code{boundary_genes}, \code{z_gene}, and \code{param}. A context with + fewer genes than \code{min_group_size} gets no prior: cTWAS leaves it out + of the fit, and a warning names it. To keep it, lower + \code{min_group_size} in both \code{\link{CtwasRegionDataOptions}} and + \code{\link{CtwasEstParamOptions}}. } \examples{ data(ctwasInputsExample) diff --git a/man/estimateH2.Rd b/man/estimateH2.Rd index f5210608a..5bac36bf7 100644 --- a/man/estimateH2.Rd +++ b/man/estimateH2.Rd @@ -25,7 +25,8 @@ estimateH2(sumstats, ldRef, method = "lder", annotations = NULL, ...) contexts = NULL, traitId = NULL, cisWindow = NULL, - BPPARAM = SerialParam() + BPPARAM = SerialParam(), + referenceOverlap = 0 ) } \arguments{ @@ -84,6 +85,15 @@ its \code{region}. Every selected row then needs a \code{traitPos}.} \item{BPPARAM}{For a \code{QtlSumStats}: a \code{BiocParallelParam} over the selected rows, each row's estimate on one worker. Default \code{SerialParam()}.} + +\item{referenceOverlap}{For a \code{QtlSumStats}: the share of the study's +samples that are in the LD reference, from 0 to 1, one value or one per +selected row. Default 0. Usually unknown, and with a reference of thousands +it hardly matters (see Details): set it when the LD came from the study's +own samples or a small cohort holding them, 1 if it holds them all. Only +\code{"lava"} reads it, and its estimates record it in +\code{reference_overlap}: HDL-L's intercept absorbs what it measures, and +FastHer assumes the study's own LD.} } \value{ An \code{\linkS4class{H2Estimate}}. From a \code{GwasSumStats}, @@ -103,25 +113,49 @@ Estimate SNP heritability, genome-wide from GWAS summary } \details{ Each per-trait method is written from its authors' published - estimator and reports what they report: + estimator and reports what they report. All three read a row's \code{z} + as the t statistic of a marginal regression, the correlation being + \eqn{r = z / \sqrt{z^2 + n - 2}}: \eqn{z / \sqrt{n}}, which HDL-L's and + FastHer's own code read instead, overstates \eqn{r^2} by a factor of + \eqn{1 / (1 - r^2)}, about 1.4 for a strong QTL at \eqn{r^2 = 0.3}. \describe{ \item{\code{"lava"}}{(default) LAVA's univariate estimator (Werme et - al. 2022): the adjusted \eqn{R^2} of the trait on the standardized - LD components that hold 99\% of the LD variance (at least 2, at - most 0.75 times the sample size), with the F test of no - heritability. The estimate is scaled by \eqn{(N_{ref} - K - 1) / - (N_{ref} - 1)} for a reference of \eqn{N_{ref}} samples and - \eqn{K} components, a correction LAVA describes but leaves off. + al. 2022), on the \eqn{K} standardized LD components that hold 99\% + of the LD variance (at least 2, at most 0.75 times the sample size). + Whether LD is the study's own is not a yes or no: how far a reference + of \eqn{N} samples, holding a share \eqn{\rho} of the \eqn{n} study + samples (\code{referenceOverlap}), lies from the study's LD in units + of its sampling noise is \eqn{\phi = 1 + (1 - 2\rho) n / N}: 0 for + the study's own samples, about 1 for a panel of thousands. The + estimate is \eqn{(\delta'\delta - m) / (1 + m (\phi - 1))} with + \eqn{m = K / (n - 1)}, which at \eqn{\phi = 0} is exactly LAVA's + adjusted \eqn{R^2} of the trait on the components. LAVA's own noise + term holds only there: with a panel of 3000 on real genotypes it gave + 0.55-0.63 at a true 0.5 with \eqn{K / n} of 0.1-0.2, 0.69 at 0.3, and + \code{NA} for 70\% of traits at 0.6; this estimate stayed at + 0.47-0.52 across references of every kind. Assuming \eqn{\rho = 0} + costs about \eqn{2 \rho K / N} of the estimate, under 10\% for a + whole cis window with a panel of thousands holding all the study's + samples, but LD from the study's own samples at that default comes out + at half or less, hence a warning when \eqn{N \le 2 n}. Divergence + between populations of one ancestry raises \eqn{\phi}, unmodelled: + about 0.54 at a true 0.5 at Fst 0.001, and 0.62 at 0.003. The p-value + tests no heritability, with \eqn{\phi} in its variance. It is the + exact F test at \eqn{\phi = 0}, but ran at about 0.09 at a nominal + 0.05 with a panel, as real genotypes' components have heavier tails + than it assumes; HDL-L's held at 0.03-0.05, so prefer it for testing. LAVA itself withholds the estimate where \eqn{K} exceeds a tenth of the sample size, which a cis window's components usually do at QTL - sample sizes; it is reported here at any \eqn{K}. On real genotypes - its error past that point was about twice that below it, still - below HDL-L's. The estimate can be negative.} + sample sizes; it is reported here at any \eqn{K}. The estimate can be + negative or exceed 1.} \item{\code{"hdlL"}}{HDL-L (Li et al. 2025): the likelihood of the statistics rotated onto the LD eigenvectors that cover 99\% of the trace, in the heritability and an intercept, with a term for the reference's sampling noise. Its p-value is the likelihood ratio - against no heritability; \code{intercept} is the fitted intercept. + against no heritability, referred to the equal mixture of a + chi-square on one degree of freedom and a point mass at 0 since that + null is on the boundary of the heritability's range, half of what + HDL-L reports; \code{intercept} is the fitted intercept. \code{NA} where no fit converges and beats that null.} \item{\code{"fastHer"}}{FastHer (Svishcheva 2025): the maximum likelihood of an infinitesimal model for the study samples, @@ -129,11 +163,13 @@ Each per-trait method is written from its authors' published expected information; \code{scale} is the fitted phenotypic variance relative to its standardized value, about 1 when the model fits. HEELS (Li, Mazumder and Lin 2023) maximizes the same - likelihood by iteration. The likelihood assumes in-sample LD and is + likelihood by iteration. The standard error holds under that model: + with few causal variants it is too small, 0.77 of the estimates' + spread at a heritability of 0.2 with one causal variant, and 0.59 at + 0.5, on real genotypes. The likelihood assumes in-sample LD and is undefined when the reference's rank, or the statistics' squared - norm in the LD's metric, reaches the sample size, as it can with an - external reference larger than the study; such traits are - \code{NA}, with a warning.} + norm in the LD's metric, reaches the sample size, as it can with LD + from outside the study; such traits are \code{NA}, with a warning.} } Every per-trait method assumes the LD reference matches the population of the statistics, as the QC step (\code{\link{summaryStatsQc}}) @@ -163,7 +199,7 @@ estimateH2(ss, ldScoreExample, method = "sldsc") # Per-trait cis heritability, each row against its own LD reference. data(qtlSumStatsExample) -estimateH2(qtlSumStatsExample) +estimateH2(qtlSumStatsExample, referenceOverlap = 1) } \references{ Werme J, van der Sluis S, Posthuma D, de Leeuw CA (2022). An diff --git a/man/filterVariantsByLdReference.Rd b/man/filterVariantsByLdReference.Rd index e994e0038..87910a3e7 100644 --- a/man/filterVariantsByLdReference.Rd +++ b/man/filterVariantsByLdReference.Rd @@ -11,7 +11,9 @@ filterVariantsByLdReference(variantIds, ldReferenceMetaFile, keepIndel = TRUE) \item{ldReferenceMetaFile}{Path to LD metadata file or PLINK prefix.} -\item{keepIndel}{Whether to keep indel variants. Default TRUE.} +\item{keepIndel}{Whether to keep variants other than SNPs. A SNP has exactly +one of A, C, G, T for each allele, so \code{I}/\code{D} codes, \code{*} +and \code{N} count as indels. Default TRUE.} } \value{ A list with: diff --git a/man/fineMappingAnnotations.Rd b/man/fineMappingAnnotations.Rd index 869eb7d79..d9131616e 100644 --- a/man/fineMappingAnnotations.Rd +++ b/man/fineMappingAnnotations.Rd @@ -21,7 +21,9 @@ fineMappingAnnotations( \item{ldRef}{The \code{\linkS4class{LdStatistic}} the annotations will be used with.} -\item{types}{Which annotation types to build; see above. Default all four.} +\item{types}{Which annotation types to build; see above. Default +\code{"maxCpp"}, \code{"sumPip"}, \code{"credibleSet"} and +\code{"highPip"}.} \item{coverage}{Credible-set coverage for \code{"credibleSet"}. Default \code{0.95}.} @@ -57,6 +59,13 @@ Turn fine-mapping results -- typically molecular QTLs -- into trait at \code{coverage} (and \code{minPurity}), else 0.} \item{\code{"highPip"}}{1 when its largest PIP is at least \code{pipThreshold}, else 0.} + \item{\code{"maxCppCs"}, \code{"sumPipCs"}}{\code{"maxCpp"} and + \code{"sumPip"} from the effects that produced a credible set + alone: each variant's \code{within_cs_pip}, its posterior in the + effect of its credible set at the fit's primary coverage and + purity, 0 outside any (continuous). Every effect a fit keeps spreads + a whole unit of posterior over its variants, however weak its + evidence, and these leave that low-confidence mass out.} } Variants are matched to the reference allele-aware (id format, ref/alt diff --git a/man/fineMappingPipeline.Rd b/man/fineMappingPipeline.Rd index 3fe90d183..5e5cf3363 100644 --- a/man/fineMappingPipeline.Rd +++ b/man/fineMappingPipeline.Rd @@ -33,8 +33,7 @@ fineMappingPipeline(data, ...) dataDrivenPriorWeightsCutoff = 1e-10, phenotypeFilterParam = PhenotypeFilterParam(), verbose = 1, - fitRetention = c("slim", "full"), - dataType = NULL + fitRetention = c("slim", "full") ) \S4method{fineMappingPipeline}{MultiStudyQtlDataset}( @@ -62,8 +61,7 @@ fineMappingPipeline(data, ...) seed = NULL, phenotypeFilterParam = PhenotypeFilterParam(), verbose = 1, - fitRetention = c("slim", "full"), - dataType = NULL + fitRetention = c("slim", "full") ) \S4method{fineMappingPipeline}{QtlSumStats}( @@ -83,8 +81,7 @@ fineMappingPipeline(data, ...) susieRssParam = SusieRssParam(), panelFilterParam = PanelFilterParam(), crossValidationParam = CrossValidationParam(), - residualizationParam = ResidualizationParam(), - dataType = NULL + residualizationParam = ResidualizationParam() ) \S4method{fineMappingPipeline}{GwasSumStats}( @@ -98,8 +95,7 @@ fineMappingPipeline(data, ...) susieRssParam = SusieRssParam(), panelFilterParam = PanelFilterParam(), crossValidationParam = CrossValidationParam(), - residualizationParam = ResidualizationParam(), - dataType = NULL + residualizationParam = ResidualizationParam() ) } \arguments{ @@ -165,7 +161,7 @@ studies, individual-level or summary-statistics, as above.} with \code{\link{GenotypeFilterParam}}: its \code{keepSamples} narrows the dataset once, and its MAF, MAC, variance, missingness, \code{keepVariants} and \code{keepIndel} settings apply to each -extracted block, against that block's samples. A field left unset +extracted block, against the samples being fit. A field left unset filters nothing. A \code{QtlDataset} records no filter of its own, so this is the only one. \code{QtlDataset} and \code{MultiStudyQtlDataset} input only.} @@ -330,13 +326,6 @@ matrices. The per-variant \code{topLoci} table is fully populated either way --- its per-credible-set columns are governed by \code{credibleSetParam}'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{writeBed}} / -\code{\link{writeGwasVcf}}. \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.} diff --git a/man/genotypes.Rd b/man/genotypes.Rd index eda3e79ed..db7fea7ed 100644 --- a/man/genotypes.Rd +++ b/man/genotypes.Rd @@ -21,7 +21,9 @@ A numeric matrix, a list of matrices, or \code{NULL}. Extract a genotype matrix from an object that carries genotype data. For a \code{QtlDataset}, the whole dataset's dosages (samples x variants), with no QC applied: to read part of it, narrow the dataset - first, e.g. \code{genotypes(x[window, samples, ])}. + first, e.g. \code{genotypes(x[window, samples, ])}. Its columns carry the + labels the genotype file gives its variants; a pipeline's results name + them by the canonical id \code{\link{variantIds}} describes. } \examples{ data(qtlDatasetExample) diff --git a/man/glmnetWeights.Rd b/man/glmnetWeights.Rd index 3189a1490..5f31eda5f 100644 --- a/man/glmnetWeights.Rd +++ b/man/glmnetWeights.Rd @@ -9,6 +9,7 @@ glmnetWeights( y, alpha, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = GlmnetOptions() ) } @@ -26,8 +27,11 @@ nothing; \code{"slim"} the selected fit, as a list of its coefficients (\code{coef}, over the variants fitted), \code{intercept}, \code{lambda} and cross-validated error there (\code{cvm}, \code{cvsd}); \code{"full"} the whole \code{cv.glmnet} object, with -every \code{lambda}'s coefficients. Either names the variants it was -fitted on.} +every \code{lambda}'s coefficients but without the copy of \code{X} its +stored calls would hold. Each names the variants it was fitted on.} + +\item{retainX}{Logical. Keep that copy of \code{X} in a \code{"full"} fit +(default \code{FALSE}). An error at any other \code{fitRetention}.} \item{methodArgs}{Additional arguments forwarded to \code{glmnet::cv.glmnet}, built with \code{\link{GlmnetOptions}}. A bare diff --git a/man/l0learnRssWeights.Rd b/man/l0learnRssWeights.Rd index 86bed3a54..90639ed9f 100644 --- a/man/l0learnRssWeights.Rd +++ b/man/l0learnRssWeights.Rd @@ -22,8 +22,8 @@ l0learnRssWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engine receives the standardized effects z / sqrt(n), -with n the median of \code{n}.} +standardized. The engine receives the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see diff --git a/man/lassoWeights.Rd b/man/lassoWeights.Rd index bfb38d721..38a951203 100644 --- a/man/lassoWeights.Rd +++ b/man/lassoWeights.Rd @@ -8,6 +8,7 @@ lassoWeights( X, y, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = GlmnetOptions() ) } @@ -22,8 +23,11 @@ nothing; \code{"slim"} the selected fit, as a list of its coefficients (\code{coef}, over the variants fitted), \code{intercept}, \code{lambda} and cross-validated error there (\code{cvm}, \code{cvsd}); \code{"full"} the whole \code{cv.glmnet} object, with -every \code{lambda}'s coefficients. Either names the variants it was -fitted on.} +every \code{lambda}'s coefficients but without the copy of \code{X} its +stored calls would hold. Each names the variants it was fitted on.} + +\item{retainX}{Logical. Keep that copy of \code{X} in a \code{"full"} fit +(default \code{FALSE}). An error at any other \code{fitRetention}.} \item{methodArgs}{Additional arguments forwarded to \code{glmnet::cv.glmnet}, built with \code{\link{GlmnetOptions}}. A bare diff --git a/man/lassosumRssWeights.Rd b/man/lassosumRssWeights.Rd index 52ec126fa..9af53e520 100644 --- a/man/lassosumRssWeights.Rd +++ b/man/lassosumRssWeights.Rd @@ -18,8 +18,8 @@ lassosumRssWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engine receives the standardized effects z / sqrt(n), -with n the median of \code{n}.} +standardized. The engine receives the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see @@ -94,8 +94,8 @@ held-out summary statistics. training part, 75\% of the samples, and its held-out remainder, drawing each split from the statistics and the LD as PUMAS does (Zhao et al. 2021). The path is refit on each training part, every candidate is scored -on the held-out part by -\deqn{\frac{c_h^T \beta}{\sqrt{\beta^T \hat\Sigma \beta}}, \quad +on the held-out part by the variance of it that it explains, +\deqn{2 c_h^T \beta - \beta^T \hat\Sigma \beta, \quad \hat\Sigma = (1 - w) R + w I,} and the full-data fit at the (\code{s}, lambda) with the best mean score is returned. \eqn{w} is the Schafer-Strimmer intensity for a correlation @@ -103,13 +103,16 @@ matrix from the reference's sample size. A reference smaller than the number of variants misses directions of the true LD, and the least-penalized fits put weight exactly there, so scoring against \eqn{R} itself would keep choosing them. The splits use R's random number -generator, so \code{set.seed()} makes the weights reproducible. The score -does not change with \eqn{\beta}'s scale, so candidates that differ only -in scale tie on it, as the ridge's do at \eqn{s = 1}, where each fit is -\eqn{c / (1 + \lambda)}. Of tied candidates, the one explaining the most -held-out variance, \eqn{2 c_h^T \beta - \beta^T \hat\Sigma \beta} averaged -over the splits, is returned, and the first in the grid where that ties -too. +generator, so \code{set.seed()} makes the weights reproducible. + +PUMAS scores by the correlation with the held-out part, +\eqn{c_h^T \beta / \sqrt{\beta^T \hat\Sigma \beta}}, which here only breaks +ties. It does not change with \eqn{\beta}'s scale, so it lets a fit win at +\eqn{s} near 1, where the LD is nearly ignored and every tag of a signal +keeps its marginal weight, with a predicted variance thousands of times the +trait's. A candidate whose fit is empty in a split or on the full data is +returned only if every candidate's is, and the first in the grid wins a tie +on both scores. \code{selection = "ldQuadratic"} is lassosum's pseudovalidation (Mak et al. 2017): the in-sample score diff --git a/man/ldpred2AutoWeights.Rd b/man/ldpred2AutoWeights.Rd index 6c752950f..3d14ef89b 100644 --- a/man/ldpred2AutoWeights.Rd +++ b/man/ldpred2AutoWeights.Rd @@ -22,8 +22,8 @@ ldpred2AutoWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The sampler reads z / sqrt(n) for each variant's own -sample size.} +standardized. The sampler reads the correlation each z stands for, +z / sqrt(z^2 + n - 2), for each variant's own sample size.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see diff --git a/man/ldpred2GridWeights.Rd b/man/ldpred2GridWeights.Rd index 37d754b46..6d80c5eb4 100644 --- a/man/ldpred2GridWeights.Rd +++ b/man/ldpred2GridWeights.Rd @@ -23,8 +23,8 @@ ldpred2GridWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The sampler reads z / sqrt(n) for each variant's own -sample size.} +standardized. The sampler reads the correlation each z stands for, +z / sqrt(z^2 + n - 2), for each variant's own sample size.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see diff --git a/man/learnTwasWeights.Rd b/man/learnTwasWeights.Rd index cae69ec18..1b58f5b81 100644 --- a/man/learnTwasWeights.Rd +++ b/man/learnTwasWeights.Rd @@ -14,6 +14,7 @@ learnTwasWeights( BPPARAM = SerialParam(), fittedModels = NULL, fitRetention = c("none", "slim", "full"), + retainX = FALSE, weightStandardized = FALSE, weightsDataType = NULL, ldReference = NULL, @@ -59,18 +60,26 @@ produce the fits.} \item{fitRetention}{How much of each per-method fit is kept on the result: \code{"none"} (default) keeps none, \code{"slim"} keeps the trimmed -payload downstream needs, \code{"full"} keeps the whole fitted object. +payload downstream needs, \code{"full"} keeps the whole fitted object but +for any copy of \code{X}. Every method keeps a fit, which names the variants it was fitted on. \code{"slim"} and \code{"full"} differ for mr.mash, glmnet (\code{lasso}, \code{enet}, \code{ridge}), ncvreg (\code{scad}, -\code{mcp}), \code{l0learn}, the Bayesian alphabet, \code{mrash} -and the DPR methods; for the others the two behave alike.} +\code{mcp}), \code{l0learn}, the Bayesian alphabet and \code{mrash}; +for the others the two behave alike.} + +\item{retainX}{Logical. Keep a fit's copy of \code{X}, for the methods whose +fit has one (\code{lasso}, \code{enet}, \code{ridge}, \code{scad}, +\code{mcp}, \code{mrash} and the DPR methods), at +\code{fitRetention = "full"} (default \code{FALSE}). An error at any +other \code{fitRetention}, and when no method in \code{weightMethods} has +such a copy.} \item{weightStandardized}{Logical. Whether the supplied \code{X} / \code{Y} are already standardized. Default \code{FALSE}.} -\item{weightsDataType}{Character or \code{NULL}. Data-type label recorded on -the weights (e.g. \code{"individual"}).} +\item{weightsDataType}{Character or \code{NULL}. The molecular type recorded +on the weights (e.g. \code{"expression"}).} \item{ldReference}{An LD reference (a genotype panel, see \code{\link{readGenotypes}}, a \code{GenotypeMixture} or an diff --git a/man/loadGenotypeRegion.Rd b/man/loadGenotypeRegion.Rd index b961008cc..a9d03221f 100644 --- a/man/loadGenotypeRegion.Rd +++ b/man/loadGenotypeRegion.Rd @@ -20,9 +20,16 @@ loadGenotypeRegion( \item{which}{Optional \code{GRanges}: load only the variants that overlap it. If NULL, loads every variant.} -\item{keepIndel}{Whether to keep indel SNPs.} +\item{keepIndel}{Whether to keep variants other than SNPs. A SNP has exactly +one of A, C, G, T for each allele, so \code{I}/\code{D} codes, \code{*} +and \code{N} count as indels.} -\item{keepVariantsPath}{Path to a file listing variants to keep.} +\item{keepVariantsPath}{Path to a file listing variants to keep: a table +with \code{chrom} and \code{pos} columns, with their alleles in \code{A2} +and \code{A1} or without any, or one variant id or rsID per line. Alleles +match in either order and on either strand; a table without alleles keeps +every variant at each position, and an rsID is matched against the +file's own variant ids.} \item{returnVariantInfo}{If TRUE, return a list with X (dosage matrix) and variant_info (data.frame). If FALSE (default), return only the dosage diff --git a/man/loadQtlDatasetFromManifest.Rd b/man/loadQtlDatasetFromManifest.Rd index 876ff32e4..0c30c5b5a 100644 --- a/man/loadQtlDatasetFromManifest.Rd +++ b/man/loadQtlDatasetFromManifest.Rd @@ -16,7 +16,9 @@ loadQtlDatasetFromManifest( \item{manifest}{A data.frame or a path to a manifest file. Recognised columns (canonical camelCase; snake_case aliases accepted): \code{context} (required), \code{phenotypePath} (required, bgzipped BED), -\code{covariatePath} (optional, per-context covariates), and the +\code{covariatePath} (optional, per-context covariates), +\code{dataType} (optional, each context's molecular type, as for +\code{\link{QtlDataset}}; a blank cell records none), and the single-valued \code{study} / \code{genotypePath} / \code{genotypeCovariatePath}.} diff --git a/man/loadQtlSumStatsFromManifest.Rd b/man/loadQtlSumStatsFromManifest.Rd index d5bfc04df..8524ee2f2 100644 --- a/man/loadQtlSumStatsFromManifest.Rd +++ b/man/loadQtlSumStatsFromManifest.Rd @@ -21,7 +21,9 @@ loadQtlSumStatsFromManifest( \item{manifest}{A data.frame or path. Columns (snake_case aliases accepted): \code{study}, \code{context}, \code{trait} (required), \code{sumStatsPath} (required), \code{columnMapping} (optional), \code{nSample} (optional -tuple-level total N), \code{varY} (optional), the single-valued +tuple-level total N), \code{varY} (optional), \code{dataType} (optional, +each tuple's molecular type, as for \code{\link{QtlSumStats}}; a blank +cell records none), the single-valued \code{genome}, and \code{ldReferencePath}, which may differ between rows (each distinct reference is read once, for the chromosomes its rows cover, and stored once). When a row supplies \code{nSample}, diff --git a/man/mcpRssWeights.Rd b/man/mcpRssWeights.Rd index eb5ada9f4..459a187c5 100644 --- a/man/mcpRssWeights.Rd +++ b/man/mcpRssWeights.Rd @@ -19,8 +19,8 @@ mcpRssWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engine receives the standardized effects z / sqrt(n), -with n the median of \code{n}.} +standardized. The engine receives the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see diff --git a/man/mcpWeights.Rd b/man/mcpWeights.Rd index d2a424f30..6d6882acb 100644 --- a/man/mcpWeights.Rd +++ b/man/mcpWeights.Rd @@ -9,6 +9,7 @@ mcpWeights( y, nfolds = 5, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = NcvregOptions() ) } @@ -26,7 +27,10 @@ nothing; \code{"slim"} the selected fit, as a list of its coefficients \code{lambda} and cross-validated error there (\code{cve}, \code{cvse}); \code{"full"} the whole \code{cv.ncvreg} object, with every \code{lambda}'s coefficients but without its copy of \code{X}. -Either names the variants it was fitted on.} +Each names the variants it was fitted on.} + +\item{retainX}{Logical. Keep that copy of \code{X} in a \code{"full"} fit +(default \code{FALSE}). An error at any other \code{fitRetention}.} \item{methodArgs}{Options forwarded to \code{ncvreg::cv.ncvreg}, built with \code{\link{NcvregOptions}}. A bare list is refused: it cannot be diff --git a/man/mergeCtwasBoundaryRegions.Rd b/man/mergeCtwasBoundaryRegions.Rd index b273d365f..3ed3e074a 100644 --- a/man/mergeCtwasBoundaryRegions.Rd +++ b/man/mergeCtwasBoundaryRegions.Rd @@ -54,13 +54,13 @@ The \code{finemapResult} list with \code{finemap_res}, inputs as the "updated" values, so the result is effectively unchanged. } \description{ -Optional step 4 of the cTWAS pipeline (default-off region - merging). A gene whose cis window straddles an LD-block boundary (a - \code{boundary_genes} member) is split across two regions in the first-pass - fine-mapping. This step selects the high-PIP boundary genes, merges each - one's adjacent regions into a single region, re-runs fine-mapping on the - merged regions, and splices the updated results back into the - \code{\link{finemapCtwasRegions}} output. Thin wrapper over +Step 4 of the cTWAS pipeline, on by default in + \code{\link{ctwasPipeline}}. A gene whose weights cross an LD-block + boundary (a \code{boundary_genes} member) is fine-mapped in one block in + the first pass, with only that block's LD. This step selects the high-PIP + boundary genes, merges each one's adjacent regions into a single region, + re-runs fine-mapping on the merged regions, and splices the updated results + back into the \code{\link{finemapCtwasRegions}} output. Thin wrapper over \code{ctwas::postprocess_region_merging()} (or \code{ctwas::postprocess_region_merging_noLD()} when the inputs carry no LD loaders). diff --git a/man/mrashWeights.Rd b/man/mrashWeights.Rd index 4cb7490a4..dcdbb3845 100644 --- a/man/mrashWeights.Rd +++ b/man/mrashWeights.Rd @@ -8,6 +8,7 @@ mrashWeights( X, y, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = MrashOptions() ) } @@ -20,8 +21,11 @@ mrashWeights( attribute of the returned weights: \code{"none"} (default) attaches nothing, \code{"slim"} the fitted model without its copy of the data (\code{data}) or the order it updated the variants in each iteration -(\code{update.order}), \code{"full"} the whole fitted model. Either -names the variants it was fitted on.} +(\code{update.order}), \code{"full"} the whole fitted model but for its +copy of \code{X}. Each names the variants it was fitted on.} + +\item{retainX}{Logical. Keep that copy of \code{X} in a \code{"full"} fit +(default \code{FALSE}). An error at any other \code{fitRetention}.} \item{methodArgs}{Options forwarded to \code{susieR::mr.ash}, built with \code{\link{MrashOptions}}. A bare list is refused: it cannot be diff --git a/man/ncvregWeights.Rd b/man/ncvregWeights.Rd index 13de01193..a53323cf8 100644 --- a/man/ncvregWeights.Rd +++ b/man/ncvregWeights.Rd @@ -10,6 +10,7 @@ ncvregWeights( penalty, nfolds = 5, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = NcvregOptions() ) } @@ -30,7 +31,10 @@ nothing; \code{"slim"} the selected fit, as a list of its coefficients \code{lambda} and cross-validated error there (\code{cve}, \code{cvse}); \code{"full"} the whole \code{cv.ncvreg} object, with every \code{lambda}'s coefficients but without its copy of \code{X}. -Either names the variants it was fitted on.} +Each names the variants it was fitted on.} + +\item{retainX}{Logical. Keep that copy of \code{X} in a \code{"full"} fit +(default \code{FALSE}). An error at any other \code{fitRetention}.} \item{methodArgs}{Options forwarded to \code{ncvreg::cv.ncvreg}, built with \code{\link{NcvregOptions}}. A bare list is refused: it cannot be diff --git a/man/prsCsWeights.Rd b/man/prsCsWeights.Rd index 7cb729cac..d29e5fcb1 100644 --- a/man/prsCsWeights.Rd +++ b/man/prsCsWeights.Rd @@ -15,8 +15,8 @@ prsCsWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engine receives the standardized effects z / sqrt(n), -with n the median of \code{n}.} +standardized. The engine receives the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see diff --git a/man/qtlEnrichment.Rd b/man/qtlEnrichment.Rd index 7465fb568..30ee09100 100644 --- a/man/qtlEnrichment.Rd +++ b/man/qtlEnrichment.Rd @@ -12,7 +12,7 @@ qtlEnrichment( piQtl = NULL, lambda = 1, impN = 25, - doubleShrinkage = FALSE, + doubleShrinkage = TRUE, besselCorrection = TRUE, BPPARAM = SerialParam(), verbose = TRUE, @@ -46,8 +46,9 @@ strong shrinkage. The default value is set to 1.0.} \item{impN}{Rounds of multiple imputation to draw QTL from, default is 25.} -\item{doubleShrinkage}{Logical. Apply the double-shrinkage correction to the -enrichment estimate. Default \code{FALSE}.} +\item{doubleShrinkage}{Logical. Shrink each imputation round's estimate and +then the combined one, as fastenloc does; \code{FALSE} shrinks only the +combined one. Default \code{TRUE}.} \item{besselCorrection}{Logical. Apply Bessel's correction when estimating the sampling variance. Default \code{TRUE}.} diff --git a/man/readFineMappingResult.Rd b/man/readFineMappingResult.Rd index 5dcbb7616..9d5e08b29 100644 --- a/man/readFineMappingResult.Rd +++ b/man/readFineMappingResult.Rd @@ -36,7 +36,7 @@ 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{dataType}{Optional molecular type, same fill-in rule.} \item{weightStandardized}{Whether the published posterior effect (\code{WGT}) is on the standardized genotype scale. Required when the diff --git a/man/readTwasWeights.Rd b/man/readTwasWeights.Rd index 92b9da667..1d397e597 100644 --- a/man/readTwasWeights.Rd +++ b/man/readTwasWeights.Rd @@ -35,7 +35,7 @@ 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{dataType}{Optional molecular type, same fill-in rule.} \item{weightStandardized}{Whether the published weights are on the standardized genotype scale. Required when the file does not record it: diff --git a/man/ridgeRssWeights.Rd b/man/ridgeRssWeights.Rd index e264181af..2e4a5aba3 100644 --- a/man/ridgeRssWeights.Rd +++ b/man/ridgeRssWeights.Rd @@ -17,8 +17,8 @@ ridgeRssWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engine receives the standardized effects z / sqrt(n), -with n the median of \code{n}.} +standardized. The engine receives the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see diff --git a/man/ridgeWeights.Rd b/man/ridgeWeights.Rd index 09d85200c..58a0af09c 100644 --- a/man/ridgeWeights.Rd +++ b/man/ridgeWeights.Rd @@ -8,6 +8,7 @@ ridgeWeights( X, y, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = GlmnetOptions() ) } @@ -22,8 +23,11 @@ nothing; \code{"slim"} the selected fit, as a list of its coefficients (\code{coef}, over the variants fitted), \code{intercept}, \code{lambda} and cross-validated error there (\code{cvm}, \code{cvsd}); \code{"full"} the whole \code{cv.glmnet} object, with -every \code{lambda}'s coefficients. Either names the variants it was -fitted on.} +every \code{lambda}'s coefficients but without the copy of \code{X} its +stored calls would hold. Each names the variants it was fitted on.} + +\item{retainX}{Logical. Keep that copy of \code{X} in a \code{"full"} fit +(default \code{FALSE}). An error at any other \code{fitRetention}.} \item{methodArgs}{Additional arguments forwarded to \code{glmnet::cv.glmnet}, built with \code{\link{GlmnetOptions}}. A bare diff --git a/man/scadRssWeights.Rd b/man/scadRssWeights.Rd index e10b6e010..6a8e40c94 100644 --- a/man/scadRssWeights.Rd +++ b/man/scadRssWeights.Rd @@ -19,8 +19,8 @@ scadRssWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engine receives the standardized effects z / sqrt(n), -with n the median of \code{n}.} +standardized. The engine receives the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see diff --git a/man/scadWeights.Rd b/man/scadWeights.Rd index fb504ecf1..b81fd26b4 100644 --- a/man/scadWeights.Rd +++ b/man/scadWeights.Rd @@ -9,6 +9,7 @@ scadWeights( y, nfolds = 5, fitRetention = c("none", "slim", "full"), + retainX = FALSE, methodArgs = NcvregOptions() ) } @@ -26,7 +27,10 @@ nothing; \code{"slim"} the selected fit, as a list of its coefficients \code{lambda} and cross-validated error there (\code{cve}, \code{cvse}); \code{"full"} the whole \code{cv.ncvreg} object, with every \code{lambda}'s coefficients but without its copy of \code{X}. -Either names the variants it was fitted on.} +Each names the variants it was fitted on.} + +\item{retainX}{Logical. Keep that copy of \code{X} in a \code{"full"} fit +(default \code{FALSE}). An error at any other \code{fitRetention}.} \item{methodArgs}{Options forwarded to \code{ncvreg::cv.ncvreg}, built with \code{\link{NcvregOptions}}. A bare list is refused: it cannot be diff --git a/man/sdprWeights.Rd b/man/sdprWeights.Rd index ea9f66004..40468b353 100644 --- a/man/sdprWeights.Rd +++ b/man/sdprWeights.Rd @@ -15,8 +15,8 @@ sdprWeights( \item{stat}{A list of summary statistics: \code{n} (per-variant sample sizes) and the marginal effects as \code{cor} (standardized), \code{z}, \code{b} with \code{seb}, or \code{b} alone, taken as already -standardized. The engine receives the standardized effects z / sqrt(n), -with n the median of \code{n}.} +standardized. The engine receives the correlation each z stands for, +z / sqrt(z^2 + n - 2), with n the median of \code{n}.} \item{ldReference}{The LD reference the statistics' variants are matched to by \code{stat$variantNames}: a genotype panel (see diff --git a/man/stratifiedHeritabilityEnrichment.Rd b/man/stratifiedHeritabilityEnrichment.Rd index 5a387c16a..b02467184 100644 --- a/man/stratifiedHeritabilityEnrichment.Rd +++ b/man/stratifiedHeritabilityEnrichment.Rd @@ -25,7 +25,12 @@ Extract the per-annotation enrichment of an \item{partitioned}{tau-based enrichment of the baseline annotations, with columns \code{annotation}, \code{tau}, \code{tauSe}, \code{enrichment}, \code{enrichmentSe}, \code{enrichmentP}, - \code{propH2}, \code{propSnps}.} + \code{propH2}, \code{propSnps}. \code{enrichmentP} is ldsc's t test + of the per-variant heritability inside the annotation against + outside it. The last five are \code{NA} for an annotation with + values outside [0, 1], where they are not shares of the variants and + the heritability and depend on where the annotation's zero is; read + \code{tau} there.} \item{score test}{the score test over the tested (candidate) annotations, with columns \code{annotation}, \code{scoreZ}, \code{scoreP}.} diff --git a/man/summaryStatsQc.Rd b/man/summaryStatsQc.Rd index 41b2a0509..3ba0446d8 100644 --- a/man/summaryStatsQc.Rd +++ b/man/summaryStatsQc.Rd @@ -46,8 +46,9 @@ disables it).} \emph{and} against the panel. It has always been a single knob, so it lives here rather than being split into two that could disagree.} -\item{keepVariants}{Optional character vector of variant IDs (SNP column) to -retain prior to harmonization.} +\item{keepVariants}{Optional character vector of variant IDs or rsIDs to +retain prior to harmonization. Alleles match in either order and on either +strand, and an rsID is matched against the \code{SNP} column.} \item{skipRegion}{Optional \code{GRanges} of regions to drop.} diff --git a/man/twasWeightsCv.Rd b/man/twasWeightsCv.Rd index 9b490b56d..9c0aad106 100644 --- a/man/twasWeightsCv.Rd +++ b/man/twasWeightsCv.Rd @@ -64,8 +64,8 @@ payload downstream needs, \code{"full"} keeps the whole fitted object. Every method keeps a fit, which names the variants it was fitted on. \code{"slim"} and \code{"full"} differ for mr.mash, glmnet (\code{lasso}, \code{enet}, \code{ridge}), ncvreg (\code{scad}, -\code{mcp}), \code{l0learn}, the Bayesian alphabet, \code{mrash} -and the DPR methods; for the others the two behave alike.} +\code{mcp}), \code{l0learn}, the Bayesian alphabet and \code{mrash}; +for the others the two behave alike.} \item{seed}{Integer or \code{NULL}. When supplied, seeds both the main-process RNG (fold partitioning, variant sub-sampling) and the diff --git a/man/twasWeightsExample.Rd b/man/twasWeightsExample.Rd index a7be35ecd..e16d4e6aa 100644 --- a/man/twasWeightsExample.Rd +++ b/man/twasWeightsExample.Rd @@ -8,7 +8,8 @@ A \code{TwasWeights} collection with one row, the gene, holding 11 methods: \code{mrash}, \code{susie}, \code{susieInf}, \code{enet}, \code{lasso}, \code{mcp}, \code{scad}, \code{l0learn}, \code{bayesR}, - \code{bayesC} and \code{ensemble}. + \code{bayesC} and \code{ensemble}. Its \code{\link{dataType}} is + \code{"expression"}. } \description{ A \code{\link{TwasWeights}} collection holding per-variant TWAS diff --git a/man/twasWeightsPipeline.Rd b/man/twasWeightsPipeline.Rd index bf193abf9..a19f81376 100644 --- a/man/twasWeightsPipeline.Rd +++ b/man/twasWeightsPipeline.Rd @@ -29,8 +29,8 @@ twasWeightsPipeline(data, ...) usePCA = FALSE, nPCs = 10L, fitRetention = c("slim", "none", "full"), + retainX = FALSE, residualizationParam = ResidualizationParam(), - weightsDataType = NULL, phenotypeFilterParam = PhenotypeFilterParam(), verbose = 1, seed = NULL @@ -47,7 +47,6 @@ twasWeightsPipeline(data, ...) fineMappingResult = NULL, twasWeights = NULL, fitRetention = c("slim", "none", "full"), - weightsDataType = NULL, verbose = 1L, panelFilterParam = PanelFilterParam(), crossValidationParam = CrossValidationParam() @@ -72,7 +71,7 @@ twasWeightsPipeline(data, ...) usePCA = FALSE, nPCs = 10L, fitRetention = c("slim", "none", "full"), - weightsDataType = NULL, + retainX = FALSE, phenotypeFilterParam = PhenotypeFilterParam(), verbose = 1, residualizationParam = ResidualizationParam(), @@ -91,10 +90,21 @@ embedded \code{QtlSumStats}, then rbinds the results.} \item{methods}{A character vector of short method names, a preset string (\code{"default"} or \code{"fastDefault"}), or a named list of -\code{_weights = args} entries. For QtlSumStats input the -default is \code{c("lasso", "prsCs", "dprGibbs")}, which there run -lassosum, PRS-CS and SDPR; the fine-mapping methods are not in it, and -are requested by name together with a \code{fineMappingResult}.} +\code{_weights = args} entries. On individual-level input the +default is \code{"default"}, which is \code{"fastDefault"} (\code{susie}, +\code{susieInf}, \code{mrash}, \code{enet}, \code{lasso}, \code{mcp}, +\code{scad} and \code{l0learn}) with \code{bayesR} and \code{bayesC}. +\code{susie} and \code{susieInf} read their fits from a +\code{fineMappingResult}, so either preset skips them, with a warning, +where no \code{fineMappingResult} holds them; naming them outside a preset +without one is an error. For QtlSumStats input +the default, which either preset also names, is +\code{c("lasso", "prsCs", "dprGibbs")}, which there run lassosum, PRS-CS +and SDPR; the fine-mapping methods are not in it, and are requested by +name together with a \code{fineMappingResult}. A +multivariate method (\code{mrmash}, \code{mvsusie}, \code{fsusie}) fits +the selected contexts and traits together and any other method each +(context, trait) alone, so the two kinds can be requested at once.} \item{backend}{The engine a method with several runs on, as a named character vector keyed by method, e.g. @@ -248,14 +258,23 @@ components fitted per context when \code{usePCA = TRUE} (default \code{ridge}, \code{scad}, \code{mcp}, \code{l0learn}, the Bayesian alphabet, \code{mrash} and the DPR methods, whose full fit is the engine's whole object where the slim one keeps the selected fit or the - posterior summaries, without any copy of the data. Every other method - keeps its fit whole or not at all, except the fine-mapping methods - (\code{susie}, \code{susieInf}, \code{susieAsh}, \code{mvsusie}, - \code{fsusie}), whose rows keep none: their weights come from the - \code{fineMappingResult}'s fit, which is where \code{ctwasPipeline()} - reads it. Requesting \code{"full"} for a run where no method can honour - it is an error, and a mixed run warns rather than dropping it in - silence.} + posterior summaries, without any copy of \code{X} unless \code{retainX} + asks for it. Every other method keeps its fit whole or not at all, except + the fine-mapping methods (\code{susie}, \code{susieInf}, + \code{susieAsh}, \code{mvsusie}, \code{fsusie}), whose rows keep none: + their weights come from the \code{fineMappingResult}'s fit, which is where + \code{ctwasPipeline()} reads it. Requesting \code{"full"} for a run where + no method can honour it is an error, and a mixed run warns rather than + dropping it in silence.} + +\item{retainX}{Logical. Keep a fit's copy of \code{X}, which can be its +largest part, at \code{fitRetention = "full"} (default \code{FALSE}). An +error at any other \code{fitRetention}, and on a run where no method's fit +has a copy: only \code{lasso}, \code{enet}, \code{ridge}, \code{scad}, +\code{mcp}, \code{mrash} and the DPR methods do. No fine-mapping fit holds +one, so \code{\link{fineMappingPipeline}} has no such argument, and summary +statistics have no \code{X}: on a \code{MultiStudyQtlDataset} it reaches +the individual-level studies.} \item{residualizationParam}{Covariate residualization settings, built with \code{\link{ResidualizationParam}} and forwarded to @@ -275,9 +294,6 @@ components fitted per context when \code{usePCA = TRUE} (default off by default, so a non-default value there is an explicit request for something the input cannot do.} -\item{weightsDataType}{Optional data-type label recorded on every -\code{TwasWeightsRow$dataType} (e.g. \code{"expression"}).} - \item{phenotypeFilterParam}{The run's phenotype QC, built with \code{\link{PhenotypeFilterParam}}: how missing phenotype values are handled (unset drops the affected samples; \code{"impute"} fills each diff --git a/man/weightsDataType.Rd b/man/weightsDataType.Rd deleted file mode 100644 index 1dc9d0805..000000000 --- a/man/weightsDataType.Rd +++ /dev/null @@ -1,50 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/AllGenerics.R, R/twasWeights.R, -% R/twasWeightsRow.R -\name{weightsDataType} -\alias{weightsDataType} -\alias{weightsDataType,TwasWeights-method} -\alias{weightsDataType,TwasWeightsRow-method} -\title{Get Data Type} -\usage{ -weightsDataType(x, ...) - -\S4method{weightsDataType}{TwasWeights}( - x, - studyName = NULL, - context = NULL, - trait = NULL, - method = NULL -) - -\S4method{weightsDataType}{TwasWeightsRow}(x) -} -\arguments{ -\item{x}{A \code{TwasWeightsRow} or \code{TwasWeights}.} - -\item{...}{Class-specific selection arguments.} - -\item{studyName}{Character (length 1) or \code{NULL}. Restrict the selection -to this study; \code{NULL} matches all studies.} - -\item{context}{Character (length 1) or \code{NULL}. Restrict the selection to -this context; \code{NULL} matches all contexts.} - -\item{trait}{Character (length 1) or \code{NULL}. Restrict the selection to -this trait; \code{NULL} matches all traits.} - -\item{method}{Character (length 1) or \code{NULL}. Restrict the selection to -this fine-mapping / weight method; \code{NULL} matches all methods.} -} -\value{ -A character vector or NULL. -} -\description{ -Extract the data-type tag. -} -\examples{ -twe <- TwasWeightsRow(variantIds = sprintf("chr1:\%d:A:G", 100L * (1:4)), - weights = rep(0.1, 4), cvResult = list(rsq = 0.5), - weightStandardized = FALSE) -weightsDataType(twe) -} diff --git a/src/bgen.cpp b/src/bgen.cpp index 5ddd56ca4..b755bdf86 100644 --- a/src/bgen.cpp +++ b/src/bgen.cpp @@ -49,6 +49,29 @@ class BitReader { return static_cast(value) / static_cast(mask); } + // Moves past `count` values without decoding them: a sample nobody asked + // for costs some arithmetic rather than a division per probability. + void skip(uint64_t count) { + uint64_t skipped = count * static_cast(bits_); + if (skipped <= static_cast(held_)) { + buffer_ >>= skipped; + held_ -= static_cast(skipped); + return; + } + skipped -= static_cast(held_); + buffer_ = 0; + held_ = 0; + pos_ += static_cast(skipped / 8); + const int partial = static_cast(skipped % 8); + if (pos_ > size_ || (partial > 0 && pos_ == size_)) { + cpp11::stop("BGEN probability data ends early."); + } + if (partial > 0) { + buffer_ = static_cast(data_[pos_++]) >> partial; + held_ = 8 - partial; + } + } + private: const unsigned char* data_; size_t size_; @@ -105,18 +128,25 @@ double genotype_number(int ploidy, int K, double cap) { } // namespace /** - * @brief The expected counts of the requested alleles in every sample. + * @brief The expected counts of the requested alleles in the requested samples. + * + * A sample's probabilities sit at a position only the samples before it can + * tell, so those are walked; but only the samples asked for are decoded, and + * the walk stops at the last of them. * * @param data One variant's uncompressed layout-2 probability data. * @param nSamples The file's sample count, which the data must repeat. * @param alleles 1-based indices of the alleles whose expected counts to * return, one output column each. - * @return An nSamples x length(alleles) matrix; NA for a sample flagged - * missing or of ploidy 0. + * @param samples 1-based, strictly ascending positions of the samples to + * return, one output row each; empty for every sample. + * @return A samples x length(alleles) matrix; NA for a sample flagged missing + * or of ploidy 0. */ [[cpp11::register]] cpp11::writable::doubles_matrix<> bgenDosageCpp(cpp11::raws data, int nSamples, - cpp11::integers alleles) { + cpp11::integers alleles, + cpp11::integers samples) { const size_t size = data.size(); const unsigned char* p = RAW(static_cast(data)); if (size < 10) cpp11::stop("BGEN probability data is truncated."); @@ -137,16 +167,23 @@ cpp11::writable::doubles_matrix<> bgenDosageCpp(cpp11::raws data, int nSamples, if (alleles[a] < 1 || alleles[a] > K) cpp11::stop("Allele %d is not one of the variant's %d alleles.", alleles[a], K); } + const int nAsked = samples.size(); + const int nRows = (nAsked > 0) ? nAsked : nSamples; + for (int s = 0; s < nAsked; ++s) { + if (samples[s] < 1 || samples[s] > nSamples || (s > 0 && samples[s] <= samples[s - 1])) + cpp11::stop("Samples must be ascending positions among the file's %d samples.", nSamples); + } // Each ploidy's genotypes, enumerated when a sample of it is first met. // The data cannot hold more probabilities than it has bits, which bounds // the enumeration of a record that is not what it claims to be. std::vector>> countsOf(64); const double cap = 8.0 * static_cast(size) / bits + 1.0; - cpp11::writable::doubles_matrix<> out(nSamples, nOut); + cpp11::writable::doubles_matrix<> out(nRows, nOut); BitReader reader(p + 10 + N, size - 10 - N, bits); std::vector stored; - for (int i = 0; i < nSamples; ++i) { + int row = 0; + for (int i = 0; i < nSamples && row < nRows; ++i) { int ploidy = ploidyBytes[i] & 63; bool missing = (ploidyBytes[i] & 128) != 0; if (!phased && ploidy > 0 && countsOf[ploidy].empty()) { @@ -158,10 +195,15 @@ cpp11::writable::doubles_matrix<> bgenDosageCpp(cpp11::raws data, int nSamples, if (ploidy > 0) { nStored = phased ? ploidy * (K - 1) : static_cast(countsOf[ploidy].size()) - 1; } + if (nAsked > 0 && samples[row] != i + 1) { + reader.skip(static_cast(nStored)); + continue; + } stored.assign(nStored, 0.0); for (int s = 0; s < nStored; ++s) stored[s] = reader.next(); if (missing || ploidy == 0) { - for (int a = 0; a < nOut; ++a) out(i, a) = NA_REAL; + for (int a = 0; a < nOut; ++a) out(row, a) = NA_REAL; + ++row; continue; } for (int a = 0; a < nOut; ++a) { @@ -189,8 +231,9 @@ cpp11::writable::doubles_matrix<> bgenDosageCpp(cpp11::raws data, int nSamples, } expected += g[nStored][allele] * last; } - out(i, a) = expected; + out(row, a) = expected; } + ++row; } return out; } diff --git a/src/cpp11.cpp b/src/cpp11.cpp index e516273b8..857465443 100644 --- a/src/cpp11.cpp +++ b/src/cpp11.cpp @@ -6,10 +6,10 @@ #include // bgen.cpp -cpp11::writable::doubles_matrix<> bgenDosageCpp(cpp11::raws data, int nSamples, cpp11::integers alleles); -extern "C" SEXP _pecotmr_bgenDosageCpp(SEXP data, SEXP nSamples, SEXP alleles) { +cpp11::writable::doubles_matrix<> bgenDosageCpp(cpp11::raws data, int nSamples, cpp11::integers alleles, cpp11::integers samples); +extern "C" SEXP _pecotmr_bgenDosageCpp(SEXP data, SEXP nSamples, SEXP alleles, SEXP samples) { BEGIN_CPP11 - return cpp11::as_sexp(bgenDosageCpp(cpp11::as_cpp>(data), cpp11::as_cpp>(nSamples), cpp11::as_cpp>(alleles))); + return cpp11::as_sexp(bgenDosageCpp(cpp11::as_cpp>(data), cpp11::as_cpp>(nSamples), cpp11::as_cpp>(alleles), cpp11::as_cpp>(samples))); END_CPP11 } // bgen.cpp @@ -114,7 +114,7 @@ extern "C" SEXP _pecotmr_vcfGtDosageCpp(SEXP gt, SEXP alleles) { extern "C" { static const R_CallMethodDef CallEntries[] = { {"_pecotmr_bayesAlphabetFactorCpp", (DL_FUNC) &_pecotmr_bayesAlphabetFactorCpp, 17}, - {"_pecotmr_bgenDosageCpp", (DL_FUNC) &_pecotmr_bgenDosageCpp, 3}, + {"_pecotmr_bgenDosageCpp", (DL_FUNC) &_pecotmr_bgenDosageCpp, 4}, {"_pecotmr_bgenSampleIdsCpp", (DL_FUNC) &_pecotmr_bgenSampleIdsCpp, 2}, {"_pecotmr_dentistIterativeImpute", (DL_FUNC) &_pecotmr_dentistIterativeImpute, 12}, {"_pecotmr_dentistIterativeImputeFactor", (DL_FUNC) &_pecotmr_dentistIterativeImputeFactor, 12}, diff --git a/tests/testthat/helper-cfgForwarding.R b/tests/testthat/helper-cfgForwarding.R index 060fc0ded..23cab8683 100644 --- a/tests/testthat/helper-cfgForwarding.R +++ b/tests/testthat/helper-cfgForwarding.R @@ -80,7 +80,7 @@ .captureJointEngine <- function(cap, env = parent.frame()) { cap$calls <- list() testthat::local_mocked_bindings( - .runJointSpecs = function( + .jointSpecsWork = function( parsedJointSpec, data, dataForm, @@ -97,7 +97,7 @@ args = args, jointMethods = jointMethods ) - NULL + pecotmr:::.qtlWorkValue(NULL) }, .package = "pecotmr", .env = env diff --git a/tests/testthat/helper-fitSamples.R b/tests/testthat/helper-fitSamples.R new file mode 100644 index 000000000..219980798 --- /dev/null +++ b/tests/testthat/helper-fitSamples.R @@ -0,0 +1,117 @@ +# Real PLINK2 genotypes under a dataset whose contexts observe only 30 of the +# panel's 100 samples, and one of whose traits (g2) is missing a value for five +# of those: what a fit's samples are differs from the panel's and between +# traits. +.fitSamplesDataset <- function( + panel = readGenotypes( + plink2Prefix = file.path(test_path("test_data"), "test_variants") + ) +) { + samples <- colnames(panel)[1:30] + traits <- c("g1", "g2", "g3") + set.seed(1) + y <- matrix( + rnorm(length(traits) * length(samples)), + nrow = length(traits), + dimnames = list(traits, samples) + ) + y["g2", 1:5] <- NA + at <- GenomicRanges::start(SummarizedExperiment::rowRanges(panel))[[100L]] + se <- SummarizedExperiment::SummarizedExperiment( + assays = list(expression = y), + rowRanges = `names<-`( + GenomicRanges::GRanges( + "chr21", + IRanges::IRanges(rep(at, length(traits)), width = 100L) + ), + traits + ), + colData = S4Vectors::DataFrame( + age = rnorm(length(samples)), + row.names = samples + ) + ) + QtlDataset( + studyName = "s", + genotypes = panel, + phenotypes = list(brain = se, liver = se) + ) +} + +# The same genotypes, their file naming every variant by an rsID. +.rsIdPanel <- function() { + stem <- file.path(test_path("test_data"), "test_variants") + dir <- tempfile("rsid_") + dir.create(dir) + out <- file.path(dir, "panel") + file.copy(str_c(stem, ".pgen"), str_c(out, ".pgen")) + file.copy(str_c(stem, ".psam"), str_c(out, ".psam")) + pvar <- utils::read.delim( + str_c(stem, ".pvar"), + check.names = FALSE, + colClasses = "character" + ) + utils::write.table( + mutate(pvar, ID = str_c("rs", row_number())), + str_c(out, ".pvar"), + sep = "\t", + quote = FALSE, + row.names = FALSE + ) + readGenotypes(plink2Prefix = out) +} + +# Two individual-level studies of the same genotypes and phenotypes. +.fitSamplesTwoStudies <- function() { + first <- .fitSamplesDataset() + second <- QtlDataset( + studyName = "s2", + genotypes = pecotmr:::.qtlGenotypeSe(first), + phenotypes = pecotmr:::.qtlPhenotypeList(first) + ) + MultiStudyQtlDataset(qtlDatasets = list(s = first, s2 = second)) +} + +# The window spanning the fixture's variants. +.fitSamplesWindow <- function() { + GenomicRanges::GRanges("chr21:17513228-17592874") +} + +# Runs `expr` counting the dosage reads and the residualized X blocks it builds; +# `samples` holds what each read asked for (NULL: every sample of the panel). +.countBlocks <- function(expr) { + reads <- list() + builds <- 0L + realRead <- pecotmr:::.handleDosage + realBuild <- pecotmr:::.qtlResidualizedGenotypes + testthat::local_mocked_bindings( + .handleDosage = function(handle, snpIdx, sampleIdx = NULL) { + reads <<- c(reads, list(sampleIdx)) + realRead(handle, snpIdx, sampleIdx) + }, + .qtlResidualizedGenotypes = function(...) { + builds <<- builds + 1L + realBuild(...) + }, + .package = "pecotmr" + ) + result <- suppressMessages(expr) + list( + result = result, + reads = length(reads), + samples = reads, + builds = builds + ) +} + +# X as a fit on `samples` should have it: on those samples, each column scaled +# to unit variance on them, and the variants that pass the filter on them. +.expectFitOn <- function(X, samples, expected) { + testthat::expect_setequal(rownames(X), samples) + testthat::expect_equal( + unname(apply(X, 2L, stats::sd)), + rep(1, ncol(X)), + tolerance = 1e-8 + ) + testthat::expect_equal(colnames(X), colnames(expected)) +} diff --git a/tests/testthat/helper-h2Classes.R b/tests/testthat/helper-h2Classes.R index 25557a5b6..728ffb1e3 100644 --- a/tests/testthat/helper-h2Classes.R +++ b/tests/testthat/helper-h2Classes.R @@ -151,7 +151,8 @@ makeTestH2Estimate <- function(with_enrichment = TRUE) { p_value = NA_real_, n_snps = 100L, n_components = NA_integer_, - scale = NA_real_ + scale = NA_real_, + reference_overlap = NA_real_ ), list( enrichment = list(enrich), diff --git a/tests/testthat/helper-savedObjects.R b/tests/testthat/helper-savedObjects.R index 4bfc88a49..a13c63225 100644 --- a/tests/testthat/helper-savedObjects.R +++ b/tests/testthat/helper-savedObjects.R @@ -129,7 +129,7 @@ fits = S4Vectors::SimpleList(map(entries, methodFits)), cvResult = S4Vectors::SimpleList(map(entries, cvResult)), standardized = map_lgl(entries, weightStandardized), - dataType = S4Vectors::SimpleList(map(entries, weightsDataType)), + dataType = S4Vectors::SimpleList(map(entries, dataType)), ldReference = rep(NA_character_, nrow(cells)) ), extra, diff --git a/tests/testthat/test_AllClasses.R b/tests/testthat/test_AllClasses.R index 1ed9696ab..4848b1f9e 100644 --- a/tests/testthat/test_AllClasses.R +++ b/tests/testthat/test_AllClasses.R @@ -1144,6 +1144,16 @@ test_that("adjustPips drops non-overlapping entries and says so", { expect_equal(as.character(mcols(out)$study), "s1") }) +test_that("adjustPips finds an entry listed in both allele orders", { + # A one-to-one match refuses both orders, which reads as disjoint sets. + bothOrders <- c(.rc_vids(1:5), sprintf("chr1:%d:G:A", 100L * 1:5)) + expect_message( + out <- adjustPips(.ac_twoEntry(), bothOrders), + "dropping 1 of 2 entries" + ) + expect_equal(as.character(mcols(out)$study), "s1") +}) + test_that("adjustPips keeps the names its rows had", { # As every rebuild does: the result used to come back unnamed. Names a # caller set stay too. @@ -1365,16 +1375,16 @@ test_that("a flip complements AF but leaves MAF and N alone", { # An allele swap sends af -> 1 - af. maf is min(af, 1 - af) and the # sample size is a count, so both are swap-invariant -- complementing # them would corrupt them. - filledAf <- af(fillFromReference(x, swapped, "AF")) + filled <- function(column) { + expect_warning( + fillFromReference(x, swapped, column), + "16 of 200 selected variant\\(s\\) are not in `reference`" + ) + } + filledAf <- af(filled("AF")) expect_equal(unique(filledAf[!ambiguous]), 0.7) - expect_equal( - unique(maf(fillFromReference(x, swapped, "MAF"))[!ambiguous]), - 0.3 - ) - expect_equal( - unique(sampleSize(fillFromReference(x, swapped, "N"))[!ambiguous]), - 0.3 - ) + expect_equal(unique(maf(filled("MAF"))[!ambiguous]), 0.3) + expect_equal(unique(sampleSize(filled("N"))[!ambiguous]), 0.3) # A swapped A/T or C/G variant is a swap or a strand flip and nothing says # which, so it is not filled. expect_true(any(ambiguous)) @@ -1765,6 +1775,20 @@ test_that("fsusieCredibleBand, fsusieAffectedRegions aggregate a collection", { expect_setequal(unique(S4Vectors::mcols(gr)$context), c("brain", "blood")) }) +test_that("fsusieAffectedRegions combines rows on several chromosomes", { + data(mvsusieFineMappingExample) + two <- mvsusieFineMappingExample + expect_identical(nrow(two), 2L) + local_mocked_bindings( + .fsusieEntryAffectedRegions = function(i, x) { + GenomicRanges::GRanges(str_c("chr", i, ":1-9")) + }, + .package = "pecotmr" + ) + expect_no_warning(gr <- suppressMessages(fsusieAffectedRegions(two))) + expect_equal(as.character(gr), c("chr1:1-9", "chr2:1-9")) +}) + test_that("fsusieAffectedRegions of non-fSuSiE entries is an empty GRanges", { e <- FineMappingRow( variantIds = "chr1:100:A:G", diff --git a/tests/testthat/test_ColocBoostResult.R b/tests/testthat/test_ColocBoostResult.R index cdb4afc2a..a706178bb 100644 --- a/tests/testthat/test_ColocBoostResult.R +++ b/tests/testthat/test_ColocBoostResult.R @@ -5,6 +5,24 @@ # pair, and it computes no PP.H0-PP.H4 decomposition at all. Several tests # below guard exactly that boundary. +test_that(".cbrTopVariable reads a set's top variable, tied or not", { + one <- data.frame( + top_variables = c("chr1:1:A:G", "chr1:2:A:G"), + row.names = c("cos1:y1_y2", "cos2:y1_y2") + ) + expect_equal(pecotmr:::.cbrTopVariable(one, "cos2:y1_y2"), "chr1:2:A:G") + # Variables tied for the top get a row each; a longer id is another set, + # even listed first. + tied <- data.frame( + top_variables = c("chr1:7:A:G", "chr1:5:A:G", "chr1:6:A:G"), + row.names = c("cos1:y1_y2_y3", "cos1:y1_y2.1", "cos1:y1_y2.2") + ) + expect_equal(pecotmr:::.cbrTopVariable(tied, "cos1:y1_y2"), "chr1:5:A:G") + expect_equal(pecotmr:::.cbrTopVariable(tied, "cos1:y1_y2_y3"), "chr1:7:A:G") + expect_true(is.na(pecotmr:::.cbrTopVariable(tied, "cos9:y1"))) + expect_true(is.na(pecotmr:::.cbrTopVariable(NULL, "cos1:y1"))) +}) + test_that("ColocBoostResult: one element per confidence set", { x <- ColocBoostResult( list(.cbr_fake()), diff --git a/tests/testthat/test_CtwasResult.R b/tests/testthat/test_CtwasResult.R index 23e5bcc39..7f85ef823 100644 --- a/tests/testthat/test_CtwasResult.R +++ b/tests/testthat/test_CtwasResult.R @@ -387,9 +387,17 @@ test_that("ctwasGroupPriors() tabulates each run's priors once, by group", { "context", "method", "group", + "group_study", + "group_context", + "group_type", "group_size", "group_prior", - "group_prior_var" + "group_prior_var", + "log_enrichment", + "log_enrichment_se", + "enrichment_p", + "group_pve", + "prop_heritability" ) ) # The liver run recorded no priors, so it adds no rows. @@ -398,6 +406,9 @@ test_that("ctwasGroupPriors() tabulates each run's priors once, by group", { expect_equal(tab$group_size, c(5000, 30)) expect_equal(tab$group_prior, c(1e-4, 0.01)) expect_equal(tab$group_prior_var, c(10, 20)) + # A hand-built run recorded neither the groups' identities nor a summary. + expect_true(all(is.na(tab$group_study))) + expect_true(all(is.na(tab$group_pve))) # No run with priors: no rows, the same columns. expect_equal(ctwasGroupPriors(cr[2L]), tab[0L, ]) # A run over two regions is listed once; what it did not record is NA. diff --git a/tests/testthat/test_GenotypeDosageSeed.R b/tests/testthat/test_GenotypeDosageSeed.R index fe7687aee..a247aaf2b 100644 --- a/tests/testthat/test_GenotypeDosageSeed.R +++ b/tests/testthat/test_GenotypeDosageSeed.R @@ -238,6 +238,50 @@ test_that("a SeqArray GDS seed reads FORMAT/DS per ALT allele", { expect_equal(.dose(panel), .vcfOverlapDs(), tolerance = 1e-6) }) +test_that("a SeqArray GDS reads some samples as the whole panel has them", { + skip_if_not_installed("SeqArray") + skip_if_not_installed("GDSArray") + # Both of the nodes a SeqArray file holds its dosages in, read in any + # order and repeated, as DelayedArray may ask. + fromGt <- suppressWarnings(readGenotypes(.writeTestSeqArray( + .writeTestVcf(.vcfGtOnly(.vcfOverlapRecords()), samples3) + ))) + fromDs <- suppressWarnings(readGenotypes(.writeTestSeqArray( + .writeTestVcf( + .vcfOverlapRecords(), + samples3, + format = c("GT", "DS"), + ds = "A" + ), + ds = TRUE + ))) + expect_equal(pecotmr:::.gdSeedField(.seedOf(fromGt)), "seqArrayGt") + expect_equal(pecotmr:::.gdSeedField(.seedOf(fromDs)), "seqArrayDs") + rows <- c(4L, 1L, 2L) + picked <- c(3L, 1L, 1L) + for (read in list( + list(panel = fromGt, want = .vcfOverlapGt()), + list(panel = fromDs, want = .vcfOverlapDs()) + )) { + dosage <- SummarizedExperiment::assay(read$panel, "dosage") + expect_equal( + unname(as.matrix(dosage[rows, picked])), + read$want[rows, picked], + tolerance = 1e-6 + ) + expect_equal( + unname(pecotmr:::.ldRefDosage( + read$panel, + rows, + meanImpute = FALSE, + sampleIdx = c(1L, 3L) + )), + t(read$want[rows, c(1L, 3L)]), + tolerance = 1e-6 + ) + } +}) + test_that("a SeqArray DS row with no value of its own is missing", { skip_if_not_installed("SeqArray") skip_if_not_installed("GDSArray") diff --git a/tests/testthat/test_H2Estimate.R b/tests/testthat/test_H2Estimate.R index 8ebd86097..8d0a6e93d 100644 --- a/tests/testthat/test_H2Estimate.R +++ b/tests/testthat/test_H2Estimate.R @@ -15,6 +15,7 @@ n_snps = 100L, n_components = NA_integer_, scale = NA_real_, + reference_overlap = NA_real_, enrichment = list(NULL), jackknife = list(NULL), score_stats = list(NULL) @@ -159,6 +160,24 @@ test_that("updateObject() rebuilds an H2Estimate saved in slots as a row", { expect_identical(updateObject(older), up) }) +test_that("updateObject() gives an estimate saved without an overlap NA", { + current <- .h2Est() + saved <- S4Vectors::`mcols<-`( + current, + value = S4Vectors::mcols(current, use.names = FALSE)[ + setdiff( + colnames(S4Vectors::mcols(current)), + "reference_overlap" + ) + ] + ) + expect_error(validObject(saved), "reference_overlap") + up <- updateObject(saved) + expect_true(validObject(up)) + expect_true(is.na(S4Vectors::mcols(up)$reference_overlap)) + expect_identical(updateObject(current), current) +}) + test_that("updateObject() drops the per-block local estimates", { saved <- .h2Saved()$local expect_warning( diff --git a/tests/testthat/test_MethodOptions.R b/tests/testthat/test_MethodOptions.R index 5c6f3b80d..6414b8ce6 100644 --- a/tests/testthat/test_MethodOptions.R +++ b/tests/testthat/test_MethodOptions.R @@ -377,7 +377,11 @@ test_that("a constructor is unchecked only when its engine takes dots", { next } out <- paste(capture.output(show(rec)), collapse = " ") - if (grepl("NOT checked", out)) unchecked <- c(unchecked, n) + # An absent package is a third, legitimate reason a record cannot be + # checked, whichever engine it is for. + if (grepl("NOT checked", out) && !grepl("is not installed", out)) { + unchecked <- c(unchecked, n) + } } # Each of these reaches an engine whose formals include `...`; where a # constructor serves several engines (poolr's three tests), one of them @@ -394,8 +398,6 @@ test_that("a constructor is unchecked only when its engine takes dots", { QvalueOptions = "qvalue::qvalue", RmaOptions = "metafor::rma" ) - # Only count the ones whose package is actually installed: an absent - # package is a third, legitimate reason a record cannot be checked. installed <- keep(dotEngines, .maPkgAvailable) expect_setequal(intersect(unchecked, names(dotEngines)), names(installed)) extra <- setdiff(unchecked, names(dotEngines)) diff --git a/tests/testthat/test_QtlDataset.R b/tests/testthat/test_QtlDataset.R index beb84d91c..4e69fe934 100644 --- a/tests/testthat/test_QtlDataset.R +++ b/tests/testthat/test_QtlDataset.R @@ -687,7 +687,7 @@ test_that(".qtlBuildResidualizationDesign intersects the blocks' samples", { # requested snpIdx, drawing dosages from a per-handle-deterministic seed so # the same indices always give the same numbers. .qh_mockExtractor <- function(seed = 42, n_samples = 12L, n_snp = 6L) { - function(handle, snpIdx) { + function(handle, snpIdx, sampleIdx = NULL) { set.seed(seed) # Build a (n_samples x n_snp) dosage matrix for the whole panel; the # caller's snpIdx subsets columns. @@ -696,7 +696,7 @@ test_that(".qtlBuildResidualizationDesign intersects the blocks' samples", { nrow = n_samples, ncol = n_snp ) - panel[, snpIdx, drop = FALSE] + panel[sampleIdx %||% seq_len(nrow(panel)), snpIdx, drop = FALSE] } } @@ -727,7 +727,8 @@ test_that(".qtlExtractBlock gives the dosages of kept variants and samples", { blk <- pecotmr:::.qtlExtractBlock(qd) expect_equal(nrow(blk$geno), 12L) expect_equal(ncol(blk$geno), 6L) - expect_equal(blk$variantIds, paste0("rs", 1:6)) + # Named by range and alleles, whatever the file calls them (rs1 to rs6). + expect_equal(blk$variantIds, str_c("chr1:", 1:6 * 100L, ":G:A")) expect_equal(blk$sampleIds, paste0("s", 1:12)) expect_equal(length(blk$maf), 6L) }) @@ -750,50 +751,467 @@ test_that(".qtlExtractBlock: keepVariants narrows the variants returned", { qd, filter = .qh_filter(keepVariants = c("rs2", "rs4")) ) - expect_setequal(blk$variantIds, c("rs2", "rs4")) + expect_setequal(blk$variantIds, c("chr1:200:G:A", "chr1:400:G:A")) }) -test_that(".qtlExtractBlock: keepSamples narrows the samples returned", { - qd <- .qh_makeDataset() - qd <- pecotmr:::.qtlRestrictSamples(qd, paste0("s", 1:6)) +test_that(".qtlExtractBlock: keepVariants match as a keep-list file does", { + # rs1 to rs6 sit at chr1:100 to chr1:600 with alleles G/A; rs3 is T/A. + qd <- .qh_makeDataset(a2 = replace(rep("G", 6L), 3L, "T")) local_mocked_bindings( .handleDosage = .qh_mockExtractor(), .package = "pecotmr" ) - blk <- pecotmr:::.qtlExtractBlock(qd) - expect_setequal(blk$sampleIds, paste0("s", 1:6)) + keptIds <- function(...) { + filter <- .qh_filter(keepVariants = c(...)) + pecotmr:::.qtlExtractBlock(qd, filter = filter)$variantIds + } + # An rsID-labelled panel is found by position and alleles, in either order. + expect_equal(keptIds("chr1:100:G:A"), "chr1:100:G:A") + expect_equal(keptIds("chr1:100:A:G"), "chr1:100:G:A") + # A palindrome spelled the other way is the same variant. + expect_equal(keptIds("chr1:300:A:T"), "chr1:300:T:A") + # A variant listed in both allele orders is no conflict. + expect_equal(keptIds("chr1:100:G:A", "chr1:100:A:G"), "chr1:100:G:A") + # Ids and rsIDs together, each matched by what it carries. + expect_equal( + keptIds("rs2", "chr1:400:G:A"), + c("chr1:200:G:A", "chr1:400:G:A") + ) + expect_length(keptIds("chr1:100:G:C"), 0L) }) -test_that(".qtlExtractBlock: a call's samples argument narrows them further", { +test_that(".qtlExtractBlock filters a held read as it would its own", { qd <- .qh_makeDataset() - qd <- pecotmr:::.qtlRestrictSamples(qd, paste0("s", 1:6)) local_mocked_bindings( .handleDosage = .qh_mockExtractor(), .package = "pecotmr" ) - blk <- pecotmr:::.qtlExtractBlock(qd, samples = c("s1", "s3", "s5")) - expect_setequal(blk$sampleIds, c("s1", "s3", "s5")) + filter <- .qh_filter(mafCutoff = 0.01, keepVariants = str_c("rs", 1:5)) + window <- GenomicRanges::GRanges("chr1:100-400") + held <- pecotmr:::.qtlReadBlock(qd, window, NULL, filter) + for (samples in list(NULL, str_c("s", 1:6))) { + expect_identical( + pecotmr:::.qtlExtractBlock(qd, window, samples, filter, held), + pecotmr:::.qtlExtractBlock(qd, window, samples, filter) + ) + } + # A window inside the one read, and none of it at all. + for (inside in c("chr1:200-300", "chr1:1-10", "chr1:600-700")) { + narrower <- GenomicRanges::GRanges(inside) + expect_identical( + pecotmr:::.qtlExtractBlock(qd, narrower, NULL, filter, held), + pecotmr:::.qtlExtractBlock(qd, narrower, NULL, filter) + ) + } }) -test_that(".qtlExtractBlock: keepVariants sharing none gives an empty block", { +test_that(".qtlExtractBlock refuses a held read missing samples it wants", { qd <- .qh_makeDataset() local_mocked_bindings( .handleDosage = .qh_mockExtractor(), .package = "pecotmr" ) - blk <- pecotmr:::.qtlExtractBlock( - qd, - filter = .qh_filter(keepVariants = "rsGHOST") + filter <- .qh_filter() + held <- pecotmr:::.qtlReadBlock(qd, NULL, str_c("s", 1:3), filter) + expect_error( + pecotmr:::.qtlExtractBlock(qd, NULL, str_c("s", 1:6), filter, held), + "does not cover" ) - expect_equal(ncol(blk$geno), 0L) - expect_equal(nrow(blk$geno), 0L) + # A sample the panel does not hold is simply not there to be read. + expect_equal( + pecotmr:::.qtlExtractBlock( + qd, + NULL, + c("s1", "nobody"), + filter, + held + )$sampleIds, + "s1" + ) +}) + +test_that(".qtlReadBlock reads what is asked for, or nothing", { + qd <- .qh_makeDataset() + seen <- list() + mock <- .qh_mockExtractor() + local_mocked_bindings( + .handleDosage = function(handle, snpIdx, sampleIdx = NULL) { + seen <<- c(seen, list(list(snpIdx, sampleIdx))) + mock(handle, snpIdx, sampleIdx) + }, + .package = "pecotmr" + ) + filter <- .qh_filter(keepVariants = c("rs2", "rs4")) + held <- pecotmr:::.qtlReadBlock(qd, NULL, c("s5", "s2"), filter) + expect_equal(rownames(held$dosage), c("s2", "s5")) + expect_equal(colnames(held$dosage), c("chr1:200:G:A", "chr1:400:G:A")) + expect_equal(seen[[1L]], list(c(2L, 4L), c(2L, 5L))) + # Every sample: no restriction goes to the reader. + pecotmr:::.qtlReadBlock(qd, NULL, NULL, filter) + expect_null(seen[[2L]][[2L]]) + # A dataset narrowed to some samples reads only those. + narrowed <- pecotmr:::.qtlRestrictSamples(qd, str_c("s", 1:6)) + pecotmr:::.qtlReadBlock(narrowed, NULL, NULL, filter) + expect_equal(seen[[3L]][[2L]], 1:6) + # No variant survives, or the window holds none: nothing is read. + empty <- list(snpIdx = integer(0), dosage = NULL) + expect_identical( + pecotmr:::.qtlReadBlock(qd, NULL, NULL, .qh_filter(keepVariants = "x")), + empty + ) + expect_identical( + pecotmr:::.qtlReadBlock( + qd, + GenomicRanges::GRanges("chr9:1-2"), + NULL, + filter + ), + empty + ) + expect_length(seen, 3L) +}) + +test_that(".qtlWindowGroups puts a window with any window it holds", { + gr <- GenomicRanges::GRanges + windows <- list( + gr("chr1:10-20"), # inside the next + gr("chr1:1-100"), + gr("chr2:1-100"), + gr("chr1:10-20"), # the first again + gr("chr1:50-150"), # overlaps the second, but is not inside it + NULL # no window: may read nothing, so it holds nothing + ) + expect_equal( + pecotmr:::.qtlWindowGroups(windows), + list(c(1L, 2L, 4L), 3L, 5L, 6L) + ) + # The widest of a group is what is read, wherever it falls among its items. + expect_identical( + pecotmr:::.qtlWidestWindow(windows[c(1L, 2L, 4L)]), + windows[[2L]] + ) + expect_null(pecotmr:::.qtlWidestWindow(windows[6L])) + # A window of several ranges is held only if each of them is. + wide <- gr("chr1:1-100") + within <- gr(c("chr1:1-10", "chr1:50-60")) + across <- gr(c("chr1:1-10", "chr2:1-10")) + expect_equal( + pecotmr:::.qtlWindowGroups(list(within, wide)), + list(c(1L, 2L)) + ) + expect_equal( + pecotmr:::.qtlWindowGroups(list(across, wide)), + list(1L, 2L) + ) + # Two ranges of one window held by different ranges of another. + twoParts <- gr(c("chr1:1-100", "chr2:1-100")) + expect_equal( + pecotmr:::.qtlWindowGroups(list(across, twoParts)), + list(c(1L, 2L)) + ) + # Nothing to compare, and one window alone. + expect_equal(pecotmr:::.qtlWindowGroups(list()), list()) + expect_equal(pecotmr:::.qtlWindowGroups(list(NULL, wide)), list(1L, 2L)) +}) + +# Plans over mocked reads and blocks: what was read and built, in order. +.qh_workMocks <- function(env = parent.frame()) { + log <- new.env(parent = emptyenv()) + log$reads <- list() + log$builds <- list() + testthat::local_mocked_bindings( + .qtlReadBlock = function(x, window, samples, filter) { + log$reads <- c(log$reads, list(list(as.character(window), samples))) + "read" + }, + .fmResidGeno = function(x, ..., contexts, region, samples, read) { + force(read) + log$builds <- c( + log$builds, + list(list(contexts, as.character(region), samples)) + ) + matrix( + 1, + nrow = 2L, + ncol = 2L, + dimnames = list(c("s1", "s2"), c("v1", "v2")) + ) + }, + .package = "pecotmr", + .env = env + ) + log +} + +test_that("windows and traits on several chromosomes combine in silence", { + gr <- GenomicRanges::GRanges + expect_no_warning( + groups <- pecotmr:::.qtlWindowGroups(list( + gr("chr1:1-9"), + gr("chr2:1-9") + )) + ) + expect_equal(groups, list(1L, 2L)) + # Each context holds a chromosome the other does not. + onChromosomes <- function(traits, chroms) { + se <- .qh_makeSe(traits = traits) + SummarizedExperiment::rowRanges(se) <- `names<-`( + gr(chroms, IRanges::IRanges(1000L, width = 500L)), + traits + ) + se + } + qd <- QtlDataset( + studyName = "study1", + genotypes = .qh_makeHandle(n_samples = 12L), + phenotypes = list( + brain = onChromosomes(c("a", "b"), c("chr1", "chr2")), + liver = onChromosomes(c("a", "c"), c("chr1", "chr3")) + ), + genotypeCovariates = matrix(numeric(0), nrow = 0, ncol = 0) + ) + expect_no_warning(span <- pecotmr:::.qtlTraitRegion(qd, "a", 100L)) + expect_equal(as.character(span), "chr1:900-1599") +}) + +test_that(".qtlRunWork runs every plan's fits on one read and one block", { + log <- .qh_workMocks() + wide <- GenomicRanges::GRanges("chr1:1-100") + other <- GenomicRanges::GRanges("chr2:1-100") + spec <- pecotmr:::.qtlFitSpec + fit <- function(item, tag, block) str_c(tag, item, ncol(block$X)) + labelled <- function(fits, label) list(label, fits) + univariate <- pecotmr:::.qtlWork( + list("a", "b", "c"), + list( + spec(wide, "brain", "s1"), + spec(other, "brain", "s3"), + spec(wide, "liver", "s1") + ), + fit, + list(tag = "u"), + labelled, + list(label = "univariate") + ) + pcs <- pecotmr:::.qtlWork( + list("d"), + list(spec(wide, "brain", "s1")), + fit, + list(tag = "p") + ) + out <- pecotmr:::.qtlRunWork( + pecotmr:::.qtlWorkOf( + list( + univariate, + pecotmr:::.qtlWorkValue("none"), + pecotmr:::.qtlWorkOf(list(pcs), rev) + ) + ), + NULL, + NULL + ) + # Each plan's fits came back to it, in its order, and through its `finish`. + expect_equal(out[[1L]], list("univariate", list("ua2", "ub2", "uc2"))) + expect_equal(out[[2L]], "none") + expect_equal(out[[3L]], list(list("pd2"))) + # One read per window; brain's block on chr1 served both plans; liver's has + # other covariates, so it is its own. + expect_length(log$reads, 2L) + expect_equal( + log$builds, + list( + list("brain", "chr1:1-100", "s1"), + list("liver", "chr1:1-100", "s1"), + list("brain", "chr2:1-100", "s3") + ) + ) + # Nothing planned, nothing read. + log$reads <- list() + expect_null(pecotmr:::.qtlRunWork( + pecotmr:::.qtlWorkValue(NULL), + NULL, + NULL + )) + expect_length(log$reads, 0L) }) -test_that(".qtlExtractBlock: keepIndel = FALSE drops indels", { - # rs2 an insertion, rs4 a deletion; the rest stay SNPs. +test_that(".qtlUnionSamples joins what fits ask for; no limit wins", { + expect_equal( + pecotmr:::.qtlUnionSamples(list(c("a", "b"), c("b", "c"))), + c("a", "b", "c") + ) + expect_null(pecotmr:::.qtlUnionSamples(list("a", NULL))) +}) + +test_that(".qtlRunWork reads nothing for an item with no genotypes to fit on", { + log <- .qh_workMocks() + seen <- list() + fit <- function(item, block) { + seen[[item]] <<- is.null(block) + item + } + out <- pecotmr:::.qtlRunWork( + pecotmr:::.qtlWork(list("g", "h"), list(NULL, NULL), fit), + NULL, + NULL + ) + expect_equal(out, list("g", "h")) + expect_true(seen$g) + expect_length(log$reads, 0L) + expect_length(log$builds, 0L) +}) + +test_that(".qtlRunWork builds a block once over the widest window it serves", { + log <- .qh_workMocks() + wide <- GenomicRanges::GRanges("chr1:1-100") + narrow <- GenomicRanges::GRanges("chr1:10-20") + views <- list() + local_mocked_bindings( + .qtlBlockFor = function(block, spec, dataset) { + views[[length(views) + 1L]] <<- as.character(spec$window) + block + }, + .package = "pecotmr" + ) + work <- pecotmr:::.qtlWork( + list("a", "b"), + list( + pecotmr:::.qtlFitSpec(narrow, "brain", "s1"), + pecotmr:::.qtlFitSpec(wide, "brain", "s1") + ), + function(item, block) item + ) + pecotmr:::.qtlRunWork(work, NULL, list(imissCutoff = 0)) + expect_length(log$reads, 1L) + expect_equal(log$builds, list(list("brain", "chr1:1-100", "s1"))) + expect_equal(views, list("chr1:10-20", "chr1:1-100")) + # A missingness filter looks across a block's variants, so a wider block + # could keep other samples: each window then gets its own, from one read. + log$reads <- list() + log$builds <- list() + pecotmr:::.qtlRunWork(work, NULL, list(imissCutoff = 0.1)) + expect_length(log$reads, 1L) + expect_equal( + log$builds, + list( + list("brain", "chr1:10-20", "s1"), + list("brain", "chr1:1-100", "s1") + ) + ) +}) + +test_that(".qtlFitBlock keeps allele frequencies if the run reports them", { + local_mocked_bindings( + .fmResidGeno = function(...) matrix(1, 2L, 2L), + .fmAfForX = function(...) c(0.1, 0.2), + .package = "pecotmr" + ) + spec <- pecotmr:::.qtlFitSpec(NULL, "brain", "s1") + run <- list(dataset = NULL, filter = NULL, residualizationParam = NULL) + expect_equal( + pecotmr:::.qtlFitBlock(spec, c(run, withAf = TRUE), NULL)$af, + c(0.1, 0.2) + ) + expect_null(pecotmr:::.qtlFitBlock(spec, c(run, withAf = FALSE), NULL)$af) + expect_null(pecotmr:::.qtlFitBlock(NULL, run, NULL)) +}) + +test_that(".qtlBlockFor cuts a block to the fit's own window", { + skip_if_not_installed("pgenlibr") + data <- .fitSamplesDataset() + wide <- .fitSamplesWindow() + narrow <- GenomicRanges::GRanges("chr21:17513228-17520000") + filter <- pecotmr:::.qtlResolveFilter(GenotypeFilterParam(mafCutoff = 0.05)) + run <- list( + dataset = data, + filter = filter, + residualizationParam = ResidualizationParam(), + withAf = TRUE + ) + samples <- colnames(data[["brain"]]) + spec <- pecotmr:::.qtlFitSpec + whole <- pecotmr:::.qtlFitBlock(spec(wide, "brain", samples), run, NULL) + own <- pecotmr:::.qtlFitBlock(spec(narrow, "brain", samples), run, NULL) + cut <- pecotmr:::.qtlBlockFor(whole, spec(narrow, "brain", samples), data) + # Exactly what a block built for the narrow window holds. + expect_lt(ncol(cut$X), ncol(whole$X)) + expect_identical(cut$X, own$X) + expect_identical(cut$af, own$af) + # Its own window: the same block, not a copy; no block stays none. + expect_identical( + pecotmr:::.qtlBlockFor(whole, spec(wide, "brain", samples), data), + whole + ) + expect_null(pecotmr:::.qtlBlockFor( + NULL, + spec(narrow, "brain", samples), + data + )) +}) + +test_that(".qtlRunWork lets go of the raw read and each block when done", { + # Finalizers on what a read and a block hold show what garbage collection + # can reach while a fit runs. + gone <- new.env(parent = emptyenv()) + gone$read <- FALSE + gone$brain <- FALSE + watched <- function(flag, value) { + holder <- new.env(parent = emptyenv()) + reg.finalizer(holder, function(e) gone[[flag]] <- TRUE) + attr(value, "watch") <- holder + value + } + local_mocked_bindings( + .qtlReadBlock = function(x, window, samples, filter) { + list(snpIdx = 1L, dosage = watched("read", matrix(1))) + }, + .fmResidGeno = function(x, ..., contexts, read) { + force(read) + X <- matrix(1, 2L, 1L, dimnames = list(c("s1", "s2"), "v1")) + if (identical(contexts, "brain")) watched("brain", X) else X + }, + .package = "pecotmr" + ) + seen <- list() + fit <- function(item, block) { + invisible(gc(FALSE)) + seen[[item]] <<- list(read = gone$read, brain = gone$brain) + item + } + window <- GenomicRanges::GRanges("chr1:1-100") + pecotmr:::.qtlRunWork( + pecotmr:::.qtlWork( + list("first", "second", "last"), + list( + pecotmr:::.qtlFitSpec(window, "brain", "s1"), + pecotmr:::.qtlFitSpec(window, "brain", "s1"), + pecotmr:::.qtlFitSpec(window, "liver", "s1") + ), + fit + ), + NULL, + NULL + ) + # A later block still needs the read while the earlier block's fits run, and + # that block is in use. + expect_false(seen$first$read) + expect_false(seen$second$brain) + # By the last block's fits both the read and the earlier block have gone. + expect_true(seen$last$read) + expect_true(seen$last$brain) +}) + +test_that(".qtlRowsOf copies only when the rows change", { + X <- matrix(1:4, 2L, dimnames = list(c("a", "b"), c("x", "y"))) + expect_identical(pecotmr:::.qtlRowsOf(X, c("a", "b")), X) + expect_identical(pecotmr:::.qtlRowsOf(X, "b"), X["b", , drop = FALSE]) +}) + +test_that(".qtlExtractBlock: keepIndel = FALSE keeps SNPs only", { + # One character each, but not a SNP: rs2 *, rs3 an I/D code, rs5 N. qd <- .qh_makeDataset( - a1 = replace(rep("A", 6L), 2L, "AT"), - a2 = replace(rep("G", 6L), 4L, "GC") + a1 = c("A", "*", "I", "A", "N", "A"), + a2 = c("G", "G", "D", "G", "G", "G") ) local_mocked_bindings( .handleDosage = .qh_mockExtractor(), @@ -803,7 +1221,7 @@ test_that(".qtlExtractBlock: keepIndel = FALSE drops indels", { qd, filter = .qh_filter(keepIndel = FALSE) ) - expect_equal(blk$variantIds, c("rs1", "rs3", "rs5", "rs6")) + expect_equal(parseVariantId(blk$variantIds)$pos, c(100L, 400L, 600L)) }) test_that(".qtlExtractBlock: keepIndel = TRUE (default) keeps indel variants", { @@ -850,7 +1268,7 @@ test_that(".qtlExtractBlock: mafCutoff retains variants above the threshold", { # Deterministic dosage panel so the directional (un-folded) effect-allele # frequency is exactly known: rs1 = 0.70, rs2 = 0.20, rs3 = 0.50, rest 0. .qh_directionalExtractor <- function() { - function(handle, snpIdx) { + function(handle, snpIdx, sampleIdx = NULL) { panel <- matrix( 0, nrow = length(sampleIds(handle)), @@ -865,7 +1283,7 @@ test_that(".qtlExtractBlock: mafCutoff retains variants above the threshold", { panel[, "rs1"] <- c(2, 2, 2, 2, 1, 1, 1, 1, 1, 1) # sum 14 -> p = 0.70 panel[, "rs2"] <- c(1, 1, 1, 1, 0, 0, 0, 0, 0, 0) # sum 4 -> p = 0.20 panel[, "rs3"] <- 1 # sum 10 -> p = 0.50 - panel[, snpIdx, drop = FALSE] + panel[sampleIdx %||% seq_len(nrow(panel)), snpIdx, drop = FALSE] } } @@ -992,14 +1410,14 @@ context("QtlDataset residualization methods") } .qr_mockExtractor <- function(seed = 42, n_samples = 12L, n_snp = 6L) { - function(handle, snpIdx) { + function(handle, snpIdx, sampleIdx = NULL) { set.seed(seed) panel <- matrix( rbinom(n_samples * n_snp, 2, 0.3), nrow = n_samples, ncol = n_snp ) - panel[, snpIdx, drop = FALSE] + panel[sampleIdx %||% seq_len(nrow(panel)), snpIdx, drop = FALSE] } } @@ -1124,6 +1542,19 @@ test_that("residualizedGenotypes: empty genotype block short-circuits to G", { expect_equal(ncol(G), 0L) }) +test_that("residualizedGenotypes keeps the panel's own labels", { + skip_if_not_installed("pgenlibr") + X <- residualizedGenotypes( + .fitSamplesDataset(.rsIdPanel()), + context = "brain" + ) + expect_true(all(startsWith(colnames(X), "rs"))) + expect_equal( + unname(X), + unname(residualizedGenotypes(.fitSamplesDataset(), context = "brain")) + ) +}) + test_that("residualizedGenotypes: produces residualized matrix shape", { qd <- .qr_makeDataset(contexts = "brain") local_mocked_bindings( @@ -1585,6 +2016,47 @@ test_that(".qtlOutlierKeepMask: returns all-TRUE when n < p + 2", { expect_true(all(keep)) }) +test_that(".qtlOutlierKeepMask judges only samples with every trait", { + set.seed(11L) + Y <- matrix( + rnorm(80), + ncol = 2L, + dimnames = list(paste0("s", 1:40), c("g1", "g2")) + ) + Y[1L, ] <- 50 + # Huge, but missing a trait: kept unjudged, and out of the estimate. + Y[2L, ] <- c(50, NA) + Y[3:5, "g2"] <- NA + complete <- stats::complete.cases(Y) + keep <- pecotmr:::.qtlOutlierKeepMask(Y, pvalThreshold = 1e-3) + expect_length(keep, 40L) + expect_false(keep[[1L]]) + expect_true(all(keep[!complete])) + expect_equal( + keep[complete], + pecotmr:::.qtlOutlierKeepMask(Y[complete, ], pvalThreshold = 1e-3) + ) + # With no sample complete, nothing is judged. + expect_equal( + pecotmr:::.qtlOutlierKeepMask( + rbind(c(1, NA), c(NA, 2)), + pvalThreshold = 1e-3 + ), + c(TRUE, TRUE) + ) +}) + +test_that(".qtlResiduals leaves a trait with no values missing throughout", { + X <- cbind(1, c(1, 2, 3, 4)) + Y <- cbind(a = c(1, 3, 2, 5), b = NA_real_) + out <- pecotmr:::.qtlResiduals(X, Y) + expect_equal( + unname(out[, 1L]), + unname(stats::lm.fit(X, Y[, "a"])$residuals) + ) + expect_true(all(is.na(out[, 2L]))) +}) + # =========================================================================== # QtlDataset construction and validity # =========================================================================== @@ -1907,7 +2379,10 @@ test_that("multi-range region spans chromosomes on a sharded handle", { gBoth <- .mr_geno(qd, rBoth) g21 <- .mr_geno(qd, r21) expect_equal(unname(gBoth[, seq_len(n21)]), unname(g21)) - expect_true(all(grepl("_c22$", colnames(gBoth)[(n21 + 1):(n21 + n22)]))) + expect_true(all(startsWith( + colnames(gBoth)[(n21 + 1):(n21 + n22)], + "chr22:" + ))) }) test_that("multi-region: single-range extraction is unchanged (regression)", { @@ -2036,7 +2511,7 @@ test_that(".qtlExtractBlock: keepSamples off the panel gives no samples", { # per-sample imiss filter), and s2/rs2 carries a single scattered NA that # survives the filter (exercising the mean-impute loop). .qh_naExtractor <- function() { - function(handle, snpIdx) { + function(handle, snpIdx, sampleIdx = NULL) { ns <- length(sampleIds(handle)) nv <- nrow(snpInfo(handle)) set.seed(123L) @@ -2050,7 +2525,7 @@ test_that(".qtlExtractBlock: keepSamples off the panel gives no samples", { ) panel["s1", ] <- NA_real_ # fully missing sample -> dropped by imiss panel["s2", "rs2"] <- NA_real_ # scattered NA -> kept then mean-imputed - panel[, snpIdx, drop = FALSE] + panel[sampleIdx %||% seq_len(nrow(panel)), snpIdx, drop = FALSE] } } @@ -2073,7 +2548,7 @@ test_that(".qtlExtractBlock: imissCutoff drops samples, imputes the rest", { # Extractor with one constant (zero-variance) column to drive the xvar filter. .qh_lowVarExtractor <- function() { - function(handle, snpIdx) { + function(handle, snpIdx, sampleIdx = NULL) { ns <- length(sampleIds(handle)) nv <- nrow(snpInfo(handle)) set.seed(99L) @@ -2086,7 +2561,7 @@ test_that(".qtlExtractBlock: imissCutoff drops samples, imputes the rest", { dimnames = list(sampleIds(handle), str_c("rs", seq_len(nv))) ) panel[, "rs1"] <- 1L # constant column -> variance 0 - panel[, snpIdx, drop = FALSE] + panel[sampleIdx %||% seq_len(nrow(panel)), snpIdx, drop = FALSE] } } @@ -3022,3 +3497,76 @@ test_that("a constant genotype column comes back as zeros, not NaN", { expect_equal(sd(out[, "a"]), 1) expect_false(anyNA(out)) }) + +# =========================================================================== +# The molecular type each context records +# =========================================================================== + +test_that("QtlDataset records each context's molecular type", { + build <- function(dataType) { + gh <- .qh_makeHandle(n_samples = 12L) + QtlDataset( + studyName = "study1", + genotypes = gh, + phenotypes = list( + brain = .qh_makeSe(n_samples = 12L), + liver = .qh_makeSe(n_samples = 12L) + ), + dataType = dataType + ) + } + # None recorded. + expect_equal( + dataType(.qh_makeDataset()), + c(brain = NA_character_, liver = NA_character_) + ) + expect_equal(dataType(build(NULL)), dataType(.qh_makeDataset())) + # One for all, by context, or for some. + expect_equal( + dataType(build("expression")), + c(brain = "expression", liver = "expression") + ) + expect_equal( + dataType(build(c(liver = "splicing", brain = "expression"))), + c(brain = "expression", liver = "splicing") + ) + expect_equal( + dataType(build(c(liver = "splicing"))), + c(brain = NA_character_, liver = "splicing") + ) + # What it records survives the subsetting that narrows a dataset. + typed <- build(c(brain = "expression", liver = "splicing")) + expect_equal(dataType(typed[, seq_len(6L), ]), dataType(typed)) + expect_equal( + dataType(suppressWarnings(typed[,, "liver"])), + c(liver = "splicing") + ) + # A context's own record stands unless the constructor is given another. + se <- S4Vectors::`metadata<-`( + .qh_makeSe(n_samples = 12L), + value = list(dataType = "methylation", other = 1) + ) + own <- QtlDataset( + studyName = "study1", + genotypes = .qh_makeHandle(n_samples = 12L), + phenotypes = list(brain = se) + ) + expect_equal(dataType(own), c(brain = "methylation")) + over <- QtlDataset( + studyName = "study1", + genotypes = .qh_makeHandle(n_samples = 12L), + phenotypes = list(brain = se), + dataType = "expression" + ) + expect_equal(dataType(over), c(brain = "expression")) + expect_equal( + S4Vectors::metadata(MultiAssayExperiment::experiments(over)[[ + "brain" + ]])$other, + 1 + ) + # Refused: not one per context, a context it does not have, not text. + expect_error(build(c("a", "b", "c")), "has 3 values for 2 context") + expect_error(build(c(lung = "a")), "names no such context: lung") + expect_error(build(1), "dataType") +}) diff --git a/tests/testthat/test_causalInferencePipeline.R b/tests/testthat/test_causalInferencePipeline.R index 0e341e001..1b98f7570 100644 --- a/tests/testthat/test_causalInferencePipeline.R +++ b/tests/testthat/test_causalInferencePipeline.R @@ -26,14 +26,14 @@ context("causalInferencePipeline") } .cip_mockExtractor <- function(seed = 7, n_samples = 30L) { - function(handle, snpIdx) { + function(handle, snpIdx, sampleIdx = NULL) { set.seed(seed) panel <- matrix( rbinom(n_samples * nrow(snpInfo(handle)), 2, 0.3), nrow = n_samples, ncol = nrow(snpInfo(handle)) ) - panel[, snpIdx, drop = FALSE] + panel[sampleIdx %||% seq_len(nrow(panel)), snpIdx, drop = FALSE] } } diff --git a/tests/testthat/test_colocPipeline.R b/tests/testthat/test_colocPipeline.R index 3d973459a..6cbd3b4cf 100644 --- a/tests/testthat/test_colocPipeline.R +++ b/tests/testthat/test_colocPipeline.R @@ -29,7 +29,8 @@ context("colocPipeline") .cp_makeFmEntry <- function( variant_ids = paste0("chr1:", 100 * (1:5), ":A:G"), withLbf = TRUE, - n_eff = 2L + n_eff = 2L, + lbf = NULL ) { pip <- seq(0.9, by = -0.15, length.out = length(variant_ids)) n <- length(variant_ids) @@ -61,12 +62,13 @@ context("colocPipeline") V = rep(0.05, n_eff) ) if (withLbf) { - fit$lbf_variable <- matrix( - rnorm(n_eff * length(variant_ids)), - nrow = n_eff, - ncol = length(variant_ids), - dimnames = list(NULL, variant_ids) - ) + fit$lbf_variable <- lbf %||% + matrix( + rnorm(n_eff * length(variant_ids)), + nrow = n_eff, + ncol = length(variant_ids), + dimnames = list(NULL, variant_ids) + ) } FineMappingRow(variantIds = variant_ids, susieFit = fit, topLoci = tl) } @@ -815,7 +817,6 @@ test_that("colocPipeline: enrichment takes table priors and reports RCP/LCP", { colocP12 = 2.5e-5, stringsAsFactors = FALSE ) - local_mocked_bindings(coloc.bf_bf = .cp_mockColocBfBf(), .package = "coloc") out <- suppressWarnings( colocPipeline( qtlFineMappingResult = qfmr, @@ -823,9 +824,7 @@ test_that("colocPipeline: enrichment takes table priors and reports RCP/LCP", { enrichment = enr ) ) - # Enrichment mode scores every (QTL effect, GWAS effect) pair itself - # rather than calling coloc.bf_bf, so the fixture's 2 x 2 effects give - # four rows; the mock used elsewhere collapses them to one. + # Every (QTL effect, GWAS effect) pair of the fixture's 2 x 2 effects. pairs <- colocPairs(out) expect_equal(nrow(pairs), 4L) expect_true(all(c("enrichment", "p12Used") %in% colnames(pairs))) @@ -847,6 +846,145 @@ test_that("colocPipeline: enrichment takes table priors and reports RCP/LCP", { expect_equal(probs, rep(1, nrow(pairs))) }) +test_that("colocPipeline: enrichment table priors reach coloc by trait", { + enr <- data.frame( + gwasStudy = "G1", + qtlStudy = "Q1", + qtlContext = "c1", + enrichment = 2.0, + colocP1 = 3e-4, + colocP2 = 7e-4, + colocP12 = 2.5e-5, + stringsAsFactors = FALSE + ) + got <- pecotmr:::.colocResolvePriors( + .cp_side("G1"), + .cp_side("Q1", "c1"), + TRUE, + enr, + ColocPriorParam() + ) + # colocP1 is fastenloc's GWAS-only prior and colocP2 its QTL-only one, + # while coloc's p1 goes with its first trait, the QTL side. + expect_equal(got$p1, 7e-4) + expect_equal(got$p2, 3e-4) + expect_equal(got$p12Used, 2.5e-5) +}) + +# The priors qtlEnrichmentPipeline derives, as fastenloc's set_enrich_params +# computes them, with both LBFs scored over the same five variants. +.cp_enlocPriors <- function(a0 = log(1e-4), a1 = 5, pQtl = 1e-3) { + list( + pi1e = stats::plogis(a0 + a1), + pi1ne = stats::plogis(a0), + pQtl = pQtl, + table = data.frame( + gwasStudy = "G1", + qtlStudy = "Q1", + qtlContext = "c1", + enrichment = expm1(a1), + colocP1 = (1 - pQtl) * stats::plogis(a0), + colocP2 = pQtl * (1 - stats::plogis(a0 + a1)), + colocP12 = pQtl * stats::plogis(a0 + a1) + ) + ) +} + +.cp_enlocRun <- function(qtlLbf, gwasLbf, pr = .cp_enlocPriors()) { + ids <- paste0("chr1:", 100 * (1:5), ":A:G") + entry <- function(lbf) { + .cp_makeFmEntry( + ids, + n_eff = 1L, + lbf = matrix(lbf, 1L, dimnames = list(NULL, ids)) + ) + } + suppressWarnings(colocPipeline( + qtlFineMappingResult = .cp_makeQtlFmr(entries = list(entry(qtlLbf))), + gwasInput = .cp_makeGwasFmr(entries = list(entry(gwasLbf))), + enrichment = pr$table + )) +} + +test_that("colocPipeline: enrichment mode reads each side's evidence", { + flat <- numeric(5) + atFirst <- c(30, 0, 0, 0, 0) + atSecond <- c(0, 30, 0, 0, 0) + pairs <- function(q, g) colocPairs(.cp_enlocRun(q, g)) + # No GWAS signal is not a colocalization, whatever the enrichment. + none <- pairs(flat, flat) + expect_gt(none$PP.H0.abf, 0.9) + expect_lt(none$LCP, 0.01) + qtlOnly <- pairs(atFirst, flat) + # coloc's H1 is its first trait alone, the QTL side. + expect_gt(qtlOnly$PP.H1.abf, 0.9) + expect_lt(qtlOnly$LCP, 0.1) + # Nor does a QTL effect without evidence count as certainly present. + gwasOnly <- pairs(flat, atFirst) + expect_gt(gwasOnly$PP.H2.abf, 0.5) + expect_lt(gwasOnly$LCP, 0.5) + expect_gt(pairs(atFirst, atFirst)$RCP, 0.99) + apart <- pairs(atFirst, atSecond) + expect_lt(apart$RCP, 0.01) + expect_gt(apart$LCP, 0.99) +}) + +test_that("colocPipeline: enrichment mode agrees with fastenloc's cases", { + pr <- .cp_enlocPriors() + # fastenloc's PIPs from Bayes factors (sigCluster::compute_pip) and its + # four configurations (controller.cc, compute_coloc_prob_exact). + fastenloc <- function(lq, lg) { + bfQ <- exp(lq) + bfG <- exp(lg) + pip <- pr$pQtl * bfQ / (pr$pQtl * sum(bfQ) + 1 - pr$pQtl) + rest <- 1 - sum(pip) + ne <- pr$pi1ne + e <- pr$pi1e + cases <- c( + (1 - ne)^5 * rest, + ne * (1 - ne)^4 * sum(bfG) * rest, + (1 - e) * (1 - ne)^4 * sum(pip), + ne * (1 - e) * (1 - ne)^3 * (sum(bfG) * sum(pip) - sum(bfG * pip)), + e * (1 - ne)^4 * sum(bfG * pip) + ) + cases / sum(cases) + } + lq <- c(4, 1, 0.5, 0, 0) + lg <- c(3, 2, 0, 0.2, 0) + got <- colocPairs(.cp_enlocRun(lq, lg)) + # Equal up to fastenloc's factor of 1 - p1 - p2 - p12 on H0 and on H3's + # inverse, here about 1e-3. fastenloc's second case is GWAS only, which + # is coloc's H2, its second trait alone. + expect_equal( + c( + got$PP.H0.abf, + got$PP.H2.abf, + got$PP.H1.abf, + got$PP.H3.abf, + got$PP.H4.abf + ), + fastenloc(lq, lg), + tolerance = 2e-3 + ) +}) + +test_that("colocPipeline: enrichment mode's variant layer matches coloc's", { + out <- .cp_enlocRun(c(6, 5, 0, 0, 0), c(6, 4, 0, 0, 0)) + pair <- colocPairs(out) + variants <- S4Vectors::mcols(colocVariants(out)) + # SNP.PP.H4 is a variant's share of H4, so colocPp sums to RCP rather than + # to its square, and a credible set can reach its coverage. + expect_equal(sum(variants$colocPp), pair$RCP) + expect_equal(sum(variants$SNP.PP.H4), 1) + expect_gt(length(colocCredibleSets(out, coverage = 0.95, minPp4 = 0)), 0L) +}) + +test_that("colocPipeline: enrichment mode scores a very strong signal", { + # 5000 on the natural-log scale overflows a plain Bayes factor. + out <- .cp_enlocRun(c(30, 0, 0, 0, 0), c(5000, 0, 0, 0, 0)) + expect_gt(colocPairs(out)$RCP, 0.99) +}) + test_that("colocPipeline: an enrichment table without the priors is refused", { qfmr <- .cp_makeQtlFmr() gfmr <- .cp_makeGwasFmr() @@ -947,7 +1085,7 @@ test_that("colocPipeline: an enrichment miss warns and drops to plain priors", { ) expect_match(w, "no enrichment entry", all = FALSE) pairs <- colocPairs(out) - expect_equal(nrow(pairs), 4L) + expect_equal(nrow(pairs), 1L) # A miss means there is no enrichment estimate for this pair, so it is # scored with the unconditional priors rather than a fabricated factor # of zero applied to a default. @@ -2003,26 +2141,6 @@ test_that(".colocWithNull adds coloc's null column at log BF 0", { ) }) -test_that("enloc reads SuSiE's natural-log Bayes factors as such", { - # With a flat GWAS effect the colocalization mass follows the QTL PIPs, - # which must be SuSiE's softmax over natural-log Bayes factors. - lbf <- c(2, 1, 0) - aligned <- list( - qtl = matrix(lbf, 1L, dimnames = list(NULL, c("a", "b", "c"))), - gwas = matrix(0, 1L, 3L, dimnames = list(NULL, c("a", "b", "c"))) - ) - out <- pecotmr:::.colocEnlocOneEffectPair( - 1L, - 1L, - aligned, - list(p1 = 1e-4, p2 = 1e-4, p12 = 5e-6) - ) - expect_equal( - unname(out$scp / sum(out$scp)), - exp(lbf) / sum(exp(lbf)) - ) -}) - test_that("the enrichment lookup matches on method when the table has it", { enr <- data.frame( gwasStudy = "G1", diff --git a/tests/testthat/test_colocboostPipeline.R b/tests/testthat/test_colocboostPipeline.R index 17ad3adf1..f8e5838c0 100644 --- a/tests/testthat/test_colocboostPipeline.R +++ b/tests/testthat/test_colocboostPipeline.R @@ -1,5 +1,160 @@ context("colocboostPipeline") +test_that("a colocboost bundle reads its window once, on the samples fit", { + skip_if_not_installed("pgenlibr") + qd <- .fitSamplesDataset() + filter <- pecotmr:::.qtlResolveFilter(GenotypeFilterParam(mafCutoff = 0.05)) + rp <- ResidualizationParam() + window <- .fitSamplesWindow() + bundle <- function(samples = NULL) { + pecotmr:::.cbIndividualBundle( + qd, + region = window, + samples = samples, + residualizationParam = rp, + genotypeFilter = filter, + phenotypeFilter = pecotmr:::.jointPhenotypeFilter(list()) + ) + } + run <- .countBlocks(bundle()) + # One read across both contexts, of only the samples they observe. + expect_equal(run$reads, 1L) + expect_equal(run$samples[[1L]], 1:30) + expect_equal(run$builds, 2L) + # g2 misses five samples, which dropping its missing values takes away. + fitSamples <- rownames(run$result$Y[[1L]]) + expect_length(fitSamples, 25L) + expected <- pecotmr:::.fmResidGeno( + qd, + rp, + filter, + contexts = "brain", + region = window, + samples = fitSamples + ) + .expectFitOn( + run$result$X[[1L]], + fitSamples, + `colnames<-`(expected, normalizeVariantId(colnames(expected))) + ) + # The run's `samples` narrow what is read and what is filtered on. + some <- colnames(qd[["brain"]])[1:12] + narrowed <- .countBlocks(bundle(some)) + expect_equal(narrowed$samples[[1L]], 1:12) + expect_setequal( + rownames(narrowed$result$X[[1L]]), + intersect(some, fitSamples) + ) +}) + +test_that("colocboostPipeline raises a failed genotype read", { + skip_if_not_installed("pgenlibr") + # A panel whose .pgen goes missing after it was opened. Every context shares + # the window's one read, so the run must stop rather than skip them all and + # return no rows. + source <- file.path(test_path("test_data"), "test_variants") + stem <- file.path(tempfile("cb_read_"), "panel") + dir.create(dirname(stem)) + on.exit(unlink(dirname(stem), recursive = TRUE), add = TRUE) + file.copy( + str_c(source, c(".pgen", ".pvar", ".psam")), + str_c(stem, c(".pgen", ".pvar", ".psam")) + ) + qd <- .fitSamplesDataset(readGenotypes(plink2Prefix = stem)) + file.remove(str_c(stem, ".pgen")) + messages <- character(0) + warnings <- character(0) + cnd <- withCallingHandlers( + tryCatch( + colocboostPipeline(qd, traitId = "g1", cisWindow = 1e5), + error = identity + ), + message = function(m) { + messages <<- c(messages, conditionMessage(m)) + invokeRestart("muffleMessage") + }, + warning = function(w) { + warnings <<- c(warnings, conditionMessage(w)) + invokeRestart("muffleWarning") + } + ) + expect_s3_class(cnd, "error") + expect_false(any(str_detect(messages, "skipping context|Nothing to run"))) + expect_false(any(str_detect(warnings, "restarting interrupted promise"))) +}) + +test_that("colocboost fits a trait with missing values on the samples it has", { + skip_if_not_installed("pgenlibr") + qd <- .fitSamplesDataset() + filter <- pecotmr:::.qtlResolveFilter(GenotypeFilterParam(mafCutoff = 0.05)) + run <- .countBlocks(pecotmr:::.cbIndividualBundle( + qd, + region = .fitSamplesWindow(), + genotypeFilter = filter, + phenotypeFilter = pecotmr:::.cbResolvePhenotypeFilter( + PhenotypeFilterParam() + ) + )) + bundle <- run$result + # Still one read; in each context one X for g1 and g3, and one for g2. + expect_equal(run$reads, 1L) + expect_equal(run$builds, 4L) + expect_equal(nrow(bundle$outcomeInfo), 6L) + expect_setequal(bundle$outcomeInfo$trait, c("g1", "g2", "g3")) + # No outcome carries a missing value, and each has an X of its samples. + expect_false(any(map_lgl(bundle$Y, anyNA))) + pairedX <- bundle$X[bundle$dict_YX[, "X"]] + expect_equal( + unname(map(bundle$Y, rownames)), + unname(map(pairedX, rownames)) + ) + isG2 <- bundle$outcomeInfo$trait == "g2" + expect_equal(unique(map_int(bundle$Y[isG2], nrow)), 25L) + expect_equal(unique(map_int(bundle$Y[!isG2], nrow)), 30L) + # Its genotypes are filtered and residualized on just those samples. + expected <- pecotmr:::.fmResidGeno( + qd, + ResidualizationParam(), + filter, + contexts = "brain", + region = .fitSamplesWindow(), + samples = rownames(bundle$Y[[which(isG2)[[1L]]]]) + ) + .expectFitOn( + pairedX[[which(isG2)[[1L]]]], + rownames(bundle$Y[[which(isG2)[[1L]]]]), + expected + ) + # colocboost takes the bundle: its own handling of a missing value does not. + skip_if_not_installed("colocboost") + fit <- suppressMessages(suppressWarnings(colocboost::colocboost( + X = bundle$X, + Y = bundle$Y, + dict_YX = bundle$dict_YX, + M = 5L, + output_level = 1L + ))) + expect_s3_class(fit, "colocboost") +}) + +test_that("a trait observed on too few samples is left out with a message", { + Y <- matrix( + c(1, 2, 3, NA, NA, 3, NA, NA, NA), + 3L, + 3L, + dimnames = list(str_c("s", 1:3), c("g1", "g2", "g3")) + ) + expect_message( + groups <- pecotmr:::.cbObservedGroups(Y, "brain"), + "leaving out g2, g3 of context 'brain'" + ) + expect_equal(groups, list(1L)) + expect_no_message(pecotmr:::.cbObservedGroups( + Y[, 1L, drop = FALSE], + "brain" + )) +}) + test_that("colocboostPipeline is exported as an S4 generic", { expect_true("colocboostPipeline" %in% getNamespaceExports("pecotmr")) expect_true(methods::isGeneric("colocboostPipeline")) @@ -18,14 +173,14 @@ context("colocboostPipeline (S4 dispatch)") # =========================================================================== .cbp_mockExtractor <- function(seed = 11, n_samples = 30L) { - function(handle, snpIdx) { + function(handle, snpIdx, sampleIdx = NULL) { set.seed(seed) panel <- matrix( rbinom(n_samples * nrow(snpInfo(handle)), 2, 0.3), nrow = n_samples, ncol = nrow(snpInfo(handle)) ) - panel[, snpIdx, drop = FALSE] + panel[sampleIdx %||% seq_len(nrow(panel)), snpIdx, drop = FALSE] } } @@ -1277,7 +1432,7 @@ test_that(".cbBuildContextXY skips a context with no genotypes", { .cbResidualizedX = function(...) NULL, .package = "pecotmr" ) - expect_null(pecotmr:::.cbBuildContextXY("c1", list())) + expect_length(pecotmr:::.cbBuildContextXY("c1", list()), 0L) }) test_that(".cbBuildContextXY skips a context with no shared samples", { @@ -1293,7 +1448,7 @@ test_that(".cbBuildContextXY skips a context with no shared samples", { res <- pecotmr:::.cbBuildContextXY("c1", list()), "no samples shared between residualized X and Y" ) - expect_null(res) + expect_length(res, 0L) }) test_that(".cbResidualizedX reports why genotypes were unavailable", { diff --git a/tests/testthat/test_crossValidation.R b/tests/testthat/test_crossValidation.R index 0ed79903a..841b1d89b 100644 --- a/tests/testthat/test_crossValidation.R +++ b/tests/testthat/test_crossValidation.R @@ -551,6 +551,22 @@ test_that("a BPPARAM other than SerialParam() is a cross-validation request", { ) }) +test_that(".bpMap keeps the cause of an error a worker's own map() wrapped", { + wrapped <- function(i) purrr::map(1:2, stop, "the real cause") + cnd <- tryCatch( + pecotmr:::.bpMap(1:2, wrapped, BPPARAM = BiocParallel::SerialParam()), + error = identity + ) + expect_match(conditionMessage(cnd), "the real cause", fixed = TRUE) + expect_match(conditionMessage(cnd), "In index: 1", fixed = TRUE) + # A worker's error with no cause of its own stays as it was. + plain <- function(i) stop("plain") + expect_error( + pecotmr:::.bpMap(1:2, plain, BPPARAM = BiocParallel::SerialParam()), + "plain" + ) +}) + test_that("a BiocParallel report without a worker error is raised as is", { cnd <- structure( class = c("bplist_error", "bperror", "error", "condition"), diff --git a/tests/testthat/test_ctwasPipeline.R b/tests/testthat/test_ctwasPipeline.R index 1ccf2b8cb..a5dbd8a58 100644 --- a/tests/testthat/test_ctwasPipeline.R +++ b/tests/testthat/test_ctwasPipeline.R @@ -59,14 +59,14 @@ context("ctwasPipeline") .ctp_snpId <- function(i) sprintf("chr1:%d:G:A", 100L * i) .ctp_mockExtractor <- function(seed = 5, n_samples = 30L) { - function(handle, snpIdx) { + function(handle, snpIdx, sampleIdx = NULL) { set.seed(seed) panel <- matrix( rbinom(n_samples * nrow(snpInfo(handle)), 2, 0.3), nrow = n_samples, ncol = nrow(snpInfo(handle)) ) - panel[, snpIdx, drop = FALSE] + panel[sampleIdx %||% seq_len(nrow(panel)), snpIdx, drop = FALSE] } } @@ -106,7 +106,7 @@ context("ctwasPipeline") # `variantIdx` picks which of the fixture's 12 variants carry the gene's # weights; it has to name five of them, one per weight. The default sits # inside block 1 -- pass 7:11 for a gene that lives in block 2. -.ctp_makeTwasWeights <- function(variantIdx = 1:5) { +.ctp_makeTwasWeights <- function(variantIdx = 1:5, trait = "t1") { e <- TwasWeightsRow( variantIds = map_chr(variantIdx, .ctp_snpId), weights = c(0.1, 0.05, -0.2, 0.3, 0.0) @@ -114,13 +114,65 @@ context("ctwasPipeline") TwasWeights( studyName = "Q1", context = "c1", - trait = "t1", + trait = trait, method = "susie", entry = list(e), ldReference = .ctp_makeHandle() ) } +# The pipeline's weight entries for one weight source on one panel. +.ctp_buildWeights <- function( + tw, + panel, + fineMappingResult = NULL, + twasWeightCutoff = 0, + csMinCor = 0.8, + minPipCutoff = 0, + maxNumVariants = Inf, + gwasSnpIds = NULL, + globalPanelInfo = NULL, + globalVariance = NULL +) { + ctx <- pecotmr:::.ctwasGeneContext( + globalPanelInfo %||% panel$snpInfo, + globalVariance %||% panel$variance, + gwasSnpIds, + fineMappingResult, + list( + twasWeightCutoff = twasWeightCutoff, + csMinCor = csMinCor, + minPipCutoff = minPipCutoff, + maxNumVariants = maxNumVariants + ) + ) + pecotmr:::.ctwasGeneEntries( + pecotmr:::.ctwasPreparedGenes(tw, ctx), + list(panel) + ) +} + +# Weight records, as far as reading a run's results needs them, for genes named +# "study|context|trait|method", keyed by the ids the pipeline gives them. +.ctp_weights <- function(genes) { + parts <- str_split(genes, fixed("|")) + records <- map(parts, function(p) { + list( + study = p[[1L]], + qtl_context = p[[2L]], + gene_name = p[[3L]], + method = p[[4L]], + type = "QTL", + context = str_c(p[[1L]], "_", p[[2L]]), + molecular_id = p[[3L]] + ) + }) + ids <- map_chr(parts, function(p) { + sprintf("%s|%s_%s_QTL_%s", p[[3L]], p[[1L]], p[[2L]], p[[4L]]) + }) + setNames(records, ids) +} + # The same per-gene weights expressed as a QtlFineMappingResult weight source: # the topLoci posterior_mean carries the weight vector (what resolveWeights # reads). Mirrors .ctp_makeTwasWeights so the two sources are comparable. @@ -166,12 +218,11 @@ context("ctwasPipeline") ss <- .ctp_makeGwasSumstats(qc = qc) list( gwasSumStats = ss, - # ctwas fine-maps one region at a time, so each block's gene has to - # carry weights on variants that block actually has LD for: block 1 - # holds variants 1..6, block 2 holds 7..12. + # One gene per block, each with weights on that block's variants: + # block 1 holds variants 1..6, block 2 holds 7..12. twasWeights = list( block1 = .ctp_makeTwasWeights(), - block2 = .ctp_makeTwasWeights(7:11) + block2 = .ctp_makeTwasWeights(7:11, trait = "t2") ) ) } @@ -254,10 +305,10 @@ test_that("assembleCtwasInputs accepts a subset of gwasSumStats' keys", { "share 6 variant" ) # Both blocks appear in region_info / snp_map (SNP-only block2 contributes - # its zSnp), but the weights list only has block1-keyed entries. + # its zSnp), but the weights list only has block1's gene. expect_setequal(inputs$region_info$region_id, c("block1", "block2")) expect_setequal(names(inputs$snp_map), c("block1", "block2")) - expect_true(all(grepl("^block1\\|", names(inputs$weights)))) + expect_identical(names(inputs$weights), "t1|Q1_c1_QTL_susie") }) test_that("assembleCtwasInputs takes a QtlFineMappingResult weight source", { @@ -285,12 +336,12 @@ test_that("assembleCtwasInputs takes a QtlFineMappingResult weight source", { ) itw <- assembleCtwasInputs( gwasSumStats = ss, - twasWeights = list(block1 = tw_std, block2 = tw_std), + twasWeights = list(block1 = tw_std), method = "susie" ) ifmr <- assembleCtwasInputs( gwasSumStats = ss, - twasWeights = list(block1 = fmr, block2 = fmr), + twasWeights = list(block1 = fmr), method = "susie" ) # Same gene keys, and the resolved weights match the standardized @@ -300,7 +351,7 @@ test_that("assembleCtwasInputs takes a QtlFineMappingResult weight source", { expect_equal(ifmr$weights[[1L]], itw$weights[[1L]]) }) -test_that("assembleCtwasInputs: boundary gene fits per-region, spans all", { +test_that("assembleCtwasInputs: a boundary gene keeps all its weights", { # Build two blocks with NON-OVERLAPPING GWAS variants. Block 1 covers # v1..v3, block 2 covers v4..v6. The gene's weight spans v2..v5 — i.e. # crosses the block boundary. With a per-block filter the gene would @@ -355,34 +406,40 @@ test_that("assembleCtwasInputs: boundary gene fits per-region, spans all", { twasWeights = list(block1 = tw), method = "susie" ) - # ctwas fine-maps one region at a time, so the FITTED vector holds only the - # variants this block has LD for -- handing susie_rss the other two produces - # a non-finite ELBO. The gene's SPAN still covers all four, which is what - # ctwas::get_boundary_genes reads to route the gene to merge_regions; a span - # clipped to the block would hide the boundary gene entirely. + # The gene's z reads all four weights, and ctwas cuts them to a region's + # variants itself when it fine-maps. The span, which + # ctwas::get_boundary_genes reads to route the gene to merge_regions, covers + # them too. entry <- inputs$weights[[1L]] wgt <- entry$wgt - expect_equal(nrow(wgt), 2L) - expect_setequal(rownames(wgt), map_chr(2:3, .ctp_snpId)) + expect_equal(nrow(wgt), 4L) + expect_setequal(rownames(wgt), map_chr(2:5, .ctp_snpId)) expect_length(entry$wgt_var, 1L) expect_gt(entry$wgt_var, 0) - expect_equal(entry$n_wgt, 2L) + expect_equal(entry$n_wgt, 4L) expect_equal(entry$p0, 200L) expect_equal(entry$p1, 500L) }) -test_that("ctwasPipeline places a bare TwasWeights (flat source) by region", { - skip_if_not_installed("ctwas") - # A bare TwasWeights is now accepted as a flat weight source and placed into - # LD blocks by region; the fixture carries no region, so placement errors - # (rather than the old 'must be a NAMED LIST' shape rejection). - expect_error( - ctwasPipeline( - gwasSumStats = .ctp_makeGwasSumstats(), - twasWeights = .ctp_makeTwasWeights(), - method = "susie" - ), - "no `traitPos` provenance" +test_that("a flat source needs no traitPos: its weights place each gene", { + local_mocked_bindings( + .handleDosage = .ctp_mockExtractor(), + .package = "pecotmr" + ) + # The weights in block 2, and a gene with no coordinates but those. + tw <- .ctp_makeTwasWeights(7:11) + expect_false(is_in("traitPos", pecotmr:::.tupleColumnNames(tw))) + inputs <- suppressWarnings(assembleCtwasInputs( + gwasSumStats = .ctp_makeGwasSumstats(), + twasWeights = tw, + method = "susie" + )) + gene <- inputs$weights[["t1|Q1_c1_QTL_susie"]] + expect_null(gene$trait_pos) + expect_equal(c(gene$p0, gene$p1), c(700L, 1100L)) + expect_equal( + pecotmr:::.ctwasOneGeneCoord(gene), + list(chrom = "chr1", start = 700L, end = 1100L) ) }) @@ -407,11 +464,19 @@ test_that("CtwasPriorParam rejects an unknown varStructure", { CtwasPriorParam(varStructure = "bogus"), "`varStructure` must be one of" ) - expect_equal(CtwasPriorParam()$varStructure, "shared_type") + expect_equal(CtwasPriorParam()$varStructure, "shared_nonSNP") expect_equal( CtwasPriorParam(varStructure = "independent")$varStructure, "independent" ) + expect_equal( + CtwasPriorParam(varStructure = "shared_type")$varStructure, + "shared_type" + ) + expect_equal( + CtwasPriorParam(varStructure = "shared_context")$varStructure, + "shared_context" + ) }) # =========================================================================== @@ -497,7 +562,7 @@ test_that(".ctwasBuildZSnp: produces a flat data.frame keyed by SNP/study", { expect_equal(nrow(df), 6L) expect_setequal( colnames(df), - c("id", "chrom", "pos", "A1", "A2", "z", "study") + c("id", "chrom", "pos", "A1", "A2", "z", "study", "N") ) expect_setequal(df$id, map_chr(1:6, .ctp_snpId)) expect_setequal(unique(df$study), "G1") @@ -948,7 +1013,7 @@ test_that(".ctwasGwasStudy: unique study, errors on mixed studies", { }) # --------------------------------------------------------------------------- -# Phase 5: internal LD-block placement (by start(region) == cTWAS p0 rule) +# Phase 5: internal LD-block placement, from the genes' weights # --------------------------------------------------------------------------- # Three LD blocks split from one GWAS by their windows, as cTWAS takes them: # one single-row slice per block, named by its blockId. The variants sit well @@ -977,34 +1042,130 @@ test_that(".ctwasGwasStudy: unique study, errors on mixed studies", { )) } -test_that(".ctwasPlaceByAnchor homes genes by start(region)", { - region <- GenomicRanges::GRanges( - c("chr1", "chr1", "chr22", "chr2"), - IRanges::IRanges( - start = c(500, 1500, 800, 100), - end = c(600, 1600, 900, 200) - ) +# One gene per element of `variants`, weights `sizes` on the variants at those +# chr:pos, for placement, which reads nothing else. +.ctp_placedGenes <- function( + variants, + sizes = rep(list(NULL), length(variants)) +) { + TwasWeights( + studyName = rep("Q1", length(variants)), + context = rep("c1", length(variants)), + trait = str_c("g", seq_along(variants)), + method = rep("susie", length(variants)), + entry = map2(variants, sizes, function(at, size) { + TwasWeightsRow( + variantIds = str_c(at, ":G:A"), + weights = size %||% rep(0.1, length(at)) + ) + }), + ldReference = .ctp_makeHandle() ) - home <- pecotmr:::.ctwasPlaceByAnchor(region, .ctp_windowGrid()) - # chr22:800 -> the chr22 block; chr2 has no block -> NA. - expect_equal(home, c("b1", "b2", "b3", NA)) +} + +test_that(".ctwasPlaceByWeights homes each gene where its weight lies", { + tw <- .ctp_placedGenes( + list( + "chr1:500", + c("chr1:900", "chr1:1100", "chr1:1200"), + "chr22:800", + "chr2:100" + ), + list(NULL, c(0.1, -0.2, 0.2), NULL, NULL) + ) + # Across blocks, the one holding most of the weight, however it is signed; + # chr2 has no block. + expect_equal( + pecotmr:::.ctwasPlaceByWeights(tw, .ctp_windowGrid()), + c("b1", "b2", "b3", NA) + ) + # The weight decides, not the count of variants. + heavy <- .ctp_placedGenes( + list(c("chr1:500", "chr1:600", "chr1:1500")), + list(c(0.1, 0.1, -0.5)) + ) + expect_equal(pecotmr:::.ctwasPlaceByWeights(heavy, .ctp_windowGrid()), "b2") }) -test_that(".ctwasPlaceByAnchor: a block holds its whole window", { +test_that(".ctwasPlaceByWeights: a block holds its whole window", { # chr1:900 lies between block 1's variant (500) and block 2's (1500): the # window, not the variants, decides. The blocks' names are only names, and # the last base of a window (1000) is its own, the next (1001) the next # block's. - region <- GenomicRanges::GRanges( - "chr1", - IRanges::IRanges(start = c(900, 1000, 1001), width = 1) - ) expect_equal( - pecotmr:::.ctwasPlaceByAnchor(region, .ctp_windowGrid()), + pecotmr:::.ctwasPlaceByWeights( + .ctp_placedGenes(list("chr1:900", "chr1:1000", "chr1:1001")), + .ctp_windowGrid() + ), c("b1", "b1", "b2") ) }) +test_that(".ctwasPlaceByWeights sums a gene's methods and its weight columns", { + both <- TwasWeights( + studyName = c("Q1", "Q1"), + context = c("c1", "c1"), + trait = c("g1", "g1"), + method = c("lasso", "susie"), + entry = list( + TwasWeightsRow(variantIds = "chr1:500:G:A", weights = 0.1), + TwasWeightsRow(variantIds = "chr1:1500:G:A", weights = 0.5) + ), + ldReference = .ctp_makeHandle() + ) + expect_equal(pecotmr:::.ctwasPlaceByWeights(both, .ctp_windowGrid()), "b2") + # Weights with several columns give cTWAS no gene to model, which the run + # reports (.ctwasAlignGeneWeights()), but they still lie somewhere. + columns <- TwasWeights( + studyName = "Q1", + context = "c1", + trait = "g1", + method = "mrmash", + entry = list(TwasWeightsRow( + variantIds = c("chr1:500:G:A", "chr1:1500:G:A"), + weights = matrix(c(0.1, 0.5, -0.1, 0.5), 2L) + )), + ldReference = .ctp_makeHandle() + ) + expect_equal( + pecotmr:::.ctwasPlaceByWeights(columns, .ctp_windowGrid()), + "b2" + ) +}) + +test_that(".ctwasPlaceByWeights: a gene with no weights has no home", { + empty <- TwasWeights( + studyName = c("Q1", "Q1"), + context = c("c1", "c1"), + trait = c("none", "g1"), + method = c("susie", "susie"), + entry = list( + TwasWeightsRow(variantIds = character(0), weights = numeric(0)), + TwasWeightsRow(variantIds = "chr1:500:G:A", weights = 0.1) + ), + ldReference = .ctp_makeHandle() + ) + expect_equal( + pecotmr:::.ctwasPlaceByWeights(empty, .ctp_windowGrid()), + c(NA, "b1") + ) + # With nothing to place at all, nothing has a home. + expect_equal( + pecotmr:::.ctwasPlaceByWeights(empty[1L, ], .ctp_windowGrid()), + NA_character_ + ) +}) + +test_that(".ctwasPlaceByWeights reads a fine-mapping weight source", { + expect_equal( + pecotmr:::.ctwasPlaceByWeights( + .ctp_makeFmrWeightSource(), + pecotmr:::.ctwasGwasByBlock(.ctp_makeGwasSumstats()) + ), + "block1" + ) +}) + test_that(".ctwasCombineWeightSources tells flat from pre-bucketed sources", { tw <- .ctp_makeTwasWeights() grid <- list(block1 = 1, block2 = 2) @@ -1019,24 +1180,23 @@ test_that(".ctwasCombineWeightSources tells flat from pre-bucketed sources", { }) test_that(".ctwasBucketWeights places a flat 2-gene source in its blocks", { - mkE <- function() { + mkE <- function(idx) { TwasWeightsRow( - variantIds = map_chr(1:5, .ctp_snpId), + variantIds = map_chr(idx, .ctp_snpId), weights = c(0.1, 0.05, -0.2, 0.3, 0.0) ) } - # Placement anchors on the GENE position (traitPos), not the weight - # variants: both genes carry the same five variants here, so only the - # anchor can tell them apart. block1 spans 100-600, block2 spans 700-1200, - # so gA @chr1:100 homes to block1 and gB @chr1:800 to block2. + # block1 spans 100-600 and block2 700-1200, so a gene's weights place it, + # wherever its traitPos says it is: gB's reads chr9, and gives only its + # coordinates in the results. tw <- TwasWeights( studyName = c("Q1", "Q1"), context = c("c1", "c1"), trait = c("gA", "gB"), method = c("susie", "susie"), - entry = list(mkE(), mkE()), + entry = list(mkE(1:5), mkE(7:11)), traitPos = GenomicRanges::GRanges( - c("chr1", "chr1"), + c("chr1", "chr9"), IRanges::IRanges(c(100, 800), c(150, 850)) ), ldReference = .ctp_makeHandle() @@ -1049,47 +1209,34 @@ test_that(".ctwasBucketWeights places a flat 2-gene source in its blocks", { expect_length(ldReference(bucketed[["block1"]]), 1L) }) -test_that(".ctwasBucketWeights: errors when source lacks traitPos", { - expect_error( - pecotmr:::.ctwasBucketWeights( - .ctp_makeTwasWeights(), - list( - chr1_1_350 = .ctp_makeGwasSumstats(), - chr1_350_700 = .ctp_makeGwasSumstats() - ) - ), - "no `traitPos` provenance" - ) -}) - # --------------------------------------------------------------------------- -# .ctwasParseGeneIds — id -> identity components +# .ctwasGeneTable — a run's genes, from their records # --------------------------------------------------------------------------- -test_that(".ctwasParseGeneIds: splits region|study|context|trait|method", { - p <- pecotmr:::.ctwasParseGeneIds(c( - "blk|Q1|brain|gA|susie", - "blk2|Q1|liver|gB|lasso" - )) - expect_equal(p$rid, c("blk", "blk2")) - expect_equal(p$study, c("Q1", "Q1")) - expect_equal(p$context, c("brain", "liver")) - expect_equal(p$trait, c("gA", "gB")) - expect_equal(p$method, c("susie", "lasso")) - expect_error(pecotmr:::.ctwasParseGeneIds("too|few|fields"), "malformed") +test_that(".ctwasGeneTable reads the genes off their records, not their ids", { + genes <- pecotmr:::.ctwasGeneTable(.ctp_weights(c( + "Q1|brain|gA|susie", + "Q1_x|liver_y|g|B|lasso" + ))) + expect_equal(genes$study, c("Q1", "Q1_x")) + expect_equal(genes$context, c("brain", "liver_y")) + expect_equal(genes$trait, c("gA", "g")) + expect_equal(genes$method, c("susie", "B")) + expect_equal(nrow(pecotmr:::.ctwasGeneTable(NULL)), 0L) }) # --------------------------------------------------------------------------- # .ctwasRunToRows — decompose a cTWAS run into per-context CtwasResult rows # --------------------------------------------------------------------------- .ctp_runResult <- function( - ids, + genes, pips, prior, snpIds = character(0), region = data.frame(region_id = "blk") ) { + weights <- .ctp_weights(genes) geneFm <- data.frame( - id = ids, + id = names(weights), type = "gene", susie_pip = pips, stringsAsFactors = FALSE @@ -1106,7 +1253,7 @@ test_that(".ctwasParseGeneIds: splits region|study|context|trait|method", { } fm <- rbind(geneFm, snpFm) list( - weights = setNames(as.list(seq_along(ids)), ids), + weights = weights, finemap_res = fm, susie_alpha_res = cbind(fm, susie_alpha = 0.5), param = list(group_prior = prior), @@ -1116,7 +1263,7 @@ test_that(".ctwasParseGeneIds: splits region|study|context|trait|method", { test_that(".ctwasRunToRows: single-context run -> one row, no jointContexts", { run <- .ctp_runResult( - c("blk|Q1|c1|gA|susie", "blk|Q1|c1|gB|susie"), + c("Q1|c1|gA|susie", "Q1|c1|gB|susie"), c(0.9, 0.2), c(c1 = 0.01, SNP = 1e-4) ) @@ -1135,10 +1282,10 @@ test_that(".ctwasRunToRows: single-context run -> one row, no jointContexts", { test_that(".ctwasRunToRows: multi-context run -> rows sharing jointContexts", { ids <- c( - "blk|Q1|brain|gA|susie", - "blk|Q1|brain|gB|susie", - "blk|Q1|liver|gA|susie", - "blk|Q1|liver|gB|susie" + "Q1|brain|gA|susie", + "Q1|brain|gB|susie", + "Q1|liver|gA|susie", + "Q1|liver|gB|susie" ) run <- .ctp_runResult( ids, @@ -1162,21 +1309,58 @@ test_that(".ctwasRunToRows: multi-context run -> rows sharing jointContexts", { ) }) -test_that(".ctwasRunToRows refuses multi-context runs with unshared genes", { +test_that(".ctwasRunToRows: the contexts of a joint run need not share genes", { run <- .ctp_runResult( - c("blk|Q1|brain|gA|susie", "blk|Q1|liver|gC|susie"), + c("Q1|brain|gA|susie", "Q1|liver|gC|susie"), c(0.9, 0.8), c(brain = 0.01, liver = 0.02, SNP = 1e-4) ) - expect_error( - pecotmr:::.ctwasRunToRows(run, "D1", "susie"), - "SAME gene set in every context" + rows <- pecotmr:::.ctwasRunToRows(run, "D1", "susie") + expect_equal( + map_chr(rows, function(r) r$context), + c("brain", "liver") + ) + expect_true(all( + map_chr(rows, function(r) r$jointContexts) == "brain,liver" + )) + expect_equal( + ctwasPosteriors(rows[[1L]]$entry)$id, + "gA|Q1_brain_QTL_susie" + ) + expect_equal( + ctwasPosteriors(rows[[2L]]$entry)$id, + "gC|Q1_liver_QTL_susie" + ) +}) + +test_that(".ctwasWarnDroppedGroups names what cTWAS left out, and only that", { + zGene <- data.frame( + id = c("a", "b", "c"), + context = c("brain", "brain", "liver"), + group = c("brain|brain", "brain|brain", "liver|liver") + ) + expect_warning( + pecotmr:::.ctwasWarnDroppedGroups( + zGene, + list( + r1 = list(groups = c("brain|brain", "SNP")), + r2 = list(groups = "SNP") + ) + ), + "liver \\(1 gene.*CtwasRegionDataOptions.*CtwasEstParamOptions" ) + expect_no_warning(pecotmr:::.ctwasWarnDroppedGroups( + zGene, + list( + r1 = list(groups = "brain|brain"), + r2 = list(groups = c("liver|liver", "SNP")) + ) + )) }) test_that(".ctwasRunToRows: empty finemap_res still yields the modeled row", { run <- list( - weights = setNames(list(1), "blk|Q1|c1|gA|susie"), + weights = .ctp_weights("Q1|c1|gA|susie"), finemap_res = NULL, param = list(), region_info = NULL @@ -1189,7 +1373,7 @@ test_that(".ctwasRunToRows: empty finemap_res still yields the modeled row", { test_that(".ctwasRunToRows: susieAlpha subset mirrors finemap per context", { run <- .ctp_runResult( - c("blk|Q1|c1|gA|susie", "blk|Q1|c1|gB|susie"), + c("Q1|c1|gA|susie", "Q1|c1|gB|susie"), c(0.9, 0.2), c(c1 = 0.01, SNP = 1e-4) ) @@ -1200,7 +1384,7 @@ test_that(".ctwasRunToRows: susieAlpha subset mirrors finemap per context", { }) test_that(".ctwasRunToRows: keepSnps adds a SNP row; by default none", { - ids <- c("blk|Q1|c1|gA|susie", "blk|Q1|c1|gB|susie") + ids <- c("Q1|c1|gA|susie", "Q1|c1|gB|susie") run <- .ctp_runResult( ids, c(0.9, 0.2), @@ -1284,7 +1468,7 @@ test_that(".ctwasBuildWeights: only raw weights are scaled by sqrt(variance)", { )), ldReference = .ctp_makeHandle() ) - wl <- pecotmr:::.ctwasBuildWeights(tw, panel) + wl <- .ctp_buildWeights(tw, panel) expected <- if (standardized) { rawW } else { @@ -1362,6 +1546,88 @@ test_that(".ctwasHarmonizeWeights: drops variants not present in the panel", { expect_equal(res$variant_id, "chr1:100:C:T") }) +test_that(".ctwasRowsAtPositions finds every row at each position", { + # Out of order, a position held twice, one no row has, and a missing one. + index <- pecotmr:::.ctwasPositionIndex(c(300L, 100L, 200L, 100L, 500L)) + at <- pecotmr:::.ctwasRowsAtPositions + expect_equal(at(index, c(100L, 500L, 400L, NA)), c(2L, 4L, 5L)) + # Repeats ask for nothing more, and the rows come back in panel order. + expect_equal(at(index, c(200L, 100L, 200L)), c(2L, 3L, 4L)) + expect_equal(at(index, 50L), integer(0)) + expect_equal(at(index, 999L), integer(0)) + expect_equal(at(index, integer(0)), integer(0)) +}) + +test_that("preparing a gene reads only the panel's rows at its positions", { + # Not in position order: a position another chromosome shares, two alleles + # at one position, one of them not in the GWAS, and a variant the gene does + # not reach. The variances are in an order of their own. + info <- data.frame( + chrom = c(1L, 1L, 2L, 1L, 1L), + id = c( + "chr1:300:A:G", + "chr1:100:C:T", + "chr2:100:C:T", + "chr1:100:C:A", + "chr1:200:G:A" + ), + pos = c(300L, 100L, 100L, 100L, 200L), + alt = c("G", "T", "T", "A", "A"), + ref = c("A", "C", "C", "C", "G"), + stringsAsFactors = FALSE + ) + variance <- setNames(c(0.1, 0.2, 0.3, 0.4, 0.5), rev(info$id)) + ctx <- pecotmr:::.ctwasGeneContext( + info, + variance, + setdiff(info$id, "chr1:100:C:A"), + NULL, + list() + ) + # One as the panel has it, one with its alleles swapped, one off the panel. + row <- TwasWeightsRow( + variantIds = c("chr1:100:C:T", "chr1:200:A:G", "chr1:999:A:T"), + weights = c(0.5, 0.3, 0.2) + ) + near <- pecotmr:::.ctwasPanelNear(row, ctx) + reached <- info$id[2:5] + expect_equal(near$spanInfo$id, reached) + expect_equal(near$refVariants$variant_id, reached) + expect_equal(near$panelSnps, setdiff(reached, "chr1:100:C:A")) + expect_equal(near$spanVariance, variance[reached]) + # The gene comes out as the whole panel would have made it. + aligned <- pecotmr:::.ctwasAlignGeneWeights( + row, + near$refVariants, + near$panelSnps + ) + whole <- pecotmr:::.ctwasAlignGeneWeights( + row, + ctx$refVariants, + ctx$spanInfo$id[ctx$inGwas] + ) + expect_equal(aligned, whole) + expect_equal(aligned$vids, c("chr1:100:C:T", "chr1:200:G:A")) + expect_equal(aligned$w, c(0.5, -0.3)) + expect_equal( + pecotmr:::.ctwasGeneSpan(aligned$vids, near$spanInfo), + pecotmr:::.ctwasGeneSpan(aligned$vids, info) + ) + # With none of its positions on the panel, no gene is made. + off <- TwasWeightsRow(variantIds = "chr1:999:A:T", weights = 0.2) + offNear <- pecotmr:::.ctwasPanelNear(off, ctx) + expect_equal(nrow(offNear$spanInfo), 0L) + expect_warning( + none <- pecotmr:::.ctwasAlignGeneWeights( + off, + offNear$refVariants, + offNear$panelSnps + ), + "No matching variants" + ) + expect_null(none) +}) + test_that(".ctwasIsSusieFit: recognizes the susie intermediate shape", { fits <- list( alpha = matrix(1 / 3, 2, 3), @@ -1470,11 +1736,14 @@ test_that(".ctwasBuildWeights: keys per-tuple weights, adds gene metadata", { tw <- .ctp_makeTwasWeights() panel <- .ctp_makeLdPanel() ids5 <- map_chr(1:5, .ctp_snpId) - wl <- pecotmr:::.ctwasBuildWeights(tw, panel) + wl <- .ctp_buildWeights(tw, panel) expect_equal(length(wl), 1L) - expect_equal(names(wl), "Q1|c1|t1|susie") + expect_equal(names(wl), "t1|Q1_c1_QTL_susie") expect_equal(wl[[1L]]$study, "Q1") - expect_equal(wl[[1L]]$context, "c1") + expect_equal(wl[[1L]]$qtl_context, "c1") + expect_equal(wl[[1L]]$context, "Q1_c1") + expect_equal(wl[[1L]]$type, "QTL") + expect_equal(wl[[1L]]$weight_name, "Q1_c1_QTL_susie") expect_equal(wl[[1L]]$gene_name, "t1") # wgt is a variants x 1 matrix with rownames = SNP IDs expect_true(is.matrix(wl[[1L]]$wgt)) @@ -1506,7 +1775,7 @@ test_that(".ctwasBuildWeights: drops variants not present in the LD panel", { ldReference = .ctp_makeHandle() ) panel <- .ctp_makeLdPanel() - wl <- pecotmr:::.ctwasBuildWeights(tw, panel) + wl <- .ctp_buildWeights(tw, panel) expect_equal(nrow(wl[[1L]]$wgt), 3L) expect_equal(rownames(wl[[1L]]$wgt), ids3) expect_equal(wl[[1L]]$n_wgt, 3L) @@ -1531,7 +1800,7 @@ test_that(".ctwasBuildWeights: intersects with gwasSnpIds when supplied", { ldReference = .ctp_makeHandle() ) panel <- .ctp_makeLdPanel() - wl <- pecotmr:::.ctwasBuildWeights( + wl <- .ctp_buildWeights( tw, panel, gwasSnpIds = blockIds @@ -1654,9 +1923,9 @@ test_that(".ctwasWeightVariances: w'Rw from the factor matches the matrix", { list(vids = map_chr(c(1L, 2L), .ctp_snpId), w = c(0.5, -0.2)), list(vids = map_chr(c(2L, 5L, 6L), .ctp_snpId), w = c(0.1, 0.3, -0.4)) ) - viaFactor <- pecotmr:::.ctwasWeightVariances(panel, prepared) + viaFactor <- pecotmr:::.ctwasWeightVariances(list(panel), prepared) viaMatrix <- pecotmr:::.ctwasWeightVariances( - c(panel, list(R = full)), + list(c(panel, list(R = full))), prepared ) expect_equal(viaFactor, viaMatrix) @@ -1667,7 +1936,134 @@ test_that(".ctwasWeightVariances: w'Rw from the factor matches the matrix", { full[prepared[[2L]]$vids, prepared[[2L]]$vids] %*% prepared[[2L]]$w )) ) - expect_identical(pecotmr:::.ctwasWeightVariances(panel, list()), numeric(0)) + expect_identical( + pecotmr:::.ctwasWeightVariances(list(panel), list()), + numeric(0) + ) + # A variant in no panel has no LD to read. + expect_error( + pecotmr:::.ctwasWeightVariances( + list(panel), + list(list(vids = "chr9:1:G:A", w = 1)) + ), + "1 weight variant\\(s\\) absent from the blocks' LD panels" + ) +}) + +test_that(".ctwasWeightVariances: the blocks a gene crosses are independent", { + idsA <- map_chr(1:3, .ctp_snpId) + idsB <- map_chr(4:6, .ctp_snpId) + panelFor <- function(ids, r) { + R <- matrix(r, 3L, 3L, dimnames = list(ids, ids)) + diag(R) <- 1 + list(snpInfo = data.frame(id = ids), R = R) + } + panels <- list(panelFor(idsA, 0.5), panelFor(idsB, 0.2)) + prepared <- list( + list(vids = c(idsA[1:2], idsB[2:3]), w = c(1, 2, 3, -1)), + list(vids = idsB[1:2], w = c(1, 1)) + ) + expect_equal( + pecotmr:::.ctwasWeightVariances(panels, prepared), + c( + # 1 + 4 + 2 * 0.5 * 2, then 9 + 1 + 2 * 0.2 * 3 * -1 + 7 + 8.8, + 1 + 1 + 2 * 0.2 + ) + ) +}) + +test_that("a gene's molecular type is what its weights record", { + entry <- function(type) { + TwasWeightsRow( + variantIds = map_chr(1:3, .ctp_snpId), + weights = c(0.1, 0.2, 0.3), + weightsDataType = type + ) + } + tw <- TwasWeights( + studyName = c("Q1", "Q1"), + context = c("brain", "liver"), + trait = c("g1", "g2"), + method = c("susie", "susie"), + entry = list(entry("expression"), entry(NULL)), + ldReference = .ctp_makeHandle() + ) + wl <- .ctp_buildWeights(tw, .ctp_makeLdPanel()) + expect_named( + wl, + c("g1|Q1_brain_expression_susie", "g2|Q1_liver_QTL_susie") + ) + expect_equal( + map_chr(unname(wl), "type"), + c("expression", "QTL") + ) + # A fine-mapping result records it per row, if at all. + fmr <- .ctp_makeFmrWeightSource() + expect_equal( + pecotmr:::.ctwasUnitType(fmr, pecotmr:::.ctwasUnit(1L, "susie")), + "QTL" + ) + S4Vectors::mcols(fmr)$dataType <- "splicing" + expect_equal( + pecotmr:::.ctwasUnitType(fmr, pecotmr:::.ctwasUnit(1L, "susie")), + "splicing" + ) + S4Vectors::mcols(fmr)$dataType <- NA_character_ + expect_equal( + pecotmr:::.ctwasUnitType(fmr, pecotmr:::.ctwasUnit(1L, "susie")), + "QTL" + ) +}) + +test_that("groups whose names cTWAS could not tell apart are refused", { + prepared <- function(study, context, type = "QTL") { + list(meta = list(study = study, context = context, type = type)) + } + expect_error( + pecotmr:::.ctwasRequireDistinctGroups(list( + prepared("a_b", "c"), + prepared("a", "b_c") + )), + "read the same once joined: a_b_c_QTL" + ) + expect_error( + pecotmr:::.ctwasRequireDistinctGroups(list(prepared("a", "b|c"))), + "holds a '\\|': a_b\\|c_QTL" + ) + expect_null(pecotmr:::.ctwasRequireDistinctGroups(list( + prepared("a", "b"), + prepared("a", "b"), + prepared("a", "b", "splicing") + ))) +}) + +test_that("a boundary gene's z is ctwas's, its blocks independent", { + skip_if_not_installed("ctwas") + data(ctwasInputsExample, envir = environment()) + inputs <- pecotmr:::.ctwasResolveLdPaths(ctwasInputsExample) + entry <- inputs$weights[[1L]] + ids <- rownames(entry$wgt) + # cTWAS's own weights' LD for a gene crossing blocks is block-diagonal + # (compute_weight_LD_from_ref): each block's LD, and zero across them. + blockLd <- map(as.character(inputs$LD_map$LD_file), inputs$LD_loader_fun) + R <- matrix(0, length(ids), length(ids), dimnames = list(ids, ids)) + within <- map(blockLd, function(ld) intersect(ids, rownames(ld))) + expect_true(all(lengths(within) > 0L)) + for (b in seq_along(blockLd)) { + R[within[[b]], within[[b]]] <- as.matrix(blockLd[[b]])[ + within[[b]], + within[[b]] + ] + } + weights <- list(list_assign(entry, R_wgt = R)) + theirs <- suppressMessages(ctwas::compute_gene_z( + inputs$z_snp, + setNames(weights, names(inputs$weights)) + )) + expect_equal(inputs$z_gene$z, theirs$z) + expect_equal(inputs$z_gene$group, theirs$group) + expect_equal(inputs$z_gene$id, theirs$id) }) test_that(".ctwasGeneZ gives ctwas::compute_gene_z's gene z-scores", { @@ -1730,21 +2126,22 @@ test_that(".ctwasBuildZGene: each weighted gene's z under its weight id", { twasZ = 1.5 ) entry <- list( - type = "c1", - context = "c1", + type = "QTL", + context = "Q1_c1", + qtl_context = "c1", gene_name = "t1", study = "Q1", method = "susie" ) - weights <- list(`block1|Q1|c1|t1|susie` = entry) + weights <- list(`t1|Q1_c1_QTL_susie` = entry) expect_equal( pecotmr:::.ctwasBuildZGene(gr, weights, NA_character_), data.frame( - id = "block1|Q1|c1|t1|susie", + id = "t1|Q1_c1_QTL_susie", z = 1.5, - type = "c1", - context = "c1", - group = "c1|c1" + type = "QTL", + context = "Q1_c1", + group = "Q1_c1|QTL" ) ) expect_null(pecotmr:::.ctwasBuildZGene(gr, list(), NA_character_)) @@ -1843,8 +2240,11 @@ test_that("assembleCtwasInputs builds ctwas's documented input shape", { expect_equal(nrow(inputs$z_snp), 12L) # snp_map keyed by region_id. expect_setequal(names(inputs$snp_map), c("block1", "block2")) - # Per-region weights keys are prefixed with the region_id. - expect_true(all(grepl("^(block1|block2)\\|", names(inputs$weights)))) + # Keyed as cTWAS keys its weights: the trait first, then the weight's name. + expect_setequal( + names(inputs$weights), + c("t1|Q1_c1_QTL_susie", "t2|Q1_c1_QTL_susie") + ) # LD_map carries the same number of rows as regions. expect_equal(nrow(inputs$LD_map), 2L) # LD / snpInfo loader closures are present. @@ -1854,11 +2254,14 @@ test_that("assembleCtwasInputs builds ctwas's documented input shape", { test_that("assembleCtwasInputs: forwards a twasZ argument as z_gene", { inp <- .ctp_makeMultiBlockInputs() - twasZ <- GenomicRanges::GRanges("chr1", IRanges::IRanges(100, 200)) + twasZ <- GenomicRanges::GRanges( + c("chr1", "chr1"), + IRanges::IRanges(c(100, 200), width = 100L) + ) S4Vectors::mcols(twasZ) <- S4Vectors::DataFrame( qtlStudy = "Q1", context = "c1", - trait = "t1", + trait = c("t1", "t2"), method = "susie", twasZ = 1.5 ) @@ -1877,13 +2280,13 @@ test_that("assembleCtwasInputs: forwards a twasZ argument as z_gene", { expect_equal(inputs$z_gene$id, names(inputs$weights)) expect_equal(inputs$z_gene$z, rep(1.5, length(inputs$weights))) expect_named(inputs$z_gene, c("id", "z", "type", "context", "group")) - expect_equal(unique(inputs$z_gene$group), "c1|c1") + expect_equal(unique(inputs$z_gene$group), "Q1_c1|QTL") }) # A twasZ as causalInferencePipeline() returns it: one row per (qtlStudy, # context, trait, method, gwasStudy). .ctp_twasZ <- function(method, gwasStudy, z) { - n <- length(method) + n <- 2L * length(method) twasZ <- GenomicRanges::GRanges( rep("chr1", n), IRanges::IRanges(rep(100L, n), width = 100L) @@ -1891,10 +2294,10 @@ test_that("assembleCtwasInputs: forwards a twasZ argument as z_gene", { S4Vectors::mcols(twasZ) <- S4Vectors::DataFrame( qtlStudy = rep("Q1", n), context = rep("c1", n), - trait = rep("t1", n), - method = method, - gwasStudy = gwasStudy, - twasZ = z + trait = rep(c("t1", "t2"), each = length(method)), + method = rep(method, 2L), + gwasStudy = rep(gwasStudy, length.out = n), + twasZ = rep(z, length.out = n) ) twasZ } @@ -1961,7 +2364,7 @@ test_that("ctwasPipeline runs assemble, est, screen, finemap; keeps state", { local_mocked_bindings( assemble_region_data = function(...) { capturedAssemble <<- list(...) - list(block1 = list(stub = TRUE)) + list(block1 = list(stub = TRUE, groups = "Q1_c1|QTL")) }, get_boundary_genes = function(...) { data.frame(id = "t1", n_regions = 2L) @@ -1987,14 +2390,14 @@ test_that("ctwasPipeline runs assemble, est, screen, finemap; keeps state", { # CtwasResult rows finds a matching finemap payload. list( finemap_res = data.frame( - id = c("block1|Q1|c1|t1|susie", "block2|Q1|c1|t1|susie"), + id = c("t1|Q1_c1_QTL_susie", "t2|Q1_c1_QTL_susie"), type = c("gene", "gene"), context = c("c1", "c1"), susie_pip = c(0.9, 0.8), stringsAsFactors = FALSE ), susie_alpha_res = data.frame( - id = c("block1|Q1|c1|t1|susie", "block2|Q1|c1|t1|susie"), + id = c("t1|Q1_c1_QTL_susie", "t2|Q1_c1_QTL_susie"), type = c("gene", "gene"), susie_alpha = c(0.9, 0.8), stringsAsFactors = FALSE @@ -2010,6 +2413,7 @@ test_that("ctwasPipeline runs assemble, est, screen, finemap; keeps state", { out <- ctwasPipeline( gwasSumStats = inp$gwasSumStats, twasWeights = inp$twasWeights, + boundaryMergeParam = BoundaryMergeParam(enabled = FALSE), method = "susie" ) # Each ctwas step was invoked exactly once. @@ -2041,7 +2445,7 @@ test_that("ctwasPipeline runs assemble, est, screen, finemap; keeps state", { c("gwasStudy", "study", "context", "method", "susie_pip") %in% names(S4Vectors::mcols(fm)) )) - expect_setequal(fm$id, c("block1|Q1|c1|t1|susie", "block2|Q1|c1|t1|susie")) + expect_setequal(fm$id, c("t1|Q1_c1_QTL_susie", "t2|Q1_c1_QTL_susie")) # susieAlpha aggregates the fuller per-effect table, likewise tagged. sa <- susieAlpha(out) expect_equal(length(sa), 2L) @@ -2055,7 +2459,9 @@ test_that("ctwasPipeline keepSnps keeps the SNP background as its own row", { skip_if_not_installed("ctwas") inp <- .ctp_makeMultiBlockInputs() local_mocked_bindings( - assemble_region_data = function(...) list(block1 = list(stub = TRUE)), + assemble_region_data = function(...) { + list(block1 = list(stub = TRUE, groups = "Q1_c1|QTL")) + }, get_boundary_genes = function(...) { data.frame(id = "t1", n_regions = 2L) }, @@ -2074,13 +2480,13 @@ test_that("ctwasPipeline keepSnps keeps the SNP background as its own row", { finemap_regions = function(...) { list( finemap_res = data.frame( - id = c("block1|Q1|c1|t1|susie", "chr1:100:G:A"), + id = c("t1|Q1_c1_QTL_susie", "chr1:100:G:A"), type = c("gene", "SNP"), susie_pip = c(0.9, 0.02), stringsAsFactors = FALSE ), susie_alpha_res = data.frame( - id = c("block1|Q1|c1|t1|susie", "chr1:100:G:A"), + id = c("t1|Q1_c1_QTL_susie", "chr1:100:G:A"), type = c("gene", "SNP"), susie_alpha = c(0.9, 0.02), stringsAsFactors = FALSE @@ -2096,6 +2502,7 @@ test_that("ctwasPipeline keepSnps keeps the SNP background as its own row", { out <- ctwasPipeline( inp$gwasSumStats, inp$twasWeights, + boundaryMergeParam = BoundaryMergeParam(enabled = FALSE), keepSnps = TRUE, method = "susie" ) @@ -2108,11 +2515,11 @@ test_that("ctwasPipeline keepSnps keeps the SNP background as its own row", { }) # =========================================================================== -# mergeCtwasBoundaryRegions + ctwasPipeline(mergeBoundary = TRUE) +# mergeCtwasBoundaryRegions + ctwasPipeline(boundaryMergeParam) # =========================================================================== .ctp_finemapResult <- function(hasLd = TRUE) { fm <- data.frame( - id = "block1|Q1|c1|t1|susie", + id = "t1|Q1_c1_QTL_susie", type = "gene", susie_pip = 0.90, stringsAsFactors = FALSE @@ -2128,11 +2535,11 @@ test_that("ctwasPipeline keepSnps keeps the SNP background as its own row", { stop = 1000 ), z_snp = data.frame(id = "chr1:100:G:A", z = 1.0), - z_gene = data.frame(id = "block1|Q1|c1|t1|susie", z = 2.0), + z_gene = data.frame(id = "t1|Q1_c1_QTL_susie", z = 2.0), # Placed by chrom / p0 / p1, as ctwas's own weights are. - weights = setNames( - list(list(wgt = 1, chrom = 1, p0 = 100L, p1 = 100L)), - "block1|Q1|c1|t1|susie" + weights = map( + .ctp_weights("Q1|c1|t1|susie"), + function(w) c(w, list(wgt = 1, chrom = 1, p0 = 100L, p1 = 100L)) ), snp_map = list(block1 = data.frame(id = "chr1:100:G:A")), param = list( @@ -2156,13 +2563,13 @@ test_that("ctwasPipeline keepSnps keeps the SNP background as its own row", { .ctp_mergeReturn <- function() { list( updated_finemap_res = data.frame( - id = "block1|Q1|c1|t1|susie", + id = "t1|Q1_c1_QTL_susie", type = "gene", susie_pip = 0.99, stringsAsFactors = FALSE ), updated_susie_alpha_res = data.frame( - id = "block1|Q1|c1|t1|susie", + id = "t1|Q1_c1_QTL_susie", susie_alpha = 0.99 ), updated_region_data = list(merged = TRUE), @@ -2172,6 +2579,33 @@ test_that("ctwasPipeline keepSnps keeps the SNP background as its own row", { ) } +test_that("mergeCtwasBoundaryRegions names a merged gene by its trait", { + skip_if_not_installed("ctwas") + # cTWAS reads the molecular_id off the id's first field, which stops short + # of a trait holding a "|", on the rows it fine-maps again as on its first + # pass. + local_mocked_bindings( + postprocess_region_merging = function(...) { + merged <- .ctp_mergeReturn() + list_assign( + merged, + updated_finemap_res = mutate( + merged$updated_finemap_res, + molecular_id = "block1" + ), + updated_susie_alpha_res = mutate( + merged$updated_susie_alpha_res, + molecular_id = "block1" + ) + ) + }, + .package = "ctwas" + ) + out <- mergeCtwasBoundaryRegions(.ctp_finemapResult(hasLd = TRUE)) + expect_equal(out$finemap_res$molecular_id, "t1") + expect_equal(out$susie_alpha_res$molecular_id, "t1") +}) + test_that("mergeCtwasBoundaryRegions passes no unnamed argument to ctwas", { skip_if_not_installed("ctwas") # Regression: methodArgs was merged with c(), which appends a MethodOptions @@ -2219,12 +2653,14 @@ test_that("mergeCtwasBoundaryRegions without LD uses the noLD merge", { expect_equal(out$finemap_res$susie_pip, 0.99) }) -test_that("ctwasPipeline mergeBoundary re-fine-maps into the CtwasResult", { +test_that("ctwasPipeline's merge step re-fine-maps into the CtwasResult", { skip_if_not_installed("ctwas") inp <- .ctp_makeMultiBlockInputs() merged <- FALSE local_mocked_bindings( - assemble_region_data = function(...) list(block1 = list(stub = TRUE)), + assemble_region_data = function(...) { + list(block1 = list(stub = TRUE, groups = "Q1_c1|QTL")) + }, get_boundary_genes = function(...) { data.frame(id = "t1", n_regions = 2L) }, @@ -2243,13 +2679,13 @@ test_that("ctwasPipeline mergeBoundary re-fine-maps into the CtwasResult", { finemap_regions = function(...) { list( finemap_res = data.frame( - id = "block1|Q1|c1|t1|susie", + id = "t1|Q1_c1_QTL_susie", type = "gene", susie_pip = 0.90, stringsAsFactors = FALSE ), susie_alpha_res = data.frame( - id = "block1|Q1|c1|t1|susie", + id = "t1|Q1_c1_QTL_susie", susie_alpha = 0.90 ) ) @@ -2258,13 +2694,13 @@ test_that("ctwasPipeline mergeBoundary re-fine-maps into the CtwasResult", { merged <<- TRUE list( updated_finemap_res = data.frame( - id = "block1|Q1|c1|t1|susie", + id = "t1|Q1_c1_QTL_susie", type = "gene", susie_pip = 0.99, stringsAsFactors = FALSE ), updated_susie_alpha_res = data.frame( - id = "block1|Q1|c1|t1|susie", + id = "t1|Q1_c1_QTL_susie", susie_alpha = 0.99 ) ) @@ -2322,10 +2758,7 @@ test_that("asCtwasResult: keepSnps adds the dedicated SNP row", { test_that("asCtwasResult: errors when the weights mix methods", { fmr <- .ctp_finemapResult() - fmr$weights <- setNames( - list(1, 2), - c("block1|Q1|c1|t1|susie", "block1|Q1|c1|t2|lasso") - ) + fmr$weights <- .ctp_weights(c("Q1|c1|t1|susie", "Q1|c1|t2|lasso")) expect_error(asCtwasResult(fmr), "mixes weight methods") }) @@ -2341,6 +2774,7 @@ test_that("fallbackToPrefit recovers from accurate-EM NaN divergence", { block1 = list( gid = "t1", sid = c("s1", "s2"), + groups = "Q1_c1|QTL", stub = TRUE ) ) @@ -2406,7 +2840,11 @@ test_that("the prior fallback drops degenerate regions before fit_EM", { assemble_region_data = function(...) { list( # 1 gene + 2 SNPs -> kept - good = list(gid = "t1", sid = c("s1", "s2")), + good = list( + gid = "t1", + sid = c("s1", "s2"), + groups = "Q1_c1|QTL" + ), degenerate = list(gid = character(0), sid = NULL) ) }, # no variables -> dropped @@ -2485,7 +2923,9 @@ test_that("the three ctwas stages can each be called on their own", { skip_if_not_installed("ctwas") inp <- .ctp_makeMultiBlockInputs() local_mocked_bindings( - assemble_region_data = function(...) list(block1 = list(stub = TRUE)), + assemble_region_data = function(...) { + list(block1 = list(stub = TRUE, groups = "Q1_c1|QTL")) + }, get_boundary_genes = function(...) { data.frame(id = "t1", n_regions = 2L) }, @@ -2538,6 +2978,7 @@ test_that("the three ctwas stages can each be called on their own", { c( "z_gene", "param", + "gwas_n", "finemap_res", "susie_alpha_res", "region_data", @@ -2561,6 +3002,381 @@ test_that("the three ctwas stages can each be called on their own", { # (mocked tests only construct the closures, never invoke them). # =========================================================================== +# The bundled GWAS split across two LD blocks by position: ctwasPipeline needs +# >= 2 blocks for the joint EM to have something to estimate against. +.ctp_twoBlockGwas <- function(gss) { + gssRanges <- range(unlist(gss)) + mid <- start(gssRanges) + floor(IRanges::width(gssRanges) / 2) + twoBlocks <- GenomicRanges::GRanges( + as.character(seqnames(gssRanges)), + IRanges::IRanges( + c(start(gssRanges), mid + 1L), + c(mid, end(gssRanges)) + ) + ) + names(twoBlocks) <- c("blockA", "blockB") + GwasSumStats( + studyName = studyName(gss), + entry = list(unlist(gss)), + genome = unique(unname(GenomeInfoDb::genome(gss))), + ldReference = ldReference(gss), + qcOptions = qcOptions(gss), + region = twoBlocks + ) +} + +# Synthetic weights for the rows of `spec` (context, trait, first: the panel +# index its five variants start at), on the bundled panel `gh`. Each gene is +# anchored on the span of its variants, and its weights differ by context. +.ctp_contextWeights <- function(spec, gh, types = NULL, studies = NULL) { + info <- .ctp_panelSnpInfo(gh) + variants <- map(spec$first, function(f) seq(f, length.out = 5L)) + weights <- ifelse(spec$context == "brain", 1, -1) + bp <- map(variants, function(i) info$BP[i]) + recorded <- if (is.null(types)) { + rep(list(NULL), nrow(spec)) + } else { + as.list(unname(types[spec$context])) + } + TwasWeights( + studyName = studies %||% rep("study1", nrow(spec)), + context = spec$context, + trait = spec$trait, + method = rep("susie", nrow(spec)), + entry = pmap( + list(variants, weights, recorded), + function(i, sign, type) { + TwasWeightsRow( + variantIds = info$SNP[i], + weights = sign * c(0.1, -0.2, 0.05, 0.15, 0.3), + weightsDataType = type + ) + } + ), + traitPos = GenomicRanges::GRanges( + str_c("chr", info$CHR[[1L]]), + IRanges::IRanges(map_int(bp, min), map_int(bp, max)) + ), + ldReference = gh + ) +} + +# The real engine on those weights, its gates relaxed for the toy panel. No +# thinning: it can empty the panel's 8-SNP block, which ctwas cannot annotate. +# `...` goes to CtwasPriorParam(). +.ctp_contextRun <- function( + spec, + minGroupSize = 1L, + estMinGroupSize = 1L, + types = NULL, + studies = NULL, + ... +) { + data(gwasSumStatsS4Example, envir = environment()) + data(qtlDatasetExample, envir = environment()) + ctwasPipeline( + gwasSumStats = .ctp_twoBlockGwas(gwasSumStatsS4Example), + twasWeights = .ctp_contextWeights( + spec, + pecotmr:::.qtlGenotypeSe(qtlDatasetExample), + types = types, + studies = studies + ), + ctwasPriorParam = CtwasPriorParam( + thin = 1, + niter = 5L, + niterPrefit = 2L, + ... + ), + methodArgs = CtwasOptions( + regionData = CtwasRegionDataOptions(min_group_size = minGroupSize), + estParam = CtwasEstParamOptions( + min_group_size = estMinGroupSize, + min_p_single_effect = 0 + ) + ), + method = "susie" + ) +} + +test_that("ctwasPipeline fits contexts that hold different genes", { + skip_if_not_installed("ctwas") + spec <- data.frame( + context = c("brain", "brain", "brain", "liver", "liver"), + trait = c("geneA", "geneB", "geneC", "geneB", "geneD"), + first = c(1L, 2L, 9L, 2L, 10L) + ) + res <- suppressMessages(suppressWarnings(.ctp_contextRun(spec))) + expect_setequal(contexts(res), c("brain", "liver")) + expect_true(all( + S4Vectors::mcols(res)$jointContexts == "brain,liver" + )) + # Each context's prior is over its own genes. + priors <- ctwasGroupPriors(res) + brainRun <- priors[priors$context == "brain", ] + sizes <- setNames(brainRun$group_size, brainRun$group) + expect_equal(sizes[["study1_brain_QTL"]], 3) + expect_equal(sizes[["study1_liver_QTL"]], 2) + # Whichever genes pass the region screen stay in their own context. + fm <- S4Vectors::mcols(ctwasPosteriors(res)) + expect_gt(nrow(fm), 0L) + expect_true(all( + fm$molecular_id[fm$context == "brain"] %in% c("geneA", "geneB", "geneC") + )) + expect_true(all( + fm$molecular_id[fm$context == "liver"] %in% c("geneB", "geneD") + )) +}) + +test_that("gene contexts share a prior variance unless asked otherwise", { + skip_if_not_installed("ctwas") + spec <- data.frame( + context = c("brain", "brain", "brain", "liver", "liver"), + trait = c("geneA", "geneB", "geneC", "geneB", "geneD"), + first = c(1L, 2L, 9L, 2L, 10L) + ) + variances <- function(...) { + res <- suppressMessages(suppressWarnings(.ctp_contextRun(spec, ...))) + priors <- ctwasGroupPriors(res) + run <- priors[priors$context == "brain", ] + setNames(run$group_prior_var, run$group) + } + # By default the two contexts pool, and the variants stay apart. + pooled <- variances() + brain <- "study1_brain_QTL" + liver <- "study1_liver_QTL" + expect_equal(pooled[[brain]], pooled[[liver]]) + expect_false(isTRUE(all.equal(pooled[[brain]], pooled[["SNP"]]))) + own <- variances(varStructure = "independent") + expect_false(isTRUE(all.equal(own[[brain]], own[[liver]]))) + shared <- variances(varStructure = "shared_all") + expect_equal(shared[[brain]], shared[["SNP"]]) + # All one type, shared_type pools them too, and each context alone is its + # own group under shared_context. + expect_equal(variances(varStructure = "shared_type"), pooled) + expect_equal(variances(varStructure = "shared_context"), own) +}) + +test_that("shared_type pools the groups of a type, on a run's real labels", { + skip_if_not_installed("ctwas") + spec <- data.frame( + context = c("brain", "brain", "brain", "liver", "liver", "lung"), + trait = c("geneA", "geneB", "geneC", "geneB", "geneD", "geneE"), + first = c(1L, 2L, 9L, 2L, 10L, 3L) + ) + types <- c(brain = "expression", liver = "expression", lung = "splicing") + res <- suppressMessages(suppressWarnings( + .ctp_contextRun(spec, types = types, varStructure = "shared_type") + )) + priors <- ctwasGroupPriors(res) + run <- priors[priors$context == "brain", ] + variances <- setNames(run$group_prior_var, run$group_context) + expect_equal(variances[["brain"]], variances[["liver"]]) + expect_false(isTRUE(all.equal(variances[["brain"]], variances[["lung"]]))) + expect_equal( + setNames(run$group_type, run$group_context)[c("brain", "lung", "SNP")], + c(brain = "expression", lung = "splicing", SNP = "SNP") + ) + expect_true("study1_lung_splicing" %in% run$group) +}) + +test_that("two studies with one context name stay apart, end to end", { + skip_if_not_installed("ctwas") + spec <- data.frame( + context = c("brain", "brain", "brain", "brain", "brain"), + trait = c("geneA", "geneB", "geneC", "geneB", "geneD"), + first = c(1L, 2L, 9L, 2L, 10L) + ) + studies <- c("gtex", "gtex", "gtex", "cmc", "cmc") + res <- suppressMessages(suppressWarnings( + .ctp_contextRun(spec, studies = studies) + )) + md <- S4Vectors::mcols(res) + expect_setequal(md$study, c("gtex", "cmc")) + expect_true(all(md$jointStudies == "cmc,gtex")) + expect_false("jointContexts" %in% colnames(md)) + priors <- ctwasGroupPriors(res) + expect_setequal( + priors$group, + c("gtex_brain_QTL", "cmc_brain_QTL", "SNP") + ) + expect_equal( + setNames(priors$group_size, priors$group)[["gtex_brain_QTL"]], + 3 + ) + expect_equal( + setNames(priors$group_size, priors$group)[["cmc_brain_QTL"]], + 2 + ) + post <- S4Vectors::mcols(ctwasPosteriors(res)) + expect_true(all( + post$molecular_id[post$study == "cmc"] %in% c("geneB", "geneD") + )) + expect_true(all(is_in(post$group, unique(priors$group)))) +}) + +test_that("a block with no gene weights feeds the priors only if asked", { + skip_if_not_installed("ctwas") + data(ctwasInputsExample, envir = environment()) + # One gene, fine-mapped in one of the two blocks. cTWAS logs what it uses. + expect_length(ctwasInputsExample$weights, 1L) + used <- function(...) { + log <- tempfile(fileext = ".log") + suppressMessages(suppressWarnings(estCtwasGroupPriors( + ctwasInputsExample, + ctwasPriorParam = CtwasPriorParam( + thin = 1, + niterPrefit = 2L, + niter = 3L + ), + methodArgs = CtwasOptions( + regionData = CtwasRegionDataOptions(min_group_size = 1L), + estParam = CtwasEstParamOptions( + min_group_size = 1L, + min_p_single_effect = 0, + logfile = log, + ... + ) + ) + ))) + grep("Using data in", readLines(log), value = TRUE) + } + expect_match(used(), "Using data in 1 regions", all = TRUE) + expect_match(used(min_gene = 0L), "Using data in 2 regions", all = TRUE) +}) + +test_that("a context too small for cTWAS is named when it is left out", { + skip_if_not_installed("ctwas") + spec <- data.frame( + context = c("brain", "brain", "brain", "liver"), + trait = c("geneA", "geneB", "geneC", "geneB"), + first = c(1L, 2L, 9L, 2L) + ) + expect_warning( + res <- suppressMessages(.ctp_contextRun(spec, minGroupSize = 2L)), + "liver \\(1 gene\\(s\\)\\)" + ) + # Its row stays, as every modeled context's does, with nothing fit. + expect_setequal(contexts(res), c("brain", "liver")) + expect_false("study1_liver_QTL" %in% ctwasGroupPriors(res)$group) + expect_false( + "liver" %in% S4Vectors::mcols(ctwasPosteriors(res))$context + ) +}) + +test_that("keeping a small context takes both of cTWAS's size limits", { + skip_if_not_installed("ctwas") + spec <- data.frame( + context = c("brain", "brain", "brain", "liver"), + trait = c("geneA", "geneB", "geneC", "geneB"), + first = c(1L, 2L, 9L, 2L) + ) + # brain clears 2 and liver does not: est_param stops, it does not drop. + expect_error( + suppressMessages(.ctp_contextRun(spec, estMinGroupSize = 2L)), + "too few variables" + ) + res <- suppressMessages(suppressWarnings(.ctp_contextRun(spec))) + expect_true("study1_liver_QTL" %in% ctwasGroupPriors(res)$group) +}) + +test_that("cTWAS fine-maps a boundary gene where most of its weight lies", { + skip_if_not_installed("ctwas") + data(ctwasInputsExample, envir = environment()) + entry <- ctwasInputsExample$weights[[1L]] + # This gene's weights reach both of the example's blocks. + blocks <- ctwasInputsExample$snp_map + inBlock <- map_dbl(blocks, function(b) { + sum(abs(entry$wgt[rownames(entry$wgt) %in% b$id, ])) + }) + expect_true(all(inBlock > 0)) + est <- suppressMessages(suppressWarnings(estCtwasGroupPriors( + ctwasInputsExample, + ctwasPriorParam = CtwasPriorParam( + thin = 1, + niterPrefit = 2L, + niter = 3L + ), + methodArgs = CtwasOptions( + regionData = CtwasRegionDataOptions(min_group_size = 1L), + estParam = CtwasEstParamOptions( + min_group_size = 1L, + min_p_single_effect = 0 + ) + ) + ))) + holders <- names(keep( + est$region_data, + function(r) names(ctwasInputsExample$weights) %in% r$gid + )) + expect_identical(holders, names(which.max(inBlock))) + expect_equal(nrow(est$boundary_genes), 1L) +}) + +test_that("merging re-fine-maps a boundary gene over both blocks, for real", { + skip_if_not_installed("ctwas") + data(ctwasFinemapExample, envir = environment()) + id <- names(ctwasFinemapExample$weights) + # Any PIP selects the example's one gene, whose weights cross its two + # blocks. + merged <- suppressMessages(suppressWarnings( + mergeCtwasBoundaryRegions(ctwasFinemapExample, pipThresh = 0) + )) + expect_equal(merged$merge_res$selected_boundary_genes$id, id) + expect_equal(nrow(merged$region_info), 1L) + # The merged region's LD reads both blocks, whose variants all enter. + expect_equal( + length(merged$snp_map[[1L]]$id), + sum(lengths(map(ctwasFinemapExample$snp_map, "id"))) + ) + genes <- merged$finemap_res[merged$finemap_res$id == id, ] + expect_equal(nrow(genes), 1L) + expect_equal( + genes$molecular_id, + ctwasFinemapExample$weights[[1L]]$molecular_id + ) + expect_true(is.finite(genes$susie_pip)) + expect_equal(genes$group, ctwasFinemapExample$z_gene$group) + # And the structured result covers the merged span. + res <- asCtwasResult(merged) + expect_equal(length(res), 1L) + expect_gt( + IRanges::width(S4Vectors::mcols(res)$regionInfo[[1L]]), + IRanges::width( + S4Vectors::mcols(asCtwasResult(ctwasFinemapExample))$regionInfo[[ + 1L + ]] + ) + ) + # With nothing above the threshold, ctwas leaves the run as it was. + untouched <- suppressMessages(suppressWarnings( + mergeCtwasBoundaryRegions(ctwasFinemapExample, pipThresh = 0.5) + )) + expect_equal(nrow(untouched$merge_res$selected_boundary_genes), 0L) + expect_equal(untouched$finemap_res, ctwasFinemapExample$finemap_res) +}) + +test_that("ctwasPipeline merges by default, and can be told not to", { + skip_if_not_installed("ctwas") + expect_true(BoundaryMergeParam()$enabled) + spec <- data.frame( + context = c("brain", "brain", "brain"), + trait = c("geneA", "geneB", "geneC"), + first = c(1L, 2L, 9L) + ) + called <- FALSE + real <- ctwas::postprocess_region_merging + local_mocked_bindings( + postprocess_region_merging = function(...) { + called <<- TRUE + real(...) + }, + .package = "ctwas" + ) + suppressMessages(suppressWarnings(.ctp_contextRun(spec))) + expect_true(called) +}) + test_that("ctwasPipeline end to end on the real engine and bundled panel", { skip_if_not_installed("ctwas") data(gwasSumStatsS4Example) @@ -2582,8 +3398,8 @@ test_that("ctwasPipeline end to end on the real engine and bundled panel", { variantIds = .ctp_panelSnpInfo(gh)$SNP[9:13], weights = c(0.2, 0.1, -0.15, 0.05, 0.0) ) - # A FLAT weight source is placed into blocks by `traitPos`, so each gene - # needs the span it sits in. + # A FLAT weight source is placed into blocks by its weights; `traitPos` + # gives each gene's coordinates in the results. geneSpan <- function(i) { bp <- .ctp_panelSnpInfo(gh)$BP[i] GenomicRanges::GRanges( @@ -2601,29 +3417,9 @@ test_that("ctwasPipeline end to end on the real engine and bundled panel", { ldReference = gh ) - # ctwasPipeline needs >= 2 blocks for the joint EM to have something to - # estimate against. The GWAS variants are split across two blocks by - # position; the same TWAS weights are offered to both. The bundled toy - # panel is still too sparse for the convergence checks, so we relax the - # gates. - gssRanges <- range(unlist(gss)) - mid <- start(gssRanges) + floor(IRanges::width(gssRanges) / 2) - twoBlocks <- GenomicRanges::GRanges( - as.character(seqnames(gssRanges)), - IRanges::IRanges( - c(start(gssRanges), mid + 1L), - c(mid, end(gssRanges)) - ) - ) - names(twoBlocks) <- c("blockA", "blockB") - gssTwoBlocks <- GwasSumStats( - studyName = studyName(gss), - entry = list(unlist(gss)), - genome = unique(unname(GenomeInfoDb::genome(gss))), - ldReference = ldReference(gss), - qcOptions = qcOptions(gss), - region = twoBlocks - ) + # The same TWAS weights are offered to both blocks. The bundled toy panel is + # still too sparse for the convergence checks, so we relax the gates. + gssTwoBlocks <- .ctp_twoBlockGwas(gss) res <- suppressMessages(suppressWarnings( ctwasPipeline( gwasSumStats = gssTwoBlocks, @@ -2660,15 +3456,13 @@ test_that("ctwasPipeline end to end on the real engine and bundled panel", { c("gwasStudy", "study", "context", "method", "susie_pip") %in% names(S4Vectors::mcols(fm)) )) - # Either gene may survive ctwas's region screen; what matters here is that - # a fine-mapped gene carries the full run identity in its id. - # ids are "|study|context|trait|method". Either gene may survive - # ctwas's region screen, so this pins the identity tagging rather than - # which gene came through. + # Either gene may survive ctwas's region screen, so this pins the identity + # tagging rather than which gene came through. expect_true(any(grepl( - "\\|study1\\|brain\\|ENSG_example2?\\|susie$", + "^ENSG_example2?\\|study1_brain_QTL_susie$", fm$id ))) + expect_true(all(fm$molecular_id %in% c("ENSG_example", "ENSG_example2"))) expect_true(all(fm$context == "brain")) # The fuller per-effect table is retained and reconstructable. sa <- susieAlpha(res) @@ -2812,7 +3606,7 @@ test_that(".ctwasBuildWeights: the variant cap and weight cutoff apply", { ldReference = gh ) secondCase <- cs[[2L]] - wl <- exec(pecotmr:::.ctwasBuildWeights, tw, ldPanel, !!!secondCase) + wl <- exec(.ctp_buildWeights, tw, ldPanel, !!!secondCase) expect_equal(wl[[1L]]$n_wgt, 3L, info = nm) expect_equal(nrow(wl[[1L]]$wgt), 3L, info = nm) expect_equal( @@ -2898,20 +3692,44 @@ test_that("boundary merge with LD carries state and splices updated_*", { # guard would surface a different error). # =========================================================================== -test_that("an unnamed twasWeights list is a flat source needing a region", { - skip_if_not_installed("ctwas") - ss <- .ctp_makeGwasSumstats() - tw <- .ctp_makeTwasWeights() # no region provenance - # An unnamed list is now treated as a flat weight source to place by region; - # without region provenance that placement can't happen. - expect_error( - assembleCtwasInputs( - gwasSumStats = ss, - twasWeights = list(tw), - method = "susie" - ), # unnamed -> flat - "no `traitPos` provenance" +test_that("a run matches its genes' variants to the panel once", { + # Each block holds a gene, and the answer is the same for all of them. + matched <- 0L + real <- pecotmr:::.ctwasPanelSnpsForGwas + local_mocked_bindings( + .handleDosage = .ctp_mockExtractor(), + .ctwasPanelSnpsForGwas = function(...) { + matched <<- matched + 1L + real(...) + }, + .package = "pecotmr" + ) + inputs <- suppressWarnings(assembleCtwasInputs( + gwasSumStats = .ctp_makeGwasSumstats(), + twasWeights = list( + .ctp_makeTwasWeights(1:5, "t1"), + .ctp_makeTwasWeights(7:11, "t2") + ), + method = "susie" + )) + expect_setequal( + names(inputs$weights), + c("t1|Q1_c1_QTL_susie", "t2|Q1_c1_QTL_susie") + ) + expect_equal(matched, 1L) +}) + +test_that("an unnamed twasWeights list is a flat source", { + local_mocked_bindings( + .handleDosage = .ctp_mockExtractor(), + .package = "pecotmr" ) + inputs <- suppressWarnings(assembleCtwasInputs( + gwasSumStats = .ctp_makeGwasSumstats(), + twasWeights = list(.ctp_makeTwasWeights()), + method = "susie" + )) + expect_identical(names(inputs$weights), "t1|Q1_c1_QTL_susie") }) test_that("assembleCtwasInputs: rejects a non-GwasSumStats gwas input", { @@ -2999,9 +3817,7 @@ test_that("assembleCtwasInputs skips a block lacking the resolved method", { gwasSumStats = ss, twasWeights = list(block1 = mkTw("ensemble"), block2 = mkTw("mrash")) ) - expect_length(inputs$weights, 1L) - expect_true(all(grepl("^block1\\|", names(inputs$weights)))) - expect_false(any(grepl("^block2\\|", names(inputs$weights)))) + expect_identical(names(inputs$weights), "t1|Q1_c1_QTL_ensemble") }) # =========================================================================== @@ -3065,7 +3881,7 @@ test_that(".ctwasBuildWeights skips a gene harmonization leaves empty", { .package = "pecotmr" ) expect_length( - pecotmr:::.ctwasBuildWeights( + .ctp_buildWeights( .ctp_makeTwasWeights(), .ctp_makeLdPanel() ), @@ -3075,7 +3891,7 @@ test_that(".ctwasBuildWeights skips a gene harmonization leaves empty", { test_that(".ctwasBuildWeights skips a gene with no variant in gwasSnpIds", { expect_length( - pecotmr:::.ctwasBuildWeights( + .ctp_buildWeights( .ctp_makeTwasWeights(), .ctp_makeLdPanel(), gwasSnpIds = .ctp_snpId(6) @@ -3113,7 +3929,7 @@ test_that(".ctwasBuildWeights renormalizes SuSiE when variants are dropped", { entry = list(ent), ldReference = .ctp_makeHandle() ) - wl <- pecotmr:::.ctwasBuildWeights(tw, panel) + wl <- .ctp_buildWeights(tw, panel) expect_equal(wl[[1L]]$n_wgt, 4L) expect_equal( unname(wl[[1L]]$wgt[ids4, 1L]), @@ -3126,14 +3942,14 @@ test_that(".ctwasBuildWeights errors on a kept variant missing its variance", { panel <- .ctp_makeLdPanel() panel$variance <- panel$variance[1:4] # drop variance for ids 5, 6 expect_error( - pecotmr:::.ctwasBuildWeights(.ctp_makeTwasWeights(), panel), + .ctp_buildWeights(.ctp_makeTwasWeights(), panel), "missing genotype variance" ) }) test_that(".ctwasBuildWeights skips a gene the filter empties", { expect_length( - pecotmr:::.ctwasBuildWeights( + .ctp_buildWeights( .ctp_makeTwasWeights(), .ctp_makeLdPanel(), twasWeightCutoff = 1.0 @@ -3255,7 +4071,14 @@ test_that(".ctwasFixMolecularId: rows without a gene id are left alone", { id = map_chr(1:2, .ctp_snpId), molecular_id = c("x", "y") ) - expect_identical(pecotmr:::.ctwasFixMolecularId(snps), snps) + expect_identical(pecotmr:::.ctwasFixMolecularId(snps, list()), snps) + # A trait whose id holds a "|" is read back from its record. + weights <- list(`a|b|Q1_c1_QTL_susie` = list(molecular_id = "a|b")) + fixed <- pecotmr:::.ctwasFixMolecularId( + data.frame(id = names(weights), molecular_id = "a"), + weights + ) + expect_equal(fixed$molecular_id, "a|b") }) test_that(".ctwasGetFinemapAux: NULL input, no match or empty topLoci: NULL", { @@ -3370,15 +4193,14 @@ test_that(".ctwasRowsToResult / .ctwasMethodFromWeights: empty-input guards", { expect_error(pecotmr:::.ctwasRowsToResult(list()), "no genes were modeled") expect_error(pecotmr:::.ctwasMethodFromWeights(NULL), "carries no weights") expect_error( - pecotmr:::.ctwasMethodFromWeights(setNames( - list(1, 2), - c("blk|Q1|c1|gA|susie", "blk|Q1|c1|gB|lasso") + pecotmr:::.ctwasMethodFromWeights(.ctp_weights( + c("Q1|c1|gA|susie", "Q1|c1|gB|lasso") )), "mixes weight methods" ) }) -test_that(".ctwasRunToRows: no weights, no rows; mixed-study context errors", { +test_that(".ctwasRunToRows: no weights, no rows; two studies, two rows", { expect_equal( pecotmr:::.ctwasRunToRows( list(weights = setNames(list(), character(0))), @@ -3388,74 +4210,111 @@ test_that(".ctwasRunToRows: no weights, no rows; mixed-study context errors", { list() ) run <- list( - weights = setNames( - list(1, 2), - c("blk|Q1|c1|gA|susie", "blk|Q2|c1|gB|susie") - ), + weights = .ctp_weights(c("Q1|c1|gA|susie", "Q2|c1|gB|susie")), finemap_res = NULL, susie_alpha_res = NULL, param = list(), region_info = NULL ) - expect_error( - pecotmr:::.ctwasRunToRows(run, "D1", "susie"), - "mixes multiple QTL studies" - ) + rows <- pecotmr:::.ctwasRunToRows(run, "D1", "susie") + expect_equal(map_chr(rows, "study"), c("Q1", "Q2")) + expect_equal(map_chr(rows, "context"), c("c1", "c1")) + expect_equal(map_chr(rows, "jointStudies"), rep("Q1,Q2", 2L)) + expect_true(all(is.na(map_chr(rows, "jointContexts")))) + result <- pecotmr:::.ctwasRowsToResult(rows) + expect_true(all(S4Vectors::mcols(result)$jointStudies == "Q1,Q2")) + expect_false("jointContexts" %in% pecotmr:::.tupleColumnNames(result)) }) -test_that(".ctwasBucketWeights warns on unplaced genes; skips empty blocks", { - mkE <- function() { - TwasWeightsRow( - variantIds = map_chr(1:5, .ctp_snpId), - weights = c(0.1, 0.05, -0.2, 0.3, 0.0) - ) - } - tw <- TwasWeights( - studyName = c("Q1", "Q1"), - context = c("c1", "c1"), - trait = c("gA", "gX"), - method = c("susie", "susie"), - entry = list(mkE(), mkE()), - traitPos = GenomicRanges::GRanges( - c("chr1", "chr9"), - IRanges::IRanges(c(100, 100), c(150, 150)) - ), - ldReference = .ctp_makeHandle() +test_that(".ctwasScoredParam adds each group's identity and cTWAS's summary", { + skip_if_not_installed("ctwas") + weights <- .ctp_weights(c("Q1|c1|gA|susie", "Q1|c2|gB|susie")) + param <- list( + group_prior = c(`Q1_c1|QTL` = 0.02, `Q1_c2|QTL` = 0.01, SNP = 1e-4), + group_prior_var = c(`Q1_c1|QTL` = 30, `Q1_c2|QTL` = 30, SNP = 10), + group_size = c(`Q1_c1|QTL` = 500, `Q1_c2|QTL` = 300, SNP = 1e6) ) - grid <- list( - chr1_1_350 = .ctp_makeGwasSumstats(), - chr1_350_700 = .ctp_makeGwasSumstats() + scored <- pecotmr:::.ctwasScoredParam( + list(param = param, weights = weights, gwas_n = 1e5) ) + expect_equal(scored$group_prior, param$group_prior) + expect_equal( + scored$group_info, + tibble::tibble( + group = c("Q1_c1|QTL", "Q1_c2|QTL", "SNP"), + study = c("Q1", "Q1", "SNP"), + context = c("c1", "c2", "SNP"), + type = c("QTL", "QTL", "SNP") + ) + ) + expect_equal( + scored$group_summary, + ctwas::summarize_param(param, gwas_n = 1e5) + ) + tab <- pecotmr:::.ctwasPriorTable(scored) + expect_equal(tab$group, c("Q1_c1_QTL", "Q1_c2_QTL", "SNP")) + expect_equal(tab$group_context, c("c1", "c2", "SNP")) + expect_equal(tab$group_type, c("QTL", "QTL", "SNP")) + # By hand: pi V k / n, and its share; the SNP group has no enrichment. + expect_equal(tab$group_pve[[1L]], 0.02 * 30 * 500 / 1e5) + expect_equal(sum(tab$prop_heritability), 1) + expect_equal( + tab$log_enrichment[[1L]], + log(0.02 / 1e-4) + ) + expect_lt(tab$enrichment_p[[1L]], 0.05) + expect_true(is.na(tab$log_enrichment[[3L]])) + expect_true(is.na(tab$enrichment_p[[3L]])) + # Nothing to score: no priors at all. + expect_null(pecotmr:::.ctwasScoredParam(list(param = NULL))) +}) + +test_that(".ctwasGroupSummary is NULL for what cTWAS cannot test", { + param <- list( + group_prior = c(a = 0.02, SNP = 1e-4), + group_prior_var = c(a = 30, SNP = 10), + group_size = c(a = 500, SNP = 1e6) + ) + expect_null(pecotmr:::.ctwasGroupSummary(param[-3L], 1e5)) + expect_null(pecotmr:::.ctwasGroupSummary( + list_modify(param, group_prior = c(a = NaN, SNP = 1e-4)), + 1e5 + )) + expect_null(pecotmr:::.ctwasGroupSummary( + list_modify(param, group_prior = c(a = 0.02)), + 1e5 + )) + skip_if_not_installed("ctwas") + # Without a sample size the enrichment stands; the heritability shares do + # not. + noN <- pecotmr:::.ctwasGroupSummary(param, NULL) + expect_true(is.finite(noN$log_enrichment[["a"]])) + expect_true(is.na(noN$group_pve[["a"]])) +}) + +test_that(".ctwasGwasN is the median, NA when the GWAS records none", { + expect_equal(pecotmr:::.ctwasGwasN(data.frame(N = c(100, 200, 900))), 200) + expect_true(is.na(pecotmr:::.ctwasGwasN(data.frame(z = 1)))) + expect_true(is.na(pecotmr:::.ctwasGwasN(data.frame(N = NA_real_)))) +}) + +test_that(".ctwasBucketWeights warns on unplaced genes; skips empty blocks", { + tw <- .ctp_placedGenes(list("chr1:500", "chr9:100")) b <- expect_warning( - pecotmr:::.ctwasBucketWeights(tw, grid), - "fell in no LD block" + pecotmr:::.ctwasBucketWeights(tw, .ctp_windowGrid()), + "1 gene(s) with no weight in any LD block were dropped", + fixed = TRUE ) - expect_named(b, "chr1_1_350") # 2nd block empty -> skipped - expect_equal(as.character(b[["chr1_1_350"]]$trait), "gA") + expect_named(b, "b1") # the other blocks hold no gene -> skipped + expect_equal(as.character(b[["b1"]]$trait), "g1") }) test_that(".ctwasBucketWeights: errors when no gene lands in any block", { - mkE <- function() { - TwasWeightsRow( - variantIds = map_chr(1:5, .ctp_snpId), - weights = c(0.1, 0.05, -0.2, 0.3, 0.0) - ) - } - twOff <- TwasWeights( - studyName = "Q1", - context = "c1", - trait = "gX", - method = "susie", - entry = list(mkE()), - traitPos = GenomicRanges::GRanges("chr9", IRanges::IRanges(100, 150)), - ldReference = .ctp_makeHandle() - ) - grid <- list( - chr1_1_350 = .ctp_makeGwasSumstats(), - chr1_350_700 = .ctp_makeGwasSumstats() - ) expect_error( - suppressWarnings(pecotmr:::.ctwasBucketWeights(twOff, grid)), + suppressWarnings(pecotmr:::.ctwasBucketWeights( + .ctp_placedGenes(list("chr9:100")), + .ctp_windowGrid() + )), "no gene placed" ) }) @@ -3750,17 +4609,6 @@ test_that("re-keying LD loaders is a no-op when no panels are cached", { ) }) -test_that("a variant is placed in the block whose half-open window holds it", { - # [start, end): an unplaceable or out-of-range anchor is NA rather than - # being snapped to the nearest block. - f <- pecotmr:::.ctwasBlockIdForVariant - expect_equal(f(1L, 50L, "1", "1", 1L, 100L, "B1"), "B1") - expect_true(is.na(f(1L, 500L, "1", "1", 1L, 100L, "B1"))) - expect_true(is.na(f(1L, NA_integer_, "1", "1", 1L, 100L, "B1"))) - # The window excludes its end. - expect_true(is.na(f(1L, 100L, "1", "1", 1L, 100L, "B1"))) -}) - # =========================================================================== # Row coordinates, trait positions and the loader panel cache @@ -4367,7 +5215,7 @@ test_that("an ensemble gene is rescued by the fine-mapping method named", { ) fmr <- .ctp_twoMethodRescue(vids) kept <- function(fineMappingMethod) { - wl <- pecotmr:::.ctwasBuildWeights( + wl <- .ctp_buildWeights( tw, .ctp_fullPanel(gh), fineMappingResult = pecotmr:::.ctwasRescueSource( diff --git a/tests/testthat/test_ctwasWrapper.R b/tests/testthat/test_ctwasWrapper.R index ba7f6639f..3e282d347 100644 --- a/tests/testthat/test_ctwasWrapper.R +++ b/tests/testthat/test_ctwasWrapper.R @@ -201,3 +201,74 @@ test_that("a ctwas step logs warnings only to the console, all to its file", { after <- utils::capture.output(logging::loginfo("after the step")) expect_true(any(grepl("after the step", after))) }) + +test_that("ctwasCombinedPips combines each run's PIPs by gene", { + skip_if_not_installed("ctwas") + data(ctwasFinemapExample, envir = environment()) + res <- asCtwasResult(ctwasFinemapExample) + combined <- ctwasCombinedPips(res) + expect_s3_class(combined, "tbl_df") + # The example's one gene, in one context: combining changes nothing. + gene <- S4Vectors::mcols(ctwasPosteriors(res)) + expect_equal(combined$molecular_id, gene$molecular_id) + expect_equal(combined$combined_pip, gene$susie_pip) + expect_equal(combined$gwasStudy, gene$gwasStudy) + expect_equal(combined$method, gene$method) + expect_equal( + combined$protocol_example_bulk_rnaseq_pip, + gene$susie_pip + ) + # A mapping reads the rows as genes. + mapped <- ctwasCombinedPips( + res, + mapping = data.frame( + molecular_id = gene$molecular_id, + gene_name = "geneA" + ) + ) + expect_equal(mapped$gene_name, "geneA") + expect_equal(mapped$combined_pip, gene$susie_pip) + expect_error(ctwasCombinedPips(list()), "must be a CtwasResult") +}) + +test_that("ctwasCombinedPips keeps runs apart, and studies in its columns", { + skip_if_not_installed("ctwas") + rows <- function(study, gwas) { + at <- IRanges::IRanges(c(100L, 200L), width = 1L) + CtwasResultEntry( + posteriors = GenomicRanges::GRanges( + "chr1", + at, + id = c("g1", "g2"), + region_id = "r1", + susie_pip = c(0.3, 0.2) + ), + susieAlpha = GenomicRanges::GRanges( + "chr1", + at, + id = c("g1", "g2"), + molecular_id = "gene", + region_id = "r1", + group = "g", + type = "QTL", + susie_set = 1L, + susie_alpha = c(0.3, 0.2), + cs = NA_character_, + in_cs = FALSE + ) + ) + } + res <- CtwasResult( + gwasStudy = c("D1", "D1", "D2"), + studyName = c("gtex", "cmc", "gtex"), + context = c("brain", "brain", "brain"), + method = c("susie", "susie", "susie"), + entry = list(rows(), rows(), rows()) + ) + combined <- ctwasCombinedPips(res) + expect_equal(combined$gwasStudy, c("D1", "D2")) + # One effect set across the studies of D1 sums them, D2's holds one study. + expect_equal(combined$combined_pip, c(1, 0.5)) + expect_equal(combined$gtex_brain_pip, c(0.5, 0.5)) + expect_equal(combined$cmc_brain_pip, c(0.5, NA)) +}) diff --git a/tests/testthat/test_enloc.R b/tests/testthat/test_enloc.R deleted file mode 100644 index fed9e4151..000000000 --- a/tests/testthat/test_enloc.R +++ /dev/null @@ -1,121 +0,0 @@ -test_that("the hypothesis probabilities are a partition", { - set.seed(1) - p <- 40L - r <- enlocClusterProbabilities( - gwasLog10Bf = rnorm(p, 1, 1), - qtlPip = { - v <- runif(p) - v / sum(v) * 0.6 - }, - pi1e = 0.02, - pi1ne = 0.001 - ) - expect_equal(r$ppH0 + r$ppH1 + r$ppH2 + r$ppH3 + r$ppH4, 1) - expect_true(all( - c(r$ppH0, r$ppH1, r$ppH2, r$ppH3, r$ppH4) >= 0 - )) -}) - -test_that("RCP and LCP are the coloc hypotheses under other names", { - set.seed(2) - p <- 25L - r <- enlocClusterProbabilities( - gwasLog10Bf = rnorm(p), - qtlPip = { - v <- runif(p) - v / sum(v) * 0.4 - }, - pi1e = 0.05, - pi1ne = 0.002 - ) - # fastenloc sums the grid's diagonal for RCP and the whole grid for LCP; - # those are exactly "one shared causal variant" and "both traits causal". - expect_equal(r$rcp, r$ppH4) - expect_equal(r$lcp, r$ppH3 + r$ppH4) - expect_equal(sum(r$scp), r$rcp) -}) - -test_that("enrichment raises the shared-variant probability", { - set.seed(3) - p <- 30L - bf <- rnorm(p, 1, 1) - pip <- runif(p) - pip <- pip / sum(pip) * 0.5 - none <- enlocClusterProbabilities(bf, pip, pi1e = 0.001, pi1ne = 0.001) - some <- enlocClusterProbabilities(bf, pip, pi1e = 0.05, pi1ne = 0.001) - # a1 = 0 means pi1e == pi1ne: being an eQTL says nothing about GWAS - # causality, which is the no-enrichment case. - expect_gt(some$rcp, none$rcp) -}) - -test_that("the priors match fastenloc's set_enrich_params", { - a0 <- -6 - a1 <- 3 - pQtl <- 0.01 - pr <- pecotmr:::.enlocPriorsFromEnrichment(a0, a1, pQtl) - expect_equal(pr$pi1e, stats::plogis(a0 + a1)) - expect_equal(pr$pi1ne, stats::plogis(a0)) - # p1 = (1 - P_eqtl) * exp(a0)/(1 + exp(a0)) - expect_equal(pr$p1, (1 - pQtl) * exp(a0) / (1 + exp(a0))) - # p2 = P_eqtl / (1 + exp(a0 + a1)) - expect_equal(pr$p2, pQtl / (1 + exp(a0 + a1))) - # p12 = P_eqtl * exp(a0 + a1)/(1 + exp(a0 + a1)) - expect_equal(pr$p12, pQtl * exp(a0 + a1) / (1 + exp(a0 + a1))) -}) - -test_that("the enrichment parameters round-trip through the priors", { - a0 <- -6 - a1 <- 3 - pr <- pecotmr:::.enlocPriorsFromEnrichment(a0, a1, pQtl = 0.01) - back <- pecotmr:::.enlocEnrichmentFromPriors(pr$p1, pr$p2, pr$p12) - expect_equal(back$a0, a0) - expect_equal(back$a1, a1) -}) - -test_that("a1 is capped before the priors are formed", { - uncapped <- pecotmr:::.enlocPriorsFromEnrichment(-6, 5, 0.01) - capped <- pecotmr:::.enlocPriorsFromEnrichment(-6, 5, 0.01, capA1 = 2) - expect_equal(capped$pi1e, stats::plogis(-6 + 2)) - expect_lt(capped$p12, uncapped$p12) -}) - -test_that("the SNP-level floor is applied only when asked", { - bf <- c(0, 0, 0) - pip <- c(1e-12, 1e-12, 1e-12) - without <- enlocClusterProbabilities( - bf, - pip, - pi1e = 0.02, - pi1ne = 0.001, - p12 = 1e-5 - ) - with <- enlocClusterProbabilities( - bf, - pip, - pi1e = 0.02, - pi1ne = 0.001, - p12 = 1e-5, - applyFloor = TRUE - ) - # fastenloc floors each SNP-level term at p12 when the full variant set - # is present, so a vanishing term cannot read as certainty against - # colocalisation. - expect_gt(with$rcp, without$rcp) -}) - -test_that("recovered GWAS Bayes factors follow the DAP-1 contrast", { - pip <- c(0.6, 0.2, 0.0) - bf <- pecotmr:::.enlocGwasLog10Bf(pip, pGwas = 1e-4, clusterPip = 0.8) - # A zero PIP is floored rather than giving -Inf. - expect_true(all(is.finite(bf))) - # Monotone in the PIP, since the other terms are shared. - expect_true(bf[[1]] > bf[[2]]) - expect_true(bf[[2]] > bf[[3]]) -}) - -test_that("mismatched input lengths are rejected", { - expect_error( - enlocClusterProbabilities(c(1, 2), c(0.1), pi1e = 0.02, pi1ne = 0.001), - "same variants" - ) -}) diff --git a/tests/testthat/test_fineMappingPipeline.R b/tests/testthat/test_fineMappingPipeline.R index afe67f78b..195de638f 100644 --- a/tests/testthat/test_fineMappingPipeline.R +++ b/tests/testthat/test_fineMappingPipeline.R @@ -31,14 +31,14 @@ context("fineMappingPipeline") } .fmp_mockExtractor <- function(seed = 3, n_samples = 40L) { - function(handle, snpIdx) { + function(handle, snpIdx, sampleIdx = NULL) { set.seed(seed) panel <- matrix( rbinom(n_samples * nrow(snpInfo(handle)), 2, 0.3), nrow = n_samples, ncol = nrow(snpInfo(handle)) ) - panel[, snpIdx, drop = FALSE] + panel[sampleIdx %||% seq_len(nrow(panel)), snpIdx, drop = FALSE] } } @@ -298,6 +298,91 @@ context("fineMappingPipeline") # .fmNormalizeMethods # =========================================================================== +test_that("fineMappingPipeline reads each window once and shares its X", { + skip_if_not_installed("pgenlibr") + qd <- .fitSamplesDataset() + run <- .countBlocks(fineMappingPipeline( + qd, + methods = "susie", + region = .fitSamplesWindow(), + seed = 1L + )) + expect_s4_class(run$result, "QtlFineMappingResult") + expect_equal(nrow(run$result), 6L) + # One read across 2 contexts and 3 traits, of only the samples they observe. + expect_equal(run$reads, 1L) + expect_equal(run$samples[[1L]], 1:30) + # In each context: one X for g1 and g3, and one for g2's different samples. + expect_equal(run$builds, 4L) + # Per-trait cis windows: one read per distinct window. + cis <- .countBlocks(fineMappingPipeline( + qd, + methods = "susie", + cisWindow = 1e4, + seed = 1L + )) + expect_equal(cis$reads, 1L) + expect_equal(nrow(cis$result), 6L) +}) + +test_that("fineMappingPipeline reads nothing when every fit is cached", { + skip_if_not_installed("pgenlibr") + qd <- .fitSamplesDataset() + first <- suppressMessages(fineMappingPipeline( + qd, + methods = "susie", + region = .fitSamplesWindow(), + seed = 1L + )) + resumed <- .countBlocks(fineMappingPipeline( + qd, + methods = "susie", + region = .fitSamplesWindow(), + fineMappingResult = first, + seed = 1L + )) + expect_equal(resumed$reads, 0L) + expect_equal(resumed$builds, 0L) + expect_equal(nrow(resumed$result), nrow(first)) +}) + +test_that("fineMappingPipeline reads a window once across its paths", { + skip_if_not_installed("pgenlibr") + window <- .fitSamplesWindow() + # The univariate fits and the PCA fits, across both contexts. + pca <- .countBlocks(fineMappingPipeline( + .fitSamplesDataset(), + methods = "susie", + region = window, + usePCA = TRUE, + nPCs = 1L, + phenotypeFilterParam = PhenotypeFilterParam(naAction = "impute"), + seed = 1L + )) + expect_equal(pca$reads, 1L) + expect_true(any(grepl("PC", as.character(pca$result$trait)))) + skip_if_not_installed("mvsusieR") + # The univariate fits and the joint ones, whichever way they are asked for. + # The fixture's contexts share phenotypes, which mvSuSiE warns about. + auto <- .countBlocks(suppressWarnings(fineMappingPipeline( + .fitSamplesDataset(), + methods = c("susie", "mvsusie"), + region = window, + seed = 1L + ))) + expect_equal(auto$reads, 1L) + expect_setequal(as.character(auto$result$method), c("susie", "mvsusie")) + bySpec <- .countBlocks(suppressWarnings(fineMappingPipeline( + .fitSamplesDataset(), + methods = c("susie", "mvsusie"), + jointSpecification = "context", + region = window, + seed = 1L + ))) + expect_equal(bySpec$reads, 1L) + expect_setequal(as.character(bySpec$result$method), c("susie", "mvsusie")) +}) + test_that(".fmNormalizeMethods: rejects NULL / empty / non-character/list", { expect_error( pecotmr:::.fmNormalizeMethods(NULL, inputKind = "QtlDataset"), @@ -827,6 +912,270 @@ test_that("fineMappingPipeline(QtlDataset): univariate rows record traitPos", { expect_lte(re[["ENSG_B"]], 3499L) # traitPos 2499 + cisWindow }) +test_that("a multi-study run reads a window once per component", { + skip_if_not_installed("pgenlibr") + skip_if_not_installed("mvsusieR") + # An explicit joint spec is fit along with the component's univariate fits. + run <- .countBlocks(suppressWarnings(fineMappingPipeline( + .fitSamplesTwoStudies(), + methods = c("susie", "mvsusie"), + jointSpecification = "context", + region = .fitSamplesWindow(), + seed = 1L + ))) + expect_equal(run$reads, 2L) + expect_setequal(as.character(run$result$study), c("s", "s2")) + expect_setequal(as.character(run$result$method), c("susie", "mvsusie")) + # A scope that holds nothing of a component leaves it its univariate fits. + scoped <- .countBlocks(suppressWarnings(fineMappingPipeline( + .fitSamplesTwoStudies(), + methods = c("susie", "mvsusie"), + jointSpecification = list(list( + axes = "context", + scope = list(study = "s") + )), + region = .fitSamplesWindow(), + seed = 1L + ))) + expect_equal(scoped$reads, 2L) + second <- as.character(scoped$result$study) == "s2" + expect_setequal(as.character(scoped$result$method)[second], "susie") + expect_true("mvsusie" %in% as.character(scoped$result$method)[!second]) +}) + +# The dimensions of every matrix a fit holds. +.fmFitMatrixShapes <- function(x) { + if (is.list(x)) { + return(list_flatten(map(unname(x), .fmFitMatrixShapes))) + } + if (length(dim(x)) >= 2L) list(dim(x)) +} + +# A side of samples and a side of variants; mvSuSiE's fitted values have a side +# of samples and one of conditions, and fSuSiE's one of positions. +.fmShapeHoldsX <- function(shape, X) { + is_in(nrow(X), shape) && max(shape) >= ncol(X) / 2L +} + +# Fits of `method` on `data` hold no copy of its genotypes, at any level. +.fmExpectFitsHoldNoX <- function(data, method, window) { + X <- residualizedGenotypes(data, context = "brain") + expect_true(.fmShapeHoldsX(dim(X), X)) + for (level in c("slim", "full")) { + res <- suppressWarnings(suppressMessages(fineMappingPipeline( + data, + methods = method, + region = window, + phenotypeFilterParam = PhenotypeFilterParam(naAction = "impute"), + fitRetention = level, + seed = 1L + ))) + shapes <- compact(.fmFitMatrixShapes(methods::slot(res, "fits"))) + expect_gt(length(shapes), 0L) + expect_false( + any(map_lgl(shapes, .fmShapeHoldsX, X = X)), + label = str_c(method, level) + ) + } +} + +# A context of traits at positions along a window of `panel`, as fSuSiE models +# them, on `panel`'s first 30 samples: each a sample's `effect`, plus noise. +.fmFunctionalDataset <- function(panel, positions, effect = 0) { + samples <- colnames(panel)[1:30] + traits <- str_c("cpg", seq_along(positions)) + noise <- matrix( + rnorm(length(traits) * length(samples)), + nrow = length(traits), + dimnames = list(traits, samples) + ) + se <- SummarizedExperiment::SummarizedExperiment( + assays = list(methylation = sweep(noise, 2L, effect, "+")), + rowRanges = `names<-`( + GenomicRanges::GRanges( + as.character(GenomicRanges::seqnames( + SummarizedExperiment::rowRanges(panel) + ))[[1L]], + IRanges::IRanges(positions, width = 1L) + ), + traits + ), + colData = S4Vectors::DataFrame(row.names = samples) + ) + QtlDataset( + studyName = "s", + genotypes = panel, + phenotypes = list(brain = se) + ) +} + +test_that("no fine-mapping fit keeps a copy of X, at any level", { + # Why this pipeline has no retainX: there is nothing for it to keep. + skip_if_not_installed("pgenlibr") + data <- .fitSamplesDataset() + window <- .fitSamplesWindow() + for (method in c("susie", "susieInf", "susieAsh")) { + .fmExpectFitsHoldNoX(data, method, window) + } +}) + +test_that("a joint fit keeps no copy of X, at any level", { + skip_if_not_installed("pgenlibr") + skip_if_not_installed("mvsusieR") + skip_if_not_installed("fsusieR") + window <- .fitSamplesWindow() + .fmExpectFitsHoldNoX(.fitSamplesDataset(), "mvsusie", window) + panel <- pecotmr:::.qtlGenotypeSe(.fitSamplesDataset()) + set.seed(1) + .fmExpectFitsHoldNoX( + .fmFunctionalDataset( + panel, + GenomicRanges::start(window) + 100L * seq_len(16L) + ), + "fsusie", + window + ) +}) + +test_that("a joint fit reports the frequencies of the samples it keeps", { + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("mvsusieR") + skip_if_not_installed("fsusieR") + samples <- str_c("s", 1:30) + nVariants <- 20L + set.seed(2) + calls <- matrix( + sample(c("0/0", "0/1", "1/1"), nVariants * 30L, replace = TRUE), + nrow = nVariants, + dimnames = list(NULL, samples) + ) + # Half of s1's calls are missing, which `imissCutoff` will not stand; one of + # s2's is, which it will. + calls[seq(1L, nVariants, by = 2L), "s1"] <- "./." + calls[1L, "s2"] <- "./." + panel <- readGenotypes(.writeTestVcf( + data.frame( + CHROM = "chr1", + POS = 100L * seq_len(nVariants), + ID = ".", + REF = "A", + ALT = "G", + calls + ), + samples + )) + dosage <- as.matrix(SummarizedExperiment::assay(panel, "dosage")) + data <- .fmFunctionalDataset( + panel, + 100L * seq_len(16L), + effect = 3 * dosage[6L, ] + ) + frequencies <- function(method, cutoff, kept) { + res <- suppressWarnings(suppressMessages(fineMappingPipeline( + data, + methods = method, + region = GenomicRanges::GRanges("chr1:1-3000"), + genotypeFilterParam = GenotypeFilterParam(imissCutoff = cutoff), + traitId = str_c("cpg", 1:16), + seed = 1L + ))) + top <- as.data.frame(S4Vectors::mcols(topLoci(res))) + expect_gt(nrow(top), 0L) + held <- dosage[ + match(top$pos, 100L * seq_len(nVariants)), + kept, + drop = FALSE + ] + expect_equal( + top$af, + unname(rowSums(held, na.rm = TRUE) / (2 * rowSums(!is.na(held)))) + ) + top$af[top$pos == 600L] + } + for (method in c("fsusie", "mvsusie")) { + withAll <- frequencies(method, 0, seq_len(30L)) + withoutS1 <- frequencies(method, 0.2, -1L) + # s1 has a call at the variant holding the signal, and it counts. + expect_false(any(withAll == withoutS1), label = method) + } +}) + +test_that("genes on several chromosomes fine-map without a seqlevels warning", { + skip_if_not_installed("VariantAnnotation") + samples <- str_c("s", 1:30) + set.seed(2) + calls <- matrix( + sample(c("0/0", "0/1", "1/1"), 40L * 30L, replace = TRUE), + nrow = 40L, + dimnames = list(NULL, samples) + ) + panel <- readGenotypes(.writeTestVcf( + data.frame( + CHROM = rep(c("chr1", "chr2"), each = 20L), + POS = rep(100L * seq_len(20L), 2L), + ID = ".", + REF = "A", + ALT = "G", + calls + ), + samples + )) + traits <- c("g1", "g2") + se <- SummarizedExperiment::SummarizedExperiment( + assays = list( + expression = matrix( + rnorm(2L * 30L), + nrow = 2L, + dimnames = list(traits, samples) + ) + ), + rowRanges = `names<-`( + GenomicRanges::GRanges( + c("chr1", "chr2"), + IRanges::IRanges(500L, width = 1L) + ), + traits + ), + colData = S4Vectors::DataFrame(row.names = samples) + ) + expect_no_warning( + res <- suppressMessages(fineMappingPipeline( + QtlDataset( + studyName = "s", + genotypes = panel, + phenotypes = list(brain = se) + ), + methods = "susie", + cisWindow = 1000L, + seed = 1L + )), + message = "sequence levels" + ) + expect_setequal(as.character(res$trait), traits) +}) + +test_that("a panel of rsIDs fine-maps as one labelled by position", { + skip_if_not_installed("pgenlibr") + window <- .fitSamplesWindow() + byId <- suppressMessages(fineMappingPipeline( + .fitSamplesDataset(), + methods = "susie", + region = window, + seed = 1L + )) + byRs <- suppressMessages(fineMappingPipeline( + .fitSamplesDataset(.rsIdPanel()), + methods = "susie", + region = window, + seed = 1L + )) + expect_gt(length(topLoci(byId)), 0L) + expect_equal( + as.data.frame(S4Vectors::mcols(topLoci(byRs))), + as.data.frame(S4Vectors::mcols(topLoci(byId))) + ) +}) + test_that(".fmAfForX gives effect-allele af aligned to colnames(X)", { qd <- .fmp_qdA local_mocked_bindings( @@ -842,7 +1191,7 @@ test_that(".fmAfForX gives effect-allele af aligned to colnames(X)", { ncol = 3L, dimnames = list( paste0("s", 1:5), - c("chr1:300:A:G", "chr1:100:A:G", "chr1:200:A:G") + c("chr1:300:G:A", "chr1:100:G:A", "chr1:200:G:A") ) ) noFilter <- pecotmr:::.qtlResolveFilter() @@ -998,19 +1347,15 @@ test_that("fineMappingPipeline(QtlDataset, usePCA): rows keyed topPC{i}", { contexts = "brain", traits = c("ENSG_A", "ENSG_B", "ENSG_C") ) - # The PCA path builds a window's block from all the context's traits; the - # univariate fits, from one trait each. - realBlock <- pecotmr:::.fmResidGenoBlock - pcaBuilds <- 0L + realResid <- pecotmr:::.fmResidGeno + builds <- 0L local_mocked_bindings( .handleDosage = .fmp_mockExtractor(), .fmFitSusieIndiv = .fmp_mockFitIndiv(), .fmPostprocessOne = .fmp_mockPostprocess(), - .fmResidGenoBlock = function(ctx, traits, ...) { - if (length(traits) > 1L) { - pcaBuilds <<- pcaBuilds + 1L - } - realBlock(ctx, traits, ...) + .fmResidGeno = function(...) { + builds <<- builds + 1L + realResid(...) }, .package = "pecotmr" ) @@ -1031,9 +1376,9 @@ test_that("fineMappingPipeline(QtlDataset, usePCA): rows keyed topPC{i}", { expect_equal(sum(pcRows), 2L) expect_setequal(as.character(res$trait)[pcRows], c("topPC1", "topPC2")) expect_setequal(as.character(res$method)[pcRows], "susie") - # The PCs differ only in the response, so they share their window's - # block: one build for the window, not one per PC. - expect_equal(pcaBuilds, 1L) + # The traits and the PCs are on one context and its samples, and the PCs' + # window holds the traits': one build for all of them. + expect_equal(builds, 1L) }) test_that(".fmLookupMrmashFit: finds the mr.mash fit by (study, trait)", { @@ -4458,7 +4803,7 @@ test_that("usePCA with a region uses the region's genotype block", { .fmFitSusieIndiv = .fmp_mockFitIndiv(), .fmPostprocessOne = .fmp_mockPostprocess(), .fmSerScreen = function(...) TRUE, - .fmDispatchJointSpecsQtlDataset = function(...) NULL, + .fmJointSpecsWork = function(...) pecotmr:::.qtlWorkValue(NULL), .package = "pecotmr" ) region <- GenomicRanges::GRanges("chr1", IRanges::IRanges(1L, 3000L)) @@ -4487,7 +4832,7 @@ test_that("usePCA skips a PC block with too few shared samples", { ) ) }, # < 2 samples shared with the PCs - .fmDispatchJointSpecsQtlDataset = function(...) .fmp_jr(), + .fmJointSpecsWork = function(...) pecotmr:::.qtlWorkValue(.fmp_jr()), .package = "pecotmr" ) res <- suppressMessages(fineMappingPipeline( @@ -4505,7 +4850,7 @@ test_that("usePCA skips a PC block the SER screens out", { local_mocked_bindings( .handleDosage = .fmp_mockExtractor(), .fmSerScreen = function(...) FALSE, # the PC block is screened out - .fmDispatchJointSpecsQtlDataset = function(...) .fmp_jr(), + .fmJointSpecsWork = function(...) pecotmr:::.qtlWorkValue(.fmp_jr()), .package = "pecotmr" ) res <- suppressMessages(fineMappingPipeline( @@ -5285,7 +5630,7 @@ test_that(".fmBlockWindow falls back to the trait's own cis window", { explicit ) # Without one, the cis path's window is the trait's position +/- cisWindow - # -- the same interval .fmResidGenoBlock() slices X over. Widening the + # -- the same interval .fmGenoWindow() gives X over. Widening the # cis window must widen the recorded window. cfgNarrow <- list(data = qtlDatasetExample, cisWindow = 1000) cfgWide <- list(data = qtlDatasetExample, cisWindow = 500000) @@ -5351,9 +5696,9 @@ test_that("a GWAS row carries its block name and window into its fit", { # Provenance columns (dataType / dataSource / standardized) # # These three are what writeBed() / writeGwasVcf() 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. +# data_source and weight_standardized. Only the first is an outside fact, read +# from the data; the other two follow from the branch the pipeline took, so they +# are derived and the summary-statistics methods refuse to be told. # --------------------------------------------------------------------------- # @noRd @@ -5367,16 +5712,24 @@ test_that("a GWAS row carries its block name and window into its fit", { ] } -test_that("fineMappingPipeline(QtlDataset) records what it is given", { +test_that("fineMappingPipeline(QtlDataset) records what the dataset says", { data(qtlDatasetExample, envir = environment()) + typed <- QtlDataset( + studyName = studyName(qtlDatasetExample), + genotypes = pecotmr:::.qtlGenotypeSe(qtlDatasetExample), + phenotypes = as.list( + MultiAssayExperiment::experiments(qtlDatasetExample) + )["brain"], + dataType = "expression" + ) res <- suppressMessages(fineMappingPipeline( - qtlDatasetExample, + typed, methods = "susie", - cisWindow = 1e6, - dataType = "bulk_rnaseq" + cisWindow = 1e6 )) cols <- .fmprov_cols(res) - expect_equal(unique(cols$dataType), "bulk_rnaseq") + expect_equal(unique(cols$dataType), "expression") + expect_equal(unique(dataType(res)), "expression") # 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 @@ -5384,7 +5737,31 @@ test_that("fineMappingPipeline(QtlDataset) records what it is given", { expect_true(all(cols$standardized)) }) -test_that("fineMappingPipeline(QtlDataset) omits dataType when not supplied", { +test_that("fineMappingPipeline(MultiStudyQtlDataset) reads each study's", { + data(qtlDatasetExample, envir = environment()) + typed <- QtlDataset( + studyName = "gtex", + genotypes = pecotmr:::.qtlGenotypeSe(qtlDatasetExample), + phenotypes = as.list( + MultiAssayExperiment::experiments(qtlDatasetExample) + )["brain"], + dataType = "expression" + ) + ms <- MultiStudyQtlDataset( + qtlDatasets = list(gtex = typed, study1 = qtlDatasetExample) + ) + res <- suppressMessages(fineMappingPipeline( + ms, + methods = "susie", + cisWindow = 1e6 + )) + types <- dataType(res) + expect_setequal(as.character(res$study), c("gtex", "study1")) + expect_true(all(types[as.character(res$study) == "gtex"] == "expression")) + expect_true(all(is.na(types[as.character(res$study) == "study1"]))) +}) + +test_that("fineMappingPipeline(QtlDataset) omits dataType when none recorded", { data(qtlDatasetExample, envir = environment()) res <- suppressMessages(fineMappingPipeline( qtlDatasetExample, @@ -5402,12 +5779,11 @@ test_that("fineMappingPipeline(QtlDataset) omits dataType when not supplied", { test_that("fineMappingPipeline(QtlSumStats) records sumstat / standardized", { res <- suppressWarnings(suppressMessages(fineMappingPipeline( - .rssf_ss, - methods = "susie", - dataType = "bulk_rnaseq" + `$<-`(.rssf_ss, "dataType", rep("splicing", length(.rssf_ss))), + methods = "susie" ))) cols <- .fmprov_cols(res) - expect_equal(unique(cols$dataType), "bulk_rnaseq") + expect_equal(unique(cols$dataType), "splicing") 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. diff --git a/tests/testthat/test_fineMappingRow.R b/tests/testthat/test_fineMappingRow.R index 7c5679e8c..5238bf140 100644 --- a/tests/testthat/test_fineMappingRow.R +++ b/tests/testthat/test_fineMappingRow.R @@ -79,6 +79,27 @@ test_that("adjustPips tolerates a chr-prefix difference with keepVariants", { expect_equal(ncol(susieFit(adj)$lbf_variable), 4) }) +test_that("adjustPips matches keepVariants as every keep list is matched", { + vids <- c( + "chr1:1:A:G", + "chr1:2:A:T", + "chr1:3:C:T", + "chr1:4:C:A", + "chr1:5:G:C" + ) + entry <- .makeAdjustEntry(vids) + keep <- c( + "chr1:1:G:A", # swapped alleles + "chr1:2:T:A", # a palindrome spelled the other way + "chr1:3:C:T", # listed in both allele orders + "chr1:3:T:C", + "rs9" # an rsID among the ids does not stop them being read + ) + adj <- adjustPips(entry, keep) + expect_equal(variantIds(adj), vids[1:3]) + expect_equal(ncol(susieFit(adj)$lbf_variable), 3) +}) + # === Tests migrated from test_s4Constructors.R (FineMappingRow) === test_that("FineMappingRow stores its slots and the accessors return them", { diff --git a/tests/testthat/test_fineMappingWrappers.R b/tests/testthat/test_fineMappingWrappers.R index 9f6c8ead7..9480d3e46 100644 --- a/tests/testthat/test_fineMappingWrappers.R +++ b/tests/testthat/test_fineMappingWrappers.R @@ -1480,7 +1480,10 @@ test_that("overlapping CSs of one method: a row per variant, smallest wins", { "0.5" = list(1L, 1L) ) inp <- .fakeFitAndCs(variant_ids, cs_at_cov, pip = 0.9) - out <- .runBuildTopLoci(inp, method = "susie") + expect_warning( + out <- .runBuildTopLoci(inp, method = "susie"), + "in multiple credible sets: L1, L2" + ) # Canonical schema is one row per variant. When a variant is in # multiple CSs at a coverage, the smallest cs_idx is reported. expect_equal(nrow(out), 1L) @@ -3312,6 +3315,24 @@ test_that("mergeSusieCs: input that is not a fine-mapping result errors", { ) } +test_that(".csGenotypesForFit finds a fit's ids however a panel names them", { + skip_if_not_installed("pgenlibr") + window <- .fitSamplesWindow() + for (data in list(.fitSamplesDataset(), .fitSamplesDataset(.rsIdPanel()))) { + ids <- pecotmr:::.qtlExtractBlock(data, window = window)$variantIds[ + c(4L, 2L, 7L) + ] + expect_equal(colnames(pecotmr:::.csGenotypesForFit(data, ids)), ids) + } + expect_error( + pecotmr:::.csGenotypesForFit( + .fitSamplesDataset(), + "chr21:17513228:A:A" + ), + "absent from the QtlDataset" + ) +}) + test_that("computeCsCorrelation (SumStats) matches get_cs_correlation", { data(qtlSumStatsExample) ss <- qtlSumStatsExample @@ -4026,6 +4047,13 @@ test_that("the fit region spans one chromosome or is refused", { ) }) +test_that("the fit region reads a variant id however a panel spells it", { + region <- function(ids) as.character(pecotmr:::.csVariantRegion(ids)) + expect_equal(region(c("chr1:100:A:G", "chr1:200:C:T")), "chr1:100-200") + expect_equal(region(c("chr1_100_A_G", "chr1_200_C_T")), "chr1:100-200") + expect_equal(region(c("1:100:A:G", "1:200:C:T")), "chr1:100-200") +}) + test_that("credible-set genotypes must cover every fit variant", { # Silently correlating a subset would report a purity for a set the # genotypes cannot actually measure. diff --git a/tests/testthat/test_genotypeHandle.R b/tests/testthat/test_genotypeHandle.R index 648547991..c7fb2c451 100644 --- a/tests/testthat/test_genotypeHandle.R +++ b/tests/testthat/test_genotypeHandle.R @@ -808,9 +808,9 @@ test_that("nothing is read until a block is actually touched", { # would re-enter the mock, not the reader. realRead <- pecotmr:::.handleDosage local_mocked_bindings( - .handleDosage = function(handle, snpIdx) { + .handleDosage = function(handle, snpIdx, sampleIdx = NULL) { reads <<- reads + 1L - realRead(handle, snpIdx) + realRead(handle, snpIdx, sampleIdx) }, .package = "pecotmr" ) @@ -824,21 +824,27 @@ test_that("nothing is read until a block is actually touched", { expect_gt(reads, 0L) }) -test_that("only the requested variants are read", { +test_that("only the requested variants and samples are read", { gh <- .gh_pgenHandle() seen <- integer(0) + seenSamples <- list() realRead <- pecotmr:::.handleDosage local_mocked_bindings( - .handleDosage = function(handle, snpIdx) { + .handleDosage = function(handle, snpIdx, sampleIdx = NULL) { seen <<- c(seen, length(snpIdx)) - realRead(handle, snpIdx) + seenSamples <<- c(seenSamples, list(sampleIdx)) + realRead(handle, snpIdx, sampleIdx) }, .package = "pecotmr" ) da <- genotypeDelayedArray(gh) - invisible(as.matrix(da[1:5, 1:4])) + block <- as.matrix(da[1:5, c(4L, 1L, 1L)]) expect_true(all(seen <= 5L)) expect_lt(sum(seen), nrow(snpInfo(gh))) + # Each distinct sample once, in file order, and the block laid out as asked. + expect_identical(unique(seenSamples), list(c(1L, 4L))) + whole <- as.matrix(da[1:5, ]) + expect_equal(unname(block), unname(whole[, c(4L, 1L, 1L)])) }) test_that("genotypeDelayedArray leaves the handle it is given whole", { diff --git a/tests/testthat/test_genotypeIo.R b/tests/testthat/test_genotypeIo.R index 1cd53e1c0..3acfbc4f6 100644 --- a/tests/testthat/test_genotypeIo.R +++ b/tests/testthat/test_genotypeIo.R @@ -624,7 +624,7 @@ test_that("loadGenotypeRegion filters by keep_variants_path for plink2", { expect_equal(ncol(result), 3L) }) -test_that("loadGenotypeRegion with keepVariantsPath reads the same dosages as a full load", { +test_that("loadGenotypeRegion keeps the dosages a full load reads", { skip_if_not_installed("pgenlibr") td <- test_path("test_data") full <- loadGenotypeRegion( @@ -641,8 +641,37 @@ test_that("loadGenotypeRegion with keepVariantsPath reads the same dosages as a returnVariantInfo = TRUE ) expect_equal(kept$X, full$X[, c(2, 5, 9), drop = FALSE]) - expect_equal(kept$variant_info, full$variant_info[c(2, 5, 9), , drop = FALSE], - ignore_attr = TRUE) + expect_equal( + kept$variant_info, + full$variant_info[c(2, 5, 9), , drop = FALSE] + ) +}) + +test_that("loadGenotypeRegion reads only the variants its filters keep", { + skip_if_not_installed("pgenlibr") + genotype <- file.path(test_path("test_data"), "test_variants") + info <- loadGenotypeRegion(genotype, returnVariantInfo = TRUE)$variant_info + keepFile <- withr::local_tempfile(fileext = ".tsv") + vroom::vroom_write( + info[c(2, 5, 9), c("chrom", "pos", "A2", "A1")], + keepFile, + delim = "\t" + ) + expectVariantsRead <- function(nVariants, ...) { + testthat::local_mocked_bindings( + .extractBlockPlink2 = function(handle, snpIdx, sampleIdx = NULL) { + expect_length(snpIdx, nVariants) + matrix(0, nrow = nSamples(handle), ncol = length(snpIdx)) + }, + .package = "pecotmr" + ) + expect_equal(ncol(loadGenotypeRegion(genotype, ...)), nVariants) + } + expectVariantsRead(3L, keepVariantsPath = keepFile) + expectVariantsRead( + sum(nchar(info$A1) == 1L & nchar(info$A2) == 1L), + keepIndel = FALSE + ) }) test_that("loadGenotypeRegion attaches afreq info for plink2", { @@ -814,6 +843,139 @@ test_that("loadGenotypeRegion drops every format's indels", { } }) +# Keep lists are matched against a panel holding a palindromic SNP (rs200), two +# variants at one site (300), an indel (rs400), a variant held twice (500), a +# site held in both allele orders (600) and a tag for an allele in an id (700). +keepListPanel <- tibble::tibble( + chrom = "chr1", + id = c( + "chr1:100:A:G", + "rs200", + "chr1:300:C:T", + "chr1:300:C:A", + "rs400", + "chr1:500:A:G", + "chr1:500:A:G", + "chr1:600:A:G", + "chr1:600:G:A", + "chr1:700:INS:T" + ), + pos = c(100L, 200L, 300L, 300L, 400L, 500L, 500L, 600L, 600L, 700L), + A2 = c("A", "A", "C", "C", "A", "A", "A", "A", "G", "A"), + A1 = c("G", "T", "T", "A", "AT", "G", "G", "G", "A", "AT") +) +keepListHeader <- "chrom\tpos\tA2\tA1" + +# Which rows of keepListPanel a keep-list file of these lines names. +keptRows <- function(...) { + path <- withr::local_tempfile(lines = c(...)) + which(pecotmr:::matchVariantsToKeep(keepListPanel, path)) +} + +test_that("a keep list matches alleles in either order and on either strand", { + expect_equal(keptRows(keepListHeader, "chr1\t100\tG\tA"), 1L) + expect_equal(keptRows(keepListHeader, "chr1\t100\tT\tC"), 1L) + expect_equal(keptRows(keepListHeader, "chr1\t100\tC\tT"), 1L) + expect_equal(keptRows(keepListHeader, "chr1\t400\tAT\tA"), 5L) + # A palindrome spelled the other way is the same variant: no sign is taken. + expect_equal(keptRows(keepListHeader, "chr1\t200\tT\tA"), 2L) + # At a site with two variants, only the one named; none for other alleles. + expect_equal(keptRows(keepListHeader, "chr1\t300\tA\tC"), 4L) + expect_length(keptRows(keepListHeader, "chr1\t100\tA\tC"), 0L) + # Variant ids are read the same way, whatever their format. + expect_equal(keptRows("chr1:100:G:A", "1_300_A_C"), c(1L, 4L)) +}) + +test_that("a variant named or held twice stays on a keep list", { + # Listed in both allele orders, which would disagree about its sign. + expect_equal( + keptRows(keepListHeader, "chr1\t100\tA\tG", "chr1\t100\tG\tA"), + 1L + ) + # Held twice by the panel: complex.bgen does this, with different dosages. + expect_equal(keptRows(keepListHeader, "chr1\t500\tA\tG"), 6:7) + expect_equal(keptRows(keepListHeader, "chr1\t600\tA\tG"), 8:9) +}) + +test_that("a keep list without alleles keeps every variant at a position", { + expect_equal(keptRows("chrom\tpos", "chr1\t300"), 3:4) +}) + +test_that("a keep list's header line and unplaced rows match nothing", { + # Neither costs the rest of the list its alleles or its matches. + expect_equal(keptRows("variant_id", "chr1:300:C:A"), 4L) + expect_equal( + keptRows(keepListHeader, "chr1\t300\tC\tA", "chr1\tNA\tC\tT"), + 4L + ) +}) + +test_that("a keep list's rsIDs are matched against the panel's labels", { + expect_equal(keptRows("rs200", "rs400"), c(2L, 5L)) + expect_equal(keptRows("chr1:100:A:G", "rs400"), c(1L, 5L)) + expect_length(keptRows("rs999"), 0L) + # A variant labelled by rsID is still found by position and alleles. + expect_equal(keptRows("chr1:200:A:T"), 2L) + expect_equal(keptRows(keepListHeader, "chr1\t400\tA\tAT"), 5L) + # An id with a tag for an allele is found by that label, as the variant is + # by its real alleles. + expect_equal(keptRows("chr1:700:INS:T"), 10L) + expect_equal(keptRows("chr1:700:A:AT"), 10L) +}) + +test_that("a keep list's tagged id meets the label however it is spelled", { + # Every label of this panel can be read, so matchVariants() parses them. + panel <- tibble::tibble( + chrom = "chr1", + id = c("chr1:100:A:G", "chr1:700:INS:T"), + pos = c(100L, 700L), + A2 = "A", + A1 = c("G", "AT") + ) + for (id in c("chr1:700:INS:T", "1_700_INS_T", "chr1:700:T:INS")) { + expect_equal(which(pecotmr:::.keepByIds(panel, id)), 2L, info = id) + } +}) + +test_that("loadGenotypeRegion reads a swapped keep list in every format", { + skip_if_not_installed("PlinkMatrix") + skip_if_not_installed("pgenlibr") + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("SNPRelate") + skip_if_not_installed("gdsfmt") + expectSwappedListRead <- function(path, which = NULL) { + full <- loadGenotypeRegion(path, which, returnVariantInfo = TRUE) + rows <- c(2L, 5L) + vi <- full$variant_info[rows, ] + keepFile <- withr::local_tempfile(fileext = ".tsv") + vroom::vroom_write( + tibble::tibble( + chrom = vi$chrom, + pos = vi$pos, + A2 = vi$A1, + A1 = vi$A2 + ), + keepFile, + delim = "\t" + ) + expect_equal( + loadGenotypeRegion(path, which, keepVariantsPath = keepFile), + full$X[, rows, drop = FALSE], + info = path + ) + } + window <- GenomicRanges::GRanges("chr21:17513228-17518122") + for (path in c( + plink_prefix, + paste0(plink_prefix, ".bed"), + vcf_path, + gds_path + )) { + expectSwappedListRead(path, window) + } + expectSwappedListRead(.bgw_fixture(n = 6L, nVar = 8L)$path) +}) + test_that("loadGenotypeRegion errors on an empty region", { skip_if_not_installed("pgenlibr") skip_if_not_installed("SNPRelate") @@ -1347,7 +1509,7 @@ test_that("loadGenotypeRegion keeps imputed dosages without a sidecar", { # sidecar are an imputed file's, not a sketch's: returned untouched, and # without a warning about restoring a scale they never had. testthat::local_mocked_bindings( - .extractBlockPlink2 = function(handle, snpIdx) { + .extractBlockPlink2 = function(handle, snpIdx, sampleIdx = NULL) { matrix(0.5, nrow = nSamples(handle), ncol = length(snpIdx)) }, .package = "pecotmr" @@ -2109,13 +2271,16 @@ test_that("a BGEN file serves every genotype route", { test_that("the BGEN dosage kernel checks what it is given", { expect_error( - pecotmr:::bgenDosageCpp(as.raw(1:5), 1L, 2L), + pecotmr:::bgenDosageCpp(as.raw(1:5), 1L, 2L, integer(0)), "truncated" ) # A record claiming more genotypes than its data could hold is refused # before they are enumerated: ploidy 63 with 65535 alleles, one byte. corrupt <- as.raw(c(1, 0, 0, 0, 255, 255, 63, 63, 63, 0, 8, 0)) - expect_error(pecotmr:::bgenDosageCpp(corrupt, 1L, 1L), "truncated") + expect_error( + pecotmr:::bgenDosageCpp(corrupt, 1L, 1L, integer(0)), + "truncated" + ) }) test_that("the BGEN dosage kernel reads any ploidy and allele count", { @@ -2131,16 +2296,126 @@ test_that("the BGEN dosage kernel reads any ploidy and allele count", { } # All the mass on (0,2,1): two copies of allele 2, one of allele 3. expect_equal( - pecotmr:::bgenDosageCpp(triploid(7L), 1L, c(2L, 3L)), + pecotmr:::bgenDosageCpp(triploid(7L), 1L, c(2L, 3L), integer(0)), matrix(c(2, 1), 1L) ) # On the implied last genotype, (0,0,3). expect_equal( - pecotmr:::bgenDosageCpp(triploid(10L), 1L, c(1L, 3L)), + pecotmr:::bgenDosageCpp(triploid(10L), 1L, c(1L, 3L), integer(0)), matrix(c(0, 3), 1L) ) }) +test_that("the BGEN dosage kernel decodes only the samples asked for", { + # Two unphased diploid samples with two alleles, 8 bits per probability: + # all the mass on (2,0) for the first and on (1,1) for the second. + record <- as.raw(c(2, 0, 0, 0, 2, 0, 2, 2, 2, 2, 0, 8, 255, 0, 0, 255)) + kernel <- pecotmr:::bgenDosageCpp + expect_equal(kernel(record, 2L, 2L, integer(0)), matrix(c(0, 1), 2L)) + expect_equal(kernel(record, 2L, 2L, 1L), matrix(0, 1L)) + expect_equal(kernel(record, 2L, 2L, 2L), matrix(1, 1L)) + expect_error(kernel(record, 2L, 2L, c(2L, 1L)), "ascending") + expect_error(kernel(record, 2L, 2L, c(1L, 1L)), "ascending") + expect_error(kernel(record, 2L, 2L, 3L), "ascending") + expect_error(kernel(record, 2L, 2L, 0L), "ascending") + # A skip past the end of the data is caught, as a read past it is. + expect_error(kernel(record[1:13], 2L, 2L, 2L), "ends early") + expect_error(kernel(record[1:14], 2L, 2L, 2L), "ends early") + # Nothing after the last sample asked for is read. + expect_equal(kernel(record[1:14], 2L, 2L, 1L), matrix(0, 1L)) +}) + +test_that("the BGEN reader reads some samples as all of them have them", { + skip_if_not_installed("DBI") + skip_if_not_installed("RSQLite") + expectSameSamples <- function(handle, rows, samples, info) { + expect_identical( + pecotmr:::.extractBlockBgen(handle, rows, samples), + pecotmr:::.extractBlockBgen(handle, rows)[samples, , drop = FALSE], + info = info + ) + } + # Any bit depth: a skip lands part-way through a byte. + for (bits in c(3L, 8L, 13L, 16L)) { + fx <- .bgw_fixture(n = 9L, nVar = 5L, bits = bits) + handle <- readGenotypeHandle(fx$path) + expectSameSamples(handle, c(4L, 1L), c(2L, 5L, 9L), bits) + expectSameSamples(handle, c(4L, 1L), 1L, bits) + } + # Layout 1, compressed or not. + for (compression in 0:1) { + fx <- .bgw_fixture(n = 9L, compression = compression, layout = 1L) + handle <- readGenotypeHandle(fx$path) + expectSameSamples(handle, 3:1, c(2L, 5L, 9L), compression) + } + # Mixed ploidy, phased records and several alleles per record. + for (name in c("complex.bgen", "haplotypes.bgen")) { + handle <- suppressWarnings( + readGenotypeHandle(test_path("test_data", "bgen", name)) + ) + rows <- seq_len(nrow(snpInfo(handle))) + expectSameSamples(handle, rows, c(2L, 4L), name) + } +}) + +test_that("every format reads some samples as the whole panel has them", { + skip_if_not_installed("PlinkMatrix") + skip_if_not_installed("pgenlibr") + skip_if_not_installed("VariantAnnotation") + skip_if_not_installed("SNPRelate") + skip_if_not_installed("gdsfmt") + paths <- c( + plink2 = plink_prefix, + plink1 = paste0(plink_prefix, ".bed"), + vcf = vcf_path, + gds = gds_path, + bgen = .bgw_fixture(n = 12L, nVar = 8L)$path + ) + rows <- c(4L, 1L, 2L) + # In any order, and repeated, as DelayedArray may ask. + samples <- c(9L, 2L, 2L, 6L) + for (format in names(paths)) { + panel <- readGenotypes(paths[[format]]) + dosage <- SummarizedExperiment::assay(panel, "dosage") + expect_equal( + as.matrix(dosage[rows, samples]), + as.matrix(dosage[rows, ])[, samples], + info = format + ) + expect_equal( + pecotmr:::.ldRefDosage( + panel, + rows, + meanImpute = FALSE, + sampleIdx = c(2L, 6L) + ), + pecotmr:::.ldRefDosage(panel, rows, meanImpute = FALSE)[ + c(2L, 6L), + ], + info = format + ) + } +}) + +test_that("a PLINK2 read of some samples returns only those", { + skip_if_not_installed("pgenlibr") + handle <- readGenotypeHandle(plink_prefix, format = "plink2") + whole <- pecotmr:::.extractBlockPlink2(handle, 1:3) + some <- pecotmr:::.extractBlockPlink2(handle, 1:3, c(2L, 7L)) + expect_equal(dim(some), c(2L, 3L)) + expect_equal(some, whole[c(2L, 7L), , drop = FALSE]) + # Per-chromosome files route the samples to each file's read. + sharded <- readGenotypes( + genoMeta = c("21" = file.path(test_data_dir, "test_variants.pgen")), + format = "plink2" + ) + dosage <- SummarizedExperiment::assay(sharded, "dosage") + expect_equal( + unname(as.matrix(dosage[1:3, c(2L, 7L)])), + unname(t(some)) + ) +}) + # =========================================================================== # The VCF and GDS panel builders: what a VCF or GDS panel's variants are # called, what is refused before anything is read, and how a per-chromosome diff --git a/tests/testthat/test_h2Annotations.R b/tests/testthat/test_h2Annotations.R index 082dcea84..9e43bfe74 100644 --- a/tests/testthat/test_h2Annotations.R +++ b/tests/testthat/test_h2Annotations.R @@ -566,6 +566,59 @@ test_that("fineMappingAnnotations builds max and summed PIP over the traits", { expect_equal(.fma_col(annot, "credibleSet"), c(1, 1, 1, 0, 1, 0)) }) +test_that("fineMappingAnnotations builds PIP columns from credible sets", { + # Two genes whose credible sets share variant 3; a variant outside any + # has no within_cs_pip, whatever its PIP. + rowOf <- function(idx, pip, within) { + ids <- sprintf("chr1:%d:A:G", idx * 100L) + FineMappingRow( + variantIds = ids, + susieFit = list(pip = stats::setNames(pip, ids)), + topLoci = data.frame( + variant_id = ids, + pip = pip, + within_cs_pip = within + ) + ) + } + x <- QtlFineMappingResult( + studyName = c("Q", "Q"), + context = c("ctx", "ctx"), + trait = c("g1", "g2"), + method = c("susie", "susie"), + entry = list( + rowOf(1:4, c(0.7, 0.2, 0.05, 0.05), c(0.7, NA, 0.04, NA)), + rowOf(3:6, c(0.6, 0.1, 0.2, 0.1), c(0.6, NA, NA, NA)) + ) + ) + annot <- fineMappingAnnotations( + x, + .fma_ref(), + types = c("maxCppCs", "sumPipCs") + ) + expect_equal(.fma_col(annot, "maxCppCs"), c(0.7, 0, 0.6, 0, 0, 0)) + expect_equal(.fma_col(annot, "sumPipCs"), c(0.7, 0, 0.64, 0, 0, 0)) + expect_setequal( + SummarizedExperiment::colData(annot)$type, + "continuous" + ) + # They stay opt-in, and a fit without the column gives an empty column, + # which is dropped with the usual warning. + expect_false(any(str_detect( + colnames(fineMappingAnnotations(.fma_qtl(), .fma_ref())), + "Cs$" + ))) + expect_warning( + none <- fineMappingAnnotations( + .fma_qtl(), + .fma_ref(), + types = c("maxCpp", "sumPipCs") + ), + "dropping 1 column" + ) + expect_identical(colnames(none), "Q|ctx|susie|maxCpp") +}) + test_that("fineMappingAnnotations rows follow the reference", { ref <- .fma_ref() annot <- fineMappingAnnotations(.fma_qtl(), ref) diff --git a/tests/testthat/test_h2EstimationWrappers.R b/tests/testthat/test_h2EstimationWrappers.R index 4680aa522..67a6331d6 100644 --- a/tests/testthat/test_h2EstimationWrappers.R +++ b/tests/testthat/test_h2EstimationWrappers.R @@ -1493,13 +1493,14 @@ test_that("each univariate estimator returns a finite h2 and a positive SE", { } }) -test_that("lderUnivariate intercept is near zero for well-calibrated data", { - # In LDER the intercept parameter a represents confounding deviation: - # the model is E[chi2Rot - 1] = n * h2/M * d + n * a - # so a ~ 0 when there is no confounding. +test_that("lderUnivariate reports its intercept on the others' scale", { + # The fit's inflation a enters as E[chi2Rot] = n * h2/M * d + 1 + n * a. The + # estimators report that intercept, 1 + n * a, about 1 without confounding. res <- pecotmr:::lderUnivariate(dat$z, dat$n, dat$eigen_ref) expect_true(is.finite(res$intercept)) - expect_true(abs(res$intercept) < 0.01) + expect_gte(res$intercept, 1) + expect_lt(res$intercept, 1 + dat$n * 0.01) + expect_gt(res$interceptSe, 0) }) # ============================================================================= @@ -1952,6 +1953,214 @@ test_that("HDL can estimate a depleted annotation", { expect_lt(part$enrichment, 1) }) +test_that("enrichment is reported only for columns within [0, 1]", { + set.seed(3) + M <- 40L + A <- `attr<-`( + cbind( + base = 1, + inside = runif(M), + above = runif(M, 0, 2), + below = rnorm(M) + ), + "userCols", + 2:4 + ) + tau <- c(1e-3, 2e-3, -1e-3, 5e-4) + folds <- matrix(tau, 6L, 4L, byrow = TRUE) + rnorm(24L, sd = 1e-4) + out <- pecotmr:::.h2StratifiedReport(A, tau, folds)$enrichment + # The fit's coefficients stay meaningful outside the domain. + expect_equal(out$tau, tau[2:4]) + expect_true(all(is.finite(out$tauSe))) + for (col in c( + "enrichment", + "enrichmentSe", + "enrichmentP", + "propH2", + "propSnps" + )) { + expect_true(is.finite(out[[col]][[1L]]), info = col) + expect_true(all(is.na(out[[col]][2:3])), info = col) + } +}) + +test_that("the enrichment p-value is ldsc's t test of inside against outside", { + set.seed(5) + M <- 50L + inside <- rbinom(M, 1L, 0.3) + A <- `attr<-`(cbind(base = 1, annot = inside), "userCols", 2L) + tau <- c(1e-3, 2e-3) + folds <- matrix(tau, 10L, 2L, byrow = TRUE) + rnorm(20L, sd = 2e-4) + # regressions.py, _overlap_output: per-variant h2 inside minus outside. + difference <- function(tt) { + v <- as.vector(A %*% tt) + sum(v[inside == 1L]) / + sum(inside) - + sum(v[inside == 0L]) / sum(1L - inside) + } + se <- pecotmr:::.jackknifeSeFromLoo(apply(folds, 1L, difference)) + out <- pecotmr:::.h2StratifiedReport(A, tau, folds)$enrichment + expect_equal( + out$enrichmentP, + 2 * pt(abs(difference(tau)) / se, 10L, lower.tail = FALSE) + ) + # A column covering every variant has no outside to compare with. + ones <- `attr<-`(cbind(ones = 1, annot = inside), "userCols", 1L) + whole <- pecotmr:::.h2StratifiedReport(ones, tau, folds)$enrichment + expect_equal(whole$enrichment, 1) + expect_true(is.na(whole$enrichmentP)) +}) + +test_that(".hdlFitBasis centers only columns whose zero is out of reach", { + M <- 20L + bin <- rep(0:1, each = 10L) + x <- seq(-1, 1, length.out = M) + # A column reaching 0 keeps its heritability parametrization; the base + # is a real variant's rate and an annotation beside it an excess. + b <- pecotmr:::.hdlFitBasis(cbind(base = 1, bin = bin)) + expect_equal(b$transform, diag(2L)) + expect_equal(b$size, c(M, 10)) + expect_equal(b$share, c(1, 1)) + expect_equal(b$lower, c(0, -1)) + expect_equal(b$upper, c(1, 1)) + # Each level of a partition is its own variants' rate. + expect_equal( + pecotmr:::.hdlFitBasis(cbind(a = bin, b = 1L - bin))$lower, + c(0, 0) + ) + # Annotations covering every variant leave the base no variant of its own. + expect_equal( + pecotmr:::.hdlFitBasis(cbind(base = 1, a = bin, b = 1L - bin))$lower, + c(-1, -1, -1) + ) + # A column above 0 throughout moves its location into the base, which + # then reads as the total heritability. + shifted <- pecotmr:::.hdlFitBasis(cbind(base = 1, x = x + 3)) + expect_equal(shifted$size, c(M, sum(abs(x)))) + expect_equal(shifted$share, c(1, 0)) + expect_equal(shifted$lower, c(0, -2)) + expect_equal(shifted$upper, c(1, 2)) + expect_equal( + as.vector(shifted$transform %*% c(0.5, 0.2)), + c(0.5 - 3 * 0.2, 0.2) + ) + # Columns that themselves span the all-ones vector cannot all be centered. + u <- seq(0.1, 0.9, length.out = M) + expect_equal( + pecotmr:::.hdlFitBasis(cbind(u = u, v = 1 - u))$transform, + diag(2L) + ) +}) + +# A block-level annotation, which the eigen-methods can identify, with values +# that keep every per-variant rate positive, and z-scores under it. +.h2ContinuousFixture <- function() { + d <- .h2RecoveryRefs(nBlocks = 12L) + set.seed(21) + raw <- rep(seq(-1.5, 1.5, length.out = 12L), each = 40L) + x <- raw - mean(raw) + rate <- 0.4 / d$M * (1 + 0.5 * x) + list(d = d, x = x, ss = .h2ScoreSumstats(d, d$simZ(rate))) +} + +test_that("HDL reads a continuous annotation whatever its zero", { + f <- .h2ContinuousFixture() + fit <- function(col) { + suppressWarnings(estimateH2( + f$ss, + f$d$eigenRef, + method = "hdl", + annotations = .h2BaselineOf(f$d, cbind(x = col)) + )) + } + codings <- list( + f$x + 3, + f$x + 10, + f$x, + f$x + 0.5, + f$x - min(f$x), + (f$x + 3) / 100 + ) + fits <- lapply(codings, fit) + taus <- map_dbl(fits, function(e) { + stratifiedHeritabilityEnrichment(e)$tau + }) * + c(1, 1, 1, 1, 1, 1 / 100) + # Moving the zero leaves the coefficient of x and the total alone, where + # it used to pin the base at 0 and take tau from the shift alone. + expect_gt(taus[[1L]], 0) + expect_equal(taus, rep(taus[[1L]], 6L), tolerance = 0.02) + expect_equal( + map_dbl(fits, heritabilityEstimate), + rep(heritabilityEstimate(fits[[1L]]), 6L), + tolerance = 0.02 + ) + # Enrichment only for the coding inside [0, 1], and with a real p-value. + part <- map(fits, stratifiedHeritabilityEnrichment) + expect_true(all(is.na(map_dbl(part[1:5], "enrichment")))) + expect_gt(part[[6L]]$enrichment, 1) + expect_gt(part[[6L]]$enrichmentP, 0) +}) + +test_that("a column of mean 0 fits by every estimator, without an enrichment", { + f <- .h2ContinuousFixture() + fits <- .h2FitAll(f$d, f$ss, .h2BaselineOf(f$d, cbind(x = f$x))) + for (m in names(fits)) { + part <- stratifiedHeritabilityEnrichment(fits[[m]]) + expect_true(is.finite(heritabilityEstimate(fits[[m]])), info = m) + expect_true(is.finite(part$tau), info = m) + expect_true(is.na(part$enrichment), info = m) + } +}) + +test_that("gLDSC's Omega floors a negative plug-in variance", { + R <- 0.5^abs(outer(1:4, 1:4, "-")) + negative <- pecotmr:::.gldscOmegaEigen( + list(R = R), + cbind(c(1, 1, 1, 1), c(-3, 0.5, 0, 0)), + 2 + ) + expect_true(all(is.finite(negative$values))) + # The floor reads the variants at 0, and only those. + floored <- pecotmr:::.gldscOmegaEigen( + list(R = R), + cbind(c(0, 1.5, 1, 1)), + 2 + ) + expect_equal(negative$values, floored$values) +}) + +test_that("sHDL's score test reads a candidate whatever its zero", { + f <- .h2ContinuousFixture() + gr <- GenomicRanges::granges(f$d$eigenRef, use.mcols = FALSE) + candidate <- function(col) { + cols <- cbind(base = 1, cand = col) + AnnotationMatrix( + cols, + gr, + data.frame( + name = colnames(cols), + tier = c("baseline", "candidate"), + type = "continuous" + ), + genome = "hg19" + ) + } + z <- function(col) { + fit <- suppressWarnings(estimateH2( + f$ss, + f$d$eigenRef, + method = "hdl", + annotations = candidate(col) + )) + scoreStats(fit)[[1L]]$z + } + # A mean of 0 used to scale the score by next to nothing, of either sign. + zero <- z(f$x) + expect_gt(zero, 0) + expect_equal(zero, z(f$x + 3), tolerance = 0.01) +}) + test_that("S-LDSC's candidate score test needs the reference's LD", { # The recovery reference's scores, without the LD they came from. d <- .h2RecoveryRefs(nBlocks = 4L) @@ -2461,6 +2670,51 @@ test_that("gLDSC reproduces the original gldsc", { } }) +test_that("stratified gLDSC reproduces the original gldsc", { + # Pinned from gldsc's own statements (R/middle_fun.R: gls.left.right, + # gls.estimator, oneout, with its crossprod helper as crossprod()) on a + # binary and a continuous annotation, and its jackknife's variances. + ref <- .h2ParityRef()$score + M <- length(ref) + cols <- cbind( + halfA = as.integer(seq_len(M) %% 2L == 0L), + probB = (seq_len(M) %% 5L) / 4 + ) + annot <- AnnotationMatrix( + cols, + GenomicRanges::granges(ref, use.mcols = FALSE), + data.frame( + name = colnames(cols), + tier = "baseline", + type = c("binary", "continuous") + ), + genome = unique(GenomeInfoDb::genome(ref)) + ) + fit <- pecotmr:::gldscUnivariate(.h2ParityZ(0.4, 5e4, 3), 5e4, ref, annot) + expect_equal( + fit$enrichment$tau, + c(-6.50492608301308e-06, 0.000122544026681053), + tolerance = 1e-5 + ) + expect_equal( + fit$enrichment$tauSe, + c(0.000105543613297095, 5.97338520034948e-05), + tolerance = 1e-5 + ) + expect_equal( + fit$enrichment$enrichment, + c(0.989676161122372, 1.09724359437187), + tolerance = 1e-6 + ) + expect_equal( + fit$enrichment$enrichmentSe, + c(0.17181162331594, 0.0548583368764506), + tolerance = 1e-6 + ) + expect_equal(fit$h2, 0.409557142849348, tolerance = 1e-6) + expect_equal(fit$intercept, -8.37517497402338, tolerance = 1e-5) +}) + # =========================================================================== # Study resolution, genome checking and the chi2 filter fallback @@ -2855,33 +3109,96 @@ test_that(".hdlLocusFit answers NA without signal, starts a flat locus at 0", { # ---- per-gene cis heritability: LAVA and FastHer ---- -test_that(".cisH2Lava is LAVA's adjusted R^2 and F test, reference-corrected", { +test_that(".cisH2Lava follows the reference's distance from the study's LD", { fx <- .hdlLocusFixture(3L, 0.1) z <- fx$corInput * sqrt(fx$n) - fit <- pecotmr:::.cisH2Lava( - z, - rep(fx$n, length(z)), - list(ldFactor = fx$ldFactor, nSamples = 150L) - ) - # LAVA's published estimator, through the matrix's own eigenpairs. e <- eigen(fx$R, symmetric = TRUE) lam <- e$values[e$values > 0] K <- which(cumsum(lam) / sum(lam) >= 0.99)[[1L]] r <- z / sqrt(z^2 + fx$n - 2) delta <- crossprod(e$vectors[, seq_len(K)], r) / sqrt(lam[seq_len(K)]) dtd <- sum(delta^2) - sigma <- (1 - dtd) / (fx$n - K - 1) - scale <- (150 - K - 1) / 149 - expect_equal(fit$nComponents, K) - expect_equal(fit$h2, (dtd - K * sigma) * scale, tolerance = 1e-8) - expect_equal( - fit$p, - pf(dtd / sigma * scale / K, K, fx$n - K - 1, lower.tail = FALSE), - tolerance = 1e-8 - ) + m <- K / (fx$n - 1) + # A reference of N holding a share rho of the study's samples, phi being + # 1 + (1 - 2 rho) n / N. Half the study's samples give 1 at any N. + cases <- list( + list(N = fx$n, rho = 1, phi = 0), + list(N = 2 * fx$n, rho = 1, phi = 0.5), + list(N = 400L, rho = 0.5, phi = 1), + list(N = fx$n, rho = 0, phi = 2), + list(N = fx$n, rho = 0.25, phi = 1.5) + ) + for (case in cases) { + fit <- pecotmr:::.cisH2Lava( + z, + rep(fx$n, length(z)), + list( + ldFactor = fx$ldFactor, + nSamples = case$N, + referenceOverlap = case$rho + ) + ) + v <- 1 + m * (case$phi - 1) + s <- (1 - m) * (fx$n + 1) / (2 * v) - 1 + expect_equal(fit$nComponents, K) + expect_equal(fit$h2, (dtd - m) / v, tolerance = 1e-8) + expect_equal( + fit$p, + pbeta(dtd, m * s, (1 - m) * s, lower.tail = FALSE), + tolerance = 1e-8 + ) + } + # No overlap is the default, and what it assumes is flagged only while the + # reference is small enough for that to matter. + bare <- function(N) { + pecotmr:::.cisH2Lava( + z, + rep(fx$n, length(z)), + list(ldFactor = fx$ldFactor, nSamples = N) + ) + } + expect_equal(bare(fx$n)$h2, (dtd - m) / (1 + m), tolerance = 1e-8) + expect_true(bare(2L * fx$n)$smallReference) + expect_false(bare(2L * fx$n + 1L)$smallReference) +}) + +test_that("LAVA's test has no Beta to fit at a very small n", { + expect_true(is.na(pecotmr:::.lavaP(0.5, 0.8, 1, 6))) + expect_true(is.finite(pecotmr:::.lavaP(0.5, 0.8, 1, 100))) }) -test_that(".cisH2Lava is NA with one LD component or contradicting LD", { +test_that("LAVA holds on real genotypes where delta'delta passes 1", { + # A panel of 3000 from one population, on real genotypes, with K / n near + # 0.6: the noise then fills most of delta'delta and the signal the rest. + data(eqtlRegionExample, envir = environment()) + af <- colMeans(eqtlRegionExample$X) / 2 + common <- eqtlRegionExample$X[, pmin(af, 1 - af) > 0.05] + set.seed(6L) + win <- common[, 301:1500] + n <- 150L + Xpop <- scale(win) + Rpop <- cor(win) + b <- replace(numeric(ncol(win)), sample.int(ncol(win), 3L), rnorm(3L)) + b <- b * sqrt(0.5 / sum(b * (Rpop %*% b))) + qtl <- sample.int(nrow(win), n, replace = TRUE) + y <- as.vector(Xpop[qtl, ] %*% b) + rnorm(n, sd = sqrt(0.5)) + r <- as.vector(cor(win[qtl, ], y)) + ld <- list( + R = cor(win[sample.int(nrow(win), 3000L, replace = TRUE), ]), + nSamples = 3000L + ) + fit <- pecotmr:::.lavaFit(r, n, ld) + dec <- pecotmr:::.cisH2Eigen(ld) + top <- seq_len(fit$nComponents) + dtd <- sum( + (crossprod(dec$vectors[, top], r) / sqrt(dec$values[top]))^2 + ) + expect_gt(dtd, 1) + expect_lt(abs(fit$h2 - 0.5), 0.2) + expect_lt(fit$p, 0.05) +}) + +test_that(".cisH2Lava is NA with too few LD components or samples", { # Two copies of one variant hold all the variance in one component. x <- c(1, -1, 2, 0, -2, 1, -1, 0) fit <- pecotmr:::.cisH2Lava( @@ -2891,21 +3208,21 @@ test_that(".cisH2Lava is NA with one LD component or contradicting LD", { ) expect_true(is.na(fit$h2) && is.na(fit$p)) expect_equal(fit$nComponents, 1L) - # Uncorrelated variants whose correlations with the trait are each near - # 1: their squares sum past 1, which the LD rules out. + # Two components leave no degrees of freedom in a study of 3. ldFactor <- cbind(c(1, 1, -1, -1), c(1, -1, 1, -1)) fit <- pecotmr:::.cisH2Lava( - c(30, 30), - c(100, 100), + c(1, 1), + c(3, 3), list(ldFactor = ldFactor, nSamples = 4L) ) expect_true(is.na(fit$h2) && is.na(fit$p)) + expect_equal(fit$nComponents, 2L) }) test_that(".cisH2FastHer is the individual-level maximum likelihood", { - # Standardized so that X'X = n R, X'y = sqrt(n) z and y'y = n, the + # Standardized so that X'X = n R, X'y = n r and y'y = n, the # summary-statistics likelihood is that of y ~ N(0, d (h2 X X' / m + - # (1 - h2) I)) itself. + # (1 - h2) I)) itself, r being the correlation behind each t statistic. set.seed(9) n <- 300 m <- 8 @@ -2913,8 +3230,9 @@ test_that(".cisH2FastHer is the individual-level maximum likelihood", { X <- scale(X) * sqrt(n / (n - 1)) y <- as.vector(X %*% rnorm(m, sd = 0.15)) + rnorm(n) y <- (y - mean(y)) / sqrt(mean((y - mean(y))^2)) + r <- as.vector(crossprod(X, y)) / n fit <- pecotmr:::.cisH2FastHer( - as.vector(crossprod(X, y)) / sqrt(n), + r * sqrt((n - 2) / (1 - r^2)), rep(n, m), list(R = crossprod(X) / n) ) @@ -2938,6 +3256,83 @@ test_that(".cisH2FastHer is the individual-level maximum likelihood", { expect_gt(fit$h2Se, 0) }) +# A strong QTL on real genotypes, with its LD estimated in the same samples: +# the correlations, their t statistics and the LD. +.cisH2StrongQtl <- function(seed, h2 = 0.6) { + data(eqtlRegionExample, envir = environment()) + G <- eqtlRegionExample$X + af <- colMeans(G) / 2 + set.seed(seed) + win <- G[, pmin(af, 1 - af) > 0.05][, 301:600] + Xs <- scale(win) * sqrt(nrow(win) / (nrow(win) - 1)) + g <- Xs[, sample(ncol(Xs), 1L)] + y <- sqrt(h2) * g + rnorm(length(g), sd = sqrt(1 - h2)) + r <- as.vector(cor(Xs, y)) + n <- nrow(Xs) + list( + r = r, + z = r * sqrt((n - 2) / (1 - r^2)), + n = n, + Xs = Xs, + y = y, + R = cor(win) + ) +} + +test_that(".cisH2HdlL reads a t statistic exactly and halves the boundary p", { + fx <- .cisH2StrongQtl(11L) + nVariant <- rep(fx$n, length(fx$z)) + ld <- list(R = fx$R, nSamples = fx$n) + fit <- pecotmr:::.cisH2HdlL(fx$z, nVariant, ld) + want <- pecotmr:::.hdlLocusFit(fx$r, ld, fx$n, fx$n) + expect_equal(fit$h2, want$h2) + expect_equal(fit$intercept, want$intercept) + expect_equal(fit$p, want$p / 2) +}) + +test_that("FastHer's likelihood is defined for a strong QTL on in-sample LD", { + # Reading t / sqrt(n) as the correlation overstates r^2 by 1 / (1 - r^2), + # which took the statistics' norm past the sample size. + fx <- .cisH2StrongQtl(12L) + nVariant <- rep(fx$n, length(fx$z)) + ld <- list(R = fx$R) + expect_gt(fx$n * max(fx$r^2) / (1 - max(fx$r^2)), 0.5 * fx$n) + fit <- pecotmr:::.cisH2FastHer(fx$z, nVariant, ld) + expect_true(is.finite(fit$h2)) + expect_true(is.na( + pecotmr:::.cisH2FastHer( + fx$r * fx$n, + nVariant, + list(R = diag(length(fx$z))) + )$h2 + )) +}) + +test_that("LAVA on in-sample LD is the adjusted R^2 of OLS on the components", { + fx <- .cisH2StrongQtl(13L, h2 = 0.2) + nVariant <- rep(fx$n, length(fx$z)) + fit <- pecotmr:::.cisH2Lava( + fx$z, + nVariant, + list(R = fx$R, nSamples = fx$n, referenceOverlap = 1) + ) + e <- eigen(fx$R, symmetric = TRUE) + K <- fit$nComponents + pcs <- fx$Xs %*% e$vectors[, seq_len(K)] + ols <- summary(lm(fx$y ~ pcs)) + expect_equal(fit$h2, ols$adj.r.squared, tolerance = 1e-6) + expect_equal( + fit$p, + pf( + ols$fstatistic[[1L]], + ols$fstatistic[[2L]], + ols$fstatistic[[3L]], + lower.tail = FALSE + ), + tolerance = 1e-6 + ) +}) + test_that(".cisH2FastHer is NA where its likelihood is undefined", { set.seed(10) ldFactor <- matrix(rnorm(60 * 30), 60, 30) @@ -2990,7 +3385,7 @@ test_that("estimateH2 gives each QtlSumStats row its cis estimate", { x <- .cisH2Example() fits <- map( c("lava", "hdlL", "fastHer"), - function(m) estimateH2(x, method = m) + function(m) estimateH2(x, method = m, referenceOverlap = 1) ) for (fit in fits) { expect_s4_class(fit, "H2Estimate") @@ -3018,25 +3413,89 @@ test_that("estimateH2 gives each QtlSumStats row its cis estimate", { expect_true(is.finite(fastHer$h2_se) && is.finite(fastHer$scale)) expect_true(is.na(fastHer$p_value)) # The default is LAVA. - expect_identical(estimateH2(x), lava) + expect_identical(methodNames(estimateH2(x, referenceOverlap = 1)), "lava") + expect_identical(estimateH2(x, referenceOverlap = 1), lava) # One estimate per selected row. two <- c(x, `$<-`(x, "trait", "ENSG_other")) - both <- estimateH2(two) + both <- estimateH2(two, referenceOverlap = 1) expect_identical(as.character(both$trait), c("ENSG_example", "ENSG_other")) expect_equal(heritabilityEstimate(both), rep(heritabilityEstimate(lava), 2)) expect_identical( - as.character(estimateH2(two, traitId = "ENSG_other")$trait), + as.character( + estimateH2( + two, + traitId = "ENSG_other", + referenceOverlap = 1 + )$trait + ), "ENSG_other" ) }) +test_that("estimateH2 reads the reference overlap, per row, for LAVA alone", { + x <- .cisH2Example() + two <- c(x, `$<-`(x, "trait", "ENSG_other")) + lava <- estimateH2(x, referenceOverlap = 1) + own <- heritabilityEstimate(lava) + half <- heritabilityEstimate(estimateH2(x, referenceOverlap = 0.5)) + # This reference is as large as the study: it is the study's own samples at + # 1 (phi 0), and half of them, with others, at 0.5 (phi 1). + expect_equal(own, half / (1 - lava$n_components / (165 - 1))) + # One per selected row. + expect_equal( + heritabilityEstimate(estimateH2(two, referenceOverlap = c(1, 0.5))), + c(own, half) + ) + expect_equal( + heritabilityEstimate( + estimateH2(two, traitId = "ENSG_other", referenceOverlap = 0.5) + ), + half + ) + expect_error( + estimateH2(two, referenceOverlap = c(1, 0.5, 0)), + "has 3 values for 2 selected" + ) + expect_error(estimateH2(x, referenceOverlap = 2), "referenceOverlap") + expect_error(estimateH2(x, referenceOverlap = NA_real_), "referenceOverlap") + # A reference cannot hold more of the study than it has samples. + big <- .cisH2Example(N = rep(1000L, 200L)) + expect_error( + estimateH2(big, referenceOverlap = 1), + "puts 1000 of the 1000 study samples in an LD reference of 165" + ) + # HDL-L and FastHer do not read it. + for (m in c("hdlL", "fastHer")) { + expect_equal( + heritabilityEstimate(estimateH2(x, method = m)), + heritabilityEstimate( + estimateH2(x, method = m, referenceOverlap = 1) + ) + ) + } +}) + +test_that("estimateH2 warns when LAVA assumes no overlap, small reference", { + x <- .cisH2Example() + two <- c(x, `$<-`(x, "trait", "ENSG_other")) + expect_warning(estimateH2(two), "2 of 2 gene\\(s\\) has at most twice") + expect_warning(estimateH2(x), "ENSG_example") + # Saying so, or a reference big enough, ends it; the other methods never had + # it. + expect_silent(estimateH2(x, referenceOverlap = 1)) + expect_silent(estimateH2(x, referenceOverlap = 0.5)) + expect_silent(estimateH2(x, method = "hdlL")) + smallStudy <- .cisH2Example(N = rep(80L, 200L)) + expect_silent(estimateH2(smallStudy)) +}) + test_that("estimateH2 analyses a row over its cis window", { tp <- GenomicRanges::GRanges( "chr22", IRanges::IRanges(15200000L, width = 1L) ) x <- .cisH2Example(traitPos = tp) - fit <- estimateH2(x, cisWindow = 1e5) + fit <- estimateH2(x, cisWindow = 1e5, referenceOverlap = 1) expect_identical( IRanges::ranges(fit[[1]]), IRanges::IRanges(15100000L, 15300000L) diff --git a/tests/testthat/test_jointEngine.R b/tests/testthat/test_jointEngine.R index 651ad6750..f8485e6c4 100644 --- a/tests/testthat/test_jointEngine.R +++ b/tests/testthat/test_jointEngine.R @@ -308,7 +308,6 @@ test_that(".runJointCell: cross-context FM uses the per-fold mr.mash prior", { trait, fitRetention, weightStandardized, - weightsDataType, verbose, ... ) { @@ -1434,8 +1433,40 @@ test_that("joint TWAS: an FM-derived method reuses fine-mapping's CV", { # An individual-level enumerator's units built into the groups the engine # fits, as .runJointCell() builds them one unit at a time. -.je_build <- function(units) { - pecotmr:::.jeConcat(map(units, pecotmr:::.unitGroups)) +.je_build <- function(units, data = NULL, args = list()) { + pecotmr:::.jeConcat(map(units, .je_buildUnit, data = data, args = args)) +} + +.je_buildUnit <- function(unit, data, args) { + if (is(unit, "JointGroup")) { + return(pecotmr:::.unitGroups(unit)) + } + hasWindow <- !is.null(args$region) || !is.null(args$cisWindow) + spec <- if (hasWindow) { + pecotmr:::.unitSpec(unit, data, args) + } else { + pecotmr:::.qtlFitSpec(NULL, unit$xContexts, unit$samples) + } + run <- list( + dataset = data, + filter = args$genotypeFilter, + residualizationParam = args$residualizationParam, + withAf = FALSE + ) + pecotmr:::.unitGroups(unit, pecotmr:::.qtlFitBlock(spec, run, NULL)) +} + +# Stands in for a window's residualized genotypes. +.je_X <- function(...) { + matrix( + 0, + 4, + 2, + dimnames = list( + paste0("s", 1:4), + c("chr1:100:A:G", "chr1:200:A:G") + ) + ) } test_that(".enumCrossContextIndividual: one group per trait in >= 2 contexts", { @@ -1446,28 +1477,19 @@ test_that(".enumCrossContextIndividual: one group per trait in >= 2 contexts", { ) local_mocked_bindings( studyName = function(data) "S", - .buildIndividualCrossContextXy = function( - data, - tid, - scopedContexts, - cisWindow, - verbose, - label, - region = NULL, - ... - ) { + .crossContextPlan = function(data, tid, ...) { if (tid == "G2") { return(NULL) } # skip branch - X <- matrix( - 0, - 4, - 2, - dimnames = list( - paste0("s", 1:4), - c("chr1:100:A:G", "chr1:200:A:G") - ) + list( + perTraitContexts = c("c1", "c2"), + Yres = list(), + fitSamples = paste0("s", 1:4), + report = list() ) + }, + .fmResidGeno = .je_X, + .crossContextAssemble = function(X, ...) { Y <- matrix( 0, 4, @@ -1559,36 +1581,26 @@ test_that(".enumCrossTraitIndividual: a group per context with 2+ traits", { ) local_mocked_bindings( studyName = function(data) "S", - .buildIndividualCrossTraitXy = function( - data, - cx, - scopedTraits, - cisWindow, - verbose, - label, - studyName, - region = NULL, - ... - ) { + .crossTraitPlan = function(data, cx, ...) { if (cx == "c2") { return(NULL) } # skip branch - X <- matrix( - 0, - 4, - 2, - dimnames = list( - paste0("s", 1:4), - c("chr1:100:A:G", "chr1:200:A:G") - ) + list( + se = .je_mkSe(), + traitsHere = c("G1", "G2"), + allY = NULL, + fitSamples = paste0("s", 1:4) ) + }, + .fmResidGeno = .je_X, + .crossTraitAssemble = function(X, allY, traitsHere, se) { Y <- matrix( 0, 4, 2, dimnames = list(paste0("s", 1:4), c("G1", "G2")) ) - list(X = X, Y = Y, traitsHere = c("G1", "G2"), se = .je_mkSe()) + list(X = X, Y = Y, traitsHere = traitsHere, se = se) }, .package = "pecotmr" ) @@ -1665,36 +1677,28 @@ test_that(".enumComposedIndividual: one group of every (context, trait)", { ) local_mocked_bindings( studyName = function(data) "S", - .buildComposedIndividualXy = function( - data, - scope, - studyName, - cisWindow, - verbose, - label, - region = NULL, - ... - ) { + .composedPlan = function(...) { + list( + tuples = list(), + allContexts = c("c1", "c2"), + allTraits = c("gA", "gB"), + YresList = list(), + fitSamples = paste0("s", 1:4) + ) + }, + .fmResidGeno = .je_X, + .composedAlignXy = function(sides, tuples) { Y <- matrix( 0, 4, 3, dimnames = list(paste0("s", 1:4), c("c1:gA", "c1:gB", "c2:gA")) ) - X <- matrix( - 0, - 4, - 2, - dimnames = list( - paste0("s", 1:4), - c("chr1:100:A:G", "chr1:200:A:G") - ) - ) - list(X = X, Y = Y, tuples = list()) + list(X = sides$X, Y = Y, tuples = list()) }, .package = "pecotmr" ) - g <- pecotmr:::.enumComposedIndividual(NULL, scope) + g <- .je_build(pecotmr:::.enumComposedIndividual(NULL, scope)) expect_length(g, 1L) expect_equal( as.character(.jgConditions(g[[1L]])$context), @@ -1709,7 +1713,7 @@ test_that(".enumComposedIndividual: one group of every (context, trait)", { test_that(".enumComposedIndividual: study not in scope / NULL xy -> empty", { local_mocked_bindings( studyName = function(data) "S", - .buildComposedIndividualXy = function(...) NULL, + .composedPlan = function(...) NULL, .package = "pecotmr" ) expect_length( @@ -1742,29 +1746,50 @@ test_that(".enumTopPcIndividual enumerates only the studies in scope", { list() ) seen <- list() + samp <- paste0("s", 1:5) local_mocked_bindings( - .enumTopPcForContext = function( - cx, + .enumContextTraits = function(...) c("g1", "g2"), + .fmResidPheno = function( data, - scope, - args, - studyName, - phenotypeFilter + contexts, + traitId, + phenotypeFilter, + ... ) { - seen[[cx]] <<- list(study = studyName, filter = phenotypeFilter) - list(cx) + seen[[contexts]] <<- phenotypeFilter + matrix( + as.numeric(1:10), + 5, + 2, + dimnames = list(samp, traitId) + ) + }, + .fmTopPcScores = function(Y, nPCs) { + matrix(as.numeric(1:5), 5, 1, dimnames = list(samp, "topPC1")) + }, + .fmResidGeno = function(...) { + abort("built a top-PC unit's X when enumerating") }, .package = "pecotmr" ) scope <- list(studies = "study1", contexts = list(study1 = c("a", "b"))) - # One unit per context, built only when the engine reaches it. + # One unit per context, its phenotypes in hand and its X not built yet. units <- pecotmr:::.enumTopPcIndividual(qd, scope) expect_length(units, 2L) - expect_length(seen, 0L) - expect_identical(.je_build(units), list("a", "b")) - expect_identical(seen$a$study, "study1") + expect_equal(units[[1L]]$samples, samp) + expect_equal(units[[1L]]$xTraits, c("g1", "g2")) # The joint engine's own phenotype QC when the run supplied none. - expect_identical(seen$a$filter$naAction, "drop") + expect_identical(seen$a$naAction, "drop") + X <- matrix(0, 5, 2, dimnames = list(samp, c("chr1:1:A:G", "chr1:2:A:G"))) + built <- pecotmr:::.unitGroups(units[[1L]], list(X = X)) + expect_length(built, 1L) + expect_identical(.jgConditions(built[[1L]])$trait, "topPC1") + # A single trait, or no usable components, leaves no unit. + local_mocked_bindings( + .enumContextTraits = function(...) "g1", + .package = "pecotmr" + ) + expect_length(pecotmr:::.enumTopPcIndividual(qd, scope), 0L) }) test_that(".enumUnivariateIndividual: a 1-condition group per context, trait", { @@ -1851,6 +1876,110 @@ test_that(".enumUnivariateIndividual: too few shared samples skips the tuple", { ) }) +test_that("a univariate group's genotypes are filtered on the samples fit", { + skip_if_not_installed("pgenlibr") + qd <- .fitSamplesDataset() + filter <- pecotmr:::.qtlResolveFilter(GenotypeFilterParam(mafCutoff = 0.05)) + args <- list( + region = .fitSamplesWindow(), + residualizationParam = ResidualizationParam(), + genotypeFilter = filter + ) + scope <- list( + studies = "s", + contexts = list(s = "brain"), + traits = list(s = c("g1", "g2", "g3")) + ) + units <- pecotmr:::.enumUnivariateIndividual(qd, scope, args) + g2 <- .je_build(units[2L], qd, args)[[1L]] + fitSamples <- rownames(pecotmr:::.jgY(g2)) + expect_length(fitSamples, 25L) + .expectFitOn( + pecotmr:::.jgX(g2), + fitSamples, + pecotmr:::.fmResidGeno( + qd, + args$residualizationParam, + filter, + contexts = "brain", + region = args$region, + samples = fitSamples + ) + ) +}) + +test_that("joint fits share one read, allele frequencies included", { + skip_if_not_installed("pgenlibr") + skip_if_not_installed("mvsusieR") + data <- .fitSamplesDataset() + run <- .countBlocks(fineMappingPipeline( + data, + methods = "mvsusie", + region = .fitSamplesWindow(), + seed = 1L + )) + expect_s4_class(run$result, "QtlFineMappingResult") + expect_equal(run$reads, 1L) + # g2 is missing the first five of the 30 samples observed, so a fit has 25, + # and the panel spells its ids with underscores. + top <- S4Vectors::mcols(topLoci(run$result)) + geno <- as.matrix(genotypes(data)) + onFit <- colMeans(geno[6:30, , drop = FALSE], na.rm = TRUE) / 2 + expect_equal( + top$af, + unname(onFit[match(top$variant_id, chartr("_", ":", names(onFit)))]) + ) +}) + +test_that("a joint unit plans its genotypes on the samples it fits", { + skip_if_not_installed("pgenlibr") + qd <- .fitSamplesDataset() + args <- list( + region = .fitSamplesWindow(), + residualizationParam = ResidualizationParam(), + genotypeFilter = pecotmr:::.qtlResolveFilter(), + phenotypeFilter = pecotmr:::.jointPhenotypeFilter(list()), + verbose = 0 + ) + scope <- list( + studies = "s", + contexts = list(s = c("brain", "liver")), + traits = list(s = c("g1", "g2", "g3")) + ) + # g2 misses the first five of the 30 samples observed. No genotypes yet. + local_mocked_bindings( + .handleDosage = function(...) abort("read genotypes when planning"), + .package = "pecotmr" + ) + samples <- colnames(qd[["brain"]]) + onFit <- samples[6:30] + crossContext <- pecotmr:::.enumCrossContextIndividual(qd, scope, args) + expect_equal( + map(crossContext, "samples"), + list(samples, onFit, samples) + ) + expect_equal(crossContext[[2L]]$xContexts, c("brain", "liver")) + expect_equal(crossContext[[2L]]$xTraits, "g2") + crossTrait <- pecotmr:::.enumCrossTraitIndividual(qd, scope, args) + expect_equal(map(crossTrait, "samples"), list(onFit, onFit)) + expect_equal(crossTrait[[1L]]$xContexts, "brain") + expect_equal(crossTrait[[1L]]$xTraits, c("g1", "g2", "g3")) + composed <- pecotmr:::.enumComposedIndividual(qd, scope, args) + expect_length(composed, 1L) + expect_equal(composed[[1L]]$samples, onFit) + expect_equal(composed[[1L]]$xContexts, c("brain", "liver")) + # Its spec names that: the window the run asked for, on those contexts. + spec <- pecotmr:::.unitSpec(composed[[1L]], qd, args) + expect_equal(spec$window, args$region) + expect_equal(spec$contexts, c("brain", "liver")) + # What cannot be fit is no unit at all, so it asks for no genotypes. + tooFew <- list_assign(scope, traits = list(s = "g1")) + expect_length(pecotmr:::.enumCrossTraitIndividual(qd, tooFew, args), 0L) + # A group already built has its own genotypes. + g <- .je_mkGroup("G1") + expect_null(pecotmr:::.unitSpec(g, qd, args)) +}) + test_that(".enumUnivariateIndividual: study not in scope -> empty", { local_mocked_bindings(studyName = function(data) "S", .package = "pecotmr") expect_length( @@ -2572,22 +2701,21 @@ test_that(".runJointCell: a token whose fitter returns NULL is skipped", { expect_null(pecotmr:::.runJointCell(cell, pipe, NULL, NULL, "mvsusie")) }) -test_that(".runJointCell reads allele frequencies for fine-mapping only", { - # They go into fine-mapping's topLoci. TWAS never reads them, and - # computing them reads each group's genotypes a second time. - cell <- .je_synthCell(list(.je_mkGroup("G1"))) - local_mocked_bindings( - fitJointGroup = function(...) NULL, - .jointAfForGroup = function(...) abort("read the genotypes again"), - .package = "pecotmr" - ) - twas <- new("TwasJointPipeline", config = list()) - expect_null(pecotmr:::.runJointCell(cell, twas, NULL, NULL, "mrmash")) - fm <- new("FmJointPipeline", config = list(coverage = 0.95)) - expect_error( - pecotmr:::.runJointCell(cell, fm, NULL, NULL, "mvsusie"), - "read the genotypes again" - ) +test_that(".jointAfForGroup reads a group's frequencies off its block", { + g <- .je_mkGroup("G1") + ids <- colnames(pecotmr:::.jgX(g)) + block <- list( + X = pecotmr:::.jgX(g)[, rev(ids), drop = FALSE], + af = rev(seq_along(ids)) / 10 + ) + expect_equal(pecotmr:::.jointAfForGroup(g, block), seq_along(ids) / 10) + # TWAS has none on its blocks, and a group already built has no block. + expect_null(pecotmr:::.jointAfForGroup( + g, + list(X = pecotmr:::.jgX(g), af = NULL) + )) + expect_null(pecotmr:::.jointAfForGroup(g, NULL)) + expect_null(pecotmr:::.jointAfForGroup("not a group", block)) }) test_that(".runJointSpecs: region mode without traitId keeps locus traits", { @@ -3032,9 +3160,9 @@ test_that(".runJointSpecs row-binds TWAS results across specs", { calls <- 0L local_mocked_bindings( .jpConfig = function(pipeline) list(ldReference = NULL), - .runOneJointSpec = function(...) { + .jointSpecWork = function(...) { calls <<- calls + 1L - str_c("R", calls) + pecotmr:::.qtlWorkValue(str_c("R", calls)) }, .rbindTwasWeights = function(out, res, ldReference) { str_c(out, "+", res) @@ -3059,9 +3187,9 @@ test_that(".runJointSpecs row-binds fine-mapping results across specs", { calls <- 0L local_mocked_bindings( .jpConfig = function(pipeline) list(ldReference = NULL), - .runOneJointSpec = function(...) { + .jointSpecWork = function(...) { calls <<- calls + 1L - str_c("F", calls) + pecotmr:::.qtlWorkValue(str_c("F", calls)) }, .rbindFineMappingResult = function(out, res, ldReference) { str_c(out, "&", res) @@ -3085,8 +3213,8 @@ test_that(".runJointSpecs row-binds fine-mapping results across specs", { test_that(".runJointSpecs skips a spec that produced nothing", { local_mocked_bindings( .jpConfig = function(pipeline) list(ldReference = NULL), - .runOneJointSpec = function(spec, ...) { - if (spec == "s1") NULL else "KEPT" + .jointSpecWork = function(spec, ...) { + pecotmr:::.qtlWorkValue(if (spec != "s1") "KEPT") }, .rbindTwasWeights = function(out, res, ldReference) { str_c(out, "+", res) @@ -3267,11 +3395,12 @@ test_that(".enumUnivariateIndividual reads a region instead of a cis window", { contexts = list(S1 = "cA"), traits = list(S1 = "t1") ) - .je_build(pecotmr:::.enumUnivariateIndividual( + args <- list(region = GenomicRanges::GRanges("chr1:1-1000")) + .je_build( + pecotmr:::.enumUnivariateIndividual(NULL, scope, args), NULL, - scope, - list(region = GenomicRanges::GRanges("chr1:1-1000")) - )) + args + ) # An explicit region replaces the trait-anchored cis window entirely -- # neither traitId nor cisWindow is passed down. expect_equal(seen$region, GenomicRanges::GRanges("chr1:1-1000")) @@ -3486,26 +3615,6 @@ test_that(".twasEnsembleCollect gives empty parts when no token contributes", { expect_identical(out$rsq, c()) }) -test_that(".jointAfForGroup is NULL without a QtlDataset or 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)) - # Anything but an individual group has no design matrix to read - # frequencies from. - expect_null(.jointAfForGroup(qtlDatasetExample, "not a group")) -}) - test_that(".jointMrmashRssWeights passes the caller's mr.mash options", { # The summary-statistics joint fit called mrmashRssWeights() with the data # alone, so options given for mr.mash never reached an explicit joint fit. @@ -3816,3 +3925,35 @@ test_that("a joint fit's rows record their position in it", { ) expect_false(is_in("jointIndex", colnames(S4Vectors::mcols(plain)))) }) + +test_that(".enumComposedIndividualGroup needs two samples both sides share", { + X <- matrix(1, 1L, 1L, dimnames = list("s1", "v1")) + planned <- list( + YresList = list(c1 = matrix(1, 1L, 1L, dimnames = list("s1", "t1"))), + tuples = NULL + ) + expect_null(pecotmr:::.enumComposedIndividualGroup("study", planned, X)) +}) + +test_that(".deferTopPc plans nothing when the traits give no scores", { + local_mocked_bindings( + .enumContextTraits = function(cx, data, scope, studyName) c("a", "b"), + .fmResidPheno = function(...) NULL, + .package = "pecotmr" + ) + expect_null( + pecotmr:::.deferTopPc("brain", NULL, NULL, list(), "study", NULL) + ) +}) + +test_that(".jointRetainX asks only a method that can hold X", { + retain <- pecotmr:::.jointRetainX + expect_true(retain(list(retainX = TRUE), "lasso")) + expect_false(retain(list(retainX = FALSE), "lasso")) + # The sumstats joint record carries no such setting. + expect_false(retain(list(), "lasso")) + # A fine-mapping token's rows keep no fit. + expect_false(retain(list(retainX = TRUE), "susie")) + # Beside a method that holds X, one that has none is not asked. + expect_false(retain(list(retainX = TRUE), "bayesR")) +}) diff --git a/tests/testthat/test_jointSpecification.R b/tests/testthat/test_jointSpecification.R index f447addd9..63ced8c9f 100644 --- a/tests/testthat/test_jointSpecification.R +++ b/tests/testthat/test_jointSpecification.R @@ -1,6 +1,166 @@ # Tests for the joint-specification grammar and input-argument parsers # (R/jointSpecification.R). +# The (X, Y) a joint fit is on, as the engine makes them: its phenotypes first, +# then, if it can be fit at all, its genotypes on the samples they leave. +.js_residGeno <- function( + data, + contexts, + traits, + cisWindow, + region, + residualizationParam, + genotypeFilter, + samples +) { + if (is.null(region)) { + return(pecotmr:::.fmResidGeno( + data, + contexts = contexts, + traitId = traits, + cisWindow = cisWindow, + samples = samples, + residualizationParam = residualizationParam, + genotypeFilter = genotypeFilter + )) + } + pecotmr:::.fmResidGeno( + data, + contexts = contexts, + region = region, + samples = samples, + residualizationParam = residualizationParam, + genotypeFilter = genotypeFilter + ) +} + +.js_crossContextXy <- function( + data, + tid, + scopedContexts, + cisWindow, + verbose, + label, + region = NULL, + residualizationParam = ResidualizationParam(), + genotypeFilter = pecotmr:::.qtlResolveFilter(), + phenotypeFilter = NULL +) { + planned <- pecotmr:::.crossContextPlan( + data, + tid, + scopedContexts, + verbose, + label, + residualizationParam, + phenotypeFilter + ) + if (is.null(planned)) { + return(NULL) + } + pecotmr:::.crossContextAssemble( + .js_residGeno( + data, + planned$perTraitContexts, + tid, + cisWindow, + region, + residualizationParam, + genotypeFilter, + planned$fitSamples + ), + planned$Yres, + planned$perTraitContexts, + planned$fitSamples, + planned$report + ) +} + +.js_crossTraitXy <- function( + data, + cx, + scopedTraits, + cisWindow, + verbose, + label, + studyName, + region = NULL, + residualizationParam = ResidualizationParam(), + genotypeFilter = pecotmr:::.qtlResolveFilter(), + phenotypeFilter = NULL +) { + planned <- pecotmr:::.crossTraitPlan( + data, + cx, + scopedTraits, + studyName, + verbose, + label, + residualizationParam, + phenotypeFilter + ) + if (is.null(planned)) { + return(NULL) + } + pecotmr:::.crossTraitAssemble( + .js_residGeno( + data, + cx, + planned$traitsHere, + cisWindow, + region, + residualizationParam, + genotypeFilter, + planned$fitSamples + ), + planned$allY, + planned$traitsHere, + planned$se + ) +} + +.js_composedXy <- function( + data, + scope, + studyName, + cisWindow, + verbose, + label, + region = NULL, + residualizationParam = ResidualizationParam(), + genotypeFilter = pecotmr:::.qtlResolveFilter(), + phenotypeFilter = NULL +) { + planned <- pecotmr:::.composedPlan( + data, + scope, + studyName, + verbose, + label, + residualizationParam, + phenotypeFilter + ) + if (is.null(planned)) { + return(NULL) + } + pecotmr:::.composedAlignXy( + list( + X = .js_residGeno( + data, + planned$allContexts, + planned$allTraits, + cisWindow, + region, + residualizationParam, + genotypeFilter, + planned$fitSamples + ), + YresList = planned$YresList + ), + planned$tuples + ) +} + # ----------------------------------------------------------------------------- # Fixture builders # ----------------------------------------------------------------------------- @@ -356,14 +516,17 @@ test_that("parseTraitIds: doubly-nested study->context validates per context", { test_that("fineMappingPipeline(QtlDataset): composed joint dispatcher wired", { # Composed dispatcher accepts axes = c("context", "trait") for individual- - # level input. On the fake fixture the genotype I/O fails when the - # dispatcher reaches the extractor, which proves the dispatcher was + # level input. Reaching the composed builder proves the dispatcher was # invoked (rather than the previous stub error). qd <- .js_makeQtlDataset( studyName = "A", contexts = c("brain", "liver"), traits = c("ENSG_A", "ENSG_B") ) + local_mocked_bindings( + .composedPlan = function(...) abort("composed builder reached"), + .package = "pecotmr" + ) expect_error( fineMappingPipeline( qd, @@ -371,7 +534,7 @@ test_that("fineMappingPipeline(QtlDataset): composed joint dispatcher wired", { cisWindow = 1000L, jointSpecification = list(c("context", "trait")) ), - "File does not exist" + "composed builder reached" ) }) @@ -477,14 +640,14 @@ context("joint dispatchers (fineMappingPipeline / twasWeightsPipeline)") } .jd_mockExtractor <- function(seed = 11, n_samples = 30L) { - function(handle, snpIdx) { + function(handle, snpIdx, sampleIdx = NULL) { set.seed(seed) panel <- matrix( rbinom(n_samples * nrow(snpInfo(handle)), 2, 0.3), nrow = n_samples, ncol = nrow(snpInfo(handle)) ) - panel[, snpIdx, drop = FALSE] + panel[sampleIdx %||% seq_len(nrow(panel)), snpIdx, drop = FALSE] } } @@ -1467,7 +1630,7 @@ test_that(".buildJointSumstatZMatrix: entries in different SNP orders error", { # Individual X/Y builders: the skip paths (return NULL / message branches) # ----------------------------------------------------------------------------- -test_that(".buildIndividualCrossContextXy: skips a trait in < 2 contexts", { +test_that("a cross-context (X, Y): skips a trait in < 2 contexts", { se1 <- .js_makeSe(traits = "g1") # g1 present se0 <- .js_makeSe(traits = "other") # g1 absent local_mocked_bindings( @@ -1476,7 +1639,7 @@ test_that(".buildIndividualCrossContextXy: skips a trait in < 2 contexts", { }, .package = "pecotmr" ) - expect_null(suppressMessages(pecotmr:::.buildIndividualCrossContextXy( + expect_null(suppressMessages(.js_crossContextXy( NULL, "g1", c("c1", "c2"), @@ -1486,7 +1649,7 @@ test_that(".buildIndividualCrossContextXy: skips a trait in < 2 contexts", { ))) }) -test_that(".buildIndividualCrossContextXy: region path + complete-case skip", { +test_that("a cross-context (X, Y): region path + complete-case skip", { se <- .js_makeSe(traits = "g1", samples = paste0("s", 1:6)) samp <- paste0("s", 1:6) region <- GenomicRanges::GRanges("chr1", IRanges::IRanges(1, 10000)) @@ -1516,7 +1679,7 @@ test_that(".buildIndividualCrossContextXy: region path + complete-case skip", { }, .package = "pecotmr" ) - expect_null(suppressMessages(pecotmr:::.buildIndividualCrossContextXy( + expect_null(suppressMessages(.js_crossContextXy( NULL, "g1", c("c1", "c2"), @@ -1527,7 +1690,7 @@ test_that(".buildIndividualCrossContextXy: region path + complete-case skip", { ))) }) -test_that(".buildIndividualCrossContextXy: too few shared samples skips", { +test_that("a cross-context (X, Y): too few shared samples skips", { se <- .js_makeSe(traits = "g1", samples = paste0("s", 1:6)) local_mocked_bindings( .qtlContextSe = function(data, context) se, @@ -1554,7 +1717,7 @@ test_that(".buildIndividualCrossContextXy: too few shared samples skips", { }, .package = "pecotmr" ) - expect_null(suppressMessages(pecotmr:::.buildIndividualCrossContextXy( + expect_null(suppressMessages(.js_crossContextXy( NULL, "g1", c("c1", "c2"), @@ -1564,6 +1727,47 @@ test_that(".buildIndividualCrossContextXy: too few shared samples skips", { ))) }) +test_that(".crossContextFitSamples skips when too few samples are shared", { + Yres <- list( + c1 = matrix(0, 2, 1, dimnames = list(c("s1", "s2"), "g1")), + c2 = matrix(0, 2, 1, dimnames = list(c("s2", "s3"), "g1")) + ) + expect_message( + res <- pecotmr:::.crossContextFitSamples( + Yres, + c("c1", "c2"), + list(verbose = 1, label = "lbl", tid = "g1") + ), + "trait 'g1' has too few shared samples across contexts" + ) + expect_null(res) +}) + +test_that(".msStudySpecs narrows a joint spec's scope to one component", { + skip_if_not_installed("pgenlibr") + qd <- .fitSamplesDataset() + spec <- function(scope) list(axes = "context", scope = scope) + # No scope, or one the component holds all of, stands as it is. + expect_identical( + pecotmr:::.msStudySpecs(list(spec(NULL)), qd), + list(spec(NULL)) + ) + # What it holds is kept, as re-parsing refuses what the data lacks. + expect_identical( + pecotmr:::.msStudySpecs( + list(spec(list(context = c("brain", "elsewhere")))), + qd + ), + list(spec(list(context = "brain"))) + ) + # A scope naming only what other components hold leaves this one no spec. + expect_length( + pecotmr:::.msStudySpecs(list(spec(list(study = "elsewhere"))), qd), + 0L + ) + expect_length(pecotmr:::.msStudySpecs(NULL, qd), 0L) +}) + test_that(".fmTraitsInRegion keeps overlapping traits; NULL keeps all", { se <- .js_makeSe(traits = c("g1", "g2")) # g1@~100, g2@~200 region <- GenomicRanges::GRanges("chr1", IRanges::IRanges(90, 160)) @@ -1579,7 +1783,7 @@ test_that(".fmTraitsInRegion keeps overlapping traits; NULL keeps all", { ) # NULL region -> unchanged }) -test_that(".buildIndividualCrossTraitXy: < 2 traits, region, complete skip", { +test_that("a cross-trait (X, Y): < 2 traits, region, complete skip", { se2 <- .js_makeSe(traits = c("g1", "g2"), samples = paste0("s", 1:6)) samp <- paste0("s", 1:6) # < 2 scoped traits in the context -> NULL. @@ -1587,7 +1791,7 @@ test_that(".buildIndividualCrossTraitXy: < 2 traits, region, complete skip", { .qtlContextSe = function(data, context) se2, .package = "pecotmr" ) - expect_null(suppressMessages(pecotmr:::.buildIndividualCrossTraitXy( + expect_null(suppressMessages(.js_crossTraitXy( NULL, "cx", "g1", @@ -1620,7 +1824,7 @@ test_that(".buildIndividualCrossTraitXy: < 2 traits, region, complete skip", { }, .package = "pecotmr" ) - expect_null(suppressMessages(pecotmr:::.buildIndividualCrossTraitXy( + expect_null(suppressMessages(.js_crossTraitXy( NULL, "cx", c("g1", "g2"), @@ -1632,7 +1836,7 @@ test_that(".buildIndividualCrossTraitXy: < 2 traits, region, complete skip", { ))) }) -test_that(".buildComposedIndividualXy: skip branches and single-context wrap", { +test_that("a composed (X, Y): skip branches and single-context wrap", { se <- .js_makeSe(traits = c("g1", "g2"), samples = paste0("s", 1:6)) samp <- paste0("s", 1:6) scope <- list(contexts = list(S = "c1"), traits = list(S = c("g1", "g2"))) @@ -1659,7 +1863,7 @@ test_that(".buildComposedIndividualXy: skip branches and single-context wrap", { }, .package = "pecotmr" ) - out <- suppressMessages(pecotmr:::.buildComposedIndividualXy( + out <- suppressMessages(.js_composedXy( NULL, scope, "S", @@ -1677,7 +1881,7 @@ test_that(".buildComposedIndividualXy: skip branches and single-context wrap", { }, .package = "pecotmr" ) - expect_null(suppressMessages(pecotmr:::.buildComposedIndividualXy( + expect_null(suppressMessages(.js_composedXy( NULL, scope1, "S", @@ -1827,7 +2031,9 @@ test_that(".twasDispatchJointSpecsQtlDataset: two blocks merge by key", { ) } local_mocked_bindings( - .twasJointSpecRegion = function(...) mkRegionRes(), + .twasJointSpecRegionWork = function(...) { + pecotmr:::.qtlWorkValue(mkRegionRes()) + }, .package = "pecotmr" ) res <- pecotmr:::.twasDispatchJointSpecsQtlDataset( @@ -2015,7 +2221,7 @@ test_that(".sp* QtlDataset: mismatched study / absent context return empty", { expect_equal(pecotmr:::.spListTraits(qd, context = "nope"), character(0)) }) -test_that(".buildIndividualCrossTraitXy: disjoint X/Y samples skip", { +test_that("a cross-trait (X, Y): disjoint X/Y samples skip", { se <- .js_makeSe(traits = c("g1", "g2"), samples = paste0("s", 1:6)) local_mocked_bindings( .qtlContextSe = function(data, context) se, @@ -2039,7 +2245,7 @@ test_that(".buildIndividualCrossTraitXy: disjoint X/Y samples skip", { }, .package = "pecotmr" ) - expect_null(suppressMessages(pecotmr:::.buildIndividualCrossTraitXy( + expect_null(suppressMessages(.js_crossTraitXy( NULL, "cx", c("g1", "g2"), @@ -2050,7 +2256,7 @@ test_that(".buildIndividualCrossTraitXy: disjoint X/Y samples skip", { ))) }) -test_that(".buildComposedIndividualXy: disjoint, missing or NA rows skip", { +test_that("a composed (X, Y): disjoint, missing or NA rows skip", { samp <- paste0("s", 1:6) se <- .js_makeSe(traits = c("g1", "g2"), samples = samp) scope <- list( @@ -2090,7 +2296,7 @@ test_that(".buildComposedIndividualXy: disjoint, missing or NA rows skip", { }, .package = "pecotmr" ) - expect_null(suppressMessages(pecotmr:::.buildComposedIndividualXy( + expect_null(suppressMessages(.js_composedXy( NULL, scope, "S", @@ -2124,7 +2330,7 @@ test_that(".buildComposedIndividualXy: disjoint, missing or NA rows skip", { }, # no g1/g2 .package = "pecotmr" ) - expect_null(suppressMessages(pecotmr:::.buildComposedIndividualXy( + expect_null(suppressMessages(.js_composedXy( NULL, list(contexts = list(S = c("c1", "c2")), traits = list(S = "g1")), "S", @@ -2168,7 +2374,7 @@ test_that(".buildComposedIndividualXy: disjoint, missing or NA rows skip", { }, .package = "pecotmr" ) - expect_null(suppressMessages(pecotmr:::.buildComposedIndividualXy( + expect_null(suppressMessages(.js_composedXy( NULL, list(contexts = list(S = c("c1", "c2")), traits = list(S = "g1")), "S", @@ -2227,7 +2433,9 @@ test_that(".fmDispatchJointSpecsQtlDataset: two region blocks are merged", { ) } local_mocked_bindings( - .fmDispatchJointSpecRegion = function(...) mkRes(), + .fmJointSpecRegionWork = function(...) { + pecotmr:::.qtlWorkValue(mkRes()) + }, .fmMergeEntries = function(entries) entries[[1L]], .package = "pecotmr" ) @@ -2255,7 +2463,7 @@ test_that(".fmDispatchJointSpecsQtlDataset: one region direct; all-NULL NULL", { entry = list(.js_fmEntry("chr1:100:A:G")) ) local_mocked_bindings( - .fmDispatchJointSpecRegion = function(...) res1, + .fmJointSpecRegionWork = function(...) pecotmr:::.qtlWorkValue(res1), .package = "pecotmr" ) out <- pecotmr:::.fmDispatchJointSpecsQtlDataset( @@ -2266,7 +2474,7 @@ test_that(".fmDispatchJointSpecsQtlDataset: one region direct; all-NULL NULL", { ) expect_identical(out, res1) # length-1 short-circuit local_mocked_bindings( - .fmDispatchJointSpecRegion = function(...) NULL, + .fmJointSpecRegionWork = function(...) pecotmr:::.qtlWorkValue(NULL), .package = "pecotmr" ) expect_null(pecotmr:::.fmDispatchJointSpecsQtlDataset( @@ -2465,6 +2673,133 @@ test_that(".msFoldStudyResults answers NULL when every fold is empty", { expect_null(pecotmr:::.msFoldStudyResults(list(NULL, NULL), NULL)) }) +test_that("the joint builders filter genotypes on the samples fit", { + skip_if_not_installed("pgenlibr") + qd <- .fitSamplesDataset() + rp <- ResidualizationParam() + filter <- pecotmr:::.qtlResolveFilter(GenotypeFilterParam(mafCutoff = 0.05)) + pf <- pecotmr:::.jointPhenotypeFilter(list()) + window <- .fitSamplesWindow() + # g2 misses five samples, which a fit on it cannot use. + contexts <- .js_crossContextXy( + qd, + "g2", + c("brain", "liver"), + NULL, + 0, + "lbl", + region = window, + residualizationParam = rp, + genotypeFilter = filter, + phenotypeFilter = pf + ) + traits <- .js_crossTraitXy( + qd, + "brain", + c("g1", "g2"), + NULL, + 0, + "lbl", + "s", + region = window, + residualizationParam = rp, + genotypeFilter = filter, + phenotypeFilter = pf + ) + composed <- .js_composedXy( + qd, + list( + contexts = list(s = c("brain", "liver")), + traits = list(s = c("g1", "g2")) + ), + "s", + NULL, + 0, + "lbl", + region = window, + residualizationParam = rp, + genotypeFilter = filter, + phenotypeFilter = pf + ) + fitSamples <- rownames(contexts$Y) + expect_length(fitSamples, 25L) + for (xy in list(contexts, traits, composed)) { + expect_setequal(rownames(xy$Y), fitSamples) + .expectFitOn( + xy$X, + fitSamples, + pecotmr:::.fmResidGeno( + qd, + rp, + filter, + contexts = "brain", + region = window, + samples = fitSamples + ) + ) + } +}) + +test_that("a joint fit with too few samples to fit reads no genotypes", { + skip_if_not_installed("pgenlibr") + qd <- .fitSamplesDataset() + local_mocked_bindings( + .handleDosage = function(...) abort("read genotypes"), + .fmResidPheno = function(data, contexts, traitId, ...) { + y <- matrix( + c(1, NA), + ncol = length(traitId), + nrow = 2L, + dimnames = list(c("s1", "s2"), traitId) + ) + if (length(contexts) == 1L) { + return(y) + } + set_names(rep(list(y), length(contexts)), contexts) + }, + .package = "pecotmr" + ) + filter <- pecotmr:::.qtlResolveFilter() + expect_null(.js_crossContextXy( + qd, + "g1", + c("brain", "liver"), + NULL, + 1, + "lbl", + region = .fitSamplesWindow(), + residualizationParam = ResidualizationParam(), + genotypeFilter = filter + )) |> + suppressMessages() + expect_null(.js_crossTraitXy( + qd, + "brain", + c("g1", "g2"), + NULL, + 0, + "lbl", + "s", + region = .fitSamplesWindow(), + residualizationParam = ResidualizationParam(), + genotypeFilter = filter + )) + expect_null(.js_composedXy( + qd, + list( + contexts = list(s = "brain"), + traits = list(s = c("g1", "g2")) + ), + "s", + NULL, + 0, + "lbl", + region = .fitSamplesWindow(), + residualizationParam = ResidualizationParam(), + genotypeFilter = filter + )) +}) + test_that("the joint builders read phenotypes under the run's phenotype QC", { # The cross-context, cross-trait and composed assemblies each read their # phenotypes through .fmResidPheno(); the run's resolved filter has to @@ -2478,7 +2813,7 @@ test_that("the joint builders read phenotypes under the run's phenotype QC", { .crossContextPerTrait = function(...) c("c1", "c2"), .crossTraitTooFew = function(...) FALSE, .qtlContextSe = function(data, context) se, - .buildResidGeno = function(...) { + .fmResidGeno = function(...) { matrix(0, 3, 1, dimnames = list(samp, "v1")) }, .fmResidPheno = function( @@ -2489,7 +2824,15 @@ test_that("the joint builders read phenotypes under the run's phenotype QC", { ... ) { seen[[length(seen) + 1L]] <<- phenotypeFilter - NULL + matrix(0, 3, 1, dimnames = list(samp, "t1")) + }, + .crossContextFitSamples = function(...) samp, + .composedFitSamples = function(...) samp, + .composedTuples = function(...) { + list( + list(context = "c1", trait = "t1"), + list(context = "c2", trait = "t2") + ) }, .crossContextAssemble = function(...) "cc", .crossTraitAssemble = function(...) "ct", @@ -2501,7 +2844,7 @@ test_that("the joint builders read phenotypes under the run's phenotype QC", { genotypeFilter = pecotmr:::.qtlResolveFilter(), phenotypeFilter = pf ) - pecotmr:::.buildIndividualCrossContextXy( + .js_crossContextXy( NULL, "t1", c("c1", "c2"), @@ -2512,7 +2855,7 @@ test_that("the joint builders read phenotypes under the run's phenotype QC", { genotypeFilter = common$genotypeFilter, phenotypeFilter = pf ) - pecotmr:::.buildIndividualCrossTraitXy( + .js_crossTraitXy( NULL, "c1", c("t1", "t2"), @@ -2524,14 +2867,13 @@ test_that("the joint builders read phenotypes under the run's phenotype QC", { genotypeFilter = common$genotypeFilter, phenotypeFilter = pf ) - pecotmr:::.composedResidualized( + pecotmr:::.composedPlan( NULL, - c("c1", "c2"), - c("t1", "t2"), - 1e5, NULL, + "S", + 0, + "lbl", common$residualizationParam, - common$genotypeFilter, pf ) expect_length(seen, 3L) diff --git a/tests/testthat/test_ld.R b/tests/testthat/test_ld.R index ecb564d70..12aec07ca 100644 --- a/tests/testthat/test_ld.R +++ b/tests/testthat/test_ld.R @@ -455,11 +455,14 @@ test_that("enforceDesignFullRank fallback to correlation pruning works", { ) colnames(X) <- sprintf("chr1:%d:A:G", 100L * seq_len(ncol(X))) C <- matrix(rnorm(n), n, 1) - result <- enforceDesignFullRank( - X, - C, - strategy = "correlation", - maxIterations = 2L + expect_warning( + result <- enforceDesignFullRank( + X, + C, + strategy = "correlation", + maxIterations = 2L + ), + "maxIterations reached" ) full_design <- cbind(1, result, C) expect_equal(qr(full_design)$rank, ncol(full_design)) @@ -944,7 +947,10 @@ test_that("computeLd population method handles column with all NA", { X[, 2] <- NA colnames(X) <- paste0("rs", 1:5) - R <- computeLd(X, method = "population") + expect_warning( + R <- computeLd(X, method = "population"), + "heterogeneous missingness" + ) expect_equal(dim(R), c(5L, 5L)) expect_equal(unname(diag(R)), rep(1, 5)) expect_equal(R[2, 4], 0) @@ -992,7 +998,10 @@ test_that("computeLd with heavy missingness still produces valid matrix", { expect_true(all(!is.na(R))) expect_equal(unname(diag(R)), rep(1, 5)) - R_pop <- computeLd(X, method = "population") + expect_warning( + R_pop <- computeLd(X, method = "population"), + "heterogeneous missingness" + ) expect_true(all(!is.na(R_pop))) expect_equal(unname(diag(R_pop)), rep(1, 5)) }) @@ -1309,7 +1318,7 @@ test_that(".ldFromRef rejects an ldReference that is not a genotype panel", { # @noRd .lds_mockExtractor <- function(seed = 7, nSamples = 30L) { - function(handle, snpIdx) { + function(handle, snpIdx, sampleIdx = NULL) { set.seed(seed) nSnp <- nrow(snpInfo(handle)) panel <- matrix( @@ -1317,7 +1326,7 @@ test_that(".ldFromRef rejects an ldReference that is not a genotype panel", { nrow = nSamples, ncol = nSnp ) - panel[, snpIdx, drop = FALSE] + panel[sampleIdx %||% seq_len(nrow(panel)), snpIdx, drop = FALSE] } } @@ -3091,8 +3100,11 @@ test_that("the same-reference check compares LdMatrix objects by their LD", { pr <- .ldrefPair() a <- pr$ld[1:200] b <- pr$ld[150:349] - expect_no_error( - pecotmr:::.requireMatchingLdRefs(a, b, "pipe") + # Those two share only part of their variants, reported once per session. + rlang::reset_warning_verbosity("pecotmrLdReferenceOverlap-pipe") + expect_warning( + pecotmr:::.requireMatchingLdRefs(a, b, "pipe"), + "share 51 variant" ) other <- LdMatrix( diag(200), diff --git a/tests/testthat/test_manifestLoaders.R b/tests/testthat/test_manifestLoaders.R index 2e1da4fe7..502850f5d 100644 --- a/tests/testthat/test_manifestLoaders.R +++ b/tests/testthat/test_manifestLoaders.R @@ -585,6 +585,32 @@ test_that("loadQtlSumStatsFromManifest builds a per-tuple collection", { expect_equal(as.character(obj$context), "Whole_Blood") }) +test_that("a QtlSumStats manifest records each tuple's molecular type", { + tmp <- withr::local_tempdir() + ssPath <- .writeSumstatsTsv(.toyGwasDf(5), file.path(tmp, "geneA.tsv")) + manifest <- data.frame( + study = "eqtl", + context = "Whole_Blood", + trait = c("geneA", "geneB", "geneC"), + sumStatsPath = ssPath, + data_type = c("expression", "splicing", ""), + stringsAsFactors = FALSE + ) + obj <- suppressWarnings(loadQtlSumStatsFromManifest( + manifest, + genome = "hg38", + ldReference = .toyLdRef() + )) + expect_equal(dataType(obj), c("expression", "splicing", NA_character_)) + manifest$data_type <- NULL + untyped <- suppressWarnings(loadQtlSumStatsFromManifest( + manifest, + genome = "hg38", + ldReference = .toyLdRef() + )) + expect_true(all(is.na(dataType(untyped)))) +}) + test_that("loadQtlSumStatsFromManifest derives the Wald z from beta/se", { tmp <- withr::local_tempdir() df <- .toyGwasDf(5) @@ -854,6 +880,46 @@ test_that("loadQtlDatasetFromManifest builds a single-study dataset", { expect_equal(contexts(qd), "ctxA") }) +test_that("a QtlDataset manifest records each context's molecular type", { + tmp <- withr::local_tempdir() + manifest <- data.frame( + context = c("ctxA", "ctxB", "ctxC"), + phenotypePath = c( + .writePhenoBed(file.path(tmp, "a.bed")), + .writePhenoBed(file.path(tmp, "b.bed")), + .writePhenoBed(file.path(tmp, "c.bed")) + ), + study = "studyX", + genotypePath = .toyRefPrefix(), + data_type = c("expression", "splicing", ""), + stringsAsFactors = FALSE + ) + qd <- loadQtlDatasetFromManifest(manifest) + expect_equal( + dataType(qd), + c(ctxA = "expression", ctxB = "splicing", ctxC = NA_character_) + ) + # A context's rows may leave it blank, and with none given nothing is set. + twoRows <- manifest[c(1L, 1L), ] + twoRows$data_type <- c("expression", NA) + expect_equal( + dataType(loadQtlDatasetFromManifest(twoRows)), + c(ctxA = "expression") + ) + blank <- manifest[1L, ] + blank$data_type <- "" + expect_equal( + dataType(loadQtlDatasetFromManifest(blank)), + c(ctxA = NA_character_) + ) + twoRows$data_type <- c("expression", "splicing") + expect_error( + loadQtlDatasetFromManifest(twoRows), + "ctxA (study 'studyX') declare several dataType values", + fixed = TRUE + ) +}) + test_that("loadQtlDatasetFromManifest accepts study/genotypes as arguments", { tmp <- withr::local_tempdir() bed <- .writePhenoBed(file.path(tmp, "ctxA.bed")) @@ -891,6 +957,24 @@ test_that("loadMultiStudyQtlDatasetFromManifest builds from >=2 studies", { expect_equal(sort(names(msd)), c("study1", "study2")) }) +test_that("a multi-study manifest types each study's contexts", { + tmp <- withr::local_tempdir() + manifest <- data.frame( + study = c("study1", "study2"), + context = "ctxA", + phenotypePath = c( + .writePhenoBed(file.path(tmp, "a.bed")), + .writePhenoBed(file.path(tmp, "b.bed")) + ), + genotypePath = .toyRefPrefix(), + dataType = c("expression", "protein"), + stringsAsFactors = FALSE + ) + msd <- loadMultiStudyQtlDatasetFromManifest(manifest) + expect_equal(dataType(msd[["study1"]]), c(ctxA = "expression")) + expect_equal(dataType(msd[["study2"]]), c(ctxA = "protein")) +}) + test_that("loadMultiStudyQtlDatasetFromManifest attaches a summary study", { tmp <- withr::local_tempdir() bedA <- .writePhenoBed(file.path(tmp, "a.bed")) diff --git a/tests/testthat/test_qtlEnrichmentPipeline.R b/tests/testthat/test_qtlEnrichmentPipeline.R index 7f1ac6c3e..26196d3c5 100644 --- a/tests/testthat/test_qtlEnrichmentPipeline.R +++ b/tests/testthat/test_qtlEnrichmentPipeline.R @@ -983,7 +983,7 @@ test_that("qtlEnrichment: one-hot fixture reproduces the pinned shrunk a1", { "sd (no shrinkage)", "sd (w/ shrinkage)" ) - single <- .qep_oneHotRun(lambda = 1) + single <- .qep_oneHotRun(lambda = 1, doubleShrinkage = FALSE) double <- .qep_oneHotRun( lambda = 1, doubleShrinkage = TRUE, @@ -1618,8 +1618,8 @@ test_that("qtlEnrichment(verbose = TRUE) reports the estimated priors", { test_that(".enrFlattenEnrichment: reads the estimator's own field names", { out <- pecotmr:::.enrFlattenEnrichment(.qep_mockEnrichment(2.0)()) - # enrichment is expm1 of the log-odds, so colocPipeline's - # p12 * (1 + enrichment) is the enloc prior p12 * exp(a1). + # enrichment is expm1 of the log-odds, reported for reading only; coloc + # scores with the colocP1, colocP2 and colocP12 columns. expect_equal(out$enrichment, 2.0) expect_equal(out$enrichmentLogOdds, log(3)) expect_equal(out$enrichmentSe, 0.1) @@ -1898,7 +1898,7 @@ test_that("QtlEnrichmentParam carries the estimator's tunable settings", { ) expect_equal(a$lambda, 1.0) expect_equal(a$impN, 25) - expect_false(a$doubleShrinkage) + expect_true(a$doubleShrinkage) expect_true(a$besselCorrection) }) diff --git a/tests/testthat/test_qtlSumStats.R b/tests/testthat/test_qtlSumStats.R index e2f696c78..84fa9ac3e 100644 --- a/tests/testthat/test_qtlSumStats.R +++ b/tests/testthat/test_qtlSumStats.R @@ -815,3 +815,35 @@ test_that("sumStats(annotateSignificance=) adds a derived logical mcol", { } expect_equal(S4Vectors::mcols(gr)$significant, exp1) }) + +test_that("QtlSumStats records each tuple's molecular type", { + build <- function(dataType = NULL) { + gr <- GenomicRanges::GRanges( + "chr1", + IRanges::IRanges(100 * 1:3, width = 1) + ) + S4Vectors::mcols(gr) <- S4Vectors::DataFrame( + SNP = paste0("rs", 1:3), + A1 = "A", + A2 = "G", + Z = c(1, 2, 3), + N = 100L + ) + QtlSumStats( + studyName = c("s1", "s1"), + context = c("brain", "liver"), + trait = c("g1", "g1"), + entry = list(gr, gr), + genome = "hg38", + dataType = dataType + ) + } + expect_equal(dataType(build()), c(NA_character_, NA_character_)) + expect_false("dataType" %in% colnames(S4Vectors::mcols(build()))) + expect_equal(dataType(build("expression")), c("expression", "expression")) + both <- build(c("expression", "splicing")) + expect_equal(dataType(both), c("expression", "splicing")) + expect_equal(dataType(both[2L]), "splicing") + expect_equal(as.character(both$dataType), c("expression", "splicing")) + expect_error(build(c("a", "b", "c")), "`dataType` must be length 1") +}) diff --git a/tests/testthat/test_regularizedRegressionWrappers.R b/tests/testthat/test_regularizedRegressionWrappers.R index 5414eddf7..ece702c41 100644 --- a/tests/testthat/test_regularizedRegressionWrappers.R +++ b/tests/testthat/test_regularizedRegressionWrappers.R @@ -1037,12 +1037,13 @@ test_that("SDPR on a factor of the LD draws as it does on the matrix", { ) }) -test_that("prsCsWeights and sdprWeights hand the engine z / sqrt(n)", { +test_that("prsCsWeights and sdprWeights hand the engine correlations", { # Both upstream tools fit the standardized marginal effects, so a z-only # stat (the QtlSumStats shape) and b with its standard error both reach # the engine on that scale. f <- .rrwStatLd() - want <- as.numeric(f$stat$z) / sqrt(f$n) + z <- as.numeric(f$stat$z) + want <- z / sqrt(z^2 + f$n - 2) local_mocked_bindings( .prsCsRun = function(bhat, ld, n, ...) list(betaEst = bhat), .sdprRun = function(bhat, ld, n, ...) list(betaEst = bhat) @@ -1061,6 +1062,32 @@ test_that("prsCsWeights and sdprWeights hand the engine z / sqrt(n)", { } }) +test_that("a QTL past r^2 = 0.5 reaches the engines as a correlation", { + # z / sqrt(n) passes 1 there, and the standard error the matrix engines + # read, sqrt((1 - bhat^2) / (n - 2)), used to come out NaN. + data(eqtlRegionExample, envir = environment()) + X <- eqtlRegionExample$X[, 1:30] + X <- X[, apply(X, 2L, sd) > 0] + set.seed(17) + Xs <- scale(X) + y <- sqrt(0.7) * Xs[, 1L] + rnorm(nrow(Xs), sd = sqrt(0.3)) + r <- as.vector(cor(Xs, y)) + n <- nrow(Xs) + stat <- list( + z = r * sqrt((n - 2) / (1 - r^2)), + n = rep(n, length(r)), + variantNames = colnames(X) + ) + expect_gt(max(abs(stat$z)) / sqrt(n), 1) + ld <- LdMatrix(cor(X), nSamples = n) + dense <- pecotmr:::.alphabetRssDense(stat, ld, 0.95) + expect_equal(dense$bhat, r) + expect_true(all(is.finite(dense$seb))) + skip_if_not_installed("qgg") + w <- bayesCRssWeights(stat, ld, "qgg", nIter = 200, nBurnin = 50) + expect_true(all(is.finite(w))) +}) + # Statistics over the first p variants of the test genotype panel, with the # same LD as an LdMatrix: a weight method must read the panel's LD (through # its standardized genotypes) as it reads the matrix. @@ -1208,7 +1235,7 @@ test_that(".rssMarginalCor reads cor / z / b and validates length", { ) expect_equal( pecotmr:::.rssMarginalCor(list(z = f$stat$z), n = f$n, p = f$p), - as.numeric(f$stat$z) / sqrt(f$n) + as.numeric(f$stat$z) / sqrt(as.numeric(f$stat$z)^2 + f$n - 2) ) expect_equal( pecotmr:::.rssMarginalCor(list(b = f$stat$b), n = f$n, p = f$p), @@ -1221,7 +1248,19 @@ test_that(".rssMarginalCor reads cor / z / b and validates length", { n = f$n, p = f$p ), - as.numeric(f$stat$z) / sqrt(f$n) + as.numeric(f$stat$z) / sqrt(as.numeric(f$stat$z)^2 + f$n - 2) + ) + # The correlation behind a t statistic, exactly, however strong: reading + # z / sqrt(n) instead would put it past 1. + r <- c(-0.9, 0.1, 0.8) + n <- 50 + expect_equal( + pecotmr:::.rssMarginalCor( + list(z = r * sqrt((n - 2) / (1 - r^2))), + n = n, + p = 3L + ), + r ) expect_error( pecotmr:::.rssMarginalCor(list(), n = f$n, p = f$p), @@ -1251,15 +1290,81 @@ test_that(".lassosumFirstMax returns the first index of the maximum", { expect_equal(pecotmr:::.lassosumFirstMax(c(-Inf, -Inf)), 1L) }) -test_that("held-out selection breaks a tie in correlation by variance", { +test_that("held-out selection picks on variance, then on correlation", { pick <- pecotmr:::.rssHeldOutPick - # Candidates 2 to 4 tie on the correlation, to rounding; of them, 3 - # explains the most held-out variance. - expect_equal(pick(c(0.1, 0.5, 0.5 + 1e-15, 0.5), c(9, 0.2, 0.3, 0.1)), 3L) - # A real difference in correlation wins whatever the variance. - expect_equal(pick(c(0.1, 0.5, 0.6, 0.5), c(9, 0.2, 0.1, 0.3)), 3L) + # A real difference in the variance wins whatever the correlation. + expect_equal(pick(c(0.9, 0.5, 0.6), c(-3, 0.2, 0.4)), 3L) + expect_equal(pick(c(0.9, 0.5, 0.6), c(0.5, 0.2, 0.4)), 1L) + # Candidates 2 to 4 tie on the variance, to rounding; of them, 3 has the + # best correlation. + expect_equal(pick(c(0.9, 0.5, 0.7, 0.6), c(-1, 0.3, 0.3 + 1e-15, 0.3)), 3L) # Tied on both, the first in the grid. - expect_equal(pick(c(0.4, 0.5, 0.5), c(1, 0.2, 0.2)), 2L) + expect_equal(pick(c(0.4, 0.5, 0.5), c(0.1, 0.2, 0.2)), 2L) +}) + +test_that("held-out selection passes over an empty fit while another exists", { + pick <- pecotmr:::.rssHeldOutPick + # An empty fit explains nothing, which beats every fit that overshoots. + expect_equal(pick(c(-Inf, 0.2, 0.3), c(0, -0.1, -0.2)), 2L) + # With nothing else left, the first in the grid. + expect_equal(pick(c(-Inf, -Inf), c(0, 0)), 1L) + # Empty on the full data but the best in every split: the fit returned + # would be empty, so the next one is. Candidate 1's splits fit the + # statistics themselves; candidate 2's are the same, heavily shrunk. + corInput <- c(0.3, 0.1, -0.1, 0.2) + refit <- list( + fn = function(trainCor, share) cbind(trainCor, 0.01 * trainCor), + args = list() + ) + sel <- function(candidates) { + set.seed(1) + pecotmr:::.rssSelectHeldOut( + candidates, + corInput, + list(R = diag(4), nSamples = 1000L), + 100, + refit + )$index + } + expect_equal(sel(cbind(corInput, 0.01 * corInput)), 1L) + expect_equal(sel(cbind(numeric(4), 0.01 * corInput)), 2L) +}) + +test_that("held-out selection refuses a fit far larger than the trait", { + # Real genotypes, a window of strong LD and one causal variant with many + # tags. The fit at s = 1 scores best on correlation, and a pick on + # correlation takes it, but its predicted variance is about 10,000. + data(eqtlRegionExample, envir = environment()) + G <- eqtlRegionExample$X + af <- colMeans(G) / 2 + G <- G[, pmin(af, 1 - af) > 0.05][, 1301:1500] + Gs <- scale(G) + h2 <- 0.6 + set.seed(1) + g <- as.numeric(Gs %*% replace(numeric(200), 1, 1)) + y <- g * sqrt(h2 / var(g)) + rnorm(nrow(G), sd = sqrt(1 - h2)) + study <- sample(nrow(G), 300L) + R <- cor(G[study, ]) + stat <- list( + cor = as.numeric(cor(G[study, ], y[study])), + n = rep(300, 200), + variantNames = colnames(G) + ) + w <- lassosumRssWeights( + stat, + LdMatrix(R, nSamples = 300L), + s = c(0.2, 1), + fitRetention = "full" + ) + predicted <- function(b) sum(b * (R %*% b)) + fit <- attr(w, "fit") + atOne <- fit$beta[, which(fit$path$s == 1)[20L]] + expect_gt(predicted(atOne), 100 * h2) + expect_lt(predicted(as.numeric(w)), 2 * h2) + expect_equal( + as.numeric(attr(w, "lassosum_selection")[["s"]]), + 0.2 + ) }) test_that(".lassosumSelectMinFbeta picks the minimum-fbeta candidate", { @@ -1627,3 +1732,48 @@ test_that("a flat prior name is routed into the prior group", { expect_equal(names(split$prior), "canonicalPriorMatrices") expect_equal(names(split$rest), "max_iter") }) + +test_that(".assertRetainX takes TRUE only for a full fit", { + check <- pecotmr:::.assertRetainX + expect_silent(check(TRUE, "full")) + expect_silent(check(FALSE, "full")) + for (level in c("none", "slim")) { + expect_silent(check(FALSE, level)) + expect_error( + check(TRUE, level), + sprintf("requires `fitRetention = \"full\"`, not \"%s\"", level), + fixed = TRUE + ) + } + expect_error(check(NA, "full"), "retainX") + expect_error(check("yes", "full"), "retainX") +}) + +test_that("each weight function holding X rejects retainX unless full", { + X <- matrix(0, 4L, 2L) + y <- numeric(4L) + rejects <- function(fn, ...) { + expect_error( + fn(X, y, ..., retainX = TRUE), + "requires `fitRetention", + fixed = TRUE + ) + expect_error( + fn(X, y, ..., fitRetention = "slim", retainX = TRUE), + "requires `fitRetention", + fixed = TRUE + ) + } + rejects(glmnetWeights, alpha = 1) + rejects(lassoWeights) + rejects(enetWeights) + rejects(ridgeWeights) + rejects(pecotmr:::ncvregWeights, penalty = "MCP") + rejects(scadWeights) + rejects(mcpWeights) + rejects(mrashWeights) + rejects(dprWeights) + rejects(dprVbWeights) + rejects(dprGibbsWeights) + rejects(dprAdaptiveGibbsWeights) +}) diff --git a/tests/testthat/test_resultWriters.R b/tests/testthat/test_resultWriters.R index b2bfeb14a..5aedeb70b 100644 --- a/tests/testthat/test_resultWriters.R +++ b/tests/testthat/test_resultWriters.R @@ -1253,7 +1253,7 @@ test_that("the TWAS-weight VCF's ##SAMPLE lines describe each model, not CV", { 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")) + expect_true(str_detect(susie, "DataType=expression")) # 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. expect_false(any(str_detect(sampleLines, "rsq=|corr=|pval=|CvRegions="))) diff --git a/tests/testthat/test_rrBayesAlphabet.R b/tests/testthat/test_rrBayesAlphabet.R index 345d33cca..c3aa0301a 100644 --- a/tests/testthat/test_rrBayesAlphabet.R +++ b/tests/testthat/test_rrBayesAlphabet.R @@ -437,7 +437,7 @@ test_that("SDPR's projection reads R (R + aI)^-1 R and R (R + aI)^-1 bhat", { d <- .rrwPanelStatLd() ldFactor <- pecotmr:::.ldFactorFromRef(d$panel, d$stat$variantNames, "test") R <- unname(crossprod(ldFactor) / (nrow(ldFactor) - 1)) - bhat <- d$stat$z / sqrt(500) + bhat <- pecotmr:::.zToCor(d$stat$z, 500) toSpan <- R %*% solve(R + 0.1 * diag(nrow(R))) projected <- pecotmr:::.alphabetProjected(bhat, ldFactor, 0.1) ldOf <- function(x) crossprod(x$ldFactor) / (nrow(x$ldFactor) - 1) @@ -473,7 +473,7 @@ test_that("BayesAlphabetRssOptions sets SDPR's projection or turns it off", { direct <- function(ldProjection) { set.seed(7) pecotmr:::bayesAlphabetRss( - d$stat$z / sqrt(500), + pecotmr:::.zToCor(d$stat$z, 500), ldFactor, 500, "bayesN", @@ -506,7 +506,7 @@ test_that("BayesAlphabetRssOptions sets SDPR's projection or turns it off", { test_that("pecotmr's sampler reports progress and refuses bad input", { d <- .rrwPanelStatLd() ldFactor <- pecotmr:::.ldFactorFromRef(d$panel, d$stat$variantNames, "test") - bhat <- d$stat$z / sqrt(500) + bhat <- pecotmr:::.zToCor(d$stat$z, 500) out <- capture.output( fit <- pecotmr:::bayesAlphabetRss( bhat, @@ -627,7 +627,7 @@ test_that("pecotmr's sampler reads the shrunk LD", { # for draw up to rounding. d <- .rrwPanelStatLd() ldFactor <- pecotmr:::.ldFactorFromRef(d$panel, d$stat$variantNames, "test") - bhat <- d$stat$z / sqrt(500) + bhat <- pecotmr:::.zToCor(d$stat$z, 500) expect_equal( .baFactorRun(bhat, ldFactor, shrinkCorr = 0.9), .baFactorRun(bhat, .baShrunkFactor(ldFactor, 0.9)), @@ -653,7 +653,7 @@ test_that("pecotmr's sampler refuses a sign jump only when asked", { test_that("pecotmr's Rao-Blackwellized sampler runs the same chain", { d <- .rrwPanelStatLd() ldFactor <- pecotmr:::.ldFactorFromRef(d$panel, d$stat$variantNames, "test") - bhat <- d$stat$z / sqrt(500) + bhat <- pecotmr:::.zToCor(d$stat$z, 500) draws <- .baFactorRun(bhat, ldFactor, "bayesC", pi = 0.1) means <- .baFactorRun( bhat, @@ -671,7 +671,7 @@ test_that("pecotmr's Rao-Blackwellized sampler runs the same chain", { test_that("pecotmr's sampler averages several chains over the kept ones", { d <- .rrwPanelStatLd() ldFactor <- pecotmr:::.ldFactorFromRef(d$panel, d$stat$variantNames, "test") - bhat <- d$stat$z / sqrt(500) + bhat <- pecotmr:::.zToCor(d$stat$z, 500) set.seed(8) fit <- pecotmr:::bayesAlphabetRss( bhat, @@ -808,7 +808,7 @@ test_that("hibayes' form sends a refused jump to the null class", { # changes the chain. d <- .rrwPanelStatLd() ldFactor <- pecotmr:::.ldFactorFromRef(d$panel, d$stat$variantNames, "test") - bhat <- d$stat$z / sqrt(500) + bhat <- pecotmr:::.zToCor(d$stat$z, 500) expect_false(identical( .baFactorRun(bhat, ldFactor, "bayesB", pi = 0.2, allowJumpSign = FALSE), .baFactorRun(bhat, ldFactor, "bayesB", pi = 0.2) diff --git a/tests/testthat/test_rrDpr.R b/tests/testthat/test_rrDpr.R index 5edf770ab..fd8600b21 100644 --- a/tests/testthat/test_rrDpr.R +++ b/tests/testthat/test_rrDpr.R @@ -46,6 +46,18 @@ test_that("dprWeights' slim fit drops the copy of the genotypes", { dprWeights(X, y, fittingMethod = "VB", fitRetention = "full"), "fit" ) - expect_true(is.matrix(full$x)) + expect_null(full$x) expect_equal(pecotmr:::.twasFitVariantIds(full), colnames(X)) + withX <- attr( + dprWeights( + X, + y, + fittingMethod = "VB", + fitRetention = "full", + retainX = TRUE + ), + "fit" + ) + expect_true(is.matrix(withX$x)) + expect_equal(pecotmr:::.twasFitVariantIds(withX), colnames(X)) }) diff --git a/tests/testthat/test_rrGlmnet.R b/tests/testthat/test_rrGlmnet.R index d7b0c71ea..92cd515f8 100644 --- a/tests/testthat/test_rrGlmnet.R +++ b/tests/testthat/test_rrGlmnet.R @@ -48,9 +48,12 @@ test_that("glmnetWeights errors when all columns are constant", { X <- matrix(rep(seq_len(p), each = n), nrow = n, ncol = p) y <- rnorm(n) - expect_error( - glmnetWeights(X, y, alpha = 1), - "matrix with 2 or more columns" + expect_warning( + expect_error( + glmnetWeights(X, y, alpha = 1), + "matrix with 2 or more columns" + ), + "dropping 5 zero-variance" ) }) @@ -138,4 +141,22 @@ test_that("glmnetWeights keeps the selected fit or the whole path", { # The path names its variants, so no second copy is added. expect_null(full$variantIds) expect_equal(pecotmr:::.twasFitVariantIds(full), fitted) + # Its stored calls name the data rather than holding it... + expect_identical(full$call$x, quote(x)) + expect_identical(full$glmnet.fit$call$x, quote(x)) + expect_identical(full$call$y, quote(y)) + # ...unless asked to keep it, and the coefficients do not depend on that. + withX <- attr( + suppressWarnings( + ridgeWeights(X, y, fitRetention = "full", retainX = TRUE) + ), + "fit" + ) + expect_true(is.matrix(withX$call$x)) + expect_gt( + length(serialize(withX$call, NULL)) - + length(serialize(full$call, NULL)), + length(serialize(X[, fitted], NULL)) + ) + expect_equal(dim(withX$glmnet.fit$beta), dim(full$glmnet.fit$beta)) }) diff --git a/tests/testthat/test_rrLassosum.R b/tests/testthat/test_rrLassosum.R index a4e254a0c..90b8569b0 100644 --- a/tests/testthat/test_rrLassosum.R +++ b/tests/testthat/test_rrLassosum.R @@ -131,7 +131,7 @@ test_that("lassosum2 fits lassosum's objective at s = delta / (1 + delta)", { skip_if_not_installed("bigsnpr") d <- .rrwPanelStatLd() R <- pecotmr:::.ldFromRef(d$panel, d$stat$variantNames, "test") - bhat <- d$stat$z / sqrt(500) + bhat <- pecotmr:::.zToCor(d$stat$z, 500) inp <- list(ld = list(R = R), bhat = bhat, n = rep(500, length(bhat))) engine <- pecotmr:::.bigsnprEngine(inp) on.exit(pecotmr:::.bigsnprRelease(engine)) diff --git a/tests/testthat/test_rrLdpred2.R b/tests/testthat/test_rrLdpred2.R index 4b5d9ce47..cc642937f 100644 --- a/tests/testthat/test_rrLdpred2.R +++ b/tests/testthat/test_rrLdpred2.R @@ -82,7 +82,7 @@ test_that("an LDpred2 chain stops once its effects outgrow the statistics", { 1L ) expect_true(run$diverged) - stat <- list(z = rep(1e-6, 5), n = rep(1, 5), variantNames = .rrwIds(5)) + stat <- list(cor = rep(1e-6, 5), n = rep(1, 5), variantNames = .rrwIds(5)) ld <- .rrwLdMatrix(crossprod(ldFactor) / 39, nSamples = 40L) skip_if_not_installed("bigsnpr") expect_error( @@ -212,12 +212,12 @@ test_that("LDpred2's heritability defaults to the window's LAVA estimate", { w <- ldpred2GridWeights(d$stat, d$panel, p = 0.1, fitRetention = "slim") sort(unique(attr(w, "fit")$path$h2)) } - local_mocked_bindings(.cisH2Lava = function(...) list(h2 = 0.2)) + local_mocked_bindings(.lavaFit = function(...) list(h2 = 0.2)) expect_equal(gridH2(), 0.2 * c(0.3, 0.7, 1, 1.4)) # Where LAVA gives no estimate, or a negative one, the grid scales # bigsnpr's floor. for (h2 in c(NA_real_, -0.05)) { - local_mocked_bindings(.cisH2Lava = function(...) list(h2 = h2)) + local_mocked_bindings(.lavaFit = function(...) list(h2 = h2)) expect_equal(gridH2(), 1e-3 * c(0.3, 0.7, 1, 1.4)) } # LAVA reads the reference's sample size. diff --git a/tests/testthat/test_rrMrash.R b/tests/testthat/test_rrMrash.R index a0075d115..e3f5bfa1d 100644 --- a/tests/testthat/test_rrMrash.R +++ b/tests/testthat/test_rrMrash.R @@ -57,7 +57,17 @@ test_that("mrashWeights' slim fit drops the copy of the data", { expect_equal(slim$variantIds, colnames(X)) set.seed(1) full <- attr(mrashWeights(X, y, fitRetention = "full"), "fit") + # All of the engine's object but its copy of X, so it names its variants. expect_true(is.list(full$data)) - expect_null(full$variantIds) + expect_null(full$data$X) + expect_false(is.null(full$update.order)) + expect_equal(full$variantIds, colnames(X)) expect_equal(pecotmr:::.twasFitVariantIds(full), colnames(X)) + withX <- attr( + mrashWeights(X, y, fitRetention = "full", retainX = TRUE), + "fit" + ) + expect_identical(colnames(withX$data$X), colnames(X)) + expect_null(withX$variantIds) + expect_equal(pecotmr:::.twasFitVariantIds(withX), colnames(X)) }) diff --git a/tests/testthat/test_rrNcvreg.R b/tests/testthat/test_rrNcvreg.R index 9b3a6f42f..c1956e7cb 100644 --- a/tests/testthat/test_rrNcvreg.R +++ b/tests/testthat/test_rrNcvreg.R @@ -66,4 +66,10 @@ test_that("ncvregWeights keeps the selected fit or the path without X", { expect_null(full$fit$X) expect_null(full$variantIds) expect_equal(pecotmr:::.twasFitVariantIds(full), colnames(X)) + withX <- attr( + mcpWeights(X, y, fitRetention = "full", retainX = TRUE), + "fit" + ) + expect_true(is.matrix(withX$fit$X)) + expect_equal(pecotmr:::.twasFitVariantIds(withX), colnames(X)) }) diff --git a/tests/testthat/test_rrPenalizedRss.R b/tests/testthat/test_rrPenalizedRss.R index 78ccfaab4..ca2bb4982 100644 --- a/tests/testthat/test_rrPenalizedRss.R +++ b/tests/testthat/test_rrPenalizedRss.R @@ -300,7 +300,7 @@ test_that("penalizedRss reproduces glmnet's elastic-net path on exact LD", { lambda = lam, standardize = FALSE, intercept = FALSE, - thresh = 1e-14 + control = list(thresh = 1e-14) ) expect_equal( rss$beta, @@ -581,9 +581,9 @@ test_that("ridgeRssWeights fits the alpha = 0 mixing over the ridge path", { }) test_that("a ridge's fits at s = 1, alike but for scale, are told apart", { - # At s = 1 each fit is c / (1 + lambda), so all tie on the selection's - # correlation. Held-out selection lets the variance each explains set - # the scale, so its pick does not depend on the grid's order; lassosum's + # At s = 1 each fit is c / (1 + lambda), so all tie on a correlation. + # Held-out selection lets the variance each explains set the scale, so + # its pick does not depend on the grid's order; lassosum's # pseudovalidation takes the grid's first. f <- .prs_weightsInput() lambda <- c(0.1, 0.3, 1, 3, 10) diff --git a/tests/testthat/test_sumstatsQc.R b/tests/testthat/test_sumstatsQc.R index 78df0e7a7..8b1eb0ec3 100644 --- a/tests/testthat/test_sumstatsQc.R +++ b/tests/testthat/test_sumstatsQc.R @@ -2325,14 +2325,14 @@ context("summaryStatsQc") } .ssQ_mockExtractor <- function(seed = 13, n_samples = 60L) { - function(handle, snpIdx) { + function(handle, snpIdx, sampleIdx = NULL) { set.seed(seed) panel <- matrix( rbinom(n_samples * nrow(snpInfo(handle)), 2, 0.3), nrow = n_samples, ncol = nrow(snpInfo(handle)) ) - panel[, snpIdx, drop = FALSE] + panel[sampleIdx %||% seq_len(nrow(panel)), snpIdx, drop = FALSE] } } @@ -2728,6 +2728,22 @@ test_that("summaryStatsQc: keepVariants subsets entries, records the drop", { expect_equal(ea$variantsOut, 2L) }) +test_that("summaryStatsQc: keepVariants match as every keep list is matched", { + # rs1 to rs4 sit at chr1:100 to chr1:400 with alleles G/A. + ss <- .ssQ_makeGwasSumStats() + nKept <- function(...) { + res <- summaryStatsQc(ss, keepVariants = c(...)) + res$qcAudit[[1L]]$variantsOut + } + other <- "chr1:400:G:A" + expect_equal(nKept("chr1:100:G:A", other), 2L) + expect_equal(nKept("chr1:100:A:G", other), 2L) + expect_equal(nKept("1_100_G_A", other), 2L) + expect_equal(nKept("chr1:100:G:A", "chr1:100:A:G", other), 2L) + # Ids and rsIDs together, each matched by what it carries. + expect_equal(nKept("rs3", other), 2L) +}) + test_that("summaryStatsQc: skipRegion drops overlapping variants", { ss <- .ssQ_makeGwasSumStats() res <- summaryStatsQc( @@ -2785,6 +2801,10 @@ test_that("summaryStatsQc: round-trips QtlSumStats inputs", { res <- summaryStatsQc(ss) expect_s4_class(res, "QtlSumStats") expect_equal(length(res$qcAudit), 1L) + # What the rows record comes through the rebuild. + typed <- `$<-`(ss, "dataType", "expression") + expect_equal(dataType(summaryStatsQc(typed)), "expression") + expect_true(is.na(dataType(res))) }) # =========================================================================== @@ -3299,14 +3319,14 @@ context("sumstatsQc internal helpers") } .ssh_mockExtractor <- function(seed = 42, n_samples = 30L) { - function(handle, snpIdx) { + function(handle, snpIdx, sampleIdx = NULL) { set.seed(seed) panel <- matrix( rbinom(n_samples * nrow(snpInfo(handle)), 2, 0.3), nrow = n_samples, ncol = nrow(snpInfo(handle)) ) - panel[, snpIdx, drop = FALSE] + panel[sampleIdx %||% seq_len(nrow(panel)), snpIdx, drop = FALSE] } } diff --git a/tests/testthat/test_tupleSelectors.R b/tests/testthat/test_tupleSelectors.R index fd9b42e49..b39ec320b 100644 --- a/tests/testthat/test_tupleSelectors.R +++ b/tests/testthat/test_tupleSelectors.R @@ -1764,3 +1764,99 @@ test_that(".twrRowResolveWeights aligns weights to the variant ids", { allNa <- TwasWeightsRow(ids, matrix(NA_real_, 3L, 2L)) expect_length(.twrRowResolveWeights(allNa)$variantIds, 0L) }) + +# =========================================================================== +# The molecular type a result's rows take from their data +# =========================================================================== + +test_that(".rowDataTypes reads the data a row was fitted on", { + data(qtlFineMappingExample, envir = environment()) + x <- qtlFineMappingExample[1L] + md <- S4Vectors::mcols(x, use.names = FALSE) + study <- as.character(md$study) + context <- as.character(md$context) + trait <- as.character(md$trait) + gr <- GenomicRanges::GRanges("chr1", IRanges::IRanges(100L, width = 1L)) + S4Vectors::mcols(gr) <- S4Vectors::DataFrame( + SNP = "rs1", + A1 = "A", + A2 = "G", + Z = 1, + N = 100L + ) + sumStats <- function(context, trait, dataType) { + QtlSumStats( + studyName = study, + context = context, + trait = trait, + entry = list(gr), + genome = "hg38", + dataType = dataType + ) + } + # Nothing recorded, or no data at all, as for a GWAS. + expect_true(is.na(pecotmr:::.rowDataTypes(x, NULL))) + expect_true(is.na(pecotmr:::.rowDataTypes( + x, + sumStats(context, trait, NULL) + ))) + # A QtlSumStats answers per (study, context, trait). + expect_equal( + pecotmr:::.rowDataTypes(x, sumStats(context, trait, "splicing")), + "splicing" + ) + expect_true(is.na( + pecotmr:::.rowDataTypes(x, sumStats(context, "other", "splicing")) + )) + # A joint row takes the one type its members share, and none if they differ. + joint <- x + S4Vectors::mcols(joint)$context <- "joint" + S4Vectors::mcols(joint)$jointContexts <- "a;b" + typedRows <- c( + sumStats("a", trait, "expression"), + sumStats("b", trait, "expression") + ) + expect_equal(pecotmr:::.rowDataTypes(joint, typedRows), "expression") + mixed <- c( + sumStats("a", trait, "expression"), + sumStats("b", trait, "splicing") + ) + expect_true(is.na(pecotmr:::.rowDataTypes(joint, mixed))) + # One no member records adds nothing to the question. + partial <- c( + sumStats("a", trait, "expression"), + sumStats("b", trait, NA_character_) + ) + expect_equal(pecotmr:::.rowDataTypes(joint, partial), "expression") +}) + +test_that(".dataTypeTable reads every kind of data", { + data(qtlDatasetExample, envir = environment()) + typed <- QtlDataset( + studyName = "gtex", + genotypes = pecotmr:::.qtlGenotypeSe(qtlDatasetExample), + phenotypes = as.list( + MultiAssayExperiment::experiments(qtlDatasetExample) + )["brain"], + dataType = "expression" + ) + expect_equal( + pecotmr:::.dataTypeTable(typed), + tibble::tibble(key = "gtex\rbrain", dataType = "expression") + ) + expect_equal(nrow(pecotmr:::.dataTypeTable(qtlDatasetExample)), 0L) + ms <- MultiStudyQtlDataset( + qtlDatasets = list(gtex = typed, study1 = qtlDatasetExample) + ) + expect_equal(pecotmr:::.dataTypeTable(ms), pecotmr:::.dataTypeTable(typed)) + expect_equal(nrow(pecotmr:::.dataTypeTable(NULL)), 0L) +}) + +test_that("every collection answers dataType(), NA where none is recorded", { + data(qtlFineMappingExample, envir = environment()) + expect_true(all(is.na(dataType(qtlFineMappingExample)))) + expect_length(dataType(qtlFineMappingExample), nrow(qtlFineMappingExample)) + typed <- qtlFineMappingExample[1L] + S4Vectors::mcols(typed)$dataType <- "expression" + expect_equal(dataType(typed), "expression") +}) diff --git a/tests/testthat/test_twasWeights.R b/tests/testthat/test_twasWeights.R index 6762535af..b3298c9ed 100644 --- a/tests/testthat/test_twasWeights.R +++ b/tests/testthat/test_twasWeights.R @@ -1229,7 +1229,7 @@ test_that("TwasWeights row accessors delegate to the entry", { method = "enet" )) expect_equal( - weightsDataType( + dataType( tw, studyName = "s1", context = "c1", @@ -2962,3 +2962,61 @@ test_that("a subset keeps only the fits its rows name", { expect_length(.fitDict(second), 1L) expect_identical(methodFits(second, "s1", "c1", "g2", "lasso"), other) }) + +test_that("learnTwasWeights keeps a fit's X only at full, and only if asked", { + skip_if_not_installed("glmnet") + set.seed(1) + X <- matrix( + rnorm(300), + nrow = 30, + dimnames = list(NULL, sprintf("chr1:%d:A:G", 1:10)) + ) + Y <- matrix(X[, 1] + rnorm(30), ncol = 1, dimnames = list(NULL, "y")) + fitAt <- function(...) { + tw <- suppressMessages(learnTwasWeights( + X, + Y, + weightMethods = "lasso", + studyName = "s", + context = "c", + trait = "y", + ..., + verbose = 0, + seed = 1L + )) + methodFits( + tw, + studyName = "s", + context = "c", + trait = "y", + method = "lasso" + ) + } + expect_identical(fitAt(fitRetention = "full")$call$x, quote(x)) + expect_true(is.matrix(fitAt(fitRetention = "full", retainX = TRUE)$call$x)) + expect_error(fitAt(retainX = TRUE), "requires `fitRetention", fixed = TRUE) + # Nothing to keep: refused before any fit, the engine not even installed. + expect_error( + learnTwasWeights( + X, + Y, + weightMethods = list( + bayes_r_weights = list(), + mrmash_weights = list() + ), + fitRetention = "full", + retainX = TRUE + ), + "hold none" + ) + expect_silent(pecotmr:::.twasAssertKeepsX(list(lassoWeights = list()))) + # One method with a copy is enough; the other loses nothing. + expect_silent(pecotmr:::.twasAssertKeepsX( + list(lasso_weights = list(), bayes_r_weights = list()) + )) + expect_error( + fitAt(fitRetention = "slim", retainX = TRUE), + "requires `fitRetention", + fixed = TRUE + ) +}) diff --git a/tests/testthat/test_twasWeightsPipeline.R b/tests/testthat/test_twasWeightsPipeline.R index 9d1c94eeb..293aec5cd 100644 --- a/tests/testthat/test_twasWeightsPipeline.R +++ b/tests/testthat/test_twasWeightsPipeline.R @@ -92,14 +92,14 @@ context("twasWeightsPipeline (S4 dispatch) with mocked weight methods") } .tp_mockExtractor <- function(seed = 1, n_samples = 40L) { - function(handle, snpIdx) { + function(handle, snpIdx, sampleIdx = NULL) { set.seed(seed) panel <- matrix( rbinom(n_samples * nrow(snpInfo(handle)), 2, 0.3), nrow = n_samples, ncol = nrow(snpInfo(handle)) ) - panel[, snpIdx, drop = FALSE] + panel[sampleIdx %||% seq_len(nrow(panel)), snpIdx, drop = FALSE] } } @@ -183,6 +183,93 @@ context("twasWeightsPipeline (S4 dispatch) with mocked weight methods") # twasWeightsPipeline(QtlDataset) # =========================================================================== +test_that("twasWeightsPipeline reads each window once and shares its X", { + skip_if_not_installed("pgenlibr") + qd <- .fitSamplesDataset() + run <- .countBlocks(twasWeightsPipeline( + qd, + methods = "lasso", + region = .fitSamplesWindow(), + seed = 1L + )) + expect_s4_class(run$result, "TwasWeights") + expect_equal(nrow(run$result), 6L) + # One read across 2 contexts and 3 traits, of only the samples they observe. + expect_equal(run$reads, 1L) + expect_equal(run$samples[[1L]], 1:30) + # In each context: one X for g1 and g3, and one for g2's different samples. + expect_equal(run$builds, 4L) + # Row order does not depend on how the fits were grouped. + expect_equal( + as.character(S4Vectors::mcols(run$result)$trait), + rep(c("g1", "g2", "g3"), times = 2L) + ) +}) + +test_that("twasWeightsPipeline reads a window once across its paths", { + skip_if_not_installed("pgenlibr") + window <- .fitSamplesWindow() + pca <- .countBlocks(twasWeightsPipeline( + .fitSamplesDataset(), + methods = "lasso", + region = window, + usePCA = TRUE, + nPCs = 1L, + phenotypeFilterParam = PhenotypeFilterParam(naAction = "impute"), + seed = 1L + )) + expect_equal(pca$reads, 1L) + skip_if_not_installed("mr.mashr") + # cv.glmnet warns that 30 samples are few per fold. + bySpec <- .countBlocks(suppressWarnings(twasWeightsPipeline( + .fitSamplesDataset(), + methods = c("lasso", "mrmash"), + jointSpecification = "context", + region = window, + seed = 1L + ))) + expect_equal(bySpec$reads, 1L) + expect_setequal(methodNames(bySpec$result), c("lasso", "mrmash")) +}) + +test_that("a multi-study TWAS run reads a window once per component", { + skip_if_not_installed("pgenlibr") + skip_if_not_installed("mr.mashr") + run <- .countBlocks(suppressWarnings(twasWeightsPipeline( + .fitSamplesTwoStudies(), + methods = c("lasso", "mrmash"), + jointSpecification = "context", + region = .fitSamplesWindow(), + seed = 1L + ))) + expect_equal(run$reads, 2L) + expect_setequal(methodNames(run$result), c("lasso", "mrmash")) + expect_setequal( + as.character(S4Vectors::mcols(run$result)$study), + c("s", "s2") + ) +}) + +test_that("a panel of rsIDs gives the weights of one labelled by position", { + skip_if_not_installed("pgenlibr") + window <- .fitSamplesWindow() + byId <- suppressMessages(twasWeightsPipeline( + .fitSamplesDataset(), + methods = "lasso", + region = window, + seed = 1L + )) + byRs <- suppressMessages(twasWeightsPipeline( + .fitSamplesDataset(.rsIdPanel()), + methods = "lasso", + region = window, + seed = 1L + )) + expect_equal(nrow(byRs), nrow(byId)) + expect_equal(variantIds(byRs[1L, ]), variantIds(byId[1L, ])) + expect_true(all(startsWith(variantIds(byRs[1L, ]), "chr21:"))) +}) + test_that("twasWeightsPipeline(QtlDataset) runs end to end, mocked solvers", { qd <- .tp_makeQtlDataset(contexts = "brain", traits = c("ENSG_A", "ENSG_B")) mocks <- c( @@ -1987,6 +2074,225 @@ test_that(".twasNormalizeMethods: NULL falls through to the 'default' preset", { expect_equal(sort(names(res$methodList)), sort(default_names)) }) +test_that("a preset string names its methods, however they are read", { + fast <- c( + "susie", + "susieInf", + "mrash", + "enet", + "lasso", + "mcp", + "scad", + "l0learn" + ) + full <- c(fast, "bayesR", "bayesC") + for (preset in list(list("fastDefault", fast), list("default", full))) { + norm <- pecotmr:::.twasNormalizeMethods(preset[[1L]]) + expect_equal(norm$tokens, preset[[2L]]) + expect_setequal( + pecotmr:::.twasTokensFromMethodList(norm$methodList), + preset[[2L]] + ) + expect_equal( + pecotmr:::.twasMethodTokensFromArg(preset[[1L]]), + preset[[2L]] + ) + } + # Named methods are not presets, even one at a time. + expect_equal(pecotmr:::.twasMethodTokensFromArg("lasso"), "lasso") + expect_equal(pecotmr:::.twasNormalizeMethods("lasso")$tokens, "lasso") +}) + +test_that("a preset skips the fine-mapping methods no result holds", { + runs <- pecotmr:::.twasRunMethods + fast <- pecotmr:::.twasExpandPresets("fastDefault") + both <- c("susie", "susieInf") + expect_warning( + out <- runs("fastDefault", NULL), + paste( + "the \"fastDefault\" preset is skipping susie, susieInf,", + "because no `fineMappingResult` holds their fits" + ), + fixed = TRUE + ) + expect_equal(out, setdiff(fast, both)) + # No `methods` is the default preset. + expect_warning(out <- runs(NULL, NULL), "\"default\" preset is skipping") + expect_equal( + out, + setdiff(pecotmr:::.twasExpandPresets("default"), both) + ) + # A result holding one leaves only the other to skip. + onlySusie <- .tp_makeStubFineMappingResult(method = "susie") + expect_warning( + out <- runs("fastDefault", onlySusie), + "skipping susieInf, because" + ) + expect_equal(out, setdiff(fast, "susieInf")) + # A result holding them both, or methods named outside a preset: no skip. + expect_no_warning( + out <- runs("fastDefault", .tp_makeStubFineMappingResult(method = both)) + ) + expect_equal(out, fast) + expect_no_warning(out <- runs(both, NULL)) + expect_equal(out, both) +}) + +test_that("naming a fine-mapping method without its fits still stops", { + qd <- .tp_makeQtlDataset(contexts = "brain", traits = "ENSG_A") + expect_error( + twasWeightsPipeline(qd, methods = c("susie", "lasso")), + "susie are fine-mapping methods", + fixed = TRUE + ) +}) + +test_that("a preset over a collection gives each kind of input its own list", { + preset <- pecotmr:::.twasMsPreset + fmr <- .tp_makeStubFineMappingResult(method = c("susie", "susieInf")) + expect_equal(preset("lasso", TRUE, NULL), "lasso") + expect_equal( + preset("fastDefault", FALSE, fmr), + pecotmr:::.twasExpandPresets("fastDefault") + ) + expect_warning( + withoutFits <- preset("fastDefault", FALSE, NULL), + "preset is skipping susie, susieInf" + ) + expect_equal( + withoutFits, + setdiff( + pecotmr:::.twasExpandPresets("fastDefault"), + c("susie", "susieInf") + ) + ) + both <- preset("default", TRUE, fmr) + expect_setequal( + pecotmr:::.methodsParamResolve(both, "QtlDataset")$tokens, + pecotmr:::.twasExpandPresets("default") + ) + expect_setequal( + pecotmr:::.methodsParamResolve(both, "QtlSumStats")$tokens, + c("lasso", "prsCs", "dprGibbs") + ) +}) + +test_that("a preset fits every method it names", { + skip_if_not_installed("pgenlibr") + skip_if_not_installed("L0Learn") + data <- .fitSamplesDataset() + window <- .fitSamplesWindow() + fineMapped <- suppressWarnings(suppressMessages(fineMappingPipeline( + data, + methods = c("susie", "susieInf"), + region = window, + seed = 1L + ))) + res <- suppressWarnings(suppressMessages(twasWeightsPipeline( + data, + methods = "fastDefault", + fineMappingResult = fineMapped, + region = window, + seed = 1L + ))) + expect_setequal( + methodNames(res), + c( + "susie", + "susieInf", + "mrash", + "enet", + "lasso", + "mcp", + "scad", + "l0learn" + ) + ) + # Over a collection, on each of its studies. + studies <- .fitSamplesTwoStudies() + fineMappedBoth <- suppressWarnings(suppressMessages(fineMappingPipeline( + studies, + methods = c("susie", "susieInf"), + region = window, + seed = 1L + ))) + across <- suppressWarnings(suppressMessages(twasWeightsPipeline( + studies, + methods = "fastDefault", + fineMappingResult = fineMappedBoth, + region = window, + seed = 1L + ))) + expect_setequal(methodNames(across), methodNames(res)) + expect_setequal( + as.character(S4Vectors::mcols(across)$study), + c("s", "s2") + ) +}) + +test_that("a preset runs without the fine-mapping methods it cannot read", { + skip_if_not_installed("pgenlibr") + skip_if_not_installed("L0Learn") + window <- .fitSamplesWindow() + res <- expect_warning( + suppressMessages(twasWeightsPipeline( + .fitSamplesDataset(), + methods = "fastDefault", + region = window, + seed = 1L + )), + "preset is skipping susie, susieInf" + ) + expect_setequal( + methodNames(res), + c("mrash", "enet", "lasso", "mcp", "scad", "l0learn") + ) + # Over a collection, once. + warnings <- character() + across <- withCallingHandlers( + suppressMessages(twasWeightsPipeline( + .fitSamplesTwoStudies(), + methods = "fastDefault", + region = window, + seed = 1L + )), + warning = function(cnd) { + warnings <<- c(warnings, conditionMessage(cnd)) + invokeRestart("muffleWarning") + } + ) + expect_equal(sum(str_detect(warnings, "preset is skipping")), 1L) + expect_setequal(methodNames(across), methodNames(res)) +}) + +test_that("on summary statistics a preset names the input's own defaults", { + preset <- pecotmr:::.twasSumStatsPreset + expect_null(preset("default")) + expect_null(preset("fastDefault")) + expect_null(preset(NULL)) + expect_equal(preset("lasso"), "lasso") + expect_equal(preset(c("lasso", "prsCs")), c("lasso", "prsCs")) + expect_equal(preset(list(lasso = list())), list(lasso = list())) + seen <- list() + local_mocked_bindings( + .twasPipelineQtlSumStats = function(data, methods, ...) { + seen <<- list(methods = methods) + abort("reached the worker") + }, + .package = "pecotmr" + ) + # No warning that a preset's own methods keep too little: it names these. + expect_error( + twasWeightsPipeline( + .tp_makeQtlSumStats(), + methods = "fastDefault", + fitRetention = "full" + ), + "reached the worker" + ) + expect_null(seen$methods) +}) + test_that(".twasNormalizeMethods: character methods go to .twasMethodLookup", { # Each case: a character `methods` (a preset or short names) and the # method list it expands to. @@ -3024,7 +3330,7 @@ test_that("QtlDataset: CrossValidationParam(fitFullData) reaches the engine", { qd <- .tp_makeQtlDataset(contexts = "brain", traits = "ENSG_A") cap <- new.env(parent = emptyenv()) local_mocked_bindings( - .twasQdsRunGrid = function(grid, marker, ...) { + .twasQdsGridWork = function(grid, marker, ...) { cap$marker <- marker abort("captured") }, @@ -3315,7 +3621,6 @@ test_that("multi-study TWAS keeps per-study options", { estimatePi = FALSE, usePCA = TRUE, nPCs = 3L, - weightsDataType = "rnaseq", fitRetention = "none", residualizationParam = ResidualizationParam( residualizeGenotype = FALSE @@ -3330,7 +3635,6 @@ test_that("multi-study TWAS keeps per-study options", { expect_false(cap$cfg$estimatePi) expect_true(cap$cfg$usePCA) expect_equal(cap$cfg$nPCs, 3L) - expect_equal(cap$cfg$weightsDataType, "rnaseq") expect_false(cap$cfg$crossValidationParam$fitFullData) # "none" is provably not the default, and lasso can honour it. expect_equal(cap$cfg$fitRetention, "none") @@ -3357,18 +3661,19 @@ test_that("MultiStudyQtlDataset: components get fitRetention as asked", { cfg <- list( methods = "lasso", fitRetention = level, + retainX = identical(level, "full"), usePCA = TRUE, - nPCs = 3L, - weightsDataType = "rnaseq" + nPCs = 3L ) pecotmr:::.twasPerStudy(qd, cfg) expect_equal(cap$args$fitRetention, level) + expect_equal(cap$args$retainX, identical(level, "full")) expect_true(cap$args$usePCA) expect_equal(cap$args$nPCs, 3L) - expect_equal(cap$args$weightsDataType, "rnaseq") pecotmr:::.twasSumStats(ss, cfg) expect_equal(cap$args$fitRetention, level) - expect_equal(cap$args$weightsDataType, "rnaseq") + # Summary statistics have no X to keep. + expect_false("retainX" %in% names(cap$args)) } }) @@ -4164,8 +4469,7 @@ test_that(".twasQssMultivariateFitOne returns no rows when the fit is absent", { NULL, methodArgs = list(), fitRetention = "none", - fineMappingResult = NULL, - weightsDataType = NULL + fineMappingResult = NULL ), "no 'mvsusie' fit found" ) @@ -4191,8 +4495,7 @@ test_that(".twasQssMultivariateFitOne promotes vector weights to a matrix", { NULL, methodArgs = list(), fitRetention = "none", - fineMappingResult = NULL, - weightsDataType = "rnaseq" + fineMappingResult = NULL ) expect_length(rows, 1L) expect_equal(rows[[1L]]$context, "cA") @@ -4235,7 +4538,9 @@ test_that(".twasMsJointPhase aborts when only mrmash asked and it fails", { test_that(".twasRunJointSpecGrid returns NULL when every region is empty", { local_mocked_bindings( - .twasJointSpecRegion = function(...) NULL, + .twasJointSpecRegionWork = function(...) { + pecotmr:::.qtlWorkValue(NULL) + }, .twasRegionLabel = function(x) "rX", .package = "pecotmr" ) @@ -4412,6 +4717,324 @@ test_that("a fine-mapping method's TWAS rows keep none of its fit", { expect_identical(pecotmr:::.jointRetentionLevel(cfg, "mrmash"), "slim") }) +test_that("both kinds of method run together with no joint spec", { + skip_if_not_installed("pgenlibr") + skip_if_not_installed("mr.mashr") + window <- .fitSamplesWindow() + alone <- function(method) { + suppressWarnings(suppressMessages(twasWeightsPipeline( + .fitSamplesDataset(), + methods = method, + region = window, + seed = 1L + ))) + } + lasso <- alone("lasso") + mrmash <- alone("mrmash") + run <- .countBlocks(suppressWarnings(twasWeightsPipeline( + .fitSamplesDataset(), + methods = c("lasso", "mrmash"), + region = window, + seed = 1L + ))) + # One read of the window serves both kinds of fit. + expect_equal(run$reads, 1L) + both <- S4Vectors::mcols(run$result) + expect_equal(nrow(run$result), nrow(lasso) + nrow(mrmash)) + # Each method's fits are what asking for it alone gives. + weightsAt <- function(res, method, context, trait) { + weights( + res, + studyName = "s", + context = context, + trait = trait, + method = method + ) + } + for (context in c("brain", "liver")) { + for (trait in c("g1", "g2", "g3")) { + expect_equal( + weightsAt(run$result, "lasso", context, trait), + weightsAt(lasso, "lasso", context, trait) + ) + expect_equal( + weightsAt(run$result, "mrmash", context, trait), + weightsAt(mrmash, "mrmash", context, trait) + ) + } + } +}) + +test_that("a multi-study run takes both kinds of method with no joint spec", { + skip_if_not_installed("pgenlibr") + skip_if_not_installed("mr.mashr") + res <- suppressWarnings(suppressMessages(twasWeightsPipeline( + .fitSamplesTwoStudies(), + methods = c("lasso", "mrmash"), + region = .fitSamplesWindow(), + seed = 1L + ))) + expect_setequal(methodNames(res), c("lasso", "mrmash")) + expect_setequal(as.character(S4Vectors::mcols(res)$study), c("s", "s2")) + expect_equal(nrow(res), 24L) +}) + +test_that(".twasQdsGridResult takes a path that fitted nothing", { + result <- pecotmr:::.twasQdsGridResult + expect_null(result(list(NULL, NULL))) + expect_identical(result(list(NULL, "fits")), "fits") + expect_identical(result(list("fits", NULL)), "fits") +}) + +test_that(".twasNormKeep keeps a multi-word method's options", { + # Their list keys (bayes_r_weights) do not strip to their tokens (bayesR). + norm <- pecotmr:::.twasNormalizeMethods(c( + "mrmash", + "bayesR", + "dprVb", + "lasso" + )) + norm$methodList$dpr_vb_weights <- list(nK = 4L) + kept <- pecotmr:::.twasNormKeep(norm, c("dprVb", "lasso")) + expect_equal(kept$tokens, c("dprVb", "lasso")) + expect_setequal( + pecotmr:::.twasTokensFromMethodList(kept$methodList), + c("dprVb", "lasso") + ) + expect_equal(kept$methodList$dpr_vb_weights, list(nK = 4L)) +}) + +test_that("a worker's engine error reaches the caller of the pipeline", { + skip_if_not_installed("pgenlibr") + local_mocked_bindings( + lassoWeights = function(...) stop("the engine's own message"), + .package = "pecotmr" + ) + expect_error( + suppressMessages(twasWeightsPipeline( + .fitSamplesDataset(), + methods = "lasso", + region = .fitSamplesWindow(), + seed = 1L + )), + "the engine's own message", + fixed = TRUE + ) +}) + +test_that("retainX reaches only a full fit that can hold X", { + # Told by each weight function's own formal. + expect_setequal( + pecotmr:::.twasTokensKeepingX(), + c( + "lasso", + "enet", + "ridge", + "scad", + "mcp", + "mrash", + "dprGibbs", + "dprVb", + "dprAdaptiveGibbs" + ) + ) + apply <- pecotmr:::.twasApplyRetainFit + expect_identical( + apply(list(), "full", TRUE, "lassoWeights"), + list(fitRetention = "full", retainX = TRUE) + ) + expect_identical( + apply(list(), "full", FALSE, "lassoWeights"), + list(fitRetention = "full") + ) + # A fit with no copy of X takes no such argument... + expect_identical( + apply(list(), "full", TRUE, "mrmashWeights"), + list(fitRetention = "full") + ) + # ...and a method's own lower level stands, with nothing to keep it in. + expect_identical( + apply(list(fitRetention = "slim"), "full", TRUE, "lassoWeights"), + list(fitRetention = "slim") + ) + expect_identical(apply(list(), "none", FALSE, "lassoWeights"), list()) +}) + +test_that("twasWeightsPipeline rejects retainX unless the fit is full", { + qd <- .tp_makeQtlDataset(contexts = "brain", traits = "ENSG_A") + required <- "requires `fitRetention = \"full\"`" + # The default level included, before any genotypes are read. + expect_error( + twasWeightsPipeline(qd, methods = "lasso", retainX = TRUE), + required, + fixed = TRUE + ) + for (level in c("slim", "none")) { + expect_error( + twasWeightsPipeline( + qd, + methods = "lasso", + fitRetention = level, + retainX = TRUE + ), + required, + fixed = TRUE + ) + } + expect_error( + twasWeightsPipeline( + qd, + methods = "lasso", + fitRetention = "full", + retainX = NA + ), + "retainX" + ) + # Summary statistics have no X to keep. + expect_error( + twasWeightsPipeline( + .tp_makeQtlSumStats(), + methods = "lasso", + fitRetention = "full", + retainX = TRUE + ), + "unused argument" + ) +}) + +test_that("a multi-study run rejects retainX before any study runs", { + local_mocked_bindings( + .twasPerStudy = function(qd, cfg) abort("a study ran"), + .package = "pecotmr" + ) + expect_error( + twasWeightsPipeline( + .tp_makeMultiStudy(), + methods = "lasso", + retainX = TRUE + ), + "requires `fitRetention = \"full\"`", + fixed = TRUE + ) +}) + +test_that("a multi-study joint record carries retainX", { + record <- pecotmr:::.twasMsJointCfg( + list( + retainX = TRUE, + genotypeFilterParam = GenotypeFilterParam(), + phenotypeFilterParam = PhenotypeFilterParam() + ), + NULL, + NULL + ) + expect_true(record$retainX) +}) + +test_that("retainX says when no method's fit has a copy of X", { + check <- pecotmr:::.twasCheckRetainX + expect_error( + check(TRUE, "full", c("bayesR", "mrmash")), + "bayesR, mrmash hold no copy of X" + ) + # A method with no copy loses nothing beside one that keeps it. + expect_silent(check(TRUE, "full", c("lasso", "bayesR"))) + expect_silent(check(FALSE, "slim", "bayesR")) + expect_silent(check(TRUE, "full", NULL)) +}) + +test_that("a TWAS fit keeps X only if asked", { + skip_if_not_installed("pgenlibr") + fitsAt <- function(retainX) { + res <- suppressMessages(twasWeightsPipeline( + .fitSamplesDataset(), + methods = "lasso", + region = .fitSamplesWindow(), + fitRetention = "full", + retainX = retainX, + seed = 1L + )) + cell <- list( + res, + studyName = "s", + context = "brain", + trait = "g1", + method = "lasso" + ) + list( + weights = exec(weights, !!!cell), + fit = exec(methodFits, !!!cell) + ) + } + full <- fitsAt(FALSE) + withX <- fitsAt(TRUE) + expect_identical(full$fit$call$x, quote(x)) + expect_true(is.matrix(withX$fit$call$x)) + # Dropping it changes nothing else the fit knows. + expect_equal(full$weights, withX$weights) + expect_equal(full$fit$cvm, withX$fit$cvm) + expect_lt( + length(serialize(full$fit, NULL)), + length(serialize(withX$fit, NULL)) + ) +}) + +test_that("retainX leaves a fine-mapping method's rows keeping no fit", { + skip_if_not_installed("pgenlibr") + ds <- .fitSamplesDataset() + window <- .fitSamplesWindow() + fm <- suppressMessages(fineMappingPipeline( + ds, + methods = "susie", + region = window, + seed = 1L + )) + res <- suppressWarnings(suppressMessages(twasWeightsPipeline( + ds, + methods = c("lasso", "susie"), + fineMappingResult = fm, + region = window, + fitRetention = "full", + retainX = TRUE, + seed = 1L + ))) + fitOf <- function(method) { + methodFits( + res, + studyName = "s", + context = "brain", + trait = "g1", + method = method + ) + } + expect_true(is.matrix(fitOf("lasso")$call$x)) + expect_length(fitOf("susie"), 0L) +}) + +test_that("a multi-study run keeps X in its individual-level fits if asked", { + skip_if_not_installed("pgenlibr") + skip_if_not_installed("mr.mashr") + res <- suppressWarnings(suppressMessages(twasWeightsPipeline( + .fitSamplesTwoStudies(), + methods = c("lasso", "mrmash"), + jointSpecification = "context", + region = .fitSamplesWindow(), + fitRetention = "full", + retainX = TRUE, + seed = 1L + ))) + for (study in c("s", "s2")) { + fit <- methodFits( + res, + studyName = study, + context = "brain", + trait = "g1", + method = "lasso" + ) + expect_true(is.matrix(fit$call$x)) + } +}) + test_that("twasWeightsPipeline says when fitRetention = 'full' can't be kept", { # Not every method distinguishes the levels; for the rest "full" would be # silently indistinguishable from "slim". The SuSiE family keeps no fit @@ -4813,7 +5436,6 @@ test_that("QtlDataset: explicit joint fits get every caller setting", { fitFullData = FALSE ), fitRetention = "full", - weightsDataType = "rnaseq", seed = 7L )), "no joint fits produced" @@ -4823,7 +5445,6 @@ test_that("QtlDataset: explicit joint fits get every caller setting", { expect_equal(ind$config$crossValidationParam$folds, 2) expect_false(ind$config$crossValidationParam$fitFullData) expect_equal(ind$config$fitRetention, "full") - expect_equal(ind$config$dataType, "rnaseq") # Honest now that the setting reaches the fit. expect_false(ind$config$standardized) expect_equal(ind$args$methodList$mrmash_weights$max_iter, 5) @@ -4837,14 +5458,12 @@ test_that("QtlSumStats: joint fits get fitRetention and the mr.mash options", { .tp_makeMultiCtxQtlSumStats(), methods = list(mrmash = list(max_iter = 5)), jointSpecification = "context", - fitRetention = "full", - weightsDataType = "rnaseq" + fitRetention = "full" )), "no joint fits produced" ) ss <- .capturedCall(cap, "sumstats") expect_equal(ss$config$fitRetention, "full") - expect_equal(ss$config$dataType, "rnaseq") expect_equal(ss$args$methodArgs$mrmash$max_iter, 5) }) @@ -4862,7 +5481,6 @@ test_that("MultiStudyQtlDataset: joint fits get every caller setting", { crossValidationParam = CrossValidationParam(folds = 2), genotypeFilterParam = GenotypeFilterParam(mafCutoff = 0.05), fitRetention = "full", - weightsDataType = "rnaseq", seed = 7L )), "no joint fits produced" @@ -4871,12 +5489,10 @@ test_that("MultiStudyQtlDataset: joint fits get every caller setting", { expect_identical(ind$args$residualizationParam, rp) expect_equal(ind$config$crossValidationParam$folds, 2) expect_equal(ind$config$fitRetention, "full") - expect_equal(ind$config$dataType, "rnaseq") expect_equal(ind$args$methodList$mrmash_weights$max_iter, 5) expect_equal(ind$args$genotypeFilter$mafCutoff, 0.05) ss <- .capturedCall(cap, "sumstats") expect_equal(ss$config$fitRetention, "full") - expect_equal(ss$config$dataType, "rnaseq") expect_equal(ss$args$methodArgs$mrmash$max_iter, 5) }) @@ -5205,3 +5821,76 @@ test_that("an alphabet method's plain options are built for its backend", { expect_equal(S4Vectors::metadata(split$methodArgs)$engine, "hibayes") expect_equal(split$methodArgs$dfvg, 5) }) + +# --------------------------------------------------------------------------- +# The molecular type the weights take from their data +# --------------------------------------------------------------------------- + +test_that("twasWeightsPipeline records each context's type from the dataset", { + plain <- .tp_makeQtlDataset(contexts = c("brain", "liver")) + qd <- QtlDataset( + studyName = "study1", + genotypes = pecotmr:::.qtlGenotypeSe(plain), + phenotypes = as.list(MultiAssayExperiment::experiments(plain))[ + c("brain", "liver") + ], + dataType = c(brain = "expression", liver = "splicing") + ) + mockArgs <- list( + .handleDosage = .tp_mockExtractor(), + lassoWeights = function(X, Y, ...) { + matrix(0.1, ncol(X), 1L, dimnames = list(colnames(X), NULL)) + }, + .package = "pecotmr" + ) + exec(local_mocked_bindings, !!!mockArgs) + res <- suppressMessages(twasWeightsPipeline( + qd, + methods = "lasso", + cisWindow = 1e6, + verbose = 0 + )) + types <- map2_chr( + as.character(res$context), + as.character(res$trait), + function(context, trait) { + dataType( + res, + studyName = "study1", + context = context, + trait = trait, + method = "lasso" + ) + } + ) + expect_equal( + types, + unname(c(brain = "expression", liver = "splicing")[ + as.character(res$context) + ]) + ) + # A dataset that records none leaves the weights without one. + untyped <- suppressMessages(twasWeightsPipeline( + plain, + methods = "lasso", + cisWindow = 1e6, + verbose = 0 + )) + expect_null(dataType( + untyped, + studyName = "study1", + context = "brain", + trait = as.character(untyped$trait)[[1L]], + method = "lasso" + )) +}) + +test_that(".twasRecordDataTypes leaves alone what the data does not answer", { + expect_null(pecotmr:::.twasRecordDataTypes(NULL, NULL)) + row <- list(lasso = NULL, enet = "old") + expect_identical(pecotmr:::.twasRowDataType(row, NA_character_), row) + expect_identical( + pecotmr:::.twasRowDataType(row, "expression"), + list(lasso = "expression", enet = "expression") + ) +}) diff --git a/tests/testthat/test_twasWeightsRow.R b/tests/testthat/test_twasWeightsRow.R index 601ad6edc..e04b1f0bf 100644 --- a/tests/testthat/test_twasWeightsRow.R +++ b/tests/testthat/test_twasWeightsRow.R @@ -36,7 +36,7 @@ test_that("TwasWeightsRow: constructor and accessors round-trip", { expect_equal(methodFits(e), list(model = "lasso")) expect_equal(cvResult(e), list(rsq = 0.4)) expect_true(isTRUE(weightStandardized(e))) - expect_equal(weightsDataType(e), "expression") + expect_equal(dataType(e), "expression") }) diff --git a/tests/testthat/test_variantId.R b/tests/testthat/test_variantId.R index 95f8de917..ce2aae234 100644 --- a/tests/testthat/test_variantId.R +++ b/tests/testthat/test_variantId.R @@ -417,6 +417,45 @@ test_that("matchVariants judges each strand-ambiguous (A/T) swap on its own", { ) }) +test_that("matchVariants returns every matching pair under allPairs", { + # One-to-one, a target listed in both allele orders disagrees about its + # sign and is dropped; as a set question, both pairs stand. + bothOrders <- c("chr1:100:A:G", "chr1:100:G:A") + expect_length(pecotmr:::matchVariants("chr1:100:A:G", bothOrders)$idxA, 0L) + m <- pecotmr:::matchVariants("chr1:100:A:G", bothOrders, allPairs = TRUE) + expect_equal(m$idxA, c(1L, 1L)) + expect_equal(m$idxB, 1:2) + expect_equal(m$sign, c(1, -1)) + # One-to-one, a reference is claimed by its first target only. + heldTwice <- rep("chr1:100:A:G", 2L) + expect_equal(pecotmr:::matchVariants(heldTwice, "chr1:100:A:G")$idxA, 1L) + expect_equal( + pecotmr:::matchVariants( + heldTwice, + "chr1:100:A:G", + allPairs = TRUE + )$idxA, + 1:2 + ) + # The alleles decide as ever. + expect_length( + pecotmr:::matchVariants( + "chr1:100:A:G", + "chr1:100:A:C", + allPairs = TRUE + )$idxA, + 0L + ) + expect_length( + pecotmr:::matchVariants( + "chr1:100:A:T", + "chr1:100:T:A", + allPairs = TRUE + )$idxA, + 0L + ) +}) + test_that("matchVariants matches within maxDistance only when alleles agree", { a <- c("chr1:100:A:G", "chr1:500:C:T") b <- c("chr1:103:A:G", "chr1:520:C:T", "chr1:101:A:C") @@ -696,6 +735,30 @@ test_that("harmonizeAlleles drops a target the reference cannot decide", { expect_equal(nrow(res$harmonizedData), 0L) }) +test_that("removeIndels refuses whatever isSnpAlleles does not call a SNP", { + ids <- c( + "chr1:100:A:G", + "chr1:200:A:AT", + "chr1:300:A:*", + "chr1:400:N:G", + "chr1:500:D:I" + ) + expect_equal(sort(pecotmr:::matchVariants(ids, ids)$idxA), 1:5) + expect_equal( + pecotmr:::matchVariants(ids, ids, removeIndels = TRUE)$idxA, + 1L + ) + tgt <- .vid_df(c("1", "1"), c(100, 200), c("A", "A"), c("G", "*")) + res <- pecotmr:::harmonizeAlleles( + tgt, + tgt, + removeIndels = TRUE, + matchMinProp = 0 + ) + expect_equal(nrow(res$harmonizedData), 1L) + expect_equal(attr(res, "qcCounts")$droppedIndel, 1L) +}) + test_that("harmonizeAlleles sees a strand-recorded second orientation too", { # A:G and C:T are the same ambiguity as A:G and G:A -- only a literal # string key would miss it. diff --git a/vignettes/coloc-pipeline.Rmd b/vignettes/coloc-pipeline.Rmd index 28224f61e..cd4ac52bc 100644 --- a/vignettes/coloc-pipeline.Rmd +++ b/vignettes/coloc-pipeline.Rmd @@ -150,10 +150,9 @@ colocPipeline( ## Enrichment-aware colocalisation (enloc mode) Passing a per-pair enrichment table --- the output of -`qtlEnrichmentPipeline()` --- switches the pair from coloc to enloc. This is -a port of `fastenloc`, not a reweighting of coloc: the priors are *derived* -from the enrichment estimate rather than the baseline `p12` being scaled by -it. +`qtlEnrichmentPipeline()` --- scores the pair with `fastenloc`'s priors. They +are *derived* from the enrichment estimate rather than the baseline `p12` +being scaled by it. The estimator returns the enrichment parameters $(a_0, a_1)$ on the log-odds scale, and the priors follow as `fastenloc`'s `set_enrich_params` writes them, @@ -177,10 +176,21 @@ and `colocP12` columns, so the table you pass already holds them. Because the enrichment determines the priors, supplying `priors` as well is an error rather than a silent precedence rule. -What makes the scoring enloc rather than coloc is that the prior depends on -whether the GWAS hit *is* the causal QTL --- $\sigma(a_0 + a_1)$ when it is, -$\sigma(a_0)$ when it is not. coloc applies one $p_{12}$ to every shared -configuration and cannot express that. +In `fastenloc` the prior depends on whether the GWAS hit *is* the causal QTL +--- $\sigma(a_0 + a_1)$ when it is, $\sigma(a_0)$ when it is not. coloc +expresses that through these three priors, since +$p_{12} / (p_1 p_2) \approx e^{a_1}$. Given Bayes factors for both traits, +`fastenloc`'s configuration sum is coloc's formula up to a factor of +$1 - p_1 - p_2 - p_{12}$ on H0 and on H3's inverse, and its documentation calls +the two nearly identical, so `coloc.bf_bf` scores the pair here too. Note the +trait order: $p_1$ above is the GWAS-only prior, while coloc's `p1` goes with +its first trait, the QTL side, so the pipeline passes the table's `colocP2` as +`p1` and `colocP1` as `p2`. + +Each effect's evidence comes from its own Bayes factors, on both sides. As with +`fastenloc` on Bayes-factor input (its `compute_pip`), a QTL effect with no +evidence is therefore not treated as certainly present, as SuSiE's `alpha` +would have it, since each effect's `alpha` sums to 1. Pairs without a matching enrichment row fall back to the baseline priors with a warning. Four columns are added: `enrichment` and `p12Used` (the per-pair diff --git a/vignettes/constructing-qtl-datasets.Rmd b/vignettes/constructing-qtl-datasets.Rmd index 6a669004d..8aceb35f7 100644 --- a/vignettes/constructing-qtl-datasets.Rmd +++ b/vignettes/constructing-qtl-datasets.Rmd @@ -137,6 +137,7 @@ are also accepted and normalized on read. | `context` (alias `cond`) | yes | context / condition label | | `phenotypePath` (alias `path`) | yes | bgzipped or plain BED phenotype file | | `covariatePath` (alias `cov_path`) | no | per-context covariate table | +| `dataType` (alias `data_type`) | no | molecular type, e.g. `expression` | | `study` | single-valued | study identifier (may also be an argument) | | `genotypePath` (alias `genotype_prefix`) | single-valued | genotype file or PLINK prefix (may also be an argument) | | `genotypeCovariatePath` | no, single-valued | genotype-derived covariates applied to all contexts | diff --git a/vignettes/constructing-sumstats.Rmd b/vignettes/constructing-sumstats.Rmd index 9ef0d48e3..92f01d91c 100644 --- a/vignettes/constructing-sumstats.Rmd +++ b/vignettes/constructing-sumstats.Rmd @@ -185,6 +185,7 @@ points at a sumstats file for that trait. | `trait` (alias `gene_id`) | yes | molecular trait id | | `sumStatsPath` (alias `path`) | yes | sumstats file for this trait | | `columnMapping`, `varY` | no | as for GWAS | +| `dataType` (alias `data_type`) | no | molecular type, e.g. `expression` | | `genome` | single-valued | may instead be an argument | | `ldReferencePath` | no | this row's LD reference (or the argument) | diff --git a/vignettes/ctwas-pipeline.Rmd b/vignettes/ctwas-pipeline.Rmd index 066d4cda7..48c565f8e 100644 --- a/vignettes/ctwas-pipeline.Rmd +++ b/vignettes/ctwas-pipeline.Rmd @@ -140,17 +140,33 @@ should carry into a real analysis: `twasWeights` accepts two shapes: - a **flat** `TwasWeights` or `QtlFineMappingResult` (or a homogeneous list - of them) carrying `region` provenance. Each gene is assigned to its home - LD block by `start(region)`, matching cTWAS's own placement rule. -- a **pre-bucketed named list** keyed by `region_id`, used as-is. - -Blocks with no gene weights still contribute their SNP-level signal to the -group prior — the whole-chromosome pattern where only a few blocks carry -genes. - -A gene whose cis window straddles a block boundary is assigned by its single -anchor. The signal it loses across the boundary is a separate concern, -handled by `mergeCtwasBoundaryRegions()` below. + of them), each gene read with the LD block holding most of its weight. A + gene's `traitPos`, if it has one, only gives its coordinates in the + results. +- a **pre-bucketed named list** keyed by `region_id`, used as-is, each gene in + one block. + +Blocks with no gene weights do not feed the group priors by default: cTWAS +skips regions with fewer genes than its `min_gene`, 1. To let their SNP-level +signal in, set `min_gene = 0` in `CtwasEstParamOptions()`. + +A gene's weights are used whole, whichever blocks they fall in; see +[Boundary genes](#boundary-genes). + +A run estimates one prior per group, and a group is a study, context and +molecular type, each estimated from its own genes. So the contexts need not +hold the same genes, and two studies with one context name stay apart. The type +is the `dataType` the data records, through `QtlDataset(dataType = )` or +`QtlSumStats(dataType = )`, which the pipelines carry onto the weights; where +none is recorded, a gene's type is `"QTL"`. Groups share one prior variance by +default (`varStructure = "shared_nonSNP"`), since a group with few causal genes +has little to estimate its own from, and `"shared_type"` pools them within each +molecular type instead. + +cTWAS leaves a group with fewer genes than `min_group_size` (100 by default) out +of the fit, and `estCtwasGroupPriors()` warns when it does. To keep it, lower +`min_group_size` in both `CtwasRegionDataOptions()` and +`CtwasEstParamOptions()`: the second stops on a group under its own limit. ## Trimming each gene's variants @@ -220,13 +236,25 @@ set. `susieAlpha()` returns the per-single-effect alpha matrix behind those PIPs. The group priors are the run's one genome-wide estimate: the EM step fits them across every region, and each region is then fine-mapped under -them. `ctwasGroupPriors()` lists them once per run, one row per group with -its size, prior inclusion probability and prior effect variance: +them. `ctwasGroupPriors()` lists them once per run, one row per group. A group +is named `{study}_{context}_{type}`, with the three as columns of their own, +since those underscores cannot be split back. Beside its size, prior inclusion +probability and prior effect variance are cTWAS's summary of them: each gene +group's enrichment over the SNP group, and the heritability each group explains: ```{r ctwas-param} ctwasGroupPriors(res) ``` +A gene modelled through several molecular traits or contexts has one PIP for +each. `ctwasCombinedPips()` gives the probability that it is causal through any +of them. Pass a `mapping` from molecular ids to gene names when the ids of a +gene's traits differ, as an expression and a splicing trait's do: + +```{r ctwas-combined} +ctwasCombinedPips(res) +``` + The rest of the EM output, iteration traces and convergence flag included, sits in the `groupPriors` column, the same on every region of the run: @@ -302,9 +330,21 @@ apart lets you substitute priors of your own into `screenCtwasRegions()`. ## Boundary genes -A gene whose cis window crosses an LD block boundary is fine-mapped in only -one of the two blocks. `mergeCtwasBoundaryRegions()` re-runs such genes over -the merged pair: +A gene whose weights cross an LD block boundary is modelled as cTWAS models it. +Its z-score reads all its weights, and it is fine-mapped in the block holding +the largest share of its absolute weights, with the part of its weights in that +block. That first pass can call a gene whose real signal is a variant across the +boundary, since the block it is fine-mapped in has no LD to explain the signal +by. So `ctwasPipeline()` merges the regions of a high-PIP boundary gene and +fine-maps them again, as cTWAS's own post-processing does. In simulations +through `ctwasPipeline()` on real LD, a non-causal gene reading a causal variant +across the boundary reached a PIP above 0.5 in up to 93% of regions after the +first pass and in up to 16% after merging, while a causal boundary gene reached +it in at least 96% either way. + +`BoundaryMergeParam()` holds the settings; `BoundaryMergeParam(enabled = FALSE)` +stops at the first pass. `mergeCtwasBoundaryRegions()` is the same step for the +granular path: ```{r ctwas-merge, eval=FALSE} merged <- mergeCtwasBoundaryRegions( @@ -315,12 +355,6 @@ merged <- mergeCtwasBoundaryRegions( ) ``` -`ctwasPipeline(boundaryMergeParam = BoundaryMergeParam(enabled = TRUE))` does -the same thing inline, with -`mergePipThresh` / `mergeFilterCs` / `mergeMaxSNP` forwarding to those -arguments. It is off by default because it only matters when a -high-PIP gene actually straddles a boundary. - ## Common parameters ```{r ctwas-params, eval=FALSE} @@ -335,11 +369,11 @@ ctwasPipeline( thin = 0.1, niterPrefit = 3, niter = 30, - varStructure = "shared_type", + varStructure = "shared_nonSNP", fallbackToPrefit = FALSE ), variantPruningParam = VariantPruningParam(), - boundaryMergeParam = BoundaryMergeParam(enabled = FALSE) + boundaryMergeParam = BoundaryMergeParam() ) ``` diff --git a/vignettes/heritability.Rmd b/vignettes/heritability.Rmd index 96fe9cb56..20af7cc1a 100644 --- a/vignettes/heritability.Rmd +++ b/vignettes/heritability.Rmd @@ -274,6 +274,16 @@ per-variant heritability on half the variants puts 75% of it in `annotA` — and the estimate is directionally right but attenuated, again a consequence of the reference's size rather than of the annotation. +`enrichmentP` is ldsc's t test of the per-variant heritability inside the +annotation against outside it, for every estimator. Those shares are only +shares for values in [0, 1], the domain enrichment is defined on (Dey et al. +2020), so for an annotation with values outside it the enrichment columns are +`NA` and `tau` is the summary to read; `standardizeTauStar()` puts it on a +scale comparable across annotations. Inside [0, 1], the enrichment of a +continuous annotation still depends on its zero meaning "not in the +annotation": `(x + 3) / 100` and `(x - min(x)) / max(x - min(x))` fit the same +model but report different enrichments. + Annotations come from files via `readAnnotations()`, which reads BED, BigWig and LDSC `.annot` formats at a given set of variant positions: @@ -292,7 +302,10 @@ test each, via `scoreStats()`. Every estimator calibrates that test by the spread of its LD blocks' scores, as it does its standard errors, so it stays valid when the regression weights are only approximately right. Molecular QTL annotations built by `fineMappingAnnotations()` from fine-mapping results are -candidates by default. +candidates by default. Every effect a fit keeps spreads a whole unit of +posterior over its variants, however weak its evidence, so its `maxCpp` and +`sumPip` columns carry some low-confidence mass; `types = c("maxCppCs", +"sumPipCs")` builds them from the effects that produced a credible set alone. All four estimators fit the same baseline design. An all-ones base annotation is added ahead of the baseline annotations unless they already @@ -416,7 +429,7 @@ reports what its authors report, and `NA` where they report nothing: ```{r cis-h2} data(qtlSumStatsExample) -cis <- estimateH2(qtlSumStatsExample) +cis <- estimateH2(qtlSumStatsExample, referenceOverlap = 1) cis cis[[1]] heritabilityIntercept(estimateH2(qtlSumStatsExample, method = "hdlL")) @@ -424,15 +437,26 @@ heritabilityIntercept(estimateH2(qtlSumStatsExample, method = "hdlL")) Each row's element is the window its estimate covers: the row's `region`, or with `cisWindow` the trait's position widened by that much on each side. -LAVA, the default, projects the statistics onto the LD components that hold -99% of the LD variance and corrects for the reference's size. LAVA itself -withholds its estimate where the components outnumber a tenth of the sample -size, which a cis window's usually do at QTL sample sizes; `estimateH2()` -reports it at any size. LAVA and HDL-L read the reference's sample size, so -an `LdMatrix` reference must record `nSamples`. FastHer's likelihood -assumes in-sample LD (HEELS maximizes the same one): where the reference's -rank or the statistics reach the study's sample size, as with an external -reference larger than the study, its estimate is `NA`, with a warning. +Every method reads `z` as a t statistic and converts it exactly to the +correlation, since \(z / \sqrt{n}\) overstates a strong QTL's. LAVA, the +default, projects the statistics onto the LD components that hold 99% of the +LD variance. Whether LD is the study's own is not a yes or no, and LAVA's +noise term depends on how far the reference's LD lies from the study's: +`referenceOverlap` is the share of the study's samples in the reference. It +defaults to 0, and with a reference of thousands it hardly matters, so you +rarely need it. Set it when the LD comes from the study's own samples or a +small cohort holding them, as here, where it is 1; at 0 such an estimate comes +out far too low, and a warning says so. Each LAVA estimate records the value it +took in its `reference_overlap` column. +LAVA itself withholds its estimate where the components outnumber a tenth of +the sample size, which a cis window's usually do at QTL sample sizes; +`estimateH2()` reports it at any size. LAVA and HDL-L read the reference's +sample size, so an `LdMatrix` reference must record `nSamples`. FastHer's +likelihood assumes in-sample LD (HEELS maximizes the same one): where the +reference's rank or the statistics reach the study's sample size, as they can +with LD from outside the study, its estimate is `NA`, with a warning. Its +standard error assumes an infinitesimal architecture and is too small with few +causal variants. ## Next steps diff --git a/vignettes/twas-weights.Rmd b/vignettes/twas-weights.Rmd index bf772e9e1..3323dfe18 100644 --- a/vignettes/twas-weights.Rmd +++ b/vignettes/twas-weights.Rmd @@ -316,7 +316,10 @@ candidate's coefficients. These methods, and LDpred2-grid, pick the returned candidate on held-out summary statistics: the statistics are split into training and held-out parts, the path is refit -on each training part, and every candidate is scored on the held-out part. +on each training part, and every candidate is scored by how much of the +held-out part's variance it explains. Unlike a correlation, that score depends +on the weights' scale, so a fit that nearly ignores the LD and predicts far +more variance than the trait has does not win. The scoring shrinks the LD toward the identity by an amount set by the reference's sample size, because a reference smaller than the number of variants misses directions of the true LD, and the least-penalized fits put @@ -330,7 +333,7 @@ ids <- sprintf("chr1:%d:A:G", 100L * seq_len(p)) R <- cor(X) dimnames(R) <- list(ids, ids) stat <- list( - z = as.vector(cor(X, y)) * sqrt(n), + cor = as.vector(cor(X, y)), n = rep(n, p), variantNames = ids ) @@ -404,7 +407,6 @@ twasWeightsPipeline( phenotypeCovariates = NULL, genotypeCovariates = NULL ), - weightsDataType = NULL, phenotypeFilterParam = PhenotypeFilterParam(naAction = "drop") ) ``` @@ -450,7 +452,7 @@ key <- list( w <- do.call(weights, c(list(tw), key)) length(w) do.call(weightStandardized, c(list(tw), key)) # TRUE from the pipeline -do.call(weightsDataType, c(list(tw), key)) # tag carried through for reporting +do.call(dataType, c(list(tw), key)) # what the data recorded, if anything ``` `weightStandardized()` is `TRUE` for anything the pipeline fitted: genotypes @@ -471,7 +473,10 @@ the LD correlations and z-scores behind a TWAS Z already use. matrix for multivariate ones. The remaining payloads are keyed the same way: `variantIds()` for the aligned variant set, `cvResult()` for the cross-validation record (`rsq`, `r`, predicted values), and `methodFits()` for -the full fit object when the pipeline kept fits (`fitRetention != "none"`). +the fit object when the pipeline kept fits (`fitRetention != "none"`). A fit +never holds a copy of the genotypes, which can be its largest part, unless +asked: `fitRetention = "full"` keeps everything else the engine returned, and +`retainX = TRUE` with it keeps that copy too. `resolveWeights()` returns the weights and their variant IDs together, which is the form the downstream TWAS and MR steps consume. diff --git a/vignettes/xqtl-enrichment.Rmd b/vignettes/xqtl-enrichment.Rmd index 84eba03e5..86c015ec0 100644 --- a/vignettes/xqtl-enrichment.Rmd +++ b/vignettes/xqtl-enrichment.Rmd @@ -99,9 +99,10 @@ enrich_res ``` The result is a data frame with one row per such combination, giving the -enrichment estimate (`enrichment`), its standard error (`enrichmentSe`), and -the log-odds (`enrichmentLogOdds`). These enrichment priors can then be -supplied to a colocalization analysis. This synthetic single-region example is +log-odds enrichment (`enrichmentLogOdds`), its standard error on that scale +(`enrichmentSe`), and the same estimate as a multiplicative factor minus one +(`enrichment`). These enrichment priors can then be supplied to a +colocalization analysis. This synthetic single-region example is for illustration only; stable enrichment estimates require many fine-mapped regions. From 2013d1d5d7fc851450184134ac9ef4088877d763 Mon Sep 17 00:00:00 2001 From: Daniel Nachun Date: Wed, 4 Nov 2026 12:39:56 -0800 Subject: [PATCH 3/3] fix mr.mashr crash --- NAMESPACE | 1 + R/regularizedRegressionWrappers.R | 73 ++++++++++++++++--- man/mrmashWrapper.Rd | 4 +- man/twasEngineOptions.Rd | 7 ++ tests/testthat/helper-fitSamples.R | 35 +++++---- tests/testthat/test_jointEngine.R | 11 +-- .../test_regularizedRegressionWrappers.R | 64 ++++++++++++++++ 7 files changed, 166 insertions(+), 29 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index bd60e583b..afff20d63 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -774,6 +774,7 @@ importFrom(purrr, detect, detect_index, discard, + discard_at, exec, imap, keep, diff --git a/R/regularizedRegressionWrappers.R b/R/regularizedRegressionWrappers.R index 20c28dfc0..c7a82ea52 100644 --- a/R/regularizedRegressionWrappers.R +++ b/R/regularizedRegressionWrappers.R @@ -1083,6 +1083,13 @@ mrmashRssWeights <- function( #' \code{MrmashOptions} and \code{MrmashRssOptions} refuse #' \code{nthreads}: mr.mash itself runs on one thread, because on macOS #' its worker threads can later hang R inside the OpenMP runtime. +#' +#' \code{MrmashOptions} also takes \code{vEigenFloor}, which is +#' \code{\link{mrmashWrapper}}'s rather than mr.mash's: the smallest +#' eigenvalue the estimated residual covariance may have, as a fraction of +#' its spectral norm. Default \code{1e-3}. Below it, as near-duplicate +#' contexts make it, the eigenvalues are raised to the floor, the covariance +#' is not updated during the fit, and a warning says so. #' @param ... Arguments for the engine, under its own names. #' @return A \code{\link{MethodOptions}} object. #' @examples @@ -1590,6 +1597,7 @@ MrmashOptions <- function(...) { "MrmashOptions" ) .configRefuseUnavailable(extra, .mrmashUnavailable(), "MrmashOptions") + .mrmashCheckVEigenFloor(extra$vEigenFloor) .newMethodOptions( "mr.mashr::mr.mash", defaults = list(), @@ -1599,14 +1607,32 @@ MrmashOptions <- function(...) { # The bag is split before the engine sees it: .mrmashSplitPriorArgs # routes the prior-covariance names to MrmashPriorParam() and the rest # to mr.mash. So the accepted set is both, read from the live - # formals of each rather than transcribed. + # formals of each rather than transcribed, plus the wrapper's own + # vEigenFloor, which never reaches mr.mash. accepted = unique(c( .engineAcceptedNames("mr.mashr::mr.mash"), - names(formals(MrmashPriorParam)) + names(formals(MrmashPriorParam)), + "vEigenFloor" )) ) } +# @noRd +.mrmashCheckVEigenFloor <- function(vEigenFloor) { + if (is.null(vEigenFloor)) { + return(invisible(NULL)) + } + isFraction <- is.numeric(vEigenFloor) && + length(vEigenFloor) == 1L && + !is.na(vEigenFloor) && + vEigenFloor >= 0 && + vEigenFloor < 1 + if (!isFraction) { + abort("MrmashOptions: `vEigenFloor` must be one number in [0, 1).") + } + invisible(NULL) +} + #' @rdname twasEngineOptions #' @export MrmashRssOptions <- function(...) { @@ -6556,7 +6582,9 @@ dprAdaptiveGibbsWeights <- function( #' (\code{update_w0}, \code{max_iter}, \code{tol}, \code{w0_threshold}, #' \code{verbose}, ...). Arguments the wrapper derives --- the prior, the #' residual covariance, the coefficient initialisation --- cannot be set -#' here. +#' here. It also takes the wrapper's own \code{vEigenFloor}, the floor on +#' the estimated residual covariance's eigenvalues; see +#' \code{\link{twasEngineOptions}}. #' #' @param V Optional residual covariance matrix (conditions x conditions), or #' \code{NULL} to estimate it. @@ -6631,7 +6659,14 @@ mrmashWrapper <- function( priorBuilt <- setup$priorBuilt time1 <- proc.time() bInit <- .mrmashInitB(X, Y, bInitMethod, standardize, BPPARAM) - vInit <- .mrmashInitV(X, Y, V, updateV, updateVMethod) + vInit <- .mrmashInitV( + X, + Y, + V, + updateV, + updateVMethod, + as.list(methodArgs)$vEigenFloor %||% 1e-3 + ) fitMrmash <- .mrmashFit( priorBuilt$S0, bInit, @@ -6740,10 +6775,13 @@ mrmashWrapper <- function( ) } -# Robust residual-covariance init. Returns list(V, updateV); a rank-deficient V -# is ridge-regularized and its update disabled. +# Robust residual-covariance init. Returns list(V, updateV); a V whose smallest +# eigenvalue is under vEigenFloor times its spectral norm has its eigenvalues +# raised to that floor and its update disabled, since updating would take it +# back to singular. Near-duplicate contexts make such a V, and mr.mash's ELBO +# can turn NaN on one. # @noRd -.mrmashInitV <- function(X, Y, V, updateV, updateVMethod) { +.mrmashInitV <- function(X, Y, V, updateV, updateVMethod, vEigenFloor) { if (!is.null(V)) { return(list(V = V, updateV = updateV)) } @@ -6751,8 +6789,21 @@ mrmashWrapper <- function( if (updateVMethod == "diagonal") { return(list(V = diag(diag(V)), updateV = updateV)) } - if (any(eigen(V)$values < 1e-8)) { - return(list(V = V + diag(1e-8, nrow(V)), updateV = FALSE)) + eigenvalues <- eigen(V, symmetric = TRUE, only.values = TRUE)$values + floorValue <- vEigenFloor * max(eigenvalues) + if (min(eigenvalues) < floorValue) { + warn(glue( + "mrmashWrapper: the residual covariance of Y is close to singular ", + "(smallest / largest eigenvalue ", + "{signif(min(eigenvalues) / max(eigenvalues), 3)}), as ", + "near-duplicate contexts make it. Raising its eigenvalues to ", + "vEigenFloor = {vEigenFloor} of the largest and not updating it ", + "during the fit; `vEigenFloor` in MrmashOptions() sets the floor." + )) + return(list( + V = V + diag(floorValue - min(eigenvalues), nrow(V)), + updateV = FALSE + )) } list(V = V, updateV = updateV) } @@ -6785,6 +6836,7 @@ mrmashWrapper <- function( # from `methodArgs` under mr.mash's own names, with pecotmr's defaults # underneath. # @noRd +#' @importFrom purrr discard_at .mrmashFit <- function( S0, bInit, @@ -6808,7 +6860,8 @@ mrmashWrapper <- function( # One thread: see .mrmashUnavailable(). nthreads = 1L ) - user <- as.list(methodArgs) + # vEigenFloor is the wrapper's own, read by .mrmashInitV(). + user <- discard_at(as.list(methodArgs), "vEigenFloor") clash <- intersect(names(user), names(derived)) if (length(clash) > 0L) { abort(glue( diff --git a/man/mrmashWrapper.Rd b/man/mrmashWrapper.Rd index fab5ec220..62dac36ce 100644 --- a/man/mrmashWrapper.Rd +++ b/man/mrmashWrapper.Rd @@ -65,7 +65,9 @@ own names, built with \code{\link{MrmashOptions}} (\code{update_w0}, \code{max_iter}, \code{tol}, \code{w0_threshold}, \code{verbose}, ...). Arguments the wrapper derives --- the prior, the residual covariance, the coefficient initialisation --- cannot be set -here.} +here. It also takes the wrapper's own \code{vEigenFloor}, the floor on +the estimated residual covariance's eigenvalues; see +\code{\link{twasEngineOptions}}.} } \value{ A mr.mash fit, stored as a list with some or all of the following diff --git a/man/twasEngineOptions.Rd b/man/twasEngineOptions.Rd index 67f4d0d46..dc741e068 100644 --- a/man/twasEngineOptions.Rd +++ b/man/twasEngineOptions.Rd @@ -121,6 +121,13 @@ Options forwarded to the engine behind a TWAS weight method, \code{MrmashOptions} and \code{MrmashRssOptions} refuse \code{nthreads}: mr.mash itself runs on one thread, because on macOS its worker threads can later hang R inside the OpenMP runtime. + + \code{MrmashOptions} also takes \code{vEigenFloor}, which is + \code{\link{mrmashWrapper}}'s rather than mr.mash's: the smallest + eigenvalue the estimated residual covariance may have, as a fraction of + its spectral norm. Default \code{1e-3}. Below it, as near-duplicate + contexts make it, the eigenvalues are raised to the floor, the covariance + is not updated during the fit, and a warning says so. } \examples{ GlmnetOptions(nfolds = 10) diff --git a/tests/testthat/helper-fitSamples.R b/tests/testthat/helper-fitSamples.R index 219980798..fd8a4e2a3 100644 --- a/tests/testthat/helper-fitSamples.R +++ b/tests/testthat/helper-fitSamples.R @@ -1,7 +1,9 @@ # Real PLINK2 genotypes under a dataset whose contexts observe only 30 of the # panel's 100 samples, and one of whose traits (g2) is missing a value for five # of those: what a fit's samples are differs from the panel's and between -# traits. +# traits. Each context draws its own phenotypes: identical ones would make a +# joint fit's residual covariance singular, and mr.mash's result on it depends +# on the platform's rounding. .fitSamplesDataset <- function( panel = readGenotypes( plink2Prefix = file.path(test_path("test_data"), "test_variants") @@ -10,15 +12,12 @@ samples <- colnames(panel)[1:30] traits <- c("g1", "g2", "g3") set.seed(1) - y <- matrix( - rnorm(length(traits) * length(samples)), - nrow = length(traits), - dimnames = list(traits, samples) - ) - y["g2", 1:5] <- NA + brain <- .fitSamplesExpression(traits, samples) + age <- rnorm(length(samples)) + liver <- .fitSamplesExpression(traits, samples) at <- GenomicRanges::start(SummarizedExperiment::rowRanges(panel))[[100L]] se <- SummarizedExperiment::SummarizedExperiment( - assays = list(expression = y), + assays = list(expression = brain), rowRanges = `names<-`( GenomicRanges::GRanges( "chr21", @@ -26,16 +25,26 @@ ), traits ), - colData = S4Vectors::DataFrame( - age = rnorm(length(samples)), - row.names = samples - ) + colData = S4Vectors::DataFrame(age = age, row.names = samples) ) + seLiver <- se + SummarizedExperiment::assay(seLiver, "expression") <- liver QtlDataset( studyName = "s", genotypes = panel, - phenotypes = list(brain = se, liver = se) + phenotypes = list(brain = se, liver = seLiver) + ) +} + +# One context's phenotypes, g2 missing for the first five samples. +.fitSamplesExpression <- function(traits, samples) { + y <- matrix( + rnorm(length(traits) * length(samples)), + nrow = length(traits), + dimnames = list(traits, samples) ) + y["g2", 1:5] <- NA + y } # The same genotypes, their file naming every variant by an rsID. diff --git a/tests/testthat/test_jointEngine.R b/tests/testthat/test_jointEngine.R index f8485e6c4..c8a45b874 100644 --- a/tests/testthat/test_jointEngine.R +++ b/tests/testthat/test_jointEngine.R @@ -1920,15 +1920,16 @@ test_that("joint fits share one read, allele frequencies included", { )) expect_s4_class(run$result, "QtlFineMappingResult") expect_equal(run$reads, 1L) - # g2 is missing the first five of the 30 samples observed, so a fit has 25, - # and the panel spells its ids with underscores. + # g2 is missing the first five of the 30 samples observed, so a fit has 25. + # Results name a variant by its range and alleles, not the file's label. top <- S4Vectors::mcols(topLoci(run$result)) geno <- as.matrix(genotypes(data)) onFit <- colMeans(geno[6:30, , drop = FALSE], na.rm = TRUE) / 2 - expect_equal( - top$af, - unname(onFit[match(top$variant_id, chartr("_", ":", names(onFit)))]) + ids <- pecotmr:::.ldRefMatchIds( + pecotmr:::.qtlGenotypeSe(data), + seq_len(ncol(geno)) ) + expect_equal(top$af, unname(onFit[match(top$variant_id, ids)])) }) test_that("a joint unit plans its genotypes on the samples it fits", { diff --git a/tests/testthat/test_regularizedRegressionWrappers.R b/tests/testthat/test_regularizedRegressionWrappers.R index ece702c41..e34ae3e2f 100644 --- a/tests/testthat/test_regularizedRegressionWrappers.R +++ b/tests/testthat/test_regularizedRegressionWrappers.R @@ -1723,6 +1723,70 @@ test_that("mrmashWrapper refuses to let methodArgs override derived values", { ) }) +test_that("near-duplicate contexts get a floor on V's eigenvalues", { + skip_if_not_installed("mr.mashr") + set.seed(1) + X <- matrix(rnorm(60 * 4), 60, 4) + y <- rnorm(60) + nearDuplicate <- cbind(y, y + 1e-5 * rnorm(60)) + initV <- function(Y, floorValue = 1e-3) { + pecotmr:::.mrmashInitV(X, Y, NULL, TRUE, "full", floorValue) + } + eigenRatio <- function(V) { + values <- eigen(V, symmetric = TRUE, only.values = TRUE)$values + min(values) / max(values) + } + expect_warning(floored <- initV(nearDuplicate), "close to singular") + expect_false(floored$updateV) + expect_equal(eigenRatio(floored$V), 1e-3, tolerance = 1e-3) + # The floor is relative: rescaling Y rescales V, not the decision. + expect_warning(scaled <- initV(1e3 * nearDuplicate), "close to singular") + expect_equal(eigenRatio(scaled$V), 1e-3, tolerance = 1e-3) + # Distinct contexts keep V as it was estimated, and its update. + distinct <- cbind(y, rnorm(60)) + expect_no_warning(kept <- initV(distinct)) + expect_true(kept$updateV) + expect_equal( + kept$V, + pecotmr:::.mrmashComputeVInit(X, distinct, FALSE) + ) + # A user's floor decides whether they count as near-duplicate. + expect_warning(initV(distinct, 0.99), "close to singular") +}) + +test_that("vEigenFloor reaches the wrapper and stops short of mr.mash", { + skip_if_not_installed("mr.mashr") + skip_if_not_installed("glmnet") + m <- .rrwMulti(n = 60, p = 6, K = 3) + fitWith <- function(floorValue) { + mrmashWeights( + X = m$X, + Y = m$Y, + methodArgs = MrmashOptions( + canonicalPriorMatrices = TRUE, + vEigenFloor = floorValue + ) + ) + } + expect_warning( + suppressMessages(fitWith(0.99)), + "vEigenFloor = 0.99" + ) + expect_no_warning( + suppressMessages(fitWith(0)), + message = "close to singular" + ) +}) + +test_that("vEigenFloor must be one fraction, on the individual path only", { + skip_if_not_installed("mr.mashr") + for (bad in list(-0.1, 1, NA_real_, c(1e-6, 1e-5), "small")) { + expect_error(MrmashOptions(vEigenFloor = bad), "must be one number") + } + expect_equal(as.list(MrmashOptions(vEigenFloor = 0))$vEigenFloor, 0) + expect_error(MrmashRssOptions(vEigenFloor = 1e-6)) +}) + test_that("a flat prior name is routed into the prior group", { # The pipeline spells per-token kwargs flat, so mrmashWeights routes # prior-construction names into mrmashWrapper's `prior` record.