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