Variant-level tests¤
jaxqtl provides variant-level association tests for scanning genotypes against molecular phenotypes.
Initialize an outcome and test variants¤
All concrete tests implement AbstractHypothesisTest[StateT]. init(X, y, offset) prepares an outcome, and
test(X, G, state) tests variants against that state. The rows of X, G, y, and a vector offset must describe
the same samples in the same order. Genotypes occupy the columns of G.
This example fits a Poisson null model once and tests two genotype blocks:
import equinox as eqx
import jax
import jax.numpy as jnp
from jaxqtl.distribution import Poisson
from jaxqtl.hypothesis import ScoreTest
from jaxqtl.infer import GeneralizedLinearModel
jax.config.update("jax_enable_x64", True)
X = jnp.ones((8, 1)) # Intercept-only null model.
y = jnp.array([2., 1., 4., 3., 5., 6., 7., 2.])
G = jnp.array([[0., 0.], [0., 1.], [1., 0.], [1., 1.],
[1., 2.], [2., 1.], [2., 2.], [0., 2.]])
test = ScoreTest(model=GeneralizedLinearModel(family=Poisson()))
initialize = eqx.filter_jit(test.init)
test_block = eqx.filter_jit(test.test)
state = initialize(X, y, 0.0)
first = test_block(X, G[:, :1], state)
second = test_block(X, G[:, 1:], state)
pvalues = jnp.concatenate((first.p, second.p))
For a single array, test(X, G, y, offset) combines initialization and testing and returns a TestResult.
Pass the same covariate matrix to initialization and testing. Reinitialize when the phenotype, offset, covariates,
or test configuration changes.
| Test | Initialization state | Work performed for each genotype block |
|---|---|---|
ScoreTest |
ScoreState: null-model residuals, weights, and fit diagnostics |
Residualize genotypes and calculate score statistics |
SpaTest |
SpaState: score state, link derivatives, and CGF state |
Calculate scores and evaluate saddlepoint tails |
WaldTest with LinearModel |
GaussianWaldState: null residuals and weights |
Fit variant effects using the residualized linear model |
WaldTest with a GLM |
GlmWaldState: response and offset |
Fit a full model containing each tested variant |
States are fixed-structure JAX PyTrees and exclude the shared covariate matrix and genotypes. WaldState is the
union of the two Wald state types. GLM Wald reuses the compiled fitting function; it still fits a separate model
for each variant.
The CLI's mapping executor compiles initialization separately from fixed-size test blocks and handles padding internally. Direct array callers control their own JIT boundaries and block shapes. Different array shapes can require separate compilations.
Implement a hypothesis test¤
Subclass AbstractHypothesisTest[StateT] with the concrete state type. Implement init, test, and the name
property, and provide model and std_err. Initialization must be independent of genotype-window width. Both
methods must support JAX transformations; file loading, block packing, and output writing belong to mapping.
jaxqtl.hypothesis.AbstractHypothesisTest
jaxqtl.hypothesis.AbstractHypothesisTest(equinox.Module, typing.Generic)
[source]
¤
Abstract base class for per-variant association tests.
Initialize once per outcome with init(X, y, offset), then reuse the resulting
state with test(X, G, state) for each genotype block. Initialization never
depends on the number of variants. States must be fixed-structure PyTrees and
should omit the shared covariate matrix. Calling an instance directly composes
both operations for a full genotype window.
Attributes:
model: Linear or generalized linear model used to fit the outcome.std_err: Coefficient covariance estimator used by tests that fit variant effects. Defaults tojaxqtl.infer.FisherInfoError.
__init__(self) -> None
¤
Initialize self. See help(type(self)) for accurate signature.
__call__(self, X: ArrayLike, G: ArrayLike, y: ArrayLike, offset: ArrayLike) -> TestResult
¤
Initialize the outcome and test all supplied variants.
Arguments:
X: Covariate matrix with shape(n, p).G: Genotype matrix with shape(n, m).y: Outcome vector with shape(n,).offset: Offset vector with shape(n,), or a scalar offset.
Returns:
A jaxqtl.hypothesis.TestResult containing per-variant statistics.
init(self, X: ArrayLike, y: ArrayLike, offset: ArrayLike) -> ~StateT
¤
Prepare an outcome independently of genotype-window shape.
Arguments:
X: Covariate matrix with shape(n, p).y: Outcome vector with shape(n,).offset: Offset vector with shape(n,), or a scalar offset.
Returns:
A test-specific PyTree reusable across genotype blocks. The state must not
retain X, which is shared across outcomes and permutation batches.
test(self, X: ArrayLike, G: ArrayLike, state: ~StateT) -> TestResult
¤
Test variants using previously initialized outcome state.
Arguments:
X: The covariate matrix used during initialization, with shape(n, p).G: Genotype matrix with shape(n, m)(variants in columns).state: State returned by this test'sinit(X, y, offset).
Returns:
A jaxqtl.hypothesis.TestResult containing per-variant statistics.
Score and Wald tests¤
jaxqtl.hypothesis.ScoreTest(jaxqtl.hypothesis.AbstractHypothesisTest)
[source]
¤
Score test for association between a variant and an outcome.
For a null (covariate-only) fit, let \(r_y\) be the working residuals and let \(g\) be a variant genotype vector. After residualizing \(g\) against covariates, the per-variant score statistic is \(U = g^{\top} W r_y\), with variance \(V = g^{\top} W g\), and the reported z-statistic is \(z = U / \sqrt{V}\) with two-sided p-value \(p = 2\Phi(-|z|)\) where \(\Phi(\cdot)\) is the Normal CDF.
This implementation requires jaxqtl.infer.FisherInfoError. Sandwich covariance estimators apply to Wald
coefficient inference and do not define a robust version of this score statistic.
Raises:
ValueError: Ifstd_erris notjaxqtl.infer.FisherInfoError.
__init__(self, model: AbstractLinearModel, std_err: AbstractVarianceEstimator = FisherInfoError()) -> None
¤
Initialize self. See help(type(self)) for accurate signature.
init(self, X: ArrayLike, y: ArrayLike, offset: ArrayLike) -> ScoreState
¤
Fit the null model and retain the state needed for score testing.
Arguments:
X: Covariate matrix with shape(n, p).y: Outcome vector with shape(n,).offset: Offset vector with shape(n,), or a scalar offset.
Returns:
A compact ScoreState with residuals, weights, and scalar fit diagnostics.
test(self, X: ArrayLike, G: ArrayLike, state: ScoreState) -> TestResult
¤
Score a genotype block using an initialized null model.
Arguments:
X: Covariate matrix used byinit, with shape(n, p).G: Genotype matrix with shape(n, m).state: State returned byinit(X, y, offset).
Returns:
Per-variant statistics and scalar null-model diagnostics in a
jaxqtl.hypothesis.TestResult.
jaxqtl.hypothesis.WaldTest(jaxqtl.hypothesis.AbstractHypothesisTest)
[source]
¤
Wald test for association between a variant and an outcome.
For each variant, this fits a full model including the variant and reports
\(\hat\beta / \mathrm{se}(\hat\beta)\). jaxqtl.infer.LinearModel uses a residualized Gaussian fast path and a
Student's t reference distribution with the full model's residual degrees of freedom. Generalized linear models
use a Normal reference distribution.
__init__(self, model: AbstractLinearModel, std_err: AbstractVarianceEstimator = FisherInfoError()) -> None
¤
Initialize self. See help(type(self)) for accurate signature.
init(self, X: ArrayLike, y: ArrayLike, offset: ArrayLike) -> jaxqtl.hypothesis._wald.GaussianWaldState | jaxqtl.hypothesis._wald.GlmWaldState
¤
Prepare an outcome for per-variant coefficient inference.
Arguments:
X: Covariate matrix with shape(n, p).y: Outcome vector with shape(n,).offset: Offset vector with shape(n,), or a scalar offset.
Returns:
Gaussian models return covariate-only residuals and weights. Generalized linear models retain the response and offset for their per-variant fits.
test(self, X: ArrayLike, G: ArrayLike, state: jaxqtl.hypothesis._wald.GaussianWaldState | jaxqtl.hypothesis._wald.GlmWaldState) -> TestResult
¤
Compute Wald statistics using initialized outcome state.
Arguments:
X: Covariate matrix used byinit, with shape(n, p).G: Genotype matrix with shape(n, m)(variants in columns).state: State returned by this test'sinit(X, y, offset).
Returns:
A jaxqtl.hypothesis.TestResult with per-variant inference and fitted
model diagnostics, including per-variant dispersion and likelihood.
Raises:
ValueError: For a linear model with no residual degrees of freedom after adding the tested variant.
Saddlepoint approximation¤
SPA starts from the score test's null fit. Choose a CGF matching the model family. It uses bisection with
finite, sign-changing brackets constructed inside the CGF domain. The normal approximation is used when SPA
is not attempted under the score cutoff and support checks. An attempted SPA calculation that does not
converge or yields an invalid correction returns NaN; ACAT propagates NaN inputs. The returned converged
field describes model fitting, not whether SPA was applied successfully.
jaxqtl.hypothesis.SpaTest(jaxqtl.hypothesis.AbstractHypothesisTest)
[source]
¤
Saddlepoint approximation (SPA) score test.
This starts from a score statistic \(S\) for each variant under the null model, then computes a saddlepoint approximation based on the cumulant generating function \(K(t)\) implied by the fitted mean model. A root \(\hat t\) is found such that \(K'(\hat t) = S\), then the Barndorff-Nielsen approximation is used via \(r^* = w + \log(v/w)/w\) with \(w = \mathrm{sign}(\hat t)\sqrt{2(\hat t S - K(\hat t))}\) and \(v = \hat t\sqrt{K''(\hat t)}\), yielding a two-sided p-value via a Normal tail approximation.
Info
For discrete distributions, a continuity correction term is included, such that \(v = (1 - \exp(-\hat t))\sqrt{K''(\hat t)}\).
This implementation requires jaxqtl.infer.FisherInfoError. Sandwich covariance estimators do not define a
robust version of the underlying score statistic or its saddlepoint approximation.
Attributes:
cgf: Cumulant generating function for the fitted response family. The caller must choose a CGF whose distribution matchesmodel.family.
Raises:
ValueError: Ifstd_erris notjaxqtl.infer.FisherInfoError.
__init__(self, model: AbstractLinearModel, std_err: AbstractVarianceEstimator = FisherInfoError(), cgf: CumulantGeneratingFunction[CGFStateT] = NegativeBinomialCGF()) -> None
¤
Initialize self. See help(type(self)) for accurate signature.
init(self, X: ArrayLike, y: ArrayLike, offset: ArrayLike) -> jaxqtl.hypothesis._spa.SpaState[CGFStateT]
¤
Fit the null model and initialize the CGF for SPA testing.
Arguments:
X: Covariate matrix with shape(n, p).y: Outcome vector with shape(n,).offset: Offset vector with shape(n,), or a scalar offset.
Returns:
A SpaState with compact score diagnostics, link derivatives, and the
selected CGF's state. The fitted model and covariates are not retained.
test(self, X: ArrayLike, G: ArrayLike, state: jaxqtl.hypothesis._spa.SpaState[CGFStateT]) -> TestResult
¤
Calculate SPA-corrected p-values using initialized outcome state.
Arguments:
X: Covariate matrix used byinit, with shape(n, p).G: Genotype matrix with shape(n, m).state: State returned byinit(X, y, offset).
Returns:
A jaxqtl.hypothesis.TestResult containing SPA-corrected p-values,
underlying score statistics, and scalar null-model diagnostics.
jaxqtl.hypothesis.CumulantGeneratingFunction
jaxqtl.hypothesis.CumulantGeneratingFunction(equinox.Module, typing.Generic)
[source]
¤
Abstract base for cumulant generating functions used by SPA.
init(self, glm_state: ModelResult) -> CGFStateT
¤
Construct CGF state from a fitted model.
Arguments:
glm_state: A fittedjaxqtl.infer.ModelResult.
Returns:
A CGF state object used by jaxqtl.hypothesis.saddlepoint_pvalue.
get_score_bounds(self, g_resid: jax.Array, state: CGFStateT) -> tuple
¤
Return bounds on the score statistic under the CGF model.
Arguments:
g_resid: Residualized genotype vector with shape(n,).state: CGF state created byinit.
Returns:
A (lower, upper) tuple of score bounds.
get_t_bounds(self, g_resid: jax.Array, state: CGFStateT) -> tuple
¤
Return bounds on the root-finding parameter t.
Arguments:
g_resid: Residualized genotype vector with shape(n,).state: CGF state created byinit.
Returns:
A (lower, upper) tuple of bounds for t.
cgf(self, t: jax.Array, state: CGFStateT) -> jax.Array
¤
Evaluate the cumulant generating function.
Arguments:
t: Evaluation point(s).state: CGF state created byinit.
Returns:
CGF value(s) evaluated at t.
jaxqtl.hypothesis.GaussianCGF(jaxqtl.hypothesis.CumulantGeneratingFunction)
[source]
¤
CGF implementation for a Gaussian mean/variance model.
For \(Y \sim \mathcal{N}(\mu, \sigma^2)\), the cumulant generating function is \(K(t) = \mu t + \frac{1}{2}\sigma^2 t^2\).
__call__(self, t: jax.Array, state: CGFStateT) -> jax.Array
¤
Evaluate the cumulant generating function.
Arguments:
t: Evaluation point(s).state: CGF state created byinit.
Returns:
CGF value(s) evaluated at t.
get_score_bounds(self, g_resid: jax.Array, state: CGFStateT) -> tuple
¤
Return bounds on the score statistic under the CGF model.
Arguments:
g_resid: Residualized genotype vector with shape(n,).state: CGF state created byinit.
Returns:
A (lower, upper) tuple of score bounds.
get_t_bounds(self, g_resid: jax.Array, state: CGFStateT) -> tuple
¤
Return bounds on the root-finding parameter t.
Arguments:
g_resid: Residualized genotype vector with shape(n,).state: CGF state created byinit.
Returns:
A (lower, upper) tuple of bounds for t.
jaxqtl.hypothesis.NegativeBinomialCGF(jaxqtl.hypothesis.CumulantGeneratingFunction)
[source]
¤
CGF implementation for a Negative Binomial mean/dispersion model.
For \(Y \sim \mathrm{NegBin}(\mu, r)\) parameterized by mean \(\mu\) and shape \(r\), the cumulant generating function can be written as \(K(t) = -r\log\left(1 - \frac{\mu}{r}(\exp(t) - 1)\right)\). This implementation uses \(r = 1/\alpha\) where \(\alpha\) is the fitted dispersion.
__call__(self, t: jax.Array, state: CGFStateT) -> jax.Array
¤
Evaluate the cumulant generating function.
Arguments:
t: Evaluation point(s).state: CGF state created byinit.
Returns:
CGF value(s) evaluated at t.
jaxqtl.hypothesis.PoissonCGF(jaxqtl.hypothesis.CumulantGeneratingFunction)
[source]
¤
CGF implementation for a Poisson mean model.
For \(Y \sim \mathrm{Poisson}(\mu)\), the cumulant generating function is \(K(t) = \log \mathbb{E}[\exp(tY)] = \mu(\exp(t) - 1)\). In this implementation, \(\mu\) is taken from the fitted model mean.
__call__(self, t: jax.Array, state: CGFStateT) -> jax.Array
¤
Evaluate the cumulant generating function.
Arguments:
t: Evaluation point(s).state: CGF state created byinit.
Returns:
CGF value(s) evaluated at t.
get_t_bounds(self, g_resid: jax.Array, state: CGFStateT) -> tuple
¤
Return bounds on the root-finding parameter t.
Arguments:
g_resid: Residualized genotype vector with shape(n,).state: CGF state created byinit.
Returns:
A (lower, upper) tuple of bounds for t.
saddlepoint_pvalue accepts ScalarLike values for score and scale, and an ArrayLike vector of
residualized genotypes with shape (n,) for g_resid. Scalar inputs may be Python scalars or scalar arrays.
CGFStateT denotes the state type returned by the selected CumulantGeneratingFunction.init; pass that state
as state when evaluating the tail probability.
jaxqtl.hypothesis.saddlepoint_pvalue(score: ScalarLike, g_resid: ArrayLike, cgf: CumulantGeneratingFunction[CGFStateT], state: CGFStateT, scale: ScalarLike = 1.0, two_sided_mode: typing.Literal['rstar', 'abs', '2min'] = 'rstar', log_p: bool = False, cutoff: float = 1.96, max_iter: int = 100) -> jax.Array
¤
Compute an SPA-corrected p-value for a score statistic.
Arguments:
score: Observed scalar score statistic.g_resid: Residualized genotype vector with shape(n,).cgf: Ajaxqtl.hypothesis.CumulantGeneratingFunctionimplementation.state: CGF state created bycgf.init.scale: Scalar multiplier applied to the SPA root-finding parameter.two_sided_mode: Strategy for two-sided p-values ("rstar","abs", or"2min").log_p: Whether to return the log p-value.cutoff: Threshold on the normal z-score above which SPA is attempted.max_iter: Maximum number of root-finding steps.
Returns:
A scalar p-value (or log p-value) for the observed score statistic. Returns NaN when an attempted SPA calculation fails to converge or is invalid.
Result type¤
jaxqtl.hypothesis.TestResult(builtins.tuple)
[source]
¤
Container for per-variant association test results.
For a genotype matrix with m variants, association fields have shape (m,).
Convergence, dispersion, and negative log-likelihood may be scalars when a score test reuses one null-model fit,
or arrays with shape (m,) when models are fitted per variant.
Attributes:
beta: Estimated variant effects.se: Standard errors of the variant effects.p: Two-sided association p-values.z: Score or Wald statistics.num_iters: Model-fitting iteration counts.converged: Model convergence indicators.disp: Fitted family dispersion or scale values.negloglikelihood: Negative log-likelihood objective at the fitted model.
Initialization states¤
jaxqtl.hypothesis.ScoreState(builtins.tuple)
[source]
¤
Compact null-fit state shared across genotype blocks.
Only residuals, weights, and scalar fit diagnostics are retained, so batched permutations do not copy covariates or unused fitted-model arrays.
jaxqtl.hypothesis.SpaState(builtins.tuple, typing.Generic)
[source]
¤
SPA preparation retaining a compact score fit and CGF-specific state.