Skip to content

Commit 97db2b2

Browse files
Yining97claude
andcommitted
RSS fine-mapping: pass finemappingOpts straight through to susie_rss
Per review: the fine-mapping parameters are not pipeline arguments. susieRssPipeline and rssAnalysisPipeline drop L/L_greedy/min_abs_corr/median_abs_corr/R_finite/R_mismatch as named args; finemappingOpts is a free-form list forwarded as-is into susie_rss, so unset keys inherit susieR defaults and a run with none set matches a manual susie_rss call. min_abs_corr (default 0.8) and median_abs_corr (default NULL) are credible-set purity options applied at susie_get_cs, not the fit, so they are isolated from finemappingOpts and routed to susie_get_cs across the reported coverages. No L_greedy clamp. median_abs_corr requires a susieR that provides it (GitHub master); it is passed to susie_get_cs only when the user sets it, so older susieR keeps working and the DESCRIPTION constraint stays >= 0.16.2 (the pixi CI environment pins an older conda susieR). Full suite 3076 pass / 0 fail; on chr21 AD_Bellenguez the unset pipeline matches a manual susie_rss run (max abs PIP diff 0), explicit step-1 params reproduce the step-1 credible sets exactly, and median_abs_corr routing is confirmed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a490c86 commit 97db2b2

7 files changed

Lines changed: 217 additions & 92 deletions

R/susie_wrapper.R

Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ postprocessFinemappingFits <- function(fits, dataX, dataY = NULL,
238238
region = NULL,
239239
priorEffTol = 1e-9,
240240
minAbsCorr = 0.8,
241+
medianAbsCorr = NULL,
241242
csInput = NULL) {
242243
fits <- fits[!vapply(fits, is.null, logical(1))]
243244
if (length(fits) == 0) stop("At least one fine-mapping fit must be supplied.")
@@ -257,6 +258,7 @@ postprocessFinemappingFits <- function(fits, dataX, dataY = NULL,
257258
signalCutoff = signalCutoff, otherQuantities = otherQuantities,
258259
region = region,
259260
priorEffTol = priorEffTol, minAbsCorr = minAbsCorr,
261+
medianAbsCorr = medianAbsCorr,
260262
csInput = csInput
261263
)
262264
})
@@ -324,6 +326,7 @@ postprocessFinemappingFit.susiF <- function(fit, method = "fsusie", csInput = NU
324326
region = NULL,
325327
priorEffTol = 1e-9,
326328
minAbsCorr = 0.8,
329+
medianAbsCorr = NULL,
327330
csInput = c("X", "Xcorr", "fsusie")) {
328331
csInput <- match.arg(csInput)
329332
variantNames <- extractVariantNames(fit)
@@ -332,7 +335,7 @@ postprocessFinemappingFit.susiF <- function(fit, method = "fsusie", csInput = NU
332335
csTables <- computeCsTables(
333336
fit, dataX = dataX, coverage = coverage,
334337
secondaryCoverage = secondaryCoverage, method = method,
335-
csInput = csInput, minAbsCorr = minAbsCorr
338+
csInput = csInput, minAbsCorr = minAbsCorr, medianAbsCorr = medianAbsCorr
336339
)
337340
topLoci <- buildTopLoci(
338341
fit, csTables, variantNames = variantNames, sumstats = sumstats,
@@ -432,7 +435,7 @@ selectEffects <- function(fit, priorEffTol = 1e-9) {
432435
computeCsTables <- function(fit, dataX, coverage = NULL,
433436
secondaryCoverage = c(0.7, 0.5),
434437
method = "susie", csInput = c("X", "Xcorr", "fsusie"),
435-
minAbsCorr = 0.8) {
438+
minAbsCorr = 0.8, medianAbsCorr = NULL) {
436439
csInput <- match.arg(csInput)
437440
primaryCoverage <- coverage
438441
if (is.null(primaryCoverage)) primaryCoverage <- fit$sets$requested_coverage
@@ -441,15 +444,16 @@ computeCsTables <- function(fit, dataX, coverage = NULL,
441444
coverages <- coverages[!is.na(coverages)]
442445

443446
tables <- lapply(coverages, function(cov) {
444-
computeCsTable(fit, dataX, coverage = cov, csInput = csInput, minAbsCorr = minAbsCorr)
447+
computeCsTable(fit, dataX, coverage = cov, csInput = csInput,
448+
minAbsCorr = minAbsCorr, medianAbsCorr = medianAbsCorr)
445449
})
446450
names(tables) <- vapply(coverages, formatCsColumn, character(1), method = method)
447451
attr(tables, "coverage") <- coverages
448452
tables
449453
}
450454

451455
computeCsTable <- function(fit, dataX, coverage, csInput = c("X", "Xcorr", "fsusie"),
452-
minAbsCorr = 0.8) {
456+
minAbsCorr = 0.8, medianAbsCorr = NULL) {
453457
csInput <- match.arg(csInput)
454458
if (csInput == "fsusie") {
455459
sets <- tryCatch(
@@ -470,12 +474,18 @@ computeCsTable <- function(fit, dataX, coverage, csInput = c("X", "Xcorr", "fsus
470474
return(list(sets = sets, cs_corr = csCorr, pip = fit$pip))
471475
}
472476

477+
# Purity thresholds for credible-set extraction. min_abs_corr / median_abs_corr
478+
# are isolated from finemappingOpts upstream and routed here; pass each only
479+
# when set. `fit` is passed positionally as `res`.
480+
csArgs <- list(coverage = coverage)
481+
if (!is.null(minAbsCorr)) csArgs$min_abs_corr <- minAbsCorr
482+
if (!is.null(medianAbsCorr)) csArgs$median_abs_corr <- medianAbsCorr
473483
if (csInput == "X") {
474-
sets <- susie_get_cs(fit, X = dataX, coverage = coverage, min_abs_corr = minAbsCorr)
484+
sets <- do.call(susie_get_cs, c(list(fit), csArgs, list(X = dataX)))
475485
out <- list(sets = sets, pip = fit$pip)
476486
out$cs_corr <- get_cs_correlation(out, X = dataX)
477487
} else {
478-
sets <- susie_get_cs(fit, Xcorr = dataX, coverage = coverage, min_abs_corr = minAbsCorr)
488+
sets <- do.call(susie_get_cs, c(list(fit), csArgs, list(Xcorr = dataX)))
479489
out <- list(sets = sets, pip = fit$pip)
480490
out$cs_corr <- get_cs_correlation(out, Xcorr = dataX)
481491
}
@@ -896,8 +906,13 @@ adjustSusieWeights <- function(twasWeightsResults, keepVariants, runAlleleQc = T
896906
#' @param ldMat LD correlation matrix. Mutually exclusive with xMat.
897907
#' @param xMat Genotype matrix (samples x variants). Mutually exclusive with ldMat.
898908
#' @param n Sample size.
899-
#' @param L Maximum number of causal configurations (default: 30).
900-
#' @param lGreedy Initial greedy number of causal configurations (default: 5).
909+
#' @param finemappingOpts Free-form list of fine-mapping options forwarded as-is
910+
#' into \code{susieR::susie_rss()} (e.g. \code{L}, \code{L_greedy},
911+
#' \code{R_finite}, \code{R_mismatch}). A key supplied is passed through; a key
912+
#' omitted inherits \code{susie_rss}'s own default. Two purity keys are special:
913+
#' \code{min_abs_corr} (default \code{0.8}) and \code{median_abs_corr} (default
914+
#' \code{NULL}) are isolated from this list and routed to
915+
#' \code{susieR::susie_get_cs()} for credible-set extraction, not the fit.
901916
#' @param analysisMethod Iteration mode for the \code{"susie_rss"} fit:
902917
#' \code{"susie_rss"} (default, normal IBSS), \code{"single_effect"} (L=1,
903918
#' single iteration), or \code{"bayesian_conditional_regression"}
@@ -919,12 +934,6 @@ adjustSusieWeights <- function(twasWeightsResults, keepVariants, runAlleleQc = T
919934
#' @param coverage Coverage level (default: 0.95).
920935
#' @param secondaryCoverage Secondary coverage levels (default: c(0.7, 0.5)).
921936
#' @param signalCutoff PIP cutoff for selecting top loci (default: 0.1).
922-
#' @param minAbsCorr Minimum absolute correlation for CS purity (default: 0.8).
923-
#' @param rFinite Controls variance inflation to account for estimating
924-
#' the R matrix from a finite reference panel. NULL (default): no
925-
#' variance inflation. Passed directly to susie_rss.
926-
#' @param rMismatch LD mismatch correction method passed directly to susie_rss.
927-
#' Default NULL disables mismatch correction.
928937
#' @param ... Additional parameters passed to susie_rss. Supplying
929938
#' \code{var_y} here, together with \code{beta} and \code{se} columns in
930939
#' \code{sumstats}, selects the \code{bhat/shat/var_y} sufficient-statistic
@@ -940,19 +949,24 @@ adjustSusieWeights <- function(twasWeightsResults, keepVariants, runAlleleQc = T
940949
#' @importFrom dplyr arrange select
941950
#' @export
942951
susieRssPipeline <- function(sumstats, ldMat = NULL, xMat = NULL, n = NULL,
943-
L = 30, lGreedy = 5,
944952
analysisMethod = c("susie_rss", "single_effect", "bayesian_conditional_regression"),
945953
methods = NULL,
946954
addSusieInf = TRUE,
947955
coverage = 0.95,
948956
secondaryCoverage = c(0.7, 0.5),
949957
signalCutoff = 0.1,
950-
minAbsCorr = 0.8,
951-
rFinite = NULL, rMismatch = NULL, ...) {
958+
finemappingOpts = list(), ...) {
952959
analysisMethod <- match.arg(analysisMethod)
953960
if (is.null(ldMat) && is.null(xMat)) stop("Either ldMat or xMat must be provided.")
954961
if (!is.null(ldMat) && !is.null(xMat)) stop("Only one of ldMat or xMat should be provided, not both.")
955-
if (!is.null(lGreedy)) lGreedy <- min(lGreedy, L)
962+
# Fine-mapping options are a free-form passthrough to susie_rss. Isolate the
963+
# two credible-set purity options (they act at susie_get_cs extraction, not the
964+
# fit) and route them separately; everything else flows into the susie_rss call.
965+
if (!is.list(finemappingOpts)) stop("finemappingOpts must be a list.")
966+
minAbsCorr <- finemappingOpts$min_abs_corr %||% 0.8
967+
medianAbsCorr <- finemappingOpts$median_abs_corr # NULL when absent (susie_get_cs default)
968+
finemappingOpts$min_abs_corr <- NULL
969+
finemappingOpts$median_abs_corr <- NULL
956970

957971
# Resolve effective methods. NULL => legacy single-method via analysisMethod.
958972
validRssMethods <- c("susie_rss", "susie_inf_rss", "susie_ash_rss")
@@ -990,10 +1004,12 @@ susieRssPipeline <- function(sumstats, ldMat = NULL, xMat = NULL, n = NULL,
9901004
names(z) <- rownames(sumstats)
9911005
}
9921006

993-
dots <- list(...)
994-
varY <- dots$varY
995-
dots$varY <- NULL
996-
if (!is.null(dots$bhat) || !is.null(dots$shat)) {
1007+
# Free-form passthrough to susie_rss: finemappingOpts (purity keys already
1008+
# isolated above) plus any extra named args. Forwarded verbatim into the fit.
1009+
fitOpts <- c(finemappingOpts, list(...))
1010+
varY <- fitOpts$varY
1011+
fitOpts$varY <- NULL
1012+
if (!is.null(fitOpts$bhat) || !is.null(fitOpts$shat)) {
9971013
stop("Pass summary effects as 'beta' and 'se' columns in sumstats; ",
9981014
"susieRssPipeline constructs bhat and shat internally.")
9991015
}
@@ -1017,33 +1033,34 @@ susieRssPipeline <- function(sumstats, ldMat = NULL, xMat = NULL, n = NULL,
10171033
shat <- sumstats$se
10181034
names(bhat) <- names(shat) <- names(z)
10191035
common <- c(list(bhat = bhat, shat = shat, var_y = varY, n = n,
1020-
coverage = coverage, R_finite = rFinite,
1021-
R_mismatch = rMismatch), dots)
1036+
coverage = coverage), fitOpts)
10221037
} else {
1023-
common <- c(list(z = z, n = n, coverage = coverage,
1024-
R_finite = rFinite, R_mismatch = rMismatch), dots)
1038+
common <- c(list(z = z, n = n, coverage = coverage), fitOpts)
10251039
}
10261040
if (!is.null(xMat)) common$X <- xMat else common$R <- ldMat
10271041

1042+
# Method-specific overrides are applied with modifyList so they win over any
1043+
# passthrough key of the same name (e.g. single_effect forces L = 1) without a
1044+
# duplicate-argument error.
10281045
fitOneSusieRss <- function() {
10291046
if (analysisMethod == "single_effect") {
1030-
do.call(susie_rss, c(common, list(L = 1, L_greedy = NULL, max_iter = 1)))
1047+
do.call(susie_rss, modifyList(common, list(L = 1, L_greedy = NULL, max_iter = 1)))
10311048
} else if (analysisMethod == "bayesian_conditional_regression") {
1032-
do.call(susie_rss, c(common, list(L = L, L_greedy = lGreedy, max_iter = 1)))
1049+
do.call(susie_rss, modifyList(common, list(max_iter = 1)))
10331050
} else {
1034-
do.call(susie_rss, c(common, list(L = L, L_greedy = lGreedy)))
1051+
do.call(susie_rss, common)
10351052
}
10361053
}
10371054
fitOneSusieInfRss <- function() {
1038-
do.call(susie_rss, c(common, list(L = L, L_greedy = lGreedy,
1039-
unmappable_effects = "inf",
1040-
convergence_method = "pip",
1041-
refine = FALSE, model_init = NULL)))
1055+
do.call(susie_rss, modifyList(common,
1056+
list(unmappable_effects = "inf",
1057+
convergence_method = "pip",
1058+
refine = FALSE, model_init = NULL)))
10421059
}
10431060
fitOneSusieAshRss <- function() {
1044-
do.call(susie_rss, c(common, list(L = L, L_greedy = lGreedy,
1045-
unmappable_effects = "ash",
1046-
convergence_method = "pip")))
1061+
do.call(susie_rss, modifyList(common,
1062+
list(unmappable_effects = "ash",
1063+
convergence_method = "pip")))
10471064
}
10481065

10491066
fittedModels <- list()
@@ -1056,11 +1073,11 @@ susieRssPipeline <- function(sumstats, ldMat = NULL, xMat = NULL, n = NULL,
10561073
identical(fitMethods, "bayesian_conditional_regression")) {
10571074
if (chainInfToSusieRss) {
10581075
chainedArgs <- prepareSusieFromInfArgs(
1059-
list(L = L, L_greedy = lGreedy),
1076+
list(L = common$L, L_greedy = common$L_greedy),
10601077
fittedModels[["susie_inf_rss"]], refineDefault = TRUE,
10611078
unmappableEffects = "none"
10621079
)
1063-
rssFit <- do.call(susie_rss, c(common, chainedArgs))
1080+
rssFit <- do.call(susie_rss, modifyList(common, chainedArgs))
10641081
} else {
10651082
rssFit <- fitOneSusieRss()
10661083
}
@@ -1071,11 +1088,11 @@ susieRssPipeline <- function(sumstats, ldMat = NULL, xMat = NULL, n = NULL,
10711088
if ("susie_ash_rss" %in% fitMethods) {
10721089
if (chainInfToSusieAshRss) {
10731090
chainedArgs <- prepareSusieFromInfArgs(
1074-
list(L = L, L_greedy = lGreedy),
1091+
list(L = common$L, L_greedy = common$L_greedy),
10751092
fittedModels[["susie_inf_rss"]], refineDefault = NULL,
10761093
unmappableEffects = "ash"
10771094
)
1078-
ashFit <- do.call(susie_rss, c(common, chainedArgs))
1095+
ashFit <- do.call(susie_rss, modifyList(common, chainedArgs))
10791096
} else {
10801097
ashFit <- fitOneSusieAshRss()
10811098
}
@@ -1113,6 +1130,7 @@ susieRssPipeline <- function(sumstats, ldMat = NULL, xMat = NULL, n = NULL,
11131130
secondaryCoverage = secondaryCoverage,
11141131
signalCutoff = signalCutoff,
11151132
minAbsCorr = minAbsCorr,
1133+
medianAbsCorr = medianAbsCorr,
11161134
csInput = ppCsInput
11171135
)
11181136
# Primary method preference: "susie_rss" > other names > first fit

R/univariate_pipeline.R

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -519,15 +519,17 @@ regionDataToSusieRssInput <- function(rssInput, ldData) {
519519
#' @param addSusieInf Logical controlling chained init when
520520
#' \code{"susie_inf_rss"} is in \code{methods} alongside
521521
#' \code{"susie_rss"} and/or \code{"susie_ash_rss"}. Default \code{TRUE}.
522-
#' @param finemappingOpts List of fine-mapping options (L, L_greedy, coverage,
523-
#' signal_cutoff, min_abs_corr).
522+
#' @param finemappingOpts Free-form list of fine-mapping options. \code{coverage}
523+
#' and \code{signal_cutoff} are pipeline-reporting choices kept here; everything
524+
#' else (e.g. \code{L}, \code{L_greedy}, \code{R_finite}, \code{R_mismatch}) is
525+
#' forwarded as-is into \code{susieR::susie_rss()} — supplied keys pass through,
526+
#' omitted keys inherit susieR defaults (a run with the fit params unset matches
527+
#' a manual \code{susie_rss()} call). The purity keys \code{min_abs_corr}
528+
#' (default \code{0.8}) and \code{median_abs_corr} (default \code{NULL}) are
529+
#' isolated and routed to \code{susieR::susie_get_cs()} instead of the fit.
524530
#' @param impute Whether to impute missing variants via RAISS (default TRUE).
525531
#' @param imputeOpts List of imputation options (rcond, R2_threshold, minimum_ld, lamb).
526532
#' @param pipCutoffToSkip PIP threshold for early stopping (default 0, no skip).
527-
#' @param rFinite Controls variance inflation to account for finite reference LD.
528-
#' Passed to \code{susieR::susie_rss()}.
529-
#' @param rMismatch LD mismatch correction method passed to \code{susieR::susie_rss()}.
530-
#' Default NULL disables mismatch correction.
531533
#' @param keepIndel Whether to keep indel variants (default TRUE).
532534
#' @param commentString Comment character for sumstat file (default "#").
533535
#' @param diagnostics Whether to include diagnostic info (default FALSE).
@@ -547,18 +549,20 @@ rssAnalysisPipeline <- function(
547549
methods = NULL,
548550
addSusieInf = TRUE,
549551
finemappingOpts = list(
550-
L = 20, L_greedy = 5,
551-
coverage = c(0.95, 0.7, 0.5), signal_cutoff = 0.025,
552-
min_abs_corr = 0.8
552+
coverage = c(0.95, 0.7, 0.5), signal_cutoff = 0.025
553553
),
554554
impute = TRUE, imputeOpts = list(rcond = 0.01, R2_threshold = 0.6, minimum_ld = 5, lamb = 0.01),
555-
pipCutoffToSkip = 0, rFinite = NULL, rMismatch = NULL,
555+
pipCutoffToSkip = 0,
556556
keepIndel = TRUE, commentString = "#", diagnostics = FALSE,
557557
binaryTraitModel = c("rss", "ols")) {
558558
binaryTraitModel <- match.arg(binaryTraitModel)
559559
if (!is(ldData, "LdData")) {
560560
stop("ldData must be an LdData object")
561561
}
562+
# R_finite / R_mismatch are susie_rss fit options supplied via finemappingOpts;
563+
# source them once here for the QC stage (which also accepts them).
564+
rFinite <- finemappingOpts$R_finite %||% finemappingOpts$rFinite
565+
rMismatch <- finemappingOpts$R_mismatch %||% finemappingOpts$rMismatch
562566
res <- list()
563567
rssInput <- loadRssData(
564568
sumstatPath = sumstatPath, columnFilePath = columnFilePath,
@@ -648,20 +652,22 @@ rssAnalysisPipeline <- function(
648652
priCoverage <- finemappingOpts$coverage[1]
649653
secCoverage <- if (length(finemappingOpts$coverage) > 1) finemappingOpts$coverage[-1] else NULL
650654

651-
finemappingOptsL_greedy <- finemappingOpts$L_greedy %||% finemappingOpts$lGreedy
652-
finemappingOptsSignalCutoff <- finemappingOpts$signal_cutoff %||% finemappingOpts$signalCutoff
653-
finemappingOptsMinAbsCorr <- finemappingOpts$min_abs_corr %||% finemappingOpts$minAbsCorr
655+
finemappingOptsSignalCutoff <- finemappingOpts$signal_cutoff %||% finemappingOpts$signalCutoff %||% 0.025
656+
# The fit/purity passthrough = finemappingOpts minus the pipeline-reporting keys
657+
# (coverage / signal_cutoff), which susieRssPipeline takes as separate arguments.
658+
# susieRssPipeline isolates min_abs_corr/median_abs_corr and forwards the rest to susie_rss.
659+
finemappingOptsForFit <- finemappingOpts
660+
finemappingOptsForFit$coverage <- NULL
661+
finemappingOptsForFit$signal_cutoff <- NULL
662+
finemappingOptsForFit$signalCutoff <- NULL
654663
res <- do.call(susieRssPipeline, c(susieReady, list(
655-
L = finemappingOpts$L, lGreedy = finemappingOptsL_greedy,
656664
analysisMethod = finemappingMethod,
657665
methods = methods,
658666
addSusieInf = addSusieInf,
659667
coverage = priCoverage,
660668
secondaryCoverage = secCoverage,
661669
signalCutoff = finemappingOptsSignalCutoff,
662-
minAbsCorr = finemappingOptsMinAbsCorr,
663-
rFinite = rFinite,
664-
rMismatch = rMismatch
670+
finemappingOpts = finemappingOptsForFit
665671
)))
666672
if (!identical(zMismatchQc, "none") || isTRUE(alleleFlipKriging)) {
667673
res$outlier_number <- qcResults$outlier_number
@@ -686,15 +692,14 @@ rssAnalysisPipeline <- function(
686692
list(sumstats = sumstats, n = n, var_y = varY),
687693
ldData
688694
)$susie_rss_input
695+
fmFit <- finemappingOpts
696+
fmFit$coverage <- NULL; fmFit$signal_cutoff <- NULL; fmFit$signalCutoff <- NULL
689697
do.call(susieRssPipeline, c(reanalysisInput, list(
690-
L = finemappingOpts$L, lGreedy = finemappingOpts$L_greedy %||% finemappingOpts$lGreedy,
691698
analysisMethod = method,
692699
coverage = priCoverage,
693700
secondaryCoverage = secCoverage,
694701
signalCutoff = finemappingOptsSignalCutoff,
695-
minAbsCorr = finemappingOptsMinAbsCorr,
696-
rFinite = rFinite,
697-
rMismatch = rMismatch
702+
finemappingOpts = fmFit
698703
)))
699704
}
700705

man/postprocessFinemappingFits.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)