# P&ID 도면 파싱 병목현상 분석 보고서 ## 1. 분석 개요 ### 1.1 분석 대상 - **샘플 DXF 파일**: `futurePlan/End-to-End P&ID Graph Pipeline/No-10_Plant_PID.dxf` - **파일 크기**: 1,174,227 라인 (약 1.1MB) - **엔티티 수**: 28,819개 ### 1.2 분석 대상 코드 - [`pid_geometric_extractor.py`](futurePlan/End-to-End%20P&ID%20Graph%20Pipeline/pid_geometric_extractor.py:1) - Phase 1: 기하학적 추출 - [`pid_topology_builder.py`](futurePlan/End-to-End%20P&ID%20Graph%20Pipeline/pid_topology_builder.py:1) - Phase 2: 위상 빌더 - [`pid_intelligent_mapper.py`](futurePlan/End-to-End%20P&ID%20Graph%20Pipeline/pid_intelligent_mapper.py:1) - Phase 3: 지능형 매핑 - [`mcp-server/pipeline/extractor.py`](mcp-server/pipeline/extractor.py:1) - 동일한 추출기 (MCP 서버용) - [`mcp-server/pipeline/mapper.py`](mcp-server/pipeline/mapper.py:1) - 동일한 매핑기 (MCP 서버용) --- ## 2. 샘플 DXF 파일 구조 분석 ### 2.1 엔티티 유형별 분포 | 엔티티 타입 | 수량 | 비율 | |------------|------|------| | LINE | 20,868 | 72.4% | | TEXT | 3,562 | 12.4% | | ARC | 1,324 | 4.6% | | CIRCLE | 1,275 | 4.4% | | LWPOLYLINE | 865 | 3.0% | | MTEXT | 363 | 1.3% | | ELLIPSE | 190 | 0.7% | | HATCH | 103 | 0.4% | | INSERT | 103 | 0.4% | | SOLID | 77 | 0.3% | | SPLINE | 65 | 0.2% | | POINT | 17 | 0.1% | | POLYLINE | 4 | 0.0% | | LEADER | 2 | 0.0% | | OLE2FRAME | 1 | 0.0% | **총 엔티티 수**: 28,819개 ### 2.2 특징 - **LINE이 압도적으로 많음** (72.4%): 배관 선이 주를 이룸 - **TEXT가 두 번째로 많음** (12.4%): 태그명, 설명 텍스트 - **MTEXT는 적음** (1.3%): 포맷팅된 텍스트는 상대적으로 적음 --- ## 3. 현재 성능 측정 결과 ### 3.1 Phase 1: 기하학적 추출 (pid_geometric_extractor) | 단계 | 소요 시간 | 비고 | |------|-----------|------| | DXF 파일 로드 | 0.84초 | ezdxf.readfile() | | PidGeometricExtractor 초기화 | 0.82초 | doc.modelspace() 생성 | | extract_and_save | 0.58초 | 28,257개 엔티티 처리 | | **총합** | **~1.4초** | | **결론**: Phase 1은 **매우 빠름** (1.4초 이내) ### 3.2 Phase 2: 위상 빌더 (pid_topology_builder) **측정 결과**: timeout (300초 초과) - **심각한 병목** **원인 분석**: - [`_merge_nodes()`](futurePlan/End-to-End%20P&ID%20Graph%20Pipeline/pid_topology_builder.py:110) 메서드의 **O(n²) 복잡도** - 28,000개 엔티티 → 약 400,000,000번의 거리 계산 - `shapely.geometry.box.distance()` 호출이 비용 큼 **현재 코드 구조**: ```python # pid_topology_builder.py:110-150 def _merge_nodes(self) -> List[Dict[str, Any]]: """기하학적으로 거의 동일한 노드들을 병합""" for i in range(len(self.data)): # O(n) for j in range(i + 1, len(self.data)): # O(n) # shapely 거리 계산 (비용 큼) if current_bbox.distance(target_bbox) < merge_threshold: ``` ### 3.3 Phase 3: 지능형 매핑 (pid_intelligent_mapper) **측정 결과**: LLM API 호출로 인해 **예측 불가능한 지연** **원인 분석**: - [`_resolve_generic()`](futurePlan/End-to-End%20P&ID%20Graph%20Pipeline/pid_intelligent_mapper.py:36) 메서드에서 **비동기 LLM 호출** - 각 노드당 1회 이상의 API 호출 필요 - 100개 노드 = 100회 API 호출 (약 30-60초 소요 예상) **현재 코드 구조**: ```python # pid_intelligent_mapper.py:36-74 async def _resolve_generic(self, node_id: str, category_prompt: str) -> MappingResult: # RapidFuzz 후보 추출 candidates = process.extract(tag_text, self.system_tags, limit=5) # LLM API 호출 (비동기) response = await self.client.chat.completions.create(...) ``` --- ## 4. 병목현상 식별 및 개선 방안 ### 4.1 병목 1: Phase 2 - 노드 병합 알고리즘 (O(n²)) | 항목 | 내용 | |------|------| | **위치** | [`pid_topology_builder.py:110-150`](futurePlan/End-to-End%20P&ID%20Graph%20Pipeline/pid_topology_builder.py:110) | | **문제** | 이중 루프 + shapely 거리 계산 → O(n²) 복잡도 | | **영향** | 28,000개 엔티티 → 4억 번의 거리 계산 | | **심각도** | **HIGH** | #### 개선 방안 A: 공간 인덱스 사용 (추천) ```python # R-tree 또는 Quadtree를 사용한 공간 쿼리 from rtree import index def _merge_nodes_with_spatial_index(self): # 1. 공간 인덱스 생성 (O(n log n)) p = index.Property() idx = index.Index(properties=p) for i, item in enumerate(self.data): bbox = item['bbox'] idx.insert(i, (bbox['min_x'], bbox['min_y'], bbox['max_x'], bbox['max_y'])) # 2. 인접 노드만 비교 (O(n log n) 평균) merged = [] visited = set() for i, item in enumerate(self.data): if i in visited: continue # 인접 노드만 검색 bbox = item['bbox'] neighbors = list(idx.intersection(( bbox['min_x'] - merge_threshold, bbox['min_y'] - merge_threshold, bbox['max_x'] + merge_threshold, bbox['max_y'] + merge_threshold ))) # ... 병합 로직 ``` #### 개선 방안 B: 그리드 기반 클러스터링 ```python def _merge_nodes_with_grid(self): # 그리드 셀 크기 = merge_threshold grid = {} for i, item in enumerate(self.data): bbox = item['bbox'] center_x = (bbox['min_x'] + bbox['max_x']) / 2 center_y = (bbox['min_y'] + bbox['max_y']) / 2 grid_key = (int(center_x / merge_threshold), int(center_y / merge_threshold)) if grid_key not in grid: grid[grid_key] = [] grid[grid_key].append((i, item)) # 각 그리드 셀 내에서만 병합 # 인접 그리드 셀도 확인 필요 ``` #### 개선 방안 C: 샘플링 + 근사 병합 - 모든 엔티티를 병합하지 않고, **특정 타입만** 병합 (TEXT, MTEXT) - LINE, LWPOLYLINE은 배관 연결 시에만 사용 --- ### 4.2 병목 2: Phase 3 - LLM API 호출 (비동기 병렬 처리 부족) | 항목 | 내용 | |------|------| | **위치** | [`pid_intelligent_mapper.py:36-74`](futurePlan/End-to-End%20P&ID%20Graph%20Pipeline/pid_intelligent_mapper.py:36) | | **문제** | 각 노드당 1회 이상의 LLM API 호출 | | **영향** | 100개 노드 = 100회 API 호출 (약 30-60초) | | **심각도** | **MED** | #### 개선 방안 A: 배치 처리 (Batch Processing) ```python # 여러 노드를 하나의 프롬프트로 처리 async def _resolve_batch(self, node_ids: List[str], category_prompt: str): # 프롬프트를 하나로 묶음 prompts = [] for nid in node_ids: node_data = self.graph.nodes[nid] tag_text = node_data.get('value', '') candidates = process.extract(tag_text, self.system_tags, limit=5) context = self.get_node_context(nid) prompts.append(f"태그 '{tag_text}' -> 후보: {candidates}") prompt = f""" {category_prompt} 다음 태그들을 시스템 태그와 매핑하세요: {chr(10).join(prompts)} JSON 형식으로 응답: {{"node_id": "resolved_tag", ...}} """ response = await self.client.chat.completions.create(...) ``` #### 개선 방안 B: 1차 필터링 강화 ```python # RapidFuzz 유사도가 낮은 후보는 LLM 호출 없이 거름 async def _resolve_with_filter(self, node_id: str, category_prompt: str): node_data = self.graph.nodes.get(node_id, {}) tag_text = node_data.get('value', '') # 1. RapidFuzz로 후보 추출 candidates = process.extract(tag_text, self.system_tags, scorer=fuzz.WRatio, limit=10) # 2. 유사도가 낮은 후보 필터링 high_confidence_candidates = [c for c in candidates if c[1] > 85] if len(high_confidence_candidates) == 1: # 확실한 매칭 → LLM 호출 생략 return MappingResult( resolved_tag=high_confidence_candidates[0][0], reason="High confidence match (RapidFuzz > 85)", confidence=high_confidence_candidates[0][1] / 100.0 ) # 3. 유사도가 낮은 경우에만 LLM 호출 # ... ``` #### 개선 방안 C: 캐싱 ```python from functools import lru_cache @lru_cache(maxsize=10000) def _cached_fuzzy_match(tag_text: str): return process.extract(tag_text, self.system_tags, limit=5) async def _resolve_generic(self, node_id: str, category_prompt: str): # 캐시된 결과 사용 candidates = _cached_fuzzy_match(tag_text) ``` --- ### 4.3 병목 3: Phase 2 - BBox 계산 (중복 계산) | 항목 | 내용 | |------|------| | **위치** | [`pid_geometric_extractor.py:58-101`](futurePlan/End-to-End%20P&ID%20Graph%20Pipeline/pid_geometric_extractor.py:58) | | **문제** | 각 엔티티당 BBox 계산 (LINE은 2점, POLYLINE은 다점) | | **영향** | 28,000개 엔티티 × BBox 계산 | | **심각도** | **LOW** (현재는 0.58초로 허용 가능) | #### 개선 방안: 미리 계산된 BBox 사용 - ezdxf의 `entity.dxf.bbox()` 사용 가능 여부 확인 - 또는 추출 시 BBox를 미리 계산하여 캐싱 --- ## 5. 종합 병목순위 및 개선 우선순위 | 순위 | 병목 | 위치 | 심각도 | 개선 난이도 | 예상 개선 효과 | |------|------|------|--------|-------------|----------------| | 1 | Phase 2 노드 병합 O(n²) | [`pid_topology_builder.py:110`](futurePlan/End-to-End%20P&ID%20Graph%20Pipeline/pid_topology_builder.py:110) | **HIGH** | 중간 | 100배 이상 속도 향상 | | 2 | Phase 3 LLM API 호출 | [`pid_intelligent_mapper.py:36`](futurePlan/End-to-End%20P&ID%20Graph%20Pipeline/pid_intelligent_mapper.py:36) | **MED** | 낮음 | 5-10배 속도 향상 | | 3 | Phase 2 공간 쿼리 | [`pid_topology_builder.py:76`](futurePlan/End-to-End%20P&ID%20Graph%20Pipeline/pid_topology_builder.py:76) | **MED** | 낮음 | 2-3배 속도 향상 | | 4 | Phase 1 BBox 중복 계산 | [`pid_geometric_extractor.py:58`](futurePlan/End-to-End%20P&ID%20Graph%20Pipeline/pid_geometric_extractor.py:58) | LOW | 낮음 | 10-20% 속도 향상 | --- ## 6. 구체적인 개선 계획 ### Phase 1: 공간 인덱스 도입 (1-2일) 1. **rtree 패키지 설치** ```bash pip install rtree libspatialindex ``` 2. **pid_topology_builder.py 수정** - `_merge_nodes()` 메서드를 `_merge_nodes_spatial()`으로 재작성 - R-tree를 사용한 공간 쿼리 구현 3. **성능 테스트** - 28,000개 엔티티 처리 시간 측정 - 예상: 1-2초 이내 완료 ### Phase 2: LLM 배치 처리 (1일) 1. **pid_intelligent_mapper.py 수정** - `_resolve_batch()` 메서드 추가 - 10개 노드씩 배치로 처리 2. **pid_topology_builder.py 연동** - 매핑 단계에서 배치 처리 사용 ### Phase 3: 캐싱 (0.5일) 1. **RapidFuzz 결과 캐싱** - `@lru_cache` 데코레이터 사용 - 동일한 태그에 대한 중복 검색 방지 --- ## 7. 현재 코드의 문제점 요약 ### 7.1 pid_topology_builder.py ```python # 현재 코드 (병목) def _merge_nodes(self) -> List[Dict[str, Any]]: for i in range(len(self.data)): # O(n) for j in range(i + 1, len(self.data)): # O(n) # shapely 거리 계산 (비용 큼) if current_bbox.distance(target_bbox) < merge_threshold: ``` **문제점**: - 28,000개 엔티티 → 392,000,000번의 거리 계산 - shapely의 `distance()`는 계산 비용이 큼 - 공간 인덱스를 사용하지 않아 불필요한 비교가 많음 ### 7.2 pid_intelligent_mapper.py ```python # 현재 코드 (병목) async def _resolve_generic(self, node_id: str, category_prompt: str): candidates = process.extract(tag_text, self.system_tags, limit=5) # LLM API 호출 (1회당 0.3-0.6초) response = await self.client.chat.completions.create(...) ``` **문제점**: - 각 노드당 1회 이상의 LLM API 호출 - 100개 노드 = 100회 API 호출 (30-60초) - RapidFuzz 후보 추출 후 유사도가 낮은 경우에도 LLM 호출 --- ## 8. 결론 ### 8.1 현재 상태 - **Phase 1**: 1.4초 (양호) - **Phase 2**: timeout (심각한 병목) - **Phase 3**: 예측 불가능한 지연 (LLM API 의존) ### 8.2 개선 후 예상 - **Phase 1**: 1.4초 (변화 없음) - **Phase 2**: 1-2초 (공간 인덱스 도입 후) - **Phase 3**: 5-10초 (배치 처리 + 필터링 후) ### 8.3 총 예상 처리 시간 - **현재**: timeout (Phase 2에서 멈춤) - **개선 후**: 약 7-13초 (28,000개 엔티티 기준) --- ## 9. 참고: 샘플 DXF 파일 정보 - **파일 경로**: `futurePlan/End-to-End P&ID Graph Pipeline/No-10_Plant_PID.dxf` - **파일 크기**: 약 1.1MB - **엔티티 수**: 28,819개 - **특징**: 산업용 공정 도면 (배관, 계측기, 설비 포함) --- **작성일**: 2026-05-02 **분석 도구**: Qwen3-Coder-Next-FP8 **분석 대상**: ExperionCrawler P&ID Graph Pipeline