mixmogam 2.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. mixmogam-2.0.1/CHANGELOG.md +1259 -0
  2. mixmogam-2.0.1/CITATION.cff +31 -0
  3. mixmogam-2.0.1/LICENSE +15 -0
  4. mixmogam-2.0.1/MANIFEST.in +11 -0
  5. mixmogam-2.0.1/PKG-INFO +231 -0
  6. mixmogam-2.0.1/README.md +184 -0
  7. mixmogam-2.0.1/benchmarks/_harness.py +365 -0
  8. mixmogam-2.0.1/benchmarks/efficiency_followup.py +171 -0
  9. mixmogam-2.0.1/benchmarks/efficiency_followup_plan.md +31 -0
  10. mixmogam-2.0.1/benchmarks/hratt_efficiency.py +402 -0
  11. mixmogam-2.0.1/benchmarks/ldak_kvik_comparison.py +690 -0
  12. mixmogam-2.0.1/docs/ROADMAP.md +86 -0
  13. mixmogam-2.0.1/docs/design.md +524 -0
  14. mixmogam-2.0.1/docs/formats.md +89 -0
  15. mixmogam-2.0.1/docs/quickstart.md +331 -0
  16. mixmogam-2.0.1/docs/review-2026-10-03.md +115 -0
  17. mixmogam-2.0.1/docs/review-2026-10-25.md +159 -0
  18. mixmogam-2.0.1/docs/review-2026-10-29.md +167 -0
  19. mixmogam-2.0.1/docs/tutorial.md +208 -0
  20. mixmogam-2.0.1/examples/mixed_model_gwas.py +41 -0
  21. mixmogam-2.0.1/examples/permutation_threshold.py +16 -0
  22. mixmogam-2.0.1/examples/stepwise_mlmm.py +18 -0
  23. mixmogam-2.0.1/mixmogam/__init__.py +44 -0
  24. mixmogam-2.0.1/mixmogam/__main__.py +6 -0
  25. mixmogam-2.0.1/mixmogam/_binary.py +194 -0
  26. mixmogam-2.0.1/mixmogam/_cg.py +110 -0
  27. mixmogam-2.0.1/mixmogam/_chol.py +291 -0
  28. mixmogam-2.0.1/mixmogam/_fast.py +190 -0
  29. mixmogam-2.0.1/mixmogam/_he.py +112 -0
  30. mixmogam-2.0.1/mixmogam/_loco.py +466 -0
  31. mixmogam-2.0.1/mixmogam/_packed.py +261 -0
  32. mixmogam-2.0.1/mixmogam/_slq.py +231 -0
  33. mixmogam-2.0.1/mixmogam/_spa.py +358 -0
  34. mixmogam-2.0.1/mixmogam/_standardize.py +720 -0
  35. mixmogam-2.0.1/mixmogam/_vb.py +1233 -0
  36. mixmogam-2.0.1/mixmogam/association.py +236 -0
  37. mixmogam-2.0.1/mixmogam/cli.py +227 -0
  38. mixmogam-2.0.1/mixmogam/genotypes.py +394 -0
  39. mixmogam-2.0.1/mixmogam/io/__init__.py +68 -0
  40. mixmogam-2.0.1/mixmogam/io/_hardcalls.py +59 -0
  41. mixmogam-2.0.1/mixmogam/io/bgen.py +169 -0
  42. mixmogam-2.0.1/mixmogam/io/covariates.py +248 -0
  43. mixmogam-2.0.1/mixmogam/io/eigenstrat.py +68 -0
  44. mixmogam-2.0.1/mixmogam/io/hdf5.py +113 -0
  45. mixmogam-2.0.1/mixmogam/io/pgen.py +148 -0
  46. mixmogam-2.0.1/mixmogam/io/phenofile.py +149 -0
  47. mixmogam-2.0.1/mixmogam/io/plink.py +195 -0
  48. mixmogam-2.0.1/mixmogam/io/regmap.py +157 -0
  49. mixmogam-2.0.1/mixmogam/io/vcf.py +140 -0
  50. mixmogam-2.0.1/mixmogam/kinship.py +406 -0
  51. mixmogam-2.0.1/mixmogam/ldscore.py +110 -0
  52. mixmogam-2.0.1/mixmogam/lmm.py +1149 -0
  53. mixmogam-2.0.1/mixmogam/pca.py +157 -0
  54. mixmogam-2.0.1/mixmogam/phenotypes.py +224 -0
  55. mixmogam-2.0.1/mixmogam/plotting.py +110 -0
  56. mixmogam-2.0.1/mixmogam/results.py +440 -0
  57. mixmogam-2.0.1/mixmogam/scan.py +398 -0
  58. mixmogam-2.0.1/mixmogam/simulate.py +105 -0
  59. mixmogam-2.0.1/mixmogam/stage1.py +356 -0
  60. mixmogam-2.0.1/mixmogam/stepwise.py +259 -0
  61. mixmogam-2.0.1/mixmogam/twostep.py +1725 -0
  62. mixmogam-2.0.1/mixmogam.egg-info/SOURCES.txt +119 -0
  63. mixmogam-2.0.1/pyproject.toml +75 -0
  64. mixmogam-2.0.1/setup.cfg +4 -0
  65. mixmogam-2.0.1/tests/__init__.py +0 -0
  66. mixmogam-2.0.1/tests/_reference.py +132 -0
  67. mixmogam-2.0.1/tests/_retrospective.py +25 -0
  68. mixmogam-2.0.1/tests/conftest.py +23 -0
  69. mixmogam-2.0.1/tests/test_association.py +50 -0
  70. mixmogam-2.0.1/tests/test_calibration.py +57 -0
  71. mixmogam-2.0.1/tests/test_cholesky_reml.py +135 -0
  72. mixmogam-2.0.1/tests/test_cli.py +266 -0
  73. mixmogam-2.0.1/tests/test_covariates.py +158 -0
  74. mixmogam-2.0.1/tests/test_data.py +238 -0
  75. mixmogam-2.0.1/tests/test_decode_and_whitening.py +98 -0
  76. mixmogam-2.0.1/tests/test_efficiency_regressions.py +247 -0
  77. mixmogam-2.0.1/tests/test_format_dispatch.py +94 -0
  78. mixmogam-2.0.1/tests/test_genotype_input_memory.py +182 -0
  79. mixmogam-2.0.1/tests/test_hratt_binary.py +468 -0
  80. mixmogam-2.0.1/tests/test_hratt_crossfit.py +333 -0
  81. mixmogam-2.0.1/tests/test_hratt_cv_tol.py +114 -0
  82. mixmogam-2.0.1/tests/test_hratt_he.py +119 -0
  83. mixmogam-2.0.1/tests/test_hratt_parallel.py +58 -0
  84. mixmogam-2.0.1/tests/test_hratt_preconditioner_reuse.py +138 -0
  85. mixmogam-2.0.1/tests/test_hratt_weights.py +505 -0
  86. mixmogam-2.0.1/tests/test_integration_at.py +98 -0
  87. mixmogam-2.0.1/tests/test_io.py +193 -0
  88. mixmogam-2.0.1/tests/test_io_bgen.py +191 -0
  89. mixmogam-2.0.1/tests/test_io_pgen.py +147 -0
  90. mixmogam-2.0.1/tests/test_io_vcf.py +129 -0
  91. mixmogam-2.0.1/tests/test_ldak_kvik_comparison.py +121 -0
  92. mixmogam-2.0.1/tests/test_ldak_kvik_preparation.py +148 -0
  93. mixmogam-2.0.1/tests/test_lmm.py +173 -0
  94. mixmogam-2.0.1/tests/test_lmm_variance_only.py +155 -0
  95. mixmogam-2.0.1/tests/test_loco_parallel.py +355 -0
  96. mixmogam-2.0.1/tests/test_loco_prepared_moments.py +132 -0
  97. mixmogam-2.0.1/tests/test_loco_workspace.py +117 -0
  98. mixmogam-2.0.1/tests/test_operator.py +158 -0
  99. mixmogam-2.0.1/tests/test_packed.py +201 -0
  100. mixmogam-2.0.1/tests/test_pca.py +137 -0
  101. mixmogam-2.0.1/tests/test_phenofile.py +129 -0
  102. mixmogam-2.0.1/tests/test_predict_rows.py +144 -0
  103. mixmogam-2.0.1/tests/test_projected_he.py +151 -0
  104. mixmogam-2.0.1/tests/test_qc_memory.py +62 -0
  105. mixmogam-2.0.1/tests/test_results.py +198 -0
  106. mixmogam-2.0.1/tests/test_results_text.py +76 -0
  107. mixmogam-2.0.1/tests/test_review_regressions.py +383 -0
  108. mixmogam-2.0.1/tests/test_scale_study.py +178 -0
  109. mixmogam-2.0.1/tests/test_scan_extras.py +287 -0
  110. mixmogam-2.0.1/tests/test_slq_adaptive.py +194 -0
  111. mixmogam-2.0.1/tests/test_slq_defaults.py +58 -0
  112. mixmogam-2.0.1/tests/test_spa.py +382 -0
  113. mixmogam-2.0.1/tests/test_stage1.py +287 -0
  114. mixmogam-2.0.1/tests/test_stepwise.py +99 -0
  115. mixmogam-2.0.1/tests/test_twostep.py +432 -0
  116. mixmogam-2.0.1/tests/test_twostep_variance_only.py +88 -0
  117. mixmogam-2.0.1/tests/test_twostep_workspace.py +237 -0
  118. mixmogam-2.0.1/tests/test_vb_column_layout.py +268 -0
  119. mixmogam-2.0.1/tests/test_vb_covariate_corrections.py +258 -0
  120. mixmogam-2.0.1/tests/test_vb_gram_preparation.py +324 -0
  121. mixmogam-2.0.1/tests/test_vb_parallel.py +584 -0
  122. mixmogam-2.0.1/tests/test_vb_workspace.py +475 -0
@@ -0,0 +1,1259 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [2.0.1] - 2026-10-29
11
+
12
+ The first release on PyPI. The package is unchanged from 2.0.0 apart from the
13
+ version string.
14
+
15
+ ### Changed
16
+
17
+ - README links are absolute GitHub URLs pinned to the `v2.0.1` tag, so they
18
+ resolve on the PyPI project page.
19
+
20
+ ## [2.0.0] - 2026-10-29
21
+
22
+ The first stable release of the 2.0 series. Its statistical content is
23
+ identical to 2.0.0.dev9: this release changes the version string, the
24
+ packaging metadata and the provenance documentation of the bundled real-data
25
+ panel, removes unused API, merges duplicated code and tests, and rewrites the
26
+ documentation and report more plainly. No estimator, default or numerical
27
+ result changed, so the measurements recorded for dev8 and dev9 still describe
28
+ this release.
29
+
30
+ ### Added
31
+
32
+ - `CITATION.cff`, and a citation and references section in the README, so the
33
+ software and the methods it implements can be cited without reading the
34
+ manuscript. Its identities come from the verified bibliography in
35
+ `report/references.json`.
36
+ - `at_data/README.md` records the provenance of the bundled *A. thaliana*
37
+ panel: the RegMap genotypes and Atwell phenotypes (Atwell et al. 2010;
38
+ Horton et al. 2012) and the TAIR10 gene annotations. Those data are third
39
+ party and are not covered by this repository's MIT licence, which applies to
40
+ the software only.
41
+ - Distribution metadata for a public release: PyPI classifiers, keywords, and
42
+ `Documentation` and `Changelog` project URLs.
43
+
44
+ ### Changed
45
+
46
+ - `LICENSE` states the copyright years as 2013-2026 instead of 2013 alone; the
47
+ grant text is unchanged.
48
+ - Distribution metadata names `mixmogam contributors` as the author, matching
49
+ the manuscript's author block. The copyright holder remains named in
50
+ `LICENSE`.
51
+ - The source distribution no longer carries the stale editable-install
52
+ metadata in `mixmogam.egg-info/`, six files whose `PKG-INFO` still recorded
53
+ the previous version, and now includes `examples/` and `CITATION.cff`.
54
+ Setuptools still writes a freshly generated `SOURCES.txt` there during every
55
+ sdist build; that one is current, not stale.
56
+ - The README's install line lists every optional extra, and the README and
57
+ research report describe the released version rather than a development one.
58
+ - The saddlepoint tail uses one root solver and one pair of row loops for
59
+ both genotype models; the binomial copy is gone. P-values are bit-identical.
60
+ - `benchmarks/hratt_efficiency.py` and `benchmarks/ldak_kvik_comparison.py`
61
+ import the shared helpers from `benchmarks/_harness.py` instead of carrying
62
+ copies.
63
+ - Tests share one `numpy_fallback` fixture (`tests/conftest.py`) in place of
64
+ seven copies of the module-loading code. The duplicated power-guard tests
65
+ became one identity check, and the second seed of the strong-structure
66
+ refit test runs only in the slow leg.
67
+ - Documentation and the report are shorter: limitations are stated once, and
68
+ version history now lives here instead of in the README and abstract.
69
+
70
+ ### Removed
71
+
72
+ - `at_data/call_method_75_info.tsv` is no longer tracked. It held 1,307 rows of
73
+ accession-collection metadata, including collectors' given and family names
74
+ with their site, region and country, and nothing in the package, its tests,
75
+ benchmarks or documentation read it. It remains in the repository's history;
76
+ rewriting history was out of scope for this release. The file stays on disk
77
+ for local use and is now ignored.
78
+ - `Genotypes.load_vcf`, `load_pgen`, `load_tped` and `load_hdf5`. Nothing
79
+ called or documented them, and `Genotypes.load` reads all four formats.
80
+ `load_plink`, `load_bgen` and `load_eigenstrat` stay.
81
+ - `Phenotypes.incidence_matrix` and `GwasResult.neg_log10_p`, which had no
82
+ caller or test.
83
+ - The ignored `dtype` argument of `scan_gxe` and `seed` argument of
84
+ `fit_two_kinships`.
85
+
86
+ ### Fixed
87
+
88
+ - `benchmarks/hratt_efficiency.py` passed `--cache-bytes` as `cache_bytes`,
89
+ which the package renamed `gram_cache_bytes` in 02a8c3a7. The worker now
90
+ uses whichever name the benchmarked source accepts. Its paired mode also
91
+ forwards `--loco-folds` to the workers.
92
+ - `benchmarks/kvik_scale.py` parsed `--max-load` but never passed it to its
93
+ fit workers; it now gates both the reference and the mixmogam runs.
94
+
95
+ ## [2.0.0.dev9] - 2026-10-29
96
+
97
+ ### Changed
98
+
99
+ - Update the documentation and research report with the 29 October package
100
+ benchmarks, including the 100,000-sample comparison, paired cache studies,
101
+ convergence diagnostics and the limits of each measurement. A compact,
102
+ checksummed evidence bundle accompanies the report. The benchmarks used
103
+ 2.0.0.dev8 at commit `d550c412`; this release changes documentation only.
104
+
105
+ ## [2.0.0.dev8] - 2026-10-29
106
+
107
+ ### Added
108
+
109
+ - `principal_components(..., solver="randomized")` provides a top-k
110
+ alternative to the exact full SVD (the unchanged default), with matching
111
+ CLI `pca --solver` and `gwas --pca-solver` options. Randomized PCs use
112
+ the same thinning, signs, metadata and working-memory guard.
113
+ - BOLT and HRATT accept `gram_cache_bytes`; a partial VB Gram cache now
114
+ warns instead of silently recomputing the remainder every sweep.
115
+ `ld_scores(..., max_memory_gib=...)` bounds dense LD-window workspaces.
116
+ - `Genotypes.variant_major()` makes the one-time layout conversion used by
117
+ analysis entry points for sample-major NumPy arrays.
118
+ - HRATT's `cv_tol` stops the hold-out fit of the elastic-net grid at its
119
+ own tolerance, and `extra["cv_converged"]` follows it. The default
120
+ (None: `vb_tol`) changes nothing (bit-identical p-values in all 16
121
+ checked configurations). `cv_tol=1e-4` is a speed option that changes the
122
+ estimator: about half the hold-out sweeps, and whole one-thread runs
123
+ 1.18-1.23x faster on 5,000 x 40,000, 1.04-1.10x on 20,000 x 10,000 and
124
+ 1.45x on the strong-structure refit panel. On 5,000 x 40,000 it changed
125
+ the chosen grid model in 4 of 32 runs, each time away from the converged
126
+ choice (log10 p moved by up to 1.45, at most one call at 0.05/m), and 37-48
127
+ calls in 2 of 3 strong-structure runs whose hold-out fit had not
128
+ converged; on 20,000 x 10,000 it changed no choice in 32 runs.
129
+
130
+ ### Changed
131
+
132
+ - HRATT cross-fits can reuse the Gram budget released by the hold-out fit;
133
+ strong-structure refits predict only the rows held out by each fold.
134
+ Automatic spectral width selection evaluates rejected candidates on the
135
+ calibration variants, with whole-genome checks near the decision boundary
136
+ or under ill-conditioning. Conditional-binomial saddlepoint tails use a
137
+ specialized CGF, falling back to the original calculation near zero and
138
+ support boundaries. These alternative arithmetic orders can change the
139
+ last bits of scores and tail probabilities. Packed sample selection
140
+ gathers two-bit codes directly with Numba, and alignment avoids copying
141
+ its newly repacked calls a second time.
142
+ - Parallel VB fits (`n_threads` above one, several model columns) let
143
+ Numba's OpenMP workers spin for 1 ms between launches instead of
144
+ sleeping at once (LLVM's default on hybrid CPUs such as Apple silicon),
145
+ which cost 26-35 us per launch at four threads, twice per 128-SNP
146
+ sub-block; runtimes that already spin longer are left alone. With four
147
+ threads whole HRATT runs are 1.12-1.24x faster on 5,000 x 40,000 (sweeps
148
+ 1.3x), 1.55x on 1,000 x 10,000 (strong-structure refit) and 1.01-1.03x
149
+ on 20,000 x 10,000, for 1.5-2x the process CPU time; two threads gain
150
+ 1.06x, one thread is unchanged. The fit's own
151
+ thread restores its blocktime on exit; nothing changes under Numba's
152
+ other threading layers or when `KMP_BLOCKTIME` or `OMP_WAIT_POLICY` is
153
+ set. Bit-identical (p-values in all 16 checked configurations at one and
154
+ four threads).
155
+ - Genotype preparation (each variant's call counts, mean, SD, covariate
156
+ coefficients and projected norm) counts calls in one contiguous pass
157
+ (integer sums of int8 calls, per-byte code counts of two-bit calls) and
158
+ then centers eight variants at a time into 256-sample float64 rows whose
159
+ covariate sums stay in registers, instead of reading 64 variants a
160
+ sample at a time, twice. It takes 0.64 ns per genotype instead of 1.81 on
161
+ a 5,000 x 40,000 panel (0.82 instead of 2.50 from two-bit calls, 1.39
162
+ instead of 2.38 with five covariates; one thread), and whole HRATT runs
163
+ take 4-10% less time on 5,000 x 40,000 and 20,000 x 10,000. Each
164
+ variant still sums its samples in order, so results are bit-identical
165
+ (p-values in all 16 checked configurations, and with four threads).
166
+ Sample-major int8 keeps the sample-at-a-time kernel.
167
+ - VB fits correct each sub-block's products and updates for the covariates
168
+ in two Numba loops over all model columns, instead of NumPy products per
169
+ held-out fold, wherever those calls cost more than the arithmetic
170
+ (covariates x columns at most 128 per held-out fold: always in HRATT's
171
+ cross-fit, up to 12 covariates in its hold-out fit and 5 in the
172
+ strong-structure refit over 22 chromosomes). HRATT's cross-fit takes 0.75 s instead of
173
+ 0.93 s on a 5,000 x 40,000 panel, and the refit 2.7 s instead of 3.5 s
174
+ on a 1,000-sample panel with strong structure; whole runs at one thread
175
+ are 4-7% faster on 5,000 x 40,000 (HE and REML, with or without five
176
+ covariates), 17% on the refit and unchanged on 20,000 x 10,000. Results
177
+ move at rounding level: bit-identical p-values with only an intercept
178
+ and no sampling weights (12 of 16 checked configurations, binary traits
179
+ included), up to 1.2e-13 in log10 p with weights or covariates there,
180
+ and up to 9e-7 on the strong-structure panel with principal components or
181
+ covariates, where reassociating one float64 product in the NumPy
182
+ corrections moves p as much. No call at 0.05/m changed. Without Numba
183
+ the NumPy products remain.
184
+ - VB fits of 2 to 32 model columns (HRATT's hold-out, cross-fitted and LOCO
185
+ fits; BOLT-LMM's LOCO fit) keep the residual and its updates
186
+ column-major, (columns, samples), so the genotype GEMMs and the residual
187
+ pass stream each column's samples. HRATT's two fits take 1.48 s instead
188
+ of 1.95 s on a 50,000 x 4,000 panel (1.55x on 50,000 x 4,096; one
189
+ thread, 1.13x with four), and whole HRATT runs are 2.4-7.2% faster on
190
+ 5,000 x 40,000 and 20,000 x 10,000. Wider fits (BOLT-LMM's 90-column
191
+ cross-validation, HRATT's strong-structure refit) keep the sample-major
192
+ layout, which is faster there. Results are bit-identical with Accelerate
193
+ (p-values in all 16 checked configurations, and with four threads, now
194
+ equal to one thread at 50,000 samples too). Other BLAS builds may differ
195
+ at float32 GEMM rounding level, and so may Accelerate with a few hundred
196
+ samples, or over 16 columns in float64 storage or at 120,000 samples.
197
+ - HRATT's HE alpha selection computes the backward products of all
198
+ candidate alphas as one GEMM per sample tile instead of one per alpha.
199
+ The step takes 0.23 s instead of 0.29 s on a 5,000 x 40,000 panel and
200
+ 0.33 s instead of 0.40 s on 20,000 x 10,000 (one thread); whole HE and
201
+ REML fits are 0.6-1.1% faster. Results are bit-identical with Accelerate
202
+ (p-values in all 16 checked configurations); other BLAS builds may differ
203
+ at rounding level. No sample tile is a single row, whose GEMV product
204
+ would round differently.
205
+ - Stochastic Lanczos REML stops once the heritability has converged instead
206
+ of always running 24 steps (new `slq_tol`, default 1e-6, on `LMM.fit`,
207
+ `hratt`, `bolt_inf` and `bolt`). After step j >= 8 it refits h2 from the
208
+ j-step rules (exact truncations) and stops when h2 changed by less than
209
+ `slq_tol` at each of the last two steps and the last three fits each found
210
+ a bracketed root; 24 steps stay the cap, and ML runs all of them. Without
211
+ the root guard, panels with more samples than variants stopped while short
212
+ rules held h2 on the grid end (0.99995 for a true 0.5). HRATT REML takes
213
+ 5.9 s instead of 7.7 s on a 5,000 x 40,000 panel (9 steps) and 5.5 s
214
+ instead of 7.2 s on 20,000 x 10,000 (14 steps); `alpha_method="reml"`
215
+ 11.8 s instead of 21.2 s and BOLT-LMM-inf 9.5 s instead of 11.2 s (one
216
+ thread). This changes a numerical default: p-values move by up to 8e-6 in
217
+ log10 (3.7e-5 on the strong-structure refit) and h2 by up to 3e-6, against
218
+ a Monte Carlo SD of 2-6e-3, and no call at 0.05/m changed in 16 checked
219
+ configurations. `slq_tol=0` restores the fixed 24 steps bit for bit.
220
+ - HRATT's cross-validation hold-out error predicts only the held-out tenth
221
+ of the samples, from their calls alone (`VBEngine.predict(beta,
222
+ rows=...)`: compiled gathers for int8 in either order and two-bit calls,
223
+ NumPy otherwise), instead of predicting every sample. The step takes
224
+ 19 ms instead of 82 ms on a 5,000 x 40,000 panel and 18 ms instead of
225
+ 87 ms on 20,000 x 10,000 (one thread; 6.5-9x with two-bit calls or
226
+ sampling weights); whole HE and REML fits are 1.3-2.1% faster. Results
227
+ are bit-identical with Accelerate (p-values in all 16 checked
228
+ configurations); other BLAS builds may differ at rounding level.
229
+ - HRATT prepares the Gram matrices of its hold-out and cross-fitted fits in
230
+ one pass: each sub-block is decoded once and gathered in cross-fit fold
231
+ order by a compiled kernel into a reused float64 buffer, and the fold
232
+ products, their sum and one product over the hold-out give both caches
233
+ (before: two decodes, two NumPy gathers and an extra all-sample product).
234
+ VB Grams are stored as packed upper triangles, the only part the sweeps
235
+ read, and HRATT's two caches omit the all-sample Gram, which neither fit
236
+ reads. `gram_cache_bytes` is now one budget shared by both caches,
237
+ filled with the hold-out Grams first. Both are held during the hold-out
238
+ fit: at 5,000 x 400,000 about 0.65 GB, 0.22 GB more than that fit held,
239
+ but below the 1 GB the cross-fit's budget-bound cache took. 1 GB holds
240
+ about 620,000 variants instead of 320,000, for BOLT too. Gram preparation
241
+ takes 0.38 s instead of 0.79 s on a 5,000 x 40,000 panel and 0.37 s
242
+ instead of 0.86 s on 20,000 x 10,000; whole HE and REML fits take 8-16%
243
+ less time (one thread), 18% with `gram_cache_bytes=1e8`, which leaves a
244
+ fifth of the cross-fit uncached as the default did at 400,000 variants,
245
+ and peak memory is up to 89 MB lower. Rounding-level: the cross-fitted
246
+ Grams are unchanged bit for bit, but the hold-out fit's training Gram
247
+ sums its products in another order, which moves the hold-out errors by
248
+ up to 2e-8 relative (with covariates; 1e-15 without) and could flip a
249
+ near-tie in the choice of prior. p-values were bit-identical in all 16
250
+ checked configurations, with four threads, with binding budgets and for
251
+ BOLT. Grams rebuilt outside the budget use the cached arithmetic, so the
252
+ budget still changes no result.
253
+
254
+ ### Fixed
255
+
256
+ - Packed calls have compiled general decoding and packing, direct byte-table
257
+ QC counts, and blockwise PLINK read/write paths. QC no longer decodes a
258
+ packed block merely to count called genotypes and dosages; arbitrary
259
+ padding bits remain ignored.
260
+ - `realized_relationship` uses one float64 widening and symmetric rank-k
261
+ products. `loco_kinships` retains only the full and current-chromosome
262
+ intermediates rather than every chromosome at once, and
263
+ `GenotypeKinship` reuses four standardized call values per variant after
264
+ its float-panel cache is exhausted.
265
+ - Dense `LMM` construction validates and symmetrizes K in bounded blocks,
266
+ avoiding about three full-matrix copies. Two-kinship profiles reuse their
267
+ validated design, use eigenvalues alone for the PSD gate, and avoid
268
+ repeated mixture temporaries.
269
+ - LD scores chunk windows to a memory budget. Derived genotype containers
270
+ no longer revalidate inherited calls or synthesize new default IDs;
271
+ analysis makes one variant-major copy when repeated decoding would
272
+ otherwise stride through sample-major input.
273
+ - Two-step methods warn when Numba is unavailable rather than silently
274
+ taking their slow Python fallbacks. BOLT's fold mask is boolean, and
275
+ batched Lanczos avoids pre-zeroing fully assigned work arrays.
276
+
277
+ No statistical default or convergence threshold was changed in that review (the
278
+ later `slq_tol` entry above is the one exception). BOLT's
279
+ model-selection cross-validation, float64 full Lanczos reorthogonalization,
280
+ the SPA threshold, and the two-kinship mixture profile remain in place.
281
+
282
+ ### Benchmarks
283
+
284
+ - The 400,000-variant, covariate and four-BLAS-thread LDAK-KVIK benchmarks were
285
+ rerun after the speed branch (26
286
+ October, master `6a63f215`) on the same panels, seeds, traits and
287
+ threads. Statistics reproduce everywhere (lambda_GC to four decimals,
288
+ the same detected causal variants, agreement rows moving in the
289
+ eighth decimal of Spearman at 400,000 variants and by up to 4e-5 in log10 p on
290
+ the 1,000 x 10,000 structured panel); on the unstructured 5,000 x 400,000 panel
291
+ HRATT-HE goes 34 -> 29 s (22 s under the null, now faster than the
292
+ reference program) and BOLT-LMM-inf 51 -> 35 s at one thread, with
293
+ HRATT-HE's peak RSS down about 0.5 GiB (the packed Gram cache) and
294
+ BOLT-LMM-inf's about 0.2 GiB; at four BLAS threads all three programs
295
+ are level. Archives `20261026T143800Z-kvik-scale`,
296
+ `20261026T144700Z-kvik-scale-blas4`,
297
+ `20261026T145400Z-kvik-covariates-small`,
298
+ `20261026T150200Z-kvik-covariates-large`,
299
+ `20261026T152200Z-kvik-covariates-small` and
300
+ `20261026T152400Z-kvik-covariates-large`, each with its before/after
301
+ table and the reference program's own day-to-day drift recorded as the
302
+ noise floor for cross-day ratios.
303
+ - A sample-size ladder (fixed m = 20,000, n = 1,000 to 50,000, one
304
+ thread, archive `20261026T161500Z-kvik-sample-scaling`) measures how
305
+ each method scales in samples: LDAK-KVIK and both HRATT paths are
306
+ linear (local exponents 0.80-0.97 over n = 20,000 to 50,000, their
307
+ sweep counts bounded, falling from 10-13 to 5), BOLT-LMM-inf is superlinear (1.21, its
308
+ conjugate-gradient count growing from 10 to 22), and `exact` is cubic
309
+ and stops at n = 2,500. HRATT-HE is the fastest method from
310
+ n = 10,000 up. Peak memory is linear in n for every streaming method.
311
+ - `benchmarks/DESIGN.md` maps the measured-evidence suite: the four
312
+ layers (invariants, resources, evidence, external comparison), what
313
+ each layer can support, the gaps the 19 and 25 October efficiency
314
+ reviews exposed, and the rules every measurement now follows (a
315
+ fresh process, operating-system resource counters, a stated
316
+ configuration, a protocol written before the first measurement).
317
+ - `benchmarks/scale_study.py` runs the study that closes those gaps,
318
+ under a protocol written before the first measurement and hashed into
319
+ the archive (`benchmarks/scale_study_plan.md`): fits at up to 600,000
320
+ variants with peak resident set size from the operating system, the
321
+ point where the default variational Gram-cache budget runs out,
322
+ invariance of the reported statistics under the memory budget, the
323
+ worker count, the storage format and the absence of the acceleration
324
+ package, type-I error, power and effect attenuation with binomial
325
+ intervals, and the cost of the pure NumPy fallbacks.
326
+ - `benchmarks/kvik_scale.py` runs the official LDAK-KVIK and mixmogam on
327
+ one identical 5,000 x 400,000 panel (the scale study's panel, exported
328
+ to PLINK and verified independently), under its own protocol
329
+ (`benchmarks/kvik_scale_plan.md`). The official program was as fast as
330
+ mixmogam's HRATT-HE (24 s against 34 s) and faster than BOLT-LMM-inf
331
+ (51 s) with a third of the memory; the two programs' p-values
332
+ correlated at 0.9991 under the null (0.998 on the mixed trait) and they detected the same 8 of 10
333
+ causal variants. The same driver also runs a structured, confounded
334
+ panel of four populations without covariates and with two and three
335
+ ancestry principal components, at 10,000 variants (the scale study's
336
+ arm B panel and trait) and at 400,000, and records what each program
337
+ reports about its own fit. With three components every method
338
+ converges and they agree, the official program included once every
339
+ predictor is adjusted for the covariates (its default adjusts only the
340
+ most significant predictors and is conservative). Without covariates,
341
+ or with two components that leave most of the environment in the trait
342
+ (at 10,000 variants they explain 42% of it), the official program
343
+ reports its own fit as failed and its statistics are inflated, and
344
+ mixmogam's variational methods do not converge; the exact path and
345
+ BOLT-LMM-inf do.
346
+ - `benchmarks/scaling_ladder.py` decomposes what a panel costs: the
347
+ archived HAPNEST panel at 50,000 samples, the same panel subsampled to
348
+ 5,000 samples, and the 400,000-variant panel. Ten times fewer samples
349
+ make the reference program 7.1 times faster and HRATT-HE 7.0 times,
350
+ with the reported iteration counts falling as well, which is why a
351
+ panel with twenty times the variants can cost the same. The run also
352
+ shows that the October archives were measured on a slower host with
353
+ older code: the same executable on the same archived panel takes
354
+ 20.1 s today against the archived 33.5 s.
355
+ - `benchmarks/_harness.py` holds the shared measurement infrastructure
356
+ (power guard, thread environment, source snapshot, environment
357
+ manifest, fresh-process measurement, p-value comparison) for the new
358
+ drivers, and `benchmarks/index.py` indexes the archives.
359
+
360
+ ## [2.0.0.dev7] - 2026-10-25
361
+
362
+ ### Added
363
+
364
+ - Standard genotype input through `Genotypes.load` and CLI `--geno-format`:
365
+ VCF/gzip VCF, BCF, PLINK 2 PGEN, and explicit probability-threshold BGEN
366
+ hard calling, alongside BED, TPED, EIGENSTRAT and HDF5. Calls retain their
367
+ counted allele and sample order; unsupported ploidy/multiallelic records
368
+ fail explicitly. Optional binary readers are in the `formats` and `bgen` extras.
369
+ - PLINK phenotype headers, whitespace tables and gzip, explicit headerless
370
+ ID columns, PLINK2 covariate headers, CSV/TSV/gzip result files, and CLI
371
+ `--binary-coding 1/2`. Fractional-dosage inference remains unsupported.
372
+ - CLI `--packed` and `--mmap` expose compact genotype storage; BED mapping
373
+ is recommended for repeated hard-call analyses. Format contracts and the
374
+ measured 25 October review are in `docs/formats.md` and
375
+ `docs/review-2026-10-25.md`.
376
+
377
+ ### Fixed
378
+
379
+ - Genotype QC bounds temporary blocks by call count and avoids a masked
380
+ genotype copy. A packed slice no longer builds a genome-wide index.
381
+ - CLI complete-case filtering now subsets supplied covariates before
382
+ appending PCs. HDF5 validates hard calls before narrowing, streams packed
383
+ exports, and handles empty dimensions. EIGENSTRAT streams genotype rows
384
+ and rejects malformed calls and inconsistent variant counts.
385
+ - Explicit wide TSV input preserves empty cells even under an `IID`
386
+ header; PLINK covariates recognize -9 as missing, with an explicit
387
+ numeric override; PSAM preamble comments are not parsed as samples.
388
+ Uncompressed BGEN is rejected before a known upstream native crash.
389
+ - Permutation scans evaluate one F tail per permutation after finding the
390
+ maximum statistic. The optional LDpred3 stage-1 LD builder reuses its
391
+ owned float buffer; invalid block sizes fail instead of looping forever.
392
+ Paired numerical results were identical; timings are in the review.
393
+ - Normalized `GenotypeKinship` collects scaling and diagonal moments in
394
+ its first traversal, eliminating two standardized passes and retaining
395
+ only one extra sample-length vector. Ordinary HRATT cross-fitting skips
396
+ a full prediction/residual that its LOCO scoring does not use; CV,
397
+ chromosome-refitted and in-sample fits retain their predictions.
398
+ - `simulate_traits(..., n_causal=0)` returned an all-NaN phenotype: the QTL
399
+ scaling divided by the zero standard deviation of a trait with no QTLs.
400
+ A trait without QTLs is now purely infinitesimal and keeps the full `h2`
401
+ rather than `h2/2` (review 2026-10-24, L3).
402
+ - `simulate_genotypes` clipped allele frequencies only in its structured
403
+ branch, so `maf` within about 0.05 of 0 or 1 raised numpy's
404
+ `ValueError: p < 0, p > 1 or p contains NaNs` from `rng.binomial`. Both
405
+ branches now clip to [0.01, 0.99] (L4).
406
+ - `gwas(..., stage1_kwargs=...)` with a method other than `"hratt"` reported
407
+ `bolt() got an unexpected keyword argument 'stage1_kwargs'` instead of the
408
+ "options of method='hratt' only" message its sibling options give (L5).
409
+
410
+ ### Testing
411
+
412
+ - `stage1="ldpred3-lassosum2"` is documented, offered by `mixmogam gwas
413
+ --stage1` and described in the changelog, but had no test at all: it was the
414
+ one branch of the LDpred3 stage-1 seam that no test had ever executed. Two
415
+ tests now cover it (determinism, finiteness, `stage1_kwargs` plumbing), and
416
+ CI gained an `external` job that installs ldpred3 and runs `-m external`,
417
+ where the whole seam's tests live (L1, L2).
418
+
419
+ ### Computational efficiency
420
+
421
+ Efficiency review of 19 October (evidence: `benchmarks/results/20261019-efficiency-review`
422
+ and `20261020-efficiency-packed-fix`). Unless noted, statistics are identical to
423
+ 2.0.0.dev6 on the paired benchmark panels; each noted exception is measured below.
424
+
425
+ - Decoding variant-major int8 calls selects each standardized value by
426
+ comparisons instead of a table gather: 0.16 instead of 0.26 ns per
427
+ genotype on one M2 Pro thread, in every streamed pass of the two-step
428
+ methods.
429
+ - `Genotypes.filter_samples` and `align_samples` kept the calls but made
430
+ them sample-major, which every later pass decoded about six times slower
431
+ (1 ns per genotype); they now keep variant-major calls variant-major,
432
+ copying in bounded variant blocks. The HDF5 and EIGENSTRAT readers
433
+ return variant-major calls, as the PLINK readers did.
434
+ - The variational coordinate kernel sweeps one model column at a time
435
+ with unsigned loop bounds, so the in-block Gram corrections vectorize:
436
+ 1.2-1.7 times faster sweeps, serial and parallel kernels identical.
437
+ - The variational residual update tests fold labels instead of reading an
438
+ (n, columns) float64 mask (BOLT-LMM's cross-validation: 90 columns).
439
+ - Gram matrices of folds that partition the samples (HRATT's cross-fit,
440
+ BOLT-LMM's cross-validation) come from fold-sorted samples, the
441
+ all-sample Gram as their sum: no all-sample GEMM and no mask scans. The
442
+ summation order changes the float32 storage rounding of the Grams, which
443
+ moves p-values by up to 1.3e-5 in log10 for BOLT-LMM's cross-validated
444
+ fit and 2e-6 for HRATT-HE and HRATT-REML (verified by swapping only this
445
+ function back: the other changes are then identical to 2.0.0.dev6).
446
+ - Predictions, out-of-fold LOCO scores and the retrospective and binary
447
+ score statistics decode their float64 rows directly (with a sample
448
+ range) instead of widening float32 copies; same values and products.
449
+ - BOLT-LMM's LD scores compute each pair once and credit both variants:
450
+ about half the window products. No p-value changed on the benchmark
451
+ panels (checked with the Gram change reverted, as above).
452
+ - The exact path's REML search starts Brent's method from its three known
453
+ bracket values (the first step is their parabola): 13-15% fewer Cholesky
454
+ factorizations on the 3,000 x 44,000 panel at the same 1e-6 tolerance
455
+ in log delta. Against 2.0.0.dev6 the exact scan's log10 p moved by at
456
+ most 3.4e-6 (this and the tolerance change below, not measured apart).
457
+ - The exact path resolves log delta to 1e-5 instead of 1e-6 (EMMA's root
458
+ finder: 1e-6 in delta). Parabolic steps land well inside it: on the same
459
+ panel 13% fewer factorizations again (229 instead of 264), variance
460
+ ratios within 2e-6 in log of those at 1e-6 and log10 p within 2.6e-6.
461
+ A 1e-4 tolerance would have saved only 4% more.
462
+ - HRATT's REML builds its 128-wide deflation basis only under strong
463
+ structure, where the ridge solves reuse it. With ancestry covariates it
464
+ kept no eigenvalue on the benchmark panels, so results are identical
465
+ without it (the trace probes have their own stream); it cost 6 passes
466
+ over 140 columns, about a seventh of a REML fit. With an
467
+ inversion-like LD block that the structure test did not flag, h2
468
+ without deflation was as close to exact REML (0.002-0.006 at n = 2,000)
469
+ as with it.
470
+
471
+ ### Fixed
472
+
473
+ - Binary and sampling-weighted result exports omit the headcount `n`,
474
+ which downstream sumstats readers may treat as effective sample size.
475
+ The API retains the headcount; explicit `n_eff` is now separate, accepts
476
+ nonintegral values, and survives CSV loading and variant selection.
477
+ - PCA checks a conservative working-memory estimate before frequency
478
+ scanning or allocating the float64 panel (default budget: 4 GiB,
479
+ configurable through the API and both CLI commands). Its SVD and
480
+ numerical results are unchanged within the budget.
481
+ - `read_regmap` streams its file in 4,096-row chunks. The bundled 250K
482
+ panel (1,307 x 214,051) peaked at a 21 GB footprint when every line,
483
+ cell and float64 value was held at once; it now reads in 37 s instead
484
+ of 60 s, with calls identical to the previous reader and to a per-line
485
+ decode. A padded missing cell (`, NA`) parses as missing instead of
486
+ failing, and non-calls are refused before the int8 narrowing.
487
+ - Benchmark power checks read the AC profile from `pmset -g custom`;
488
+ `pmset -g` can omit profile settings. Inactive battery settings do not
489
+ reject an AC-powered run, and missing AC settings fail explicitly.
490
+ `sim_study.py`, `run_benchmarks.py`, `structure_calibration.py`,
491
+ `ldak_kvik_reference.py` and `review_memory.py` use the same check;
492
+ the first four refused only battery power, not Low Power Mode.
493
+ - The report abstract retains the follow-up study's failed effect-bias
494
+ criterion (weighted S3: -5.4%, MCSE 2.1%) and weighted-runtime criterion
495
+ (1.55 times HRATT-HE versus a target of 1.3).
496
+
497
+ ### Added
498
+
499
+ - `hratt(stage1=...)` / CLI `--stage1`: the step-(1e) whole-genome predictor
500
+ is a seam (`mixmogam.stage1`) with an optional LDpred3 implementation.
501
+ `ldpred3-inf` and `ldpred3-lassosum2` fit cross-fitted effects from
502
+ covariate-adjusted marginal statistics and in-cohort LD blocks
503
+ (deterministic; quantitative traits and `loco_folds >= 2` only; the
504
+ separate `ldpred3` package is imported only when requested and stays out
505
+ of the install requirements); `auto` keeps whichever candidate scores
506
+ better on the hold-out. The default `enet` path is unchanged, down to the
507
+ CSV bytes. Effects are the projected standardized row units of
508
+ `_out_of_fold_loco`, and the LD blocks are group-pure, so the LOCO score
509
+ is the genome-wide fitted value less the group's own share exactly under
510
+ the block-independent model. `mixmogam.stage1` documents the accepted
511
+ consequences (blockwise LD, second-order hyperparameter leakage,
512
+ cohort-shared LD across folds).
513
+ - `benchmarks/atwell_controls.py`: a prespecified positive-control check
514
+ on the bundled RegMap panel. Seven Atwell phenotypes have a known major
515
+ gene; a gene is found when its 50 kb window reaches 0.05/m. The 17
516
+ October run ([`20261017-atwell-controls`](benchmarks/results/20261017-atwell-controls/README.md))
517
+ found RPS5, RPM1 (twice), RPS2, FRI and HKT1 with `exact`, `bolt-inf`,
518
+ `hratt` and OLS with 10 PCs. No method found MOT1 on raw Mo values,
519
+ so criterion A1 failed. On log10 values an exploratory rerun ranked MOT1
520
+ first. HRATT's variational fits did not converge on the five traits
521
+ whose heritability estimate reached its upper bound. The report gains
522
+ Section 4.13, and `tests/test_integration_at.py` now requires `exact` to
523
+ find RPS5 and RPM1 on a partial panel (it fails when phenotypes are
524
+ shuffled across accessions).
525
+ - Optional `gwas(..., genome_build=...)` / CLI `--genome-build` declares
526
+ the coordinate assembly; CSV, DataFrame and variant selection preserve
527
+ the label without guessing or converting coordinates. The `af` contract
528
+ explicitly counts `effect_allele`, including frequencies above 0.5.
529
+ - `mixmogam.pca.principal_components`: built-in ancestry principal
530
+ components, the left singular vectors of a thinned marker panel
531
+ (default: 2,000 random variants with sample MAF >= 5%, standardized,
532
+ exact SVD with fixed column signs). `hratt_followups.py` uses it in its
533
+ "+PCs" cells instead of its own randomized approximation; the thinned
534
+ panels and seeds are unchanged, so only the eigensolver changed.
535
+ `ldak_kvik_comparison.py` keeps its bounded-memory full-panel Lanczos
536
+ until that comparison is next rerun.
537
+ - The S4 rerun with the built-in PCs
538
+ ([`20261008-hratt-followups-builtin-pcs`](benchmarks/results/20261008-hratt-followups-builtin-pcs/README.md),
539
+ 130 cases): F5 passed (lambda_GC 0.984-0.989; MAF 1-5% rates at 1e-3
540
+ 0.82-0.98 times nominal, against 3.95-7.05 with pooled frequencies),
541
+ reproducing the 6 October S4 rows within Monte Carlo error. One case
542
+ segfaulted inside Apple Accelerate's `dgemm` under load and regenerates
543
+ cleanly (run README).
544
+ - `mixmogam.io.covariates.read_covariates` / `write_covariates`: a
545
+ covariate-file reader for the PLINK 1 `.cov` layout (FID IID plus
546
+ covariates, header optional) and the single-ID layout, aligned to
547
+ genotypes by sample ID. Missing values are rejected naming the samples.
548
+ - `GwasResult.n` and an `n` column in the summary-statistics CSV (the
549
+ headers already matched downstream sumstats column aliases; the sample
550
+ count was missing). `read_csv` reads `n`/`n_eff` back; older files load
551
+ unchanged with `n` unset.
552
+ - A thin `mixmogam` command line (`[project.scripts]`; also
553
+ `python -m mixmogam`): `mixmogam gwas` scans one trait from PLINK
554
+ genotypes and phenotype/covariate/weight files (`--pcs K` appends the
555
+ built-in PCs), `mixmogam pca` writes PC covariates. Outputs are
556
+ byte-identical to the same call through the API.
557
+ - The prespecified validation of the 2.0.0.dev6 follow-ups
558
+ ([`20261006-hratt-followups`](benchmarks/results/20261006-hratt-followups/README.md),
559
+ 660 cases) passed five of seven criteria.
560
+ - Cross-fitted effects were within 2% of the population slopes, where
561
+ in-sample scores were 28-36% low. Weighted effects under selection on
562
+ the outcome were 5.4% low, and weighted least squares 4.0%.
563
+ - Calibration held at 1-20% prevalence. Under ancestry-dependent weights
564
+ with principal components, rates for MAF 1-5% at 1e-3 were 0.85-0.96
565
+ times nominal, against 4.0-6.5 with pooled allele frequencies.
566
+ - Cross-fitting took 1.08-1.11 times as long. Weighted fits took 1.55
567
+ times as long as HRATT-HE, against a target of 1.3.
568
+ - `--loco-folds` in `hratt_thread_scaling.py` and `hratt_efficiency.py`.
569
+ - `--max-load` in `ldak_kvik_comparison.py`, `hratt_efficiency.py`,
570
+ `hratt_he_comparison.py` and `hratt_thread_scaling.py`, as in
571
+ `efficiency_paired.py`: each timed fit waits until the one-minute load
572
+ average is below the limit, and records the load and the wait.
573
+
574
+ ### Removed
575
+
576
+ - The adaptive-rank arguments of `randomized_eigh_op` (`variance`, `total`,
577
+ `rank_cap`, `initial_k`); no caller used them and `k` is now always the
578
+ returned width.
579
+ - The ignored `max_points` argument of `plot_manhattan`.
580
+ - The unused inverse functions in the phenotype transformation table.
581
+
582
+ ### Changed
583
+
584
+ - The tutorial's ancestry PCs come from `mixmogam.pca` (its weighted-HRATT
585
+ lambda_GC 1.02 to 1.03; figures unchanged), and the report's evidence
586
+ table records the built-in-PC rerun.
587
+ - The report (9 October) covers the changes of 7-8 October. Table 10 and the
588
+ abstract give the 20,000-variant cross-fitted fits with threaded fold
589
+ products (7 October: 33.2 and 29.2 s on four threads, against 34.6 and
590
+ 31.6 s with 2.0.0.dev6); in-sample, storage and official rows are
591
+ unchanged. Section 2.13 describes the built-in PCs, Section 4.12 their S4
592
+ rerun, and the software section the command line, covariate files, the
593
+ CSV `n` column and the tutorial.
594
+ - `hratt_followups.py --scenarios` runs a subset of the selection
595
+ scenarios; the sampling streams are unchanged.
596
+ - The parallel-fit tests use a small NumPy panel instead of `phensim`, so
597
+ CI runs them rather than skipping 23 of them.
598
+ - Corrected the whitening docstring (the dropped spectrum is scaled at its
599
+ mean), the six-column threshold in `docs/design.md`, the cross-fitting
600
+ test introduction and the `hratt_efficiency.py` comparison note.
601
+
602
+ - Benchmark drivers are named by what they compare:
603
+ - `ldak_kvik_comparison.py` (was `kvik_simulation.py`) and
604
+ `ldak_kvik_reference.py` run official LDAK-KVIK;
605
+ - `hratt_efficiency.py`, `hratt_he_comparison.py`,
606
+ `hratt_he_validation.py` and `hratt_thread_scaling.py` time or check
607
+ HRATT, with LDAK-KVIK optional;
608
+ - likewise their plans, tests and `report/make_ldak_kvik_figures.py`.
609
+
610
+ Archives keep the names they ran with.
611
+ - The report's matched LDAK-KVIK comparisons, HAPNEST workloads and
612
+ HE-versus-REML fits are reruns with 2.0.0.dev6 (`20261006-*-dev6`), each
613
+ timed fit gated on the host load. Exact LOCO and BOLT-inf results are
614
+ unchanged; HRATT's cross-fitted scores changed 0.3-1.8% of its decisions
615
+ at 1% and its rejection rates by at most 0.2 percentage points. On one
616
+ host and day, 2.0.0.dev6 took 1.07 (exact LOCO) and 1.03 (HRATT) times as
617
+ long as commit e4089d8 at n = 800
618
+ ([`20261006-same-day-e4089d8-dev6`](benchmarks/results/20261006-same-day-e4089d8-dev6/README.md)).
619
+ - The 20,000-variant workload was rerun with 2.0.0.dev6
620
+ ([`20261006-kvik-20k-dev6`](benchmarks/results/20261006-kvik-20k-dev6/README.md)),
621
+ with cross-fitted and with in-sample scores. Cross-fitting took 1.28-1.38
622
+ times the in-sample time on one thread and 1.53-1.59 on four: its five
623
+ fold fits stayed below the six columns from which the variational sweep
624
+ threaded its genotype products (`_vb._GEMM_MIN_COLUMNS`), so four threads
625
+ gained 1.27-1.33-fold instead of 1.47-1.57, and one and four threads gave
626
+ identical association results. The next item removes that threshold.
627
+ - The variational sweep threads its genotype products from 50,000 samples
628
+ whatever the number of model columns; `_vb._GEMM_MIN_COLUMNS` (six) is
629
+ removed. Six was the smallest shape probed when it was set, not a
630
+ break-even: at 50,000 samples, threaded products sped fitted sweeps up
631
+ 1.31-1.40-fold for two to eight columns alike (1.04-1.08 at 25,000).
632
+ HRATT's cross-fitted fold fits now use them. In a rerun of the
633
+ 20,000-variant workload
634
+ ([`20261007-kvik-20k-crossfit-gemm`](benchmarks/results/20261007-kvik-20k-crossfit-gemm/README.md)),
635
+ four threads sped cross-fitted fits up 1.40 and 1.48 times instead of 1.27
636
+ and 1.33, and in same-day pairs four-thread fits took 0.90-0.94 times as
637
+ long as with 2.0.0.dev6. One-thread results are unchanged bit for bit.
638
+ Four-thread cross-fitted results now differ from one-thread ones by
639
+ rounding, as in-sample results already did (up to 1.3e-7 in effects and
640
+ 1.3e-5 in p), with identical decisions at 0.05, 0.01, 0.001, Bonferroni
641
+ and 5e-8.
642
+ - `HRATT_ALPHAS` is in `twostep.__all__`, as `HRATT_GRID` is.
643
+ - Docstrings corrected: the stochastic trace estimator estimates tr f(A),
644
+ not tr f(A) / n; complete orthogonal HE probes need squared norm df to
645
+ give tr(K²) exactly; exact LMM scans take one GEMM per SNP block; window
646
+ kinships reconstruct the full GRM only weighted by their variant counts;
647
+ `simulate_traits`'s `u` is the total genetic value; two-kinship fits need
648
+ positive semidefinite kinships, which a scaled IBS matrix generally is
649
+ not. Public methods without docstrings now have them.
650
+
651
+ ### Fixed
652
+
653
+ - `Phenotypes.transform(..., revert=True)` after two transformations
654
+ restored the values before the last one and labelled them untransformed.
655
+ Transformations, including `box_cox`, now keep the untransformed values,
656
+ which a revert restores.
657
+ - Saddlepoint tails of weighted and binary HRATT ran on every Numba worker
658
+ whatever `n_threads`; they now use at most `n_threads`, with unchanged
659
+ values.
660
+
661
+ ## [2.0.0.dev6] - 2026-10-06
662
+
663
+ ### Fixed
664
+
665
+ - Refine the last Cholesky REML search interval before accepting its
666
+ boundary; the missed interior optimum made exact LOCO results change
667
+ when chromosome labels were renamed.
668
+ - Weighted/binary HRATT saddlepoint tails use sample-specific genotype
669
+ distributions with covariates, conditional on the missing-call mask.
670
+ Correcting only the pooled distribution's variance left extreme tails
671
+ inflated. The conditional model assumes Hardy-Weinberg equilibrium.
672
+ - BOLT mixture results report infinitesimal-model effects and standard
673
+ errors (`extra["effect_method"]`), removing attenuation from slopes
674
+ against in-sample mixture residuals while retaining the mixture test.
675
+ - Weighted/binary standard errors no longer depend on the saddlepoint
676
+ tail rule. Binary effects remain one-step approximations: CSV/DataFrame
677
+ labels are `beta_one_step` and `se_null_score`; CSV reload retains this
678
+ interpretation and `posterior_probabilities()` rejects them.
679
+ - Two-kinship profiles include weights zero and one, reject nonidentifiable
680
+ covariance components, and return `weight=None` with zero shares when
681
+ the ordinary linear model wins. Only the best candidate fit is retained.
682
+ - Genotypic tests reject covariate-confounded contrasts consisting only
683
+ of projection round-off, rather than treating them as extra degrees of
684
+ freedom.
685
+ - EIGENSTRAT imports preserve both supplied allele columns through PLINK
686
+ export instead of replacing them with synthetic labels.
687
+ - `Phenotypes.most_normal()` respects transformation domains and excludes
688
+ constant results; counts above one cannot collapse under arcsine-square-root.
689
+
690
+ ### Added
691
+
692
+ - HRATT for case-control outcomes and sampling weights:
693
+ `gwas(..., method="hratt", trait="binary")` and `sample_weights=w`
694
+ (e.g. inverse probabilities of participation), alone or together. Step 1
695
+ fits a row-scaled weighted model (the logistic working response for binary
696
+ traits); with weights, heritability comes from HE with design-consistent
697
+ moments. Step 2 tests each score against its variance under genotype
698
+ exchangeability, with a saddlepoint approximation of the genotype
699
+ distribution above |z| = 2 (`spa_threshold`); binary scores are those of
700
+ the logistic model with the LOCO score as offset, and effects one-step log
701
+ odds ratios. The Huber-White sandwich and the model-based logistic
702
+ variance are not used: in pilots the sandwich score test was inflated
703
+ 17-fold at 1e-3 for MAF 1-5% under outcome-dependent selection, against
704
+ 0.75-fold for the retrospective test, and with HRATT's offsets the
705
+ model-based logistic variance gave lambda_GC 0.55 with 50 cases in 5,000,
706
+ against 1.01-1.04. New extras of weighted or binary fits: `trait`,
707
+ `heritability_method`, `spa_threshold`, `n_spa`; with weights
708
+ `kish_n` and `design_effect`; for binary traits `n_cases`, `n_controls`,
709
+ `prevalence`, `offset_sd`, `null_converged`, `mu0_clipped`. `gwas` rejects
710
+ `trait`, `sample_weights` and `spa_threshold` for other methods.
711
+ Unweighted quantitative HRATT is unchanged, bit for bit. Validation
712
+ ([`20261005-hratt-weights-binary`](benchmarks/results/20261005-hratt-weights-binary/README.md),
713
+ prespecified, 30 null and 10 mixed replicates per cell): two of seven
714
+ criteria passed. Weighted quantitative tests were calibrated, including
715
+ under selection on the outcome (the sandwich: 12-fold at 1e-3); with
716
+ weights that depend on ancestry they were anti-conservative for MAF 1-5%
717
+ (5.75-fold at 1e-3); binary per-tail rates were lopsided at 1-5%
718
+ prevalence with near-nominal two-sided rates; 2 of 30 replicates at 1%
719
+ prevalence stopped with a separation error.
720
+
721
+ - `spa_two_sided="doubled"` doubles the saddlepoint tail beyond the
722
+ observed score, LDAK's default, putting half the level in each tail of a
723
+ skewed null. The default, `"distance"`, adds both tails at +-|u|, as
724
+ SPAtest, SAIGE and REGENIE do. `gwas` rejects `loco_folds` and
725
+ `spa_two_sided` for methods other than HRATT.
726
+
727
+ ### Changed
728
+
729
+ - HRATT's LOCO scores are cross-fitted (`loco_folds=5`, new): each fold's
730
+ predictor excludes that fold's outcomes, while variance components and
731
+ model selection still use the whole trait.
732
+ - Five genome-wide variational fits run, each on four fifths of the
733
+ samples (folds stratified by case status for binary traits).
734
+ - A sample's score for chromosome group g is its own fold's fit without
735
+ group g's variants, as in REGENIE's level 0.
736
+ - Under strong structure each group is refitted instead. Splitting a
737
+ genome-wide fit leaves ancestry in the residual: at Fst 0.15 without
738
+ principal components, the null chromosome's lambda_GC was 1.5-4.4 split
739
+ and 0.9-1.1 refitted.
740
+ - Each group's score enters with its out-of-fold calibration slope, kept
741
+ in [0, 1] (`extra["offset_slope"]`).
742
+ - In-sample scores absorbed part of every effect: in simulations, HRATT's
743
+ effects were 0.59-0.69 of the truth and are now 0.95-1.0, at equal
744
+ power.
745
+ - At 1% prevalence, in-sample scores gave cases offsets of tens of logit
746
+ units (+43 against -0.4 for controls on a null trait). This is what
747
+ stopped 2 of 30 replicates of the 5 October validation; that case now
748
+ runs with lambda_GC 1.04.
749
+ - `loco_folds=1` restores in-sample scores, bit for bit for unweighted
750
+ traits.
751
+ - In a one-replicate pilot at 5,000 samples, 20,000 markers and ten
752
+ groups, the cross-fitted fits took a median 1.11 times as long. The
753
+ prespecified validation (`benchmarks/hratt_followups_plan.md`) is
754
+ running; its archive will follow.
755
+ - The retrospective tests of weighted and binary HRATT take each sample's
756
+ allele frequency from the covariates, as SPAmix does.
757
+ - Deviations from the mean are shrunk by the fit's F statistic, so
758
+ covariates unrelated to genotype leave the old variance.
759
+ - The score variance becomes zvar |a|^2 rho_j.
760
+ - With ancestry principal components among the covariates, weights that
761
+ depend on ancestry gave low-frequency rejection rates of 1.2-1.4-fold
762
+ at 1e-3 for MAF 1-5% in a prototype of the S4 scenario. The pooled
763
+ variance gave 6.7-7.2-fold even with the PCs.
764
+ - HRATT warns when strong structure remains after the covariates in
765
+ weighted or binary analyses.
766
+ - Binary null fits take the cross-fitted score as a covariate, its
767
+ coefficient kept in [0, 1]:
768
+ - a fixed offset above one;
769
+ - dropped if it anti-predicts or its fit diverges (`offsets_dropped`),
770
+ instead of stopping the analysis.
771
+
772
+
773
+ - `heritability_method` defaults to `"auto"` (was `"reml"`): REML without
774
+ sampling weights, HE with them.
775
+
776
+ - The KVIK-style method is now HRATT, the Heritability-weighted Residual
777
+ Association Two-step Test: `gwas(..., method="hratt")`,
778
+ `twostep.hratt`, `HRATT_GRID` and `HRATT_ALPHAS`, and results report
779
+ `extra["method"] == "hratt"`. HRATT is inspired by LDAK-KVIK (Hof & Speed
780
+ 2025) and follows its two-step design, but its variance fitting,
781
+ variational sweep and calibration solves are this package's own and its
782
+ results differ from the reference program's, so it no longer carries that
783
+ name. `method="kvik"`, `twostep.kvik`, `KVIK_GRID` and `KVIK_ALPHAS` are
784
+ removed without aliases; archived benchmark runs keep the old name.
785
+
786
+ ### Removed
787
+
788
+ - `cache_bytes` from `bolt_inf`, `bolt`, `hratt` and `LocoGenotypes`: the
789
+ float copy of the standardized genotypes is gone, and every pass decodes
790
+ them. The cache only helped data small enough to fit its 4e9-byte budget
791
+ (paired fits: [`20261004-packed-calls`](benchmarks/results/20261004-packed-calls/README.md);
792
+ final comparison under Benchmarks).
793
+
794
+ ### Memory
795
+
796
+ - Calls can be stored two bits each, a quarter of the int8 bytes:
797
+ `Genotypes(G, packed=True)`, or `read_plink(prefix, packed=True)` to hold
798
+ the bed's codes in memory and `mmap=True` to map them from the file.
799
+ `PackedCalls` keeps PLINK's bed layout; indexing returns int8 calls,
800
+ filters keep the calls packed and `write_plink` copies the codes. The
801
+ two-step engines read the codes directly, with results identical to int8
802
+ storage. Preparation now takes each variant's mean and variance from exact
803
+ call counts, the same for every thread count, layout and storage format;
804
+ prepared values move by floating-point rounding.
805
+ - Prepared genotypes are never stored covariate-projected. Preparation keeps
806
+ each variant's three standardized call values, its covariate coefficients
807
+ c = z Q and the squared norm of its projected values. Operator products
808
+ project their n x c operands instead; variational sweeps keep their
809
+ residual unprojected and correct each 128-variant block's products by
810
+ k x q terms; Gram matrices, the HE diagonal and LD scores add rank-q
811
+ corrections to products of unprojected rows. No pass projects variants.
812
+ - With the `fast` extra, one compiled kernel prepares the variants for every
813
+ thread count and storage order, over 64-variant tiles, with identical
814
+ values, including the one-thread default. Decoding follows the storage
815
+ order: one M2 Pro thread decodes about 0.26 ns per genotype from
816
+ variant-major (PLINK) storage and 0.7 ns from sample-major arrays, against
817
+ 0.6 and 3.7 ns in 2.0.0.dev5.
818
+ - One-pass consumers decode 16 MiB slices (at least 256 variants, at most
819
+ 512 MiB) into reused buffers.
820
+ - BOLT-LMM's in-sample LD scores stream each chromosome through a sliding
821
+ position window. They held every projected genotype (4 bytes per genotype)
822
+ whatever `cache_bytes`.
823
+ - The spectral denominator projects one LOCO basis at a time.
824
+ - Variational Gram matrices are stored in the genotype precision (half the
825
+ bytes with float32 storage), and blocks are cached in order up to the
826
+ 1e9-byte budget instead of all or none; the rest are recomputed each sweep
827
+ for the folds that fit uses, so a full-data fit after cross-validation
828
+ rebuilds one Gram per block rather than one per fold. Paired fits
829
+ ([`20261004-gram-cache`](benchmarks/results/20261004-gram-cache/README.md)):
830
+ HRATT-HE at 2,000 samples and 300,000 markers peaked 0.27 GiB lower, and
831
+ BOLT-LMM's five-fold Grams, 1.87 GB in float64 and so recomputed every
832
+ sweep, now fit (0.95 GB); the cache makes those cross-validation fits
833
+ 1.5 (BOLT-LMM) and 2.5 (HRATT) times faster.
834
+ - HRATT and BOLT-LMM release the cross-validation fit's effects, residuals and
835
+ masks once its scores are taken, and the LOCO fit's effects and the
836
+ variational engine's Gram cache once its residuals are extracted.
837
+ BOLT-LMM's cross-validation held 90 float64 effect columns (720 bytes per
838
+ variant) through the LOCO fit.
839
+
840
+ ### Benchmarks
841
+
842
+ - Paired fits of 2.0.0.dev5 against 12aa504
843
+ ([`20261004-genotype-streaming`](benchmarks/results/20261004-genotype-streaming/README.md),
844
+ idle host, two fresh-process repetitions; final numbers below). Report
845
+ Sections 2.11 and 4.10 describe the method and results;
846
+ `efficiency_paired.py` gained the BOLT-LMM, uncached and marker-scaling
847
+ workloads.
848
+ - Rerun the mixmogam methods of the matched phensim (n = 800, 2,000 and
849
+ 4,000), HAPNEST (10K and 50K), HE-versus-REML and 20,000-marker benchmarks
850
+ with 2.0.0.dev5, on their archived inputs. Official LDAK-KVIK outputs are
851
+ reused after the inputs are verified by hash. `kvik_simulation.py
852
+ --rerun-from` and a `--methods` option in `kvik_thread_scaling.py` and
853
+ `kvik_he_comparison.py` support this.
854
+ - A same-day 2.0.0.dev4/dev5 check at 50,000 samples found no memory change
855
+ and REML fits 1.57 times faster. The HE path was unchanged. Identical
856
+ dev4 code took 27% less time than on the day of the original runs, so most of
857
+ the differences from those archives in time and peak RSS reflect host
858
+ conditions.
859
+ - Rerun the same benchmarks with commit e4089d8 (`20261005-*-e4089d8`); the
860
+ report now uses these reruns. At 50,000 samples and 20,000 markers,
861
+ four-thread HRATT-HE peaked at 1.4 GiB instead of 5.1, or 0.73 GiB with
862
+ two-bit calls (official LDAK-KVIK: 0.65). A same-day check against dev5
863
+ ([`20261005-same-day-dev5-e4089d8`](benchmarks/results/20261005-same-day-dev5-e4089d8/README.md))
864
+ found four-thread HE 17% faster and one-thread REML 1.2 times slower
865
+ without the cache. Across the reruns, rejection rates, power and QTL
866
+ detection are unchanged, and log10 p moved by at most 1.8e-4 (same-day
867
+ check: 1.8e-5). One- versus four-thread differences
868
+ on the PC-adjusted 20K panel grew tenfold (up to 2.0e-5 in p), with
869
+ unchanged decisions. The 20K cache experiment is now an int8 against
870
+ two-bit storage experiment: `kvik_efficiency.py` and
871
+ `kvik_thread_scaling.py` gained `--storage`.
872
+ - `efficiency_paired.py` compares any number of labelled sources
873
+ (`--source LABEL=PATH`), skips workloads a source cannot run (the float
874
+ genotype cache, two-bit calls) instead of failing or running a fit twice,
875
+ calls older sources by the method's earlier name, rotates the source order
876
+ over repetitions, and with `--max-load` starts each measurement only below
877
+ a given load average; it records the load and the wait. dev5 against
878
+ commit d51b4c3, three gated repetitions
879
+ ([`20261005-efficiency-paired-hratt`](benchmarks/results/20261005-efficiency-paired-hratt/README.md)):
880
+ the two-step paths need 2.4 to 2.8 times less memory with int8 calls and
881
+ 3.1 to 4.2 times less with two-bit calls; HRATT-HE runs 4% (one thread)
882
+ and 14% (four threads) faster, BOLT-LMM-inf, BOLT-LMM and HRATT with REML
883
+ 1.19 to 1.23 times slower; HRATT-HE's peak RSS grows by 0.36 bytes per
884
+ genotype with two-bit calls against 4.06 with dev5's cache. Report
885
+ Tables 15 and 16 now come from this archive. The Lanczos study, rerun
886
+ with commit e4089d8
887
+ ([`20261005-slq-defaults-e4089d8`](benchmarks/results/20261005-slq-defaults-e4089d8/README.md)),
888
+ reproduces dev5's to 5e-7 and renders Table 14.
889
+
890
+ ## [2.0.0.dev5] - 2026-10-04
891
+
892
+ ### Computational efficiency
893
+
894
+ Archived paired fits against 2.0.0.dev4
895
+ ([`20261004-efficiency-paired`](benchmarks/results/20261004-efficiency-paired/README.md):
896
+ fresh processes, alternating order, two repetitions, M2 Pro under unrelated
897
+ background load): exact LOCO at n = 3,000 and m = 44,000 took 13.3 s instead
898
+ of 79.6 s; bolt-inf at n = 10,000 and m = 30,000 7.3 s instead of 12.9 s;
899
+ KVIK with REML 9.8 s instead of 14.4 s; MLMM at n = 2,000 with ten steps
900
+ 5.5 s instead of 27.4 s; uncached KVIK-HE at n = m = 20,000 with four threads
901
+ peaked at 0.93 GiB instead of 1.14 GiB with identical associations. The new
902
+ probe count and calibration draw change two-step results within their Monte
903
+ Carlo error (up to 0.07 in log10 p at p = 4e-31); the exact scan agrees to
904
+ 3e-5 in log10 p. The bullets give each change's own development measurement.
905
+
906
+ - Exact LOCO scans fit each group's REML variance ratio through Cholesky
907
+ factorizations of K_{-g} + delta I and whiten SNPs by triangular solves,
908
+ instead of one eigendecomposition per group. LAPACK's eigensolvers used
909
+ about 1.2 cores here; at n = 3,000 with 22 chromosome groups the scan
910
+ took 14.5 s instead of 79 s. Delta agrees with the EMMA fit to 1e-6 and
911
+ log10 p to 1.2e-7 (float64). Results add `variance_solver` and
912
+ `reml_factorizations`; `LMM` itself keeps its eigendecomposition.
913
+ - Stochastic Lanczos REML defaults to 24 steps and 48 probes (was 96 and
914
+ 12). Each step is a genotype pass whose cost hardly depends on the column
915
+ count up to about 64, and 24 steps reproduced 96 to five digits across
916
+ unstructured, structured, high-heritability and fewer-markers-than-samples
917
+ spectra. The probes carried the error: at n = 500 with strong structure
918
+ the h2 SD over probe seeds fell from 0.12 (one error of 0.38) to 0.02
919
+ ([`20261004-slq-defaults`](benchmarks/results/20261004-slq-defaults/README.md)).
920
+ bolt-inf at n = 10,000 and m = 30,000 took 8.5 s instead of 12.7 s.
921
+ Estimates change within their Monte Carlo error. Probes that a covariate
922
+ projection reduces to rounding noise are dropped.
923
+ - BOLT-LMM, BOLT-LMM-inf and KVIK under strong structure compute one
924
+ randomized kinship basis for both the REML deflation and the CG
925
+ preconditioner, and solve the calibration SNPs in the same CG run as the
926
+ LOCO residuals. Calibration candidates are now drawn before the residuals
927
+ and filtered by chi2 < 5 afterwards (the same distribution), so a given
928
+ `random_state` selects different SNPs than before. bolt-inf at n = 10,000
929
+ took 7.4 s instead of 8.5 s.
930
+ - Relationship matrices standardize genotype blocks in one fused Numba
931
+ pass (1.6 times faster serially, 5 times with six threads, same values to
932
+ rounding of the moments). `gwas(method="exact")` accepts `n_threads` for
933
+ parallel standardization and block conversion with identical results;
934
+ n = 3,000 exact LOCO took 12.9 s serially and 11.8 s with six threads.
935
+ - `LMM` scans whiten in eigen coordinates, one GEMM per SNP block instead
936
+ of two (1.5 times faster in float32, 1.7 in float64, and closer to the
937
+ float64 statistics in float32); permutations keep sample coordinates and
938
+ so their per-seed results. Eigenvectors come from the divide-and-conquer
939
+ driver (23-42% faster than MRRR, MRRR as fallback) and are stored
940
+ contiguously in descending order.
941
+ - Uncached two-step passes (`cache_bytes` below the float32 cache size)
942
+ decode hard calls through per-variant value tables and project covariates
943
+ in the preparation route's own arithmetic, so streamed blocks still equal
944
+ cached ones bit for bit; operator products instead project their n x c
945
+ operands once. Decoded slices and VB subblocks reuse buffers. An uncached
946
+ kinship pass at n = 10,000 and m = 30,000 took 128 ms instead of 1,033 ms
947
+ (cached 64 ms). KVIK-HE at n = m = 20,000 with four threads peaked at
948
+ 0.97 GiB uncached against 2.26 GiB cached, 16% slower with identical
949
+ associations; uncached bolt-inf agrees with cached to 1.4e-5 in log10 p.
950
+ - `mlmm` rotates the SNPs into the kinship's eigen coordinates once (new
951
+ `cache_bytes`, default 4e9; `block`) and rescales them for each forward
952
+ scan, O(n m q) per step instead of O(n^2 m). Forward scans default to
953
+ float32, like `gwas` (was float64); cofactor tests and likelihoods stay
954
+ float64. At n = 2,000, m = 50,000 and ten forward steps it took 5.5 s
955
+ instead of 27.2 s (8.9 s with `dtype=np.float64`), selecting the same
956
+ cofactors.
957
+ - Archive the paired comparison with 2.0.0.dev4 and the Lanczos
958
+ steps-versus-probes study, with their drivers `benchmarks/efficiency_paired.py`
959
+ and `benchmarks/slq_defaults.py`, and the release checks in
960
+ `20261004-release-dev5`.
961
+ - Describe the methods in a new manuscript Section 2.10, report the paired
962
+ fits and Lanczos accuracy in two tables rendered by
963
+ `report/make_dev5_tables.py`, and update the README, quickstart and design
964
+ notes.
965
+
966
+ ## [2.0.0.dev4] - 2026-10-03
967
+
968
+ ### KVIK computational efficiency
969
+
970
+ - Add explicit `n_threads` for parallel Numba updates across independent
971
+ KVIK candidate models and LOCO fits, preserving within-model update and
972
+ reduction order. Restore the caller's thread mask after fitting.
973
+ - Parallelize optional genotype preparation over independent variants with
974
+ float64 two-pass variance and projection, bounded private worker buffers,
975
+ and the unchanged one-thread NumPy default.
976
+ - Reuse projection coefficients for direct, parallel hard-call decoding;
977
+ uncached variational sweeps decode only their current small SNP block.
978
+ - Use contiguous BLAS inputs and parallel sample slices for large residual
979
+ matrix products, with reusable workspaces and restored BLAS thread limits.
980
+ - Bound hard-call validation, uint8 missing-code conversion and packed BED
981
+ input scratch instead of allocating masks or retaining packed inputs for
982
+ the complete dataset.
983
+ - Reuse full-data Gram matrices and final predictions between cross-validation
984
+ and LOCO; compute only the held-out Gram slices actually requested. Reuse
985
+ the spectral preconditioner in the strong-structure calibration path.
986
+ - Reuse variational fitting buffers and fuse residual bookkeeping with Numba,
987
+ retaining float64 residuals, the existing posterior updates and convergence
988
+ rule, and the NumPy fallback.
989
+ - Bound large temporary arrays in standardization, prediction, retrospective
990
+ statistics and HE diagonal calculation. Genotype precision and fitting
991
+ defaults remain unchanged.
992
+ - Add a paired time/RSS comparison against frozen pre-optimization source on
993
+ existing phensim HAPNEST inputs, with source/input hashes and full numerical
994
+ and convergence comparisons; see `benchmarks/kvik_efficiency.py`.
995
+ - Add `heritability_method="he"` to KVIK: reuse the alpha scan's products for
996
+ a covariance-projected, nonnegative two-component moment fit. Report raw
997
+ estimates, boundaries, trace-probe uncertainty and the variational noise
998
+ floor. This is a separate estimator; REML remains the default.
999
+ - Fit only the needed variance components in two-step methods, avoiding the
1000
+ unused GLS completion. Compute weighted kinship traces only when requested.
1001
+ - Prepare called-sample moments and kinship trace once; uncached requested-row
1002
+ access converts only those rows. Decode BED into contiguous variant columns
1003
+ while preserving the public sample-by-variant array and allele counts.
1004
+ - Extend the verified HAPNEST workload to exactly 20,000 retained variants
1005
+ at 50,000 samples. Genotype-only chromosome quotas precede PC and phenotype
1006
+ generation; preparation can run independently of association methods.
1007
+ - Archive 24 one/four-thread comparisons with official LDAK-KVIK and 12
1008
+ paired cache fits. Cache routes give identical saved results; six thread
1009
+ comparisons exceed strict array tolerances without changing the checked
1010
+ association decisions. Retain timing ranges and the observed system-wide
1011
+ swapping limitation, rather than claiming uniform superiority to LDAK.
1012
+ - Update the manuscript, API guides and research agenda with HE, parallel
1013
+ execution, numerical audits and measured time/memory tradeoffs. Timed source
1014
+ snapshots retain their original version; this release does not rewrite them.
1015
+
1016
+ ## [2.0.0.dev3] - 2026-10-03
1017
+
1018
+ ### Matched reference benchmark
1019
+
1020
+ - Extend measured workloads to 50,000 samples with phensim's new HAPNEST
1021
+ model, keeping all production fitting defaults. Preparation uses tiled
1022
+ genotype operations, audited iterative PCs, and isolated processes.
1023
+ - Add simulator speed/RSS evidence through 100,000 samples, resource tables,
1024
+ uncertainty-aware matched results and empirical-reference research priorities
1025
+ to the report. Simulation scale and association-validation claims remain
1026
+ distinct; large-n cells have one realization each.
1027
+ - Add a prespecified phensim simulation comparison with the official
1028
+ LDAK-KVIK executable, including population structure, within-population
1029
+ LD, environmental confounding and paired PC adjustment.
1030
+ - Validate actual BED calls independently and check external-reader sample
1031
+ IDs, allele orientation, frequencies and call rates. Retain raw results,
1032
+ failures, seeds, source snapshots, convergence diagnostics and process RSS.
1033
+ - Distinguish global genetic-null calibration, null-chromosome rejection
1034
+ under mixed traits, and direct causal-marker detection with uncertainty
1035
+ across independent genotype/phenotype replicates.
1036
+
1037
+ ## [2.0.0.dev2] - 2026-10-03
1038
+
1039
+ ### Research report
1040
+
1041
+ - Expand the manuscript with model and calibration derivations, explicit
1042
+ evidence boundaries, and six testable research priorities. Withdraw the
1043
+ invalid external-program results and qualify historical QTL-free loci.
1044
+ - Add a reproducible known-covariance denominator experiment with held-out
1045
+ audit variants, analytic tail probabilities, and phenotype-replicate
1046
+ uncertainty; regenerate the report figures with provenance hashes.
1047
+
1048
+ ## [2.0.0.dev1] - 2026-10-03
1049
+
1050
+ ### Critical review
1051
+
1052
+ - Correct PLINK BED bit encoding/decoding against a specification fixture;
1053
+ preserve counted A1/A2 alleles, accept standard TPED allele pairs, and keep
1054
+ distinct chromosome labels. Legacy dosage TPED files warn on import.
1055
+ - Count diploid MAC in allele copies, retain counted-allele frequency, reject
1056
+ invalid hard calls/duplicate IDs, and represent absent aligned samples as
1057
+ missing. Merge RegMap chromosomes on the variant axis in a common sample
1058
+ order. Reject silently overwritten phenotype replicates.
1059
+ - Correct the extra genetic-variance factor in BLUP and include the genetic
1060
+ component in operator predictions. Use ML's n denominator, respect changes
1061
+ to fitting options, provide an OLS fit when K is absent, and stabilize
1062
+ likelihood quadratics against large fixed effects.
1063
+ - Reject phenotypes explained entirely by covariates and operations on a
1064
+ superseded fit, rather than reporting projection roundoff or new parameters.
1065
+ - Apply weights in streaming kinships, bound their genotype cache, correct
1066
+ overlapping/gapped window complements, and remove the extra group axis
1067
+ from LOCO product storage. Reject nonconverged CG association solves.
1068
+ - Permute independent residual coordinates for the default whitened scheme;
1069
+ retain the prior approximation explicitly as `scheme="projected"`.
1070
+ Mean-impute missing genotypic contrasts and mark non-estimable SNPs NaN.
1071
+ - Replace the invalid RSS-based pseudo-posterior with a normal-approximation
1072
+ Bayes factor requiring explicit `prior_variance` and beta/se. This is an
1073
+ intentional API correction; the old values are not posterior probabilities.
1074
+ - Preserve IDs, effects, standard errors, alleles and precision through CSV;
1075
+ include test oracles in source distributions and declare the pandas extra.
1076
+ CI now covers Python 3.10 without optional analysis dependencies.
1077
+ - Fix small-sample spectral scans, forward block size, reject ignored options,
1078
+ and expose incomplete variational fits. SWLM no longer uses an inapplicable
1079
+ genetic-variance stopping rule; MLMM resets its consecutive-low-h2 counter.
1080
+ - Mark old reports as historical and withdraw the matched-input LDAK
1081
+ comparison pending a rerun with corrected encoding, MAC and missingness.
1082
+ See `docs/review-2026-10-03.md` for evidence and remaining priorities.
1083
+
1084
+ ### Fixed
1085
+
1086
+ - `LMM` and its cached `LMFit` referenced each other, so a dropped model
1087
+ (its K and eigendecomposition) waited for the cyclic garbage collector
1088
+ instead of being freed. An exact LOCO `gwas()` stranded its 25 group
1089
+ models, about 8 GB at n = 4,000, and the sim study reached a 17 GB
1090
+ footprint. `LMM.fit_result` now stores the fit without the back
1091
+ reference and returns it bound to the model; a fit returned by
1092
+ `fit()` still keeps its model alive.
1093
+ - The simulation study's S2 confounder was phensim's
1094
+ `simulate_confounded_trait` axis, the leading eigenvector of the
1095
+ tested SNPs' GRM: in a panmictic sample a weighted sum of those SNPs,
1096
+ so S2 could not compare LOCO with non-LOCO. S2 now samples two demes
1097
+ with msprime and puts the confounder on an environment that differs
1098
+ between them (archive 20261003T122520Z-sim-study).
1099
+ - The simulation study cut phensim's contiguous coalescent segment into
1100
+ "chromosomes" every 200 SNPs, through strong LD: every S1 false locus
1101
+ was a QTL tag in the neighbouring block. S1, S3 and S4 blocks are now
1102
+ ldpred3's LD split (bigsnpr's `snp_ldsplit`) into the same number of
1103
+ blocks of 100-400 SNPs.
1104
+ - `GwasResult.genomic_control` returned median(p)/0.5, which is not
1105
+ lambda_GC and runs the other way (below 1 meant inflation). It now
1106
+ returns the median 1-df chi-square over 0.455. The archived sim-study
1107
+ notes were read on the wrong scale; dated errata are appended to both
1108
+ NOTES.md files. Re-read: the truncated top-1024 scan at n = 10k was
1109
+ anti-conservative (lambda_GC 1.30, 84 vs 50 significant non-causal
1110
+ SNPs), not conservative, and exact non-LOCO scans were deflated
1111
+ (0.85-0.93, proximal contamination).
1112
+ - `scan_gxe` tested the interaction column without the SNP main effect:
1113
+ with a 0/1 environment a pure main-effect SNP gave interaction
1114
+ p ~ 1e-6 in 20/20 replicates. The 1-df test is now the interaction
1115
+ given the main effect; E is added to the covariates (with a warning)
1116
+ when absent; `polygenic_gxe=True` adds a K * (EE') variance component
1117
+ (Sul et al. 2016). The previous "GLS oracle" test fitted the same
1118
+ mis-specified design and now uses the full one.
1119
+ - `permutation_min_p` permuted the raw phenotype, which breaks the
1120
+ kinship covariance: family-wise error 7.6% +/- 0.6% at a nominal 5%
1121
+ under 4 populations at F_ST 0.3. It now permutes GLS-whitened null
1122
+ residuals (Abney 2015, MVNpermute); `scheme="raw"` keeps v1 behaviour.
1123
+ In the sim study the genome-wide 5% threshold moved from 8.6e-5 to
1124
+ 4.4e-6 (Bonferroni 2.5e-6): the archived "30x less conservative than
1125
+ Bonferroni" was mostly the invalid permutation.
1126
+ - `stepwise.mlmm` was not the published MLMM: it selected with plain BIC
1127
+ (Segura et al. 2012 found it too tolerant), scored REML likelihoods
1128
+ across different fixed-effect designs, compared backward models
1129
+ against the wrong likelihood, sliced a list it had mutated, and fed
1130
+ missing calls into cofactors as dosage -1. It is now a port of v1's
1131
+ `linear_models.mlmm`: ML likelihoods, EBIC and mBonf (and BIC)
1132
+ selection over all forward and backward models, v1's pseudo-
1133
+ heritability stop rule, mean-imputed cofactors. `mlmm_bic` is removed;
1134
+ `mlmm(...)["selected"]` holds every criterion's model.
1135
+ - The quickstart's per-chromosome LOCO recipe scanned the SNPs outside
1136
+ each chromosome with that chromosome's LOCO kinship (backwards).
1137
+ - docs/methods.tex: the LDAK-KVIK reference ("Tian et al. 2024") did not
1138
+ exist and is replaced by Hof & Speed (2025, Nat Genet); the BOLT-LMM
1139
+ title was wrong and the truncated spectrum was attributed to it; the
1140
+ whitening operator `\H` typeset as a Hungarian-umlaut accent.
1141
+ - `power_analysis` counted LD tags of causal variants as false
1142
+ positives (`n_false_positive`); the field is now
1143
+ `n_significant_noncausal`, and `locus_summary` gives locus-level
1144
+ power and FDR. The simulation study reports loci.
1145
+
1146
+ ### Added
1147
+
1148
+ - `report/`: a paper-style LaTeX report of the evidence: per-SNP
1149
+ calibration under structure on *A. thaliana* and simulated data, the
1150
+ confounding and LD-block simulation designs, LOCO and computation.
1151
+ `report/make_figures.py` rebuilds its figures and tables from the
1152
+ archived results; the methods documentation covers PC covariates under
1153
+ strong environmental stratification and the model/fit memory design.
1154
+ - `mixmogam.gwas()`: leave-one-chromosome-out association by default.
1155
+ `method="exact"` is EMMAX with one eigendecomposition and REML refit
1156
+ per LOCO group; `"bolt-inf"`, `"bolt"` and `"kvik"` are the two-step
1157
+ K-free statistics; `"auto"` picks exact up to n = 5,000. More than 25
1158
+ chromosomes are merged into 25 contiguous groups.
1159
+ - `mixmogam.twostep`: BOLT-LMM-inf (batched CG LOCO solves, retrospective
1160
+ statistic, calibration against exact prospective statistics at 30
1161
+ SNPs), BOLT-LMM (two-Gaussian mixture prior by blocked variational
1162
+ Bayes, 5-fold CV over BOLT's 18-point grid, LD Score regression
1163
+ intercept calibration) and LDAK-KVIK (structure test, LDAK-Thin power
1164
+ model, elastic-net prior, OLS-on-offset statistics with KVIK's lambda
1165
+ rule), on a shared streaming LOCO operator. Each reports
1166
+ `calibration_cv`, the spread of the exact/retrospective denominator
1167
+ ratio its single calibration constant assumes away.
1168
+ - Structure-aware denominator (`denominator="spectral"`, a mixmogam
1169
+ extension): a low-rank-plus-flat approximation of the prospective
1170
+ denominator on the top eigenvectors of each SNP's own LOCO kinship.
1171
+ On null SNPs binned by structure loading, single-constant statistics
1172
+ ran from lambda_GC 1.34-1.37 to 0.64-0.73 (FPR at 1%: 2.5-2.9% to
1173
+ 0.1%) under simulated F_ST 0.3. On A. thaliana RegMap genotypes the
1174
+ reference LDAK binary's KVIK ran from 1.21 to 0.92. The spectral
1175
+ denominator holds BOLT-LMM-inf within 0.92-1.05 of exact in every
1176
+ quintile (`benchmarks/structure_calibration.py`, `kvik_reference.py`).
1177
+ For the mixture and elastic-net statistics it is a heuristic transfer.
1178
+ The basis width adapts (64 to 512 eigenvectors) until the calibration
1179
+ spread over the 30 exact SNPs falls below 3%.
1180
+ - `ldscore.ld_scores` / `ldsc_intercept`, `results.locus_summary`,
1181
+ `LMM` fits on streaming operators without the truncated
1182
+ eigendecomposition (GLS by conjugate gradients).
1183
+
1184
+ ### Changed
1185
+
1186
+ - Stochastic Lanczos REML runs the y rule and all trace probes as one
1187
+ batched Lanczos process (`_slq.lanczos_quadrature_batch`): one pass over
1188
+ the genotypes per step instead of one per probe, same probes, fits
1189
+ equal to 1e-13. LDAK-KVIK picks alpha by randomized Haseman-Elston
1190
+ regression with all alphas in one pass (`alpha_method="he"`, LDAK-KVIK's
1191
+ own approach) and runs one REML fit at that alpha; `alpha_method="reml"`
1192
+ keeps one REML fit per alpha. The variational-Bayes kernel reorders its
1193
+ loops for contiguous access, shares one erfc between log Phi and the
1194
+ Mills ratio, and runs its GEMMs in float32 with an exact float64
1195
+ residual at the end. On A. thaliana (n = 1,307, m = 53k) `kvik` went
1196
+ from ~150 s to ~15 s (REML alpha selection: ~41 s); the LDAK binary
1197
+ takes ~9 s.
1198
+ - Scans on a truncated spectrum warn: they were anti-conservative in
1199
+ simulation. Docstrings no longer attribute the truncated spectrum to
1200
+ BOLT-LMM or SNP-subsampled fits to LDAK-KVIK.
1201
+
1202
+ - K-free large-n path (BOLT-LMM's design): `GenotypeKinship`
1203
+ wraps the genotype store as a streaming linear operator (K x =
1204
+ Z(Z'x)/m over SNP blocks, machine-precision equal to the dense GRM),
1205
+ and `LMM` accepts it directly for the randomized eigensolver, SLQ
1206
+ fitting, scanning and gBLUP -- no n x n matrix is ever formed. SLQ
1207
+ deflation is now adaptive by spectral gap (well-separated extremes
1208
+ only), fixing a systematic bias when the deflation window crossed a
1209
+ near-degenerate bulk.
1210
+ - Scan transform casts the eigenbasis once per dtype, so float32 scans
1211
+ run genuine float32 GEMMs end-to-end: ~3.3x faster than float64 at
1212
+ n=2000-5000 (the earlier "float32" path computed in float64
1213
+ internally); headline scan speed-up vs the v1-style loop is now 123x.
1214
+ - Fixed `scan_gxe`: the interaction column is now formed in raw space
1215
+ (g * E) and then transformed and residualized. The previous
1216
+ product-of-transforms approximation was wrong in general and
1217
+ catastrophically degenerate when E is also a covariate; the corrected
1218
+ statistic is certified against a direct GLS oracle in the tests.
1219
+ - Complete revision of the package (the Python-2-era flat modules of v1.0 are
1220
+ preserved under the `v1.0-legacy` git tag). mixmogam 2.0 is a modern Python
1221
+ (>=3.10) package with a NumPy/SciPy BLAS-3 mixed-model engine:
1222
+ - `LMM` fits variance components with the EMMA algorithm (vectorized
1223
+ log-delta grid + Newton refinement, REML or ML) and scans SNPs in batched
1224
+ GEMM blocks instead of one least-squares solve per SNP, with float32 scan
1225
+ arithmetic and chunked SNP streaming.
1226
+ - Kinship suite (IBS, realized relationship/IBD, scaling, LOCO, windowed
1227
+ local/global) replaces the broken `SNPsDataSet` kinship helpers.
1228
+ - Genotype/phenotype containers and IO for PLINK (bed and legacy tped),
1229
+ EIGENSTRAT, the v1 CSV formats, and HDF5 (new v2 layout, reads v1).
1230
+ - GWAS results container with Manhattan/QQ plotting, genomic-control
1231
+ inflation, Bayes factors / posterior probabilities from SNP priors,
1232
+ power/FDR analysis.
1233
+ - Stepwise multi-locus mixed model (MLMM), GxE scans, batched permutation
1234
+ thresholds, 2-df genotypic tests, and 2-kinship mixture models.
1235
+ - Test suite with a reference oracle ported from the v1 math; the old
1236
+ package had none.
1237
+ - Large-n paths: randomized top-k spectrum with a flat tail correction
1238
+ for n > 8000 (anti-conservative; see Fixed above), and a deflated
1239
+ stochastic-Lanczos-quadrature REML/ML solver that needs no O(n^3)
1240
+ eigendecomposition; batched permutation thresholds push all
1241
+ replicates through one SNP-block GEMM pass.
1242
+ - Optional Numba kernel for the fused int8-to-float block conversion
1243
+ with mean imputation (`fast` extra).
1244
+ - Benchmarks (measured suite under `benchmarks/`), docs (`docs/`), and
1245
+ runnable examples (`examples/`).
1246
+ - Simulation study on phensim coalescent data: LMM vs plain-LM genomic
1247
+ control (lambda_GC 3.56 -> 0.98 on the corrected scale; the archive
1248
+ printed 0.41 -> 1.01), SLQ fits within 1-2% of exact, top-1024 scans
1249
+ with 94/100 top-hit overlap, subsampled-kinship h2 within 0.01
1250
+ (`benchmarks/sim_study.py`, archive 20261002T194331Z-sim-study).
1251
+ - Missing genotypes are now handled by per-SNP missingness filtering plus mean
1252
+ imputation (EMMAX-consistent) so SNP blocks stay batchable (v1's per-SNP
1253
+ sample subsetting is not available).
1254
+ - Fixed v1 defects by design: `get_ibd_kinship_matrix` TypeError, the
1255
+ nonexistent `_calc_*_kinship_*` methods behind chromosome-vs-rest and
1256
+ region-split kinships, `update_kinship` stray-`self` bug, integer-division
1257
+ and precedence bugs in the QQ helpers, and modeless `h5py.File` calls.
1258
+ - Dropped the optional rpy2 / R `nearPD` Cholesky fallback (never reached on
1259
+ the eigendecomposition path) and the dead PLINK .ped parser.