chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .drawing import *
|
||||
from .plot import *
|
||||
from .positioning import *
|
||||
@@ -0,0 +1,253 @@
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
|
||||
def default_style(
|
||||
num_v: int,
|
||||
num_e: int,
|
||||
v_color: Union[str, list] = "r",
|
||||
e_color: Union[str, list] = "gray",
|
||||
e_fill_color: Union[str, list] = "whitesmoke",
|
||||
):
|
||||
_v_color = "r"
|
||||
_e_color = "gray"
|
||||
_e_fill_color = "whitesmoke"
|
||||
|
||||
v_color = fill_color(v_color, _v_color, num_v)
|
||||
e_color = fill_color(e_color, _e_color, num_e)
|
||||
e_fill_color = fill_color(e_fill_color, _e_fill_color, num_e)
|
||||
|
||||
return v_color, e_color, e_fill_color
|
||||
|
||||
|
||||
def default_bipartite_style(
|
||||
num_u: int,
|
||||
num_v: int,
|
||||
num_e: int,
|
||||
u_color: Union[str, list] = "m",
|
||||
v_color: Union[str, list] = "r",
|
||||
e_color: Union[str, list] = "gray",
|
||||
e_fill_color: Union[str, list] = "whitesmoke",
|
||||
):
|
||||
_u_color = "m"
|
||||
_v_color = "r"
|
||||
_e_color = "gray"
|
||||
_e_fill_color = "whitesmoke"
|
||||
|
||||
u_color = fill_color(u_color, _u_color, num_u)
|
||||
v_color = fill_color(v_color, _v_color, num_v)
|
||||
e_color = fill_color(e_color, _e_color, num_e)
|
||||
e_fill_color = fill_color(e_fill_color, _e_fill_color, num_e)
|
||||
|
||||
return u_color, v_color, e_color, e_fill_color
|
||||
|
||||
|
||||
def default_hypergraph_style(
|
||||
num_v: int,
|
||||
num_e: int,
|
||||
v_color: Union[str, list] = "r",
|
||||
e_color: Union[str, list] = "gray",
|
||||
e_fill_color: Union[str, list] = "whitesmoke",
|
||||
):
|
||||
_v_color = "r"
|
||||
_e_color = "gray"
|
||||
_e_fill_color = "whitesmoke"
|
||||
|
||||
v_color = fill_color(v_color, _v_color, num_v)
|
||||
e_color = fill_color(e_color, _e_color, num_e)
|
||||
e_fill_color = fill_color(e_fill_color, _e_fill_color, num_e)
|
||||
|
||||
return v_color, e_color, e_fill_color
|
||||
|
||||
|
||||
def default_size(
|
||||
num_v: int,
|
||||
e_list: List[tuple],
|
||||
v_size: Union[float, list] = 1.0,
|
||||
v_line_width: Union[float, list] = 1.0,
|
||||
e_line_width: Union[float, list] = 1.0,
|
||||
font_size: float = None,
|
||||
):
|
||||
import numpy as np
|
||||
|
||||
_v_size = 1 / np.sqrt(num_v + 10) * 0.1
|
||||
_v_line_width = 1 * np.exp(-num_v / 50)
|
||||
_e_line_width = 1 * np.exp(-len(e_list) / 120)
|
||||
_font_size = 20 * np.exp(-num_v / 100)
|
||||
v_size = fill_sizes(v_size, _v_size, num_v)
|
||||
v_line_width = fill_sizes(v_line_width, _v_line_width, num_v)
|
||||
print("len(e_list):", e_list)
|
||||
e_line_width = fill_sizes(e_line_width, _e_line_width, len(e_list))
|
||||
|
||||
font_size = _font_size if font_size is None else font_size
|
||||
|
||||
return v_size, v_line_width, e_line_width, font_size
|
||||
|
||||
|
||||
def default_bipartite_size(
|
||||
num_u: int,
|
||||
num_v: int,
|
||||
e_list: List[tuple],
|
||||
u_size: Union[float, list] = 1.0,
|
||||
u_line_width: Union[float, list] = 1.0,
|
||||
v_size: Union[float, list] = 1.0,
|
||||
v_line_width: Union[float, list] = 1.0,
|
||||
e_line_width: Union[float, list] = 1.0,
|
||||
u_font_size: float = 1.0,
|
||||
v_font_size: float = 1.0,
|
||||
):
|
||||
import numpy as np
|
||||
|
||||
_u_size = 1 / np.sqrt(num_u + 12) * 0.08
|
||||
_u_line_width = 1 * np.exp(-num_u / 50)
|
||||
_v_size = 1 / np.sqrt(num_v + 12) * 0.08
|
||||
_v_line_width = 1 * np.exp(-num_v / 50)
|
||||
_e_line_width = 1 * np.exp(-len(e_list) / 50)
|
||||
_u_font_size = 12 * np.exp(-((num_u / num_v) ** 0.3) * (num_u + num_v) / 100)
|
||||
_v_font_size = 12 * np.exp(-((num_v / num_u) ** 0.3) * (num_u + num_v) / 100)
|
||||
|
||||
u_size = fill_sizes(u_size, _u_size, num_u)
|
||||
u_line_width = fill_sizes(u_line_width, _u_line_width, num_u)
|
||||
v_size = fill_sizes(v_size, _v_size, num_v)
|
||||
v_line_width = fill_sizes(v_line_width, _v_line_width, num_v)
|
||||
e_line_width = fill_sizes(e_line_width, _e_line_width, len(e_list))
|
||||
|
||||
u_font_size = _u_font_size if u_font_size is None else u_font_size * _u_font_size
|
||||
v_font_size = _v_font_size if v_font_size is None else v_font_size * _v_font_size
|
||||
|
||||
return (
|
||||
u_size,
|
||||
u_line_width,
|
||||
v_size,
|
||||
v_line_width,
|
||||
e_line_width,
|
||||
u_font_size,
|
||||
v_font_size,
|
||||
)
|
||||
|
||||
|
||||
def default_strength(
|
||||
num_v: int,
|
||||
e_list: List[tuple],
|
||||
push_v_strength: float = 1.0,
|
||||
push_e_strength: float = 1.0,
|
||||
pull_e_strength: float = 1.0,
|
||||
pull_center_strength: float = 1.0,
|
||||
):
|
||||
_push_v_strength = 0.006
|
||||
_push_e_strength = 0.0
|
||||
_pull_e_strength = 0.045
|
||||
_pull_center_strength = 0.01
|
||||
|
||||
push_v_strength = fill_strength(push_v_strength, _push_v_strength)
|
||||
push_e_strength = fill_strength(push_e_strength, _push_e_strength)
|
||||
pull_e_strength = fill_strength(pull_e_strength, _pull_e_strength)
|
||||
pull_center_strength = fill_strength(pull_center_strength, _pull_center_strength)
|
||||
|
||||
return push_v_strength, push_e_strength, pull_e_strength, pull_center_strength
|
||||
|
||||
|
||||
def default_bipartite_strength(
|
||||
num_u: int,
|
||||
num_v: int,
|
||||
e_list: List[tuple],
|
||||
push_u_strength: float = 1.0,
|
||||
push_v_strength: float = 1.0,
|
||||
push_e_strength: float = 1.0,
|
||||
pull_e_strength: float = 1.0,
|
||||
pull_u_center_strength: float = 1.0,
|
||||
pull_v_center_strength: float = 1.0,
|
||||
):
|
||||
_push_u_strength = 0.005
|
||||
_push_v_strength = 0.005
|
||||
_push_e_strength = 0.0
|
||||
_pull_e_strength = 0.03
|
||||
_pull_u_center_strength = 0.04
|
||||
_pull_v_center_strength = 0.04
|
||||
|
||||
push_u_strength = fill_strength(push_u_strength, _push_u_strength)
|
||||
push_v_strength = fill_strength(push_v_strength, _push_v_strength)
|
||||
push_e_strength = fill_strength(push_e_strength, _push_e_strength)
|
||||
pull_e_strength = fill_strength(pull_e_strength, _pull_e_strength)
|
||||
pull_u_center_strength = fill_strength(
|
||||
pull_u_center_strength, _pull_u_center_strength
|
||||
)
|
||||
pull_v_center_strength = fill_strength(
|
||||
pull_v_center_strength, _pull_v_center_strength
|
||||
)
|
||||
|
||||
return (
|
||||
push_u_strength,
|
||||
push_v_strength,
|
||||
push_e_strength,
|
||||
pull_e_strength,
|
||||
pull_u_center_strength,
|
||||
pull_v_center_strength,
|
||||
)
|
||||
|
||||
|
||||
def default_hypergraph_strength(
|
||||
num_v: int,
|
||||
e_list: List[tuple],
|
||||
push_v_strength: float = 1.0,
|
||||
push_e_strength: float = 1.0,
|
||||
pull_e_strength: float = 1.0,
|
||||
pull_center_strength: float = 1.0,
|
||||
):
|
||||
_push_v_strength = 0.006
|
||||
_push_e_strength = 0.008
|
||||
_pull_e_strength = 0.007
|
||||
_pull_center_strength = 0.001
|
||||
|
||||
push_v_strength = fill_strength(push_v_strength, _push_v_strength)
|
||||
push_e_strength = fill_strength(push_e_strength, _push_e_strength)
|
||||
pull_e_strength = fill_strength(pull_e_strength, _pull_e_strength)
|
||||
pull_center_strength = fill_strength(pull_center_strength, _pull_center_strength)
|
||||
|
||||
return push_v_strength, push_e_strength, pull_e_strength, pull_center_strength
|
||||
|
||||
|
||||
def fill_color(
|
||||
custom_color: Optional[Union[str, list]], default_color: Any, length: int
|
||||
):
|
||||
if custom_color is None:
|
||||
return [default_color] * length
|
||||
elif isinstance(custom_color, list):
|
||||
if (
|
||||
isinstance(custom_color[0], str)
|
||||
or isinstance(custom_color[0], tuple)
|
||||
or isinstance(custom_color[0], list)
|
||||
):
|
||||
return custom_color
|
||||
else:
|
||||
return [custom_color] * length
|
||||
elif isinstance(custom_color, str):
|
||||
return [custom_color] * length
|
||||
else:
|
||||
raise ValueError("The specified value is not a valid type.")
|
||||
|
||||
|
||||
def fill_sizes(
|
||||
custom_scales: Optional[Union[float, list]], default_value: Any, length: int
|
||||
):
|
||||
if custom_scales is None:
|
||||
return [default_value] * length
|
||||
elif isinstance(custom_scales, list):
|
||||
assert (
|
||||
len(custom_scales) == length
|
||||
), "The specified value list has the wrong length."
|
||||
return [default_value * scale for scale in custom_scales]
|
||||
elif isinstance(custom_scales, float):
|
||||
return [default_value * custom_scales] * length
|
||||
elif isinstance(custom_scales, int):
|
||||
return [default_value * float(custom_scales)] * length
|
||||
else:
|
||||
raise ValueError("The specified value is not a valid type.")
|
||||
|
||||
|
||||
def fill_strength(custom_scale: Optional[float], default_value: float):
|
||||
if custom_scale is None:
|
||||
return default_value
|
||||
return custom_scale * default_value
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
import math
|
||||
|
||||
from math import pi
|
||||
|
||||
|
||||
def radian_from_atan(x, y):
|
||||
if x == 0:
|
||||
return pi / 2 if y > 0 else 3 * pi / 2
|
||||
if y == 0:
|
||||
return 0 if x > 0 else pi
|
||||
r = math.atan(y / x)
|
||||
if x > 0 and y > 0:
|
||||
return r
|
||||
elif x > 0 and y < 0:
|
||||
return r + 2 * pi
|
||||
elif x < 0 and y > 0:
|
||||
return r + pi
|
||||
else:
|
||||
return r + pi
|
||||
|
||||
|
||||
def vlen(vector):
|
||||
return math.sqrt(vector[0] ** 2 + vector[1] ** 2)
|
||||
|
||||
|
||||
def common_tangent_radian(r1, r2, d):
|
||||
if r1 < 0 or r2 < 0:
|
||||
raise ValueError("Circle radii must be non-negative.")
|
||||
if d <= 0 or d < abs(r2 - r1):
|
||||
raise ValueError("No common tangent exists for the given circles.")
|
||||
value = abs(r2 - r1) / d
|
||||
if value > 1.0:
|
||||
value = 1.0
|
||||
elif value < -1.0:
|
||||
value = -1.0
|
||||
alpha = math.acos(value)
|
||||
alpha = alpha if r1 > r2 else pi - alpha
|
||||
return alpha
|
||||
|
||||
|
||||
def polar_position(r, theta, start_point):
|
||||
import numpy as np
|
||||
|
||||
x = r * math.cos(theta)
|
||||
y = r * math.sin(theta)
|
||||
return np.array([x, y]) + start_point
|
||||
|
||||
|
||||
def rad_2_deg(rad):
|
||||
return rad * 180 / pi
|
||||
@@ -0,0 +1,33 @@
|
||||
from typing import List
|
||||
|
||||
from .simulator import Simulator
|
||||
from .utils import edge_list_to_incidence_matrix
|
||||
from .utils import init_pos
|
||||
|
||||
|
||||
def force_layout(
|
||||
num_v: int,
|
||||
e_list: List[tuple],
|
||||
push_v_strength: float,
|
||||
push_e_strength: float,
|
||||
pull_e_strength: float,
|
||||
pull_center_strength: float,
|
||||
):
|
||||
import numpy as np
|
||||
|
||||
v_coor = init_pos(num_v, scale=5)
|
||||
assert v_coor.max() <= 5.0 and v_coor.min() >= -5.0
|
||||
centers = [np.array([0, 0])]
|
||||
sim = Simulator(
|
||||
nums=num_v,
|
||||
forces={
|
||||
Simulator.NODE_ATTRACTION: pull_e_strength,
|
||||
Simulator.NODE_REPULSION: push_v_strength,
|
||||
Simulator.EDGE_REPULSION: push_e_strength,
|
||||
Simulator.CENTER_GRAVITY: pull_center_strength,
|
||||
},
|
||||
centers=centers,
|
||||
)
|
||||
v_coor = sim.simulate(v_coor, edge_list_to_incidence_matrix(num_v, e_list))
|
||||
v_coor = (v_coor - v_coor.min(0)) / (v_coor.max(0) - v_coor.min(0)) * 0.8 + 0.1
|
||||
return v_coor
|
||||
@@ -0,0 +1,233 @@
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
__all__ = [
|
||||
"plot_Followers",
|
||||
"plot_Connected_Communities",
|
||||
"plot_Betweenness_Centrality",
|
||||
"plot_Neighborhood_Followers",
|
||||
]
|
||||
|
||||
|
||||
# Number of Followers
|
||||
def plot_Followers(G, SHS):
|
||||
"""
|
||||
Returns the CDF curves of "Number of Followers" of SH spanners and ordinary users in graph G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph.
|
||||
|
||||
SHS : list
|
||||
The SH Spanners in graph G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
plt : CDF curves
|
||||
the CDF curves of "Number of Followers" of SH spanners and ordinary users in graph G.
|
||||
"""
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import statsmodels.api as sm
|
||||
|
||||
assert len(SHS) < len(
|
||||
G.nodes
|
||||
), "The number of SHS must be less than the number of nodes in the graph."
|
||||
OU = []
|
||||
for i in G:
|
||||
if i not in SHS:
|
||||
OU.append(i)
|
||||
degree = G.degree()
|
||||
sample1 = []
|
||||
sample2 = []
|
||||
for i in degree.keys():
|
||||
if i in OU:
|
||||
sample1.append(degree[i])
|
||||
elif i in SHS:
|
||||
sample2.append(degree[i])
|
||||
X1 = np.linspace(min(sample1), max(sample1))
|
||||
ecdf = sm.distributions.ECDF(sample1)
|
||||
Y1 = ecdf(X1)
|
||||
X2 = np.linspace(min(sample2), max(sample2))
|
||||
ecdf = sm.distributions.ECDF(sample2)
|
||||
Y2 = ecdf(X2)
|
||||
plt.plot(X1, Y1, "b--", label="Ordinary User")
|
||||
plt.plot(X2, Y2, "r", label="SH Spanner")
|
||||
plt.title("Number of Followers")
|
||||
plt.xlabel("Number of Followers")
|
||||
plt.ylabel("Cumulative Distribution Function")
|
||||
plt.legend(loc="lower right")
|
||||
plt.show()
|
||||
|
||||
|
||||
# Number of Connected Communities
|
||||
def plot_Connected_Communities(G, SHS):
|
||||
"""
|
||||
Returns the CDF curves of "Number of Connected Communities" of SH spanners and ordinary users in graph G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph.
|
||||
|
||||
SHS : list
|
||||
The SH Spanners in graph G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
plt : CDF curves
|
||||
the CDF curves of "Number of Connected Communities" of SH spanners and ordinary users in graph G.
|
||||
"""
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import statsmodels.api as sm
|
||||
|
||||
OU = []
|
||||
for i in G:
|
||||
if i not in SHS:
|
||||
OU.append(i)
|
||||
sample1 = []
|
||||
sample2 = []
|
||||
cmts = eg.LPA(G)
|
||||
for i in OU:
|
||||
s = set()
|
||||
neighbors = G.neighbors(node=i)
|
||||
for j in neighbors:
|
||||
for k in cmts:
|
||||
if j in cmts[k]:
|
||||
s.add(k)
|
||||
sample1.append(len(s))
|
||||
for i in SHS:
|
||||
s = set()
|
||||
neighbors = G.neighbors(node=i)
|
||||
for j in neighbors:
|
||||
for k in cmts:
|
||||
if j in cmts[k]:
|
||||
s.add(k)
|
||||
sample2.append(len(s))
|
||||
print(len(cmts))
|
||||
print(sample1)
|
||||
print(sample2)
|
||||
X1 = np.linspace(min(sample1), max(sample1))
|
||||
ecdf = sm.distributions.ECDF(sample1)
|
||||
Y1 = ecdf(X1)
|
||||
X2 = np.linspace(min(sample2), max(sample2))
|
||||
ecdf = sm.distributions.ECDF(sample2)
|
||||
Y2 = ecdf(X2)
|
||||
plt.plot(X1, Y1, "b--", label="Ordinary User")
|
||||
plt.plot(X2, Y2, "r", label="SH Spanner")
|
||||
plt.title("Number of Connected Communities")
|
||||
plt.xlabel("Number of Connected Communities")
|
||||
plt.ylabel("Cumulative Distribution Function")
|
||||
plt.legend(loc="lower right")
|
||||
plt.show()
|
||||
|
||||
|
||||
# Betweenness Centrality
|
||||
def plot_Betweenness_Centrality(G, SHS):
|
||||
"""
|
||||
Returns the CDF curves of "Betweenness Centralitys" of SH spanners and ordinary users in graph G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph.
|
||||
|
||||
SHS : list
|
||||
The SH Spanners in graph G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
plt : CDF curves
|
||||
the CDF curves of "Betweenness Centrality" of SH spanners and ordinary users in graph G.
|
||||
"""
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import statsmodels.api as sm
|
||||
|
||||
OU = []
|
||||
for i in G:
|
||||
if i not in SHS:
|
||||
OU.append(i)
|
||||
bc = eg.betweenness_centrality(G)
|
||||
bc = dict(zip(G.nodes, bc))
|
||||
sample1 = []
|
||||
sample2 = []
|
||||
for i in bc.keys():
|
||||
if i in OU:
|
||||
sample1.append(bc[i])
|
||||
else:
|
||||
sample2.append(bc[i])
|
||||
X1 = np.linspace(min(sample1), max(sample1))
|
||||
ecdf = sm.distributions.ECDF(sample1)
|
||||
Y1 = ecdf(X1)
|
||||
X2 = np.linspace(min(sample2), max(sample2))
|
||||
ecdf = sm.distributions.ECDF(sample2)
|
||||
Y2 = ecdf(X2)
|
||||
plt.plot(X1, Y1, "b--", label="Ordinary User")
|
||||
plt.plot(X2, Y2, "r", label="SH Spanner")
|
||||
plt.title("Betweenness Centrality")
|
||||
plt.xlabel("Betweenness Centrality")
|
||||
plt.ylabel("Cumulative Distribution Function")
|
||||
plt.legend(loc="lower right")
|
||||
plt.show()
|
||||
|
||||
|
||||
# Arg. Number of Followers of the Neighborhood Users
|
||||
def plot_Neighborhood_Followers(G, SHS):
|
||||
"""
|
||||
Returns the CDF curves of "Arg. Number of Followers of the Neighborhood Users" of SH spanners and ordinary users in graph G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph.
|
||||
|
||||
SHS : list
|
||||
The SH Spanners in graph G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
plt : CDF curves
|
||||
the CDF curves of "Arg. Number of Followers of the Neighborhood Users
|
||||
" of SH spanners and ordinary users in graph G.
|
||||
"""
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import statsmodels.api as sm
|
||||
|
||||
OU = []
|
||||
for i in G:
|
||||
if i not in SHS:
|
||||
OU.append(i)
|
||||
sample1 = []
|
||||
sample2 = []
|
||||
degree = G.degree()
|
||||
for i in OU:
|
||||
num = 0
|
||||
sum = 0
|
||||
for neighbor in G.neighbors(node=i):
|
||||
num = num + 1
|
||||
sum = sum + degree[neighbor]
|
||||
sample1.append(sum / num)
|
||||
for i in SHS:
|
||||
num = 0
|
||||
sum = 0
|
||||
for neighbor in G.neighbors(node=i):
|
||||
num = num + 1
|
||||
sum = sum + degree[neighbor]
|
||||
sample2.append(sum / num)
|
||||
X1 = np.linspace(min(sample1), max(sample1))
|
||||
ecdf = sm.distributions.ECDF(sample1)
|
||||
Y1 = ecdf(X1)
|
||||
X2 = np.linspace(min(sample2), max(sample2))
|
||||
ecdf = sm.distributions.ECDF(sample2)
|
||||
Y2 = ecdf(X2)
|
||||
plt.plot(X1, Y1, "b--", label="Ordinary User")
|
||||
plt.plot(X2, Y2, "r", label="SH Spanner")
|
||||
plt.title("Arg. Number of Followers of the Neighborhood Users")
|
||||
plt.xlabel("Arg. Number of Followers of the Neighborhood Users")
|
||||
plt.ylabel("Cumulative Distribution Function")
|
||||
plt.legend(loc="lower right")
|
||||
plt.show()
|
||||
@@ -0,0 +1,646 @@
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = [
|
||||
"random_position",
|
||||
"circular_position",
|
||||
"shell_position",
|
||||
"rescale_position",
|
||||
"kamada_kawai_layout",
|
||||
# "spring_layout",
|
||||
# "fruchterman_reingold_layout",
|
||||
# "_process_params",
|
||||
# "_fruchterman_reingold",
|
||||
# "_sparse_fruchterman_reingold",
|
||||
]
|
||||
|
||||
|
||||
def random_position(G, center=None, dim=2, random_seed=None):
|
||||
"""
|
||||
Returns random position for each node in graph G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
center : array-like or None, optional (default : None)
|
||||
Coordinate pair around which to center the layout
|
||||
|
||||
dim : int, optional (default : 2)
|
||||
Dimension of layout
|
||||
|
||||
random_seed : int or None, optional (default : None)
|
||||
Seed for RandomState instance
|
||||
|
||||
Returns
|
||||
----------
|
||||
pos : dict
|
||||
A dictionary of positions keyed by node
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
center = _get_center(center, dim)
|
||||
|
||||
rng = np.random.RandomState(seed=random_seed)
|
||||
pos = rng.rand(len(G), dim) + center
|
||||
pos = pos.astype(np.float32)
|
||||
pos = dict(zip(G, pos))
|
||||
|
||||
return pos
|
||||
|
||||
|
||||
def circular_position(G, center=None, scale=1):
|
||||
"""
|
||||
Position nodes on a circle, the dimension is 2.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
A position will be assigned to every node in G
|
||||
|
||||
center : array-like or None, optional (default : None)
|
||||
Coordinate pair around which to center the layout
|
||||
|
||||
scale : number, optional (default : 1)
|
||||
Scale factor for positions
|
||||
|
||||
Returns
|
||||
-------
|
||||
pos : dict
|
||||
A dictionary of positions keyed by node
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
center = _get_center(center, dim=2)
|
||||
|
||||
if len(G) == 0:
|
||||
pos = {}
|
||||
elif len(G) == 1:
|
||||
pos = {G.nodes[0]: center}
|
||||
else:
|
||||
theta = np.linspace(0, 1, len(G), endpoint=False) * 2 * np.pi
|
||||
theta = theta.astype(np.float32)
|
||||
pos = np.column_stack([np.cos(theta), np.sin(theta)])
|
||||
pos = rescale_position(pos, scale=scale) + center
|
||||
pos = dict(zip(G, pos))
|
||||
|
||||
return pos
|
||||
|
||||
|
||||
def shell_position(G, nlist=None, scale=1, center=None):
|
||||
"""
|
||||
Position nodes in concentric circles, the dimension is 2.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
nlist : list of lists or None, optional (default : None)
|
||||
List of node lists for each shell.
|
||||
|
||||
scale : number, optional (default : 1)
|
||||
Scale factor for positions.
|
||||
|
||||
center : array-like or None, optional (default : None)
|
||||
Coordinate pair around which to center the layout.
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
pos : dict
|
||||
A dictionary of positions keyed by node
|
||||
|
||||
Notes
|
||||
-----
|
||||
This algorithm currently only works in two dimensions and does not
|
||||
try to minimize edge crossings.
|
||||
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
center = _get_center(center, dim=2)
|
||||
|
||||
if len(G) == 0:
|
||||
return {}
|
||||
if len(G) == 1:
|
||||
return {G.nodes[0]: center}
|
||||
|
||||
if nlist is None:
|
||||
# draw the whole graph in one shell
|
||||
nlist = [list(G)]
|
||||
|
||||
if len(nlist[0]) == 1:
|
||||
# single node at center
|
||||
radius = 0.0
|
||||
else:
|
||||
# else start at r=1
|
||||
radius = 1.0
|
||||
|
||||
npos = {}
|
||||
for nodes in nlist:
|
||||
# Discard the extra angle since it matches 0 radians.
|
||||
theta = np.linspace(0, 1, len(nodes), endpoint=False) * 2 * np.pi
|
||||
theta = theta.astype(np.float32)
|
||||
pos = np.column_stack([np.cos(theta), np.sin(theta)])
|
||||
if len(pos) > 1:
|
||||
pos = rescale_position(pos, scale=scale * radius / len(nlist)) + center
|
||||
else:
|
||||
pos = np.array([(scale * radius + center[0], center[1])])
|
||||
npos.update(zip(nodes, pos))
|
||||
radius += 1.0
|
||||
|
||||
return npos
|
||||
|
||||
|
||||
def _get_center(center, dim):
|
||||
import numpy as np
|
||||
|
||||
if center is None:
|
||||
center = np.zeros(dim)
|
||||
else:
|
||||
center = np.asarray(center)
|
||||
|
||||
if dim < 2:
|
||||
raise ValueError("cannot handle dimensions < 2")
|
||||
|
||||
if len(center) != dim:
|
||||
msg = "length of center coordinates must match dimension of layout"
|
||||
raise ValueError(msg)
|
||||
|
||||
return center
|
||||
|
||||
|
||||
def rescale_position(pos, scale=1):
|
||||
"""
|
||||
Returns scaled position array to (-scale, scale) in all axes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pos : numpy array
|
||||
positions to be scaled. Each row is a position.
|
||||
|
||||
scale : number, optional (default : 1)
|
||||
The size of the resulting extent in all directions.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pos : numpy array
|
||||
scaled positions. Each row is a position.
|
||||
"""
|
||||
# Find max length over all dimensions
|
||||
assert (
|
||||
len(pos.shape) != 1
|
||||
), "One-dimensional ndarray is not available for rescaling."
|
||||
lim = 0 # max coordinate for all axes
|
||||
for i in range(pos.shape[1]):
|
||||
pos[:, i] -= pos[:, i].mean()
|
||||
lim = max(abs(pos[:, i]).max(), lim)
|
||||
# rescale to (-scale, scale) in all directions, preserves aspect
|
||||
if lim > 0:
|
||||
for i in range(pos.shape[1]):
|
||||
pos[:, i] *= scale / lim
|
||||
return pos
|
||||
|
||||
|
||||
def kamada_kawai_layout(
|
||||
G, dist=None, pos=None, weight="weight", scale=1, center=None, dim=2
|
||||
):
|
||||
"""Position nodes using Kamada-Kawai basic-length cost-function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph or list of nodes
|
||||
A position will be assigned to every node in G.
|
||||
|
||||
dist : dict (default=None)
|
||||
A two-level dictionary of optimal distances between nodes,
|
||||
indexed by source and destination node.
|
||||
If None, the distance is computed using shortest_path_length().
|
||||
|
||||
pos : dict or None optional (default=None)
|
||||
Initial positions for nodes as a dictionary with node as keys
|
||||
and values as a coordinate list or tuple. If None, then use
|
||||
circular_layout() for dim >= 2 and a linear layout for dim == 1.
|
||||
|
||||
weight : string or None optional (default='weight')
|
||||
The edge attribute that holds the numerical value used for
|
||||
the edge weight. If None, then all edge weights are 1.
|
||||
|
||||
scale : number (default: 1)
|
||||
Scale factor for positions.
|
||||
|
||||
center : array-like or None
|
||||
Coordinate pair around which to center the layout.
|
||||
|
||||
dim : int
|
||||
Dimension of layout.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pos : dict
|
||||
A dictionary of positions keyed by node
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> pos = eg.kamada_kawai_layout(G)
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
nNodes = len(G)
|
||||
if nNodes == 0:
|
||||
return {}
|
||||
|
||||
if dist is None:
|
||||
dist = dict(eg.Floyd(G))
|
||||
dist_mtx = 1e6 * np.ones((nNodes, nNodes))
|
||||
for row, nr in enumerate(G):
|
||||
if nr not in dist:
|
||||
continue
|
||||
rdist = dist[nr]
|
||||
for col, nc in enumerate(G):
|
||||
if nc not in rdist:
|
||||
continue
|
||||
dist_mtx[row][col] = rdist[nc]
|
||||
|
||||
if pos is None:
|
||||
if dim >= 3:
|
||||
pos = eg.random_position(G, dim=dim)
|
||||
elif dim == 2:
|
||||
pos = eg.circular_position(G)
|
||||
else:
|
||||
pos = {n: pt for n, pt in zip(G, np.linspace(0, 1, len(G)))}
|
||||
|
||||
pos_arr = np.array([pos[n] for n in G])
|
||||
|
||||
pos = _kamada_kawai_solve(dist_mtx, pos_arr, dim)
|
||||
|
||||
if center is None:
|
||||
center = np.zeros(dim)
|
||||
else:
|
||||
center = np.asarray(center)
|
||||
|
||||
if len(center) != dim:
|
||||
msg = "length of center coordinates must match dimension of layout"
|
||||
raise ValueError(msg)
|
||||
|
||||
pos = eg.rescale_position(pos, scale=scale) + center
|
||||
return dict(zip(G, pos))
|
||||
|
||||
|
||||
def _kamada_kawai_solve(dist_mtx, pos_arr, dim):
|
||||
# Anneal node locations based on the Kamada-Kawai cost-function,
|
||||
# using the supplied matrix of preferred inter-node distances,
|
||||
# and starting locations.
|
||||
|
||||
import numpy as np
|
||||
|
||||
from scipy.optimize import minimize
|
||||
|
||||
meanwt = 1e-3
|
||||
costargs = (np, 1 / (dist_mtx + np.eye(dist_mtx.shape[0]) * 1e-3), meanwt, dim)
|
||||
|
||||
optresult = minimize(
|
||||
_kamada_kawai_costfn,
|
||||
pos_arr.ravel(),
|
||||
method="L-BFGS-B",
|
||||
args=costargs,
|
||||
jac=True,
|
||||
)
|
||||
|
||||
return optresult.x.reshape((-1, dim))
|
||||
|
||||
|
||||
def _kamada_kawai_costfn(pos_vec, np, invdist, meanweight, dim):
|
||||
# Cost-function and gradient for Kamada-Kawai layout algorithm
|
||||
nNodes = invdist.shape[0]
|
||||
pos_arr = pos_vec.reshape((nNodes, dim))
|
||||
|
||||
delta = pos_arr[:, np.newaxis, :] - pos_arr[np.newaxis, :, :]
|
||||
nodesep = np.linalg.norm(delta, axis=-1)
|
||||
direction = np.einsum("ijk,ij->ijk", delta, 1 / (nodesep + np.eye(nNodes) * 1e-3))
|
||||
|
||||
offset = nodesep * invdist - 1.0
|
||||
offset[np.diag_indices(nNodes)] = 0
|
||||
|
||||
cost = 0.5 * np.sum(offset**2)
|
||||
grad = np.einsum("ij,ij,ijk->ik", invdist, offset, direction) - np.einsum(
|
||||
"ij,ij,ijk->jk", invdist, offset, direction
|
||||
)
|
||||
|
||||
# Additional parabolic term to encourage mean position to be near origin:
|
||||
sumpos = np.sum(pos_arr, axis=0)
|
||||
cost += 0.5 * meanweight * np.sum(sumpos**2)
|
||||
grad += meanweight * sumpos
|
||||
|
||||
return (cost, grad.ravel())
|
||||
|
||||
|
||||
# @np_random_state(10)
|
||||
# def spring_layout(
|
||||
# G,
|
||||
# k=None,
|
||||
# pos=None,
|
||||
# fixed=None,
|
||||
# iterations=50,
|
||||
# threshold=1e-4,
|
||||
# weight="weight",
|
||||
# scale=1,
|
||||
# center=None,
|
||||
# dim=2,
|
||||
# seed=None,
|
||||
# ):
|
||||
# """Position nodes using Fruchterman-Reingold force-directed algorithm.
|
||||
#
|
||||
# The algorithm simulates a force-directed representation of the network
|
||||
# treating edges as springs holding nodes close, while treating nodes
|
||||
# as repelling objects, sometimes called an anti-gravity force.
|
||||
# Simulation continues until the positions are close to an equilibrium.
|
||||
#
|
||||
# There are some hard-coded values: minimal distance between
|
||||
# nodes (0.01) and "temperature" of 0.1 to ensure nodes don't fly away.
|
||||
# During the simulation, `k` helps determine the distance between nodes,
|
||||
# though `scale` and `center` determine the size and place after
|
||||
# rescaling occurs at the end of the simulation.
|
||||
#
|
||||
# Fixing some nodes doesn't allow them to move in the simulation.
|
||||
# It also turns off the rescaling feature at the simulation's end.
|
||||
# In addition, setting `scale` to `None` turns off rescaling.
|
||||
#
|
||||
# Parameters
|
||||
# ----------
|
||||
# G : EasyGraph graph or list of nodes
|
||||
# A position will be assigned to every node in G.
|
||||
#
|
||||
# k : float (default=None)
|
||||
# Optimal distance between nodes. If None the distance is set to
|
||||
# 1/sqrt(n) where n is the number of nodes. Increase this value
|
||||
# to move nodes farther apart.
|
||||
#
|
||||
# pos : dict or None optional (default=None)
|
||||
# Initial positions for nodes as a dictionary with node as keys
|
||||
# and values as a coordinate list or tuple. If None, then use
|
||||
# random initial positions.
|
||||
#
|
||||
# fixed : list or None optional (default=None)
|
||||
# Nodes to keep fixed at initial position.
|
||||
# Nodes not in ``G.nodes`` are ignored.
|
||||
# ValueError raised if `fixed` specified and `pos` not.
|
||||
#
|
||||
# iterations : int optional (default=50)
|
||||
# Maximum number of iterations taken
|
||||
#
|
||||
# threshold: float optional (default = 1e-4)
|
||||
# Threshold for relative error in node position changes.
|
||||
# The iteration stops if the error is below this threshold.
|
||||
#
|
||||
# weight : string or None optional (default='weight')
|
||||
# The edge attribute that holds the numerical value used for
|
||||
# the edge weight. Larger means a stronger attractive force.
|
||||
# If None, then all edge weights are 1.
|
||||
#
|
||||
# scale : number or None (default: 1)
|
||||
# Scale factor for positions. Not used unless `fixed is None`.
|
||||
# If scale is None, no rescaling is performed.
|
||||
#
|
||||
# center : array-like or None
|
||||
# Coordinate pair around which to center the layout.
|
||||
# Not used unless `fixed is None`.
|
||||
#
|
||||
# dim : int
|
||||
# Dimension of layout.
|
||||
#
|
||||
# seed : int, RandomState instance or None optional (default=None)
|
||||
# Set the random state for deterministic node layouts.
|
||||
# If int, `seed` is the seed used by the random number generator,
|
||||
# if numpy.random.RandomState instance, `seed` is the random
|
||||
# number generator,
|
||||
# if None, the random number generator is the RandomState instance used
|
||||
# by numpy.random.
|
||||
#
|
||||
# Returns
|
||||
# -------
|
||||
# pos : dict
|
||||
# A dictionary of positions keyed by node
|
||||
#
|
||||
# Examples
|
||||
# --------
|
||||
# >>> G = eg.path_graph(4)
|
||||
# >>> pos = eg.spring_layout(G)
|
||||
#
|
||||
#
|
||||
# """
|
||||
# import numpy as np
|
||||
#
|
||||
# G, center = _process_params(G, center, dim)
|
||||
#
|
||||
# if fixed is not None:
|
||||
# if pos is None:
|
||||
# raise ValueError("nodes are fixed without positions given")
|
||||
# for node in fixed:
|
||||
# if node not in pos:
|
||||
# raise ValueError("nodes are fixed without positions given")
|
||||
# nfixed = {node: i for i, node in enumerate(G)}
|
||||
# fixed = np.asarray([nfixed[node] for node in fixed if node in nfixed])
|
||||
#
|
||||
# if pos is not None:
|
||||
# # Determine size of existing domain to adjust initial positions
|
||||
# dom_size = max(coord for pos_tup in pos.values() for coord in pos_tup)
|
||||
# if dom_size == 0:
|
||||
# dom_size = 1
|
||||
# pos_arr = seed.rand(len(G), dim) * dom_size + center
|
||||
#
|
||||
# for i, n in enumerate(G):
|
||||
# if n in pos:
|
||||
# pos_arr[i] = np.asarray(pos[n])
|
||||
# else:
|
||||
# pos_arr = None
|
||||
# dom_size = 1
|
||||
#
|
||||
# if len(G) == 0:
|
||||
# return {}
|
||||
# if len(G) == 1:
|
||||
# return {eg.utils.arbitrary_element(G.nodes()): center}
|
||||
#
|
||||
# try:
|
||||
# # Sparse matrix
|
||||
# if len(G) < 500: # sparse solver for large graphs
|
||||
# raise ValueError
|
||||
# A = eg.to_scipy_sparse_array(G, weight=weight, dtype="f")
|
||||
# if k is None and fixed is not None:
|
||||
# # We must adjust k by domain size for layouts not near 1x1
|
||||
# nnodes, _ = A.shape
|
||||
# k = dom_size / np.sqrt(nnodes)
|
||||
# pos = _sparse_fruchterman_reingold(
|
||||
# A, k, pos_arr, fixed, iterations, threshold, dim, seed
|
||||
# )
|
||||
# except ValueError:
|
||||
# A = eg.to_numpy_array(G, weight=weight)
|
||||
# if k is None and fixed is not None:
|
||||
# # We must adjust k by domain size for layouts not near 1x1
|
||||
# nnodes, _ = A.shape
|
||||
# k = dom_size / np.sqrt(nnodes)
|
||||
# pos = _fruchterman_reingold(
|
||||
# A, k, pos_arr, fixed, iterations, threshold, dim, seed
|
||||
# )
|
||||
# if fixed is None and scale is not None:
|
||||
# pos = rescale_position(pos, scale=scale) + center
|
||||
# pos = dict(zip(G, pos))
|
||||
# return pos
|
||||
#
|
||||
# fruchterman_reingold_layout = spring_layout
|
||||
#
|
||||
# def _process_params(G, center, dim):
|
||||
# # Some boilerplate code.
|
||||
# import numpy as np
|
||||
#
|
||||
# if not isinstance(G, eg.Graph):
|
||||
# empty_graph = eg.Graph()
|
||||
# empty_graph.add_nodes_from(G)
|
||||
# G = empty_graph
|
||||
#
|
||||
# if center is None:
|
||||
# center = np.zeros(dim)
|
||||
# else:
|
||||
# center = np.asarray(center)
|
||||
#
|
||||
# if len(center) != dim:
|
||||
# msg = "length of center coordinates must match dimension of layout"
|
||||
# raise ValueError(msg)
|
||||
#
|
||||
# return G, center
|
||||
#
|
||||
# @np_random_state(7)
|
||||
# def _fruchterman_reingold(
|
||||
# A, k=None, pos=None, fixed=None, iterations=50, threshold=1e-4, dim=2, seed=None
|
||||
# ):
|
||||
# # Position nodes in adjacency matrix A using Fruchterman-Reingold
|
||||
# # Entry point for NetworkX graph is fruchterman_reingold_layout()
|
||||
# import numpy as np
|
||||
#
|
||||
# try:
|
||||
# nnodes, _ = A.shape
|
||||
# except AttributeError as err:
|
||||
# msg = "fruchterman_reingold() takes an adjacency matrix as input"
|
||||
# raise EasyGraphError(msg) from err
|
||||
#
|
||||
# if pos is None:
|
||||
# # random initial positions
|
||||
# pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype)
|
||||
# else:
|
||||
# # make sure positions are of same type as matrix
|
||||
# pos = pos.astype(A.dtype)
|
||||
#
|
||||
# # optimal distance between nodes
|
||||
# if k is None:
|
||||
# k = np.sqrt(1.0 / nnodes)
|
||||
# # the initial "temperature" is about .1 of domain area (=1x1)
|
||||
# # this is the largest step allowed in the dynamics.
|
||||
# # We need to calculate this in case our fixed positions force our domain
|
||||
# # to be much bigger than 1x1
|
||||
# t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
|
||||
# # simple cooling scheme.
|
||||
# # linearly step down by dt on each iteration so last iteration is size dt.
|
||||
# dt = t / (iterations + 1)
|
||||
# delta = np.zeros((pos.shape[0], pos.shape[0], pos.shape[1]), dtype=A.dtype)
|
||||
# # the inscrutable (but fast) version
|
||||
# # this is still O(V^2)
|
||||
# # could use multilevel methods to speed this up significantly
|
||||
# for iteration in range(iterations):
|
||||
# # matrix of difference between points
|
||||
# delta = pos[:, np.newaxis, :] - pos[np.newaxis, :, :]
|
||||
# # distance between points
|
||||
# distance = np.linalg.norm(delta, axis=-1)
|
||||
# # enforce minimum distance of 0.01
|
||||
# np.clip(distance, 0.01, None, out=distance)
|
||||
# # displacement "force"
|
||||
# displacement = np.einsum(
|
||||
# "ijk,ij->ik", delta, (k * k / distance**2 - A * distance / k)
|
||||
# )
|
||||
# # update positions
|
||||
# length = np.linalg.norm(displacement, axis=-1)
|
||||
# length = np.where(length < 0.01, 0.1, length)
|
||||
# delta_pos = np.einsum("ij,i->ij", displacement, t / length)
|
||||
# if fixed is not None:
|
||||
# # don't change positions of fixed nodes
|
||||
# delta_pos[fixed] = 0.0
|
||||
# pos += delta_pos
|
||||
# # cool temperature
|
||||
# t -= dt
|
||||
# if (np.linalg.norm(delta_pos) / nnodes) < threshold:
|
||||
# break
|
||||
# return pos
|
||||
#
|
||||
# @np_random_state(7)
|
||||
# def _sparse_fruchterman_reingold(
|
||||
# A, k=None, pos=None, fixed=None, iterations=50, threshold=1e-4, dim=2, seed=None
|
||||
# ):
|
||||
# # Position nodes in adjacency matrix A using Fruchterman-Reingold
|
||||
# # Entry point for NetworkX graph is fruchterman_reingold_layout()
|
||||
# # Sparse version
|
||||
# import numpy as np
|
||||
# import scipy as sp
|
||||
# import scipy.sparse # call as sp.sparse
|
||||
#
|
||||
# try:
|
||||
# nnodes, _ = A.shape
|
||||
# except AttributeError as err:
|
||||
# msg = "fruchterman_reingold() takes an adjacency matrix as input"
|
||||
# raise EasyGraphError(msg) from err
|
||||
# # make sure we have a LIst of Lists representation
|
||||
# try:
|
||||
# A = A.tolil()
|
||||
# except AttributeError:
|
||||
# A = (sp.sparse.coo_array(A)).tolil()
|
||||
#
|
||||
# if pos is None:
|
||||
# # random initial positions
|
||||
# pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype)
|
||||
# else:
|
||||
# # make sure positions are of same type as matrix
|
||||
# pos = pos.astype(A.dtype)
|
||||
#
|
||||
# # no fixed nodes
|
||||
# if fixed is None:
|
||||
# fixed = []
|
||||
#
|
||||
# # optimal distance between nodes
|
||||
# if k is None:
|
||||
# k = np.sqrt(1.0 / nnodes)
|
||||
# # the initial "temperature" is about .1 of domain area (=1x1)
|
||||
# # this is the largest step allowed in the dynamics.
|
||||
# t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
|
||||
# # simple cooling scheme.
|
||||
# # linearly step down by dt on each iteration so last iteration is size dt.
|
||||
# dt = t / (iterations + 1)
|
||||
#
|
||||
# displacement = np.zeros((dim, nnodes))
|
||||
# for iteration in range(iterations):
|
||||
# displacement *= 0
|
||||
# # loop over rows
|
||||
# for i in range(A.shape[0]):
|
||||
# if i in fixed:
|
||||
# continue
|
||||
# # difference between this row's node position and all others
|
||||
# delta = (pos[i] - pos).T
|
||||
# # distance between points
|
||||
# distance = np.sqrt((delta**2).sum(axis=0))
|
||||
# # enforce minimum distance of 0.01
|
||||
# distance = np.where(distance < 0.01, 0.01, distance)
|
||||
# # the adjacency matrix row
|
||||
# Ai = A.getrowview(i).toarray() # TODO: revisit w/ sparse 1D container
|
||||
# # displacement "force"
|
||||
# displacement[:, i] += (
|
||||
# delta * (k * k / distance**2 - Ai * distance / k)
|
||||
# ).sum(axis=1)
|
||||
# # update positions
|
||||
# length = np.sqrt((displacement**2).sum(axis=0))
|
||||
# length = np.where(length < 0.01, 0.1, length)
|
||||
# delta_pos = (displacement * t / length).T
|
||||
# pos += delta_pos
|
||||
# # cool temperature
|
||||
# t -= dt
|
||||
# if (np.linalg.norm(delta_pos) / nnodes) < threshold:
|
||||
# break
|
||||
# return pos
|
||||
@@ -0,0 +1,195 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from .utils import safe_div
|
||||
|
||||
|
||||
class Simulator:
|
||||
NODE_ATTRACTION = 0
|
||||
NODE_REPULSION = 1
|
||||
EDGE_REPULSION = 2
|
||||
CENTER_GRAVITY = 3
|
||||
|
||||
def __init__(self, nums, forces, centers=1, damping_factor=0.999) -> None:
|
||||
self.nums = [nums] if isinstance(nums, int) else nums
|
||||
|
||||
self.node_attraction = forces.get(self.NODE_ATTRACTION, None)
|
||||
self.node_repulsion = forces.get(self.NODE_REPULSION, None)
|
||||
self.edge_repulsion = forces.get(self.EDGE_REPULSION, None)
|
||||
self.center_gravity = forces.get(self.CENTER_GRAVITY, None)
|
||||
|
||||
self.n_centers = len(centers)
|
||||
self.centers = centers
|
||||
|
||||
if self.node_repulsion is not None and isinstance(self.node_repulsion, float):
|
||||
self.node_repulsion = [self.node_repulsion] * self.n_centers
|
||||
if self.center_gravity is not None and isinstance(self.center_gravity, float):
|
||||
self.center_gravity = [self.center_gravity] * self.n_centers
|
||||
|
||||
self.damping_factor = damping_factor
|
||||
|
||||
def simulate(self, init_position, H, max_iter=400, epsilon=0.001, dt=2.0) -> None:
|
||||
import numpy as np
|
||||
|
||||
"""
|
||||
Simulate the force-directed layout algorithm.
|
||||
"""
|
||||
position = init_position.copy()
|
||||
velocity = np.zeros_like(position)
|
||||
damping = 1.0
|
||||
for it in range(max_iter):
|
||||
position, velocity, stop = self._step(
|
||||
position, velocity, H, epsilon, damping, dt
|
||||
)
|
||||
if stop:
|
||||
break
|
||||
damping *= self.damping_factor
|
||||
return position
|
||||
|
||||
def _step(self, position, velocity, H, epsilon, damping, dt):
|
||||
import numpy as np
|
||||
|
||||
from sklearn.metrics import euclidean_distances
|
||||
|
||||
"""
|
||||
One step of the simulation.
|
||||
"""
|
||||
v2v_dist = euclidean_distances(position)
|
||||
e_center = np.matmul(H.T, position) / H.sum(axis=0).reshape(-1, 1)
|
||||
v2e_dist = euclidean_distances(position, e_center) * H
|
||||
e2e_dist = euclidean_distances(e_center)
|
||||
|
||||
centers = self.centers
|
||||
|
||||
force = np.zeros_like(position)
|
||||
if self.node_attraction is not None:
|
||||
f = (
|
||||
self._node_attraction(position, e_center, v2e_dist)
|
||||
* self.node_attraction
|
||||
)
|
||||
assert np.isnan(f).sum() == 0
|
||||
force += f
|
||||
if self.node_repulsion is not None:
|
||||
f = self._node_repulsion(position, v2v_dist)
|
||||
if self.n_centers == 1:
|
||||
f *= self.node_repulsion[0]
|
||||
else:
|
||||
masks = np.zeros((position.shape[0], 1))
|
||||
masks[: self.nums[0]] = self.node_repulsion[0]
|
||||
masks[self.nums[0] :] = self.node_repulsion[1]
|
||||
f *= masks
|
||||
assert np.isnan(f).sum() == 0
|
||||
force += f
|
||||
if self.edge_repulsion is not None:
|
||||
f = self._edge_repulsion(e_center, H, e2e_dist) * self.edge_repulsion
|
||||
assert np.isnan(f).sum() == 0
|
||||
force += f
|
||||
if self.center_gravity is not None:
|
||||
masks = [np.zeros((position.shape[0], 1)), np.zeros((position.shape[0], 1))]
|
||||
masks[0][: self.nums[0]] = 1
|
||||
masks[1][self.nums[0] :] = 1
|
||||
for center, gravity, mask in zip(centers, self.center_gravity, masks):
|
||||
v2c_dist = euclidean_distances(position, center.reshape(1, -1)).reshape(
|
||||
-1, 1
|
||||
)
|
||||
f = self._center_gravity(position, center, v2c_dist) * gravity * mask
|
||||
assert np.isnan(f).sum() == 0
|
||||
force += f
|
||||
|
||||
force *= damping
|
||||
|
||||
force = np.clip(force, -0.1, 0.1)
|
||||
position += force * dt
|
||||
velocity = force
|
||||
|
||||
return position, velocity, self._stop_condition(velocity, epsilon)
|
||||
|
||||
def _node_attraction(self, position, e_center, v2e_dist, x0=0.1, k=1.0):
|
||||
import numpy as np
|
||||
|
||||
"""
|
||||
Node attracted by edge center.
|
||||
"""
|
||||
x = deepcopy(v2e_dist)
|
||||
x[v2e_dist > 0] -= x0
|
||||
f_scale = k * x # (n, m)
|
||||
f_dir = (
|
||||
e_center[np.newaxis, :, :] - position[:, np.newaxis, :]
|
||||
) # (1, m, 2) - (n, 1, 2) -> (n, m, 2)
|
||||
f_dir_len = np.linalg.norm(f_dir, axis=2) # (n, m)
|
||||
# f_dir = f_dir / f_dir_len[:, :, np.newaxis] # (n, m, 2)
|
||||
f_dir = safe_div(f_dir, f_dir_len[:, :, np.newaxis]) # (n, m, 2)
|
||||
f = f_scale[:, :, np.newaxis] * f_dir # (n, m, 2)
|
||||
f = f.sum(axis=1) # (n, 2)
|
||||
return f
|
||||
|
||||
def _node_repulsion(self, position, v2v_dist, k=1.0):
|
||||
import numpy as np
|
||||
|
||||
"""
|
||||
Node repulsed by other nodes.
|
||||
"""
|
||||
dist = v2v_dist.copy()
|
||||
r, c = np.diag_indices_from(dist)
|
||||
dist[r, c] = np.inf
|
||||
|
||||
f_scale = k / (dist**2) # (n, n) with diag 0
|
||||
f_dir = (
|
||||
position[:, np.newaxis, :] - position[np.newaxis, :, :]
|
||||
) # (n, 1, 2) - (1, n, 2) -> (n, n, 2)
|
||||
f_dir_len = np.linalg.norm(f_dir, axis=2) # (n, n)
|
||||
f_dir_len[r, c] = np.inf
|
||||
# f_dir = f_dir / f_dir_len[:, :, np.newaxis] # (n, n, 2)
|
||||
f_dir = safe_div(f_dir, f_dir_len[:, :, np.newaxis]) # (n, n, 2)
|
||||
f = f_scale[:, :, np.newaxis] * f_dir # (n, n, 2)
|
||||
f[r, c] = 0
|
||||
f = f.sum(axis=1) # (n, 2)
|
||||
return f
|
||||
|
||||
def _edge_repulsion(self, e_center, H, e2e_dist, k=1.0, min_dist=1e-6):
|
||||
import numpy as np
|
||||
|
||||
"""
|
||||
Edge repulsed by other edges.
|
||||
"""
|
||||
dist = e2e_dist.copy()
|
||||
r, c = np.diag_indices_from(dist)
|
||||
dist[r, c] = np.inf
|
||||
|
||||
f_scale = k / (dist**2) # (m, m)
|
||||
f_dir = (
|
||||
e_center[:, np.newaxis, :] - e_center[np.newaxis, :, :]
|
||||
) # (m, 1, 2) - (1, m, 2) -> (m, m, 2)
|
||||
f_dir_len = np.linalg.norm(f_dir, axis=2) # (m, m)
|
||||
f_dir_len[r, c] = np.inf
|
||||
# 使用最小距离阈值
|
||||
f_dir = safe_div(f_dir, f_dir_len[:, :, np.newaxis]) # (m, m, 2)
|
||||
f = f_scale[:, :, np.newaxis] * f_dir # (m, m, 2)
|
||||
f[r, c] = 0
|
||||
f = f.sum(axis=1) # (m, 2)
|
||||
return np.matmul(H, f)
|
||||
|
||||
def _center_gravity(self, position, center, v2c_dist, k=1):
|
||||
import numpy as np
|
||||
|
||||
"""
|
||||
Node attracted by center.
|
||||
"""
|
||||
f_scale = v2c_dist # (n, 1)
|
||||
f_dir = (
|
||||
center[np.newaxis, np.newaxis, :] - position[:, np.newaxis, :]
|
||||
) # (1, 1, 2) - (n, 1, 2) -> (n, 1, 2)
|
||||
f_dir_len = np.linalg.norm(f_dir, axis=2) # (n, 1)
|
||||
# f_dir = f_dir / f_dir_len[:, :, np.newaxis] # (n, 1, 2)
|
||||
f_dir = safe_div(f_dir, f_dir_len[:, :, np.newaxis]) # (n, 1, 2)
|
||||
f = f_scale[:, :, np.newaxis] * f_dir # (n, 1, 2)
|
||||
# f = jitter(f)
|
||||
f = f.sum(axis=1) * k
|
||||
return f
|
||||
|
||||
def _stop_condition(self, velocity, epsilon):
|
||||
import numpy as np
|
||||
|
||||
"""
|
||||
Stop condition.
|
||||
"""
|
||||
return np.linalg.norm(velocity) < epsilon
|
||||
@@ -0,0 +1,27 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class TestGeometry(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G = eg.datasets.get_graph_karateclub()
|
||||
|
||||
def test_overall(self):
|
||||
eg.draw_SHS_center(self.G, [1, 33, 34], style="side")
|
||||
eg.draw_SHS_center(self.G, [1, 33, 34], style="center")
|
||||
eg.draw_SHS_center_kk(self.G, [1, 33, 34], style="side")
|
||||
eg.draw_SHS_center_kk(self.G, [1, 33, 34], style="center")
|
||||
eg.draw_kamada_kawai(self.G, style="side")
|
||||
eg.draw_kamada_kawai(self.G, style="center")
|
||||
eg.draw_SHS_center(self.G, [1, 33, 34], rate=0.8, style="side")
|
||||
eg.draw_SHS_center(self.G, [1, 33, 34], rate=0.8, style="center")
|
||||
eg.draw_SHS_center_kk(self.G, [1, 33, 34], rate=0.8, style="side")
|
||||
eg.draw_SHS_center_kk(self.G, [1, 33, 34], rate=0.8, style="center")
|
||||
eg.draw_kamada_kawai(self.G, rate=0.8, style="side")
|
||||
eg.draw_kamada_kawai(self.G, rate=0.8, style="center")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
# pretty awesome images
|
||||
@@ -0,0 +1,78 @@
|
||||
import math
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from easygraph.functions.drawing.geometry import common_tangent_radian
|
||||
from easygraph.functions.drawing.geometry import polar_position
|
||||
from easygraph.functions.drawing.geometry import rad_2_deg
|
||||
from easygraph.functions.drawing.geometry import radian_from_atan
|
||||
from easygraph.functions.drawing.geometry import vlen
|
||||
|
||||
|
||||
class TestGeometryUtils(unittest.TestCase):
|
||||
def test_radian_from_atan_axes(self):
|
||||
self.assertAlmostEqual(radian_from_atan(0, 1), math.pi / 2)
|
||||
self.assertAlmostEqual(radian_from_atan(0, -1), 3 * math.pi / 2)
|
||||
self.assertAlmostEqual(radian_from_atan(1, 0), 0)
|
||||
self.assertAlmostEqual(radian_from_atan(-1, 0), math.pi)
|
||||
|
||||
def test_radian_from_atan_quadrants(self):
|
||||
# Q1
|
||||
self.assertAlmostEqual(radian_from_atan(1, 1), math.atan(1))
|
||||
# Q4
|
||||
self.assertAlmostEqual(radian_from_atan(1, -1), math.atan(-1) + 2 * math.pi)
|
||||
# Q2
|
||||
self.assertAlmostEqual(radian_from_atan(-1, 1), math.atan(-1) + math.pi)
|
||||
# Q3
|
||||
self.assertAlmostEqual(radian_from_atan(-1, -1), math.atan(1) + math.pi)
|
||||
|
||||
def test_radian_from_atan_zero_vector(self):
|
||||
result = radian_from_atan(0, 0)
|
||||
self.assertAlmostEqual(result, 3 * math.pi / 2)
|
||||
|
||||
def test_vlen(self):
|
||||
self.assertEqual(vlen((3, 4)), 5.0)
|
||||
self.assertEqual(vlen((0, 0)), 0.0)
|
||||
self.assertAlmostEqual(vlen((-3, -4)), 5.0)
|
||||
|
||||
def test_common_tangent_radian_basic(self):
|
||||
r1, r2, d = 3, 2, 5
|
||||
angle = common_tangent_radian(r1, r2, d)
|
||||
expected = math.acos(abs(r2 - r1) / d)
|
||||
self.assertAlmostEqual(angle, expected)
|
||||
|
||||
def test_common_tangent_radian_reversed(self):
|
||||
r1, r2, d = 2, 3, 5
|
||||
angle = common_tangent_radian(r1, r2, d)
|
||||
expected = math.pi - math.acos(abs(r2 - r1) / d)
|
||||
self.assertAlmostEqual(angle, expected)
|
||||
|
||||
def test_common_tangent_radian_touching(self):
|
||||
self.assertAlmostEqual(common_tangent_radian(3, 3, 5), math.pi / 2)
|
||||
|
||||
def test_common_tangent_radian_invalid(self):
|
||||
with self.assertRaises(ValueError):
|
||||
common_tangent_radian(5, 1, 2)
|
||||
|
||||
def test_polar_position_origin(self):
|
||||
pos = polar_position(0, 0, np.array([5, 5]))
|
||||
np.testing.assert_array_almost_equal(pos, np.array([5, 5]))
|
||||
|
||||
def test_polar_position_90deg(self):
|
||||
pos = polar_position(1, math.pi / 2, np.array([0, 0]))
|
||||
np.testing.assert_array_almost_equal(pos, np.array([0, 1]))
|
||||
|
||||
def test_polar_position_negative_angle(self):
|
||||
pos = polar_position(1, -math.pi / 2, np.array([1, 1]))
|
||||
np.testing.assert_array_almost_equal(pos, np.array([1, 0]))
|
||||
|
||||
def test_rad_2_deg(self):
|
||||
self.assertEqual(rad_2_deg(0), 0)
|
||||
self.assertEqual(rad_2_deg(math.pi), 180)
|
||||
self.assertEqual(rad_2_deg(2 * math.pi), 360)
|
||||
self.assertEqual(rad_2_deg(-math.pi / 2), -90)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,39 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import statsmodels.api as sm
|
||||
|
||||
|
||||
class TestPlot(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ds = eg.datasets.get_graph_karateclub()
|
||||
self.edges = [
|
||||
(1, 4),
|
||||
(2, 4),
|
||||
("String", "Bool"),
|
||||
(4, 1),
|
||||
(0, 4),
|
||||
(4, 256),
|
||||
((1, 2), (3, 4)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
self.shs = eg.common_greedy(self.ds, int(len(self.ds.nodes) / 3))
|
||||
|
||||
def test_plot_Followers(self):
|
||||
eg.functions.plot_Followers(self.ds, self.shs)
|
||||
|
||||
def test_plot_Connected_Communities(self):
|
||||
eg.functions.plot_Connected_Communities(self.ds, self.shs)
|
||||
|
||||
def test_plot_Neighborhood_Followers(self):
|
||||
eg.functions.plot_Neighborhood_Followers(self.ds, self.shs)
|
||||
|
||||
def test_plot_Betweenness_Centrality(self):
|
||||
eg.functions.plot_Betweenness_Centrality(self.ds, self.shs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,56 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
|
||||
class TestPositioning(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ds = eg.datasets.get_graph_karateclub()
|
||||
self.edges = [
|
||||
(1, 4),
|
||||
(2, 4),
|
||||
("String", "Bool"),
|
||||
(4, 1),
|
||||
(0, 4),
|
||||
(4, 256),
|
||||
((1, 2), (3, 4)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
self.shs = eg.common_greedy(self.ds, int(len(self.ds.nodes) / 3))
|
||||
|
||||
def test_random_position(self):
|
||||
print()
|
||||
for i in self.test_graphs:
|
||||
print(eg.random_position(i))
|
||||
|
||||
def test_circular_position(self):
|
||||
print()
|
||||
for i in self.test_graphs:
|
||||
print(eg.circular_position(i))
|
||||
|
||||
def test_shell_position(self):
|
||||
print()
|
||||
for i in self.test_graphs:
|
||||
print(eg.shell_position(i))
|
||||
|
||||
def test_rescale_position(self):
|
||||
print()
|
||||
for i in self.test_graphs:
|
||||
try:
|
||||
pos = eg.random_position(i)
|
||||
obj = np.array(list(pos.values()))
|
||||
print(eg.rescale_position(obj))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def test_kamada_kawai_layout(self):
|
||||
print()
|
||||
for i in self.test_graphs:
|
||||
print(eg.kamada_kawai_layout(i))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,596 @@
|
||||
from itertools import chain
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from matplotlib.axes import Axes
|
||||
from matplotlib.collections import LineCollection
|
||||
from matplotlib.collections import PatchCollection
|
||||
from matplotlib.patches import Circle
|
||||
from matplotlib.patches import PathPatch
|
||||
from matplotlib.path import Path
|
||||
from scipy.spatial import ConvexHull
|
||||
|
||||
from .geometry import common_tangent_radian
|
||||
from .geometry import polar_position
|
||||
from .geometry import rad_2_deg
|
||||
from .geometry import radian_from_atan
|
||||
from .geometry import vlen
|
||||
|
||||
|
||||
# from fa2 import ForceAtlas2
|
||||
# import bezier
|
||||
# import numpy as np
|
||||
# from easygraph import to_networkx
|
||||
# from easygraph.utils.exception import EasyGraphError
|
||||
# import easygraph as eg
|
||||
|
||||
|
||||
def safe_div(a: np.ndarray, b: np.ndarray, jitter_scale: float = 0.000001):
|
||||
mask = b == 0
|
||||
b[mask] = 1
|
||||
eps = 1e-10
|
||||
inv_b = np.divide(1.0, np.maximum(b, eps))
|
||||
res = a * inv_b
|
||||
if mask.sum() > 0:
|
||||
res[mask.repeat(2, 2)] = np.random.randn(mask.sum() * 2) * jitter_scale
|
||||
return res
|
||||
|
||||
|
||||
def init_pos(num_v: int, center: Tuple[float, float] = (0, 0), scale: float = 1.0):
|
||||
return (np.random.rand(num_v, 2) * 2 - 1) * scale + center
|
||||
|
||||
|
||||
def draw_line_edge(
|
||||
ax: Axes,
|
||||
v_coor: np.array,
|
||||
v_size: list,
|
||||
e_list: List[Tuple[int, int]],
|
||||
show_arrow: bool,
|
||||
e_color: list,
|
||||
e_line_width: list,
|
||||
):
|
||||
arrow_head_width = (
|
||||
[0.015 * w for w in e_line_width] if show_arrow else [0] * len(e_list)
|
||||
)
|
||||
|
||||
for eidx, e in enumerate(e_list):
|
||||
start_pos = v_coor[e[0]]
|
||||
end_pos = v_coor[e[1]]
|
||||
|
||||
dir = end_pos - start_pos
|
||||
dir = dir / np.linalg.norm(dir)
|
||||
|
||||
start_pos = start_pos + dir * v_size[e[0]]
|
||||
end_pos = end_pos - dir * v_size[e[1]]
|
||||
|
||||
x, y = start_pos[0], start_pos[1]
|
||||
dx, dy = end_pos[0] - x, end_pos[1] - y
|
||||
|
||||
ax.arrow(
|
||||
x,
|
||||
y,
|
||||
dx,
|
||||
dy,
|
||||
head_width=arrow_head_width[eidx],
|
||||
color=e_color[eidx],
|
||||
linewidth=e_line_width[eidx],
|
||||
length_includes_head=True,
|
||||
)
|
||||
|
||||
|
||||
def draw_circle_edge(
|
||||
ax: Axes,
|
||||
v_coor: List[Tuple[float, float]],
|
||||
v_size: list,
|
||||
e_list: List[Tuple[int, int]],
|
||||
e_color: list,
|
||||
e_fill_color: list,
|
||||
e_line_width: list,
|
||||
):
|
||||
n_v = len(v_coor)
|
||||
line_paths, arc_paths, vertices = hull_layout(n_v, e_list, v_coor, v_size)
|
||||
for eidx, lines in enumerate(line_paths):
|
||||
pathdata = []
|
||||
for line in lines:
|
||||
if len(line) == 0:
|
||||
continue
|
||||
start_pos, end_pos = line
|
||||
pathdata.append((Path.MOVETO, start_pos.tolist()))
|
||||
pathdata.append((Path.LINETO, end_pos.tolist()))
|
||||
|
||||
if len(list(zip(*pathdata))) == 0:
|
||||
continue
|
||||
codes, verts = zip(*pathdata)
|
||||
path = Path(verts, codes)
|
||||
|
||||
ax.add_patch(
|
||||
PathPatch(
|
||||
path,
|
||||
linewidth=e_line_width[eidx],
|
||||
facecolor=e_fill_color[eidx],
|
||||
edgecolor=e_color[eidx],
|
||||
)
|
||||
)
|
||||
|
||||
for eidx, arcs in enumerate(arc_paths):
|
||||
for arc in arcs:
|
||||
center, theta1, theta2, radius = arc
|
||||
x, y = center[0], center[1]
|
||||
|
||||
patcjes_arc = matplotlib.patches.Arc(
|
||||
(x, y),
|
||||
2 * radius,
|
||||
2 * radius,
|
||||
theta1=theta1,
|
||||
theta2=theta2,
|
||||
color=e_color[eidx],
|
||||
linewidth=e_line_width[eidx],
|
||||
# edgecolor=e_color[eidx],
|
||||
edgecolor=e_color[eidx],
|
||||
facecolor=e_fill_color[eidx],
|
||||
)
|
||||
|
||||
ax.add_patch(
|
||||
matplotlib.patches.Arc(
|
||||
(x, y),
|
||||
2 * radius,
|
||||
2 * radius,
|
||||
theta1=theta1,
|
||||
theta2=theta2,
|
||||
color=e_color[eidx],
|
||||
linewidth=e_line_width[eidx],
|
||||
# edgecolor=e_color[eidx],
|
||||
edgecolor=e_color[eidx],
|
||||
facecolor=e_fill_color[eidx],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def edge_list_to_incidence_matrix(num_v: int, e_list: List[tuple]) -> np.ndarray:
|
||||
v_idx = list(chain(*e_list))
|
||||
e_idx = [[idx] * len(e) for idx, e in enumerate(e_list)]
|
||||
e_idx = list(chain(*e_idx))
|
||||
H = np.zeros((num_v, len(e_list)))
|
||||
H[v_idx, e_idx] = 1
|
||||
return H
|
||||
|
||||
|
||||
def draw_vertex(
|
||||
ax: Axes,
|
||||
v_coor: List[Tuple[float, float]],
|
||||
v_label: Optional[List[str]],
|
||||
font_size: int,
|
||||
font_family: str,
|
||||
v_size: list,
|
||||
v_color: list,
|
||||
edgecolors,
|
||||
v_line_width: list,
|
||||
):
|
||||
patches = []
|
||||
n = v_coor.shape[0]
|
||||
if v_label is None:
|
||||
v_label = [""] * n
|
||||
for coor, label, size, width in zip(v_coor.tolist(), v_label, v_size, v_line_width):
|
||||
circle = Circle(coor, size)
|
||||
circle.lineWidth = width
|
||||
# circle.label = label
|
||||
if label != "":
|
||||
x, y = coor[0], coor[1]
|
||||
offset = 0, -1.3 * size
|
||||
x += offset[0]
|
||||
y += offset[1]
|
||||
ax.text(
|
||||
x,
|
||||
y,
|
||||
label,
|
||||
fontsize=font_size,
|
||||
fontfamily=font_family,
|
||||
ha="center",
|
||||
va="top",
|
||||
)
|
||||
patches.append(circle)
|
||||
edgecolors = "black" if edgecolors == None else edgecolors
|
||||
p = PatchCollection(patches, facecolors=v_color, edgecolors=edgecolors)
|
||||
ax.add_collection(p)
|
||||
|
||||
|
||||
def hull_layout(n_v, e_list, pos, v_size, radius_increment=0.3):
|
||||
line_paths = [None] * len(e_list)
|
||||
arc_paths = [None] * len(e_list)
|
||||
|
||||
polygons_vertices_index = []
|
||||
vertices_radius = np.array(v_size)
|
||||
vertices_increased_radius = vertices_radius * radius_increment
|
||||
vertices_radius += vertices_increased_radius
|
||||
|
||||
e_degree = [len(e) for e in e_list]
|
||||
e_idxs = np.argsort(np.array(e_degree))
|
||||
|
||||
# for edge in e_list:
|
||||
for e_idx in e_idxs:
|
||||
edge = list(e_list[e_idx])
|
||||
|
||||
line_path_for_e = []
|
||||
arc_path_for_e = []
|
||||
|
||||
if len(edge) == 1:
|
||||
arc_path_for_e.append([pos[edge[0]], 0, 360, vertices_radius[edge[0]]])
|
||||
|
||||
vertices_radius[edge] += vertices_increased_radius[edge]
|
||||
|
||||
line_paths[e_idx] = line_path_for_e
|
||||
arc_paths[e_idx] = arc_path_for_e
|
||||
continue
|
||||
|
||||
pos_in_edge = pos[edge]
|
||||
if len(edge) == 2:
|
||||
vertices_index = np.array((0, 1), dtype=np.int64)
|
||||
else:
|
||||
hull = ConvexHull(pos_in_edge)
|
||||
vertices_index = hull.vertices
|
||||
|
||||
n_vertices = vertices_index.shape[0]
|
||||
|
||||
vertices_index = np.append(vertices_index, vertices_index[0]) # close the loop
|
||||
|
||||
thetas = []
|
||||
|
||||
for i in range(n_vertices):
|
||||
# line
|
||||
i1 = edge[vertices_index[i]]
|
||||
i2 = edge[vertices_index[i + 1]]
|
||||
|
||||
r1 = vertices_radius[i1]
|
||||
r2 = vertices_radius[i2]
|
||||
|
||||
p1 = pos[i1]
|
||||
p2 = pos[i2]
|
||||
|
||||
dp = p2 - p1
|
||||
dp_len = vlen(dp)
|
||||
|
||||
beta = radian_from_atan(dp[0], dp[1])
|
||||
alpha = common_tangent_radian(r1, r2, dp_len)
|
||||
|
||||
theta = beta - alpha
|
||||
start_point = polar_position(r1, theta, p1)
|
||||
end_point = polar_position(r2, theta, p2)
|
||||
|
||||
line_path_for_e.append((start_point, end_point))
|
||||
thetas.append(theta)
|
||||
|
||||
for i in range(n_vertices):
|
||||
# arcs
|
||||
theta_1 = thetas[i - 1]
|
||||
theta_2 = thetas[i]
|
||||
|
||||
arc_center = pos[edge[vertices_index[i]]]
|
||||
radius = vertices_radius[edge[vertices_index[i]]]
|
||||
|
||||
theta_1, theta_2 = rad_2_deg(theta_1), rad_2_deg(theta_2)
|
||||
arc_path_for_e.append((arc_center, theta_1, theta_2, radius))
|
||||
|
||||
vertices_radius[edge] += vertices_increased_radius[edge]
|
||||
|
||||
polygons_vertices_index.append(vertices_index.copy())
|
||||
|
||||
# line_paths.append(line_path_for_e)
|
||||
# arc_paths.append(arc_path_for_e)
|
||||
line_paths[e_idx] = line_path_for_e
|
||||
arc_paths[e_idx] = arc_path_for_e
|
||||
|
||||
return line_paths, arc_paths, polygons_vertices_index
|
||||
|
||||
|
||||
def apply_alpha(colors, alpha, elem_list, cmap=None, vmin=None, vmax=None):
|
||||
"""Apply an alpha (or list of alphas) to the colors provided.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
colors : color string or array of floats (default='r')
|
||||
Color of element. Can be a single color format string,
|
||||
or a sequence of colors with the same length as nodelist.
|
||||
If numeric values are specified they will be mapped to
|
||||
colors using the cmap and vmin,vmax parameters. See
|
||||
matplotlib.scatter for more details.
|
||||
|
||||
alpha : float or array of floats
|
||||
Alpha values for elements. This can be a single alpha value, in
|
||||
which case it will be applied to all the elements of color. Otherwise,
|
||||
if it is an array, the elements of alpha will be applied to the colors
|
||||
in order (cycling through alpha multiple times if necessary).
|
||||
|
||||
elem_list : array of networkx objects
|
||||
The list of elements which are being colored. These could be nodes,
|
||||
edges or labels.
|
||||
|
||||
cmap : matplotlib colormap
|
||||
Color map for use if colors is a list of floats corresponding to points
|
||||
on a color mapping.
|
||||
|
||||
vmin, vmax : float
|
||||
Minimum and maximum values for normalizing colors if a colormap is used
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
rgba_colors : numpy ndarray
|
||||
Array containing RGBA format values for each of the node colours.
|
||||
|
||||
"""
|
||||
from itertools import cycle
|
||||
from itertools import islice
|
||||
from numbers import Number
|
||||
|
||||
import matplotlib as mpl
|
||||
import matplotlib.cm # call as mpl.cm
|
||||
import matplotlib.colors # call as mpl.colors
|
||||
import numpy as np
|
||||
|
||||
# If we have been provided with a list of numbers as long as elem_list,
|
||||
# apply the color mapping.
|
||||
if len(colors) == len(elem_list) and isinstance(colors[0], Number):
|
||||
mapper = mpl.cm.ScalarMappable(cmap=cmap)
|
||||
mapper.set_clim(vmin, vmax)
|
||||
rgba_colors = mapper.to_rgba(colors)
|
||||
# Otherwise, convert colors to matplotlib's RGB using the colorConverter
|
||||
# object. These are converted to numpy ndarrays to be consistent with the
|
||||
# to_rgba method of ScalarMappable.
|
||||
else:
|
||||
try:
|
||||
rgba_colors = np.array([mpl.colors.colorConverter.to_rgba(colors)])
|
||||
except ValueError:
|
||||
rgba_colors = np.array(
|
||||
[mpl.colors.colorConverter.to_rgba(color) for color in colors]
|
||||
)
|
||||
# Set the final column of the rgba_colors to have the relevant alpha values
|
||||
try:
|
||||
# If alpha is longer than the number of colors, resize to the number of
|
||||
# elements. Also, if rgba_colors.size (the number of elements of
|
||||
# rgba_colors) is the same as the number of elements, resize the array,
|
||||
# to avoid it being interpreted as a colormap by scatter()
|
||||
if len(alpha) > len(rgba_colors) or rgba_colors.size == len(elem_list):
|
||||
rgba_colors = np.resize(rgba_colors, (len(elem_list), 4))
|
||||
rgba_colors[1:, 0] = rgba_colors[0, 0]
|
||||
rgba_colors[1:, 1] = rgba_colors[0, 1]
|
||||
rgba_colors[1:, 2] = rgba_colors[0, 2]
|
||||
rgba_colors[:, 3] = list(islice(cycle(alpha), len(rgba_colors)))
|
||||
except TypeError:
|
||||
rgba_colors[:, -1] = alpha
|
||||
return rgba_colors
|
||||
|
||||
|
||||
# def draw_easygraph_nodes(
|
||||
# G,
|
||||
# pos,
|
||||
# nodelist=None,
|
||||
# node_size=300,
|
||||
# node_color="#1f78b4",
|
||||
# node_shape="o",
|
||||
# alpha=None,
|
||||
# cmap=None,
|
||||
# vmin=None,
|
||||
# vmax=None,
|
||||
# ax=None,
|
||||
# linewidths=None,
|
||||
# edgecolors=None,
|
||||
# label=None,
|
||||
# margins=None,
|
||||
# ):
|
||||
# """Draw the nodes of the graph G.
|
||||
|
||||
# This draws only the nodes of the graph G.
|
||||
|
||||
# Parameters
|
||||
# ----------
|
||||
# G : graph
|
||||
# A easygraph graph
|
||||
|
||||
# pos : dictionary
|
||||
# A dictionary with nodes as keys and positions as values.
|
||||
# Positions should be sequences of length 2.
|
||||
|
||||
# ax : Matplotlib Axes object, optional
|
||||
# Draw the graph in the specified Matplotlib axes.
|
||||
|
||||
# nodelist : list (default list(G))
|
||||
# Draw only specified nodes
|
||||
|
||||
# node_size : scalar or array (default=300)
|
||||
# Size of nodes. If an array it must be the same length as nodelist.
|
||||
|
||||
# node_color : color or array of colors (default='#1f78b4')
|
||||
# Node color. Can be a single color or a sequence of colors with the same
|
||||
# length as nodelist. Color can be string or rgb (or rgba) tuple of
|
||||
# floats from 0-1. If numeric values are specified they will be
|
||||
# mapped to colors using the cmap and vmin,vmax parameters. See
|
||||
# matplotlib.scatter for more details.
|
||||
|
||||
# node_shape : string (default='o')
|
||||
# The shape of the node. Specification is as matplotlib.scatter
|
||||
# marker, one of 'so^>v<dph8'.
|
||||
|
||||
# alpha : float or array of floats (default=None)
|
||||
# The node transparency. This can be a single alpha value,
|
||||
# in which case it will be applied to all the nodes of color. Otherwise,
|
||||
# if it is an array, the elements of alpha will be applied to the colors
|
||||
# in order (cycling through alpha multiple times if necessary).
|
||||
|
||||
# cmap : Matplotlib colormap (default=None)
|
||||
# Colormap for mapping intensities of nodes
|
||||
|
||||
# vmin,vmax : floats or None (default=None)
|
||||
# Minimum and maximum for node colormap scaling
|
||||
|
||||
# linewidths : [None | scalar | sequence] (default=1.0)
|
||||
# Line width of symbol border
|
||||
|
||||
# edgecolors : [None | scalar | sequence] (default = node_color)
|
||||
# Colors of node borders
|
||||
|
||||
# label : [None | string]
|
||||
# Label for legend
|
||||
|
||||
# margins : float or 2-tuple, optional
|
||||
# Sets the padding for axis autoscaling. Increase margin to prevent
|
||||
# clipping for nodes that are near the edges of an image. Values should
|
||||
# be in the range ``[0, 1]``. See :meth:`matplotlib.axes.Axes.margins`
|
||||
# for details. The default is `None`, which uses the Matplotlib default.
|
||||
|
||||
# Returns
|
||||
# -------
|
||||
# matplotlib.collections.PathCollection
|
||||
# `PathCollection` of the nodes.
|
||||
|
||||
# Examples
|
||||
# --------
|
||||
# >>> from easygraph.datasets import get_graph_karateclub
|
||||
# >>> import easygraph as eg
|
||||
# >>> G = get_graph_karateclub()
|
||||
# >>> nodes = eg.draw_easygraph_nodes(G, pos=eg.circular_position(G))
|
||||
|
||||
|
||||
# """
|
||||
# from collections.abc import Iterable
|
||||
|
||||
# import matplotlib as mpl
|
||||
# import matplotlib.collections # call as mpl.collections
|
||||
# import matplotlib.pyplot as plt
|
||||
# import numpy as np
|
||||
|
||||
# if ax is None:
|
||||
# ax = plt.gca()
|
||||
|
||||
# if nodelist is None:
|
||||
# nodelist = list(G)
|
||||
|
||||
# if len(nodelist) == 0: # empty nodelist, no drawing
|
||||
# return mpl.collections.PathCollection(None)
|
||||
|
||||
# try:
|
||||
# xy = np.asarray([pos[v] for v in nodelist])
|
||||
# except KeyError as err:
|
||||
# raise EasyGraphError(f"Node {err} has no position.") from err
|
||||
|
||||
# if isinstance(alpha, Iterable):
|
||||
# node_color = apply_alpha(node_color, alpha, nodelist, cmap, vmin, vmax)
|
||||
# alpha = None
|
||||
|
||||
# node_collection = ax.scatter(
|
||||
# xy[:, 0],
|
||||
# xy[:, 1],
|
||||
# s=node_size,
|
||||
# c=node_color,
|
||||
# marker=node_shape,
|
||||
# cmap=cmap,
|
||||
# vmin=vmin,
|
||||
# vmax=vmax,
|
||||
# alpha=alpha,
|
||||
# linewidths=linewidths,
|
||||
# edgecolors=edgecolors,
|
||||
# label=label,
|
||||
# )
|
||||
# ax.tick_params(
|
||||
# axis="both",
|
||||
# which="both",
|
||||
# bottom=False,
|
||||
# left=False,
|
||||
# labelbottom=False,
|
||||
# labelleft=False,
|
||||
# )
|
||||
|
||||
# if margins is not None:
|
||||
# if isinstance(margins, Iterable):
|
||||
# ax.margins(*margins)
|
||||
# else:
|
||||
# ax.margins(margins)
|
||||
|
||||
# node_collection.set_zorder(2)
|
||||
# return node_collection
|
||||
|
||||
|
||||
# def draw_curved_edges(G, pos, dist_ratio=0.2, bezier_precision=20, polarity='random'):
|
||||
# # Get nodes into np array
|
||||
# edges = np.array(G.edges())
|
||||
# l = edges.shape[0]
|
||||
|
||||
# if polarity == 'random':
|
||||
# # Random polarity of curve
|
||||
# rnd = np.where(np.random.randint(2, size=l)==0, -1, 1)
|
||||
# else:
|
||||
# # Create a fixed (hashed) polarity column in the case we use fixed polarity
|
||||
# # This is useful, e.g., for animations
|
||||
# rnd = np.where(np.mod(np.vectorize(hash)(edges[:,0])+np.vectorize(hash)(edges[:,1]),2)==0,-1,1)
|
||||
|
||||
# # Coordinates (x,y) of both nodes for each edge
|
||||
# # e.g., https://stackoverflow.com/questions/16992713/translate-every-element-in-numpy-array-according-to-key
|
||||
# # Note the np.vectorize method doesn't work for all node position dictionaries for some reason
|
||||
# u, inv = np.unique(edges, return_inverse = True)
|
||||
# coords = np.array([pos[x] for x in u])[inv].reshape([edges.shape[0], 2, edges.shape[1]])
|
||||
# coords_node1 = coords[:,0,:]
|
||||
# coords_node2 = coords[:,1,:]
|
||||
|
||||
# # Swap node1/node2 allocations to make sure the directionality works correctly
|
||||
# should_swap = coords_node1[:,0] > coords_node2[:,0]
|
||||
# coords_node1[should_swap], coords_node2[should_swap] = coords_node2[should_swap], coords_node1[should_swap]
|
||||
|
||||
# # Distance for control points
|
||||
# dist = dist_ratio * np.sqrt(np.sum((coords_node1-coords_node2)**2, axis=1))
|
||||
|
||||
# # Gradients of line connecting node & perpendicular
|
||||
# m1 = (coords_node2[:,1]-coords_node1[:,1])/(coords_node2[:,0]-coords_node1[:,0])
|
||||
# m2 = -1/m1
|
||||
|
||||
# # Temporary points along the line which connects two nodes
|
||||
# # e.g., https://math.stackexchange.com/questions/656500/given-a-point-slope-and-a-distance-along-that-slope-easily-find-a-second-p
|
||||
# t1 = dist/np.sqrt(1+m1**2)
|
||||
# v1 = np.array([np.ones(l),m1])
|
||||
# coords_node1_displace = coords_node1 + (v1*t1).T
|
||||
# coords_node2_displace = coords_node2 - (v1*t1).T
|
||||
|
||||
# # Control points, same distance but along perpendicular line
|
||||
# # rnd gives the 'polarity' to determine which side of the line the curve should arc
|
||||
# t2 = dist/np.sqrt(1+m2**2)
|
||||
# v2 = np.array([np.ones(len(edges)),m2])
|
||||
# coords_node1_ctrl = coords_node1_displace + (rnd*v2*t2).T
|
||||
# coords_node2_ctrl = coords_node2_displace + (rnd*v2*t2).T
|
||||
|
||||
# # Combine all these four (x,y) columns into a 'node matrix'
|
||||
# node_matrix = np.array([coords_node1, coords_node1_ctrl, coords_node2_ctrl, coords_node2])
|
||||
|
||||
# # Create the Bezier curves and store them in a list
|
||||
# curveplots = []
|
||||
# for i in range(l):
|
||||
# nodes = node_matrix[:,i,:].T
|
||||
# curveplots.append(bezier.Curve(nodes, degree=3).evaluate_multi(np.linspace(0,1,bezier_precision)).T)
|
||||
# # Return an array of these curves
|
||||
# curves = np.array(curveplots)
|
||||
# return curves
|
||||
|
||||
# def draw_curved_graph(G, colors, ax):
|
||||
# #G = to_networkx(G)
|
||||
# # layout
|
||||
# pos = eg.spring_layout(G, iterations=50)
|
||||
# eg.draw_networkx_nodes(G, pos, ax=ax, node_size=200, node_color=colors[0], alpha=0.5)
|
||||
|
||||
# # 绘制标签
|
||||
# eg.draw_networkx_labels(G, pos, ax=ax, font_size=8, font_family='Arial', font_color='black')
|
||||
|
||||
# # Produce the curves
|
||||
# curves = draw_curved_edges(G, pos)
|
||||
# lc = LineCollection(curves, color=colors[1], alpha=0.4)
|
||||
|
||||
# # 添加连线
|
||||
# ax.add_collection(lc)
|
||||
|
||||
# # 设置坐标轴参数
|
||||
# ax.tick_params(axis='both', which='both', bottom=False, left=False, labelbottom=False, labelleft=False)
|
||||
|
||||
# plt.savefig('Figure.pdf')
|
||||
# plt.show()
|
||||
Reference in New Issue
Block a user