Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/build_and_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ jobs:
matrix:
python-version: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v7
with:
Expand All @@ -123,9 +125,10 @@ jobs:

- name: Install and test the stable-ABI wheel
run: |
python -m pip install --upgrade pip numpy
python -m pip install --upgrade pip numpy pytest
python -m pip install --no-index --find-links dist ruranges
python -c "import numpy as np, ruranges.numpy as k; s=np.array([1,5],dtype=np.int64); e=np.array([4,9],dtype=np.int64); g=np.zeros(2,dtype=np.uint32); assert len(k.merge(starts=s,ends=e,groups=g,slack=0)[0])==2; print('ruranges ABI3 ok')"
pytest --import-mode=importlib tests
python -I -c "import numpy as np, ruranges.numpy as k; s=np.array([1,5],dtype=np.int64); e=np.array([4,9],dtype=np.int64); g=np.zeros(2,dtype=np.uint32); assert len(k.merge(starts=s,ends=e,groups=g,slack=0)[0])==2; print('ruranges ABI3 ok')"

publish:
name: Verify and publish
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "ruranges-py"
# Keep this synchronized with `project.version` in pyproject.toml.
version = "0.2.7"
version = "0.2.8"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ classifiers = [
]

# Keep this synchronized with `package.version` in Cargo.toml.
version = "0.2.7"
version = "0.2.8"

[tool.maturin]
module-name = "ruranges.ruranges"
Expand Down
21 changes: 14 additions & 7 deletions ruranges/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,11 @@ def sort_intervals(
groups=groups,
starts=starts,
ends=ends,
sort_reverse_direction=sort_reverse_direction,
sort_reverse_direction=(
None
if sort_reverse_direction is None
else _as_1d_array(sort_reverse_direction, name="sort_reverse_direction")
),
)


Expand Down Expand Up @@ -826,7 +830,7 @@ def window(
groups = _groups_or_arange(groups, len(starts)),
starts=starts,
ends=ends,
negative_strand=negative_strand,
negative_strand=_as_1d_array(negative_strand, name="negative_strand"),
window_size=window_size,
)

Expand Down Expand Up @@ -872,7 +876,7 @@ def tile(
groups = None,
starts=starts,
ends=ends,
negative_strand=negative_strand,
negative_strand=_as_1d_array(negative_strand, name="negative_strand"),
tile_size=tile_size,
)

Expand Down Expand Up @@ -920,7 +924,7 @@ def spliced_subsequence( # same public signature
starts,
ends,
groups,
strand_flags=strand_flags,
strand_flags=_as_1d_array(strand_flags, name="strand_flags"),
slice_starts=slice_starts,
slice_ends=slice_ends,
force_plus_strand=force_plus_strand,
Expand Down Expand Up @@ -994,7 +998,7 @@ def extend(
starts=starts,
ends=ends,
groups=groups,
negative_strand=negative_strand,
negative_strand=_as_1d_array(negative_strand, name="negative_strand"),
ext_3=ext_3,
ext_5=ext_5,
)
Expand Down Expand Up @@ -1048,7 +1052,7 @@ def group_cumsum(
starts=starts,
ends=ends,
groups=groups,
negative_strand=negative_strand,
negative_strand=_as_1d_array(negative_strand, name="negative_strand"),
sort=sort,
)

Expand Down Expand Up @@ -1343,10 +1347,13 @@ def _resolve_rust_fn(


def _as_1d_array(a: NDArray | Sequence[Any], *, name: str) -> np.ndarray:
# The Rust kernels read their inputs as slices, which needs C-contiguous
# memory; a strided view (pandas >= 3.1 can return one from Series.values)
# is copied here rather than rejected there.
arr = np.asarray(a)
if arr.ndim != 1:
raise ValueError(f"`{name}` must be a 1-D array.")
return arr
return np.ascontiguousarray(arr)


def _int_bounds(arr: np.ndarray, *, name: str) -> tuple[int, int]:
Expand Down
100 changes: 100 additions & 0 deletions tests/test_strided_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Every kernel accepts strided arrays, as pandas >= 3.1 can return from Series.values.

The Rust kernels read their inputs as slices, which need C-contiguous memory.
Each case runs with small coordinates, which the wrapper casts to int32 (a
copy), and with coordinates past int32, which it passes through as int64 (no
copy), so a strided array reaches the kernel unless the wrapper fixes it.
"""

import numpy as np
import pytest

from ruranges import numpy as rn

BIG = 2**40


def strided(values, dtype=np.int64):
block = np.zeros((len(values), 3), dtype=dtype)
block[:, 1] = values
column = block[:, 1]
assert not column.flags.c_contiguous
return column


def starts(lo):
return strided([lo, lo + 4])


def ends(lo):
return strided([lo + 2, lo + 8])


def groups():
return strided([0, 0], np.uint32)


def negative_strand():
return strided([False, True], np.bool_)


def pair(lo):
return dict(starts=starts(lo), ends=ends(lo), starts2=starts(lo), ends2=ends(lo), groups=groups(), groups2=groups())


CASES = {
"overlaps": lambda lo: rn.overlaps(**pair(lo)),
"nearest": lambda lo: rn.nearest(**pair(lo)),
"subtract": lambda lo: rn.subtract(**pair(lo)),
"count_overlaps": lambda lo: rn.count_overlaps(**pair(lo)),
"sort_intervals": lambda lo: rn.sort_intervals(
starts=starts(lo), ends=ends(lo), groups=groups(), sort_reverse_direction=negative_strand()
),
"cluster": lambda lo: rn.cluster(starts=starts(lo), ends=ends(lo), groups=groups()),
"merge": lambda lo: rn.merge(starts=starts(lo), ends=ends(lo), groups=groups()),
"max_disjoint": lambda lo: rn.max_disjoint(starts=starts(lo), ends=ends(lo), groups=groups()),
"extend": lambda lo: rn.extend(
starts=starts(lo), ends=ends(lo), groups=groups(), negative_strand=negative_strand(), ext_3=1, ext_5=1
),
"window": lambda lo: rn.window(
starts=starts(lo), ends=ends(lo), groups=groups(), negative_strand=negative_strand(), window_size=2
),
"tile": lambda lo: rn.tile(starts=starts(lo), ends=ends(lo), negative_strand=negative_strand(), tile_size=2),
"group_cumsum": lambda lo: rn.group_cumsum(
starts=starts(lo), ends=ends(lo), groups=groups(), negative_strand=negative_strand()
),
"spliced_subsequence": lambda lo: rn.spliced_subsequence(
starts=starts(lo), ends=ends(lo), groups=groups(), strand_flags=negative_strand(), start=0, end=1
),
"complement": lambda lo: rn.complement(
starts=starts(lo),
ends=ends(lo),
groups=groups(),
chrom_len_ids=strided([0, 1], np.uint32),
chrom_lens=strided([lo + 100, lo + 100]),
),
"genome_bounds": lambda lo: rn.genome_bounds(
groups=groups(), starts=starts(lo), ends=ends(lo), chrom_length=strided([lo + 100, lo + 100])
),
"map_to_global": lambda lo: rn.map_to_global(
starts=starts(0),
ends=ends(0),
groups=groups(),
strand=negative_strand(),
starts2=starts(0),
ends2=ends(0),
groups2=groups(),
chr_code2=groups(),
genome_start2=starts(lo),
genome_end2=ends(lo),
strand2=negative_strand(),
),
"sort_groups": lambda lo: rn.sort_groups(groups()),
"fold_ranks": lambda lo: rn.fold_ranks(groups(), 1, groups(), 1),
}


@pytest.mark.parametrize("lo", [1, BIG])
@pytest.mark.parametrize("name", CASES)
def test_kernel_accepts_strided_input(name, lo):
CASES[name](lo)
Loading