migec 2.2.0__tar.gz → 2.2.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 (213) hide show
  1. {migec-2.2.0 → migec-2.2.1}/CHANGELOG.md +82 -0
  2. {migec-2.2.0 → migec-2.2.1}/CLAUDE.md +25 -2
  3. {migec-2.2.0 → migec-2.2.1}/PKG-INFO +123 -17
  4. {migec-2.2.0 → migec-2.2.1}/README.md +122 -16
  5. {migec-2.2.0 → migec-2.2.1}/ROADMAP.md +15 -9
  6. {migec-2.2.0 → migec-2.2.1}/SOURCES.md +47 -0
  7. migec-2.2.1/assets/ctdna_titration.tsv +35 -0
  8. migec-2.2.1/assets/ctdna_titration_runs.tsv +101 -0
  9. {migec-2.2.0 → migec-2.2.1}/docs/downstream.rst +4 -0
  10. {migec-2.2.0 → migec-2.2.1}/docs/examples.rst +24 -6
  11. {migec-2.2.0 → migec-2.2.1}/docs/index.rst +4 -0
  12. migec-2.2.1/docs/nextflow.rst +204 -0
  13. {migec-2.2.0 → migec-2.2.1}/docs/reference.rst +3 -2
  14. migec-2.2.1/docs/variants.rst +316 -0
  15. {migec-2.2.0 → migec-2.2.1}/include/migec/version.hpp +1 -1
  16. migec-2.2.1/integrations/nextflow/README.md +89 -0
  17. migec-2.2.1/integrations/nextflow/main.nf +63 -0
  18. migec-2.2.1/integrations/nextflow/modules/downstream/align/environment.yml +8 -0
  19. migec-2.2.1/integrations/nextflow/modules/downstream/align/main.nf +74 -0
  20. migec-2.2.1/integrations/nextflow/modules/downstream/arda/environment.yml +8 -0
  21. migec-2.2.1/integrations/nextflow/modules/downstream/arda/main.nf +56 -0
  22. migec-2.2.1/integrations/nextflow/modules/downstream/callvariants/environment.yml +7 -0
  23. migec-2.2.1/integrations/nextflow/modules/downstream/callvariants/main.nf +81 -0
  24. {migec-2.2.0 → migec-2.2.1}/integrations/nextflow/nextflow.config +25 -0
  25. migec-2.2.1/integrations/nextflow/subworkflows/migec_airr/main.nf +35 -0
  26. migec-2.2.1/integrations/nextflow/subworkflows/migec_ctdna/main.nf +46 -0
  27. migec-2.2.1/integrations/slurm/README.md +88 -0
  28. migec-2.2.1/integrations/slurm/migec_array.sbatch +69 -0
  29. migec-2.2.1/integrations/slurm/migec_sample.sbatch +81 -0
  30. migec-2.2.1/integrations/slurm/samples.tsv +6 -0
  31. migec-2.2.1/notebooks/README.md +63 -0
  32. migec-2.2.1/notebooks/airr_repertoire.py +221 -0
  33. {migec-2.2.0 → migec-2.2.1}/notebooks/barcode_space.py +1 -1
  34. migec-2.2.1/notebooks/ctdna_variants.py +274 -0
  35. migec-2.2.1/notebooks/exome_capture.py +189 -0
  36. {migec-2.2.0 → migec-2.2.1}/notebooks/platforms.py +1 -1
  37. {migec-2.2.0 → migec-2.2.1}/notebooks/refine_diagnostics.py +1 -1
  38. {migec-2.2.0 → migec-2.2.1}/pyproject.toml +1 -1
  39. {migec-2.2.0 → migec-2.2.1}/python/migec/__init__.py +1 -1
  40. migec-2.2.1/scripts/ctdna_titration.py +366 -0
  41. migec-2.2.1/scripts/sra_fetch.py +307 -0
  42. {migec-2.2.0 → migec-2.2.1}/skills/migec/SKILL.md +70 -1
  43. migec-2.2.1/tests/unit/__init__.py +0 -0
  44. migec-2.2.1/tests/unit/test_ctdna_tools.py +194 -0
  45. migec-2.2.0/docs/nextflow.rst +0 -116
  46. migec-2.2.0/integrations/nextflow/README.md +0 -36
  47. {migec-2.2.0 → migec-2.2.1}/.github/workflows/ci.yml +0 -0
  48. {migec-2.2.0 → migec-2.2.1}/.github/workflows/docs.yml +0 -0
  49. {migec-2.2.0 → migec-2.2.1}/.github/workflows/publish.yml +0 -0
  50. {migec-2.2.0 → migec-2.2.1}/.github/workflows/testpypi.yml +0 -0
  51. {migec-2.2.0 → migec-2.2.1}/.gitignore +0 -0
  52. {migec-2.2.0 → migec-2.2.1}/CMakeLists.txt +0 -0
  53. {migec-2.2.0 → migec-2.2.1}/LICENSE +0 -0
  54. {migec-2.2.0 → migec-2.2.1}/assets/PBMC.cell_rank.tsv +0 -0
  55. {migec-2.2.0 → migec-2.2.1}/assets/PBMC.sizes.tsv +0 -0
  56. {migec-2.2.0 → migec-2.2.1}/assets/PBMC.umi_errors.tsv +0 -0
  57. {migec-2.2.0 → migec-2.2.1}/assets/SRR1763769.mig.tsv +0 -0
  58. {migec-2.2.0 → migec-2.2.1}/assets/assemble.coverage.tsv +0 -0
  59. {migec-2.2.0 → migec-2.2.1}/assets/assemble.quality_by_depth.tsv +0 -0
  60. {migec-2.2.0 → migec-2.2.1}/assets/assemble_coverage.gp +0 -0
  61. {migec-2.2.0 → migec-2.2.1}/assets/assemble_coverage.svg +0 -0
  62. {migec-2.2.0 → migec-2.2.1}/assets/benchmark_threads.gp +0 -0
  63. {migec-2.2.0 → migec-2.2.1}/assets/benchmark_threads.json +0 -0
  64. {migec-2.2.0 → migec-2.2.1}/assets/benchmark_threads.svg +0 -0
  65. {migec-2.2.0 → migec-2.2.1}/assets/benchmark_threads.tsv +0 -0
  66. {migec-2.2.0 → migec-2.2.1}/assets/cell_rank.gp +0 -0
  67. {migec-2.2.0 → migec-2.2.1}/assets/cell_rank.svg +0 -0
  68. {migec-2.2.0 → migec-2.2.1}/assets/consensus_error.gp +0 -0
  69. {migec-2.2.0 → migec-2.2.1}/assets/consensus_error.svg +0 -0
  70. {migec-2.2.0 → migec-2.2.1}/assets/consensus_layout.gp +0 -0
  71. {migec-2.2.0 → migec-2.2.1}/assets/consensus_layout.svg +0 -0
  72. {migec-2.2.0 → migec-2.2.1}/assets/consensus_quality.gp +0 -0
  73. {migec-2.2.0 → migec-2.2.1}/assets/consensus_quality.svg +0 -0
  74. {migec-2.2.0 → migec-2.2.1}/assets/mig_size_spectrum.gp +0 -0
  75. {migec-2.2.0 → migec-2.2.1}/assets/mig_size_spectrum.svg +0 -0
  76. {migec-2.2.0 → migec-2.2.1}/assets/mig_size_zipf.gp +0 -0
  77. {migec-2.2.0 → migec-2.2.1}/assets/mig_size_zipf.svg +0 -0
  78. {migec-2.2.0 → migec-2.2.1}/assets/pipeline.dot +0 -0
  79. {migec-2.2.0 → migec-2.2.1}/assets/pipeline.svg +0 -0
  80. {migec-2.2.0 → migec-2.2.1}/assets/umi_error_children.gp +0 -0
  81. {migec-2.2.0 → migec-2.2.1}/assets/umi_error_children.svg +0 -0
  82. {migec-2.2.0 → migec-2.2.1}/assets/umi_error_rate.gp +0 -0
  83. {migec-2.2.0 → migec-2.2.1}/assets/umi_error_rate.svg +0 -0
  84. {migec-2.2.0 → migec-2.2.1}/docs/Makefile +0 -0
  85. {migec-2.2.0 → migec-2.2.1}/docs/assemble.rst +0 -0
  86. {migec-2.2.0 → migec-2.2.1}/docs/barcode_space.rst +0 -0
  87. {migec-2.2.0 → migec-2.2.1}/docs/checkout.rst +0 -0
  88. {migec-2.2.0 → migec-2.2.1}/docs/commands.rst +0 -0
  89. {migec-2.2.0 → migec-2.2.1}/docs/conf.py +0 -0
  90. {migec-2.2.0 → migec-2.2.1}/docs/formats.rst +0 -0
  91. {migec-2.2.0 → migec-2.2.1}/docs/fragmented.rst +0 -0
  92. {migec-2.2.0 → migec-2.2.1}/docs/grouping.rst +0 -0
  93. {migec-2.2.0 → migec-2.2.1}/docs/installation.rst +0 -0
  94. {migec-2.2.0 → migec-2.2.1}/docs/layouts.rst +0 -0
  95. {migec-2.2.0 → migec-2.2.1}/docs/method.rst +0 -0
  96. {migec-2.2.0 → migec-2.2.1}/docs/nulls.rst +0 -0
  97. {migec-2.2.0 → migec-2.2.1}/docs/performance.rst +0 -0
  98. {migec-2.2.0 → migec-2.2.1}/docs/plots.rst +0 -0
  99. {migec-2.2.0 → migec-2.2.1}/docs/quality_floor.rst +0 -0
  100. {migec-2.2.0 → migec-2.2.1}/docs/refine.rst +0 -0
  101. {migec-2.2.0 → migec-2.2.1}/docs/roadmap.rst +0 -0
  102. {migec-2.2.0 → migec-2.2.1}/docs/subsample.rst +0 -0
  103. {migec-2.2.0 → migec-2.2.1}/docs/suggest.rst +0 -0
  104. {migec-2.2.0 → migec-2.2.1}/docs/umi_errors.rst +0 -0
  105. {migec-2.2.0 → migec-2.2.1}/docs/umi_statistics.rst +0 -0
  106. {migec-2.2.0 → migec-2.2.1}/docs/validation.rst +0 -0
  107. {migec-2.2.0 → migec-2.2.1}/include/migec/assemble.hpp +0 -0
  108. {migec-2.2.0 → migec-2.2.1}/include/migec/checkout.hpp +0 -0
  109. {migec-2.2.0 → migec-2.2.1}/include/migec/consensus.hpp +0 -0
  110. {migec-2.2.0 → migec-2.2.1}/include/migec/fastq.hpp +0 -0
  111. {migec-2.2.0 → migec-2.2.1}/include/migec/mig_record.hpp +0 -0
  112. {migec-2.2.0 → migec-2.2.1}/include/migec/parallel.hpp +0 -0
  113. {migec-2.2.0 → migec-2.2.1}/include/migec/pattern.hpp +0 -0
  114. {migec-2.2.0 → migec-2.2.1}/include/migec/refine.hpp +0 -0
  115. {migec-2.2.0 → migec-2.2.1}/include/migec/resource.hpp +0 -0
  116. {migec-2.2.0 → migec-2.2.1}/include/migec/subsample.hpp +0 -0
  117. {migec-2.2.0 → migec-2.2.1}/include/migec/suggest.hpp +0 -0
  118. {migec-2.2.0 → migec-2.2.1}/include/migec/types.hpp +0 -0
  119. {migec-2.2.0 → migec-2.2.1}/include/migec/umi_stats.hpp +0 -0
  120. {migec-2.2.0 → migec-2.2.1}/include/migec/whitelist.hpp +0 -0
  121. /migec-2.2.0/tests/__init__.py → /migec-2.2.1/integrations/nextflow/assets/NO_FILE +0 -0
  122. {migec-2.2.0 → migec-2.2.1}/integrations/nextflow/modules/migec/assemble/main.nf +0 -0
  123. {migec-2.2.0 → migec-2.2.1}/integrations/nextflow/modules/migec/assemble/meta.yml +0 -0
  124. {migec-2.2.0 → migec-2.2.1}/integrations/nextflow/modules/migec/checkout/main.nf +0 -0
  125. {migec-2.2.0 → migec-2.2.1}/integrations/nextflow/modules/migec/checkout/meta.yml +0 -0
  126. {migec-2.2.0 → migec-2.2.1}/integrations/nextflow/modules/migec/environment.yml +0 -0
  127. {migec-2.2.0 → migec-2.2.1}/integrations/nextflow/modules/migec/plot/main.nf +0 -0
  128. {migec-2.2.0 → migec-2.2.1}/integrations/nextflow/modules/migec/refine/main.nf +0 -0
  129. {migec-2.2.0 → migec-2.2.1}/integrations/nextflow/modules/migec/refine/meta.yml +0 -0
  130. {migec-2.2.0 → migec-2.2.1}/integrations/nextflow/subworkflows/migec/main.nf +0 -0
  131. {migec-2.2.0 → migec-2.2.1}/project/README.md +0 -0
  132. {migec-2.2.0 → migec-2.2.1}/project/design-assemble.md +0 -0
  133. {migec-2.2.0 → migec-2.2.1}/project/design-benchmarks.md +0 -0
  134. {migec-2.2.0 → migec-2.2.1}/project/design-checkout.md +0 -0
  135. {migec-2.2.0 → migec-2.2.1}/project/design-io-interop.md +0 -0
  136. {migec-2.2.0 → migec-2.2.1}/project/design-refine.md +0 -0
  137. {migec-2.2.0 → migec-2.2.1}/project/design-repo.md +0 -0
  138. {migec-2.2.0 → migec-2.2.1}/project/review-algorithms.md +0 -0
  139. {migec-2.2.0 → migec-2.2.1}/project/review-scope.md +0 -0
  140. {migec-2.2.0 → migec-2.2.1}/python/migec/assemble.py +0 -0
  141. {migec-2.2.0 → migec-2.2.1}/python/migec/checkout.py +0 -0
  142. {migec-2.2.0 → migec-2.2.1}/python/migec/cli.py +0 -0
  143. {migec-2.2.0 → migec-2.2.1}/python/migec/plot.py +0 -0
  144. {migec-2.2.0 → migec-2.2.1}/python/migec/refine.py +0 -0
  145. {migec-2.2.0 → migec-2.2.1}/python/migec/sheet.py +0 -0
  146. {migec-2.2.0 → migec-2.2.1}/python/migec/subsample.py +0 -0
  147. {migec-2.2.0 → migec-2.2.1}/python/migec/suggest.py +0 -0
  148. {migec-2.2.0 → migec-2.2.1}/scripts/benchmark_threads.py +0 -0
  149. {migec-2.2.0 → migec-2.2.1}/scripts/collision_check.py +0 -0
  150. {migec-2.2.0 → migec-2.2.1}/scripts/compare_calib.py +0 -0
  151. {migec-2.2.0 → migec-2.2.1}/scripts/correction_accuracy.py +0 -0
  152. {migec-2.2.0 → migec-2.2.1}/scripts/diagnose.py +0 -0
  153. {migec-2.2.0 → migec-2.2.1}/scripts/example_figures.py +0 -0
  154. {migec-2.2.0 → migec-2.2.1}/scripts/permutation_nulls.py +0 -0
  155. {migec-2.2.0 → migec-2.2.1}/scripts/quality_floor.py +0 -0
  156. {migec-2.2.0 → migec-2.2.1}/scripts/read_start_dispersion.py +0 -0
  157. {migec-2.2.0 → migec-2.2.1}/scripts/spikein_ratio.py +0 -0
  158. {migec-2.2.0 → migec-2.2.1}/setup.sh +0 -0
  159. {migec-2.2.0 → migec-2.2.1}/src/_bindings.cpp +0 -0
  160. {migec-2.2.0 → migec-2.2.1}/src/assemble.cpp +0 -0
  161. {migec-2.2.0 → migec-2.2.1}/src/checkout.cpp +0 -0
  162. {migec-2.2.0 → migec-2.2.1}/src/consensus.cpp +0 -0
  163. {migec-2.2.0 → migec-2.2.1}/src/fastq.cpp +0 -0
  164. {migec-2.2.0 → migec-2.2.1}/src/mig_record.cpp +0 -0
  165. {migec-2.2.0 → migec-2.2.1}/src/pattern.cpp +0 -0
  166. {migec-2.2.0 → migec-2.2.1}/src/refine.cpp +0 -0
  167. {migec-2.2.0 → migec-2.2.1}/src/resource.cpp +0 -0
  168. {migec-2.2.0 → migec-2.2.1}/src/subsample.cpp +0 -0
  169. {migec-2.2.0 → migec-2.2.1}/src/suggest.cpp +0 -0
  170. {migec-2.2.0 → migec-2.2.1}/src/types.cpp +0 -0
  171. {migec-2.2.0 → migec-2.2.1}/src/umi_stats.cpp +0 -0
  172. {migec-2.2.0 → migec-2.2.1}/src/whitelist.cpp +0 -0
  173. {migec-2.2.0/tests/benchmark → migec-2.2.1/tests}/__init__.py +0 -0
  174. {migec-2.2.0/tests/realworld → migec-2.2.1/tests/benchmark}/__init__.py +0 -0
  175. {migec-2.2.0 → migec-2.2.1}/tests/benchmark/test_assemble_speed.py +0 -0
  176. {migec-2.2.0 → migec-2.2.1}/tests/benchmark/test_checkout_speed.py +0 -0
  177. {migec-2.2.0 → migec-2.2.1}/tests/benchmark/test_refine_speed.py +0 -0
  178. {migec-2.2.0 → migec-2.2.1}/tests/conftest.py +0 -0
  179. {migec-2.2.0 → migec-2.2.1}/tests/cpp/doctest.h +0 -0
  180. {migec-2.2.0 → migec-2.2.1}/tests/cpp/test_checkout.cpp +0 -0
  181. {migec-2.2.0 → migec-2.2.1}/tests/cpp/test_checkout_run.cpp +0 -0
  182. {migec-2.2.0 → migec-2.2.1}/tests/cpp/test_consensus.cpp +0 -0
  183. {migec-2.2.0 → migec-2.2.1}/tests/cpp/test_fastq.cpp +0 -0
  184. {migec-2.2.0 → migec-2.2.1}/tests/cpp/test_mig_record.cpp +0 -0
  185. {migec-2.2.0 → migec-2.2.1}/tests/cpp/test_parallel_stages.cpp +0 -0
  186. {migec-2.2.0 → migec-2.2.1}/tests/cpp/test_pattern.cpp +0 -0
  187. {migec-2.2.0 → migec-2.2.1}/tests/cpp/test_types.cpp +0 -0
  188. {migec-2.2.0 → migec-2.2.1}/tests/cpp/test_umi_stats.cpp +0 -0
  189. {migec-2.2.0 → migec-2.2.1}/tests/cpp/test_whitelist.cpp +0 -0
  190. {migec-2.2.0 → migec-2.2.1}/tests/data/migec_spikein_truth.tsv +0 -0
  191. {migec-2.2.0/tests/synthetic → migec-2.2.1/tests/realworld}/__init__.py +0 -0
  192. {migec-2.2.0/tests/unit → migec-2.2.1/tests/synthetic}/__init__.py +0 -0
  193. {migec-2.2.0 → migec-2.2.1}/tests/synthetic/_sim.py +0 -0
  194. {migec-2.2.0 → migec-2.2.1}/tests/synthetic/test_assemble.py +0 -0
  195. {migec-2.2.0 → migec-2.2.1}/tests/synthetic/test_barcode_space.py +0 -0
  196. {migec-2.2.0 → migec-2.2.1}/tests/synthetic/test_checkout_end_to_end.py +0 -0
  197. {migec-2.2.0 → migec-2.2.1}/tests/synthetic/test_dual_end.py +0 -0
  198. {migec-2.2.0 → migec-2.2.1}/tests/synthetic/test_grouping_accuracy.py +0 -0
  199. {migec-2.2.0 → migec-2.2.1}/tests/synthetic/test_nulls.py +0 -0
  200. {migec-2.2.0 → migec-2.2.1}/tests/synthetic/test_quality_floor.py +0 -0
  201. {migec-2.2.0 → migec-2.2.1}/tests/synthetic/test_refine.py +0 -0
  202. {migec-2.2.0 → migec-2.2.1}/tests/synthetic/test_sim.py +0 -0
  203. {migec-2.2.0 → migec-2.2.1}/tests/synthetic/test_subsample.py +0 -0
  204. {migec-2.2.0 → migec-2.2.1}/tests/synthetic/test_suggest.py +0 -0
  205. {migec-2.2.0 → migec-2.2.1}/tests/synthetic/test_thread_invariance.py +0 -0
  206. {migec-2.2.0 → migec-2.2.1}/tests/synthetic/test_umi_errors.py +0 -0
  207. {migec-2.2.0 → migec-2.2.1}/tests/unit/test_bindings.py +0 -0
  208. {migec-2.2.0 → migec-2.2.1}/tests/unit/test_compare_calib.py +0 -0
  209. {migec-2.2.0 → migec-2.2.1}/tests/unit/test_core.py +0 -0
  210. {migec-2.2.0 → migec-2.2.1}/tests/unit/test_downstream.py +0 -0
  211. {migec-2.2.0 → migec-2.2.1}/tests/unit/test_plot.py +0 -0
  212. {migec-2.2.0 → migec-2.2.1}/tests/unit/test_sheet_and_cli.py +0 -0
  213. {migec-2.2.0 → migec-2.2.1}/tests/unit/test_spikein_ratio.py +0 -0
@@ -4,6 +4,88 @@ Hand-written and prose-heavy: each entry says what changed and, where it matters
4
4
  prevents. Releases before 2.0.0 are the Groovy MIGEC and are described by their git tags on the
5
5
  `legacy-v1` branch.
6
6
 
7
+ ## 2.2.1 — 2026-08-14
8
+
9
+ Documentation, pipelines and tooling. Note: **the wheel is unchanged from 2.2.0** — no command,
10
+ flag, output file or number that migec itself produces is different. Everything below is in the
11
+ repository: the SLURM templates, the Nextflow downstream layer, two more notebooks, a new docs
12
+ page, and the two scripts behind it.
13
+
14
+ ### Pipelines you can actually run
15
+
16
+ `integrations/slurm/` is new: `migec_sample.sbatch` (one sample, environment-driven) and
17
+ `migec_array.sbatch` (one array task per sample-sheet row), with an example sheet and a README that
18
+ sizes the request per stage. Both run as **ordinary bash without SLURM** — every SLURM variable has
19
+ a fallback — which is how they are tested here and how a layout should be checked before a cohort
20
+ is queued. Note: array task 1 is the first *data* row; the header is skipped rather than counted,
21
+ so the range is `1-(rows - 1)`, and an `--array=0-N` runs a task that reads the header as a sample.
22
+
23
+ `integrations/nextflow/` gained a runnable entry point (`--mode consensus | ctdna | airr`) and a
24
+ downstream layer: `modules/downstream/{align,callvariants,arda}` plus `subworkflows/migec_ctdna`
25
+ and `subworkflows/migec_airr`. The align module **checks its own output** — if no `MI:Z:` tag
26
+ reaches the BAM it exits non-zero and names the flag each aligner needs, because that failure is
27
+ otherwise silent and surfaces much later as an inexplicably untagged BAM.
28
+
29
+ ### Notebooks
30
+
31
+ `notebooks/README.md`, plus `exome_capture.py` (why coordinate deduplication undercounts a capture
32
+ panel) and `airr_repertoire.py` (clonotype counts from reads against molecules). Both are
33
+ simulated, so both run offline against a known answer. Never: the PEP 723 headers were
34
+ **under-declared** — altair renders a polars frame through pyarrow and then pandas, so a clean
35
+ machine failed three imports deep with an error naming neither. Every header is now what the
36
+ notebook actually needs, and all of them were executed to prove it.
37
+
38
+ ### Which variant caller, and the count that decides it
39
+
40
+ `docs/variants.rst` answers the question people arrive with — *rare variants out of ctDNA, what do
41
+ I run after `assemble`?* — and extends `docs/downstream.rst`'s transport-vs-deduplicate rule one
42
+ level: `Mutect2`, `LoFreq`, `FreeBayes`, `VarDict` and `bcftools` read a BAM and ignore `RX`, so
43
+ they **compose**; `UMI-VarCal`, `UMIErrorCorrect`, `DREAMS-vc` and `Shearwater` group and consense
44
+ themselves, so they **replace** a stage. Never: do not apply a UMI-aware caller's family-size
45
+ filter to a consensus — after `assemble` every family has size 1 by construction, so
46
+ `--min-family-size 3` discards the entire library and reports zero variants without erroring.
47
+
48
+ The caller ranking is quoted from Maruzani et al. 2024 (BMC Genomics,
49
+ [doi:10.1186/s12864-024-10737-w](https://doi.org/10.1186/s12864-024-10737-w)), which is the only
50
+ independent comparison of the six. The page says which half of it transfers and why.
51
+
52
+ ### The ctDNA ground truth was found, not built
53
+
54
+ `ROADMAP.md` item 7 said this had to be constructed because Maruzani's deposited runs carry no UMI.
55
+ That is true of those two runs and not of the public record. Screening SRA read structure instead
56
+ turned up **`PRJNA788522`** (72 runs, cell-free DNA reference material at certified 0 / 0.125 /
57
+ 0.25 / 1% VAF, crossed with 5/20/80 ng input and 3.3/10/30x depth, three replicates) and
58
+ **`PRJNA507366`** (28 runs, six polymerases on the same material plus 0.031% and 0.0625% VAF).
59
+ Both kept a real 12 nt inline UMI, which `migec suggest` recovers from base composition alone.
60
+
61
+ Note: `PRJNA507366`'s design is in `library_name`, not `sample_alias` — every alias there reads
62
+ `SeraCare_Reference_Material`, so reading the alias alone would have called the study undesigned.
63
+
64
+ ### `scripts/sra_fetch.py` — fetch on demand instead of mirroring
65
+
66
+ `probe` reports reads-per-spot and their lengths from metadata alone, which is the cheap "did the
67
+ UMI survive deposition" test; `url` gives the direct S3/HTTPS links with md5 and size; `peek`
68
+ streams the first N spots for `migec suggest`; `get` downloads and converts. Never: the
69
+ obvious-looking source is the slow one — ENA serves ready-made FASTQ and skips `fasterq-dump`, but
70
+ caps at one connection. Measured on `SRR17220895`: **ENA 200 kB/s against NCBI S3 at 6.7 MB/s over
71
+ 8 connections**, so S3 is the default and `--prefer ena` is there for the studies that deposited a
72
+ third file the `.sra` object folds away.
73
+
74
+ This is what keeps `isalgo/umi_data` small: anything with a public accession is regenerated by a
75
+ one-line command recorded in `SOURCES.md`, and only the CI fixtures and the semi-internal data that
76
+ has nowhere else to live are mirrored.
77
+
78
+ ### `scripts/ctdna_titration.py` and `notebooks/ctdna_variants.py`
79
+
80
+ The three stages over the titration, reporting molecules per site against the certified frequency.
81
+ Never: **the molecule total of a multiplex panel is not the count at any one site** — a variant sits
82
+ on one amplicon, so the total overstates the evidence by exactly the panel size. The amplicon count
83
+ is measured from consensus prefixes rather than assumed, and its share threshold has to sit in the
84
+ gap below the smallest real amplicon: at 1% the count moved with depth (5 amplicons on the deepest
85
+ run, 10 on the shallowest) because a shallow consensus carries more payload error, which deflated
86
+ the per-amplicon count on exactly the runs where the evidence was thinnest. The script warns when
87
+ the count is not constant across runs, which is the check that the threshold is in the gap.
88
+
7
89
  ## 2.2.0 — 2026-08-13
8
90
 
9
91
  Throughput and one new measurement. No breaking change: every 2.1.0 command, flag and output file
@@ -465,12 +465,35 @@ correction is written up in `project/review-algorithms.md`.
465
465
  5. **i7xi5 contingency table** — needs nothing that is not already in the headers.
466
466
  6. **`2026-migec-benchmark`** and the published comparisons. This is what the version number is
467
467
  waiting on, not the code.
468
- 7. **ctDNA ground truth** (COSMIC at 0.005-0.075 VAF). Must be built: Maruzani's deposited runs
469
- carry no UMI.
468
+ 7. **Run the callers themselves** against the ctDNA ground truth below. It no longer has to be
469
+ built -- only the call sets are missing.
470
470
  8. **R1/R2 overlap merge**, as a case of `--contig`'s placement and never a second matcher.
471
471
  9. **Bit-parallel matcher**, last and deliberately: the scan is not the bottleneck.
472
+ - **The ctDNA ground truth was FOUND, not built (2026-08-13).** `PRJNA788522` (72 runs, cfDNA
473
+ reference material at certified 0/0.125/0.25/1% VAF x 5/20/80 ng x 3.3/10/30x, 3 reps) and
474
+ `PRJNA507366` (28 runs, six polymerases plus 0.031%/0.0625% VAF) both kept a **real 12 nt inline
475
+ UMI**, and `migec suggest` recovers it from base composition alone. Never: "no public ctDNA data
476
+ has usable UMIs" is a statement about the two runs Maruzani picked, not about the archive --
477
+ check read structure (`scripts/sra_fetch.py probe`) before believing it. Note: `PRJNA507366`'s
478
+ design is in **`library_name`**, not `sample_alias`; every alias there is
479
+ `SeraCare_Reference_Material`, so the alias alone makes a designed study look undesigned.
480
+ - **Never: the molecule total of a multiplex panel is not the count at a site.** A variant sits on
481
+ one amplicon, so the evidence a caller gets is `molecules / amplicons`. The amplicon count is
482
+ measured from consensus prefixes, and its share threshold must sit in the **gap** below the
483
+ smallest real amplicon (5%, where the gap is 7.6% to 1.1%): at 1% the count moved with depth --
484
+ 5 on the deepest run, 10 on the shallowest, because a shallow consensus carries more payload
485
+ error -- which deflates the per-amplicon count on exactly the runs where evidence is thinnest.
486
+ - **Note: `scripts/sra_fetch.py` is the fetch-on-demand path**, so `isalgo/umi_data` holds only CI
487
+ fixtures and semi-internal data. Never: ENA's ready-made FASTQ looks like the lazy source and is
488
+ 33x slower -- **200 kB/s against NCBI S3's 6.7 MB/s** on 8 connections, measured on SRR17220895.
489
+ S3 plus `fasterq-dump` is the default; `--prefer ena` exists for studies that deposited a third
490
+ file the `.sra` folds away.
472
491
  - **Note: Britanova et al aging (bulk TCR, shallow) lives on aldan3** and is the real dataset for the
473
492
  1-3 read regime. Not pulled yet. aldan3 compute goes through SLURM, never the frontend.
493
+ - **Note: the ctDNA titration runs on aldan3** (`~/migec-ctdna`, `migec_ctdna.sbatch`, partition
494
+ `medium`, 16 cores / 32 GB). The job builds its own micromamba env because the cluster's system
495
+ python is **3.8** and migec needs >= 3.10, and it installs from PyPI rather than from source,
496
+ which doubles as a check that the published linux wheels work.
474
497
  - The archive is pushed: `legacy-v1` + tag `v1-final`, and master is the rewrite. Recovery point
475
498
  `~/backup/migec-local-mirror-2026-08-13.git`. Note: The canonical repo is **antigenomics/migec**;
476
499
  `mikessh/migec` is a redirect, and `gh` commands must use the former.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: migec
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: UMI barcode extraction, correction and consensus assembly for barcoded sequencing
5
5
  Keywords: umi,barcode,consensus,airr,repseq,single-cell,sequencing
6
6
  Author-Email: Mikhail Shugay <mikhail.shugay@gmail.com>
@@ -51,10 +51,10 @@ Computational Biology* 2017).
51
51
 
52
52
  > **Version 2 is under construction.** All three stages work today — `checkout`, `refine` and
53
53
  > `assemble` — with cell barcodes, whitelists, dual-end and positional (10x) layouts, cell calling,
54
- > QC figures, and `suggest`/`subsample`/`plot`. Index hopping, `.mig` bucket output and the published benchmark
55
- > comparisons are what remain; see [`ROADMAP.md`](ROADMAP.md). The Groovy MIGEC 1.2.9 is archived on
56
- > branch [`legacy-v1`](../../tree/legacy-v1) and at tag `v1-final` — Java users want the jars on the
57
- > [1.2.9 release](../../releases/tag/1.2.9).
54
+ > QC figures, and `suggest`/`subsample`/`plot`. Index hopping, `.mig` bucket output and the
55
+ > published benchmark comparisons are what remain; see [`ROADMAP.md`](ROADMAP.md). The Groovy
56
+ > MIGEC 1.2.9 is archived on branch [`legacy-v1`](../../tree/legacy-v1) and at tag `v1-final` —
57
+ > Java users want the jars on the [1.2.9 release](../../releases/tag/1.2.9).
58
58
 
59
59
  ## Why
60
60
 
@@ -105,13 +105,49 @@ one buys.
105
105
  > **Never** run alevin, bustools or STARsolo on a consensus FASTQ. They read the barcode out of a
106
106
  > *raw* barcode read and deduplicate themselves; migec already did, and that read no longer exists.
107
107
 
108
+ ### Calling rare variants
109
+
110
+ The same line decides which variant caller to use. `Mutect2`, `LoFreq`, `FreeBayes` and `VarDict`
111
+ read a BAM and never look at `RX`, so they **compose**: one consensus is one molecule, so their
112
+ depth already is a molecule count. `UMI-VarCal` and `UMIErrorCorrect` do their own grouping and
113
+ consensus, so they **replace** `assemble` rather than following it — and a `--min-family-size 3`
114
+ filter after `assemble` discards the whole library, because every family already has size 1.
115
+
116
+ Which caller matters less than how many molecules it is handed, and that is fixed before any
117
+ software runs: sequencing deeper buys more reads per molecule, only more input DNA buys molecules.
118
+ [docs/variants.rst](docs/variants.rst) has the caller table, the published comparison, and the run
119
+ over cell-free DNA reference material at known allele frequency that puts numbers on it.
120
+
108
121
  ## Install
109
122
 
110
123
  ```bash
111
- pip install migec
124
+ uv tool install migec # or: uv pip install migec, or: pip install migec
125
+ migec info # prints the three version strings; they must agree
126
+ ```
127
+
128
+ Wheels for CPython 3.10–3.13 on Linux x86-64 and macOS arm64, so nothing compiles. Python 3.10 or
129
+ newer is required — on a cluster whose system Python is older, bring your own:
130
+
131
+ ```bash
132
+ uv venv --python 3.12 ~/envs/migec && source ~/envs/migec/bin/activate && uv pip install migec
133
+ ```
134
+
135
+ **From source**, for development:
136
+
137
+ ```bash
138
+ git clone https://github.com/antigenomics/migec && cd migec
139
+ bash setup.sh # uv venv, editable install, and asserts the C++ extension imports
112
140
  ```
113
141
 
114
- Wheels for CPython 3.10–3.13 on Linux x86-64 and macOS arm64. From source: `bash setup.sh`.
142
+ `setup.sh` needs [uv](https://docs.astral.sh/uv/getting-started/installation/) and a C++20
143
+ compiler. It deliberately asserts that `migec._core` imports rather than only that the package
144
+ does — without that check a failed C++ build looks like a successful install and only fails much
145
+ later. Then:
146
+
147
+ ```bash
148
+ cmake -S . -B build -DMIGEC_TESTS=ON && cmake --build build -j && ctest --test-dir build
149
+ python -m pytest tests/unit tests/synthetic -q
150
+ ```
115
151
 
116
152
  ## Usage
117
153
 
@@ -535,6 +571,30 @@ It is also the memory-hostile case, because distinct barcodes are what everythin
535
571
  it is what the benchmarks use: 1,179,549 reads/s at 1.02 reads/UMI, 282 B resident per distinct
536
572
  barcode, still bounded by the bucket rather than the library.
537
573
 
574
+ ### For rare variants, the molecule count decides — not the caller
575
+
576
+ Run over 100 runs of two SiMSen-Seq studies on cell-free DNA reference material at *certified*
577
+ allele frequencies (`PRJNA788522`, `PRJNA507366`; `scripts/ctdna_titration.py`). At 0.125% VAF the
578
+ DNA input alone settles the outcome:
579
+
580
+ | input | depth | molecules at the site | variant molecules | P(at least 3 copies) |
581
+ |---|---|---|---|---|
582
+ | 5 ng | 3.3× | 2,699 | 3.4 | **0.65** |
583
+ | 5 ng | 10× | 4,530 | 5.7 | **0.89** |
584
+ | 20 ng | 3.3× | 7,162 | 9.0 | 0.99 |
585
+ | 20 ng | 10× | 11,961 | 15.0 | 1.00 |
586
+
587
+ At 5 ng a 0.125% variant is a coin flip and **no caller fixes it** — a third of replicates simply do
588
+ not contain three copies. The decision that mattered was made at extraction.
589
+ [docs/variants.rst](docs/variants.rst) has the caller table (standard callers *compose* with migec,
590
+ UMI-aware ones *replace* a stage), the 33-run true-negative arm, and the rest of the numbers.
591
+
592
+ The same data gives an independent check on the barcode-error estimate: `PRJNA507366` varies the
593
+ polymerase while holding template and protocol fixed, and `refine` separates the high-fidelity
594
+ enzymes from the standard ones by **3 Phred, a factor of two** — Accuprime/Platinum/Phusion at
595
+ Q30.8–31.4 against Platinum HiFi at Q34.0 and Platinum SuperFi at Q34.8–35.1 — from the barcodes
596
+ alone, with no reference, no alignment, and no knowledge of which enzyme it was looking at.
597
+
538
598
  ### Speed and memory are reported, not assumed
539
599
 
540
600
  `--threads` defaults to one per core on **all three stages**, and **the output is byte-identical
@@ -646,21 +706,67 @@ distinct molecules. Both are reported; only the collision form feeds any decisio
646
706
 
647
707
  ## Pipelines
648
708
 
649
- `integrations/nextflow/` is an nf-core-style local module set — four processes, a subworkflow,
650
- `meta.yml`, `nextflow.config`, `environment.yml` — that drops into
651
- [nf-core/airrflow](https://nf-co.re/airrflow) or anything else that hands you FASTQ pairs. SLURM is
652
- the pipeline's business, not the module's: it declares `label` and `task.cpus` and nothing more.
709
+ Two, for two situations ([docs/nextflow.rst](docs/nextflow.rst) has both):
710
+
711
+ **Nextflow** — `integrations/nextflow/` runs the three stages and then keeps going:
712
+
713
+ ```bash
714
+ nextflow run integrations/nextflow --mode ctdna --input 'd/*_R{1,2}.fq.gz' --preset tso500 --fasta ref.fa
715
+ nextflow run integrations/nextflow --mode airr --input 'd/*_R{1,2}.fq.gz' --preset migec
716
+ nextflow run integrations/nextflow --mode consensus --input 'd/*_R{1,2}.fq.gz' --preset 10x-v2
717
+ ```
718
+
719
+ `ctdna` aligns and calls variants, `airr` calls clonotypes with arda, `consensus` stops at the
720
+ consensus. It drops into [nf-core/airrflow](https://nf-co.re/airrflow) or anything else that hands
721
+ you FASTQ pairs. The align module fails loudly if the `MI:Z:` tag does not reach the BAM, because
722
+ that failure is otherwise silent.
723
+
724
+ **SLURM** — `integrations/slurm/` is two sbatch templates and a sample sheet, for a cohort where
725
+ the consensus *is* the deliverable:
726
+
727
+ ```bash
728
+ sbatch --array=1-$(($(wc -l < samples.tsv) - 1)) integrations/slurm/migec_array.sbatch samples.tsv
729
+ R1=s1.fq.gz SAMPLE=s1 BC_PATTERN='0:12' bash integrations/slurm/migec_sample.sbatch # no SLURM
730
+ ```
731
+
732
+ Both run as ordinary bash without SLURM — that is how they are tested, and how a layout should be
733
+ checked before a cohort is queued.
653
734
 
654
735
  All three stages thread, and each is byte-identical at any `-t`, so a retry with different cores
655
- cannot change a result — which is what makes an escalating `errorStrategy 'retry'` safe here.
656
- Per-sample keys in `meta` beat the `params.migec_*` defaults, so one run can mix chemistries. See
657
- [docs/nextflow.rst](docs/nextflow.rst); nextflow is not installed on the machine these docs were
658
- measured on, so the modules are reviewed against the nf-core spec rather than verified by a run.
736
+ cannot change a result — which is what makes an escalating `errorStrategy 'retry'` or `--requeue`
737
+ safe. Per-sample keys in `meta` beat the `params.*` defaults, so one run can mix chemistries.
738
+
739
+ > The SLURM templates have been run end to end here. Nextflow has not — it is not installed on this
740
+ > machine, so the modules are reviewed against the nf-core spec rather than verified by a run.
741
+
742
+ ## Notebooks
743
+
744
+ Six [marimo](https://marimo.io) notebooks, each a plain Python file with its own dependency header,
745
+ so `uv` builds the environment and nothing needs installing first:
746
+
747
+ ```bash
748
+ uv run marimo edit notebooks/ctdna_variants.py
749
+ ```
750
+
751
+ `platforms.py` (every layout), `barcode_space.py` (collisions and the error budget),
752
+ `refine_diagnostics.py` (where the molecules went), `exome_capture.py` (why coordinate
753
+ deduplication undercounts), `airr_repertoire.py` (reads against molecules for clonotype counts),
754
+ `ctdna_variants.py` (how many molecules a caller actually gets). See
755
+ [notebooks/README.md](notebooks/README.md); four of the six need no network at all.
659
756
 
660
757
  ## Documentation
661
758
 
662
- <https://antigenomics.github.io/migec/> — see [`docs/formats.rst`](docs/formats.rst) for the on-disk
663
- format, and [`ROADMAP.md`](ROADMAP.md) for what is implemented.
759
+ <https://antigenomics.github.io/migec/>. The pages worth knowing about:
760
+
761
+ | | |
762
+ |---|---|
763
+ | [`docs/layouts.rst`](docs/layouts.rst) | where the barcode is: the grammar, every preset, fgbio read structures |
764
+ | [`docs/variants.rst`](docs/variants.rst) | which variant caller, and the molecule count that decides whether any can see it |
765
+ | [`docs/downstream.rst`](docs/downstream.rst) | what consumes the consensus, measured tool by tool |
766
+ | [`docs/nextflow.rst`](docs/nextflow.rst) | the Nextflow modules and the SLURM templates |
767
+ | [`docs/formats.rst`](docs/formats.rst) | the inter-stage contract: tags, `.mig` buckets, every TSV column |
768
+ | [`SOURCES.md`](SOURCES.md) | every dataset, where it came from, and the command that re-fetches it |
769
+ | [`ROADMAP.md`](ROADMAP.md) | what is implemented and what is not |
664
770
 
665
771
  ## Citing
666
772
 
@@ -17,10 +17,10 @@ Computational Biology* 2017).
17
17
 
18
18
  > **Version 2 is under construction.** All three stages work today — `checkout`, `refine` and
19
19
  > `assemble` — with cell barcodes, whitelists, dual-end and positional (10x) layouts, cell calling,
20
- > QC figures, and `suggest`/`subsample`/`plot`. Index hopping, `.mig` bucket output and the published benchmark
21
- > comparisons are what remain; see [`ROADMAP.md`](ROADMAP.md). The Groovy MIGEC 1.2.9 is archived on
22
- > branch [`legacy-v1`](../../tree/legacy-v1) and at tag `v1-final` — Java users want the jars on the
23
- > [1.2.9 release](../../releases/tag/1.2.9).
20
+ > QC figures, and `suggest`/`subsample`/`plot`. Index hopping, `.mig` bucket output and the
21
+ > published benchmark comparisons are what remain; see [`ROADMAP.md`](ROADMAP.md). The Groovy
22
+ > MIGEC 1.2.9 is archived on branch [`legacy-v1`](../../tree/legacy-v1) and at tag `v1-final` —
23
+ > Java users want the jars on the [1.2.9 release](../../releases/tag/1.2.9).
24
24
 
25
25
  ## Why
26
26
 
@@ -71,13 +71,49 @@ one buys.
71
71
  > **Never** run alevin, bustools or STARsolo on a consensus FASTQ. They read the barcode out of a
72
72
  > *raw* barcode read and deduplicate themselves; migec already did, and that read no longer exists.
73
73
 
74
+ ### Calling rare variants
75
+
76
+ The same line decides which variant caller to use. `Mutect2`, `LoFreq`, `FreeBayes` and `VarDict`
77
+ read a BAM and never look at `RX`, so they **compose**: one consensus is one molecule, so their
78
+ depth already is a molecule count. `UMI-VarCal` and `UMIErrorCorrect` do their own grouping and
79
+ consensus, so they **replace** `assemble` rather than following it — and a `--min-family-size 3`
80
+ filter after `assemble` discards the whole library, because every family already has size 1.
81
+
82
+ Which caller matters less than how many molecules it is handed, and that is fixed before any
83
+ software runs: sequencing deeper buys more reads per molecule, only more input DNA buys molecules.
84
+ [docs/variants.rst](docs/variants.rst) has the caller table, the published comparison, and the run
85
+ over cell-free DNA reference material at known allele frequency that puts numbers on it.
86
+
74
87
  ## Install
75
88
 
76
89
  ```bash
77
- pip install migec
90
+ uv tool install migec # or: uv pip install migec, or: pip install migec
91
+ migec info # prints the three version strings; they must agree
92
+ ```
93
+
94
+ Wheels for CPython 3.10–3.13 on Linux x86-64 and macOS arm64, so nothing compiles. Python 3.10 or
95
+ newer is required — on a cluster whose system Python is older, bring your own:
96
+
97
+ ```bash
98
+ uv venv --python 3.12 ~/envs/migec && source ~/envs/migec/bin/activate && uv pip install migec
99
+ ```
100
+
101
+ **From source**, for development:
102
+
103
+ ```bash
104
+ git clone https://github.com/antigenomics/migec && cd migec
105
+ bash setup.sh # uv venv, editable install, and asserts the C++ extension imports
78
106
  ```
79
107
 
80
- Wheels for CPython 3.10–3.13 on Linux x86-64 and macOS arm64. From source: `bash setup.sh`.
108
+ `setup.sh` needs [uv](https://docs.astral.sh/uv/getting-started/installation/) and a C++20
109
+ compiler. It deliberately asserts that `migec._core` imports rather than only that the package
110
+ does — without that check a failed C++ build looks like a successful install and only fails much
111
+ later. Then:
112
+
113
+ ```bash
114
+ cmake -S . -B build -DMIGEC_TESTS=ON && cmake --build build -j && ctest --test-dir build
115
+ python -m pytest tests/unit tests/synthetic -q
116
+ ```
81
117
 
82
118
  ## Usage
83
119
 
@@ -501,6 +537,30 @@ It is also the memory-hostile case, because distinct barcodes are what everythin
501
537
  it is what the benchmarks use: 1,179,549 reads/s at 1.02 reads/UMI, 282 B resident per distinct
502
538
  barcode, still bounded by the bucket rather than the library.
503
539
 
540
+ ### For rare variants, the molecule count decides — not the caller
541
+
542
+ Run over 100 runs of two SiMSen-Seq studies on cell-free DNA reference material at *certified*
543
+ allele frequencies (`PRJNA788522`, `PRJNA507366`; `scripts/ctdna_titration.py`). At 0.125% VAF the
544
+ DNA input alone settles the outcome:
545
+
546
+ | input | depth | molecules at the site | variant molecules | P(at least 3 copies) |
547
+ |---|---|---|---|---|
548
+ | 5 ng | 3.3× | 2,699 | 3.4 | **0.65** |
549
+ | 5 ng | 10× | 4,530 | 5.7 | **0.89** |
550
+ | 20 ng | 3.3× | 7,162 | 9.0 | 0.99 |
551
+ | 20 ng | 10× | 11,961 | 15.0 | 1.00 |
552
+
553
+ At 5 ng a 0.125% variant is a coin flip and **no caller fixes it** — a third of replicates simply do
554
+ not contain three copies. The decision that mattered was made at extraction.
555
+ [docs/variants.rst](docs/variants.rst) has the caller table (standard callers *compose* with migec,
556
+ UMI-aware ones *replace* a stage), the 33-run true-negative arm, and the rest of the numbers.
557
+
558
+ The same data gives an independent check on the barcode-error estimate: `PRJNA507366` varies the
559
+ polymerase while holding template and protocol fixed, and `refine` separates the high-fidelity
560
+ enzymes from the standard ones by **3 Phred, a factor of two** — Accuprime/Platinum/Phusion at
561
+ Q30.8–31.4 against Platinum HiFi at Q34.0 and Platinum SuperFi at Q34.8–35.1 — from the barcodes
562
+ alone, with no reference, no alignment, and no knowledge of which enzyme it was looking at.
563
+
504
564
  ### Speed and memory are reported, not assumed
505
565
 
506
566
  `--threads` defaults to one per core on **all three stages**, and **the output is byte-identical
@@ -612,21 +672,67 @@ distinct molecules. Both are reported; only the collision form feeds any decisio
612
672
 
613
673
  ## Pipelines
614
674
 
615
- `integrations/nextflow/` is an nf-core-style local module set — four processes, a subworkflow,
616
- `meta.yml`, `nextflow.config`, `environment.yml` — that drops into
617
- [nf-core/airrflow](https://nf-co.re/airrflow) or anything else that hands you FASTQ pairs. SLURM is
618
- the pipeline's business, not the module's: it declares `label` and `task.cpus` and nothing more.
675
+ Two, for two situations ([docs/nextflow.rst](docs/nextflow.rst) has both):
676
+
677
+ **Nextflow** — `integrations/nextflow/` runs the three stages and then keeps going:
678
+
679
+ ```bash
680
+ nextflow run integrations/nextflow --mode ctdna --input 'd/*_R{1,2}.fq.gz' --preset tso500 --fasta ref.fa
681
+ nextflow run integrations/nextflow --mode airr --input 'd/*_R{1,2}.fq.gz' --preset migec
682
+ nextflow run integrations/nextflow --mode consensus --input 'd/*_R{1,2}.fq.gz' --preset 10x-v2
683
+ ```
684
+
685
+ `ctdna` aligns and calls variants, `airr` calls clonotypes with arda, `consensus` stops at the
686
+ consensus. It drops into [nf-core/airrflow](https://nf-co.re/airrflow) or anything else that hands
687
+ you FASTQ pairs. The align module fails loudly if the `MI:Z:` tag does not reach the BAM, because
688
+ that failure is otherwise silent.
689
+
690
+ **SLURM** — `integrations/slurm/` is two sbatch templates and a sample sheet, for a cohort where
691
+ the consensus *is* the deliverable:
692
+
693
+ ```bash
694
+ sbatch --array=1-$(($(wc -l < samples.tsv) - 1)) integrations/slurm/migec_array.sbatch samples.tsv
695
+ R1=s1.fq.gz SAMPLE=s1 BC_PATTERN='0:12' bash integrations/slurm/migec_sample.sbatch # no SLURM
696
+ ```
697
+
698
+ Both run as ordinary bash without SLURM — that is how they are tested, and how a layout should be
699
+ checked before a cohort is queued.
619
700
 
620
701
  All three stages thread, and each is byte-identical at any `-t`, so a retry with different cores
621
- cannot change a result — which is what makes an escalating `errorStrategy 'retry'` safe here.
622
- Per-sample keys in `meta` beat the `params.migec_*` defaults, so one run can mix chemistries. See
623
- [docs/nextflow.rst](docs/nextflow.rst); nextflow is not installed on the machine these docs were
624
- measured on, so the modules are reviewed against the nf-core spec rather than verified by a run.
702
+ cannot change a result — which is what makes an escalating `errorStrategy 'retry'` or `--requeue`
703
+ safe. Per-sample keys in `meta` beat the `params.*` defaults, so one run can mix chemistries.
704
+
705
+ > The SLURM templates have been run end to end here. Nextflow has not — it is not installed on this
706
+ > machine, so the modules are reviewed against the nf-core spec rather than verified by a run.
707
+
708
+ ## Notebooks
709
+
710
+ Six [marimo](https://marimo.io) notebooks, each a plain Python file with its own dependency header,
711
+ so `uv` builds the environment and nothing needs installing first:
712
+
713
+ ```bash
714
+ uv run marimo edit notebooks/ctdna_variants.py
715
+ ```
716
+
717
+ `platforms.py` (every layout), `barcode_space.py` (collisions and the error budget),
718
+ `refine_diagnostics.py` (where the molecules went), `exome_capture.py` (why coordinate
719
+ deduplication undercounts), `airr_repertoire.py` (reads against molecules for clonotype counts),
720
+ `ctdna_variants.py` (how many molecules a caller actually gets). See
721
+ [notebooks/README.md](notebooks/README.md); four of the six need no network at all.
625
722
 
626
723
  ## Documentation
627
724
 
628
- <https://antigenomics.github.io/migec/> — see [`docs/formats.rst`](docs/formats.rst) for the on-disk
629
- format, and [`ROADMAP.md`](ROADMAP.md) for what is implemented.
725
+ <https://antigenomics.github.io/migec/>. The pages worth knowing about:
726
+
727
+ | | |
728
+ |---|---|
729
+ | [`docs/layouts.rst`](docs/layouts.rst) | where the barcode is: the grammar, every preset, fgbio read structures |
730
+ | [`docs/variants.rst`](docs/variants.rst) | which variant caller, and the molecule count that decides whether any can see it |
731
+ | [`docs/downstream.rst`](docs/downstream.rst) | what consumes the consensus, measured tool by tool |
732
+ | [`docs/nextflow.rst`](docs/nextflow.rst) | the Nextflow modules and the SLURM templates |
733
+ | [`docs/formats.rst`](docs/formats.rst) | the inter-stage contract: tags, `.mig` buckets, every TSV column |
734
+ | [`SOURCES.md`](SOURCES.md) | every dataset, where it came from, and the command that re-fetches it |
735
+ | [`ROADMAP.md`](ROADMAP.md) | what is implemented and what is not |
630
736
 
631
737
  ## Citing
632
738
 
@@ -43,13 +43,14 @@ Everything below is either open or half-open. Nothing else on this page is.
43
43
  | 4 | `--rt-error auto` | M1 | falls straight out of (3). The floor is a property of the enzyme and the cycle count, so fitting it per dataset is the honest default once there is something to fit against |
44
44
  | 5 | i7xi5 contingency table | M2 | the only way index hopping is actually estimable, and it needs nothing that is not already in the headers |
45
45
  | 6 | `2026-migec-benchmark`, the published comparisons | M5 | MIGEC v1, MAGERI, UMI-tools, Calib, fgbio, Cell Ranger, UMI-VarCal, UMIErrorCorrect. This is what the version number is waiting on, not the code |
46
- | 7 | ctDNA ground truth, COSMIC at 0.005-0.075 VAF | M5 | the one claim with no ground truth yet. Never: Maruzani's deposited runs carry **no** UMI, so they have to be simulated -- and their own rule assumes co-terminal reads, which X1 falsified |
46
+ | 7 | ctDNA ground truth | M5 | **no longer has to be built.** `PRJNA788522` and `PRJNA507366` are cfDNA reference material at certified VAF (0 / 0.031 / 0.0625 / 0.125 / 0.25 / 1%) with a **real 12 nt inline UMI that survived deposition**, and a true-negative `WT` arm. Maruzani's two runs carry no UMI and had to be simulated; these do not. `SOURCES.md`, `docs/variants.rst` |
47
47
  | 8 | R1/R2 overlap merge | M1 | a special case of `--contig`'s placement, never a second matcher in `checkout` |
48
48
  | 9 | Bit-parallel matcher | M2 | last, deliberately: the scan is O(offsets x pattern) and is **not** the bottleneck. It goes in when a benchmark says so |
49
49
 
50
- Two things are blocked on data rather than on work: **Britanova et al aging** (bulk TCR, shallow --
51
- the real 1-3 reads/UMI dataset) lives on aldan3 and has not been pulled, and the ctDNA ground truth
52
- above has to be built rather than downloaded.
50
+ One thing is still blocked on data rather than on work: **Britanova et al aging** (bulk TCR,
51
+ shallow -- the real 1-3 reads/UMI dataset) lives on aldan3 and has not been pulled. The ctDNA
52
+ ground truth is no longer on that list: it was found rather than built, by screening SRA read
53
+ structure instead of trusting the published claim that none exists (`scripts/sra_fetch.py probe`).
53
54
 
54
55
  ## M0 — skeleton, format, simulator
55
56
 
@@ -299,11 +300,16 @@ model has to use the evidence that survives at one read:
299
300
  - [ ] `2026-migec-benchmark` repo, `isalgo/umi_data`, comparisons against MIGEC v1, MAGERI,
300
301
  UMI-tools, Calib, fgbio, Cell Ranger, **UMI-VarCal and UMIErrorCorrect** (the two UMI-aware
301
302
  callers benchmarked by Maruzani et al. 2024 for low-frequency ctDNA)
302
- - [ ] ctDNA ground truth in the style of Maruzani et al.: COSMIC variants spiked at 0.005–0.075 VAF
303
- into a real cfDNA background at 200x/450x/850x. Never: Their deposited runs carry **no** UMI
304
- (aligned BAM submissions, `suggest` finds no pattern, `CMP_LINKAGE_GROUP` empty), so the UMIs
305
- have to be simulated — and their rule assigns them to reads sharing start *and* end, which is
306
- the co-terminal assumption X1 falsified
303
+ - [x] ctDNA ground truth **located, not simulated** (2026-08-13): `PRJNA788522` (72 runs, cfDNA
304
+ reference material at 0 / 0.125 / 0.25 / 1% VAF x 5/20/80 ng x 3.3/10/30x, three replicates)
305
+ and `PRJNA507366` (28 runs, six polymerases plus 0.031% / 0.0625% VAF). Both carry a real
306
+ 12 nt inline UMI. Never: Maruzani's runs carry **no** UMI (aligned BAM submissions, `suggest`
307
+ finds no pattern, `CMP_LINKAGE_GROUP` empty), so theirs had to be simulated — 9 nt, Phred
308
+ fixed at 37, assigned to reads sharing start *and* end, which is the co-terminal assumption
309
+ X1 falsified. That is a property of the two runs they picked, not of the public record
310
+ - [ ] Variant calls on top of it: run the callers themselves (LoFreq, Mutect2, UMI-VarCal,
311
+ UMIErrorCorrect) against the `WT` true-negative arm and the certified dilutions. `docs/variants.rst`
312
+ reports the molecule counts and the detectability arithmetic; the call sets are the next step
307
313
  - Gate: grouping ARI ≥0.99; residual error ≤1e-5 on a clonal control; ≥3× MIGEC v1 wall-clock
308
314
 
309
315
  ## Deliberately not doing
@@ -16,6 +16,7 @@ are never conflated in a table row.
16
16
  | `assets/benchmark_threads.tsv` + `.json` | this machine, 2026-08-13 | **experimental** (a timing measurement) | `python scripts/benchmark_threads.py --reads 2000000 -o assets/` |
17
17
  | `assets/SRR1763769.mig.tsv`, `assets/assemble.coverage.tsv` | `isalgo/umi_data` CI fixture | derived | `migec refine ci/SRR1763769_umi0.5pct.fq.gz -o r/ && migec assemble r/CTRL.fq.gz -o a/` |
18
18
  | `assets/*.svg`, `assets/*.gp` | the tables beside them | derived | `migec plot assets/ -o assets/` |
19
+ | `assets/ctdna_titration.tsv` (+ `_runs.tsv`) | 100 runs of `PRJNA788522` + `PRJNA507366` | derived (migec over experimental data) | `python scripts/sra_fetch.py get <runs> -o simsen/` then `python scripts/ctdna_titration.py --reads simsen/ --out ctdna/ --design design.tsv` |
19
20
 
20
21
  Result tables and figures are output, not data, so they live here next to the script that made
21
22
  them rather than in `isalgo/umi_data`. Test corpora go the other way: HuggingFace, never git.
@@ -158,6 +159,52 @@ numbers inherits it.
158
159
  background at controlled depth is the ground truth a consensus-quality claim wants — and two more
159
160
  comparators for M5, `UMI-VarCal` and `UMIErrorCorrect`.
160
161
 
162
+ ### SiMSen-Seq — the ctDNA data that DID keep its UMIs (fetched 2026-08-13)
163
+
164
+ Found by screening SRA rather than by assuming, after the Maruzani entry above established that
165
+ their two accessions had not. Both studies are the Gothenburg SiMSen-Seq protocol on **commercial
166
+ cell-free DNA reference material with certified mutant allele frequencies**, and both deposited
167
+ the reads untrimmed, so the **12 nt inline UMI is still there**. `migec suggest` recovers it from
168
+ base composition alone with no knowledge of the protocol — a 12 nt near-uniform run at cycles
169
+ 0-11, then the constant 16 nt spacer (`ATGGGAAAGAGTGTCC`), which is exactly UMIErrorCorrect's
170
+ `-ul 12 -sl 16`.
171
+
172
+ | Item | Value |
173
+ |---|---|
174
+ | Pattern | `--bc-pattern '0:12'`, single-end, positional (no `--max-offset`) |
175
+ | Panel | multiplex amplicon, measured from the consensus prefixes: **5 amplicons** for `PRJNA788522`, **3** for `PRJNA507366` (whose own labels say `3plx`) |
176
+ | Cluster copy | `/projects/tcr_bcr_rnaseq/migec_ctdna/fastq` on aldan3, 100 runs, 6.9 GB (2026-08-13). A convenience cache — the fetch command above regenerates it |
177
+ | Provenance | experimental (SRA), commercial reference material; the certified VAFs are the vendor's |
178
+ | Fetch | `python scripts/sra_fetch.py get <runs> -o data/` (NCBI S3, ~7 MB/s) |
179
+ | Analyse | `python scripts/ctdna_titration.py --reads data/ --out out/ --design design.tsv` |
180
+ | Designs | `curl -sG 'https://www.ebi.ac.uk/ena/portal/api/search' -d result=read_run -d query=study_accession=<PRJ> -d fields=run_accession,sample_alias -d format=tsv -d limit=0` |
181
+
182
+ **`PRJNA788522` — the titration.** Österlund T, Filges S, Johansson G, Ståhlberg A.
183
+ *UMIErrorCorrect and UMIAnalyzer: Software for Consensus Read Generation, Error Correction, and
184
+ Visualization Using Unique Molecular Identifiers.* **Clin Chem** 2022;68(11):1425–1435.
185
+ [doi:10.1093/clinchem/hvac136](https://doi.org/10.1093/clinchem/hvac136), PMID 36031761.
186
+
187
+ 72 runs, 41.8 M reads, Illumina MiniSeq, single-end 151 nt. The design is in `sample_alias` as
188
+ `<input>ng_<vaf>_<depth>x_rep_<n>`: mutant allele frequency **0% (`WT`), 0.125%, 0.25%, 1%** plus
189
+ an undiluted `cell_line` arm, crossed with DNA input **5 / 20 / 80 ng** and depth **3.3 / 10 / 30x
190
+ reads per UMI**, three replicates each. Note: the `WT` arm is a **true negative** — its variant
191
+ frequency is zero by construction, so anything a caller reports there is its own false-positive
192
+ rate on real chemistry rather than on a simulation.
193
+
194
+ **`PRJNA507366` — the polymerase panel.** Filges S, Yamada E, Ståhlberg A, Godfrey TE. *Impact of
195
+ Polymerase Fidelity on Background Error Rates in Next-Generation Sequencing with Unique Molecular
196
+ Identifiers/Barcodes.* **Sci Rep** 2019;9(1):3503.
197
+ [doi:10.1038/s41598-019-39762-6](https://doi.org/10.1038/s41598-019-39762-6), PMID 30837525.
198
+
199
+ 28 runs, 81.4 M reads. Note: the design is **not** in `sample_alias` (every row reads
200
+ `SeraCare_Reference_Material`) — it is in **`library_name`** (the enzyme: Phusion, Accuprime,
201
+ Accuprime_hifi, Platinum, Platinum_Hifi, Platinum superfi, three replicates each) and in
202
+ `experiment_title` (`Wildtype` against `0.031% VAF` / `0.0625% VAF` / `0.125% VAF`). Reading the
203
+ alias alone would have called this study undesigned. It extends the frequency range **4x below**
204
+ PRJNA788522's floor, and it is the only public dataset here that varies the polymerase while
205
+ holding template and protocol fixed — which is the comparison `--rt-error auto` needs and the one
206
+ McInerney 2014's published fidelities can be checked against.
207
+
161
208
  ### Calib (github.com/vpc-ccg/calib)
162
209
 
163
210
  | Item | Value |
@@ -0,0 +1,35 @@
1
+ study enzyme input_ng arm depth n amplicons reads molecules_per_amplicon reads_per_molecule barcode_phred variant_molecules p_enough
2
+ PRJNA507366 Accuprime WT 3 3 1.95274e+06 79751.7 8.09 30.8233 0 0
3
+ PRJNA507366 Accuprime_hifi WT 3 3 2.25763e+06 93253.7 8.07 30.8367 0 0
4
+ PRJNA507366 Phusion WT 3 3 1.84092e+06 95455.7 6.43667 31.3967 0 0
5
+ PRJNA507366 PlatRegular 80 WT 3 3 4.22022e+06 57728 24.3433 28.8667 0 0
6
+ PRJNA507366 PlatSuperfi 80 WT 3 3 3.91793e+06 134430 9.73667 29.37 0 0
7
+ PRJNA507366 Platinum WT 3 3 2.15418e+06 86146.7 8.35667 31.0067 0 0
8
+ PRJNA507366 Platinum superfi 50 0.125% 1 3 3.8665e+06 73286 17.59 34.78 91.61 1
9
+ PRJNA507366 Platinum superfi 70 0.031% 3 3 4.07284e+06 89542 15.1367 35.1067 27.7567 1
10
+ PRJNA507366 Platinum superfi 70 0.0625% 3 3 3.60725e+06 86139 13.89 34.9567 53.8367 1
11
+ PRJNA507366 Platinum_Hifi WT 3 3 1.81798e+06 80859.3 7.54 33.9867 0 0
12
+ PRJNA788522 20 0.125 10.0 3 5 1.06487e+06 11960.7 17.7367 28.6467 14.9533 0.999933
13
+ PRJNA788522 20 0.125 3.3 3 5 321953 7162.33 8.98667 27.65 8.95333 0.9935
14
+ PRJNA788522 20 0.25 10.0 3 5 1.15595e+06 13349.7 17.1167 29.02 33.3733 1
15
+ PRJNA788522 20 0.25 3.3 3 5 289023 8048.67 7.14333 27.7033 20.12 1
16
+ PRJNA788522 20 1 10.0 3 5 445961 8630 10.3233 27.27 86.3 1
17
+ PRJNA788522 20 1 3.3 3 5 131423 5970.67 4.40667 25.4833 59.7067 1
18
+ PRJNA788522 20 WT 10.0 3 5 1.07623e+06 13988.7 15.3567 29.6467 0 0
19
+ PRJNA788522 20 WT 3.3 3 5 334784 8479 7.89 27.3833 0 0
20
+ PRJNA788522 20 cell_line 10.0 3 5 813932 10299.3 15.7467 29.9133
21
+ PRJNA788522 20 cell_line 3.3 3 5 232375 6310 7.39333 26.27
22
+ PRJNA788522 20 cell_line 30.0 3 5 1.9048e+06 16808.7 22.6567 30.9867
23
+ PRJNA788522 5 0.125 10.0 3 5 434820 4530 20.02 29.03 5.66333 0.894
24
+ PRJNA788522 5 0.125 3.3 3 5 182150 2699 13.47 28.1 3.37 0.654933
25
+ PRJNA788522 5 0.25 10.0 3 5 909770 5899.33 28.9033 29.14 14.75 0.9994
26
+ PRJNA788522 5 0.25 3.3 3 5 182433 2945.67 12.32 28.17 7.36333 0.976367
27
+ PRJNA788522 5 1 10.0 3 5 501350 4129 24.2467 29.0267 41.29 1
28
+ PRJNA788522 5 1 3.3 3 5 167841 2888.33 11.5833 28.31 28.8833 1
29
+ PRJNA788522 5 WT 10.0 3 5 533736 4428 24.04 29.21 0 0
30
+ PRJNA788522 5 WT 3.3 3 5 146169 2868.67 10.1533 28.4567 0 0
31
+ PRJNA788522 5 cell_line 10.0 3 5 304544 4559.33 13.5433 27.53
32
+ PRJNA788522 5 cell_line 3.3 3 5 76465.3 2958.33 5.18 20.6033
33
+ PRJNA788522 5 cell_line 30.0 3 5 635122 6664.33 19.0733 29.2467
34
+ PRJNA788522 80 cell_line 10.0 3 5 1.49859e+06 26793 11.2 31.46
35
+ PRJNA788522 80 cell_line 3.3 3 5 582020 19310 6.02667 29.8167