commsubspace 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.
- commsubspace-0.1.0/LICENSE +21 -0
- commsubspace-0.1.0/PKG-INFO +68 -0
- commsubspace-0.1.0/README.md +47 -0
- commsubspace-0.1.0/commsubspace/__init__.py +10 -0
- commsubspace-0.1.0/commsubspace/_linalg.py +64 -0
- commsubspace-0.1.0/commsubspace/_noise_floor.py +107 -0
- commsubspace-0.1.0/commsubspace/communication_subspace.py +523 -0
- commsubspace-0.1.0/commsubspace/contrastive_communication_subspace.py +556 -0
- commsubspace-0.1.0/commsubspace.egg-info/PKG-INFO +68 -0
- commsubspace-0.1.0/commsubspace.egg-info/SOURCES.txt +14 -0
- commsubspace-0.1.0/commsubspace.egg-info/dependency_links.txt +1 -0
- commsubspace-0.1.0/commsubspace.egg-info/requires.txt +3 -0
- commsubspace-0.1.0/commsubspace.egg-info/top_level.txt +1 -0
- commsubspace-0.1.0/pyproject.toml +34 -0
- commsubspace-0.1.0/setup.cfg +4 -0
- commsubspace-0.1.0/tests/test_packaging.py +114 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Eliezyer de Oliveira
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: commsubspace
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Communication subspaces between neural populations: CCA, reduced-rank regression and contrastive communication subspaces (cCS).
|
|
5
|
+
Author-email: Eliezyer de Oliveira <eliezyer.deoliveira@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/eliezyer/commsubspace
|
|
8
|
+
Project-URL: Issues, https://github.com/eliezyer/commsubspace/issues
|
|
9
|
+
Keywords: neuroscience,communication subspace,CCA,reduced rank regression,contrastive
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: numpy
|
|
18
|
+
Requires-Dist: scipy
|
|
19
|
+
Requires-Dist: scikit-learn
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# contrastive communication subspace
|
|
23
|
+
|
|
24
|
+
Python code for **contrastive communication subspace** and also implementations of regular communication subspace (CS) analysis between two neural populations (region 1 and region 2):
|
|
25
|
+
|
|
26
|
+
- `ReducedRankRegress` - reduced-rank regression (RRR) from region 1 to region 2. The original method used for CS in Semedo et al., 2019
|
|
27
|
+
- `CrossDecomposition` - CCA (`method='correlation'`) and PLS-SVD (`method='covariance'`). Other ways of achieving CS
|
|
28
|
+
- `cCS` - **contrastive communication subspace**: finds pairs of directions whose coupling between the two regions differs most between two conditions (A and B). Objectives: `'difference'`, `'ratio'`, `'normalized_difference'` and `'normalized_index'`.
|
|
29
|
+
|
|
30
|
+
All data arrays are `(n_samples, n_features)`.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install commsubspace
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Requires Python >= 3.10, NumPy, SciPy and scikit-learn.
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import numpy as np
|
|
44
|
+
from commsubspace import cCS, CrossDecomposition, ReducedRankRegress
|
|
45
|
+
|
|
46
|
+
rng = np.random.default_rng(0)
|
|
47
|
+
region1_A, region2_A = rng.standard_normal((500, 20)), rng.standard_normal((500, 15)) # condition A
|
|
48
|
+
region1_B, region2_B = rng.standard_normal((400, 20)), rng.standard_normal((400, 15)) # condition B
|
|
49
|
+
|
|
50
|
+
# Contrastive communication subspace: coupling that differs between conditions A and B.
|
|
51
|
+
ccs = cCS(method="normalized_index").fit(region1_A, region2_A, region1_B, region2_B)
|
|
52
|
+
print(ccs.region1_loadings_.shape, ccs.contrast_values_.shape) # (20, N_dim), (N_dim,)
|
|
53
|
+
|
|
54
|
+
# Canonical correlation analysis between the two regions (condition A only).
|
|
55
|
+
cca = CrossDecomposition(method="correlation").fit(region1_A, region2_A)
|
|
56
|
+
scores_region1, scores_region2 = cca.transform(region1_A, region2_A)
|
|
57
|
+
|
|
58
|
+
# Reduced-rank regression of region 2 on region 1, with 3 predictive dimensions.
|
|
59
|
+
rrr = ReducedRankRegress(rank=3).fit(region1_A, region2_A)
|
|
60
|
+
predicted_region2 = rrr.predict(region1_A)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
See the docstrings of `cCS`, `CrossDecomposition` and `ReducedRankRegress` for all options, fitted attributes and
|
|
64
|
+
units. Fitted `cCS` objects carry a `meta_` dictionary with the parameters, random seed and analysis version.
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT, see `LICENSE`.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# contrastive communication subspace
|
|
2
|
+
|
|
3
|
+
Python code for **contrastive communication subspace** and also implementations of regular communication subspace (CS) analysis between two neural populations (region 1 and region 2):
|
|
4
|
+
|
|
5
|
+
- `ReducedRankRegress` - reduced-rank regression (RRR) from region 1 to region 2. The original method used for CS in Semedo et al., 2019
|
|
6
|
+
- `CrossDecomposition` - CCA (`method='correlation'`) and PLS-SVD (`method='covariance'`). Other ways of achieving CS
|
|
7
|
+
- `cCS` - **contrastive communication subspace**: finds pairs of directions whose coupling between the two regions differs most between two conditions (A and B). Objectives: `'difference'`, `'ratio'`, `'normalized_difference'` and `'normalized_index'`.
|
|
8
|
+
|
|
9
|
+
All data arrays are `(n_samples, n_features)`.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install commsubspace
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Python >= 3.10, NumPy, SciPy and scikit-learn.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import numpy as np
|
|
23
|
+
from commsubspace import cCS, CrossDecomposition, ReducedRankRegress
|
|
24
|
+
|
|
25
|
+
rng = np.random.default_rng(0)
|
|
26
|
+
region1_A, region2_A = rng.standard_normal((500, 20)), rng.standard_normal((500, 15)) # condition A
|
|
27
|
+
region1_B, region2_B = rng.standard_normal((400, 20)), rng.standard_normal((400, 15)) # condition B
|
|
28
|
+
|
|
29
|
+
# Contrastive communication subspace: coupling that differs between conditions A and B.
|
|
30
|
+
ccs = cCS(method="normalized_index").fit(region1_A, region2_A, region1_B, region2_B)
|
|
31
|
+
print(ccs.region1_loadings_.shape, ccs.contrast_values_.shape) # (20, N_dim), (N_dim,)
|
|
32
|
+
|
|
33
|
+
# Canonical correlation analysis between the two regions (condition A only).
|
|
34
|
+
cca = CrossDecomposition(method="correlation").fit(region1_A, region2_A)
|
|
35
|
+
scores_region1, scores_region2 = cca.transform(region1_A, region2_A)
|
|
36
|
+
|
|
37
|
+
# Reduced-rank regression of region 2 on region 1, with 3 predictive dimensions.
|
|
38
|
+
rrr = ReducedRankRegress(rank=3).fit(region1_A, region2_A)
|
|
39
|
+
predicted_region2 = rrr.predict(region1_A)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
See the docstrings of `cCS`, `CrossDecomposition` and `ReducedRankRegress` for all options, fitted attributes and
|
|
43
|
+
units. Fitted `cCS` objects carry a `meta_` dictionary with the parameters, random seed and analysis version.
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
MIT, see `LICENSE`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# commsubspace/__init__.py
|
|
2
|
+
# Public API of the commsubspace package (communication subspaces between two neural populations).
|
|
3
|
+
|
|
4
|
+
from .communication_subspace import CrossDecomposition, ReducedRankRegress
|
|
5
|
+
from .contrastive_communication_subspace import cCS
|
|
6
|
+
|
|
7
|
+
# Keep in sync with ``version`` in pyproject.toml (checked by tests/test_packaging.py).
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
__all__ = ["cCS", "CrossDecomposition", "ReducedRankRegress", "__version__"]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Small linear-algebra helpers shared by the contrastive communication subspace."""
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def psd_inverse_sqrt_factor(matrix, relative_tolerance):
|
|
8
|
+
"""Whitening factor F of a PSD matrix on its numerical range: F^T M F = I.
|
|
9
|
+
|
|
10
|
+
With M = Q diag(lambda) Q^T, F = Q_r diag(lambda_r)^(-1/2) where only the
|
|
11
|
+
eigenvalues above ``relative_tolerance * max(lambda)`` are kept (numerical rank,
|
|
12
|
+
the same condition-number rule as ``numpy.linalg.matrix_rank``). Directions in
|
|
13
|
+
which M is numerically zero are left out, so no ridge is needed.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
matrix : ndarray, shape (m, m)
|
|
18
|
+
Symmetric positive semi-definite matrix (units: those of the matrix).
|
|
19
|
+
relative_tolerance : float
|
|
20
|
+
Relative eigenvalue cutoff in (0, 1).
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
ndarray, shape (m, r)
|
|
25
|
+
Factor with r = numerical rank of ``matrix``; columns are Q_r / sqrt(lambda_r).
|
|
26
|
+
Units: 1 / sqrt(units of ``matrix``).
|
|
27
|
+
"""
|
|
28
|
+
eigenvalues, eigenvectors = np.linalg.eigh(matrix)
|
|
29
|
+
kept = eigenvalues > relative_tolerance * eigenvalues.max()
|
|
30
|
+
return eigenvectors[:, kept] / np.sqrt(eigenvalues[kept])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def floored_polar_roots(matrix, floor):
|
|
34
|
+
"""Square roots sqrt(M M^T) and sqrt(M^T M) of a rectangular matrix via its SVD.
|
|
35
|
+
|
|
36
|
+
Taking the SVD of ``M`` directly (instead of eigen-decomposing M M^T) keeps small
|
|
37
|
+
singular values accurate: forming M M^T squares the round-off error, so a true
|
|
38
|
+
singular value of 1e-12 would be reported as about 1e-8.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
matrix : ndarray, shape (m, n)
|
|
43
|
+
Real matrix (e.g. a cross-covariance between region 1 and region 2).
|
|
44
|
+
floor : float
|
|
45
|
+
Lower bound applied to every eigenvalue of both roots, including those of
|
|
46
|
+
directions outside the range of ``matrix`` (whose singular value is 0). Same
|
|
47
|
+
units as the singular values of ``matrix``. ``0`` gives the plain roots.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
left_root : ndarray, shape (m, m)
|
|
52
|
+
U diag(max(s, floor)) U^T, equal to sqrt(M M^T) when floor = 0.
|
|
53
|
+
right_root : ndarray, shape (n, n)
|
|
54
|
+
V diag(max(s, floor)) V^T, equal to sqrt(M^T M) when floor = 0.
|
|
55
|
+
"""
|
|
56
|
+
left_vectors, singular_values, right_vectors_t = np.linalg.svd(matrix, full_matrices=True)
|
|
57
|
+
n_rows, n_cols = matrix.shape
|
|
58
|
+
left_values = np.full(n_rows, 0.0)
|
|
59
|
+
left_values[:singular_values.size] = singular_values
|
|
60
|
+
right_values = np.full(n_cols, 0.0)
|
|
61
|
+
right_values[:singular_values.size] = singular_values
|
|
62
|
+
left_root = (left_vectors * np.maximum(left_values, floor)) @ left_vectors.T
|
|
63
|
+
right_root = (right_vectors_t.T * np.maximum(right_values, floor)) @ right_vectors_t
|
|
64
|
+
return left_root, right_root
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Noise floors for cross-covariance matrices.
|
|
3
|
+
|
|
4
|
+
A "noise floor" is the size (largest singular value) that a cross-covariance
|
|
5
|
+
matrix of two *unrelated* populations would reach just from finite-sample
|
|
6
|
+
estimation error. Singular values below it cannot be told apart from noise.
|
|
7
|
+
|
|
8
|
+
Two estimates are provided:
|
|
9
|
+
|
|
10
|
+
* analytic: random-matrix edge (sqrt(p) + sqrt(q)) for a p x q matrix of
|
|
11
|
+
independent unit-variance entries (Davidson & Szarek 2001; Vershynin 2018,
|
|
12
|
+
High-Dimensional Probability, Sec. 4.4; asymptotics: Bai & Yin 1993).
|
|
13
|
+
Assumes independent samples and white signals (see ``effective_n``).
|
|
14
|
+
* shuffle: top singular value of the cross-covariance after circularly shifting
|
|
15
|
+
one region in time; preserves autocorrelation of each region and destroys
|
|
16
|
+
their coupling.
|
|
17
|
+
|
|
18
|
+
All cross-covariances here are "sum type", C = R1^T R2 (no division by the number
|
|
19
|
+
of samples), the same convention used by ``cCS``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def analytic_noise_floor(n_samples, n_features_1, n_features_2, std_1, std_2,
|
|
26
|
+
effective_n=None):
|
|
27
|
+
"""Analytic noise floor of the sum-type cross-covariance R1^T R2.
|
|
28
|
+
|
|
29
|
+
For independent, white signals the entries of R1^T R2 have standard
|
|
30
|
+
deviation sqrt(n_samples) * std_1 * std_2, and the largest singular value of
|
|
31
|
+
such a matrix is about (sqrt(p) + sqrt(q)) times that. With autocorrelated
|
|
32
|
+
samples, the effective number of independent samples ``effective_n`` replaces
|
|
33
|
+
``n_samples`` inside the square root of the estimation error:
|
|
34
|
+
floor = n_samples * std_1 * std_2 * (sqrt(p) + sqrt(q)) / sqrt(effective_n).
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
n_samples : int
|
|
39
|
+
Number of samples T of the condition.
|
|
40
|
+
n_features_1, n_features_2 : int
|
|
41
|
+
Number of features p (region 1) and q (region 2).
|
|
42
|
+
std_1, std_2 : float
|
|
43
|
+
Root-mean-square column standard deviation of region 1 and region 2
|
|
44
|
+
(same units as the data). For unit-norm columns this is 1/sqrt(T).
|
|
45
|
+
effective_n : float or None
|
|
46
|
+
Effective number of independent samples. ``None`` means ``n_samples``.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
float
|
|
51
|
+
Noise floor in the units of R1^T R2 (data units squared, or
|
|
52
|
+
correlation units when columns have unit norm).
|
|
53
|
+
"""
|
|
54
|
+
if effective_n is None:
|
|
55
|
+
effective_n = n_samples
|
|
56
|
+
edge = np.sqrt(n_features_1) + np.sqrt(n_features_2)
|
|
57
|
+
return float(n_samples * std_1 * std_2 * edge / np.sqrt(effective_n))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def random_circular_shift(data, rng, min_fraction=0.1):
|
|
61
|
+
"""Circularly shift ``data`` along time by a random lag.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
data : ndarray, shape (n_samples, n_features)
|
|
66
|
+
rng : numpy.random.Generator
|
|
67
|
+
min_fraction : float
|
|
68
|
+
The shift is drawn uniformly from [min_fraction * n_samples,
|
|
69
|
+
(1 - min_fraction) * n_samples] so the shifted copy is never nearly
|
|
70
|
+
aligned with the original.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
ndarray, shape (n_samples, n_features)
|
|
75
|
+
Row-rolled copy of ``data`` (same units).
|
|
76
|
+
"""
|
|
77
|
+
n_samples = data.shape[0]
|
|
78
|
+
lowest = max(1, int(min_fraction * n_samples))
|
|
79
|
+
highest = max(lowest + 1, n_samples - lowest + 1)
|
|
80
|
+
return np.roll(data, int(rng.integers(lowest, highest)), axis=0)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def shuffle_noise_floor(region1, region2, n_shuffles, rng):
|
|
84
|
+
"""Noise floor from circularly shifting region 2 relative to region 1.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
region1 : ndarray, shape (n_samples, p)
|
|
89
|
+
region2 : ndarray, shape (n_samples, q)
|
|
90
|
+
Same rows as ``region1`` (simultaneous samples), same units as used for
|
|
91
|
+
the cross-covariance being floored.
|
|
92
|
+
n_shuffles : int
|
|
93
|
+
Number of random circular shifts.
|
|
94
|
+
rng : numpy.random.Generator
|
|
95
|
+
Source of the random shifts (seed it for reproducibility).
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
float
|
|
100
|
+
Mean over shuffles of the largest singular value of
|
|
101
|
+
region1^T shift(region2), in the units of region1^T region2.
|
|
102
|
+
"""
|
|
103
|
+
top_singular_values = np.empty(n_shuffles)
|
|
104
|
+
for index in range(n_shuffles):
|
|
105
|
+
shifted = random_circular_shift(region2, rng)
|
|
106
|
+
top_singular_values[index] = np.linalg.svd(region1.T @ shifted, compute_uv=False)[0]
|
|
107
|
+
return float(top_singular_values.mean())
|