1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
|
"""Trophic levels"""
import networkx as nx
from networkx.utils import not_implemented_for
__all__ = ["trophic_levels", "trophic_differences", "trophic_incoherence_parameter"]
@not_implemented_for("undirected")
@nx._dispatchable(edge_attrs="weight")
def trophic_levels(G, weight="weight"):
r"""Compute the trophic levels of nodes.
The trophic level of a node $i$ is
.. math::
s_i = 1 + \frac{1}{k^{in}_i} \sum_{j} a_{ij} s_j
where $k^{in}_i$ is the in-degree of i
.. math::
k^{in}_i = \sum_{j} a_{ij}
and nodes with $k^{in}_i = 0$ have $s_i = 1$ by convention.
These are calculated using the method outlined in Levine [1]_.
Parameters
----------
G : DiGraph
A directed networkx graph
Returns
-------
nodes : dict
Dictionary of nodes with trophic level as the value.
References
----------
.. [1] Stephen Levine (1980) J. theor. Biol. 83, 195-207
"""
import numpy as np
# find adjacency matrix
a = nx.adjacency_matrix(G, weight=weight).T.toarray()
# drop rows/columns where in-degree is zero
rowsum = np.sum(a, axis=1)
p = a[rowsum != 0][:, rowsum != 0]
# normalise so sum of in-degree weights is 1 along each row
p = p / rowsum[rowsum != 0][:, np.newaxis]
# calculate trophic levels
nn = p.shape[0]
i = np.eye(nn)
try:
n = np.linalg.inv(i - p)
except np.linalg.LinAlgError as err:
# LinAlgError is raised when there is a non-basal node
msg = (
"Trophic levels are only defined for graphs where every "
+ "node has a path from a basal node (basal nodes are nodes "
+ "with no incoming edges)."
)
raise nx.NetworkXError(msg) from err
y = n.sum(axis=1) + 1
levels = {}
# all nodes with in-degree zero have trophic level == 1
zero_node_ids = (node_id for node_id, degree in G.in_degree if degree == 0)
for node_id in zero_node_ids:
levels[node_id] = 1
# all other nodes have levels as calculated
nonzero_node_ids = (node_id for node_id, degree in G.in_degree if degree != 0)
for i, node_id in enumerate(nonzero_node_ids):
levels[node_id] = y.item(i)
return levels
@not_implemented_for("undirected")
@nx._dispatchable(edge_attrs="weight")
def trophic_differences(G, weight="weight"):
r"""Compute the trophic differences of the edges of a directed graph.
The trophic difference $x_ij$ for each edge is defined in Johnson et al.
[1]_ as:
.. math::
x_ij = s_j - s_i
Where $s_i$ is the trophic level of node $i$.
Parameters
----------
G : DiGraph
A directed networkx graph
Returns
-------
diffs : dict
Dictionary of edges with trophic differences as the value.
References
----------
.. [1] Samuel Johnson, Virginia Dominguez-Garcia, Luca Donetti, Miguel A.
Munoz (2014) PNAS "Trophic coherence determines food-web stability"
"""
levels = trophic_levels(G, weight=weight)
diffs = {}
for u, v in G.edges:
diffs[(u, v)] = levels[v] - levels[u]
return diffs
@not_implemented_for("undirected")
@nx._dispatchable(edge_attrs="weight")
def trophic_incoherence_parameter(G, weight="weight", cannibalism=False):
r"""Compute the trophic incoherence parameter of a graph.
Trophic coherence is defined as the homogeneity of the distribution of
trophic distances: the more similar, the more coherent. This is measured by
the standard deviation of the trophic differences and referred to as the
trophic incoherence parameter $q$ by [1].
Parameters
----------
G : DiGraph
A directed networkx graph
cannibalism: Boolean
If set to False, self edges are not considered in the calculation
Returns
-------
trophic_incoherence_parameter : float
The trophic coherence of a graph
References
----------
.. [1] Samuel Johnson, Virginia Dominguez-Garcia, Luca Donetti, Miguel A.
Munoz (2014) PNAS "Trophic coherence determines food-web stability"
"""
import numpy as np
if cannibalism:
diffs = trophic_differences(G, weight=weight)
else:
# If no cannibalism, remove self-edges
self_loops = list(nx.selfloop_edges(G))
if self_loops:
# Make a copy so we do not change G's edges in memory
G_2 = G.copy()
G_2.remove_edges_from(self_loops)
else:
# Avoid copy otherwise
G_2 = G
diffs = trophic_differences(G_2, weight=weight)
return float(np.std(list(diffs.values())))
|