RhinocerosでモデリングしたネットワークをPythonで読み込む②

前回は、建築・都市におけるネットワーク分析の一例として歩行空間ネットワークを取り上げ、地図情報をもとにRhinocerosで実際に歩行空間ネットワークを描きました。
[→RhinocerosでモデリングしたネットワークをPythonで読み込む①](https://qiita.com/TBT-GOAT/items/ed1725690dee18bf39d0)

本記事では、描いたネットワークを分析ができる形にすることが目的です。

# 目標
1. Rhinocerosでネットワークを描く
1. Rhinocerosで描いたネットワークをPythonで読み込めるように書き出す
1. 書き出したファイルをPythonで読み込んで隣接配列を作る

本記事では第2項に取り組みます。

# 隣接配列
ネットワークを表すデータ構造には隣接行列、距離行列、隣接配列などがあります。本記事では、ネットワークを隣接配列として表現する方法を紹介します。

その前に、それぞれのデータ構造について簡単な説明を以下に述べます。

**・隣接行列**
順序付けられたノードの隣接関係を0/1（隣接していない/隣接している）で表した正方行列を、隣接行列と言います。エッジの重みを考えず、ノードの隣接関係だけに着目したものを、厳密には**グラフ**と呼びます。

例えば、
<img src="https://tbt-goat.github.io/home/tips/20201223/fig1.png" width=25%>
を隣接行列で表すと、

```math
\begin{pmatrix}
0&1&0&1\\
1&0&1&0\\
0&1&0&1\\
1&0&1&0
\end{pmatrix}
```

になります。

隣接行列を使えば、ノードからノードへの経路数を求めるなどが代数的にできます。上のグラフはエッジに向きがない（無向グラフ）ので隣接行列は対称ですが、対称性を崩して一方通行のエッジを表現することも可能です（有向グラフ）。

**・距離行列**
隣接行列の各成分は、ノード間の隣接関係を表していました。距離行列は、その要素にノード間を結ぶエッジの重みが与えられたものです。先ほどのグラフのエッジに重みを与えたネットワーク
<img src="https://tbt-goat.github.io/home/tips/20201223/fig2.png" width=25%>
の距離行列は、

```math
\begin{pmatrix}
0&1.5&0&8\\
1.5&0&5&0\\
0&5&0&2.3\\
8&0&2.3&0
\end{pmatrix}
```

になります。隣接していないノード間の要素に、便宜的に0を置いていますが、エッジの重みが0であることを意味しないことに注意してください。

**・隣接配列**
先のふたつのデータ構造は、どちらも行列を用いたものです。ここで、建築や都市の中にあるネットワークを考えてみると、あるノードと隣接するノードは高々4つか5つくらいであることが想像できます。そのとき、隣接行列、距離行列は0がたくさん並んだものになって、とても無駄が多いものになります。そこで、隣接しているノード間の関係だけを記憶しておこうというのが隣接配列です。

先ほどのネットワークの隣接配列を実際に作ってみると、次のようになります。

```math
\begin{align}
&1: &[2, 4]\\
&2: &[1, 3]\\
&3: &[2, 4]\\
&4: &[1, 3]
\end{align}
```

コロンの左に注目するノード、右に隣接するノードの配列を記しています。同時に、エッジの重みも同様にして記憶しておきましょう。これを仮に距離配列と呼びます。

```math
\begin{align}
&1: &[1.5, 8]\\
&2: &[1.5, 5]\\
&3: &[5, 2.3]\\
&4: &[8, 2.3]
\end{align}
```

隣接配列と距離配列の順序が対応していることに注意してください。この対応によって、「隣接配列からノード3とノード2が隣接していることがわかる。そのエッジの重みは、ノード2がノード3の隣接配列の1番目にあるから、ノード3の距離配列の1番目を見て5だとわかる」というような検索が可能になります。

隣接配列を使えば、ネットワークが大きなものになっても要領よく記憶しておくことができます。やりたい分析や解析によっては隣接行列や距離行列が必要になることもありますが、当面は隣接配列で十分だと思われます。

# モデリングしたネットワークを隣接配列にして書き出す
それではいよいよ、Rhinocerosでモデリングしたネットワークを隣接配列にしていきます。
環境はWindows10、Rhinoceros 5です。

## 方法
以下のコードでは、ネットワークがすべて直線分でモデリングされていることを要求します。

まず、Rhinocerosのコマンドに、

```
EditPythonScript
```

と入力してPythonエディタを起動し、以下のコードを張り付けて、走らせます。check pointと書かれた部分については、後に説明を加えます。

```rhino_network_write.py
# coding utf-8

import rhinoscriptsyntax as rs

import math
import random
import sys
import os

""" check point1 """
edges = rs.GetObjects("select objects")

adjacentList = {}
attributeList = {}
""" check point2 """
coordinations = []
coordinations_rounded = []  # for checking if a point is searched already or not

"""
example
adjacentList =  {0: [1, 2, 3], 1: [0, 2],...}
attributeList = {0: [2.0, 5.0, 8.6], 1: [2.0, 4.7], ...}
coordinations = [[4.5, 8.2, 0.0], [5.2, 3.6, 0.0], ...]
"""

cnt = 0   # point id
for i in range(len(edges)):
    edge = edges[i]
    try:
        end_points = rs.CurvePoints(edge)
    except:
        rs.ObjectColor(edge, (255, 0, 0))
        print("Error", edge)
        break
    
    l = rs.CurveLength(edge)
    """ check point3 """
    kind = rs.ObjectLayer(edge)
    
    for p in end_points:
        """ check point4 """
        p_rounded = [round(p[0], 2), round(p[1], 2), round(p[2], 2)] # round to the second decimal place
        if p_rounded in coordinations_rounded: # this point is already searched
            pass
        else: # search a new point 
            coordinations.append(p)
            coordinations_rounded.append(p_rounded)
            adjacentList[cnt] = []
            attributeList[cnt] = []
            cnt += 1
            
    # two end points are connected
    point_ids = [coordinations_rounded.index([round(p[0], 2), round(p[1], 2), round(p[2], 2)]) for p in end_points]
    
    adjacentList[point_ids[0]].append(point_ids[1])
    adjacentList[point_ids[1]].append(point_ids[0])
    
    attributeList[point_ids[0]].append((l, kind)) # you can add another info in attributeList
    attributeList[point_ids[1]].append((l, kind))

for k in adjacentList.keys():
    rs.AddTextDot(k, coordinations[k])

# set your path to the project folder
f = open("C:\\Users\\Owner\\Desktop\\adjacentList.txt", "w")
for k, v in adjacentList.items():
    print(k, v)
    f.write(str(k)+"\n")
    for nei in v:
        f.write(str(nei)+",")
    f.write("\n")
f.close()

f = open("C:\\Users\\Owner\\Desktop\\attributeList.txt", "w")
for k, v in attributeList.items():
    print(k, v)
    f.write(str(k)+"\n")
    for ats in v:   #ats = (length, layer_name)
        for at in ats:
            f.write(str(at)+"\n")
    f.write("_\n")
f.close()

f = open("C:\\Users\\Owner\\Desktop\\coordinations.txt", "w")
for p in coordinations:
    for u in p:
        f.write(str(u)+"\n")
f.close()
```

コードが走り終わると、adjacentList.txt、attributeList.txt、coordinations.txtが得られます。

### コードの注記
**・check point1**
コードを走らせると、まず、オブジェクトを選択するよう求められますが、それがこの箇所になります。選択するオブジェクトは、モデリングしたネットワークを構成する直線分すべてです。

**・check point2**
分析結果の可視化など、追々ノードの座標が必要になるので、このコードの中で記憶しておきます。

**・check point3**
このコードでは、エッジの重みとしてエッジの長さを想定しています。ただ、分析の目的によっては長さだけでなく、性質（平坦な道なのか階段なのか、一般道なのか高速道なのか）も紐づけておきたくなることがあります。それらの性質ごとにレイヤー分けしてモデリングしたならば、この行でそのレイヤー名をエッジに紐づけて記憶しておくことができます。レイヤー名は半角英数字としてください。

**・check point4**
ネットワークをモデリングする際、スナップを効かせて線分の端点は一致させておくことが可能で、たいていの場合、座標が等しいかどうかでノードの同一性を確かめることが可能です。ただ、時折スナップを効かせられていないことに気づかなかった場合、同じノードが違うノードとしてデータ化されてしまうことがあります。それを防ぐために、あらかじめ許容誤差を設定しておいて、座標の差異がその誤差の範囲内なら同一ノードとみなすこととしています。ここでは、その誤差を小数第2位としていますが、ネットワークの広さやモデリングの際の単位に気を付けて設定してください。

### デモンストレーション
RhinocerosでPythonを動かすのが初めての人用に、画面遷移を示したいと思います。

まず、Rhinocerosを起動し、モデリングしたネットワークのファイルを開きます。次に、コマンドに

```
EditPythonScript
```

と打ち込み、エディタを起動します。
<img src="https://tbt-goat.github.io/home/tips/20201223/demo1.png">
上図では、歩道（network\street）と横断歩道（network\crosswalk）をレイヤーで分けてモデリングしています。

エディタ上部の緑の▶をクリックしてコードを走らせます。すると、Rhinocerosのコマンドに

```
select objects:
```

と出てくるので、ネットワーク全体を選択してEnterを押します。
<img src="https://tbt-goat.github.io/home/tips/20201223/demo2.png">

すると、adjacentList.txt、attributeList.txt、coordinations.txtが得られるとともに、ノードの位置にテキストドットで番号が振られます。出来上がったデータが正しいかどうか確かめる手段の一つとしてお使いください。
<img src="https://tbt-goat.github.io/home/tips/20201223/demo3.png">


# 【次回】書き出したネットワークデータをPythonで読み込む
お気づきの方も多いかもしれませんが、実は、本記事で紹介したコードの中で、すでに、隣接配列そのものを一度作っており、それをまた.txtにして書き出すという回りくどいことをしました。Pythonにはネットワーク分析関連のモジュールが豊富に用意されているのですが、Rhinoceros上のPythonでは使えないことが多いためこのようなことをしています。

次回は、得られた.txtを隣接配列に再翻訳することで、Python上での分析を可能にします。
[→RhinocerosでモデリングしたネットワークをPythonで読み込む③](https://qiita.com/TBT-GOAT/items/17424c30a091ec594e39)

一連の記事に関連するコードは、以下にあります。
[→関連するコード](https://github.com/TBT-GOAT/network_rhino)


# ホームページのご案内
建築・都市計画数理に関する私の研究の内容や、研究にまつわるtips等は、個人ホームページにて公開しています。ゆくゆくは、気軽に使える分析ツールも作っていきたいと思っているので、ぜひ覗きに来てください。
[→田端祥太の個人ホームページ](https://tbt-goat.github.io/home/index.html)