maxent_graph 0.3.1__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.
- maxent_graph/MaxentGraph.py +206 -0
- maxent_graph/__init__.py +11 -0
- maxent_graph/bicm.py +301 -0
- maxent_graph/biecm.py +308 -0
- maxent_graph/bwcm.py +177 -0
- maxent_graph/decm.py +228 -0
- maxent_graph/ecm.py +199 -0
- maxent_graph/poibin.py +269 -0
- maxent_graph/rcm.py +158 -0
- maxent_graph/util.py +133 -0
- maxent_graph-0.3.1.dist-info/METADATA +50 -0
- maxent_graph-0.3.1.dist-info/RECORD +14 -0
- maxent_graph-0.3.1.dist-info/WHEEL +4 -0
- maxent_graph-0.3.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contains ABC for Maximum Entropy graph null model.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
import time
|
|
7
|
+
from abc import abstractmethod, ABC
|
|
8
|
+
from collections import namedtuple
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import scipy.optimize
|
|
12
|
+
from jax import jit, jacfwd, jacrev, grad
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
from .util import wrap_with_array, print_percentiles, jax_class_jit, hvp
|
|
16
|
+
|
|
17
|
+
Solution = namedtuple(
|
|
18
|
+
"Solution", ["x", "nll", "residual_error_norm", "relative_error", "total_time"]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MaxentGraph(ABC):
|
|
23
|
+
"""
|
|
24
|
+
ABC for Maximum Entropy graph null model.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def bounds(self):
|
|
29
|
+
"""
|
|
30
|
+
Returns the bounds on the parameters vector.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def clip(self, v):
|
|
34
|
+
"""
|
|
35
|
+
Clips the parameters vector according to bounds.
|
|
36
|
+
"""
|
|
37
|
+
(lower, upper), _bounds_object = self.bounds()
|
|
38
|
+
return np.clip(v, lower, upper)
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def transform_parameters(self, v):
|
|
42
|
+
"""
|
|
43
|
+
Transforms parameters to bounded form.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def transform_parameters_inv(self, v):
|
|
48
|
+
"""
|
|
49
|
+
Transforms parameters to all real numbers for optimization convenience.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def order_node_sequence(self):
|
|
54
|
+
"""
|
|
55
|
+
Concatenates node constraint sequence in a canonical order.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def get_initial_guess(self, option):
|
|
60
|
+
"""
|
|
61
|
+
Gets initial guess.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def expected_node_sequence(self, v):
|
|
66
|
+
"""
|
|
67
|
+
Computes the expected node constraint using matrices.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def expected_node_sequence_loops(self, v):
|
|
72
|
+
"""
|
|
73
|
+
Computes the expected node constraint using loops.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
@jax_class_jit
|
|
77
|
+
def node_sequence_residuals(self, v):
|
|
78
|
+
"""
|
|
79
|
+
Computes the residuals of the expected node constraint sequence minus the actual sequence.
|
|
80
|
+
"""
|
|
81
|
+
return self.expected_node_sequence(v) - self.order_node_sequence()
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def neg_log_likelihood_loops(self, v):
|
|
85
|
+
"""
|
|
86
|
+
Computes the negative log-likelihood using loops.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def neg_log_likelihood(self, v):
|
|
91
|
+
"""
|
|
92
|
+
Computes the negative log-likelihood using matrix operations.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def compute_relative_error(self, expected):
|
|
96
|
+
"""
|
|
97
|
+
Computes relative error for solution for every element of the sequence.
|
|
98
|
+
"""
|
|
99
|
+
actual = self.order_node_sequence()
|
|
100
|
+
|
|
101
|
+
# okay not actually relative error but close enough
|
|
102
|
+
return np.abs(expected - actual) / (1 + np.abs(actual))
|
|
103
|
+
|
|
104
|
+
def solve(self, x0, method="trust-krylov", verbose=False):
|
|
105
|
+
"""
|
|
106
|
+
Solves for the parameters of the null model using either bounded minimization of the
|
|
107
|
+
negative log-likelihood or bounded least-squares minimization of the equation residuals.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
args = {}
|
|
111
|
+
|
|
112
|
+
# for some reason scipy prefers hess over hessp if the former is passed
|
|
113
|
+
# but since the latter is more efficient, only pass hess when necessary
|
|
114
|
+
if method in ["trust-exact", "dogleg"]:
|
|
115
|
+
hess = jit(jacfwd(jacrev(self.neg_log_likelihood)))
|
|
116
|
+
args["hess"] = hess
|
|
117
|
+
elif method in ["Newton-CG", "trust-ncg", "trust-krylov", "trust-constr"]:
|
|
118
|
+
hessp = jit(hvp(self.neg_log_likelihood))
|
|
119
|
+
args["hessp"] = hessp
|
|
120
|
+
|
|
121
|
+
if method in ["trf", "dogbox", "lm"]:
|
|
122
|
+
f = self.node_sequence_residuals
|
|
123
|
+
jac = jit(jacrev(self.expected_node_sequence))
|
|
124
|
+
args["jac"] = jac
|
|
125
|
+
solver = scipy.optimize.least_squares
|
|
126
|
+
elif method in [
|
|
127
|
+
"Nelder-Mead",
|
|
128
|
+
"Powell",
|
|
129
|
+
"CG",
|
|
130
|
+
"BFGS",
|
|
131
|
+
"Newton-CG",
|
|
132
|
+
"L-BFGS-B",
|
|
133
|
+
"TNC",
|
|
134
|
+
"COBYLA",
|
|
135
|
+
"SLSQP",
|
|
136
|
+
"trust-constr",
|
|
137
|
+
"dogleg",
|
|
138
|
+
"trust-ncg",
|
|
139
|
+
"trust-exact",
|
|
140
|
+
"trust-krylov",
|
|
141
|
+
]:
|
|
142
|
+
f = self.neg_log_likelihood
|
|
143
|
+
jac = jit(grad(self.neg_log_likelihood))
|
|
144
|
+
|
|
145
|
+
# lbfgsb is fussy. wont accept jax's devicearray
|
|
146
|
+
# there may be others, though
|
|
147
|
+
if method in ["L-BFGS-B"]:
|
|
148
|
+
jac = wrap_with_array(jac)
|
|
149
|
+
|
|
150
|
+
if method in [
|
|
151
|
+
"CG",
|
|
152
|
+
"BFGS",
|
|
153
|
+
"Newton-CG",
|
|
154
|
+
"L-BFGS-B",
|
|
155
|
+
"TNC",
|
|
156
|
+
"SLSQP",
|
|
157
|
+
"dogleg",
|
|
158
|
+
"trust-ncg",
|
|
159
|
+
"trust-krylov",
|
|
160
|
+
"trust-exact",
|
|
161
|
+
"trust-constr",
|
|
162
|
+
]:
|
|
163
|
+
args["jac"] = jac
|
|
164
|
+
|
|
165
|
+
solver = scipy.optimize.minimize
|
|
166
|
+
else:
|
|
167
|
+
raise ValueError("Invalid optimization method")
|
|
168
|
+
|
|
169
|
+
start = time.time()
|
|
170
|
+
sol = solver(f, x0=x0, method=method, **args)
|
|
171
|
+
end = time.time()
|
|
172
|
+
|
|
173
|
+
total_time = end - start
|
|
174
|
+
|
|
175
|
+
eq_r = self.node_sequence_residuals(sol.x)
|
|
176
|
+
expected = self.expected_node_sequence(sol.x)
|
|
177
|
+
residual_error_norm = np.linalg.norm(eq_r, ord=2)
|
|
178
|
+
relative_error = self.compute_relative_error(expected)
|
|
179
|
+
nll = self.neg_log_likelihood(sol.x)
|
|
180
|
+
|
|
181
|
+
if not sol.success:
|
|
182
|
+
if np.max(relative_error) < 0.5:
|
|
183
|
+
warnings.warn(
|
|
184
|
+
"Didn't succeed according to algorithm, but max relative error is low.",
|
|
185
|
+
RuntimeWarning,
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
raise RuntimeError(
|
|
189
|
+
f"Didn't succeed in minimization. Message: {sol.message}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if verbose:
|
|
193
|
+
print(f"Took {total_time} seconds")
|
|
194
|
+
print("Relative error for expected degree/strength sequence: ")
|
|
195
|
+
print()
|
|
196
|
+
print_percentiles(relative_error)
|
|
197
|
+
|
|
198
|
+
print(f"\nResidual error: {residual_error_norm}")
|
|
199
|
+
|
|
200
|
+
return Solution(
|
|
201
|
+
x=sol.x,
|
|
202
|
+
nll=float(nll),
|
|
203
|
+
residual_error_norm=residual_error_norm,
|
|
204
|
+
relative_error=relative_error,
|
|
205
|
+
total_time=total_time,
|
|
206
|
+
)
|
maxent_graph/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .bicm import BICM as BICM
|
|
2
|
+
from .biecm import BIECM as BIECM
|
|
3
|
+
from .bwcm import BWCM as BWCM
|
|
4
|
+
from .decm import DECM as DECM
|
|
5
|
+
from .ecm import ECM as ECM
|
|
6
|
+
from .rcm import RCM as RCM
|
|
7
|
+
|
|
8
|
+
import jax
|
|
9
|
+
|
|
10
|
+
# ensure jax is using doubles. important.
|
|
11
|
+
jax.config.update("jax_enable_x64", True)
|
maxent_graph/bicm.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy.optimize
|
|
3
|
+
import scipy.special
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import itertools
|
|
6
|
+
import numba
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
|
|
11
|
+
import jax.numpy as jnp
|
|
12
|
+
|
|
13
|
+
from .MaxentGraph import MaxentGraph
|
|
14
|
+
from .util import EPS, flatten, jax_class_jit, R_to_zero_to_inf
|
|
15
|
+
from . import poibin
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BICM(MaxentGraph):
|
|
19
|
+
def __init__(self, B, transform=0):
|
|
20
|
+
self.B = B
|
|
21
|
+
num_rows, num_cols = B.shape
|
|
22
|
+
|
|
23
|
+
self.num_edges = B.count_nonzero()
|
|
24
|
+
|
|
25
|
+
row_sums = flatten(B.sum(axis=1)).astype(np.float64)
|
|
26
|
+
col_sums = flatten(B.sum(axis=0)).astype(np.float64)
|
|
27
|
+
|
|
28
|
+
assert len(row_sums) == num_rows
|
|
29
|
+
assert len(col_sums) == num_cols
|
|
30
|
+
|
|
31
|
+
# since in empirical networks there will be many nodes with the same degree
|
|
32
|
+
# we can count them and use that information to speed up solving the equations.
|
|
33
|
+
# the bicm doesn't distinguish between nodes with the same degree.
|
|
34
|
+
# we also want to keep track of which nodes have which degree (for later). for that we just use pd's groupby
|
|
35
|
+
row_degrees, row_inverse, row_multiplicity = np.unique(
|
|
36
|
+
row_sums, return_index=False, return_inverse=True, return_counts=True
|
|
37
|
+
)
|
|
38
|
+
row_df = pd.DataFrame(row_sums)
|
|
39
|
+
self.row_groups = row_df.groupby(by=0).groups
|
|
40
|
+
self.row_degrees = row_degrees
|
|
41
|
+
self.row_inverse = row_inverse
|
|
42
|
+
self.row_multiplicity = row_multiplicity
|
|
43
|
+
|
|
44
|
+
col_degrees, col_inverse, col_multiplicity = np.unique(
|
|
45
|
+
col_sums, return_index=False, return_inverse=True, return_counts=True
|
|
46
|
+
)
|
|
47
|
+
col_df = pd.DataFrame(col_sums)
|
|
48
|
+
self.col_groups = col_df.groupby(by=0).groups
|
|
49
|
+
self.col_degrees = col_degrees
|
|
50
|
+
self.col_inverse = col_inverse
|
|
51
|
+
self.col_multiplicity = col_multiplicity
|
|
52
|
+
|
|
53
|
+
self.n_row_degrees = len(self.row_degrees)
|
|
54
|
+
self.n_col_degrees = len(self.col_degrees)
|
|
55
|
+
self.total_unique = self.n_row_degrees + self.n_col_degrees
|
|
56
|
+
|
|
57
|
+
self.transform, self.inv_transform = R_to_zero_to_inf[transform]
|
|
58
|
+
|
|
59
|
+
def bounds(self):
|
|
60
|
+
lower_bounds = np.array([EPS] * self.total_unique)
|
|
61
|
+
upper_bounds = np.array([np.inf] * self.total_unique)
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
(lower_bounds, upper_bounds),
|
|
65
|
+
scipy.optimize.Bounds(lower_bounds, upper_bounds),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def order_node_sequence(self):
|
|
69
|
+
return np.concatenate([self.row_degrees, self.col_degrees])
|
|
70
|
+
|
|
71
|
+
@jax_class_jit
|
|
72
|
+
def transform_parameters(self, v):
|
|
73
|
+
return self.transform(v)
|
|
74
|
+
|
|
75
|
+
@jax_class_jit
|
|
76
|
+
def transform_parameters_inv(self, v):
|
|
77
|
+
return self.inv_transform(v)
|
|
78
|
+
|
|
79
|
+
def get_initial_guess(self, option=1):
|
|
80
|
+
if option == 1:
|
|
81
|
+
x0_rows = self.row_degrees / np.max(self.row_degrees)
|
|
82
|
+
x0_cols = self.col_degrees / np.max(self.col_degrees)
|
|
83
|
+
elif option == 2:
|
|
84
|
+
x0_rows = self.row_degrees / np.sqrt(np.sum(self.row_degrees) + 1)
|
|
85
|
+
x0_cols = self.col_degrees / np.sqrt(np.sum(self.col_degrees) + 1)
|
|
86
|
+
elif option == 3:
|
|
87
|
+
denom = np.sqrt(np.sum(self.row_degrees) * np.sum(self.col_degrees))
|
|
88
|
+
|
|
89
|
+
x0_rows = self.row_degrees / denom
|
|
90
|
+
x0_cols = self.col_degrees / denom
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError("Invalid option value. Choose from 1-3.")
|
|
93
|
+
|
|
94
|
+
initial_guess = self.clip(np.concatenate([x0_rows, x0_cols]))
|
|
95
|
+
|
|
96
|
+
return self.transform_parameters_inv(initial_guess)
|
|
97
|
+
|
|
98
|
+
@jax_class_jit
|
|
99
|
+
def expected_node_sequence(self, v):
|
|
100
|
+
z = self.transform_parameters(v)
|
|
101
|
+
|
|
102
|
+
x = z[: self.n_row_degrees]
|
|
103
|
+
y = z[self.n_row_degrees :]
|
|
104
|
+
|
|
105
|
+
xy = jnp.outer(x, y)
|
|
106
|
+
p = xy / (1 + xy)
|
|
107
|
+
|
|
108
|
+
# row expected
|
|
109
|
+
# multiply every row by col_multiplicity then sum across columns
|
|
110
|
+
row_expected = (p * self.col_multiplicity).sum(axis=1)
|
|
111
|
+
|
|
112
|
+
# multiply every column by row_multiplicity then sum across rows
|
|
113
|
+
col_expected = (p.T * self.row_multiplicity).sum(axis=1)
|
|
114
|
+
|
|
115
|
+
return jnp.concatenate((row_expected, col_expected))
|
|
116
|
+
|
|
117
|
+
def expected_node_sequence_loops(self, v):
|
|
118
|
+
z = self.transform_parameters(v)
|
|
119
|
+
|
|
120
|
+
x = z[: self.n_row_degrees]
|
|
121
|
+
y = z[self.n_row_degrees :]
|
|
122
|
+
|
|
123
|
+
row_expected = np.zeros(self.n_row_degrees)
|
|
124
|
+
col_expected = np.zeros(self.n_col_degrees)
|
|
125
|
+
|
|
126
|
+
for i in range(self.n_row_degrees):
|
|
127
|
+
for j in range(self.n_col_degrees):
|
|
128
|
+
x_ij = x[i] * y[j]
|
|
129
|
+
v = x_ij / (1.0 + x_ij)
|
|
130
|
+
row_expected[i] += self.col_multiplicity[j] * v
|
|
131
|
+
col_expected[j] += self.row_multiplicity[i] * v
|
|
132
|
+
|
|
133
|
+
return np.concatenate((row_expected, col_expected))
|
|
134
|
+
|
|
135
|
+
def neg_log_likelihood_loops(self, v):
|
|
136
|
+
z = self.transform_parameters(v)
|
|
137
|
+
|
|
138
|
+
x = z[: self.n_row_degrees]
|
|
139
|
+
y = z[self.n_row_degrees :]
|
|
140
|
+
llhood = 0
|
|
141
|
+
|
|
142
|
+
for i in range(self.n_row_degrees):
|
|
143
|
+
llhood += self.row_degrees[i] * self.row_multiplicity[i] * np.log(x[i])
|
|
144
|
+
|
|
145
|
+
for i in range(self.n_col_degrees):
|
|
146
|
+
llhood += self.col_degrees[i] * self.col_multiplicity[i] * np.log(y[i])
|
|
147
|
+
|
|
148
|
+
for i in range(self.n_row_degrees):
|
|
149
|
+
for j in range(self.n_col_degrees):
|
|
150
|
+
llhood -= (
|
|
151
|
+
self.row_multiplicity[i]
|
|
152
|
+
* self.col_multiplicity[j]
|
|
153
|
+
* np.log(1 + x[i] * y[j])
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return -llhood
|
|
157
|
+
|
|
158
|
+
@jax_class_jit
|
|
159
|
+
def neg_log_likelihood(self, v):
|
|
160
|
+
z = self.transform_parameters(v)
|
|
161
|
+
|
|
162
|
+
x = z[: self.n_row_degrees]
|
|
163
|
+
y = z[self.n_row_degrees :]
|
|
164
|
+
|
|
165
|
+
llhood = jnp.sum(self.row_degrees * self.row_multiplicity * jnp.log(x))
|
|
166
|
+
llhood += jnp.sum(self.col_degrees * self.col_multiplicity * jnp.log(y))
|
|
167
|
+
|
|
168
|
+
Q = jnp.log(1 + jnp.outer(x, y))
|
|
169
|
+
Q = Q * self.col_multiplicity
|
|
170
|
+
Q = Q.T * self.row_multiplicity
|
|
171
|
+
# don't need to transpose back because we're summing anyways
|
|
172
|
+
llhood -= jnp.sum(Q)
|
|
173
|
+
|
|
174
|
+
return -llhood
|
|
175
|
+
|
|
176
|
+
def get_fitness_model_solution(self):
|
|
177
|
+
"""
|
|
178
|
+
Just a good initial guess based on the 'fitness' assumption
|
|
179
|
+
"""
|
|
180
|
+
start = time.time()
|
|
181
|
+
solution = scipy.optimize.root_scalar(
|
|
182
|
+
self.fitness_zero,
|
|
183
|
+
args=(
|
|
184
|
+
self.num_edges,
|
|
185
|
+
self.row_degrees,
|
|
186
|
+
self.row_multiplicity,
|
|
187
|
+
self.col_degrees,
|
|
188
|
+
self.col_multiplicity,
|
|
189
|
+
),
|
|
190
|
+
method=None,
|
|
191
|
+
x0=1e-10,
|
|
192
|
+
x1=0.01,
|
|
193
|
+
fprime=self.fitness_zero_prime,
|
|
194
|
+
fprime2=self.fitness_zero_prime_prime,
|
|
195
|
+
)
|
|
196
|
+
print(f"Fitness model solution took {time.time() - start}")
|
|
197
|
+
|
|
198
|
+
z = solution.root
|
|
199
|
+
|
|
200
|
+
return self.inv_transform(np.sqrt(z) * self.order_node_sequence())
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
@numba.jit(nopython=True)
|
|
204
|
+
def fitness_zero(x, E, row_degrees, row_mult, col_degrees, col_mult):
|
|
205
|
+
s = -E
|
|
206
|
+
for i, _ in enumerate(row_degrees):
|
|
207
|
+
for j, _ in enumerate(col_degrees):
|
|
208
|
+
kk = row_degrees[i] * col_degrees[j]
|
|
209
|
+
s += row_mult[i] * col_mult[j] * x * kk / (1 + x * kk)
|
|
210
|
+
return s
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
@numba.jit(nopython=True)
|
|
214
|
+
def fitness_zero_prime(x, E, row_degrees, row_mult, col_degrees, col_mult):
|
|
215
|
+
s = 0
|
|
216
|
+
for i, _ in enumerate(row_degrees):
|
|
217
|
+
for j, _ in enumerate(col_degrees):
|
|
218
|
+
kk = row_degrees[i] * col_degrees[j]
|
|
219
|
+
s += row_mult[i] * col_mult[j] * kk / (1 + x * kk) ** 2
|
|
220
|
+
return s
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
@numba.jit(nopython=True)
|
|
224
|
+
def fitness_zero_prime_prime(x, E, row_degrees, row_mult, col_degrees, col_mult):
|
|
225
|
+
s = 0
|
|
226
|
+
for i, _ in enumerate(row_degrees):
|
|
227
|
+
for j, _ in enumerate(col_degrees):
|
|
228
|
+
kk = row_degrees[i] * col_degrees[j]
|
|
229
|
+
s += row_mult[i] * col_mult[j] * -2 * kk**2 / (1 + x * kk) ** 3
|
|
230
|
+
return s
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
@numba.jit(nopython=True, parallel=True)
|
|
234
|
+
def get_probs_with_multiplicity(i, j, x, y):
|
|
235
|
+
v_i = x[i] * y
|
|
236
|
+
v_j = x[j] * y
|
|
237
|
+
|
|
238
|
+
ps_i = v_i / (1 + v_i)
|
|
239
|
+
ps_j = v_j / (1 + v_j)
|
|
240
|
+
|
|
241
|
+
expected_lambda_motif_probs = ps_i * ps_j
|
|
242
|
+
|
|
243
|
+
return expected_lambda_motif_probs
|
|
244
|
+
|
|
245
|
+
def get_projection(self, solution, p_val=0.05):
|
|
246
|
+
B = self.B
|
|
247
|
+
# faster indexing when dense. but, more memory. in most cases it won't be sparse so this is fine.
|
|
248
|
+
# this will be symmetric
|
|
249
|
+
observed_lambda_motif_counts = (B @ B.T).todense()
|
|
250
|
+
nonzero_set = set(zip(*observed_lambda_motif_counts.nonzero()))
|
|
251
|
+
|
|
252
|
+
print(f"Nonzero lambda-motif counts to check pval of {len(nonzero_set)}")
|
|
253
|
+
print(f"Total possible pairs: {scipy.special.comb(B.shape[0], 2)}")
|
|
254
|
+
|
|
255
|
+
z = self.transform(solution)
|
|
256
|
+
|
|
257
|
+
# numba doesn't like xlaarrays
|
|
258
|
+
x = np.array(z[: self.n_row_degrees])
|
|
259
|
+
y = np.array(z[self.n_row_degrees :])
|
|
260
|
+
|
|
261
|
+
print(f"Unique degrees {(self.n_row_degrees, self.n_col_degrees)}")
|
|
262
|
+
|
|
263
|
+
edgelist = []
|
|
264
|
+
print(
|
|
265
|
+
f"Total unique row degree pairs to check {scipy.special.comb(self.n_row_degrees, 2)}"
|
|
266
|
+
)
|
|
267
|
+
for i, j in tqdm(itertools.combinations(range(self.n_row_degrees), 2)):
|
|
268
|
+
degree_i = self.row_degrees[i]
|
|
269
|
+
degree_j = self.row_degrees[j]
|
|
270
|
+
|
|
271
|
+
expected_lambda_motif_probs_with_mult = self.get_probs_with_multiplicity(
|
|
272
|
+
i, j, x, y
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
mean = poibin.mean_with_multiplicity(
|
|
276
|
+
expected_lambda_motif_probs_with_mult, self.col_multiplicity
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# filter zeros to speed up loop
|
|
280
|
+
# this may slow it down if the number of nonzeros is very dense, but only slightly
|
|
281
|
+
# if it's sparse then this can substantially speed up
|
|
282
|
+
to_check = nonzero_set & set(
|
|
283
|
+
itertools.product(self.row_groups[degree_i], self.row_groups[degree_j])
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
for orig_i, orig_j in to_check:
|
|
287
|
+
observed = observed_lambda_motif_counts[orig_i, orig_j]
|
|
288
|
+
assert observed != 0
|
|
289
|
+
q = poibin.poisson_wh_cdf(observed, mean)
|
|
290
|
+
p = 1 - q
|
|
291
|
+
if p < p_val:
|
|
292
|
+
# the WH approx to the poisson isn't numerically stable this low, nor is the dc_fft
|
|
293
|
+
# breaks down in the 1-e14/1e-15 range
|
|
294
|
+
if p < 1e-13:
|
|
295
|
+
# technically an upper bound on poisson approx, but it keeps close enough for these very small values to get order of magnitude
|
|
296
|
+
# and is numerically stable/fast. seems to be stable at least until 1e-300
|
|
297
|
+
# certainly NOT low relative error this small, just order of magnitude is right
|
|
298
|
+
p = poibin.poisson_upper(observed, mean)
|
|
299
|
+
edgelist.append((orig_i, orig_j, -np.log(p)))
|
|
300
|
+
|
|
301
|
+
return edgelist
|