useTopologyOrdering.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import type { TopologyData, TopoNodeData } from '../windTopology.data';
  2. /**
  3. * 通风网络拓扑层内排序:median / barycenter 多轮扫描法(Sugiyama 框架的 crossing reduction 阶段)。
  4. *
  5. * 背景:层级已由 level 字段固定为语义层(0=总进风 … 6=总回风),层内排序只影响相邻层
  6. * 巷道连线的交叉数。二部交叉最小化(bipartite crossing minimization)为 NP-hard,
  7. * 本模块采用工业图布局库(dagre/ELK)同款启发式:
  8. * 1) 每层节点按"其相邻层邻居位置的 median(中位数)/ barycenter(均值)"计算关键值;
  9. * 2) 左右多轮往返扫描(down/up sweep),保留历史交叉数最优解。
  10. *
  11. * 确定性保证:固定轮次(默认 20)+ 三元比较稳定排序(关键值、输入 index),
  12. * 同一输入永远输出同一顺序,避免 resize / 刷新导致的布局抖动。
  13. *
  14. * 不参与排序、保持输入序(不影响交叉数的节点/边):
  15. * - 未分级层(level 无效);
  16. * - 跨层巷道边(两端 level 差 > 1)及其端点;
  17. * - 根边(地面 ↔ lv0 / lv6)与地面节点(单节点层)。
  18. * 无任何相邻层边的节点关键值为 +Infinity:排在层尾,且彼此保持相对输入序。
  19. */
  20. export interface OrderingOptions {
  21. /** 邻居位置聚合方法:median=中位数(默认,抗离群,交叉 ≤ 3×最优 有理论保证)、barycenter=均值 */
  22. method?: 'median' | 'barycenter';
  23. /** 往返扫描轮数(固定值,保证确定性) */
  24. sweepRounds?: number;
  25. }
  26. /** 参与排序的层级 key(0=总进风 1..5 现有层级 6=总回风),与 levelTextMap / computeLayout 分组一致 */
  27. const ORDER_KEYS = ['0', '1', '2', '3', '4', '5', '6'];
  28. /** 层级 key:0..6 有效 → 数字字符串;否则 'ungraded' */
  29. function levelKeyOf(p: TopoNodeData): string {
  30. const lv = Number(p.level);
  31. return Number.isFinite(lv) && lv >= 0 && lv <= 6 ? String(lv) : 'ungraded';
  32. }
  33. /**
  34. * 构建相邻层(level 差恰为 1)巷道边表:
  35. * key = "低层-高层"(如 '2-3'),value = 方向规范化的边数组 [低层节点 id, 高层节点 id]。
  36. * 渲染方向(parent → child)不影响交叉判定,统一规范化为低层 → 高层。
  37. */
  38. function buildPairEdges(data: TopologyData): Map<string, Array<[string, string]>> {
  39. const levelById = new Map<string, number>();
  40. for (const n of data.nodes) {
  41. if (n.category !== 1) continue;
  42. const lv = Number(n.level);
  43. if (Number.isFinite(lv) && lv >= 0 && lv <= 6) levelById.set(n.id, lv);
  44. }
  45. const edgesByPair = new Map<string, Array<[string, string]>>();
  46. for (const l of data.links) {
  47. if (l.kind !== 'roadway') continue;
  48. const sl = levelById.get(l.source);
  49. const tl = levelById.get(l.target);
  50. if (sl === undefined || tl === undefined) continue;
  51. if (Math.abs(sl - tl) !== 1) continue;
  52. const lo = Math.min(sl, tl);
  53. const hi = Math.max(sl, tl);
  54. const key = `${lo}-${hi}`;
  55. if (!edgesByPair.has(key)) edgesByPair.set(key, []);
  56. edgesByPair.get(key)!.push(sl < tl ? [l.source, l.target] : [l.target, l.source]);
  57. }
  58. return edgesByPair;
  59. }
  60. /** 层序数组 → 节点秩映射(nodeId → 层内 index);undefined(空层)返回空 map */
  61. function rankOf(order: string[] | undefined): Map<string, number> {
  62. const rank = new Map<string, number>();
  63. if (order) order.forEach((id, i) => rank.set(id, i));
  64. return rank;
  65. }
  66. /**
  67. * 相邻两层交叉数:edges 中每条边一端在 A 层、一端在 B 层;
  68. * 两对边 (a1,b1)、(a2,b2) 交叉 ⇔ a1/a2 在 A 层秩的相对顺序与 b1/b2 在 B 层秩相反。
  69. * O(E²) 直接统计(本系统每矿测风点 ≤ 数百,足够)。
  70. */
  71. export function countCrossings(rankA: Map<string, number>, rankB: Map<string, number>, edges: Array<[string, string]>): number {
  72. let total = 0;
  73. for (let i = 0; i < edges.length; i++) {
  74. const [a1, b1] = edges[i];
  75. const ra1 = rankA.get(a1);
  76. const rb1 = rankB.get(b1);
  77. if (ra1 === undefined || rb1 === undefined) continue;
  78. for (let j = i + 1; j < edges.length; j++) {
  79. const [a2, b2] = edges[j];
  80. const ra2 = rankA.get(a2);
  81. const rb2 = rankB.get(b2);
  82. if (ra2 === undefined || rb2 === undefined) continue;
  83. if ((ra1 < ra2 && rb1 > rb2) || (ra1 > ra2 && rb1 < rb2)) total++;
  84. }
  85. }
  86. return total;
  87. }
  88. /** 复用已有边表的全图总交叉数(4 个相邻层对 lv1-lv2 … lv4-lv5 交叉数之和) */
  89. function countTotalCrossingsWith(edgesByPair: Map<string, Array<[string, string]>>, order: Map<string, string[]>): number {
  90. let total = 0;
  91. for (let i = 0; i < ORDER_KEYS.length - 1; i++) {
  92. const lo = ORDER_KEYS[i];
  93. const hi = ORDER_KEYS[i + 1];
  94. const edges = edgesByPair.get(`${lo}-${hi}`);
  95. if (!edges || edges.length === 0) continue;
  96. total += countCrossings(rankOf(order.get(lo)), rankOf(order.get(hi)), edges);
  97. }
  98. return total;
  99. }
  100. /** 全图总交叉数:order 为 orderNodesByLevel 返回的层序(key '0'..'6') */
  101. export function countTotalCrossings(data: TopologyData, order: Map<string, string[]>): number {
  102. return countTotalCrossingsWith(buildPairEdges(data), order);
  103. }
  104. /** 深拷贝层序(仅复制参与排序的层) */
  105. function snapshotOrder(order: Map<string, string[]>): Map<string, string[]> {
  106. const copy = new Map<string, string[]>();
  107. for (const [k, arr] of order) copy.set(k, [...arr]);
  108. return copy;
  109. }
  110. /**
  111. * 单层重排:目标层节点按"其在 neighbor 层的邻居位置"的 median/barycenter 升序稳定排序。
  112. * 无邻居的节点关键值 +Infinity → 排层尾且彼此保持相对输入序。
  113. */
  114. function sweepLayer(
  115. current: Map<string, string[]>,
  116. edges: Array<[string, string]> | undefined,
  117. targetKey: string,
  118. neighborKey: string,
  119. isTargetHigh: boolean,
  120. method: 'median' | 'barycenter'
  121. ): void {
  122. const target = current.get(targetKey);
  123. if (!target || target.length <= 1 || !edges || edges.length === 0) return;
  124. const neighborRank = rankOf(current.get(neighborKey));
  125. // 目标层节点 → 其邻居(neighbor 层)位置集合(边已规范化为 [低层, 高层])
  126. const neighborPos = new Map<string, number[]>();
  127. for (const [loId, hiId] of edges) {
  128. const tId = isTargetHigh ? hiId : loId;
  129. const nId = isTargetHigh ? loId : hiId;
  130. const r = neighborRank.get(nId);
  131. if (r === undefined) continue;
  132. if (!neighborPos.has(tId)) neighborPos.set(tId, []);
  133. neighborPos.get(tId)!.push(r);
  134. }
  135. const keyOf = (id: string): number => {
  136. const arr = neighborPos.get(id);
  137. if (!arr || arr.length === 0) return Infinity;
  138. if (method === 'barycenter') {
  139. let sum = 0;
  140. for (const v of arr) sum += v;
  141. return sum / arr.length;
  142. }
  143. arr.sort((x, y) => x - y);
  144. const m = Math.floor(arr.length / 2);
  145. return arr.length % 2 === 1 ? arr[m] : (arr[m - 1] + arr[m]) / 2;
  146. };
  147. const indexed = target.map((id, idx) => ({ id, idx }));
  148. indexed.sort((p, q) => {
  149. const kp = keyOf(p.id);
  150. const kq = keyOf(q.id);
  151. if (kp !== kq) return kp - kq;
  152. return p.idx - q.idx;
  153. });
  154. current.set(
  155. targetKey,
  156. indexed.map((p) => p.id)
  157. );
  158. }
  159. /**
  160. * 层内排序主入口:返回各层有序节点 id(key:'0'..'6' 与 'ungraded')。
  161. * - 参与排序层(lv0..lv6):以输入序(data.nodes 顺序,即后端返回序)为初始序,
  162. * sweep 后返回历史交叉数最优解;
  163. * - 未分级层:始终返回输入序(不参与扫描);
  164. * - 空层:返回空数组。
  165. */
  166. export function orderNodesByLevel(data: TopologyData, opts?: OrderingOptions): Map<string, string[]> {
  167. const method = opts?.method ?? 'median';
  168. const sweepRounds = opts?.sweepRounds ?? 20;
  169. // 1) 按输入序分层
  170. const layers = new Map<string, string[]>();
  171. for (const n of data.nodes) {
  172. if (n.category !== 1) continue;
  173. const key = levelKeyOf(n);
  174. if (!layers.has(key)) layers.set(key, []);
  175. layers.get(key)!.push(n.id);
  176. }
  177. const edgesByPair = buildPairEdges(data);
  178. // 2) 初始层序(仅参与层)与历史最优记录
  179. const current = new Map<string, string[]>();
  180. for (const key of ORDER_KEYS) {
  181. current.set(key, layers.get(key) ? [...layers.get(key)!] : []);
  182. }
  183. let best = snapshotOrder(current);
  184. let bestCross = countTotalCrossingsWith(edgesByPair, current);
  185. const evalTotal = () => {
  186. const c = countTotalCrossingsWith(edgesByPair, current);
  187. if (c < bestCross) {
  188. bestCross = c;
  189. best = snapshotOrder(current);
  190. }
  191. };
  192. // 3) 多轮往返扫描:down(上→下,用低层位置重排高层)+ up(下→上,用高层位置重排低层)
  193. for (let round = 0; round < sweepRounds; round++) {
  194. for (let i = 0; i < ORDER_KEYS.length - 1; i++) {
  195. const key = `${ORDER_KEYS[i]}-${ORDER_KEYS[i + 1]}`;
  196. sweepLayer(current, edgesByPair.get(key), ORDER_KEYS[i + 1], ORDER_KEYS[i], true, method);
  197. evalTotal();
  198. }
  199. for (let i = ORDER_KEYS.length - 2; i >= 0; i--) {
  200. const key = `${ORDER_KEYS[i]}-${ORDER_KEYS[i + 1]}`;
  201. sweepLayer(current, edgesByPair.get(key), ORDER_KEYS[i], ORDER_KEYS[i + 1], false, method);
  202. evalTotal();
  203. }
  204. }
  205. // 4) 合并输出:参与层用历史最优,未分级层用输入序
  206. const result = new Map<string, string[]>();
  207. result.set('ungraded', layers.get('ungraded') ? [...layers.get('ungraded')!] : []);
  208. for (const key of ORDER_KEYS) {
  209. result.set(key, best.get(key) ? [...best.get(key)!] : []);
  210. }
  211. return result;
  212. }