Changelog#
Changelog#
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]#
Added#
GLM design API for cross-sample pattern comparison. New public
compare_glm(spectra, design, contrast, …)generalises the two-group test to arbitrary OLS designs (binary, continuous, multi-factor) with an analytic Wald null. The two-group case is recovered exactly.Comparator.test_diff_freq(...)gains acontrast=argument (column name, dict, or contrast vector).Analytic Wald null for
log_l2(null="wald") oncompare_two_groups,compare_two_groups_masked, andcompare_glm. Per-gene statistic is integrated via Liu’s approximation against a pooled-across-genes full within-group Σ (a single 30×30 eigendecomposition before each Liu integration); bypasses the small-n permutation BH-floor while keeping mean within-group null FPR at ~0.012 across the three benchmark panels. Emits aUserWarningat residual df < 3. The masked variant uses a mask-aware pooled estimator with per-gene noncentrality scaling so genes with different observed cohorts get correctly-scaled eigenvalues.Analytic Welch t null for
compare_two_groups_scalar(null="wald", now the default). Computes per-gene two-sided p-values from the Welch-Satterthwaite t-distribution; lets the DE companion bypass the permutation1/(n_perm+1)raw-p floor on small cohorts. The previous permutation null is preserved asnull="permutation".normalize_shape: bool = Falsekeyword on every spectrum-input comparison test (compare_two_groups,compare_two_groups_masked,compare_glm). When True, divides each per-(sample, gene) spectrum by its sum along the frequency axis before the statistic is computed, so the test fires only on shape-only redistribution of power across radial frequencies. Statistic-agnostic; default False preserves prior behaviour.Effective-rank diagnostics for the within-group covariance used by the Wald null:
quadsv.effective_rank(cov, weights=None)primitive (K_eff = (Σλ)² / Σλ²),quadsv.gene_pattern_diversity(spectra)for per-sample heterogeneity,quadsv.within_group_pattern_diversity(spectra, groups)for cohort-level, and a chainableComparator.effective_rank(level=…)accessor.Top-level convenience exports:
quadsv.Detector(data, …)andquadsv.Comparator(data_list, …)factories that dispatch onAnnDatavsSpatialData;quadsv.compute_null_params,quadsv.auto_chunk_size,quadsv.liu_sfpromoted to top level (canonicalquadsv.statisticspaths still work).Public-API freeze test (
tests/test_public_api.py) snapshots__all__, docstring presence, canonical-path identity, and asserts removed legacy paths raiseModuleNotFoundError.Convenience input modes for
Comparator.normalize_covariates. In addition to the existing per-sampleSequence[np.ndarray]of pre-rasterized(n_covariates, ny, nx)images, the method now accepts a sharedSequence[str]of column names; the subclass interprets it natively:ComparatorIrregularlooks each key up inadata.obs.columnsfirst, thenadata.var_names(preferring obs on collision); the resolved per-spot vector is NUFFTed directly onto the sample’s k-grid — so the same call accepts deconvolved cell-type proportion columns and per-gene expression columns (e.g., a housekeeping or marker gene) interchangeably.ComparatorGridforwards the keys asvalue_key=tospatialdata.rasterize_bins, so any combination of.obscolumns andvar_namesin the comparator’s table works.
Mode is detected from the first element’s type. Both paths reduce to the same
(n_covariates, K)per-sample features fed into the log-space residualization, so the math is identical — only the input boilerplate is different.
Changed#
Breaking: package layout migrated to
src/quadsv/with the four conceptual layers as physical subpackages —quadsv.kernels.{fft,nufft},quadsv.detectors.{base,irregular,grid},quadsv.comparators.{__init__,multisample}.import quadsvandfrom quadsv import …keep working; editable installs must be reissued (pip install -e ".[dev]"). Lint / format commands now targetsrc/ tests/.Breaking: unified
normalize_*surface API inquadsv.comparators.multisample(no aliases):normalize_by_background→normalize_backgroundresidualize_against_covariates→normalize_covariatesshape_normalize→normalize_shapeConsistent first-argspectra, keyword-only after,eps=1e-12default on every helper, and NumPy-style docstrings with LaTeX math.normalize_covariates’s first positional arg is renamedgene_spectra→spectra, and its implementation now operates in log-space: it residualiseslog(spectra + ε)against[1, log(C^T + ε)]and exponentiates, so the output stays strictly positive and composes cleanly with downstreamlog_l2tests. Log-spacenormalize_covariatesalso commutes exactly withnormalize_background(left- vs right-multiplication of the log-spectrum matrix by orthogonal-projection matrices on disjoint axes), so the two can be applied in either order. The remaining chainable comparator instance method follows the rename:.residualize()→.normalize_covariates()
Breaking:
Comparator.fit()renamed toComparator.compute_spectra(). The method computes per-sample radial-binned power spectra rather than fitting model parameters; the new name describes the operation directly and matches the codebase’s verb-first method convention. All three keyword arguments (n_jobs,landmark_genes,progress) and the chainablereturn selfbehaviour are unchanged.Breaking:
designmoved from Comparator constructor to test time. The cross-sample contrast is no longer a construction argument — it is supplied directly to.test_diff_freq(design, ...)/.test_diff_expr(design, ...)(positional first arg), and to.effective_rank(level="within_group", design=...)for the group-conditioned diagnostic. A single fitted comparator can now serve any number of unrelated contrasts on the samespectra_without recomputing per-sample spectra.min_samples_per_groupfollowsdesigntotest_diff_freq(kwarg) since it’s a property of the design’s group sizes, not of the spectra.designaccepts the same three forms as before:1-D array / Series of binary labels → two-sample dispatch (
compare_two_groups/compare_two_groups_masked);2-D
np.ndarrayof shape(n_samples, p)→ GLM design matrix, used verbatim bycompare_glm;pandas.DataFrame→ GLM design, patsy-encoded bycompare_glm.
Breaking: default
nullswitched from"permutation"to"wald"across the entire comparison surface —Comparator.test_diff_freq,quadsv.comparators.multisample.compare_two_groups, andquadsv.comparators.multisample.compare_two_groups_masked. The Wald (Liu mixture-χ²) null bypasses the small-n permutation BH-floor and is the only path that works on every dispatch target (binary perm/Wald + GLM Wald), so it makes a single sensible package-wide default. Callers who want the permutation null must now passnull="permutation"explicitly. As a related ergonomic fix,compare_two_groups{,_masked}(statistic="welch_t_cauchy", null="wald")no longer raises —welch_t_cauchycarries its own analytic null (documented as ignoring thenullkwarg) so the package defaultnull="wald"is treated as a no-op for that statistic.Breaking: statistical-test naming cleanup in
quadsv.comparators.multisampleand the correspondingComparator.test_diff_*methods:compare_designs→compare_glm. The plural form was awkward (one design per call);compare_glmnames the test family at the call site and parallels the binarycompare_two_groupscleanly.Statistic
"cauchy_welch"→"welch_t_cauchy". The new token reads in pipeline order (per-bin Welch t first, gene-level Cauchy combination second) and disambiguates from naming the gene-level aggregator alone.null="welch"→null="wald"oncompare_two_groups_scalar/Comparator.test_diff_expr. The Welch t-statistic is a Wald-type test, so unifying the analytic-null token across the API removes a confusing “wald on the spectrum path, welch on the scalar path” asymmetry. The specific null distribution (Welch-Satterthwaite t vs Liu mixture-χ²) is still spelled out in each function’s docstring.null="liu"alias retired. Theliutoken referred to the numerical algorithm used to integrate the Wald χ² mixture tail (seequadsv.statistics.liu_sf), not a separate statistical concept. Single canonical token:wald.
Breaking: Comparator attribute surface narrowed (sklearn-style moderate-privacy convention). The public surface is now
samples,gene_names,feature_mode,freq_edges, plus the trailing-underscore fitted attributes (spectra_,dc_,presence_,rotation_angles_).design/groups_are no longer carried as instance state — the comparator is design-agnostic. Internal config knobs that were inadvertently public are now single-underscore-prefixed:_n_radial_bins,_fft_solver,_workers,_presence_threshold,_spacings,_grid_shapes,_spectrum_fft_solver,_fft_chunk_size,_spacing_override,_bins,_table_name,_col_key,_row_key,_value_key.Breaking: Comparator test methods renamed and aligned with the standalone
compare_*API inquadsv.comparators.multisample:.test_pattern()→.test_diff_freq()— gains a newnormalize_shape: bool = Falsekeyword, forwarded to its dispatch target (compare_two_groups,compare_two_groups_masked, orcompare_glm) so users get the shape-only DF path without mutatingcmp.spectra_..test_expression()→.test_diff_expr()— gains an explicitnull: str = "wald"keyword (the analytic Welch-Satterthwaite t-distribution path oncompare_two_groups_scalar); the previous always-permutation behaviour is reachable vianull="permutation".
Removed#
Breaking:
groups=/design=constructor kwargs onComparatorIrregularandComparatorGridare gone. Supply the 1-D labels or design matrix to the test method instead (cmp.test_diff_freq(design, ...),cmp.test_diff_expr(design, ...)). The comparator no longer carries design state; one fitted comparator can serve any number of contrasts on the same spectra.Breaking:
Comparator.shape_normalize()chainable method retired. Use the equivalentcmp.test_diff_freq(..., normalize_shape=True)keyword path for the one-shot non-destructive test, or callquadsv.comparators.multisample.normalize_shape(cmp.spectra_)directly to obtain the standalone transform. The previous in-place method silently mutatedcmp.spectra_and surprised subsequent.test_diff_freq()/.test_diff_expr()calls on the same comparator.Breaking: the
test = test_patternalias retired. Use the explicitcmp.test_diff_freq(...)(orcmp.test_diff_expr(...)for the DE companion); the unqualifiedcmp.test()was ambiguous once the API exposed two complementary tests.Breaking:
centerargument retired across the comparator API.ComparatorIrregular,ComparatorGrid, andcompute_sample_spectrumno longer acceptcenter. Per-gene mean centring (the previous default) is now the only spectrum normalisation path. The_ZSCORE_CLIPconstant, thezscore_clipparameter, and the per-bin clamp in the NUFFT loop are deleted (~50 LOC).Breaking:
benchmark_statisticsfunction and the matchingComparator.benchmark()method retired. Invokecompare_two_groupsdirectly with eachstatistic=value to A/B compare on the same fitted spectra (~95 LOC).Breaking:
statistic="hotelling_lw"andstatistic="mmd_rbf"paths retired from every comparison function. Both were impractically slow and consistently dominated on sensitivity bylog_l2 + null='wald'orwelch_t_cauchy._AVAILABLE_STATISTICSnow reads("log_l2", "welch_t_cauchy").Breaking: six legacy-path shim modules removed —
quadsv.fft,quadsv.nufft,quadsv.detector,quadsv.detector_grid,quadsv._detector_base,quadsv.multisample. Use the canonical subpackage paths.Breaking: backend ABCs
KernelandMatrixKernelBaseno longer re-exported from top-levelquadsv. They live atquadsv.kernelsand are intended for backend authors.
Fixed#
CI workflow install step referenced non-existent extras (
[dev,test,spatial]and[docs,spatial]); narrowed to the actual[dev]/[docs]extras inpyproject.toml.
Release Process#
[ ] Run full test suite:
pytest tests/ --cov=quadsv[ ] Check documentation builds:
sphinx-build -b html docs/ docs/_build/[ ] Update version in
pyproject.toml[ ] Update this CHANGELOG
[ ] Create git tag:
git tag -a v0.1.0 -m "Release v0.1.0"[ ] Build package:
python -m build[ ] Upload to PyPI:
python -m twine upload dist/*
[0.1.0] - 2026-02-02#
Added#
Initial public release
Q-test framework for univariate spatial pattern detection
R-test framework for bivariate spatial co-expression
Core kernel methods: Gaussian, Matérn, CAR, Graph Laplacian, Moran’s I
Implicit mode for scalable large-N computation (N > 5000)
FFT acceleration for regular grid data (Visium HD)
PatternDetector for AnnData integration (genome-wide SVG detection)
PatternDetectorFFT for large-scale Visium HD analysis
Null approximation methods: CLT, Welch/Satterthwaite, Liu
Comprehensive test suite (unit + integration tests)
Tutorial test cases demonstrating all major workflows
Complete documentation with quickstart and theory sections
Support for Python 3.10, 3.11, 3.12
[0.1.1]#
Fixed#
Fix type hinting issues in
quadsv.kernelsmodule