diff --git a/.benchmarks/Linux-CPython-3.12-64bit/0004_illico-scaling-w-threads.json b/.benchmarks/Linux-CPython-3.12-64bit/0004_illico-scaling-w-threads.json
index dd23682..6b8c067 100644
--- a/.benchmarks/Linux-CPython-3.12-64bit/0004_illico-scaling-w-threads.json
+++ b/.benchmarks/Linux-CPython-3.12-64bit/0004_illico-scaling-w-threads.json
@@ -914,4 +914,4 @@
],
"datetime": "2025-12-21T22:43:39.949875+00:00",
"version": "5.2.3"
-}
\ No newline at end of file
+}
diff --git a/.benchmarks/Linux-CPython-3.12-64bit/0009_pdex-speed-bench.json b/.benchmarks/Linux-CPython-3.12-64bit/0009_pdex-speed-bench.json
new file mode 100644
index 0000000..6d898c7
--- /dev/null
+++ b/.benchmarks/Linux-CPython-3.12-64bit/0009_pdex-speed-bench.json
@@ -0,0 +1,212 @@
+{
+ "machine_info": {
+ "node": "vcc-cpu-0",
+ "processor": "x86_64",
+ "machine": "x86_64",
+ "python_compiler": "GCC 14.3.0",
+ "python_implementation": "CPython",
+ "python_implementation_version": "3.12.13",
+ "python_version": "3.12.13",
+ "python_build": [
+ "main",
+ "Mar 5 2026 16:50:00"
+ ],
+ "release": "5.15.0-1074-oracle",
+ "system": "Linux",
+ "cpu": {
+ "python_version": "3.12.13.final.0 (64 bit)",
+ "cpuinfo_version": [
+ 9,
+ 0,
+ 0
+ ],
+ "cpuinfo_version_string": "9.0.0",
+ "arch": "X86_64",
+ "bits": 64,
+ "count": 255,
+ "arch_string_raw": "x86_64",
+ "vendor_id_raw": "AuthenticAMD",
+ "brand_raw": "AMD EPYC 7J13 64-Core Processor",
+ "hz_advertised_friendly": "2.5500 GHz",
+ "hz_actual_friendly": "2.5500 GHz",
+ "hz_advertised": [
+ 2550000000,
+ 0
+ ],
+ "hz_actual": [
+ 2550000000,
+ 0
+ ],
+ "stepping": 1,
+ "model": 1,
+ "family": 25,
+ "flags": [
+ "3dnowext",
+ "3dnowprefetch",
+ "abm",
+ "adx",
+ "aes",
+ "amd_ppin",
+ "aperfmperf",
+ "apic",
+ "arat",
+ "avic",
+ "avx",
+ "avx2",
+ "bmi1",
+ "bmi2",
+ "bpext",
+ "cat_l3",
+ "cdp_l3",
+ "clflush",
+ "clflushopt",
+ "clwb",
+ "clzero",
+ "cmov",
+ "cmp_legacy",
+ "constant_tsc",
+ "cpb",
+ "cpuid",
+ "cqm",
+ "cqm_llc",
+ "cqm_mbm_local",
+ "cqm_mbm_total",
+ "cqm_occup_llc",
+ "cr8_legacy",
+ "cx16",
+ "cx8",
+ "dbx",
+ "de",
+ "decodeassists",
+ "erms",
+ "extapic",
+ "extd_apicid",
+ "f16c",
+ "flushbyasid",
+ "fma",
+ "fpu",
+ "fsgsbase",
+ "fsrm",
+ "fxsr",
+ "fxsr_opt",
+ "ht",
+ "hw_pstate",
+ "ibpb",
+ "ibrs",
+ "ibs",
+ "invpcid",
+ "invpcid_single",
+ "irperf",
+ "lahf_lm",
+ "lbrv",
+ "lm",
+ "mba",
+ "mca",
+ "mce",
+ "misalignsse",
+ "mmx",
+ "mmxext",
+ "monitor",
+ "movbe",
+ "msr",
+ "mtrr",
+ "mwaitx",
+ "nonstop_tsc",
+ "nopl",
+ "npt",
+ "nrip_save",
+ "nx",
+ "ospke",
+ "osvw",
+ "osxsave",
+ "overflow_recov",
+ "pae",
+ "pat",
+ "pausefilter",
+ "pci_l2i",
+ "pcid",
+ "pclmulqdq",
+ "pdpe1gb",
+ "perfctr_core",
+ "perfctr_llc",
+ "perfctr_nb",
+ "pfthreshold",
+ "pge",
+ "pku",
+ "pni",
+ "popcnt",
+ "pqe",
+ "pqm",
+ "pse",
+ "pse36",
+ "rapl",
+ "rdpid",
+ "rdpru",
+ "rdrand",
+ "rdrnd",
+ "rdseed",
+ "rdt_a",
+ "rdtscp",
+ "rep_good",
+ "sep",
+ "sha",
+ "sha_ni",
+ "skinit",
+ "smap",
+ "smca",
+ "smep",
+ "ssbd",
+ "sse",
+ "sse2",
+ "sse4_1",
+ "sse4_2",
+ "sse4a",
+ "ssse3",
+ "stibp",
+ "succor",
+ "svm",
+ "svm_lock",
+ "syscall",
+ "tce",
+ "topoext",
+ "tsc",
+ "tsc_scale",
+ "umip",
+ "v_spec_ctrl",
+ "v_vmsave_vmload",
+ "vaes",
+ "vgif",
+ "vmcb_clean",
+ "vme",
+ "vmmcall",
+ "vpclmulqdq",
+ "wbnoinvd",
+ "wdt",
+ "x2apic",
+ "xgetbv1",
+ "xsave",
+ "xsavec",
+ "xsaveerptr",
+ "xsaveopt",
+ "xsaves"
+ ],
+ "l3_cache_size": 524288,
+ "l2_cache_size": 67108864,
+ "l1_data_cache_size": 4194304,
+ "l1_instruction_cache_size": 4194304,
+ "l2_cache_line_size": 512,
+ "l2_cache_associativity": 6
+ }
+ },
+ "commit_info": {
+ "id": "8df6bdafb48cad80e7bde4d9ce0bb8b926b5c987",
+ "time": "2026-03-14T11:07:06+01:00",
+ "author_time": "2026-03-14T11:07:06+01:00",
+ "dirty": true,
+ "project": "illico",
+ "branch": "feat/scanpy-adaptors"
+ },
+ "benchmarks": [],
+ "datetime": "2026-03-14T10:31:40.886240+00:00",
+ "version": "5.2.3"
+}
diff --git a/.benchmarks/Linux-CPython-3.12-64bit/0010_pdex-speed-bench.json b/.benchmarks/Linux-CPython-3.12-64bit/0010_pdex-speed-bench.json
new file mode 100644
index 0000000..c30249f
--- /dev/null
+++ b/.benchmarks/Linux-CPython-3.12-64bit/0010_pdex-speed-bench.json
@@ -0,0 +1,933 @@
+{
+ "machine_info": {
+ "node": "vcc-cpu-0",
+ "processor": "x86_64",
+ "machine": "x86_64",
+ "python_compiler": "GCC 14.3.0",
+ "python_implementation": "CPython",
+ "python_implementation_version": "3.12.13",
+ "python_version": "3.12.13",
+ "python_build": [
+ "main",
+ "Mar 5 2026 16:50:00"
+ ],
+ "release": "5.15.0-1074-oracle",
+ "system": "Linux",
+ "cpu": {
+ "python_version": "3.12.13.final.0 (64 bit)",
+ "cpuinfo_version": [
+ 9,
+ 0,
+ 0
+ ],
+ "cpuinfo_version_string": "9.0.0",
+ "arch": "X86_64",
+ "bits": 64,
+ "count": 255,
+ "arch_string_raw": "x86_64",
+ "vendor_id_raw": "AuthenticAMD",
+ "brand_raw": "AMD EPYC 7J13 64-Core Processor",
+ "hz_advertised_friendly": "2.5500 GHz",
+ "hz_actual_friendly": "2.5500 GHz",
+ "hz_advertised": [
+ 2550000000,
+ 0
+ ],
+ "hz_actual": [
+ 2550000000,
+ 0
+ ],
+ "stepping": 1,
+ "model": 1,
+ "family": 25,
+ "flags": [
+ "3dnowext",
+ "3dnowprefetch",
+ "abm",
+ "adx",
+ "aes",
+ "amd_ppin",
+ "aperfmperf",
+ "apic",
+ "arat",
+ "avic",
+ "avx",
+ "avx2",
+ "bmi1",
+ "bmi2",
+ "bpext",
+ "cat_l3",
+ "cdp_l3",
+ "clflush",
+ "clflushopt",
+ "clwb",
+ "clzero",
+ "cmov",
+ "cmp_legacy",
+ "constant_tsc",
+ "cpb",
+ "cpuid",
+ "cqm",
+ "cqm_llc",
+ "cqm_mbm_local",
+ "cqm_mbm_total",
+ "cqm_occup_llc",
+ "cr8_legacy",
+ "cx16",
+ "cx8",
+ "dbx",
+ "de",
+ "decodeassists",
+ "erms",
+ "extapic",
+ "extd_apicid",
+ "f16c",
+ "flushbyasid",
+ "fma",
+ "fpu",
+ "fsgsbase",
+ "fsrm",
+ "fxsr",
+ "fxsr_opt",
+ "ht",
+ "hw_pstate",
+ "ibpb",
+ "ibrs",
+ "ibs",
+ "invpcid",
+ "invpcid_single",
+ "irperf",
+ "lahf_lm",
+ "lbrv",
+ "lm",
+ "mba",
+ "mca",
+ "mce",
+ "misalignsse",
+ "mmx",
+ "mmxext",
+ "monitor",
+ "movbe",
+ "msr",
+ "mtrr",
+ "mwaitx",
+ "nonstop_tsc",
+ "nopl",
+ "npt",
+ "nrip_save",
+ "nx",
+ "ospke",
+ "osvw",
+ "osxsave",
+ "overflow_recov",
+ "pae",
+ "pat",
+ "pausefilter",
+ "pci_l2i",
+ "pcid",
+ "pclmulqdq",
+ "pdpe1gb",
+ "perfctr_core",
+ "perfctr_llc",
+ "perfctr_nb",
+ "pfthreshold",
+ "pge",
+ "pku",
+ "pni",
+ "popcnt",
+ "pqe",
+ "pqm",
+ "pse",
+ "pse36",
+ "rapl",
+ "rdpid",
+ "rdpru",
+ "rdrand",
+ "rdrnd",
+ "rdseed",
+ "rdt_a",
+ "rdtscp",
+ "rep_good",
+ "sep",
+ "sha",
+ "sha_ni",
+ "skinit",
+ "smap",
+ "smca",
+ "smep",
+ "ssbd",
+ "sse",
+ "sse2",
+ "sse4_1",
+ "sse4_2",
+ "sse4a",
+ "ssse3",
+ "stibp",
+ "succor",
+ "svm",
+ "svm_lock",
+ "syscall",
+ "tce",
+ "topoext",
+ "tsc",
+ "tsc_scale",
+ "umip",
+ "v_spec_ctrl",
+ "v_vmsave_vmload",
+ "vaes",
+ "vgif",
+ "vmcb_clean",
+ "vme",
+ "vmmcall",
+ "vpclmulqdq",
+ "wbnoinvd",
+ "wdt",
+ "x2apic",
+ "xgetbv1",
+ "xsave",
+ "xsavec",
+ "xsaveerptr",
+ "xsaveopt",
+ "xsaves"
+ ],
+ "l3_cache_size": 524288,
+ "l2_cache_size": 67108864,
+ "l1_data_cache_size": 4194304,
+ "l1_instruction_cache_size": 4194304,
+ "l2_cache_line_size": 512,
+ "l2_cache_associativity": 6
+ }
+ },
+ "commit_info": {
+ "id": "8df6bdafb48cad80e7bde4d9ce0bb8b926b5c987",
+ "time": "2026-03-14T11:07:06+01:00",
+ "author_time": "2026-03-14T11:07:06+01:00",
+ "dirty": true,
+ "project": "illico",
+ "branch": "feat/scanpy-adaptors"
+ },
+ "benchmarks": [
+ {
+ "group": "k562-dense-ovo",
+ "name": "test_speed_benchmark[k562-dense-20%-pdex-ovo-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[k562-dense-20%-pdex-ovo-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "k562",
+ "dense",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovo",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "k562-dense-20%-pdex-ovo-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 278.3875277582556,
+ "max": 278.3875277582556,
+ "mean": 278.3875277582556,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 278.3875277582556,
+ "iqr": 0.0,
+ "q1": 278.3875277582556,
+ "q3": 278.3875277582556,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 278.3875277582556,
+ "hd15iqr": 278.3875277582556,
+ "ops": 0.0035921149487284994,
+ "total": 278.3875277582556,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "k562-dense-ovr",
+ "name": "test_speed_benchmark[k562-dense-20%-pdex-ovr-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[k562-dense-20%-pdex-ovr-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "k562",
+ "dense",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovr",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "k562-dense-20%-pdex-ovr-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 13896.134372888133,
+ "max": 13896.134372888133,
+ "mean": 13896.134372888133,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 13896.134372888133,
+ "iqr": 0.0,
+ "q1": 13896.134372888133,
+ "q3": 13896.134372888133,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 13896.134372888133,
+ "hd15iqr": 13896.134372888133,
+ "ops": 7.196245899514591e-05,
+ "total": 13896.134372888133,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "k562-csr-ovo",
+ "name": "test_speed_benchmark[k562-csr-20%-pdex-ovo-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[k562-csr-20%-pdex-ovo-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "k562",
+ "csr",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovo",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "k562-csr-20%-pdex-ovo-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 96.72045657783747,
+ "max": 96.72045657783747,
+ "mean": 96.72045657783747,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 96.72045657783747,
+ "iqr": 0.0,
+ "q1": 96.72045657783747,
+ "q3": 96.72045657783747,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 96.72045657783747,
+ "hd15iqr": 96.72045657783747,
+ "ops": 0.010339074435564028,
+ "total": 96.72045657783747,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "k562-csr-ovr",
+ "name": "test_speed_benchmark[k562-csr-20%-pdex-ovr-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[k562-csr-20%-pdex-ovr-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "k562",
+ "csr",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovr",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "k562-csr-20%-pdex-ovr-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 12970.162738580257,
+ "max": 12970.162738580257,
+ "mean": 12970.162738580257,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 12970.162738580257,
+ "iqr": 0.0,
+ "q1": 12970.162738580257,
+ "q3": 12970.162738580257,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 12970.162738580257,
+ "hd15iqr": 12970.162738580257,
+ "ops": 7.710003491517195e-05,
+ "total": 12970.162738580257,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "rpe1-dense-ovo",
+ "name": "test_speed_benchmark[rpe1-dense-20%-pdex-ovo-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[rpe1-dense-20%-pdex-ovo-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "rpe1",
+ "dense",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovo",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "rpe1-dense-20%-pdex-ovo-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 367.01337652653456,
+ "max": 367.01337652653456,
+ "mean": 367.01337652653456,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 367.01337652653456,
+ "iqr": 0.0,
+ "q1": 367.01337652653456,
+ "q3": 367.01337652653456,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 367.01337652653456,
+ "hd15iqr": 367.01337652653456,
+ "ops": 0.002724696329774513,
+ "total": 367.01337652653456,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "rpe1-dense-ovr",
+ "name": "test_speed_benchmark[rpe1-dense-20%-pdex-ovr-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[rpe1-dense-20%-pdex-ovr-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "rpe1",
+ "dense",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovr",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "rpe1-dense-20%-pdex-ovr-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 13846.857066752389,
+ "max": 13846.857066752389,
+ "mean": 13846.857066752389,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 13846.857066752389,
+ "iqr": 0.0,
+ "q1": 13846.857066752389,
+ "q3": 13846.857066752389,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 13846.857066752389,
+ "hd15iqr": 13846.857066752389,
+ "ops": 7.221855437513646e-05,
+ "total": 13846.857066752389,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "rpe1-csr-ovo",
+ "name": "test_speed_benchmark[rpe1-csr-20%-pdex-ovo-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[rpe1-csr-20%-pdex-ovo-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "rpe1",
+ "csr",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovo",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "rpe1-csr-20%-pdex-ovo-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 111.19303246960044,
+ "max": 111.19303246960044,
+ "mean": 111.19303246960044,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 111.19303246960044,
+ "iqr": 0.0,
+ "q1": 111.19303246960044,
+ "q3": 111.19303246960044,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 111.19303246960044,
+ "hd15iqr": 111.19303246960044,
+ "ops": 0.008993369258756339,
+ "total": 111.19303246960044,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "rpe1-csr-ovr",
+ "name": "test_speed_benchmark[rpe1-csr-20%-pdex-ovr-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[rpe1-csr-20%-pdex-ovr-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "rpe1",
+ "csr",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovr",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "rpe1-csr-20%-pdex-ovr-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 11848.103162704036,
+ "max": 11848.103162704036,
+ "mean": 11848.103162704036,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 11848.103162704036,
+ "iqr": 0.0,
+ "q1": 11848.103162704036,
+ "q3": 11848.103162704036,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 11848.103162704036,
+ "hd15iqr": 11848.103162704036,
+ "ops": 8.440169588899619e-05,
+ "total": 11848.103162704036,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "jurkat-dense-ovo",
+ "name": "test_speed_benchmark[jurkat-dense-20%-pdex-ovo-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[jurkat-dense-20%-pdex-ovo-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "jurkat",
+ "dense",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovo",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "jurkat-dense-20%-pdex-ovo-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 390.7197499424219,
+ "max": 390.7197499424219,
+ "mean": 390.7197499424219,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 390.7197499424219,
+ "iqr": 0.0,
+ "q1": 390.7197499424219,
+ "q3": 390.7197499424219,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 390.7197499424219,
+ "hd15iqr": 390.7197499424219,
+ "ops": 0.002559379197359141,
+ "total": 390.7197499424219,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "jurkat-dense-ovr",
+ "name": "test_speed_benchmark[jurkat-dense-20%-pdex-ovr-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[jurkat-dense-20%-pdex-ovr-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "jurkat",
+ "dense",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovr",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "jurkat-dense-20%-pdex-ovr-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 14462.085311977193,
+ "max": 14462.085311977193,
+ "mean": 14462.085311977193,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 14462.085311977193,
+ "iqr": 0.0,
+ "q1": 14462.085311977193,
+ "q3": 14462.085311977193,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 14462.085311977193,
+ "hd15iqr": 14462.085311977193,
+ "ops": 6.914632146249484e-05,
+ "total": 14462.085311977193,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "jurkat-csr-ovo",
+ "name": "test_speed_benchmark[jurkat-csr-20%-pdex-ovo-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[jurkat-csr-20%-pdex-ovo-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "jurkat",
+ "csr",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovo",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "jurkat-csr-20%-pdex-ovo-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 117.05236682482064,
+ "max": 117.05236682482064,
+ "mean": 117.05236682482064,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 117.05236682482064,
+ "iqr": 0.0,
+ "q1": 117.05236682482064,
+ "q3": 117.05236682482064,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 117.05236682482064,
+ "hd15iqr": 117.05236682482064,
+ "ops": 0.008543184790928573,
+ "total": 117.05236682482064,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "jurkat-csr-ovr",
+ "name": "test_speed_benchmark[jurkat-csr-20%-pdex-ovr-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[jurkat-csr-20%-pdex-ovr-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "jurkat",
+ "csr",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovr",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "jurkat-csr-20%-pdex-ovr-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 12244.574935900047,
+ "max": 12244.574935900047,
+ "mean": 12244.574935900047,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 12244.574935900047,
+ "iqr": 0.0,
+ "q1": 12244.574935900047,
+ "q3": 12244.574935900047,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 12244.574935900047,
+ "hd15iqr": 12244.574935900047,
+ "ops": 8.166882110934578e-05,
+ "total": 12244.574935900047,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "hepg2-dense-ovo",
+ "name": "test_speed_benchmark[hepg2-dense-20%-pdex-ovo-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[hepg2-dense-20%-pdex-ovo-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "hepg2",
+ "dense",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovo",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "hepg2-dense-20%-pdex-ovo-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 172.539394473657,
+ "max": 172.539394473657,
+ "mean": 172.539394473657,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 172.539394473657,
+ "iqr": 0.0,
+ "q1": 172.539394473657,
+ "q3": 172.539394473657,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 172.539394473657,
+ "hd15iqr": 172.539394473657,
+ "ops": 0.005795777845694701,
+ "total": 172.539394473657,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "hepg2-dense-ovr",
+ "name": "test_speed_benchmark[hepg2-dense-20%-pdex-ovr-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[hepg2-dense-20%-pdex-ovr-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "hepg2",
+ "dense",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovr",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "hepg2-dense-20%-pdex-ovr-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 8431.8718478363,
+ "max": 8431.8718478363,
+ "mean": 8431.8718478363,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 8431.8718478363,
+ "iqr": 0.0,
+ "q1": 8431.8718478363,
+ "q3": 8431.8718478363,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 8431.8718478363,
+ "hd15iqr": 8431.8718478363,
+ "ops": 0.00011859762791065303,
+ "total": 8431.8718478363,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "hepg2-csr-ovo",
+ "name": "test_speed_benchmark[hepg2-csr-20%-pdex-ovo-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[hepg2-csr-20%-pdex-ovo-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "hepg2",
+ "csr",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovo",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "hepg2-csr-20%-pdex-ovo-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 58.73078696243465,
+ "max": 58.73078696243465,
+ "mean": 58.73078696243465,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 58.73078696243465,
+ "iqr": 0.0,
+ "q1": 58.73078696243465,
+ "q3": 58.73078696243465,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 58.73078696243465,
+ "hd15iqr": 58.73078696243465,
+ "ops": 0.017026844892094148,
+ "total": 58.73078696243465,
+ "iterations": 1
+ }
+ },
+ {
+ "group": "hepg2-csr-ovr",
+ "name": "test_speed_benchmark[hepg2-csr-20%-pdex-ovr-nthreads=8-numba]",
+ "fullname": "tests/test_asymptotic_wilcoxon.py::test_speed_benchmark[hepg2-csr-20%-pdex-ovr-nthreads=8-numba]",
+ "params": {
+ "adata": [
+ "hepg2",
+ "csr",
+ 0.2
+ ],
+ "method": "pdex",
+ "test": "ovr",
+ "num_threads": 8,
+ "use_rust": false
+ },
+ "param": "hepg2-csr-20%-pdex-ovr-nthreads=8-numba",
+ "extra_info": {},
+ "options": {
+ "disable_gc": false,
+ "timer": "perf_counter",
+ "min_rounds": 5,
+ "max_time": 1.0,
+ "min_time": 5e-06,
+ "warmup": false
+ },
+ "stats": {
+ "min": 8050.4555141329765,
+ "max": 8050.4555141329765,
+ "mean": 8050.4555141329765,
+ "stddev": 0,
+ "rounds": 1,
+ "median": 8050.4555141329765,
+ "iqr": 0.0,
+ "q1": 8050.4555141329765,
+ "q3": 8050.4555141329765,
+ "iqr_outliers": 0,
+ "stddev_outliers": 0,
+ "outliers": "0;0",
+ "ld15iqr": 8050.4555141329765,
+ "hd15iqr": 8050.4555141329765,
+ "ops": 0.00012421657361430668,
+ "total": 8050.4555141329765,
+ "iterations": 1
+ }
+ }
+ ],
+ "datetime": "2026-03-15T13:47:08.982867+00:00",
+ "version": "5.2.3"
+}
diff --git a/.github/workflows/maturin-CI.yaml b/.github/workflows/maturin-CI.yaml
index e6b8479..43b15f0 100644
--- a/.github/workflows/maturin-CI.yaml
+++ b/.github/workflows/maturin-CI.yaml
@@ -179,3 +179,9 @@ jobs:
run: uv publish 'wheels-*/*'
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
+ - name: Upload wheels to GitHub Release
+ uses: softprops/action-gh-release@v1
+ with:
+ files: wheels-*/*
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/README.md b/README.md
index e0a47bd..121cdf1 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
# illico
`illico` is a python library performing fast and lightweight wilcoxon rank-sum tests (same as `scanpy.tl.rank_genes_groups(…, method="wilcoxon")`), useful for single-cell RNASeq data analyses and processing.
-Approximate speed benchmarks ran on k562-essential can be found below.
+Approximate speed benchmarks (done on a 8-CPUs, 1 GPU machine) ran on k562-essential (~300k cells, 8k genes, 2k perturbations) can be found below.
-| Test | Format | illico | scanpy | pdex |
-|----------------------------------|--------|--------|--------|------|
-| OVO (reference="non-targeting") | Dense | ~30s | ~1h | ~4h |
-| OVO (reference="non-targeting") | Sparse | ~30s | ~1h30 | ~4h |
-| OVR (reference=None) | Dense | ~30s | ~11h | X |
-| OVR (reference=None) | Sparse | ~30s | ~10h | X |
+| Test | Format | illico | scanpy | pdex | rapids-singlecell (GPU) |
+|----------------------------------|--------|--------|--------|------|------------------ |
+| OVO (reference="non-targeting") | Dense | ~20s | ~1h | ~20min | ~25min |
+| OVO (reference="non-targeting") | Sparse | ~15s | ~1h30min | ~8min | ~1h10min |
+| OVR (reference=None) | Dense | ~10s | >10h | >10h | ~1min |
+| OVR (reference=None) | Sparse | ~10s | >10h | >10h | ~1min |
## Installation
illico is compatible with python 3.11 and onward:
@@ -30,7 +30,10 @@ de_genes = asymptotic_wilcoxon(
group_keys="perturbation",
reference=["non-targeting"|None], # <- `None` computes cluster-wise DE genes. Any other `str` will be interpreted as label of the control cells.
is_log1p=[False|True], # <-- Specify if your data underwent log1p or not
+ return_as_scanpy=[False|True], # <-- Whether to return a dict compatible with Scanpy's `rank_genes_groups` function, or a pd.DataFrame holding all p-values, statistics, and fold-change
)
+# Eventually, if return_as_scanpy=True:
+adata.uns["rank_genes_groups"] = de_genes
```
## Release notes
@@ -312,5 +315,5 @@ The name *illico* is a wordplay inspired by the R package `presto` (now the Wilc
# Other tools available
1. `scanpy` also implements OVO and OVR asymptotic wilcoxon rank-sum tests.
-2. `pdex` only implements OVO wilcoxon rank-sum tests.
-3. As of December 2025, `rapids-singlecell` has a pending PR adding a `rank_genes_groups` feature. I could not benchmark this solution as I had no GPU available, but it is expected that it runs at least as fast as `illico`, because GPU-based.
+2. `pdex` also implements OVO and OVR wilcoxon rank-sum tests.
+3. As of March 2026, `rapids-singlecell` also implements OVO and OVR asymptotic wilcoxon rank-sum tests on GPU, with a focus on out-of-core datasets. If you are working with large datasets that do not fit in memory, you should check it out. For in-memory datasets, `illico` was benchmarked to be faster, even if CPU based.
diff --git a/assets/illico-scaling-rust.jpg b/assets/illico-scaling-rust.jpg
new file mode 100644
index 0000000..9de1cef
Binary files /dev/null and b/assets/illico-scaling-rust.jpg differ
diff --git a/assets/method-runtimes-comparison.png b/assets/method-runtimes-comparison.png
index 66b10e1..f190226 100644
Binary files a/assets/method-runtimes-comparison.png and b/assets/method-runtimes-comparison.png differ
diff --git a/changelog.md b/changelog.md
index c719cf4..7037559 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,6 +1,13 @@
Changelog
=========
+Version 0.4.0
+------------
+- Added option to return scanpy-friendly output with `return_as_scanpy` arg. `asymptotic_wilcoxon` returns either:
+ - A `pandas.DataFrame` with columns `feature`, `p_value`, `fold_change`, and `statistic` (default), if `return_as_scanpy=False`
+ - A dictionary containing the same keys as `scanpy.tl.rank_genes_groups`, if `return_as_scanpy=True`. Similarly as scanpy, genes are ordered by decreasing z-score.
+- Improved the batching mechanism, fixed the 'auto' mode that was excluding the very last gene in previous versions.
+
Version 0.3.0
------------
- Rust backend is available for all tests. Compare Rust vs Numba with `poetry run pytest-benchmark compare 0003 0005`:
diff --git a/docs/benchmarks.md b/docs/benchmarks.md
index 9108920..23d5d9f 100644
--- a/docs/benchmarks.md
+++ b/docs/benchmarks.md
@@ -2,13 +2,12 @@
## Benchmarking against other solutions
-In order for benchmarks to run in a reasonable amount of time, the timings reported below were obtained by running each solution on **a subset of each cell line** (20% of the genes). All solutions were find to scale linearly with the number of genes (columns in the adata). Extrapolating (x5) the elapsed times below will approximate runtime of those solutions on the whole datasets. Numbers in parenthesis report the multiplicative factor versus the fastest solution of each benchmark. A "benchmark" is defined by:
+A *benchmark* is defined by:
1. The cell line (K562 essential, RPE1, Hep-G2, Jurkat) used as input.
2. The data format (CSR, or dense) used to contain the expression matrix.
3. The test performed: OVO (`reference="non-targeting"`) or OVR (`reference=None`).
-💡 Keep in mind that `pdex` does not implement *OVR* test.
@@ -17,15 +16,9 @@ In order for benchmarks to run in a reasonable amount of time, the timings repor
## Scalability
-`illico` scales reasonably well with your compute budget. On the K562-essential dataset spanning 8 threads instead of 1 brings a 7-folds speedup.
-
-```bash
----------------------- benchmark 'k562-dense-ovo': 4 tests -----------------------
-Name (time in s) Mean
-----------------------------------------------------------------------------------
-test_speed_benchmark[k562-dense-100%-illico-ovo-nthreads=8] 29.6962 (1.0)
-test_speed_benchmark[k562-dense-100%-illico-ovo-nthreads=4] 53.4369 (1.80)
-test_speed_benchmark[k562-dense-100%-illico-ovo-nthreads=2] 100.3919 (3.38)
-test_speed_benchmark[k562-dense-100%-illico-ovo-nthreads=1] 208.2443 (7.01)
-----------------------------------------------------------------------------------
-```
+`illico` scales reasonably well with your compute budget, with a quasi-linear speedup up to 16 threads.
+
+
+
+ Throughput of illico with increasing compute budget, compared to a perfect scaling.
+
diff --git a/docs/normalization.md b/docs/normalization.md
index f8b0e4b..76ce632 100644
--- a/docs/normalization.md
+++ b/docs/normalization.md
@@ -1,4 +1,5 @@
# Normalization and log1p
1. `illico` does not care about your data being normalized or not, it is up to you to apply the preprocessing of your choice before running the tests. It is expected that `illico` is slower if ran on total-count normalized data by a factor ~2. This is because if applied on non total-count normalized data, sorting relies on radix sort which is faster than the usual quicksort (that is used if testing total-count normalized data).
-2. In order to avoid any unintended conversion, or relie on failure-prone rules of thumb, **`illico` requires the user to indicate if the input data is log1p or not**. This is only used to compute appropriate fold-change, and does not impact test (p-value and statistic) results.
+
+2. In order to avoid any unintended conversion, or rely on failure-prone rules of thumb, **`illico` requires the user to indicate if the input data is log1p or not**. This is only used to compute appropriate fold-change, and does not impact test (p-value and statistic) results. Note also that unlike scanpy, if data is log1p, fold-change is computed by exponentiating before aggregation by default. This behavior can be changed by passing `exp_post_agg=False` to `asymptotic_wilcoxon`. See the [results](#results) section for more details on that.
diff --git a/docs/overview.md b/docs/overview.md
index f2f5154..45befbf 100644
--- a/docs/overview.md
+++ b/docs/overview.md
@@ -2,7 +2,7 @@
*illico* is a python library performing blazing fast asymptotic wilcoxon rank-sum tests (same as `scanpy.tl.rank_genes_groups(…, method="wilcoxon")`), useful for single-cell RNASeq data analyses and processing. `illico`'s features are:
-1. 🚀 Blazing fast: On K562 (essential) dataset (~300k cells, 8k genes, 2k perturbations), `illico` computes DE genes (with `reference="non-targeting"`) in a mere 30 seconds. That's more than 100 times faster than both `pdex` or `scanpy` with the same compute ressources (8 CPUs).
+1. 🚀 Blazing fast: On K562 (essential) dataset (~300k cells, 8k genes, 2k perturbations), `illico` computes DE genes (with `reference="non-targeting"`) in a mere 20 seconds. That's more than 100 times faster than both `pdex` or `scanpy` with the same compute ressources (8 CPUs).
2. 💠 No compromise: on synthetic data, `illico`'s p-values matched `scipy.stats.mannwhitneyu` up to a relative difference of 1.e-12, and an absolute tolerance of 0.
3. ⚡ Thread-first: `illico` eventually parallelizes the processing (if specified by the user) over **threads**, never processes. This saves you from all the fixed cost of multiprocessing, such as spanning processes, duplicating data across processes, and communication costs.
4. 🐞 Data format agnostic: whether your data is dense, sparse along rows, or sparse along columns, `illico` will deal with it while never converting the whole data to whichever format is more optimized.
diff --git a/docs/results.md b/docs/results.md
index 991c9ba..919a9d0 100644
--- a/docs/results.md
+++ b/docs/results.md
@@ -18,12 +18,26 @@ The test suite implemented in the CI and used to develop `illico` targets a prec
### Fold-change
-The fold-change computed by illico is the most naive form of the fold-change:
+
+The fold-change computed by `illico` depends on the value of `is_log1p` and `exp_post_agg` as follows:
+| `is_log1p` | `exp_post_agg` | Fold-change equation | Remark |
+|---|---|---|---|
+| `False` | `True` | $\text{fold-change} = \frac{E[X_{\text{perturbed}}]}{E[X_{\text{control}}]}$ | |
+| `False` | `False` | $\text{fold-change} = \frac{E[X_{\text{perturbed}}]}{E[X_{\text{control}}]}$ | |
+| `True` | `True` | $\text{fold-change} = \frac{E[e^{X_{\text{perturbed}}} - 1]}{E[e^{X_{\text{control}}} - 1]}$ | 🎯 Scanpy's default |
+| `True` | `False` | $\text{fold-change} = \frac{e^{E[X_{\text{perturbed}}]} - 1}{e^{E[X_{\text{control}}]} - 1}$ | |
+
+⚠️ Please note that by default, `scanpy.rank_genes_groups` assumes that your data is log1p-transformed, and exponentiates after aggregation. Consequently, if you are coming from `scanpy` and want to drop-in replace `scanpy.tl.rank_genes_groups`, you should set:
+```python
+asymptotic_wilcoxon(adata, …, is_log1p=True, exp_post_agg=True)
+```
+
+
diff --git a/docs/usage.md b/docs/usage.md
index 8fdb2aa..601ba94 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -5,6 +5,7 @@ This library exposes one single function that returns a `pd.DataFrame` holding p
1. It is **required** to indicate if the data you run the tests on underwent log1p transform. This only impacts the fold-change calculation and not the test results (p-values, u-stats). The choice was made to not try to guess this information, as those often lead to error-prone and potentially harmful rules of thumb.
2. By default, `illico.asymptotic_wilcoxon` will use what lies in `adata.X` to compute DE genes. If you want a specific layer to be used to perform the tests, you must specify it.
3. By default again, `illico.asymptotic_wilcoxon` will apply continuity correction and tie correction factors. This is controllable with the `use_continuity` and `tie_correct` arguments.
+4. If you are coming from the `scanpy` ecosystem and want a drop-in replacement of `sc.tl.rank_genes_groups(…, method="wilcoxon")`, you can set `return_as_scanpy=True` when calling `illico.asymptotic_wilcoxon`. This will return a dictionary formatted for Scanpy's `rank_genes_groups` results, which you can then attach to `adata.uns["rank_genes_groups"]` and use with the rest of your Scanpy workflow as usual. See last section.
## DE genes compared to control cells
@@ -20,6 +21,7 @@ de_genes = asymptotic_wilcoxon(
group_keys="perturbation",
reference="non-targeting",
is_log1p=[False|True], # <-- Specify if your data underwent log1p or not
+ return_as_scanpy=[False|True], # <-- Whether to return a dict compatible with Scanpy's `rank_genes_groups` function, or a pd.DataFrame
)
```
@@ -38,3 +40,24 @@ de_genes = asymptotic_wilcoxon(adata, group_keys="cluster", reference=None, is_l
```
In this case, the resulting dataframe contains `n_clusters * n_genes` rows and the same three columns: `(p_value, statistic, fold_change)`. In this case, the wilcoxon rank-sum test is performed between cells belonging to cluster *c_i* and all the other cells (one-versus-the-rest), for all *c_i*.
+
+## Integrating with Scanpy
+Users coming from the `scanpy` ecosystem looking for a drop-in replacement of `sc.tl.rank_genes_groups(…, method="wilcoxon")` can set `return_as_scanpy=True` when calling `illico.asymptotic_wilcoxon`. This will return a dictionary formatted for Scanpy's `rank_genes_groups` results. Example:
+
+```python
+from illico import asymptotic_wilcoxon
+adata = ad.read_h5ad('dataset.h5ad') # (n_cells, n_genes)
+
+# ... Your preprocessing steps here ...
+
+de_genes = asymptotic_wilcoxon(
+ adata,
+ group_keys="perturbation",
+ reference="non-targeting",
+ is_log1p=[False|True], # <-- Specify if your data underwent log1p or not
+ return_as_scanpy=True, # <-- /!\
+ )
+adata.uns["rank_genes_groups"] = de_genes # Attach results to adata.uns
+# Then the rest of your scanpy workflow can remain unchanged, for example:
+sc.pl.rank_genes_groups(adata, sharey=False)
+```
diff --git a/illico/asymptotic_wilcoxon.py b/illico/asymptotic_wilcoxon.py
index c3db9a5..e27f4c9 100644
--- a/illico/asymptotic_wilcoxon.py
+++ b/illico/asymptotic_wilcoxon.py
@@ -1,4 +1,3 @@
-import math
from typing import Literal
import anndata as ad
@@ -6,11 +5,13 @@
import pandas as pd
from joblib import Parallel, delayed
from loguru import logger
+from numba import set_num_threads
from scipy import sparse
from tqdm.auto import tqdm
from illico.utils.compile import _precompile
from illico.utils.groups import GroupContainer, encode_and_count_groups
+from illico.utils.math import compute_batch_bounds
from illico.utils.memory import log_memory_usage
from illico.utils.ranking import check_indices_sorted_per_parcel
from illico.utils.registry import (
@@ -20,8 +21,7 @@
nb_dispatcher_registry,
rs_dispatcher_registry,
)
-
-# from illico.utils.math import _warn_log1p
+from illico.utils.scanpy import format_illico_results_for_scanpy
__all__ = ["asymptotic_wilcoxon"]
@@ -36,6 +36,7 @@ def operator(
use_continuity: bool,
alternative: str,
tie_correct: bool,
+ exp_post_agg: bool,
use_rust: bool,
results: np.ndarray,
):
@@ -61,13 +62,14 @@ def operator(
# Convert to numba-compatible format
X = data_handler.to_nb(fetched_data)
# Call the dispatcher
- pvalues, statistics, fold_change = dispatcher(
+ pvalues, statistics, zscores, fold_change = dispatcher(
X,
*bounds,
group_container,
is_log1p,
use_continuity,
tie_correct,
+ exp_post_agg,
alternative,
)
# Copy results into the shared array, do it thread-wise for cleaner garbage collection and speed
@@ -76,7 +78,8 @@ def operator(
# The copy should be GIL-free
results[:, lb:ub, 0] = pvalues
results[:, lb:ub, 1] = statistics
- results[:, lb:ub, 2] = fold_change # Technically Rust returns f32, but numpy handles casting here
+ results[:, lb:ub, 2] = zscores
+ results[:, lb:ub, 3] = fold_change # Technically Rust returns f32, but numpy handles casting here
return (lb, ub)
@@ -90,10 +93,13 @@ def asymptotic_wilcoxon(
alternative: str = "two-sided",
use_continuity: bool = True,
tie_correct: bool = True,
+ exp_post_agg: bool = False,
layer: str | None = None,
precompile: bool = True,
use_rust: bool = True,
-):
+ return_as_scanpy: bool = False,
+ corr_method: Literal["benjamini-hochberg", "bonferroni"] = "benjamini-hochberg",
+) -> pd.DataFrame | dict:
"""Perform asymptotic Mann-Whitney tests for differential gene expression.
Mann-Whitney test is the same as Wilcoxon rank-sum test.
@@ -125,20 +131,38 @@ def asymptotic_wilcoxon(
Whether to apply continuity correction.
tie_correct : bool, default=True
Whether to apply tie correction in the test statistic.
+ exp_post_agg : bool, default=False
+ Whether to exponentiate the fold change after aggregation. This is relevant if the input data is log1p. See documentation for details.
+ Note that `scanpy.rank_genes_groups` assumes the data to be log1p, and exponentiates post aggregation by default.
layer : str or None, default=None
Layer in `adata.layers` to use for the data. If `None`, uses `adata.X`.
precompile : bool, default=True
Whether to precompile necessary functions for performance. It is recommended to set this to `True`.
use_rust : bool, default=True
Whether to use the Rust implementation of the test. If `False`, uses the Numba implementation.
+ return_as_scanpy : bool, default=False
+ Whether to return results in a format compatible with Scanpy's `rank_genes_groups` function.
+ If yes, the output is a dictionary that can be attached to the `adata` object like this:
+ `adata.uns['rank_genes_groups'] = asymptotic_wilcoxon(..., return_as_scanpy=True)`
+ corr_method: str, default="benjamini-hochberg"
+ Method to use for multiple testing correction. One of 'benjamini-hochberg' or 'bonferroni'.
+
Returns
-------
- pd.DataFrame
+ Either one of pd.DataFrame or Dict, depending on the value of `return_as_scanpy`:
A DataFrame with MultiIndex (pert, feature) containing columns:
- 'p_value': P-value from the Mann-Whitney test
- 'statistic': Test statistic (U-statistic)
+ - 'z-scores': Test z-score
- 'fold_change': Fold change between groups
+ Or a dictionary formatted for Scanpy's `rank_genes_groups` results, containing:
+ - 'params': Dictionary of parameters used for the test
+ - 'names': Record array of gene names sorted by significance for each group
+ - 'scores': Record array of test statistics sorted by significance for each group
+ - 'pvals': Record array of p-values sorted by significance for each group
+ - 'pvals_adj': Record array of adjusted p-values sorted by significance for each group
+ - 'logfoldchanges': Record array of log2 fold changes sorted by significance for each group
Raises
------
@@ -200,6 +224,7 @@ def asymptotic_wilcoxon(
# Check that the input CSR is sorted.
if isinstance(X, sparse.csr_matrix):
+ set_num_threads(n_threads) # Set the number of threads for Numba to use in the check function
if not check_indices_sorted_per_parcel(X.indices, X.indptr):
raise ValueError(
"Input data matrix indices are not sorted. This is very unusual and may lead to incorrect results. "
@@ -227,25 +252,11 @@ def asymptotic_wilcoxon(
# Allocate the results dataframes
cols = pd.Series(adata.var_names, name="feature", dtype=str)
rows = pd.Series(unique_raw_groups, name="pert", dtype=str)
- results = np.empty((len(rows), len(cols), 3), dtype=np.float64)
+ results = np.empty((len(rows), len(cols), 4), dtype=np.float64)
- # Adapt batch size to leverage multithreading regarding the number of genes, if requested
- if n_genes < 256:
- batch_size = n_genes # No batching for small number of genes
- n_threads = 1 # No multithreading for small number of genes
- iterator = [[0, n_genes]]
- elif isinstance(batch_size, int):
- batch_size = min(batch_size, math.ceil(n_genes / n_threads))
- bounds = np.append(np.arange(0, n_genes, batch_size), n_genes)
- iterator = list(zip(bounds[:-1], bounds[1:]))
- elif batch_size == "auto":
- n_dispatches = max(int(n_genes / 256 / n_threads), 1) # Aim for approximately 256 genes per chunk
- splits = np.array_split(np.arange(n_genes + 1), indices_or_sections=n_threads * n_dispatches)
- iterator = [split[[0, -1]] for split in splits]
- batch_size = int(np.ceil(n_genes / (n_dispatches * n_threads)))
- else:
- raise ValueError(f"Invalid batch_size value: {batch_size}. Must be 'auto' or an integer.")
- logger.trace(f"Using batch size of {batch_size} for {n_threads} threads and {n_genes} genes.")
+ # Compute the batch bounds for each thread
+ iterator, batch_size = compute_batch_bounds(n_genes, batch_size, n_threads)
+ logger.trace(f"Processing {n_genes} genes through {len(iterator)} batches with {n_threads} threads.")
# Compute estimated mem footprint
_ = log_memory_usage(data_handler, group_container, batch_size, n_threads)
@@ -265,6 +276,7 @@ def asymptotic_wilcoxon(
use_continuity,
alternative,
tie_correct,
+ exp_post_agg,
use_rust,
results,
)
@@ -272,11 +284,22 @@ def asymptotic_wilcoxon(
):
pbar.update(group_container.counts.size * (ub - lb))
+ if not return_as_scanpy:
# Return a pd.DataFrame to index results
results = pd.DataFrame(
- data=results.reshape(-1, 3),
+ data=results.reshape(-1, 4),
index=pd.MultiIndex.from_product([rows, cols], names=["pert", "feature"]),
- columns=["p_value", "statistic", "fold_change"],
+ columns=["p_value", "statistic", "z_score", "fold_change"],
+ )
+ else:
+ # Return a dict formatted for Scanpy's rank_genes_groups results
+ results = format_illico_results_for_scanpy(
+ adata=adata,
+ reference=reference,
+ group_keys=group_keys,
+ layer=layer,
+ values=results,
+ corr_method=corr_method,
)
return results
diff --git a/illico/ovo/dense_ovo.py b/illico/ovo/dense_ovo.py
index 95afd0b..e31aeab 100644
--- a/illico/ovo/dense_ovo.py
+++ b/illico/ovo/dense_ovo.py
@@ -30,7 +30,7 @@ def dense_ovo_mwu_kernel(
alternative (Literal["two-sided", "less", "greater"]): Type of alternative hypothesis
Returns:
- tuple[np.ndarray]: two-sided p-values, U-statistics. Each of shape (n_genes,).
+ tuple[np.ndarray]: two-sided p-values, U-statistics, z-scores. Each of shape (n_genes,).
Author: Rémy Dubois
"""
@@ -39,15 +39,16 @@ def dense_ovo_mwu_kernel(
U_statistics = np.empty(ncols, dtype=np.float64)
pvals = np.empty(ncols, dtype=np.float64)
+ zscores = np.empty(ncols, dtype=np.float64)
n = n_ref + n_tgt
mu = n_ref * n_tgt / 2.0
for j in range(ncols):
- R1, tie_sum = rank_sum_and_ties_from_sorted(sorted_ref_data[:, j], sorted_tgt_data[:, j])
+ ranksum, tie_sum = rank_sum_and_ties_from_sorted(sorted_ref_data[:, j], sorted_tgt_data[:, j])
# Compute U-stat
- U1 = n_ref * n_tgt + n_tgt * (n_tgt + 1) / 2 - R1
+ U1 = ranksum - n_tgt * (n_tgt + 1) / 2.0
- pvals[j] = compute_pval(
+ pvals[j], zscores[j] = compute_pval(
n_ref=n_ref,
n_tgt=n_tgt,
n=n,
@@ -59,7 +60,7 @@ def dense_ovo_mwu_kernel(
)
U_statistics[j] = U1
- return pvals, U_statistics
+ return pvals, U_statistics, zscores
@nb_dispatcher_registry.register(Test.OVO, KernelDataFormat.DENSE)
@@ -72,6 +73,7 @@ def dense_ovo_mwu_kernel_over_contiguous_col_chunk(
is_log1p: bool,
use_continuity: bool = True,
tie_correct: bool = True,
+ exp_post_agg: bool = False,
alternative: Literal["two-sided", "less", "greater"] = "two-sided",
) -> tuple[np.ndarray]:
"""Perform OVO tests group-wise and gene(col)-wise.
@@ -93,6 +95,7 @@ def dense_ovo_mwu_kernel_over_contiguous_col_chunk(
grpc (GroupContainer): GroupContainer, contains information about which group each row belongs to.
use_continuity (bool, optional): Apply continuity factor or not. Defaults to True.
tie_correct (bool, optional): Whether to apply tie correction when computing p-values. Defaults to True.
+ exp_post_agg (bool, optional): Whether to exponentiate the fold change after aggregation. This is relevant if the input data is log1p. See documentation for details. Note that `scanpy.rank_genes_groups` assumes the data to be log1p, and exponentiates post aggregation by default. Defaults to False.
alternative (Literal["two-sided", "less", "greater"]): Type of alternative hypothesis
is_log1p (bool, optional): User-indicated flag telling if data underwent log1p transform or not. Defaults to False.
@@ -100,7 +103,7 @@ def dense_ovo_mwu_kernel_over_contiguous_col_chunk(
ValueError: If bounds are not intelligible.
Returns:
- tuple[np.ndarray]: two-sided p-values, U-statistics, fold change. Each
+ tuple[np.ndarray]: two-sided p-values, U-statistics, z-scores, fold change. Each
of shape (n_groups, chunk_ub - chunk_lb).
Author: Rémy Dubois
@@ -114,16 +117,20 @@ def dense_ovo_mwu_kernel_over_contiguous_col_chunk(
_sort_along_axis_inplace(ref_chunk, axis=0)
pvalues = np.empty((n_groups, chunk_ub - chunk_lb), dtype=np.float64)
+ zscores = np.empty((n_groups, chunk_ub - chunk_lb), dtype=np.float64)
statistics = np.empty((n_groups, chunk_ub - chunk_lb), dtype=np.float64)
for group_id in range(n_groups):
if group_id == grpc.encoded_ref_group:
+ pvalues[group_id, :] = 1.0
+ zscores[group_id, :] = 0.0
+ statistics[group_id, :] = -1.0
continue
tgt_indices = grpc.indices[grpc.indptr[group_id] : grpc.indptr[group_id + 1]]
# tgt_chunk = np.asfortranarray(chunk[tgt_indices, :])
tgt_chunk = chunk_and_fortranize(X, chunk_lb, chunk_ub, tgt_indices)
_sort_along_axis_inplace(tgt_chunk, axis=0)
- pvalues[group_id], statistics[group_id] = dense_ovo_mwu_kernel(
+ pvalues[group_id], statistics[group_id], zscores[group_id] = dense_ovo_mwu_kernel(
sorted_ref_data=ref_chunk,
sorted_tgt_data=tgt_chunk,
use_continuity=use_continuity,
@@ -132,6 +139,6 @@ def dense_ovo_mwu_kernel_over_contiguous_col_chunk(
)
# Compute fold change
- fc = dense_fold_change(chunk, grpc, is_log1p=is_log1p)
+ fc = dense_fold_change(chunk, grpc, is_log1p=is_log1p, exp_post_agg=exp_post_agg)
- return pvalues, statistics, fc
+ return pvalues, statistics, zscores, fc
diff --git a/illico/ovo/sparse_ovo.py b/illico/ovo/sparse_ovo.py
index dac71ac..80989eb 100644
--- a/illico/ovo/sparse_ovo.py
+++ b/illico/ovo/sparse_ovo.py
@@ -43,7 +43,7 @@ def single_group_sparse_ovo_mwu_kernel(
ValueError: If shape mismatche
Returns:
- tuple[np.ndarray]: two-sided p-values, U-statistics. Each of shape (n_genes,).
+ tuple[np.ndarray]: two-sided p-values, U-statistics, zscores. Each of shape (n_genes,).
Author: Rémy Dubois
"""
@@ -59,6 +59,7 @@ def single_group_sparse_ovo_mwu_kernel(
n_zeros_ref = (n_ref - diff(sorted_ref_data.indptr)).astype(np.int64)
U_statistics = np.empty(n_cols_ref, dtype=np.float64)
pvals = np.empty(n_cols_ref, dtype=np.float64)
+ zscores = np.empty(n_cols_ref, dtype=np.float64)
n = n_ref + n_tgt
mu = n_ref * n_tgt / 2.0
for j in range(n_cols_ref):
@@ -79,11 +80,11 @@ def single_group_sparse_ovo_mwu_kernel(
R1 = R1_nz + n0 * (n_zeros_ref[j] + n0 + 1) / 2.0 # Add sumranks of zeros
# Compute U-stat
- U1 = n_ref * n_tgt + n_tgt * (n_tgt + 1) / 2 - R1
+ U1 = R1 - n_tgt * (n_tgt + 1) / 2
# Compute sigma
tie_sum += n_zeros_combined**3 - n_zeros_combined
- pvals[j] = compute_pval(
+ pvals[j], zscores[j] = compute_pval(
n_ref=n_ref,
n_tgt=n_tgt,
n=n,
@@ -97,7 +98,7 @@ def single_group_sparse_ovo_mwu_kernel(
# Regardless of the alternative, always return U1 like scipy
U_statistics[j] = U1
- return pvals, U_statistics
+ return pvals, U_statistics, zscores
@njit(nogil=True, fastmath=True, cache=False)
@@ -120,7 +121,7 @@ def multi_group_sparse_ovo_mwu_kernel(
alternative (Literal["two-sided", "less", "greater"]): Type of alternative hypothesis
Returns:
- tuple[np.ndarray]: two-sided p-values, U-statistics. Each of shape (n_groups, n_genes).
+ tuple[np.ndarray]: two-sided p-values, U-statistics, zscores. Each of shape (n_groups, n_genes).
Author: Rémy Dubois
"""
@@ -135,17 +136,19 @@ def multi_group_sparse_ovo_mwu_kernel(
# Now go through all the groups one by one
pvalues = np.empty((n_groups, X.shape[1]), dtype=np.float64)
+ zscores = np.empty((n_groups, X.shape[1]), dtype=np.float64)
statistics = np.empty((n_groups, X.shape[1]), dtype=np.float64)
for group_id in range(group_indptr.size - 1):
if group_id == ref_group_id:
pvalues[group_id, :] = 1.0
+ zscores[group_id, :] = 0.0
statistics[group_id, :] = -1.0
continue
tgt_idxs = group_indices[group_indptr[group_id] : group_indptr[group_id + 1]]
X_tgt = csr_get_rows_into_csc(X, tgt_idxs)
_sort_csc_columns_inplace(X_tgt)
- pvalue, statistic = single_group_sparse_ovo_mwu_kernel(
+ pvalue, statistic, zscore = single_group_sparse_ovo_mwu_kernel(
sorted_ref_data=csc_X_ref,
sorted_tgt_data=X_tgt,
use_continuity=use_continuity,
@@ -154,8 +157,9 @@ def multi_group_sparse_ovo_mwu_kernel(
)
pvalues[group_id, :] = pvalue
statistics[group_id, :] = statistic
+ zscores[group_id, :] = zscore
- return pvalues, statistics
+ return pvalues, statistics, zscores
# Not jitting this and sorting all the cells at once is 1.5x slower. Ideally, it would be faster to sort only groups one by one but
@@ -170,6 +174,7 @@ def csc_ovo_mwu_kernel_over_contiguous_col_chunk(
is_log1p: bool,
use_continuity: bool = True,
tie_correct: bool = True,
+ exp_post_agg: bool = False,
alternative: Literal["two-sided", "less", "greater"] = "two-sided",
):
"""Perform OVO test over contiguous chunk of column of a CSC matrix.
@@ -182,20 +187,21 @@ def csc_ovo_mwu_kernel_over_contiguous_col_chunk(
is_log1p (bool): User-indicated flag telling if data underwent log1p transform.
use_continuity (bool): Whether to use continuity correction when computing p-values.
tie_correct (bool): Whether to apply tie correction when computing p-values.
+ exp_post_agg (bool, optional): Whether to exponentiate the fold change after aggregation. This is relevant if the input data is log1p. See documentation for details. Note that `scanpy.rank_genes_groups` assumes the data to be log1p, and exponentiates post aggregation by default. Defaults to False.
alternative (Literal["two-sided", "less", "greater"]): Type of alternative hypothesis
Raises:
ValueError: If chunk bounds are inintelligible.
Returns:
- tuple[np.ndarray]: two-sided p-values, U-statistics, fold change. Each
+ tuple[np.ndarray]: two-sided p-values, U-statistics, z-scores, fold change. Each
of shape (n_groups, chunk_ub - chunk_lb).
Author: Rémy Dubois
"""
# This copies the data, so all that follow can happen in-place
csr_chunk = csc_get_contig_cols_into_csr(X, chunk_lb, chunk_ub)
- pvalues, statistics = multi_group_sparse_ovo_mwu_kernel(
+ pvalues, statistics, zscores = multi_group_sparse_ovo_mwu_kernel(
X=csr_chunk,
grpc=grpc,
ref_group_id=grpc.encoded_ref_group,
@@ -205,9 +211,9 @@ def csc_ovo_mwu_kernel_over_contiguous_col_chunk(
)
# Compute fold change
- fold_change = csr_fold_change(csr_chunk, grpc, is_log1p=is_log1p)
+ fold_change = csr_fold_change(csr_chunk, grpc, is_log1p=is_log1p, exp_post_agg=exp_post_agg)
- return pvalues, statistics, fold_change
+ return pvalues, statistics, zscores, fold_change
# Real scale tests on whole H1 showed 24secs on 8 threads and 2min45s on 1, so a speedup of 165 / 24 = 6.875x
@@ -221,6 +227,7 @@ def csr_ovo_mwu_kernel_over_contiguous_col_chunk(
is_log1p: bool,
use_continuity: bool = True,
tie_correct: bool = True,
+ exp_post_agg: bool = False,
alternative: Literal["two-sided", "less", "greater"] = "two-sided",
):
"""Perform OVO test over contiguous chunk of column of a CSR matrix.
@@ -233,20 +240,21 @@ def csr_ovo_mwu_kernel_over_contiguous_col_chunk(
is_log1p (bool): User-indicated flag telling if data underwent log1p transform.
use_continuity (bool): Whether to use continuity correction when computing p-values.
tie_correct (bool): Whether to apply tie correction when computing p-values.
+ exp_post_agg (bool, optional): Whether to exponentiate the fold change after aggregation. This is relevant if the input data is log1p. See documentation for details. Note that `scanpy.rank_genes_groups` assumes the data to be log1p, and exponentiates post aggregation by default. Defaults to False.
alternative (Literal["two-sided", "less", "greater"]): Type of alternative hypothesis
Raises:
ValueError: If chunk bounds are inintelligible.
Returns:
- tuple[np.ndarray]: two-sided p-values, U-statistics, fold change. Each
+ tuple[np.ndarray]: two-sided p-values, U-statistics, z-scores, fold change. Each
of shape (n_groups, chunk_ub - chunk_lb).
Author: Rémy Dubois
"""
# This copies the data, so all that follow can happen in-place
csr_chunk = csr_get_contig_cols_into_csr(X, chunk_lb, chunk_ub)
- pvalues, statistics = multi_group_sparse_ovo_mwu_kernel(
+ pvalues, statistics, zscores = multi_group_sparse_ovo_mwu_kernel(
X=csr_chunk,
grpc=grpc,
ref_group_id=grpc.encoded_ref_group,
@@ -255,6 +263,6 @@ def csr_ovo_mwu_kernel_over_contiguous_col_chunk(
alternative=alternative,
)
# Compute fold change
- fold_change = csr_fold_change(csr_chunk, grpc, is_log1p=is_log1p)
+ fold_change = csr_fold_change(csr_chunk, grpc, is_log1p=is_log1p, exp_post_agg=exp_post_agg)
- return pvalues, statistics, fold_change
+ return pvalues, statistics, zscores, fold_change
diff --git a/illico/ovr/dense_ovr.py b/illico/ovr/dense_ovr.py
index 223896e..6cc0dd8 100644
--- a/illico/ovr/dense_ovr.py
+++ b/illico/ovr/dense_ovr.py
@@ -22,6 +22,7 @@ def dense_ovr_mwu_kernel_over_contiguous_col_chunk(
is_log1p: bool,
use_continuity: bool = True,
tie_correct: bool = True,
+ exp_post_agg: bool = False,
alternative: Literal["two-sided", "less", "greater"] = "two-sided",
) -> tuple[np.ndarray]:
"""Compute OVR ranksum test on a dense matrix of expression counts.
@@ -34,10 +35,11 @@ def dense_ovr_mwu_kernel_over_contiguous_col_chunk(
transformation or not. Defaults to False.
use_continuity (bool, optional): Whether to use continuity correction when computing p-values. Defaults to True.
tie_correct (bool, optional): Whether to apply tie correction when computing p-values. Defaults to True.
+ exp_post_agg (bool, optional): Whether to exponentiate the fold change after aggregation. This is relevant if the input data is log1p. See documentation for details. Note that `scanpy.rank_genes_groups` assumes the data to be log1p, and exponentiates post aggregation by default. Defaults to False.
alternative (Literal["two-sided", "less", "greater"]): Type of alternative hypothesis. Defaults to "two-sided".
Returns:
- tuple[np.ndarray]: Two-sided p-values, U-statistic and fold change.
+ tuple[np.ndarray]: Two-sided p-values, U-statistic, z-scores and fold change.
Each np.ndarray of shape (n_groups, n_genes)
Author: Rémy Dubois
@@ -57,13 +59,14 @@ def dense_ovr_mwu_kernel_over_contiguous_col_chunk(
n = chunk.shape[0]
n_ref = np.expand_dims(n - grpc.counts, -1) # (g, 1)
n_tgt = np.expand_dims(grpc.counts, -1) # (g, 1)
- statistics = (n_ref * n_tgt + n_tgt * (n_tgt + 1) / 2) - ranksums
+ statistics = ranksums - n_tgt * (n_tgt + 1) / 2
mu = n_ref * n_tgt / 2.0
# Compute pvals
pvals = np.empty(shape=(grpc.counts.size, chunk.shape[1]), dtype=np.float64)
+ zscores = np.empty(shape=(grpc.counts.size, chunk.shape[1]), dtype=np.float64)
for j in range(chunk.shape[1]):
for k in range(grpc.counts.size):
- pvals[k, j] = compute_pval(
+ pvals[k, j], zscores[k, j] = compute_pval(
n_ref=n_ref[k, 0],
n_tgt=n_tgt[k, 0],
n=n,
@@ -75,6 +78,6 @@ def dense_ovr_mwu_kernel_over_contiguous_col_chunk(
)
# Get fold change
- fold_change = dense_fold_change(chunk, grpc=grpc, is_log1p=is_log1p)
+ fold_change = dense_fold_change(chunk, grpc=grpc, is_log1p=is_log1p, exp_post_agg=exp_post_agg)
- return pvals, statistics, fold_change
+ return pvals, statistics, zscores, fold_change
diff --git a/illico/ovr/sparse_ovr.py b/illico/ovr/sparse_ovr.py
index 74001fc..1e7e8b6 100644
--- a/illico/ovr/sparse_ovr.py
+++ b/illico/ovr/sparse_ovr.py
@@ -40,7 +40,7 @@ def sparse_ovr_mwu_kernel(
alternative (Literal["two-sided", "less", "greater"]): Type of alternative hypothesis. Defaults to "two-sided".
Returns:
- tuple[np.ndarray]: Two-sided p-values and U-statistics, per group and per gene (column).
+ tuple[np.ndarray]: Two-sided p-values, U-statistics and z-scores, per group and per gene (column).
Author: Rémy Dubois
"""
@@ -50,6 +50,7 @@ def sparse_ovr_mwu_kernel(
# Allocate placeholders for U stats and pvals
U = np.empty((group_counts.size, n_cols), dtype=np.float64)
pvals = np.empty((group_counts.size, n_cols), dtype=np.float64)
+ zscores = np.empty((group_counts.size, n_cols), dtype=np.float64)
# Note that because this function does not involve inner parallelism, this could be allocated per-col, but I find it cleaner this way
nnz_per_group = np.zeros((group_counts.size, n_cols), dtype=np.float64)
@@ -79,11 +80,11 @@ def sparse_ovr_mwu_kernel(
""" Step 3: Add ranksums of zero elements, per group"""
# add zero contribution: number of zeros * avg rank
R1 = R1_nz[:, j] + nz_per_group * (n0 + 1) / 2.0
- U[:, j] = n_ref * n_tgt + n_tgt * (n_tgt + 1) / 2 - R1
+ U[:, j] = R1 - n_tgt * (n_tgt + 1) / 2
tie_sum += n0**3 - n0
for k in range(group_counts.size):
- pvals[k, j] = compute_pval(
+ pvals[k, j], zscores[k, j] = compute_pval(
n_ref=n_ref[k],
n_tgt=n_tgt[k],
n=n,
@@ -94,7 +95,7 @@ def sparse_ovr_mwu_kernel(
alternative=alternative,
)
- return pvals, U
+ return pvals, U, zscores
@nb_dispatcher_registry.register(Test.OVR, KernelDataFormat.CSC)
@@ -107,6 +108,7 @@ def csc_ovr_mwu_kernel_over_contiguous_col_chunk(
is_log1p: bool,
use_continuity: bool = True,
tie_correct: bool = True,
+ exp_post_agg: bool = False,
alternative: Literal["two-sided", "less", "greater"] = "two-sided",
):
"""Perform OVR ranksum test over the contiguous column chunk defined by the bounds.
@@ -121,13 +123,14 @@ def csc_ovr_mwu_kernel_over_contiguous_col_chunk(
is_log1p (bool): User-indicated flag telling if data was log1p transformed or not.
use_continuity (bool): Whether to use continuity correction when computing p-values. Defaults to True.
tie_correct (bool): Whether to apply tie correction when computing p-values. Defaults to True.
+ exp_post_agg (bool, optional): Whether to exponentiate the fold change after aggregation. This is relevant if the input data is log1p. See documentation for details. Note that `scanpy.rank_genes_groups` assumes the data to be log1p, and exponentiates post aggregation by default. Defaults to False.
alternative (Literal["two-sided", "less", "greater"]): Type of alternative hypothesis.
Raises:
ValueError: If bounds are not intelligible
Returns:
- tuple[np.ndarray]: two-sided p-values, u-statistics and fold changes,
+ tuple[np.ndarray]: two-sided p-values, u-statistics, z-scores and fold changes,
each of shape (n_groups, chunk_lb - chunk_ub).
Author: Rémy Dubois
@@ -142,7 +145,7 @@ def csc_ovr_mwu_kernel_over_contiguous_col_chunk(
# for j in range(csc_chunk.shape[1]):
# start, end = csc_chunk.indptr[j], csc_chunk.indptr[j + 1]
# idxs[start:end] = np.argsort(csc_chunk.data[start:end])
- pvalues, statistics = sparse_ovr_mwu_kernel(
+ pvalues, statistics, zscores = sparse_ovr_mwu_kernel(
X=csc_chunk,
groups=grpc.encoded_groups,
group_counts=grpc.counts,
@@ -151,8 +154,8 @@ def csc_ovr_mwu_kernel_over_contiguous_col_chunk(
alternative=alternative,
)
- fold_change = csc_fold_change(X=csc_chunk, grpc=grpc, is_log1p=is_log1p)
- return pvalues, statistics, fold_change
+ fold_change = csc_fold_change(X=csc_chunk, grpc=grpc, is_log1p=is_log1p, exp_post_agg=exp_post_agg)
+ return pvalues, statistics, zscores, fold_change
@nb_dispatcher_registry.register(Test.OVR, KernelDataFormat.CSR)
@@ -165,6 +168,7 @@ def csr_ovr_mwu_kernel_over_contiguous_col_chunk(
is_log1p: bool,
use_continuity: bool = True,
tie_correct: bool = True,
+ exp_post_agg: bool = False,
alternative: Literal["two-sided", "less", "greater"] = "two-sided",
) -> tuple[np.ndarray]:
"""Perform OVR ranksum test over the contiguous column chunk defined by the bounds.
@@ -179,13 +183,14 @@ def csr_ovr_mwu_kernel_over_contiguous_col_chunk(
is_log1p (bool): User-indicated flag telling if data was log1p transformed or not.
use_continuity (bool): Whether to use continuity correction when computing p-values.
tie_correct (bool): Whether to apply tie correction when computing p-values.
+ exp_post_agg (bool, optional): Whether to exponentiate the fold change after aggregation. This is relevant if the input data is log1p. See documentation for details. Note that `scanpy.rank_genes_groups` assumes the data to be log1p, and exponentiates post aggregation by default. Defaults to False.
alternative (Literal["two-sided", "less", "greater"]): Type of alternative hypothesis
Raises:
ValueError: If bounds are not intelligible
Returns:
- tuple[np.ndarray]: two-sided p-values, u-statistics and fold changes,
+ tuple[np.ndarray]: two-sided p-values, u-statistics, z-scores and fold changes,
each of shape (n_groups, chunk_lb - chunk_ub).
Author: Rémy Dubois
@@ -195,7 +200,7 @@ def csr_ovr_mwu_kernel_over_contiguous_col_chunk(
csc_chunk = csr_get_contig_cols_into_csc(csr_matrix=X, chunk_lb=chunk_lb, chunk_ub=chunk_ub)
# TODO: same remark as csc regarding sorting
- pvalues, statistics = sparse_ovr_mwu_kernel(
+ pvalues, statistics, zscores = sparse_ovr_mwu_kernel(
X=csc_chunk,
groups=grpc.encoded_groups,
group_counts=grpc.counts,
@@ -203,6 +208,6 @@ def csr_ovr_mwu_kernel_over_contiguous_col_chunk(
tie_correct=tie_correct,
alternative=alternative,
)
- fold_change = csc_fold_change(X=csc_chunk, grpc=grpc, is_log1p=is_log1p)
+ fold_change = csc_fold_change(X=csc_chunk, grpc=grpc, is_log1p=is_log1p, exp_post_agg=exp_post_agg)
- return pvalues, statistics, fold_change
+ return pvalues, statistics, zscores, fold_change
diff --git a/illico/utils/compile.py b/illico/utils/compile.py
index 1457d77..541dee6 100644
--- a/illico/utils/compile.py
+++ b/illico/utils/compile.py
@@ -42,10 +42,11 @@ def _precompile(data_handler: DataHandler, reference: Any | None):
types.boolean,
types.boolean,
types.boolean,
+ types.boolean,
types.string,
)
- # This is the output: three float64 2D arrays
- out_sig = types.UniTuple(types.float64[:, ::1], 3)
+ # This is the output: four float64 2D arrays
+ out_sig = types.UniTuple(types.float64[:, ::1], 4)
input_type = data_handler.input_signature()
if reference is None:
diff --git a/illico/utils/math.py b/illico/utils/math.py
index f88c82a..70ad37b 100644
--- a/illico/utils/math.py
+++ b/illico/utils/math.py
@@ -2,7 +2,7 @@
import math
import warnings
-from typing import Literal
+from typing import List, Literal, Tuple
import numpy as np
from numba import njit
@@ -73,7 +73,7 @@ def compute_pval(
mu: float,
contin_corr: float = 0.0,
alternative: Literal["two-sided", "less", "greater"] = "two-sided",
-) -> float:
+) -> tuple[float, float]:
"""Compute p-value.
This small piece of code was isolated here because it was duplicated in the
@@ -90,7 +90,7 @@ def compute_pval(
alternative (Literal["two-sided", "less", "greater"]): Type of alternative hypothesis.
Returns:
- float: P-value
+ tuple[float]: P-value and z-score
Author: Rémy Dubois
"""
@@ -100,24 +100,23 @@ def compute_pval(
if alternative == "two-sided": # two-sided
# Compute both-sided statistic
- min_u = min(U, n_ref * n_tgt - U)
- delta = min_u - mu
- z = (np.abs(delta) + np.sign(delta) * contin_corr) / sigma
- return math.erfc(z / math.sqrt(2.0))
+ delta = U - mu
+ z = (delta - np.sign(delta) * contin_corr) / sigma
+ return math.erfc(abs(z) / math.sqrt(2.0)), z
elif alternative == "greater": # greater (right-tailed)
delta = U - mu
z = (delta - contin_corr) / sigma
# P(Z > z) = 0.5 * erfc(z / sqrt(2))
- return 0.5 * math.erfc(z / math.sqrt(2.0))
+ return 0.5 * math.erfc(z / math.sqrt(2.0)), z
elif alternative == "less": # less (left-tailed)
delta = U - mu
z = (delta + contin_corr) / sigma
# P(Z < z) = 0.5 * erfc(-z / sqrt(2))
- return 0.5 * math.erfc(-z / math.sqrt(2.0))
+ return 0.5 * math.erfc(-z / math.sqrt(2.0)), z
else:
raise ValueError(f"Unsupported alternative hypothesis: {alternative}")
else:
- return 1.0
+ return 1.0, 0.0
@njit(nogil=True, cache=False)
@@ -168,12 +167,13 @@ def _warn_log1p(X: np.ndarray | sc_sparse.spmatrix, is_log1p: bool, sample_size:
@njit(nogil=True, fastmath=True, cache=False)
-def fold_change_from_summed_expr(group_agg_counts: np.ndarray, grpc: GroupContainer) -> np.ndarray:
+def fold_change_from_summed_expr(group_agg_counts: np.ndarray, grpc: GroupContainer, exp_post_agg: bool) -> np.ndarray:
"""Compute fold change from summed expression values, per group.
Args:
group_agg_counts (np.ndarray): Sum of expression values of shape (n_groups, n_genes)
grpc (GroupContainer): GroupContainer holding group information
+ exp_post_agg (bool): Whether to exponentiate the fold change after aggregation. This is relevant if the input data is log1p. See documentation for details.
Returns:
np.ndarray: Fold change values of shape (n_groups, n_genes)
@@ -191,18 +191,24 @@ def fold_change_from_summed_expr(group_agg_counts: np.ndarray, grpc: GroupContai
else:
# Else, the reference is the reference group
mu_ref = np.expand_dims(mu_tgt[grpc.encoded_ref_group], 0) # (1, n_genes)
- fold_change = np.where(mu_ref == 0, np.inf, mu_tgt / mu_ref)
+
+ if exp_post_agg:
+ fold_change = np.where(mu_ref == 0, np.inf, np.expm1(mu_tgt) / np.expm1(mu_ref))
+ else:
+ fold_change = np.where(mu_ref == 0, np.inf, mu_tgt / mu_ref)
+
return fold_change
@njit(nogil=True, fastmath=True, cache=False)
-def dense_fold_change(X: np.ndarray, grpc: GroupContainer, is_log1p: bool) -> np.ndarray:
+def dense_fold_change(X: np.ndarray, grpc: GroupContainer, is_log1p: bool, exp_post_agg: bool) -> np.ndarray:
"""Compute fold change from a dense array of expression counts.
Args:
X (np.ndarray): Expression counts
grpc (GroupContainer): GroupContainer holding group information
is_log1p (bool): User-indicated flag if data is log1p or not.
+ exp_post_agg (bool): Whether to exponentiate the fold change after aggregation. This is relevant if the input data is log1p. See documentation for details.
Returns:
np.ndarray: Fold change values of shape (n_groups, n_genes)
@@ -211,15 +217,12 @@ def dense_fold_change(X: np.ndarray, grpc: GroupContainer, is_log1p: bool) -> np
"""
group_agg_counts = np.zeros(shape=(grpc.counts.size, X.shape[1]), dtype=np.float64)
# Sum expressions per group
- _add_at_vec(group_agg_counts, grpc.encoded_groups, np.expm1(X) if is_log1p else X)
- # for group_id in range(grpc.counts.size):
- # idx_start = grpc.indptr[group_id]
- # idx_end = grpc.indptr[group_id + 1]
- # if is_log1p:
- # group_agg_counts[group_id, :] = np.expm1(X[idx_start:idx_end]).sum(axis=0)
- # else:
- # group_agg_counts[group_id, :] = X[idx_start:idx_end].sum(axis=0)
- fold_change = fold_change_from_summed_expr(group_agg_counts, grpc)
+ if is_log1p and not exp_post_agg:
+ _add_at_vec(group_agg_counts, grpc.encoded_groups, np.expm1(X))
+ else:
+ _add_at_vec(group_agg_counts, grpc.encoded_groups, X)
+
+ fold_change = fold_change_from_summed_expr(group_agg_counts, grpc, exp_post_agg=exp_post_agg & is_log1p)
return fold_change
@@ -278,3 +281,49 @@ def chunk_and_fortranize(X: np.ndarray, chunk_lb: int, chunk_ub: int, indices: n
for j in range(0, chunk_ub - chunk_lb):
chunk[i, j] = X[i, chunk_lb + j]
return chunk
+
+
+def compute_batch_bounds(n_genes: int, batch_size: Literal["auto"] | int, n_threads: int) -> List[Tuple[int, int]]:
+ """Computes ideal batch bounds for processing genes in batches.
+ This function ensures no worker is starving. This could happen if we have 8 workers but 9 batches to allocate.
+ In this case, because each batch takes the same time to be processed, all but one workers will be idle waiting for one worker to process the last batch.
+
+ Args:
+ n_genes (int): Total number of genes
+ batch_size (Literal["auto"] | int): Batch size, or "auto" to compute ideal batch size.
+ n_threads (int): Number of threads to use.
+ Returns:
+ List[Tuple[int, int]]: List of (lower_bound, upper_bound) for each batch. Upper bound is excluding, following slicing conventions.
+ """
+ # No batching nor multithreading for small inputs
+ if n_genes < n_threads or n_genes < 256:
+ batch_size = n_genes
+ # n_threads = 1
+ batch_size = n_genes
+ bounds_iterator = [[0, n_genes]]
+ elif isinstance(batch_size, int):
+ # batch_size = min(batch_size, math.ceil(n_genes / n_threads))
+ bounds = list(range(0, n_genes + 1, batch_size))
+ if bounds[-1] != n_genes:
+ bounds.append(n_genes)
+ bounds_iterator = list(zip(bounds[:-1], bounds[1:]))
+ elif batch_size == "auto":
+ target_batch_size = 256
+ min_batches = (n_genes + target_batch_size - 1) // target_batch_size
+ num_batches = ((min_batches + n_threads - 1) // n_threads) * n_threads
+ base_size = n_genes // num_batches
+ remainder = n_genes % num_batches
+ bounds_iterator = []
+ start = 0
+ for i in range(num_batches):
+ end = start + base_size + (1 if i < remainder else 0)
+ bounds_iterator.append((start, end))
+ start = end
+ # Append the last gene as the right bound is excluding
+ if bounds_iterator[-1][1] != n_genes:
+ bounds_iterator[-1][1] = n_genes
+ batch_size = base_size
+ else:
+ raise ValueError(f"Invalid batch_size value: {batch_size}. Must be 'auto' or an integer.")
+
+ return bounds_iterator, batch_size
diff --git a/illico/utils/ranking.py b/illico/utils/ranking.py
index b2798a2..e9ca69a 100644
--- a/illico/utils/ranking.py
+++ b/illico/utils/ranking.py
@@ -1,5 +1,5 @@
import numpy as np
-from numba import njit
+from numba import njit, prange
from illico.utils.sparse.csc import CSCMatrix, _assert_is_csc
@@ -242,7 +242,7 @@ def check_if_sorted(arr: np.ndarray) -> bool:
return True
-@njit(nogil=True, cache=False)
+@njit(nogil=True, cache=True, parallel=True)
def check_indices_sorted_per_parcel(
indices: np.ndarray,
indptr: np.ndarray,
@@ -264,10 +264,10 @@ def check_indices_sorted_per_parcel(
bool
True if all indices subarrays are sorted. False otherwise.
"""
- for k in range(indptr.size - 1):
+ is_sorted = np.empty(indptr.size - 1, dtype=np.bool_)
+ for k in prange(indptr.size - 1):
start = indptr[k]
end = indptr[k + 1]
indices_slice = indices[start:end]
- if not check_if_sorted(indices_slice):
- return False
- return True
+ is_sorted[k] = check_if_sorted(indices_slice)
+ return np.all(is_sorted)
diff --git a/illico/utils/scanpy.py b/illico/utils/scanpy.py
new file mode 100644
index 0000000..c57156a
--- /dev/null
+++ b/illico/utils/scanpy.py
@@ -0,0 +1,70 @@
+"""Utilities for formatting illico results to be compatible with Scanpy's output format."""
+
+from typing import Literal
+
+import anndata as ad
+import numpy as np
+from statsmodels.stats.multitest import multipletests
+from tqdm import trange
+
+
+def adjust_pvalues(
+ pvals: np.ndarray, method: Literal["benjamini-hochberg", "bonferroni"] = "benjamini-hochberg"
+) -> np.ndarray:
+ """Adjust p-values row-wise (pert-wise) for multiple testing."""
+ assert pvals.ndim == 2
+ adj_pvals = np.empty_like(pvals)
+ n_tests = pvals.shape[1]
+ for i in trange(pvals.shape[0], desc="Adjusting p-values…", leave=False):
+ if method == "benjamini-hochberg":
+ _, adj_pvals[i], _, _ = multipletests(pvals[i], method="fdr_bh", alpha=0.05)
+ elif method == "bonferroni":
+ adjusted = pvals[i] * n_tests
+ adjusted = np.minimum(adjusted, 1.0)
+ adj_pvals[i] = adjusted
+ else:
+ raise ValueError(f"Unknown adjustment method: {method}")
+
+ return adj_pvals
+
+
+def format_illico_results_for_scanpy(
+ adata: ad.AnnData,
+ reference: str | None,
+ group_keys: str,
+ layer: str | None,
+ values: np.ndarray,
+ corr_method: Literal["benjamini-hochberg", "bonferroni"] = "benjamini-hochberg",
+) -> dict:
+ """Format illico results to be compatible with Scanpy's output format."""
+ # Evict the reference group from the results if provided
+ sorted_pert_names = sorted(adata.obs[group_keys].unique())
+ if reference is not None:
+ mask = np.array([name != reference for name in sorted_pert_names])
+ values = values[mask, :, :]
+ sorted_pert_names = [name for name in sorted_pert_names if name != reference]
+
+ # Sort by signed z-score
+ indices = np.argsort(values[:, :, 2], axis=1)[:, ::-1]
+ # Adjust p-values
+ pvals_adj = adjust_pvalues(values[:, :, 0], method=corr_method)
+
+ # Format output
+ output = dict(
+ params=dict(
+ groupby=group_keys,
+ reference=reference,
+ method="wilcoxon",
+ use_raw=False,
+ layer=layer,
+ corr_method=corr_method,
+ ),
+ names=np.rec.fromarrays(adata.var_names.values[indices], names=sorted_pert_names),
+ scores=np.rec.fromarrays(np.take_along_axis(values[:, :, 2], indices, axis=1), names=sorted_pert_names),
+ pvals=np.rec.fromarrays(np.take_along_axis(values[:, :, 0], indices, axis=1), names=sorted_pert_names),
+ pvals_adj=np.rec.fromarrays(np.take_along_axis(pvals_adj, indices, axis=1), names=sorted_pert_names),
+ logfoldchanges=np.rec.fromarrays(
+ np.take_along_axis(np.log2(values[:, :, 3]), indices, axis=1), names=sorted_pert_names
+ ),
+ )
+ return output
diff --git a/illico/utils/sparse/csc.py b/illico/utils/sparse/csc.py
index c70a681..8292649 100644
--- a/illico/utils/sparse/csc.py
+++ b/illico/utils/sparse/csc.py
@@ -186,13 +186,14 @@ def csc_get_contig_cols_into_csr(csc_matrix: CSCMatrix, chunk_lb: int, chunk_ub:
@njit(nogil=True, fastmath=True, cache=False)
-def csc_fold_change(X: CSCMatrix, grpc: GroupContainer, is_log1p: bool) -> np.ndarray:
+def csc_fold_change(X: CSCMatrix, grpc: GroupContainer, is_log1p: bool, exp_post_agg: bool) -> np.ndarray:
"""Compute fold change from a CSC matrix of expression counts.
Args:
X (CSCMatrix): Input expression counts CSC matrix
grpc (GroupContainer): GroupContainer
is_log1p (bool): User-indicated flag telling if data was log1p or not.
+ exp_post_agg (bool): Whether to exponentiate the fold change after aggregation. This is relevant if the input data is log1p. See documentation for details. Note that `scanpy.rank_genes_groups` assumes the data to be log1p, and exponentiates post aggregation by default. Defaults to False.
Returns:
np.ndarray: Fold change of change (n_groups, n_genes)
@@ -206,8 +207,11 @@ def csc_fold_change(X: CSCMatrix, grpc: GroupContainer, is_log1p: bool) -> np.nd
start = X.indptr[j]
end = X.indptr[j + 1]
row_indices = X.indices[start:end]
- row_data = np.expm1(X.data[start:end]) if is_log1p else X.data[start:end]
+ if is_log1p and not exp_post_agg:
+ row_data = np.expm1(X.data[start:end])
+ else:
+ row_data = X.data[start:end]
group_id = grpc.encoded_groups[row_indices]
_add_at_vec(group_agg_counts[:, j], group_id, row_data)
- fold_change = fold_change_from_summed_expr(group_agg_counts, grpc)
+ fold_change = fold_change_from_summed_expr(group_agg_counts, grpc, exp_post_agg=exp_post_agg & is_log1p)
return fold_change
diff --git a/illico/utils/sparse/csr.py b/illico/utils/sparse/csr.py
index 6801ce5..d58d98b 100644
--- a/illico/utils/sparse/csr.py
+++ b/illico/utils/sparse/csr.py
@@ -261,13 +261,14 @@ def csr_get_contig_cols_into_csc(csr_matrix: CSRMatrix, chunk_lb: int, chunk_ub:
# TODO: move this in the same file as its subroutines, so that caching as no risk of staling
@njit(nogil=True, fastmath=True, cache=False)
-def csr_fold_change(X: CSRMatrix, grpc: GroupContainer, is_log1p: bool) -> np.ndarray:
+def csr_fold_change(X: CSRMatrix, grpc: GroupContainer, is_log1p: bool, exp_post_agg: bool = False) -> np.ndarray:
"""Compute fold change from a CSR matrix of expression counts.
Args:
X (CSRMatrix): Input expression counts CSR matrix
grpc (GroupContainer): GroupContainer
is_log1p (bool): User-indicated flag telling if data was log1p or not.
+ exp_post_agg (bool, optional): Whether to exponentiate the fold change after aggregation. This is relevant if the input data is log1p. See documentation for details. Note that `scanpy.rank_genes_groups` assumes the data to be log1p, and exponentiates post aggregation by default. Defaults to False.
Returns:
np.ndarray: Fold change of change (n_groups, n_genes)
@@ -281,8 +282,11 @@ def csr_fold_change(X: CSRMatrix, grpc: GroupContainer, is_log1p: bool) -> np.nd
start = X.indptr[i]
end = X.indptr[i + 1]
col_indices = X.indices[start:end]
- row_data = np.expm1(X.data[start:end]) if is_log1p else X.data[start:end]
+ if is_log1p and not exp_post_agg:
+ row_data = np.expm1(X.data[start:end])
+ else:
+ row_data = X.data[start:end]
group_id = grpc.encoded_groups[i]
_add_at_vec(group_agg_counts[group_id], col_indices, row_data)
- fold_change = fold_change_from_summed_expr(group_agg_counts, grpc)
+ fold_change = fold_change_from_summed_expr(group_agg_counts, grpc, exp_post_agg & is_log1p)
return fold_change
diff --git a/poetry.lock b/poetry.lock
index 1e648eb..311d69c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
[[package]]
name = "accessible-pygments"
@@ -19,25 +19,6 @@ pygments = ">=1.5"
dev = ["pillow", "pkginfo (>=1.10)", "playwright", "pre-commit", "setuptools", "twine (>=5.0)"]
tests = ["hypothesis", "pytest"]
-[[package]]
-name = "adpbulk"
-version = "0.1.4"
-description = "Pseudo-Bulking Single-Cell RNA-seq"
-optional = false
-python-versions = "*"
-groups = ["dev"]
-files = [
- {file = "adpbulk-0.1.4-py2.py3-none-any.whl", hash = "sha256:08c3cd2f657733abed97d48e0e09ba67dd2c742671b2c26dc45e7cf6575489dd"},
- {file = "adpbulk-0.1.4.tar.gz", hash = "sha256:56676cb16c9d0fae9cd06a1b4d611586dbf6851b0cd3d0209979f419f8300a37"},
-]
-
-[package.dependencies]
-anndata = ">=0.7.4"
-numpy = ">=1.17.0"
-pandas = ">=0.21"
-pytest = "*"
-tqdm = "*"
-
[[package]]
name = "alabaster"
version = "1.0.0"
@@ -81,8 +62,8 @@ dev = ["towncrier (>=24.8.0)"]
dev-doc = ["towncrier (>=24.8.0)"]
doc = ["awkward (>=2.3)", "dask[array] (>=2023.5.1,<2024.8.dev0 || >=2024.10.dev0,<2025.2.dev0 || >=2025.9.dev0)", "ipython", "myst-nb", "myst-parser", "scanpydoc[theme,typehints] (>=0.15.3)", "sphinx (>=8.2.1,<9)", "sphinx-autodoc-typehints (>=2.2.0)", "sphinx-book-theme (>=1.1.0)", "sphinx-copybutton", "sphinx-design (>=0.5.0)", "sphinx-issues (>=5.0.1)", "sphinx-toolbox (>=3.8.0)", "sphinxext-opengraph", "towncrier (>=24.8.0)"]
gpu = ["cupy"]
-lazy = ["aiohttp", "dask[array] (>=2023.5.1,<2024.8.dev0 || >=2024.10.dev0,<2025.2.dev0 || >=2025.9.dev0)", "requests", "xarray (>=2025.06.1)"]
-test = ["aiohttp", "awkward (>=2.3.2)", "boltons", "dask[array] (>=2023.5.1,<2024.8.dev0 || >=2024.10.dev0,<2025.2.dev0 || >=2025.9.dev0)", "dask[distributed]", "filelock", "joblib", "loompy (>=3.0.5)", "matplotlib", "openpyxl", "pooch", "pyarrow", "pytest", "pytest-cov", "pytest-memray", "pytest-mock", "pytest-randomly", "pytest-xdist[psutil]", "requests", "scanpy (>=1.10)", "scikit-learn", "xarray (>=2025.06.1)"]
+lazy = ["aiohttp", "dask[array] (>=2023.5.1,<2024.8.dev0 || >=2024.10.dev0,<2025.2.dev0 || >=2025.9.dev0)", "requests", "xarray (>=2025.6.1)"]
+test = ["aiohttp", "awkward (>=2.3.2)", "boltons", "dask[array] (>=2023.5.1,<2024.8.dev0 || >=2024.10.dev0,<2025.2.dev0 || >=2025.9.dev0)", "dask[distributed]", "filelock", "joblib", "loompy (>=3.0.5)", "matplotlib", "openpyxl", "pooch", "pyarrow", "pytest", "pytest-cov", "pytest-memray", "pytest-mock", "pytest-randomly", "pytest-xdist[psutil]", "requests", "scanpy (>=1.10)", "scikit-learn", "xarray (>=2025.6.1)"]
test-min = ["awkward (>=2.3.2)", "boltons", "dask[array] (>=2023.5.1,<2024.8.dev0 || >=2024.10.dev0,<2025.2.dev0 || >=2025.9.dev0)", "dask[distributed]", "filelock", "joblib", "loompy (>=3.0.5)", "matplotlib", "openpyxl", "pooch", "pyarrow", "pytest", "pytest-cov", "pytest-memray", "pytest-mock", "pytest-randomly", "pytest-xdist[psutil]", "scanpy (>=1.10)", "scikit-learn"]
[[package]]
@@ -530,74 +511,74 @@ testing = ["packaging"]
[[package]]
name = "filelock"
-version = "3.25.0"
+version = "3.25.2"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
- {file = "filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047"},
- {file = "filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3"},
+ {file = "filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70"},
+ {file = "filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694"},
]
[[package]]
name = "fonttools"
-version = "4.62.0"
+version = "4.62.1"
description = "Tools to manipulate font files"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
- {file = "fonttools-4.62.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:62b6a3d0028e458e9b59501cf7124a84cd69681c433570e4861aff4fb54a236c"},
- {file = "fonttools-4.62.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:966557078b55e697f65300b18025c54e872d7908d1899b7314d7c16e64868cb2"},
- {file = "fonttools-4.62.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf34861145b516cddd19b07ae6f4a61ea1c6326031b960ec9ddce8ee815e888"},
- {file = "fonttools-4.62.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e2ff573de2775508c8a366351fb901c4ced5dc6cf2d87dd15c973bedcdd5216"},
- {file = "fonttools-4.62.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:55b189a1b3033860a38e4e5bd0626c5aa25c7ce9caee7bc784a8caec7a675401"},
- {file = "fonttools-4.62.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:825f98cd14907c74a4d0a3f7db8570886ffce9c6369fed1385020febf919abf6"},
- {file = "fonttools-4.62.0-cp310-cp310-win32.whl", hash = "sha256:c858030560f92a054444c6e46745227bfd3bb4e55383c80d79462cd47289e4b5"},
- {file = "fonttools-4.62.0-cp310-cp310-win_amd64.whl", hash = "sha256:9bf75eb69330e34ad2a096fac67887102c8537991eb6cac1507fc835bbb70e0a"},
- {file = "fonttools-4.62.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:196cafef9aeec5258425bd31a4e9a414b2ee0d1557bca184d7923d3d3bcd90f9"},
- {file = "fonttools-4.62.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:153afc3012ff8761b1733e8fbe5d98623409774c44ffd88fbcb780e240c11d13"},
- {file = "fonttools-4.62.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13b663fb197334de84db790353d59da2a7288fd14e9be329f5debc63ec0500a5"},
- {file = "fonttools-4.62.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:591220d5333264b1df0d3285adbdfe2af4f6a45bbf9ca2b485f97c9f577c49ff"},
- {file = "fonttools-4.62.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:579f35c121528a50c96bf6fcb6a393e81e7f896d4326bf40e379f1c971603db9"},
- {file = "fonttools-4.62.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:44956b003151d5a289eba6c71fe590d63509267c37e26de1766ba15d9c589582"},
- {file = "fonttools-4.62.0-cp311-cp311-win32.whl", hash = "sha256:42c7848fa8836ab92c23b1617c407a905642521ff2d7897fe2bf8381530172f1"},
- {file = "fonttools-4.62.0-cp311-cp311-win_amd64.whl", hash = "sha256:4da779e8f342a32856075ddb193b2a024ad900bc04ecb744014c32409ae871ed"},
- {file = "fonttools-4.62.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:22bde4dc12a9e09b5ced77f3b5053d96cf10c4976c6ac0dee293418ef289d221"},
- {file = "fonttools-4.62.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7199c73b326bad892f1cb53ffdd002128bfd58a89b8f662204fbf1daf8d62e85"},
- {file = "fonttools-4.62.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d732938633681d6e2324e601b79e93f7f72395ec8681f9cdae5a8c08bc167e72"},
- {file = "fonttools-4.62.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31a804c16d76038cc4e3826e07678efb0a02dc4f15396ea8e07088adbfb2578e"},
- {file = "fonttools-4.62.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:090e74ac86e68c20150e665ef8e7e0c20cb9f8b395302c9419fa2e4d332c3b51"},
- {file = "fonttools-4.62.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8f086120e8be9e99ca1288aa5ce519833f93fe0ec6ebad2380c1dee18781f0b5"},
- {file = "fonttools-4.62.0-cp312-cp312-win32.whl", hash = "sha256:37a73e5e38fd05c637daede6ffed5f3496096be7df6e4a3198d32af038f87527"},
- {file = "fonttools-4.62.0-cp312-cp312-win_amd64.whl", hash = "sha256:658ab837c878c4d2a652fcbb319547ea41693890e6434cf619e66f79387af3b8"},
- {file = "fonttools-4.62.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:274c8b8a87e439faf565d3bcd3f9f9e31bca7740755776a4a90a4bfeaa722efa"},
- {file = "fonttools-4.62.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93e27131a5a0ae82aaadcffe309b1bae195f6711689722af026862bede05c07c"},
- {file = "fonttools-4.62.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83c6524c5b93bad9c2939d88e619fedc62e913c19e673f25d5ab74e7a5d074e5"},
- {file = "fonttools-4.62.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:106aec9226f9498fc5345125ff7200842c01eda273ae038f5049b0916907acee"},
- {file = "fonttools-4.62.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15d86b96c79013320f13bc1b15f94789edb376c0a2d22fb6088f33637e8dfcbc"},
- {file = "fonttools-4.62.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f16c07e5250d5d71d0f990a59460bc5620c3cc456121f2cfb5b60475699905f"},
- {file = "fonttools-4.62.0-cp313-cp313-win32.whl", hash = "sha256:d31558890f3fa00d4f937d12708f90c7c142c803c23eaeb395a71f987a77ebe3"},
- {file = "fonttools-4.62.0-cp313-cp313-win_amd64.whl", hash = "sha256:6826a5aa53fb6def8a66bf423939745f415546c4e92478a7c531b8b6282b6c3b"},
- {file = "fonttools-4.62.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4fa5a9c716e2f75ef34b5a5c2ca0ee4848d795daa7e6792bf30fd4abf8993449"},
- {file = "fonttools-4.62.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:625f5cbeb0b8f4e42343eaeb4bc2786718ddd84760a2f5e55fdd3db049047c00"},
- {file = "fonttools-4.62.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6247e58b96b982709cd569a91a2ba935d406dccf17b6aa615afaed37ac3856aa"},
- {file = "fonttools-4.62.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:840632ea9c1eab7b7f01c369e408c0721c287dfd7500ab937398430689852fd1"},
- {file = "fonttools-4.62.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:28a9ea2a7467a816d1bec22658b0cce4443ac60abac3e293bdee78beb74588f3"},
- {file = "fonttools-4.62.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ae611294f768d413949fd12693a8cba0e6332fbc1e07aba60121be35eac68d0"},
- {file = "fonttools-4.62.0-cp314-cp314-win32.whl", hash = "sha256:273acb61f316d07570a80ed5ff0a14a23700eedbec0ad968b949abaa4d3f6bb5"},
- {file = "fonttools-4.62.0-cp314-cp314-win_amd64.whl", hash = "sha256:a5f974006d14f735c6c878fc4b117ad031dc93638ddcc450ca69f8fd64d5e104"},
- {file = "fonttools-4.62.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0361a7d41d86937f1f752717c19f719d0fde064d3011038f9f19bdf5fc2f5c95"},
- {file = "fonttools-4.62.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4108c12773b3c97aa592311557c405d5b4fc03db2b969ed928fcf68e7b3c887"},
- {file = "fonttools-4.62.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b448075f32708e8fb377fe7687f769a5f51a027172c591ba9a58693631b077a8"},
- {file = "fonttools-4.62.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5f1fa8cc9f1a56a3e33ee6b954d6d9235e6b9d11eb7a6c9dfe2c2f829dc24db"},
- {file = "fonttools-4.62.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f8c8ea812f82db1e884b9cdb663080453e28f0f9a1f5027a5adb59c4cc8d38d1"},
- {file = "fonttools-4.62.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:03c6068adfdc67c565d217e92386b1cdd951abd4240d65180cec62fa74ba31b2"},
- {file = "fonttools-4.62.0-cp314-cp314t-win32.whl", hash = "sha256:d28d5baacb0017d384df14722a63abe6e0230d8ce642b1615a27d78ffe3bc983"},
- {file = "fonttools-4.62.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3f9e20c4618f1e04190c802acae6dc337cb6db9fa61e492fd97cd5c5a9ff6d07"},
- {file = "fonttools-4.62.0-py3-none-any.whl", hash = "sha256:75064f19a10c50c74b336aa5ebe7b1f89fd0fb5255807bfd4b0c6317098f4af3"},
- {file = "fonttools-4.62.0.tar.gz", hash = "sha256:0dc477c12b8076b4eb9af2e440421b0433ffa9e1dcb39e0640a6c94665ed1098"},
+ {file = "fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c"},
+ {file = "fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a"},
+ {file = "fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3"},
+ {file = "fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23"},
+ {file = "fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d"},
+ {file = "fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae"},
+ {file = "fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed"},
+ {file = "fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9"},
+ {file = "fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7"},
+ {file = "fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14"},
+ {file = "fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7"},
+ {file = "fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b"},
+ {file = "fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1"},
+ {file = "fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416"},
+ {file = "fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53"},
+ {file = "fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2"},
+ {file = "fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974"},
+ {file = "fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9"},
+ {file = "fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936"},
+ {file = "fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392"},
+ {file = "fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04"},
+ {file = "fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d"},
+ {file = "fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c"},
+ {file = "fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42"},
+ {file = "fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79"},
+ {file = "fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe"},
+ {file = "fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68"},
+ {file = "fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1"},
+ {file = "fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069"},
+ {file = "fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9"},
+ {file = "fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24"},
+ {file = "fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056"},
+ {file = "fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca"},
+ {file = "fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca"},
+ {file = "fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782"},
+ {file = "fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae"},
+ {file = "fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7"},
+ {file = "fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a"},
+ {file = "fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800"},
+ {file = "fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e"},
+ {file = "fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82"},
+ {file = "fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260"},
+ {file = "fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4"},
+ {file = "fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b"},
+ {file = "fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87"},
+ {file = "fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c"},
+ {file = "fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a"},
+ {file = "fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e"},
+ {file = "fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd"},
+ {file = "fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d"},
]
[package.extras]
@@ -613,57 +594,6 @@ type1 = ["xattr ; sys_platform == \"darwin\""]
unicode = ["unicodedata2 (>=17.0.0) ; python_version <= \"3.14\""]
woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"]
-[[package]]
-name = "formulaic"
-version = "1.2.1"
-description = "An implementation of Wilkinson formulas."
-optional = false
-python-versions = ">=3.9"
-groups = ["dev"]
-files = [
- {file = "formulaic-1.2.1-py3-none-any.whl", hash = "sha256:661d6d2467aa961b9afb3a1e2a187494239793c63eb729e422d1307afa98b43b"},
- {file = "formulaic-1.2.1.tar.gz", hash = "sha256:dc79476baa2d811b35798893eb2f2c1e51edee8d7a9c1429b400e56f4e0beccc"},
-]
-
-[package.dependencies]
-interface-meta = ">=1.2.0"
-narwhals = ">=1.17"
-numpy = ">=1.20.0"
-pandas = ">=1.3"
-scipy = ">=1.6"
-typing-extensions = ">=4.2.0"
-wrapt = [
- {version = ">=1.0", markers = "python_version < \"3.13\""},
- {version = ">=1.17.0rc1", markers = "python_version >= \"3.13\""},
-]
-
-[package.extras]
-arrow = ["pyarrow (>=1)"]
-calculus = ["sympy (>=1.3,!=1.10)"]
-polars = ["polars (>=1)"]
-
-[[package]]
-name = "formulaic-contrasts"
-version = "1.0.0"
-description = "Build contrasts for models defined with formulaic"
-optional = false
-python-versions = ">=3.10"
-groups = ["dev"]
-files = [
- {file = "formulaic_contrasts-1.0.0-py3-none-any.whl", hash = "sha256:e1220d315cf446bdec9385375ca4da43896e4ba68114ebea1b2a37efa5d097f5"},
- {file = "formulaic_contrasts-1.0.0.tar.gz", hash = "sha256:0a575a810bf1fba28938259d86a3ae2ae90cb9826fca84b9409085170862f701"},
-]
-
-[package.dependencies]
-formulaic = "*"
-pandas = "*"
-session-info = "*"
-
-[package.extras]
-dev = ["pre-commit", "twine (>=4.0.2)"]
-doc = ["docutils (>=0.8,<0.18.dev0 || >=0.20.dev0)", "ipykernel", "ipython", "myst-nb (>=1.1)", "pandas", "setuptools", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinx-book-theme (>=1)", "sphinx-copybutton", "sphinx-tabs", "sphinxcontrib-bibtex (>=1)", "sphinxext-opengraph", "statsmodels"]
-test = ["coverage", "numpy", "pytest"]
-
[[package]]
name = "furo"
version = "2025.12.19"
@@ -681,7 +611,7 @@ accessible-pygments = ">=0.0.5"
beautifulsoup4 = "*"
pygments = ">=2.7"
sphinx = ">=7.0,<10.0"
-sphinx-basic-ng = ">=1.0.0.beta2"
+sphinx-basic-ng = ">=1.0.0b2"
[[package]]
name = "google-crc32c"
@@ -824,7 +754,7 @@ description = "Getting image size from png/jpeg/jpeg2000/gif file"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
groups = ["doc"]
-markers = "python_version >= \"3.13\""
+markers = "python_version >= \"3.12\""
files = [
{file = "imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899"},
{file = "imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f"},
@@ -837,7 +767,7 @@ description = "Get image size from headers (BMP/PNG/JPEG/JPEG2000/GIF/TIFF/SVG/N
optional = false
python-versions = "<3.15,>=3.10"
groups = ["doc"]
-markers = "python_version < \"3.13\""
+markers = "python_version == \"3.11\""
files = [
{file = "imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96"},
{file = "imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3"},
@@ -879,18 +809,6 @@ files = [
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
]
-[[package]]
-name = "interface-meta"
-version = "1.3.0"
-description = "`interface_meta` provides a convenient way to expose an extensible API with enforced method signatures and consistent documentation."
-optional = false
-python-versions = ">=3.7,<4.0"
-groups = ["dev"]
-files = [
- {file = "interface_meta-1.3.0-py3-none-any.whl", hash = "sha256:de35dc5241431886e709e20a14d6597ed07c9f1e8b4bfcffde2190ca5b700ee8"},
- {file = "interface_meta-1.3.0.tar.gz", hash = "sha256:8a4493f8bdb73fb9655dcd5115bc897e207319e36c8835f39c516a2d7e9d79a1"},
-]
-
[[package]]
name = "ipdb"
version = "0.13.13"
@@ -1250,7 +1168,7 @@ colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
[package.extras]
-dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""]
+dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==0.910) ; python_version < \"3.6\"", "mypy (==0.971) ; python_version == \"3.6\"", "mypy (==1.13.0) ; python_version >= \"3.8\"", "mypy (==1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""]
[[package]]
name = "markdown-it-py"
@@ -1522,61 +1440,61 @@ psutil = "*"
[[package]]
name = "memray"
-version = "1.19.1"
+version = "1.19.2"
description = "A memory profiler for Python applications"
optional = false
python-versions = ">=3.7.0"
groups = ["dev"]
files = [
- {file = "memray-1.19.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:64f226ebf287cb8815069f3a5cbb5823f9c20ef65ea684b1601a7433662967fe"},
- {file = "memray-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f981fd89bda1de0a29b628447672945e40076fa96848f74510f02bf9069a8b7"},
- {file = "memray-1.19.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:bd2519ab3eb2114ae299277671e754c38b808342e8161d4aeee138d335f43ea0"},
- {file = "memray-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:90c77eeebda3dbce527b3436087c08f2bee772837ca86d5f906d4f8abc6297b8"},
- {file = "memray-1.19.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb9bb9b6bae11353d6cab0eb1199dd86a06d472dfaa4f7c92756dff1c40bf080"},
- {file = "memray-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5469d48c6f9799d3061115d5f05401432a232551d778a3be129b74ff1cf3d91e"},
- {file = "memray-1.19.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2cb5026aede2805215edc442519d85ecf0604e98bd1d9ef6be060004547f6688"},
- {file = "memray-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2eb057b3e9545a1bc90ca71911834bde019f66d7e5306729abce3478a23855b"},
- {file = "memray-1.19.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:004588fbf0ac91fb15d58e09b16c5fc28644ee48893b04dfcd28338ae56e378d"},
- {file = "memray-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:548cb205ef856f546275754abc1c5f7f6aafac427c247bb4581791eaaa47a770"},
- {file = "memray-1.19.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b53dfb11f22d390a3d58cfee59eef8c2385bcc3cff5e7f79c80fa5952bc224a4"},
- {file = "memray-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:464a2705c601ab2ff59d26c99345965a33bcd98065877a0a1884d9a17745ccd1"},
- {file = "memray-1.19.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:57b0430e4800b8cbc38e1f529ad7af959cc96386e00773c8af57c46eddb15ecd"},
- {file = "memray-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:595414e753a0152282974b954827aeaf552dc02f47ed16a2743821ed461b6c51"},
- {file = "memray-1.19.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ead57c4be9ea89b78d8ce2017f8f3e28f552fc2279cf5d24bf75d70bdfe39ca7"},
- {file = "memray-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:41d829191287a995eea8b832fe7c8de243cf9e5d32d53169952695c7210e3a6b"},
- {file = "memray-1.19.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5855a9c3f3cfcf8ef01151514332535756b5d7be17bdba84016b0ca57d86f7f8"},
- {file = "memray-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:216918a42abdd3c18c4771862584feda3a24bf7205db6f000a41be9ddc1c98b4"},
- {file = "memray-1.19.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:56b20156c2754762ccfcfa03fd88ce33ecd712aacd302ef099a871b3197fe4a2"},
- {file = "memray-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e9d93995a91a8383fda95a1f7a15247aca2abd2f80f7f7c7ff56b3d89a5d7893"},
- {file = "memray-1.19.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:500956020d245ad3440cc2fae06c1d781f339e30f8d58654bc5ae9f51f999fab"},
- {file = "memray-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9770b6046a8b7d7bb982c2cefb324a60a0ee7cd7c35c58c0df058355a9a54676"},
- {file = "memray-1.19.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89593bfec1924aff4571e7bb00066b1cd832a828d701b0380009d790139aa814"},
- {file = "memray-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dd00e0b4f5820f7a793691c0faeb15e4fbb5472198184605c29d0a961355741"},
- {file = "memray-1.19.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:b202f1d96211d73712f5db8281c437dcbbd9cc91e520ca44b8406466b9672624"},
- {file = "memray-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f1a99cbb0b413a945e07529e521c7441cb46e4d5e6868dd810cbdaa80af0b74c"},
- {file = "memray-1.19.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:c48461e7a8ba0b12ae740316e41564e18db2533ebeb1a093b2c8232d9c7c2653"},
- {file = "memray-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:265812e729c90a9240d6a23dfa89d8bea11cb67d37a1411f7a690948584ff024"},
- {file = "memray-1.19.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:808eb35fa012fa8e25582e3c9b76d9f0471e87776c7cd86e6c149da34fed22ed"},
- {file = "memray-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4b33fa1b6e8619e589882b44e6bdce0ad51d8bea2dd24f7afae6efcfcd8ffa8"},
- {file = "memray-1.19.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e905d04e337e1482af988f349b1062ec330408bf1d8e5b0cfe8c0c7b47959692"},
- {file = "memray-1.19.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa0d8d8df8a0cad97e934dcd1cb698af00ffb10cd79277907c2cf97212f0bd9"},
- {file = "memray-1.19.1-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2fe3886eef669017810782ce63b1cdf8a426f07a27ea6a9f73d9dc3e5c448b0b"},
- {file = "memray-1.19.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7238113251d325da2d405b067ec180842b93a7fb10ff06fb3f7c261225b33ae"},
- {file = "memray-1.19.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:334754a0ad5664a703516307772a4555ffdc616586168b28efd31e8862a6cdb1"},
- {file = "memray-1.19.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a193cc20bbe60eccee8c9b4d9eb78ccd69a3248f0291d5d1a7fdda62aa19b53"},
- {file = "memray-1.19.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:427af714758c98d08e86fc93c7ccdf0bb96a0bb355fccb7624ef6700fd87d4ab"},
- {file = "memray-1.19.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c459257314d33207709dde136be74d8e37b102eac38dd5649f76ef946c4efd37"},
- {file = "memray-1.19.1-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ff5cabd4ca6672c09e1f22ab95b95b845e94f41fbd4b17021a30603705636dbc"},
- {file = "memray-1.19.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:142482bb2fc0d41fb5a221de51f1fcc6d80b4421d2d7760c8f8e847600338640"},
- {file = "memray-1.19.1-cp38-cp38-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af1e95ec4b2e651452ba54cc7bb261c054ec18bc940b230e3a2a5c981079495"},
- {file = "memray-1.19.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:1c66e535bd2382a79a1769d3ae591aaf14d5695ec0a1de38d65a6f904f1c4b17"},
- {file = "memray-1.19.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:5afc80c460d5a317c01c9beeb5fbef98227b2aff47d7e37e504d5a7b8cc3f318"},
- {file = "memray-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8fb301bc6671f01fc5d453bcd3446f345a1e8753df1bf3c17dc42e23b13e675"},
- {file = "memray-1.19.1-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5135e8d79d6a0590d0794be914c2ecf751afb6a1618f10db8225e71235719428"},
- {file = "memray-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d42f8c8d6e304213a93f3776b23a0dead4eced293e4aed5df471551a8ca6b2b"},
- {file = "memray-1.19.1-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1159da44ff7cb3cd67919684e07bc3a65228d208cfd95757ad5b5736813bc07b"},
- {file = "memray-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2302d50d0d3af992940cc372dd0cbea63dd0eeffc36dee685bffeab6466d4d73"},
- {file = "memray-1.19.1.tar.gz", hash = "sha256:7fcf306eae2c00144920b01913f42fa7f235af7a80fa3226ab124672a5cb1d8f"},
+ {file = "memray-1.19.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:50d7130bb0c8609176b3b691c8b67fc92805180166e087549a59e7881ae8cf36"},
+ {file = "memray-1.19.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3643d601c4c1c413a62fb296598ed05dce1e1c3a58ea5c8659ae98ad36ce3a7a"},
+ {file = "memray-1.19.2-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:661aca0dbf4c448eef93f2f0bd0852eeefe3de2460e8105c2160c86e308beea5"},
+ {file = "memray-1.19.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d13f33f1fa76165c5596e73bc45a366d58066be567fb131498cd770fa87f5a02"},
+ {file = "memray-1.19.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74291aa9bbf54ff2ac5df2665c792d490c576720dd2cbad89af53528bda5443f"},
+ {file = "memray-1.19.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:716a1b2569e049d0cb769015e5be9138bd97bd157e67920cc9e215e011fbb9cd"},
+ {file = "memray-1.19.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:c8d35a9f5b222165c5aedbfc18b79dc5161a724963a4fca8d1053faa0b571195"},
+ {file = "memray-1.19.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3735567011cc22339aee2c59b5fc94d1bdd4a23f9990e02a2c3cccc9c3cf6de4"},
+ {file = "memray-1.19.2-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ab78af759eebcb8d8ecef173042515711d2dcc9600d5dd446d1592b24a89b7d9"},
+ {file = "memray-1.19.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f3ae7983297d168cdcc2d05cd93a4934b9b6fe0d341a91ac5b71bf45f9cec06c"},
+ {file = "memray-1.19.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08a4316d7a92eb415024b46988844ed0fd44b2d02ca00fa4a21f2481c1f803e6"},
+ {file = "memray-1.19.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dbdb14fd31e2a031312755dc76146aeff9d0889e82ccffe231f1f20f50526f57"},
+ {file = "memray-1.19.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:22d4482f559ffa91a9727693e7e338856bee5e316f922839bf8b96e0f9b8a4de"},
+ {file = "memray-1.19.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fd1476868177ee8d9f7f85e5a085a20cc3c3a8228a23ced72749265885d55ca"},
+ {file = "memray-1.19.2-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:23375d50faa199e1c1bc2e89f08691f6812478fddb49a1b82bebe6ef5a56df2c"},
+ {file = "memray-1.19.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8ef3d8e4fba0b26280b550278a0660554283135cbccc34e2d49ba82a1945eb61"},
+ {file = "memray-1.19.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4d6cf9597ae5d60f7893a0b7b6b9af9c349121446b3c1e7b9ac1d8b5d45a505"},
+ {file = "memray-1.19.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:716a0a0e9048d21da98f9107fa030a76138eb694a16a81ad15eace54fddef4cd"},
+ {file = "memray-1.19.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:13aa87ad34cc88b3f31f7205e0a4543c391032e8600dc0c0cbf22555ff816d97"},
+ {file = "memray-1.19.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d6b249618a3e4fa8e10291445a2b2dfaf6f188e7cc1765966aac8fb52cb22066"},
+ {file = "memray-1.19.2-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:34985e5e638ef8d4d54de8173c5e4481c478930f545bd0eb4738a631beb63d04"},
+ {file = "memray-1.19.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee0fcfafd1e8535bdc0d0ed75bcdd48d436a6f62d467df91871366cbb3bbaebc"},
+ {file = "memray-1.19.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:846185c393ff0dc6bca55819b1c83b510b77d8d561b7c0c50f4873f69579e35d"},
+ {file = "memray-1.19.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cc31327ed71e9f6ef7e9ed558e764f0e9c3f01da13ad8547734eb65fbeade1d"},
+ {file = "memray-1.19.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:410377c0eae8d544421f74b919a18e119279fe1a2fa5ff381404b55aeb4c6514"},
+ {file = "memray-1.19.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a53dc4032581ed075fcb62a4acc0ced14fb90a8269159d4e53dfac7af269c255"},
+ {file = "memray-1.19.2-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a7630865fbf3823aa2d1a6f7536f7aec88cf8ccf5b2498aad44adbc733f6bd2e"},
+ {file = "memray-1.19.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c23e2b4be22a23cf5cae08854549e3460869a36c5f4bedc739b646ac97da4a60"},
+ {file = "memray-1.19.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95b6c02ca7f8555b5bee1c54c50cbbcf2033e07ebca95dade2ac3a27bb36b320"},
+ {file = "memray-1.19.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:907470e2684568eb91a993ae69a08b1430494c8f2f6ef489b4b78519d9dae3d0"},
+ {file = "memray-1.19.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:124138f35fea36c434256c417f1b8cb32f78769f208530c1e56bf2c2b7654120"},
+ {file = "memray-1.19.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:240192dc98ff0b3501055521bfd73566d339808b11bd5af10865afe6ae18abef"},
+ {file = "memray-1.19.2-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:edb7a3c2a9e97fb409b352f6c316598c7c0c3c22732e73704d25b9eb75ae2f2d"},
+ {file = "memray-1.19.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b6a43db4c1466446a905a77944813253231ac0269f758c6c6bc03ceb1821c1b6"},
+ {file = "memray-1.19.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf951dae8d27d502fbc549f6784460a70cce05b1e71bf5446d8692a74051f14f"},
+ {file = "memray-1.19.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8033b78232555bb1856b3298bef2898ec8b334d3d465c1822c665206d1fa910a"},
+ {file = "memray-1.19.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:40feab802855fd21b56abc0bf916ff013515fd50c77bc70530d686dcfcab7c2c"},
+ {file = "memray-1.19.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8dde0b6714efad49cfd16584c94d8565363878318ee93ad7995c3510850568c"},
+ {file = "memray-1.19.2-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:efbbb9375101fcb036a958fd97cf6b99f2f7112d19fe0a4f05bb14425b4b91af"},
+ {file = "memray-1.19.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31704c3dc06ae3781ba6fcd8194caae126c172e54a18eddb1e9802ed6b22e821"},
+ {file = "memray-1.19.2-cp38-cp38-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb5c90111e0229243a8dc39a2042c59406a45c6d63913834d7ae45eeb97501b5"},
+ {file = "memray-1.19.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2c836c63854f16e72cc4a5bcbb459871f64b4738ba115d1e5eb0258c7a708230"},
+ {file = "memray-1.19.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:f82ee0a0b50a04894dacfbe49db1c259fa8a19efb094514b0100e9916d3b1c55"},
+ {file = "memray-1.19.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b1c58a54372707b3977c079ef93e751109f0bfe566adc7bd640971d123d8d11"},
+ {file = "memray-1.19.2-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:fa236140320ef1b8801cd289962fd81a2d7e59484cc3ecdbc851d1b5c321795e"},
+ {file = "memray-1.19.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:816baeda8e62fddf99c900bdc9e748339dba65df091a7c7ceb0f4f9544c2e7ec"},
+ {file = "memray-1.19.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1532d5dcf8036ec55e43ab6d6b1ff4e70b11a3902dd1c8396b6d2a24ec69d98"},
+ {file = "memray-1.19.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:86060df2e8e18cc867335c50bf92deb973d4dff856bdb565e17fc86ca7a6619b"},
+ {file = "memray-1.19.2.tar.gz", hash = "sha256:680cb90ac4564d140673ac9d8b7a7e07a8405bd1fb8f933da22616f93124ca84"},
]
[package.dependencies]
@@ -1618,32 +1536,6 @@ rtd = ["ipython", "sphinx (>=8)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-bo
testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pygments (<2.20)", "pytest (>=9,<10)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest (>=0.3.0,<0.4.0)"]
testing-docutils = ["pygments", "pytest (>=9,<10)", "pytest-param-files (>=0.6.0,<0.7.0)"]
-[[package]]
-name = "narwhals"
-version = "2.17.0"
-description = "Extremely lightweight compatibility layer between dataframe libraries"
-optional = false
-python-versions = ">=3.9"
-groups = ["dev"]
-files = [
- {file = "narwhals-2.17.0-py3-none-any.whl", hash = "sha256:2ac5307b7c2b275a7d66eeda906b8605e3d7a760951e188dcfff86e8ebe083dd"},
- {file = "narwhals-2.17.0.tar.gz", hash = "sha256:ebd5bc95bcfa2f8e89a8ac09e2765a63055162837208e67b42d6eeb6651d5e67"},
-]
-
-[package.extras]
-cudf = ["cudf-cu12 (>=24.10.0)"]
-dask = ["dask[dataframe] (>=2024.8)"]
-duckdb = ["duckdb (>=1.1)"]
-ibis = ["ibis-framework (>=6.0.0)", "packaging", "pyarrow-hotfix", "rich"]
-modin = ["modin"]
-pandas = ["pandas (>=1.1.3)"]
-polars = ["polars (>=0.20.4)"]
-pyarrow = ["pyarrow (>=13.0.0)"]
-pyspark = ["pyspark (>=3.5.0)"]
-pyspark-connect = ["pyspark[connect] (>=3.5.0)"]
-sql = ["duckdb (>=1.1)", "sqlparse"]
-sqlframe = ["sqlframe (>=3.22.0,!=3.39.3)"]
-
[[package]]
name = "natsort"
version = "8.4.0"
@@ -1667,7 +1559,7 @@ description = "Python package for creating and manipulating graphs and networks"
optional = false
python-versions = ">=3.11"
groups = ["dev"]
-markers = "python_version >= \"3.13\""
+markers = "python_version >= \"3.12\""
files = [
{file = "networkx-3.6-py3-none-any.whl", hash = "sha256:cdb395b105806062473d3be36458d8f1459a4e4b98e236a66c3a48996e07684f"},
{file = "networkx-3.6.tar.gz", hash = "sha256:285276002ad1f7f7da0f7b42f004bcba70d381e936559166363707fdad3d72ad"},
@@ -1691,7 +1583,7 @@ description = "Python package for creating and manipulating graphs and networks"
optional = false
python-versions = "!=3.14.1,>=3.11"
groups = ["dev"]
-markers = "python_version < \"3.13\""
+markers = "python_version == \"3.11\""
files = [
{file = "networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762"},
{file = "networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509"},
@@ -1722,38 +1614,54 @@ files = [
[[package]]
name = "numba"
-version = "0.63.1"
+version = "0.64.0"
description = "compiling Python code using LLVM"
optional = false
python-versions = ">=3.10"
groups = ["main", "dev"]
files = [
- {file = "numba-0.63.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6d6bf5bf00f7db629305caaec82a2ffb8abe2bf45eaad0d0738dc7de4113779"},
- {file = "numba-0.63.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08653d0dfc9cc9c4c9a8fba29ceb1f2d5340c3b86c4a7e5e07e42b643bc6a2f4"},
- {file = "numba-0.63.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09eebf5650246ce2a4e9a8d38270e2d4b0b0ae978103bafb38ed7adc5ea906e"},
- {file = "numba-0.63.1-cp310-cp310-win_amd64.whl", hash = "sha256:f8bba17421d865d8c0f7be2142754ebce53e009daba41c44cf6909207d1a8d7d"},
- {file = "numba-0.63.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b33db00f18ccc790ee9911ce03fcdfe9d5124637d1ecc266f5ae0df06e02fec3"},
- {file = "numba-0.63.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d31ea186a78a7c0f6b1b2a3fe68057fdb291b045c52d86232b5383b6cf4fc25"},
- {file = "numba-0.63.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed3bb2fbdb651d6aac394388130a7001aab6f4541837123a4b4ab8b02716530c"},
- {file = "numba-0.63.1-cp311-cp311-win_amd64.whl", hash = "sha256:1ecbff7688f044b1601be70113e2fb1835367ee0b28ffa8f3adf3a05418c5c87"},
- {file = "numba-0.63.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2819cd52afa5d8d04e057bdfd54367575105f8829350d8fb5e4066fb7591cc71"},
- {file = "numba-0.63.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cfd45dbd3d409e713b1ccfdc2ee72ca82006860254429f4ef01867fdba5845f"},
- {file = "numba-0.63.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a599df6976c03b7ecf15d05302696f79f7e6d10d620367407517943355bcb0"},
- {file = "numba-0.63.1-cp312-cp312-win_amd64.whl", hash = "sha256:bbad8c63e4fc7eb3cdb2c2da52178e180419f7969f9a685f283b313a70b92af3"},
- {file = "numba-0.63.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:0bd4fd820ef7442dcc07da184c3f54bb41d2bdb7b35bacf3448e73d081f730dc"},
- {file = "numba-0.63.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53de693abe4be3bd4dee38e1c55f01c55ff644a6a3696a3670589e6e4c39cde2"},
- {file = "numba-0.63.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81227821a72a763c3d4ac290abbb4371d855b59fdf85d5af22a47c0e86bf8c7e"},
- {file = "numba-0.63.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb227b07c2ac37b09432a9bda5142047a2d1055646e089d4a240a2643e508102"},
- {file = "numba-0.63.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f180883e5508940cc83de8a8bea37fc6dd20fbe4e5558d4659b8b9bef5ff4731"},
- {file = "numba-0.63.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0938764afa82a47c0e895637a6c55547a42c9e1d35cac42285b1fa60a8b02bb"},
- {file = "numba-0.63.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f90a929fa5094e062d4e0368ede1f4497d5e40f800e80aa5222c4734236a2894"},
- {file = "numba-0.63.1-cp314-cp314-win_amd64.whl", hash = "sha256:8d6d5ce85f572ed4e1a135dbb8c0114538f9dd0e3657eeb0bb64ab204cbe2a8f"},
- {file = "numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b"},
+ {file = "numba-0.64.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc09b79440952e3098eeebea4bf6e8d2355fb7f12734fcd9fc5039f0dca90727"},
+ {file = "numba-0.64.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1afe3a80b8c2f376b211fb7a49e536ef9eafc92436afc95a2f41ea5392f8cc65"},
+ {file = "numba-0.64.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23804194b93b8cd416c6444b5fbc4956082a45fed2d25436ef49c594666e7f7e"},
+ {file = "numba-0.64.0-cp310-cp310-win_amd64.whl", hash = "sha256:e2a9fe998bb2cf848960b34db02c2c3b5e02cf82c07a26d9eef3494069740278"},
+ {file = "numba-0.64.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:766156ee4b8afeeb2b2e23c81307c5d19031f18d5ce76ae2c5fb1429e72fa92b"},
+ {file = "numba-0.64.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d17071b4ffc9d39b75d8e6c101a36f0c81b646123859898c9799cb31807c8f78"},
+ {file = "numba-0.64.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ead5630434133bac87fa67526eacb264535e4e9a2d5ec780e0b4fc381a7d275"},
+ {file = "numba-0.64.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2b1fd93e7aaac07d6fbaed059c00679f591f2423885c206d8c1b55d65ca3f2d"},
+ {file = "numba-0.64.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:69440a8e8bc1a81028446f06b363e28635aa67bd51b1e498023f03b812e0ce68"},
+ {file = "numba-0.64.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13721011f693ba558b8dd4e4db7f2640462bba1b855bdc804be45bbeb55031a"},
+ {file = "numba-0.64.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0b180b1133f2b5d8b3f09d96b6d7a9e51a7da5dda3c09e998b5bcfac85d222c"},
+ {file = "numba-0.64.0-cp312-cp312-win_amd64.whl", hash = "sha256:e63dc94023b47894849b8b106db28ccb98b49d5498b98878fac1a38f83ac007a"},
+ {file = "numba-0.64.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3bab2c872194dcd985f1153b70782ec0fbbe348fffef340264eacd3a76d59fd6"},
+ {file = "numba-0.64.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:703a246c60832cad231d2e73c1182f25bf3cc8b699759ec8fe58a2dbc689a70c"},
+ {file = "numba-0.64.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e2e49a7900ee971d32af7609adc0cfe6aa7477c6f6cccdf6d8138538cf7756f"},
+ {file = "numba-0.64.0-cp313-cp313-win_amd64.whl", hash = "sha256:396f43c3f77e78d7ec84cdfc6b04969c78f8f169351b3c4db814b97e7acf4245"},
+ {file = "numba-0.64.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f565d55eaeff382cbc86c63c8c610347453af3d1e7afb2b6569aac1c9b5c93ce"},
+ {file = "numba-0.64.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9b55169b18892c783f85e9ad9e6f5297a6d12967e4414e6b71361086025ff0bb"},
+ {file = "numba-0.64.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:196bcafa02c9dd1707e068434f6d5cedde0feb787e3432f7f1f0e993cc336c4c"},
+ {file = "numba-0.64.0-cp314-cp314-win_amd64.whl", hash = "sha256:213e9acbe7f1c05090592e79020315c1749dd52517b90e94c517dca3f014d4a1"},
+ {file = "numba-0.64.0.tar.gz", hash = "sha256:95e7300af648baa3308127b1955b52ce6d11889d16e8cfe637b4f85d2fca52b1"},
]
[package.dependencies]
llvmlite = "==0.46.*"
-numpy = ">=1.22,<2.4"
+numpy = ">=1.22,<2.5"
+
+[[package]]
+name = "numba-mwu"
+version = "0.1.1"
+description = "Numba-accelerated Mann-Whitney U test with sparse matrix support."
+optional = false
+python-versions = ">=3.11"
+groups = ["dev"]
+files = [
+ {file = "numba_mwu-0.1.1-py3-none-any.whl", hash = "sha256:994cde066cf387b7271baed307e5d9cbc2e1127c51785f3ec9e4b22fd300d7be"},
+ {file = "numba_mwu-0.1.1.tar.gz", hash = "sha256:dfb74ff5d84c8923efeb1abbee56e3a4fdf3d8bdb8ce5f50a786d1ac84b3a400"},
+]
+
+[package.dependencies]
+numba = ">=0.64.0"
+scipy = ">=1.17.1"
[[package]]
name = "numcodecs"
@@ -1802,86 +1710,84 @@ zfpy = ["zfpy (>=1.0.0)"]
[[package]]
name = "numpy"
-version = "2.3.5"
+version = "2.4.3"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.11"
groups = ["main", "dev"]
files = [
- {file = "numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10"},
- {file = "numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218"},
- {file = "numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d"},
- {file = "numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5"},
- {file = "numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7"},
- {file = "numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4"},
- {file = "numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e"},
- {file = "numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748"},
- {file = "numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c"},
- {file = "numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c"},
- {file = "numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa"},
- {file = "numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e"},
- {file = "numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769"},
- {file = "numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5"},
- {file = "numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4"},
- {file = "numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d"},
- {file = "numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28"},
- {file = "numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b"},
- {file = "numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c"},
- {file = "numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952"},
- {file = "numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa"},
- {file = "numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013"},
- {file = "numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff"},
- {file = "numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188"},
- {file = "numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0"},
- {file = "numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903"},
- {file = "numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d"},
- {file = "numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017"},
- {file = "numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf"},
- {file = "numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce"},
- {file = "numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e"},
- {file = "numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b"},
- {file = "numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae"},
- {file = "numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd"},
- {file = "numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f"},
- {file = "numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a"},
- {file = "numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139"},
- {file = "numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e"},
- {file = "numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9"},
- {file = "numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946"},
- {file = "numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1"},
- {file = "numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3"},
- {file = "numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234"},
- {file = "numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7"},
- {file = "numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82"},
- {file = "numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0"},
- {file = "numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63"},
- {file = "numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9"},
- {file = "numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b"},
- {file = "numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520"},
- {file = "numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c"},
- {file = "numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8"},
- {file = "numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248"},
- {file = "numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e"},
- {file = "numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2"},
- {file = "numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41"},
- {file = "numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad"},
- {file = "numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39"},
- {file = "numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20"},
- {file = "numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52"},
- {file = "numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b"},
- {file = "numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3"},
- {file = "numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227"},
- {file = "numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5"},
- {file = "numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf"},
- {file = "numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42"},
- {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310"},
- {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c"},
- {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18"},
- {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff"},
- {file = "numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb"},
- {file = "numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7"},
- {file = "numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425"},
- {file = "numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0"},
+ {file = "numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb"},
+ {file = "numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147"},
+ {file = "numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920"},
+ {file = "numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9"},
+ {file = "numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470"},
+ {file = "numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71"},
+ {file = "numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15"},
+ {file = "numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52"},
+ {file = "numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd"},
+ {file = "numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec"},
+ {file = "numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67"},
+ {file = "numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef"},
+ {file = "numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e"},
+ {file = "numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4"},
+ {file = "numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18"},
+ {file = "numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5"},
+ {file = "numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97"},
+ {file = "numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c"},
+ {file = "numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc"},
+ {file = "numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9"},
+ {file = "numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5"},
+ {file = "numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e"},
+ {file = "numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3"},
+ {file = "numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9"},
+ {file = "numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee"},
+ {file = "numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f"},
+ {file = "numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f"},
+ {file = "numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc"},
+ {file = "numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476"},
+ {file = "numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92"},
+ {file = "numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687"},
+ {file = "numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd"},
+ {file = "numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d"},
+ {file = "numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875"},
+ {file = "numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070"},
+ {file = "numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73"},
+ {file = "numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368"},
+ {file = "numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22"},
+ {file = "numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a"},
+ {file = "numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349"},
+ {file = "numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c"},
+ {file = "numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26"},
+ {file = "numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02"},
+ {file = "numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4"},
+ {file = "numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168"},
+ {file = "numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b"},
+ {file = "numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950"},
+ {file = "numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd"},
+ {file = "numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24"},
+ {file = "numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0"},
+ {file = "numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0"},
+ {file = "numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a"},
+ {file = "numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc"},
+ {file = "numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7"},
+ {file = "numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657"},
+ {file = "numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7"},
+ {file = "numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093"},
+ {file = "numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a"},
+ {file = "numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611"},
+ {file = "numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720"},
+ {file = "numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5"},
+ {file = "numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0"},
+ {file = "numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b"},
+ {file = "numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e"},
+ {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028"},
+ {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8"},
+ {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152"},
+ {file = "numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395"},
+ {file = "numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79"},
+ {file = "numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857"},
+ {file = "numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5"},
+ {file = "numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd"},
]
[[package]]
@@ -2031,27 +1937,26 @@ test = ["pytest", "pytest-cov", "scipy"]
[[package]]
name = "pdex"
-version = "0.1.28"
+version = "0.2.0"
description = "Parallel differential expression for single-cell perturbation sequencing"
optional = false
-python-versions = ">=3.10"
+python-versions = ">=3.11"
groups = ["dev"]
files = [
- {file = "pdex-0.1.28-py3-none-any.whl", hash = "sha256:a57bde53bb8c0b3fcb23d207a7612f633031e25d2d9298a014aa6a0d69ca7405"},
- {file = "pdex-0.1.28.tar.gz", hash = "sha256:52b0f442eaa73705074f388c2070427b1259e28442addd27e05e645da63e7390"},
+ {file = "pdex-0.2.0-py3-none-any.whl", hash = "sha256:10dc5e4e093a5c7bd5a63fa1d601f1cb6ddb2796b6d7d5f0e557b47e1d94439e"},
+ {file = "pdex-0.2.0.tar.gz", hash = "sha256:711e89483f892d1f3008daaca01fdc15aa81b5f5438b3efb09e080ce4e0adb60"},
]
[package.dependencies]
-adpbulk = ">=0.1.4"
-anndata = ">=0.9.0"
-numba = ">=0.61.2"
-numpy = ">=1.0.0"
-pandas = ">=2.0.0"
-polars = ">=1.30.0"
-pyarrow = ">=18.0.0"
-pydeseq2 = ">=0.5.1"
-scipy = ">=1.15.2"
-tqdm = ">=4.67.1"
+anndata = ">=0.12.10"
+numba = ">=0.64.0"
+numba-mwu = ">=0.1.1"
+numpy = ">=2.4.2"
+pandas = ">=2.3.3"
+polars = ">=1.38.1"
+pyarrow = ">=23.0.1"
+scipy = ">=1.17.1"
+tqdm = ">=4.67.3"
[[package]]
name = "pexpect"
@@ -2208,18 +2113,18 @@ testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]]
name = "polars"
-version = "1.38.1"
+version = "1.39.0"
description = "Blazingly fast DataFrame library"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
- {file = "polars-1.38.1-py3-none-any.whl", hash = "sha256:a29479c48fed4984d88b656486d221f638cba45d3e961631a50ee5fdde38cb2c"},
- {file = "polars-1.38.1.tar.gz", hash = "sha256:803a2be5344ef880ad625addfb8f641995cfd777413b08a10de0897345778239"},
+ {file = "polars-1.39.0-py3-none-any.whl", hash = "sha256:4d1198b41bc47561673d9f54d0f595125202a3f53e3502821802958d3e60efe9"},
+ {file = "polars-1.39.0.tar.gz", hash = "sha256:e63a25fb7682ae660e36067915a7c71a653b17f82308a8eb67a190a80daf0710"},
]
[package.dependencies]
-polars-runtime-32 = "1.38.1"
+polars-runtime-32 = "1.39.0"
[package.extras]
adbc = ["adbc-driver-manager[dbapi]", "adbc-driver-sqlite[dbapi]"]
@@ -2242,8 +2147,8 @@ plot = ["altair (>=5.4.0)"]
polars-cloud = ["polars_cloud (>=0.4.0)"]
pyarrow = ["pyarrow (>=7.0.0)"]
pydantic = ["pydantic"]
-rt64 = ["polars-runtime-64 (==1.38.1)"]
-rtcompat = ["polars-runtime-compat (==1.38.1)"]
+rt64 = ["polars-runtime-64 (==1.39.0)"]
+rtcompat = ["polars-runtime-compat (==1.39.0)"]
sqlalchemy = ["polars[pandas]", "sqlalchemy"]
style = ["great-tables (>=0.8.0)"]
timezone = ["tzdata ; platform_system == \"Windows\""]
@@ -2252,21 +2157,21 @@ xlsxwriter = ["xlsxwriter"]
[[package]]
name = "polars-runtime-32"
-version = "1.38.1"
+version = "1.39.0"
description = "Blazingly fast DataFrame library"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
- {file = "polars_runtime_32-1.38.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:18154e96044724a0ac38ce155cf63aa03c02dd70500efbbf1a61b08cadd269ef"},
- {file = "polars_runtime_32-1.38.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c49acac34cc4049ed188f1eb67d6ff3971a39b4af7f7b734b367119970f313ac"},
- {file = "polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fef2ef2626a954e010e006cc8e4de467ecf32d08008f130cea1c78911f545323"},
- {file = "polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8a5f7a8125e2d50e2e060296551c929aec09be23a9edcb2b12ca923f555a5ba"},
- {file = "polars_runtime_32-1.38.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:10d19cd9863e129273b18b7fcaab625b5c8143c2d22b3e549067b78efa32e4fa"},
- {file = "polars_runtime_32-1.38.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61e8d73c614b46a00d2f853625a7569a2e4a0999333e876354ac81d1bf1bb5e2"},
- {file = "polars_runtime_32-1.38.1-cp310-abi3-win_amd64.whl", hash = "sha256:08c2b3b93509c1141ac97891294ff5c5b0c548a373f583eaaea873a4bf506437"},
- {file = "polars_runtime_32-1.38.1-cp310-abi3-win_arm64.whl", hash = "sha256:6d07d0cc832bfe4fb54b6e04218c2c27afcfa6b9498f9f6bbf262a00d58cc7c4"},
- {file = "polars_runtime_32-1.38.1.tar.gz", hash = "sha256:04f20ed1f5c58771f34296a27029dc755a9e4b1390caeaef8f317e06fdfce2ec"},
+ {file = "polars_runtime_32-1.39.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4a4bc06ca97238d963979e3f888fbb500ee607f03cefe43a9062381e259503e2"},
+ {file = "polars_runtime_32-1.39.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e9914b9e168634bc21d07ee03b8fa92d0aaa8ac7b2bb1c9e2f1f78622aa1b8f4"},
+ {file = "polars_runtime_32-1.39.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ded58f1c28e17ecbff8625cb1ad93016761260348acb79b1a4cd077970e89e5"},
+ {file = "polars_runtime_32-1.39.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b82c872b25ef6628462f90f1b6b3950779aee36889e83b3693d0a69684d3d86a"},
+ {file = "polars_runtime_32-1.39.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4a0e9d6b56362f3ba1a33d0538ae14c9b9a8e0fb835f86abfc82fa7b2c7d89c9"},
+ {file = "polars_runtime_32-1.39.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0daea3919661ba672b00bd01b5547cd29bb6414732457abb72cbc75103cf3c90"},
+ {file = "polars_runtime_32-1.39.0-cp310-abi3-win_amd64.whl", hash = "sha256:d6e9d1cf264aacfe5bf03241c04ef435d0f9cfec3fbe079acc3a7328a737961a"},
+ {file = "polars_runtime_32-1.39.0-cp310-abi3-win_arm64.whl", hash = "sha256:d69abde5f148566860bbe910010847bd7791e72f7c8063a4d2c462246a33a72a"},
+ {file = "polars_runtime_32-1.39.0.tar.gz", hash = "sha256:f5aabed8c7318fcad5173e83bee385445f54b5f8c83b1ec9eab78bdffa293141"},
]
[[package]]
@@ -2438,32 +2343,6 @@ files = [
{file = "pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019"},
]
-[[package]]
-name = "pydeseq2"
-version = "0.5.4"
-description = "A python implementation of DESeq2."
-optional = false
-python-versions = ">=3.11"
-groups = ["dev"]
-files = [
- {file = "pydeseq2-0.5.4-py3-none-any.whl", hash = "sha256:690458824f1c4df0d13dbf7e5bdc1298f6dfb444b04a6e2aef9e7d3f21ba30dd"},
- {file = "pydeseq2-0.5.4.tar.gz", hash = "sha256:49d6f47840b5444ea2b69be7857c6c4e58f369066a0fb24bc52f7d3a62bbd92c"},
-]
-
-[package.dependencies]
-anndata = ">=0.11.0"
-formulaic = ">=1.0.2"
-formulaic-contrasts = ">=0.2.0"
-matplotlib = ">=3.9.0"
-numpy = ">=2.0.0"
-pandas = ">=2.2.0"
-scikit-learn = ">=1.4.0"
-scipy = ">=1.12.0"
-
-[package.extras]
-dev = ["coverage", "mypy (==1.18.2)", "numpydoc", "pandas-stubs", "pre-commit (>=2.16.0)", "pytest (>=8.4.0)", "ruff (==0.14.0)", "scipy-stubs"]
-doc = ["docutils", "gitpython (>=3.1.42)", "ipython", "jupyter", "myst-parser", "sphinx (>=8.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx-click", "sphinx-gallery", "sphinx-rtd-theme", "sphinxcontrib-bibtex", "sphinxcontrib-googleanalytics (>=0.5)", "texttable"]
-
[[package]]
name = "pygal"
version = "3.1.0"
@@ -2649,14 +2528,14 @@ six = ">=1.5"
[[package]]
name = "python-discovery"
-version = "1.1.1"
+version = "1.1.3"
description = "Python interpreter discovery"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
- {file = "python_discovery-1.1.1-py3-none-any.whl", hash = "sha256:69f11073fa2392251e405d4e847d60ffffd25fd762a0dc4d1a7d6b9c3f79f1a3"},
- {file = "python_discovery-1.1.1.tar.gz", hash = "sha256:584c08b141c5b7029f206b4e8b78b1a1764b22121e21519b89dec56936e95b0a"},
+ {file = "python_discovery-1.1.3-py3-none-any.whl", hash = "sha256:90e795f0121bc84572e737c9aa9966311b9fde44ffb88a5953b3ec9b31c6945e"},
+ {file = "python_discovery-1.1.3.tar.gz", hash = "sha256:7acca36e818cd88e9b2ba03e045ad7e93e1713e29c6bbfba5d90202310b7baa5"},
]
[package.dependencies]
@@ -2853,10 +2732,10 @@ umap-learn = ">=0.5.6"
[package.extras]
bbknn = ["bbknn"]
-dask = ["anndata[dask]", "dask[array] (>=2022.09.2)"]
-dask-ml = ["anndata[dask]", "dask-ml", "dask[array] (>=2022.09.2)"]
+dask = ["anndata[dask]", "dask[array] (>=2022.9.2)"]
+dask-ml = ["anndata[dask]", "dask-ml", "dask[array] (>=2022.9.2)"]
dev = ["scipy-stubs", "towncrier"]
-doc = ["anndata[dask]", "dask-ml", "dask[array] (>=2022.09.2)", "igraph", "igraph (>=0.10.8)", "ipython (>=7.20)", "leidenalg (>=0.9.0)", "myst-nb (>=1)", "myst-parser (>=2)", "nbsphinx (>=0.9)", "sam-algorithm", "scanpydoc (>=0.15.3)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=1.25.2)", "sphinx-book-theme (>=1.1.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues (>=5.0.1)", "sphinx-tabs", "sphinxcontrib-bibtex", "sphinxext-opengraph"]
+doc = ["anndata[dask]", "dask-ml", "dask[array] (>=2022.9.2)", "igraph", "igraph (>=0.10.8)", "ipython (>=7.20)", "leidenalg (>=0.9.0)", "myst-nb (>=1)", "myst-parser (>=2)", "nbsphinx (>=0.9)", "sam-algorithm", "scanpydoc (>=0.15.3)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=1.25.2)", "sphinx-book-theme (>=1.1.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues (>=5.0.1)", "sphinx-tabs", "sphinxcontrib-bibtex", "sphinxext-opengraph"]
harmony = ["harmonypy"]
leiden = ["igraph (>=0.10.8)", "leidenalg (>=0.9.0)"]
louvain = ["igraph", "louvain (>=0.8.2)", "setuptools (<81)"]
@@ -2866,7 +2745,7 @@ rapids = ["cudf (>=0.9)", "cugraph (>=0.9)", "cuml (>=0.9)"]
scanorama = ["scanorama"]
scrublet = ["scikit-image"]
skmisc = ["scikit-misc (>=0.1.4)"]
-test = ["anndata[dask]", "dask-ml", "dask[array] (>=2022.09.2)", "igraph", "igraph (>=0.10.8)", "leidenalg (>=0.9.0)", "louvain (>=0.8.2)", "pytest (>=8.2.2)", "pytest-cov", "pytest-mock", "pytest-randomly", "pytest-rerunfailures", "pytest-xdist[psutil]", "scikit-image", "scikit-misc (>=0.1.4)", "setuptools (<81)", "tuna", "zarr (<3)"]
+test = ["anndata[dask]", "dask-ml", "dask[array] (>=2022.9.2)", "igraph", "igraph (>=0.10.8)", "leidenalg (>=0.9.0)", "louvain (>=0.8.2)", "pytest (>=8.2.2)", "pytest-cov", "pytest-mock", "pytest-randomly", "pytest-rerunfailures", "pytest-xdist[psutil]", "scikit-image", "scikit-misc (>=0.1.4)", "setuptools (<81)", "tuna", "zarr (<3)"]
test-min = ["pytest (>=8.2.2)", "pytest-cov", "pytest-mock", "pytest-randomly", "pytest-rerunfailures", "pytest-xdist[psutil]", "tuna"]
[[package]]
@@ -3084,21 +2963,6 @@ dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest
docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx (<6.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues"]
stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"]
-[[package]]
-name = "session-info"
-version = "1.0.1"
-description = "session_info outputs version information for modules loaded in the current session, Python, and the OS."
-optional = false
-python-versions = ">=3.6"
-groups = ["dev"]
-files = [
- {file = "session_info-1.0.1-py3-none-any.whl", hash = "sha256:451d191e51816070b9f21a6ff3f6eb5d6015ae2738e8db63ac4e6398260a5838"},
- {file = "session_info-1.0.1.tar.gz", hash = "sha256:d71950d5a8ce7f7f7d5e86aa208c148c4e50b5440b77d5544d422b48e4f3ed41"},
-]
-
-[package.dependencies]
-stdlib_list = "*"
-
[[package]]
name = "session-info2"
version = "0.4"
@@ -3434,35 +3298,16 @@ build = ["cython (>=3.0.10)"]
develop = ["colorama", "cython (>=3.0.10)", "cython (>=3.0.10,<4)", "flake8", "isort", "jinja2", "joblib", "matplotlib (>=3)", "pytest (>=7.3.0,<8)", "pytest-cov", "pytest-randomly", "pytest-xdist", "pywinpty ; os_name == \"nt\"", "setuptools_scm[toml] (>=8.0,<9.0)"]
docs = ["ipykernel", "jupyter_client", "matplotlib", "nbconvert", "nbformat", "numpydoc", "pandas-datareader", "sphinx"]
-[[package]]
-name = "stdlib-list"
-version = "0.12.0"
-description = "A list of Python Standard Libraries (2.7 through 3.14)."
-optional = false
-python-versions = ">=3.9"
-groups = ["dev"]
-files = [
- {file = "stdlib_list-0.12.0-py3-none-any.whl", hash = "sha256:df2d11e97f53812a1756fb5510393a11e3b389ebd9239dc831c7f349957f62f2"},
- {file = "stdlib_list-0.12.0.tar.gz", hash = "sha256:517824f27ee89e591d8ae7c1dd9ff34f672eae50ee886ea31bb8816d77535675"},
-]
-
-[package.extras]
-dev = ["build", "stdlib-list[doc,lint,test]"]
-doc = ["furo", "sphinx"]
-lint = ["mypy", "ruff"]
-support = ["sphobjinv"]
-test = ["coverage[toml]", "pytest", "pytest-cov"]
-
[[package]]
name = "textual"
-version = "8.0.2"
+version = "8.1.1"
description = "Modern Text User Interface framework"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["dev"]
files = [
- {file = "textual-8.0.2-py3-none-any.whl", hash = "sha256:4ceadbe0e8a30eb80f9995000f4d031f711420a31b02da38f3482957b7c50ce4"},
- {file = "textual-8.0.2.tar.gz", hash = "sha256:7b342f3ee9a5f2f1bd42d7b598cae00ff1275da68536769510db4b7fe8cabf5d"},
+ {file = "textual-8.1.1-py3-none-any.whl", hash = "sha256:6712f96e335cd782e76193dee16b9c8875fe0699d923bc8d3f1228fd23e773a6"},
+ {file = "textual-8.1.1.tar.gz", hash = "sha256:eef0256a6131f06a20ad7576412138c1f30f92ddeedd055953c08d97044bc317"},
]
[package.dependencies]
@@ -3655,109 +3500,6 @@ files = [
[package.extras]
dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"]
-[[package]]
-name = "wrapt"
-version = "2.1.2"
-description = "Module for decorators, wrappers and monkey patching."
-optional = false
-python-versions = ">=3.9"
-groups = ["dev"]
-files = [
- {file = "wrapt-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a86d99a14f76facb269dc148590c01aaf47584071809a70da30555228158c"},
- {file = "wrapt-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a819e39017f95bf7aede768f75915635aa8f671f2993c036991b8d3bfe8dbb6f"},
- {file = "wrapt-2.1.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5681123e60aed0e64c7d44f72bbf8b4ce45f79d81467e2c4c728629f5baf06eb"},
- {file = "wrapt-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8b28e97a44d21836259739ae76284e180b18abbb4dcfdff07a415cf1016c3e"},
- {file = "wrapt-2.1.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef91c95a50596fcdc31397eb6955476f82ae8a3f5a8eabdc13611b60ee380ba"},
- {file = "wrapt-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dad63212b168de8569b1c512f4eac4b57f2c6934b30df32d6ee9534a79f1493f"},
- {file = "wrapt-2.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d307aa6888d5efab2c1cde09843d48c843990be13069003184b67d426d145394"},
- {file = "wrapt-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c87cf3f0c85e27b3ac7d9ad95da166bf8739ca215a8b171e8404a2d739897a45"},
- {file = "wrapt-2.1.2-cp310-cp310-win32.whl", hash = "sha256:d1c5fea4f9fe3762e2b905fdd67df51e4be7a73b7674957af2d2ade71a5c075d"},
- {file = "wrapt-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:d8f7740e1af13dff2684e4d56fe604a7e04d6c94e737a60568d8d4238b9a0c71"},
- {file = "wrapt-2.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:1c6cc827c00dc839350155f316f1f8b4b0c370f52b6a19e782e2bda89600c7dc"},
- {file = "wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb"},
- {file = "wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d"},
- {file = "wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894"},
- {file = "wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842"},
- {file = "wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8"},
- {file = "wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6"},
- {file = "wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9"},
- {file = "wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15"},
- {file = "wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b"},
- {file = "wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1"},
- {file = "wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a"},
- {file = "wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9"},
- {file = "wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748"},
- {file = "wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e"},
- {file = "wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8"},
- {file = "wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c"},
- {file = "wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c"},
- {file = "wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1"},
- {file = "wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2"},
- {file = "wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0"},
- {file = "wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63"},
- {file = "wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf"},
- {file = "wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b"},
- {file = "wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e"},
- {file = "wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb"},
- {file = "wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca"},
- {file = "wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267"},
- {file = "wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f"},
- {file = "wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8"},
- {file = "wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413"},
- {file = "wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6"},
- {file = "wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1"},
- {file = "wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf"},
- {file = "wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b"},
- {file = "wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18"},
- {file = "wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d"},
- {file = "wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015"},
- {file = "wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92"},
- {file = "wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf"},
- {file = "wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67"},
- {file = "wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a"},
- {file = "wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd"},
- {file = "wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f"},
- {file = "wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679"},
- {file = "wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9"},
- {file = "wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9"},
- {file = "wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e"},
- {file = "wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c"},
- {file = "wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a"},
- {file = "wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90"},
- {file = "wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586"},
- {file = "wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19"},
- {file = "wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508"},
- {file = "wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04"},
- {file = "wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575"},
- {file = "wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb"},
- {file = "wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22"},
- {file = "wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596"},
- {file = "wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044"},
- {file = "wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b"},
- {file = "wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf"},
- {file = "wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2"},
- {file = "wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3"},
- {file = "wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7"},
- {file = "wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5"},
- {file = "wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00"},
- {file = "wrapt-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5e0fa9cc32300daf9eb09a1f5bdc6deb9a79defd70d5356ba453bcd50aef3742"},
- {file = "wrapt-2.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:710f6e5dfaf6a5d5c397d2d6758a78fecd9649deb21f1b645f5b57a328d63050"},
- {file = "wrapt-2.1.2-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:305d8a1755116bfdad5dda9e771dcb2138990a1d66e9edd81658816edf51aed1"},
- {file = "wrapt-2.1.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0d8fc30a43b5fe191cf2b1a0c82bab2571dadd38e7c0062ee87d6df858dd06e"},
- {file = "wrapt-2.1.2-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a5d516e22aedb7c9c1d47cba1c63160b1a6f61ec2f3948d127cd38d5cfbb556f"},
- {file = "wrapt-2.1.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:45914e8efbe4b9d5102fcf0e8e2e3258b83a5d5fba9f8f7b6d15681e9d29ffe0"},
- {file = "wrapt-2.1.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:478282ebd3795a089154fb16d3db360e103aa13d3b2ad30f8f6aac0d2207de0e"},
- {file = "wrapt-2.1.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3756219045f73fb28c5d7662778e4156fbd06cf823c4d2d4b19f97305e52819c"},
- {file = "wrapt-2.1.2-cp39-cp39-win32.whl", hash = "sha256:b8aefb4dbb18d904b96827435a763fa42fc1f08ea096a391710407a60983ced8"},
- {file = "wrapt-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:e5aeab8fe15c3dff75cfee94260dcd9cded012d4ff06add036c28fae7718593b"},
- {file = "wrapt-2.1.2-cp39-cp39-win_arm64.whl", hash = "sha256:f069e113743a21a3defac6677f000068ebb931639f789b5b226598e247a4c89e"},
- {file = "wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8"},
- {file = "wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e"},
-]
-
-[package.extras]
-dev = ["pytest", "setuptools"]
-
[[package]]
name = "zarr"
version = "3.1.5"
@@ -3810,4 +3552,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<4.0"
-content-hash = "3a483b0ac83646f2970c37d2873099e6612a235de6ec404e3567a3957ad1f634"
+content-hash = "848bc1ea60cf3ec8562bec618c4e17f0b0eaacf2808e94e3835e41904f15a480"
diff --git a/pyproject.toml b/pyproject.toml
index 4a81a55..5557658 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "illico"
-version = "0.3.0"
+version = "0.4.0"
description = "Fast asymptotic mannwhitney-u test"
authors = [
{name = "remydubois",email = "remydubois14@gmail.com"}
@@ -9,7 +9,7 @@ readme = "README.md"
requires-python = ">=3.11,<4.0"
dependencies = [
"anndata (>=0.11)",
- "numba (>=0.63.1,<0.64.0)",
+ "numba (>=0.63.1)",
"pandas (>=2.3.3,<3.0.0)",
"scipy (>=1.16.3,<2.0.0)",
"tqdm (>=4.67.1,<5.0.0)",
@@ -40,7 +40,7 @@ dev = [
"memory-profiler (>=0.61.0,<0.62.0)",
"pytest-regex (>=0.2.0,<0.3.0)",
"memray (>=1.19.1,<2.0.0)",
- "pdex (>=0.1.27,<0.2.0)",
+ "pdex (>=0.2.0)",
"ipdb (>=0.13.13,<0.14.0)",
]
doc = [
diff --git a/src/dense_ovo.rs b/src/dense_ovo.rs
index dee6a53..6366e39 100644
--- a/src/dense_ovo.rs
+++ b/src/dense_ovo.rs
@@ -20,26 +20,23 @@ pub fn dense_ovo_kernel(
alternative: &String,
mut p_values: ArrayViewMut1,
mut u_stats: ArrayViewMut1,
+ mut zscores: ArrayViewMut1,
) -> Result<(), String> {
let n_ctrl = sorted_controls.dim().0 as f64;
let n_cols = sorted_controls.dim().1 as f64;
let n_tgt = sorted_tgt.dim().0 as f64;
- // declare placeholder for results
- // let mut u_stats = Array1::zeros(n_cols as usize);
- // let mut p_values = Array1::zeros(n_cols as usize);
-
let n = n_ctrl + n_tgt;
let mu = n_ctrl * n_tgt / 2.;
- let u_base = n_ctrl as f64 * n_tgt + n_tgt * (n_tgt + 1.) / 2.;
+ let remainder = &n_tgt * (&n_tgt + 1.) / 2.;
for j in 0..n_cols as usize {
let (rs, ts) = rank_sum_and_ties(sorted_controls.column(j), sorted_tgt.column(j));
- let u = u_base - rs;
+ let u = rs - remainder;
let contin_corr = if use_continuity { 0.5 } else { 0. };
- let pv = compute_pvalue(
+ let (pv, zscore) = compute_pvalue(
n_ctrl,
n_tgt,
n,
@@ -51,6 +48,7 @@ pub fn dense_ovo_kernel(
)?;
p_values[j] = pv;
u_stats[j] = u;
+ zscores[j] = zscore;
}
return Ok(());
@@ -68,6 +66,7 @@ pub fn dense_ovo_kernel_rust<'py>(
let sorted_controls = sorted_controls.as_array();
let mut p_values = Array1::zeros(sorted_controls.dim().1);
let mut u_stats = Array1::zeros(sorted_controls.dim().1);
+ let mut zscores = Array1::zeros(sorted_controls.dim().1);
_ = dense_ovo_kernel(
sorted_controls,
sorted_tgt.as_array(),
@@ -76,6 +75,7 @@ pub fn dense_ovo_kernel_rust<'py>(
&alternative,
p_values.view_mut(),
u_stats.view_mut(),
+ zscores.view_mut(),
)
.map_err(PyValueError::new_err)?;
return Ok((
@@ -92,8 +92,9 @@ pub fn dense_ovo_over_contiguous_col_chunk(
is_log1p: bool,
use_continuity: bool,
tie_correct: bool,
+ exp_post_agg: bool,
alternative: &String,
-) -> Result<(Array2, Array2, Array2), String> {
+) -> Result<(Array2, Array2, Array2, Array2), String> {
if chunk_lb >= chunk_ub {
return Err(format!(
"Chunking error: lower bound ({}) is not smaller than upper bound ({}).",
@@ -122,11 +123,13 @@ pub fn dense_ovo_over_contiguous_col_chunk(
let n_cols = chunk_ub - chunk_lb;
let mut p_values = Array2::::zeros((n_groups, n_cols));
let mut u_stats = Array2::::zeros((n_groups, n_cols));
+ let mut zscores = Array2::::zeros((n_groups, n_cols));
for i in 0..n_groups {
if i as isize == grpc.encoded_ref_group {
p_values.row_mut(i).fill(1.);
u_stats.row_mut(i).fill(-1.);
+ zscores.row_mut(i).fill(0.);
} else {
// Grab indices of the target group's cells
let tgt_indices = grpc.indices.slice(s![grpc.indptr[i]..grpc.indptr[i + 1]]);
@@ -144,6 +147,7 @@ pub fn dense_ovo_over_contiguous_col_chunk(
alternative,
p_values.row_mut(i),
u_stats.row_mut(i),
+ zscores.row_mut(i),
)?;
// Fill in placeholders
@@ -151,12 +155,17 @@ pub fn dense_ovo_over_contiguous_col_chunk(
// u_stats.row_mut(i).assign(&u);
}
}
- let fc = dense_fold_change(x.slice(s![.., chunk_lb..chunk_ub]), &grpc, is_log1p, false)?;
- return Ok((p_values, u_stats, fc));
+ let fc = dense_fold_change(
+ x.slice(s![.., chunk_lb..chunk_ub]),
+ &grpc,
+ is_log1p,
+ exp_post_agg,
+ )?;
+ return Ok((p_values, u_stats, zscores, fc));
}
macro_rules! run_ovo_branch {
- ($py:expr, $x:expr, $chunk_lb:expr, $chunk_ub:expr, $grpc:expr, $is_log1p:expr, $use_continuity:expr, $tie_correct:expr, $alternative:expr, $dt:ty) => {{
+ ($py:expr, $x:expr, $chunk_lb:expr, $chunk_ub:expr, $grpc:expr, $is_log1p:expr, $use_continuity:expr, $tie_correct:expr, $exp_post_agg:expr, $alternative:expr, $dt:ty) => {{
let x_pyarray = $x.extract::>()?;
let x = x_pyarray.as_array();
$py.detach(|| {
@@ -168,6 +177,7 @@ macro_rules! run_ovo_branch {
$is_log1p,
$use_continuity,
$tie_correct,
+ $exp_post_agg,
&$alternative,
)
})
@@ -177,6 +187,7 @@ macro_rules! run_ovo_branch {
type PyArr2<'py> = Bound<'py, PyArray2>;
+#[rustfmt::skip]
#[pyfunction]
pub fn dense_ovo_over_contiguous_col_chunk_rust<'py>(
py: Python<'py>,
@@ -188,35 +199,23 @@ pub fn dense_ovo_over_contiguous_col_chunk_rust<'py>(
is_log1p: bool,
use_continuity: bool,
tie_correct: bool,
+ exp_post_agg: bool,
alternative: String,
-) -> PyResult<(PyArr2<'py>, PyArr2<'py>, Bound<'py, PyArray2>)> {
+) -> PyResult<(
+ PyArr2<'py>,
+ PyArr2<'py>,
+ PyArr2<'py>,
+ Bound<'py, PyArray2>,
+)> {
let grpc = grpc.as_group_container();
// let x = x.as_array();
let data_dtype: String = x.getattr("dtype")?.getattr("str")?.extract()?;
- let (p_values, u_stats, fc) = match data_dtype.as_str() {
+ let (p_values, u_stats, zscores, fc) = match data_dtype.as_str() {
"f32" | " run_ovo_branch!(
- py,
- x,
- chunk_lb,
- chunk_ub,
- grpc,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f32
+ py, x, chunk_lb, chunk_ub, grpc, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f32
),
"f64" | " run_ovo_branch!(
- py,
- x,
- chunk_lb,
- chunk_ub,
- grpc,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f64
+ py, x, chunk_lb, chunk_ub, grpc, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f64
),
_ => Err(PyValueError::new_err(format!(
"Input data should be f32 or f64, received {}",
@@ -226,6 +225,7 @@ pub fn dense_ovo_over_contiguous_col_chunk_rust<'py>(
return Ok((
PyArray2::from_array(py, &p_values),
PyArray2::from_array(py, &u_stats),
+ PyArray2::from_array(py, &zscores),
PyArray2::from_array(py, &fc),
));
}
diff --git a/src/dense_ovr.rs b/src/dense_ovr.rs
index 722f5bf..b18da26 100644
--- a/src/dense_ovr.rs
+++ b/src/dense_ovr.rs
@@ -18,14 +18,15 @@ pub fn dense_ovr_kernel(
is_log1p: bool,
use_continuity: bool,
tie_correct: bool,
- alternative: String,
exp_post_agg: bool,
-) -> Result<(Array2, Array2, Array2), String> {
+ alternative: String,
+) -> Result<(Array2, Array2, Array2, Array2), String> {
let chunk = chunk_and_fortranize(&x, chunk_lb, chunk_ub, None)?;
// Now compute stats and pvalues
let n_groups = grpc.counts.len();
let mut p_values = Array2::zeros((n_groups, chunk_ub - chunk_lb));
let mut u_stats = Array2::zeros((n_groups, chunk_ub - chunk_lb));
+ let mut zscores = Array2::zeros((n_groups, chunk_ub - chunk_lb));
// Compute ranksum and tie sum
let mut ranksums: Array2 = Array2::zeros((n_groups, chunk_ub - chunk_lb));
@@ -45,11 +46,11 @@ pub fn dense_ovr_kernel(
let n_ref = grpc.counts.mapv(|v| n - v as f64);
let n_tgt = grpc.counts.mapv(|x| x as f64);
let mu = &n_ref * &n_tgt / 2.;
- let u_base = &n_ref * &n_tgt + &n_tgt * (&n_tgt.map(|x| x + 1.)) / 2.;
+ let remainder = &n_tgt * (&n_tgt.map(|x| x + 1.)) / 2.;
for i in 0..n_groups {
for j in 0..chunk.dim().1 {
- u_stats[[i, j]] = u_base[i] - ranksums[[i, j]];
- let pv = compute_pvalue(
+ u_stats[[i, j]] = ranksums[[i, j]] - remainder[i];
+ let (pv, z) = compute_pvalue(
n_ref[i],
n_tgt[i],
n,
@@ -59,18 +60,19 @@ pub fn dense_ovr_kernel(
if use_continuity { 0.5 } else { 0. },
&alternative,
)?;
- p_values[[i, j]] = pv
+ p_values[[i, j]] = pv;
+ zscores[[i, j]] = z
}
}
// TODO: dense_fold_change could actually take an normal array, not a view, as we build and own it with chunk_and_fortranize
let fold_change = dense_fold_change(chunk.view(), &grpc, is_log1p, exp_post_agg)?;
- Ok((p_values, u_stats, fold_change))
+ Ok((p_values, u_stats, zscores, fold_change))
}
macro_rules! run_ovr_branch {
- ($py:expr, $x:expr, $chunk_lb:expr, $chunk_ub:expr, $grpc:expr, $is_log1p:expr, $use_continuity:expr, $tie_correct:expr, $alternative:expr, $dt:ty) => {{
+ ($py:expr, $x:expr, $chunk_lb:expr, $chunk_ub:expr, $grpc:expr, $is_log1p:expr, $use_continuity:expr, $tie_correct:expr, $exp_post_agg:expr, $alternative:expr, $dt:ty) => {{
let x_pyarray = $x.extract::>()?;
let x = x_pyarray.as_array();
$py.detach(|| {
@@ -82,8 +84,8 @@ macro_rules! run_ovr_branch {
$is_log1p,
$use_continuity,
$tie_correct,
+ $exp_post_agg,
$alternative,
- false,
)
})
.map_err(PyValueError::new_err)
@@ -92,6 +94,7 @@ macro_rules! run_ovr_branch {
type PyArr2<'py> = Bound<'py, PyArray2>;
+#[rustfmt::skip]
#[pyfunction]
pub fn dense_ovr_over_contiguous_col_chunk_rust<'py>(
py: Python<'py>,
@@ -103,36 +106,23 @@ pub fn dense_ovr_over_contiguous_col_chunk_rust<'py>(
is_log1p: bool,
use_continuity: bool,
tie_correct: bool,
+ exp_post_agg: bool,
alternative: String,
-) -> PyResult<(PyArr2<'py>, PyArr2<'py>, Bound<'py, PyArray2>)> {
- // let x = x.as_array();
+) -> PyResult<(
+ PyArr2<'py>,
+ PyArr2<'py>,
+ PyArr2<'py>,
+ Bound<'py, PyArray2>,
+)> {
let grpc = grpc.as_group_container();
let data_dtype: String = x.getattr("dtype")?.getattr("str")?.extract()?;
- let (p, u, fc) = match data_dtype.as_str() {
+ let (p, u, z, fc) = match data_dtype.as_str() {
"f32" | " run_ovr_branch!(
- py,
- x,
- chunk_lb,
- chunk_ub,
- grpc,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f32
+ py, x, chunk_lb, chunk_ub, grpc, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f32
),
"f64" | " run_ovr_branch!(
- py,
- x,
- chunk_lb,
- chunk_ub,
- grpc,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f64
+ py, x, chunk_lb, chunk_ub, grpc, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f64
),
_ => Err(PyValueError::new_err(format!(
"Input data should be f32 or f64, received {}",
@@ -142,6 +132,7 @@ pub fn dense_ovr_over_contiguous_col_chunk_rust<'py>(
Ok((
PyArray2::from_array(py, &p),
PyArray2::from_array(py, &u),
+ PyArray2::from_array(py, &z),
PyArray2::from_array(py, &fc),
))
}
diff --git a/src/sparse/csc.rs b/src/sparse/csc.rs
index a24b98e..2d146a8 100644
--- a/src/sparse/csc.rs
+++ b/src/sparse/csc.rs
@@ -155,6 +155,7 @@ pub fn csc_fold_change(
x: &OwnedCSCMatrix,
grpc: &GroupContainer,
is_log1p: bool,
+ exp_post_agg: bool,
) -> Result, String> {
let mut summed_expr = Array2::zeros((grpc.counts.len(), x.shape.1));
@@ -165,12 +166,16 @@ pub fn csc_fold_change(
let row_idx = x.indices[i].to_usize();
let group_idx = grpc.encoded_groups[row_idx];
let val = x.data[i].to_f32();
- summed_expr[[group_idx, j]] += if is_log1p { val.exp_m1() } else { val };
+ summed_expr[[group_idx, j]] += if is_log1p && !exp_post_agg {
+ val.exp_m1()
+ } else {
+ val
+ };
}
}
// println!("Summed expr: {:?}", summed_expr);
- let fc = fold_change_from_summed_expr(summed_expr, &grpc, false)?;
+ let fc = fold_change_from_summed_expr(summed_expr, &grpc, exp_post_agg && is_log1p)?;
Ok(fc)
}
diff --git a/src/sparse/csr.rs b/src/sparse/csr.rs
index 227a090..f357ae8 100644
--- a/src/sparse/csr.rs
+++ b/src/sparse/csr.rs
@@ -80,6 +80,7 @@ pub fn csr_fold_change(
x: &OwnedCSRMatrix,
grpc: &GroupContainer,
is_log1p: bool,
+ exp_post_agg: bool,
) -> Result, String> {
// Compute summed expression
let mut summed_expr = Array2::::zeros((grpc.counts.len(), x.shape.1));
@@ -91,10 +92,14 @@ pub fn csr_fold_change(
let col_idx = x.indices[pointer].to_usize();
let group_idx = grpc.encoded_groups[i];
let val = x.data[pointer].to_f32();
- summed_expr[[group_idx, col_idx]] += if is_log1p { val.exp_m1() } else { val };
+ summed_expr[[group_idx, col_idx]] += if is_log1p && !exp_post_agg {
+ val.exp_m1()
+ } else {
+ val
+ };
}
}
- let fc = fold_change_from_summed_expr(summed_expr, &grpc, false)?;
+ let fc = fold_change_from_summed_expr(summed_expr, &grpc, exp_post_agg && is_log1p)?;
Ok(fc)
}
@@ -133,13 +138,9 @@ impl<'py, D: SparseFloat, I: SparseIndex> CSRMatrix<'py, D, I> {
) -> Result, String> {
let mut bounds = Array2::zeros((self.shape.0, 2));
let mut n_nzeros = Array1::zeros(self.shape.0 + 1);
+ let indices = self.indices.as_slice().ok_or_else(|| format!("Error"))?;
for i in 0..self.shape.0 {
- let col_indices = self
- .indices
- .slice(s![self.indptr[i].to_usize()..self.indptr[i + 1].to_usize()]);
- let col_indices = col_indices
- .as_slice()
- .ok_or_else(|| format!("CSR indices should a C-contiguous array."))?;
+ let col_indices = &indices[self.indptr[i].to_usize()..self.indptr[i + 1].to_usize()];
let cb = searchsorted_left(col_indices, chunk_lb);
let rb = searchsorted_left(col_indices, chunk_ub);
@@ -156,8 +157,10 @@ impl<'py, D: SparseFloat, I: SparseIndex> CSRMatrix<'py, D, I> {
}
// Now retrieve data and indices for the chunk, across all rows
- let mut new_data = Array1::zeros(n_nzeros.sum());
- let mut new_indices = Array1::zeros(n_nzeros.sum());
+ let nnz_total = indptr[indptr.len() - 1] as usize;
+ let mut new_data = vec![D::zero(); nnz_total];
+ let mut new_indices = vec![I::zero(); nnz_total];
+ let chunk_lb_gen = I::from(chunk_lb).unwrap();
for i in 0..self.shape.0 {
let org_start = self.indptr[i].to_usize();
let (chunk_start, chunk_end) =
@@ -165,24 +168,19 @@ impl<'py, D: SparseFloat, I: SparseIndex> CSRMatrix<'py, D, I> {
if chunk_start == chunk_end {
continue;
}
- // Grab data and indices corresponding to the chunk, for this row
- let data_chunk = self.data.slice(s![chunk_start..chunk_end]).to_owned();
- // let mut indices_chunk = self.indices.slice(s![chunk_start..chunk_end]).to_owned();
- // indices_chunk -= chunk_lb as i32; // offset the col indices
- let indices_chunk = self
- .indices
- .slice(s![chunk_start..chunk_end])
- .mapv(|x| I::from(x.to_usize() - chunk_lb).unwrap());
- // Now assign chunks into placeholders
- let mut data_placeholder = new_data.slice_mut(s![indptr[i]..indptr[i + 1]]);
- let mut indices_placeholder = new_indices.slice_mut(s![indptr[i]..indptr[i + 1]]);
- data_placeholder.assign(&data_chunk);
- indices_placeholder.assign(&indices_chunk);
+ let data_chunk = self.data.slice(s![chunk_start..chunk_end]).to_vec();
+ new_data[indptr[i] as usize..indptr[i + 1] as usize].copy_from_slice(&data_chunk);
+ let indices_chunk = self.indices.slice(s![chunk_start..chunk_end]).to_vec();
+ // new_indices[indptr[i] as usize ..indptr[i + 1] as usize].copy_from_slice(&indices_chunk);
+ for (k, i) in (indptr[i] as usize..indptr[i + 1] as usize).enumerate() {
+ new_indices[i] = indices_chunk[k] - chunk_lb_gen
+ }
}
+
Ok(OwnedCSRMatrix {
- data: new_data,
- indices: new_indices,
+ data: Array1::from_vec(new_data),
+ indices: Array1::from_vec(new_indices),
indptr: indptr.mapv(|x| I::from(x).unwrap()),
shape: (self.shape.0, chunk_ub - chunk_lb),
})
diff --git a/src/sparse_ovo.rs b/src/sparse_ovo.rs
index 50f31d4..2c436f8 100644
--- a/src/sparse_ovo.rs
+++ b/src/sparse_ovo.rs
@@ -19,6 +19,7 @@ pub fn single_group_sparse_ovo_mwu_kernel(
alternative: &String,
mut p_values: ArrayViewMut1,
mut u_stats: ArrayViewMut1,
+ mut zscores: ArrayViewMut1,
) -> Result<(), String> {
let n_cols_ctrl = ctrl.shape.1;
let n_ctrl = ctrl.shape.0 as f64;
@@ -43,10 +44,8 @@ pub fn single_group_sparse_ovo_mwu_kernel(
n_zeros_tgt[j] = n_tgt - (tgt.indptr[j + 1] - tgt.indptr[j]).to_usize() as f64;
}
- // Pre allocate results
let mu = n_ctrl * n_tgt / 2.;
-
- let u_base = (n_ctrl * n_tgt) + n_tgt * (n_tgt + 1.) / 2.;
+ let remainder = n_tgt * (n_tgt + 1.) / 2.;
for j in 0..n_cols_ctrl {
let n_zeros_total = n_zeros_ctrl[j] + n_zeros_tgt[j];
let (lbc, ubc) = (ctrl.indptr[j].to_usize(), ctrl.indptr[j + 1].to_usize());
@@ -66,9 +65,9 @@ pub fn single_group_sparse_ovo_mwu_kernel(
tiesum += n_zeros_total.powi(3) - n_zeros_total;
// Compute U-stats
- let u = u_base - ranksum;
+ let u = ranksum - remainder;
- let pv = compute_pvalue(
+ let (pv, z) = compute_pvalue(
n_ctrl,
n_tgt,
n_total,
@@ -80,6 +79,7 @@ pub fn single_group_sparse_ovo_mwu_kernel(
)?;
p_values[j] = pv;
u_stats[j] = u;
+ zscores[j] = z;
}
Ok(())
@@ -91,7 +91,7 @@ pub fn multigroup_sparse_ovo_mwu_kernel(
use_continuity: bool,
tie_correct: bool,
alternative: String,
-) -> Result<(Array2, Array2), String> {
+) -> Result<(Array2, Array2, Array2), String> {
if grpc.encoded_ref_group < 0 {
return Err(format!(
"Encoded ref group can not be negative. Received {}.",
@@ -109,31 +109,35 @@ pub fn multigroup_sparse_ovo_mwu_kernel(
let n_groups = grpc.counts.len();
let mut pvalues = Array2::zeros((n_groups, x.shape.1));
let mut u_stats = Array2::zeros((n_groups, x.shape.1));
+ let mut zscores = Array2::zeros((n_groups, x.shape.1));
for group_idx in 0..n_groups {
if group_idx == encoded_ref_group {
pvalues.row_mut(group_idx).fill(1.);
u_stats.row_mut(group_idx).fill(-1.);
+ zscores.row_mut(group_idx).fill(0.);
+ } else {
+ // Chunk the target
+ let start = grpc.indptr[group_idx as usize];
+ let end = grpc.indptr[group_idx as usize + 1];
+ let tgt_indices = grpc.indices.slice(s![start..end]);
+ let mut tgt_chunk = x.index_rows_into_csc(tgt_indices)?;
+ tgt_chunk.sort_columns_inplace()?;
+
+ // Now compute p-values and u-stats
+ _ = single_group_sparse_ovo_mwu_kernel(
+ &control_chunk,
+ tgt_chunk,
+ use_continuity,
+ tie_correct,
+ &alternative,
+ pvalues.row_mut(group_idx),
+ u_stats.row_mut(group_idx),
+ zscores.row_mut(group_idx),
+ )?;
}
- // Chunk the target
- let start = grpc.indptr[group_idx as usize];
- let end = grpc.indptr[group_idx as usize + 1];
- let tgt_indices = grpc.indices.slice(s![start..end]);
- let mut tgt_chunk = x.index_rows_into_csc(tgt_indices)?;
- tgt_chunk.sort_columns_inplace()?;
-
- // Now compute p-values and u-stats
- _ = single_group_sparse_ovo_mwu_kernel(
- &control_chunk,
- tgt_chunk,
- use_continuity,
- tie_correct,
- &alternative,
- pvalues.row_mut(group_idx),
- u_stats.row_mut(group_idx),
- )?;
}
- Ok((pvalues, u_stats))
+ Ok((pvalues, u_stats, zscores))
}
pub fn csc_ovo_mwu_kernel_over_contiguous_col_chunk<'py, D: SparseFloat, I: SparseIndex>(
@@ -144,16 +148,17 @@ pub fn csc_ovo_mwu_kernel_over_contiguous_col_chunk<'py, D: SparseFloat, I: Spar
is_log1p: bool,
use_continuity: bool,
tie_correct: bool,
+ exp_post_agg: bool,
alternative: String,
-) -> Result<(Array2, Array2, Array2), String> {
+) -> Result<(Array2, Array2, Array2, Array2), String> {
let chunk = x.contig_cols_into_csr(chunk_lb, chunk_ub)?;
- let (pvalues, u_stats) =
+ let (pvalues, u_stats, zscores) =
multigroup_sparse_ovo_mwu_kernel(&chunk, &grpc, use_continuity, tie_correct, alternative)?;
- let fc = csr_fold_change(&chunk, &grpc, is_log1p)?;
+ let fc = csr_fold_change(&chunk, &grpc, is_log1p, exp_post_agg)?;
- Ok((pvalues, u_stats, fc))
+ Ok((pvalues, u_stats, zscores, fc))
}
pub fn csr_ovo_mwu_kernel_over_contiguous_col_chunk<'py, D: SparseFloat, I: SparseIndex>(
@@ -164,105 +169,29 @@ pub fn csr_ovo_mwu_kernel_over_contiguous_col_chunk<'py, D: SparseFloat, I: Spar
is_log1p: bool,
use_continuity: bool,
tie_correct: bool,
+ exp_post_agg: bool,
alternative: String,
-) -> Result<(Array2, Array2, Array2), String> {
+) -> Result<(Array2, Array2, Array2, Array2), String> {
let chunk = x.contig_cols_into_csr(chunk_lb, chunk_ub)?;
- let (pvalues, u_stats) =
+ let (pvalues, u_stats, zscores) =
multigroup_sparse_ovo_mwu_kernel(&chunk, &grpc, use_continuity, tie_correct, alternative)?;
- let fc = csr_fold_change(&chunk, &grpc, is_log1p)?;
+ let fc = csr_fold_change(&chunk, &grpc, is_log1p, exp_post_agg)?;
- Ok((pvalues, u_stats, fc))
+ Ok((pvalues, u_stats, zscores, fc))
}
-// macro_rules! ovo_mwu_kernel_over_contiguous_col_chunk {
-// (x:expr, $grpc:expr, $chunk_lb:expr, $chunk_ub:expr, $is_log1p:expr, $use_continuity:expr, $tie_correct:expr, $alternative: expr) => {
-// let chunk = x.contig_cols_into_csr($chunk_lb, $chunk_ub);
-// let (pvalues, u_stats) =
-// multigroup_sparse_ovo_mwu_kernel(&chunk, &grpc, use_continuity, tie_correct, alternative)?;
-
-// let fc = csr_fold_change(&chunk, &grpc, is_log1p)?;
-
-// (pvalues, u_stats, fc)
-// };
-// }
-
type PyArr2f32<'py> = Bound<'py, PyArray2>;
type PyArr2f64<'py> = Bound<'py, PyArray2>;
-// #[pyfunction]
-// pub fn csc_ovo_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
-// py: Python<'py>,
-// x: PyCSCMatrix<'py>,
-// chunk_lb: usize,
-// chunk_ub: usize,
-// grpc: GroupContainerNamedTuple,
-// is_log1p: bool,
-// use_continuity: bool,
-// tie_correct: bool,
-// alternative: String,
-// ) -> PyResult<(PyArr2f64<'py>, PyArr2f64<'py>, PyArr2f32<'py>)> {
-// let x = x.as_csc_matrix();
-// let grpc = grpc.as_group_container();
-
-// let (pvalues, u_stats, fc) = py
-// .detach(|| {
-// csc_ovo_mwu_kernel_over_contiguous_col_chunk(
-// &x,
-// grpc,
-// chunk_lb,
-// chunk_ub,
-// is_log1p,
-// use_continuity,
-// tie_correct,
-// alternative,
-// )
-// })
-// .map_err(PyValueError::new_err)?;
-
-// Ok((
-// PyArray2::from_array(py, &pvalues),
-// PyArray2::from_array(py, &u_stats),
-// PyArray2::from_array(py, &fc),
-// ))
-// }
-
-// macro_rules! run_csr_branch {
-// ($x:expr, $py:expr, $grpc:expr, $chunk_lb:expr, $chunk_ub:expr, $is_log1p:expr, $use_continuity:expr, $tie_correct:expr, $alternative:expr, $dt:ty, $it:ty) => {{
-// let data = $x.data.extract::>()?;
-// let indices = $x.indices.extract::>()?;
-// let indptr = $x.indptr.extract::>()?;
-
-// let csr = CSRMatrix {
-// data: data.as_array(),
-// indices: indices.as_array(),
-// indptr: indptr.as_array(),
-// shape: $x.shape,
-// };
-
-// $py.detach(|| {
-// csr_ovo_mwu_kernel_over_contiguous_col_chunk(
-// &csr,
-// $grpc,
-// $chunk_lb,
-// $chunk_ub,
-// $is_log1p,
-// $use_continuity,
-// $tie_correct,
-// $alternative,
-// )
-// })
-// .map_err(PyValueError::new_err)
-// }};
-// }
-
// The extraction into PyArray + conversion to Array + compute has to be done in one single function, because dtypes are not known at compile time and pyfunctions dont accept generic traits.
// Hence, it is not possible to have let's say a function returning a dtyped object: even PyAny.extract -> PyArray because PyArray has to be typed.
// Previous implementation was 1/ FromPyObject's .extract returning a PyArray, 2/ then .as_csr returning an Array. None of those can be compiled in the dtype-agnostic setup.
// Hence, conversion into pyarray, then conversion into arrays must happen in the same scope when dtype is known.
+#[rustfmt::skip]
macro_rules! run_branch {
- ($format:expr, $x:expr, $py:expr, $grpc:expr, $chunk_lb:expr, $chunk_ub:expr, $is_log1p:expr, $use_continuity:expr, $tie_correct:expr, $alternative:expr, $dt:ty, $it:ty) => {{
+ ($format:expr, $x:expr, $py:expr, $grpc:expr, $chunk_lb:expr, $chunk_ub:expr, $is_log1p:expr, $use_continuity:expr, $tie_correct:expr, $exp_post_agg:expr, $alternative:expr, $dt:ty, $it:ty) => {{
let data = $x.data.extract::>()?;
let indices = $x.indices.extract::>()?;
let indptr = $x.indptr.extract::>()?;
@@ -279,16 +208,8 @@ macro_rules! run_branch {
};
$py.detach(|| {
- // ovo_mwu_kernel_over_contiguous_col_chunk!(
csr_ovo_mwu_kernel_over_contiguous_col_chunk(
- &csr,
- $grpc,
- $chunk_lb,
- $chunk_ub,
- $is_log1p,
- $use_continuity,
- $tie_correct,
- $alternative,
+ &csr, $grpc, $chunk_lb, $chunk_ub, $is_log1p, $use_continuity, $tie_correct, $exp_post_agg, $alternative,
)
})
.map_err(PyValueError::new_err)
@@ -303,14 +224,7 @@ macro_rules! run_branch {
$py.detach(|| {
csc_ovo_mwu_kernel_over_contiguous_col_chunk(
- &csc,
- $grpc,
- $chunk_lb,
- $chunk_ub,
- $is_log1p,
- $use_continuity,
- $tie_correct,
- $alternative,
+ &csc, $grpc, $chunk_lb, $chunk_ub, $is_log1p, $use_continuity, $tie_correct, $exp_post_agg, $alternative,
)
})
.map_err(PyValueError::new_err)
@@ -320,6 +234,7 @@ macro_rules! run_branch {
}};
}
+#[rustfmt::skip]
#[pyfunction]
pub fn csr_ovo_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
py: Python<'py>,
@@ -330,68 +245,30 @@ pub fn csr_ovo_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
is_log1p: bool,
use_continuity: bool,
tie_correct: bool,
+ exp_post_agg: bool,
alternative: String,
-) -> PyResult<(PyArr2f64<'py>, PyArr2f64<'py>, PyArr2f32<'py>)> {
+) -> PyResult<(
+ PyArr2f64<'py>,
+ PyArr2f64<'py>,
+ PyArr2f64<'py>,
+ PyArr2f32<'py>,
+)> {
let grpc = grpc.as_group_container();
let data_dtype: String = x.data.getattr("dtype")?.getattr("str")?.extract()?;
let idx_dtype: String = x.indices.getattr("dtype")?.getattr("str")?.extract()?;
- let (pv, u, fc) = match (data_dtype.as_str(), idx_dtype.as_str()) {
+ let (pv, u, z, fc) = match (data_dtype.as_str(), idx_dtype.as_str()) {
("f32" | " run_branch!(
- "CSR",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f32,
- i32
+ "CSR", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f32, i32
),
("f64" | " run_branch!(
- "CSR",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f64,
- i32
+ "CSR", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f64, i32
),
("f32" | " run_branch!(
- "CSR",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f32,
- i64
+ "CSR", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f32, i64
),
("f64" | " run_branch!(
- "CSR",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f64,
- i64
+ "CSR", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f64, i64
),
_ => Err(PyValueError::new_err(format!(
"Error casting data (only f32 and f64 supported, received {}) and indices (only int32 and int64 supported, received {}).",
@@ -402,10 +279,12 @@ pub fn csr_ovo_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
return Ok((
PyArray2::from_array(py, &pv),
PyArray2::from_array(py, &u),
+ PyArray2::from_array(py, &z),
PyArray2::from_array(py, &fc),
));
}
+#[rustfmt::skip]
#[pyfunction]
pub fn csc_ovo_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
py: Python<'py>,
@@ -416,68 +295,30 @@ pub fn csc_ovo_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
is_log1p: bool,
use_continuity: bool,
tie_correct: bool,
+ exp_post_agg: bool,
alternative: String,
-) -> PyResult<(PyArr2f64<'py>, PyArr2f64<'py>, PyArr2f32<'py>)> {
+) -> PyResult<(
+ PyArr2f64<'py>,
+ PyArr2f64<'py>,
+ PyArr2f64<'py>,
+ PyArr2f32<'py>,
+)> {
let grpc = grpc.as_group_container();
let data_dtype: String = x.data.getattr("dtype")?.getattr("str")?.extract()?;
let idx_dtype: String = x.indices.getattr("dtype")?.getattr("str")?.extract()?;
- let (pv, u, fc) = match (data_dtype.as_str(), idx_dtype.as_str()) {
+ let (pv, u, z, fc) = match (data_dtype.as_str(), idx_dtype.as_str()) {
("f32" | " run_branch!(
- "CSC",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f32,
- i32
+ "CSC", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f32, i32
),
("f64" | " run_branch!(
- "CSC",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f64,
- i32
+ "CSC", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f64, i32
),
("f32" | " run_branch!(
- "CSC",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f32,
- i64
+ "CSC", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f32, i64
),
("f64" | " run_branch!(
- "CSC",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f64,
- i64
+ "CSC", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f64, i64
),
_ => Err(PyValueError::new_err(format!(
"Error casting data (only f32 and f64 supported, received {}) and indices (only int32 and int64 supported, received {}).",
@@ -488,64 +329,7 @@ pub fn csc_ovo_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
return Ok((
PyArray2::from_array(py, &pv),
PyArray2::from_array(py, &u),
+ PyArray2::from_array(py, &z),
PyArray2::from_array(py, &fc),
));
}
-// #[pyfunction]
-// pub fn csr_ovo_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
-// py: Python<'py>,
-// x: PyCSRMatrix2<'py>,
-// chunk_lb: usize,
-// chunk_ub: usize,
-// grpc: GroupContainerNamedTuple,
-// is_log1p: bool,
-// use_continuity: bool,
-// tie_correct: bool,
-// alternative: String,
-// ) -> PyResult<(PyArr2f64<'py>, PyArr2f64<'py>, PyArr2f32<'py>)> {
-// let grpc = grpc.as_group_container();
-
-// let data_dtype: String = x.data.getattr("dtype")?.getattr("str")?.extract()?;
-// let idx_dtype: String = x.indices.getattr("dtype")?.getattr("str")?.extract()?;
-// let (pv, u, fc) = match (data_dtype.as_str(), idx_dtype.as_str()) {
-// ("f32" | " {
-// let data = x.data.extract::>()?;
-// let indices = x.indices.extract::>()?;
-// let indptr = x.indptr.extract::>()?;
-// let csr = CSRMatrix { data: data.as_array(), indices: indices.as_array(), indptr: indptr.as_array(), shape: x.shape};
-// let (pv, u, fc) = py.detach(|| {csr_ovo_mwu_kernel_over_contiguous_col_chunk(&csr, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, alternative)}).map_err(PyValueError::new_err)?;
-// Ok((pv, u, fc))
-// },
-// ("f64" | " {
-// let data = x.data.extract::>()?;
-// let indices = x.indices.extract::>()?;
-// let indptr = x.indptr.extract::>()?;
-// let csr = CSRMatrix { data: data.as_array(), indices: indices.as_array(), indptr: indptr.as_array(), shape: x.shape};
-// let (pv, u, fc) = py.detach(|| {csr_ovo_mwu_kernel_over_contiguous_col_chunk(&csr, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, alternative)}).map_err(PyValueError::new_err)?;
-// Ok((pv, u, fc))
-// },
-// ("f32" | " {
-// let data = x.data.extract::>()?;
-// let indices = x.indices.extract::>()?;
-// let indptr = x.indptr.extract::>()?;
-// let csr = CSRMatrix { data: data.as_array(), indices: indices.as_array(), indptr: indptr.as_array(), shape: x.shape};
-// let (pv, u, fc) = py.detach(|| {csr_ovo_mwu_kernel_over_contiguous_col_chunk(&csr, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, alternative)}).map_err(PyValueError::new_err)?;
-// Ok((pv, u, fc))
-// },
-// ("f64" | " {
-// let data = x.data.extract::>()?;
-// let indices = x.indices.extract::>()?;
-// let indptr = x.indptr.extract::>()?;
-// let csr = CSRMatrix { data: data.as_array(), indices: indices.as_array(), indptr: indptr.as_array(), shape: x.shape};
-// let (pv, u, fc) = py.detach(|| {csr_ovo_mwu_kernel_over_contiguous_col_chunk(&csr, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, alternative)}).map_err(PyValueError::new_err)?;
-// Ok((pv, u, fc))
-// },
-// _ => Err(PyValueError::new_err(format!("Error casting {} and {}", data_dtype, idx_dtype))),
-// }?;
-// return Ok((
-// PyArray2::from_array(py, &pv),
-// PyArray2::from_array(py, &u),
-// PyArray2::from_array(py, &fc),
-// ))
-
-// }
diff --git a/src/sparse_ovr.rs b/src/sparse_ovr.rs
index 926d49c..c99ba02 100644
--- a/src/sparse_ovr.rs
+++ b/src/sparse_ovr.rs
@@ -16,13 +16,14 @@ pub fn sparse_ovr_mwu_kernel(
use_continuity: bool,
tie_correct: bool,
alternative: String,
-) -> Result<(Array2, Array2), String> {
+) -> Result<(Array2, Array2, Array2), String> {
let n_cols = x.shape.1;
let n_zeros = x.shape.0 - x.indptr.diff(1, Axis(0)).mapv(|x| x.to_usize());
// Allocate placeholders for results
- let mut u_stats = Array2::zeros((grpc.counts.len(), x.shape.1));
let mut p_values = Array2::zeros((grpc.counts.len(), x.shape.1));
+ let mut u_stats = Array2::zeros((grpc.counts.len(), x.shape.1));
+ let mut zscores = Array2::zeros((grpc.counts.len(), x.shape.1));
let n_total = x.shape.0 as f64;
let n_ref = grpc.counts.mapv(|x| n_total - x as f64);
@@ -34,7 +35,7 @@ pub fn sparse_ovr_mwu_kernel(
let mut ranksum = Array2::zeros((grpc.counts.len(), x.shape.1));
let mut tiesum = Array1::::zeros(x.shape.1);
let mut nnz_per_group = Array2::::zeros((grpc.counts.len(), x.shape.1));
- let u_base = &n_ref * &n_tgt + &n_tgt * (&n_tgt + 1.) / 2.;
+ let remainder = &n_tgt * (&n_tgt + 1.) / 2.;
// Note: ideally I would benchmark between the two scenarios: keep all of n, nz, nnz as usize and convert the end result to f64,
// or convert them right away to f64. Summation on f64 is slower than on integers but casting and memory alloc of vectors takes time.
for j in 0..n_cols {
@@ -73,12 +74,12 @@ pub fn sparse_ovr_mwu_kernel(
ts += (n_zeros[j].pow(3) - n_zeros[j]) as f64;
// Now compute u-stat: one value per group
- let u_stat = &u_base - &rs;
+ let u_stat = &rs - &remainder;
u_stats.column_mut(j).assign(&u_stat); // Assign
// Now compute p-values
for k in 0..grpc.counts.len() {
- let p = compute_pvalue(
+ let (p, z) = compute_pvalue(
n_ref[k],
n_tgt[k],
n_total,
@@ -89,9 +90,10 @@ pub fn sparse_ovr_mwu_kernel(
&alternative,
)?;
p_values[[k, j]] = p;
+ zscores[[k, j]] = z;
}
}
- Ok((p_values, u_stats))
+ Ok((p_values, u_stats, zscores))
}
pub fn csr_ovr_mwu_kernel_over_contiguous_col_chunk<'py, D: SparseFloat, I: SparseIndex>(
@@ -102,15 +104,16 @@ pub fn csr_ovr_mwu_kernel_over_contiguous_col_chunk<'py, D: SparseFloat, I: Spar
is_log1p: bool,
use_continuity: bool,
tie_correct: bool,
+ exp_post_agg: bool,
alternative: String,
-) -> Result<(Array2, Array2, Array2), String> {
+) -> Result<(Array2, Array2, Array2, Array2), String> {
let csc_chunk = x.contig_col_chunk_into_csc(chunk_lb, chunk_ub)?;
- let (p_values, u_stats) =
+ let (p_values, u_stats, zscores) =
sparse_ovr_mwu_kernel(&csc_chunk, &grpc, use_continuity, tie_correct, alternative)?;
- let fc = csc_fold_change(&csc_chunk, &grpc, is_log1p)?;
- Ok((p_values, u_stats, fc))
+ let fc = csc_fold_change(&csc_chunk, &grpc, is_log1p, exp_post_agg)?;
+ Ok((p_values, u_stats, zscores, fc))
}
pub fn csc_ovr_mwu_kernel_over_contiguous_col_chunk<'py, D: SparseFloat, I: SparseIndex>(
@@ -121,19 +124,21 @@ pub fn csc_ovr_mwu_kernel_over_contiguous_col_chunk<'py, D: SparseFloat, I: Spar
is_log1p: bool,
use_continuity: bool,
tie_correct: bool,
+ exp_post_agg: bool,
alternative: String,
-) -> Result<(Array2, Array2, Array2), String> {
+) -> Result<(Array2, Array2, Array2, Array2), String> {
let csc_chunk = x.contig_col_chunk_into_csc(chunk_lb, chunk_ub)?;
- let (p_values, u_stats) =
+ let (p_values, u_stats, zscores) =
sparse_ovr_mwu_kernel(&csc_chunk, &grpc, use_continuity, tie_correct, alternative)?;
- let fc = csc_fold_change(&csc_chunk, &grpc, is_log1p)?;
- Ok((p_values, u_stats, fc))
+ let fc = csc_fold_change(&csc_chunk, &grpc, is_log1p, exp_post_agg)?;
+ Ok((p_values, u_stats, zscores, fc))
}
+#[rustfmt::skip]
macro_rules! run_branch {
- ($format:expr, $x:expr, $py:expr, $grpc:expr, $chunk_lb:expr, $chunk_ub:expr, $is_log1p:expr, $use_continuity:expr, $tie_correct:expr, $alternative:expr, $dt:ty, $it:ty) => {{
+ ($format:expr, $x:expr, $py:expr, $grpc:expr, $chunk_lb:expr, $chunk_ub:expr, $is_log1p:expr, $use_continuity:expr, $tie_correct:expr, $exp_post_agg:expr, $alternative:expr, $dt:ty, $it:ty) => {{
let data = $x.data.extract::>()?;
let indices = $x.indices.extract::>()?;
let indptr = $x.indptr.extract::>()?;
@@ -152,14 +157,7 @@ macro_rules! run_branch {
$py.detach(|| {
// ovo_mwu_kernel_over_contiguous_col_chunk!(
csr_ovr_mwu_kernel_over_contiguous_col_chunk(
- &csr,
- $chunk_lb,
- $chunk_ub,
- $grpc,
- $is_log1p,
- $use_continuity,
- $tie_correct,
- $alternative,
+ &csr, $chunk_lb, $chunk_ub, $grpc, $is_log1p, $use_continuity, $tie_correct, $exp_post_agg, $alternative,
)
})
.map_err(PyValueError::new_err)
@@ -174,14 +172,7 @@ macro_rules! run_branch {
$py.detach(|| {
csc_ovr_mwu_kernel_over_contiguous_col_chunk(
- &csc,
- $chunk_lb,
- $chunk_ub,
- $grpc,
- $is_log1p,
- $use_continuity,
- $tie_correct,
- $alternative,
+ &csc, $chunk_lb, $chunk_ub, $grpc, $is_log1p, $use_continuity, $tie_correct, $exp_post_agg, $alternative,
)
})
.map_err(PyValueError::new_err)
@@ -193,6 +184,8 @@ macro_rules! run_branch {
type PyArr2f32<'py> = Bound<'py, PyArray2>;
type PyArr2f64<'py> = Bound<'py, PyArray2>;
+
+#[rustfmt::skip]
#[pyfunction]
pub fn csr_ovr_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
py: Python<'py>,
@@ -203,70 +196,31 @@ pub fn csr_ovr_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
is_log1p: bool,
use_continuity: bool,
tie_correct: bool,
+ exp_post_agg: bool,
alternative: String,
-) -> PyResult<(PyArr2f64<'py>, PyArr2f64<'py>, PyArr2f32<'py>)> {
- // let x = x.as_csr_matrix();
+) -> PyResult<(
+ PyArr2f64<'py>,
+ PyArr2f64<'py>,
+ PyArr2f64<'py>,
+ PyArr2f32<'py>,
+)> {
let grpc = grpc.as_group_container();
let data_dtype: String = x.data.getattr("dtype")?.getattr("str")?.extract()?;
let indices_dtype: String = x.indices.getattr("dtype")?.getattr("str")?.extract()?;
- let (pvalues, u_stats, fc) = match (data_dtype.as_str(), indices_dtype.as_str()) {
+ let (pvalues, u_stats, zscores, fc) = match (data_dtype.as_str(), indices_dtype.as_str()) {
("f32" | " run_branch!(
- "CSR",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f32,
- i32
+ "CSR", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f32, i32
),
("f64" | " run_branch!(
- "CSR",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f64,
- i32
+ "CSR", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f64, i32
),
("f32" | " run_branch!(
- "CSR",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f32,
- i64
+ "CSR", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f32, i64
),
("f64" | " run_branch!(
- "CSR",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f64,
- i64
+ "CSR", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f64, i64
),
_ => Err(PyValueError::new_err(format!(
"Error casting data (only f32 and f64 supported, received {}) and indices (only int32 and int64 supported, received {}).",
@@ -277,10 +231,12 @@ pub fn csr_ovr_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
Ok((
PyArray2::from_array(py, &pvalues),
PyArray2::from_array(py, &u_stats),
+ PyArray2::from_array(py, &zscores),
PyArray2::from_array(py, &fc),
))
}
+#[rustfmt::skip]
#[pyfunction]
pub fn csc_ovr_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
py: Python<'py>,
@@ -291,70 +247,31 @@ pub fn csc_ovr_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
is_log1p: bool,
use_continuity: bool,
tie_correct: bool,
+ exp_post_agg: bool,
alternative: String,
-) -> PyResult<(PyArr2f64<'py>, PyArr2f64<'py>, PyArr2f32<'py>)> {
- // let x = x.as_CSC_matrix();
+) -> PyResult<(
+ PyArr2f64<'py>,
+ PyArr2f64<'py>,
+ PyArr2f64<'py>,
+ PyArr2f32<'py>,
+)> {
let grpc = grpc.as_group_container();
let data_dtype: String = x.data.getattr("dtype")?.getattr("str")?.extract()?;
let indices_dtype: String = x.indices.getattr("dtype")?.getattr("str")?.extract()?;
- let (pvalues, u_stats, fc) = match (data_dtype.as_str(), indices_dtype.as_str()) {
+ let (pvalues, u_stats, zscores, fc) = match (data_dtype.as_str(), indices_dtype.as_str()) {
("f32" | " run_branch!(
- "CSC",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f32,
- i32
+ "CSC", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f32, i32
),
("f64" | " run_branch!(
- "CSC",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f64,
- i32
+ "CSC", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f64, i32
),
("f32" | " run_branch!(
- "CSC",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f32,
- i64
+ "CSC", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f32, i64
),
("f64" | " run_branch!(
- "CSC",
- x,
- py,
- grpc,
- chunk_lb,
- chunk_ub,
- is_log1p,
- use_continuity,
- tie_correct,
- alternative,
- f64,
- i64
+ "CSC", x, py, grpc, chunk_lb, chunk_ub, is_log1p, use_continuity, tie_correct, exp_post_agg, alternative, f64, i64
),
_ => Err(PyValueError::new_err(format!(
"Error casting data (only f32 and f64 supported, received {}) and indices (only int32 and int64 supported, received {}).",
@@ -365,41 +282,7 @@ pub fn csc_ovr_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
Ok((
PyArray2::from_array(py, &pvalues),
PyArray2::from_array(py, &u_stats),
+ PyArray2::from_array(py, &zscores),
PyArray2::from_array(py, &fc),
))
}
-// #[pyfunction]
-// pub fn csc_ovr_mwu_kernel_over_contiguous_col_chunk_rust<'py>(
-// py: Python<'py>,
-// x: PyCSCMatrix,
-// chunk_lb: usize,
-// chunk_ub: usize,
-// grpc: GroupContainerNamedTuple,
-// is_log1p: bool,
-// use_continuity: bool,
-// tie_correct: bool,
-// alternative: String,
-// ) -> PyResult<(PyArr2f64<'py>, PyArr2f64<'py>, PyArr2f32<'py>)> {
-// let x = x.as_csc_matrix();
-// let grpc = grpc.as_group_container();
-
-// let (pvalues, u_stats, fc) = py
-// .detach(|| {
-// csc_ovr_mwu_kernel_over_contiguous_col_chunk(
-// &x,
-// chunk_lb,
-// chunk_ub,
-// grpc,
-// is_log1p,
-// use_continuity,
-// tie_correct,
-// alternative,
-// )
-// })
-// .map_err(PyValueError::new_err)?;
-// Ok((
-// PyArray2::from_array(py, &pvalues),
-// PyArray2::from_array(py, &u_stats),
-// PyArray2::from_array(py, &fc),
-// ))
-// }
diff --git a/src/stats.rs b/src/stats.rs
index 3330b94..7a966dd 100644
--- a/src/stats.rs
+++ b/src/stats.rs
@@ -10,32 +10,31 @@ pub fn compute_pvalue(
mu: f64,
contin_corr: f64,
alternative: &String,
-) -> Result {
+) -> Result<(f64, f64), String> {
let tie_corr: f64 = 1.0 - tie_sum / (n * (n - 1.) * (n + 1.));
if tie_corr > 1e-9 {
let sigma: f64 = (n_ref * n_tgt * (n_ref + n_tgt + 1.) / 12.0 * tie_corr).powf(0.5);
match alternative.as_str() {
"two-sided" => {
- let min_u = U.min(n_ref * n_tgt - U);
- let delta = min_u - mu;
- let z = (delta.abs() + delta.signum() * contin_corr) / sigma;
- return Ok(erfc(z / (2.0 as f64).sqrt()));
+ let delta = U - mu;
+ let z = (delta - delta.signum() * contin_corr) / sigma;
+ return Ok((erfc(z.abs() / (2.0 as f64).sqrt()), z));
}
"greater" => {
let delta = U - mu;
let z = (delta - contin_corr) / sigma;
- return Ok(0.5 * erfc(z / (2.0 as f64).sqrt()));
+ return Ok((0.5 * erfc(z / (2.0 as f64).sqrt()), z));
}
"less" => {
let delta = U - mu;
let z = (delta + contin_corr) / sigma;
- return Ok(0.5 * erfc(-z / (2.0 as f64).sqrt()));
+ return Ok((0.5 * erfc(-z / (2.0 as f64).sqrt()), z));
}
_ => Err(format!("Invalid alternative: received {alternative}.")),
}
} else {
- return Ok(1.0 as f64);
+ return Ok((1.0, 0.));
}
}
@@ -49,8 +48,7 @@ pub fn compute_pvalue_rust(
mu: f64,
contin_corr: f64,
alternative: String,
-) -> PyResult {
- // let p_value: f64 = compute_pvalue(n_ref, n_tgt, n, tie_sum, U, mu, contin_corr, &alternative)?;
+) -> PyResult<(f64, f64)> {
compute_pvalue(
n_ref as f64,
n_tgt as f64,
diff --git a/tests/test_asymptotic_wilcoxon.py b/tests/test_asymptotic_wilcoxon.py
index 72b45bb..a546b2e 100644
--- a/tests/test_asymptotic_wilcoxon.py
+++ b/tests/test_asymptotic_wilcoxon.py
@@ -1,5 +1,6 @@
import contextlib
import gc
+import math
import os
import re
import warnings
@@ -12,8 +13,7 @@
import pytest
import scanpy as sc
from numba import set_num_threads
-from pdex import parallel_differential_expression
-from pdex._single_cell import parallel_differential_expression_vec_wrapper
+from pdex import pdex
from scipy import sparse as py_sparse
from scipy.stats import mannwhitneyu
@@ -60,7 +60,7 @@ def scanpy_mannwhitneyu(adata, groupby_key, reference):
return df.set_index(["target", "feature"])
-def scipy_mannwhitneyu(adata, groupby_key, reference, use_continuity, alternative, is_log1p=False):
+def scipy_mannwhitneyu(adata, groupby_key, reference, use_continuity, alternative, exp_post_agg=False, is_log1p=False):
if reference is not None:
ref_counts = adata[adata.obs[groupby_key].eq(reference)].X
if not isinstance(ref_counts, np.ndarray):
@@ -83,15 +83,15 @@ def scipy_mannwhitneyu(adata, groupby_key, reference, use_continuity, alternativ
ref_counts = ref_counts.toarray()
# Compute FC
- if is_log1p:
- grp_counts = np.expm1(grp_counts)
- ref_counts = np.expm1(ref_counts)
+ if is_log1p and not exp_post_agg:
fc = np.expm1(grp_counts).mean(axis=0) / np.expm1(ref_counts).mean(axis=0)
+ if is_log1p and exp_post_agg:
+ fc = np.expm1(grp_counts.mean(axis=0)) / np.expm1(ref_counts.mean(axis=0))
else:
fc = np.mean(grp_counts, axis=0) / np.mean(ref_counts, axis=0)
stats, pvals = mannwhitneyu(
- ref_counts, grp_counts, axis=0, method="asymptotic", use_continuity=use_continuity, alternative=alternative
+ grp_counts, ref_counts, axis=0, method="asymptotic", use_continuity=use_continuity, alternative=alternative
)
results.append(
pd.DataFrame(
@@ -108,6 +108,104 @@ def scipy_mannwhitneyu(adata, groupby_key, reference, use_continuity, alternativ
return results
+@pytest.mark.parametrize("use_rust", [True, False], ids=["rust", "numba"])
+@pytest.mark.parametrize("exp_post_agg", [True, False], ids=["exp-post-agg", "exp-pre-agg"])
+@pytest.mark.parametrize("is_log1p", [True, False], ids=["is-log1p", "is-not-log1p"])
+@pytest.mark.parametrize("test", ["ovo", "ovr"])
+def test_fold_change_asymptotic_wilcoxon(eager_rand_adata, test, is_log1p, exp_post_agg, use_rust):
+ """Keep this in a separate test as this does not impact p-values and u-stats."""
+ if test == "ovo":
+ reference = eager_rand_adata.obs.pert.iloc[0]
+ else:
+ reference = None
+
+ asy_results = asymptotic_wilcoxon(
+ adata=eager_rand_adata,
+ is_log1p=False,
+ group_keys="pert",
+ reference=reference,
+ use_continuity=True,
+ tie_correct=True,
+ exp_post_agg=exp_post_agg,
+ n_threads=1,
+ batch_size=16,
+ alternative="two-sided",
+ use_rust=use_rust,
+ )
+
+ scipy_results = scipy_mannwhitneyu(
+ adata=eager_rand_adata,
+ groupby_key="pert",
+ reference=reference,
+ is_log1p=False,
+ use_continuity=True,
+ alternative="two-sided",
+ exp_post_agg=exp_post_agg,
+ )
+ # Test FC with mid tolerance
+ np.testing.assert_allclose(
+ asy_results.loc[scipy_results.index].fold_change.values,
+ scipy_results.fold_change.values,
+ atol=0.0,
+ rtol=1.0e-6,
+ )
+
+
+@pytest.mark.parametrize("corr_method", ["benjamini-hochberg", "bonferroni"])
+@pytest.mark.parametrize("test", ["ovo", "ovr"])
+def test_scanpy_format_output(eager_rand_adata, test, corr_method):
+ """Test that the output of `asymptotic_wilcoxon` with `return_as_scanpy=True` is compatible with Scanpy's output format, and that the values are close to those obtained with Scanpy's implementation.
+ Note: because Scanpy only implements a subset of all the possible setups, this test is kept separately from `test_asymptotic_wilcoxon`, and only sweep the parameters that are relevant to Scanpy's implementation.
+ """
+ if test == "ovo":
+ reference = eager_rand_adata.obs.pert.iloc[0]
+ else:
+ reference = None
+
+ asy_results = asymptotic_wilcoxon(
+ adata=eager_rand_adata,
+ group_keys="pert",
+ is_log1p=True, # Scanpy assumes log1p
+ exp_post_agg=True, # Post-aggregation exponentiation is needed to match Scanpy's fold change output
+ reference=reference,
+ use_continuity=False, # False because scanpy does not apply continuity correction
+ tie_correct=False, # False because scanpy takes a lot of time to adjust
+ n_threads=1,
+ batch_size=16,
+ alternative="two-sided", # Scanpy only implments two-sided test
+ use_rust=True,
+ return_as_scanpy=True,
+ corr_method=corr_method,
+ )
+
+ sc.tl.rank_genes_groups(
+ eager_rand_adata,
+ groupby="pert",
+ method="wilcoxon",
+ reference=reference if test == "ovo" else "rest",
+ n_genes=eager_rand_adata.n_vars,
+ tie_correct=False,
+ corr_method=corr_method,
+ )
+ scanpy_results = eager_rand_adata.uns["rank_genes_groups"]
+ assert set(asy_results.keys()) == set(scanpy_results.keys()), "Output keys do not match Scanpy's output format."
+
+ for k, ref in scanpy_results.items():
+ if k in ["params", "names"]:
+ # We can skip names ordering check as if incorrect, other values will mismatch
+ continue
+ res = np.array(asy_results[k].tolist())
+ ref = np.array(ref.tolist())
+ mask = np.isfinite(ref) * np.isfinite(res) # Mask to ignore inf values in the comparison
+ np.testing.assert_allclose(
+ ref[mask],
+ res[mask],
+ rtol=0,
+ atol=1e-6,
+ err_msg=f"Mismatch in '{k}' values between asymptotic_wilcoxon and Scanpy outputs.",
+ )
+
+
@pytest.mark.parametrize("use_rust", [True, False], ids=["rust", "numba"])
@pytest.mark.parametrize("alternative", ["two-sided", "less", "greater"])
@pytest.mark.parametrize("tie_correct", [True, False], ids=["tie-correct", "no-tie-correct"])
@@ -285,19 +383,21 @@ def run():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
if method == "pdex":
- parallel_differential_expression(
- data,
- groupby_key="gene",
- reference="non-targeting",
- num_workers=num_threads,
- )
- elif method == "pdexp":
- parallel_differential_expression_vec_wrapper(
+ mode = "ref" if test == "ovo" else "all"
+ pdex(
data,
- groupby_key="gene",
+ groupby="gene",
+ mode=mode,
reference="non-targeting",
- num_workers=num_threads,
+ threads=num_threads,
)
+ # elif method == "pdexp":
+ # parallel_differential_expression_vec_wrapper(
+ # data,
+ # groupby_key="gene",
+ # reference="non-targeting",
+ # num_workers=num_threads,
+ # )
elif method == "illico":
reference = "non-targeting" if test == "ovo" else None
asymptotic_wilcoxon(
@@ -332,12 +432,14 @@ def run():
@pytest.mark.parametrize("use_rust", [True, False], ids=["rust", "numba"])
@pytest.mark.parametrize("num_threads", [1, 2, 4, 8, 16], ids=lambda v: f"nthreads={v}")
@pytest.mark.parametrize("test", ["ovo", "ovr"])
-@pytest.mark.parametrize("method", ["illico", "scanpy", "pdex", "pdexp"])
+@pytest.mark.parametrize("method", ["illico", "scanpy", "pdex"])
def test_speed_benchmark(adata, method, test, num_threads, use_rust, benchmark, request):
"""Not a test, just a speed benchmark."""
- if test != "ovo" and method in ["pdex", "pdexp"]:
- # This exits the test, not running the benchmark, and not raising an error
- pytest.skip("pdex only implements OVO test.")
+ # if test != "ovo" and method in ["pdex"]:
+ # # This exits the test, not running the benchmark, and not raising an error
+ # pytest.skip("pdex only implements OVO test.")
+ if use_rust and method != "illico":
+ pytest.skip("Rust implementation only available for illico method.")
# Compile
if method == "illico":
@@ -382,3 +484,51 @@ def test_memory_benchmark(adata, method, test, num_threads, request):
# Cleanup the file if an error happened
_fp.unlink(missing_ok=True)
raise e
+
+
+def test_asymptotic_wilcoxon_auto_batchsize(eager_rand_adata):
+ """Test that the auto batch size splits the data in appropriate chunks, not missing any column."""
+ reference = None
+
+ target_n_cols = 1024 # 4 batches of 256 cols each
+ bigger_eager_rand_adata = ad.concat(
+ [eager_rand_adata] * int(math.ceil(target_n_cols / eager_rand_adata.n_vars)), axis=1
+ )
+ bigger_eager_rand_adata.var_names_make_unique()
+ bigger_eager_rand_adata.obs = eager_rand_adata.obs.copy()
+ asy_results = asymptotic_wilcoxon(
+ adata=bigger_eager_rand_adata,
+ is_log1p=False,
+ group_keys="pert",
+ reference=reference,
+ use_continuity=True,
+ tie_correct=True,
+ n_threads=1,
+ batch_size="auto",
+ alternative="two-sided",
+ use_rust=True,
+ )
+
+ scipy_results = scipy_mannwhitneyu(
+ adata=bigger_eager_rand_adata,
+ groupby_key="pert",
+ reference=reference,
+ is_log1p=False,
+ use_continuity=True,
+ alternative="two-sided",
+ )
+
+ # Test statistics exactly
+ np.testing.assert_allclose(
+ asy_results.loc[scipy_results.index].statistic.values,
+ scipy_results.statistic.values,
+ atol=0.0,
+ rtol=0.0,
+ )
+ # Test p-values with low tolerance
+ np.testing.assert_allclose(
+ asy_results.loc[scipy_results.index].p_value.values,
+ scipy_results.p_value.values,
+ atol=0.0,
+ rtol=1.0e-12,
+ )
diff --git a/tests/utils/test_compile.py b/tests/utils/test_compile.py
index 3ab665d..6baa890 100644
--- a/tests/utils/test_compile.py
+++ b/tests/utils/test_compile.py
@@ -21,7 +21,7 @@ def test_precompile(rand_adata, test):
_, grpc = encode_and_count_groups(rand_adata.obs.pert.values, reference)
X, bounds = data_handler.fetch(0, rand_adata.X.shape[1])
X_nb = data_handler.to_nb(X)
- dispatcher(X_nb, *bounds, grpc, False, True, True, "two-sided")
+ dispatcher(X_nb, *bounds, grpc, False, True, True, False, "two-sided")
# Assert no other signature was added
assert len(dispatcher.nopython_signatures) == len(
leg_sig
diff --git a/tox.ini b/tox.ini
index d0cd38d..14ad863 100644
--- a/tox.ini
+++ b/tox.ini
@@ -30,11 +30,19 @@ commands =
poetry run pytest -m speed_bench --regex ".*k562-(csr|dense)-100%-illico-.*numba.*" {env:BENCH_STORAGE} --benchmark-save "illico-scaling-w-threads-numba" {posargs}
# This runs speed benchmarks for competitor backends with 8 threads
-[testenv:speed-bench-ref{,-quick}]
+[testenv:speed-bench-scanpy{,-quick}]
commands =
poetry run pytest -m speed_bench --regex ".*(csr|dense)-{env:FRACTION}-scanpy-.*-nthreads=8" {env:BENCH_STORAGE} --benchmark-save "scanpy-speed-bench" {posargs}
+
+[testenv:speed-bench-pdex{,-quick}]
+commands =
poetry run pytest -m speed_bench --regex ".*(csr|dense)-{env:FRACTION}-pdex-.*-nthreads=8" {env:BENCH_STORAGE} --benchmark-save "pdex-speed-bench" {posargs}
+[testenv:speed-bench-ref{,-quick}]
+commands =
+ {[testenv:speed-bench-scanpy{,-quick}]commands}
+ {[testenv:speed-bench-pdex{,-quick}]commands}
+
# This runs speed benchmarks for illico with 8 threads, to be re-run everytime before any new release / PR
[testenv:speed-bench-illico{,-quick}]