Files
ExperionCrawler/P&ID_병목현상_분석_보고서.md

12 KiB
Raw Blame History

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 분석 대상 코드


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() 메서드의 O(n²) 복잡도
  • 28,000개 엔티티 → 약 400,000,000번의 거리 계산
  • shapely.geometry.box.distance() 호출이 비용 큼

현재 코드 구조:

# 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() 메서드에서 비동기 LLM 호출
  • 각 노드당 1회 이상의 API 호출 필요
  • 100개 노드 = 100회 API 호출 (약 30-60초 소요 예상)

현재 코드 구조:

# 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
문제 이중 루프 + shapely 거리 계산 → O(n²) 복잡도
영향 28,000개 엔티티 → 4억 번의 거리 계산
심각도 HIGH

개선 방안 A: 공간 인덱스 사용 (추천)

# 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: 그리드 기반 클러스터링

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
문제 각 노드당 1회 이상의 LLM API 호출
영향 100개 노드 = 100회 API 호출 (약 30-60초)
심각도 MED

개선 방안 A: 배치 처리 (Batch Processing)

# 여러 노드를 하나의 프롬프트로 처리
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차 필터링 강화

# 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: 캐싱

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
문제 각 엔티티당 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 HIGH 중간 100배 이상 속도 향상
2 Phase 3 LLM API 호출 pid_intelligent_mapper.py:36 MED 낮음 5-10배 속도 향상
3 Phase 2 공간 쿼리 pid_topology_builder.py:76 MED 낮음 2-3배 속도 향상
4 Phase 1 BBox 중복 계산 pid_geometric_extractor.py:58 LOW 낮음 10-20% 속도 향상

6. 구체적인 개선 계획

Phase 1: 공간 인덱스 도입 (1-2일)

  1. rtree 패키지 설치

    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

# 현재 코드 (병목)
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

# 현재 코드 (병목)
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