Skip to content

Commit c403156

Browse files
authored
Merge pull request #7 from MiquelSendra/fix-raw-handling-py313
Fix adata.raw handling after slicing (Python ≥3.11 / 3.13)
2 parents d309f49 + add9d2d commit c403156

6 files changed

Lines changed: 169 additions & 98 deletions

File tree

‎.github/workflows/test_and_deploy.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
strategy:
2222
matrix:
2323
platform: [ubuntu-latest, windows-latest, macos-latest]
24-
python-version: [3.8, 3.9, '3.10']
24+
python-version: ['3.11', '3.12', '3.13']
2525

2626
steps:
2727
- uses: actions/checkout@v2

‎pyproject.toml‎

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,59 @@
11
[build-system]
2-
requires = ["setuptools", "wheel"]
2+
requires = ["setuptools>=42.0.0", "wheel"]
33
build-backend = "setuptools.build_meta"
44

5-
[tool.briefcase]
6-
project_name = "sc-3D"
7-
author = "Leo Guignard"
5+
[tool.setuptools.packages.find]
6+
where = ["src"]
7+
8+
[project]
9+
authors = [
10+
{ name = "Léo Guignard", email = "leo.guignard@univ-amu.fr"},
11+
{ name = "Miquel Sendra"},
12+
]
13+
maintainers = [
14+
{name = "Léo Guignard", email = "leo.guignard@univ-amu.fr"}
15+
]
16+
name = "sc-3D"
17+
description = "Array alignment and 3D differential expression for 3D spatial omics"
18+
version = "2.0.0"
819
license = "MIT"
20+
license-files = [ "LICENSE" ]
21+
readme = {file = "README.md", content-type = "text/markdown"}
22+
requires-python = ">= 3.11"
23+
classifiers = [
24+
"Development Status :: 4 - Beta",
25+
"Intended Audience :: Developers",
26+
"Operating System :: OS Independent",
27+
"Programming Language :: Python",
28+
"Programming Language :: Python :: 3",
29+
"Programming Language :: Python :: 3 :: Only",
30+
"Programming Language :: Python :: 3.11",
31+
"Programming Language :: Python :: 3.12",
32+
"Programming Language :: Python :: 3.13",
33+
]
34+
35+
dependencies = [
36+
"scipy",
37+
"numpy",
38+
"matplotlib",
39+
"pandas",
40+
"seaborn",
41+
"scikit-learn",
42+
"anndata",
43+
]
44+
45+
[project.optional-dependencies]
46+
testing = [
47+
"tox",
48+
"pytest",
49+
"pytest-cov",
50+
]
51+
52+
[project.urls]
53+
"Bug Tracker" = "https://github.com/GuignardLab/sc3D/issues"
54+
"Documentation" = "https://github.com/GuignardLab/sc3D#README.md"
55+
"Source Code" = "https://github.com/GuignardLab/sc3D"
56+
"User Support" = "https://github.com/GuignardLab/sc3D/issues"
957

1058
[tool.black]
1159
line-length = 79
@@ -25,13 +73,11 @@ push = false
2573
[tool.bumpver.file_patterns]
2674
"pyproject.toml" = [
2775
'current_version = "{version}"',
76+
'version = "{version}"',
2877
]
2978
"src/sc3D/__init__.py" = [
3079
'__version__ = "{version}"',
3180
]
32-
"setup.cfg" = [
33-
'version = {version}',
34-
]
3581
"CITATION.cff" = [
3682
"version: {version}",
3783
]

‎setup.cfg‎

Lines changed: 0 additions & 57 deletions
This file was deleted.

‎src/sc3D/_tests/test_sc3D.py‎

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,67 @@
11
from sc3D import SpatialOmicArray
22
import numpy as np
33

4+
em = SpatialOmicArray("data/data_test.h5ad", store_anndata=True)
5+
6+
em2 = SpatialOmicArray(
7+
"data/DLPFC.h5ad",
8+
tissue_id="layer_guess",
9+
pos_id="spatial",
10+
array_id="z",
11+
z_space=30,
12+
store_anndata=True,
13+
)
14+
415

516
def test_sc3D():
6-
em = SpatialOmicArray("data/data_test.h5ad", store_anndata=True)
717
assert len(em.all_cells) == 120
18+
19+
20+
def test_smooth():
821
em.smooth_data()
22+
23+
24+
def test_plot_coverslip():
925
em.plot_coverslip(7)
26+
27+
28+
def test_3D_diff():
1029
em.get_3D_differential_expression([21])
30+
31+
32+
def test_plot_diff():
1133
em.plot_top_3D_diff_expr_genes([21, 23], repetition_allowed=True)
1234
em.plot_top_3D_diff_expr_genes([21, 23], repetition_allowed=False)
35+
36+
37+
def test_vol_neighbs():
1338
em.plot_volume_vs_neighbs(21)
39+
40+
41+
def test_spatial_outliers():
1442
em.removing_spatial_outliers()
43+
44+
45+
def test_volumes():
1546
em.compute_volumes()
47+
48+
49+
def test_z_pos():
1650
em.set_zpos()
1751

18-
em = SpatialOmicArray(
19-
"data/DLPFC.h5ad",
20-
tissue_id="layer_guess",
21-
pos_id="spatial",
22-
array_id="z",
23-
z_space=30,
24-
store_anndata=True,
25-
)
26-
em.produce_em()
27-
em.registration_3d()
28-
origin = np.mean([em.final[c] for c in em.all_cells], axis=0)
52+
53+
def test_produce():
54+
em2.produce_em()
55+
56+
57+
def test_registration():
58+
em2.registration_3d()
59+
60+
61+
def test_plot_slices():
62+
origin = np.mean([em2.final[c] for c in em2.all_cells], axis=0)
2963
origin = np.hstack([origin, 80])
3064
angles = np.array([-5.0, 5.0, 0.0])
31-
_ = em.plot_slice(
65+
_ = em2.plot_slice(
3266
angles, color_map="viridis", origin=origin, thickness=30, nb_interp=5
3367
)

‎src/sc3D/sc3D.py‎

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
from scipy.spatial.distance import cdist
1818
from scipy.optimize import linear_sum_assignment
1919
from scipy.interpolate import InterpolatedUnivariateSpline, interp1d
20-
from scipy.stats import zscore, linregress
20+
from scipy.stats import zscore
21+
from scipy.stats import linregress
22+
from scipy.sparse import issparse
2123
from seaborn import scatterplot
2224
import json
2325
from pathlib import Path
@@ -148,10 +150,10 @@ def read_anndata(
148150
+ orig[-self.nb_CS_end_ignore :]
149151
)
150152
data = data[~(data.obs[array_id].isin(cs_to_remove))]
151-
if data.raw is not None:
152-
data.raw = data.raw.to_adata()
153-
else:
154-
data.raw = data.copy()
153+
# if data.raw is not None:
154+
# data.raw = data.raw.to_adata()
155+
# else:
156+
# data.raw = data.copy()
155157
ids = range(len(data))
156158
self.all_cells = list(ids)
157159
self.cell_names = dict(
@@ -196,7 +198,7 @@ def read_anndata(
196198
self.gene_expression = {id_: [] for id_ in ids}
197199

198200
self.array_id_num_pos = array_id_num_pos
199-
if array_id in data.obs_keys():
201+
if array_id in data.obs:
200202
if data.obs[array_id].dtype != int:
201203
exp = re.compile("[0-9]+")
202204
cs = list(
@@ -724,12 +726,10 @@ def smooth_data(self, inplace=True):
724726
dist_sum = GG.sum(axis=1)
725727
product_n = product / dist_sum.reshape(-1, 1)
726728
product_sparse = sp.sparse.csr_array(product_n)
727-
tmp_raw = self.anndata.raw.to_adata()
728-
tmp_raw.X = product_sparse.toarray()
729-
if inplace:
730-
self.anndata.raw = tmp_raw
731-
else:
732-
return tmp_raw
729+
# tmp_raw = self.anndata.raw.to_adata()
730+
print(f"{self.anndata.X.shape=}\n{product_sparse.toarray().shape=}")
731+
self.anndata.raw._X = product_sparse.toarray()
732+
return self.anndata
733733

734734
def downsample(self, spacing=10, pos_id="pos_3D"):
735735
"""
@@ -991,7 +991,6 @@ def removing_spatial_outliers(self, th=0.2, n_components=3):
991991
l_all = list(self.all_cells)
992992
if hasattr(self, "anndata"):
993993
self.anndata = self.anndata[l_all]
994-
self.anndata.raw = self.anndata.raw.to_adata()
995994
for t, c in self.cells_from_cover_slip.items():
996995
c.intersection_update(self.filtered_cells)
997996
for t, c in self.cells_from_tissue.items():
@@ -1096,9 +1095,9 @@ def registration_3d(
10961095
"no filtering will be applied"
10971096
)
10981097
if work_with_raw:
1099-
raw_data = self.anndata.raw.to_adata()
1098+
raw_data = self.anndata.raw
11001099
else:
1101-
raw_data = self.anndata.copy()
1100+
raw_data = self.anndata
11021101
if sc_imp:
11031102
if min_counts_genes is not None:
11041103
filter_1 = sc.pp.filter_genes(
@@ -1810,7 +1809,56 @@ def save_anndata(self, output_path):
18101809
Args:
18111810
output_path (str): path to the output anndata file ('.h5ad' file)
18121811
"""
1813-
data_tmp = self.anndata.copy()
1812+
a = self.anndata
1813+
1814+
if a.is_view:
1815+
ref = a._adata_ref
1816+
1817+
rows = ref.obs_names.get_indexer(a.obs_names)
1818+
cols = ref.var_names.get_indexer(a.var_names)
1819+
if (rows < 0).any() or (cols < 0).any():
1820+
raise ValueError(
1821+
"Could not map view obs/var names back to parent AnnData."
1822+
)
1823+
1824+
X = ref.X[rows, :]
1825+
X = X[:, cols]
1826+
if issparse(X):
1827+
X = X.tocsr()
1828+
1829+
data_tmp = anndata.AnnData(X=X, obs=a.obs.copy(), var=a.var.copy())
1830+
data_tmp.uns = dict(a.uns)
1831+
1832+
# --- IMPORTANT: keep all genes accessible for the napari viewer ---
1833+
# The viewer expects embryo.anndata.raw to contain the full gene set.
1834+
if ref.raw is not None:
1835+
Xraw = ref.raw.X[rows, :] # keep ALL genes (no col subset)
1836+
if issparse(Xraw):
1837+
Xraw = Xraw.tocsr()
1838+
raw_tmp = anndata.AnnData(
1839+
X=Xraw, obs=a.obs.copy(), var=ref.raw.var.copy()
1840+
)
1841+
data_tmp.raw = raw_tmp
1842+
1843+
else:
1844+
data_tmp = a # already materialized
1845+
# Ensure raw exists for viewer if possible
1846+
if (
1847+
data_tmp.raw is None
1848+
and getattr(a, "_adata_ref", None) is not None
1849+
and a._adata_ref.raw is not None
1850+
):
1851+
ref = a._adata_ref
1852+
rows = ref.obs_names.get_indexer(a.obs_names)
1853+
Xraw = ref.raw.X[rows, :]
1854+
if issparse(Xraw):
1855+
Xraw = Xraw.tocsr()
1856+
raw_tmp = anndata.AnnData(
1857+
X=Xraw, obs=a.obs.copy(), var=ref.raw.var.copy()
1858+
)
1859+
data_tmp.raw = raw_tmp
1860+
1861+
# add registered coords
18141862
all_c_sorted = sorted(self.all_cells)
18151863
pos_final = np.array([self.pos_3D[c] for c in all_c_sorted])
18161864
data_tmp.obsm["X_spatial_registered"] = pos_final

‎tox.ini‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# For more information about tox, see https://tox.readthedocs.io/en/latest/
22
[tox]
3-
envlist = py{38,39,310}-{linux,macos,windows}
3+
envlist = py{311,312,313}-{linux,macos,windows}
44
isolated_build=true
55

66
[gh-actions]
77
python =
8-
3.8: py38
9-
3.9: py39
10-
3.10: py310
8+
3.11: py311
9+
3.12: py312
10+
3.13: py313
1111

1212
[gh-actions:env]
1313
PLATFORM =

0 commit comments

Comments
 (0)