From 8a3278fb101635fc8a94fc0fb63cfd350e344bc6 Mon Sep 17 00:00:00 2001 From: Giovanni Palla Date: Fri, 19 Dec 2025 16:57:31 -0800 Subject: [PATCH 1/2] fix: make PyDESeq2.fit respect layer counts --- .../_pydeseq2.py | 10 ++++++-- .../test_pydeseq2.py | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/pertpy/tools/_differential_gene_expression/_pydeseq2.py b/pertpy/tools/_differential_gene_expression/_pydeseq2.py index b6131f37..acaa3a12 100644 --- a/pertpy/tools/_differential_gene_expression/_pydeseq2.py +++ b/pertpy/tools/_differential_gene_expression/_pydeseq2.py @@ -42,12 +42,18 @@ def fit(self, **kwargs) -> pd.DataFrame: try: usable_cpus = len(os.sched_getaffinity(0)) # type: ignore # os.sched_getaffinity is not available on Windows and macOS except AttributeError: - usable_cpus = os.cpu_count() + usable_cpus = os.cpu_count() or 1 inference = DefaultInference(n_cpus=kwargs.pop("n_cpus", usable_cpus)) + adata_for_dds = self.adata + if self.layer is not None: + # pydeseq2 always uses `adata.X` as the count matrix, so ensure that `X` + # reflects the chosen layer without mutating the user-provided `.X`. + adata_for_dds = AnnData(X=self.data, obs=self.adata.obs, var=self.adata.var) + dds = DeseqDataSet( - adata=self.adata, + adata=adata_for_dds, design=self.design, # initialize using design matrix, not formula refit_cooks=True, inference=inference, diff --git a/tests/tools/_differential_gene_expression/test_pydeseq2.py b/tests/tools/_differential_gene_expression/test_pydeseq2.py index 2139b7c1..474fe675 100644 --- a/tests/tools/_differential_gene_expression/test_pydeseq2.py +++ b/tests/tools/_differential_gene_expression/test_pydeseq2.py @@ -77,3 +77,26 @@ def test_pydeseq2_formula(test_adata): res_2 = model2.test_contrasts(model2.contrast("condition", "A", "B")) npt.assert_almost_equal(res_2.log_fc.values, res_1.log_fc.values) + + +def test_pydeseq2_uses_layer_counts_for_fit(test_adata): + """Regression test: when `layer` is provided, `.fit()` must use that layer as counts, not `.X`.""" + import numpy as np + + from pertpy.tools._differential_gene_expression import PyDESeq2 + + adata = test_adata.copy() + adata.layers["sum"] = adata.X.copy() + + # Make X explicitly non-integer to reproduce the failure mode if `.fit()` ignores `layer=`. + X_layer_before = np.array(adata.layers["sum"], copy=True) + adata.X = np.array(adata.X, dtype=float) / 2.0 + X_before_fit = np.array(adata.X, copy=True) + + model = PyDESeq2(adata=adata, layer="sum", design="~condition") + model.fit(n_cpus=1) + res_df = model.test_contrasts(model.contrast("condition", "A", "B")) + + assert len(res_df) == adata.n_vars + npt.assert_array_equal(X_layer_before, adata.layers["sum"]) + npt.assert_array_equal(X_before_fit, np.array(adata.X)) From d767b393a414cc5d38904c341932cda53e50b339 Mon Sep 17 00:00:00 2001 From: Giovanni Palla Date: Mon, 22 Dec 2025 14:10:49 -0800 Subject: [PATCH 2/2] tests: tidy PyDESeq2 layer regression test --- .../test_pydeseq2.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/tools/_differential_gene_expression/test_pydeseq2.py b/tests/tools/_differential_gene_expression/test_pydeseq2.py index 474fe675..160cdedf 100644 --- a/tests/tools/_differential_gene_expression/test_pydeseq2.py +++ b/tests/tools/_differential_gene_expression/test_pydeseq2.py @@ -1,5 +1,6 @@ from importlib.util import find_spec +import numpy as np import numpy.testing as npt import pytest @@ -81,22 +82,19 @@ def test_pydeseq2_formula(test_adata): def test_pydeseq2_uses_layer_counts_for_fit(test_adata): """Regression test: when `layer` is provided, `.fit()` must use that layer as counts, not `.X`.""" - import numpy as np - from pertpy.tools._differential_gene_expression import PyDESeq2 - adata = test_adata.copy() - adata.layers["sum"] = adata.X.copy() + test_adata.layers["sum"] = test_adata.X.copy() # Make X explicitly non-integer to reproduce the failure mode if `.fit()` ignores `layer=`. - X_layer_before = np.array(adata.layers["sum"], copy=True) - adata.X = np.array(adata.X, dtype=float) / 2.0 - X_before_fit = np.array(adata.X, copy=True) + X_layer_before = np.array(test_adata.layers["sum"], copy=True) + test_adata.X = np.array(test_adata.X, dtype=float) / 2.0 + X_before_fit = np.array(test_adata.X, copy=True) - model = PyDESeq2(adata=adata, layer="sum", design="~condition") + model = PyDESeq2(adata=test_adata, layer="sum", design="~condition") model.fit(n_cpus=1) res_df = model.test_contrasts(model.contrast("condition", "A", "B")) - assert len(res_df) == adata.n_vars - npt.assert_array_equal(X_layer_before, adata.layers["sum"]) - npt.assert_array_equal(X_before_fit, np.array(adata.X)) + assert len(res_df) == test_adata.n_vars + npt.assert_array_equal(X_layer_before, test_adata.layers["sum"]) + npt.assert_array_equal(X_before_fit, np.array(test_adata.X))