afquery 0.3.2__tar.gz → 0.3.3__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 (161) hide show
  1. {afquery-0.3.2 → afquery-0.3.3}/PKG-INFO +1 -1
  2. {afquery-0.3.2 → afquery-0.3.3}/docs/advanced/debugging-results.md +2 -2
  3. {afquery-0.3.2 → afquery-0.3.3}/docs/advanced/filter-pass-tracking.md +16 -13
  4. {afquery-0.3.2 → afquery-0.3.3}/docs/advanced/multi-cohort-strategies.md +7 -3
  5. {afquery-0.3.2 → afquery-0.3.3}/docs/advanced/performance.md +2 -2
  6. {afquery-0.3.2 → afquery-0.3.3}/docs/advanced/ploidy-and-sex-chroms.md +3 -3
  7. {afquery-0.3.2 → afquery-0.3.3}/docs/faq.md +1 -1
  8. {afquery-0.3.2 → afquery-0.3.3}/docs/getting-started/concepts.md +7 -7
  9. {afquery-0.3.2 → afquery-0.3.3}/docs/getting-started/quickstart.md +1 -0
  10. {afquery-0.3.2 → afquery-0.3.3}/docs/getting-started/tutorial.md +2 -2
  11. {afquery-0.3.2 → afquery-0.3.3}/docs/getting-started/understanding-output.md +2 -2
  12. {afquery-0.3.2 → afquery-0.3.3}/docs/guides/annotate-vcf.md +1 -1
  13. {afquery-0.3.2 → afquery-0.3.3}/docs/guides/create-database.md +6 -8
  14. {afquery-0.3.2 → afquery-0.3.3}/docs/guides/sample-filtering.md +1 -1
  15. {afquery-0.3.2 → afquery-0.3.3}/docs/guides/update-database.md +1 -1
  16. {afquery-0.3.2 → afquery-0.3.3}/docs/index.md +2 -2
  17. {afquery-0.3.2 → afquery-0.3.3}/docs/reference/data-model.md +22 -17
  18. {afquery-0.3.2 → afquery-0.3.3}/docs/reference/glossary.md +2 -2
  19. {afquery-0.3.2 → afquery-0.3.3}/docs/reference/python-api.md +1 -1
  20. {afquery-0.3.2 → afquery-0.3.3}/docs/troubleshooting.md +1 -1
  21. {afquery-0.3.2 → afquery-0.3.3}/docs/use-cases/acmg-use-cases.md +1 -1
  22. {afquery-0.3.2 → afquery-0.3.3}/docs/use-cases/sex-specific-af.md +4 -4
  23. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/_version.py +2 -2
  24. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/cli.py +10 -12
  25. {afquery-0.3.2 → afquery-0.3.3}/tests/test_cli.py +28 -0
  26. {afquery-0.3.2 → afquery-0.3.3}/.dockerignore +0 -0
  27. {afquery-0.3.2 → afquery-0.3.3}/.github/workflows/ci.yml +0 -0
  28. {afquery-0.3.2 → afquery-0.3.3}/.github/workflows/docs.yml +0 -0
  29. {afquery-0.3.2 → afquery-0.3.3}/.github/workflows/release.yml +0 -0
  30. {afquery-0.3.2 → afquery-0.3.3}/.gitignore +0 -0
  31. {afquery-0.3.2 → afquery-0.3.3}/CONTRIBUTING.md +0 -0
  32. {afquery-0.3.2 → afquery-0.3.3}/Dockerfile +0 -0
  33. {afquery-0.3.2 → afquery-0.3.3}/LICENSE +0 -0
  34. {afquery-0.3.2 → afquery-0.3.3}/README.md +0 -0
  35. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/.gitignore +0 -0
  36. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/INSTALL.md +0 -0
  37. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/README.md +0 -0
  38. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/Snakefile +0 -0
  39. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/capture_kit/.gitignore +0 -0
  40. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/capture_kit/01a_assign_samples.py +0 -0
  41. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/capture_kit/01b_write_manifests.py +0 -0
  42. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/capture_kit/02_build_databases.py +0 -0
  43. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/capture_kit/03_compute_metrics.py +0 -0
  44. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/capture_kit/04_classify_acmg.py +0 -0
  45. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/capture_kit/05_plot_figures.py +0 -0
  46. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/capture_kit/README.md +0 -0
  47. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/capture_kit/Snakefile +0 -0
  48. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/capture_kit/__init__.py +0 -0
  49. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/capture_kit/beds/SureSelect_v5.bed +0 -0
  50. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/capture_kit/beds/SureSelect_v6.bed +0 -0
  51. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/capture_kit/beds/SureSelect_v7.bed +0 -0
  52. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/capture_kit/config.py +0 -0
  53. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/config.yaml +0 -0
  54. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/envs/benchmark.yaml +0 -0
  55. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/.gitignore +0 -0
  56. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/01_prepare_data.py +0 -0
  57. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/02_query_scaling.py +0 -0
  58. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/03_build.py +0 -0
  59. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/04_annotate.py +0 -0
  60. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/05_vs_bcftools.py +0 -0
  61. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/06_plot.py +0 -0
  62. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/README.md +0 -0
  63. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/Snakefile +0 -0
  64. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/collect_annotate.py +0 -0
  65. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/collect_bcftools.py +0 -0
  66. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/collect_build_perf.py +0 -0
  67. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/collect_prepare.py +0 -0
  68. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/collect_query_scaling.py +0 -0
  69. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/config.py +0 -0
  70. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/performance/config_smoke.py +0 -0
  71. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/shared/__init__.py +0 -0
  72. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/shared/config.py +0 -0
  73. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/shared/rules/download_1kg.smk +0 -0
  74. {afquery-0.3.2 → afquery-0.3.3}/benchmarks/shared/utils.py +0 -0
  75. {afquery-0.3.2 → afquery-0.3.3}/docs/advanced/benchmarking.md +0 -0
  76. {afquery-0.3.2 → afquery-0.3.3}/docs/advanced/coverage-evidence.md +0 -0
  77. {afquery-0.3.2 → afquery-0.3.3}/docs/advanced/pipeline-integration.md +0 -0
  78. {afquery-0.3.2 → afquery-0.3.3}/docs/assets/img/gap2_mixed_technologies.png +0 -0
  79. {afquery-0.3.2 → afquery-0.3.3}/docs/getting-started/installation.md +0 -0
  80. {afquery-0.3.2 → afquery-0.3.3}/docs/getting-started/motivation.md +0 -0
  81. {afquery-0.3.2 → afquery-0.3.3}/docs/getting-started/preprocessing.md +0 -0
  82. {afquery-0.3.2 → afquery-0.3.3}/docs/guides/dump-export.md +0 -0
  83. {afquery-0.3.2 → afquery-0.3.3}/docs/guides/manifest-format.md +0 -0
  84. {afquery-0.3.2 → afquery-0.3.3}/docs/guides/query.md +0 -0
  85. {afquery-0.3.2 → afquery-0.3.3}/docs/guides/variant-info.md +0 -0
  86. {afquery-0.3.2 → afquery-0.3.3}/docs/reference/cli.md +0 -0
  87. {afquery-0.3.2 → afquery-0.3.3}/docs/scripts/gen_gap2_figure.py +0 -0
  88. {afquery-0.3.2 → afquery-0.3.3}/docs/stylesheets/extra.css +0 -0
  89. {afquery-0.3.2 → afquery-0.3.3}/docs/use-cases/clinical-prioritization.md +0 -0
  90. {afquery-0.3.2 → afquery-0.3.3}/docs/use-cases/cohort-stratification.md +0 -0
  91. {afquery-0.3.2 → afquery-0.3.3}/docs/use-cases/population-specific-af.md +0 -0
  92. {afquery-0.3.2 → afquery-0.3.3}/docs/use-cases/pseudo-controls.md +0 -0
  93. {afquery-0.3.2 → afquery-0.3.3}/docs/use-cases/technology-integration.md +0 -0
  94. {afquery-0.3.2 → afquery-0.3.3}/examples/demo/README.md +0 -0
  95. {afquery-0.3.2 → afquery-0.3.3}/examples/demo/create_demo_data.py +0 -0
  96. {afquery-0.3.2 → afquery-0.3.3}/mkdocs.yml +0 -0
  97. {afquery-0.3.2 → afquery-0.3.3}/pyproject.toml +0 -0
  98. {afquery-0.3.2 → afquery-0.3.3}/recipes/afquery/meta.yaml +0 -0
  99. {afquery-0.3.2 → afquery-0.3.3}/resources/normalize_vcf.sh +0 -0
  100. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/__init__.py +0 -0
  101. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/annotate.py +0 -0
  102. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/benchmark.py +0 -0
  103. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/bitmaps.py +0 -0
  104. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/capture.py +0 -0
  105. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/cli.pyr +0 -0
  106. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/constants.py +0 -0
  107. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/database.py +0 -0
  108. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/dump.py +0 -0
  109. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/models.py +0 -0
  110. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/ploidy.py +0 -0
  111. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/preprocess/__init__.py +0 -0
  112. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/preprocess/build.py +0 -0
  113. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/preprocess/compact.py +0 -0
  114. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/preprocess/ingest.py +0 -0
  115. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/preprocess/manifest.py +0 -0
  116. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/preprocess/regions.py +0 -0
  117. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/preprocess/synth.py +0 -0
  118. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/preprocess/update.py +0 -0
  119. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/query.py +0 -0
  120. {afquery-0.3.2 → afquery-0.3.3}/src/afquery/variant_info.py +0 -0
  121. {afquery-0.3.2 → afquery-0.3.3}/tests/conftest.py +0 -0
  122. {afquery-0.3.2 → afquery-0.3.3}/tests/data/annotate_input.vcf +0 -0
  123. {afquery-0.3.2 → afquery-0.3.3}/tests/data/annotate_multi_bucket.vcf +0 -0
  124. {afquery-0.3.2 → afquery-0.3.3}/tests/data/annotate_multi_chrom.vcf +0 -0
  125. {afquery-0.3.2 → afquery-0.3.3}/tests/data/beds/wes_kit_a.bed +0 -0
  126. {afquery-0.3.2 → afquery-0.3.3}/tests/data/beds/wes_kit_b.bed +0 -0
  127. {afquery-0.3.2 → afquery-0.3.3}/tests/data/expected_results.json +0 -0
  128. {afquery-0.3.2 → afquery-0.3.3}/tests/data/manifest.tsv +0 -0
  129. {afquery-0.3.2 → afquery-0.3.3}/tests/data/vcfs/S00.vcf +0 -0
  130. {afquery-0.3.2 → afquery-0.3.3}/tests/data/vcfs/S01.vcf +0 -0
  131. {afquery-0.3.2 → afquery-0.3.3}/tests/data/vcfs/S02.vcf +0 -0
  132. {afquery-0.3.2 → afquery-0.3.3}/tests/data/vcfs/S03.vcf +0 -0
  133. {afquery-0.3.2 → afquery-0.3.3}/tests/data/vcfs/S04.vcf +0 -0
  134. {afquery-0.3.2 → afquery-0.3.3}/tests/data/vcfs/S05.vcf +0 -0
  135. {afquery-0.3.2 → afquery-0.3.3}/tests/data/vcfs/S06.vcf +0 -0
  136. {afquery-0.3.2 → afquery-0.3.3}/tests/data/vcfs/S07.vcf +0 -0
  137. {afquery-0.3.2 → afquery-0.3.3}/tests/data/vcfs/S08.vcf +0 -0
  138. {afquery-0.3.2 → afquery-0.3.3}/tests/data/vcfs/S09.vcf +0 -0
  139. {afquery-0.3.2 → afquery-0.3.3}/tests/test_annotate.py +0 -0
  140. {afquery-0.3.2 → afquery-0.3.3}/tests/test_batch.py +0 -0
  141. {afquery-0.3.2 → afquery-0.3.3}/tests/test_benchmark.py +0 -0
  142. {afquery-0.3.2 → afquery-0.3.3}/tests/test_bitmaps.py +0 -0
  143. {afquery-0.3.2 → afquery-0.3.3}/tests/test_capture.py +0 -0
  144. {afquery-0.3.2 → afquery-0.3.3}/tests/test_cli_docs_consistency.py +0 -0
  145. {afquery-0.3.2 → afquery-0.3.3}/tests/test_compact.py +0 -0
  146. {afquery-0.3.2 → afquery-0.3.3}/tests/test_constants.py +0 -0
  147. {afquery-0.3.2 → afquery-0.3.3}/tests/test_dump.py +0 -0
  148. {afquery-0.3.2 → afquery-0.3.3}/tests/test_haploid_stats.py +0 -0
  149. {afquery-0.3.2 → afquery-0.3.3}/tests/test_info.py +0 -0
  150. {afquery-0.3.2 → afquery-0.3.3}/tests/test_no_coverage.py +0 -0
  151. {afquery-0.3.2 → afquery-0.3.3}/tests/test_pass_filter.py +0 -0
  152. {afquery-0.3.2 → afquery-0.3.3}/tests/test_ploidy.py +0 -0
  153. {afquery-0.3.2 → afquery-0.3.3}/tests/test_preprocess.py +0 -0
  154. {afquery-0.3.2 → afquery-0.3.3}/tests/test_query.py +0 -0
  155. {afquery-0.3.2 → afquery-0.3.3}/tests/test_sample_filter.py +0 -0
  156. {afquery-0.3.2 → afquery-0.3.3}/tests/test_synth.py +0 -0
  157. {afquery-0.3.2 → afquery-0.3.3}/tests/test_synthetic_stats.py +0 -0
  158. {afquery-0.3.2 → afquery-0.3.3}/tests/test_update.py +0 -0
  159. {afquery-0.3.2 → afquery-0.3.3}/tests/test_update_metadata.py +0 -0
  160. {afquery-0.3.2 → afquery-0.3.3}/tests/test_variant_info.py +0 -0
  161. {afquery-0.3.2 → afquery-0.3.3}/tests/test_warnings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: afquery
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Genomic allele frequency query engine with bitmap-encoded genotypes
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -34,13 +34,13 @@ A variant you expect to find is not in the database:
34
34
  | Check | Details |
35
35
  |-------|---------|
36
36
  | Was it in the source VCFs? | AFQuery only stores variants present in ingested VCFs |
37
- | Was it FILTER=PASS? | Default ingestion skips non-PASS variants. Check with `afquery info` for `pass_only_filter` |
37
+ | Was the alt allele ever called? | A variant is stored only if at least one sample carries the alt allele or has a failed call there. A site seen only as `0/0` in every sample is genuinely absent. Note that non-PASS calls *are* kept (in `fail_bitmap`) — such a variant is not missing, it just shows `AC=0`. |
38
38
  | Multiallelic sites | AFQuery stores each ALT separately. Query the specific ALT allele, not just position |
39
39
  | Chromosome naming | Ensure consistent `chr` prefix usage |
40
40
 
41
41
  ### 4. Unexpected N_FAIL > 0
42
42
 
43
- `N_FAIL > 0` means some eligible samples had the alt allele called but with `FILTER≠PASS`. These samples are excluded from AC/AN. This is usually benign (1–2 samples), but a high N_FAIL warrants investigation:
43
+ `N_FAIL > 0` means some eligible samples had a call with `FILTER≠PASS` at this position. These samples are excluded from AC, but they remain eligible and still count in AN. This is usually benign (1–2 samples), but a high N_FAIL warrants investigation:
44
44
 
45
45
  | N_FAIL relative to n_eligible | Likely cause |
46
46
  |---|---|
@@ -11,9 +11,10 @@ In VCF format, the FILTER column indicates whether a variant call passed quality
11
11
  - `PASS` or `.` (missing) — the variant passed all filters
12
12
  - Any other value (e.g., `LowQual`, `VQSRTrancheSNP99.90to100.00`) — the variant failed one or more filters
13
13
 
14
- AFQuery default behavior:
14
+ AFQuery's default behavior:
15
15
 
16
- - **PASS-only**: only `FILTER=PASS` variants are counted in AC/AN. This is always enforced.
16
+ - Only `FILTER=PASS` calls contribute to **AC** (and therefore to **AF**). This is always enforced.
17
+ - **AN** is not affected by the filter — it counts every eligible sample at the position, failed calls included.
17
18
 
18
19
  ---
19
20
 
@@ -21,19 +22,19 @@ AFQuery default behavior:
21
22
 
22
23
  AFQuery stores a third bitmap per variant alongside `het_bitmap` and `hom_bitmap`:
23
24
 
24
- - **`fail_bitmap`** — bit set for each sample that has a non-ref genotype (AC>0) AND `FILTER≠PASS`
25
+ - **`fail_bitmap`** — bit set for each sample whose call at this site has `FILTER≠PASS`. In practice this is two cases: a sample that carries the alt allele but whose call failed a filter, and a sample with a missing genotype (`./.`) at a site that itself failed a filter.
25
26
 
26
- This means:
27
+ What this means for a sample in `fail_bitmap`:
27
28
 
28
- - A sample in `fail_bitmap` was genotyped with the alt allele but the call failed QC
29
- - Such samples are **not** counted in AC/AN (they don't affect AF)
30
- - Their count is exposed as `N_FAIL`
29
+ - Its alt alleles are **not** counted in AC, so it does not raise AF.
30
+ - It is still an eligible sample, so it **is** counted in AN. The failed call lowers AF by sitting in the denominator without contributing to the numerator.
31
+ - Its count is exposed separately as `N_FAIL`.
31
32
 
32
33
  ---
33
34
 
34
35
  ## Database Creation
35
36
 
36
- The `fail_bitmap` is always written and always tracks PASS-only ingestion:
37
+ The `fail_bitmap` is always written, regardless of build options:
37
38
 
38
39
  ```bash
39
40
  afquery create-db --manifest manifest.tsv --output-dir ./db/ --genome-build GRCh38
@@ -52,10 +53,10 @@ afquery query --db ./db/ --locus chr1:925952
52
53
  ```
53
54
 
54
55
  ```
55
- chr1:925952 G>A AC=142 AN=2742 AF=0.0518 n_eligible=1371 N_HET=138 N_HOM_ALT=2 N_HOM_REF=1231 N_FAIL=7
56
+ chr1:925952 G>A AC=142 AN=2742 AF=0.0518 n_eligible=1371 N_HET=138 N_HOM_ALT=2 N_HOM_REF=1224 N_FAIL=7
56
57
  ```
57
58
 
58
- `N_FAIL=7` means 7 eligible samples had the alt allele called but with FILTER≠PASS.
59
+ `N_FAIL=7` means 7 eligible samples had a call with FILTER≠PASS at this site. They are part of `n_eligible` (and of AN), so the genotype counts still add up: 138 + 2 + 1224 + 7 = 1371.
59
60
 
60
61
  ### Python API
61
62
 
@@ -83,21 +84,23 @@ Each carrier row shows its `filter` column as `PASS` or `FAIL`, along with sampl
83
84
 
84
85
  ## VCF Annotation
85
86
 
86
- AFQuery adds an additional INFO field to annotated VCFs:
87
+ Among the INFO fields `afquery annotate` writes, one reports the failed-call count:
87
88
 
88
89
  | Field | Type | Description |
89
90
  |-------|------|-------------|
90
- | `AFQUERY_N_FAIL` | Integer | Eligible samples with FILTER≠PASS at this variant |
91
+ | `AFQUERY_N_FAIL` | Integer | Eligible samples with `FILTER≠PASS` at this variant |
91
92
 
92
93
  ```bash
93
94
  afquery annotate --db ./db/ --input variants.vcf --output annotated.vcf
94
95
  ```
95
96
 
97
+ See [Understanding Output](../getting-started/understanding-output.md#vcf-annotation-fields) for the full set of `AFQUERY_*` fields.
98
+
96
99
  ---
97
100
 
98
101
  ## PASS-Only Enforcement
99
102
 
100
- AF reflects the quality-filtered allele frequency — the frequency of the alt allele among high-quality calls. This is appropriate for most clinical and research use cases. PASS-only ingestion is always enforced.
103
+ PASS-only counting is always enforced and cannot be turned off. It applies to the numerator only: a failed call never adds an alt allele to AC, but the sample stays in the eligible set and so remains in AN. The result is a deliberately conservative AF — a failed carrier weighs on the denominator without lifting the numerator, which is the safe direction for clinical and research use.
101
104
 
102
105
  ---
103
106
 
@@ -61,7 +61,7 @@ All samples in one database, with phenotype codes distinguishing cohorts.
61
61
  ### Manifest Design
62
62
 
63
63
  ```tsv
64
- sample_id vcf_path sex technology phenotype_codes
64
+ sample_name vcf_path sex tech_name phenotype_codes
65
65
  CARD_001 /data/card/001.vcf.gz female wgs cardiology,EUR,control
66
66
  CARD_002 /data/card/002.vcf.gz male wgs cardiology,EUR,case_HCM
67
67
  NEURO_001 /data/neuro/001.vcf.gz female wes neurology,AFR,case_epilepsy
@@ -99,7 +99,7 @@ afquery query --db ./merged/ --locus chr1:12345678 --ref C --alt T \
99
99
  |-----------|-------------|
100
100
  | Single database to maintain | Rebuilding requires all VCFs accessible |
101
101
  | Cross-cohort queries via phenotype filters | Phenotype code design must be planned upfront |
102
- | One annotation pass covers all cohorts | Adding a new cohort requires `afquery update` |
102
+ | One annotation pass covers all cohorts | Adding a new cohort requires `afquery update-db` |
103
103
  | Flexible ad-hoc stratification | Larger database, longer rebuild time |
104
104
 
105
105
  ---
@@ -134,7 +134,11 @@ Maintain both per-cohort and merged databases.
134
134
  afquery annotate --db /databases/institutional/cardiology/ \
135
135
  --input patient.vcf.gz --output step1.vcf.gz
136
136
 
137
- # Annotate against shared controls (use a different INFO prefix via Python API)
137
+ # Annotate against shared controls in a second pass
138
+ # Note: the AFQUERY_* INFO fields are overwritten on each pass — if you need
139
+ # both sets of frequencies, extract the values from step1.vcf.gz first.
140
+ afquery annotate --db /databases/shared/combined_controls/ \
141
+ --input step1.vcf.gz --output step2.vcf.gz
138
142
  ```
139
143
 
140
144
  ---
@@ -50,7 +50,7 @@ Expected scaling (50K samples, 2,500 buckets, GRCh38):
50
50
  | 32 | ~18 min | ~24× |
51
51
  | 52 | ~13 min | ~38× |
52
52
 
53
- Scaling is near-linear up to ~32 cores; beyond that, I/O and SQLite contention limit further gains.
53
+ Scaling is near-linear up to ~32 cores; beyond that, disk I/O contention limits further gains.
54
54
 
55
55
  Total RAM required: `build_threads × build_memory`
56
56
 
@@ -80,7 +80,7 @@ afquery create-db ... --threads 32 --build-threads 16 --build-memory 4GB
80
80
  graph TD
81
81
  A["Query Request<br/>chr1:925952"]
82
82
  B["Open DuckDB<br/>connection"]
83
- C["Locate Parquet<br/>bucket_0/data.parquet"]
83
+ C["Locate Parquet<br/>bucket_0.parquet"]
84
84
  D["Read rows<br/>matching pos"]
85
85
  E["Deserialize<br/>bitmaps"]
86
86
  F["Bitmap AND<br/>with eligible<br/>sample set"]
@@ -103,12 +103,12 @@ afquery query --db ./db/ --locus chrM:3243
103
103
 
104
104
  For every query result, the following identity holds:
105
105
 
106
- **N_HET + N_HOM_ALT + N_HOM_REF + N_FAIL = n_eligible**
106
+ **N_HET + N_HOM_ALT + N_HOM_REF + N_FAIL + N_NO_COVERAGE = n_eligible**
107
107
 
108
- This can be used to validate results. N_HOM_REF is the number of eligible samples that are homozygous reference (i.e., do not carry the alt allele and passed quality filters).
108
+ This can be used to validate results. N_HOM_REF is the number of eligible samples that are homozygous reference (i.e., do not carry the alt allele and passed quality filters). N_NO_COVERAGE is 0 unless a coverage-evidence filter is active — see [Coverage Evidence](coverage-evidence.md).
109
109
 
110
110
  !!! note "Mutual exclusivity"
111
- N_HET, N_HOM_ALT, N_HOM_REF, and N_FAIL are mutually exclusive. A sample with a non-ref allele but FILTER≠PASS is counted in N_FAIL only — it does not appear in N_HET or N_HOM_ALT. Likewise, N_HOM_REF counts only PASS-filtered samples.
111
+ N_HET, N_HOM_ALT, N_HOM_REF, N_FAIL, and N_NO_COVERAGE are mutually exclusive. A sample with a non-ref allele but FILTER≠PASS is counted in N_FAIL only — it does not appear in N_HET or N_HOM_ALT. Likewise, N_HOM_REF counts only PASS-filtered samples.
112
112
 
113
113
  ### chrX non-PAR
114
114
 
@@ -149,7 +149,7 @@ On chrX non-PAR positions, males contribute AN=1 and females contribute AN=2. Wh
149
149
 
150
150
  ## What does N_FAIL mean in query output?
151
151
 
152
- `N_FAIL` is the count of eligible samples that had the alt allele called but with `FILTER≠PASS`. Shown as `N_FAIL=N` in text output. These samples are not counted in AC/AN. See [FILTER=PASS Tracking](advanced/filter-pass-tracking.md).
152
+ `N_FAIL` is the count of eligible samples whose call had `FILTER≠PASS` at the position. Shown as `N_FAIL=N` in text output. These samples are excluded from AC, but they stay eligible and still count toward AN. See [FILTER=PASS Tracking](advanced/filter-pass-tracking.md).
153
153
 
154
154
  ---
155
155
 
@@ -126,13 +126,13 @@ graph TB
126
126
 
127
127
  ### Per-Variant Bitmaps
128
128
 
129
- For each variant row, AFQuery stores three [Roaring Bitmaps](https://roaringbitmap.org/):
129
+ For each variant row, AFQuery stores per-sample genotype information as [Roaring Bitmaps](https://roaringbitmap.org/). Three of them drive every query:
130
130
 
131
- - **`het_bitmap`** — bit set for each sample that is heterozygous (GT=0/1 or 1/0)
132
- - **`hom_bitmap`** — bit set for each sample that is homozygous alt (GT=1/1 or GT=1)
133
- - **`fail_bitmap`** bit set for each sample called with FILTER≠PASS
131
+ - **`het_bitmap`** — sample is heterozygous (`GT=0/1`) with `FILTER=PASS`
132
+ - **`hom_bitmap`** — sample is homozygous alt (`GT=1/1`, or `GT=1` on haploid regions) with `FILTER=PASS`
133
+ - **`fail_bitmap`** — the sample's call at this site has `FILTER≠PASS`
134
134
 
135
- Each sample has a stable integer ID (0-indexed). The bit position in the bitmap equals the sample ID.
135
+ Each sample has a stable integer ID (0-indexed). The bit position in the bitmap equals the sample ID. Databases built with coverage-quality filters carry two additional bitmaps — see [Data Model](../reference/data-model.md#parquet-schema).
136
136
 
137
137
  ### Parquet Storage
138
138
 
@@ -141,8 +141,8 @@ Bitmaps are serialized and stored in Parquet files, partitioned by chromosome an
141
141
  ```
142
142
  variants/
143
143
  chr1/
144
- bucket_0/ ← positions 0–999,999
145
- bucket_1/ ← positions 1,000,000–1,999,999
144
+ bucket_0.parquet ← positions 0–999,999
145
+ bucket_1.parquet ← positions 1,000,000–1,999,999
146
146
  ...
147
147
  chr2/
148
148
  ...
@@ -135,6 +135,7 @@ The output VCF gains INFO fields (see [Annotate a VCF](../guides/annotate-vcf.md
135
135
  | `AFQUERY_N_HOM_ALT` | A (per ALT) | Homozygous alt sample count |
136
136
  | `AFQUERY_N_HOM_REF` | A (per ALT) | Homozygous ref sample count |
137
137
  | `AFQUERY_N_FAIL` | 1 (per site) | Samples with FILTER≠PASS |
138
+ | `AFQUERY_N_NO_COVERAGE` | A (per ALT) | Eligible samples lacking coverage evidence (0 unless a coverage-evidence filter is active) |
138
139
 
139
140
  ---
140
141
 
@@ -86,7 +86,7 @@ Every query result includes the following fields:
86
86
  | **AC** | Allele count — alt allele copies in eligible samples |
87
87
  | **AN** | Allele number — total alleles examined (adjusted for coverage and ploidy) |
88
88
  | **AF** | Allele frequency — `AC / AN` |
89
- | **n_eligible** | Samples in the filtered set (before coverage or ploidy adjustments). Here, 10 = no filter applied. |
89
+ | **n_eligible** | Eligible samples at this position — those passing the metadata filters *and* covered here. Ploidy is applied afterwards, to AN. Here, 10 = no filter applied and every sample is covered. |
90
90
  | **N_HET** | Eligible samples heterozygous for the alt allele |
91
91
  | **N_HOM_ALT** | Eligible samples homozygous for the alt allele |
92
92
  | **N_HOM_REF** | Eligible samples homozygous reference |
@@ -94,7 +94,7 @@ Every query result includes the following fields:
94
94
 
95
95
  For `chr1:925952`: AN=20 = 10 diploid samples × 2 alleles; AC=6 = 4 het samples (1 copy each) + 1 hom-alt (2 copies).
96
96
 
97
- **Accounting identity:** `n_eligible = N_HET + N_HOM_ALT + N_HOM_REF + N_FAIL` always holds (10 = 4+1+5+0 ✓).
97
+ **Accounting identity:** `n_eligible = N_HET + N_HOM_ALT + N_HOM_REF + N_FAIL + N_NO_COVERAGE` always holds. `N_NO_COVERAGE` is 0 here (it only becomes non-zero with a coverage-evidence filter), so 10 = 4+1+5+0+0 ✓.
98
98
 
99
99
  ### N_FAIL in practice
100
100
 
@@ -14,8 +14,8 @@ This page explains what each field in AFQuery output means and how to interpret
14
14
  | **N_HET** | int | Number of eligible samples heterozygous for the alt allele (GT=0/1) |
15
15
  | **N_HOM_ALT** | int | Number of eligible samples homozygous for the alt allele (GT=1/1 or GT=1). Includes haploid carriers on sex chromosomes and chrM. See [Ploidy](../advanced/ploidy-and-sex-chroms.md#genotype-counting). |
16
16
  | **N_HOM_REF** | int | Number of eligible samples homozygous reference (GT=0/0 or GT=0) |
17
- | **n_eligible** | int | Number of samples in the eligible set (after sex/phenotype/tech filters) |
18
- | **N_FAIL** | int | Number of eligible samples with a non-ref allele called but FILTER≠PASS at this position. These samples are counted *only* in N_FAIL — not in N_HET, N_HOM_ALT, or N_HOM_REF. |
17
+ | **n_eligible** | int | Number of eligible samples — those passing the sex/phenotype/tech filters *and* covered at this position |
18
+ | **N_FAIL** | int | Number of eligible samples whose call at this position had FILTER≠PASS. These samples are counted *only* in N_FAIL — not in N_HET, N_HOM_ALT, or N_HOM_REF — but they stay eligible and still count toward AN. |
19
19
  | **N_NO_COVERAGE** | int | Number of eligible samples whose tech lacks coverage evidence at this position. Excluded from `N_HOM_REF` to keep AC/AN conservative. Always `0` unless a coverage-evidence filter is active. See [Coverage Evidence](../advanced/coverage-evidence.md). |
20
20
 
21
21
 
@@ -26,7 +26,7 @@ afquery annotate \
26
26
  | `AFQUERY_N_HET` | Integer | A (per ALT) | Heterozygous sample count |
27
27
  | `AFQUERY_N_HOM_ALT` | Integer | A (per ALT) | Homozygous alt sample count |
28
28
  | `AFQUERY_N_HOM_REF` | Integer | A (per ALT) | Homozygous ref sample count |
29
- | `AFQUERY_N_FAIL` | Integer | 1 (per site) | Samples with FILTER≠PASS and alt allele called. Mutually exclusive with N_HET/N_HOM_ALT/N_HOM_REF. |
29
+ | `AFQUERY_N_FAIL` | Integer | 1 (per site) | Eligible samples whose call had FILTER≠PASS. Excluded from AC, but still counted in AN. Mutually exclusive with N_HET/N_HOM_ALT/N_HOM_REF. |
30
30
  | `AFQUERY_N_NO_COVERAGE` | Integer | A (per ALT) | Eligible samples whose tech lacks coverage evidence at this position. Excluded from `N_HOM_REF` to keep AC/AN conservative. Always `0` unless a coverage-evidence filter is active. See [Coverage Evidence](../advanced/coverage-evidence.md). |
31
31
 
32
32
  !!! note "Multi-allelic sites"
@@ -27,8 +27,8 @@ afquery create-db \
27
27
 
28
28
  ## What Happens
29
29
 
30
- 1. **Ingest phase** — Each VCF is parsed with cyvcf2. Genotypes and INFO fields are written to a SQLite temporary database, one row per variant per sample.
31
- 2. **Build phase** — DuckDB reads the SQLite data, aggregates per 1-Mbp bucket, and writes Roaring Bitmap Parquet files partitioned by chromosome/bucket.
30
+ 1. **Ingest phase** — Each VCF is parsed with cyvcf2. Genotypes and quality fields are written to a temporary per-sample Parquet file, one row per variant per sample.
31
+ 2. **Build phase** — DuckDB reads the temporary Parquet files, aggregates per 1-Mbp bucket, and writes Roaring Bitmap Parquet files partitioned by chromosome and bucket.
32
32
  3. **Finalize** — `manifest.json` and `metadata.sqlite` are written to the output directory.
33
33
 
34
34
  ---
@@ -39,12 +39,10 @@ afquery create-db \
39
39
  ./db/
40
40
  ├── manifest.json # Build configuration (genome build, schema version, etc.)
41
41
  ├── metadata.sqlite # Sample/phenotype/technology/changelog metadata
42
- ├── variants/ # Hive-partitioned Parquet files
42
+ ├── variants/ # Parquet files partitioned by chromosome and bucket
43
43
  │ ├── chr1/
44
- │ │ ├── bucket_0/ # Positions 0–999,999
45
- │ │ │ └── data.parquet
46
- │ │ ├── bucket_1/ # Positions 1,000,000–1,999,999
47
- │ │ │ └── data.parquet
44
+ │ │ ├── bucket_0.parquet # Positions 0–999,999
45
+ │ │ ├── bucket_1.parquet # Positions 1,000,000–1,999,999
48
46
  │ │ └── ...
49
47
  │ ├── chr2/
50
48
  │ └── ...
@@ -95,7 +93,7 @@ afquery create-db --manifest manifest.tsv --output-dir ./db/ --genome-build GRCh
95
93
 
96
94
  ## FILTER=PASS Behavior
97
95
 
98
- By default, only variants with `FILTER=PASS` (or no FILTER field) are counted in AC/AN. Variants that fail filters are tracked in `fail_bitmap`. PASS-only ingestion is always enforced — there is currently no CLI option to change this behaviour.
96
+ Only `FILTER=PASS` calls (or calls with no FILTER field) contribute to AC, and therefore to AF. AN is not affected — it counts every eligible sample, failed calls included. Calls that fail a filter are tracked in `fail_bitmap` and surfaced as `N_FAIL`. PASS-only counting is always enforced — there is currently no CLI option to change this behaviour.
99
97
 
100
98
  See [FILTER=PASS Tracking](../advanced/filter-pass-tracking.md) for details.
101
99
 
@@ -1,6 +1,6 @@
1
1
  # Sample Filtering
2
2
 
3
- AFQuery supports flexible metadata-based selection of samples for AF computation. Filters are available on all query commands (`query`, `annotate`, `dump`).
3
+ AFQuery supports flexible metadata-based selection of samples for AF computation. Filters are available on all query commands (`query`, `annotate`, `dump`, `variant-info`).
4
4
 
5
5
  **Phenotype codes are arbitrary string labels** — you define them in your manifest. They can be ICD-10 codes, HPO terms, project tags (`control`, `pilot`), or any strings meaningful to your cohort. The filtering system does not interpret the codes — it matches them exactly as stored.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Update a Database
2
2
 
3
- `afquery update-db` supports three operations: adding new samples, removing existing samples, and compacting the database to reclaim space.
3
+ `afquery update-db` adds new samples, removes existing samples, updates sample metadata, and compacts the database to reclaim space.
4
4
 
5
5
  ### Update Timeline
6
6
 
@@ -31,7 +31,7 @@ AFQuery pre-indexes genotypes as [Roaring Bitmaps](https://roaringbitmap.org/) i
31
31
  - **Ploidy-aware** — correct AN on chrX PAR/non-PAR, chrY, and chrM.
32
32
  - **Technology-aware AN** — per-position capture BED intersection across WGS, WES kits, and panels.
33
33
  - **Carrier lookup** — list samples carrying any variant with full metadata (sex, tech, phenotypes, genotype, FILTER status).
34
- - **VCF annotation** — add `AFQUERY_AC/AN/AF/N_HET/N_HOM_ALT/N_HOM_REF/N_FAIL` INFO fields from any sample subset.
34
+ - **VCF annotation** — add `AFQUERY_AC/AN/AF/N_HET/N_HOM_ALT/N_HOM_REF/N_FAIL/N_NO_COVERAGE` INFO fields from any sample subset.
35
35
  - **Audit changelog** — every database operation is recorded for reproducibility.
36
36
 
37
37
  ---
@@ -43,7 +43,7 @@ graph TD
43
43
  A["🔍 Input VCFs<br/>single-sample"]
44
44
  B["📥 Ingest<br/>cyvcf2 reads VCFs →<br/>per-sample Parquet files emitted"]
45
45
  C["🏗️ Build<br/>DuckDB aggregates per bucket →<br/>Roaring Bitmaps → Parquet"]
46
- D["💾 Database on Disk<br/>variants/chr*/bucket_*/<br/>capture/*.pkl<br/>metadata.sqlite<br/>manifest.json"]
46
+ D["💾 Database on Disk<br/>variants/chr*/bucket_*.parquet<br/>capture/*.pkl<br/>metadata.sqlite<br/>manifest.json"]
47
47
  E["⚡ Query Engine<br/>Load bitmap → filter samples →<br/>compute AC/AN/AF<br/>~10-100ms"]
48
48
  F["📊 Annotate"]
49
49
  G["🔄 Update"]
@@ -10,12 +10,10 @@ This page documents the on-disk layout of an AFQuery database, including file fo
10
10
  <db_dir>/
11
11
  ├── manifest.json # Build configuration
12
12
  ├── metadata.sqlite # Sample/phenotype/technology/changelog metadata
13
- ├── variants/ # Hive-partitioned Parquet variant data
13
+ ├── variants/ # Parquet variant data, partitioned by chromosome and bucket
14
14
  │ ├── chr1/
15
- │ │ ├── bucket_0/ # Positions 0–999,999
16
- │ │ │ └── data.parquet
17
- │ │ ├── bucket_1/ # Positions 1,000,000–1,999,999
18
- │ │ │ └── data.parquet
15
+ │ │ ├── bucket_0.parquet # Positions 0–999,999
16
+ │ │ ├── bucket_1.parquet # Positions 1,000,000–1,999,999
19
17
  │ │ └── ...
20
18
  │ ├── chr2/
21
19
  │ └── ...
@@ -33,11 +31,13 @@ Stores the build configuration used during `create-db`.
33
31
  | Field | Type | Description |
34
32
  |-------|------|-------------|
35
33
  | `genome_build` | string | `"GRCh37"` or `"GRCh38"` |
36
- | `schema_version` | string | `"2.0"` |
37
- | `pass_only_filter` | bool | Whether FILTER=PASS was enforced during ingest |
34
+ | `version` | string | manifest.json format version |
38
35
  | `db_version` | string | User-specified version label |
36
+ | `sample_count` | int | Number of samples at build time |
37
+ | `schema_version` | string | `"2.0"`, or `"3.0"` when the database was built with coverage-quality filters |
38
+ | `pass_only_filter` | bool | Always `true` — PASS-only counting is always enforced |
39
+ | `coverage_filter` | object | The `min_dp` / `min_gq` / `min_qual` / `min_covered` thresholds used at build time (all `0` when unused) |
39
40
  | `created_at` | string | ISO 8601 timestamp |
40
- | `manifest_path` | string | Path to original manifest TSV |
41
41
 
42
42
  ---
43
43
 
@@ -102,12 +102,16 @@ Each bucket Parquet file has this schema:
102
102
  | `pos` | `uint32` | 1-based genomic position |
103
103
  | `ref` | `large_utf8` | Reference allele |
104
104
  | `alt` | `large_utf8` | Alternate allele |
105
- | `het_bitmap` | `large_binary` | Serialized Roaring Bitmap of heterozygous sample IDs |
106
- | `hom_bitmap` | `large_binary` | Serialized Roaring Bitmap of homozygous alt sample IDs |
107
- | `fail_bitmap` | `large_binary` | Serialized Roaring Bitmap of FILTER≠PASS sample IDs |
105
+ | `het_bitmap` | `large_binary` | Roaring Bitmap of heterozygous, `FILTER=PASS` sample IDs |
106
+ | `hom_bitmap` | `large_binary` | Roaring Bitmap of homozygous-alt, `FILTER=PASS` sample IDs |
107
+ | `fail_bitmap` | `large_binary` | Roaring Bitmap of sample IDs whose call has `FILTER≠PASS` at this site |
108
+ | `filtered_bitmap` | `large_binary` | Roaring Bitmap of WES non-carriers with uncertain coverage |
109
+ | `quality_pass_bitmap` | `large_binary` | Roaring Bitmap of carriers meeting the DP/GQ/QUAL thresholds |
108
110
 
109
111
  Rows are sorted by `(pos, alt)` within each bucket.
110
112
 
113
+ `filtered_bitmap` and `quality_pass_bitmap` back the [coverage-evidence](../advanced/coverage-evidence.md) feature. The columns are always present, but they are empty unless the database was built with coverage-quality filters (schema version `3.0`).
114
+
111
115
  !!! important "large_utf8 / large_binary"
112
116
  AFQuery uses `large_utf8` and `large_binary` (64-bit offsets) rather than `utf8` / `binary` (32-bit). This is required for compatibility with DuckDB's Parquet reader on large chromosomes.
113
117
 
@@ -118,9 +122,10 @@ Rows are sorted by `(pos, alt)` within each bucket.
118
122
  Bitmaps use the [Roaring Bitmap](https://roaringbitmap.org/) format, serialized by pyroaring's portable serialization.
119
123
 
120
124
  - Bit position = sample ID (0-indexed integer)
121
- - `het_bitmap`: bit set iff sample is heterozygous at this variant
122
- - `hom_bitmap`: bit set iff sample is homozygous alt at this variant
123
- - `fail_bitmap`: bit set iff sample has genotype AC>0 AND FILTER≠PASS
125
+ - `het_bitmap`: bit set iff the sample is heterozygous with `FILTER=PASS`
126
+ - `hom_bitmap`: bit set iff the sample is homozygous alt with `FILTER=PASS`
127
+ - `fail_bitmap`: bit set iff the sample's call has `FILTER≠PASS` — either a carrier whose call failed a filter, or a missing genotype (`./.`) at a failed site
128
+ - `filtered_bitmap`, `quality_pass_bitmap`: see the Parquet schema above; both are empty unless the database was built with coverage-quality filters
124
129
 
125
130
  To deserialize in Python:
126
131
  ```python
@@ -152,11 +157,11 @@ bucket_id = pos // 1_000_000
152
157
  ```
153
158
 
154
159
  !!! warning "DuckDB integer arithmetic"
155
- When computing bucket IDs in DuckDB SQL, always use:
160
+ When computing bucket IDs in DuckDB SQL, always use the integer-division operator:
156
161
  ```sql
157
- CAST(pos AS BIGINT) / 1000000
162
+ CAST(pos AS BIGINT) // 1000000
158
163
  ```
159
- Not `CAST(pos / 1000000 AS BIGINT)` — DuckDB performs float division first and rounds, producing wrong bucket IDs.
164
+ Not `CAST(pos / 1000000 AS BIGINT)` — the `/` operator does float division, and casting the rounded result afterwards produces wrong bucket IDs.
160
165
 
161
166
  ---
162
167
 
@@ -34,7 +34,7 @@ A sample that passes all query filters (sex, phenotype, technology) and has cove
34
34
 
35
35
  ## N_FAIL
36
36
 
37
- The count of eligible samples at a position where the genotype call had FILTER≠PASS in the source VCF. These samples are not counted in AC/AN. See [FILTER=PASS Tracking](../advanced/filter-pass-tracking.md).
37
+ The count of eligible samples whose call at a position had FILTER≠PASS in the source VCF. These samples are excluded from AC, but they remain eligible and so still count toward AN. See [FILTER=PASS Tracking](../advanced/filter-pass-tracking.md).
38
38
 
39
39
  ## N_NO_COVERAGE
40
40
 
@@ -66,7 +66,7 @@ A compressed bitset data structure that efficiently stores sets of integers. AFQ
66
66
 
67
67
  ## Schema Version
68
68
 
69
- The AFQuery database format version stored in `manifest.json`. Each variant stores `het_bitmap`, `hom_bitmap`, and `fail_bitmap`. See [Data Model](data-model.md).
69
+ The AFQuery database format version stored in `manifest.json` — `2.0`, or `3.0` when the database was built with coverage-quality filters. See [Data Model](data-model.md).
70
70
 
71
71
  ## Technology
72
72
 
@@ -453,7 +453,7 @@ class QueryResult:
453
453
  N_HET: int # Heterozygous count
454
454
  N_HOM_ALT: int # Homozygous alt count
455
455
  N_HOM_REF: int # Homozygous ref count
456
- N_FAIL: int # Samples with alt allele called but FILTER≠PASS
456
+ N_FAIL: int # Eligible samples whose call had FILTER≠PASS (excluded from AC, kept in AN)
457
457
  N_NO_COVERAGE: int # Eligible samples whose tech lacks evidence (excluded from N_HOM_REF)
458
458
  ```
459
459
 
@@ -77,7 +77,7 @@ AFQuery requires DuckDB to use Parquet for all temporary files. Arrow IPC is not
77
77
 
78
78
  **Cause:** A DuckDB float-division rounding bug was present in older AFQuery versions. When computing bucket IDs, `CAST(pos / 1000000 AS BIGINT)` rounds floats incorrectly (e.g., position 1,500,000 → bucket 2 instead of 1).
79
79
 
80
- **Fix:** Upgrade to the latest AFQuery version. The fix uses `CAST(pos AS BIGINT) / 1000000` for correct integer division. Rebuild the database after upgrading.
80
+ **Fix:** Upgrade to the latest AFQuery version. The fix uses `CAST(pos AS BIGINT) // 1000000` — the integer-division operator — for correct bucket IDs. Rebuild the database after upgrading.
81
81
 
82
82
  ---
83
83
 
@@ -207,7 +207,7 @@ On chrX non-PAR regions, males contribute AN=1 and females AN=2. A cohort with 8
207
207
 
208
208
  ### High N_FAIL at the Variant Site
209
209
 
210
- `N_FAIL` counts eligible samples that were genotyped with the alt allele but failed quality filters (`FILTER≠PASS`). These samples are excluded from AC/AN, so AF is not directly inflated. However, a high N_FAIL indicates the site has systematic QC problems — and warrants caution before applying ACMG criteria.
210
+ `N_FAIL` counts eligible samples whose call at the site failed quality filters (`FILTER≠PASS`). These samples are excluded from AC but stay in AN, so they cannot inflate AF — if anything they make it slightly more conservative. A high N_FAIL still indicates the site has systematic QC problems, and warrants caution before applying ACMG criteria.
211
211
 
212
212
  | N_FAIL / n_eligible | Interpretation |
213
213
  |---|---|
@@ -34,10 +34,10 @@ afquery query --db ./db/ --locus chrX:153296777
34
34
  ```
35
35
 
36
36
  ```
37
- chrX:153296777 C>T AC=15 AN=2500 AF=0.0060 n_eligible=1500 N_HET=5 N_HOM_ALT=0 N_HOM_REF=1495 N_FAIL=0
37
+ chrX:153296777 C>T AC=15 AN=2500 AF=0.0060 n_eligible=1500 N_HET=5 N_HOM_ALT=10 N_HOM_REF=1485 N_FAIL=0
38
38
  ```
39
39
 
40
- AN=2500: 500 males × 1 + 1000 females × 2 = 2500 (mixed ploidy)
40
+ AN=2500: 500 males × 1 + 1000 females × 2 = 2500 (mixed ploidy). The 10 hemizygous male carriers land in N_HOM_ALT, not N_HET — on haploid positions there is no heterozygous state.
41
41
 
42
42
  ### 2. Query males only (hemizygous frequency)
43
43
 
@@ -46,10 +46,10 @@ afquery query --db ./db/ --locus chrX:153296777 --sex male
46
46
  ```
47
47
 
48
48
  ```
49
- chrX:153296777 C>T AC=10 AN=500 AF=0.0200 n_eligible=500 N_HET=10 N_HOM_ALT=0 N_HOM_REF=490 N_FAIL=0
49
+ chrX:153296777 C>T AC=10 AN=500 AF=0.0200 n_eligible=500 N_HET=0 N_HOM_ALT=10 N_HOM_REF=490 N_FAIL=0
50
50
  ```
51
51
 
52
- Hemizygous rate: 2% (N_HET here represents hemizygous males, GT=1)
52
+ Hemizygous rate: 2%. The carriers appear in N_HOM_ALT, not N_HET — males are haploid at chrX non-PAR, so `GT=1` carriers are counted as homozygous-alt.
53
53
 
54
54
  ### 3. Query females only (carrier frequency)
55
55
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.3.2'
22
- __version_tuple__ = version_tuple = (0, 3, 2)
21
+ __version__ = version = '0.3.3'
22
+ __version_tuple__ = version_tuple = (0, 3, 3)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -74,26 +74,23 @@ def _print_results(results, fmt: str) -> None:
74
74
  "ref": r.variant.ref, "alt": r.variant.alt,
75
75
  "AC": r.AC, "AN": r.AN, "AF": r.AF, "n_eligible": r.n_samples_eligible,
76
76
  "N_HET": r.N_HET, "N_HOM_ALT": r.N_HOM_ALT, "N_HOM_REF": r.N_HOM_REF,
77
+ "N_FAIL": r.N_FAIL, "N_NO_COVERAGE": r.N_NO_COVERAGE,
77
78
  }
78
- if r.N_FAIL is not None:
79
- entry["N_FAIL"] = r.N_FAIL
80
79
  out.append(entry)
81
80
  click.echo(json.dumps(out, indent=2))
82
81
  elif fmt == "tsv":
83
- has_fail = any(r.N_FAIL is not None for r in results)
84
- header = "chrom\tpos\tref\talt\tAC\tAN\tAF\tn_eligible\tN_HET\tN_HOM_ALT\tN_HOM_REF"
85
- if has_fail:
86
- header += "\tN_FAIL"
82
+ header = (
83
+ "chrom\tpos\tref\talt\tAC\tAN\tAF\tn_eligible\t"
84
+ "N_HET\tN_HOM_ALT\tN_HOM_REF\tN_FAIL\tN_NO_COVERAGE"
85
+ )
87
86
  click.echo(header)
88
87
  for r in results:
89
88
  af = f"{r.AF:.6f}" if r.AF is not None else "NA"
90
89
  line = (
91
90
  f"{r.variant.chrom}\t{r.variant.pos}\t{r.variant.ref}\t"
92
91
  f"{r.variant.alt}\t{r.AC}\t{r.AN}\t{af}\t{r.n_samples_eligible}\t"
93
- f"{r.N_HET}\t{r.N_HOM_ALT}\t{r.N_HOM_REF}"
92
+ f"{r.N_HET}\t{r.N_HOM_ALT}\t{r.N_HOM_REF}\t{r.N_FAIL}\t{r.N_NO_COVERAGE}"
94
93
  )
95
- if has_fail:
96
- line += f"\t{r.N_FAIL if r.N_FAIL is not None else '?'}"
97
94
  click.echo(line)
98
95
  else: # text
99
96
  if not results:
@@ -101,11 +98,11 @@ def _print_results(results, fmt: str) -> None:
101
98
  return
102
99
  for r in results:
103
100
  af = f"{r.AF:.4f}" if r.AF is not None else "NA"
104
- fail_str = f" N_FAIL={r.N_FAIL}" if r.N_FAIL is not None else ""
105
101
  click.echo(
106
102
  f"{r.variant.chrom}:{r.variant.pos} {r.variant.ref}>{r.variant.alt} "
107
103
  f"AC={r.AC} AN={r.AN} AF={af} n_eligible={r.n_samples_eligible} "
108
- f"N_HET={r.N_HET} N_HOM_ALT={r.N_HOM_ALT} N_HOM_REF={r.N_HOM_REF}{fail_str}"
104
+ f"N_HET={r.N_HET} N_HOM_ALT={r.N_HOM_ALT} N_HOM_REF={r.N_HOM_REF} "
105
+ f"N_FAIL={r.N_FAIL} N_NO_COVERAGE={r.N_NO_COVERAGE}"
109
106
  )
110
107
 
111
108
 
@@ -324,7 +321,8 @@ def annotate(db, input_vcf, output_vcf, phenotype, sex, tech, threads, verbose,
324
321
  AFQUERY_N_HET heterozygous carrier count (per ALT)
325
322
  AFQUERY_N_HOM_ALT homozygous alt count (per ALT)
326
323
  AFQUERY_N_HOM_REF homozygous ref count (per ALT)
327
- AFQUERY_N_FAIL samples with FILTER!=PASS
324
+ AFQUERY_N_FAIL samples with FILTER!=PASS (per site)
325
+ AFQUERY_N_NO_COVERAGE eligible samples lacking coverage evidence (per ALT)
328
326
  """
329
327
  if no_warn:
330
328
  import warnings
@@ -435,6 +435,34 @@ def test_query_tsv_with_fail_col(runner, test_db):
435
435
  assert "N_FAIL" in lines[0]
436
436
 
437
437
 
438
+ def test_query_output_includes_n_no_coverage_all_formats(runner, test_db):
439
+ """N_FAIL and N_NO_COVERAGE appear in text/tsv/json output, and TSV columns line up."""
440
+ base = ["query", "--db", test_db, "--locus", "chr1:1500"]
441
+
442
+ text = runner.invoke(cli, base)
443
+ assert text.exit_code == 0
444
+ assert "N_FAIL=" in text.output
445
+ assert "N_NO_COVERAGE=" in text.output
446
+
447
+ tsv = runner.invoke(cli, base + ["--format", "tsv"])
448
+ assert tsv.exit_code == 0
449
+ tsv_lines = tsv.output.strip().splitlines()
450
+ assert len(tsv_lines) >= 2
451
+ header = tsv_lines[0].split("\t")
452
+ assert "N_FAIL" in header
453
+ assert header[-1] == "N_NO_COVERAGE"
454
+ for row in tsv_lines[1:]:
455
+ assert len(row.split("\t")) == len(header)
456
+
457
+ js = runner.invoke(cli, base + ["--format", "json"])
458
+ assert js.exit_code == 0
459
+ data = json.loads(js.output)
460
+ assert len(data) >= 1
461
+ for entry in data:
462
+ assert isinstance(entry["N_FAIL"], int)
463
+ assert isinstance(entry["N_NO_COVERAGE"], int)
464
+
465
+
438
466
  # --- afquery annotate: verbose flag ---
439
467
 
440
468
  def test_annotate_verbose(runner, test_db, tmp_path):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes