Skip to content

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 to jaxqtl.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's init(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:

__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 by init, with shape (n, p).
  • G: Genotype matrix with shape (n, m).
  • state: State returned by init(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 by init, with shape (n, p).
  • G: Genotype matrix with shape (n, m) (variants in columns).
  • state: State returned by this test's init(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 matches model.family.

Raises:

__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 by init, with shape (n, p).
  • G: Genotype matrix with shape (n, m).
  • state: State returned by init(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:

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 by init.

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 by init.

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 by init.

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 by init.

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 by init.

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 by init.

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 by init.

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 by init.

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 by init.

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: A jaxqtl.hypothesis.CumulantGeneratingFunction implementation.
  • state: CGF state created by cgf.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.

jaxqtl.hypothesis.GaussianWaldState(builtins.tuple) [source] ¤

Covariate-only residuals and weights for Gaussian coefficient inference.

jaxqtl.hypothesis.GlmWaldState(builtins.tuple) [source] ¤

Outcome and offset for fitting a full GLM for each variant.