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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Version 0.7.0

- Extended `__getitem__` subscripting on `RangedSummarizedExperiment` to support direct slicing by `GenomicRanges` or `CompressedGenomicRangesList` objects.

## Version 0.6.0 - 0.6.5

- Classes now extend `BiocObject` from biocutils, which provides a metadata field.
Expand Down
11 changes: 11 additions & 0 deletions src/summarizedexperiment/RangedSummarizedExperiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,17 @@ def seq_info(self) -> SeqInfo:

# rest of them are inherited from BaseSE.

def _normalize_row_slice(self, rows: Union[str, int, bool, Sequence]):

if isinstance(rows, (GenomicRanges, CompressedGenomicRangesList)):
hits = self.row_ranges.find_overlaps(query=rows)
rows = hits.get_column("self_hits")
elif hasattr(rows, "find_overlaps"):
hits = self.row_ranges.find_overlaps(query=rows)
rows = hits.get_column("self_hits")

return super()._normalize_row_slice(rows)

def get_slice(
self,
rows: Optional[Union[str, int, bool, Sequence]],
Expand Down
13 changes: 7 additions & 6 deletions src/summarizedexperiment/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ def _guess_assay_shape(assays, rows, cols, row_names, col_names) -> tuple:
_keys = list(assays.keys())
if len(_keys) > 0:
_first = _keys[0]
return assays[_first].shape
mat = assays[_first]
if not hasattr(mat, "shape"):
raise TypeError(f"Assay: '{_first}' is not a supported matrix representation.")
return mat.shape

_r = 0
if rows is not None:
Expand Down Expand Up @@ -201,14 +204,14 @@ def __init__(
self._cols = _sanitize_frame(column_data, self._shape[1])

if row_names is None:
row_names = self._rows.row_names
row_names = self._rows.row_names if hasattr(self._rows, "row_names") else None

if row_names is not None and not isinstance(row_names, ut.Names):
row_names = ut.Names(row_names)
self._row_names = row_names

if column_names is None:
column_names = self._cols.row_names
column_names = self._cols.row_names if hasattr(self._cols, "row_names") else None

if column_names is not None and not isinstance(column_names, ut.Names):
column_names = ut.Names(column_names)
Expand Down Expand Up @@ -1125,7 +1128,7 @@ def __getitem__(
Returns:
Same type as caller with the sliced rows and columns.
"""
if isinstance(args, (str, int)):
if not isinstance(args, tuple):
return self.get_slice(args, slice(None))

if isinstance(args, tuple):
Expand All @@ -1139,8 +1142,6 @@ def __getitem__(
else:
raise ValueError(f"`{type(self).__name__}` only supports 2-dimensional slicing.")

raise TypeError("args must be a sequence or a scalar integer or string or a tuple of atmost 2 values.")

################################
######>> AnnData interop <<#####
################################
Expand Down
159 changes: 99 additions & 60 deletions tests/test_RSE_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import numpy as np
import pandas as pd
import pytest
from genomicranges import GenomicRanges
from iranges import IRanges

from summarizedexperiment.RangedSummarizedExperiment import RangedSummarizedExperiment
from summarizedexperiment.SummarizedExperiment import SummarizedExperiment

Expand All @@ -15,42 +18,36 @@
nrows = 200
ncols = 6
counts = np.random.rand(nrows, ncols)
df_gr = pd.DataFrame(
{
"seqnames": [
"chr1",
"chr2",
"chr2",
"chr2",
"chr1",
"chr1",
"chr3",
"chr3",
"chr3",
"chr3",
]
* 20,
"starts": range(100, 300),
"ends": range(110, 310),
"strand": ["-", "+", "+", "*", "*", "+", "+", "+", "-", "-"] * 20,
"score": range(0, 200),
"GC": [random() for _ in range(10)] * 20,
}
)
df_gr = pd.DataFrame({
"seqnames": [
"chr1",
"chr2",
"chr2",
"chr2",
"chr1",
"chr1",
"chr3",
"chr3",
"chr3",
"chr3",
]
* 20,
"starts": range(100, 300),
"ends": range(110, 310),
"strand": ["-", "+", "+", "*", "*", "+", "+", "+", "-", "-"] * 20,
"score": range(0, 200),
"GC": [random() for _ in range(10)] * 20,
})

gr = genomicranges.GenomicRanges.from_pandas(df_gr)

col_data = pd.DataFrame(
{
"treatment": ["ChIP", "Input"] * 3,
}
)
col_data = pd.DataFrame({
"treatment": ["ChIP", "Input"] * 3,
})


def test_RSE_props():
tse = RangedSummarizedExperiment(
assays={"counts": counts}, row_ranges=gr, column_data=col_data
)
tse = RangedSummarizedExperiment(assays={"counts": counts}, row_ranges=gr, column_data=col_data)

assert tse is not None
assert isinstance(tse, RangedSummarizedExperiment)
Expand Down Expand Up @@ -78,9 +75,7 @@ def test_RSE_props():


def test_RSE_subset():
tse = RangedSummarizedExperiment(
assays={"counts": counts}, row_ranges=gr, column_data=col_data
)
tse = RangedSummarizedExperiment(assays={"counts": counts}, row_ranges=gr, column_data=col_data)

assert tse is not None
assert isinstance(tse, RangedSummarizedExperiment)
Expand All @@ -96,9 +91,7 @@ def test_RSE_subset():


def test_RSE_subset_assays():
tse = RangedSummarizedExperiment(
assays={"counts": counts}, row_ranges=gr, column_data=col_data
)
tse = RangedSummarizedExperiment(assays={"counts": counts}, row_ranges=gr, column_data=col_data)

assert tse is not None
assert isinstance(tse, RangedSummarizedExperiment)
Expand All @@ -112,9 +105,7 @@ def test_RSE_subset_assays():


def test_RSE_coverage():
tse = RangedSummarizedExperiment(
assays={"counts": counts}, row_ranges=gr, column_data=col_data
)
tse = RangedSummarizedExperiment(assays={"counts": counts}, row_ranges=gr, column_data=col_data)

assert tse is not None
assert isinstance(tse, RangedSummarizedExperiment)
Expand All @@ -125,9 +116,7 @@ def test_RSE_coverage():


def test_RSE_distance_methods():
tse = RangedSummarizedExperiment(
assays={"counts": counts}, row_ranges=gr, column_data=col_data
)
tse = RangedSummarizedExperiment(assays={"counts": counts}, row_ranges=gr, column_data=col_data)

assert tse is not None
assert isinstance(tse, RangedSummarizedExperiment)
Expand All @@ -143,9 +132,7 @@ def test_RSE_distance_methods():


def test_RSE_range_methods():
tse = RangedSummarizedExperiment(
assays={"counts": counts}, row_ranges=gr, column_data=col_data
)
tse = RangedSummarizedExperiment(assays={"counts": counts}, row_ranges=gr, column_data=col_data)

assert tse is not None
assert isinstance(tse, RangedSummarizedExperiment)
Expand All @@ -170,9 +157,7 @@ def test_RSE_range_methods():


def test_RSE_search():
tse = RangedSummarizedExperiment(
assays={"counts": counts}, row_ranges=gr, column_data=col_data
)
tse = RangedSummarizedExperiment(assays={"counts": counts}, row_ranges=gr, column_data=col_data)

assert tse is not None
assert isinstance(tse, RangedSummarizedExperiment)
Expand All @@ -185,9 +170,7 @@ def test_RSE_search():


def test_RSE_sort_order():
tse = RangedSummarizedExperiment(
assays={"counts": counts}, row_ranges=gr, column_data=col_data
)
tse = RangedSummarizedExperiment(assays={"counts": counts}, row_ranges=gr, column_data=col_data)

assert tse is not None
assert isinstance(tse, RangedSummarizedExperiment)
Expand All @@ -200,10 +183,9 @@ def test_RSE_sort_order():
assert res is not None
assert len(res.row_ranges) == len(tse.row_ranges)


def test_RSE_assay_getters_and_setters():
tse = RangedSummarizedExperiment(
assays={"counts": counts}, row_ranges=gr, column_data=col_data
)
tse = RangedSummarizedExperiment(assays={"counts": counts}, row_ranges=gr, column_data=col_data)

assert tse is not None
assert isinstance(tse, RangedSummarizedExperiment)
Expand All @@ -221,21 +203,19 @@ def test_RSE_assay_getters_and_setters():
assert tse.get_assay("new_counts") is not None
assert new_tse.get_assay("new_counts") is not None


def test_RSE_to_se():
rse = RangedSummarizedExperiment(
assays={"counts": counts}, row_ranges=gr, column_data=col_data
)
rse = RangedSummarizedExperiment(assays={"counts": counts}, row_ranges=gr, column_data=col_data)

se = rse.to_summarizedexperiment()

assert se is not None
assert isinstance(se, SummarizedExperiment)
assert rse.shape == se.shape


def test_RSE_to_se_with_ranges():
tse = RangedSummarizedExperiment(
assays={"counts": counts}, row_ranges=gr, column_data=col_data
)
tse = RangedSummarizedExperiment(assays={"counts": counts}, row_ranges=gr, column_data=col_data)

se = tse.to_summarizedexperiment()

Expand All @@ -247,3 +227,62 @@ def test_RSE_to_se_with_ranges():
assert "seqnames" in se.row_data.column_names
assert "starts" in se.row_data.column_names
assert "ends" in se.row_data.column_names


def test_RSE_combine():
gr1 = GenomicRanges(seqnames=["chr1", "chr2"], ranges=IRanges([1, 100], [10, 50]), strand=["+", "-"])
rse1 = RangedSummarizedExperiment(
assays={"counts": np.array([[1, 2], [3, 4]])}, row_ranges=gr1, column_names=["sample1", "sample2"]
)

gr2 = GenomicRanges(seqnames=["chr3"], ranges=IRanges([200], [20]), strand=["*"])
rse2 = RangedSummarizedExperiment(
assays={"counts": np.array([[5, 6]])}, row_ranges=gr2, column_names=["sample1", "sample2"]
)

combined_rows = rse1.combine_rows(rse2)
assert combined_rows.shape == (3, 2)
assert list(combined_rows.row_ranges.get_seqnames()) == ["chr1", "chr2", "chr3"]

combined_rows_rel = rse1.relaxed_combine_rows(rse2)
assert combined_rows_rel.shape == (3, 2)

rse3 = RangedSummarizedExperiment(assays={"counts": np.array([[7], [8]])}, row_ranges=gr1, column_names=["sample3"])
combined_cols = rse1.combine_columns(rse3)
assert combined_cols.shape == (2, 3)
assert list(combined_cols.column_names) == ["sample1", "sample2", "sample3"]

combined_cols_rel = rse1.relaxed_combine_columns(rse3)
assert combined_cols_rel.shape == (2, 3)


def test_RSE_genomic_slice():
df_gr = pd.DataFrame({
"seqnames": ["chr1", "chr2", "chr3"],
"starts": [100, 200, 300],
"ends": [150, 250, 350],
"strand": ["+", "-", "*"],
})
gr = genomicranges.GenomicRanges.from_pandas(df_gr)

rse = RangedSummarizedExperiment(
assays={"counts": np.random.rand(3, 2)},
row_ranges=gr,
)

query_df = pd.DataFrame({
"seqnames": ["chr2"],
"starts": [220],
"ends": [230],
"strand": ["-"],
})
query_gr = genomicranges.GenomicRanges.from_pandas(query_df)

sliced = rse[query_gr, :]
assert sliced is not None
assert sliced.shape == (1, 2)
assert sliced.row_ranges.get_seqnames() == ["chr2"]

sliced_1d = rse[query_gr]
assert sliced_1d.shape == (1, 2)
assert sliced_1d.row_ranges.get_seqnames() == ["chr2"]
Loading
Loading