seqpro 0.13.0__tar.gz → 0.15.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 (112) hide show
  1. {seqpro-0.13.0/.claude/skills/zensical-setup → seqpro-0.15.0/.claude/skills/zensical}/SKILL.md +1 -1
  2. {seqpro-0.13.0 → seqpro-0.15.0}/.github/workflows/release.yaml +2 -9
  3. {seqpro-0.13.0 → seqpro-0.15.0}/CHANGELOG.md +38 -0
  4. {seqpro-0.13.0 → seqpro-0.15.0}/CLAUDE.md +2 -0
  5. {seqpro-0.13.0 → seqpro-0.15.0}/PKG-INFO +1 -1
  6. seqpro-0.15.0/benchmarks/bench_to_packed.py +187 -0
  7. seqpro-0.15.0/docs/superpowers/plans/2026-05-31-rag-to-packed.md +815 -0
  8. seqpro-0.15.0/docs/superpowers/plans/2026-06-05-translate-unknown-codon-policy.md +1085 -0
  9. seqpro-0.15.0/docs/superpowers/specs/2026-05-31-rag-to-packed-design.md +134 -0
  10. seqpro-0.15.0/docs/superpowers/specs/2026-06-05-translate-unknown-codon-policy-design.md +213 -0
  11. {seqpro-0.13.0 → seqpro-0.15.0}/pyproject.toml +1 -1
  12. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/_encoders.py +4 -5
  13. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/_numba.py +110 -12
  14. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/alphabets/_alphabets.py +138 -21
  15. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/rag/__init__.py +2 -1
  16. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/rag/_array.py +22 -11
  17. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/rag/_ops.py +148 -5
  18. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/xr/__init__.py +2 -1
  19. {seqpro-0.13.0 → seqpro-0.15.0}/skills/seqpro/SKILL.md +4 -3
  20. {seqpro-0.13.0 → seqpro-0.15.0}/tests/bench_translate_lut.py +4 -4
  21. seqpro-0.15.0/tests/test_rag_to_packed.py +130 -0
  22. {seqpro-0.13.0 → seqpro-0.15.0}/tests/test_translate.py +218 -12
  23. seqpro-0.13.0/.claude/skills/zensical-setup/.skillfish.json +0 -10
  24. {seqpro-0.13.0 → seqpro-0.15.0}/.gitattributes +0 -0
  25. {seqpro-0.13.0 → seqpro-0.15.0}/.github/workflows/bump.yaml +0 -0
  26. {seqpro-0.13.0 → seqpro-0.15.0}/.github/workflows/docs.yml +0 -0
  27. {seqpro-0.13.0 → seqpro-0.15.0}/.github/workflows/lint.yaml +0 -0
  28. {seqpro-0.13.0 → seqpro-0.15.0}/.github/workflows/merge.yaml +0 -0
  29. {seqpro-0.13.0 → seqpro-0.15.0}/.github/workflows/publish.yaml +0 -0
  30. {seqpro-0.13.0 → seqpro-0.15.0}/.github/workflows/release-pipeline.yaml +0 -0
  31. {seqpro-0.13.0 → seqpro-0.15.0}/.github/workflows/test.yaml +0 -0
  32. {seqpro-0.13.0 → seqpro-0.15.0}/.gitignore +0 -0
  33. {seqpro-0.13.0 → seqpro-0.15.0}/.pre-commit-config.yaml +0 -0
  34. {seqpro-0.13.0 → seqpro-0.15.0}/Cargo.lock +0 -0
  35. {seqpro-0.13.0 → seqpro-0.15.0}/Cargo.toml +0 -0
  36. {seqpro-0.13.0 → seqpro-0.15.0}/LICENSE +0 -0
  37. {seqpro-0.13.0 → seqpro-0.15.0}/README.md +0 -0
  38. {seqpro-0.13.0 → seqpro-0.15.0}/benches/kshuffle.rs +0 -0
  39. {seqpro-0.13.0 → seqpro-0.15.0}/docs/api/alphabets.md +0 -0
  40. {seqpro-0.13.0 → seqpro-0.15.0}/docs/api/bed.md +0 -0
  41. {seqpro-0.13.0 → seqpro-0.15.0}/docs/api/gtf.md +0 -0
  42. {seqpro-0.13.0 → seqpro-0.15.0}/docs/api/index.md +0 -0
  43. {seqpro-0.13.0 → seqpro-0.15.0}/docs/api/ragged.md +0 -0
  44. {seqpro-0.13.0 → seqpro-0.15.0}/docs/api/types.md +0 -0
  45. {seqpro-0.13.0 → seqpro-0.15.0}/docs/index.md +0 -0
  46. {seqpro-0.13.0 → seqpro-0.15.0}/docs/ragged.md +0 -0
  47. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/plans/2026-05-04-ragged-record-array.md +0 -0
  48. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/plans/2026-05-05-documentation-site.md +0 -0
  49. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/plans/2026-05-05-narwhals-coord-schema.md +0 -0
  50. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/plans/2026-05-05-ragged-zip-and-record-introspection.md +0 -0
  51. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/plans/2026-05-20-kshuffle-optimization.md +0 -0
  52. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/plans/2026-05-20-kshuffle-pooled-buffers-and-k2-fast-path.md +0 -0
  53. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/plans/2026-05-20-kshuffle-wilson-single-pass.md +0 -0
  54. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/plans/2026-05-20-release-pipeline.md +0 -0
  55. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/plans/2026-05-28-translate-lut-validation.md +0 -0
  56. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/plans/2026-05-31-flat-buffer-to-padded.md +0 -0
  57. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/specs/2026-05-04-ragged-record-array-design.md +0 -0
  58. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/specs/2026-05-05-documentation-site-design.md +0 -0
  59. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/specs/2026-05-05-narwhals-coord-schema-design.md +0 -0
  60. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/specs/2026-05-05-ragged-zip-and-record-introspection-design.md +0 -0
  61. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/specs/2026-05-20-kshuffle-optimization-design.md +0 -0
  62. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/specs/2026-05-20-kshuffle-pooled-buffers-and-k2-fast-path-design.md +0 -0
  63. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/specs/2026-05-20-kshuffle-wilson-single-pass-design.md +0 -0
  64. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/specs/2026-05-20-release-pipeline-design.md +0 -0
  65. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/specs/2026-05-28-translate-lut-and-validation-design.md +0 -0
  66. {seqpro-0.13.0 → seqpro-0.15.0}/docs/superpowers/specs/2026-05-31-flat-buffer-to-padded-design.md +0 -0
  67. {seqpro-0.13.0 → seqpro-0.15.0}/meta.yaml +0 -0
  68. {seqpro-0.13.0 → seqpro-0.15.0}/pixi.lock +0 -0
  69. {seqpro-0.13.0 → seqpro-0.15.0}/pixi.toml +0 -0
  70. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/__init__.py +0 -0
  71. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/_analyzers.py +0 -0
  72. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/_cleaners.py +0 -0
  73. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/_coords.py +0 -0
  74. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/_modifiers.py +0 -0
  75. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/_types.py +0 -0
  76. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/_utils.py +0 -0
  77. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/alphabets/__init__.py +0 -0
  78. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/bed.py +0 -0
  79. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/experimental/_experimental.py +0 -0
  80. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/experimental/_visualizers.py +0 -0
  81. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/gtf.py +0 -0
  82. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/py.typed +0 -0
  83. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/rag/_gufuncs.py +0 -0
  84. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/rag/_types.py +0 -0
  85. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/rag/_utils.py +0 -0
  86. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/transforms/__init__.py +0 -0
  87. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/transforms/augmentation.py +0 -0
  88. {seqpro-0.13.0 → seqpro-0.15.0}/python/seqpro/transforms/tmm.py +0 -0
  89. {seqpro-0.13.0 → seqpro-0.15.0}/scratch_bench_rc.py +0 -0
  90. {seqpro-0.13.0 → seqpro-0.15.0}/scratch_bench_to_padded.py +0 -0
  91. {seqpro-0.13.0 → seqpro-0.15.0}/src/kmer_encode.rs +0 -0
  92. {seqpro-0.13.0 → seqpro-0.15.0}/src/kshuffle.rs +0 -0
  93. {seqpro-0.13.0 → seqpro-0.15.0}/src/kshuffle_ref.rs +0 -0
  94. {seqpro-0.13.0 → seqpro-0.15.0}/src/lib.rs +0 -0
  95. {seqpro-0.13.0 → seqpro-0.15.0}/tests/_shape_fixtures.py +0 -0
  96. {seqpro-0.13.0 → seqpro-0.15.0}/tests/bed/test_pyranges.py +0 -0
  97. {seqpro-0.13.0 → seqpro-0.15.0}/tests/bed/test_read.py +0 -0
  98. {seqpro-0.13.0 → seqpro-0.15.0}/tests/bed/test_sort.py +0 -0
  99. {seqpro-0.13.0 → seqpro-0.15.0}/tests/bed/test_with_length.py +0 -0
  100. {seqpro-0.13.0 → seqpro-0.15.0}/tests/conftest.py +0 -0
  101. {seqpro-0.13.0 → seqpro-0.15.0}/tests/test_analyzers.py +0 -0
  102. {seqpro-0.13.0 → seqpro-0.15.0}/tests/test_coords.py +0 -0
  103. {seqpro-0.13.0 → seqpro-0.15.0}/tests/test_encoders.py +0 -0
  104. {seqpro-0.13.0 → seqpro-0.15.0}/tests/test_modifiers.py +0 -0
  105. {seqpro-0.13.0 → seqpro-0.15.0}/tests/test_ohe.py +0 -0
  106. {seqpro-0.13.0 → seqpro-0.15.0}/tests/test_ragged.py +0 -0
  107. {seqpro-0.13.0 → seqpro-0.15.0}/tests/test_ragged_rc.py +0 -0
  108. {seqpro-0.13.0 → seqpro-0.15.0}/tests/test_ragged_to_padded.py +0 -0
  109. {seqpro-0.13.0 → seqpro-0.15.0}/tests/test_shape_matrix.py +0 -0
  110. {seqpro-0.13.0 → seqpro-0.15.0}/tests/test_tokenize.py +0 -0
  111. {seqpro-0.13.0 → seqpro-0.15.0}/tests/test_transforms.py +0 -0
  112. {seqpro-0.13.0 → seqpro-0.15.0}/zensical.toml +0 -0
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: zensical-setup
2
+ name: zensical
3
3
  description: >
4
4
  Guide for setting up and configuring Zensical documentation sites.
5
5
  Use when: create documentation site, configure zensical.toml, set up
@@ -1,9 +1,6 @@
1
1
  name: Create release
2
2
 
3
3
  on:
4
- push:
5
- tags:
6
- - '[0-9]+.[0-9]+.[0-9]+'
7
4
  workflow_dispatch:
8
5
  inputs:
9
6
  tag:
@@ -30,15 +27,11 @@ jobs:
30
27
  - uses: actions/checkout@v4
31
28
  with:
32
29
  fetch-depth: 0
30
+ ref: ${{ inputs.tag }}
33
31
 
34
32
  - name: Resolve tag
35
33
  id: tag
36
- run: |
37
- case "${{ github.event_name }}" in
38
- push) echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT ;;
39
- workflow_dispatch) echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT ;;
40
- workflow_call) echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT ;;
41
- esac
34
+ run: echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
42
35
 
43
36
  - name: Extract changelog section
44
37
  run: |
@@ -1,3 +1,41 @@
1
+ ## 0.15.0 (2026-06-07)
2
+
3
+ ### Feat
4
+
5
+ - **translate**: Ragged pad + drop paths via unknown=
6
+ - **translate**: add unknown= param (pad path) and overloads
7
+ - **translate**: case-insensitive validate=True
8
+ - **translate**: add _nb_drop_unknown_codons compaction kernel
9
+
10
+ ### Fix
11
+
12
+ - **translate**: update seqpro.xr translate call for marker_byte arg
13
+ - **translate**: restrict unknown marker to ASCII; document drop dense-table assumption
14
+ - **translate**: marker sentinel + case-folding in gufunc_translate_lut
15
+ - **translate**: marker sentinel + case-folding in gufunc_translate
16
+
17
+ ## 0.14.0 (2026-06-01)
18
+
19
+ ### Feat
20
+
21
+ - **rag**: add Ragged.to_packed method and export to_packed
22
+ - **rag**: add Numba-parallelized to_packed for flat layouts
23
+
24
+ ### Fix
25
+
26
+ - **ci**: check out release tag and drop duplicate release trigger
27
+ - **rag**: produce canonical list-of-records layout in to_packed
28
+
29
+ ### Refactor
30
+
31
+ - **rag**: make Ragged.to_packed copy keyword-only; add multibyte-trailing test
32
+ - **rag**: drop unused as_contiguous packing path in unbox
33
+
34
+ ### Perf
35
+
36
+ - **rag**: microbenchmark to_packed throughput vs ak.to_packed
37
+ - **rag**: use to_packed at internal ak.to_packed call sites
38
+
1
39
  ## 0.13.0 (2026-06-01)
2
40
 
3
41
  ### Feat
@@ -72,6 +72,8 @@ Only `kshuffle.rs` is compiled. It's exposed as `seqpro._k_shuffle` (called by `
72
72
  - Axis arguments (`length_axis`, `ohe_axis`) are always integers referring to the NumPy axis index; negative indexing is supported.
73
73
  - The `transforms/` module wraps functional ops into callable objects with `__call__` for use in data pipelines.
74
74
  - Conventional commits are enforced — use `feat:`, `fix:`, `ci:`, `bump:`, `refactor:`, `docs:`, etc. prefixes.
75
+ - **Validation is opt-in and front-loaded.** Add fast-fail/input validation via a `validate=` flag (or equivalent single opt-in), not per-feature `error` modes. There must be one obvious way to ask "is this input clean?" — don't duplicate the check across parameters.
76
+ - **No naive NumPy in hot paths.** Never use raw Python loops or naive NumPy (e.g. per-segment `np.concatenate`, Python `for` over sequences) where a Numba kernel is faster and leaner — unless the NumPy version is *verifiably* comparable in time and memory. When Numba is a poor fit (graph algorithms like k-shuffle), use the Rust/PyO3 extension (`src/`).
75
77
 
76
78
  ## Skills
77
79
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seqpro
3
- Version: 0.13.0
3
+ Version: 0.15.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -0,0 +1,187 @@
1
+ """Microbenchmark: seqpro.rag.to_packed vs ak.to_packed vs NumPy gather.
2
+
3
+ Run in the bench env:
4
+ pixi run -e bench python benchmarks/bench_to_packed.py --out bench_to_packed
5
+
6
+ Outputs <out>.csv and <out>_*.png. Sweeps n_rows, mean length, length
7
+ distribution, source (RAM vs memmap), dtype itemsize, and thread count, and
8
+ prints an effect-size ranking of which axes most affect the speedup ratio.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import os
15
+ import time
16
+ from pathlib import Path
17
+
18
+ import numpy as np
19
+
20
+
21
+ def make_lengths(rng, n_rows, mean_len, distribution):
22
+ if distribution == "uniform":
23
+ lo, hi = max(1, mean_len // 2), mean_len * 2
24
+ return rng.integers(lo, hi + 1, size=n_rows)
25
+ # long-tailed: most rows short, a few very long (load imbalance)
26
+ lengths = rng.integers(1, max(2, mean_len // 4), size=n_rows)
27
+ n_big = max(1, n_rows // 100)
28
+ lengths[rng.choice(n_rows, n_big, replace=False)] = mean_len * 50
29
+ return lengths
30
+
31
+
32
+ def build(rng, n_rows, mean_len, distribution, dtype, source, tmpdir):
33
+ import seqpro.rag as spr
34
+
35
+ lengths = make_lengths(rng, n_rows, mean_len, distribution).astype(np.int64)
36
+ total = int(lengths.sum())
37
+ if source == "memmap":
38
+ path = Path(tmpdir) / "data.dat"
39
+ data = np.memmap(path, dtype=dtype, mode="w+", shape=(total,))
40
+ data[:] = rng.integers(0, 255, size=total).astype(dtype)
41
+ else:
42
+ data = rng.integers(0, 255, size=total).astype(dtype)
43
+ rag = spr.Ragged.from_lengths(data, lengths)
44
+ # reorder so the layout is an (unpacked) ListArray, forcing a real gather
45
+ return rag[rng.permutation(n_rows)], total * np.dtype(dtype).itemsize
46
+
47
+
48
+ def numpy_gather(rag):
49
+ import seqpro.rag as spr
50
+
51
+ parts = rag._parts
52
+ offs = parts.offsets
53
+ starts, stops = (offs[0], offs[1]) if offs.ndim == 2 else (offs[:-1], offs[1:])
54
+ lengths = stops - starts
55
+ out_off = np.empty(lengths.size + 1, dtype=np.int64)
56
+ out_off[0] = 0
57
+ np.cumsum(lengths, out=out_off[1:])
58
+ idx = np.repeat(starts - out_off[:-1], lengths) + np.arange(int(out_off[-1]))
59
+ return spr.Ragged.from_offsets(parts.data[idx], parts.shape, out_off)
60
+
61
+
62
+ def timeit(fn, arg, repeats):
63
+ fn(arg) # warm up (JIT, caches)
64
+ times = []
65
+ for _ in range(repeats):
66
+ t0 = time.perf_counter()
67
+ fn(arg)
68
+ times.append(time.perf_counter() - t0)
69
+ return float(np.median(times))
70
+
71
+
72
+ def main():
73
+ p = argparse.ArgumentParser()
74
+ p.add_argument("--out", default="bench_to_packed")
75
+ p.add_argument("--repeats", type=int, default=5)
76
+ p.add_argument(
77
+ "--n-rows", type=int, nargs="+", default=[1000, 10000, 100000, 1000000]
78
+ )
79
+ p.add_argument("--mean-len", type=int, nargs="+", default=[16, 256, 4096])
80
+ p.add_argument("--distributions", nargs="+", default=["uniform", "longtail"])
81
+ p.add_argument("--sources", nargs="+", default=["ram", "memmap"])
82
+ p.add_argument("--dtypes", nargs="+", default=["uint8", "float64"])
83
+ p.add_argument("--threads", type=int, nargs="+", default=[int(os.cpu_count() or 1)])
84
+ args = p.parse_args()
85
+
86
+ import awkward as ak
87
+ import pandas as pd
88
+ import seqpro.rag as spr # noqa: F401 (ensures kernel import)
89
+ import tempfile
90
+
91
+ rng = np.random.default_rng(0)
92
+ rows = []
93
+ with tempfile.TemporaryDirectory() as tmp:
94
+ for nthreads in args.threads:
95
+ import numba
96
+
97
+ numba.set_num_threads(nthreads)
98
+ impls = {
99
+ "seqpro": lambda r: r.to_packed(),
100
+ "ak": lambda r: spr.Ragged(ak.to_packed(r)),
101
+ "numpy": numpy_gather,
102
+ }
103
+ for n_rows in args.n_rows:
104
+ for mean_len in args.mean_len:
105
+ for dist in args.distributions:
106
+ for src in args.sources:
107
+ for dt in args.dtypes:
108
+ rag, nbytes = build(
109
+ rng, n_rows, mean_len, dist, dt, src, tmp
110
+ )
111
+ for name, fn in impls.items():
112
+ t = timeit(fn, rag, args.repeats)
113
+ rows.append(
114
+ dict(
115
+ impl=name,
116
+ n_rows=n_rows,
117
+ mean_len=mean_len,
118
+ distribution=dist,
119
+ source=src,
120
+ dtype=dt,
121
+ threads=nthreads,
122
+ seconds=t,
123
+ gbps=nbytes / t / 1e9,
124
+ )
125
+ )
126
+
127
+ df = pd.DataFrame(rows)
128
+ df.to_csv(f"{args.out}.csv", index=False)
129
+ _plots_and_summary(df, args.out)
130
+
131
+
132
+ def _plots_and_summary(df, out):
133
+ import matplotlib
134
+
135
+ matplotlib.use("Agg")
136
+ import matplotlib.pyplot as plt
137
+ import seaborn as sns
138
+
139
+ # throughput vs n_rows, faceted by mean_len x dtype, hue impl
140
+ g = sns.relplot(
141
+ data=df,
142
+ x="n_rows",
143
+ y="gbps",
144
+ hue="impl",
145
+ style="source",
146
+ col="mean_len",
147
+ row="dtype",
148
+ kind="line",
149
+ marker="o",
150
+ facet_kws=dict(sharey=False),
151
+ )
152
+ g.set(xscale="log")
153
+ g.savefig(f"{out}_throughput.png", dpi=120)
154
+
155
+ # thread scaling for seqpro
156
+ if df["threads"].nunique() > 1:
157
+ sub = df[df.impl == "seqpro"]
158
+ gt = sns.relplot(
159
+ data=sub, x="threads", y="gbps", hue="mean_len", kind="line", marker="o"
160
+ )
161
+ gt.savefig(f"{out}_threads.png", dpi=120)
162
+ plt.close("all")
163
+
164
+ # effect-size ranking on speedup (seqpro / ak) — goal 2
165
+ wide = df.pivot_table(
166
+ index=["n_rows", "mean_len", "distribution", "source", "dtype", "threads"],
167
+ columns="impl",
168
+ values="gbps",
169
+ ).reset_index()
170
+ wide["speedup"] = wide["seqpro"] / wide["ak"]
171
+ print("\n=== Speedup (seqpro / ak) summary ===")
172
+ print(wide["speedup"].describe())
173
+ print("\n=== Mean speedup by axis (effect size) ===")
174
+ axes = ["n_rows", "mean_len", "distribution", "source", "dtype", "threads"]
175
+ spreads = {}
176
+ for ax in axes:
177
+ means = wide.groupby(ax)["speedup"].mean()
178
+ spreads[ax] = means.max() - means.min()
179
+ print(f"\n{ax}:")
180
+ print(means)
181
+ print("\n=== Axes ranked by speedup spread (most influential first) ===")
182
+ for ax, s in sorted(spreads.items(), key=lambda kv: -kv[1]):
183
+ print(f" {ax:14s} spread={s:.2f}")
184
+
185
+
186
+ if __name__ == "__main__":
187
+ main()