test_trophic.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. """Test trophic levels, trophic differences and trophic coherence
  2. """
  3. import pytest
  4. np = pytest.importorskip("numpy")
  5. pytest.importorskip("scipy")
  6. import networkx as nx
  7. def test_trophic_levels():
  8. """Trivial example"""
  9. G = nx.DiGraph()
  10. G.add_edge("a", "b")
  11. G.add_edge("b", "c")
  12. d = nx.trophic_levels(G)
  13. assert d == {"a": 1, "b": 2, "c": 3}
  14. def test_trophic_levels_levine():
  15. """Example from Figure 5 in Stephen Levine (1980) J. theor. Biol. 83,
  16. 195-207
  17. """
  18. S = nx.DiGraph()
  19. S.add_edge(1, 2, weight=1.0)
  20. S.add_edge(1, 3, weight=0.2)
  21. S.add_edge(1, 4, weight=0.8)
  22. S.add_edge(2, 3, weight=0.2)
  23. S.add_edge(2, 5, weight=0.3)
  24. S.add_edge(4, 3, weight=0.6)
  25. S.add_edge(4, 5, weight=0.7)
  26. S.add_edge(5, 4, weight=0.2)
  27. # save copy for later, test intermediate implementation details first
  28. S2 = S.copy()
  29. # drop nodes of in-degree zero
  30. z = [nid for nid, d in S.in_degree if d == 0]
  31. for nid in z:
  32. S.remove_node(nid)
  33. # find adjacency matrix
  34. q = nx.linalg.graphmatrix.adjacency_matrix(S).T
  35. # fmt: off
  36. expected_q = np.array([
  37. [0, 0, 0., 0],
  38. [0.2, 0, 0.6, 0],
  39. [0, 0, 0, 0.2],
  40. [0.3, 0, 0.7, 0]
  41. ])
  42. # fmt: on
  43. assert np.array_equal(q.todense(), expected_q)
  44. # must be square, size of number of nodes
  45. assert len(q.shape) == 2
  46. assert q.shape[0] == q.shape[1]
  47. assert q.shape[0] == len(S)
  48. nn = q.shape[0]
  49. i = np.eye(nn)
  50. n = np.linalg.inv(i - q)
  51. y = np.asarray(n) @ np.ones(nn)
  52. expected_y = np.array([1, 2.07906977, 1.46511628, 2.3255814])
  53. assert np.allclose(y, expected_y)
  54. expected_d = {1: 1, 2: 2, 3: 3.07906977, 4: 2.46511628, 5: 3.3255814}
  55. d = nx.trophic_levels(S2)
  56. for nid, level in d.items():
  57. expected_level = expected_d[nid]
  58. assert expected_level == pytest.approx(level, abs=1e-7)
  59. def test_trophic_levels_simple():
  60. matrix_a = np.array([[0, 0], [1, 0]])
  61. G = nx.from_numpy_array(matrix_a, create_using=nx.DiGraph)
  62. d = nx.trophic_levels(G)
  63. assert d[0] == pytest.approx(2, abs=1e-7)
  64. assert d[1] == pytest.approx(1, abs=1e-7)
  65. def test_trophic_levels_more_complex():
  66. # fmt: off
  67. matrix = np.array([
  68. [0, 1, 0, 0],
  69. [0, 0, 1, 0],
  70. [0, 0, 0, 1],
  71. [0, 0, 0, 0]
  72. ])
  73. # fmt: on
  74. G = nx.from_numpy_array(matrix, create_using=nx.DiGraph)
  75. d = nx.trophic_levels(G)
  76. expected_result = [1, 2, 3, 4]
  77. for ind in range(4):
  78. assert d[ind] == pytest.approx(expected_result[ind], abs=1e-7)
  79. # fmt: off
  80. matrix = np.array([
  81. [0, 1, 1, 0],
  82. [0, 0, 1, 1],
  83. [0, 0, 0, 1],
  84. [0, 0, 0, 0]
  85. ])
  86. # fmt: on
  87. G = nx.from_numpy_array(matrix, create_using=nx.DiGraph)
  88. d = nx.trophic_levels(G)
  89. expected_result = [1, 2, 2.5, 3.25]
  90. print("Calculated result: ", d)
  91. print("Expected Result: ", expected_result)
  92. for ind in range(4):
  93. assert d[ind] == pytest.approx(expected_result[ind], abs=1e-7)
  94. def test_trophic_levels_even_more_complex():
  95. # fmt: off
  96. # Another, bigger matrix
  97. matrix = np.array([
  98. [0, 0, 0, 0, 0],
  99. [0, 1, 0, 1, 0],
  100. [1, 0, 0, 0, 0],
  101. [0, 1, 0, 0, 0],
  102. [0, 0, 0, 1, 0]
  103. ])
  104. # Generated this linear system using pen and paper:
  105. K = np.array([
  106. [1, 0, -1, 0, 0],
  107. [0, 0.5, 0, -0.5, 0],
  108. [0, 0, 1, 0, 0],
  109. [0, -0.5, 0, 1, -0.5],
  110. [0, 0, 0, 0, 1],
  111. ])
  112. # fmt: on
  113. result_1 = np.ravel(np.linalg.inv(K) @ np.ones(5))
  114. G = nx.from_numpy_array(matrix, create_using=nx.DiGraph)
  115. result_2 = nx.trophic_levels(G)
  116. for ind in range(5):
  117. assert result_1[ind] == pytest.approx(result_2[ind], abs=1e-7)
  118. def test_trophic_levels_singular_matrix():
  119. """Should raise an error with graphs with only non-basal nodes"""
  120. matrix = np.identity(4)
  121. G = nx.from_numpy_array(matrix, create_using=nx.DiGraph)
  122. with pytest.raises(nx.NetworkXError) as e:
  123. nx.trophic_levels(G)
  124. msg = (
  125. "Trophic levels are only defined for graphs where every node "
  126. + "has a path from a basal node (basal nodes are nodes with no "
  127. + "incoming edges)."
  128. )
  129. assert msg in str(e.value)
  130. def test_trophic_levels_singular_with_basal():
  131. """Should fail to compute if there are any parts of the graph which are not
  132. reachable from any basal node (with in-degree zero).
  133. """
  134. G = nx.DiGraph()
  135. # a has in-degree zero
  136. G.add_edge("a", "b")
  137. # b is one level above a, c and d
  138. G.add_edge("c", "b")
  139. G.add_edge("d", "b")
  140. # c and d form a loop, neither are reachable from a
  141. G.add_edge("c", "d")
  142. G.add_edge("d", "c")
  143. with pytest.raises(nx.NetworkXError) as e:
  144. nx.trophic_levels(G)
  145. msg = (
  146. "Trophic levels are only defined for graphs where every node "
  147. + "has a path from a basal node (basal nodes are nodes with no "
  148. + "incoming edges)."
  149. )
  150. assert msg in str(e.value)
  151. # if self-loops are allowed, smaller example:
  152. G = nx.DiGraph()
  153. G.add_edge("a", "b") # a has in-degree zero
  154. G.add_edge("c", "b") # b is one level above a and c
  155. G.add_edge("c", "c") # c has a self-loop
  156. with pytest.raises(nx.NetworkXError) as e:
  157. nx.trophic_levels(G)
  158. msg = (
  159. "Trophic levels are only defined for graphs where every node "
  160. + "has a path from a basal node (basal nodes are nodes with no "
  161. + "incoming edges)."
  162. )
  163. assert msg in str(e.value)
  164. def test_trophic_differences():
  165. matrix_a = np.array([[0, 1], [0, 0]])
  166. G = nx.from_numpy_array(matrix_a, create_using=nx.DiGraph)
  167. diffs = nx.trophic_differences(G)
  168. assert diffs[(0, 1)] == pytest.approx(1, abs=1e-7)
  169. # fmt: off
  170. matrix_b = np.array([
  171. [0, 1, 1, 0],
  172. [0, 0, 1, 1],
  173. [0, 0, 0, 1],
  174. [0, 0, 0, 0]
  175. ])
  176. # fmt: on
  177. G = nx.from_numpy_array(matrix_b, create_using=nx.DiGraph)
  178. diffs = nx.trophic_differences(G)
  179. assert diffs[(0, 1)] == pytest.approx(1, abs=1e-7)
  180. assert diffs[(0, 2)] == pytest.approx(1.5, abs=1e-7)
  181. assert diffs[(1, 2)] == pytest.approx(0.5, abs=1e-7)
  182. assert diffs[(1, 3)] == pytest.approx(1.25, abs=1e-7)
  183. assert diffs[(2, 3)] == pytest.approx(0.75, abs=1e-7)
  184. def test_trophic_incoherence_parameter_no_cannibalism():
  185. matrix_a = np.array([[0, 1], [0, 0]])
  186. G = nx.from_numpy_array(matrix_a, create_using=nx.DiGraph)
  187. q = nx.trophic_incoherence_parameter(G, cannibalism=False)
  188. assert q == pytest.approx(0, abs=1e-7)
  189. # fmt: off
  190. matrix_b = np.array([
  191. [0, 1, 1, 0],
  192. [0, 0, 1, 1],
  193. [0, 0, 0, 1],
  194. [0, 0, 0, 0]
  195. ])
  196. # fmt: on
  197. G = nx.from_numpy_array(matrix_b, create_using=nx.DiGraph)
  198. q = nx.trophic_incoherence_parameter(G, cannibalism=False)
  199. assert q == pytest.approx(np.std([1, 1.5, 0.5, 0.75, 1.25]), abs=1e-7)
  200. # fmt: off
  201. matrix_c = np.array([
  202. [0, 1, 1, 0],
  203. [0, 1, 1, 1],
  204. [0, 0, 0, 1],
  205. [0, 0, 0, 1]
  206. ])
  207. # fmt: on
  208. G = nx.from_numpy_array(matrix_c, create_using=nx.DiGraph)
  209. q = nx.trophic_incoherence_parameter(G, cannibalism=False)
  210. # Ignore the -link
  211. assert q == pytest.approx(np.std([1, 1.5, 0.5, 0.75, 1.25]), abs=1e-7)
  212. # no self-loops case
  213. # fmt: off
  214. matrix_d = np.array([
  215. [0, 1, 1, 0],
  216. [0, 0, 1, 1],
  217. [0, 0, 0, 1],
  218. [0, 0, 0, 0]
  219. ])
  220. # fmt: on
  221. G = nx.from_numpy_array(matrix_d, create_using=nx.DiGraph)
  222. q = nx.trophic_incoherence_parameter(G, cannibalism=False)
  223. # Ignore the -link
  224. assert q == pytest.approx(np.std([1, 1.5, 0.5, 0.75, 1.25]), abs=1e-7)
  225. def test_trophic_incoherence_parameter_cannibalism():
  226. matrix_a = np.array([[0, 1], [0, 0]])
  227. G = nx.from_numpy_array(matrix_a, create_using=nx.DiGraph)
  228. q = nx.trophic_incoherence_parameter(G, cannibalism=True)
  229. assert q == pytest.approx(0, abs=1e-7)
  230. # fmt: off
  231. matrix_b = np.array([
  232. [0, 0, 0, 0, 0],
  233. [0, 1, 0, 1, 0],
  234. [1, 0, 0, 0, 0],
  235. [0, 1, 0, 0, 0],
  236. [0, 0, 0, 1, 0]
  237. ])
  238. # fmt: on
  239. G = nx.from_numpy_array(matrix_b, create_using=nx.DiGraph)
  240. q = nx.trophic_incoherence_parameter(G, cannibalism=True)
  241. assert q == pytest.approx(2, abs=1e-7)
  242. # fmt: off
  243. matrix_c = np.array([
  244. [0, 1, 1, 0],
  245. [0, 0, 1, 1],
  246. [0, 0, 0, 1],
  247. [0, 0, 0, 0]
  248. ])
  249. # fmt: on
  250. G = nx.from_numpy_array(matrix_c, create_using=nx.DiGraph)
  251. q = nx.trophic_incoherence_parameter(G, cannibalism=True)
  252. # Ignore the -link
  253. assert q == pytest.approx(np.std([1, 1.5, 0.5, 0.75, 1.25]), abs=1e-7)