graph.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. from django.db import models
  2. from datetime import datetime
  3. from random import randint
  4. from api.utils import *
  5. import numpy as np
  6. from scipy.spatial import SphericalVoronoi
  7. from sklearn.preprocessing import MinMaxScaler #scikit-learn
  8. import networkx as nx
  9. from community import community_louvain #python-louvain
  10. graphForAlgo = [
  11. ('optimize', 'optimize'),
  12. ('group', 'group'),
  13. ('predict', 'predict'),
  14. ]
  15. class GraphManager(models.Manager):
  16. def checkDuplicate(self, token):
  17. try:
  18. self.get(token=token)
  19. return True
  20. except GraphToken.DoesNotExist:
  21. return False
  22. return True
  23. class GraphToken(models.Model):
  24. # 用于访问图的验证码
  25. create_time = models.DateTimeField(auto_now_add=True)
  26. graph = models.ForeignKey(to="api.Graph", on_delete=models.CASCADE, related_name="own_tokens")
  27. token = models.CharField(max_length=8)
  28. objects = GraphManager()
  29. def checkExpire(self):
  30. now = datetime.now()
  31. diff = now - self.create_time
  32. # 超过五分钟过期
  33. return diff.total_seconds() > 300
  34. class Meta:
  35. app_label = 'api'
  36. class GraphManager(models.Manager):
  37. def statistic(self, user):
  38. graphs = user.user_own_graphs.all()
  39. return {
  40. 'amount': len(graphs),
  41. }
  42. '''功能体探测功能的三维坐标生成 start'''
  43. def createFromResultGroupAlgo(self, result):
  44. print("Group3D")
  45. # 参数配置
  46. GROUP_SPHERE_RADIUS = 10.0 # 社团分布球体半径
  47. D_CORE_RADIUS = 1.5 # D类节点核心区半径
  48. SI_SHELL_RADIUS = 4.0 # S/I类节点分布半径
  49. MIN_GROUP_DIST = 8.0 # 社团间最小间距
  50. nodeJson = result.nodeFile.toJson()
  51. edgeJson = result.edgeFile.toJson()
  52. # 内部函数
  53. def _uniform_sphere_sampling(n, radius=1.0):
  54. """均匀球面采样"""
  55. points = []
  56. phi = np.pi * (3.0 - np.sqrt(5.0)) # 黄金角度
  57. for i in range(n):
  58. y = 1 - (i / (n-1)) * 2
  59. radius_at_y = np.sqrt(1 - y*y)
  60. theta = phi * i
  61. x = np.cos(theta) * radius_at_y
  62. z = np.sin(theta) * radius_at_y
  63. points.append(radius * np.array([x, y, z]))
  64. return np.array(points)
  65. # 内部函数
  66. def _optimize_layout(node_coords, groups, group_coords):
  67. """带社团约束的优化"""
  68. # 参数设置
  69. NODE_REPULSION = 0.1 # 节点间斥力
  70. GROUP_ATTRACTION = 0.05 # 社团内引力
  71. ITERATIONS = 200
  72. ids = list(node_coords.keys())
  73. positions = np.array([node_coords[id] for id in ids])
  74. group_map = {id: gid for gid, data in groups.items() for id in data['D']+data['SI']}
  75. for _ in range(ITERATIONS):
  76. # 节点间斥力
  77. diffs = positions[:, None] - positions[None, :] # 3D差分矩阵
  78. dists = np.linalg.norm(diffs, axis=-1)
  79. np.fill_diagonal(dists, np.inf)
  80. repulsion = NODE_REPULSION / (dists**2 + 1e-6)
  81. repulsion[dists > 5.0] = 0 # 仅处理近距离节点
  82. # 社团内引力
  83. attraction = np.zeros_like(positions)
  84. for i, id in enumerate(ids):
  85. gid = group_map[id]
  86. center = group_coords[gid]['center']
  87. dir_to_center = center - positions[i]
  88. dist_to_center = np.linalg.norm(dir_to_center)
  89. if dist_to_center > 2*SI_SHELL_RADIUS:
  90. attraction[i] = GROUP_ATTRACTION * dir_to_center
  91. # 更新位置
  92. movement = np.sum(repulsion[:, :, None] * diffs, axis=1) + attraction
  93. positions += 0.1 * movement
  94. # 归一化到0-10范围
  95. scaler = MinMaxScaler(feature_range=(0, 10))
  96. positions = scaler.fit_transform(positions)
  97. return {id: tuple(pos) for id, pos in zip(ids, positions)}
  98. # 内部函数
  99. def _generate_si_coordinates(num_points, radius):
  100. # 生成随机方向
  101. points = np.random.randn(num_points, 3) # 标准正态分布采样
  102. # 归一化到单位球面
  103. norms = np.linalg.norm(points, axis=1, keepdims=True)
  104. points_normalized = points / norms
  105. # 缩放到目标半径
  106. points_scaled = points_normalized * radius
  107. return points_scaled
  108. # 按group分组
  109. groups = {}
  110. for node in nodeJson:
  111. group_id = None
  112. for meta in node['meta']:
  113. if 'group' in meta:
  114. group_id = meta['group']
  115. if not group_id:
  116. print(node, group_id, "非Group优化结果被用于进行Group图形布局生成")
  117. groups.setdefault(group_id, {'D': [], 'SI': []})
  118. if node['type'] == 'D':
  119. groups[group_id]['D'].append(node['id'])
  120. else:
  121. groups[group_id]['SI'].append(node['id'])
  122. # === 步骤1: 为每个group分配空间位置 ===
  123. group_coords = {}
  124. num_groups = len(groups)
  125. points = _uniform_sphere_sampling(num_groups, radius=GROUP_SPHERE_RADIUS)
  126. # 确保最小间距
  127. for i in range(len(points)):
  128. for j in range(i+1, len(points)):
  129. dist = np.linalg.norm(points[i]-points[j])
  130. if dist < MIN_GROUP_DIST:
  131. direction = (points[j] - points[i]) / dist
  132. points[j] = points[i] + direction * MIN_GROUP_DIST
  133. # 分配group中心坐标
  134. for idx, (group_id, members) in enumerate(groups.items()):
  135. group_coords[group_id] = {
  136. 'center': points[idx],
  137. 'D_count': len(members['D']),
  138. 'SI_count': len(members['SI'])
  139. }
  140. # === 步骤2: 生成各group内部坐标 ===
  141. node_coords = {}
  142. for group_id, data in groups.items():
  143. center = group_coords[group_id]['center']
  144. # D类节点:均匀分布在核心球体内
  145. for node_id in data['D']:
  146. r = D_CORE_RADIUS * np.random.rand()**0.5 # 密度向中心聚集
  147. theta = np.random.uniform(0, 2*np.pi)
  148. phi = np.arccos(2*np.random.rand() - 1)
  149. dx = r * np.sin(phi) * np.cos(theta)
  150. dy = r * np.sin(phi) * np.sin(theta)
  151. dz = r * np.cos(phi)
  152. node_coords[node_id] = center + np.array([dx, dy, dz])
  153. # SI类节点:分布在球壳层
  154. shell_radius = SI_SHELL_RADIUS + 0.5*np.abs(np.random.randn()) # 添加随机扰动
  155. points = _generate_si_coordinates(len(data['SI']), shell_radius)
  156. # 使用球形Voronoi分布避免重叠
  157. sv = SphericalVoronoi(points, radius=shell_radius)
  158. sv.sort_vertices_of_regions()
  159. for i, node_id in enumerate(data['SI']):
  160. point = sv.points[i] * shell_radius
  161. node_coords[node_id] = center + point
  162. # === 步骤3: 全局优化 ===
  163. # return _optimize_layout(node_coords, groups, group_coords)
  164. final_coords = _optimize_layout(node_coords, groups, group_coords)
  165. # 将坐标添加到每个节点字典
  166. for node in nodeJson:
  167. node_id = node['id']
  168. x, y, z = final_coords[node_id]
  169. # 添加三维坐标字段
  170. node['coordinates'] = {
  171. 'x': round(x, 4), # 保留4位小数
  172. 'y': round(y, 4),
  173. 'z': round(z, 4)
  174. }
  175. print(nodeJson)
  176. '''结果示例输出,并用三维视图显示START'''
  177. # for node_id, (x, y, z) in final_coords.items():
  178. # print(f"Node {node_id}: ({x:.2f}, {y:.2f}, {z:.2f})")
  179. # import matplotlib.pyplot as plt
  180. # from mpl_toolkits.mplot3d import Axes3D
  181. # fig = plt.figure(figsize=(10, 8))
  182. # ax = fig.add_subplot(111, projection='3d')
  183. # # 按类型绘制节点
  184. # # types = {n['id']: n['meta']['group'] for n in nodeJson}
  185. # types = {}
  186. # for n in nodeJson:
  187. # for meta in n['meta']:
  188. # if 'group' in meta:
  189. # types[n['id']] = str(meta['group'])
  190. # colors = {'1': 'red', '2': 'green', '3': 'blue', '4': 'yellow', '5': 'black'}
  191. # for node_id, (x, y, z) in final_coords.items():
  192. # ax.scatter(x, y, z,
  193. # c=colors[types[node_id]],
  194. # s=50 if types[node_id] == 'D' else 30,
  195. # marker='o' if types[node_id] == 'D' else '^')
  196. # # 绘制边
  197. # for edge in edgeJson:
  198. # x = [final_coords[edge['from']][0], final_coords[edge['to']][0]]
  199. # y = [final_coords[edge['from']][1], final_coords[edge['to']][1]]
  200. # z = [final_coords[edge['from']][2], final_coords[edge['to']][2]]
  201. # ax.plot(x, y, z, c='gray', alpha=0.3)
  202. # ax.set_xlabel('X')
  203. # ax.set_ylabel('Y')
  204. # ax.set_zlabel('Z')
  205. # plt.show()
  206. '''结果示例输出,并用三维视图显示END'''
  207. return self.create(
  208. result=result,
  209. user=result.user,
  210. nodes=nodeJson,
  211. edges=edgeJson,
  212. type=result.plan.algorithm.type,
  213. )
  214. '''功能体探测功能的三维坐标生成 end'''
  215. def createFromResult(self, result):
  216. nodeJson = result.nodeFile.toJson()
  217. edgeJson = result.edgeFile.toJson()
  218. # 功能体探测算法需要额外将同一功能体聚集,单独处理
  219. if result.plan.algorithm.type == 'group':
  220. return self.createFromResultGroupAlgo(result)
  221. # 只有3d和VR视图需要生成图,需要给每个节点赋值一个三维坐标,该坐标需要满足一定尺度
  222. '''START'''
  223. # 创建NetworkX图对象
  224. G = nx.Graph()
  225. G.add_nodes_from([(n['id'], {'type': n['type']}) for n in nodeJson])
  226. G.add_edges_from([(e['from'], e['to']) for e in edgeJson])
  227. # 使用Louvain算法检测社团
  228. partition = community_louvain.best_partition(G)
  229. communities = {}
  230. for node, comm_id in partition.items():
  231. communities.setdefault(comm_id, []).append(node)
  232. '''定义函数'''
  233. def generate_3d_coordinates(nodes, communities):
  234. """ 计算三维空间坐标布局 """
  235. # 参数设置
  236. D_LAYER_RADIUS = 1.0 # D类型节点分布半径
  237. I_S_LAYER_RADIUS = 3.0 # I/S类型节点分布半径, 因为D类靠中心,I/S类靠外围
  238. COMMUNITY_SPACING = 8.0 # 社团间距
  239. # 初始化坐标字典
  240. coords = {}
  241. # 为每个社团分配空间区域
  242. comm_centers = {}
  243. for i, (comm_id, members) in enumerate(communities.items()):
  244. # 在三维空间分配不同象限
  245. angle = i * 2*np.pi / len(communities)
  246. comm_centers[comm_id] = (
  247. COMMUNITY_SPACING * np.cos(angle),
  248. COMMUNITY_SPACING * np.sin(angle),
  249. COMMUNITY_SPACING * (i % 2) # Z轴分层
  250. )
  251. # 为每个节点生成坐标
  252. for node in nodes:
  253. node_id = node['id']
  254. comm_id = partition[node_id]
  255. base_x, base_y, base_z = comm_centers[comm_id]
  256. # 根据节点类型确定分布层
  257. if node['type'] == 'D':
  258. # D类型节点紧密分布在社团中心附近
  259. r = D_LAYER_RADIUS * np.random.rand()
  260. theta = np.random.uniform(0, 2*np.pi)
  261. phi = np.random.uniform(0, np.pi)
  262. x = base_x + r * np.sin(phi) * np.cos(theta)
  263. y = base_y + r * np.sin(phi) * np.sin(theta)
  264. z = base_z + r * np.cos(phi)
  265. else:
  266. # I/S类型节点分布在更大半径的球面上
  267. r = I_S_LAYER_RADIUS
  268. theta = np.random.uniform(0, 2*np.pi)
  269. phi = np.random.uniform(0, np.pi)
  270. x = base_x + r * np.sin(phi) * np.cos(theta)
  271. y = base_y + r * np.sin(phi) * np.sin(theta)
  272. z = base_z + r * np.cos(phi)
  273. coords[node_id] = (x, y, z)
  274. return coords
  275. def optimize_overlap(coords, iterations=100):
  276. """ 使用斥力优化减少节点重叠 """
  277. nodes = list(coords.keys())
  278. positions = np.array(list(coords.values()))
  279. # 设置优化参数
  280. repulsion = 0.1 # 斥力强度
  281. min_dist = 0.5 # 最小间距
  282. for _ in range(iterations):
  283. # 计算所有节点间距
  284. diffs = positions[:, np.newaxis, :] - positions[np.newaxis, :, :]
  285. dists = np.linalg.norm(diffs, axis=2)
  286. # 避免自比较
  287. np.fill_diagonal(dists, np.inf)
  288. # 计算斥力
  289. with np.errstate(divide='ignore'):
  290. forces = repulsion / (dists**2)
  291. forces[dists > min_dist] = 0
  292. # 更新位置
  293. movement = np.sum(forces[:, :, np.newaxis] * diffs, axis=1)
  294. positions += movement
  295. # 归一化到0-10范围
  296. scaler = MinMaxScaler(feature_range=(0, 10))
  297. positions = scaler.fit_transform(positions)
  298. return {node: tuple(pos) for node, pos in zip(nodes, positions)}
  299. '''结束定义'''
  300. # 生成初始坐标
  301. initial_coords = generate_3d_coordinates(nodeJson, communities)
  302. # 优化防止重叠
  303. final_coords = optimize_overlap(initial_coords)
  304. '''结果示例输出,并用三维视图显示START'''
  305. # for node_id, (x, y, z) in final_coords.items():
  306. # print(f"Node {node_id}: ({x:.2f}, {y:.2f}, {z:.2f})")
  307. # import matplotlib.pyplot as plt
  308. # from mpl_toolkits.mplot3d import Axes3D
  309. # fig = plt.figure(figsize=(10, 8))
  310. # ax = fig.add_subplot(111, projection='3d')
  311. # # 按类型绘制节点
  312. # types = {n['id']: n['type'] for n in nodeJson}
  313. # colors = {'D': 'red', 'I': 'green', 'S': 'blue'}
  314. # for node_id, (x, y, z) in final_coords.items():
  315. # ax.scatter(x, y, z,
  316. # c=colors[types[node_id]],
  317. # s=50 if types[node_id] == 'D' else 30,
  318. # marker='o' if types[node_id] == 'D' else '^')
  319. # # 绘制边
  320. # for edge in edgeJson:
  321. # x = [final_coords[edge['from']][0], final_coords[edge['to']][0]]
  322. # y = [final_coords[edge['from']][1], final_coords[edge['to']][1]]
  323. # z = [final_coords[edge['from']][2], final_coords[edge['to']][2]]
  324. # ax.plot(x, y, z, c='gray', alpha=0.3)
  325. # ax.set_xlabel('X')
  326. # ax.set_ylabel('Y')
  327. # ax.set_zlabel('Z')
  328. # plt.show()
  329. '''结果示例输出,并用三维视图显示END'''
  330. '''END'''
  331. # 将坐标添加到每个节点字典
  332. for node in nodeJson:
  333. node_id = node['id']
  334. x, y, z = final_coords[node_id]
  335. # 添加三维坐标字段
  336. node['coordinates'] = {
  337. 'x': round(x, 4), # 保留4位小数
  338. 'y': round(y, 4),
  339. 'z': round(z, 4)
  340. }
  341. return self.create(
  342. result=result,
  343. user=result.user,
  344. nodes=nodeJson,
  345. edges=edgeJson,
  346. type=result.plan.algorithm.type,
  347. )
  348. class Graph(models.Model):
  349. create_time = models.DateTimeField(auto_now_add=True)
  350. update_time = models.DateTimeField(auto_now=True)
  351. type = models.CharField(choices=graphForAlgo, default='optimize', max_length=16)
  352. # 根据算法不同,生成的图数据结构不同
  353. nodes = models.JSONField()
  354. edges = models.JSONField()
  355. result = models.ForeignKey(to="api.Result", on_delete=models.CASCADE, related_name="own_graphs")
  356. user = models.ForeignKey(to="api.User", on_delete=models.CASCADE, related_name="user_own_graphs")
  357. objects = GraphManager()
  358. def generateToken(self):
  359. # 删除旧验证码
  360. if self.own_tokens.exists():
  361. for token in self.own_tokens.all():
  362. token.delete()
  363. # 生成验证码
  364. token = GraphToken()
  365. token.graph = self
  366. token.token = ''.join([str(randint(0,9)) for n in range(6)])
  367. while(GraphToken.objects.checkDuplicate(token.token)):
  368. token.token = ''.join([str(randint(0,9)) for n in range(6)])
  369. token.save()
  370. return token.token
  371. class Meta:
  372. app_label = 'api'