graph.py 18 KB

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