mirpy-lib 4.0.0__tar.gz → 4.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/CHANGELOG.md +60 -0
  2. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/PKG-INFO +28 -13
  3. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/README.md +25 -10
  4. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/signature.rst +21 -1
  5. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/pyproject.toml +2 -2
  6. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/skills/mirpy/SKILL.md +4 -4
  7. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/__init__.py +1 -1
  8. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/cli.py +47 -9
  9. mirpy_lib-4.2.0/src/mir/resources/signature/corpora.json +92 -0
  10. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/signature/signature.py +43 -24
  11. mirpy_lib-4.0.0/src/mir/resources/signature/rsig_memory.json +0 -94
  12. mirpy_lib-4.0.0/src/mir/resources/signature/rsig_memory.npz +0 -0
  13. mirpy_lib-4.0.0/src/mir/resources/signature/rsig_naive.json +0 -94
  14. mirpy_lib-4.0.0/src/mir/resources/signature/rsig_naive.npz +0 -0
  15. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/.gitignore +0 -0
  16. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/LICENSE +0 -0
  17. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/assets/mirpy_dark.png +0 -0
  18. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/assets/mirpy_dark.svg +0 -0
  19. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/assets/mirpy_dark_bg.svg +0 -0
  20. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/assets/mirpy_light.png +0 -0
  21. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/assets/mirpy_light.svg +0 -0
  22. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/assets/mirpy_light_bg.svg +0 -0
  23. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/data/signature_reference_studies.tsv +0 -0
  24. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/Makefile +0 -0
  25. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/_ext/themed_graphviz.py +0 -0
  26. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/_static/custom.css +0 -0
  27. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/_static/depth_robustness.png +0 -0
  28. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/_static/mirpy_dark.png +0 -0
  29. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/_static/mirpy_dark.svg +0 -0
  30. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/_static/mirpy_light.png +0 -0
  31. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/_static/mirpy_light.svg +0 -0
  32. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/_static/sample_embedding_diagram.png +0 -0
  33. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/_templates/site-nav.html +0 -0
  34. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/api.rst +0 -0
  35. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/channels.rst +0 -0
  36. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/conf.py +0 -0
  37. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/examples.rst +0 -0
  38. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/index.rst +0 -0
  39. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/math.rst +0 -0
  40. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/notebooks.rst +0 -0
  41. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/preprocessing.rst +0 -0
  42. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/docs/usage.rst +0 -0
  43. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/setup.sh +0 -0
  44. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/aliases.py +0 -0
  45. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/alleles.py +0 -0
  46. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/bench/__init__.py +0 -0
  47. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/bench/eval.py +0 -0
  48. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/bench/metrics.py +0 -0
  49. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/bench/theory.py +0 -0
  50. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/bench/vdjdb.py +0 -0
  51. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/cohort.py +0 -0
  52. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/density.py +0 -0
  53. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/distances/__init__.py +0 -0
  54. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/distances/germline.py +0 -0
  55. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/distances/junction.py +0 -0
  56. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/distances/shm.py +0 -0
  57. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/embedding/__init__.py +0 -0
  58. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/embedding/pca.py +0 -0
  59. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/embedding/presets.py +0 -0
  60. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/embedding/prototypes.py +0 -0
  61. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/embedding/tcremp.py +0 -0
  62. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/explain.py +0 -0
  63. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/generate.py +0 -0
  64. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/ml/__init__.py +0 -0
  65. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/ml/bundle.py +0 -0
  66. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/ml/codec.py +0 -0
  67. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/ml/decoder.py +0 -0
  68. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/ml/diffusion.py +0 -0
  69. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/ml/encoder.py +0 -0
  70. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/ml/set_encoder.py +0 -0
  71. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/ml/tokenize.py +0 -0
  72. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/ml/train.py +0 -0
  73. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/py.typed +0 -0
  74. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/repertoire.py +0 -0
  75. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/__init__.py +0 -0
  76. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/gene_library/README.md +0 -0
  77. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/gene_library/__init__.py +0 -0
  78. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/gene_library/build_gene_library.py +0 -0
  79. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/gene_library/build_region_annotations.py +0 -0
  80. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/gene_library/imgt_gene_library.txt +0 -0
  81. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/gene_library/olga_gene_library.txt +0 -0
  82. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/gene_library/region_annotations.txt +0 -0
  83. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/build_germline_dist.py +0 -0
  84. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/human_IGH.npz +0 -0
  85. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/human_IGK.npz +0 -0
  86. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/human_IGL.npz +0 -0
  87. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/human_TRA.npz +0 -0
  88. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/human_TRB.npz +0 -0
  89. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/human_TRD.npz +0 -0
  90. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/human_TRG.npz +0 -0
  91. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/mouse_IGH.npz +0 -0
  92. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/mouse_IGK.npz +0 -0
  93. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/mouse_IGL.npz +0 -0
  94. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/mouse_TRA.npz +0 -0
  95. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/mouse_TRB.npz +0 -0
  96. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/mouse_TRD.npz +0 -0
  97. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/germline_dist/mouse_TRG.npz +0 -0
  98. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/prototypes/.gitkeep +0 -0
  99. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/prototypes/generate_prototypes.py +0 -0
  100. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/prototypes/human_IGH.tsv +0 -0
  101. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/prototypes/human_IGK.tsv +0 -0
  102. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/prototypes/human_IGL.tsv +0 -0
  103. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/prototypes/human_TRA.tsv +0 -0
  104. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/prototypes/human_TRB.tsv +0 -0
  105. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/prototypes/human_TRD.tsv +0 -0
  106. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/prototypes/human_TRG.tsv +0 -0
  107. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/prototypes/manifest.json +0 -0
  108. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/prototypes/mouse_TRA.tsv +0 -0
  109. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/resources/prototypes/mouse_TRB.tsv +0 -0
  110. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/signature/__init__.py +0 -0
  111. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/signature/features.py +0 -0
  112. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/track.py +0 -0
  113. {mirpy_lib-4.0.0 → mirpy_lib-4.2.0}/src/mir/twin.py +0 -0
@@ -3,6 +3,66 @@
3
3
  All notable changes to `mirpy-lib` (import `mir`). This project follows semantic versioning; the v3 line is a
4
4
  greenfield ML/embedding rewrite (the classical v1.x/v2 toolkit is frozen on branch `legacy-v2`).
5
5
 
6
+ ## 4.2.0 — 2026-09-28
7
+
8
+ The `rsig` half of the three real corpora, and the mechanism that ships them. Requires vdjtools
9
+ **>=4.2.0**, which owns the fit and the resolver.
10
+
11
+ **Five new corpora**: `blood` (11,117 samples, 947 study_ids), `blood-uncapped` (22,441), `tissue`
12
+ (21,131 / 1,934), `tissue-uncapped` (33,874) and `deep-tcr` (3,936 amplicon samples, TRA + TRB). All
13
+ nine corpora are now fitted at **k=256** per locus, up from 128, and the full rotation ships —
14
+ truncating downward at apply time is exact, so a wider artifact serves every narrower request.
15
+
16
+ **Artifacts are no longer bundled in the wheel.** At k=256 an `rsig` artifact is ~3.1 MB and the pair
17
+ of halves across nine corpora is ~110 MB, so the wheel carries a few KB of `corpora.json` — every
18
+ corpus name with its size and SHA-256 — and the library fetches an artifact on first use into
19
+ `$VDJTOOLS_CORPUS_DIR` (default `~/.cache/vdjtools/signature`), verified against that digest. One
20
+ resolver serves both halves, so they cannot disagree about precedence: a local path always beats a
21
+ download. `mir corpus --fetch all` pre-warms the cache, which is the install-time step.
22
+
23
+ `publish.yml` publishes **only for a `v*` tag** now: without that guard, creating the `corpora-*`
24
+ release fires it and tries to upload whatever version `pyproject` carries.
25
+
26
+ The two halves of a corpus are published by the library that owns each — `vsig` in vdjtools' release
27
+ assets, `rsig` in mirpy's — so neither repo has to know the other's layout, and they share one cache
28
+ directory because a `vsig`/`rsig` pair belongs in one place.
29
+
30
+ A real corpus's manifest carries no cohort label, no dataset name and no accession: the population is
31
+ recorded as rule text with aggregate counts, per-locus read floors and the measured read band. A
32
+ rotation is about features and components.
33
+
34
+ ## 4.1.0 — 2026-09-28
35
+
36
+ **Two corpora that describe a real compartment**, `synthetic-blood` and `synthetic-tissue`, shipped
37
+ as `rsig_synthetic-blood.npz` / `rsig_synthetic-tissue.npz` beside the `naive` and `memory` pair.
38
+ Requires vdjtools **>=4.1.0**, which is where the draw lives: one `corpus_plan` resolves a corpus name
39
+ for both halves and one `corpus_meta` writes both manifests, so a `vsig_<name>`/`rsig_<name>` pair
40
+ cannot disagree about the cohort, the three ladders, the rank correlations or the germline it was drawn
41
+ from -- which is what makes joining them on `sample_id` mean anything.
42
+
43
+ Each repertoire is a naive/memory **mixture** drawn across three quantile ladders measured per locus
44
+ on the compartment it is named after: clonotype richness, reads per expanded clone, and the singleton
45
+ fraction that stands in for the naive share. The construction, why reads per *expanded* clone is the
46
+ drawable quantity, and the acceptance table are in [vdjtools'
47
+ CHANGELOG](https://github.com/antigenomics/vdjtools/blob/master/CHANGELOG.md) and
48
+ `docs/signature.rst`. The short version: `naive` and `memory` draw depth over 2.4x-11.0x per locus,
49
+ while real blood TRB richness spans 43x (74 to 3,162 clonotypes, n = 34,365 samples) and tissue IGH
50
+ 259x -- and a corpus estimates its bounds, centre and per-PC scaling from its own draw, none of which
51
+ extrapolates.
52
+
53
+ ### Changed: `synthesize` takes a corpus name, and the rsig manifest records its depth
54
+
55
+ `mir.signature.synthesize`'s first parameter is `corpus_name` (`naive` | `memory` |
56
+ `synthetic-blood` | `synthetic-tissue`), not `regime`. `mir corpus --corpus` accepts all four, and
57
+ `--size` defaults to `auto` -- 10,000 for a pure regime, the cohort's median richness per locus for a
58
+ `synthetic-*` one. `--depth-spread` is new and is **refused** on a `synthetic-*` corpus rather than
59
+ silently ignored, because that band is measured.
60
+
61
+ The rsig manifest now carries `depth_spread`, `depth_spread_requested`, `cohort`, `richness_band`,
62
+ `expanded_count_band`, `singleton_frac_band` and `rank_corr`. It previously carried **no depth field at
63
+ all**, which left the one thing a reader needs in order to know whether a corpus covers their samples
64
+ readable only from the vsig half.
65
+
6
66
  ## 4.0.0 — 2026-09-27
7
67
 
8
68
  **Signatures rewritten from scratch.** The geometry half; the statistics half is vdjtools, which
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mirpy-lib
3
- Version: 4.0.0
3
+ Version: 4.2.0
4
4
  Summary: ML-oriented embeddings for immune receptor repertoires (TCR/BCR)
5
5
  Project-URL: Homepage, https://github.com/antigenomics/mirpy
6
6
  Project-URL: Repository, https://github.com/antigenomics/mirpy
@@ -24,7 +24,7 @@ Requires-Dist: polars
24
24
  Requires-Dist: scikit-learn
25
25
  Requires-Dist: scipy
26
26
  Requires-Dist: seqtree>=1.0.0
27
- Requires-Dist: vdjtools>=4.0.1
27
+ Requires-Dist: vdjtools>=4.2.0
28
28
  Provides-Extra: ann
29
29
  Requires-Dist: numba>=0.60; extra == 'ann'
30
30
  Requires-Dist: pynndescent; extra == 'ann'
@@ -38,7 +38,7 @@ Requires-Dist: matplotlib; extra == 'bench'
38
38
  Requires-Dist: seaborn; extra == 'bench'
39
39
  Provides-Extra: build
40
40
  Requires-Dist: biopython; extra == 'build'
41
- Requires-Dist: vdjtools[model]>=4.0.1; extra == 'build'
41
+ Requires-Dist: vdjtools[model]>=4.2.0; extra == 'build'
42
42
  Provides-Extra: dev
43
43
  Requires-Dist: psutil>=5; extra == 'dev'
44
44
  Requires-Dist: pytest-cov; extra == 'dev'
@@ -145,9 +145,9 @@ mir embed repertoires cohort/*.tsv.gz -o phi.tsv --mmd mmd.tsv
145
145
 
146
146
  # the portable signature -> one fixed, named, standardised feature vector per sample
147
147
  # One tool per half: mirpy emits the geometry, vdjtools the statistics. Join on sample_id.
148
- mir corpus --corpus naive --smoke -o rsig_naive.npz # fit a corpus; no cohort needed
149
- mir signature --corpus rsig_naive.npz cohort/*.tsv.gz -o rsig.parquet # geometry half
150
- mir signature --corpus rsig_naive.npz --components 32 --describe # exactly what you get
148
+ mir signature --corpus blood cohort/*.tsv.gz -o rsig.parquet # fetched on first use
149
+ mir signature --corpus blood --components 32 --describe # exactly what you get
150
+ mir corpus --fetch all # pre-warm the corpus cache
151
151
  ```
152
152
 
153
153
  `mir embed clonotypes -h` / `mir embed repertoires -h` list every flag (species, locus,
@@ -503,17 +503,30 @@ regression, boosting or an MLP with no scaler of their own.
503
503
  Command line and library both, and they emit the same columns:
504
504
 
505
505
  ```bash
506
- mir corpus --corpus naive --smoke -o rsig_naive.npz
507
- mir signature --corpus rsig_naive.npz cohort/*.tsv.gz -o rsig.parquet
506
+ mir signature --corpus synthetic-blood cohort/*.tsv.gz -o rsig.parquet
507
+ mir corpus --corpus synthetic-tissue -o rsig_synthetic-tissue.npz # or build your own
508
508
  ```
509
509
 
510
510
  ```python
511
511
  from mir.signature import Corpus, rsig_cohort
512
512
 
513
- corpus = Corpus.load("rsig_naive.npz") # REQUIRED -- there is no default
514
- F = rsig_cohort(samples, corpus, n_jobs=0) # one row per sample
513
+ corpus = Corpus.load("rsig_synthetic-blood.npz") # REQUIRED -- there is no default
514
+ F = rsig_cohort(samples, corpus, n_jobs=0) # one row per sample
515
515
  ```
516
516
 
517
+ **Nine corpora, all at 256 components per locus.** Three are fitted on real repertoires -- `blood`
518
+ (11,117 bulk RNA-seq blood samples, 947 study groups), `tissue` (21,131 / 1,934) and `deep-tcr` (3,936
519
+ amplicon samples, TRA+TRB) -- each with an uncapped variant so the per-study cap's effect is
520
+ measurable. `synthetic-blood` and `synthetic-tissue` draw each repertoire as a naive/memory mixture
521
+ across three quantile ladders measured per locus on that compartment (richness, reads per expanded
522
+ clone, and the singleton fraction that stands in for the naive share), so they span the real depth
523
+ range while needing no cohort to rebuild; `naive` and `memory` remain the pure-regime references.
524
+
525
+ Artifacts are **fetched on first use** rather than bundled: at k=256 the nine corpora are ~110 MB
526
+ across both halves, so the wheel carries a few KB of index and downloads what you name into
527
+ `~/.cache/vdjtools/signature`, verified against the shipped SHA-256. `mir corpus --fetch all`
528
+ pre-warms it.
529
+
517
530
  Two halves, joined on `sample_id` and namespaced so they never collide: `vsig` (statistics of the
518
531
  clone-size vector, from [vdjtools](https://github.com/antigenomics/vdjtools)) and `rsig` (geometry —
519
532
  functionals of `Φ`). A column is `<sig>:<block>:<locus>:<feature>`. What comes out per locus is
@@ -532,9 +545,11 @@ and its centre and scale were fitted on zero rows of that artifact and arrived f
532
545
  corpus. Two independent fits, stitched; one shipped reference paired a centre of exactly `0.0` with
533
546
  a scale plainly fitted from data and put a corpus-typical sample **81 robust deviations** out.
534
547
 
535
- The two synthetic corpora need no cohort at all and are reproducible by anyone who installs the
536
- library — `naive` (every clone size 1, what the recombination model emits) and `memory` (Zipf
537
- rank-abundance clone sizes). `deep-tcr`, `blood` and `tissue` are fitted on real cohorts.
548
+ All four shipped corpora need no cohort at all and are reproducible by anyone who installs the
549
+ library — `synthetic-blood` and `synthetic-tissue` (the naive/memory mixture drawn across a
550
+ compartment's measured per-locus ladders), plus the pure regimes `naive` (every clone size 1, what
551
+ the recombination model emits) and `memory` (Zipf rank-abundance clone sizes). `deep-tcr`, `blood`
552
+ and `tissue` will be fitted on real cohorts and are not shipped yet.
538
553
 
539
554
  **Holes are never zeros.** An unsequenced locus, a compartment below its clonotype floor, or a
540
555
  statistic the sample is too shallow to estimate is `nan` plus a `mask:` column, because a model that
@@ -89,9 +89,9 @@ mir embed repertoires cohort/*.tsv.gz -o phi.tsv --mmd mmd.tsv
89
89
 
90
90
  # the portable signature -> one fixed, named, standardised feature vector per sample
91
91
  # One tool per half: mirpy emits the geometry, vdjtools the statistics. Join on sample_id.
92
- mir corpus --corpus naive --smoke -o rsig_naive.npz # fit a corpus; no cohort needed
93
- mir signature --corpus rsig_naive.npz cohort/*.tsv.gz -o rsig.parquet # geometry half
94
- mir signature --corpus rsig_naive.npz --components 32 --describe # exactly what you get
92
+ mir signature --corpus blood cohort/*.tsv.gz -o rsig.parquet # fetched on first use
93
+ mir signature --corpus blood --components 32 --describe # exactly what you get
94
+ mir corpus --fetch all # pre-warm the corpus cache
95
95
  ```
96
96
 
97
97
  `mir embed clonotypes -h` / `mir embed repertoires -h` list every flag (species, locus,
@@ -447,17 +447,30 @@ regression, boosting or an MLP with no scaler of their own.
447
447
  Command line and library both, and they emit the same columns:
448
448
 
449
449
  ```bash
450
- mir corpus --corpus naive --smoke -o rsig_naive.npz
451
- mir signature --corpus rsig_naive.npz cohort/*.tsv.gz -o rsig.parquet
450
+ mir signature --corpus synthetic-blood cohort/*.tsv.gz -o rsig.parquet
451
+ mir corpus --corpus synthetic-tissue -o rsig_synthetic-tissue.npz # or build your own
452
452
  ```
453
453
 
454
454
  ```python
455
455
  from mir.signature import Corpus, rsig_cohort
456
456
 
457
- corpus = Corpus.load("rsig_naive.npz") # REQUIRED -- there is no default
458
- F = rsig_cohort(samples, corpus, n_jobs=0) # one row per sample
457
+ corpus = Corpus.load("rsig_synthetic-blood.npz") # REQUIRED -- there is no default
458
+ F = rsig_cohort(samples, corpus, n_jobs=0) # one row per sample
459
459
  ```
460
460
 
461
+ **Nine corpora, all at 256 components per locus.** Three are fitted on real repertoires -- `blood`
462
+ (11,117 bulk RNA-seq blood samples, 947 study groups), `tissue` (21,131 / 1,934) and `deep-tcr` (3,936
463
+ amplicon samples, TRA+TRB) -- each with an uncapped variant so the per-study cap's effect is
464
+ measurable. `synthetic-blood` and `synthetic-tissue` draw each repertoire as a naive/memory mixture
465
+ across three quantile ladders measured per locus on that compartment (richness, reads per expanded
466
+ clone, and the singleton fraction that stands in for the naive share), so they span the real depth
467
+ range while needing no cohort to rebuild; `naive` and `memory` remain the pure-regime references.
468
+
469
+ Artifacts are **fetched on first use** rather than bundled: at k=256 the nine corpora are ~110 MB
470
+ across both halves, so the wheel carries a few KB of index and downloads what you name into
471
+ `~/.cache/vdjtools/signature`, verified against the shipped SHA-256. `mir corpus --fetch all`
472
+ pre-warms it.
473
+
461
474
  Two halves, joined on `sample_id` and namespaced so they never collide: `vsig` (statistics of the
462
475
  clone-size vector, from [vdjtools](https://github.com/antigenomics/vdjtools)) and `rsig` (geometry —
463
476
  functionals of `Φ`). A column is `<sig>:<block>:<locus>:<feature>`. What comes out per locus is
@@ -476,9 +489,11 @@ and its centre and scale were fitted on zero rows of that artifact and arrived f
476
489
  corpus. Two independent fits, stitched; one shipped reference paired a centre of exactly `0.0` with
477
490
  a scale plainly fitted from data and put a corpus-typical sample **81 robust deviations** out.
478
491
 
479
- The two synthetic corpora need no cohort at all and are reproducible by anyone who installs the
480
- library — `naive` (every clone size 1, what the recombination model emits) and `memory` (Zipf
481
- rank-abundance clone sizes). `deep-tcr`, `blood` and `tissue` are fitted on real cohorts.
492
+ All four shipped corpora need no cohort at all and are reproducible by anyone who installs the
493
+ library — `synthetic-blood` and `synthetic-tissue` (the naive/memory mixture drawn across a
494
+ compartment's measured per-locus ladders), plus the pure regimes `naive` (every clone size 1, what
495
+ the recombination model emits) and `memory` (Zipf rank-abundance clone sizes). `deep-tcr`, `blood`
496
+ and `tissue` will be fitted on real cohorts and are not shipped yet.
482
497
 
483
498
  **Holes are never zeros.** An unsequenced locus, a compartment below its clonotype floor, or a
484
499
  statistic the sample is too shallow to estimate is `nan` plus a `mask:` column, because a model that
@@ -24,7 +24,10 @@ Quickstart
24
24
 
25
25
  .. code-block:: bash
26
26
 
27
- # build a corpus once -- uses no samples from anybody's cohort
27
+ # four corpora ship with the wheel; name one, no build and no cohort needed
28
+ mir signature --corpus synthetic-blood cohort/*.tsv.gz -o rsig.tsv
29
+
30
+ # or build your own -- still uses no samples from anybody's cohort
28
31
  mir corpus --corpus naive --smoke -o naive_rsig.npz
29
32
 
30
33
  mir signature --corpus naive_rsig.npz cohort/*.tsv.gz -o rsig.tsv
@@ -187,11 +190,28 @@ Building a corpus
187
190
 
188
191
  .. code-block:: bash
189
192
 
193
+ mir corpus --corpus synthetic-blood -o synthetic-blood_rsig.npz
194
+ mir corpus --corpus synthetic-tissue -o synthetic-tissue_rsig.npz
190
195
  mir corpus --corpus naive -o naive_rsig.npz
191
196
  mir corpus --corpus memory --size n_eff --components 0.95 -o memory_rsig.npz
192
197
  mir corpus --smoke -o /tmp/smoke.npz
193
198
  mir corpus --corpus naive -j 8 -o naive_rsig.npz # 8 worker processes
194
199
 
200
+ Four corpora ship, all synthetic. ``synthetic-blood`` and ``synthetic-tissue`` are the ones to reach
201
+ for: each repertoire is a naive/memory **mixture** drawn from three quantile ladders measured per
202
+ locus on the cohort it is named after -- richness, reads per expanded clone, and the singleton
203
+ fraction that stands in for the naive compartment -- through that cohort's measured rank
204
+ correlations. ``naive`` and ``memory`` are the pure regimes, and neither varies what a cohort varies:
205
+ ``memory`` at a fixed size has the same read count in every sample. The construction, the
206
+ acceptance table and why reads per *expanded* clone is the drawable quantity are in `the vdjtools
207
+ half <https://docs.isalgo.dev/vdjtools/signature.html#the-two-cohort-corpora-three-measured-ladders-per-locus>`_,
208
+ since one module draws for both.
209
+
210
+ Both halves must be built from the **same corpus name and seed**: they resolve the name through one
211
+ ``corpus_plan`` and write the manifest through one ``corpus_meta``, so the cohort, the three ladders,
212
+ the rank correlations and the germline fingerprint cannot disagree between ``vsig_<name>`` and
213
+ ``rsig_<name>`` -- which is what makes joining them on ``sample_id`` meaningful.
214
+
195
215
  The build runs across worker **processes**, one contiguous block of samples each; ``-j/--jobs``
196
216
  defaults to every core the process may use (``-j 1`` stays in-process). Both halves share one
197
217
  builder, so a sample is the same repertoire in either -- its generator is seeded from
@@ -36,7 +36,7 @@ dependencies = [
36
36
  # `vdjtools.signature.corpus`, which does not exist before 4.0.0, and 4.0.0 never
37
37
  # reached PyPI. On the old `>=3.18.0` pip would have resolved vdjtools 3.18.1 beside
38
38
  # mirpy 4.0.0 and every signature entry point would have failed on import.
39
- "vdjtools>=4.0.1",
39
+ "vdjtools>=4.2.0",
40
40
  ]
41
41
  license = { text = "GPL-3.0-or-later" }
42
42
 
@@ -46,7 +46,7 @@ annotate = ["vdjmatch"]
46
46
  # Build-time only: regenerate region_annotations.txt (arda) and the baked
47
47
  # germline distance matrices (BioPython). Runtime needs neither. arda comes
48
48
  # via vdjtools[model] (single arda version across the ecosystem), not pinned directly.
49
- build = ["vdjtools[model]>=4.0.1", "biopython"]
49
+ build = ["vdjtools[model]>=4.2.0", "biopython"]
50
50
  # Part 2: neural codecs and density methods.
51
51
  ml = ["torch"]
52
52
  # Benchmark harness: DBSCAN eps selection + plotting + HuggingFace dataset fetch +
@@ -63,11 +63,11 @@ pip install "mirpy-lib[build]" # BioPython + arda: regenerate baked r
63
63
  ```bash
64
64
  mir embed clonotypes SAMPLE -o out.parquet # per-clonotype table (e0…)
65
65
  mir embed repertoires S1 S2 … -o phi.tsv --mmd mmd.tsv # one Φ(S) per sample per chain (phi0…)
66
- mir corpus --corpus naive --smoke -o rsig_naive.npz # build + fit a corpus
67
- mir signature S1 S2 … --corpus rsig_naive.npz -o rsig.parquet # geometry ONLY (rsig)
66
+ mir corpus --fetch all # pre-warm the corpus cache
67
+ mir signature S1 S2 … --corpus blood -o rsig.parquet # geometry ONLY (rsig)
68
68
  # the vsig half is `vdjtools signature`, against the SAME corpus name and seed;
69
69
  # join the two on sample_id for the full vector
70
- mir signature --corpus rsig_naive.npz --describe # exactly what this invocation emits
70
+ mir signature --corpus blood --describe # exactly what this invocation emits
71
71
  ```
72
72
 
73
73
  - Reads anything `vdjtools.io.read` sniffs (AIRR TSV, vdjtools, MiXCR, immunoSEQ, parquet).
@@ -153,7 +153,7 @@ one contract: `vsig` (statistics, from `vdjtools.signature`) and `rsig` (geometr
153
153
 
154
154
  ```python
155
155
  from mir.signature import Corpus, rsig, rsig_cohort, raw_and_channels, synthesize
156
- corpus = Corpus.load("rsig_naive.npz") # REQUIRED -- there is no default corpus
156
+ corpus = Corpus.load(bundled_path("blood")) # REQUIRED -- no default corpus
157
157
  v = rsig({"TRB": df}, corpus)
158
158
  F = rsig_cohort(samples, corpus, n_jobs=0)
159
159
  raw, chan = raw_and_channels({"TRB": df}, corpus.vocab) # un-rotated, natural units
@@ -15,7 +15,7 @@ from __future__ import annotations
15
15
 
16
16
  import os
17
17
 
18
- __version__ = "4.0.0"
18
+ __version__ = "4.2.0"
19
19
 
20
20
  __all__ = ["__version__", "get_resource_path", "TCREmp", "PairedTCREmp"]
21
21
 
@@ -407,12 +407,30 @@ def cmd_corpus(a: argparse.Namespace) -> None:
407
407
 
408
408
  from mir.signature import synthesize
409
409
 
410
- size = a.size
411
- if size not in ("n_eff", "p05", "p95"):
410
+ if a.fetch:
411
+ from vdjtools.signature.corpus import corpora_index, corpus_cache_dir, fetch_artifact
412
+
413
+ from mir.signature.signature import CORPUS_REPO, _RES, bundled_names
414
+ idx = corpora_index(_RES)
415
+ if not idx:
416
+ raise SystemExit("this version ships no corpus index, so there is nothing to fetch; "
417
+ f"the corpora it knows are {', '.join(bundled_names()) or '(none)'}")
418
+ for nm in (sorted(idx) if a.fetch == "all" else [a.fetch]):
419
+ if nm not in idx:
420
+ raise SystemExit(f"no published corpus named {nm!r}; have {', '.join(sorted(idx))}")
421
+ p = fetch_artifact(nm, sig="rsig", res_dir=_RES, repo=CORPUS_REPO)
422
+ print(f"[mir] {p} {p.stat().st_size / 1e6:.2f} MB", file=sys.stderr)
423
+ print(f"[mir] cache: {corpus_cache_dir()}", file=sys.stderr)
424
+ return
425
+ if not a.output:
426
+ raise SystemExit("-o/--output is required when building a corpus (--fetch does not use it)")
427
+
428
+ size = None if a.size == "auto" else a.size
429
+ if a.size not in ("auto", "n_eff", "p05", "p95"):
412
430
  try:
413
- size = int(size)
431
+ size = int(a.size)
414
432
  except ValueError:
415
- raise SystemExit(f"--size must be an integer or one of n_eff, p05, p95; "
433
+ raise SystemExit(f"--size must be an integer or one of auto, n_eff, p05, p95; "
416
434
  f"got {a.size!r}") from None
417
435
  n_samples = a.samples
418
436
  if a.smoke:
@@ -424,6 +442,7 @@ def cmd_corpus(a: argparse.Namespace) -> None:
424
442
  art, _rows = synthesize(a.corpus, n_samples=n_samples, size=size, seed=a.seed,
425
443
  n_components=ks, mode=a.winsorize, winsor_p=a.winsor_p,
426
444
  source=a.source, species=a.species, n_jobs=a.jobs,
445
+ depth_spread=a.depth_spread,
427
446
  progress=lambda loc, d, t: print(
428
447
  f" {loc:4s} {d}/{t} {time.time() - t0:5.0f}s",
429
448
  file=sys.stderr, flush=True), **kw)
@@ -552,6 +571,7 @@ def build_parser() -> argparse.ArgumentParser:
552
571
  "installs the library.\n"),
553
572
  epilog=(
554
573
  " mir corpus --corpus naive -o naive.npz\n"
574
+ " mir corpus --corpus synthetic-blood -o synthetic-blood.npz\n"
555
575
  " mir corpus --corpus memory --size n_eff --components 0.95 -o memory.npz\n"
556
576
  " mir corpus --smoke -o /tmp/smoke.npz # minutes, not hours\n"
557
577
  "\n"
@@ -562,12 +582,24 @@ def build_parser() -> argparse.ArgumentParser:
562
582
  "Use the SAME name and seed as `vdjtools corpus` so the two halves describe the\n"
563
583
  "same repertoires; that is what makes joining them meaningful.\n"),
564
584
  )
565
- c2.add_argument("--corpus", default="naive", choices=("naive", "memory"),
566
- help="naive: every clone size 1; memory: Zipf rank-abundance clone sizes")
567
- c2.add_argument("-o", "--output", required=True, help="artifact path; writes .npz and .json")
585
+ c2.add_argument("--corpus", default="naive",
586
+ choices=("naive", "memory", "synthetic-blood", "synthetic-tissue"),
587
+ help="naive: every clone size 1; memory: Zipf rank-abundance clone sizes; "
588
+ "synthetic-blood / synthetic-tissue: the mixture of the two, drawn across "
589
+ "that real cohort's measured per-locus richness, reads per clonotype and "
590
+ "singleton fraction")
591
+ c2.add_argument("-o", "--output", help="artifact path; writes .npz and .json. Required "
592
+ "unless --fetch, which downloads into the cache instead")
593
+ c2.add_argument("--fetch", metavar="NAME|all",
594
+ help="download a published corpus instead of building one, into the cache "
595
+ "($VDJTOOLS_CORPUS_DIR or ~/.cache/vdjtools/signature); `all` pre-warms "
596
+ "every corpus, which is the install-time step since an artifact is "
597
+ "otherwise fetched on first use")
568
598
  c2.add_argument("--samples", type=int, default=10_000, help="repertoires in the corpus")
569
- c2.add_argument("--size", default="10000",
570
- help="receptors per repertoire: an integer, or n_eff / p05 / p95")
599
+ c2.add_argument("--size", default="auto",
600
+ help="receptors per repertoire: an integer, or n_eff / p05 / p95. Default auto "
601
+ "is the corpus's own -- 10000 for naive/memory, the centre of the cohort's "
602
+ "measured richness band for a synthetic-* one")
571
603
  c2.add_argument("--components", default="128",
572
604
  help="components per locus: an integer count, or a variance fraction")
573
605
  c2.add_argument("--winsorize", default="features", choices=("features", "pcs", "none"))
@@ -576,6 +608,12 @@ def build_parser() -> argparse.ArgumentParser:
576
608
  c2.add_argument("--loci", default=None, help="comma-separated subset (default: all seven)")
577
609
  c2.add_argument("--source", default="olga", choices=("olga", "learned", "arda"))
578
610
  c2.add_argument("--species", default="human")
611
+ c2.add_argument("--depth-spread", type=float, default=None,
612
+ help="multiplicative depth range each repertoire's size is drawn log-uniformly "
613
+ "across, around --size; default is the corpus's own (2.4x-11.0x for "
614
+ "naive/memory, 43x on blood TRB and 259x on tissue IGH for the "
615
+ "synthetic-* ones). --size 3162 --depth-spread 1000 spans 100 to 100000 "
616
+ "receptors. Must match the vsig half to describe the same repertoires")
579
617
  c2.add_argument("--smoke", action="store_true",
580
618
  help="reduced build (200 samples of 1000) for tests and the cmp check")
581
619
  c2.add_argument("-j", "--jobs", type=int, default=0,
@@ -0,0 +1,92 @@
1
+ {
2
+ "blood": {
3
+ "json": {
4
+ "bytes": 2522,
5
+ "sha256": "366d788ec0ad0866eaf29ab9f9595daf33699b0db0b8e2a6e38a25f72b3caeeb"
6
+ },
7
+ "npz": {
8
+ "bytes": 3106113,
9
+ "sha256": "eed16130f1a49a3adce5d06cd58f94908ad872834c652aabd1ef52230cca1dc1"
10
+ }
11
+ },
12
+ "blood-uncapped": {
13
+ "json": {
14
+ "bytes": 2542,
15
+ "sha256": "527dc2f0a5bd659ea6ce4a545be6629300d0a0f4c8247e8910e30393900c1c51"
16
+ },
17
+ "npz": {
18
+ "bytes": 3106878,
19
+ "sha256": "7c647d147d82a3bf1d4ac1545014976367fb2b6848e1dfda48c62266f5a3cbd0"
20
+ }
21
+ },
22
+ "deep-tcr": {
23
+ "json": {
24
+ "bytes": 1662,
25
+ "sha256": "dfe85f918778f1d4dcc0702bbb225aba9fc8418aa44874ab6e7db4bb2162c9af"
26
+ },
27
+ "npz": {
28
+ "bytes": 1008863,
29
+ "sha256": "68e102627f2d9db454520321fa3b8c253bd2909bfbec8d6395959003f9535760"
30
+ }
31
+ },
32
+ "memory": {
33
+ "json": {
34
+ "bytes": 2247,
35
+ "sha256": "2aea36575627e83dd991cbace08efbccd72f27ff8d65d32a952ec2bbc03d102c"
36
+ },
37
+ "npz": {
38
+ "bytes": 3103450,
39
+ "sha256": "45c77bdf918b55e4a21044246dfbb205a4e65e3ab7564a68de16216f9b175df3"
40
+ }
41
+ },
42
+ "naive": {
43
+ "json": {
44
+ "bytes": 2241,
45
+ "sha256": "8127ff977fd5ede5ba78471ac4ae305f3fe22852960c6ebd2da233b1e2558f33"
46
+ },
47
+ "npz": {
48
+ "bytes": 3101749,
49
+ "sha256": "007fbc2fc1c9f4228ef88aec2bd690a0294218e74dfff93fc472ee6caa5613e5"
50
+ }
51
+ },
52
+ "synthetic-blood": {
53
+ "json": {
54
+ "bytes": 3812,
55
+ "sha256": "9560ddd0d6fff1a3fcb345153f1e0cf17b99a45a44681124481c508f164845f6"
56
+ },
57
+ "npz": {
58
+ "bytes": 3102697,
59
+ "sha256": "fd3722027de7957b69dc8a9e0a35e548c94de9aff5e441ccc10ccd28083fad68"
60
+ }
61
+ },
62
+ "synthetic-tissue": {
63
+ "json": {
64
+ "bytes": 3805,
65
+ "sha256": "4c7f12a1313c9f52aec2f61e11cde0efaf91e25e61304aab353b0542d9e270e9"
66
+ },
67
+ "npz": {
68
+ "bytes": 3102745,
69
+ "sha256": "bf66da96d8a778d9a626d1dfa1eb738b50869034fa248404d270dd54ddf3ab8d"
70
+ }
71
+ },
72
+ "tissue": {
73
+ "json": {
74
+ "bytes": 2520,
75
+ "sha256": "8936971ddf6d12bb39f9a4bcd9def6b2c9e075811fc044636760102effcacfd5"
76
+ },
77
+ "npz": {
78
+ "bytes": 3105251,
79
+ "sha256": "531e814ecdd053327adc3c4baf9cf81e5e73b80deadc77b89dd0098d82dd8eee"
80
+ }
81
+ },
82
+ "tissue-uncapped": {
83
+ "json": {
84
+ "bytes": 2538,
85
+ "sha256": "983d38cd3e597e3698528ded9a15ca6665199fee9122da7a11c0926e9ba7644b"
86
+ },
87
+ "npz": {
88
+ "bytes": 3105365,
89
+ "sha256": "aa82dae5dcf149292061caad4d1744f2665c3cf482f454fe4ac1e72fa4f4e636"
90
+ }
91
+ }
92
+ }
@@ -63,24 +63,33 @@ _register()
63
63
  _RES = __import__("pathlib").Path(__file__).resolve().parent.parent / "resources" / "signature"
64
64
 
65
65
 
66
+ #: The repo whose release assets carry the rsig artifacts. The vsig half lives in vdjtools' releases;
67
+ #: each half is published by the library that owns it, so a corpus name resolves to two downloads from
68
+ #: two repos rather than one repo having to know about the other's layout.
69
+ CORPUS_REPO = "antigenomics/mirpy"
70
+
71
+
66
72
  def bundled_names() -> list[str]:
67
- """Corpus names with an installed rsig artifact."""
68
- if not _RES.is_dir():
69
- return []
70
- return sorted(p.stem.removeprefix("rsig_") for p in _RES.glob("rsig_*.npz"))
73
+ """Every corpus name this version knows -- installed, cached, or downloadable."""
74
+ from vdjtools.signature.corpus import corpora_index, corpus_cache_dir
75
+
76
+ got = {p.stem.removeprefix("rsig_") for p in _RES.glob("rsig_*.npz")} if _RES.is_dir() else set()
77
+ cache = corpus_cache_dir()
78
+ if cache.is_dir():
79
+ got |= {p.stem.removeprefix("rsig_") for p in cache.glob("rsig_*.npz")}
80
+ return sorted(got | set(corpora_index(_RES)))
71
81
 
72
82
 
73
83
  def bundled_path(name):
74
- """Resolve a bundled corpus name or a filesystem path to an artifact, else ``None``."""
75
- from pathlib import Path
84
+ """Resolve a corpus name or a filesystem path to an ``rsig`` artifact, else ``None``.
85
+
86
+ Downloads on first use if the name is in the shipped index and not yet cached. One resolver,
87
+ shared with the vsig half, so the two cannot disagree about precedence -- a local path always
88
+ wins over a download.
89
+ """
90
+ from vdjtools.signature.corpus import resolve_artifact
76
91
 
77
- direct = Path(name)
78
- if direct.suffix == ".npz" and direct.exists():
79
- return direct
80
- if direct.with_suffix(".npz").exists():
81
- return direct.with_suffix(".npz")
82
- cand = _RES / f"rsig_{name}.npz"
83
- return cand if cand.exists() else None
92
+ return resolve_artifact(name, sig="rsig", res_dir=_RES, repo=CORPUS_REPO)
84
93
 
85
94
 
86
95
  def _model(species: str, locus: str):
@@ -261,11 +270,11 @@ def rsig_cohort(samples, corpus: C.Corpus, *, n_jobs: int = 1,
261
270
  return pl.DataFrame(rows).select([c for c in want if any(c in r for r in rows)])
262
271
 
263
272
 
264
- def synthesize(regime: str, *, loci: "tuple[str, ...]" = L.LOCI, n_samples: int = 10_000,
265
- size: "int | str" = C.DEFAULT_SIZE, seed: int = C.SEED,
273
+ def synthesize(corpus_name: str, *, loci: "tuple[str, ...]" = L.LOCI, n_samples: int = 10_000,
274
+ size: "int | str | None" = None, seed: int = C.SEED,
266
275
  n_components: "int | float" = C.DEFAULT_COMPONENTS, mode: str = "features",
267
276
  winsor_p: float = 0.01, source: str = "olga", species: str = "human",
268
- progress=None, n_jobs: int = 1):
277
+ progress=None, n_jobs: int = 1, depth_spread: "float | str | None" = None):
269
278
  """Build and fit an ``rsig`` corpus on the same synthetic repertoires ``vsig`` uses.
270
279
 
271
280
  The receptors, the seeds and the drawn depths are all
@@ -274,8 +283,17 @@ def synthesize(regime: str, *, loci: "tuple[str, ...]" = L.LOCI, n_samples: int
274
283
  ``sample_id`` meaningful rather than merely type-correct.
275
284
 
276
285
  Args:
286
+ corpus_name: One of :data:`vdjtools.signature.corpus.SYNTHETIC` -- ``"naive"`` / ``"memory"``
287
+ for the pure regimes, ``"synthetic-blood"`` / ``"synthetic-tissue"`` for the naive/memory
288
+ mixture drawn across that cohort's measured per-locus richness, read-depth and
289
+ singleton-fraction bands.
277
290
  n_jobs: Worker **processes** (not kernel threads). ``0`` means every available core; ``1``,
278
291
  the default, runs in-process. The artifact is bit-identical at every value.
292
+ size: ``None`` is the corpus's own -- 10,000 for a pure regime, the geometric centre of the
293
+ cohort's measured richness band for a ``synthetic-*`` one.
294
+ depth_spread: Multiplicative depth range each repertoire's size is drawn log-uniformly
295
+ across, around ``size``; ``None`` is the corpus's own. Must match the ``vsig`` half's
296
+ value for the two to describe the same repertoires.
279
297
 
280
298
  Returns:
281
299
  ``(corpus, mats)`` -- ``mats`` being ``{locus: (matrix, columns)}``, the corpus matrix itself.
@@ -284,23 +302,24 @@ def synthesize(regime: str, *, loci: "tuple[str, ...]" = L.LOCI, n_samples: int
284
302
 
285
303
  import mir
286
304
 
305
+ regime, cohort, size, depth_spread = C.corpus_plan(corpus_name, size=size,
306
+ depth_spread=depth_spread)
287
307
  vocab = {loc: {} for loc in loci}
288
308
  # Parallel across samples, in the one shared builder: an rsig row is a pure function of its
289
309
  # sample, so a worker draws its own repertoires from the memory-mapped pools and only the row of
290
310
  # numbers comes back. ``n_jobs`` does not change a single value.
291
311
  mats = C.build_matrices(
292
312
  regime, sig="rsig", vocab=vocab, loci=loci, n_samples=n_samples, size=size, seed=seed,
293
- source=source, n_jobs=n_jobs, progress=progress,
313
+ source=source, n_jobs=n_jobs, progress=progress, depth_spread=depth_spread, cohort=cohort,
294
314
  featurise=partial(raw_and_channels, vocab=vocab, species=species))
295
315
  return C.fit_matrices(
296
- mats, vocab, sig="rsig", name=regime, mode=mode, n_components=n_components,
316
+ mats, vocab, sig="rsig", name=corpus_name, mode=mode, n_components=n_components,
297
317
  winsor_p=winsor_p,
298
- meta={"regime": regime, "n_samples": n_samples, "seed": seed, "source": source,
299
- "size": size, "species": species, "K": F.K,
300
- "size_per_locus": {k: C.resolved_size(k, size) for k in loci},
301
- "mir_version": mir.__version__, "loci": list(loci),
302
- # The pools come out of these models; retraining one moves its locus. Gated on load.
303
- "models": C.model_fingerprint(loci, source)}), mats
318
+ # One manifest builder for both halves: a `vsig_<name>`/`rsig_<name>` pair that disagreed
319
+ # about the depths or the singleton fractions would be a joinability claim nobody can check.
320
+ meta=C.corpus_meta(regime, cohort, loci, n_samples=n_samples, size=size, seed=seed,
321
+ source=source, depth_spread=depth_spread,
322
+ mir_version=mir.__version__, species=species, K=F.K)), mats
304
323
 
305
324
 
306
325
  def _demo() -> None:
@@ -1,94 +0,0 @@
1
- {
2
- "K": 256,
3
- "content_sha256": "02be6824a511fca836f59eeaf51fcdd1dfe98d6f8d36cf80cfeb8731f28c3d22",
4
- "k": {
5
- "IGH": 128,
6
- "IGK": 128,
7
- "IGL": 128,
8
- "TRA": 128,
9
- "TRB": 128,
10
- "TRD": 128,
11
- "TRG": 128
12
- },
13
- "loci": [
14
- "TRA",
15
- "TRB",
16
- "TRG",
17
- "TRD",
18
- "IGH",
19
- "IGK",
20
- "IGL"
21
- ],
22
- "mir_version": "3.20.2",
23
- "mode": "features",
24
- "models": {
25
- "IGH": "olga:human_B_heavy@e21ec62f2139",
26
- "IGK": "olga:human_B_kappa@f160ed8ce718",
27
- "IGL": "olga:human_B_lambda@4756d868be49",
28
- "TRA": "olga:human_T_alpha@2a4e83d16fdf",
29
- "TRB": "olga:human_T_beta@2fccd0006de8",
30
- "TRD": "olga:human_T_delta@def2922b705f",
31
- "TRG": "olga:human_T_gamma@d25719222561"
32
- },
33
- "n_components_requested": 128,
34
- "n_rows": {
35
- "IGH": 10000,
36
- "IGK": 10000,
37
- "IGL": 10000,
38
- "TRA": 10000,
39
- "TRB": 10000,
40
- "TRD": 10000,
41
- "TRG": 10000
42
- },
43
- "n_samples": 10000,
44
- "name": "memory",
45
- "numpy": "2.5.2",
46
- "platform": "Linux-5.4.0-215-generic-x86_64-with-glibc2.31",
47
- "regime": "memory",
48
- "seed": 20260927,
49
- "sig": "rsig",
50
- "size": 10000,
51
- "size_per_locus": {
52
- "IGH": 10000,
53
- "IGK": 10000,
54
- "IGL": 10000,
55
- "TRA": 10000,
56
- "TRB": 10000,
57
- "TRD": 10000,
58
- "TRG": 10000
59
- },
60
- "source": "olga",
61
- "species": "human",
62
- "trimmed_frac": {
63
- "IGH": 0.010038860103626942,
64
- "IGK": 0.010038860103626942,
65
- "IGL": 0.010038860103626942,
66
- "TRA": 0.010038860103626942,
67
- "TRB": 0.010038860103626942,
68
- "TRD": 0.010038860103626942,
69
- "TRG": 0.010038860103626942
70
- },
71
- "variance_at_k": {
72
- "IGH": 0.9910758445764348,
73
- "IGK": 0.9993485868815669,
74
- "IGL": 0.9989373382451864,
75
- "TRA": 0.996276863840755,
76
- "TRB": 0.9956540910199058,
77
- "TRD": 0.9987458060164907,
78
- "TRG": 0.9992160798179918
79
- },
80
- "vocab": {
81
- "IGH": {},
82
- "IGK": {},
83
- "IGL": {},
84
- "TRA": {},
85
- "TRB": {},
86
- "TRD": {},
87
- "TRG": {}
88
- },
89
- "winsor_p": 0.01,
90
- "winsor_ps": [
91
- 0.01,
92
- 0.05
93
- ]
94
- }
@@ -1,94 +0,0 @@
1
- {
2
- "K": 256,
3
- "content_sha256": "6cc52f9da4a790b4553a978327d9ed41e7036856861322648c0483c6b15519d1",
4
- "k": {
5
- "IGH": 128,
6
- "IGK": 128,
7
- "IGL": 128,
8
- "TRA": 128,
9
- "TRB": 128,
10
- "TRD": 128,
11
- "TRG": 128
12
- },
13
- "loci": [
14
- "TRA",
15
- "TRB",
16
- "TRG",
17
- "TRD",
18
- "IGH",
19
- "IGK",
20
- "IGL"
21
- ],
22
- "mir_version": "3.20.2",
23
- "mode": "features",
24
- "models": {
25
- "IGH": "olga:human_B_heavy@e21ec62f2139",
26
- "IGK": "olga:human_B_kappa@f160ed8ce718",
27
- "IGL": "olga:human_B_lambda@4756d868be49",
28
- "TRA": "olga:human_T_alpha@2a4e83d16fdf",
29
- "TRB": "olga:human_T_beta@2fccd0006de8",
30
- "TRD": "olga:human_T_delta@def2922b705f",
31
- "TRG": "olga:human_T_gamma@d25719222561"
32
- },
33
- "n_components_requested": 128,
34
- "n_rows": {
35
- "IGH": 10000,
36
- "IGK": 10000,
37
- "IGL": 10000,
38
- "TRA": 10000,
39
- "TRB": 10000,
40
- "TRD": 10000,
41
- "TRG": 10000
42
- },
43
- "n_samples": 10000,
44
- "name": "naive",
45
- "numpy": "2.5.2",
46
- "platform": "Linux-5.4.0-215-generic-x86_64-with-glibc2.31",
47
- "regime": "naive",
48
- "seed": 20260927,
49
- "sig": "rsig",
50
- "size": 10000,
51
- "size_per_locus": {
52
- "IGH": 10000,
53
- "IGK": 10000,
54
- "IGL": 10000,
55
- "TRA": 10000,
56
- "TRB": 10000,
57
- "TRD": 10000,
58
- "TRG": 10000
59
- },
60
- "source": "olga",
61
- "species": "human",
62
- "trimmed_frac": {
63
- "IGH": 0.010012970168612191,
64
- "IGK": 0.01001284046692607,
65
- "IGL": 0.01001284046692607,
66
- "TRA": 0.010012710765239949,
67
- "TRB": 0.010012970168612191,
68
- "TRD": 0.010012970168612191,
69
- "TRG": 0.010012710765239949
70
- },
71
- "variance_at_k": {
72
- "IGH": 0.9887366548085227,
73
- "IGK": 0.9991668470828621,
74
- "IGL": 0.9986028566065589,
75
- "TRA": 0.9917447530288632,
76
- "TRB": 0.9923344290339433,
77
- "TRD": 0.9966133463827523,
78
- "TRG": 0.9986828452151979
79
- },
80
- "vocab": {
81
- "IGH": {},
82
- "IGK": {},
83
- "IGL": {},
84
- "TRA": {},
85
- "TRB": {},
86
- "TRD": {},
87
- "TRG": {}
88
- },
89
- "winsor_p": 0.01,
90
- "winsor_ps": [
91
- 0.01,
92
- 0.05
93
- ]
94
- }
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