commsubspace 0.1.0__py3-none-any.whl
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/__init__.py +10 -0
- commsubspace/_linalg.py +64 -0
- commsubspace/_noise_floor.py +107 -0
- commsubspace/communication_subspace.py +523 -0
- commsubspace/contrastive_communication_subspace.py +556 -0
- commsubspace-0.1.0.dist-info/METADATA +68 -0
- commsubspace-0.1.0.dist-info/RECORD +10 -0
- commsubspace-0.1.0.dist-info/WHEEL +5 -0
- commsubspace-0.1.0.dist-info/licenses/LICENSE +21 -0
- commsubspace-0.1.0.dist-info/top_level.txt +1 -0
commsubspace/__init__.py
ADDED
|
@@ -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__"]
|
commsubspace/_linalg.py
ADDED
|
@@ -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())
|