seqpro 0.17.0__tar.gz → 0.18.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 (145) hide show
  1. {seqpro-0.17.0 → seqpro-0.18.0}/.github/workflows/bench.yaml +1 -1
  2. {seqpro-0.17.0 → seqpro-0.18.0}/CHANGELOG.md +12 -0
  3. {seqpro-0.17.0 → seqpro-0.18.0}/PKG-INFO +1 -1
  4. {seqpro-0.17.0 → seqpro-0.18.0}/pixi.lock +14 -0
  5. {seqpro-0.17.0 → seqpro-0.18.0}/pixi.toml +4 -0
  6. {seqpro-0.17.0 → seqpro-0.18.0}/pyproject.toml +1 -1
  7. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/rag/_core.py +54 -1
  8. seqpro-0.18.0/tests/test_ragged_record_indexing.py +47 -0
  9. seqpro-0.18.0/tests/test_ragged_subclass.py +60 -0
  10. {seqpro-0.17.0 → seqpro-0.18.0}/.claude/skills/zensical/SKILL.md +0 -0
  11. {seqpro-0.17.0 → seqpro-0.18.0}/.gitattributes +0 -0
  12. {seqpro-0.17.0 → seqpro-0.18.0}/.github/workflows/bump.yaml +0 -0
  13. {seqpro-0.17.0 → seqpro-0.18.0}/.github/workflows/docs.yml +0 -0
  14. {seqpro-0.17.0 → seqpro-0.18.0}/.github/workflows/lint.yaml +0 -0
  15. {seqpro-0.17.0 → seqpro-0.18.0}/.github/workflows/merge.yaml +0 -0
  16. {seqpro-0.17.0 → seqpro-0.18.0}/.github/workflows/publish.yaml +0 -0
  17. {seqpro-0.17.0 → seqpro-0.18.0}/.github/workflows/release-pipeline.yaml +0 -0
  18. {seqpro-0.17.0 → seqpro-0.18.0}/.github/workflows/release.yaml +0 -0
  19. {seqpro-0.17.0 → seqpro-0.18.0}/.github/workflows/test.yaml +0 -0
  20. {seqpro-0.17.0 → seqpro-0.18.0}/.gitignore +0 -0
  21. {seqpro-0.17.0 → seqpro-0.18.0}/.pre-commit-config.yaml +0 -0
  22. {seqpro-0.17.0 → seqpro-0.18.0}/CLAUDE.md +0 -0
  23. {seqpro-0.17.0 → seqpro-0.18.0}/Cargo.lock +0 -0
  24. {seqpro-0.17.0 → seqpro-0.18.0}/Cargo.toml +0 -0
  25. {seqpro-0.17.0 → seqpro-0.18.0}/LICENSE +0 -0
  26. {seqpro-0.17.0 → seqpro-0.18.0}/README.md +0 -0
  27. {seqpro-0.17.0 → seqpro-0.18.0}/benches/bench_tokenize_translate.py +0 -0
  28. {seqpro-0.17.0 → seqpro-0.18.0}/benches/kshuffle.rs +0 -0
  29. {seqpro-0.17.0 → seqpro-0.18.0}/benchmarks/bench_ragged_backends.py +0 -0
  30. {seqpro-0.17.0 → seqpro-0.18.0}/benchmarks/bench_to_packed.py +0 -0
  31. {seqpro-0.17.0 → seqpro-0.18.0}/docs/api/alphabets.md +0 -0
  32. {seqpro-0.17.0 → seqpro-0.18.0}/docs/api/bed.md +0 -0
  33. {seqpro-0.17.0 → seqpro-0.18.0}/docs/api/gtf.md +0 -0
  34. {seqpro-0.17.0 → seqpro-0.18.0}/docs/api/index.md +0 -0
  35. {seqpro-0.17.0 → seqpro-0.18.0}/docs/api/ragged.md +0 -0
  36. {seqpro-0.17.0 → seqpro-0.18.0}/docs/api/types.md +0 -0
  37. {seqpro-0.17.0 → seqpro-0.18.0}/docs/index.md +0 -0
  38. {seqpro-0.17.0 → seqpro-0.18.0}/docs/ragged.md +0 -0
  39. {seqpro-0.17.0 → seqpro-0.18.0}/docs/roadmap/rust-ragged.md +0 -0
  40. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-05-04-ragged-record-array.md +0 -0
  41. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-05-05-documentation-site.md +0 -0
  42. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-05-05-narwhals-coord-schema.md +0 -0
  43. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-05-05-ragged-zip-and-record-introspection.md +0 -0
  44. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-05-20-kshuffle-optimization.md +0 -0
  45. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-05-20-kshuffle-pooled-buffers-and-k2-fast-path.md +0 -0
  46. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-05-20-kshuffle-wilson-single-pass.md +0 -0
  47. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-05-20-release-pipeline.md +0 -0
  48. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-05-28-translate-lut-validation.md +0 -0
  49. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-05-31-flat-buffer-to-padded.md +0 -0
  50. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-05-31-rag-to-packed.md +0 -0
  51. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-06-05-translate-unknown-codon-policy.md +0 -0
  52. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-06-12-tokenize-lut-codspeed.md +0 -0
  53. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-06-18-rust-tokenize-translate.md +0 -0
  54. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-06-19-rust-ragged-core.md +0 -0
  55. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-06-20-rust-ragged-records.md +0 -0
  56. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-06-20-rust-ragged-spec-c.md +0 -0
  57. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-06-21-ragged-throughput-gate.md +0 -0
  58. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/plans/2026-06-21-rust-ragged-consumer-audit.md +0 -0
  59. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-05-04-ragged-record-array-design.md +0 -0
  60. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-05-05-documentation-site-design.md +0 -0
  61. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-05-05-narwhals-coord-schema-design.md +0 -0
  62. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-05-05-ragged-zip-and-record-introspection-design.md +0 -0
  63. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-05-20-kshuffle-optimization-design.md +0 -0
  64. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-05-20-kshuffle-pooled-buffers-and-k2-fast-path-design.md +0 -0
  65. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-05-20-kshuffle-wilson-single-pass-design.md +0 -0
  66. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-05-20-release-pipeline-design.md +0 -0
  67. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-05-28-translate-lut-and-validation-design.md +0 -0
  68. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-05-31-flat-buffer-to-padded-design.md +0 -0
  69. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-05-31-rag-to-packed-design.md +0 -0
  70. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-06-05-translate-unknown-codon-policy-design.md +0 -0
  71. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-06-12-tokenize-lut-codspeed-design.md +0 -0
  72. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-06-18-rust-tokenize-translate-design.md +0 -0
  73. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-06-19-rust-ragged-core-design.md +0 -0
  74. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-06-20-rust-ragged-nested-design.md +0 -0
  75. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-06-20-rust-ragged-records-design.md +0 -0
  76. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-06-21-ragged-throughput-gate-design.md +0 -0
  77. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-06-21-rust-ragged-audit-ledger.md +0 -0
  78. {seqpro-0.17.0 → seqpro-0.18.0}/docs/superpowers/specs/2026-06-21-rust-ragged-consumer-audit-design.md +0 -0
  79. {seqpro-0.17.0 → seqpro-0.18.0}/meta.yaml +0 -0
  80. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/__init__.py +0 -0
  81. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/_analyzers.py +0 -0
  82. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/_cleaners.py +0 -0
  83. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/_coords.py +0 -0
  84. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/_encoders.py +0 -0
  85. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/_modifiers.py +0 -0
  86. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/_numba.py +0 -0
  87. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/_types.py +0 -0
  88. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/_utils.py +0 -0
  89. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/alphabets/__init__.py +0 -0
  90. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/alphabets/_alphabets.py +0 -0
  91. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/bed.py +0 -0
  92. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/experimental/_experimental.py +0 -0
  93. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/experimental/_visualizers.py +0 -0
  94. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/gtf.py +0 -0
  95. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/py.typed +0 -0
  96. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/rag/__init__.py +0 -0
  97. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/rag/_ak_interop.py +0 -0
  98. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/rag/_ingest.py +0 -0
  99. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/rag/_layout.py +0 -0
  100. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/rag/_ops.py +0 -0
  101. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/rag/_types.py +0 -0
  102. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/rag/_utils.py +0 -0
  103. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/transforms/__init__.py +0 -0
  104. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/transforms/augmentation.py +0 -0
  105. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/transforms/tmm.py +0 -0
  106. {seqpro-0.17.0 → seqpro-0.18.0}/python/seqpro/xr/__init__.py +0 -0
  107. {seqpro-0.17.0 → seqpro-0.18.0}/scratch_bench_rc.py +0 -0
  108. {seqpro-0.17.0 → seqpro-0.18.0}/scratch_bench_to_padded.py +0 -0
  109. {seqpro-0.17.0 → seqpro-0.18.0}/skills/seqpro/SKILL.md +0 -0
  110. {seqpro-0.17.0 → seqpro-0.18.0}/src/kmer_encode.rs +0 -0
  111. {seqpro-0.17.0 → seqpro-0.18.0}/src/kshuffle.rs +0 -0
  112. {seqpro-0.17.0 → seqpro-0.18.0}/src/kshuffle_ref.rs +0 -0
  113. {seqpro-0.17.0 → seqpro-0.18.0}/src/lib.rs +0 -0
  114. {seqpro-0.17.0 → seqpro-0.18.0}/src/ragged.rs +0 -0
  115. {seqpro-0.17.0 → seqpro-0.18.0}/src/translate.rs +0 -0
  116. {seqpro-0.17.0 → seqpro-0.18.0}/tests/_shape_fixtures.py +0 -0
  117. {seqpro-0.17.0 → seqpro-0.18.0}/tests/bed/test_pyranges.py +0 -0
  118. {seqpro-0.17.0 → seqpro-0.18.0}/tests/bed/test_read.py +0 -0
  119. {seqpro-0.17.0 → seqpro-0.18.0}/tests/bed/test_sort.py +0 -0
  120. {seqpro-0.17.0 → seqpro-0.18.0}/tests/bed/test_with_length.py +0 -0
  121. {seqpro-0.17.0 → seqpro-0.18.0}/tests/bench_translate_lut.py +0 -0
  122. {seqpro-0.17.0 → seqpro-0.18.0}/tests/conftest.py +0 -0
  123. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_analyzers.py +0 -0
  124. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_bench_tokenize.py +0 -0
  125. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_concatenate.py +0 -0
  126. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_coords.py +0 -0
  127. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_core_ragged_surface.py +0 -0
  128. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_encoders.py +0 -0
  129. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_ingest.py +0 -0
  130. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_modifiers.py +0 -0
  131. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_ohe.py +0 -0
  132. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_rag_to_packed.py +0 -0
  133. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_ragged.py +0 -0
  134. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_ragged_core.py +0 -0
  135. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_ragged_core_records.py +0 -0
  136. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_ragged_nested_consumers.py +0 -0
  137. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_ragged_nested_diff.py +0 -0
  138. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_ragged_rc.py +0 -0
  139. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_ragged_to_padded.py +0 -0
  140. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_shape_matrix.py +0 -0
  141. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_tokenize.py +0 -0
  142. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_transforms.py +0 -0
  143. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_translate.py +0 -0
  144. {seqpro-0.17.0 → seqpro-0.18.0}/tests/test_translate_rust.py +0 -0
  145. {seqpro-0.17.0 → seqpro-0.18.0}/zensical.toml +0 -0
@@ -30,7 +30,7 @@ jobs:
30
30
  environments: bench
31
31
  locked: true
32
32
  - name: Build extension
33
- run: pixi run -e bench maturin develop --uv
33
+ run: pixi run -e bench maturin develop
34
34
  - name: Run benchmarks
35
35
  uses: CodSpeedHQ/action@v4
36
36
  with:
@@ -1,3 +1,15 @@
1
+ ## 0.18.0 (2026-06-23)
2
+
3
+ ### Feat
4
+
5
+ - **rag**: reshape/squeeze/to_packed preserve Ragged subclass
6
+ - **rag**: __getitem__ preserves subclass on positional indexing
7
+ - **rag**: add _with_layout subclass-preserving constructor
8
+
9
+ ### Fix
10
+
11
+ - **rag**: non-tuple record indexing matches numpy (A[x] == A[(x,)])
12
+
1
13
  ## 0.17.0 (2026-06-23)
2
14
 
3
15
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seqpro
3
- Version: 0.17.0
3
+ Version: 0.18.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -132,6 +132,7 @@ environments:
132
132
  - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda
133
133
  - conda: https://conda.anaconda.org/conda-forge/noarch/patsy-1.0.2-pyhcf101f3_0.conda
134
134
  - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda
135
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh145f28c_0.conda
135
136
  - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda
136
137
  - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda
137
138
  - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
@@ -262,6 +263,7 @@ environments:
262
263
  - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.7-pyhcf101f3_0.conda
263
264
  - conda: https://conda.anaconda.org/conda-forge/noarch/patsy-1.0.2-pyhcf101f3_0.conda
264
265
  - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda
266
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh145f28c_0.conda
265
267
  - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.6-pyhcf101f3_0.conda
266
268
  - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda
267
269
  - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda
@@ -6564,6 +6566,18 @@ packages:
6564
6566
  - pkg:pypi/pickleshare?source=hash-mapping
6565
6567
  size: 11748
6566
6568
  timestamp: 1733327448200
6569
+ - conda: https://conda.anaconda.org/conda-forge/noarch/pip-26.1.2-pyh145f28c_0.conda
6570
+ sha256: 9e673d3c6003f416df11670a14b026a04e3a45ebec55357987e15b860f138f2a
6571
+ md5: 733cc07ed34162ac50b936464b163366
6572
+ depends:
6573
+ - python >=3.13.0a0
6574
+ license: MIT
6575
+ license_family: MIT
6576
+ purls:
6577
+ - pkg:pypi/pip?source=compressed-mapping
6578
+ run_exports: {}
6579
+ size: 1202377
6580
+ timestamp: 1780262809860
6567
6581
  - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda
6568
6582
  sha256: 0f48999a28019c329cd3f6fd2f01f09fc32cc832f7d6bbe38087ddac858feaa3
6569
6583
  md5: 424844562f5d337077b445ec6b1398a7
@@ -95,6 +95,10 @@ bench = "pytest tests/test_bench_tokenize.py --codspeed"
95
95
  rag-gate = "python benchmarks/bench_ragged_backends.py"
96
96
 
97
97
  [feature.bench.dependencies]
98
+ # pip is required so `maturin develop` can install the editable build into
99
+ # the bench env. (`--uv` instead trips a uv installer bug on conda-managed
100
+ # transitive deps: "Installed distribution could not be found: ncls".)
101
+ pip = "*"
98
102
  marimo = ">=0.23.5,<0.24"
99
103
  seaborn = ">=0.13.2,<0.14"
100
104
  statsmodels = ">=0.14.6,<0.15"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "seqpro"
3
- version = "0.17.0"
3
+ version = "0.18.0"
4
4
  authors = [
5
5
  { name = "David Laub", email = "dlaub@ucsd.edu" },
6
6
  { name = "Adam Klie", email = "aklie@ucsd.edu" },
@@ -75,6 +75,15 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
75
75
  validate_layout(data)
76
76
  self._layout = data
77
77
 
78
+ def _with_layout(self, layout: Any) -> "Ragged[Any]":
79
+ """Reconstruct a same-kind container around ``layout``, preserving the
80
+ concrete subclass. Bypasses ``__init__`` (subclasses carry no state beyond
81
+ ``_layout``; see the subclassing contract). Used by structural transforms
82
+ so a ``Ragged`` subclass survives slicing/reshape/squeeze/to_packed."""
83
+ obj = object.__new__(type(self))
84
+ obj._layout = layout
85
+ return obj
86
+
78
87
  @property
79
88
  def _is_record(self) -> bool:
80
89
  return isinstance(self._layout, RecordLayout)
@@ -425,6 +434,19 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
425
434
  def __getitem__(
426
435
  self, where: Any
427
436
  ) -> "NDArray[Any] | bytes | dict[str, Any] | Ragged[Any]":
437
+ result = self._getitem(where)
438
+ # Preserve the concrete subclass for positional (structural) results.
439
+ # A string key is field extraction -> keep the bare field as base Ragged.
440
+ # Non-Ragged results (dict / bytes / ndarray / scalar) pass through.
441
+ if (
442
+ type(self) is not Ragged
443
+ and not isinstance(where, str)
444
+ and isinstance(result, Ragged)
445
+ ):
446
+ return self._with_layout(result._layout)
447
+ return result
448
+
449
+ def _getitem(self, where: Any) -> Any:
428
450
  # np.newaxis / None: insert a size-1 leading axis.
429
451
  # e.g. shape (batch, None)[None] -> (1, batch, None)
430
452
  if where is None:
@@ -813,7 +835,18 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
813
835
  except KeyError:
814
836
  raise KeyError(where)
815
837
  return Ragged(field) # field.offsets[0] is the shared object (zero-copy)
816
- return self._getitem_record_rows(where) # Task 8
838
+ # numpy contract: a non-tuple key is treated as a 1-tuple (A[x] == A[(x,)]).
839
+ # For a single-ragged-axis record with >1 leading fixed axis, route through
840
+ # the multidim peel path directly (under the SAME guard it requires, so we
841
+ # never re-dispatch through __getitem__ and risk recursion). Records that are
842
+ # not single-None fall through to the flat record-rows path unchanged.
843
+ if (
844
+ not isinstance(where, tuple)
845
+ and self.rag_dim > 1
846
+ and rec.shape.count(None) == 1
847
+ ):
848
+ return self._getitem_tuple_multidim((where,))
849
+ return self._getitem_record_rows(where)
817
850
 
818
851
  def _gather_indices(
819
852
  self, where: Any, starts: "NDArray[Any]", stops: "NDArray[Any]"
@@ -1161,6 +1194,14 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
1161
1194
 
1162
1195
  def squeeze(
1163
1196
  self, axis: int | tuple[int, ...] | None = None
1197
+ ) -> "Ragged[Any] | NDArray[Any]":
1198
+ result = self._squeeze_impl(axis)
1199
+ if type(self) is not Ragged and isinstance(result, Ragged):
1200
+ return self._with_layout(result._layout)
1201
+ return result
1202
+
1203
+ def _squeeze_impl(
1204
+ self, axis: int | tuple[int, ...] | None = None
1164
1205
  ) -> "Ragged[Any] | NDArray[Any]":
1165
1206
  if isinstance(self._layout, RecordLayout):
1166
1207
  return Ragged.from_fields(
@@ -1210,6 +1251,12 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
1210
1251
  )
1211
1252
 
1212
1253
  def reshape(self, *shape: int | None) -> "Ragged[Any]":
1254
+ result = self._reshape_impl(*shape)
1255
+ if type(self) is not Ragged and isinstance(result, Ragged):
1256
+ return self._with_layout(result._layout)
1257
+ return result
1258
+
1259
+ def _reshape_impl(self, *shape: int | None) -> "Ragged[Any]":
1213
1260
  if isinstance(self._layout, RecordLayout):
1214
1261
  return Ragged.from_fields(
1215
1262
  {f: Ragged(fl).reshape(*shape) for f, fl in self._layout.fields.items()}
@@ -1270,6 +1317,12 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
1270
1317
  return raw.reshape(reshape_arg)
1271
1318
 
1272
1319
  def to_packed(self, *, copy: bool = True) -> "Ragged[Any]":
1320
+ result = self._to_packed_impl(copy=copy)
1321
+ if type(self) is not Ragged and isinstance(result, Ragged):
1322
+ return self._with_layout(result._layout)
1323
+ return result
1324
+
1325
+ def _to_packed_impl(self, *, copy: bool = True) -> "Ragged[Any]":
1273
1326
  from ._ops import _pack_parts
1274
1327
 
1275
1328
  if isinstance(self._layout, RecordLayout):
@@ -0,0 +1,47 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from seqpro.rag import Ragged
5
+
6
+
7
+ def _multidim_record():
8
+ """A (3, 2, None) numeric record: 6 ragged segments over a (batch=3, ploidy=2)
9
+ leading grid."""
10
+ lengths = np.array([[1, 2], [3, 1], [2, 2]], np.uint32) # (3, 2)
11
+ data = np.arange(int(lengths.sum()), dtype=np.int32)
12
+ start = Ragged.from_lengths(data, lengths.reshape(-1)).reshape(3, 2, None)
13
+ return Ragged.from_fields({"start": start})
14
+
15
+
16
+ @pytest.mark.parametrize(
17
+ "key",
18
+ [
19
+ 0,
20
+ slice(1, 3),
21
+ slice(None),
22
+ np.array([True, False, True]),
23
+ np.array([0, 2]),
24
+ ],
25
+ ids=["int", "slice", "full_slice", "mask", "int_array"],
26
+ )
27
+ def test_nontuple_equals_one_tuple_multidim_record(key):
28
+ """numpy contract: rec[k] must equal rec[(k,)] for a multi-leading-axis record."""
29
+ rec = _multidim_record()
30
+ got = rec[key]
31
+ want = rec[(key,)]
32
+ assert type(got) is type(want), (type(got), type(want))
33
+ if isinstance(want, Ragged):
34
+ assert got.shape == want.shape, (got.shape, want.shape)
35
+ np.testing.assert_array_equal(
36
+ np.asarray(got["start"].data), np.asarray(want["start"].data)
37
+ )
38
+ np.testing.assert_array_equal(
39
+ np.asarray(got["start"].offsets[0]), np.asarray(want["start"].offsets[0])
40
+ )
41
+
42
+
43
+ def test_slice_preserves_ploidy_axis():
44
+ """Regression for the specific bug: rec[1:3] kept the ploidy axis."""
45
+ rec = _multidim_record()
46
+ assert rec[1:3].shape == (2, 2, None)
47
+ assert rec[0].shape == (2, None)
@@ -0,0 +1,60 @@
1
+ import numpy as np
2
+
3
+ from seqpro.rag import Ragged
4
+
5
+
6
+ class _Sub(Ragged):
7
+ __slots__ = ()
8
+
9
+
10
+ def _ragged():
11
+ return Ragged.from_lengths(
12
+ np.arange(6, dtype=np.int32), np.array([2, 1, 3], np.uint32)
13
+ )
14
+
15
+
16
+ def test_with_layout_preserves_subclass():
17
+ sub = _Sub(_ragged()._layout)
18
+ out = sub._with_layout(_ragged()._layout)
19
+ assert type(out) is _Sub
20
+
21
+
22
+ def test_with_layout_base_returns_base():
23
+ base = _ragged()
24
+ out = base._with_layout(_ragged()._layout)
25
+ assert type(out) is Ragged
26
+
27
+
28
+ def _record():
29
+ a = Ragged.from_lengths(
30
+ np.arange(6, dtype=np.int32), np.array([2, 1, 3], np.uint32)
31
+ )
32
+ b = Ragged.from_lengths(
33
+ np.arange(6, dtype=np.int32) * 10, np.array([2, 1, 3], np.uint32)
34
+ )
35
+ return Ragged.from_fields({"a": a, "b": b})
36
+
37
+
38
+ def test_getitem_positional_preserves_subclass():
39
+ sub = _Sub(_record()._layout)
40
+ assert type(sub[0:2]) is _Sub # positional row slice -> subclass
41
+ assert type(sub[np.array([0, 2])]) is _Sub
42
+
43
+
44
+ def test_getitem_field_extraction_stays_base():
45
+ sub = _Sub(_record()._layout)
46
+ assert type(sub["a"]) is Ragged # string key -> bare field, base Ragged
47
+
48
+
49
+ def test_reshape_squeeze_to_packed_preserve_subclass():
50
+ sub = _Sub(_record()._layout) # shape (3, None) record
51
+ assert type(sub.reshape(1, 3, None)) is _Sub
52
+ assert type(sub.reshape(1, 3, None).squeeze(0)) is _Sub
53
+ assert type(sub.to_packed()) is _Sub
54
+
55
+
56
+ def test_reshape_squeeze_to_packed_base_unchanged():
57
+ base = _record()
58
+ assert type(base.reshape(1, 3, None)) is Ragged
59
+ assert type(base.reshape(1, 3, None).squeeze(0)) is Ragged
60
+ assert type(base.to_packed()) is Ragged
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes