graph.py 18 KB

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