pypando 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pypando-0.1.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 QuaDBio Lab (Pando, the R package this is ported from)
4
+ Copyright (c) 2026 omicverse contributors (py-Pando Python port)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
pypando-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: pypando
3
+ Version: 0.1.0
4
+ Summary: Pure-Python Pando — gene regulatory network inference from paired single-cell RNA + ATAC (regions, motifs, per-gene TF x binding-site models, modules), AnnData-native, parity-tested against R.
5
+ License: MIT License
6
+
7
+ Copyright (c) 2021 QuaDBio Lab (Pando, the R package this is ported from)
8
+ Copyright (c) 2026 omicverse contributors (py-Pando Python port)
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/omicverse/py-Pando
29
+ Project-URL: Repository, https://github.com/omicverse/py-Pando
30
+ Project-URL: Issues, https://github.com/omicverse/py-Pando/issues
31
+ Project-URL: Upstream R package, https://github.com/quadbio/Pando
32
+ Keywords: single-cell,multiome,scATAC-seq,gene-regulatory-network,pando,transcription-factor,motif,anndata
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Intended Audience :: Science/Research
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3 :: Only
39
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
40
+ Requires-Python: >=3.9
41
+ Description-Content-Type: text/markdown
42
+ License-File: LICENSE
43
+ Requires-Dist: numpy>=1.23
44
+ Requires-Dist: scipy>=1.10
45
+ Requires-Dist: pandas>=1.5
46
+ Requires-Dist: anndata>=0.9
47
+ Requires-Dist: scikit-learn>=1.4
48
+ Requires-Dist: pyfaidx>=0.7
49
+ Requires-Dist: MOODS-python>=1.9.4
50
+ Requires-Dist: pooch>=1.6
51
+ Provides-Extra: xgb
52
+ Requires-Dist: xgboost>=1.7; extra == "xgb"
53
+ Provides-Extra: umap
54
+ Requires-Dist: umap-learn; extra == "umap"
55
+ Provides-Extra: dev
56
+ Requires-Dist: pytest>=7; extra == "dev"
57
+ Requires-Dist: xgboost>=1.7; extra == "dev"
58
+ Dynamic: license-file
59
+
60
+ # py-Pando
61
+
62
+ **Pure-Python Pando**: gene regulatory network inference from paired single-cell RNA + ATAC data, AnnData-native, parity-tested against the R package.
63
+
64
+ [Pando](https://github.com/quadbio/Pando) (Fleck *et al.*, *Nature* 2023, [doi:10.1038/s41586-022-05459-w](https://doi.org/10.1038/s41586-022-05459-w)) fits, for every target gene, a regression of its expression on TF expression x accessibility of TF-binding peaks near the gene, and reads significant TF–peak terms as regulatory links. py-Pando ports it following the [omicverse-rebuildr](https://github.com/omicverse/omicverse-rebuildr) protocol: the R source is the specification, and pre-registered parity gates (`data/manifest.yaml`) were committed before any algorithmic code.
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ pip install pypando # core
70
+ pip install "pypando[xgb]" # + xgboost models
71
+ pip install "pypando[umap]" # + UMAP layout of the module graph
72
+ ```
73
+
74
+ ## Quick start
75
+
76
+ ```python
77
+ import pypando as pp
78
+
79
+ # rna: AnnData, cells x genes, log-normalised (Seurat LogNormalize)
80
+ # atac: AnnData, cells x peaks, TF-IDF (Signac RunTFIDF), var_names "chr-start-end"
81
+ annotation = pp.annotation_from_gtf("gencode.v41.basic.annotation.gff3.gz")
82
+
83
+ grn = pp.initiate_grn(rna, atac, annotation) # peaks minus exons
84
+ grn = pp.find_motifs(grn, genome="hg38.fa") # Pando's 1,590 motifs, MOODS
85
+ grn = pp.infer_grn(grn, genes=targets, method="glm") # one model per target gene
86
+ grn = pp.find_modules(grn) # TF modules
87
+
88
+ pp.coef(grn) # TF / target / region / estimate / std_err / pval / padj / corr
89
+ pp.gof(grn) # R^2 and number of variables per target gene
90
+ pp.NetworkModules(grn) # module table + genes_pos / genes_neg / regions / peaks
91
+ ```
92
+
93
+ Within Epione: `epione.grn.pando(rna, atac, genome="hg38", genes=targets)`.
94
+
95
+ ## Function map
96
+
97
+ | R (Pando 1.1.1) | Python | Parity on the fixture |
98
+ |---|---|---|
99
+ | `initiate_grn` | `pp.initiate_grn` | identical regions (with and without `regions=` phastCons) |
100
+ | `find_motifs` | `pp.find_motifs` | identical region x motif matrix (1,240,962 hits) |
101
+ | `find_peaks_near_genes` (Signac, GREAT) | `pp.find_peaks_near_genes` | identical |
102
+ | `infer_grn(method = "glm")` | `pp.infer_grn` | identical rows; estimates / SE / t / rsq to ~1e-14; -log10 p to 4e-13 |
103
+ | `infer_grn(method = "glmnet")` | | lambda identical; coefficients Pearson 0.9999 (see notes) |
104
+ | `infer_grn(method = "cv.glmnet")` | | R's folds reproduced; coefficients within 5e-14 |
105
+ | `infer_grn(method = "xgb")` | | **gate fails vs installed R** (xgboost >= 3 drops Pando's parameters); identical (2e-16) with them honoured |
106
+ | `infer_grn(method = "bagging_ridge" / "bayesian_ridge")` | | ~1e-14 (NumPy seeded as in R) |
107
+ | `find_modules` | `pp.find_modules` | identical modules |
108
+ | `fit_model`, `fit_glm`, `fit_glmnet`, `fit_cvglmnet`, `fit_xgb`, `fit_bagging_ridge`, `fit_bayesian_ridge` | same names | per-function, to ~1e-14 (notebook 3) |
109
+ | `format_coefs`, `sparse_cor`, `aggregate_matrix`, `aggregate_assay`, `GetAssaySummary` | same names | exact |
110
+ | `get_network_graph` | `pp.get_network_graph` | graph + PageRank; UMAP layout via umap-learn (not R's uwot stream) |
111
+ | `coef`, `gof`, `GetNetwork`, `NetworkModules`, `NetworkTFs`, `NetworkRegions`, `NetworkFeatures`, `NetworkParams`, `NetworkGraph`, `DefaultNetwork`, `GetGRN`, `Params`, `create_GRNData` | same names | accessors |
112
+ | `plot_gof`, `plot_module_metrics`, `plot_network_graph` | same names (matplotlib) | — |
113
+ | `fit_brms` | — | not ported (Stan) |
114
+ | `get_tf_network`, `plot_tf_network` | — | not ported (see report) |
115
+
116
+ Full numbers: [RECONSTRUCTION_REPORT.md](RECONSTRUCTION_REPORT.md) and `examples/compare_R_vs_Python.ipynb`.
117
+
118
+ ## What makes the glm network identical
119
+
120
+ R's `glm` uses LINPACK's `dqrdc2` QR with limited pivoting: a column whose remaining norm falls below `1e-11` of its original norm is moved to the end and its coefficient becomes `NA`. Pando's per-gene designs (hundreds to thousands of `peak x TF` products, often nearly collinear) sit right at that threshold, so the pivot decisions depend on the last bits of BLAS level-1 results. py-Pando's QR (`pypando._blas`, numba) calls the same BLAS routines (`dnrm2`, `ddot`, `daxpy`, `dscal`) in the same order as R's Fortran, multiplies with `dgemv` like R's `%*%`, sums in long double like R's `sum()`, and inverts with LAPACK `dpotri` like `chol2inv`. With the same BLAS build (here OpenBLAS 0.3.34) and one BLAS thread (`infer_grn(blas_threads=1)`, the default), the glm network is bit-identical to R's, including genes where R's IRLS does not converge.
121
+
122
+ ## Notes
123
+
124
+ * **cv.glmnet** folds come from an emulation of R's `set.seed()` + `sample()` (`pypando.rrng`), so `infer_grn(method="cv.glmnet", seed=42)` uses R's folds after `set.seed(42)`. Fold fits follow glmnet 5.0: each fold on its own lambda path, predictions interpolated onto the full path (glmnet 4.x refitted folds on the full path; `cv_glmnet_gaussian(fold_lambda="full")`).
125
+ * **glmnet**: py-Pando's coordinate descent follows glmnet's path rules (standardisation, lambda sequence, `fdev` / `devmax` stopping) but not its strong-rule screening, so for genes with hundreds of variables both stop within glmnet's 1e-7 tolerance at slightly different coefficients. `fit_glmnet` ports `family = "gaussian"` given as a string, which is how `infer_grn` calls it; R's `fit_glmnet` called directly with its default `family = gaussian` (a function) goes to glmnet's generic `glmnet.path` solver, which is not ported.
126
+ * **xgb**: Pando calls `xgboost::xgboost(data, label, params = list(max_depth = 3, eta = 0.01), ...)`. xgboost >= 3 changed that function's interface and silently ignores `params`, training with depth 6 and learning rate 0.3. py-Pando uses Pando's parameters; with them honoured in R (`xgb.train`), importances match to 1e-15.
127
+ * **bagging_ridge** draws from NumPy's global RNG, as Pando's scikit-learn call does from R; `infer_grn(seed=...)` seeds it.
128
+ * **New in Python** (not in R): `region_background`, `regions_near_genes` and `subset_regions` scan only the regions a target gene can reach, against the background of all regions — the network is identical to a full scan.
129
+
130
+ ## Reproducing the R comparison
131
+
132
+ ```bash
133
+ python tests/make_fixture.py pbmc_granulocyte_sorted_10k_filtered_feature_bc_matrix.h5 data/fixture
134
+ Rscript tests/r_reference_driver.R data/fixture ref # R 4.4.3, Pando 1.1.1
135
+ python tests/_run_candidate.py ref cand hg38.fa
136
+ python tests/parity_check.py ref cand results.json
137
+ PYPANDO_REF_DIR=ref PYPANDO_FASTA=hg38.fa PYPANDO_CAND_DIR=cand pytest tests/test_exact_match.py
138
+ ```
139
+
140
+ ## Citation
141
+
142
+ Fleck, J.S., Jansen, S.M.J., Wollny, D. *et al.* Inferring and perturbing cell fate regulomes in human brain organoids. *Nature* 621, 365–372 (2023).
143
+
144
+ ## License
145
+
146
+ MIT, as the upstream R package (see `LICENSE`).
@@ -0,0 +1,87 @@
1
+ # py-Pando
2
+
3
+ **Pure-Python Pando**: gene regulatory network inference from paired single-cell RNA + ATAC data, AnnData-native, parity-tested against the R package.
4
+
5
+ [Pando](https://github.com/quadbio/Pando) (Fleck *et al.*, *Nature* 2023, [doi:10.1038/s41586-022-05459-w](https://doi.org/10.1038/s41586-022-05459-w)) fits, for every target gene, a regression of its expression on TF expression x accessibility of TF-binding peaks near the gene, and reads significant TF–peak terms as regulatory links. py-Pando ports it following the [omicverse-rebuildr](https://github.com/omicverse/omicverse-rebuildr) protocol: the R source is the specification, and pre-registered parity gates (`data/manifest.yaml`) were committed before any algorithmic code.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install pypando # core
11
+ pip install "pypando[xgb]" # + xgboost models
12
+ pip install "pypando[umap]" # + UMAP layout of the module graph
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ ```python
18
+ import pypando as pp
19
+
20
+ # rna: AnnData, cells x genes, log-normalised (Seurat LogNormalize)
21
+ # atac: AnnData, cells x peaks, TF-IDF (Signac RunTFIDF), var_names "chr-start-end"
22
+ annotation = pp.annotation_from_gtf("gencode.v41.basic.annotation.gff3.gz")
23
+
24
+ grn = pp.initiate_grn(rna, atac, annotation) # peaks minus exons
25
+ grn = pp.find_motifs(grn, genome="hg38.fa") # Pando's 1,590 motifs, MOODS
26
+ grn = pp.infer_grn(grn, genes=targets, method="glm") # one model per target gene
27
+ grn = pp.find_modules(grn) # TF modules
28
+
29
+ pp.coef(grn) # TF / target / region / estimate / std_err / pval / padj / corr
30
+ pp.gof(grn) # R^2 and number of variables per target gene
31
+ pp.NetworkModules(grn) # module table + genes_pos / genes_neg / regions / peaks
32
+ ```
33
+
34
+ Within Epione: `epione.grn.pando(rna, atac, genome="hg38", genes=targets)`.
35
+
36
+ ## Function map
37
+
38
+ | R (Pando 1.1.1) | Python | Parity on the fixture |
39
+ |---|---|---|
40
+ | `initiate_grn` | `pp.initiate_grn` | identical regions (with and without `regions=` phastCons) |
41
+ | `find_motifs` | `pp.find_motifs` | identical region x motif matrix (1,240,962 hits) |
42
+ | `find_peaks_near_genes` (Signac, GREAT) | `pp.find_peaks_near_genes` | identical |
43
+ | `infer_grn(method = "glm")` | `pp.infer_grn` | identical rows; estimates / SE / t / rsq to ~1e-14; -log10 p to 4e-13 |
44
+ | `infer_grn(method = "glmnet")` | | lambda identical; coefficients Pearson 0.9999 (see notes) |
45
+ | `infer_grn(method = "cv.glmnet")` | | R's folds reproduced; coefficients within 5e-14 |
46
+ | `infer_grn(method = "xgb")` | | **gate fails vs installed R** (xgboost >= 3 drops Pando's parameters); identical (2e-16) with them honoured |
47
+ | `infer_grn(method = "bagging_ridge" / "bayesian_ridge")` | | ~1e-14 (NumPy seeded as in R) |
48
+ | `find_modules` | `pp.find_modules` | identical modules |
49
+ | `fit_model`, `fit_glm`, `fit_glmnet`, `fit_cvglmnet`, `fit_xgb`, `fit_bagging_ridge`, `fit_bayesian_ridge` | same names | per-function, to ~1e-14 (notebook 3) |
50
+ | `format_coefs`, `sparse_cor`, `aggregate_matrix`, `aggregate_assay`, `GetAssaySummary` | same names | exact |
51
+ | `get_network_graph` | `pp.get_network_graph` | graph + PageRank; UMAP layout via umap-learn (not R's uwot stream) |
52
+ | `coef`, `gof`, `GetNetwork`, `NetworkModules`, `NetworkTFs`, `NetworkRegions`, `NetworkFeatures`, `NetworkParams`, `NetworkGraph`, `DefaultNetwork`, `GetGRN`, `Params`, `create_GRNData` | same names | accessors |
53
+ | `plot_gof`, `plot_module_metrics`, `plot_network_graph` | same names (matplotlib) | — |
54
+ | `fit_brms` | — | not ported (Stan) |
55
+ | `get_tf_network`, `plot_tf_network` | — | not ported (see report) |
56
+
57
+ Full numbers: [RECONSTRUCTION_REPORT.md](RECONSTRUCTION_REPORT.md) and `examples/compare_R_vs_Python.ipynb`.
58
+
59
+ ## What makes the glm network identical
60
+
61
+ R's `glm` uses LINPACK's `dqrdc2` QR with limited pivoting: a column whose remaining norm falls below `1e-11` of its original norm is moved to the end and its coefficient becomes `NA`. Pando's per-gene designs (hundreds to thousands of `peak x TF` products, often nearly collinear) sit right at that threshold, so the pivot decisions depend on the last bits of BLAS level-1 results. py-Pando's QR (`pypando._blas`, numba) calls the same BLAS routines (`dnrm2`, `ddot`, `daxpy`, `dscal`) in the same order as R's Fortran, multiplies with `dgemv` like R's `%*%`, sums in long double like R's `sum()`, and inverts with LAPACK `dpotri` like `chol2inv`. With the same BLAS build (here OpenBLAS 0.3.34) and one BLAS thread (`infer_grn(blas_threads=1)`, the default), the glm network is bit-identical to R's, including genes where R's IRLS does not converge.
62
+
63
+ ## Notes
64
+
65
+ * **cv.glmnet** folds come from an emulation of R's `set.seed()` + `sample()` (`pypando.rrng`), so `infer_grn(method="cv.glmnet", seed=42)` uses R's folds after `set.seed(42)`. Fold fits follow glmnet 5.0: each fold on its own lambda path, predictions interpolated onto the full path (glmnet 4.x refitted folds on the full path; `cv_glmnet_gaussian(fold_lambda="full")`).
66
+ * **glmnet**: py-Pando's coordinate descent follows glmnet's path rules (standardisation, lambda sequence, `fdev` / `devmax` stopping) but not its strong-rule screening, so for genes with hundreds of variables both stop within glmnet's 1e-7 tolerance at slightly different coefficients. `fit_glmnet` ports `family = "gaussian"` given as a string, which is how `infer_grn` calls it; R's `fit_glmnet` called directly with its default `family = gaussian` (a function) goes to glmnet's generic `glmnet.path` solver, which is not ported.
67
+ * **xgb**: Pando calls `xgboost::xgboost(data, label, params = list(max_depth = 3, eta = 0.01), ...)`. xgboost >= 3 changed that function's interface and silently ignores `params`, training with depth 6 and learning rate 0.3. py-Pando uses Pando's parameters; with them honoured in R (`xgb.train`), importances match to 1e-15.
68
+ * **bagging_ridge** draws from NumPy's global RNG, as Pando's scikit-learn call does from R; `infer_grn(seed=...)` seeds it.
69
+ * **New in Python** (not in R): `region_background`, `regions_near_genes` and `subset_regions` scan only the regions a target gene can reach, against the background of all regions — the network is identical to a full scan.
70
+
71
+ ## Reproducing the R comparison
72
+
73
+ ```bash
74
+ python tests/make_fixture.py pbmc_granulocyte_sorted_10k_filtered_feature_bc_matrix.h5 data/fixture
75
+ Rscript tests/r_reference_driver.R data/fixture ref # R 4.4.3, Pando 1.1.1
76
+ python tests/_run_candidate.py ref cand hg38.fa
77
+ python tests/parity_check.py ref cand results.json
78
+ PYPANDO_REF_DIR=ref PYPANDO_FASTA=hg38.fa PYPANDO_CAND_DIR=cand pytest tests/test_exact_match.py
79
+ ```
80
+
81
+ ## Citation
82
+
83
+ Fleck, J.S., Jansen, S.M.J., Wollny, D. *et al.* Inferring and perturbing cell fate regulomes in human brain organoids. *Nature* 621, 365–372 (2023).
84
+
85
+ ## License
86
+
87
+ MIT, as the upstream R package (see `LICENSE`).
@@ -0,0 +1,117 @@
1
+ """py-Pando — pure-Python port of Pando (Fleck et al., Nature 2023).
2
+
3
+ Pando infers gene regulatory networks from paired single-cell RNA and ATAC
4
+ data: for every target gene it regresses expression on TF expression x
5
+ accessibility of TF-binding peaks near the gene.
6
+
7
+ Functional API (mirrors the R package)::
8
+
9
+ import pypando as pp
10
+ grn = pp.initiate_grn(rna, atac, annotation)
11
+ grn = pp.find_motifs(grn, genome="hg38.fa")
12
+ grn = pp.infer_grn(grn, genes=targets)
13
+ grn = pp.find_modules(grn)
14
+ pp.coef(grn); pp.gof(grn); pp.NetworkModules(grn)
15
+ """
16
+ from . import data
17
+ from .annotation import annotation_from_gtf
18
+ from .core import Pando
19
+ from .grn import (
20
+ find_modules,
21
+ find_motifs,
22
+ find_peaks_near_genes,
23
+ fit_grn_models,
24
+ format_coefs,
25
+ infer_grn,
26
+ initiate_grn,
27
+ region_background,
28
+ regions_near_genes,
29
+ subset_regions,
30
+ )
31
+ from .graph import get_network_graph
32
+ from .models import (
33
+ fit_bagging_ridge,
34
+ fit_bayesian_ridge,
35
+ fit_brms,
36
+ fit_cvglmnet,
37
+ fit_glm,
38
+ fit_glmnet,
39
+ fit_model,
40
+ fit_xgb,
41
+ )
42
+ from .motifs import PFM, load_motif2tf, load_pando_motifs
43
+ from .objects import (
44
+ GRNData,
45
+ Modules,
46
+ Network,
47
+ Regions,
48
+ RegulatoryNetwork,
49
+ DefaultNetwork,
50
+ GetGRN,
51
+ GetNetwork,
52
+ NetworkFeatures,
53
+ NetworkGraph,
54
+ NetworkModules,
55
+ NetworkParams,
56
+ NetworkRegions,
57
+ NetworkTFs,
58
+ Params,
59
+ coef,
60
+ gof,
61
+ )
62
+ from .objects import GetAssaySummary, aggregate_assay, create_GRNData
63
+ from .plots import plot_gof, plot_module_metrics, plot_network_graph
64
+ from .utils import aggregate_matrix, sparse_cor
65
+
66
+ __all__ = [
67
+ "Pando",
68
+ "annotation_from_gtf",
69
+ "initiate_grn",
70
+ "find_motifs",
71
+ "find_peaks_near_genes",
72
+ "infer_grn",
73
+ "fit_grn_models",
74
+ "format_coefs",
75
+ "find_modules",
76
+ "region_background",
77
+ "regions_near_genes",
78
+ "subset_regions",
79
+ "get_network_graph",
80
+ "fit_model",
81
+ "fit_glm",
82
+ "fit_glmnet",
83
+ "fit_cvglmnet",
84
+ "fit_xgb",
85
+ "fit_bagging_ridge",
86
+ "fit_bayesian_ridge",
87
+ "fit_brms",
88
+ "PFM",
89
+ "load_pando_motifs",
90
+ "load_motif2tf",
91
+ "GRNData",
92
+ "RegulatoryNetwork",
93
+ "Regions",
94
+ "Network",
95
+ "Modules",
96
+ "GetGRN",
97
+ "GetNetwork",
98
+ "DefaultNetwork",
99
+ "NetworkFeatures",
100
+ "NetworkTFs",
101
+ "NetworkRegions",
102
+ "NetworkModules",
103
+ "NetworkParams",
104
+ "NetworkGraph",
105
+ "Params",
106
+ "coef",
107
+ "gof",
108
+ "aggregate_matrix",
109
+ "aggregate_assay",
110
+ "GetAssaySummary",
111
+ "create_GRNData",
112
+ "plot_gof",
113
+ "plot_module_metrics",
114
+ "plot_network_graph",
115
+ "sparse_cor",
116
+ ]
117
+ __version__ = "0.1.0"
@@ -0,0 +1,177 @@
1
+ """R's QR (``dqrdc2`` / ``dqrls``) compiled with numba, calling the BLAS.
2
+
3
+ R's Fortran routines call the BLAS level-1 kernels ``dnrm2``, ``ddot``,
4
+ ``daxpy`` and ``dscal``. Floating-point results of those kernels depend on
5
+ their summation order, so for decisions that sit on a knife edge (a column
6
+ is "negligible" if its residual norm falls below ``1e-11`` times its
7
+ original norm) the only way to reproduce R is to make the same calls in the
8
+ same order. This module does that through SciPy's BLAS (the library NumPy
9
+ and SciPy are linked to); with the same BLAS build as R's, the QR is
10
+ bit-identical to R's.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import ctypes
15
+
16
+ import numpy as np
17
+
18
+ __all__ = ["dqrdc2_blas", "dqrls_blas", "available"]
19
+
20
+ try:
21
+ import numba
22
+ from numba import njit
23
+ import scipy.linalg.cython_blas as _cb
24
+
25
+ def _fn(name, restype, *argtypes):
26
+ cap = _cb.__pyx_capi__[name]
27
+ ctypes.pythonapi.PyCapsule_GetName.restype = ctypes.c_char_p
28
+ ctypes.pythonapi.PyCapsule_GetName.argtypes = [ctypes.py_object]
29
+ ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
30
+ ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p]
31
+ ptr = ctypes.pythonapi.PyCapsule_GetPointer(cap, ctypes.pythonapi.PyCapsule_GetName(cap))
32
+ return ctypes.CFUNCTYPE(restype, *argtypes)(ptr)
33
+
34
+ _P = ctypes.c_void_p
35
+ _dnrm2 = _fn("dnrm2", ctypes.c_double, _P, _P, _P)
36
+ _ddot = _fn("ddot", ctypes.c_double, _P, _P, _P, _P, _P)
37
+ _daxpy = _fn("daxpy", None, _P, _P, _P, _P, _P, _P)
38
+ _dscal = _fn("dscal", None, _P, _P, _P, _P)
39
+ available = True
40
+ except Exception: # pragma: no cover - numba or the capsules missing
41
+ available = False
42
+
43
+
44
+ if available:
45
+
46
+ @njit(cache=False)
47
+ def _dqrdc2(x, tol, jpvt, qraux, work, ib, db):
48
+ # x: Fortran-ordered (n, p). ib: int32 scratch[4], db: float64 scratch[2]
49
+ n, p = x.shape
50
+ xp = x.ctypes.data
51
+ ld = n
52
+ step = 8 # sizeof(double)
53
+ one = ib.ctypes.data # ib[0] = 1 (incx)
54
+ nn = ib.ctypes.data + 4 # ib[1] = length
55
+ ib[0] = 1
56
+ if n > 0:
57
+ for j in range(p):
58
+ ib[1] = n
59
+ v = _dnrm2(nn, xp + (j * ld) * step, one)
60
+ qraux[j] = v
61
+ work[j, 0] = v
62
+ work[j, 1] = v if v != 0.0 else 1.0
63
+ lup = min(n, p)
64
+ k = p + 1
65
+ for l in range(lup):
66
+ while not ((l + 1) >= k or qraux[l] >= work[l, 1] * tol):
67
+ for i in range(n):
68
+ t = x[i, l]
69
+ for j in range(l + 1, p):
70
+ x[i, j - 1] = x[i, j]
71
+ x[i, p - 1] = t
72
+ i0 = jpvt[l]
73
+ t = qraux[l]
74
+ tt = work[l, 0]
75
+ ttt = work[l, 1]
76
+ for j in range(l + 1, p):
77
+ jpvt[j - 1] = jpvt[j]
78
+ qraux[j - 1] = qraux[j]
79
+ work[j - 1, 0] = work[j, 0]
80
+ work[j - 1, 1] = work[j, 1]
81
+ jpvt[p - 1] = i0
82
+ qraux[p - 1] = t
83
+ work[p - 1, 0] = tt
84
+ work[p - 1, 1] = ttt
85
+ k -= 1
86
+ if (l + 1) != n:
87
+ m = n - l
88
+ ib[1] = m
89
+ col_l = xp + (l * ld + l) * step
90
+ nrmxl = _dnrm2(nn, col_l, one)
91
+ if nrmxl != 0.0:
92
+ if x[l, l] != 0.0:
93
+ nrmxl = abs(nrmxl) if x[l, l] >= 0 else -abs(nrmxl)
94
+ db[0] = 1.0 / nrmxl
95
+ _dscal(nn, db.ctypes.data, col_l, one)
96
+ x[l, l] = 1.0 + x[l, l]
97
+ for j in range(l + 1, p):
98
+ col_j = xp + (j * ld + l) * step
99
+ t = -_ddot(nn, col_l, one, col_j, one) / x[l, l]
100
+ db[1] = t
101
+ _daxpy(nn, db.ctypes.data + 8, col_l, one, col_j, one)
102
+ if qraux[j] != 0.0:
103
+ r = abs(x[l, j]) / qraux[j]
104
+ tt = 1.0 - r * r
105
+ if tt < 0.0:
106
+ tt = 0.0
107
+ t = tt
108
+ if abs(t) >= 1e-6:
109
+ qraux[j] = qraux[j] * np.sqrt(t)
110
+ else:
111
+ ib[2] = n - l - 1
112
+ qraux[j] = _dnrm2(ib.ctypes.data + 8, xp + (j * ld + l + 1) * step, one)
113
+ work[j, 0] = qraux[j]
114
+ qraux[l] = x[l, l]
115
+ x[l, l] = -nrmxl
116
+ return min(k - 1, n)
117
+
118
+ @njit(cache=False)
119
+ def _dqrsl_b(x, k, qraux, y, b, ib, db):
120
+ # qty (job digit b) then b (job digit c), as dqrsl with job 1110
121
+ n = x.shape[0]
122
+ xp = x.ctypes.data
123
+ step = 8
124
+ ib[0] = 1
125
+ one = ib.ctypes.data
126
+ nn = ib.ctypes.data + 4
127
+ qty = y.copy()
128
+ qp = qty.ctypes.data
129
+ ju = min(k, n - 1)
130
+ for j in range(ju):
131
+ if qraux[j] != 0.0:
132
+ temp = x[j, j]
133
+ x[j, j] = qraux[j]
134
+ ib[1] = n - j
135
+ col = xp + (j * n + j) * step
136
+ t = -_ddot(nn, col, one, qp + j * step, one) / x[j, j]
137
+ db[1] = t
138
+ _daxpy(nn, db.ctypes.data + 8, col, one, qp + j * step, one)
139
+ x[j, j] = temp
140
+ for i in range(k):
141
+ b[i] = qty[i]
142
+ bp = b.ctypes.data
143
+ for jj in range(k):
144
+ j = k - jj - 1
145
+ if x[j, j] == 0.0:
146
+ return j + 1 # info
147
+ b[j] = b[j] / x[j, j]
148
+ if j > 0:
149
+ db[1] = -b[j]
150
+ ib[1] = j
151
+ _daxpy(nn, db.ctypes.data + 8, xp + (j * n) * step, one, bp, one)
152
+ return 0
153
+
154
+
155
+ def dqrdc2_blas(x: np.ndarray, tol: float):
156
+ """Same contract as :func:`pypando.rstats.dqrdc2`, BLAS-exact."""
157
+ X = np.array(x, dtype=np.float64, order="F", copy=True)
158
+ p = X.shape[1]
159
+ jpvt = np.arange(p, dtype=np.int64)
160
+ qraux = np.zeros(p)
161
+ work = np.zeros((p, 2))
162
+ ib = np.zeros(4, dtype=np.int32)
163
+ db = np.zeros(2)
164
+ k = _dqrdc2(X, float(tol), jpvt, qraux, work, ib, db)
165
+ return X, int(k), qraux, jpvt
166
+
167
+
168
+ def dqrls_blas(x: np.ndarray, y: np.ndarray, tol: float):
169
+ """Same contract as :func:`pypando.rstats.dqrls`, BLAS-exact."""
170
+ qr, k, qraux, pivot = dqrdc2_blas(x, tol)
171
+ p = qr.shape[1]
172
+ b = np.zeros(p)
173
+ if k > 0:
174
+ ib = np.zeros(4, dtype=np.int32)
175
+ db = np.zeros(2)
176
+ _dqrsl_b(qr, k, qraux, np.ascontiguousarray(y, dtype=np.float64), b, ib, db)
177
+ return {"coefficients": b, "rank": k, "pivot": pivot, "qr": qr, "qraux": qraux}
@@ -0,0 +1,77 @@
1
+ """One-sample Wilcoxon signed rank test as in R 4.4 (``stats::wilcox.test``).
2
+
3
+ R 4.4.x (the reference environment, R 4.4.3) drops zeros, ranks
4
+ ``abs(x)`` without rounding (``digits.rank = Inf``) and uses the exact
5
+ null distribution when ``n < 50`` with no ties and no zeros; otherwise the
6
+ normal approximation with tie correction and continuity correction.
7
+ R >= 4.6 changed the zero handling; this follows 4.4.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import math
12
+
13
+ import numpy as np
14
+ from scipy import stats
15
+
16
+ __all__ = ["wilcox_signed_rank_pvalue", "psignrank"]
17
+
18
+
19
+ def _csignrank_counts(n: int) -> np.ndarray:
20
+ """Number of subsets of {1..n} with each sum 0..n(n+1)/2."""
21
+ u = n * (n + 1) // 2
22
+ c = np.zeros(u + 1, dtype=object)
23
+ c[0] = 1
24
+ for k in range(1, n + 1):
25
+ c[k:] = c[k:] + c[:-k] if k <= u else c[k:]
26
+ return c
27
+
28
+
29
+ def psignrank(q: float, n: int, lower_tail: bool = True) -> float:
30
+ """``stats::psignrank`` (exact)."""
31
+ q = math.floor(q + 1e-7)
32
+ u = n * (n + 1) // 2
33
+ if q < 0:
34
+ return 0.0 if lower_tail else 1.0
35
+ if q >= u:
36
+ return 1.0 if lower_tail else 0.0
37
+ c = _csignrank_counts(n)
38
+ total = 2**n
39
+ low = sum(c[: q + 1])
40
+ p = low / total
41
+ return float(p) if lower_tail else float((total - low) / total)
42
+
43
+
44
+ def wilcox_signed_rank_pvalue(x, mu: float = 0.0, correct: bool = True, exact=None) -> float:
45
+ """``wilcox.test(x, mu = mu)$p.value``, two-sided, R 4.4 semantics."""
46
+ x = np.asarray(x, dtype=np.float64)
47
+ x = x[~np.isnan(x)]
48
+ if len(x) < 1:
49
+ raise ValueError("not enough (non-missing) 'x' observations")
50
+ x = x - mu
51
+ zeroes = bool(np.any(x == 0))
52
+ if zeroes:
53
+ x = x[x != 0]
54
+ n = float(len(x))
55
+ if exact is None:
56
+ exact = n < 50
57
+ r = stats.rankdata(np.abs(x), method="average")
58
+ statistic = float(r[x > 0].sum())
59
+ ties = len(r) != len(np.unique(r))
60
+ if exact and not ties and not zeroes:
61
+ ni = int(n)
62
+ if statistic > n * (n + 1) / 4:
63
+ p = psignrank(statistic - 1, ni, lower_tail=False)
64
+ else:
65
+ p = psignrank(statistic, ni)
66
+ return min(2 * p, 1.0)
67
+ _, nties = np.unique(r, return_counts=True)
68
+ z = statistic - n * (n + 1) / 4
69
+ sigma = math.sqrt(max(0.0, n * (n + 1) * (2 * n + 1) / 24 - float(np.sum(nties.astype(float) ** 3 - nties)) / 48))
70
+ correction = math.copysign(0.5, z) if (correct and z != 0) else 0.0
71
+ # IEEE arithmetic as in R: sigma = 0 (e.g. all values were zeros and were
72
+ # dropped) gives z = NaN (0/0) or +-Inf, and the p-value NaN or 0.
73
+ with np.errstate(divide="ignore", invalid="ignore"):
74
+ z = np.float64(z - correction) / np.float64(sigma)
75
+ if np.isnan(z):
76
+ return float("nan")
77
+ return float(2 * min(stats.norm.cdf(z), stats.norm.sf(z)))