graph.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  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. total_nodes = len(nodes)
  263. num_communities = len(communities)
  264. # 基础参数
  265. BASE_NODE_SPACE = 0.5 # 单个节点基础空间需求
  266. D_LAYER_MULTIPLIER = 1.2 # D层紧凑系数
  267. I_S_LAYER_MULTIPLIER = 2 # I/S层扩展系数
  268. # 动态调整参数
  269. community_spacing = 8.0 * (num_communities**0.5) # 社区间距与社区数量正相关
  270. density_factor = (total_nodes / 1000)**0.5 # 密度调整系数
  271. # 初始化坐标字典
  272. coords = {}
  273. # 为每个社团分配空间区域
  274. comm_centers = {}
  275. for i, (comm_id, members) in enumerate(communities.items()):
  276. # 计算社区半径(基于社区规模)
  277. comm_radius = 1.2 * (len(members)**0.33) * density_factor
  278. # 三维空间分布
  279. theta = i * 2*np.pi / num_communities
  280. phi = np.pi * (i % num_communities) / num_communities
  281. comm_centers[comm_id] = (
  282. community_spacing * np.cos(theta) * (1 + 0.2*np.sin(phi)),
  283. community_spacing * np.sin(theta) * (1 + 0.2*np.cos(phi)),
  284. community_spacing * (i % 2) * 0.5 * density_factor # Z轴分层
  285. )
  286. # 为每个节点生成坐标
  287. for node in nodes:
  288. node_id = node['id']
  289. comm_id = partition[node_id]
  290. members = communities[comm_id]
  291. base_x, base_y, base_z = comm_centers[comm_id]
  292. # 根据社区规模调整分布参数
  293. comm_density = len(members) / total_nodes
  294. node_space = BASE_NODE_SPACE / (comm_density + 0.1)
  295. if node['type'] == 'D':
  296. # D类型节点使用高斯分布
  297. r = D_LAYER_MULTIPLIER * node_space * np.random.randn()
  298. theta = np.random.vonmises(0, 2)
  299. z_offset = np.random.normal(0, 0.3)
  300. else:
  301. # I/S类型使用均匀球面分布
  302. r = I_S_LAYER_MULTIPLIER * node_space * (1 + 0.5*np.random.rand())
  303. theta = np.random.uniform(0, 2*np.pi)
  304. z_offset = np.random.uniform(-1, 1)
  305. # 球坐标转笛卡尔坐标
  306. phi = np.arccos(2*np.random.rand() - 1)
  307. x = base_x + r * np.sin(phi) * np.cos(theta)
  308. y = base_y + r * np.sin(phi) * np.sin(theta)
  309. z = base_z + 0.5 * z_offset * density_factor
  310. coords[node_id] = (x, y, z)
  311. return coords
  312. def optimize_overlap(coords, iterations=100):
  313. """ 改进的斥力优化算法 """
  314. nodes = list(coords.keys())
  315. positions = np.array(list(coords.values()))
  316. total_nodes = len(nodes)
  317. # 动态参数
  318. min_dist = 1 * (total_nodes/1000)**(-0.3) # 最小间距随密度调整
  319. repulsion = 0.2 * (total_nodes/500) # 斥力强度随规模调整
  320. cooling_factor = 0.95 # 模拟退火冷却系数
  321. # 添加随机扰动
  322. positions += np.random.normal(0, 0.1, positions.shape)
  323. for i in range(iterations):
  324. # 计算节点间距
  325. deltas = positions[:, np.newaxis, :] - positions[np.newaxis, :, :]
  326. dists = np.linalg.norm(deltas, axis=2)
  327. # 避免自比较
  328. np.fill_diagonal(dists, np.inf)
  329. # 动态调整参数
  330. current_repulsion = repulsion * (cooling_factor)**(i)
  331. current_min_dist = min_dist * (1 + 0.1*i/iterations) # 逐步收紧间距
  332. # 计算斥力矩阵
  333. with np.errstate(divide='ignore', invalid='ignore'):
  334. forces = np.where(
  335. dists < current_min_dist,
  336. current_repulsion / (dists**2 + 1e-9),
  337. 0
  338. )
  339. # 计算合力
  340. force_vectors = (deltas / (dists[:, :, np.newaxis] + 1e-9)) * forces[:, :, np.newaxis]
  341. movement = np.sum(force_vectors, axis=1)
  342. # 应用移动限制
  343. max_move = 0.5 * (1 + i/iterations) # 逐步减小最大移动步长
  344. movement = np.clip(movement, -max_move, max_move)
  345. positions += movement
  346. # 自适应归一化
  347. pos_min = np.min(positions, axis=0)
  348. pos_max = np.max(positions, axis=0)
  349. range_size = pos_max - pos_min
  350. scale = 10 / np.max(range_size) # 保持比例缩放
  351. return {node: tuple((pos - pos_min) * scale) for node, pos in zip(nodes, positions)}
  352. '''结束定义'''
  353. # 生成初始坐标
  354. initial_coords = generate_3d_coordinates(nodeJson, communities)
  355. # 优化防止重叠
  356. final_coords = optimize_overlap(initial_coords)
  357. '''结果示例输出,并用三维视图显示START'''
  358. # for node_id, (x, y, z) in final_coords.items():
  359. # print(f"Node {node_id}: ({x:.2f}, {y:.2f}, {z:.2f})")
  360. # import matplotlib.pyplot as plt
  361. # from mpl_toolkits.mplot3d import Axes3D
  362. # fig = plt.figure(figsize=(10, 8))
  363. # ax = fig.add_subplot(111, projection='3d')
  364. # # 按类型绘制节点
  365. # types = {n['id']: n['type'] for n in nodeJson}
  366. # colors = {'D': 'red', 'I': 'green', 'S': 'blue'}
  367. # for node_id, (x, y, z) in final_coords.items():
  368. # ax.scatter(x, y, z,
  369. # c=colors[types[node_id]],
  370. # s=50 if types[node_id] == 'D' else 30,
  371. # marker='o' if types[node_id] == 'D' else '^')
  372. # # 绘制边
  373. # for edge in edgeJson:
  374. # x = [final_coords[edge['from']][0], final_coords[edge['to']][0]]
  375. # y = [final_coords[edge['from']][1], final_coords[edge['to']][1]]
  376. # z = [final_coords[edge['from']][2], final_coords[edge['to']][2]]
  377. # ax.plot(x, y, z, c='gray', alpha=0.3)
  378. # ax.set_xlabel('X')
  379. # ax.set_ylabel('Y')
  380. # ax.set_zlabel('Z')
  381. # plt.show()
  382. '''结果示例输出,并用三维视图显示END'''
  383. '''END'''
  384. # 将坐标添加到每个节点字典
  385. for node in nodeJson:
  386. node_id = node['id']
  387. x, y, z = final_coords[node_id]
  388. # 添加三维坐标字段
  389. node['coordinates'] = {
  390. 'x': round(x, 4), # 保留4位小数
  391. 'y': round(y, 4),
  392. 'z': round(z, 4)
  393. }
  394. return self.create(
  395. result=result,
  396. user=result.user,
  397. nodes=nodeJson,
  398. edges=edgeJson,
  399. type=result.plan.algorithm.type,
  400. )
  401. class Graph(models.Model):
  402. create_time = models.DateTimeField(auto_now_add=True)
  403. update_time = models.DateTimeField(auto_now=True)
  404. type = models.CharField(choices=graphForAlgo, default='optimize', max_length=16)
  405. # 根据算法不同,生成的图数据结构不同
  406. nodes = models.JSONField()
  407. edges = models.JSONField()
  408. result = models.ForeignKey(to="api.Result", on_delete=models.CASCADE, related_name="own_graphs")
  409. user = models.ForeignKey(to="api.User", on_delete=models.CASCADE, related_name="user_own_graphs")
  410. objects = GraphManager()
  411. def generateToken(self):
  412. # 删除旧验证码
  413. if self.own_tokens.exists():
  414. for token in self.own_tokens.all():
  415. token.delete()
  416. # 生成验证码
  417. token = GraphToken()
  418. token.graph = self
  419. token.token = ''.join([str(randint(0,9)) for n in range(6)])
  420. while(GraphToken.objects.checkDuplicate(token.token)):
  421. token.token = ''.join([str(randint(0,9)) for n in range(6)])
  422. token.save()
  423. return token.token
  424. class Meta:
  425. app_label = 'api'