/** * GraphProcessor - Bộ Xử Lý Đồ Thị * Chuyển đổi dữ liệu đồ thị từ ONNXParser sang định dạng Cytoscape.js * Requirements: 4.2, 4.3, 4.6, 15.5 */ class GraphProcessor { /** * Xử lý mô hình đã phân tích và tạo cấu trúc dữ liệu cho Cytoscape.js * @param {Object} parsedModel - Mô hình đã phân tích từ ONNXParser * @returns {{ elements: Array, stats: Object }} */ processGraph(parsedModel) { if (!parsedModel || !parsedModel.graph) { return { elements: [], stats: { nodeCount: 0, edgeCount: 0, initializerCount: 0, inputCount: 0, outputCount: 0 } }; } const { graph, inputs = [], outputs = [], initializers = [] } = parsedModel; const { nodes = [], edges = [], initializers: graphInitializers = [] } = graph; // Tập hợp tên đầu vào và đầu ra để gán class const inputNames = new Set((inputs || []).map(i => i.name)); const outputNames = new Set((outputs || []).map(o => o.name)); // Xây dựng map: tên tensor → nodeId (để tìm nút đầu vào/đầu ra) const outputTensorToNodeId = new Map(); for (const node of nodes) { for (const out of (node.outputs || [])) { if (out) outputTensorToNodeId.set(out, node.id); } } const elements = []; // ── Nút đầu vào mô hình (input-node) ────────────────────────────────── for (const input of inputs) { elements.push({ data: { id: `input_${input.name}`, label: input.name, name: input.name, opType: 'Input', shape: input.shape, dataType: input.dataType, description: input.description || '', nodeType: 'input' }, classes: 'input-node' }); } // ── Nút đầu ra mô hình (output-node) ────────────────────────────────── for (const output of outputs) { elements.push({ data: { id: `output_${output.name}`, label: output.name, name: output.name, opType: 'Output', shape: output.shape, dataType: output.dataType, description: output.description || '', nodeType: 'output' }, classes: 'output-node' }); } // ── Nút bộ khởi tạo (initializer-node) ──────────────────────────────── const allInitializers = initializers.length > 0 ? initializers : graphInitializers; for (const init of allInitializers) { elements.push({ data: { id: `init_${init.name}`, label: init.name, name: init.name, opType: 'Initializer', shape: init.shape, dataType: init.dataType, size: init.size, elementCount: init.elementCount, nodeType: 'initializer' }, classes: 'initializer-node' }); } // ── Kiểm tra đồ thị lớn → clustering ────────────────────────────────── const threshold = (typeof CONFIG !== 'undefined' && CONFIG.PERFORMANCE) ? CONFIG.PERFORMANCE.LAZY_LOAD_THRESHOLD : 1000; if (nodes.length > threshold) { return this._processLargeGraph( nodes, edges, inputs, outputs, allInitializers, elements, threshold ); } // ── Nút toán tử (op-node) ────────────────────────────────────────────── for (const node of nodes) { elements.push({ data: { id: node.id, label: node.opType || node.name || node.id, name: node.name, opType: node.opType || '', attributes: node.attributes || {}, inputs: node.inputs || [], outputs: node.outputs || [], domain: node.domain || '', nodeType: 'op' }, classes: 'op-node' }); } // ── Cạnh giữa các nút toán tử ───────────────────────────────────────── let edgeIndex = 0; for (const edge of edges) { elements.push({ data: { id: `edge_${edgeIndex++}`, source: edge.source, target: edge.target, label: edge.label || '' }, classes: 'edge' }); } // ── Cạnh từ input-node → op-node ────────────────────────────────────── for (const node of nodes) { for (const inputTensor of (node.inputs || [])) { if (inputNames.has(inputTensor)) { elements.push({ data: { id: `edge_${edgeIndex++}`, source: `input_${inputTensor}`, target: node.id, label: inputTensor }, classes: 'edge' }); } } } // ── Cạnh từ op-node → output-node ───────────────────────────────────── for (const node of nodes) { for (const outputTensor of (node.outputs || [])) { if (outputNames.has(outputTensor)) { elements.push({ data: { id: `edge_${edgeIndex++}`, source: node.id, target: `output_${outputTensor}`, label: outputTensor }, classes: 'edge' }); } } } const stats = { nodeCount: nodes.length, edgeCount: edges.length, initializerCount: allInitializers.length, inputCount: inputs.length, outputCount: outputs.length }; return { elements, stats }; } // ─── Private Helpers ──────────────────────────────────────────────────────── /** * Xử lý đồ thị lớn bằng cách nhóm các nút theo opType (clustering) * @private */ _processLargeGraph(nodes, edges, inputs, outputs, initializers, baseElements, threshold) { // Nhóm nút theo opType const clusters = new Map(); for (const node of nodes) { const opType = node.opType || 'Unknown'; if (!clusters.has(opType)) { clusters.set(opType, []); } clusters.get(opType).push(node); } const elements = [...baseElements]; let edgeIndex = 0; // Tạo nút cluster cho mỗi opType for (const [opType, clusterNodes] of clusters) { const clusterId = `cluster_${opType}`; elements.push({ data: { id: clusterId, label: `${opType} (${clusterNodes.length})`, name: opType, opType: opType, nodeCount: clusterNodes.length, nodeIds: clusterNodes.map(n => n.id), nodeType: 'cluster', isCluster: true }, classes: 'op-node cluster-node' }); } // Xây dựng map: nodeId → clusterId const nodeToCluster = new Map(); for (const [opType, clusterNodes] of clusters) { const clusterId = `cluster_${opType}`; for (const node of clusterNodes) { nodeToCluster.set(node.id, clusterId); } } // Tạo cạnh giữa các cluster (tránh trùng lặp) const clusterEdgeSet = new Set(); for (const edge of edges) { const srcCluster = nodeToCluster.get(edge.source); const tgtCluster = nodeToCluster.get(edge.target); if (srcCluster && tgtCluster && srcCluster !== tgtCluster) { const key = `${srcCluster}→${tgtCluster}`; if (!clusterEdgeSet.has(key)) { clusterEdgeSet.add(key); elements.push({ data: { id: `edge_${edgeIndex++}`, source: srcCluster, target: tgtCluster, label: '' }, classes: 'edge' }); } } } // Cạnh từ input-node → cluster const inputNames = new Set((inputs || []).map(i => i.name)); const outputNames = new Set((outputs || []).map(o => o.name)); const inputClusterEdgeSet = new Set(); for (const node of nodes) { const clusterId = nodeToCluster.get(node.id); for (const inputTensor of (node.inputs || [])) { if (inputNames.has(inputTensor)) { const key = `input_${inputTensor}→${clusterId}`; if (!inputClusterEdgeSet.has(key)) { inputClusterEdgeSet.add(key); elements.push({ data: { id: `edge_${edgeIndex++}`, source: `input_${inputTensor}`, target: clusterId, label: inputTensor }, classes: 'edge' }); } } } for (const outputTensor of (node.outputs || [])) { if (outputNames.has(outputTensor)) { const key = `${clusterId}→output_${outputTensor}`; if (!inputClusterEdgeSet.has(key)) { inputClusterEdgeSet.add(key); elements.push({ data: { id: `edge_${edgeIndex++}`, source: clusterId, target: `output_${outputTensor}`, label: outputTensor }, classes: 'edge' }); } } } } const stats = { nodeCount: nodes.length, edgeCount: edges.length, initializerCount: initializers.length, inputCount: inputs.length, outputCount: outputs.length, clusterCount: clusters.size, isClustered: true }; return { elements, stats }; } } // Export as global for browser usage window.GraphProcessor = GraphProcessor;