- 昨日の記事で、pythonのGraphillionパッケージを使ってみた
- 大まかなことが見えたので、整理し直して、チュートリアルの例から少し離れて、自分なりにGraphillionを使ってみるつもりで書いてみる
- python自体にも不慣れなので、その辺りも確認しながら
- まず、準備
sudo easy_install networkx # グラフオブジェクト用ライブラリ
sudo easy_install matplotlib # プロット用ライブラリ
sudo easy_install graphillion # Graphillion本体
sudo easy_install itertools # 列挙・組み合わせ関連のライブラリ
from graphillion import GraphSet
import graphillion.tutorial as tl
import networkx as nx
import matplotlib.pylab as plt
import itertools as it
- 世界を作る。Graphillionが扱うのは、全体集合としての無向グラフとそのサブグラフたちであるので、基本は全体集合としてのグラフの定義から始める
- グラフはノード集合とエッジ集合の組でできる。どんなものからスタートしてもよいが、ノード数を与え、その完全グラフ(すべてのノード間にエッジがある)を世界~universeとすれば、そのサブグラフを定義して、それを「小さな世界」とすることで任意のことができるので、まずはそこからやってみる
n = 5
k = it.combinations(range(1,n),2)
universe = []
for element in k:
universe.append((element))
universe
>>> universe
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
- このitertoolsというのは組み合わせ・順列などを整理してあるライブラリのようなので、少し横道にそれるが試してみる→こちら
- 完全グラフを世界にする用意ができたので、graphillionのグラフ世界を作ってみる
GraphSet.set_universe(universe)
- この操作により、GraphSetに「世界」が定まり、そこに定義されたエッジの集合の部分集合を部分グラフとしてオブジェクトとして扱えるようになる。これをしないと、GraphSetオブジェクト(世界グラフの部分グラフの集合)が作れない
- グラフセット(部分集合)の作り方はいくつかある
- 部分グラフの集合として作る
- エッジを含める・含めないという条件によって全サブグラフについて選別してサブグラフ集合を作る
- 条件を満足するサブグラフの集合を作る点ではエッジ選別と同じだが、「パス」「クリーク」「木・全域木」「サイクル」などグラフ性質を指定して集合を作る
- 以上は使い勝手がよいように提供された方法だが、カスタマイズするための低レベル方法もある
- エッジリストによって部分グラフを表し、その集合を作る
graph1 = [(1,4)]
gs1 = GraphSet([graph1])
gs1 = GraphSet([graph1])
gs1
>>> gs1
GraphSet([[(1, 4)]])
gs2 = [(1,2),(2,3)]
gs2 = GraphSet([graph1,graph2])
gs2
>>> gs2
GraphSet([[(1, 4)], [(1, 2), (2, 3)]])
- 辺について条件をつけて、その条件に合うすべての部分グラフの集合を作る
edYes = [(1,2),(1,3),(2,3),(2,4)]
edNo = [(3,4)]
gs3 = GraphSet({'include': edYes, 'exclude': edNo})
for element in gs3:
print(element)
>>> for element in gs3:
... print(element)
...
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)]
[(1, 2), (1, 3), (2, 3), (2, 4)]
-
- {}は条件指定をするが、includeの指定もexcludeの指定もなければ、すべての部分グラフを持つグラフセットができる。今、世界は完全グラフなので、この場合には、ノード集合の冪集合になる
gs4 = GraphSet({})
for element in gs4:
print(element)
>>> for element in gs4:
... print(element)
...
...
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)]
[(1, 2), (1, 3), (1, 4), (2, 3), (3, 4)]
[(1, 2), (1, 3), (1, 4), (2, 3)]
[(1, 2), (1, 3), (2, 3), (2, 4), (3, 4)]
[(1, 2), (1, 3), (2, 3), (2, 4)]
[(1, 2), (1, 3), (2, 3), (3, 4)]
[(1, 2), (1, 3), (2, 3)]
[(1, 2), (1, 3), (1, 4), (2, 4), (3, 4)]
[(1, 2), (1, 3), (1, 4), (2, 4)]
[(1, 2), (1, 3), (1, 4), (3, 4)]
[(1, 2), (1, 3), (1, 4)]
[(1, 2), (1, 3), (2, 4), (3, 4)]
[(1, 2), (1, 3), (2, 4)]
[(1, 2), (1, 3), (3, 4)]
[(1, 2), (1, 3)]
[(1, 2), (1, 4), (2, 3), (2, 4), (3, 4)]
[(1, 2), (1, 4), (2, 3), (2, 4)]
[(1, 2), (1, 4), (2, 3), (3, 4)]
[(1, 2), (1, 4), (2, 3)]
[(1, 2), (2, 3), (2, 4), (3, 4)]
[(1, 2), (2, 3), (2, 4)]
[(1, 2), (2, 3), (3, 4)]
[(1, 2), (2, 3)]
[(1, 2), (1, 4), (2, 4), (3, 4)]
[(1, 2), (1, 4), (2, 4)]
[(1, 2), (1, 4), (3, 4)]
[(1, 2), (1, 4)]
[(1, 2), (2, 4), (3, 4)]
[(1, 2), (2, 4)]
[(1, 2), (3, 4)]
[(1, 2)]
[(1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
[(1, 3), (1, 4), (2, 3), (2, 4)]
[(1, 3), (1, 4), (2, 3), (3, 4)]
[(1, 3), (1, 4), (2, 3)]
[(1, 3), (2, 3), (2, 4), (3, 4)]
[(1, 3), (2, 3), (2, 4)]
[(1, 3), (2, 3), (3, 4)]
[(1, 3), (2, 3)]
[(1, 3), (1, 4), (2, 4), (3, 4)]
[(1, 3), (1, 4), (2, 4)]
[(1, 3), (1, 4), (3, 4)]
[(1, 3), (1, 4)]
[(1, 3), (2, 4), (3, 4)]
[(1, 3), (2, 4)]
[(1, 3), (3, 4)]
[(1, 3)]
[(1, 4), (2, 3), (2, 4), (3, 4)]
[(1, 4), (2, 3), (2, 4)]
[(1, 4), (2, 3), (3, 4)]
[(1, 4), (2, 3)]
[(2, 3), (2, 4), (3, 4)]
[(2, 3), (2, 4)]
[(2, 3), (3, 4)]
[(2, 3)]
[(1, 4), (2, 4), (3, 4)]
[(1, 4), (2, 4)]
[(1, 4), (3, 4)]
[(1, 4)]
[(2, 4), (3, 4)]
[(2, 4)]
[(3, 4)]
[]
>>>
len(gs4)
>>> len(gs4)
64
- 木、パス、サイクル、クリークなど
- ある「世界」から条件に合致したサブグラフを抜き出すのに、「世界」が完全グラフだとちょっと退屈なので、次のようにしてみる
- まず、完全グラフという世界から、連結グラフのグラフセットを作り、そのうちの一つを世界に置き換えることにする
gconn = GraphSet.connected_components(range(1,n))
import random
gr = GraphSet(random.sample(gconn,1))
-
- どんなグラフなのかを図示するなら、grというグラフセットから要素を取り出し(1個しか要素がないので、取り出したものは、「それ自身」)、チュートリアルのdraw()関数を使う
tl.draw(gr.choice())
-
- ランダムに色々なグラフができているかを見たければ、以下を繰り返せばよい
tl.draw(GraphSet(random.sample(gconn,1)).choice())
-
- ちなみに、お絵描きはnetworkxを使っているので、グラフセットの要素をnetworkxのGraphオブジェクトにして、描くこともできる
nxg = nx.Graph(gr.choice())
pos = nx.spring_layout(nxg)
nx.draw(nxg,pos)
nx.draw_networkx_labels(nxg,pos)
plt.show()
GraphSet.set_universe(gr.choice())
gclq = GraphSet.cliques(3)
for element in gclq:
print(element)
gtree = GraphSet.trees(1)
for element in gtree:
print(element)
gtree2 = GraphSet.trees(1,is_spanning=True)
for element in gtree2:
print(element)
-
- Forest(森)は木の集合。is_spanning=Trueなら、複数のルートで全ノードを覆う場合が出せる(1ノードの木は出力しない???)
gforest = GraphSet.forests([1,2],is_spanning=True)
for element in gforest:
print(element)
gcycle = GraphSet.cycles()
for element in gcycle:
print(element)
ghamilton = GraphSet.cycles(True)
for element in ghamilton:
print(element)
gpath = GraphSet.paths(1,2)
for element in gpath:
print(element)
gpathH = GraphSet.paths(1,2,True)
for element in gpathH:
print(element)
- 低レベルでカスタマイズ対応した作成法
- 上記の色々なサブグラフ集合の作成は、GraphSet.graphsという低レベル方法を使って書いたもの。それぞれがどのように書いてあるかを抜き書きしてみることで、GraphSet.graphsについて理解することにする
- graphset.pyに定義されている
- 連結
@staticmethod
def connected_components(vertices, graphset=None):
"""Returns a GraphSet of connected components.
Args:
vertices: A list of vertices to be connected.
graphset: Optional. A GraphSet object. Components to be
stored are selected from this object.
Returns:
A new GraphSet object.
"""
return GraphSet.graphs(vertex_groups=[vertices], graphset=graphset)
-
-
- これを見ると、univreseを変更しなくてもオプション指定のgraphsetに適当なグラフセットを与えれば、グラフセットの中で条件を満たすグラフを取り出してくれるようだ。その様式はgraphsの仕様なので、他のすべてのgraphsを用いた方法に共通する
- 基本は1引数で、引数としてノードのリストを与えることで、graphsが返すのは、指定ノードのすべてが連結であるグラフが選ばれることがわかる
- 『オプション vertex_groupsは連結成分を指定する』(リストが複数のリストからなっていれば、それぞれのリストが連結を、異なるリストに属するノードは非連結であることを要請する
- クリーク
- エッジ本数をノード数kの完全グラフの本数としてneで指定している
- 各ノードの次数は、クリークに含まれていれば、k-1、含まれなければ、0なので、[0,k-1]というリストを持たせる
def cliques(k, graphset=None):
"""Returns a GraphSet of k-cliques.
Args:
k: An integer. The number of vertices in a clique.
graphset: Optional. A GraphSet object. Cliques to be
stored are selected from this object.
Returns:
A new GraphSet object.
"""
dc = {}
for v in GraphSet._vertices:
dc[v] = xrange(0, k, k - 1)
ne = range(k * (k - 1) / 2, k * (k - 1) / 2 + 1)
return GraphSet.graphs(vertex_groups=[[]], degree_constraints=dc,
num_edges=ne, graphset=graphset)
-
- 木
- ルートノードに指定したノードごとに連結・非連結の分離をし
- ノードの次数はspanning出なければ制約はなく、spanningならば、1以上、ノード数-1以下
- ループは許さない→木の条件はこれか!
def trees(root=None, is_spanning=False, graphset=None):
"""Returns a GraphSet of trees.
Args:
root: Optional. A vertex, at which trees are rooted.
is_spanning: Optional. True or False. If true, trees must
be composed of all vertices.
graphset: Optional. A GraphSet object. Trees to be stored
are selected from this object.
Returns:
A new GraphSet object.
"""
vg = [[]] if root is None else [[root]]
dc = None
if is_spanning:
dc = {}
for v in GraphSet._vertices:
dc[v] = xrange(1, len(GraphSet._vertices))
return GraphSet.graphs(vertex_groups=vg, degree_constraints=dc,
no_loop=True, graphset=graphset)
-
- 森:木が複数→ルートが複数で、ルートごとに連結・非連結を制御する
- 木と違うのは、vertex_groups制約。リストのリストとし、それぞれの要素としてのリストは、ノードID1個を持った「長さ1のリスト」
def forests(roots, is_spanning=False, graphset=None):
vg = [[r] for r in roots]
dc = None
if is_spanning:
dc = {}
for v in GraphSet._vertices:
if v not in roots:
dc[v] = xrange(1, len(GraphSet._vertices))
return GraphSet.graphs(vertex_groups=vg, degree_constraints=dc,
no_loop=True, graphset=graphset)
-
- サイクル
- ハミルトンサイクルの場合にはすべてのノード次数が2
- ハミルトンではないサイクルの場合には、ノード次数は0か2
def cycles(is_hamilton=False, graphset=None):
Args:
is_hamilton: Optional. True or False. If true, cycles must
be composed of all vertices.
graphset: Optional. A GraphSet object. Cycles to be stored
are selected from this object.
Returns:
A new GraphSet object.
"""
dc = {}
for v in GraphSet._vertices:
dc[v] = 2 if is_hamilton else xrange(0, 3, 2)
return GraphSet.graphs(vertex_groups=[[]], degree_constraints=dc,
graphset=graphset)
def paths(terminal1, terminal2, is_hamilton=False, graphset=None):
Args:
terminal1 and terminal2: Both end vertices of a paths.
graphset: Optional. A GraphSet object. Paths to be stored
are selected from this object.
Returns:
A new GraphSet object.
"""
dc = {}
for v in GraphSet._vertices:
if v in (terminal1, terminal2):
dc[v] = 1
else:
dc[v] = 2 if is_hamilton else xrange(0, 3, 2)
return GraphSet.graphs(vertex_groups=[[terminal1, terminal2]],
degree_constraints=dc,
no_loop=True, graphset=graphset)
Args:
vertex_groups: Optional. A nested list. Vertices in an
inner list are connected while those in different inner
lists are disconnected. For `[[1, 5], [3]]`, 1 and 5 are
connected, while they are not connected with 3.
degree_constraints: Optional. A dict with a vertex and a
range or int. The degree of a vertex is restricted by the
range. For `{1: 2, 6: range(2)}`, the degree of vertex 1
is 2 and that of 6 is less than 2, while others are not
cared.
num_edges: Optional. A range or int. This argument
specifies the number of edges used in graphs to be stored.
For `range(5)`, less than 5 edges can be used.
no_loop: Optional. True or False. This argument specifies
if loop is not allowed.
graphset: Optional. A GraphSet object. Graphs to be stored
are selected from this object.
linear_constraints: Optional. A list of linear constraints.
A linear constraint consists of weighted edges and
lower/upper bounds. An edge weight is a positive or
negative number, which defaults to 1. Weights of the edges
that are not included in the constraint are zeros. For
instance, `linear_constraints=[([(1, 2, 0.6), (2, 5),
(3, 6, 1.2)], (1.5, 2.0))]`, feasible graph weights are
between 1.5 and 2.0, e.g., `[(1, 2), (2, 3), (3, 6)]` or
`[(1, 2), (2, 5), (5, 6)]`.
See graphillion/test/graphset.py in detail.