seqpro 0.22.0__tar.gz → 0.23.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.
- {seqpro-0.22.0 → seqpro-0.23.0}/CHANGELOG.md +17 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/PKG-INFO +1 -1
- {seqpro-0.22.0 → seqpro-0.23.0}/pyproject.toml +1 -1
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/rag/_core.py +308 -149
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/rag/_ingest.py +18 -18
- {seqpro-0.22.0 → seqpro-0.23.0}/skills/seqpro/SKILL.md +7 -4
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_ragged_core.py +4 -4
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_ragged_core_records.py +2 -1
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_ragged_nested_diff.py +1 -1
- seqpro-0.23.0/tests/test_ragged_nested_fixed_axis.py +154 -0
- seqpro-0.23.0/tests/test_ragged_tuple_index.py +203 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/.claude/skills/zensical/SKILL.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/.gitattributes +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/.github/workflows/bench.yaml +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/.github/workflows/bump.yaml +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/.github/workflows/docs.yml +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/.github/workflows/lint.yaml +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/.github/workflows/merge.yaml +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/.github/workflows/publish.yaml +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/.github/workflows/release-pipeline.yaml +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/.github/workflows/release.yaml +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/.github/workflows/test.yaml +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/.gitignore +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/.pre-commit-config.yaml +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/CLAUDE.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/Cargo.lock +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/Cargo.toml +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/LICENSE +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/README.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/benches/bench_tokenize_translate.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/benches/kshuffle.rs +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/benchmarks/bench_ragged_backends.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/benchmarks/bench_ragged_gather.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/benchmarks/bench_to_packed.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/crates/seqpro-core/Cargo.toml +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/crates/seqpro-core/src/lib.rs +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/crates/seqpro-core/src/ragged.rs +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/api/alphabets.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/api/bed.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/api/gtf.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/api/index.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/api/ragged.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/api/types.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/index.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/ragged.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/roadmap/rust-ragged.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-05-04-ragged-record-array.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-05-05-documentation-site.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-05-05-narwhals-coord-schema.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-05-05-ragged-zip-and-record-introspection.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-05-20-kshuffle-optimization.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-05-20-kshuffle-pooled-buffers-and-k2-fast-path.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-05-20-kshuffle-wilson-single-pass.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-05-20-release-pipeline.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-05-28-translate-lut-validation.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-05-31-flat-buffer-to-padded.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-05-31-rag-to-packed.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-06-05-translate-unknown-codon-policy.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-06-12-tokenize-lut-codspeed.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-06-18-rust-tokenize-translate.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-06-19-rust-ragged-core.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-06-20-rust-ragged-records.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-06-20-rust-ragged-spec-c.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-06-21-ragged-throughput-gate.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-06-21-rust-ragged-consumer-audit.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-06-23-ragged-subclass-getitem.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-06-24-ragged-getitem-fastpaths.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-06-25-ragged-string-hashing.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-07-21-ragged-gather-ok.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/plans/2026-07-27-ragged-string-getitem.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-05-04-ragged-record-array-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-05-05-documentation-site-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-05-05-narwhals-coord-schema-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-05-05-ragged-zip-and-record-introspection-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-05-20-kshuffle-optimization-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-05-20-kshuffle-pooled-buffers-and-k2-fast-path-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-05-20-kshuffle-wilson-single-pass-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-05-20-release-pipeline-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-05-28-translate-lut-and-validation-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-05-31-flat-buffer-to-padded-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-05-31-rag-to-packed-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-06-05-translate-unknown-codon-policy-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-06-12-tokenize-lut-codspeed-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-06-18-rust-tokenize-translate-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-06-19-rust-ragged-core-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-06-20-rust-ragged-nested-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-06-20-rust-ragged-records-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-06-21-ragged-throughput-gate-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-06-21-rust-ragged-audit-ledger.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-06-21-rust-ragged-consumer-audit-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-06-23-ragged-subclass-getitem-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-06-24-ragged-getitem-fastpaths-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-06-25-ragged-string-hashing-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-07-21-ragged-gather-ok-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/docs/superpowers/specs/2026-07-27-ragged-string-getitem-design.md +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/meta.yaml +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/pixi.lock +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/pixi.toml +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/__init__.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/_analyzers.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/_cleaners.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/_coords.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/_encoders.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/_modifiers.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/_numba.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/_types.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/_utils.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/alphabets/__init__.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/alphabets/_alphabets.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/bed.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/experimental/_experimental.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/experimental/_visualizers.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/gtf.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/py.typed +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/rag/__init__.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/rag/_ak_interop.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/rag/_layout.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/rag/_ops.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/rag/_types.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/rag/_utils.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/transforms/__init__.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/transforms/augmentation.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/transforms/tmm.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/python/seqpro/xr/__init__.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/scratch_bench_rc.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/scratch_bench_to_padded.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/src/hashing.rs +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/src/kmer_encode.rs +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/src/kshuffle.rs +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/src/kshuffle_ref.rs +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/src/lib.rs +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/src/ragged.rs +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/src/translate.rs +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/_shape_fixtures.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/bed/test_pyranges.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/bed/test_read.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/bed/test_sort.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/bed/test_with_length.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/bench_ragged_getitem.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/bench_translate_lut.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/conftest.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_analyzers.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_bench_tokenize.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_concatenate.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_coords.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_core_ragged_surface.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_encoders.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_ingest.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_modifiers.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_ohe.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_rag_to_packed.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_ragged.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_ragged_hash.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_ragged_nested_consumers.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_ragged_rc.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_ragged_record_indexing.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_ragged_slice_fastpath.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_ragged_subclass.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_ragged_to_padded.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_shape_matrix.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_tokenize.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_transforms.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_translate.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/tests/test_translate_rust.py +0 -0
- {seqpro-0.22.0 → seqpro-0.23.0}/zensical.toml +0 -0
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
## 0.23.0 (2026-09-26)
|
|
2
|
+
|
|
3
|
+
### BREAKING CHANGE
|
|
4
|
+
|
|
5
|
+
- rag[:, k] and similar tuple keys now follow NumPy and
|
|
6
|
+
awkward semantics; code relying on the old axis-0 behavior must index
|
|
7
|
+
rows directly (rag[k]).
|
|
8
|
+
- Ragged record to_ak() for arrays with one leading axis
|
|
9
|
+
now returns records at the leaf (e.g. 2 * var * {a, b}) instead of one
|
|
10
|
+
record of lists per row.
|
|
11
|
+
|
|
12
|
+
### Fix
|
|
13
|
+
|
|
14
|
+
- **rag**: apply each tuple key to the next output axis
|
|
15
|
+
- **rag**: emit leaf-level records from to_ak() for every rag_dim
|
|
16
|
+
- **rag**: honor fixed axes between the outer axis and nested ragged axes
|
|
17
|
+
|
|
1
18
|
## 0.22.0 (2026-07-27)
|
|
2
19
|
|
|
3
20
|
### BREAKING CHANGE
|
|
@@ -82,8 +82,71 @@ def _peel_string_row(rl: "RaggedLayout[Any]", lo: int, hi: int) -> "RaggedLayout
|
|
|
82
82
|
)
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def _is_int(k: Any) -> bool:
|
|
86
|
+
return isinstance(k, (int, np.integer))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _last_ragged_axis(shape: tuple[int | None, ...]) -> int:
|
|
90
|
+
return len(shape) - 1 - shape[::-1].index(None)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _elem_index(starts: NDArray[Any], stops: NDArray[Any], k: int) -> NDArray[np.int64]:
|
|
94
|
+
"""Flat index of element ``k`` (negative counts from the end) of every row."""
|
|
95
|
+
lens = stops - starts
|
|
96
|
+
if k >= 0:
|
|
97
|
+
bad, idx = lens <= k, starts + k
|
|
98
|
+
else:
|
|
99
|
+
bad, idx = lens < -k, stops + k
|
|
100
|
+
if np.any(bad):
|
|
101
|
+
raise IndexError(f"index {k} is out of range for some ragged rows")
|
|
102
|
+
return idx.astype(np.int64)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _slice_bounds(
|
|
106
|
+
starts: NDArray[Any], stops: NDArray[Any], sl: slice
|
|
107
|
+
) -> tuple[NDArray[Any], NDArray[Any]]:
|
|
108
|
+
"""Apply a step-1 slice to every row; bounds follow Python slice semantics."""
|
|
109
|
+
if sl.step not in (None, 1):
|
|
110
|
+
raise NotImplementedError("stepped slices on a ragged axis are unsupported")
|
|
111
|
+
lens = stops - starts
|
|
112
|
+
|
|
113
|
+
def resolve(v: Any, default: NDArray[Any]) -> NDArray[Any]:
|
|
114
|
+
if v is None:
|
|
115
|
+
return default
|
|
116
|
+
v = int(v)
|
|
117
|
+
return np.clip(lens + v if v < 0 else np.full_like(lens, v), 0, lens)
|
|
118
|
+
|
|
119
|
+
lo = resolve(sl.start, np.zeros_like(lens))
|
|
120
|
+
hi = np.maximum(resolve(sl.stop, lens), lo)
|
|
121
|
+
return (starts + lo).astype(OFFSET_TYPE), (starts + hi).astype(OFFSET_TYPE)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _ranges(starts: NDArray[Any], counts: NDArray[Any]) -> NDArray[np.int64]:
|
|
125
|
+
"""Concatenate ``range(s, s + c)`` for every ``(s, c)`` pair, vectorized."""
|
|
126
|
+
counts = counts.astype(np.int64)
|
|
127
|
+
ends = np.cumsum(counts)
|
|
128
|
+
shift = np.repeat(starts.astype(np.int64) - (ends - counts), counts)
|
|
129
|
+
return np.arange(int(ends[-1]) if len(ends) else 0, dtype=np.int64) + shift
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _index_output_axis(obj: Any, p: int, k: Any) -> Any:
|
|
133
|
+
"""Index output axis ``p`` of any ``Ragged.__getitem__`` result with ``k``."""
|
|
134
|
+
if isinstance(obj, Ragged):
|
|
135
|
+
return obj._index_axis(p, k)
|
|
136
|
+
if isinstance(obj, dict):
|
|
137
|
+
return {name: _index_output_axis(v, p, k) for name, v in obj.items()}
|
|
138
|
+
if isinstance(obj, bytes) and p == 0:
|
|
139
|
+
return obj[k]
|
|
140
|
+
return np.asarray(obj)[(slice(None),) * p + (k,)]
|
|
141
|
+
|
|
142
|
+
|
|
85
143
|
class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
|
|
86
|
-
"""A non-branching ragged array with
|
|
144
|
+
"""A non-branching ragged array with one ragged axis, or two adjacent ones.
|
|
145
|
+
|
|
146
|
+
Shape is ``(*leading_int, None x R, *trailing_int)`` with ``R`` in ``{1, 2}``.
|
|
147
|
+
Fixed leading axes may sit between the outermost axis and the ragged axes,
|
|
148
|
+
e.g. ``(batch, ploidy, ~variants, ~window)``.
|
|
149
|
+
"""
|
|
87
150
|
|
|
88
151
|
__slots__ = ("_layout",)
|
|
89
152
|
|
|
@@ -652,73 +715,46 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
|
|
|
652
715
|
str_offsets=self._rl.str_offsets,
|
|
653
716
|
)
|
|
654
717
|
return Ragged(new_layout)
|
|
655
|
-
if (
|
|
656
|
-
isinstance(where, tuple)
|
|
657
|
-
and len(where) == 2
|
|
658
|
-
and not self._is_record
|
|
659
|
-
and self._rl.n_ragged == 2
|
|
660
|
-
and self._is_full_slice(where[0])
|
|
661
|
-
):
|
|
662
|
-
return self._getitem_inner(where[1])
|
|
663
718
|
if isinstance(where, tuple):
|
|
664
|
-
#
|
|
665
|
-
#
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
# tuple just indexes it then reaches into the ragged/trailing dims; the
|
|
670
|
-
# combining path would mis-handle the trailing fixed dim (e.g. a
|
|
671
|
-
# (d0, None, K) array padded by to_padded), so fall through to the
|
|
672
|
-
# sequential per-key path below.
|
|
673
|
-
if self._layout.shape.count(None) == 1 and self.rag_dim > 1:
|
|
674
|
-
return self._getitem_tuple_multidim(where)
|
|
675
|
-
# Sequential per-key path. np.newaxis (None) keys are handled here:
|
|
676
|
-
# track their positions, strip them from the tuple, apply the remaining
|
|
677
|
-
# keys, then insert size-1 axes at the correct positions in the result.
|
|
678
|
-
has_none = any(k is None for k in where)
|
|
679
|
-
if has_none:
|
|
680
|
-
# Separate None positions from real indexing keys.
|
|
681
|
-
real_keys: list[Any] = []
|
|
682
|
-
none_positions: list[int] = []
|
|
683
|
-
pos = 0 # tracks the output axis number as we accumulate
|
|
684
|
-
for k in where:
|
|
685
|
-
if k is None:
|
|
686
|
-
none_positions.append(pos)
|
|
687
|
-
pos += 1 # new axis consumes one output position
|
|
688
|
-
else:
|
|
689
|
-
real_keys.append(k)
|
|
690
|
-
# An integer key collapses one axis (no output position);
|
|
691
|
-
# a slice/array key keeps one axis.
|
|
692
|
-
if not isinstance(k, (int, np.integer)):
|
|
693
|
-
pos += 1
|
|
694
|
-
result: Any = self
|
|
695
|
-
for k in real_keys:
|
|
696
|
-
result = result[k]
|
|
697
|
-
# Insert size-1 leading axes at recorded positions.
|
|
698
|
-
if isinstance(result, Ragged):
|
|
699
|
-
for p in reversed(none_positions):
|
|
700
|
-
cur_shape = result._layout.shape
|
|
701
|
-
new_shape = cur_shape[:p] + (1,) + cur_shape[p:]
|
|
702
|
-
result = Ragged(
|
|
703
|
-
RaggedLayout(
|
|
704
|
-
data=result._rl.data,
|
|
705
|
-
offsets=list(result._layout.offsets),
|
|
706
|
-
shape=new_shape,
|
|
707
|
-
str_offsets=result._rl.str_offsets,
|
|
708
|
-
)
|
|
709
|
-
)
|
|
710
|
-
else:
|
|
711
|
-
# numpy array or scalar: use np.expand_dims
|
|
712
|
-
for p in reversed(none_positions):
|
|
713
|
-
result = np.expand_dims(result, p)
|
|
714
|
-
return result
|
|
715
|
-
result = self
|
|
719
|
+
# np.newaxis (None) keys: track their output positions, strip them,
|
|
720
|
+
# apply the remaining keys, then insert size-1 axes at those positions.
|
|
721
|
+
real_keys: list[Any] = []
|
|
722
|
+
none_positions: list[int] = []
|
|
723
|
+
pos = 0 # tracks the output axis number as we accumulate
|
|
716
724
|
for k in where:
|
|
717
|
-
|
|
725
|
+
if k is None:
|
|
726
|
+
none_positions.append(pos)
|
|
727
|
+
pos += 1 # new axis consumes one output position
|
|
728
|
+
else:
|
|
729
|
+
real_keys.append(k)
|
|
730
|
+
# An integer key collapses one axis (no output position);
|
|
731
|
+
# a slice/array key keeps one axis.
|
|
732
|
+
if not _is_int(k):
|
|
733
|
+
pos += 1
|
|
734
|
+
result: Any = self._index_keys(tuple(real_keys))
|
|
735
|
+
# Insert size-1 leading axes at recorded positions.
|
|
736
|
+
if isinstance(result, Ragged):
|
|
737
|
+
for p in reversed(none_positions):
|
|
738
|
+
cur_shape = result._layout.shape
|
|
739
|
+
new_shape = cur_shape[:p] + (1,) + cur_shape[p:]
|
|
740
|
+
result = Ragged(
|
|
741
|
+
RaggedLayout(
|
|
742
|
+
data=result._rl.data,
|
|
743
|
+
offsets=list(result._layout.offsets),
|
|
744
|
+
shape=new_shape,
|
|
745
|
+
str_offsets=result._rl.str_offsets,
|
|
746
|
+
)
|
|
747
|
+
)
|
|
748
|
+
else:
|
|
749
|
+
# numpy array or scalar: use np.expand_dims
|
|
750
|
+
for p in reversed(none_positions):
|
|
751
|
+
result = np.expand_dims(result, p)
|
|
718
752
|
return result
|
|
719
753
|
if isinstance(self._layout, RecordLayout):
|
|
720
754
|
return self._getitem_record(where)
|
|
721
755
|
if self._layout.n_ragged == 2:
|
|
756
|
+
if self.rag_dim > 1:
|
|
757
|
+
return self._getitem_leading((where,))[0]
|
|
722
758
|
return self._getitem_r2(where)
|
|
723
759
|
# Multi-dim leading shape: when rag_dim > 1 (e.g. shape (d0, d1, ..., None))
|
|
724
760
|
# index the first axis treating each "row" as a block of n_inner contiguous
|
|
@@ -847,25 +883,32 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
|
|
|
847
883
|
)
|
|
848
884
|
)
|
|
849
885
|
|
|
850
|
-
def
|
|
851
|
-
"""
|
|
886
|
+
def _getitem_leading(self, where: tuple[Any, ...]) -> tuple[Any, int]:
|
|
887
|
+
"""Index the leading fixed axes of a Ragged (record or not) with rag_dim > 1.
|
|
888
|
+
|
|
889
|
+
Returns ``(result, n_kept)``: ``n_kept`` is how many output axes the keys
|
|
890
|
+
left in front of the first unindexed axis, i.e. where the next key goes.
|
|
852
891
|
|
|
853
892
|
Each key in ``where`` targets successive leading integer axes (like NumPy
|
|
854
893
|
multi-dim indexing). All leading-dim keys are resolved simultaneously into
|
|
855
894
|
a single flat segment selection so that e.g. ``rag[:, [0]]`` correctly
|
|
856
895
|
selects from axis 1 (samples), not axis 0 (ranges).
|
|
857
896
|
|
|
858
|
-
|
|
859
|
-
|
|
897
|
+
For R=2 the leading keys select outer (O0) segments; O1 stays global, the
|
|
898
|
+
same lazy gather as ``_getitem_r2``. When every leading axis of an R=2
|
|
899
|
+
array gets an int key, the one selected outer segment is peeled like
|
|
900
|
+
``rag[i]`` on a ``rag_dim == 1`` array, so ``rag[i, j] == rag[i][j]``.
|
|
901
|
+
(R=1 keeps its 1-segment ``(None, ...)`` result.)
|
|
902
|
+
|
|
903
|
+
``where`` must not be longer than ``rag_dim``; ``_index_keys`` applies any
|
|
904
|
+
keys past the leading axes.
|
|
860
905
|
"""
|
|
861
906
|
rag_dim = self.rag_dim
|
|
862
907
|
leading = self._layout.shape[:rag_dim] # e.g. (d0, d1, ..., dk)
|
|
863
908
|
trailing = self._layout.shape[rag_dim:] # (None, *trailing_int)
|
|
864
909
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
leading_keys = where[:n_leading_keys]
|
|
868
|
-
remainder_keys = where[n_leading_keys:]
|
|
910
|
+
n_leading_keys = len(where)
|
|
911
|
+
leading_keys = where
|
|
869
912
|
|
|
870
913
|
# For each leading dim, resolve the key to an index array (or scalar).
|
|
871
914
|
# We track whether each dim produces an int (scalar), a "slice" range, or a
|
|
@@ -973,14 +1016,17 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
|
|
|
973
1016
|
grid_combined = grid_combined + grids[j] * strides[j]
|
|
974
1017
|
flat_seg_idx = np.asarray(grid_combined.reshape(-1), dtype=np.int64)
|
|
975
1018
|
|
|
976
|
-
# Gather
|
|
977
|
-
|
|
1019
|
+
# Gather the outer segments' starts/stops; deeper levels stay global.
|
|
1020
|
+
outer, *inner_offsets = self._layout.offsets
|
|
1021
|
+
starts, stops = _level_bounds(outer)
|
|
978
1022
|
new_starts = np.ascontiguousarray(starts[flat_seg_idx], dtype=OFFSET_TYPE)
|
|
979
1023
|
new_stops = np.ascontiguousarray(stops[flat_seg_idx], dtype=OFFSET_TYPE)
|
|
980
|
-
new_offsets = np.stack([new_starts, new_stops], 0)
|
|
1024
|
+
new_offsets = [np.stack([new_starts, new_stops], 0), *inner_offsets]
|
|
981
1025
|
|
|
982
|
-
# Build new shape: scalar axes squeezed, array-selected axes kept
|
|
983
|
-
|
|
1026
|
+
# Build new shape: scalar axes squeezed, array-selected axes kept. An R=2
|
|
1027
|
+
# array with every leading axis squeezed keeps a size-1 axis to peel below.
|
|
1028
|
+
all_int = not out_leading and len(new_offsets) == 2
|
|
1029
|
+
new_leading: tuple[int | None, ...] = (1,) if all_int else tuple(out_leading)
|
|
984
1030
|
new_shape: tuple[int | None, ...] = (*new_leading, *trailing)
|
|
985
1031
|
|
|
986
1032
|
# Build result: record Ragged or non-record Ragged depending on layout type
|
|
@@ -989,7 +1035,7 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
|
|
|
989
1035
|
new_fields = {
|
|
990
1036
|
name: RaggedLayout(
|
|
991
1037
|
data=fl.data,
|
|
992
|
-
offsets=
|
|
1038
|
+
offsets=new_offsets,
|
|
993
1039
|
shape=(
|
|
994
1040
|
*new_leading,
|
|
995
1041
|
*fl.shape[fl.shape.index(None) :],
|
|
@@ -1000,7 +1046,7 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
|
|
|
1000
1046
|
}
|
|
1001
1047
|
result: Any = Ragged(
|
|
1002
1048
|
RecordLayout(
|
|
1003
|
-
offsets=
|
|
1049
|
+
offsets=new_offsets,
|
|
1004
1050
|
shape=new_shape,
|
|
1005
1051
|
fields=new_fields,
|
|
1006
1052
|
)
|
|
@@ -1009,18 +1055,183 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
|
|
|
1009
1055
|
result = Ragged(
|
|
1010
1056
|
RaggedLayout(
|
|
1011
1057
|
data=self._rl.data,
|
|
1012
|
-
offsets=
|
|
1058
|
+
offsets=new_offsets,
|
|
1013
1059
|
shape=new_shape,
|
|
1014
1060
|
str_offsets=self._rl.str_offsets,
|
|
1015
1061
|
)
|
|
1016
1062
|
)
|
|
1063
|
+
if all_int:
|
|
1064
|
+
return result[0], 0
|
|
1065
|
+
return result, len(new_leading)
|
|
1017
1066
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
result = result[k]
|
|
1067
|
+
def _index_keys(self, keys: tuple[Any, ...]) -> Any:
|
|
1068
|
+
"""Apply a tuple of (non-newaxis) keys, each to the next output axis.
|
|
1021
1069
|
|
|
1070
|
+
A key after a non-int key must not go to axis 0 of the intermediate
|
|
1071
|
+
result (issue #74): track the output axis ``p`` it targets instead.
|
|
1072
|
+
"""
|
|
1073
|
+
if not keys:
|
|
1074
|
+
return self
|
|
1075
|
+
if None in self._layout.shape and self.rag_dim > 1:
|
|
1076
|
+
n_lead = min(len(keys), self.rag_dim)
|
|
1077
|
+
result, p = self._getitem_leading(keys[:n_lead])
|
|
1078
|
+
rest = keys[n_lead:]
|
|
1079
|
+
else:
|
|
1080
|
+
result, p, rest = self, 0, keys
|
|
1081
|
+
|
|
1082
|
+
def is_fancy(k: Any) -> bool:
|
|
1083
|
+
return not _is_int(k) and not isinstance(k, slice)
|
|
1084
|
+
|
|
1085
|
+
if any(map(is_fancy, rest)) and sum(map(is_fancy, keys)) > 1:
|
|
1086
|
+
# Leading fancy keys broadcast jointly in _getitem_leading; fancy keys
|
|
1087
|
+
# applied one axis at a time would not follow NumPy's broadcasting.
|
|
1088
|
+
raise NotImplementedError(
|
|
1089
|
+
"multiple fancy keys are only supported on leading fixed axes"
|
|
1090
|
+
)
|
|
1091
|
+
for k in rest:
|
|
1092
|
+
result = _index_output_axis(result, p, k)
|
|
1093
|
+
if not _is_int(k):
|
|
1094
|
+
p += 1
|
|
1022
1095
|
return result
|
|
1023
1096
|
|
|
1097
|
+
def _index_axis(self, p: int, k: Any) -> Any:
|
|
1098
|
+
"""Index output axis ``p`` with one key, leaving every other axis intact."""
|
|
1099
|
+
shape = self._layout.shape
|
|
1100
|
+
if None not in shape: # flat opaque-string collection
|
|
1101
|
+
if p == 0:
|
|
1102
|
+
return self._getitem(k)
|
|
1103
|
+
raise NotImplementedError("indexing into opaque strings is unsupported")
|
|
1104
|
+
rag_dim = self.rag_dim
|
|
1105
|
+
if p < rag_dim:
|
|
1106
|
+
if p == 0:
|
|
1107
|
+
return self._getitem(k)
|
|
1108
|
+
return self._getitem_leading((*(slice(None),) * p, k))[0]
|
|
1109
|
+
n_rag = shape.count(None)
|
|
1110
|
+
if p < rag_dim + n_rag:
|
|
1111
|
+
return self._index_level(p - rag_dim, k)
|
|
1112
|
+
if p < len(shape):
|
|
1113
|
+
return self._index_trailing(p, k)
|
|
1114
|
+
if self._is_record or self._rl.is_string:
|
|
1115
|
+
raise NotImplementedError("indexing into opaque strings is unsupported")
|
|
1116
|
+
raise IndexError(f"too many indices for a Ragged of shape {shape}")
|
|
1117
|
+
|
|
1118
|
+
def _index_level(self, j: int, k: Any) -> Any:
|
|
1119
|
+
"""Index ragged axis ``rag_dim + j``, i.e. offsets level ``j``, with ``k``."""
|
|
1120
|
+
layout = self._layout
|
|
1121
|
+
offs = list(layout.offsets)
|
|
1122
|
+
starts, stops = _level_bounds(offs[j])
|
|
1123
|
+
if isinstance(k, slice):
|
|
1124
|
+
offs[j] = np.stack(_slice_bounds(starts, stops, k), 0)
|
|
1125
|
+
return self._rebuild(offs, layout.shape)
|
|
1126
|
+
if not _is_int(k):
|
|
1127
|
+
if len(offs) == 2 and j == 0 and not self._is_record:
|
|
1128
|
+
return self._getitem_inner_gather(k)
|
|
1129
|
+
raise NotImplementedError(
|
|
1130
|
+
"fancy or boolean keys on a ragged axis are unsupported"
|
|
1131
|
+
)
|
|
1132
|
+
k = int(k)
|
|
1133
|
+
axis = self.rag_dim + j
|
|
1134
|
+
new_shape = layout.shape[:axis] + layout.shape[axis + 1 :]
|
|
1135
|
+
if j < len(offs) - 1:
|
|
1136
|
+
# Pick one sub-list per row: gather the next level's bounds.
|
|
1137
|
+
idx = _elem_index(starts, stops, k)
|
|
1138
|
+
nxt_starts, nxt_stops = _level_bounds(offs[j + 1])
|
|
1139
|
+
picked = np.stack([nxt_starts[idx], nxt_stops[idx]], 0)
|
|
1140
|
+
return self._rebuild([*offs[:j], picked, *offs[j + 2 :]], new_shape)
|
|
1141
|
+
if j == 0:
|
|
1142
|
+
# Innermost level is the only one: one element per leading row.
|
|
1143
|
+
return self._take(_elem_index(starts, stops, k), [], new_shape)
|
|
1144
|
+
# Innermost of two levels: one element per middle row that O0 references.
|
|
1145
|
+
# Gathering only those rows repacks O0 and skips unreferenced rows.
|
|
1146
|
+
o0_starts, o0_stops = _level_bounds(offs[0])
|
|
1147
|
+
counts = o0_stops - o0_starts
|
|
1148
|
+
rows = _ranges(o0_starts, counts)
|
|
1149
|
+
idx = _elem_index(starts[rows], stops[rows], k)
|
|
1150
|
+
return self._take(idx, [lengths_to_offsets(counts)], new_shape)
|
|
1151
|
+
|
|
1152
|
+
def _index_trailing(self, p: int, k: Any) -> "Ragged[Any]":
|
|
1153
|
+
"""Index a trailing fixed axis: a plain index on the data buffer."""
|
|
1154
|
+
if self._is_record:
|
|
1155
|
+
raise NotImplementedError(
|
|
1156
|
+
"indexing a trailing axis of a record Ragged is unsupported; "
|
|
1157
|
+
"index a field instead"
|
|
1158
|
+
)
|
|
1159
|
+
rl = self._rl
|
|
1160
|
+
last = _last_ragged_axis(rl.shape)
|
|
1161
|
+
new_data = rl.data[(*(slice(None),) * (p - last), k)]
|
|
1162
|
+
return Ragged(
|
|
1163
|
+
RaggedLayout(
|
|
1164
|
+
data=new_data,
|
|
1165
|
+
offsets=list(rl.offsets),
|
|
1166
|
+
shape=(*rl.shape[: last + 1], *new_data.shape[1:]),
|
|
1167
|
+
str_offsets=rl.str_offsets,
|
|
1168
|
+
)
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
def _leaves(self) -> "dict[str, RaggedLayout[Any]] | None":
|
|
1172
|
+
"""Record fields, or None for a single leaf."""
|
|
1173
|
+
if isinstance(self._layout, RecordLayout):
|
|
1174
|
+
return self._layout.fields
|
|
1175
|
+
return None
|
|
1176
|
+
|
|
1177
|
+
def _rebuild(
|
|
1178
|
+
self, offsets: list[NDArray[Any]], shape: tuple[int | None, ...]
|
|
1179
|
+
) -> "Ragged[Any]":
|
|
1180
|
+
"""Same data, new offsets and shape (fields keep their own trailing dims)."""
|
|
1181
|
+
prefix = shape[: _last_ragged_axis(shape) + 1]
|
|
1182
|
+
fields = self._leaves()
|
|
1183
|
+
if fields is None:
|
|
1184
|
+
rl = self._rl
|
|
1185
|
+
return Ragged(RaggedLayout(rl.data, offsets, shape, rl.str_offsets))
|
|
1186
|
+
new_fields = {
|
|
1187
|
+
name: RaggedLayout(
|
|
1188
|
+
fl.data,
|
|
1189
|
+
offsets,
|
|
1190
|
+
(*prefix, *fl.shape[_last_ragged_axis(fl.shape) + 1 :]),
|
|
1191
|
+
fl.str_offsets,
|
|
1192
|
+
)
|
|
1193
|
+
for name, fl in fields.items()
|
|
1194
|
+
}
|
|
1195
|
+
return Ragged(RecordLayout(offsets, shape, new_fields))
|
|
1196
|
+
|
|
1197
|
+
def _take(
|
|
1198
|
+
self,
|
|
1199
|
+
idx: NDArray[np.int64],
|
|
1200
|
+
offsets: list[NDArray[Any]],
|
|
1201
|
+
shape: tuple[int | None, ...],
|
|
1202
|
+
) -> Any:
|
|
1203
|
+
"""Gather elements ``idx`` of every leaf under new ``offsets``/``shape``.
|
|
1204
|
+
|
|
1205
|
+
``shape`` is the result shape including the record's (first field's)
|
|
1206
|
+
trailing dims; with no ``offsets`` left the result is dense (a dict of
|
|
1207
|
+
per-field results for a record), or a flat opaque-string collection.
|
|
1208
|
+
"""
|
|
1209
|
+
n_trailing = len(self._layout.shape) - 1 - _last_ragged_axis(self._layout.shape)
|
|
1210
|
+
prefix = shape[: len(shape) - n_trailing]
|
|
1211
|
+
|
|
1212
|
+
def leaf(fl: "RaggedLayout[Any]") -> Any:
|
|
1213
|
+
if fl.str_offsets is not None:
|
|
1214
|
+
str_starts, str_stops = _level_bounds(fl.str_offsets)
|
|
1215
|
+
str_offsets = np.stack([str_starts[idx], str_stops[idx]], 0)
|
|
1216
|
+
return Ragged(RaggedLayout(fl.data, offsets, prefix, str_offsets))
|
|
1217
|
+
data = fl.data[idx]
|
|
1218
|
+
if not offsets:
|
|
1219
|
+
dense = data.reshape(
|
|
1220
|
+
(*cast("tuple[int, ...]", prefix), *data.shape[1:])
|
|
1221
|
+
)
|
|
1222
|
+
return dense[()] # a NumPy scalar when every axis was indexed
|
|
1223
|
+
return Ragged(RaggedLayout(data, offsets, (*prefix, *data.shape[1:])))
|
|
1224
|
+
|
|
1225
|
+
fields = self._leaves()
|
|
1226
|
+
if fields is None:
|
|
1227
|
+
return leaf(self._rl)
|
|
1228
|
+
out = {name: leaf(fl) for name, fl in fields.items()}
|
|
1229
|
+
if not offsets:
|
|
1230
|
+
return out
|
|
1231
|
+
return Ragged(
|
|
1232
|
+
RecordLayout(offsets, shape, {name: r._layout for name, r in out.items()})
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1024
1235
|
def _getitem_record(self, where: Any) -> Any:
|
|
1025
1236
|
rec = self._layout
|
|
1026
1237
|
assert isinstance(rec, RecordLayout)
|
|
@@ -1031,16 +1242,10 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
|
|
|
1031
1242
|
raise KeyError(where)
|
|
1032
1243
|
return Ragged(field) # field.offsets[0] is the shared object (zero-copy)
|
|
1033
1244
|
# numpy contract: a non-tuple key is treated as a 1-tuple (A[x] == A[(x,)]).
|
|
1034
|
-
#
|
|
1035
|
-
#
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
if (
|
|
1039
|
-
not isinstance(where, tuple)
|
|
1040
|
-
and self.rag_dim > 1
|
|
1041
|
-
and rec.shape.count(None) == 1
|
|
1042
|
-
):
|
|
1043
|
-
return self._getitem_tuple_multidim((where,))
|
|
1245
|
+
# A record with >1 leading fixed axis routes through the multidim path
|
|
1246
|
+
# directly (never re-dispatching through __getitem__, so no recursion).
|
|
1247
|
+
if not isinstance(where, tuple) and self.rag_dim > 1:
|
|
1248
|
+
return self._getitem_leading((where,))[0]
|
|
1044
1249
|
return self._getitem_record_rows(where)
|
|
1045
1250
|
|
|
1046
1251
|
def _gather_indices(
|
|
@@ -1107,58 +1312,12 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
|
|
|
1107
1312
|
as contiguous OFFSET_TYPE arrays for the shared ragged axis."""
|
|
1108
1313
|
return self._gather_indices(where, *self._starts_stops())
|
|
1109
1314
|
|
|
1110
|
-
@staticmethod
|
|
1111
|
-
def _is_full_slice(k: Any) -> bool:
|
|
1112
|
-
return isinstance(k, slice) and k == slice(None)
|
|
1113
|
-
|
|
1114
|
-
def _getitem_inner(self, sel: Any) -> "Ragged[Any]":
|
|
1115
|
-
o0, o1 = self._layout.offsets
|
|
1116
|
-
o0_starts, o0_stops = _level_bounds(o0)
|
|
1117
|
-
o1_starts, o1_stops = _level_bounds(o1)
|
|
1118
|
-
if isinstance(sel, (int, np.integer)): # k-th middle of each group -> R=1
|
|
1119
|
-
counts = o0_stops - o0_starts
|
|
1120
|
-
if np.any(sel >= counts) or np.any(-sel > counts):
|
|
1121
|
-
raise IndexError(f"inner index {sel} out of range for some group")
|
|
1122
|
-
mid_idx = (o0_starts + (sel if sel >= 0 else counts + sel)).astype(np.int64)
|
|
1123
|
-
ds = np.ascontiguousarray(o1_starts[mid_idx], dtype=OFFSET_TYPE)
|
|
1124
|
-
de = np.ascontiguousarray(o1_stops[mid_idx], dtype=OFFSET_TYPE)
|
|
1125
|
-
trailing = self._layout.shape[self.rag_dim + 2 :]
|
|
1126
|
-
return Ragged(
|
|
1127
|
-
RaggedLayout(
|
|
1128
|
-
data=self._rl.data,
|
|
1129
|
-
offsets=[np.stack([ds, de], 0)],
|
|
1130
|
-
shape=(len(mid_idx), None, *trailing),
|
|
1131
|
-
)
|
|
1132
|
-
)
|
|
1133
|
-
if isinstance(sel, slice): # local per-group slice -> R=2
|
|
1134
|
-
start, stop, step = sel.indices(1 << 62)
|
|
1135
|
-
if step != 1:
|
|
1136
|
-
raise NotImplementedError("step != 1 inner slices are unsupported")
|
|
1137
|
-
if (sel.start is not None and sel.start < 0) or (
|
|
1138
|
-
sel.stop is not None and sel.stop < 0
|
|
1139
|
-
):
|
|
1140
|
-
raise NotImplementedError(
|
|
1141
|
-
"negative inner-slice bounds (rag[:, -k:]) are unsupported"
|
|
1142
|
-
)
|
|
1143
|
-
new_starts = np.minimum(o0_starts + start, o0_stops)
|
|
1144
|
-
new_stops = np.maximum(np.minimum(o0_starts + stop, o0_stops), new_starts)
|
|
1145
|
-
new_o0 = np.stack(
|
|
1146
|
-
[new_starts.astype(OFFSET_TYPE), new_stops.astype(OFFSET_TYPE)], 0
|
|
1147
|
-
)
|
|
1148
|
-
trailing = self._layout.shape[self.rag_dim + 2 :]
|
|
1149
|
-
return Ragged(
|
|
1150
|
-
RaggedLayout(
|
|
1151
|
-
data=self._rl.data,
|
|
1152
|
-
offsets=[new_o0, o1],
|
|
1153
|
-
shape=(len(new_starts), None, None, *trailing),
|
|
1154
|
-
)
|
|
1155
|
-
)
|
|
1156
|
-
return self._getitem_inner_gather(sel) # mask / int-array -> Task 8
|
|
1157
|
-
|
|
1158
1315
|
def _getitem_inner_gather(self, sel: Any) -> "Ragged[Any]":
|
|
1316
|
+
"""Mask or int-array key on the first of two ragged axes."""
|
|
1159
1317
|
o0, o1 = self._layout.offsets
|
|
1160
1318
|
o0_starts, o0_stops = _level_bounds(o0)
|
|
1161
1319
|
o1_starts, o1_stops = _level_bounds(o1)
|
|
1320
|
+
leading = self._layout.shape[: self.rag_dim]
|
|
1162
1321
|
trailing = self._layout.shape[self.rag_dim + 2 :]
|
|
1163
1322
|
if _where_is_bool(sel): # mask over the global middle axis -> R=2
|
|
1164
1323
|
from seqpro.seqpro import _ragged_nested_gather # type: ignore[missing-import]
|
|
@@ -1175,23 +1334,23 @@ class Ragged(NDArrayOperatorsMixin, Generic[RDTYPE_co]):
|
|
|
1175
1334
|
RaggedLayout(
|
|
1176
1335
|
data=self._rl.data,
|
|
1177
1336
|
offsets=[new_o0, np.stack([ds, de], 0)],
|
|
1178
|
-
shape=(
|
|
1337
|
+
shape=(*leading, None, None, *trailing),
|
|
1179
1338
|
)
|
|
1180
1339
|
)
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
counts
|
|
1185
|
-
if np.any(idx.max() >= counts) or np.any(idx.min() < -counts.min()):
|
|
1340
|
+
# uniform per-group int array: middle (g, i) = element idx[i] of group g
|
|
1341
|
+
idx = np.atleast_1d(np.asarray(sel, dtype=np.int64))
|
|
1342
|
+
counts = (o0_stops - o0_starts)[:, None]
|
|
1343
|
+
if np.any(idx >= counts) or np.any(-idx > counts):
|
|
1186
1344
|
raise IndexError("uniform inner index out of range for some group")
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1345
|
+
mid = np.where(idx >= 0, o0_starts[:, None], o0_stops[:, None]) + idx
|
|
1346
|
+
mid = mid.reshape(-1)
|
|
1347
|
+
ds = np.ascontiguousarray(o1_starts[mid], dtype=OFFSET_TYPE)
|
|
1348
|
+
de = np.ascontiguousarray(o1_stops[mid], dtype=OFFSET_TYPE)
|
|
1190
1349
|
return Ragged(
|
|
1191
1350
|
RaggedLayout(
|
|
1192
1351
|
data=self._rl.data,
|
|
1193
1352
|
offsets=[np.stack([ds, de], 0)],
|
|
1194
|
-
shape=(
|
|
1353
|
+
shape=(*leading, len(idx), None, *trailing),
|
|
1195
1354
|
)
|
|
1196
1355
|
)
|
|
1197
1356
|
|
|
@@ -155,18 +155,10 @@ def to_ak(rag: Any) -> ak.Array:
|
|
|
155
155
|
|
|
156
156
|
if isinstance(rag._layout, RecordLayout):
|
|
157
157
|
rec = rag._layout
|
|
158
|
-
#
|
|
159
|
-
# (
|
|
160
|
-
#
|
|
161
|
-
|
|
162
|
-
if rag_dim <= 1:
|
|
163
|
-
return ak.zip({f: to_ak(rag[f]) for f in rag.fields}, depth_limit=1)
|
|
164
|
-
|
|
165
|
-
# Multi-leading-axis path: shape like (b, p, ~v) where rag_dim >= 2 and there is
|
|
166
|
-
# exactly one ragged axis. Per-field to_ak() produces incompatible outer shapes:
|
|
167
|
-
# - numeric fields get RegularArray wrappers from _parts_to_content
|
|
168
|
-
# - string-under-axis fields do NOT (string path only wraps one level)
|
|
169
|
-
# Fix: build leaf contents from layout buffers and re-wrap uniformly.
|
|
158
|
+
# Records always sit at the leaf (the ak.zip default), whatever rag_dim is
|
|
159
|
+
# (#75). Per-field to_ak() + ak.zip can't do this in general: numeric
|
|
160
|
+
# fields get RegularArray wrappers but string-under-axis fields don't.
|
|
161
|
+
# So build leaf contents from the layout buffers and re-wrap uniformly.
|
|
170
162
|
field_leaves: list[Any] = []
|
|
171
163
|
field_names: list[str] = []
|
|
172
164
|
for name, fl in rec.fields.items():
|
|
@@ -198,12 +190,15 @@ def to_ak(rag: Any) -> ak.Array:
|
|
|
198
190
|
|
|
199
191
|
content: Any = RecordArray(field_leaves, field_names)
|
|
200
192
|
|
|
201
|
-
# Re-wrap by iterating reversed(shape[1:]), applying
|
|
202
|
-
# (innermost ragged first) and RegularArray for
|
|
203
|
-
# consumed innermost-to-outermost so
|
|
204
|
-
#
|
|
193
|
+
# Re-wrap by iterating reversed(shape[1 : last ragged axis + 1]), applying
|
|
194
|
+
# ListOffsetArray for None dims (innermost ragged first) and RegularArray for
|
|
195
|
+
# fixed leading dims. Shared offsets are consumed innermost-to-outermost so
|
|
196
|
+
# that the outer list wraps the inner list. Trailing fixed dims already live
|
|
197
|
+
# in each field's leaf, and the outermost dim (shape[0]) is implicit in the
|
|
198
|
+
# final array length.
|
|
199
|
+
last_rag = len(rec.shape) - 1 - rec.shape[::-1].index(None)
|
|
205
200
|
off_iter = iter(reversed(rec.offsets))
|
|
206
|
-
for size in reversed(rec.shape[1:]):
|
|
201
|
+
for size in reversed(rec.shape[1 : last_rag + 1]):
|
|
207
202
|
if size is None:
|
|
208
203
|
off = next(off_iter)
|
|
209
204
|
content = _wrap_list(off, content)
|
|
@@ -223,7 +218,12 @@ def to_ak(rag: Any) -> ak.Array:
|
|
|
223
218
|
)
|
|
224
219
|
else:
|
|
225
220
|
leaf = NumpyArray(np.ascontiguousarray(data)) # type: ignore[arg-type]
|
|
226
|
-
|
|
221
|
+
content = _wrap_list(o0, _wrap_list(o1, leaf))
|
|
222
|
+
# Fixed dims between the outer axis and the first ragged axis (e.g. ploidy
|
|
223
|
+
# in (b, p, ~v, ~w)) partition O0's segments into regular groups.
|
|
224
|
+
for size in reversed(rag._layout.shape[1 : rag.rag_dim]):
|
|
225
|
+
content = RegularArray(content, size)
|
|
226
|
+
return ak.Array(content)
|
|
227
227
|
|
|
228
228
|
# --- string-under-axis path (shape (L0, None), is_string=True) ---
|
|
229
229
|
if rag._rl.str_offsets is not None and rag._layout.offsets:
|