wade-rnaseq 0.1.0__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.
- wade_rnaseq-0.1.0/.gitattributes +3 -0
- wade_rnaseq-0.1.0/.github/workflows/ci.yml +160 -0
- wade_rnaseq-0.1.0/.gitignore +29 -0
- wade_rnaseq-0.1.0/CONTRIBUTING.md +182 -0
- wade_rnaseq-0.1.0/Cargo.lock +440 -0
- wade_rnaseq-0.1.0/Cargo.toml +29 -0
- wade_rnaseq-0.1.0/ISSUES.md +176 -0
- wade_rnaseq-0.1.0/LICENSE +674 -0
- wade_rnaseq-0.1.0/PKG-INFO +197 -0
- wade_rnaseq-0.1.0/README.md +156 -0
- wade_rnaseq-0.1.0/docs/figures/gene.png +0 -0
- wade_rnaseq-0.1.0/docs/figures/stages.png +0 -0
- wade_rnaseq-0.1.0/docs/figures/volcano.png +0 -0
- wade_rnaseq-0.1.0/docs/manual.md +460 -0
- wade_rnaseq-0.1.0/docs/method.md +907 -0
- wade_rnaseq-0.1.0/docs/pvalue-review.md +562 -0
- wade_rnaseq-0.1.0/mamba_env.yaml +33 -0
- wade_rnaseq-0.1.0/notebooks/_mathjax-quiet.html +29 -0
- wade_rnaseq-0.1.0/notebooks/benchmark.qmd +954 -0
- wade_rnaseq-0.1.0/notebooks/demo.qmd +910 -0
- wade_rnaseq-0.1.0/notebooks/rna100k.qmd +510 -0
- wade_rnaseq-0.1.0/pyproject.toml +52 -0
- wade_rnaseq-0.1.0/rust/src/lib.rs +924 -0
- wade_rnaseq-0.1.0/src/wade/__init__.py +92 -0
- wade_rnaseq-0.1.0/src/wade/api.py +899 -0
- wade_rnaseq-0.1.0/src/wade/diagnostics.py +185 -0
- wade_rnaseq-0.1.0/src/wade/io.py +610 -0
- wade_rnaseq-0.1.0/src/wade/normalize.py +158 -0
- wade_rnaseq-0.1.0/src/wade/permutation.py +384 -0
- wade_rnaseq-0.1.0/src/wade/plotting/__init__.py +421 -0
- wade_rnaseq-0.1.0/src/wade/plotting/_matplotlib.py +240 -0
- wade_rnaseq-0.1.0/src/wade/plotting/_plotly.py +431 -0
- wade_rnaseq-0.1.0/src/wade/plotting/data.py +805 -0
- wade_rnaseq-0.1.0/src/wade/plotting/theme.py +146 -0
- wade_rnaseq-0.1.0/src/wade/pvalues.py +267 -0
- wade_rnaseq-0.1.0/src/wade/py.typed +0 -0
- wade_rnaseq-0.1.0/src/wade/quantiles.py +94 -0
- wade_rnaseq-0.1.0/src/wade/saddlepoint.py +340 -0
- wade_rnaseq-0.1.0/src/wade/stats.py +129 -0
- wade_rnaseq-0.1.0/src/wade/subset.py +429 -0
- wade_rnaseq-0.1.0/src/wade/thinning.py +306 -0
- wade_rnaseq-0.1.0/tests/conftest.py +348 -0
- wade_rnaseq-0.1.0/tests/fixtures/bh_padjust.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/ecdf_denserank.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/even_larger.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/gate_closed.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/gpd_branches.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/log2scaled.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/m2.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/main.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/nonames.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/nperms0.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/onesample.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/quantile_type7.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/refine.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/tailconc.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/tiesheavy.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/tiny.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/vecnorm.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/weighted.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/worked_example.json +1 -0
- wade_rnaseq-0.1.0/tests/fixtures/zerolib.json +1 -0
- wade_rnaseq-0.1.0/tests/portrun.py +65 -0
- wade_rnaseq-0.1.0/tests/test_chunking.py +192 -0
- wade_rnaseq-0.1.0/tests/test_core_completion.py +146 -0
- wade_rnaseq-0.1.0/tests/test_diagnostics_qc.py +95 -0
- wade_rnaseq-0.1.0/tests/test_divergences.py +105 -0
- wade_rnaseq-0.1.0/tests/test_fit_kernel.py +160 -0
- wade_rnaseq-0.1.0/tests/test_grid_cap.py +177 -0
- wade_rnaseq-0.1.0/tests/test_independent_reference.py +236 -0
- wade_rnaseq-0.1.0/tests/test_input_edges.py +87 -0
- wade_rnaseq-0.1.0/tests/test_io.py +407 -0
- wade_rnaseq-0.1.0/tests/test_kernel.py +176 -0
- wade_rnaseq-0.1.0/tests/test_layer0_fixture_integrity.py +187 -0
- wade_rnaseq-0.1.0/tests/test_layer1_grid.py +107 -0
- wade_rnaseq-0.1.0/tests/test_layer2_normalization.py +198 -0
- wade_rnaseq-0.1.0/tests/test_layer3_quantile_grids.py +129 -0
- wade_rnaseq-0.1.0/tests/test_layer4_reductions.py +173 -0
- wade_rnaseq-0.1.0/tests/test_layer5_null.py +119 -0
- wade_rnaseq-0.1.0/tests/test_layer6_pvalues.py +266 -0
- wade_rnaseq-0.1.0/tests/test_layer7_frame.py +161 -0
- wade_rnaseq-0.1.0/tests/test_layer9_validation.py +198 -0
- wade_rnaseq-0.1.0/tests/test_packaging.py +72 -0
- wade_rnaseq-0.1.0/tests/test_plotting.py +1154 -0
- wade_rnaseq-0.1.0/tests/test_promised_surface.py +112 -0
- wade_rnaseq-0.1.0/tests/test_ranking.py +113 -0
- wade_rnaseq-0.1.0/tests/test_result_memory.py +104 -0
- wade_rnaseq-0.1.0/tests/test_saddlepoint.py +296 -0
- wade_rnaseq-0.1.0/tests/test_scale.py +204 -0
- wade_rnaseq-0.1.0/tests/test_stage1_gemm.py +145 -0
- wade_rnaseq-0.1.0/tests/test_strata.py +171 -0
- wade_rnaseq-0.1.0/tests/test_subset.py +447 -0
- wade_rnaseq-0.1.0/tests/test_subset_kernel.py +376 -0
- wade_rnaseq-0.1.0/tests/test_thinning.py +240 -0
- wade_rnaseq-0.1.0/tools/bench_scaling.py +141 -0
- wade_rnaseq-0.1.0/tools/competitors.py +673 -0
- wade_rnaseq-0.1.0/tools/pvalue_meandiff.py +38 -0
- wade_rnaseq-0.1.0/tools/pvalue_saddlepoint_counts.py +47 -0
- wade_rnaseq-0.1.0/tools/pvalue_study.py +663 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
publish:
|
|
10
|
+
description: "Upload the built wheels and sdist to PyPI"
|
|
11
|
+
type: boolean
|
|
12
|
+
default: false
|
|
13
|
+
|
|
14
|
+
concurrency:
|
|
15
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
16
|
+
cancel-in-progress: true
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
test:
|
|
20
|
+
name: ${{ matrix.os }} / py${{ matrix.python }}
|
|
21
|
+
runs-on: ${{ matrix.os }}
|
|
22
|
+
strategy:
|
|
23
|
+
fail-fast: false
|
|
24
|
+
matrix:
|
|
25
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
26
|
+
python: ["3.10", "3.13"]
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: ${{ matrix.python }}
|
|
32
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
33
|
+
with:
|
|
34
|
+
components: clippy
|
|
35
|
+
- uses: Swatinem/rust-cache@v2
|
|
36
|
+
- name: Install
|
|
37
|
+
run: |
|
|
38
|
+
python -m pip install --upgrade pip
|
|
39
|
+
pip install -e ".[test,io,plot]"
|
|
40
|
+
- name: Lint the kernel
|
|
41
|
+
run: cargo clippy --release -- -D warnings
|
|
42
|
+
- name: Test
|
|
43
|
+
run: pytest -q
|
|
44
|
+
|
|
45
|
+
oldest-numpy:
|
|
46
|
+
name: numpy floor (1.24)
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
- uses: actions/setup-python@v5
|
|
51
|
+
with:
|
|
52
|
+
python-version: "3.10"
|
|
53
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
54
|
+
- run: |
|
|
55
|
+
python -m pip install --upgrade pip
|
|
56
|
+
pip install -e ".[test]"
|
|
57
|
+
pip install "numpy==1.24.*"
|
|
58
|
+
pytest -q -m "not validation"
|
|
59
|
+
|
|
60
|
+
no-kernel:
|
|
61
|
+
name: NumPy fallback (kernel removed)
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/checkout@v4
|
|
65
|
+
- uses: actions/setup-python@v5
|
|
66
|
+
with:
|
|
67
|
+
python-version: "3.12"
|
|
68
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
69
|
+
- name: Build, then delete the kernel and run the readable path
|
|
70
|
+
run: |
|
|
71
|
+
python -m pip install --upgrade pip
|
|
72
|
+
pip install -e ".[test,io]"
|
|
73
|
+
find src/wade -name "_kernel*.so" -delete
|
|
74
|
+
python -c "import wade; assert not wade.permutation.HAVE_RUST_KERNEL"
|
|
75
|
+
pytest -q -m "not kernel"
|
|
76
|
+
|
|
77
|
+
wheels:
|
|
78
|
+
name: wheel / ${{ matrix.os }} ${{ matrix.target }}
|
|
79
|
+
runs-on: ${{ matrix.os }}
|
|
80
|
+
strategy:
|
|
81
|
+
fail-fast: false
|
|
82
|
+
matrix:
|
|
83
|
+
# One row per wheel PyPI needs. abi3-py310 means a single wheel per
|
|
84
|
+
# platform covers every supported Python. Every row builds on a native
|
|
85
|
+
# runner, so every wheel is import-tested.
|
|
86
|
+
include:
|
|
87
|
+
- { os: ubuntu-latest, target: x86_64 }
|
|
88
|
+
- { os: ubuntu-24.04-arm, target: aarch64 }
|
|
89
|
+
- { os: macos-latest, target: aarch64-apple-darwin }
|
|
90
|
+
- { os: macos-15-intel, target: x86_64-apple-darwin }
|
|
91
|
+
- { os: windows-latest, target: x64 }
|
|
92
|
+
steps:
|
|
93
|
+
- uses: actions/checkout@v4
|
|
94
|
+
- uses: actions/setup-python@v5
|
|
95
|
+
with:
|
|
96
|
+
python-version: "3.12"
|
|
97
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
98
|
+
- name: Build wheel
|
|
99
|
+
uses: PyO3/maturin-action@v1
|
|
100
|
+
with:
|
|
101
|
+
command: build
|
|
102
|
+
target: ${{ matrix.target }}
|
|
103
|
+
manylinux: auto
|
|
104
|
+
args: --release --out dist
|
|
105
|
+
- name: Check the wheel installs and imports on its own
|
|
106
|
+
shell: bash
|
|
107
|
+
run: |
|
|
108
|
+
python -m pip install --upgrade pip
|
|
109
|
+
# Install the declared dependency first, then the wheel from dist and
|
|
110
|
+
# nowhere else, so nothing is pulled from PyPI under the same name.
|
|
111
|
+
pip install "numpy>=1.24"
|
|
112
|
+
pip install --no-index --find-links dist --no-deps wade-rnaseq
|
|
113
|
+
# A wheel that silently fell back to NumPy is the failure this job
|
|
114
|
+
# exists to catch.
|
|
115
|
+
python -c "import wade; assert wade.permutation.HAVE_RUST_KERNEL, \
|
|
116
|
+
'wheel has no compiled kernel'; print(wade.__version__, 'kernel ok')"
|
|
117
|
+
- uses: actions/upload-artifact@v4
|
|
118
|
+
with:
|
|
119
|
+
name: wheel-${{ matrix.os }}-${{ matrix.target }}
|
|
120
|
+
path: dist/*.whl
|
|
121
|
+
|
|
122
|
+
sdist:
|
|
123
|
+
runs-on: ubuntu-latest
|
|
124
|
+
steps:
|
|
125
|
+
- uses: actions/checkout@v4
|
|
126
|
+
- uses: actions/setup-python@v5
|
|
127
|
+
with: { python-version: "3.12" }
|
|
128
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
129
|
+
- uses: PyO3/maturin-action@v1
|
|
130
|
+
with: { command: sdist, args: --out dist }
|
|
131
|
+
- name: Check the sdist builds from scratch
|
|
132
|
+
run: |
|
|
133
|
+
python -m pip install --upgrade pip
|
|
134
|
+
pip install dist/*.tar.gz
|
|
135
|
+
python -c "import wade; assert wade.permutation.HAVE_RUST_KERNEL; print(wade.__version__)"
|
|
136
|
+
- uses: actions/upload-artifact@v4
|
|
137
|
+
with: { name: sdist, path: dist/*.tar.gz }
|
|
138
|
+
|
|
139
|
+
# Deliberately NOT triggered by a tag. A PyPI upload cannot be undone and a
|
|
140
|
+
# version number cannot be reused, so publishing is a button press: run the
|
|
141
|
+
# workflow manually with `publish` checked. Everything above still runs
|
|
142
|
+
# first, so nothing reaches PyPI that has not passed the full matrix.
|
|
143
|
+
#
|
|
144
|
+
# Needs a PyPI trusted publisher for this repo (pypi.org -> the project ->
|
|
145
|
+
# Publishing), and a GitHub environment named `pypi`. No API token.
|
|
146
|
+
publish:
|
|
147
|
+
needs: [test, oldest-numpy, no-kernel, wheels, sdist]
|
|
148
|
+
if: inputs.publish
|
|
149
|
+
runs-on: ubuntu-latest
|
|
150
|
+
environment: pypi
|
|
151
|
+
permissions:
|
|
152
|
+
id-token: write
|
|
153
|
+
steps:
|
|
154
|
+
- uses: actions/download-artifact@v4
|
|
155
|
+
with:
|
|
156
|
+
path: dist
|
|
157
|
+
pattern: "{wheel-*,sdist}"
|
|
158
|
+
merge-multiple: true
|
|
159
|
+
- run: ls -l dist
|
|
160
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
.mypy_cache/
|
|
8
|
+
.venv/
|
|
9
|
+
dist/
|
|
10
|
+
|
|
11
|
+
# The compiled kernel (rebuilt by `pip install -e .`) and Rust build output
|
|
12
|
+
src/wade/_kernel*.so
|
|
13
|
+
src/wade/_kernel*.pyd
|
|
14
|
+
/target/
|
|
15
|
+
|
|
16
|
+
# Rendered notebooks carry data-derived values; the .qmd source is the artifact.
|
|
17
|
+
# A leading underscore marks a Quarto partial, which is source.
|
|
18
|
+
notebooks/*.html
|
|
19
|
+
!notebooks/_*.html
|
|
20
|
+
notebooks/*.pdf
|
|
21
|
+
notebooks/*.quarto_ipynb
|
|
22
|
+
notebooks/*_files/
|
|
23
|
+
notebooks/.quarto/
|
|
24
|
+
.quarto/
|
|
25
|
+
|
|
26
|
+
# Editors and OS
|
|
27
|
+
.vscode/
|
|
28
|
+
.idea/
|
|
29
|
+
.DS_Store
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
How to build WADE from source, run its tests, find your way around, and the
|
|
4
|
+
things that have cost time before. The method is in
|
|
5
|
+
[`docs/method.md`](docs/method.md); the work queue is
|
|
6
|
+
[`ISSUES.md`](ISSUES.md).
|
|
7
|
+
|
|
8
|
+
## Build and test
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
git clone https://github.com/mkiyer/wade && cd wade
|
|
12
|
+
mamba env create -f mamba_env.yaml && conda activate wade # Rust toolchain included
|
|
13
|
+
pip install -e . --no-build-isolation # builds the kernel
|
|
14
|
+
pytest -q # ~20 s
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Without conda: a Python 3.10+ virtual environment plus a Rust toolchain
|
|
18
|
+
([rustup](https://rustup.rs); on Windows also the MSVC Build Tools), then
|
|
19
|
+
`pip install -e ".[all,test]"`. The `--no-build-isolation` flag only saves
|
|
20
|
+
pip fetching maturin into a temporary environment; drop it if maturin is not
|
|
21
|
+
installed. `maturin develop --release` is the fastest rebuild loop once
|
|
22
|
+
maturin is on your path.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pytest -q -m "not kernel" # if you built without the Rust toolchain
|
|
26
|
+
pytest -q -m "not validation" # skip the simulations (~8 s)
|
|
27
|
+
cargo clippy --release -- -D warnings
|
|
28
|
+
python -c "import wade; assert wade.permutation.HAVE_RUST_KERNEL"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
CI runs the suite on three OSes × two Pythons, at the declared NumPy floor,
|
|
32
|
+
with the kernel deleted, and builds every wheel on a native runner. The
|
|
33
|
+
development environment has every optional dependency installed and
|
|
34
|
+
therefore cannot see the class of bug where a test assumes one; reproduce a
|
|
35
|
+
bare install with a venv and `pip install -e ".[test,io]"`.
|
|
36
|
+
|
|
37
|
+
## Layout
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
src/wade/
|
|
41
|
+
api.py wade(), wade_contrast(), WadeResult — the one driver, over gene chunks
|
|
42
|
+
io.py as_counts(), condition(), write_results(), manifest — the data boundary
|
|
43
|
+
normalize.py tpm_like() and the continuity jitter
|
|
44
|
+
quantiles.py the descending probability grid and type-7 quantiles
|
|
45
|
+
stats.py wade_stats(): the grids, mean_shift, w1, fc
|
|
46
|
+
subset.py the bridge, the subset test, affected_fraction, direction, subset_log2_fc, the bootstrap
|
|
47
|
+
thinning.py fit_fold_change(), thin_counts(), one_count() — the count-native shift correction
|
|
48
|
+
permutation.py draw_perms(), the two nulls, kernel dispatch, permutation_space(), detectability_floor()
|
|
49
|
+
pvalues.py empirical p, GPD refinement, BH
|
|
50
|
+
saddlepoint.py stage1="saddlepoint": the exact permutation tail of a subset sum
|
|
51
|
+
diagnostics.py wade_gene(), library_qc(), subset_drivers()
|
|
52
|
+
plotting/ theme.py (every colour and label) → data.py (pure NumPy dataclasses)
|
|
53
|
+
→ _plotly.py / _matplotlib.py (thin renderers) → __init__.py (the plot_* functions)
|
|
54
|
+
rust/src/lib.rs three kernels: null_statistics, subset_null, fit_bisect
|
|
55
|
+
tests/ layered suite; tests/fixtures/*.json are permanent committed data
|
|
56
|
+
tools/ reproduction scripts: bench_scaling, pvalue_study, competitors
|
|
57
|
+
notebooks/ demo.qmd (the README's figures), benchmark.qmd, rna100k.qmd
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Plotting is an extension.** `theme.py` imports nothing, `data.py` imports
|
|
61
|
+
no backend, the renderers import their library inside their functions, and
|
|
62
|
+
`import wade` stays NumPy-only; tests pin all of that, and a test greps the
|
|
63
|
+
package for colour literals outside `theme.py`. Every figure's arrays are a
|
|
64
|
+
dataclass with a `table()`, which is the seam a third renderer would be
|
|
65
|
+
written against (about 100 lines).
|
|
66
|
+
|
|
67
|
+
## Rules
|
|
68
|
+
|
|
69
|
+
**Nothing changes a reported number.** `pytest -q` is the check, and the
|
|
70
|
+
parity ledger it prints at the end (worst relative deviation over every
|
|
71
|
+
fixture comparison) is the sharper one. Paths that do change numbers are
|
|
72
|
+
opt-in and named: `stage1="gemm"`, `stage1="saddlepoint"`,
|
|
73
|
+
`fit_backend="rust"`. `backend="auto"` must never let the machine pick the
|
|
74
|
+
answer.
|
|
75
|
+
|
|
76
|
+
**Measure before writing.** Every number in a docstring or document was
|
|
77
|
+
measured, not reasoned. Three claims written from theory turned out wrong
|
|
78
|
+
when run (the scan's argmax "obeys the arcsine law"; "the participation ratio
|
|
79
|
+
works on the log scale" without the fourth moment; "permutation gives the
|
|
80
|
+
right null for the subset test"). Build a planted-ground-truth simulation and
|
|
81
|
+
check.
|
|
82
|
+
|
|
83
|
+
**Prefer deleting to adding.** Before adding a parameter or a code path, ask
|
|
84
|
+
whether an existing one covers the case: CPM is `normalizer=1.0`; the chunked
|
|
85
|
+
driver with one chunk *is* the one-pass driver; a demonstration of why a
|
|
86
|
+
design was rejected belongs in a test, not on the entry point.
|
|
87
|
+
|
|
88
|
+
**A numerical routine checks its own answer.** Four separate bugs in the
|
|
89
|
+
saddlepoint solver each returned a plausible wrong p-value rather than an
|
|
90
|
+
error, and each was caught only because the final answer is verified against
|
|
91
|
+
both saddlepoint equations before it is returned. Anything approximating a
|
|
92
|
+
tail gets the same treatment.
|
|
93
|
+
|
|
94
|
+
## Things that will bite you
|
|
95
|
+
|
|
96
|
+
**Tolerances go on the quantity's scale, never per element on something
|
|
97
|
+
that crosses zero.** `mean_shift` is a difference of two large nearly equal
|
|
98
|
+
means, and the bridge passes through zero; a per-element relative tolerance
|
|
99
|
+
on either measures cancellation, not arithmetic. Which BLAS and which libm
|
|
100
|
+
NumPy links decides summation order and the last bit of `log2`, and one
|
|
101
|
+
machine has one of each: a `1e-12` per-element tolerance passed for a
|
|
102
|
+
fortnight on the development laptop and failed on the first CI run under a
|
|
103
|
+
different BLAS, and a `1e-15` one passed on arm64 and failed on Linux. Use
|
|
104
|
+
`conftest.assert_close_scaled`.
|
|
105
|
+
|
|
106
|
+
**Chunking is bit-identical only because every coupling is structural.**
|
|
107
|
+
The jitter is drawn once and indexed per chunk; library sizes are always one
|
|
108
|
+
full-matrix pass (a column sum's association depends on blocking); the
|
|
109
|
+
fold-change fit and the thinning consume their random streams in gene order
|
|
110
|
+
across chunks (iteration-outer, chunk-inner; case blocks then control blocks).
|
|
111
|
+
And no chunk is ever a single row: NumPy reduces a `(1, m)` array by a
|
|
112
|
+
different code path than the same row inside a larger matrix, at last-ulp
|
|
113
|
+
cost, so `gene_chunks` refuses `gene_chunk=1` and folds a one-row remainder
|
|
114
|
+
into the previous chunk.
|
|
115
|
+
|
|
116
|
+
**The fit kernel is deliberately not bitwise.** No two binomial samplers
|
|
117
|
+
consume randomness alike, so `fit_backend="rust"` fits a slightly different
|
|
118
|
+
fold change for the same seed (measured max |log ratio| 0.035). Its
|
|
119
|
+
guarantees are different ones: deterministic given the seed, chunk- and
|
|
120
|
+
thread-invariant (each gene's stream is a function of `(seed, global gene
|
|
121
|
+
index)`), and statistically held to the NumPy path.
|
|
122
|
+
|
|
123
|
+
**Composition couples every gene.** Library-size normalization divides each
|
|
124
|
+
gene by a column total every gene contributes to, so a simulation with a
|
|
125
|
+
large fraction of strongly differential genes produces nonsense null genes
|
|
126
|
+
(five strongly-up genes among 205 moved null genes' `direction` from ~0 to
|
|
127
|
+
near −1). Keep the signal fraction of the *library mass* under ~10%, or pass
|
|
128
|
+
`lib_sizes=np.ones(n)` to bypass normalization.
|
|
129
|
+
|
|
130
|
+
**The subset stage needs `B ≥ 500` to refine at all** and the empirical floor
|
|
131
|
+
is `1/(B+1)`, so a figure of the subset stage on a few hundred genes needs
|
|
132
|
+
`nperms=2000` or no subset gene clears BH. A planted 5% subset in 200 cases
|
|
133
|
+
sits near the combinatorial floor and is found at raw `p` but not at FDR;
|
|
134
|
+
that is the floor doing its job.
|
|
135
|
+
|
|
136
|
+
**The fixtures cannot be regenerated, and do not need to be.** The R
|
|
137
|
+
implementation they were generated from was deleted. `tests/fixtures/*.json`
|
|
138
|
+
are permanent committed data whose correctness is established by
|
|
139
|
+
`tests/test_independent_reference.py`, which re-derives every value from
|
|
140
|
+
`numpy.quantile`, SciPy's BH and the closed forms and is forbidden from
|
|
141
|
+
importing WADE. Do that work first if you ever add a fixture; restore one
|
|
142
|
+
from git, never rebuild. Each double is stored as a decimal *and* a hex
|
|
143
|
+
float and the loader asserts they agree.
|
|
144
|
+
|
|
145
|
+
**plotly has quiet failure modes.** `add_hline` / `add_vline` / `add_vrect`
|
|
146
|
+
with `row=`/`col=` skip subplots that do not yet contain data, so add shapes
|
|
147
|
+
after the traces. An explicit range on one axis breaks autorange on axes
|
|
148
|
+
`matches`-ed to it. `shared_yaxes="rows"` shares every row; for one row use
|
|
149
|
+
`update_yaxes(matches=...)`. `make_subplots` silently drops an empty
|
|
150
|
+
`subplot_titles` entry. In matplotlib a colour bar is an `Axes`, so
|
|
151
|
+
`len(fig.axes)` is one more than the number of panels.
|
|
152
|
+
|
|
153
|
+
**The kernel's boundary.** The kernels return full `genes × permutations`
|
|
154
|
+
null matrices, not p-values: that is what the GPD needs, and it is the only
|
|
155
|
+
place a kernel bug is cleanly separable from a p-value bug. Type 7 is
|
|
156
|
+
reimplemented in Rust term for term with the no-interpolation guard on ties;
|
|
157
|
+
accumulation is sequential; parallelism is over genes so no accumulation
|
|
158
|
+
crosses a thread. Permutations are an input, a `(n_perms, samples)` label
|
|
159
|
+
matrix, on the same argument path production uses.
|
|
160
|
+
|
|
161
|
+
## Test-suite conventions
|
|
162
|
+
|
|
163
|
+
- Layers 0–7 (`test_layer*.py`) validate the NumPy path against the fixtures
|
|
164
|
+
inside-out: intermediate quantities localize a disagreement, endpoints only
|
|
165
|
+
detect one. Stop at the first failing layer. `test_kernel.py` holds the
|
|
166
|
+
kernel to the same fixtures separately.
|
|
167
|
+
- Parity runs pin `alternative="greater"`; the package defaults to
|
|
168
|
+
two-sided.
|
|
169
|
+
- Every fixture comparison goes through `assert_close`, which records into a
|
|
170
|
+
ledger printed at the end of the run. Report the worst-case deviation, not
|
|
171
|
+
"the assertions passed". 1e-12 relative is a real bug; 1e-14 is summation
|
|
172
|
+
order.
|
|
173
|
+
- Every plotting test parametrizes over `available_backends()`, which yields
|
|
174
|
+
zero cases when neither backend is installed. Never index into it.
|
|
175
|
+
|
|
176
|
+
## Notebooks
|
|
177
|
+
|
|
178
|
+
`quarto render notebooks/demo.qmd` regenerates `docs/figures/*.png` and the
|
|
179
|
+
numbers the README quotes; it is the master source for both.
|
|
180
|
+
`benchmark.qmd` takes about 20 minutes, almost all of it one permutation
|
|
181
|
+
sweep shared by nine methods. Quarto is installed separately; the conda
|
|
182
|
+
environment supplies the Jupyter kernel.
|