# 🕸️ Graph Pipeline Phase 2: 위상 모델링 (Topology Modeling)

이 문서는 P&ID Graph Pipeline의 두 번째 단계인 **위상 모델링**의 상세 구현 계획을 다룹니다. 1단계에서 추출한 기하학적 객체(좌표, BBox)를 기반으로, 설비 간의 **연결성(Connectivity)**과 **흐름(Flow)**을 정의하는 지식 그래프(Knowledge Graph)를 구축하는 것이 목표입니다.

---

## 🚩 [Supervisor's Audit] 진단 결과 및 개선 권고

**감독자 진단 일자:** 2026-05-02
**진단 결과:** ⚠️ **부분적 보완 필요 (Partial Improvement Required)**

### 🔍 주요 진단 내용
1.  **연결성 추론의 단순성 (Critical):** 현재 `_find_connected_nodes`가 단순 BBox 교차(`intersects`)만 확인하고 있습니다. 실제 P&ID에서 배관(Line)은 설비의 외곽선에 닿거나 매우 근접한 형태로 나타나며, 단순 BBox 교차는 오탐(False Positive) 확률이 매우 높습니다.
2.  **방향성 정의 부재 (Medium):** `DiGraph`를 사용하지만, 실제 엣지에 방향성을 부여하는 구체적인 로직(화살표 인식, 공정 흐름 규칙)이 예시 코드에 누락되어 있습니다.
3.  **임계값 하드코딩 (Low):** `min_dist < 50.0`과 같은 임계값이 하드코딩되어 있어, 도면 스케일(Scale)이 변경될 경우 대응이 불가능합니다.
4.  **데이터 무결성 검증 부족 (Medium):** 그래프 생성 후 고립된 노드(Isolated Nodes)나 비정상적인 루프에 대한 검증 단계가 없습니다.

### 🛠️ 수정 및 반영 사항
- **연결성 로직 고도화:** BBox 교차 방식에서 $\rightarrow$ **Line End-point 기반 근접 분석** 방식으로 변경.
- **방향성 추론 단계 명시:** 화살표 심볼 및 공정 흐름 기반의 `source` $\rightarrow$ `target` 결정 로직 추가.
- **설정의 외부화:** 임계값($\epsilon$)을 설정 파일이나 파라미터로 관리하도록 구조 변경.
- **검증 단계 추가:** 그래프 구축 후 위상 무결성 검사(Topology Validation) 단계 도입.

---

## 📦 1. 필수 패키지 및 환경 설정

### 1.1 Python 패키지
| 패키지 | 용도 | 비고 |
|---|---|---|
| `networkx` | 그래프 데이터 구조 생성 및 알고리즘 분석 | 핵심 라이브러리 |
| `shapely` | 객체 간 거리 계산 및 포함 관계 분석 | 1단계와 연계 |
| `scikit-learn` | (선택) KD-Tree를 이용한 고속 근접 이웃 검색 | 대규모 도면 최적화 |
| `matplotlib` | 생성된 그래프의 위상 구조 시각화 검증 | 디버깅용 |

### 1.2 설치 명령어
```bash
pip install networkx shapely scikit-learn matplotlib
```

---

## 📐 2. 상세 설계 구조

### 2.1 그래프 정의 (Graph Definition)
*   **노드 (Nodes):**
    *   `Equipment`: 펌프, 탱크, 열교환기 등 (속성: ID, 타입, BBox, CenterPoint)
    *   `Instrument`: 전송기, 밸브, 게이지 등 (속성: ID, 타입, BBox, CenterPoint)
    *   `Tag`: 텍스트 기반 태그 (속성: TagName, Value, BBox)
*   **엣지 (Edges):**
    *   `Pipe`: 설비-설비, 설비-계기 간의 물리적 연결 (속성: LineNumber, 방향성, 연결타입)
    *   `Association`: 태그-설비 간의 논리적 연결 (속성: 관계 타입 - 예: 'belongs_to')

### 2.2 위상 추론 로직 (Topology Inference)
1.  **태그-설비 결합 (Tag-to-Entity Binding):**
    *   태그 텍스트의 BBox와 가장 가까운 심볼(Equipment/Instrument)을 찾아 `Association` 엣지를 생성합니다.
2.  **배관 연결성 분석 (Line Connectivity) [개선]:**
    *   `LINE` 또는 `POLYLINE`의 **시작점과 끝점(End-points)**을 추출합니다.
    *   각 끝점이 특정 설비의 BBox 내부에 있거나, 설정된 임계 거리($\epsilon$) 이내에 있을 때만 `Pipe` 엣지로 연결합니다. (단순 BBox 교차 방식 지양)
3.  **흐름 방향성 부여 (Flow Direction) [추가]:**
    *   배관 상의 화살표 심볼 위치와 방향을 분석하여 `source` $\rightarrow$ `target`을 결정합니다.
    *   화살표가 없는 경우, 공정 표준(예: 탱크 $\rightarrow$ 펌프 $\rightarrow$ 밸브)에 따른 기본 방향을 부여합니다.
4.  **위상 무결성 검증 (Topology Validation) [추가]:**
    *   연결되지 않은 고립 노드 탐색 및 리포팅.
    *   비정상적인 사이클(Cycle) 또는 단절 구간 확인.

---

## 💻 3. 실제 구현 코딩 가이드 (Example)

### 3.1 그래프 구축 핵심 코드
```python
import networkx as nx
from shapely.geometry import box, Point, LineString

class PidTopologyBuilder:
    def __init__(self, geometric_data, all_extracted_tags=None, config=None):
        """
        - geometric_data: Phase 1에서 추출된 기하학적 데이터
        - all_extracted_tags: 통합된 태그 리스트
        - config: {'dist_threshold': 50.0, 'tag_threshold': 100.0} 등 설정값
        """
        self.data = geometric_data
        self.all_tags = all_extracted_tags if all_extracted_tags else []
        self.config = config if config else {'dist_threshold': 50.0, 'tag_threshold': 100.0}
        self.G = nx.DiGraph()      # 방향성 그래프 생성

    def build_graph(self):
        # 1. 모든 객체를 노드로 추가
        for item in self.data:
            # Phase 1에서 'clean_value'로 저장했으므로 이를 value로 사용
            self.G.add_node(item['id'],
                           type=item['type'],
                           bbox=box(*item['bbox'].values()),
                           value=item.get('clean_value'))

        # 2. 분산 추출된 태그 통합 및 노드 추가
        for tag in self.all_tags:
            self.G.add_node(tag['id'],
                           type='TEXT',
                           bbox=box(*tag['bbox'].values()),
                           value=tag.get('tagName'))

        # 3. 태그-설비 논리적 연결 (Association)
        tags = [n for n, d in self.G.nodes(data=True) if d['type'] == 'TEXT']
        equipments = [n for n, d in self.G.nodes(data=True) if d['type'] != 'TEXT']

        for tag in tags:
            best_match = self._find_nearest_equipment(tag, equipments)
            if best_match:
                self.G.add_edge(tag, best_match, relation='associated_with')

        # 4. 배관 기반 물리적 연결 (Pipe) [개선됨]
        lines = [n for n, d in self.G.nodes(data=True) if d['type'] in ['LINE', 'POLYLINE']]
        for line_id in lines:
            # Phase 1에서 추출한 coordinates를 사용하여 LineString 생성
            coords = self.G.nodes[line_id].get('coordinates', [])
            if not coords:
                continue
            line_geom = LineString(coords)
            endpoints = [line_geom.coords[0], line_geom.coords[-1]]
            
            connected_nodes = []
            for pt in endpoints:
                p = Point(pt)
                for eq_id in equipments:
                    if self.G.nodes[eq_id]['bbox'].distance(p) < self.config['dist_threshold']:
                        connected_nodes.append(eq_id)
            
            if len(connected_nodes) >= 2:
                # 방향성 추론 로직 (단순화: 시작점 -> 끝점)
                self.G.add_edge(connected_nodes[0], connected_nodes[1], relation='pipe')

    def _find_nearest_equipment(self, tag_id, equipment_ids):
        tag_bbox = self.G.nodes[tag_id]['bbox']
        min_dist = float('inf')
        nearest = None
        for eq_id in equipment_ids:
            eq_bbox = self.G.nodes[eq_id]['bbox']
            dist = tag_bbox.distance(eq_bbox)
            if dist < min_dist:
                min_dist = dist
                nearest = eq_id
        return nearest if min_dist < self.config['tag_threshold'] else None

    def validate_topology(self):
        """위상 무결성 검증"""
        isolated = list(nx.isolates(self.G))
        return {"isolated_nodes": isolated, "node_count": self.G.number_of_nodes(), "edge_count": self.G.number_of_edges()}

# 실행 예시
all_tags = flatten_results([worker1_res, worker2_res])
config = {'dist_threshold': 30.0, 'tag_threshold': 80.0}
builder = PidTopologyBuilder(geometric_data, all_extracted_tags=all_tags, config=config)
builder.build_graph()
validation_res = builder.validate_topology()
print(f"Validation Result: {validation_res}")
```

### 3.2 위상 분석 유틸리티: 영향도 분석 (Impact Analysis)
```python
def analyze_impact(graph, start_node):
    """특정 설비 장애 시 하류(Downstream)에 영향을 받는 모든 노드 추출"""
    # BFS를 통해 도달 가능한 모든 노드 탐색
    impacted_nodes = nx.descendants(graph, start_node)
    return list(impacted_nodes)

# 예: P-101 펌프 고장 시 영향 분석
affected = analyze_impact(graph, "node_P101")
print(f"Impacted Equipment: {affected}")
```

---

## 🚀 4. Phase 2 완료 기준 (Definition of Done)

- [ ] 모든 설비와 계기가 그래프의 **노드(Node)**로 변환되었는가?
- [ ] 분산 추출된 태그 리스트가 `flatten_results`를 통해 통합되어 그래프에 반영되었는가?
- [ ] 태그와 설비 간의 **논리적 연결(Association)**이 정확하게 매핑되었는가?
- [ ] 배관(Line)의 **끝점 분석**을 통해 설비 간의 **물리적 연결(Pipe Edge)**이 생성되었는가? (BBox 교차 방식 배제)
- [ ] 화살표 및 공정 규칙에 기반한 **방향성(Directionality)**이 엣지에 부여되었는가?
- [ ] `validate_topology`를 통해 고립 노드 및 위상 오류가 검토되었는가?
- [ ] `nx.descendants` 등을 통해 특정 노드로부터의 **흐름 추적(Flow Tracing)**이 가능한가?
- [ ] 생성된 그래프 구조가 JSON(GraphML 등) 형태로 저장되어 Phase 3로 전달 가능한가?
