deepkick commited on
Commit
59fa244
·
0 Parent(s):

Initial commit: PID2Graph × Claude VLM evaluation + Gradio demo

Browse files

Zero-shot P&ID graph extraction pipeline (semantic equipment + pipe
connectivity) built on Claude Opus 4.6 vision, benchmarked against the
PID2Graph OPEN100 subset, and wrapped in a Gradio demo with three
preset samples.

- pid2graph_eval/: Pydantic schema, graphml loader + semantic collapse,
structured-outputs VLM extractor (temperature=0), bbox-distance tiled
merge with seam-artifact filter, node/edge P/R/F1 metrics, CLI.
- app.py: Gradio Blocks demo — preset picker, tile toggle, NetworkX
bbox-based visualization of pred vs GT, per-run metrics table. Ships
with a workaround for the gradio_client 1.3.0 bool-schema bug.
- samples/: 3 OPEN100 diagrams (small/medium/large) with ground truth.

.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ .ipynb_checkpoints/
7
+
8
+ # Virtual environments
9
+ .venv/
10
+ venv/
11
+ env/
12
+
13
+ # Secrets
14
+ .env
15
+ .env.*
16
+ !.env.example
17
+
18
+ # PID2Graph dataset (9.3 GB — pulled from Zenodo, not committed)
19
+ PID2Graph.zip
20
+ PID2Graph.zip*.tmp
21
+ PID2Graph/
22
+ md5sums.txt
23
+ urls.txt
24
+ nohup.out
25
+
26
+ # Evaluation artifacts (reproducible; keep locally, don't commit)
27
+ results.json
28
+ results_*.json
29
+
30
+ # OS / editor
31
+ .DS_Store
32
+ Thumbs.db
33
+ .vscode/
34
+ .idea/
35
+ *.swp
README.md ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PID2Graph × VLM Eval
2
+
3
+ P&ID(配管計装図)画像から VLM(Claude Opus 4.6)でグラフ構造(シンボル+接続関係)を抽出し、[PID2Graph](https://zenodo.org/records/14803338) データセットの正解と定量比較する実験。
4
+
5
+ 製造業の技術図面に対する VLM のゼロショット構造抽出性能を評価し、CV との**ハイブリッド戦略**の必要性を実データで検証した。
6
+
7
+ ## 背景と目的
8
+
9
+ P&ID のデジタル化(画像 → 構造化データ)は、設備管理 DB への自動登録やシミュレーションモデルの自動生成に不可欠な技術である。従来は CNN ベースのシンボル検出+線検出+ルールベース接続推定というモジュラーパイプラインが主流だったが、顧客ごとに異なる図面規格への対応コストが課題だった。
10
+
11
+ 本実験では、VLM のゼロショット能力を活用して **訓練データなし** でどこまで構造抽出が可能かを定量的に検証する。
12
+
13
+ ## データセット
14
+
15
+ [PID2Graph](https://zenodo.org/records/14803338)(Stürmer et al., 2025)の **OPEN100** サブセットを使用。
16
+
17
+ - 12 枚の実 P&ID 画像(原子力プロジェクト OPEN100)
18
+ - 正解は `.graphml` 形式(ノード: シンボル種別 + bounding box、エッジ: 接続種別)
19
+ - 画像サイズ: 約 600×500 〜 2604×1744 ピクセル
20
+
21
+ ## 手法
22
+
23
+ ### Semantic Equipment フィルタリング
24
+
25
+ 正解データには connector(配管接続点)225 個、crossing(交差)71 個など、線レベルのプリミティブが大量に含まれる。VLM のゼロショットでこれらを網羅的に検出するのは非現実的なため、**意味のある機器シンボルのみ** を抽出・評価対象とした。
26
+
27
+ | 対象(semantic) | 除外(primitive) |
28
+ |---|---|
29
+ | valve, instrumentation, tank, pump, inlet/outlet | connector, crossing, arrow, background, general |
30
+
31
+ GT 側も同じフィルタを適用し、primitive を経由するエッジは直接エッジに縮約(collapse)してフェアな比較を行った。
32
+
33
+ ### 構造化出力
34
+
35
+ Claude API の `messages.parse()` に Pydantic スキーマ(`GraphOut`)を渡し、JSON スキーマ違反を SDK 側で検出。`temperature=0` で決定論的サンプリングを強制。
36
+
37
+ ```json
38
+ {
39
+ "nodes": [
40
+ {"id": "n1", "type": "valve", "label": "TCV 1401", "bbox": [578, 930, 586, 944]}
41
+ ],
42
+ "edges": [
43
+ {"source": "n1", "target": "n2", "label": "solid"}
44
+ ]
45
+ }
46
+ ```
47
+
48
+ ### タイル分割(2×2)
49
+
50
+ 大きな図面を 4 タイルに分割(10-20% オーバーラップ)し、各タイルごとに VLM で抽出した後、bbox 距離ベースで重複排除+マージ。
51
+
52
+ **Seam Filter:** タイル境界付近で配管の切れ端を inlet/outlet と誤検出する問題に対し、タイル内側境界 ±50px 以内かつ図面外縁でない inlet/outlet を FP として除去する後処理を追加。
53
+
54
+ ## 実験結果
55
+
56
+ ### 12 枚全件ベースライン
57
+
58
+ | 指標 | Precision | Recall | F1 | TP | FP | FN |
59
+ |------|-----------|--------|----|----|----|-----|
60
+ | **Nodes(single-shot)** | 0.941 | 0.699 | 0.802 | 527 | 33 | 227 |
61
+ | **Nodes(tiled + seam filter)** | 0.828 | 0.948 | 0.884 | 715 | 149 | 39 |
62
+ | Edges(single-shot) | 0.086 | 0.018 | 0.030 | 38 | 402 | 2033 |
63
+
64
+ ### Single-shot vs Tiled(サイズ別比較)
65
+
66
+ | 図面 | GT nodes | Single F1 | Tiled F1 | ΔF1 | Tiled Precision | Tiled Recall |
67
+ |------|----------|-----------|----------|------|-----------------|--------------|
68
+ | **Small (#1, GT=27)** | 27 | 0.739 | 0.783 | +0.044 | 0.643 | 1.000 |
69
+ | **Medium (#3, GT=53)** | 53 | 0.796 | 0.955 | +0.159 | 0.914 | 1.000 |
70
+ | **Large (#0, GT=82)** | 82 | 0.706 | **0.964** | **+0.258** | 0.952 | 0.976 |
71
+
72
+ ### カテゴリ別 Recall(12 枚集計、single-shot)
73
+
74
+ | type | GT | pred | Recall |
75
+ |---|---|---|---|
76
+ | instrumentation | 350 | 267 | 0.76 |
77
+ | valve | 257 | 128 | **0.50** ← 最大の失点 |
78
+ | inlet/outlet | 96 | 100 | 1.04 |
79
+ | tank | 35 | 43 | 1.23 |
80
+ | pump | 16 | 22 | 1.38 |
81
+
82
+ ## 考察
83
+
84
+ ### 1. VLM は「意味理解に強く、空間的な網羅性に弱い」
85
+
86
+ - **Precision 0.94:** VLM が検出したシンボルの種別判定はほぼ正確。「見えているものを正しく分類する」能力は非常に高い
87
+ - **Recall 0.70(single-shot):** 正解の 30% を見落とす。特に valve(Recall 0.50)は小さなシンボルが画像リサイズで潰れるため
88
+
89
+ これは理解力の問題ではなく **解像度の限界** である。
90
+
91
+ ### 2. タイル分割で解像度問題を解決
92
+
93
+ 図面が大きいほどタイル分割の効果が顕著:
94
+
95
+ - Small(GT=27): ΔF1 = +0.044(効果薄、FP 増加コスト)
96
+ - Medium(GT=53): ΔF1 = +0.159(Precision 維持しつつ Recall 1.000)
97
+ - **Large(GT=82): ΔF1 = +0.258**(Precision も 0.889→0.952 に向上)
98
+
99
+ Large では 1-shot 時に曖昧な小要素を「とりあえず出す」��検出が、タイル分割で高解像度になることで消える。**実用的には図面サイズに応じた適応的モード切替が最適。**
100
+
101
+ ### 3. エッジ抽出は VLM 単体では実用にならない
102
+
103
+ Edge F1 ≈ 0.03 は全条件で共通。VLM は配管の線形状を追跡しておらず、テキストタグの類似性から接続を推測している。これは **CV 的な線追跡ロジックとの組み合わせ** でしか解決できない。
104
+
105
+ ### 4. BPMN 構造抽出論文(Deka & Devereux, 2026)との比較
106
+
107
+ 同種のタスク(図面画像→構造化 JSON)で VLM を評価した SAC 2026 採択論文との対比:
108
+
109
+ | 観点 | BPMN 論文 | 本実験(P&ID) |
110
+ |---|---|---|
111
+ | ノード数/図面 | 数十個 | 数百個(1 桁多い) |
112
+ | Relation 抽出 | GPT-4.1 で F1 0.475 | Edge F1 ≈ 0.03 |
113
+ | OCR 補完効果 | 中位モデルで有効、上位モデルには逆効果 | 未検証(今後の課題) |
114
+ | 最有効プロンプト | DFS+BFS Hybrid | 今後検証予定 |
115
+
116
+ P&ID は BPMN より要素数が 1 桁多く、シンボル種類も工業規格(ISO 10628)に依存するため、難度が大幅に高い。
117
+
118
+ ## 今後の課題:エッジ(接続関係)抽出の改善
119
+
120
+ 今回の実験ではノード検出で F1 0.964 を達成したが、エッジ抽出は F1 ≈ 0.03 で実用にならなかった。接続関係の抽出は P&ID デジタル化の核心であり、最優先の研究課題である。
121
+
122
+ ### アプローチ 1: モジュラーパイプライン(CV 主導)
123
+
124
+ 従来の P&ID デジタル化で主流のアプローチ。
125
+
126
+ - **線検出:** 画像の二値化 → スケルトン化 → Hough 変換で配管ラインを検出(Moon et al., 2021)
127
+ - **接続推定:** 検出済みシンボルの bbox と線分端点の近接性からグラフ探索で接続を復元
128
+ - **課題:** モジュール間のエラー伝播。線の交差・分岐での追跡失敗が接続推定精度を大きく下げる
129
+
130
+ 参考実装: [Azure P&ID Digitization](https://github.com/Azure-Samples/digitization-of-piping-and-instrument-diagrams)(Microsoft ISE, 2024)
131
+
132
+ ### アプローチ 2: End-to-End Transformer(Relationformer)
133
+
134
+ PID2Graph 論文(Stürmer et al., 2025)が提案。シンボルと接続関係を同時に抽出する。
135
+
136
+ - Node AP 83.63%, Edge mAP 75.46%(モジュラー方式に対してエッジ検出で 25%以上の改善)
137
+ - 接続関係を直接推論するため、線検出のエラー伝播がない
138
+ - **課題:** 専用モデルの訓練が必要。新しい図面規格への汎化にはドメイン特化のファインチューニングが不可欠
139
+
140
+ ### アプローチ 3: ハイブリッド(CV + VLM)← 提案する方向性
141
+
142
+ 今回の実験結果と先行研究を踏まえた、最も有望なアプローチ。
143
+
144
+ ```
145
+ [P&ID 画像]
146
+
147
+ ├─ Stage 1: ノード検出(CV or VLM タイル分割)
148
+ │ → 今回の手法で Precision 0.95, Recall 0.95 を達成済み
149
+
150
+ ├─ Stage 2: 線検出・追跡(CV)
151
+ │ → スケルトン化 + Hough 変換で配管ラインを抽出
152
+ │ → 検出済みノードの bbox を除去した画像に対して実行
153
+
154
+ ├─ Stage 3: 接続関係の推論(VLM)
155
+ │ → 確定済みノード一覧 + 線検出結果 + 元画像を VLM に渡す
156
+ │ → "これらのノード間の接続関係を列挙せよ" と推論
157
+ │ → VLM を「審判(Judge)」として活用(Ghosh et al., 2025)
158
+
159
+ └─ Stage 4: 後処理・検証
160
+ → グラフ整合性チェック(孤立ノード除去、双方向性確認)
161
+ → ドメイン知識による制約(バルブは必ず 2 本の配管に接続、等)
162
+ ```
163
+
164
+ **根拠となる先行研究:**
165
+
166
+ - **VLM as a Judge**(Ghosh et al., 2025): VLM を直接検出に使わず、CV の結果を VLM が評価・補完するフレームワーク。Gemini 2.5 で F1 0.876
167
+ - **Arrow-Guided VLM**(Spies et al., 2025): CV 検出器で矢印方向を特定してから VLM に座標付きプロンプトで渡す。QA 精度 +9pp
168
+ - **TextFlow**(Shukla et al., 2024): VLM の出力を構造化テキスト(Graphviz/Mermaid)に変換し、LLM で推論する 2 段階分離
169
+ - **Florence-2 ファインチューニング**(Khan et al., 2024): 0.23B パラメータの軽量 VLM で、GPT-4o の zero-shot 比 F1 +52.4%。ドメイン特化ファインチューニングの有効性を実証
170
+
171
+ ### その他の改善課題
172
+
173
+ | 課題 | 期待効果 | 優先度 |
174
+ |------|---------|--------|
175
+ | IoU ベース dedup(タイルマージ時) | tank/pump の過検出を解消、Precision 向上 | 高 |
176
+ | 適応的タイル切替(GT サイズ推定→閾値判定) | 小図面での FP 削減 | 中 |
177
+ | DFS+BFS Hybrid プロンプト | BPMN 論文で最有効。P&ID での検証が必要 | 中 |
178
+ | Sonnet 4.6 との比較 | コスト 1/3 で精度差を測定 | 低 |
179
+
180
+ ## デモ
181
+
182
+ Gradio ベースのインタ��クティブデモ。P&ID 画像をアップロード → VLM でグラフ構造を抽出 → 正解 graphml と比較 → スコア表示 + NetworkX でグラフ可視化。
183
+
184
+ ### 起動方法
185
+
186
+ ```bash
187
+ git clone https://github.com/deepkick/pid2graph-vlm-eval.git
188
+ cd pid2graph-vlm-eval
189
+ python3 -m venv .venv
190
+ source .venv/bin/activate
191
+ pip install -r requirements.txt
192
+
193
+ # .env に API キーを設定
194
+ echo "ANTHROPIC_API_KEY=sk-ant-..." > .env
195
+
196
+ # Gradio デモ起動
197
+ python app.py
198
+ # → http://127.0.0.1:7860
199
+ ```
200
+
201
+ ### CLI での評価実行
202
+
203
+ ```bash
204
+ # 12 枚全件(single-shot)
205
+ python -m pid2graph_eval.cli \
206
+ --image-dir "PID2Graph/Complete/PID2Graph OPEN100" \
207
+ --gt-dir "PID2Graph/Complete/PID2Graph OPEN100" \
208
+ --semantic-only --output results.json
209
+
210
+ # タイル分割モード
211
+ python -m pid2graph_eval.cli \
212
+ --image-dir "PID2Graph/Complete/PID2Graph OPEN100" \
213
+ --gt-dir "PID2Graph/Complete/PID2Graph OPEN100" \
214
+ --semantic-only --tiled --output results_tiled.json
215
+ ```
216
+
217
+ ## プロジェクト構成
218
+
219
+ ```
220
+ pid2graph-vlm-eval/
221
+ ├── app.py # Gradio デモ UI
222
+ ├── requirements.txt
223
+ ├── .env # API キー(.gitignore 対象)
224
+ ├── pid2graph_eval/
225
+ │ ├── schema.py # Pydantic モデル(GraphOut)
226
+ │ ├── gt_loader.py # graphml → 正規化ノード/エッジ
227
+ │ ├── extractor.py # Claude API 呼び出し(single / tiled)
228
+ │ ├── metrics.py # P/R/F1 算出(ノード / エッジ)
229
+ │ ├── tile.py # タイル分割・マージ・seam filter
230
+ │ └── cli.py # CLI エントリポイント
231
+ ├── samples/ # OPEN100 から 3 枚(small/medium/large)
232
+ │ ├── open100_01_small.png
233
+ │ ├── open100_01_small.graphml
234
+ │ ├── open100_03_medium.png
235
+ │ ├── open100_03_medium.graphml
236
+ │ ├── open100_00_large.png
237
+ │ └── open100_00_large.graphml
238
+ └── results/ # 評価結果 JSON
239
+ ```
240
+
241
+ ## 使用技術
242
+
243
+ - **VLM:** Claude Opus 4.6(Anthropic API、構造化出力)
244
+ - **評価データ:** PID2Graph OPEN100(CC BY-SA 4.0)
245
+ - **ライブラリ:** NetworkX, Pydantic, Pillow, tqdm, Gradio, matplotlib
246
+ - **開発環境:** Claude Code + VS Code(Dev Container)
247
+
248
+ ## 参考文献
249
+
250
+ | 論文 | 手法 | URL |
251
+ |------|------|-----|
252
+ | Stürmer et al., 2025 — PID2Graph | Relationformer(End-to-End) | [arXiv:2411.13929](https://arxiv.org/abs/2411.13929) |
253
+ | Ghosh et al., 2025 — VLM as a Judge | CV 検出 + VLM 品質評価 | [arXiv:2510.03376](https://arxiv.org/abs/2510.03376) |
254
+ | Shteriyanov et al., 2025 | OCR + VLM + マルチモーダルプロンプト | [Wiley](https://onlinelibrary.wiley.com/doi/abs/10.1002/smr.70072) |
255
+ | Deka & Devereux, 2026 — BPMN-VLM | VLM + OCR 補完 | [arXiv:2511.22448](https://arxiv.org/abs/2511.22448) |
256
+ | Spies et al., 2025 — Arrow-Guided VLM | 物体検出 + VLM 推論 | [arXiv:2505.07864](https://arxiv.org/abs/2505.07864) |
257
+ | Khan et al., 2024 | Florence-2 ファインチューニング | [arXiv:2411.03707](https://arxiv.org/abs/2411.03707) |
258
+ | Shukla et al., 2024 — TextFlow | VLM → 構造化テキスト → LLM 推論 | [arXiv:2412.16420](https://arxiv.org/abs/2412.16420) |
259
+ | Moon et al., 2021 | スケルトン化 + Hough 変換 | [MDPI](https://www.mdpi.com/2076-3417/11/21/10054) |
260
+ | Goldstein et al., 2025 | P&ID → Neo4j 知識グラフ → RAG | [arXiv:2502.18928](https://arxiv.org/abs/2502.18928) |
261
+
262
+ ## License
263
+
264
+ MIT
app.py ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio demo: P&ID graph extraction with Claude VLM + evaluation.
2
+
3
+ Usage (local):
4
+ ANTHROPIC_API_KEY=sk-ant-... python app.py
5
+
6
+ Or put the key in a `.env` next to this file. The app:
7
+ 1. Takes a P&ID image (preset or upload)
8
+ 2. Runs extraction (optionally tiled 2x2) via Claude Opus 4.6
9
+ 3. If a ground-truth graphml is provided, collapses it to semantic-only
10
+ form and computes node/edge P/R/F1 via `pid2graph_eval.metrics`
11
+ 4. Draws both the prediction and the ground truth as NetworkX graphs
12
+ using bbox-based layouts so the topology matches the source image
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import time
20
+ from pathlib import Path
21
+ from typing import Optional
22
+
23
+ # Matplotlib backend must be set before pyplot import for headless use;
24
+ # the `matplotlib.use()` call below taints every subsequent import with
25
+ # E402 ("module level import not at top of file"), which is expected here.
26
+ import matplotlib
27
+
28
+ matplotlib.use("Agg")
29
+ import matplotlib.patches as mpatches # noqa: E402
30
+ import matplotlib.pyplot as plt # noqa: E402
31
+ import networkx as nx # noqa: E402
32
+
33
+ import anthropic # noqa: E402
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Gradio 4.44 / gradio_client 1.3.0 bug workaround
37
+ # ---------------------------------------------------------------------------
38
+ # At `/info` boot, Gradio walks every component's JSON schema via
39
+ # `gradio_client.utils._json_schema_to_python_type`. That function does not
40
+ # handle bool schemas (`additionalProperties: false` or `true`, both of which
41
+ # are valid JSON Schema) — it recurses with the bool and then `if "const" in
42
+ # schema:` on line 863 raises `TypeError: argument of type 'bool' is not
43
+ # iterable`. Patch the function here before importing gradio so the crash is
44
+ # avoided regardless of which component triggers it. (Fixed upstream in later
45
+ # gradio_client releases; we stay on 4.44 because Python 3.9 can't run
46
+ # gradio 5.) Harmless on versions where the bug is already fixed.
47
+ import gradio_client.utils as _gc_utils # noqa: E402
48
+
49
+ _orig_json_schema_to_python_type = _gc_utils._json_schema_to_python_type
50
+
51
+
52
+ def _patched_json_schema_to_python_type(schema, defs=None): # type: ignore[override]
53
+ if isinstance(schema, bool):
54
+ # `True` means "any value is allowed"; `False` means "no value".
55
+ return "Any" if schema else "None"
56
+ return _orig_json_schema_to_python_type(schema, defs)
57
+
58
+
59
+ _gc_utils._json_schema_to_python_type = _patched_json_schema_to_python_type
60
+
61
+ import gradio as gr # noqa: E402 (must come after the monkey-patch)
62
+ from dotenv import load_dotenv # noqa: E402
63
+
64
+ from pid2graph_eval.extractor import ( # noqa: E402
65
+ DEFAULT_MODEL,
66
+ extract_graph,
67
+ extract_graph_tiled,
68
+ )
69
+ from pid2graph_eval.gt_loader import ( # noqa: E402
70
+ SEMANTIC_EQUIPMENT_TYPES,
71
+ collapse_through_primitives,
72
+ filter_by_types,
73
+ load_graphml,
74
+ )
75
+ from pid2graph_eval.metrics import evaluate # noqa: E402
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Config
79
+ # ---------------------------------------------------------------------------
80
+
81
+ APP_ROOT = Path(__file__).parent
82
+ SAMPLES_DIR = APP_ROOT / "samples"
83
+
84
+ load_dotenv(APP_ROOT / ".env")
85
+
86
+ # Presets: (display name) -> (image path, graphml path)
87
+ PRESETS: dict[str, tuple[Path, Path]] = {
88
+ "OPEN100 #1 — small (27 semantic nodes)": (
89
+ SAMPLES_DIR / "open100_01_small.png",
90
+ SAMPLES_DIR / "open100_01_small.graphml",
91
+ ),
92
+ "OPEN100 #3 — medium (53 semantic nodes)": (
93
+ SAMPLES_DIR / "open100_03_medium.png",
94
+ SAMPLES_DIR / "open100_03_medium.graphml",
95
+ ),
96
+ "OPEN100 #0 — large (82 semantic nodes)": (
97
+ SAMPLES_DIR / "open100_00_large.png",
98
+ SAMPLES_DIR / "open100_00_large.graphml",
99
+ ),
100
+ }
101
+
102
+ NONE_LABEL = "(none — upload your own)"
103
+
104
+ # Fixed palette so pred/GT visualizations use matching colors.
105
+ TYPE_COLORS: dict[str, str] = {
106
+ "valve": "#ff6b6b",
107
+ "pump": "#4ecdc4",
108
+ "tank": "#ffd93d",
109
+ "instrumentation": "#6bcfff",
110
+ "inlet/outlet": "#c47bff",
111
+ }
112
+
113
+ LEGEND_HANDLES = [
114
+ mpatches.Patch(color=c, label=t) for t, c in TYPE_COLORS.items()
115
+ ]
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Visualization
120
+ # ---------------------------------------------------------------------------
121
+
122
+ def _bbox_to_xyxy(bbox) -> Optional[tuple[float, float, float, float]]:
123
+ """Normalize a bbox to `(xmin, ymin, xmax, ymax)` floats.
124
+
125
+ Accepts both shapes that flow through the pipeline:
126
+
127
+ * **list / tuple** `[x1, y1, x2, y2]` — produced by `gt_loader._bbox`
128
+ and by `tile.merge_tile_graphs` for the tiled pred path.
129
+ * **dict** `{"xmin": ..., "ymin": ..., "xmax": ..., "ymax": ...}` —
130
+ produced by `GraphOut.to_dict()` in single-shot mode, because the
131
+ Pydantic `BBox` model round-trips through `model_dump()`.
132
+
133
+ Returns `None` if the bbox is missing or malformed.
134
+ """
135
+ if bbox is None:
136
+ return None
137
+ if isinstance(bbox, dict):
138
+ try:
139
+ return (
140
+ float(bbox["xmin"]),
141
+ float(bbox["ymin"]),
142
+ float(bbox["xmax"]),
143
+ float(bbox["ymax"]),
144
+ )
145
+ except (KeyError, TypeError, ValueError):
146
+ return None
147
+ if isinstance(bbox, (list, tuple)) and len(bbox) == 4:
148
+ try:
149
+ return (
150
+ float(bbox[0]),
151
+ float(bbox[1]),
152
+ float(bbox[2]),
153
+ float(bbox[3]),
154
+ )
155
+ except (TypeError, ValueError):
156
+ return None
157
+ return None
158
+
159
+
160
+ def draw_graph(graph_dict: dict, title: str, figsize=(8, 6)) -> plt.Figure:
161
+ """Render a graph as a matplotlib figure.
162
+
163
+ Node positions come from bbox centers when available (so the drawing
164
+ preserves the spatial layout of the original P&ID); nodes without a
165
+ bbox fall back to networkx spring layout.
166
+ """
167
+ fig, ax = plt.subplots(figsize=figsize, dpi=110)
168
+
169
+ G = nx.Graph()
170
+ pos: dict[str, tuple[float, float]] = {}
171
+ colors: list[str] = []
172
+ node_list: list[str] = []
173
+
174
+ for n in graph_dict.get("nodes", []):
175
+ nid = n["id"]
176
+ G.add_node(nid)
177
+ node_list.append(nid)
178
+ colors.append(TYPE_COLORS.get(n.get("type", ""), "#cccccc"))
179
+
180
+ coords = _bbox_to_xyxy(n.get("bbox"))
181
+ if coords is not None:
182
+ x1, y1, x2, y2 = coords
183
+ cx = (x1 + x2) / 2.0
184
+ cy = (y1 + y2) / 2.0
185
+ pos[nid] = (cx, -cy) # flip y so the image is right-side up
186
+
187
+ for e in graph_dict.get("edges", []):
188
+ s, t = e.get("source"), e.get("target")
189
+ if s in G.nodes and t in G.nodes:
190
+ G.add_edge(s, t)
191
+
192
+ # Fall back to spring layout for any nodes that lack a bbox.
193
+ missing = [nid for nid in G.nodes if nid not in pos]
194
+ if missing:
195
+ if not pos:
196
+ pos = nx.spring_layout(G, seed=42)
197
+ else:
198
+ # Place missing nodes near the existing bbox cloud center.
199
+ xs = [p[0] for p in pos.values()]
200
+ ys = [p[1] for p in pos.values()]
201
+ cx0 = sum(xs) / len(xs)
202
+ cy0 = sum(ys) / len(ys)
203
+ for nid in missing:
204
+ pos[nid] = (cx0, cy0)
205
+
206
+ nx.draw_networkx_edges(G, pos, alpha=0.35, width=0.6, ax=ax)
207
+ nx.draw_networkx_nodes(
208
+ G, pos,
209
+ nodelist=node_list,
210
+ node_color=colors,
211
+ node_size=55,
212
+ linewidths=0.5,
213
+ edgecolors="#222",
214
+ ax=ax,
215
+ )
216
+
217
+ ax.set_title(title, fontsize=11)
218
+ ax.set_aspect("equal")
219
+ ax.axis("off")
220
+ ax.legend(handles=LEGEND_HANDLES, loc="lower right", fontsize=7, framealpha=0.9)
221
+ fig.tight_layout()
222
+ return fig
223
+
224
+
225
+ # ---------------------------------------------------------------------------
226
+ # Pipeline
227
+ # ---------------------------------------------------------------------------
228
+
229
+ def _preset_paths(preset_name: str) -> tuple[Optional[str], Optional[str]]:
230
+ """Resolve a preset dropdown selection to (image_path, graphml_path)."""
231
+ if preset_name == NONE_LABEL or preset_name not in PRESETS:
232
+ return None, None
233
+ img, gt = PRESETS[preset_name]
234
+ return (str(img) if img.exists() else None,
235
+ str(gt) if gt.exists() else None)
236
+
237
+
238
+ def _format_metrics(metrics: dict, latency_s: float, mode: str) -> str:
239
+ nm = metrics["nodes"]
240
+ em = metrics["edges"]
241
+ return f"""
242
+ ### Metrics
243
+
244
+ | | Precision | Recall | F1 | TP | FP | FN |
245
+ |---|---:|---:|---:|---:|---:|---:|
246
+ | **Nodes** | {nm['precision']:.3f} | {nm['recall']:.3f} | **{nm['f1']:.3f}** | {nm['tp']} | {nm['fp']} | {nm['fn']} |
247
+ | **Edges** | {em['precision']:.3f} | {em['recall']:.3f} | **{em['f1']:.3f}** | {em['tp']} | {em['fp']} | {em['fn']} |
248
+
249
+ - Pred: **{metrics['n_pred_nodes']}** ノード / **{metrics['n_pred_edges']}** エッジ
250
+ - GT (semantic-collapsed): **{metrics['n_gt_nodes']}** ノード / **{metrics['n_gt_edges']}** エッジ
251
+ - Mode: `{mode}` · Latency: **{latency_s:.1f}s**
252
+ """
253
+
254
+
255
+ def _format_pred_only(pred_dict: dict, latency_s: float, mode: str) -> str:
256
+ return f"""
257
+ ### Prediction
258
+
259
+ - **{len(pred_dict['nodes'])}** ノード / **{len(pred_dict['edges'])}** エッジ
260
+ - Mode: `{mode}` · Latency: **{latency_s:.1f}s**
261
+ - (正解 graphml 未指定のため評価スキップ)
262
+ """
263
+
264
+
265
+ def run_extraction(
266
+ preset_name: str,
267
+ image_path: Optional[str],
268
+ gt_path: Optional[str],
269
+ use_tiling: bool,
270
+ progress: gr.Progress = gr.Progress(),
271
+ ) -> tuple[str, Optional[plt.Figure], Optional[plt.Figure], str]:
272
+ """Entry point wired to the Run button."""
273
+ # Preset overrides manual upload so the demo is reproducible.
274
+ preset_img, preset_gt = _preset_paths(preset_name)
275
+ if preset_img:
276
+ image_path = preset_img
277
+ if preset_gt:
278
+ gt_path = preset_gt
279
+
280
+ if not image_path:
281
+ return (
282
+ "⚠️ 画像をアップロードするか、プリセットを選択してください。",
283
+ None, None, "",
284
+ )
285
+
286
+ if not os.environ.get("ANTHROPIC_API_KEY"):
287
+ return (
288
+ "⚠️ `ANTHROPIC_API_KEY` が設定されていません。`.env` に追記して再起動してください。",
289
+ None, None, "",
290
+ )
291
+
292
+ client = anthropic.Anthropic()
293
+ mode = "tiled 2x2 + seam filter" if use_tiling else "single-shot"
294
+
295
+ try:
296
+ progress(0.05, desc=f"VLM 抽出開始 ({mode})…")
297
+ t0 = time.time()
298
+ if use_tiling:
299
+ pred_dict = extract_graph_tiled(
300
+ Path(image_path),
301
+ client=client,
302
+ rows=2,
303
+ cols=2,
304
+ overlap=0.1,
305
+ dedup_px=40.0,
306
+ )
307
+ else:
308
+ pred = extract_graph(Path(image_path), client=client)
309
+ pred_dict = pred.to_dict()
310
+ latency = time.time() - t0
311
+ progress(0.55, desc="予測を semantic types に絞り込み…")
312
+
313
+ # Defensive: drop anything non-semantic the VLM may have emitted.
314
+ pred_dict = filter_by_types(pred_dict, SEMANTIC_EQUIPMENT_TYPES)
315
+
316
+ except Exception as e:
317
+ return (f"❌ VLM 抽出中にエラー: `{e}`", None, None, "")
318
+
319
+ progress(0.65, desc="予測グラフを描画…")
320
+ pred_fig = draw_graph(
321
+ pred_dict,
322
+ title=f"Prediction — {len(pred_dict['nodes'])} nodes, {len(pred_dict['edges'])} edges",
323
+ )
324
+
325
+ gt_fig = None
326
+ metrics_md = _format_pred_only(pred_dict, latency, mode)
327
+
328
+ if gt_path and Path(gt_path).exists():
329
+ try:
330
+ progress(0.75, desc="GT graphml をロード & 縮約…")
331
+ gt_raw = load_graphml(Path(gt_path))
332
+ gt_dict = collapse_through_primitives(gt_raw, SEMANTIC_EQUIPMENT_TYPES)
333
+
334
+ progress(0.85, desc="P/R/F1 を評価…")
335
+ metrics = evaluate(
336
+ pred_dict,
337
+ gt_dict,
338
+ directed=False,
339
+ match_threshold=0.5,
340
+ )
341
+ metrics_md = _format_metrics(metrics, latency, mode)
342
+
343
+ progress(0.95, desc="GT グラフを描画…")
344
+ gt_fig = draw_graph(
345
+ gt_dict,
346
+ title=f"Ground Truth — {len(gt_dict['nodes'])} nodes, {len(gt_dict['edges'])} edges",
347
+ )
348
+ except Exception as e:
349
+ metrics_md += f"\n\n⚠️ GT 処理でエラー: `{e}`"
350
+
351
+ # Strip heavy-ish keys before JSON display.
352
+ display_dict = {
353
+ "nodes": pred_dict["nodes"],
354
+ "edges": pred_dict["edges"],
355
+ }
356
+ pred_json = json.dumps(display_dict, indent=2, ensure_ascii=False)
357
+
358
+ progress(1.0, desc="完了")
359
+ return metrics_md, pred_fig, gt_fig, pred_json
360
+
361
+
362
+ def on_preset_change(preset_name: str):
363
+ """When a preset is picked, auto-fill the image and graphml fields."""
364
+ img, gt = _preset_paths(preset_name)
365
+ return img, gt
366
+
367
+
368
+ # ---------------------------------------------------------------------------
369
+ # UI
370
+ # ---------------------------------------------------------------------------
371
+
372
+ DESCRIPTION = """
373
+ # PID2Graph × Claude VLM Demo
374
+
375
+ P&ID (配管計装図) を Claude Opus 4.6 のビジョンで読み取り、シンボル(valve / pump /
376
+ tank / instrumentation / inlet・outlet)とその接続関係を JSON グラフに変換します。
377
+ 正解 graphml を指定すると、ノード/エッジ単位の Precision / Recall / F1 を算出します。
378
+
379
+ - **タイル分割 (2x2)**: 大きな図面では 1 枚を 4 タイルに分割してから抽出し、マージ時に
380
+ bbox 距離で重複排除 + タイル境界の inlet/outlet FP を後処理で除去します。
381
+ - **評価ルール**: GT 側は semantic equipment のみを残し、配管プリミティブ (connector /
382
+ crossing / arrow / background) を経由する接続を 1 エッジに縮約します。
383
+ - **VLM 設定**: `temperature=0` で決定論的サンプリング、構造化出力で JSON スキーマを強制。
384
+ """
385
+
386
+
387
+ def build_ui() -> gr.Blocks:
388
+ with gr.Blocks(title="PID2Graph × Claude VLM Demo") as demo:
389
+ gr.Markdown(DESCRIPTION)
390
+
391
+ with gr.Row():
392
+ with gr.Column(scale=1):
393
+ preset = gr.Dropdown(
394
+ choices=[NONE_LABEL] + list(PRESETS.keys()),
395
+ value=NONE_LABEL,
396
+ label="プリセット (OPEN100 より)",
397
+ )
398
+ image_in = gr.Image(
399
+ type="filepath",
400
+ label="P&ID 画像",
401
+ height=260,
402
+ )
403
+ gt_in = gr.File(
404
+ label="正解 graphml (任意)",
405
+ file_types=[".graphml", ".xml"],
406
+ type="filepath",
407
+ )
408
+ tiling = gr.Checkbox(
409
+ value=True,
410
+ label="タイル分割 (2x2) で抽出 — 高精度だがコスト・時間 4 倍",
411
+ )
412
+ run_btn = gr.Button("抽出実行", variant="primary")
413
+ gr.Markdown(
414
+ "モデル: `" + DEFAULT_MODEL + "` · 所要時間目安: single ~20s / tiled ~60-80s"
415
+ )
416
+
417
+ with gr.Column(scale=2):
418
+ metrics_md = gr.Markdown()
419
+ with gr.Row():
420
+ pred_plot = gr.Plot(label="Prediction")
421
+ gt_plot = gr.Plot(label="Ground Truth")
422
+ with gr.Accordion("予測 JSON (nodes / edges)", open=False):
423
+ # NOTE: using Textbox rather than `gr.Code(language="json")`
424
+ # because the latter's schema has tripped the gradio_client
425
+ # `additionalProperties: false` bug on 4.44.1 in the past.
426
+ # Textbox is a plain string component — zero schema surface.
427
+ pred_json = gr.Textbox(
428
+ label="",
429
+ lines=20,
430
+ max_lines=30,
431
+ show_copy_button=True,
432
+ interactive=False,
433
+ )
434
+
435
+ preset.change(on_preset_change, inputs=[preset], outputs=[image_in, gt_in])
436
+ run_btn.click(
437
+ run_extraction,
438
+ inputs=[preset, image_in, gt_in, tiling],
439
+ outputs=[metrics_md, pred_plot, gt_plot, pred_json],
440
+ )
441
+
442
+ return demo
443
+
444
+
445
+ if __name__ == "__main__":
446
+ # `show_api=False` hides the docs panel in the UI; the monkey-patch
447
+ # at the top of this file is what actually prevents the 4.44 crash,
448
+ # but disabling the docs is cheap defense-in-depth.
449
+ build_ui().launch(show_api=False)
pid2graph_eval/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """PID2Graph evaluation toolkit: VLM extraction + graph metrics."""
pid2graph_eval/cli.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CLI entrypoint for the PID2Graph evaluation skeleton.
2
+
3
+ Usage:
4
+ python -m pid2graph_eval.cli \
5
+ --image-dir path/to/images \
6
+ --gt-dir path/to/graphml \
7
+ --output results.json \
8
+ --limit 10
9
+
10
+ The loader pairs each image with the graphml of the same stem
11
+ (`A-001.png` ↔ `A-001.graphml`). Adjust `pair_samples` once you know the
12
+ actual PID2Graph on-disk layout.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ import anthropic
23
+ from tqdm import tqdm
24
+
25
+ from .extractor import (
26
+ DEFAULT_MAX_TOKENS,
27
+ DEFAULT_MODEL,
28
+ extract_graph,
29
+ extract_graph_tiled,
30
+ )
31
+ from .gt_loader import (
32
+ SEMANTIC_EQUIPMENT_TYPES,
33
+ collapse_through_primitives,
34
+ filter_by_types,
35
+ load_graphml,
36
+ summarize,
37
+ )
38
+ from .metrics import aggregate, evaluate
39
+
40
+ IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp")
41
+ GT_EXTS = (".graphml", ".xml")
42
+
43
+
44
+ def pair_samples(image_dir: Path, gt_dir: Path) -> list[tuple[Path, Path]]:
45
+ """Match each image to a graphml file by filename stem."""
46
+ gt_by_stem: dict[str, Path] = {}
47
+ for ext in GT_EXTS:
48
+ for p in gt_dir.rglob(f"*{ext}"):
49
+ gt_by_stem[p.stem] = p
50
+
51
+ pairs: list[tuple[Path, Path]] = []
52
+ for ext in IMAGE_EXTS:
53
+ for img in sorted(image_dir.rglob(f"*{ext}")):
54
+ gt = gt_by_stem.get(img.stem)
55
+ if gt is not None:
56
+ pairs.append((img, gt))
57
+ return pairs
58
+
59
+
60
+ def run(args: argparse.Namespace) -> int:
61
+ image_dir = Path(args.image_dir)
62
+ gt_dir = Path(args.gt_dir)
63
+ output = Path(args.output)
64
+
65
+ if not image_dir.exists():
66
+ print(f"error: --image-dir does not exist: {image_dir}", file=sys.stderr)
67
+ return 2
68
+ if not gt_dir.exists():
69
+ print(f"error: --gt-dir does not exist: {gt_dir}", file=sys.stderr)
70
+ return 2
71
+
72
+ pairs = pair_samples(image_dir, gt_dir)
73
+ if args.limit:
74
+ pairs = pairs[: args.limit]
75
+ if not pairs:
76
+ print("error: no (image, graphml) pairs found — check stems match", file=sys.stderr)
77
+ return 1
78
+ print(f"found {len(pairs)} sample pair(s)")
79
+
80
+ client = anthropic.Anthropic()
81
+ per_sample: list[dict] = []
82
+ errors: list[dict] = []
83
+
84
+ for image_path, gt_path in tqdm(pairs, desc="eval"):
85
+ try:
86
+ gt_graph = load_graphml(gt_path)
87
+ except Exception as e: # parsing a single bad file shouldn't kill the run
88
+ errors.append({"sample": image_path.stem, "stage": "gt_load", "error": str(e)})
89
+ continue
90
+
91
+ # Semantic-only mode: drop line-primitive nodes from the GT and
92
+ # re-wire the remaining semantic nodes via the original pipe
93
+ # connectivity (BFS through primitives). This matches the format
94
+ # the VLM is instructed to emit — one direct edge per physical
95
+ # pipeline, regardless of how many junctions it passes through.
96
+ if args.semantic_only:
97
+ gt_graph = collapse_through_primitives(gt_graph, SEMANTIC_EQUIPMENT_TYPES)
98
+
99
+ if args.dry_run:
100
+ per_sample.append(
101
+ {
102
+ "sample": image_path.stem,
103
+ "gt_summary": summarize(gt_graph),
104
+ }
105
+ )
106
+ continue
107
+
108
+ try:
109
+ if args.tile_rows > 1 or args.tile_cols > 1:
110
+ pred_dict = extract_graph_tiled(
111
+ image_path,
112
+ client=client,
113
+ model=args.model,
114
+ max_tokens=args.max_tokens,
115
+ rows=args.tile_rows,
116
+ cols=args.tile_cols,
117
+ overlap=args.tile_overlap,
118
+ dedup_px=args.dedup_px,
119
+ seam_filter=not args.no_seam_filter,
120
+ seam_threshold=args.seam_threshold_px,
121
+ edge_threshold=args.edge_threshold_px,
122
+ )
123
+ else:
124
+ pred = extract_graph(
125
+ image_path,
126
+ client=client,
127
+ model=args.model,
128
+ max_tokens=args.max_tokens,
129
+ )
130
+ pred_dict = pred.to_dict()
131
+ except Exception as e:
132
+ errors.append({"sample": image_path.stem, "stage": "vlm", "error": str(e)})
133
+ continue
134
+
135
+ if args.semantic_only:
136
+ pred_dict = filter_by_types(
137
+ {**pred_dict, "directed": gt_graph.get("directed", False)},
138
+ SEMANTIC_EQUIPMENT_TYPES,
139
+ )
140
+
141
+ # Default to whatever the GT file says; allow CLI override.
142
+ if args.force_undirected:
143
+ directed = False
144
+ elif args.force_directed:
145
+ directed = True
146
+ else:
147
+ directed = gt_graph.get("directed", True)
148
+
149
+ metrics = evaluate(
150
+ pred_dict,
151
+ gt_graph,
152
+ directed=directed,
153
+ match_threshold=args.match_threshold,
154
+ )
155
+ per_sample.append(
156
+ {
157
+ "sample": image_path.stem,
158
+ "metrics": metrics,
159
+ "pred": pred_dict,
160
+ "gt_summary": summarize(gt_graph),
161
+ }
162
+ )
163
+
164
+ result: dict = {
165
+ "config": {
166
+ "model": args.model,
167
+ "max_tokens": args.max_tokens,
168
+ "directed": (
169
+ False if args.force_undirected
170
+ else True if args.force_directed
171
+ else "auto"
172
+ ),
173
+ "semantic_only": args.semantic_only,
174
+ "match_threshold": args.match_threshold,
175
+ "tile_rows": args.tile_rows,
176
+ "tile_cols": args.tile_cols,
177
+ "tile_overlap": args.tile_overlap,
178
+ "dedup_px": args.dedup_px,
179
+ "seam_filter": not args.no_seam_filter,
180
+ "seam_threshold_px": args.seam_threshold_px,
181
+ "edge_threshold_px": args.edge_threshold_px,
182
+ "limit": args.limit,
183
+ "dry_run": args.dry_run,
184
+ },
185
+ "per_sample": per_sample,
186
+ "errors": errors,
187
+ }
188
+ if not args.dry_run and per_sample:
189
+ result["aggregate"] = aggregate([s["metrics"] for s in per_sample if "metrics" in s])
190
+
191
+ output.parent.mkdir(parents=True, exist_ok=True)
192
+ output.write_text(json.dumps(result, indent=2, ensure_ascii=False))
193
+ print(f"wrote {output}")
194
+ if "aggregate" in result:
195
+ agg = result["aggregate"]
196
+ print(
197
+ f" nodes F1={agg['nodes_micro']['f1']:.3f} "
198
+ f"P={agg['nodes_micro']['precision']:.3f} "
199
+ f"R={agg['nodes_micro']['recall']:.3f}"
200
+ )
201
+ print(
202
+ f" edges F1={agg['edges_micro']['f1']:.3f} "
203
+ f"P={agg['edges_micro']['precision']:.3f} "
204
+ f"R={agg['edges_micro']['recall']:.3f}"
205
+ )
206
+ if errors:
207
+ print(f" {len(errors)} error(s) — see `errors` in output JSON")
208
+ return 0
209
+
210
+
211
+ def main() -> int:
212
+ p = argparse.ArgumentParser(description="PID2Graph VLM evaluation skeleton")
213
+ p.add_argument("--image-dir", required=True, help="Directory containing P&ID images")
214
+ p.add_argument("--gt-dir", required=True, help="Directory containing graphml ground truth")
215
+ p.add_argument("--output", default="results.json", help="Where to write the JSON report")
216
+ p.add_argument("--model", default=DEFAULT_MODEL, help=f"Claude model id (default: {DEFAULT_MODEL})")
217
+ p.add_argument(
218
+ "--max-tokens",
219
+ type=int,
220
+ default=DEFAULT_MAX_TOKENS,
221
+ help=f"VLM max output tokens (default: {DEFAULT_MAX_TOKENS}, streamed)",
222
+ )
223
+ p.add_argument("--limit", type=int, default=0, help="Only process the first N samples (0 = all)")
224
+ p.add_argument("--match-threshold", type=float, default=0.5, help="Node similarity threshold")
225
+ p.add_argument(
226
+ "--semantic-only",
227
+ action="store_true",
228
+ help=(
229
+ "Restrict both prediction and GT to semantic equipment "
230
+ "(valve, pump, tank, instrumentation, inlet/outlet); "
231
+ "drops pipe-primitive nodes like connector/crossing/arrow."
232
+ ),
233
+ )
234
+ p.add_argument(
235
+ "--tile-rows",
236
+ type=int,
237
+ default=1,
238
+ help="Tile the image into this many rows before VLM extraction (default 1 = off)",
239
+ )
240
+ p.add_argument(
241
+ "--tile-cols",
242
+ type=int,
243
+ default=1,
244
+ help="Tile the image into this many columns before VLM extraction (default 1 = off)",
245
+ )
246
+ p.add_argument(
247
+ "--tile-overlap",
248
+ type=float,
249
+ default=0.1,
250
+ help="Fractional overlap between adjacent tiles (default 0.1 = 10%%)",
251
+ )
252
+ p.add_argument(
253
+ "--dedup-px",
254
+ type=float,
255
+ default=40.0,
256
+ help="Bbox-center distance (pixels) under which two same-type nodes are merged",
257
+ )
258
+ p.add_argument(
259
+ "--no-seam-filter",
260
+ action="store_true",
261
+ help=(
262
+ "Disable the inlet/outlet tile-seam FP filter. By default, "
263
+ "tiled extraction drops inlet/outlet nodes whose bbox center "
264
+ "sits within 50px of an inner tile seam and is not within "
265
+ "30px of the outer image border."
266
+ ),
267
+ )
268
+ p.add_argument(
269
+ "--seam-threshold-px",
270
+ type=float,
271
+ default=50.0,
272
+ help="Distance (px) from an inner tile seam that triggers FP filtering",
273
+ )
274
+ p.add_argument(
275
+ "--edge-threshold-px",
276
+ type=float,
277
+ default=30.0,
278
+ help="Distance (px) from the outer image edge that exempts a node from filtering",
279
+ )
280
+ p.add_argument(
281
+ "--force-undirected",
282
+ action="store_true",
283
+ help="Force undirected edge matching (default: use whatever the GT file says)",
284
+ )
285
+ p.add_argument(
286
+ "--force-directed",
287
+ action="store_true",
288
+ help="Force directed edge matching (default: use whatever the GT file says)",
289
+ )
290
+ p.add_argument(
291
+ "--dry-run",
292
+ action="store_true",
293
+ help="Skip VLM calls; just load GT and print summaries (for loader debugging)",
294
+ )
295
+ return run(p.parse_args())
296
+
297
+
298
+ if __name__ == "__main__":
299
+ raise SystemExit(main())
pid2graph_eval/extractor.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Claude VLM-based P&ID graph extraction.
2
+
3
+ Uses the Anthropic SDK with:
4
+ - Vision input (base64-encoded P&ID image)
5
+ - Streaming (required for high `max_tokens` to avoid HTTP timeouts)
6
+ - Structured outputs via `output_config.format={"type": "json_schema", ...}`
7
+ so the model is forced to return our Pydantic schema
8
+ - Prompt caching on the (stable) system prompt so repeated evaluation runs
9
+ only pay full price once per 5-minute window
10
+
11
+ Public API:
12
+ extract_graph(image_path, client=None, model="claude-opus-4-6") -> GraphOut
13
+ extract_graph_tiled(image_path, ..., rows=2, cols=2) -> dict
14
+
15
+ The tiled variant splits the image into overlapping tiles, runs the VLM
16
+ on each, and merges the results using bbox-distance deduplication. Used
17
+ to break the ~50-symbol recall ceiling on large diagrams.
18
+
19
+ Note on scope (semantic-only mode):
20
+ This prompt intentionally excludes PID2Graph's line-level primitives
21
+ (connector, crossing, arrow, background, general). It targets only
22
+ the five semantic equipment categories the VLM can recognize. The
23
+ matching CLI flag `--semantic-only` filters the ground truth to the
24
+ same five categories so P/R/F1 are comparable.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import base64
30
+ import json
31
+ from pathlib import Path
32
+ from typing import Optional
33
+
34
+ import anthropic
35
+
36
+ from .schema import GraphOut
37
+ from .tile import (
38
+ Tile,
39
+ filter_seam_artifacts,
40
+ merge_tile_graphs,
41
+ split_image,
42
+ tile_to_base64_png,
43
+ )
44
+
45
+ DEFAULT_MODEL = "claude-opus-4-6"
46
+ DEFAULT_MAX_TOKENS = 64000 # streaming is required at this size
47
+ DEFAULT_TEMPERATURE = 0.0 # deterministic sampling — kills run-to-run variance
48
+
49
+ SYSTEM_PROMPT = """\
50
+ You are an expert annotator for the PID2Graph benchmark, which turns
51
+ Piping and Instrumentation Diagrams (P&IDs) into node/edge graphs.
52
+
53
+ Your task: given a P&ID image (which may be a full diagram or a cropped
54
+ region of one), emit every SEMANTIC EQUIPMENT symbol visible as a node,
55
+ and every pipeline connection between two such symbols as an edge.
56
+ Line-level primitives (pipe junctions, crossings, arrowheads, background
57
+ regions) must NOT be emitted.
58
+
59
+ NODE CATEGORIES — use EXACTLY one of these 5 lowercase labels for `type`:
60
+
61
+ - valve all valve bodies: gate, ball, check, control, globe,
62
+ butterfly, needle, etc. Each valve body is one node.
63
+ - pump pumps and compressors (rotating equipment)
64
+ - tank storage tanks, drums, vessels, columns, reactors,
65
+ heat exchangers, any large process equipment
66
+ - instrumentation circle/balloon instrument bubbles (PI, TI, FIC, TC,
67
+ LT, PDT, ...). Each bubble is one node.
68
+ - inlet/outlet diagram boundary terminals where a pipe enters or
69
+ leaves the drawing. Use the literal string with a
70
+ slash: "inlet/outlet".
71
+
72
+ Do not invent other type names. Do not use synonyms. Do not use title case.
73
+ Do not emit connector, crossing, arrow, background, or general — those
74
+ are excluded from this task. If a drawn glyph does not clearly fit one of
75
+ the five categories above, skip it.
76
+
77
+ PER-NODE FIELDS:
78
+ id unique id within THIS response, e.g. "n1", "n2", "n3", ...
79
+ type one of the 5 labels above
80
+ label leave null — PID2Graph ground truth does not store printed tags
81
+ bbox REQUIRED. An object {xmin, ymin, xmax, ymax} with each coordinate
82
+ normalized to [0.0, 1.0] relative to the image you are shown.
83
+ (0, 0) is the top-left corner, (1, 1) is the bottom-right. Give
84
+ your best estimate of the symbol's tight bounding box. If you
85
+ cannot estimate it, still emit a best-effort box — never omit
86
+ the field and never return null.
87
+
88
+ EDGES — edges in PID2Graph are UNDIRECTED. Emit an edge whenever two of
89
+ the semantic nodes above are joined by a continuous pipeline, even if
90
+ that pipeline passes through several pipe junctions, crossings, or
91
+ arrowheads on its way. Those intermediate points must not appear as
92
+ nodes; collapse the whole physical pipeline into a single direct edge
93
+ between its two semantic endpoints.
94
+
95
+ PER-EDGE FIELDS:
96
+ source / target node ids from the `nodes` list
97
+ type use "solid"
98
+ label null
99
+
100
+ Guidelines:
101
+ * Be exhaustive within the semantic scope.
102
+ * Ignore the title block, legend, border, and revision history.
103
+ * If the image is a cropped region (e.g. one tile of a larger diagram),
104
+ only emit symbols and edges visible within this crop. Do not infer
105
+ connections that continue off-crop — another tile will cover them.
106
+ * Return ONLY the JSON object matching the schema — no prose, no markdown.
107
+ """
108
+
109
+ USER_INSTRUCTION = (
110
+ "Extract the semantic-equipment graph from this P&ID image as JSON. "
111
+ "Only the 5 categories listed in the instructions — no pipe primitives. "
112
+ "Include a bbox (normalized [0, 1]) for every node."
113
+ )
114
+
115
+
116
+ def _guess_media_type(path: Path) -> str:
117
+ ext = path.suffix.lower()
118
+ return {
119
+ ".png": "image/png",
120
+ ".jpg": "image/jpeg",
121
+ ".jpeg": "image/jpeg",
122
+ ".gif": "image/gif",
123
+ ".webp": "image/webp",
124
+ }.get(ext, "image/png")
125
+
126
+
127
+ def _load_image_b64(path: Path) -> tuple[str, str]:
128
+ data = base64.standard_b64encode(path.read_bytes()).decode("utf-8")
129
+ return data, _guess_media_type(path)
130
+
131
+
132
+ def _graphout_schema() -> dict:
133
+ """JSON schema for GraphOut, ready for `output_config.format`."""
134
+ return GraphOut.model_json_schema()
135
+
136
+
137
+ def _call_vlm(
138
+ image_data: str,
139
+ media_type: str,
140
+ client: anthropic.Anthropic,
141
+ model: str,
142
+ max_tokens: int,
143
+ tag: str,
144
+ temperature: float = DEFAULT_TEMPERATURE,
145
+ ) -> GraphOut:
146
+ """Shared VLM request path used by both whole-image and tile extraction.
147
+
148
+ `tag` is a short human-readable identifier (filename, tile name) used
149
+ only to make error messages point at the right sample.
150
+
151
+ `temperature=0.0` by default for reproducible evaluation runs.
152
+ """
153
+ schema = _graphout_schema()
154
+
155
+ with client.messages.stream(
156
+ model=model,
157
+ max_tokens=max_tokens,
158
+ temperature=temperature,
159
+ system=[
160
+ {
161
+ "type": "text",
162
+ "text": SYSTEM_PROMPT,
163
+ "cache_control": {"type": "ephemeral"},
164
+ }
165
+ ],
166
+ messages=[
167
+ {
168
+ "role": "user",
169
+ "content": [
170
+ {
171
+ "type": "image",
172
+ "source": {
173
+ "type": "base64",
174
+ "media_type": media_type,
175
+ "data": image_data,
176
+ },
177
+ },
178
+ {"type": "text", "text": USER_INSTRUCTION},
179
+ ],
180
+ }
181
+ ],
182
+ output_config={
183
+ "format": {
184
+ "type": "json_schema",
185
+ "schema": schema,
186
+ }
187
+ },
188
+ ) as stream:
189
+ final = stream.get_final_message()
190
+
191
+ if final.stop_reason == "refusal":
192
+ raise RuntimeError(f"VLM refused to answer for {tag} (stop_reason=refusal)")
193
+ if final.stop_reason == "max_tokens":
194
+ raise RuntimeError(
195
+ f"VLM hit max_tokens={max_tokens} for {tag} — "
196
+ f"output truncated; raise max_tokens or simplify the prompt"
197
+ )
198
+
199
+ text_block = next((b for b in final.content if b.type == "text"), None)
200
+ if text_block is None:
201
+ raise RuntimeError(
202
+ f"VLM emitted no text block for {tag} (stop_reason={final.stop_reason})"
203
+ )
204
+
205
+ try:
206
+ data = json.loads(text_block.text)
207
+ except json.JSONDecodeError as e:
208
+ raise RuntimeError(
209
+ f"VLM output is not valid JSON for {tag}: {e}\n"
210
+ f"first 500 chars: {text_block.text[:500]}"
211
+ )
212
+
213
+ return GraphOut.model_validate(data)
214
+
215
+
216
+ def extract_graph(
217
+ image_path: Path,
218
+ client: Optional[anthropic.Anthropic] = None,
219
+ model: str = DEFAULT_MODEL,
220
+ max_tokens: int = DEFAULT_MAX_TOKENS,
221
+ temperature: float = DEFAULT_TEMPERATURE,
222
+ ) -> GraphOut:
223
+ """Run the VLM on the whole image and return the parsed graph."""
224
+ client = client or anthropic.Anthropic()
225
+ image_path = Path(image_path)
226
+ image_data, media_type = _load_image_b64(image_path)
227
+ return _call_vlm(
228
+ image_data, media_type, client, model, max_tokens,
229
+ image_path.name, temperature=temperature,
230
+ )
231
+
232
+
233
+ def extract_graph_tiled(
234
+ image_path: Path,
235
+ client: Optional[anthropic.Anthropic] = None,
236
+ model: str = DEFAULT_MODEL,
237
+ max_tokens: int = DEFAULT_MAX_TOKENS,
238
+ temperature: float = DEFAULT_TEMPERATURE,
239
+ rows: int = 2,
240
+ cols: int = 2,
241
+ overlap: float = 0.1,
242
+ dedup_px: float = 40.0,
243
+ seam_filter: bool = True,
244
+ seam_filter_types: tuple[str, ...] = ("inlet/outlet",),
245
+ seam_threshold: float = 50.0,
246
+ edge_threshold: float = 30.0,
247
+ ) -> dict:
248
+ """Tile the image, extract each tile, merge via bbox-distance dedup.
249
+
250
+ Returns a plain dict (not a GraphOut) because merged nodes carry
251
+ extra `bbox` (in global pixel coordinates) and `source_tiles` fields
252
+ that don't fit the response schema. The dict shape is still
253
+ compatible with `metrics.evaluate()`.
254
+
255
+ Deduplication rules:
256
+ * Two nodes from different tiles are merged iff they share the
257
+ same `type` AND their bbox centers are within `dedup_px` pixels
258
+ in the un-tiled global image.
259
+ * Nodes with a null bbox are dropped (they can't be deduped).
260
+ * Edges are remapped through the merge map; undirected duplicates
261
+ are collapsed; self-loops (both endpoints merged to the same
262
+ global node) are dropped.
263
+ """
264
+ client = client or anthropic.Anthropic()
265
+ image_path = Path(image_path)
266
+ tiles: list[Tile] = split_image(image_path, rows=rows, cols=cols, overlap=overlap)
267
+
268
+ per_tile: list[tuple[GraphOut, Tile]] = []
269
+ for tile in tiles:
270
+ image_data, media_type = tile_to_base64_png(tile)
271
+ tag = f"{image_path.name}:{tile.name}"
272
+ graph = _call_vlm(
273
+ image_data, media_type, client, model, max_tokens,
274
+ tag, temperature=temperature,
275
+ )
276
+ per_tile.append((graph, tile))
277
+
278
+ merged = merge_tile_graphs(per_tile, dedup_px=dedup_px)
279
+
280
+ if seam_filter:
281
+ merged = filter_seam_artifacts(
282
+ merged,
283
+ tiles,
284
+ types=seam_filter_types,
285
+ seam_threshold=seam_threshold,
286
+ edge_threshold=edge_threshold,
287
+ )
288
+
289
+ # Attach provenance for downstream debugging / aggregation.
290
+ merged["tile_stats"] = [
291
+ {
292
+ "tile": tile.name,
293
+ "n_nodes": len(graph.nodes),
294
+ "n_edges": len(graph.edges),
295
+ "nodes_with_bbox": sum(1 for n in graph.nodes if n.bbox is not None),
296
+ }
297
+ for graph, tile in per_tile
298
+ ]
299
+ return merged
pid2graph_eval/gt_loader.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load PID2Graph ground-truth graphml files and normalize to the common schema.
2
+
3
+ PID2Graph OPEN100 schema (observed):
4
+ node attrs: label (category), xmin, xmax, ymin, ymax (bounding box)
5
+ edge attrs: edge_label (only 'solid' observed)
6
+ graph: undirected
7
+
8
+ Ten node categories are used across the dataset:
9
+ connector, crossing, arrow, instrumentation, valve, general,
10
+ inlet/outlet, background, tank, pump
11
+
12
+ There are no printed tags (like "P-101") in the graphml — PID2Graph is a
13
+ pure symbol + connectivity benchmark, not an OCR benchmark. The `label`
14
+ field in our normalized dict is therefore always None for this dataset
15
+ and metrics fall back to type-only matching.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from pathlib import Path
21
+ from typing import Iterable, Optional
22
+
23
+ import networkx as nx
24
+
25
+ # The PID2Graph graphml files store the category under `label`. Kept as a
26
+ # tuple so other graphml-based datasets with different conventions can be
27
+ # supported by extending the candidate list.
28
+ NODE_TYPE_KEYS: tuple[str, ...] = ("label", "type", "category", "class")
29
+ EDGE_TYPE_KEYS: tuple[str, ...] = ("edge_label", "type", "category")
30
+
31
+ # The official PID2Graph OPEN100 categories. Exposed so extractor.py can
32
+ # put them straight into the VLM prompt.
33
+ PID2GRAPH_NODE_TYPES: tuple[str, ...] = (
34
+ "connector",
35
+ "crossing",
36
+ "arrow",
37
+ "instrumentation",
38
+ "valve",
39
+ "general",
40
+ "inlet/outlet",
41
+ "background",
42
+ "tank",
43
+ "pump",
44
+ )
45
+
46
+ # The subset used by the "semantic-only" evaluation mode: real equipment
47
+ # and instrument symbols, excluding line-level primitives (connector /
48
+ # crossing / arrow / background) AND the `general` catch-all, whose
49
+ # shape definition is too vague for zero-shot VLM detection to handle.
50
+ SEMANTIC_EQUIPMENT_TYPES: frozenset[str] = frozenset(
51
+ {
52
+ "valve",
53
+ "pump",
54
+ "tank",
55
+ "instrumentation",
56
+ "inlet/outlet",
57
+ }
58
+ )
59
+
60
+
61
+ def _norm_type(t: Optional[str]) -> str:
62
+ return (t or "").strip().lower()
63
+
64
+
65
+ def filter_by_types(graph: dict, allowed: frozenset[str]) -> dict:
66
+ """Return a copy of `graph` keeping only nodes whose type is in `allowed`.
67
+
68
+ Edges are kept only when BOTH endpoints survive the filter. All
69
+ non-{nodes, edges} keys (e.g. `directed`, `tile_stats`,
70
+ `seam_filtered`) are passed through unchanged so downstream code
71
+ can still inspect provenance.
72
+ """
73
+ keep_ids: set[str] = set()
74
+ new_nodes: list[dict] = []
75
+ for n in graph["nodes"]:
76
+ if _norm_type(n.get("type")) in allowed:
77
+ keep_ids.add(n["id"])
78
+ new_nodes.append(n)
79
+
80
+ new_edges = [
81
+ e
82
+ for e in graph["edges"]
83
+ if e["source"] in keep_ids and e["target"] in keep_ids
84
+ ]
85
+
86
+ out = dict(graph)
87
+ out["nodes"] = new_nodes
88
+ out["edges"] = new_edges
89
+ return out
90
+
91
+
92
+ def collapse_through_primitives(graph: dict, semantic_types: frozenset[str]) -> dict:
93
+ """Keep only semantic nodes; re-wire edges by walking through primitives.
94
+
95
+ Two semantic nodes are connected in the result iff there is a path
96
+ between them in the original graph consisting of zero or more
97
+ NON-semantic nodes (e.g. `connector`, `crossing`, `arrow`). This
98
+ matches what the VLM is asked to produce: one direct semantic-to-
99
+ semantic edge per physical pipeline, regardless of how many pipe
100
+ junctions it passes through.
101
+
102
+ The resulting graph is always treated as undirected — PID2Graph's
103
+ underlying graphml is undirected and path-based equivalence has no
104
+ natural orientation.
105
+ """
106
+ sem_ids: set[str] = {
107
+ n["id"] for n in graph["nodes"] if _norm_type(n.get("type")) in semantic_types
108
+ }
109
+
110
+ # Undirected adjacency for the full graph
111
+ adj: dict[str, list[str]] = {n["id"]: [] for n in graph["nodes"]}
112
+ for e in graph["edges"]:
113
+ s, t = e["source"], e["target"]
114
+ if s in adj and t in adj:
115
+ adj[s].append(t)
116
+ adj[t].append(s)
117
+
118
+ new_edges: set[tuple[str, str]] = set()
119
+
120
+ # BFS from each semantic node through primitive nodes; whenever we
121
+ # land on another semantic node, record the edge and stop expanding
122
+ # past it. Visiting primitives multiple times from different
123
+ # starting points is fine; the edge-set deduplicates results.
124
+ for start in sem_ids:
125
+ visited = {start}
126
+ stack: list[str] = [start]
127
+ while stack:
128
+ cur = stack.pop()
129
+ for nb in adj.get(cur, ()):
130
+ if nb in visited:
131
+ continue
132
+ visited.add(nb)
133
+ if nb in sem_ids:
134
+ a, b = sorted((start, nb))
135
+ new_edges.add((a, b))
136
+ # Don't recurse past a semantic boundary.
137
+ else:
138
+ stack.append(nb)
139
+
140
+ new_nodes = [n for n in graph["nodes"] if n["id"] in sem_ids]
141
+ new_edges_list = [
142
+ {
143
+ "source": a,
144
+ "target": b,
145
+ "type": "solid",
146
+ "label": None,
147
+ "raw_attrs": {},
148
+ }
149
+ for a, b in sorted(new_edges)
150
+ ]
151
+
152
+ return {
153
+ "nodes": new_nodes,
154
+ "edges": new_edges_list,
155
+ "directed": False,
156
+ }
157
+
158
+
159
+ def _first_attr(attrs: dict, keys: Iterable[str]) -> Optional[str]:
160
+ for k in keys:
161
+ v = attrs.get(k)
162
+ if v is None:
163
+ continue
164
+ s = str(v).strip()
165
+ if s:
166
+ return s
167
+ return None
168
+
169
+
170
+ def _bbox(attrs: dict) -> Optional[list[float]]:
171
+ try:
172
+ return [
173
+ float(attrs["xmin"]),
174
+ float(attrs["ymin"]),
175
+ float(attrs["xmax"]),
176
+ float(attrs["ymax"]),
177
+ ]
178
+ except (KeyError, TypeError, ValueError):
179
+ return None
180
+
181
+
182
+ def load_graphml(path: Path) -> dict:
183
+ """Parse a graphml file into `{nodes, edges, directed}`.
184
+
185
+ Each node/edge keeps its original attributes under `raw_attrs` so
186
+ experiments can try alternative fields without re-reading the file.
187
+ """
188
+ G = nx.read_graphml(path)
189
+
190
+ nodes: list[dict] = []
191
+ for node_id, attrs in G.nodes(data=True):
192
+ nodes.append(
193
+ {
194
+ "id": str(node_id),
195
+ "type": _first_attr(attrs, NODE_TYPE_KEYS) or "",
196
+ "label": None, # PID2Graph has no printed tag in GT
197
+ "bbox": _bbox(attrs),
198
+ "raw_attrs": dict(attrs),
199
+ }
200
+ )
201
+
202
+ edges: list[dict] = []
203
+ for u, v, attrs in G.edges(data=True):
204
+ edges.append(
205
+ {
206
+ "source": str(u),
207
+ "target": str(v),
208
+ "type": _first_attr(attrs, EDGE_TYPE_KEYS),
209
+ "label": None,
210
+ "raw_attrs": dict(attrs),
211
+ }
212
+ )
213
+
214
+ return {
215
+ "nodes": nodes,
216
+ "edges": edges,
217
+ "directed": G.is_directed(),
218
+ }
219
+
220
+
221
+ def summarize(graph: dict) -> dict:
222
+ """Quick stats for sanity-checking the loader on a new dataset."""
223
+ type_counts: dict[str, int] = {}
224
+ for n in graph["nodes"]:
225
+ t = n["type"] or "<empty>"
226
+ type_counts[t] = type_counts.get(t, 0) + 1
227
+ edge_type_counts: dict[str, int] = {}
228
+ for e in graph["edges"]:
229
+ t = e["type"] or "<empty>"
230
+ edge_type_counts[t] = edge_type_counts.get(t, 0) + 1
231
+ return {
232
+ "n_nodes": len(graph["nodes"]),
233
+ "n_edges": len(graph["edges"]),
234
+ "directed": graph.get("directed", False),
235
+ "node_types": type_counts,
236
+ "edge_types": edge_type_counts,
237
+ }
pid2graph_eval/metrics.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Graph-level evaluation: node matching, then edge matching, then P/R/F1.
2
+
3
+ Matching strategy (skeleton — greedy by descending similarity):
4
+ 1. Score every (predicted_node, gt_node) pair with `node_similarity`.
5
+ 2. Greedily pick the highest-scoring pair until no pair clears the
6
+ threshold or all nodes on one side are consumed.
7
+ 3. Map predicted edges through the resulting id alignment and intersect
8
+ with GT edges. Undirected matching is optional.
9
+
10
+ TODO: swap greedy for Hungarian (scipy.optimize.linear_sum_assignment)
11
+ once we have a feel for the data. Greedy is good enough for a first
12
+ pass and keeps dependencies small.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Optional
18
+
19
+
20
+ def _norm(s: Optional[str]) -> str:
21
+ return (s or "").strip().lower().replace("-", "_").replace(" ", "_")
22
+
23
+
24
+ def node_similarity(pred: dict, gt: dict) -> float:
25
+ """Score a predicted node against a GT node in [0, 1].
26
+
27
+ Both `type` match and `label` match contribute. If either side has no
28
+ label, we fall back to type-only matching.
29
+ """
30
+ tp, tg = _norm(pred.get("type")), _norm(gt.get("type"))
31
+ lp, lg = _norm(pred.get("label")), _norm(gt.get("label"))
32
+
33
+ type_match = 1.0 if tp and tp == tg else 0.0
34
+
35
+ if lp and lg:
36
+ label_match = 1.0 if lp == lg else 0.0
37
+ return 0.5 * type_match + 0.5 * label_match
38
+
39
+ # Only one side (or neither) has a label — rely on type alone.
40
+ return type_match
41
+
42
+
43
+ def match_nodes(
44
+ pred_nodes: list[dict],
45
+ gt_nodes: list[dict],
46
+ threshold: float = 0.5,
47
+ ) -> dict[str, str]:
48
+ """Greedy 1:1 matching. Returns `{pred_id: gt_id}` for matched pairs."""
49
+ scored: list[tuple[float, str, str]] = []
50
+ for p in pred_nodes:
51
+ for g in gt_nodes:
52
+ s = node_similarity(p, g)
53
+ if s >= threshold:
54
+ scored.append((s, p["id"], g["id"]))
55
+ scored.sort(reverse=True)
56
+
57
+ matches: dict[str, str] = {}
58
+ used_gt: set[str] = set()
59
+ for _score, pid, gid in scored:
60
+ if pid in matches or gid in used_gt:
61
+ continue
62
+ matches[pid] = gid
63
+ used_gt.add(gid)
64
+ return matches
65
+
66
+
67
+ def _prf(tp: int, fp: int, fn: int) -> dict:
68
+ precision = tp / (tp + fp) if (tp + fp) else 0.0
69
+ recall = tp / (tp + fn) if (tp + fn) else 0.0
70
+ f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0.0
71
+ return {
72
+ "precision": precision,
73
+ "recall": recall,
74
+ "f1": f1,
75
+ "tp": tp,
76
+ "fp": fp,
77
+ "fn": fn,
78
+ }
79
+
80
+
81
+ def evaluate(pred: dict, gt: dict, directed: bool = True, match_threshold: float = 0.5) -> dict:
82
+ """Compare a single predicted graph against a single GT graph.
83
+
84
+ Args:
85
+ pred: `{nodes, edges}` as emitted by `GraphOut.to_dict()`.
86
+ gt: `{nodes, edges}` as emitted by `gt_loader.load_graphml()`.
87
+ directed: if False, (u, v) and (v, u) are treated as the same edge.
88
+ match_threshold: minimum similarity to accept a node pairing.
89
+
90
+ Returns:
91
+ Dict with `nodes`, `edges`, and `n_matched_nodes` keys.
92
+ """
93
+ # --- nodes ------------------------------------------------------------
94
+ node_map = match_nodes(pred["nodes"], gt["nodes"], threshold=match_threshold)
95
+ tp_n = len(node_map)
96
+ fp_n = len(pred["nodes"]) - tp_n
97
+ fn_n = len(gt["nodes"]) - tp_n
98
+ node_metrics = _prf(tp_n, fp_n, fn_n)
99
+
100
+ # --- edges ------------------------------------------------------------
101
+ # Translate predicted edges through the node alignment. Unmatched
102
+ # endpoints make the edge uncountable (it becomes a guaranteed FP).
103
+ def canon(u: str, v: str) -> tuple[str, str]:
104
+ return (u, v) if directed else tuple(sorted((u, v))) # type: ignore[return-value]
105
+
106
+ pred_edges_canon: set[tuple[str, str]] = set()
107
+ unmappable_pred_edges = 0
108
+ for e in pred["edges"]:
109
+ if e["source"] in node_map and e["target"] in node_map:
110
+ pred_edges_canon.add(canon(node_map[e["source"]], node_map[e["target"]]))
111
+ else:
112
+ unmappable_pred_edges += 1
113
+
114
+ gt_edges_canon: set[tuple[str, str]] = set(
115
+ canon(e["source"], e["target"]) for e in gt["edges"]
116
+ )
117
+
118
+ tp_e = len(pred_edges_canon & gt_edges_canon)
119
+ fp_e = (len(pred_edges_canon) - tp_e) + unmappable_pred_edges
120
+ fn_e = len(gt_edges_canon) - tp_e
121
+ edge_metrics = _prf(tp_e, fp_e, fn_e)
122
+
123
+ return {
124
+ "nodes": node_metrics,
125
+ "edges": edge_metrics,
126
+ "n_pred_nodes": len(pred["nodes"]),
127
+ "n_gt_nodes": len(gt["nodes"]),
128
+ "n_pred_edges": len(pred["edges"]),
129
+ "n_gt_edges": len(gt["edges"]),
130
+ }
131
+
132
+
133
+ def aggregate(per_sample: list[dict]) -> dict:
134
+ """Micro-average across samples by summing TP/FP/FN."""
135
+ def sum_keys(metric: str) -> dict:
136
+ tp = sum(s[metric]["tp"] for s in per_sample)
137
+ fp = sum(s[metric]["fp"] for s in per_sample)
138
+ fn = sum(s[metric]["fn"] for s in per_sample)
139
+ return _prf(tp, fp, fn)
140
+
141
+ return {
142
+ "n_samples": len(per_sample),
143
+ "nodes_micro": sum_keys("nodes"),
144
+ "edges_micro": sum_keys("edges"),
145
+ }
pid2graph_eval/schema.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Common graph schema shared by predictions and normalized ground truth.
2
+
3
+ The VLM is asked to emit a `GraphOut` instance (enforced via structured outputs).
4
+ Ground truth graphml is normalized into the same dict shape by `gt_loader.py`,
5
+ so both sides can be fed directly into `metrics.evaluate()`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import List, Optional
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field
13
+
14
+
15
+ class BBox(BaseModel):
16
+ """Axis-aligned bounding box in normalized image coordinates.
17
+
18
+ Each coordinate is in [0.0, 1.0] relative to the image the VLM was
19
+ shown. When extraction runs on a cropped tile, these are tile-local
20
+ coordinates; the tiled merger converts them back to global image
21
+ pixel coordinates before deduplication.
22
+ """
23
+
24
+ model_config = ConfigDict(extra="forbid")
25
+
26
+ xmin: float = Field(description="Left edge, normalized [0, 1] within the visible image.")
27
+ ymin: float = Field(description="Top edge, normalized [0, 1].")
28
+ xmax: float = Field(description="Right edge, normalized [0, 1].")
29
+ ymax: float = Field(description="Bottom edge, normalized [0, 1].")
30
+
31
+
32
+ class NodeOut(BaseModel):
33
+ """A single symbol in the P&ID (pump, valve, tank, sensor, ...)."""
34
+
35
+ model_config = ConfigDict(extra="forbid")
36
+
37
+ id: str = Field(description="Unique node id within this diagram (e.g. 'n1').")
38
+ type: str = Field(
39
+ description=(
40
+ "Category of the symbol, lowercase, singular. "
41
+ "Examples: 'pump', 'valve', 'tank', 'heat_exchanger', 'sensor', "
42
+ "'controller', 'compressor', 'column'."
43
+ )
44
+ )
45
+ label: Optional[str] = Field(
46
+ default=None,
47
+ description="Tag/identifier printed next to the symbol if visible (e.g. 'P-101').",
48
+ )
49
+ bbox: Optional[BBox] = Field(
50
+ default=None,
51
+ description=(
52
+ "Tight bounding box of the symbol, with each coordinate "
53
+ "normalized to [0, 1] relative to the image the VLM sees. "
54
+ "Required by the tiled extraction pipeline for deduplication."
55
+ ),
56
+ )
57
+
58
+
59
+ class EdgeOut(BaseModel):
60
+ """A pipeline or signal connection between two symbols."""
61
+
62
+ model_config = ConfigDict(extra="forbid")
63
+
64
+ source: str = Field(description="Source node id.")
65
+ target: str = Field(description="Target node id.")
66
+ type: Optional[str] = Field(
67
+ default=None,
68
+ description="Connection type: 'pipe', 'signal', 'electrical', ...",
69
+ )
70
+ label: Optional[str] = Field(
71
+ default=None,
72
+ description="Line tag if visible (e.g. '2\"-PW-101').",
73
+ )
74
+
75
+
76
+ class GraphOut(BaseModel):
77
+ """Top-level graph extracted from a P&ID image."""
78
+
79
+ model_config = ConfigDict(extra="forbid")
80
+
81
+ nodes: List[NodeOut]
82
+ edges: List[EdgeOut]
83
+
84
+ def to_dict(self) -> dict:
85
+ """Shape that `metrics.evaluate()` consumes."""
86
+ return {
87
+ "nodes": [n.model_dump() for n in self.nodes],
88
+ "edges": [e.model_dump() for e in self.edges],
89
+ }
pid2graph_eval/tile.py ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Image tiling and multi-tile result merging.
2
+
3
+ Rationale:
4
+ Whole-page P&ID extraction hits a node-recall ceiling around 50-60%
5
+ on large diagrams because the VLM can only resolve ~50 symbols at
6
+ ~1.15MP vision downsampling. Splitting the image into overlapping
7
+ tiles, extracting each, then merging via bbox-distance deduplication
8
+ lets the model zoom in on smaller regions at full pixel budget.
9
+
10
+ Coordinate conventions:
11
+ - The VLM sees an individual tile and reports bbox in *normalized*
12
+ [0, 1] coordinates relative to that tile (see `schema.BBox`).
13
+ - `merge_tile_graphs` converts each tile-local normalized bbox into
14
+ global image pixel coordinates and deduplicates across tiles using
15
+ the Euclidean distance between bbox centers.
16
+
17
+ Overlap:
18
+ Tiles are grown outward by `overlap` fraction of the un-overlapped
19
+ tile size on each internal seam so a symbol that happens to straddle
20
+ a split line appears fully in at least one tile. The dedup step then
21
+ collapses the two detections into one node.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import base64
27
+ import io
28
+ from dataclasses import dataclass, field
29
+ from pathlib import Path
30
+ from typing import Iterable
31
+
32
+ from PIL import Image
33
+
34
+ from .schema import BBox, GraphOut
35
+
36
+
37
+ @dataclass
38
+ class Tile:
39
+ """One cropped region of the source image, ready to send to the VLM."""
40
+
41
+ image: Image.Image
42
+ x0: int # top-left x in global (full-image) pixel coordinates
43
+ y0: int # top-left y in global pixel coordinates
44
+ w: int # tile width in global pixels
45
+ h: int # tile height in global pixels
46
+ parent_w: int
47
+ parent_h: int
48
+ name: str # short label, e.g. "r0c0"
49
+
50
+
51
+ def split_image(
52
+ image_path: Path | str,
53
+ rows: int = 2,
54
+ cols: int = 2,
55
+ overlap: float = 0.1,
56
+ ) -> list[Tile]:
57
+ """Split `image_path` into `rows * cols` overlapping tiles.
58
+
59
+ `overlap` is the fractional grow-outward applied to each seam. With
60
+ `overlap=0.1` and 2x2 tiling each tile is 60% wide and 60% tall
61
+ (50% + 10% overlap on the internal edge). Boundary tiles are clipped
62
+ to the image extent so they never exceed the parent dimensions.
63
+ """
64
+ img = Image.open(Path(image_path)).convert("RGB")
65
+ W, H = img.size
66
+
67
+ base_tw = W / cols
68
+ base_th = H / rows
69
+ ow = int(round(base_tw * overlap))
70
+ oh = int(round(base_th * overlap))
71
+
72
+ tiles: list[Tile] = []
73
+ for r in range(rows):
74
+ for c in range(cols):
75
+ x0 = max(0, int(round(c * base_tw)) - ow)
76
+ y0 = max(0, int(round(r * base_th)) - oh)
77
+ x1 = min(W, int(round((c + 1) * base_tw)) + ow)
78
+ y1 = min(H, int(round((r + 1) * base_th)) + oh)
79
+ tile_img = img.crop((x0, y0, x1, y1))
80
+ tiles.append(
81
+ Tile(
82
+ image=tile_img,
83
+ x0=x0,
84
+ y0=y0,
85
+ w=x1 - x0,
86
+ h=y1 - y0,
87
+ parent_w=W,
88
+ parent_h=H,
89
+ name=f"r{r}c{c}",
90
+ )
91
+ )
92
+ return tiles
93
+
94
+
95
+ def tile_to_base64_png(tile: Tile) -> tuple[str, str]:
96
+ """Encode a tile as base64 PNG for the Messages API `source.data` field."""
97
+ buf = io.BytesIO()
98
+ tile.image.save(buf, format="PNG")
99
+ return base64.standard_b64encode(buf.getvalue()).decode("utf-8"), "image/png"
100
+
101
+
102
+ def _tile_bbox_to_global_px(bbox: BBox, tile: Tile) -> tuple[float, float, float, float]:
103
+ """Convert a tile-local normalized bbox to global pixel coordinates."""
104
+ return (
105
+ tile.x0 + bbox.xmin * tile.w,
106
+ tile.y0 + bbox.ymin * tile.h,
107
+ tile.x0 + bbox.xmax * tile.w,
108
+ tile.y0 + bbox.ymax * tile.h,
109
+ )
110
+
111
+
112
+ def _center(bbox_px: tuple[float, float, float, float]) -> tuple[float, float]:
113
+ xmin, ymin, xmax, ymax = bbox_px
114
+ return (xmin + xmax) / 2.0, (ymin + ymax) / 2.0
115
+
116
+
117
+ @dataclass
118
+ class _MergedNode:
119
+ id: str
120
+ type: str
121
+ label: str | None
122
+ bbox_px: tuple[float, float, float, float]
123
+ center: tuple[float, float]
124
+ source_tiles: list[str] = field(default_factory=list)
125
+
126
+
127
+ def merge_tile_graphs(
128
+ tile_results: Iterable[tuple[GraphOut, Tile]],
129
+ dedup_px: float = 40.0,
130
+ ) -> dict:
131
+ """Merge per-tile predictions into one global graph.
132
+
133
+ Two nodes are considered the same symbol iff they share the same
134
+ `type` AND their bbox centers are within `dedup_px` pixels in the
135
+ global (un-tiled) image coordinate space. The first occurrence
136
+ wins; later duplicates are absorbed and their tile name is recorded
137
+ on `source_tiles` for debugging.
138
+
139
+ Edges are re-wired through the tile-local → global id map. An edge
140
+ whose endpoints map to the same global node is dropped, and
141
+ undirected duplicates across tiles are collapsed.
142
+
143
+ Returns a plain dict matching the shape `metrics.evaluate()` expects,
144
+ plus a `directed: False` flag. Extra `bbox` and `source_tiles` fields
145
+ on each node are ignored by the evaluator but useful for visualizing
146
+ predictions later.
147
+ """
148
+ global_nodes: list[_MergedNode] = []
149
+ id_map: dict[tuple[str, str], str] = {} # (tile.name, tile_local_id) -> global id
150
+ next_id = 1
151
+
152
+ tile_results = list(tile_results)
153
+
154
+ # --- nodes: dedup by (type, bbox center distance) -------------------
155
+ for graph, tile in tile_results:
156
+ for node in graph.nodes:
157
+ if node.bbox is None:
158
+ # No bbox → can't dedupe across tiles safely. Skip.
159
+ continue
160
+ bbox_px = _tile_bbox_to_global_px(node.bbox, tile)
161
+ cx, cy = _center(bbox_px)
162
+
163
+ matched_gid: str | None = None
164
+ for gn in global_nodes:
165
+ if gn.type != node.type:
166
+ continue
167
+ dx = gn.center[0] - cx
168
+ dy = gn.center[1] - cy
169
+ if (dx * dx + dy * dy) ** 0.5 <= dedup_px:
170
+ matched_gid = gn.id
171
+ break
172
+
173
+ if matched_gid is None:
174
+ gid = f"n{next_id}"
175
+ next_id += 1
176
+ global_nodes.append(
177
+ _MergedNode(
178
+ id=gid,
179
+ type=node.type,
180
+ label=node.label,
181
+ bbox_px=bbox_px,
182
+ center=(cx, cy),
183
+ source_tiles=[tile.name],
184
+ )
185
+ )
186
+ id_map[(tile.name, node.id)] = gid
187
+ else:
188
+ id_map[(tile.name, node.id)] = matched_gid
189
+ for gn in global_nodes:
190
+ if gn.id == matched_gid:
191
+ if tile.name not in gn.source_tiles:
192
+ gn.source_tiles.append(tile.name)
193
+ break
194
+
195
+ # --- edges: remap ids, drop self-loops and duplicates ---------------
196
+ edge_set: set[tuple[str, str]] = set()
197
+ final_edges: list[dict] = []
198
+ for graph, tile in tile_results:
199
+ for edge in graph.edges:
200
+ src = id_map.get((tile.name, edge.source))
201
+ tgt = id_map.get((tile.name, edge.target))
202
+ if src is None or tgt is None:
203
+ continue
204
+ if src == tgt:
205
+ continue
206
+ a, b = sorted((src, tgt)) # undirected canonicalization
207
+ key = (a, b)
208
+ if key in edge_set:
209
+ continue
210
+ edge_set.add(key)
211
+ final_edges.append(
212
+ {
213
+ "source": a,
214
+ "target": b,
215
+ "type": edge.type or "solid",
216
+ "label": edge.label,
217
+ }
218
+ )
219
+
220
+ out_nodes = [
221
+ {
222
+ "id": gn.id,
223
+ "type": gn.type,
224
+ "label": gn.label,
225
+ "bbox": list(gn.bbox_px),
226
+ "source_tiles": gn.source_tiles,
227
+ }
228
+ for gn in global_nodes
229
+ ]
230
+
231
+ return {
232
+ "nodes": out_nodes,
233
+ "edges": final_edges,
234
+ "directed": False,
235
+ }
236
+
237
+
238
+ def _inner_seam_lines(tiles: list[Tile]) -> tuple[list[float], list[float]]:
239
+ """Return (vertical_seam_xs, horizontal_seam_ys) for inner tile borders.
240
+
241
+ For 2x2 tiling with overlap, each internal seam corresponds to TWO
242
+ pixel x-coordinates: where one tile ends and where the adjacent tile
243
+ begins. Both are returned so the FP filter can match either edge of
244
+ the overlap band.
245
+ """
246
+ if not tiles:
247
+ return [], []
248
+ parent_w = tiles[0].parent_w
249
+ parent_h = tiles[0].parent_h
250
+ vx: set[float] = set()
251
+ hy: set[float] = set()
252
+ for t in tiles:
253
+ if t.x0 > 0:
254
+ vx.add(float(t.x0))
255
+ if t.x0 + t.w < parent_w:
256
+ vx.add(float(t.x0 + t.w))
257
+ if t.y0 > 0:
258
+ hy.add(float(t.y0))
259
+ if t.y0 + t.h < parent_h:
260
+ hy.add(float(t.y0 + t.h))
261
+ return sorted(vx), sorted(hy)
262
+
263
+
264
+ def filter_seam_artifacts(
265
+ merged: dict,
266
+ tiles: list[Tile],
267
+ types: tuple[str, ...] = ("inlet/outlet",),
268
+ seam_threshold: float = 50.0,
269
+ edge_threshold: float = 30.0,
270
+ ) -> dict:
271
+ """Drop nodes that look like tile-boundary false positives.
272
+
273
+ A node is filtered iff ALL of the following hold:
274
+ * its `type` is in `types` (default: inlet/outlet only),
275
+ * its bbox center lies within `seam_threshold` pixels of any inner
276
+ tile seam (vertical OR horizontal), AND
277
+ * its bbox center is NOT within `edge_threshold` pixels of the
278
+ outer image border (real boundary inlet/outlets stay).
279
+
280
+ Edges referencing dropped nodes are also removed. The dropped nodes
281
+ are recorded under `seam_filtered` so the report can show what was
282
+ pruned.
283
+
284
+ Nodes whose `type` is not in `types`, or that have no `bbox`, are
285
+ passed through untouched.
286
+ """
287
+ if not tiles:
288
+ return merged
289
+
290
+ parent_w = tiles[0].parent_w
291
+ parent_h = tiles[0].parent_h
292
+ vx, hy = _inner_seam_lines(tiles)
293
+ types_set = set(types)
294
+
295
+ keep_ids: set[str] = set()
296
+ new_nodes: list[dict] = []
297
+ dropped: list[dict] = []
298
+
299
+ for n in merged["nodes"]:
300
+ if n.get("type") not in types_set:
301
+ keep_ids.add(n["id"])
302
+ new_nodes.append(n)
303
+ continue
304
+
305
+ bbox = n.get("bbox")
306
+ if not bbox or len(bbox) != 4:
307
+ keep_ids.add(n["id"])
308
+ new_nodes.append(n)
309
+ continue
310
+
311
+ cx = (bbox[0] + bbox[2]) / 2.0
312
+ cy = (bbox[1] + bbox[3]) / 2.0
313
+
314
+ near_outer = (
315
+ cx < edge_threshold
316
+ or cx > parent_w - edge_threshold
317
+ or cy < edge_threshold
318
+ or cy > parent_h - edge_threshold
319
+ )
320
+ near_seam_x = any(abs(cx - x) <= seam_threshold for x in vx)
321
+ near_seam_y = any(abs(cy - y) <= seam_threshold for y in hy)
322
+ near_seam = near_seam_x or near_seam_y
323
+
324
+ if near_seam and not near_outer:
325
+ dropped.append({"id": n["id"], "type": n["type"], "bbox": bbox})
326
+ continue
327
+
328
+ keep_ids.add(n["id"])
329
+ new_nodes.append(n)
330
+
331
+ new_edges = [
332
+ e
333
+ for e in merged["edges"]
334
+ if e["source"] in keep_ids and e["target"] in keep_ids
335
+ ]
336
+
337
+ out = dict(merged)
338
+ out["nodes"] = new_nodes
339
+ out["edges"] = new_edges
340
+ out["seam_filtered"] = dropped
341
+ return out
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ anthropic>=0.40.0
2
+ pydantic>=2.0
3
+ networkx>=3.0
4
+ Pillow>=10.0
5
+ tqdm>=4.65
6
+ # gradio 5 requires Python 3.10+; stay on 4.x for Python 3.9 compat
7
+ gradio>=4.44,<5.0
8
+ # gradio 4.x imports HfFolder which was removed in huggingface_hub 1.0
9
+ huggingface_hub>=0.26,<1.0
10
+ matplotlib>=3.7
11
+ python-dotenv>=1.0
samples/open100_00_large.graphml ADDED
The diff for this file is too large to render. See raw diff
 
samples/open100_00_large.png ADDED

Git LFS Details

  • SHA256: 2451fbe9b8d0494a822901493cfbf1d2656f0bd7678f0f25bb2266b5e78a7dc9
  • Pointer size: 131 Bytes
  • Size of remote file: 842 kB
samples/open100_01_small.graphml ADDED
The diff for this file is too large to render. See raw diff
 
samples/open100_01_small.png ADDED

Git LFS Details

  • SHA256: c8c696042bf7a16eb30e492a312af274c312a2049a7eb765ca82f84d3770eba8
  • Pointer size: 131 Bytes
  • Size of remote file: 795 kB
samples/open100_03_medium.graphml ADDED
@@ -0,0 +1,2812 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version='1.0' encoding='utf-8'?>
2
+ <graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
3
+ <key id="d9" for="edge" attr.name="edge_label" attr.type="string" />
4
+ <key id="d8" for="node" attr.name="ymax" attr.type="long" />
5
+ <key id="d7" for="node" attr.name="xmax" attr.type="long" />
6
+ <key id="d6" for="node" attr.name="ymin" attr.type="long" />
7
+ <key id="d5" for="node" attr.name="xmin" attr.type="long" />
8
+ <key id="d4" for="node" attr.name="ymax" attr.type="double" />
9
+ <key id="d3" for="node" attr.name="ymin" attr.type="double" />
10
+ <key id="d2" for="node" attr.name="xmax" attr.type="double" />
11
+ <key id="d1" for="node" attr.name="xmin" attr.type="double" />
12
+ <key id="d0" for="node" attr.name="label" attr.type="string" />
13
+ <graph edgedefault="undirected">
14
+ <node id="arrow1">
15
+ <data key="d0">arrow</data>
16
+ <data key="d1">435.2</data>
17
+ <data key="d2">443.81</data>
18
+ <data key="d3">1133.9</data>
19
+ <data key="d4">1145.2</data>
20
+ </node>
21
+ <node id="arrow2">
22
+ <data key="d0">arrow</data>
23
+ <data key="d1">938.16</data>
24
+ <data key="d2">950.2</data>
25
+ <data key="d3">1141.49</data>
26
+ <data key="d4">1151.6</data>
27
+ </node>
28
+ <node id="instrumentation3">
29
+ <data key="d0">instrumentation</data>
30
+ <data key="d1">548.81</data>
31
+ <data key="d2">585.88</data>
32
+ <data key="d3">752.27</data>
33
+ <data key="d4">787.96</data>
34
+ </node>
35
+ <node id="arrow4">
36
+ <data key="d0">arrow</data>
37
+ <data key="d1">947.02</data>
38
+ <data key="d2">956.5</data>
39
+ <data key="d3">974.77</data>
40
+ <data key="d4">987.7</data>
41
+ </node>
42
+ <node id="arrow5">
43
+ <data key="d0">arrow</data>
44
+ <data key="d1">1050.04</data>
45
+ <data key="d2">1060.16</data>
46
+ <data key="d3">326.4</data>
47
+ <data key="d4">338.08</data>
48
+ </node>
49
+ <node id="background6">
50
+ <data key="d0">background</data>
51
+ <data key="d1">0.0</data>
52
+ <data key="d2">2290.0</data>
53
+ <data key="d3">0.0</data>
54
+ <data key="d4">42.19</data>
55
+ </node>
56
+ <node id="arrow7">
57
+ <data key="d0">arrow</data>
58
+ <data key="d1">1132.0</data>
59
+ <data key="d2">1140.19</data>
60
+ <data key="d3">807.19</data>
61
+ <data key="d4">819.4</data>
62
+ </node>
63
+ <node id="valve8">
64
+ <data key="d0">valve</data>
65
+ <data key="d1">686.6</data>
66
+ <data key="d2">705.0</data>
67
+ <data key="d3">412.24</data>
68
+ <data key="d4">423.8</data>
69
+ </node>
70
+ <node id="general9">
71
+ <data key="d0">general</data>
72
+ <data key="d1">326.69</data>
73
+ <data key="d2">333.49</data>
74
+ <data key="d3">524.17</data>
75
+ <data key="d4">535.47</data>
76
+ </node>
77
+ <node id="valve10">
78
+ <data key="d0">valve</data>
79
+ <data key="d1">1389.74</data>
80
+ <data key="d2">1408.61</data>
81
+ <data key="d3">588.82</data>
82
+ <data key="d4">620.35</data>
83
+ </node>
84
+ <node id="instrumentation11">
85
+ <data key="d0">instrumentation</data>
86
+ <data key="d1">895.0</data>
87
+ <data key="d2">931.05</data>
88
+ <data key="d3">328.63</data>
89
+ <data key="d4">364.32</data>
90
+ </node>
91
+ <node id="valve12">
92
+ <data key="d0">valve</data>
93
+ <data key="d1">1263.56</data>
94
+ <data key="d2">1282.56</data>
95
+ <data key="d3">959.3</data>
96
+ <data key="d4">973.12</data>
97
+ </node>
98
+ <node id="general13">
99
+ <data key="d0">general</data>
100
+ <data key="d1">326.69</data>
101
+ <data key="d2">333.49</data>
102
+ <data key="d3">620.8</data>
103
+ <data key="d4">632.1</data>
104
+ </node>
105
+ <node id="tank14">
106
+ <data key="d0">tank</data>
107
+ <data key="d1">385.98</data>
108
+ <data key="d2">668.68</data>
109
+ <data key="d3">352.8</data>
110
+ <data key="d4">435.92</data>
111
+ </node>
112
+ <node id="general15">
113
+ <data key="d0">general</data>
114
+ <data key="d1">416.18</data>
115
+ <data key="d2">430.91</data>
116
+ <data key="d3">335.68</data>
117
+ <data key="d4">344.36</data>
118
+ </node>
119
+ <node id="inlet/outlet16">
120
+ <data key="d0">inlet/outlet</data>
121
+ <data key="d1">1749.4</data>
122
+ <data key="d2">1912.0</data>
123
+ <data key="d3">595.9</data>
124
+ <data key="d4">632.0</data>
125
+ </node>
126
+ <node id="valve17">
127
+ <data key="d0">valve</data>
128
+ <data key="d1">749.65</data>
129
+ <data key="d2">768.85</data>
130
+ <data key="d3">620.17</data>
131
+ <data key="d4">632.57</data>
132
+ </node>
133
+ <node id="tank18">
134
+ <data key="d0">tank</data>
135
+ <data key="d1">334.14</data>
136
+ <data key="d2">736.83</data>
137
+ <data key="d3">496.71</data>
138
+ <data key="d4">660.73</data>
139
+ </node>
140
+ <node id="valve19">
141
+ <data key="d0">valve</data>
142
+ <data key="d1">485.86</data>
143
+ <data key="d2">504.05</data>
144
+ <data key="d3">1115.7</data>
145
+ <data key="d4">1153.58</data>
146
+ </node>
147
+ <node id="instrumentation20">
148
+ <data key="d0">instrumentation</data>
149
+ <data key="d1">661.06</data>
150
+ <data key="d2">698.13</data>
151
+ <data key="d3">448.36</data>
152
+ <data key="d4">484.05</data>
153
+ </node>
154
+ <node id="pump21">
155
+ <data key="d0">pump</data>
156
+ <data key="d1">596.74</data>
157
+ <data key="d2">722.31</data>
158
+ <data key="d3">1199.17</data>
159
+ <data key="d4">1250.5</data>
160
+ </node>
161
+ <node id="valve22">
162
+ <data key="d0">valve</data>
163
+ <data key="d1">781.09</data>
164
+ <data key="d2">800.43</data>
165
+ <data key="d3">789.01</data>
166
+ <data key="d4">824.64</data>
167
+ </node>
168
+ <node id="valve23">
169
+ <data key="d0">valve</data>
170
+ <data key="d1">874.46</data>
171
+ <data key="d2">894.18</data>
172
+ <data key="d3">357.62</data>
173
+ <data key="d4">389.8</data>
174
+ </node>
175
+ <node id="valve24">
176
+ <data key="d0">valve</data>
177
+ <data key="d1">989.34</data>
178
+ <data key="d2">1009.12</data>
179
+ <data key="d3">941.32</data>
180
+ <data key="d4">973.28</data>
181
+ </node>
182
+ <node id="general25">
183
+ <data key="d0">general</data>
184
+ <data key="d1">520.6</data>
185
+ <data key="d2">535.32</data>
186
+ <data key="d3">335.8</data>
187
+ <data key="d4">344.48</data>
188
+ </node>
189
+ <node id="valve26">
190
+ <data key="d0">valve</data>
191
+ <data key="d1">1049.6</data>
192
+ <data key="d2">1061.17</data>
193
+ <data key="d3">917.12</data>
194
+ <data key="d4">936.15</data>
195
+ </node>
196
+ <node id="valve27">
197
+ <data key="d0">valve</data>
198
+ <data key="d1">1081.87</data>
199
+ <data key="d2">1093.44</data>
200
+ <data key="d3">917.2</data>
201
+ <data key="d4">936.22</data>
202
+ </node>
203
+ <node id="instrumentation28">
204
+ <data key="d0">instrumentation</data>
205
+ <data key="d1">1312.48</data>
206
+ <data key="d2">1349.55</data>
207
+ <data key="d3">661.02</data>
208
+ <data key="d4">696.71</data>
209
+ </node>
210
+ <node id="valve29">
211
+ <data key="d0">valve</data>
212
+ <data key="d1">1106.2</data>
213
+ <data key="d2">1142.39</data>
214
+ <data key="d3">876.76</data>
215
+ <data key="d4">895.78</data>
216
+ </node>
217
+ <node id="instrumentation30">
218
+ <data key="d0">instrumentation</data>
219
+ <data key="d1">676.84</data>
220
+ <data key="d2">713.91</data>
221
+ <data key="d3">751.63</data>
222
+ <data key="d4">787.32</data>
223
+ </node>
224
+ <node id="general31">
225
+ <data key="d0">general</data>
226
+ <data key="d1">585.29</data>
227
+ <data key="d2">600.02</data>
228
+ <data key="d3">335.8</data>
229
+ <data key="d4">344.48</data>
230
+ </node>
231
+ <node id="instrumentation32">
232
+ <data key="d0">instrumentation</data>
233
+ <data key="d1">863.03</data>
234
+ <data key="d2">900.1</data>
235
+ <data key="d3">465.28</data>
236
+ <data key="d4">500.97</data>
237
+ </node>
238
+ <node id="general33">
239
+ <data key="d0">general</data>
240
+ <data key="d1">519.81</data>
241
+ <data key="d2">534.53</data>
242
+ <data key="d3">444.15</data>
243
+ <data key="d4">452.83</data>
244
+ </node>
245
+ <node id="instrumentation34">
246
+ <data key="d0">instrumentation</data>
247
+ <data key="d1">822.41</data>
248
+ <data key="d2">859.48</data>
249
+ <data key="d3">576.3</data>
250
+ <data key="d4">611.99</data>
251
+ </node>
252
+ <node id="instrumentation35">
253
+ <data key="d0">instrumentation</data>
254
+ <data key="d1">862.82</data>
255
+ <data key="d2">899.89</data>
256
+ <data key="d3">576.93</data>
257
+ <data key="d4">612.63</data>
258
+ </node>
259
+ <node id="arrow36">
260
+ <data key="d0">arrow</data>
261
+ <data key="d1">937.79</data>
262
+ <data key="d2">949.8</data>
263
+ <data key="d3">814.05</data>
264
+ <data key="d4">822.4</data>
265
+ </node>
266
+ <node id="general37">
267
+ <data key="d0">general</data>
268
+ <data key="d1">1480.56</data>
269
+ <data key="d2">1493.46</data>
270
+ <data key="d3">907.16</data>
271
+ <data key="d4">914.66</data>
272
+ </node>
273
+ <node id="instrumentation38">
274
+ <data key="d0">instrumentation</data>
275
+ <data key="d1">749.0</data>
276
+ <data key="d2">783.67</data>
277
+ <data key="d3">774.18</data>
278
+ <data key="d4">809.0</data>
279
+ </node>
280
+ <node id="general39">
281
+ <data key="d0">general</data>
282
+ <data key="d1">1360.8</data>
283
+ <data key="d2">1372.7</data>
284
+ <data key="d3">906.9</data>
285
+ <data key="d4">913.8</data>
286
+ </node>
287
+ <node id="general40">
288
+ <data key="d0">general</data>
289
+ <data key="d1">1506.12</data>
290
+ <data key="d2">1517.72</data>
291
+ <data key="d3">986.19</data>
292
+ <data key="d4">993.69</data>
293
+ </node>
294
+ <node id="instrumentation41">
295
+ <data key="d0">instrumentation</data>
296
+ <data key="d1">1269.09</data>
297
+ <data key="d2">1306.16</data>
298
+ <data key="d3">660.81</data>
299
+ <data key="d4">696.5</data>
300
+ </node>
301
+ <node id="general42">
302
+ <data key="d0">general</data>
303
+ <data key="d1">520.86</data>
304
+ <data key="d2">535.58</data>
305
+ <data key="d3">479.13</data>
306
+ <data key="d4">487.8</data>
307
+ </node>
308
+ <node id="general43">
309
+ <data key="d0">general</data>
310
+ <data key="d1">1300.56</data>
311
+ <data key="d2">1308.92</data>
312
+ <data key="d3">959.44</data>
313
+ <data key="d4">972.26</data>
314
+ </node>
315
+ <node id="general44">
316
+ <data key="d0">general</data>
317
+ <data key="d1">1301.57</data>
318
+ <data key="d2">1308.92</data>
319
+ <data key="d3">927.62</data>
320
+ <data key="d4">941.3</data>
321
+ </node>
322
+ <node id="pump45">
323
+ <data key="d0">pump</data>
324
+ <data key="d1">597.23</data>
325
+ <data key="d2">721.6</data>
326
+ <data key="d3">865.6</data>
327
+ <data key="d4">913.39</data>
328
+ </node>
329
+ <node id="general46">
330
+ <data key="d0">general</data>
331
+ <data key="d1">1263.7</data>
332
+ <data key="d2">1282.85</data>
333
+ <data key="d3">928.63</data>
334
+ <data key="d4">940.9</data>
335
+ </node>
336
+ <node id="arrow47">
337
+ <data key="d0">arrow</data>
338
+ <data key="d1">1157.81</data>
339
+ <data key="d2">1170.8</data>
340
+ <data key="d3">961.31</data>
341
+ <data key="d4">971.42</data>
342
+ </node>
343
+ <node id="general48">
344
+ <data key="d0">general</data>
345
+ <data key="d1">673.52</data>
346
+ <data key="d2">680.92</data>
347
+ <data key="d3">412.05</data>
348
+ <data key="d4">423.85</data>
349
+ </node>
350
+ <node id="general49">
351
+ <data key="d0">general</data>
352
+ <data key="d1">676.62</data>
353
+ <data key="d2">684.82</data>
354
+ <data key="d3">377.15</data>
355
+ <data key="d4">390.05</data>
356
+ </node>
357
+ <node id="general50">
358
+ <data key="d0">general</data>
359
+ <data key="d1">737.8</data>
360
+ <data key="d2">744.6</data>
361
+ <data key="d3">620.6</data>
362
+ <data key="d4">631.9</data>
363
+ </node>
364
+ <node id="instrumentation51">
365
+ <data key="d0">instrumentation</data>
366
+ <data key="d1">1004.17</data>
367
+ <data key="d2">1039.2</data>
368
+ <data key="d3">913.3</data>
369
+ <data key="d4">947.57</data>
370
+ </node>
371
+ <node id="general52">
372
+ <data key="d0">general</data>
373
+ <data key="d1">726.63</data>
374
+ <data key="d2">768.7</data>
375
+ <data key="d3">915.62</data>
376
+ <data key="d4">955.0</data>
377
+ </node>
378
+ <node id="instrumentation53">
379
+ <data key="d0">instrumentation</data>
380
+ <data key="d1">1097.43</data>
381
+ <data key="d2">1132.0</data>
382
+ <data key="d3">898.94</data>
383
+ <data key="d4">934.0</data>
384
+ </node>
385
+ <node id="general54">
386
+ <data key="d0">general</data>
387
+ <data key="d1">432.43</data>
388
+ <data key="d2">447.16</data>
389
+ <data key="d3">667.14</data>
390
+ <data key="d4">675.82</data>
391
+ </node>
392
+ <node id="general55">
393
+ <data key="d0">general</data>
394
+ <data key="d1">1316.55</data>
395
+ <data key="d2">1563.88</data>
396
+ <data key="d3">916.39</data>
397
+ <data key="d4">983.79</data>
398
+ </node>
399
+ <node id="arrow56">
400
+ <data key="d0">arrow</data>
401
+ <data key="d1">1225.8</data>
402
+ <data key="d2">1237.9</data>
403
+ <data key="d3">800.83</data>
404
+ <data key="d4">810.93</data>
405
+ </node>
406
+ <node id="instrumentation57">
407
+ <data key="d0">instrumentation</data>
408
+ <data key="d1">452.9</data>
409
+ <data key="d2">486.42</data>
410
+ <data key="d3">1102.1</data>
411
+ <data key="d4">1136.95</data>
412
+ </node>
413
+ <node id="arrow58">
414
+ <data key="d0">arrow</data>
415
+ <data key="d1">644.8</data>
416
+ <data key="d2">652.31</data>
417
+ <data key="d3">1148.25</data>
418
+ <data key="d4">1159.3</data>
419
+ </node>
420
+ <node id="instrumentation59">
421
+ <data key="d0">instrumentation</data>
422
+ <data key="d1">678.44</data>
423
+ <data key="d2">713.1</data>
424
+ <data key="d3">1081.1</data>
425
+ <data key="d4">1115.15</data>
426
+ </node>
427
+ <node id="valve60">
428
+ <data key="d0">valve</data>
429
+ <data key="d1">766.7</data>
430
+ <data key="d2">784.6</data>
431
+ <data key="d3">1116.45</data>
432
+ <data key="d4">1151.9</data>
433
+ </node>
434
+ <node id="instrumentation61">
435
+ <data key="d0">instrumentation</data>
436
+ <data key="d1">461.3</data>
437
+ <data key="d2">494.7</data>
438
+ <data key="d3">774.7</data>
439
+ <data key="d4">808.04</data>
440
+ </node>
441
+ <node id="instrumentation62">
442
+ <data key="d0">instrumentation</data>
443
+ <data key="d1">1149.56</data>
444
+ <data key="d2">1185.4</data>
445
+ <data key="d3">745.03</data>
446
+ <data key="d4">778.9</data>
447
+ </node>
448
+ <node id="arrow63">
449
+ <data key="d0">arrow</data>
450
+ <data key="d1">643.04</data>
451
+ <data key="d2">653.16</data>
452
+ <data key="d3">820.4</data>
453
+ <data key="d4">831.4</data>
454
+ </node>
455
+ <node id="background64">
456
+ <data key="d0">background</data>
457
+ <data key="d1">1945.53</data>
458
+ <data key="d2">2290.0</data>
459
+ <data key="d3">0.0</data>
460
+ <data key="d4">1536.0</data>
461
+ </node>
462
+ <node id="arrow65">
463
+ <data key="d0">arrow</data>
464
+ <data key="d1">997.79</data>
465
+ <data key="d2">1007.92</data>
466
+ <data key="d3">494.86</data>
467
+ <data key="d4">510.68</data>
468
+ </node>
469
+ <node id="instrumentation66">
470
+ <data key="d0">instrumentation</data>
471
+ <data key="d1">1406.6</data>
472
+ <data key="d2">1441.43</data>
473
+ <data key="d3">560.3</data>
474
+ <data key="d4">594.62</data>
475
+ </node>
476
+ <node id="valve67">
477
+ <data key="d0">valve</data>
478
+ <data key="d1">494.01</data>
479
+ <data key="d2">513.32</data>
480
+ <data key="d3">788.4</data>
481
+ <data key="d4">823.99</data>
482
+ </node>
483
+ <node id="instrumentation68">
484
+ <data key="d0">instrumentation</data>
485
+ <data key="d1">976.16</data>
486
+ <data key="d2">1013.23</data>
487
+ <data key="d3">1052.18</data>
488
+ <data key="d4">1087.88</data>
489
+ </node>
490
+ <node id="instrumentation69">
491
+ <data key="d0">instrumentation</data>
492
+ <data key="d1">1188.44</data>
493
+ <data key="d2">1225.51</data>
494
+ <data key="d3">916.47</data>
495
+ <data key="d4">952.16</data>
496
+ </node>
497
+ <node id="arrow70">
498
+ <data key="d0">arrow</data>
499
+ <data key="d1">1081.97</data>
500
+ <data key="d2">1092.1</data>
501
+ <data key="d3">509.91</data>
502
+ <data key="d4">525.72</data>
503
+ </node>
504
+ <node id="instrumentation71">
505
+ <data key="d0">instrumentation</data>
506
+ <data key="d1">1020.83</data>
507
+ <data key="d2">1057.9</data>
508
+ <data key="d3">1052.61</data>
509
+ <data key="d4">1088.3</data>
510
+ </node>
511
+ <node id="arrow72">
512
+ <data key="d0">arrow</data>
513
+ <data key="d1">1240.9</data>
514
+ <data key="d2">1253.98</data>
515
+ <data key="d3">929.46</data>
516
+ <data key="d4">939.57</data>
517
+ </node>
518
+ <node id="instrumentation73">
519
+ <data key="d0">instrumentation</data>
520
+ <data key="d1">549.39</data>
521
+ <data key="d2">586.47</data>
522
+ <data key="d3">1080.33</data>
523
+ <data key="d4">1116.02</data>
524
+ </node>
525
+ <node id="instrumentation74">
526
+ <data key="d0">instrumentation</data>
527
+ <data key="d1">715.51</data>
528
+ <data key="d2">752.58</data>
529
+ <data key="d3">343.29</data>
530
+ <data key="d4">378.98</data>
531
+ </node>
532
+ <node id="general75">
533
+ <data key="d0">general</data>
534
+ <data key="d1">724.4</data>
535
+ <data key="d2">768.0</data>
536
+ <data key="d3">1250.74</data>
537
+ <data key="d4">1290.8</data>
538
+ </node>
539
+ <node id="instrumentation76">
540
+ <data key="d0">instrumentation</data>
541
+ <data key="d1">731.62</data>
542
+ <data key="d2">768.69</data>
543
+ <data key="d3">1101.51</data>
544
+ <data key="d4">1137.2</data>
545
+ </node>
546
+ <node id="valve77">
547
+ <data key="d0">valve</data>
548
+ <data key="d1">813.5</data>
549
+ <data key="d2">832.94</data>
550
+ <data key="d3">1137.38</data>
551
+ <data key="d4">1152.83</data>
552
+ </node>
553
+ <node id="instrumentation78">
554
+ <data key="d0">instrumentation</data>
555
+ <data key="d1">820.28</data>
556
+ <data key="d2">857.35</data>
557
+ <data key="d3">465.06</data>
558
+ <data key="d4">500.76</data>
559
+ </node>
560
+ <node id="instrumentation79">
561
+ <data key="d0">instrumentation</data>
562
+ <data key="d1">1460.82</data>
563
+ <data key="d2">1497.89</data>
564
+ <data key="d3">552.04</data>
565
+ <data key="d4">587.73</data>
566
+ </node>
567
+ <node id="background80">
568
+ <data key="d0">background</data>
569
+ <data key="d1">0.0</data>
570
+ <data key="d2">2285.51</data>
571
+ <data key="d3">1494.1</data>
572
+ <data key="d4">1536.0</data>
573
+ </node>
574
+ <node id="background81">
575
+ <data key="d0">background</data>
576
+ <data key="d1">0.0</data>
577
+ <data key="d2">33.65</data>
578
+ <data key="d3">0.0</data>
579
+ <data key="d4">1536.0</data>
580
+ </node>
581
+ <node id="inlet/outlet82">
582
+ <data key="d0">inlet/outlet</data>
583
+ <data key="d1">1749.6</data>
584
+ <data key="d2">1914.81</data>
585
+ <data key="d3">306.0</data>
586
+ <data key="d4">341.1</data>
587
+ </node>
588
+ <node id="instrumentation83">
589
+ <data key="d0">instrumentation</data>
590
+ <data key="d1">470.33</data>
591
+ <data key="d2">507.4</data>
592
+ <data key="d3">688.47</data>
593
+ <data key="d4">724.16</data>
594
+ </node>
595
+ <node id="arrow84">
596
+ <data key="d0">arrow</data>
597
+ <data key="d1">602.1</data>
598
+ <data key="d2">613.79</data>
599
+ <data key="d3">1142.3</data>
600
+ <data key="d4">1151.2</data>
601
+ </node>
602
+ <node id="instrumentation85">
603
+ <data key="d0">instrumentation</data>
604
+ <data key="d1">514.56</data>
605
+ <data key="d2">551.64</data>
606
+ <data key="d3">688.47</data>
607
+ <data key="d4">724.16</data>
608
+ </node>
609
+ <node id="valve86">
610
+ <data key="d0">valve</data>
611
+ <data key="d1">837.79</data>
612
+ <data key="d2">858.84</data>
613
+ <data key="d3">809.46</data>
614
+ <data key="d4">824.31</data>
615
+ </node>
616
+ <node id="instrumentation87">
617
+ <data key="d0">instrumentation</data>
618
+ <data key="d1">674.88</data>
619
+ <data key="d2">710.7</data>
620
+ <data key="d3">344.1</data>
621
+ <data key="d4">376.64</data>
622
+ </node>
623
+ <node id="instrumentation88">
624
+ <data key="d0">instrumentation</data>
625
+ <data key="d1">616.18</data>
626
+ <data key="d2">653.26</data>
627
+ <data key="d3">262.26</data>
628
+ <data key="d4">297.95</data>
629
+ </node>
630
+ <node id="inlet/outlet89">
631
+ <data key="d0">inlet/outlet</data>
632
+ <data key="d1">1749.2</data>
633
+ <data key="d2">1913.19</data>
634
+ <data key="d3">451.62</data>
635
+ <data key="d4">488.6</data>
636
+ </node>
637
+ <node id="instrumentation90">
638
+ <data key="d0">instrumentation</data>
639
+ <data key="d1">617.25</data>
640
+ <data key="d2">654.32</data>
641
+ <data key="d3">294.38</data>
642
+ <data key="d4">330.07</data>
643
+ </node>
644
+ <node id="instrumentation91">
645
+ <data key="d0">instrumentation</data>
646
+ <data key="d1">547.27</data>
647
+ <data key="d2">584.35</data>
648
+ <data key="d3">292.89</data>
649
+ <data key="d4">328.58</data>
650
+ </node>
651
+ <node id="arrow92">
652
+ <data key="d0">arrow</data>
653
+ <data key="d1">601.32</data>
654
+ <data key="d2">613.4</data>
655
+ <data key="d3">813.07</data>
656
+ <data key="d4">823.18</data>
657
+ </node>
658
+ <node id="connector93">
659
+ <data key="d0">connector</data>
660
+ <data key="d5">1001</data>
661
+ <data key="d6">507</data>
662
+ <data key="d7">1009</data>
663
+ <data key="d8">515</data>
664
+ </node>
665
+ <node id="crossing94">
666
+ <data key="d0">crossing</data>
667
+ <data key="d5">947</data>
668
+ <data key="d6">871</data>
669
+ <data key="d7">955</data>
670
+ <data key="d8">879</data>
671
+ </node>
672
+ <node id="crossing95">
673
+ <data key="d0">crossing</data>
674
+ <data key="d5">1001</data>
675
+ <data key="d6">871</data>
676
+ <data key="d7">1009</data>
677
+ <data key="d8">879</data>
678
+ </node>
679
+ <node id="connector96">
680
+ <data key="d0">connector</data>
681
+ <data key="d5">1084</data>
682
+ <data key="d6">913</data>
683
+ <data key="d7">1092</data>
684
+ <data key="d8">921</data>
685
+ </node>
686
+ <node id="connector97">
687
+ <data key="d0">connector</data>
688
+ <data key="d5">1084</data>
689
+ <data key="d6">522</data>
690
+ <data key="d7">1092</data>
691
+ <data key="d8">530</data>
692
+ </node>
693
+ <node id="connector98">
694
+ <data key="d0">connector</data>
695
+ <data key="d5">665</data>
696
+ <data key="d6">357</data>
697
+ <data key="d7">673</data>
698
+ <data key="d8">365</data>
699
+ </node>
700
+ <node id="connector99">
701
+ <data key="d0">connector</data>
702
+ <data key="d5">671</data>
703
+ <data key="d6">357</data>
704
+ <data key="d7">679</data>
705
+ <data key="d8">365</data>
706
+ </node>
707
+ <node id="connector100">
708
+ <data key="d0">connector</data>
709
+ <data key="d5">563</data>
710
+ <data key="d6">784</data>
711
+ <data key="d7">571</data>
712
+ <data key="d8">792</data>
713
+ </node>
714
+ <node id="crossing101">
715
+ <data key="d0">crossing</data>
716
+ <data key="d5">564</data>
717
+ <data key="d6">815</data>
718
+ <data key="d7">572</data>
719
+ <data key="d8">823</data>
720
+ </node>
721
+ <node id="connector102">
722
+ <data key="d0">connector</data>
723
+ <data key="d5">675</data>
724
+ <data key="d6">493</data>
725
+ <data key="d7">683</data>
726
+ <data key="d8">501</data>
727
+ </node>
728
+ <node id="connector103">
729
+ <data key="d0">connector</data>
730
+ <data key="d5">676</data>
731
+ <data key="d6">480</data>
732
+ <data key="d7">684</data>
733
+ <data key="d8">488</data>
734
+ </node>
735
+ <node id="connector104">
736
+ <data key="d0">connector</data>
737
+ <data key="d5">500</data>
738
+ <data key="d6">1143</data>
739
+ <data key="d7">508</data>
740
+ <data key="d8">1151</data>
741
+ </node>
742
+ <node id="crossing105">
743
+ <data key="d0">crossing</data>
744
+ <data key="d5">564</data>
745
+ <data key="d6">1142</data>
746
+ <data key="d7">572</data>
747
+ <data key="d8">1150</data>
748
+ </node>
749
+ <node id="connector106">
750
+ <data key="d0">connector</data>
751
+ <data key="d5">523</data>
752
+ <data key="d6">349</data>
753
+ <data key="d7">531</data>
754
+ <data key="d8">357</data>
755
+ </node>
756
+ <node id="connector107">
757
+ <data key="d0">connector</data>
758
+ <data key="d5">524</data>
759
+ <data key="d6">340</data>
760
+ <data key="d7">532</data>
761
+ <data key="d8">348</data>
762
+ </node>
763
+ <node id="connector108">
764
+ <data key="d0">connector</data>
765
+ <data key="d5">1260</data>
766
+ <data key="d6">963</data>
767
+ <data key="d7">1268</data>
768
+ <data key="d8">971</data>
769
+ </node>
770
+ <node id="crossing109">
771
+ <data key="d0">crossing</data>
772
+ <data key="d5">1202</data>
773
+ <data key="d6">962</data>
774
+ <data key="d7">1210</data>
775
+ <data key="d8">970</data>
776
+ </node>
777
+ <node id="connector110">
778
+ <data key="d0">connector</data>
779
+ <data key="d5">796</data>
780
+ <data key="d6">814</data>
781
+ <data key="d7">804</data>
782
+ <data key="d8">822</data>
783
+ </node>
784
+ <node id="connector111">
785
+ <data key="d0">connector</data>
786
+ <data key="d5">834</data>
787
+ <data key="d6">813</data>
788
+ <data key="d7">842</data>
789
+ <data key="d8">821</data>
790
+ </node>
791
+ <node id="connector112">
792
+ <data key="d0">connector</data>
793
+ <data key="d5">597</data>
794
+ <data key="d6">814</data>
795
+ <data key="d7">605</data>
796
+ <data key="d8">822</data>
797
+ </node>
798
+ <node id="connector113">
799
+ <data key="d0">connector</data>
800
+ <data key="d5">611</data>
801
+ <data key="d6">493</data>
802
+ <data key="d7">619</data>
803
+ <data key="d8">501</data>
804
+ </node>
805
+ <node id="connector114">
806
+ <data key="d0">connector</data>
807
+ <data key="d5">612</data>
808
+ <data key="d6">432</data>
809
+ <data key="d7">620</data>
810
+ <data key="d8">440</data>
811
+ </node>
812
+ <node id="connector115">
813
+ <data key="d0">connector</data>
814
+ <data key="d5">1133</data>
815
+ <data key="d6">803</data>
816
+ <data key="d7">1141</data>
817
+ <data key="d8">811</data>
818
+ </node>
819
+ <node id="crossing116">
820
+ <data key="d0">crossing</data>
821
+ <data key="d5">1164</data>
822
+ <data key="d6">802</data>
823
+ <data key="d7">1172</data>
824
+ <data key="d8">810</data>
825
+ </node>
826
+ <node id="crossing117">
827
+ <data key="d0">crossing</data>
828
+ <data key="d5">1133</data>
829
+ <data key="d6">802</data>
830
+ <data key="d7">1141</data>
831
+ <data key="d8">810</data>
832
+ </node>
833
+ <node id="connector118">
834
+ <data key="d0">connector</data>
835
+ <data key="d5">644</data>
836
+ <data key="d6">1144</data>
837
+ <data key="d7">652</data>
838
+ <data key="d8">1152</data>
839
+ </node>
840
+ <node id="crossing119">
841
+ <data key="d0">crossing</data>
842
+ <data key="d5">692</data>
843
+ <data key="d6">1143</data>
844
+ <data key="d7">700</data>
845
+ <data key="d8">1151</data>
846
+ </node>
847
+ <node id="crossing120">
848
+ <data key="d0">crossing</data>
849
+ <data key="d5">645</data>
850
+ <data key="d6">1143</data>
851
+ <data key="d7">653</data>
852
+ <data key="d8">1151</data>
853
+ </node>
854
+ <node id="crossing121">
855
+ <data key="d0">crossing</data>
856
+ <data key="d5">1083</data>
857
+ <data key="d6">963</data>
858
+ <data key="d7">1091</data>
859
+ <data key="d8">971</data>
860
+ </node>
861
+ <node id="crossing122">
862
+ <data key="d0">crossing</data>
863
+ <data key="d5">1133</data>
864
+ <data key="d6">963</data>
865
+ <data key="d7">1141</data>
866
+ <data key="d8">971</data>
867
+ </node>
868
+ <node id="connector123">
869
+ <data key="d0">connector</data>
870
+ <data key="d5">1051</data>
871
+ <data key="d6">322</data>
872
+ <data key="d7">1059</data>
873
+ <data key="d8">330</data>
874
+ </node>
875
+ <node id="connector124">
876
+ <data key="d0">connector</data>
877
+ <data key="d5">1746</data>
878
+ <data key="d6">319</data>
879
+ <data key="d7">1754</data>
880
+ <data key="d8">327</data>
881
+ </node>
882
+ <node id="crossing125">
883
+ <data key="d0">crossing</data>
884
+ <data key="d5">1051</data>
885
+ <data key="d6">319</data>
886
+ <data key="d7">1059</data>
887
+ <data key="d8">327</data>
888
+ </node>
889
+ <node id="connector126">
890
+ <data key="d0">connector</data>
891
+ <data key="d5">816</data>
892
+ <data key="d6">479</data>
893
+ <data key="d7">824</data>
894
+ <data key="d8">487</data>
895
+ </node>
896
+ <node id="crossing127">
897
+ <data key="d0">crossing</data>
898
+ <data key="d5">788</data>
899
+ <data key="d6">479</data>
900
+ <data key="d7">796</data>
901
+ <data key="d8">487</data>
902
+ </node>
903
+ <node id="connector128">
904
+ <data key="d0">connector</data>
905
+ <data key="d5">1052</data>
906
+ <data key="d6">334</data>
907
+ <data key="d7">1060</data>
908
+ <data key="d8">342</data>
909
+ </node>
910
+ <node id="connector129">
911
+ <data key="d0">connector</data>
912
+ <data key="d5">1051</data>
913
+ <data key="d6">913</data>
914
+ <data key="d7">1059</data>
915
+ <data key="d8">921</data>
916
+ </node>
917
+ <node id="connector130">
918
+ <data key="d0">connector</data>
919
+ <data key="d5">1164</data>
920
+ <data key="d6">775</data>
921
+ <data key="d7">1172</data>
922
+ <data key="d8">783</data>
923
+ </node>
924
+ <node id="connector131">
925
+ <data key="d0">connector</data>
926
+ <data key="d5">746</data>
927
+ <data key="d6">623</data>
928
+ <data key="d7">754</data>
929
+ <data key="d8">631</data>
930
+ </node>
931
+ <node id="crossing132">
932
+ <data key="d0">crossing</data>
933
+ <data key="d5">741</data>
934
+ <data key="d6">622</data>
935
+ <data key="d7">749</data>
936
+ <data key="d8">630</data>
937
+ </node>
938
+ <node id="connector133">
939
+ <data key="d0">connector</data>
940
+ <data key="d5">1297</data>
941
+ <data key="d6">962</data>
942
+ <data key="d7">1305</data>
943
+ <data key="d8">970</data>
944
+ </node>
945
+ <node id="connector134">
946
+ <data key="d0">connector</data>
947
+ <data key="d5">1279</data>
948
+ <data key="d6">962</data>
949
+ <data key="d7">1287</data>
950
+ <data key="d8">970</data>
951
+ </node>
952
+ <node id="connector135">
953
+ <data key="d0">connector</data>
954
+ <data key="d5">524</data>
955
+ <data key="d6">440</data>
956
+ <data key="d7">532</data>
957
+ <data key="d8">448</data>
958
+ </node>
959
+ <node id="connector136">
960
+ <data key="d0">connector</data>
961
+ <data key="d5">523</data>
962
+ <data key="d6">432</data>
963
+ <data key="d7">531</data>
964
+ <data key="d8">440</data>
965
+ </node>
966
+ <node id="connector137">
967
+ <data key="d0">connector</data>
968
+ <data key="d5">1508</data>
969
+ <data key="d6">980</data>
970
+ <data key="d7">1516</data>
971
+ <data key="d8">988</data>
972
+ </node>
973
+ <node id="connector138">
974
+ <data key="d0">connector</data>
975
+ <data key="d5">1509</data>
976
+ <data key="d6">982</data>
977
+ <data key="d7">1517</data>
978
+ <data key="d8">990</data>
979
+ </node>
980
+ <node id="connector139">
981
+ <data key="d0">connector</data>
982
+ <data key="d5">1123</data>
983
+ <data key="d6">895</data>
984
+ <data key="d7">1131</data>
985
+ <data key="d8">903</data>
986
+ </node>
987
+ <node id="connector140">
988
+ <data key="d0">connector</data>
989
+ <data key="d5">1126</data>
990
+ <data key="d6">892</data>
991
+ <data key="d7">1134</data>
992
+ <data key="d8">900</data>
993
+ </node>
994
+ <node id="connector141">
995
+ <data key="d0">connector</data>
996
+ <data key="d5">1204</data>
997
+ <data key="d6">948</data>
998
+ <data key="d7">1212</data>
999
+ <data key="d8">956</data>
1000
+ </node>
1001
+ <node id="connector142">
1002
+ <data key="d0">connector</data>
1003
+ <data key="d5">734</data>
1004
+ <data key="d6">622</data>
1005
+ <data key="d7">742</data>
1006
+ <data key="d8">630</data>
1007
+ </node>
1008
+ <node id="connector143">
1009
+ <data key="d0">connector</data>
1010
+ <data key="d5">733</data>
1011
+ <data key="d6">622</data>
1012
+ <data key="d7">741</data>
1013
+ <data key="d8">630</data>
1014
+ </node>
1015
+ <node id="connector144">
1016
+ <data key="d0">connector</data>
1017
+ <data key="d5">1250</data>
1018
+ <data key="d6">931</data>
1019
+ <data key="d7">1258</data>
1020
+ <data key="d8">939</data>
1021
+ </node>
1022
+ <node id="connector145">
1023
+ <data key="d0">connector</data>
1024
+ <data key="d5">1260</data>
1025
+ <data key="d6">931</data>
1026
+ <data key="d7">1268</data>
1027
+ <data key="d8">939</data>
1028
+ </node>
1029
+ <node id="connector146">
1030
+ <data key="d0">connector</data>
1031
+ <data key="d5">946</data>
1032
+ <data key="d6">814</data>
1033
+ <data key="d7">954</data>
1034
+ <data key="d8">822</data>
1035
+ </node>
1036
+ <node id="crossing147">
1037
+ <data key="d0">crossing</data>
1038
+ <data key="d5">948</data>
1039
+ <data key="d6">814</data>
1040
+ <data key="d7">956</data>
1041
+ <data key="d8">822</data>
1042
+ </node>
1043
+ <node id="connector148">
1044
+ <data key="d0">connector</data>
1045
+ <data key="d5">1222</data>
1046
+ <data key="d6">802</data>
1047
+ <data key="d7">1230</data>
1048
+ <data key="d8">810</data>
1049
+ </node>
1050
+ <node id="connector149">
1051
+ <data key="d0">connector</data>
1052
+ <data key="d5">855</data>
1053
+ <data key="d6">815</data>
1054
+ <data key="d7">863</data>
1055
+ <data key="d8">823</data>
1056
+ </node>
1057
+ <node id="connector150">
1058
+ <data key="d0">connector</data>
1059
+ <data key="d5">934</data>
1060
+ <data key="d6">814</data>
1061
+ <data key="d7">942</data>
1062
+ <data key="d8">822</data>
1063
+ </node>
1064
+ <node id="connector151">
1065
+ <data key="d0">connector</data>
1066
+ <data key="d5">829</data>
1067
+ <data key="d6">1142</data>
1068
+ <data key="d7">837</data>
1069
+ <data key="d8">1150</data>
1070
+ </node>
1071
+ <node id="connector152">
1072
+ <data key="d0">connector</data>
1073
+ <data key="d5">934</data>
1074
+ <data key="d6">1143</data>
1075
+ <data key="d7">942</data>
1076
+ <data key="d8">1151</data>
1077
+ </node>
1078
+ <node id="connector153">
1079
+ <data key="d0">connector</data>
1080
+ <data key="d5">1363</data>
1081
+ <data key="d6">910</data>
1082
+ <data key="d7">1371</data>
1083
+ <data key="d8">918</data>
1084
+ </node>
1085
+ <node id="connector154">
1086
+ <data key="d0">connector</data>
1087
+ <data key="d5">1363</data>
1088
+ <data key="d6">912</data>
1089
+ <data key="d7">1371</data>
1090
+ <data key="d8">920</data>
1091
+ </node>
1092
+ <node id="connector155">
1093
+ <data key="d0">connector</data>
1094
+ <data key="d5">645</data>
1095
+ <data key="d6">862</data>
1096
+ <data key="d7">653</data>
1097
+ <data key="d8">870</data>
1098
+ </node>
1099
+ <node id="connector156">
1100
+ <data key="d0">connector</data>
1101
+ <data key="d5">644</data>
1102
+ <data key="d6">827</data>
1103
+ <data key="d7">652</data>
1104
+ <data key="d8">835</data>
1105
+ </node>
1106
+ <node id="connector157">
1107
+ <data key="d0">connector</data>
1108
+ <data key="d5">436</data>
1109
+ <data key="d6">1130</data>
1110
+ <data key="d7">444</data>
1111
+ <data key="d8">1138</data>
1112
+ </node>
1113
+ <node id="crossing158">
1114
+ <data key="d0">crossing</data>
1115
+ <data key="d5">436</data>
1116
+ <data key="d6">815</data>
1117
+ <data key="d7">444</data>
1118
+ <data key="d8">823</data>
1119
+ </node>
1120
+ <node id="connector159">
1121
+ <data key="d0">connector</data>
1122
+ <data key="d5">524</data>
1123
+ <data key="d6">493</data>
1124
+ <data key="d7">532</data>
1125
+ <data key="d8">501</data>
1126
+ </node>
1127
+ <node id="connector160">
1128
+ <data key="d0">connector</data>
1129
+ <data key="d5">524</data>
1130
+ <data key="d6">484</data>
1131
+ <data key="d7">532</data>
1132
+ <data key="d8">492</data>
1133
+ </node>
1134
+ <node id="connector161">
1135
+ <data key="d0">connector</data>
1136
+ <data key="d5">1082</data>
1137
+ <data key="d6">506</data>
1138
+ <data key="d7">1090</data>
1139
+ <data key="d8">514</data>
1140
+ </node>
1141
+ <node id="connector162">
1142
+ <data key="d0">connector</data>
1143
+ <data key="d5">1745</data>
1144
+ <data key="d6">465</data>
1145
+ <data key="d7">1753</data>
1146
+ <data key="d8">473</data>
1147
+ </node>
1148
+ <node id="crossing163">
1149
+ <data key="d0">crossing</data>
1150
+ <data key="d5">1083</data>
1151
+ <data key="d6">467</data>
1152
+ <data key="d7">1091</data>
1153
+ <data key="d8">475</data>
1154
+ </node>
1155
+ <node id="connector164">
1156
+ <data key="d0">connector</data>
1157
+ <data key="d5">1405</data>
1158
+ <data key="d6">610</data>
1159
+ <data key="d7">1413</data>
1160
+ <data key="d8">618</data>
1161
+ </node>
1162
+ <node id="crossing165">
1163
+ <data key="d0">crossing</data>
1164
+ <data key="d5">1475</data>
1165
+ <data key="d6">611</data>
1166
+ <data key="d7">1483</data>
1167
+ <data key="d8">619</data>
1168
+ </node>
1169
+ <node id="connector166">
1170
+ <data key="d0">connector</data>
1171
+ <data key="d5">435</data>
1172
+ <data key="d6">672</data>
1173
+ <data key="d7">443</data>
1174
+ <data key="d8">680</data>
1175
+ </node>
1176
+ <node id="crossing167">
1177
+ <data key="d0">crossing</data>
1178
+ <data key="d5">436</data>
1179
+ <data key="d6">702</data>
1180
+ <data key="d7">444</data>
1181
+ <data key="d8">710</data>
1182
+ </node>
1183
+ <node id="connector168">
1184
+ <data key="d0">connector</data>
1185
+ <data key="d5">1132</data>
1186
+ <data key="d6">815</data>
1187
+ <data key="d7">1140</data>
1188
+ <data key="d8">823</data>
1189
+ </node>
1190
+ <node id="connector169">
1191
+ <data key="d0">connector</data>
1192
+ <data key="d5">1133</data>
1193
+ <data key="d6">873</data>
1194
+ <data key="d7">1141</data>
1195
+ <data key="d8">881</data>
1196
+ </node>
1197
+ <node id="crossing170">
1198
+ <data key="d0">crossing</data>
1199
+ <data key="d5">1052</data>
1200
+ <data key="d6">963</data>
1201
+ <data key="d7">1060</data>
1202
+ <data key="d8">971</data>
1203
+ </node>
1204
+ <node id="connector171">
1205
+ <data key="d0">connector</data>
1206
+ <data key="d5">1745</data>
1207
+ <data key="d6">610</data>
1208
+ <data key="d7">1753</data>
1209
+ <data key="d8">618</data>
1210
+ </node>
1211
+ <node id="connector172">
1212
+ <data key="d0">connector</data>
1213
+ <data key="d5">1475</data>
1214
+ <data key="d6">584</data>
1215
+ <data key="d7">1483</data>
1216
+ <data key="d8">592</data>
1217
+ </node>
1218
+ <node id="connector173">
1219
+ <data key="d0">connector</data>
1220
+ <data key="d5">329</data>
1221
+ <data key="d6">526</data>
1222
+ <data key="d7">337</data>
1223
+ <data key="d8">534</data>
1224
+ </node>
1225
+ <node id="connector174">
1226
+ <data key="d0">connector</data>
1227
+ <data key="d5">330</data>
1228
+ <data key="d6">526</data>
1229
+ <data key="d7">338</data>
1230
+ <data key="d8">534</data>
1231
+ </node>
1232
+ <node id="connector175">
1233
+ <data key="d0">connector</data>
1234
+ <data key="d5">644</data>
1235
+ <data key="d6">816</data>
1236
+ <data key="d7">652</data>
1237
+ <data key="d8">824</data>
1238
+ </node>
1239
+ <node id="crossing176">
1240
+ <data key="d0">crossing</data>
1241
+ <data key="d5">691</data>
1242
+ <data key="d6">814</data>
1243
+ <data key="d7">699</data>
1244
+ <data key="d8">822</data>
1245
+ </node>
1246
+ <node id="crossing177">
1247
+ <data key="d0">crossing</data>
1248
+ <data key="d5">643</data>
1249
+ <data key="d6">814</data>
1250
+ <data key="d7">651</data>
1251
+ <data key="d8">822</data>
1252
+ </node>
1253
+ <node id="connector178">
1254
+ <data key="d0">connector</data>
1255
+ <data key="d5">809</data>
1256
+ <data key="d6">1143</data>
1257
+ <data key="d7">817</data>
1258
+ <data key="d8">1151</data>
1259
+ </node>
1260
+ <node id="connector179">
1261
+ <data key="d0">connector</data>
1262
+ <data key="d5">781</data>
1263
+ <data key="d6">1142</data>
1264
+ <data key="d7">789</data>
1265
+ <data key="d8">1150</data>
1266
+ </node>
1267
+ <node id="connector180">
1268
+ <data key="d0">connector</data>
1269
+ <data key="d5">999</data>
1270
+ <data key="d6">491</data>
1271
+ <data key="d7">1007</data>
1272
+ <data key="d8">499</data>
1273
+ </node>
1274
+ <node id="connector181">
1275
+ <data key="d0">connector</data>
1276
+ <data key="d5">890</data>
1277
+ <data key="d6">379</data>
1278
+ <data key="d7">898</data>
1279
+ <data key="d8">387</data>
1280
+ </node>
1281
+ <node id="crossing182">
1282
+ <data key="d0">crossing</data>
1283
+ <data key="d5">999</data>
1284
+ <data key="d6">380</data>
1285
+ <data key="d7">1007</data>
1286
+ <data key="d8">388</data>
1287
+ </node>
1288
+ <node id="connector183">
1289
+ <data key="d0">connector</data>
1290
+ <data key="d5">1083</data>
1291
+ <data key="d6">932</data>
1292
+ <data key="d7">1091</data>
1293
+ <data key="d8">940</data>
1294
+ </node>
1295
+ <node id="connector184">
1296
+ <data key="d0">connector</data>
1297
+ <data key="d5">1154</data>
1298
+ <data key="d6">963</data>
1299
+ <data key="d7">1162</data>
1300
+ <data key="d8">971</data>
1301
+ </node>
1302
+ <node id="connector185">
1303
+ <data key="d0">connector</data>
1304
+ <data key="d5">562</data>
1305
+ <data key="d6">325</data>
1306
+ <data key="d7">570</data>
1307
+ <data key="d8">333</data>
1308
+ </node>
1309
+ <node id="connector186">
1310
+ <data key="d0">connector</data>
1311
+ <data key="d5">562</data>
1312
+ <data key="d6">349</data>
1313
+ <data key="d7">570</data>
1314
+ <data key="d8">357</data>
1315
+ </node>
1316
+ <node id="crossing187">
1317
+ <data key="d0">crossing</data>
1318
+ <data key="d5">1235</data>
1319
+ <data key="d6">673</data>
1320
+ <data key="d7">1243</data>
1321
+ <data key="d8">681</data>
1322
+ </node>
1323
+ <node id="crossing188">
1324
+ <data key="d0">crossing</data>
1325
+ <data key="d5">1236</data>
1326
+ <data key="d6">802</data>
1327
+ <data key="d7">1244</data>
1328
+ <data key="d8">810</data>
1329
+ </node>
1330
+ <node id="connector189">
1331
+ <data key="d0">connector</data>
1332
+ <data key="d5">818</data>
1333
+ <data key="d6">591</data>
1334
+ <data key="d7">826</data>
1335
+ <data key="d8">599</data>
1336
+ </node>
1337
+ <node id="crossing190">
1338
+ <data key="d0">crossing</data>
1339
+ <data key="d5">788</data>
1340
+ <data key="d6">591</data>
1341
+ <data key="d7">796</data>
1342
+ <data key="d8">599</data>
1343
+ </node>
1344
+ <node id="connector191">
1345
+ <data key="d0">connector</data>
1346
+ <data key="d5">691</data>
1347
+ <data key="d6">783</data>
1348
+ <data key="d7">699</data>
1349
+ <data key="d8">791</data>
1350
+ </node>
1351
+ <node id="connector192">
1352
+ <data key="d0">connector</data>
1353
+ <data key="d5">509</data>
1354
+ <data key="d6">814</data>
1355
+ <data key="d7">517</data>
1356
+ <data key="d8">822</data>
1357
+ </node>
1358
+ <node id="connector193">
1359
+ <data key="d0">connector</data>
1360
+ <data key="d5">691</data>
1361
+ <data key="d6">1111</data>
1362
+ <data key="d7">699</data>
1363
+ <data key="d8">1119</data>
1364
+ </node>
1365
+ <node id="connector194">
1366
+ <data key="d0">connector</data>
1367
+ <data key="d5">564</data>
1368
+ <data key="d6">1112</data>
1369
+ <data key="d7">572</data>
1370
+ <data key="d8">1120</data>
1371
+ </node>
1372
+ <node id="connector195">
1373
+ <data key="d0">connector</data>
1374
+ <data key="d5">1305</data>
1375
+ <data key="d6">930</data>
1376
+ <data key="d7">1313</data>
1377
+ <data key="d8">938</data>
1378
+ </node>
1379
+ <node id="connector196">
1380
+ <data key="d0">connector</data>
1381
+ <data key="d5">1313</data>
1382
+ <data key="d6">931</data>
1383
+ <data key="d7">1321</data>
1384
+ <data key="d8">939</data>
1385
+ </node>
1386
+ <node id="connector197">
1387
+ <data key="d0">connector</data>
1388
+ <data key="d5">890</data>
1389
+ <data key="d6">366</data>
1390
+ <data key="d7">898</data>
1391
+ <data key="d8">374</data>
1392
+ </node>
1393
+ <node id="connector198">
1394
+ <data key="d0">connector</data>
1395
+ <data key="d5">894</data>
1396
+ <data key="d6">360</data>
1397
+ <data key="d7">902</data>
1398
+ <data key="d8">368</data>
1399
+ </node>
1400
+ <node id="connector199">
1401
+ <data key="d0">connector</data>
1402
+ <data key="d5">1279</data>
1403
+ <data key="d6">930</data>
1404
+ <data key="d7">1287</data>
1405
+ <data key="d8">938</data>
1406
+ </node>
1407
+ <node id="connector200">
1408
+ <data key="d0">connector</data>
1409
+ <data key="d5">1298</data>
1410
+ <data key="d6">931</data>
1411
+ <data key="d7">1306</data>
1412
+ <data key="d8">939</data>
1413
+ </node>
1414
+ <node id="connector201">
1415
+ <data key="d0">connector</data>
1416
+ <data key="d5">436</data>
1417
+ <data key="d6">663</data>
1418
+ <data key="d7">444</data>
1419
+ <data key="d8">671</data>
1420
+ </node>
1421
+ <node id="connector202">
1422
+ <data key="d0">connector</data>
1423
+ <data key="d5">435</data>
1424
+ <data key="d6">657</data>
1425
+ <data key="d7">443</data>
1426
+ <data key="d8">665</data>
1427
+ </node>
1428
+ <node id="connector203">
1429
+ <data key="d0">connector</data>
1430
+ <data key="d5">490</data>
1431
+ <data key="d6">815</data>
1432
+ <data key="d7">498</data>
1433
+ <data key="d8">823</data>
1434
+ </node>
1435
+ <node id="connector204">
1436
+ <data key="d0">connector</data>
1437
+ <data key="d5">329</data>
1438
+ <data key="d6">623</data>
1439
+ <data key="d7">337</data>
1440
+ <data key="d8">631</data>
1441
+ </node>
1442
+ <node id="connector205">
1443
+ <data key="d0">connector</data>
1444
+ <data key="d5">330</data>
1445
+ <data key="d6">622</data>
1446
+ <data key="d7">338</data>
1447
+ <data key="d8">630</data>
1448
+ </node>
1449
+ <node id="connector206">
1450
+ <data key="d0">connector</data>
1451
+ <data key="d5">609</data>
1452
+ <data key="d6">814</data>
1453
+ <data key="d7">617</data>
1454
+ <data key="d8">822</data>
1455
+ </node>
1456
+ <node id="connector207">
1457
+ <data key="d0">connector</data>
1458
+ <data key="d5">612</data>
1459
+ <data key="d6">862</data>
1460
+ <data key="d7">620</data>
1461
+ <data key="d8">870</data>
1462
+ </node>
1463
+ <node id="crossing208">
1464
+ <data key="d0">crossing</data>
1465
+ <data key="d5">612</data>
1466
+ <data key="d6">814</data>
1467
+ <data key="d7">620</data>
1468
+ <data key="d8">822</data>
1469
+ </node>
1470
+ <node id="connector209">
1471
+ <data key="d0">connector</data>
1472
+ <data key="d5">1167</data>
1473
+ <data key="d6">962</data>
1474
+ <data key="d7">1175</data>
1475
+ <data key="d8">970</data>
1476
+ </node>
1477
+ <node id="connector210">
1478
+ <data key="d0">connector</data>
1479
+ <data key="d5">1050</data>
1480
+ <data key="d6">932</data>
1481
+ <data key="d7">1058</data>
1482
+ <data key="d8">940</data>
1483
+ </node>
1484
+ <node id="connector211">
1485
+ <data key="d0">connector</data>
1486
+ <data key="d5">452</data>
1487
+ <data key="d6">432</data>
1488
+ <data key="d7">460</data>
1489
+ <data key="d8">440</data>
1490
+ </node>
1491
+ <node id="connector212">
1492
+ <data key="d0">connector</data>
1493
+ <data key="d5">451</data>
1494
+ <data key="d6">493</data>
1495
+ <data key="d7">459</data>
1496
+ <data key="d8">501</data>
1497
+ </node>
1498
+ <node id="connector213">
1499
+ <data key="d0">connector</data>
1500
+ <data key="d5">1234</data>
1501
+ <data key="d6">802</data>
1502
+ <data key="d7">1242</data>
1503
+ <data key="d8">810</data>
1504
+ </node>
1505
+ <node id="connector214">
1506
+ <data key="d0">connector</data>
1507
+ <data key="d5">523</data>
1508
+ <data key="d6">475</data>
1509
+ <data key="d7">531</data>
1510
+ <data key="d8">483</data>
1511
+ </node>
1512
+ <node id="connector215">
1513
+ <data key="d0">connector</data>
1514
+ <data key="d5">524</data>
1515
+ <data key="d6">449</data>
1516
+ <data key="d7">532</data>
1517
+ <data key="d8">457</data>
1518
+ </node>
1519
+ <node id="connector216">
1520
+ <data key="d0">connector</data>
1521
+ <data key="d5">777</data>
1522
+ <data key="d6">816</data>
1523
+ <data key="d7">785</data>
1524
+ <data key="d8">824</data>
1525
+ </node>
1526
+ <node id="connector217">
1527
+ <data key="d0">connector</data>
1528
+ <data key="d5">765</data>
1529
+ <data key="d6">622</data>
1530
+ <data key="d7">773</data>
1531
+ <data key="d8">630</data>
1532
+ </node>
1533
+ <node id="crossing218">
1534
+ <data key="d0">crossing</data>
1535
+ <data key="d5">787</data>
1536
+ <data key="d6">622</data>
1537
+ <data key="d7">795</data>
1538
+ <data key="d8">630</data>
1539
+ </node>
1540
+ <node id="connector219">
1541
+ <data key="d0">connector</data>
1542
+ <data key="d5">490</data>
1543
+ <data key="d6">804</data>
1544
+ <data key="d7">498</data>
1545
+ <data key="d8">812</data>
1546
+ </node>
1547
+ <node id="connector220">
1548
+ <data key="d0">connector</data>
1549
+ <data key="d5">490</data>
1550
+ <data key="d6">804</data>
1551
+ <data key="d7">498</data>
1552
+ <data key="d8">812</data>
1553
+ </node>
1554
+ <node id="connector221">
1555
+ <data key="d0">connector</data>
1556
+ <data key="d5">598</data>
1557
+ <data key="d6">1143</data>
1558
+ <data key="d7">606</data>
1559
+ <data key="d8">1151</data>
1560
+ </node>
1561
+ <node id="connector222">
1562
+ <data key="d0">connector</data>
1563
+ <data key="d5">665</data>
1564
+ <data key="d6">414</data>
1565
+ <data key="d7">673</data>
1566
+ <data key="d8">422</data>
1567
+ </node>
1568
+ <node id="connector223">
1569
+ <data key="d0">connector</data>
1570
+ <data key="d5">670</data>
1571
+ <data key="d6">414</data>
1572
+ <data key="d7">678</data>
1573
+ <data key="d8">422</data>
1574
+ </node>
1575
+ <node id="connector224">
1576
+ <data key="d0">connector</data>
1577
+ <data key="d5">985</data>
1578
+ <data key="d6">963</data>
1579
+ <data key="d7">993</data>
1580
+ <data key="d8">971</data>
1581
+ </node>
1582
+ <node id="crossing225">
1583
+ <data key="d0">crossing</data>
1584
+ <data key="d5">948</data>
1585
+ <data key="d6">963</data>
1586
+ <data key="d7">956</data>
1587
+ <data key="d8">971</data>
1588
+ </node>
1589
+ <node id="connector226">
1590
+ <data key="d0">connector</data>
1591
+ <data key="d5">1132</data>
1592
+ <data key="d6">892</data>
1593
+ <data key="d7">1140</data>
1594
+ <data key="d8">900</data>
1595
+ </node>
1596
+ <node id="connector227">
1597
+ <data key="d0">connector</data>
1598
+ <data key="d5">701</data>
1599
+ <data key="d6">414</data>
1600
+ <data key="d7">709</data>
1601
+ <data key="d8">422</data>
1602
+ </node>
1603
+ <node id="crossing228">
1604
+ <data key="d0">crossing</data>
1605
+ <data key="d5">788</data>
1606
+ <data key="d6">414</data>
1607
+ <data key="d7">796</data>
1608
+ <data key="d8">422</data>
1609
+ </node>
1610
+ <node id="connector229">
1611
+ <data key="d0">connector</data>
1612
+ <data key="d5">763</data>
1613
+ <data key="d6">1143</data>
1614
+ <data key="d7">771</data>
1615
+ <data key="d8">1151</data>
1616
+ </node>
1617
+ <node id="connector230">
1618
+ <data key="d0">connector</data>
1619
+ <data key="d5">1405</data>
1620
+ <data key="d6">597</data>
1621
+ <data key="d7">1413</data>
1622
+ <data key="d8">605</data>
1623
+ </node>
1624
+ <node id="connector231">
1625
+ <data key="d0">connector</data>
1626
+ <data key="d5">1408</data>
1627
+ <data key="d6">591</data>
1628
+ <data key="d7">1416</data>
1629
+ <data key="d8">599</data>
1630
+ </node>
1631
+ <node id="connector232">
1632
+ <data key="d0">connector</data>
1633
+ <data key="d5">972</data>
1634
+ <data key="d6">1067</data>
1635
+ <data key="d7">980</data>
1636
+ <data key="d8">1075</data>
1637
+ </node>
1638
+ <node id="crossing233">
1639
+ <data key="d0">crossing</data>
1640
+ <data key="d5">948</data>
1641
+ <data key="d6">1066</data>
1642
+ <data key="d7">956</data>
1643
+ <data key="d8">1074</data>
1644
+ </node>
1645
+ <node id="connector234">
1646
+ <data key="d0">connector</data>
1647
+ <data key="d5">631</data>
1648
+ <data key="d6">349</data>
1649
+ <data key="d7">639</data>
1650
+ <data key="d8">357</data>
1651
+ </node>
1652
+ <node id="connector235">
1653
+ <data key="d0">connector</data>
1654
+ <data key="d5">631</data>
1655
+ <data key="d6">326</data>
1656
+ <data key="d7">639</data>
1657
+ <data key="d8">334</data>
1658
+ </node>
1659
+ <node id="connector236">
1660
+ <data key="d0">connector</data>
1661
+ <data key="d5">435</data>
1662
+ <data key="d6">493</data>
1663
+ <data key="d7">443</data>
1664
+ <data key="d8">501</data>
1665
+ </node>
1666
+ <node id="connector237">
1667
+ <data key="d0">connector</data>
1668
+ <data key="d5">435</data>
1669
+ <data key="d6">432</data>
1670
+ <data key="d7">443</data>
1671
+ <data key="d8">440</data>
1672
+ </node>
1673
+ <node id="connector238">
1674
+ <data key="d0">connector</data>
1675
+ <data key="d5">948</data>
1676
+ <data key="d6">984</data>
1677
+ <data key="d7">956</data>
1678
+ <data key="d8">992</data>
1679
+ </node>
1680
+ <node id="connector239">
1681
+ <data key="d0">connector</data>
1682
+ <data key="d5">482</data>
1683
+ <data key="d6">1133</data>
1684
+ <data key="d7">490</data>
1685
+ <data key="d8">1141</data>
1686
+ </node>
1687
+ <node id="connector240">
1688
+ <data key="d0">connector</data>
1689
+ <data key="d5">482</data>
1690
+ <data key="d6">1133</data>
1691
+ <data key="d7">490</data>
1692
+ <data key="d8">1141</data>
1693
+ </node>
1694
+ <node id="connector241">
1695
+ <data key="d0">connector</data>
1696
+ <data key="d5">419</data>
1697
+ <data key="d6">349</data>
1698
+ <data key="d7">427</data>
1699
+ <data key="d8">357</data>
1700
+ </node>
1701
+ <node id="connector242">
1702
+ <data key="d0">connector</data>
1703
+ <data key="d5">420</data>
1704
+ <data key="d6">340</data>
1705
+ <data key="d7">428</data>
1706
+ <data key="d8">348</data>
1707
+ </node>
1708
+ <node id="connector243">
1709
+ <data key="d0">connector</data>
1710
+ <data key="d5">777</data>
1711
+ <data key="d6">803</data>
1712
+ <data key="d7">785</data>
1713
+ <data key="d8">811</data>
1714
+ </node>
1715
+ <node id="connector244">
1716
+ <data key="d0">connector</data>
1717
+ <data key="d5">779</data>
1718
+ <data key="d6">805</data>
1719
+ <data key="d7">787</data>
1720
+ <data key="d8">813</data>
1721
+ </node>
1722
+ <node id="connector245">
1723
+ <data key="d0">connector</data>
1724
+ <data key="d5">763</data>
1725
+ <data key="d6">1133</data>
1726
+ <data key="d7">771</data>
1727
+ <data key="d8">1141</data>
1728
+ </node>
1729
+ <node id="connector246">
1730
+ <data key="d0">connector</data>
1731
+ <data key="d5">763</data>
1732
+ <data key="d6">1133</data>
1733
+ <data key="d7">771</data>
1734
+ <data key="d8">1141</data>
1735
+ </node>
1736
+ <node id="connector247">
1737
+ <data key="d0">connector</data>
1738
+ <data key="d5">1005</data>
1739
+ <data key="d6">946</data>
1740
+ <data key="d7">1013</data>
1741
+ <data key="d8">954</data>
1742
+ </node>
1743
+ <node id="connector248">
1744
+ <data key="d0">connector</data>
1745
+ <data key="d5">1007</data>
1746
+ <data key="d6">944</data>
1747
+ <data key="d7">1015</data>
1748
+ <data key="d8">952</data>
1749
+ </node>
1750
+ <node id="connector249">
1751
+ <data key="d0">connector</data>
1752
+ <data key="d5">677</data>
1753
+ <data key="d6">414</data>
1754
+ <data key="d7">685</data>
1755
+ <data key="d8">422</data>
1756
+ </node>
1757
+ <node id="connector250">
1758
+ <data key="d0">connector</data>
1759
+ <data key="d5">683</data>
1760
+ <data key="d6">414</data>
1761
+ <data key="d7">691</data>
1762
+ <data key="d8">422</data>
1763
+ </node>
1764
+ <node id="connector251">
1765
+ <data key="d0">connector</data>
1766
+ <data key="d5">946</data>
1767
+ <data key="d6">1142</data>
1768
+ <data key="d7">954</data>
1769
+ <data key="d8">1150</data>
1770
+ </node>
1771
+ <node id="crossing252">
1772
+ <data key="d0">crossing</data>
1773
+ <data key="d5">948</data>
1774
+ <data key="d6">1142</data>
1775
+ <data key="d7">956</data>
1776
+ <data key="d8">1150</data>
1777
+ </node>
1778
+ <node id="connector253">
1779
+ <data key="d0">connector</data>
1780
+ <data key="d5">436</data>
1781
+ <data key="d6">1141</data>
1782
+ <data key="d7">444</data>
1783
+ <data key="d8">1149</data>
1784
+ </node>
1785
+ <node id="connector254">
1786
+ <data key="d0">connector</data>
1787
+ <data key="d5">482</data>
1788
+ <data key="d6">1143</data>
1789
+ <data key="d7">490</data>
1790
+ <data key="d8">1151</data>
1791
+ </node>
1792
+ <node id="crossing255">
1793
+ <data key="d0">crossing</data>
1794
+ <data key="d5">436</data>
1795
+ <data key="d6">1142</data>
1796
+ <data key="d7">444</data>
1797
+ <data key="d8">1150</data>
1798
+ </node>
1799
+ <node id="connector256">
1800
+ <data key="d0">connector</data>
1801
+ <data key="d5">587</data>
1802
+ <data key="d6">349</data>
1803
+ <data key="d7">595</data>
1804
+ <data key="d8">357</data>
1805
+ </node>
1806
+ <node id="connector257">
1807
+ <data key="d0">connector</data>
1808
+ <data key="d5">588</data>
1809
+ <data key="d6">340</data>
1810
+ <data key="d7">596</data>
1811
+ <data key="d8">348</data>
1812
+ </node>
1813
+ <node id="connector258">
1814
+ <data key="d0">connector</data>
1815
+ <data key="d5">1386</data>
1816
+ <data key="d6">609</data>
1817
+ <data key="d7">1394</data>
1818
+ <data key="d8">617</data>
1819
+ </node>
1820
+ <node id="crossing259">
1821
+ <data key="d0">crossing</data>
1822
+ <data key="d5">1236</data>
1823
+ <data key="d6">609</data>
1824
+ <data key="d7">1244</data>
1825
+ <data key="d8">617</data>
1826
+ </node>
1827
+ <node id="connector260">
1828
+ <data key="d0">connector</data>
1829
+ <data key="d5">1265</data>
1830
+ <data key="d6">675</data>
1831
+ <data key="d7">1273</data>
1832
+ <data key="d8">683</data>
1833
+ </node>
1834
+ <node id="connector261">
1835
+ <data key="d0">connector</data>
1836
+ <data key="d5">595</data>
1837
+ <data key="d6">432</data>
1838
+ <data key="d7">603</data>
1839
+ <data key="d8">440</data>
1840
+ </node>
1841
+ <node id="connector262">
1842
+ <data key="d0">connector</data>
1843
+ <data key="d5">595</data>
1844
+ <data key="d6">493</data>
1845
+ <data key="d7">603</data>
1846
+ <data key="d8">501</data>
1847
+ </node>
1848
+ <node id="connector263">
1849
+ <data key="d0">connector</data>
1850
+ <data key="d5">1005</data>
1851
+ <data key="d6">963</data>
1852
+ <data key="d7">1013</data>
1853
+ <data key="d8">971</data>
1854
+ </node>
1855
+ <node id="connector264">
1856
+ <data key="d0">connector</data>
1857
+ <data key="d5">466</data>
1858
+ <data key="d6">702</data>
1859
+ <data key="d7">474</data>
1860
+ <data key="d8">710</data>
1861
+ </node>
1862
+ <node id="connector265">
1863
+ <data key="d0">connector</data>
1864
+ <data key="d5">1237</data>
1865
+ <data key="d6">930</data>
1866
+ <data key="d7">1245</data>
1867
+ <data key="d8">938</data>
1868
+ </node>
1869
+ <node id="crossing266">
1870
+ <data key="d0">crossing</data>
1871
+ <data key="d5">1236</data>
1872
+ <data key="d6">931</data>
1873
+ <data key="d7">1244</data>
1874
+ <data key="d8">939</data>
1875
+ </node>
1876
+ <node id="connector267">
1877
+ <data key="d0">connector</data>
1878
+ <data key="d5">948</data>
1879
+ <data key="d6">971</data>
1880
+ <data key="d7">956</data>
1881
+ <data key="d8">979</data>
1882
+ </node>
1883
+ <node id="connector268">
1884
+ <data key="d0">connector</data>
1885
+ <data key="d5">1305</data>
1886
+ <data key="d6">963</data>
1887
+ <data key="d7">1313</data>
1888
+ <data key="d8">971</data>
1889
+ </node>
1890
+ <node id="connector269">
1891
+ <data key="d0">connector</data>
1892
+ <data key="d5">1313</data>
1893
+ <data key="d6">962</data>
1894
+ <data key="d7">1321</data>
1895
+ <data key="d8">970</data>
1896
+ </node>
1897
+ <node id="connector270">
1898
+ <data key="d0">connector</data>
1899
+ <data key="d5">644</data>
1900
+ <data key="d6">1155</data>
1901
+ <data key="d7">652</data>
1902
+ <data key="d8">1163</data>
1903
+ </node>
1904
+ <node id="connector271">
1905
+ <data key="d0">connector</data>
1906
+ <data key="d5">645</data>
1907
+ <data key="d6">1195</data>
1908
+ <data key="d7">653</data>
1909
+ <data key="d8">1203</data>
1910
+ </node>
1911
+ <node id="connector272">
1912
+ <data key="d0">connector</data>
1913
+ <data key="d5">870</data>
1914
+ <data key="d6">379</data>
1915
+ <data key="d7">878</data>
1916
+ <data key="d8">387</data>
1917
+ </node>
1918
+ <node id="connector273">
1919
+ <data key="d0">connector</data>
1920
+ <data key="d5">681</data>
1921
+ <data key="d6">380</data>
1922
+ <data key="d7">689</data>
1923
+ <data key="d8">388</data>
1924
+ </node>
1925
+ <node id="connector274">
1926
+ <data key="d0">connector</data>
1927
+ <data key="d5">612</data>
1928
+ <data key="d6">1195</data>
1929
+ <data key="d7">620</data>
1930
+ <data key="d8">1203</data>
1931
+ </node>
1932
+ <node id="connector275">
1933
+ <data key="d0">connector</data>
1934
+ <data key="d5">610</data>
1935
+ <data key="d6">1142</data>
1936
+ <data key="d7">618</data>
1937
+ <data key="d8">1150</data>
1938
+ </node>
1939
+ <node id="crossing276">
1940
+ <data key="d0">crossing</data>
1941
+ <data key="d5">611</data>
1942
+ <data key="d6">1142</data>
1943
+ <data key="d7">619</data>
1944
+ <data key="d8">1150</data>
1945
+ </node>
1946
+ <node id="connector277">
1947
+ <data key="d0">connector</data>
1948
+ <data key="d5">1483</data>
1949
+ <data key="d6">911</data>
1950
+ <data key="d7">1491</data>
1951
+ <data key="d8">919</data>
1952
+ </node>
1953
+ <node id="connector278">
1954
+ <data key="d0">connector</data>
1955
+ <data key="d5">1483</data>
1956
+ <data key="d6">912</data>
1957
+ <data key="d7">1491</data>
1958
+ <data key="d8">920</data>
1959
+ </node>
1960
+ <node id="connector279">
1961
+ <data key="d0">connector</data>
1962
+ <data key="d5">673</data>
1963
+ <data key="d6">379</data>
1964
+ <data key="d7">681</data>
1965
+ <data key="d8">387</data>
1966
+ </node>
1967
+ <node id="connector280">
1968
+ <data key="d0">connector</data>
1969
+ <data key="d5">665</data>
1970
+ <data key="d6">380</data>
1971
+ <data key="d7">673</data>
1972
+ <data key="d8">388</data>
1973
+ </node>
1974
+ <edge source="arrow1" target="connector157">
1975
+ <data key="d9">solid</data>
1976
+ </edge>
1977
+ <edge source="arrow1" target="connector253">
1978
+ <data key="d9">solid</data>
1979
+ </edge>
1980
+ <edge source="arrow2" target="connector152">
1981
+ <data key="d9">solid</data>
1982
+ </edge>
1983
+ <edge source="arrow2" target="connector251">
1984
+ <data key="d9">solid</data>
1985
+ </edge>
1986
+ <edge source="instrumentation3" target="connector100">
1987
+ <data key="d9">solid</data>
1988
+ </edge>
1989
+ <edge source="arrow4" target="connector238">
1990
+ <data key="d9">solid</data>
1991
+ </edge>
1992
+ <edge source="arrow4" target="connector267">
1993
+ <data key="d9">solid</data>
1994
+ </edge>
1995
+ <edge source="arrow5" target="connector123">
1996
+ <data key="d9">solid</data>
1997
+ </edge>
1998
+ <edge source="arrow5" target="connector128">
1999
+ <data key="d9">solid</data>
2000
+ </edge>
2001
+ <edge source="arrow7" target="connector115">
2002
+ <data key="d9">solid</data>
2003
+ </edge>
2004
+ <edge source="arrow7" target="connector168">
2005
+ <data key="d9">solid</data>
2006
+ </edge>
2007
+ <edge source="valve8" target="connector227">
2008
+ <data key="d9">solid</data>
2009
+ </edge>
2010
+ <edge source="valve8" target="connector250">
2011
+ <data key="d9">solid</data>
2012
+ </edge>
2013
+ <edge source="general9" target="connector173">
2014
+ <data key="d9">solid</data>
2015
+ </edge>
2016
+ <edge source="valve10" target="instrumentation66">
2017
+ <data key="d9">solid</data>
2018
+ </edge>
2019
+ <edge source="valve10" target="connector164">
2020
+ <data key="d9">solid</data>
2021
+ </edge>
2022
+ <edge source="valve10" target="connector230">
2023
+ <data key="d9">solid</data>
2024
+ </edge>
2025
+ <edge source="valve10" target="connector258">
2026
+ <data key="d9">solid</data>
2027
+ </edge>
2028
+ <edge source="instrumentation11" target="connector198">
2029
+ <data key="d9">solid</data>
2030
+ </edge>
2031
+ <edge source="valve12" target="connector108">
2032
+ <data key="d9">solid</data>
2033
+ </edge>
2034
+ <edge source="valve12" target="connector134">
2035
+ <data key="d9">solid</data>
2036
+ </edge>
2037
+ <edge source="general13" target="connector204">
2038
+ <data key="d9">solid</data>
2039
+ </edge>
2040
+ <edge source="tank14" target="connector98">
2041
+ <data key="d9">solid</data>
2042
+ </edge>
2043
+ <edge source="tank14" target="connector106">
2044
+ <data key="d9">solid</data>
2045
+ </edge>
2046
+ <edge source="tank14" target="connector114">
2047
+ <data key="d9">solid</data>
2048
+ </edge>
2049
+ <edge source="tank14" target="connector136">
2050
+ <data key="d9">solid</data>
2051
+ </edge>
2052
+ <edge source="tank14" target="connector186">
2053
+ <data key="d9">solid</data>
2054
+ </edge>
2055
+ <edge source="tank14" target="connector211">
2056
+ <data key="d9">solid</data>
2057
+ </edge>
2058
+ <edge source="tank14" target="connector222">
2059
+ <data key="d9">solid</data>
2060
+ </edge>
2061
+ <edge source="tank14" target="connector234">
2062
+ <data key="d9">solid</data>
2063
+ </edge>
2064
+ <edge source="tank14" target="connector237">
2065
+ <data key="d9">solid</data>
2066
+ </edge>
2067
+ <edge source="tank14" target="connector241">
2068
+ <data key="d9">solid</data>
2069
+ </edge>
2070
+ <edge source="tank14" target="connector256">
2071
+ <data key="d9">solid</data>
2072
+ </edge>
2073
+ <edge source="tank14" target="connector261">
2074
+ <data key="d9">solid</data>
2075
+ </edge>
2076
+ <edge source="tank14" target="connector280">
2077
+ <data key="d9">solid</data>
2078
+ </edge>
2079
+ <edge source="general15" target="connector242">
2080
+ <data key="d9">solid</data>
2081
+ </edge>
2082
+ <edge source="inlet/outlet16" target="connector171">
2083
+ <data key="d9">solid</data>
2084
+ </edge>
2085
+ <edge source="valve17" target="connector131">
2086
+ <data key="d9">solid</data>
2087
+ </edge>
2088
+ <edge source="valve17" target="connector217">
2089
+ <data key="d9">solid</data>
2090
+ </edge>
2091
+ <edge source="tank18" target="connector102">
2092
+ <data key="d9">solid</data>
2093
+ </edge>
2094
+ <edge source="tank18" target="connector113">
2095
+ <data key="d9">solid</data>
2096
+ </edge>
2097
+ <edge source="tank18" target="connector143">
2098
+ <data key="d9">solid</data>
2099
+ </edge>
2100
+ <edge source="tank18" target="connector159">
2101
+ <data key="d9">solid</data>
2102
+ </edge>
2103
+ <edge source="tank18" target="connector174">
2104
+ <data key="d9">solid</data>
2105
+ </edge>
2106
+ <edge source="tank18" target="connector202">
2107
+ <data key="d9">solid</data>
2108
+ </edge>
2109
+ <edge source="tank18" target="connector205">
2110
+ <data key="d9">solid</data>
2111
+ </edge>
2112
+ <edge source="tank18" target="connector212">
2113
+ <data key="d9">solid</data>
2114
+ </edge>
2115
+ <edge source="tank18" target="connector236">
2116
+ <data key="d9">solid</data>
2117
+ </edge>
2118
+ <edge source="tank18" target="connector262">
2119
+ <data key="d9">solid</data>
2120
+ </edge>
2121
+ <edge source="valve19" target="instrumentation57">
2122
+ <data key="d9">solid</data>
2123
+ </edge>
2124
+ <edge source="valve19" target="connector104">
2125
+ <data key="d9">solid</data>
2126
+ </edge>
2127
+ <edge source="valve19" target="connector240">
2128
+ <data key="d9">solid</data>
2129
+ </edge>
2130
+ <edge source="valve19" target="connector254">
2131
+ <data key="d9">solid</data>
2132
+ </edge>
2133
+ <edge source="instrumentation20" target="connector103">
2134
+ <data key="d9">solid</data>
2135
+ </edge>
2136
+ <edge source="pump21" target="connector271">
2137
+ <data key="d9">solid</data>
2138
+ </edge>
2139
+ <edge source="pump21" target="connector274">
2140
+ <data key="d9">solid</data>
2141
+ </edge>
2142
+ <edge source="valve22" target="instrumentation38">
2143
+ <data key="d9">solid</data>
2144
+ </edge>
2145
+ <edge source="valve22" target="connector110">
2146
+ <data key="d9">solid</data>
2147
+ </edge>
2148
+ <edge source="valve22" target="connector216">
2149
+ <data key="d9">solid</data>
2150
+ </edge>
2151
+ <edge source="valve22" target="connector243">
2152
+ <data key="d9">solid</data>
2153
+ </edge>
2154
+ <edge source="valve23" target="connector181">
2155
+ <data key="d9">solid</data>
2156
+ </edge>
2157
+ <edge source="valve23" target="connector197">
2158
+ <data key="d9">solid</data>
2159
+ </edge>
2160
+ <edge source="valve23" target="connector272">
2161
+ <data key="d9">solid</data>
2162
+ </edge>
2163
+ <edge source="valve24" target="instrumentation51">
2164
+ <data key="d9">solid</data>
2165
+ </edge>
2166
+ <edge source="valve24" target="connector224">
2167
+ <data key="d9">solid</data>
2168
+ </edge>
2169
+ <edge source="valve24" target="connector247">
2170
+ <data key="d9">solid</data>
2171
+ </edge>
2172
+ <edge source="valve24" target="connector263">
2173
+ <data key="d9">solid</data>
2174
+ </edge>
2175
+ <edge source="general25" target="connector107">
2176
+ <data key="d9">solid</data>
2177
+ </edge>
2178
+ <edge source="valve26" target="connector129">
2179
+ <data key="d9">solid</data>
2180
+ </edge>
2181
+ <edge source="valve26" target="connector210">
2182
+ <data key="d9">solid</data>
2183
+ </edge>
2184
+ <edge source="valve27" target="connector96">
2185
+ <data key="d9">solid</data>
2186
+ </edge>
2187
+ <edge source="valve27" target="connector183">
2188
+ <data key="d9">solid</data>
2189
+ </edge>
2190
+ <edge source="valve29" target="connector140">
2191
+ <data key="d9">solid</data>
2192
+ </edge>
2193
+ <edge source="valve29" target="connector169">
2194
+ <data key="d9">solid</data>
2195
+ </edge>
2196
+ <edge source="valve29" target="connector226">
2197
+ <data key="d9">solid</data>
2198
+ </edge>
2199
+ <edge source="instrumentation30" target="connector191">
2200
+ <data key="d9">solid</data>
2201
+ </edge>
2202
+ <edge source="general31" target="connector257">
2203
+ <data key="d9">solid</data>
2204
+ </edge>
2205
+ <edge source="general33" target="connector135">
2206
+ <data key="d9">solid</data>
2207
+ </edge>
2208
+ <edge source="general33" target="connector215">
2209
+ <data key="d9">solid</data>
2210
+ </edge>
2211
+ <edge source="instrumentation34" target="connector189">
2212
+ <data key="d9">solid</data>
2213
+ </edge>
2214
+ <edge source="arrow36" target="connector146">
2215
+ <data key="d9">solid</data>
2216
+ </edge>
2217
+ <edge source="arrow36" target="connector150">
2218
+ <data key="d9">solid</data>
2219
+ </edge>
2220
+ <edge source="general37" target="connector277">
2221
+ <data key="d9">solid</data>
2222
+ </edge>
2223
+ <edge source="instrumentation38" target="connector244">
2224
+ <data key="d9">solid</data>
2225
+ </edge>
2226
+ <edge source="general39" target="connector153">
2227
+ <data key="d9">solid</data>
2228
+ </edge>
2229
+ <edge source="general40" target="connector138">
2230
+ <data key="d9">solid</data>
2231
+ </edge>
2232
+ <edge source="instrumentation41" target="connector260">
2233
+ <data key="d9">solid</data>
2234
+ </edge>
2235
+ <edge source="general42" target="connector160">
2236
+ <data key="d9">solid</data>
2237
+ </edge>
2238
+ <edge source="general42" target="connector214">
2239
+ <data key="d9">solid</data>
2240
+ </edge>
2241
+ <edge source="general43" target="connector133">
2242
+ <data key="d9">solid</data>
2243
+ </edge>
2244
+ <edge source="general43" target="connector268">
2245
+ <data key="d9">solid</data>
2246
+ </edge>
2247
+ <edge source="general44" target="connector195">
2248
+ <data key="d9">solid</data>
2249
+ </edge>
2250
+ <edge source="general44" target="connector200">
2251
+ <data key="d9">solid</data>
2252
+ </edge>
2253
+ <edge source="pump45" target="connector155">
2254
+ <data key="d9">solid</data>
2255
+ </edge>
2256
+ <edge source="pump45" target="connector207">
2257
+ <data key="d9">solid</data>
2258
+ </edge>
2259
+ <edge source="general46" target="connector145">
2260
+ <data key="d9">solid</data>
2261
+ </edge>
2262
+ <edge source="general46" target="connector199">
2263
+ <data key="d9">solid</data>
2264
+ </edge>
2265
+ <edge source="arrow47" target="connector184">
2266
+ <data key="d9">solid</data>
2267
+ </edge>
2268
+ <edge source="arrow47" target="connector209">
2269
+ <data key="d9">solid</data>
2270
+ </edge>
2271
+ <edge source="general48" target="connector223">
2272
+ <data key="d9">solid</data>
2273
+ </edge>
2274
+ <edge source="general48" target="connector249">
2275
+ <data key="d9">solid</data>
2276
+ </edge>
2277
+ <edge source="general49" target="connector273">
2278
+ <data key="d9">solid</data>
2279
+ </edge>
2280
+ <edge source="general49" target="connector279">
2281
+ <data key="d9">solid</data>
2282
+ </edge>
2283
+ <edge source="general50" target="connector142">
2284
+ <data key="d9">solid</data>
2285
+ </edge>
2286
+ <edge source="instrumentation51" target="connector248">
2287
+ <data key="d9">solid</data>
2288
+ </edge>
2289
+ <edge source="instrumentation53" target="connector139">
2290
+ <data key="d9">solid</data>
2291
+ </edge>
2292
+ <edge source="general54" target="connector166">
2293
+ <data key="d9">solid</data>
2294
+ </edge>
2295
+ <edge source="general54" target="connector201">
2296
+ <data key="d9">solid</data>
2297
+ </edge>
2298
+ <edge source="general55" target="connector137">
2299
+ <data key="d9">solid</data>
2300
+ </edge>
2301
+ <edge source="general55" target="connector154">
2302
+ <data key="d9">solid</data>
2303
+ </edge>
2304
+ <edge source="general55" target="connector196">
2305
+ <data key="d9">solid</data>
2306
+ </edge>
2307
+ <edge source="general55" target="connector269">
2308
+ <data key="d9">solid</data>
2309
+ </edge>
2310
+ <edge source="general55" target="connector278">
2311
+ <data key="d9">solid</data>
2312
+ </edge>
2313
+ <edge source="arrow56" target="connector148">
2314
+ <data key="d9">solid</data>
2315
+ </edge>
2316
+ <edge source="arrow56" target="connector213">
2317
+ <data key="d9">solid</data>
2318
+ </edge>
2319
+ <edge source="instrumentation57" target="connector239">
2320
+ <data key="d9">solid</data>
2321
+ </edge>
2322
+ <edge source="arrow58" target="connector118">
2323
+ <data key="d9">solid</data>
2324
+ </edge>
2325
+ <edge source="arrow58" target="connector270">
2326
+ <data key="d9">solid</data>
2327
+ </edge>
2328
+ <edge source="instrumentation59" target="connector193">
2329
+ <data key="d9">solid</data>
2330
+ </edge>
2331
+ <edge source="valve60" target="instrumentation76">
2332
+ <data key="d9">solid</data>
2333
+ </edge>
2334
+ <edge source="valve60" target="connector179">
2335
+ <data key="d9">solid</data>
2336
+ </edge>
2337
+ <edge source="valve60" target="connector229">
2338
+ <data key="d9">solid</data>
2339
+ </edge>
2340
+ <edge source="valve60" target="connector245">
2341
+ <data key="d9">solid</data>
2342
+ </edge>
2343
+ <edge source="instrumentation61" target="valve67">
2344
+ <data key="d9">solid</data>
2345
+ </edge>
2346
+ <edge source="instrumentation61" target="connector219">
2347
+ <data key="d9">solid</data>
2348
+ </edge>
2349
+ <edge source="instrumentation62" target="connector130">
2350
+ <data key="d9">solid</data>
2351
+ </edge>
2352
+ <edge source="arrow63" target="connector156">
2353
+ <data key="d9">solid</data>
2354
+ </edge>
2355
+ <edge source="arrow63" target="connector175">
2356
+ <data key="d9">solid</data>
2357
+ </edge>
2358
+ <edge source="arrow65" target="connector93">
2359
+ <data key="d9">solid</data>
2360
+ </edge>
2361
+ <edge source="arrow65" target="connector180">
2362
+ <data key="d9">solid</data>
2363
+ </edge>
2364
+ <edge source="instrumentation66" target="connector231">
2365
+ <data key="d9">solid</data>
2366
+ </edge>
2367
+ <edge source="valve67" target="connector192">
2368
+ <data key="d9">solid</data>
2369
+ </edge>
2370
+ <edge source="valve67" target="connector203">
2371
+ <data key="d9">solid</data>
2372
+ </edge>
2373
+ <edge source="valve67" target="connector220">
2374
+ <data key="d9">solid</data>
2375
+ </edge>
2376
+ <edge source="instrumentation68" target="connector232">
2377
+ <data key="d9">solid</data>
2378
+ </edge>
2379
+ <edge source="instrumentation69" target="connector141">
2380
+ <data key="d9">solid</data>
2381
+ </edge>
2382
+ <edge source="arrow70" target="connector97">
2383
+ <data key="d9">solid</data>
2384
+ </edge>
2385
+ <edge source="arrow70" target="connector161">
2386
+ <data key="d9">solid</data>
2387
+ </edge>
2388
+ <edge source="arrow72" target="connector144">
2389
+ <data key="d9">solid</data>
2390
+ </edge>
2391
+ <edge source="arrow72" target="connector265">
2392
+ <data key="d9">solid</data>
2393
+ </edge>
2394
+ <edge source="instrumentation73" target="connector194">
2395
+ <data key="d9">solid</data>
2396
+ </edge>
2397
+ <edge source="instrumentation76" target="connector246">
2398
+ <data key="d9">solid</data>
2399
+ </edge>
2400
+ <edge source="valve77" target="connector151">
2401
+ <data key="d9">solid</data>
2402
+ </edge>
2403
+ <edge source="valve77" target="connector178">
2404
+ <data key="d9">solid</data>
2405
+ </edge>
2406
+ <edge source="instrumentation78" target="connector126">
2407
+ <data key="d9">solid</data>
2408
+ </edge>
2409
+ <edge source="instrumentation79" target="connector172">
2410
+ <data key="d9">solid</data>
2411
+ </edge>
2412
+ <edge source="inlet/outlet82" target="connector124">
2413
+ <data key="d9">solid</data>
2414
+ </edge>
2415
+ <edge source="instrumentation83" target="connector264">
2416
+ <data key="d9">solid</data>
2417
+ </edge>
2418
+ <edge source="arrow84" target="connector221">
2419
+ <data key="d9">solid</data>
2420
+ </edge>
2421
+ <edge source="arrow84" target="connector275">
2422
+ <data key="d9">solid</data>
2423
+ </edge>
2424
+ <edge source="valve86" target="connector111">
2425
+ <data key="d9">solid</data>
2426
+ </edge>
2427
+ <edge source="valve86" target="connector149">
2428
+ <data key="d9">solid</data>
2429
+ </edge>
2430
+ <edge source="instrumentation87" target="connector99">
2431
+ <data key="d9">solid</data>
2432
+ </edge>
2433
+ <edge source="instrumentation88" target="instrumentation90">
2434
+ <data key="d9">solid</data>
2435
+ </edge>
2436
+ <edge source="inlet/outlet89" target="connector162">
2437
+ <data key="d9">solid</data>
2438
+ </edge>
2439
+ <edge source="instrumentation90" target="connector235">
2440
+ <data key="d9">solid</data>
2441
+ </edge>
2442
+ <edge source="instrumentation91" target="connector185">
2443
+ <data key="d9">solid</data>
2444
+ </edge>
2445
+ <edge source="arrow92" target="connector112">
2446
+ <data key="d9">solid</data>
2447
+ </edge>
2448
+ <edge source="arrow92" target="connector206">
2449
+ <data key="d9">solid</data>
2450
+ </edge>
2451
+ <edge source="connector93" target="crossing95">
2452
+ <data key="d9">solid</data>
2453
+ </edge>
2454
+ <edge source="crossing94" target="crossing95">
2455
+ <data key="d9">solid</data>
2456
+ </edge>
2457
+ <edge source="crossing94" target="crossing147">
2458
+ <data key="d9">solid</data>
2459
+ </edge>
2460
+ <edge source="crossing94" target="crossing225">
2461
+ <data key="d9">solid</data>
2462
+ </edge>
2463
+ <edge source="connector96" target="connector97">
2464
+ <data key="d9">solid</data>
2465
+ </edge>
2466
+ <edge source="connector98" target="connector99">
2467
+ <data key="d9">solid</data>
2468
+ </edge>
2469
+ <edge source="connector100" target="crossing101">
2470
+ <data key="d9">solid</data>
2471
+ </edge>
2472
+ <edge source="crossing101" target="connector112">
2473
+ <data key="d9">solid</data>
2474
+ </edge>
2475
+ <edge source="crossing101" target="connector192">
2476
+ <data key="d9">solid</data>
2477
+ </edge>
2478
+ <edge source="connector102" target="connector103">
2479
+ <data key="d9">solid</data>
2480
+ </edge>
2481
+ <edge source="connector104" target="crossing105">
2482
+ <data key="d9">solid</data>
2483
+ </edge>
2484
+ <edge source="crossing105" target="connector194">
2485
+ <data key="d9">solid</data>
2486
+ </edge>
2487
+ <edge source="crossing105" target="connector221">
2488
+ <data key="d9">solid</data>
2489
+ </edge>
2490
+ <edge source="connector106" target="connector107">
2491
+ <data key="d9">solid</data>
2492
+ </edge>
2493
+ <edge source="connector108" target="crossing109">
2494
+ <data key="d9">solid</data>
2495
+ </edge>
2496
+ <edge source="crossing109" target="connector141">
2497
+ <data key="d9">solid</data>
2498
+ </edge>
2499
+ <edge source="crossing109" target="connector209">
2500
+ <data key="d9">solid</data>
2501
+ </edge>
2502
+ <edge source="connector110" target="connector111">
2503
+ <data key="d9">solid</data>
2504
+ </edge>
2505
+ <edge source="connector113" target="connector114">
2506
+ <data key="d9">solid</data>
2507
+ </edge>
2508
+ <edge source="connector115" target="crossing117">
2509
+ <data key="d9">solid</data>
2510
+ </edge>
2511
+ <edge source="crossing116" target="crossing117">
2512
+ <data key="d9">solid</data>
2513
+ </edge>
2514
+ <edge source="crossing116" target="connector130">
2515
+ <data key="d9">solid</data>
2516
+ </edge>
2517
+ <edge source="crossing116" target="connector148">
2518
+ <data key="d9">solid</data>
2519
+ </edge>
2520
+ <edge source="connector118" target="crossing120">
2521
+ <data key="d9">solid</data>
2522
+ </edge>
2523
+ <edge source="crossing119" target="crossing120">
2524
+ <data key="d9">solid</data>
2525
+ </edge>
2526
+ <edge source="crossing119" target="connector193">
2527
+ <data key="d9">solid</data>
2528
+ </edge>
2529
+ <edge source="crossing119" target="connector229">
2530
+ <data key="d9">solid</data>
2531
+ </edge>
2532
+ <edge source="crossing121" target="crossing122">
2533
+ <data key="d9">solid</data>
2534
+ </edge>
2535
+ <edge source="crossing121" target="crossing170">
2536
+ <data key="d9">solid</data>
2537
+ </edge>
2538
+ <edge source="crossing121" target="connector183">
2539
+ <data key="d9">solid</data>
2540
+ </edge>
2541
+ <edge source="crossing122" target="connector184">
2542
+ <data key="d9">solid</data>
2543
+ </edge>
2544
+ <edge source="crossing122" target="connector226">
2545
+ <data key="d9">solid</data>
2546
+ </edge>
2547
+ <edge source="connector123" target="crossing125">
2548
+ <data key="d9">solid</data>
2549
+ </edge>
2550
+ <edge source="connector124" target="crossing125">
2551
+ <data key="d9">solid</data>
2552
+ </edge>
2553
+ <edge source="connector126" target="crossing127">
2554
+ <data key="d9">solid</data>
2555
+ </edge>
2556
+ <edge source="crossing127" target="crossing218">
2557
+ <data key="d9">solid</data>
2558
+ </edge>
2559
+ <edge source="crossing127" target="crossing228">
2560
+ <data key="d9">solid</data>
2561
+ </edge>
2562
+ <edge source="connector128" target="connector129">
2563
+ <data key="d9">solid</data>
2564
+ </edge>
2565
+ <edge source="connector131" target="crossing132">
2566
+ <data key="d9">solid</data>
2567
+ </edge>
2568
+ <edge source="connector133" target="connector134">
2569
+ <data key="d9">solid</data>
2570
+ </edge>
2571
+ <edge source="connector135" target="connector136">
2572
+ <data key="d9">solid</data>
2573
+ </edge>
2574
+ <edge source="connector137" target="connector138">
2575
+ <data key="d9">solid</data>
2576
+ </edge>
2577
+ <edge source="connector139" target="connector140">
2578
+ <data key="d9">solid</data>
2579
+ </edge>
2580
+ <edge source="connector142" target="connector143">
2581
+ <data key="d9">solid</data>
2582
+ </edge>
2583
+ <edge source="connector144" target="connector145">
2584
+ <data key="d9">solid</data>
2585
+ </edge>
2586
+ <edge source="connector146" target="crossing147">
2587
+ <data key="d9">solid</data>
2588
+ </edge>
2589
+ <edge source="connector149" target="connector150">
2590
+ <data key="d9">solid</data>
2591
+ </edge>
2592
+ <edge source="connector151" target="connector152">
2593
+ <data key="d9">solid</data>
2594
+ </edge>
2595
+ <edge source="connector153" target="connector154">
2596
+ <data key="d9">solid</data>
2597
+ </edge>
2598
+ <edge source="connector155" target="connector156">
2599
+ <data key="d9">solid</data>
2600
+ </edge>
2601
+ <edge source="connector157" target="crossing158">
2602
+ <data key="d9">solid</data>
2603
+ </edge>
2604
+ <edge source="crossing158" target="connector203">
2605
+ <data key="d9">solid</data>
2606
+ </edge>
2607
+ <edge source="crossing158" target="crossing167">
2608
+ <data key="d9">solid</data>
2609
+ </edge>
2610
+ <edge source="connector159" target="connector160">
2611
+ <data key="d9">solid</data>
2612
+ </edge>
2613
+ <edge source="connector161" target="crossing163">
2614
+ <data key="d9">solid</data>
2615
+ </edge>
2616
+ <edge source="connector162" target="crossing163">
2617
+ <data key="d9">solid</data>
2618
+ </edge>
2619
+ <edge source="connector164" target="crossing165">
2620
+ <data key="d9">solid</data>
2621
+ </edge>
2622
+ <edge source="crossing165" target="connector171">
2623
+ <data key="d9">solid</data>
2624
+ </edge>
2625
+ <edge source="crossing165" target="connector172">
2626
+ <data key="d9">solid</data>
2627
+ </edge>
2628
+ <edge source="connector166" target="crossing167">
2629
+ <data key="d9">solid</data>
2630
+ </edge>
2631
+ <edge source="crossing167" target="connector264">
2632
+ <data key="d9">solid</data>
2633
+ </edge>
2634
+ <edge source="connector168" target="connector169">
2635
+ <data key="d9">solid</data>
2636
+ </edge>
2637
+ <edge source="crossing170" target="connector210">
2638
+ <data key="d9">solid</data>
2639
+ </edge>
2640
+ <edge source="crossing170" target="connector263">
2641
+ <data key="d9">solid</data>
2642
+ </edge>
2643
+ <edge source="connector173" target="connector174">
2644
+ <data key="d9">solid</data>
2645
+ </edge>
2646
+ <edge source="connector175" target="crossing177">
2647
+ <data key="d9">solid</data>
2648
+ </edge>
2649
+ <edge source="crossing176" target="crossing177">
2650
+ <data key="d9">solid</data>
2651
+ </edge>
2652
+ <edge source="crossing176" target="connector191">
2653
+ <data key="d9">solid</data>
2654
+ </edge>
2655
+ <edge source="crossing176" target="connector216">
2656
+ <data key="d9">solid</data>
2657
+ </edge>
2658
+ <edge source="connector178" target="connector179">
2659
+ <data key="d9">solid</data>
2660
+ </edge>
2661
+ <edge source="connector180" target="crossing182">
2662
+ <data key="d9">solid</data>
2663
+ </edge>
2664
+ <edge source="connector181" target="crossing182">
2665
+ <data key="d9">solid</data>
2666
+ </edge>
2667
+ <edge source="connector185" target="connector186">
2668
+ <data key="d9">solid</data>
2669
+ </edge>
2670
+ <edge source="crossing187" target="crossing188">
2671
+ <data key="d9">solid</data>
2672
+ </edge>
2673
+ <edge source="crossing187" target="crossing259">
2674
+ <data key="d9">solid</data>
2675
+ </edge>
2676
+ <edge source="crossing187" target="connector260">
2677
+ <data key="d9">solid</data>
2678
+ </edge>
2679
+ <edge source="crossing188" target="connector213">
2680
+ <data key="d9">solid</data>
2681
+ </edge>
2682
+ <edge source="crossing188" target="crossing266">
2683
+ <data key="d9">solid</data>
2684
+ </edge>
2685
+ <edge source="connector189" target="crossing190">
2686
+ <data key="d9">solid</data>
2687
+ </edge>
2688
+ <edge source="connector195" target="connector196">
2689
+ <data key="d9">solid</data>
2690
+ </edge>
2691
+ <edge source="connector197" target="connector198">
2692
+ <data key="d9">solid</data>
2693
+ </edge>
2694
+ <edge source="connector199" target="connector200">
2695
+ <data key="d9">solid</data>
2696
+ </edge>
2697
+ <edge source="connector201" target="connector202">
2698
+ <data key="d9">solid</data>
2699
+ </edge>
2700
+ <edge source="connector204" target="connector205">
2701
+ <data key="d9">solid</data>
2702
+ </edge>
2703
+ <edge source="connector206" target="crossing208">
2704
+ <data key="d9">solid</data>
2705
+ </edge>
2706
+ <edge source="connector207" target="crossing208">
2707
+ <data key="d9">solid</data>
2708
+ </edge>
2709
+ <edge source="connector211" target="connector212">
2710
+ <data key="d9">solid</data>
2711
+ </edge>
2712
+ <edge source="connector214" target="connector215">
2713
+ <data key="d9">solid</data>
2714
+ </edge>
2715
+ <edge source="connector217" target="crossing218">
2716
+ <data key="d9">solid</data>
2717
+ </edge>
2718
+ <edge source="connector219" target="connector220">
2719
+ <data key="d9">solid</data>
2720
+ </edge>
2721
+ <edge source="connector222" target="connector223">
2722
+ <data key="d9">solid</data>
2723
+ </edge>
2724
+ <edge source="connector224" target="crossing225">
2725
+ <data key="d9">solid</data>
2726
+ </edge>
2727
+ <edge source="crossing225" target="connector267">
2728
+ <data key="d9">solid</data>
2729
+ </edge>
2730
+ <edge source="connector227" target="crossing228">
2731
+ <data key="d9">solid</data>
2732
+ </edge>
2733
+ <edge source="connector230" target="connector231">
2734
+ <data key="d9">solid</data>
2735
+ </edge>
2736
+ <edge source="connector232" target="crossing233">
2737
+ <data key="d9">solid</data>
2738
+ </edge>
2739
+ <edge source="crossing233" target="connector238">
2740
+ <data key="d9">solid</data>
2741
+ </edge>
2742
+ <edge source="crossing233" target="crossing252">
2743
+ <data key="d9">solid</data>
2744
+ </edge>
2745
+ <edge source="connector234" target="connector235">
2746
+ <data key="d9">solid</data>
2747
+ </edge>
2748
+ <edge source="connector236" target="connector237">
2749
+ <data key="d9">solid</data>
2750
+ </edge>
2751
+ <edge source="connector239" target="connector240">
2752
+ <data key="d9">solid</data>
2753
+ </edge>
2754
+ <edge source="connector241" target="connector242">
2755
+ <data key="d9">solid</data>
2756
+ </edge>
2757
+ <edge source="connector243" target="connector244">
2758
+ <data key="d9">solid</data>
2759
+ </edge>
2760
+ <edge source="connector245" target="connector246">
2761
+ <data key="d9">solid</data>
2762
+ </edge>
2763
+ <edge source="connector247" target="connector248">
2764
+ <data key="d9">solid</data>
2765
+ </edge>
2766
+ <edge source="connector249" target="connector250">
2767
+ <data key="d9">solid</data>
2768
+ </edge>
2769
+ <edge source="connector251" target="crossing252">
2770
+ <data key="d9">solid</data>
2771
+ </edge>
2772
+ <edge source="connector253" target="crossing255">
2773
+ <data key="d9">solid</data>
2774
+ </edge>
2775
+ <edge source="connector254" target="crossing255">
2776
+ <data key="d9">solid</data>
2777
+ </edge>
2778
+ <edge source="connector256" target="connector257">
2779
+ <data key="d9">solid</data>
2780
+ </edge>
2781
+ <edge source="connector258" target="crossing259">
2782
+ <data key="d9">solid</data>
2783
+ </edge>
2784
+ <edge source="connector261" target="connector262">
2785
+ <data key="d9">solid</data>
2786
+ </edge>
2787
+ <edge source="connector265" target="crossing266">
2788
+ <data key="d9">solid</data>
2789
+ </edge>
2790
+ <edge source="connector268" target="connector269">
2791
+ <data key="d9">solid</data>
2792
+ </edge>
2793
+ <edge source="connector270" target="connector271">
2794
+ <data key="d9">solid</data>
2795
+ </edge>
2796
+ <edge source="connector272" target="connector273">
2797
+ <data key="d9">solid</data>
2798
+ </edge>
2799
+ <edge source="connector274" target="crossing276">
2800
+ <data key="d9">solid</data>
2801
+ </edge>
2802
+ <edge source="connector275" target="crossing276">
2803
+ <data key="d9">solid</data>
2804
+ </edge>
2805
+ <edge source="connector277" target="connector278">
2806
+ <data key="d9">solid</data>
2807
+ </edge>
2808
+ <edge source="connector279" target="connector280">
2809
+ <data key="d9">solid</data>
2810
+ </edge>
2811
+ </graph>
2812
+ </graphml>
samples/open100_03_medium.png ADDED

Git LFS Details

  • SHA256: 293d416db064be7c78def7f3f9856a6060e65b7d9e7b15d2b42d273a98fc49e1
  • Pointer size: 131 Bytes
  • Size of remote file: 609 kB