# P&ID 그래프 병목 진단 및 수정 방안

> 작성일: 2026-05-04  
> 대상 파일: `futurePlan/End-to-End P&ID Graph Pipeline/No-10_Plant_PID.dxf`  
> 상태: **진단 완료, 수정 계획 수립**

---

## 1. DXF 파일 특성

| 항목 | 값 |
|------|-----|
| 파일 크기 | 1,174,227 라인 (약 1.1MB) |
| 총 엔티티 | 28,819개 |
| 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%) |
| 기타 | 102개 (0.4%) |

---

## 2. 확인된 병목 지점

### 🔴 병목 1: Phase 2 - `build_graph()` 내 배관-설비 연결 로직 (HIGH)

**위치**: [`mcp-server/pipeline/topology.py:81-129`](mcp-server/pipeline/topology.py:81)

**문제**: LINE/LWPOLYLINE(약 21,733개)과 설비(CIRCLE/ARC/기타, 약 3,272개)를 **전체 조합으로 비교**

```python
# topology.py:82-99
lines = [n for n, d in self.G.nodes(data=True) if d['type'] in ['LINE', 'LWPOLYLINE']]
for line_id in lines:  # 21,733개
    ...
    for eq_id in equipments:  # 3,272개
        eq_bbox = self.G.nodes[eq_id]['bbox']
        if line_geom.intersects(eq_bbox):      # shapely 교차 계산 (고비용)
            connected_nodes.append(eq_id)
        elif line_geom.distance(eq_bbox) < self.config['dist_threshold']:  # shapely 거리 계산 (고비용)
            connected_nodes.append(eq_id)
```

**연산량**: 21,733 × 3,272 = **약 7,110만 회**의 shapely 기하학 연산  
**실제 성능**: **10분 이상** (타임아웃 발생)

---

### 🟠 병목 2: Phase 2 - `_find_nearest_equipment()` 태그-설비 매핑 (MED)

**위치**: [`mcp-server/pipeline/topology.py:131-163`](mcp-server/pipeline/topology.py:131)

**문제**: TEXT/MTEXT(약 3,925개)와 설비(약 3,272개)를 전체 비교

```python
# topology.py:143-148
for eq_id in equipment_ids:  # 3,272개
    eq_bbox = self.G.nodes[eq_id]['bbox']
    dist = tag_bbox.distance(eq_bbox)  # shapely 거리 계산
    if dist > self.config['tag_threshold']:
        continue
```

**연산량**: 3,925 × 3,272 = **약 1,280만 회**의 거리 계산  
**실제 성능**: 수초 ~ 수십초 (설비 수에 따라 변동)

---

### 🟡 병목 3: Phase 1 - DXF 엔티티 순회 추출 (LOW)

**위치**: [`mcp-server/pipeline/extractor.py:124-162`](mcp-server/pipeline/extractor.py:124)

**문제**: 28,819개 엔티티를 하나씩 순회하며 BBox 계산

```python
# extractor.py:124-162
for entity in self.msp:  # 28,819개
    bbox_obj = self.get_bbox(entity)  # 각 엔티티당 BBox 계산
    ...
```

**실제 성능**: ~1.4초 (허용 가능)

---

## 3. 근본 원인

**공간 인덱스(R-tree, Quadtree) 미사용**으로 모든 노드 쌍을 O(n²)으로 비교함.

shapely의 `intersects()`, `distance()`는 정확한 기하학 연산이지만 계산 비용이 큼.  
불필요하게 먼 객체 쌍에도 이 연산을 수행하는 것이 핵심 문제.

---

## 4. 수정 계획

### 전략: 3단계 점진적 개선

| 단계 | 내용 | 예상 효과 | 작업량 |
|------|------|-----------|--------|
| Step 1 | BBox 빠른 필터링 추가 | 90% 이상 연산 축소 | 소 |
| Step 2 | 그리드 기반 공간 인덱스 | O(n²) → O(n log n) | 중 |
| Step 3 | pid_worker.py 통합 및 테스트 | E2E 검증 | 소 |

---

## 5. 상세 TODO List

> 각 단계는 독립적으로 커밋 가능. 중간에 끊겨도 다음 작업자가 이 문서로 이어갈 수 있음.

### Step 1: BBox 빠른 필터링 추가 (우선순위: HIGH)

- [ ] **1-1**. `topology.py`에 `_bbox_quick_filter()` 함수 추가
  - shapely 없이 `min_x/max_x/min_y/max_y`로 간단한 거리 계산
  - 임계값보다 먼 객체는 즉시 제외
  - **참고**: [`mcp-server/pipeline/extractor.py:176-183`](mcp-server/pipeline/extractor.py:176)의 `is_near()` 함수가 이미 shapely box 사용 중 → 이 함수도 함께 수정 필요

- [ ] **1-2**. `build_graph()` 내 배관-설비 연결 루프 수정 (topology.py:81-129)
  - 기존: `for eq_id in equipments:` → 전체 순회
  - 수정: `_bbox_quick_filter()`로 후보 집합 축소 후 순회
  - 예상: 7,110만 회 → 약 100만 회로 축소

- [ ] **1-3**. `_find_nearest_equipment()` 수정 (topology.py:131-163)
  - 동일하게 BBox 빠른 필터링 적용
  - 예상: 1,280만 회 → 약 50만 회로 축소

- [ ] **1-4**. 성능 테스트 실행
  - `test_pid_bottleneck_analysis.py`로 Phase 2 시간 측정
  - 목표: 10분 → 30초 이내

- [ ] **1-5**. 커밋: `fix(#bottleneck): topology.py BBox 빠른 필터링 추가`

---

### Step 2: 그리드 기반 공간 인덱스 (우선순위: MED)

- [ ] **2-1**. `topology.py`에 `SpatialGrid` 클래스 추가
  - 그리드 셀 크기 = `dist_threshold`
  - 각 엔티티를 중심 좌표로 그리드 셀에 할당
  - 인접 셀(3×3)만 검색 대상

- [ ] **2-2**. `build_graph()`에서 `SpatialGrid` 사용
  - 배관-설비 연결 시 그리드 기반 후보 검색
  - 예상: 100만 회 → 약 10만 회로 추가 축소

- [ ] **2-3**. `_find_nearest_equipment()`에서 `SpatialGrid` 사용
  - 태그-설비 매핑 시 그리드 기반 후보 검색

- [ ] **2-4**. 성능 테스트
  - 목표: 30초 → 2초 이내

- [ ] **2-5**. 커밋: `perf(#bottleneck): topology.py 그리드 기반 공간 인덱스 도입`

---

### Step 3: pid_worker.py 통합 및 E2E 테스트 (우선순위: LOW)

- [ ] **3-1**. `pid_worker.py`의 `build_pid_graph_parallel` 도구 확인
  - [`mcp-server/worker/pid_worker.py:201-507`](mcp-server/worker/pid_worker.py:201) 읽기
  - 최적화된 `topology.py`가 정상적으로 호출되는지 확인

- [ ] **3-2**. MCP 서버 재시작 후 E2E 테스트
  - `parse_pid_drawing` 도구로 No-10_Plant_PID.dxf 파싱
  - 전체 파이프라인 완료 시간 측정

- [ ] **3-3**. 결과 검증
  - 생성된 그래프 JSON 확인
  - 노드/엣지 수 비교 (기존 결과와 일치하는지)

- [ ] **3-4**. 커밋: `test(#bottleneck): E2E 테스트 및 성능 검증`

---

## 6. 참고: 관련 파일 목록

| 파일 | 역할 | 수정 대상 |
|------|------|-----------|
| [`mcp-server/pipeline/topology.py`](mcp-server/pipeline/topology.py:1) | 위상 빌더 | **주요 수정 대상** |
| [`mcp-server/pipeline/extractor.py`](mcp-server/pipeline/extractor.py:1) | 기하학적 추출 | `is_near()` 함수 수정 |
| [`mcp-server/pipeline/mapper.py`](mcp-server/pipeline/mapper.py:1) | 지능형 매핑 | 변경 없음 |
| [`mcp-server/pipeline/analyzer.py`](mcp-server/pipeline/analyzer.py:1) | 영향도 분석 | 변경 없음 |
| [`mcp-server/worker/pid_worker.py`](mcp-server/worker/pid_worker.py:1) | P&ID 워커 | Step 3 통합 테스트 |
| [`test_pid_bottleneck_analysis.py`](test_pid_bottleneck_analysis.py:1) | 성능 테스트 | Step 1-2 검증용 |

---

## 7. 진행 상태

| 단계 | 상태 | 비고 |
|------|------|------|
| 진단 | ✅ 완료 | 2026-05-04 |
| Step 1: BBox 빠른 필터링 | ✅ 완료 | SpatialGrid로 통합 |
| Step 2: 그리드 기반 공간 인덱스 | ✅ 완료 | SpatialGrid 클래스 추가 |
| Step 3: E2E 테스트 | ✅ 완료 | 성능 검증 완료 |
| 추가: build_graph() 중복 제거 | ✅ 완료 | pid_worker.py 수정 |

---

## 8. 복귀 가이드

작업을 끊었다가 다시 시작할 때:

1. 이 문서의 **"진행 상태"** 테이블 확인
2. 가장 최근 완료된 Step 다음 TODO부터 시작
3. 각 Step의 마지막 항목이 "커밋"이므로, git log로 실제 진행 확인
4. 성능 테스트는 `test_pid_bottleneck_analysis.py` 실행
