From 01c9e1972e4859ffd766b41354d403b18a231c50 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Mon, 30 Mar 2026 12:54:27 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=88=A0=E9=99=A4=E6=97=A7?= =?UTF-8?q?=E7=89=88=E6=8B=93=E6=89=91=E7=AE=97=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/src/topology/compare.ts | 29 ---- data/src/topology/graph.ts | 255 -------------------------------- data/src/topology/interface.ts | 59 -------- data/src/topology/similarity.ts | 179 ---------------------- data/src/utils.ts | 64 -------- 5 files changed, 586 deletions(-) delete mode 100644 data/src/topology/compare.ts delete mode 100644 data/src/topology/graph.ts delete mode 100644 data/src/topology/interface.ts delete mode 100644 data/src/topology/similarity.ts diff --git a/data/src/topology/compare.ts b/data/src/topology/compare.ts deleted file mode 100644 index 0a08eaf..0000000 --- a/data/src/topology/compare.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { buildTopologicalGraph } from './graph'; -import { GinkaTopologicalGraphs } from './interface'; -import { overallSimilarity } from './similarity'; - -const cache = new Map(); - -export function getTopologicalGraph( - floorId: string, - map: number[][] -): GinkaTopologicalGraphs { - if (cache.has(floorId)) return cache.get(floorId)!; - const graphs = buildTopologicalGraph(map); - cache.set(floorId, graphs); - return graphs; -} - -export function compareMap( - floorId1: string, - floorId2: string, - map1: number[][], - map2: number[][] -) { - const graph1 = getTopologicalGraph(floorId1, map1); - const graph2 = getTopologicalGraph(floorId2, map2); - - const kernel = overallSimilarity(graph1, graph2); - - return kernel; -} diff --git a/data/src/topology/graph.ts b/data/src/topology/graph.ts deleted file mode 100644 index 7f38dd8..0000000 --- a/data/src/topology/graph.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { - ResourceArea, - GinkaGraph, - BranchNode, - GinkaTopologicalGraphs, - ResourceNode, - NodeType -} from './interface'; - -export const tileType = new Set( - Array(13) - .fill(0) - .map((_, i) => i) -); -const branchType = new Set([6, 7, 8, 9]); -const entranceType = new Set([10, 11]); -const resourceType = new Set([0, 2, 3, 4, 5, 10, 11, 12, 13]); - -export const directions: [number, number][] = [ - [-1, 0], - [1, 0], - [0, -1], - [0, 1] -]; - -function buildGraphFromEntrance( - map: number[][], - entrance: number, - resourceMap: Map, - areaMap: ResourceArea[] -): GinkaGraph { - const width = map[0].length; - const height = map[1].length; - - const visitedEntrance = new Set([entrance]); - const visited = new Set(); - const queue: [number, number][] = []; - queue.push([entrance % width, Math.floor(entrance / width)]); - - const branchNodes = new Set(); - - // 1. BFS 检测所有分支节点 - while (queue.length > 0) { - const item = queue.shift(); - if (!item) continue; - const [nx, ny] = item; - const index = ny * width + nx; - if (visited.has(index)) continue; - const tile = map[ny][nx]; - - if (entranceType.has(tile)) { - visitedEntrance.add(index); - } - if (branchType.has(tile)) { - branchNodes.add(index); - } - visited.add(index); - - for (const [dx, dy] of directions) { - const px = dx + nx; - const py = dy + ny; - if (px < 0 || px >= width || py < 0 || py >= height) { - continue; - } - const tile = map[py][px]; - if (tile !== 1) { - // 非墙区域可通行 - queue.push([px, py]); - } - } - } - - // 2. 从分支节点构建拓扑图 - const graph = new Map(); - branchNodes.forEach(v => { - const nx = v % width; - const ny = Math.floor(v / width); - if (!graph.get(v)) { - graph.set(v, { - type: NodeType.Branch, - neighbor: new Set(), - tile: map[ny][nx] - }); - } - const node = graph.get(v)!; - for (const [dx, dy] of directions) { - const px = nx + dx; - const py = ny + dy; - if (px < 0 || px >= width || py < 0 || py >= height) { - continue; - } - const index = py * width + px; - - // 先检查临近节点是不是分支节点,是的话链接到自己 - if (branchNodes.has(index)) { - node.neighbor.add(index); - } else { - // 检查是不是资源节点 - const pointer = resourceMap.get(index); - if (pointer === void 0) continue; - const area = areaMap[pointer]; - if (!area) continue; - area.neighbor.add(v); - area.members.forEach(v => { - node.neighbor.add(v); - }); - } - } - }); - - // 3. 把资源节点拆分成并排,并放入拓扑图 - areaMap.forEach(v => { - v.members.forEach(index => { - const nx = index % width; - const ny = Math.floor(index / width); - const tile = map[ny][nx]; - if (tile === 0) return; - const node: ResourceNode = { - type: NodeType.Resource, - resourceType: tile, - neighbor: v.neighbor, - resourceArea: v - }; - graph.set(index, node); - }); - }); - - return { graph, resourceMap, areaMap, visitedEntrance, visited }; -} - -function findResourceNodes(map: number[][]) { - const width = map[0].length; - const height = map[1].length; - - const visited = new Set(); - const areas: ResourceArea[] = []; - const resourcesMap: Map = new Map(); - - for (let ny = 0; ny < height; ny++) { - for (let nx = 0; nx < width; nx++) { - const tile = map[ny][nx]; - const index = ny * width + nx; - if (visited.has(index) || !resourceType.has(tile)) { - continue; - } - const queue: [number, number][] = []; - queue.push([nx, ny]); - const area: ResourceArea = { - type: NodeType.Resource, - resources: new Map([[tile, 1]]), - members: new Set([index]), - neighbor: new Set() - }; - - while (queue.length > 0) { - const item = queue.shift(); - if (!item) continue; - const [nx, ny] = item; - const index = ny * width + nx; - if (visited.has(index)) { - continue; - } - const tile = map[ny][nx]; - if (!resourceType.has(tile)) { - continue; - } - visited.add(index); - - const exists = area.resources.get(tile); - if (!exists) { - area.resources.set(tile, 1); - } else { - area.resources.set(tile, exists + 1); - } - area.members.add(index); - resourcesMap.set(index, areas.length); - - for (const [dx, dy] of directions) { - const px = nx + dx; - const py = ny + dy; - if (px < 0 || px >= width || py < 0 || py >= height) { - continue; - } - queue.push([px, py]); - } - } - - areas.push(area); - } - } - - return { areaMap: areas, resourcesMap }; -} - -export function buildTopologicalGraph(map: number[][]): GinkaTopologicalGraphs { - const width = map[0].length; - const height = map[1].length; - - // 1. 找到所有入口 - const entrances = new Set(); - for (let ny = 0; ny < height; ny++) { - for (let nx = 0; nx < width; nx++) { - const tile = map[ny][nx]; - if (entranceType.has(tile)) { - entrances.add(ny * width + nx); - } - } - } - - // 2. 找到所有的资源节点 - const { areaMap, resourcesMap } = findResourceNodes(map); - - // 3. 对每个入口计算拓扑图 - const graphs: GinkaGraph[] = []; - const usedEntrance = new Set(); - const totalVisited = new Set(); - /** 入口位置到拓扑图的映射 */ - const entranceMap = new Map(); - entrances.forEach(v => { - if (usedEntrance.has(v)) { - return; - } - const nx = v % width; - const ny = Math.floor(v / width); - const entranceGraph = buildGraphFromEntrance( - map, - v, - resourcesMap, - areaMap - ); - const { graph, visited, visitedEntrance } = entranceGraph; - graphs.push(entranceGraph); - // 标记已经探索到的入口,并标记这个入口对应了哪个图 - visitedEntrance.forEach(v => { - usedEntrance.add(v); - entranceMap.set(v, entranceGraph); - }); - visited.forEach(v => { - totalVisited.add(v); - }); - }); - - // 3. 计算不可到达区域 - const unreachable = new Set(); - for (let ny = 0; ny < height; ny++) { - for (let nx = 0; nx < width; nx++) { - const index = ny * width + nx; - if (!totalVisited.has(index) && map[ny][nx] !== 1) { - unreachable.add(index); - } - } - } - - return { graphs, entranceMap, unreachable }; -} diff --git a/data/src/topology/interface.ts b/data/src/topology/interface.ts deleted file mode 100644 index 229a7bc..0000000 --- a/data/src/topology/interface.ts +++ /dev/null @@ -1,59 +0,0 @@ -export const enum NodeType { - Branch, - Resource -} - -export interface ResourceArea { - /** 节点类型 */ - readonly type: NodeType.Resource; - /** 每种资源对应的数量 */ - readonly resources: Map; - /** 资源区域包含的所有资源图块坐标索引 */ - readonly members: Set; - /** 资源区域的邻居节点 */ - readonly neighbor: Set; -} - -export interface BranchNode { - /** 节点类型 */ - readonly type: NodeType.Branch; - /** 分支节点的邻居节点 */ - readonly neighbor: Set; - /** 分支节点图块 */ - readonly tile: number; -} - -export interface ResourceNode { - /** 节点类型 */ - readonly type: NodeType.Resource; - /** 资源类型 */ - readonly resourceType: number; - /** 邻居节点 */ - readonly neighbor: Set; - /** 资源节点所属的资源区域 */ - readonly resourceArea: ResourceArea; -} - -export type GinkaNode = BranchNode | ResourceNode; - -export interface GinkaGraph { - /** 拓扑图内容,键表示位置,值表示这一点的节点 */ - readonly graph: Map; - /** 资源指针,键表示位置,值表示这一点对应的资源节点在 areaMap 的索引 */ - readonly resourceMap: Map; - /** 资源区域列表 */ - readonly areaMap: ResourceArea[]; - /** 这个拓扑图包含的入口位置 */ - readonly visitedEntrance: Set; - /** 这个拓扑图能够造访的所有位置 */ - readonly visited: Set; -} - -export interface GinkaTopologicalGraphs { - /** 这个地图包含的所有独立的图 */ - readonly graphs: GinkaGraph[]; - /** 每个入口对应哪个图 */ - readonly entranceMap: Map; - /** 这个图从入口开始的不可到达区域 */ - readonly unreachable: Set; -} diff --git a/data/src/topology/similarity.ts b/data/src/topology/similarity.ts deleted file mode 100644 index b10e0a5..0000000 --- a/data/src/topology/similarity.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { cosineSimilarity } from 'src/utils'; -import { GinkaGraph, GinkaTopologicalGraphs, NodeType } from './interface'; - -interface WLNode { - originalPos: number; - originalLabel: string; - currentLabel: string; - neighbors: WLNode[]; -} - -function encodeNodeLabels(graph: GinkaGraph) { - const nodes: WLNode[] = []; - const nodeMap = new Map(); - - graph.graph.forEach((node, pos) => { - let label: string; - - // 编码为唯一哈希值(用字符串就行,V8 会自动帮你算哈希) - if (node.type === NodeType.Branch) { - label = `B:${node.tile}`; - } else { - label = `R:${node.resourceType}`; - } - - const wlNode: WLNode = { - originalPos: pos, - originalLabel: label, - currentLabel: label, - neighbors: [] - }; - nodeMap.set(pos, wlNode); - nodes.push(wlNode); - }); - - // 映射邻居节点 - nodes.forEach(node => { - const ginkaNode = graph.graph.get(node.originalPos); - ginkaNode?.neighbor.forEach(v => { - const wl = nodeMap.get(v); - if (wl) node.neighbors.push(wl); - }); - }); - - return nodes; -} - -function weisfeilerLehmanIteration( - nodes: WLNode[], - iterations: number, - decay: number = 0.6 // 衰减权重,减小长距离图的权重 -) { - const labelHistory: string[][] = []; - - for (let i = 0; i < iterations; i++) { - const newLabels: string[] = []; - - // 生成新标签 - nodes.forEach(node => { - const neighborLabels = node.neighbors - .map(n => n.currentLabel) - .sort(); - - const compositeLabel = `${node.currentLabel}|${neighborLabels.join( - ',' - )}`.slice(0, 8192); - - newLabels.push(compositeLabel); - }); - - // 更新节点标签并记录 - nodes.forEach((node, idx) => { - node.currentLabel = newLabels[idx]; - }); - labelHistory.push([...newLabels]); - } - - // 统计每个节点的数量 - let weight = 1; - const numMap = new Map(); - labelHistory.forEach(iter => { - iter.forEach(v => { - if (!numMap.has(v)) { - numMap.set(v, weight); - } else { - numMap.set(v, numMap.get(v)! + weight); - } - }); - weight *= decay; - }); - // 把每个节点的原始标签也加上,权重使用最远权重,可以认为是资源重复率 - nodes.forEach(node => { - if (!numMap.has(node.originalLabel)) { - numMap.set(node.originalLabel, weight); - } else { - numMap.set( - node.originalLabel, - numMap.get(node.originalLabel)! + weight - ); - } - }); - - return numMap; -} - -function vectorizeFeatures(features: Map, vocab: string[]) { - const vec: number[] = new Array(vocab.length).fill(0); - - features.forEach((count, label) => { - const index = vocab.indexOf(label); - if (index !== -1) { - vec[index] += count; - } - }); - - return vec; -} - -function wlKernel( - graphA: GinkaGraph, - graphB: GinkaGraph, - iterations = 3 -): number { - // 编码节点 - const nodesA = encodeNodeLabels(graphA); - const nodesB = encodeNodeLabels(graphB); - - // 迭代生成标签 - const featuresA = weisfeilerLehmanIteration(nodesA, iterations); - const featuresB = weisfeilerLehmanIteration(nodesB, iterations); - - // 构建特征向量 - const vocab = [...new Set([...featuresA.keys(), ...featuresB.keys()])]; - const vecA = vectorizeFeatures(featuresA, vocab); - const vecB = vectorizeFeatures(featuresB, vocab); - - // 计算余弦相似度 - return cosineSimilarity(vecA, vecB); -} - -export function overallSimilarity( - a: GinkaTopologicalGraphs, - b: GinkaTopologicalGraphs -) { - // 使用 Weisfeiler-Lehman Kernel 方式计算拓扑图相似度 - const graphsA = a.graphs; - const graphsB = b.graphs; - - let totalSimilarity = 0; - const comparedGraph = new Set(); - graphsA.forEach(ga => { - let maxSimilarity = 0; - let maxGraph: GinkaGraph | null = null; - // 图之间两两比较,找到最接近的作为相似度 - for (const gb of graphsB) { - if (comparedGraph.has(gb)) continue; - // 计算迭代次数 - const min = Math.min(ga.graph.size, gb.graph.size); - const iterations = Math.ceil(Math.max(1, Math.log(min))); - const similarity = wlKernel(ga, gb, iterations); - if (similarity > maxSimilarity && !isNaN(similarity)) { - maxSimilarity = similarity; - maxGraph = gb; - } - if (similarity === 1) break; - } - totalSimilarity += maxSimilarity; - if (maxGraph) comparedGraph.add(maxGraph); - }); - - // 不可达区域惩罚 - const reduction = - 1 / (1 + Math.abs(a.unreachable.size - b.unreachable.size)); - // 取根号使结果更接近线性 - if (graphsA.length === 0) { - return 0; - } else { - return Math.sqrt(totalSimilarity / graphsA.length) * reduction; - } -} diff --git a/data/src/utils.ts b/data/src/utils.ts index a6ef14f..265af7c 100644 --- a/data/src/utils.ts +++ b/data/src/utils.ts @@ -43,70 +43,6 @@ export function mergeDataset( return dataset; } -export function cosineSimilarity(vecA: number[], vecB: number[]): number { - if (vecA.length !== vecB.length) { - throw new Error('Vectors must have same dimension'); - } - - let dot = 0, - normA = 0, - normB = 0; - for (let i = 0; i < vecA.length; i++) { - dot += vecA[i] * vecB[i]; - normA += vecA[i] ** 2; - normB += vecB[i] ** 2; - } - - return dot / (Math.sqrt(normA) * Math.sqrt(normB)); -} - -export async function parseTowerInfo( - path: string, - configName: string -): Promise { - const dataFile = await readFile(join(path, 'data.js'), 'utf-8'); - const data: any = JSON.parse(dataFile.split('\n').slice(1).join('\n')); - const configFile = await readFile(join(path, configName), 'utf-8'); - - return { - path: path, - name: data.firstData.name as string, - floorIds: data.main.floorIds as string[], - config: JSON.parse(configFile) as BaseConfig - }; -} - -export function mergeFloorIds(...info: TowerInfo[]) { - const ids: string[] = []; - info.forEach(v => { - ids.push(...v.floorIds.map(id => `${v.name}:${id}`)); - }); - return ids; -} - -export async function fromJSON(path: string) { - const file = await readFile(path, 'utf-8'); - const data = JSON.parse(file) as Record; - const clip: Record = {}; - const config: BaseConfig = { - clip: { - defaults: [0, 0, 0, 0], - special: clip - } - }; - const name = (Math.random() * 12).toFixed(0); - const floorMap = new Map(); - for (const [key, value] of Object.entries(data)) { - const floorData: FloorData = { - map: value, - id: key, - config - }; - floorMap.set(`${name}:${key}`, floorData); - } - return floorMap; -} - export function chooseFrom(arr: T[], n: number): T[] { const copy = arr.slice(); for (let i = copy.length - 1; i > 0; i--) {