From ac7e9a85b77db5fe338137bd0cde06569a1dc2be Mon Sep 17 00:00:00 2001 From: MiquelSendra Date: Tue, 13 Jan 2026 21:13:45 +0100 Subject: [PATCH 1/6] Refactor raw data handling in SpatialOmicArray to improve compatibility with newer anndata versions --- src/sc3D/sc3D.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/sc3D/sc3D.py b/src/sc3D/sc3D.py index b8eb066..42f6ad6 100644 --- a/src/sc3D/sc3D.py +++ b/src/sc3D/sc3D.py @@ -148,10 +148,10 @@ def read_anndata( + orig[-self.nb_CS_end_ignore :] ) data = data[~(data.obs[array_id].isin(cs_to_remove))] - if data.raw is not None: - data.raw = data.raw.to_adata() - else: - data.raw = data.copy() + # if data.raw is not None: + # data.raw = data.raw.to_adata() + # else: + # data.raw = data.copy() ids = range(len(data)) self.all_cells = list(ids) self.cell_names = dict( @@ -724,12 +724,9 @@ def smooth_data(self, inplace=True): dist_sum = GG.sum(axis=1) product_n = product / dist_sum.reshape(-1, 1) product_sparse = sp.sparse.csr_array(product_n) - tmp_raw = self.anndata.raw.to_adata() - tmp_raw.X = product_sparse.toarray() - if inplace: - self.anndata.raw = tmp_raw - else: - return tmp_raw + # tmp_raw = self.anndata.raw.to_adata() + self.anndata.X = product_sparse.toarray() + return self.anndata def downsample(self, spacing=10, pos_id="pos_3D"): """ @@ -991,7 +988,6 @@ def removing_spatial_outliers(self, th=0.2, n_components=3): l_all = list(self.all_cells) if hasattr(self, "anndata"): self.anndata = self.anndata[l_all] - self.anndata.raw = self.anndata.raw.to_adata() for t, c in self.cells_from_cover_slip.items(): c.intersection_update(self.filtered_cells) for t, c in self.cells_from_tissue.items(): @@ -1096,9 +1092,9 @@ def registration_3d( "no filtering will be applied" ) if work_with_raw: - raw_data = self.anndata.raw.to_adata() + raw_data = self.anndata.raw else: - raw_data = self.anndata.copy() + raw_data = self.anndata if sc_imp: if min_counts_genes is not None: filter_1 = sc.pp.filter_genes( From 1d6751874c7b7dd40dc50a22f94ea1a2c5443713 Mon Sep 17 00:00:00 2001 From: MiquelSendra Date: Wed, 14 Jan 2026 15:05:05 +0100 Subject: [PATCH 2/6] Fix save_anndata for AnnData views by rebuilding from parent to avoid copy errors --- src/sc3D/sc3D.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/sc3D/sc3D.py b/src/sc3D/sc3D.py index 42f6ad6..4966ffe 100644 --- a/src/sc3D/sc3D.py +++ b/src/sc3D/sc3D.py @@ -5,6 +5,7 @@ Author: Leo Guignard (leo.guignard...@AT@...univ-amu.fr) """ from collections import Counter +from copy import copy from itertools import combinations import re @@ -17,7 +18,9 @@ from scipy.spatial.distance import cdist from scipy.optimize import linear_sum_assignment from scipy.interpolate import InterpolatedUnivariateSpline, interp1d -from scipy.stats import zscore, linregress +from scipy.stats import zscore +from scipy.stats import linregress +from scipy.sparse import issparse from seaborn import scatterplot import json from pathlib import Path @@ -1806,7 +1809,33 @@ def save_anndata(self, output_path): Args: output_path (str): path to the output anndata file ('.h5ad' file) """ - data_tmp = self.anndata.copy() + a = self.anndata + + if a.is_view: + # Materialize from the parent using name-based indexers + ref = a._adata_ref # parent AnnData + + rows = ref.obs_names.get_indexer(a.obs_names) + cols = ref.var_names.get_indexer(a.var_names) + + if (rows < 0).any() or (cols < 0).any(): + raise ValueError("Could not map view obs/var names back to parent AnnData.") + + X = ref.X[rows, :] # row subset (array x slice) + X = X[:, cols] # col subset (slice x array) + + # ensure CSR matrix (optional but usually safer for writing) + if issparse(X): + X = X.tocsr() + + data_tmp = anndata.AnnData(X=X, obs=a.obs.copy(), var=a.var.copy()) + # keep uns (optional) + data_tmp.uns = dict(a.uns) + + else: + data_tmp = a # already actual object, not a view + + # add registered coords all_c_sorted = sorted(self.all_cells) pos_final = np.array([self.pos_3D[c] for c in all_c_sorted]) data_tmp.obsm["X_spatial_registered"] = pos_final From c0e39614779a0ed4faf04d8fc41fde31f2e49166 Mon Sep 17 00:00:00 2001 From: MiquelSendra Date: Wed, 14 Jan 2026 16:06:22 +0100 Subject: [PATCH 3/6] Fix save_anndata to preserve full raw gene set for napari viewer compatibility --- src/sc3D/sc3D.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/sc3D/sc3D.py b/src/sc3D/sc3D.py index 4966ffe..050fee1 100644 --- a/src/sc3D/sc3D.py +++ b/src/sc3D/sc3D.py @@ -1812,28 +1812,42 @@ def save_anndata(self, output_path): a = self.anndata if a.is_view: - # Materialize from the parent using name-based indexers - ref = a._adata_ref # parent AnnData + ref = a._adata_ref rows = ref.obs_names.get_indexer(a.obs_names) cols = ref.var_names.get_indexer(a.var_names) - if (rows < 0).any() or (cols < 0).any(): raise ValueError("Could not map view obs/var names back to parent AnnData.") - X = ref.X[rows, :] # row subset (array x slice) - X = X[:, cols] # col subset (slice x array) - - # ensure CSR matrix (optional but usually safer for writing) + X = ref.X[rows, :] + X = X[:, cols] if issparse(X): X = X.tocsr() data_tmp = anndata.AnnData(X=X, obs=a.obs.copy(), var=a.var.copy()) - # keep uns (optional) data_tmp.uns = dict(a.uns) + # --- IMPORTANT: keep all genes accessible for the napari viewer --- + # The viewer expects embryo.anndata.raw to contain the full gene set. + if ref.raw is not None: + Xraw = ref.raw.X[rows, :] # keep ALL genes (no col subset) + if issparse(Xraw): + Xraw = Xraw.tocsr() + raw_tmp = anndata.AnnData(X=Xraw, obs=a.obs.copy(), var=ref.raw.var.copy()) + data_tmp.raw = raw_tmp + else: - data_tmp = a # already actual object, not a view + data_tmp = a # already materialized + # Ensure raw exists for viewer if possible + if data_tmp.raw is None and getattr(a, "_adata_ref", None) is not None and a._adata_ref.raw is not None: + ref = a._adata_ref + rows = ref.obs_names.get_indexer(a.obs_names) + Xraw = ref.raw.X[rows, :] + if issparse(Xraw): + Xraw = Xraw.tocsr() + raw_tmp = anndata.AnnData(X=Xraw, obs=a.obs.copy(), var=ref.raw.var.copy()) + data_tmp.raw = raw_tmp + # add registered coords all_c_sorted = sorted(self.all_cells) From 096c700c59605047b58771bb7404b1418be94ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Guignard?= Date: Thu, 15 Jan 2026 12:16:16 +0100 Subject: [PATCH 4/6] fix of smoothing and deletion of setup.cfg --- pyproject.toml | 60 +++++++++++++++++++++++++++++++----- setup.cfg | 57 ---------------------------------- src/sc3D/_tests/test_sc3D.py | 60 ++++++++++++++++++++++++++++-------- src/sc3D/sc3D.py | 24 ++++++++++----- 4 files changed, 117 insertions(+), 84 deletions(-) delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml index 7954493..89bda74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,59 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["setuptools>=42.0.0", "wheel"] build-backend = "setuptools.build_meta" -[tool.briefcase] -project_name = "sc-3D" -author = "Leo Guignard" +[tool.setuptools.packages.find] +where = ["src"] + +[project] +authors = [ + { name = "Léo Guignard", email = "leo.guignard@univ-amu.fr"}, + { name = "Miquel Sendra"}, +] +maintainers = [ + {name = "Léo Guignard", email = "leo.guignard@univ-amu.fr"} +] +name = "sc-3D" +description = "Array alignment and 3D differential expression for 3D spatial omics" +version = "2.0.0" license = "MIT" +license-files = [ "LICENSE" ] +readme = {file = "README.md", content-type = "text/markdown"} +requires-python = ">= 3.11" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +dependencies = [ + "scipy", + "numpy", + "matplotlib", + "pandas", + "seaborn", + "scikit-learn", + "anndata", +] + +[project.optional-dependencies] +testing = [ + "tox", + "pytest", + "pytest-cov", +] + +[project.urls] +"Bug Tracker" = "https://github.com/GuignardLab/sc3D/issues" +"Documentation" = "https://github.com/GuignardLab/sc3D#README.md" +"Source Code" = "https://github.com/GuignardLab/sc3D" +"User Support" = "https://github.com/GuignardLab/sc3D/issues" [tool.black] line-length = 79 @@ -25,13 +73,11 @@ push = false [tool.bumpver.file_patterns] "pyproject.toml" = [ 'current_version = "{version}"', + 'version = "{version}"', ] "src/sc3D/__init__.py" = [ '__version__ = "{version}"', ] -"setup.cfg" = [ - 'version = {version}', -] "CITATION.cff" = [ "version: {version}", ] \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 60fe4e1..0000000 --- a/setup.cfg +++ /dev/null @@ -1,57 +0,0 @@ -[metadata] -name = sc-3D -version = 2.0.0 -author = Leo Guignard -author_email = leo.guignard@univ-amu.fr -url = https://github.com/GuignardLab/sc3D -license = MIT -description = Array alignment and 3D differential expression for 3D sc omics -long_description = file: README.md -long_description_content_type = text/markdown -summary = Array alignment and 3D differential expression for 3D sc omics -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - Topic :: Software Development :: Testing - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Operating System :: OS Independent - License :: OSI Approved :: MIT License -project_urls = - Bug Tracker = https://github.com/GuignardLab/sc3D/issues - Documentation = https://github.com/GuignardLab/sc3D#README.md - Source Code = https://github.com/GuignardLab/sc3D - User Support = https://github.com/GuignardLab/sc3D/issues - Twitter = https://twitter.com/guignardlab -python_requires = >=3.8 -classifier = - Operating System :: OS Independent - -[options] -packages = find: -include_package_data = True -python_requires = >=3.8 -package_dir = - =src - -# add your package requirements here -install_requires = - scipy - numpy - matplotlib - pandas - seaborn - scikit-learn - anndata - -[options.extras_require] -testing = - tox - pytest # https://docs.pytest.org/en/latest/contents.html - pytest-cov # https://pytest-cov.readthedocs.io/en/latest/ - -[options.packages.find] -where = src \ No newline at end of file diff --git a/src/sc3D/_tests/test_sc3D.py b/src/sc3D/_tests/test_sc3D.py index 31cffba..ed3fcaa 100644 --- a/src/sc3D/_tests/test_sc3D.py +++ b/src/sc3D/_tests/test_sc3D.py @@ -1,33 +1,67 @@ from sc3D import SpatialOmicArray import numpy as np +em = SpatialOmicArray("data/data_test.h5ad", store_anndata=True) + +em2 = SpatialOmicArray( + "data/DLPFC.h5ad", + tissue_id="layer_guess", + pos_id="spatial", + array_id="z", + z_space=30, + store_anndata=True, +) + def test_sc3D(): - em = SpatialOmicArray("data/data_test.h5ad", store_anndata=True) assert len(em.all_cells) == 120 + + +def test_smooth(): em.smooth_data() + + +def test_plot_coverslip(): em.plot_coverslip(7) + + +def test_3D_diff(): em.get_3D_differential_expression([21]) + + +def test_plot_diff(): em.plot_top_3D_diff_expr_genes([21, 23], repetition_allowed=True) em.plot_top_3D_diff_expr_genes([21, 23], repetition_allowed=False) + + +def test_vol_neighbs(): em.plot_volume_vs_neighbs(21) + + +def test_spatial_outliers(): em.removing_spatial_outliers() + + +def test_volumes(): em.compute_volumes() + + +def test_z_pos(): em.set_zpos() - em = SpatialOmicArray( - "data/DLPFC.h5ad", - tissue_id="layer_guess", - pos_id="spatial", - array_id="z", - z_space=30, - store_anndata=True, - ) - em.produce_em() - em.registration_3d() - origin = np.mean([em.final[c] for c in em.all_cells], axis=0) + +def test_produce(): + em2.produce_em() + + +def test_registration(): + em2.registration_3d() + + +def test_plot_slices(): + origin = np.mean([em2.final[c] for c in em2.all_cells], axis=0) origin = np.hstack([origin, 80]) angles = np.array([-5.0, 5.0, 0.0]) - _ = em.plot_slice( + _ = em2.plot_slice( angles, color_map="viridis", origin=origin, thickness=30, nb_interp=5 ) diff --git a/src/sc3D/sc3D.py b/src/sc3D/sc3D.py index 050fee1..88c99bb 100644 --- a/src/sc3D/sc3D.py +++ b/src/sc3D/sc3D.py @@ -199,7 +199,7 @@ def read_anndata( self.gene_expression = {id_: [] for id_ in ids} self.array_id_num_pos = array_id_num_pos - if array_id in data.obs_keys(): + if array_id in data.obs: if data.obs[array_id].dtype != int: exp = re.compile("[0-9]+") cs = list( @@ -728,7 +728,8 @@ def smooth_data(self, inplace=True): product_n = product / dist_sum.reshape(-1, 1) product_sparse = sp.sparse.csr_array(product_n) # tmp_raw = self.anndata.raw.to_adata() - self.anndata.X = product_sparse.toarray() + print(f"{self.anndata.X.shape=}\n{product_sparse.toarray().shape=}") + self.anndata.raw._X = product_sparse.toarray() return self.anndata def downsample(self, spacing=10, pos_id="pos_3D"): @@ -1817,7 +1818,9 @@ def save_anndata(self, output_path): rows = ref.obs_names.get_indexer(a.obs_names) cols = ref.var_names.get_indexer(a.var_names) if (rows < 0).any() or (cols < 0).any(): - raise ValueError("Could not map view obs/var names back to parent AnnData.") + raise ValueError( + "Could not map view obs/var names back to parent AnnData." + ) X = ref.X[rows, :] X = X[:, cols] @@ -1833,22 +1836,29 @@ def save_anndata(self, output_path): Xraw = ref.raw.X[rows, :] # keep ALL genes (no col subset) if issparse(Xraw): Xraw = Xraw.tocsr() - raw_tmp = anndata.AnnData(X=Xraw, obs=a.obs.copy(), var=ref.raw.var.copy()) + raw_tmp = anndata.AnnData( + X=Xraw, obs=a.obs.copy(), var=ref.raw.var.copy() + ) data_tmp.raw = raw_tmp else: data_tmp = a # already materialized # Ensure raw exists for viewer if possible - if data_tmp.raw is None and getattr(a, "_adata_ref", None) is not None and a._adata_ref.raw is not None: + if ( + data_tmp.raw is None + and getattr(a, "_adata_ref", None) is not None + and a._adata_ref.raw is not None + ): ref = a._adata_ref rows = ref.obs_names.get_indexer(a.obs_names) Xraw = ref.raw.X[rows, :] if issparse(Xraw): Xraw = Xraw.tocsr() - raw_tmp = anndata.AnnData(X=Xraw, obs=a.obs.copy(), var=ref.raw.var.copy()) + raw_tmp = anndata.AnnData( + X=Xraw, obs=a.obs.copy(), var=ref.raw.var.copy() + ) data_tmp.raw = raw_tmp - # add registered coords all_c_sorted = sorted(self.all_cells) pos_final = np.array([self.pos_3D[c] for c in all_c_sorted]) From 8ef413022279ab74e585e82ae20f13a5c58f4366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Guignard?= Date: Thu, 15 Jan 2026 14:45:59 +0100 Subject: [PATCH 5/6] actuallising tox commands --- .github/workflows/test_and_deploy.yml | 2 +- tox.ini | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 8fc83f4..cd46652 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: platform: [ubuntu-latest, windows-latest, macos-latest] - python-version: [3.8, 3.9, '3.10'] + python-version: ['3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v2 diff --git a/tox.ini b/tox.ini index aa5df71..3318ab2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,13 @@ # For more information about tox, see https://tox.readthedocs.io/en/latest/ [tox] -envlist = py{38,39,310}-{linux,macos,windows} +envlist = py{311,312,313}-{linux,macos,windows} isolated_build=true [gh-actions] python = - 3.8: py38 - 3.9: py39 - 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 [gh-actions:env] PLATFORM = From add9d2d271e440e2e0f5b6dd6d65f4ed1c490e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Guignard?= Date: Thu, 15 Jan 2026 14:52:23 +0100 Subject: [PATCH 6/6] removing unused import --- src/sc3D/sc3D.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sc3D/sc3D.py b/src/sc3D/sc3D.py index 88c99bb..03cda9c 100644 --- a/src/sc3D/sc3D.py +++ b/src/sc3D/sc3D.py @@ -5,7 +5,6 @@ Author: Leo Guignard (leo.guignard...@AT@...univ-amu.fr) """ from collections import Counter -from copy import copy from itertools import combinations import re