Pythonの状態遷移パッケージ(transitions)を理解する【HSM編1】

Pythonで状態遷移を実装したり動作確認をしたい方に、Pythonの状態遷移パッケージ「[transitions](https://github.com/pytransitions/transitions)」の使い方を説明していきたいと思います。
transitionsはPythonで状態遷移を実現するためのパッケージで、状態遷移そのものは組込みとか制御などでよく使われるものですが、それをPythonで実現したい場合にこのパッケージが有用かと思います。
その他、transitionsの概要やインストール方法、当記事で作成している状態遷移図といったグラフ表示機能の導入(GraphMachine)や設定については[準備編](https://qiita.com/noca/items/53c01ac5afb687a98db8)の記事を参照頂けたらと思います。

# 今回の話
今回はHSMについての内容になります。
まずはHSMとは何かというところから、基本的な定義と動作方法、および若干の応用例を示します。
HSM独自のメソッドやメンバは次回以降に譲り、まずはその特殊なステートマシンについての紹介です。

# HSMとは
HSMは*Hierarchical State Machine(階層型ステートマシン)*の略語であり、その名の通り**階層構造を持つステートマシン**になります。
階層型というのはどういうことかというと、ステートマシンのある状態内に更に状態と遷移を持つステートマシンといったイメージになります。
例えばUML(Unified Modeling Language)のステートマシン図でもこの階層型ステートマシンは定義可能であり、以下のような図で表されます。
[![alt](http://www.plantuml.com/plantuml/svg/SoWkIImgAStDuG8oIb8Lb1oL51AB5HnXN2SmrqcXcai12622XX1HGnH18uYW4LWmMYjMGTSEg4J1IY6jA3_NgIGVeXme19OmWaaO0cN0Ue1Yg5gCm7fgkRWSmU9GOKUWA9T3QbuAqD40)](http://www.plantuml.com/plantuml/uml/SoWkIImgAStDuG8oIb8Lb1oL51AB5HnXN2SmrqcXcai12622XX1HGnH18uYW4LWmMYjMGTSEg4J1IY6jA3_NgIGVeXme19OmWaaO0cN0Ue1Yg5gCm7fgkRWSmU9GOKUWA9T3QbuAqD40)
このように状態BにはB1とB2という子状態があり、階層構造のステートマシンになっています。
これと同等の事がtransitionsパッケージでも実現可能です。

# transitionsによるHSM
まずは上記で示したUMLと同等の階層型ステートマシンをtransitionsパッケージを用いて実現してみます。
以下はサンプルコードになりますが、HSMはtransitionsの拡張機能であるためtransitions.extensionsからHierarchicalMachineクラスをimportする必要があります。

```Python:HSMの簡単な定義例
from transitions.extensions import HierarchicalMachine as Machine

states = [{'name': 'A'}, 
		  {'name': 'B', 'children':['B1', 'B2']}]	# 階層を持つ状態B

transitions = [
  ['fromAtoB', 	'A',	'B'],
  ['fromBtoB1', 	'B',	'B_B1'],
  ['fromB1toB2', 	'B_B1',	'B_B2'],
  ['fromB2toB1', 	'B_B2',	'B_B1'],
]

class Model(object):
	pass

model = Model()
machine = Machine(model=model, states=states, transitions=transitions, initial=states[0]['name'], 
				  auto_transitions=False, ordered_transitions=False)
```

HSMを実現する場合にも、この程度のコード量で実現できます。
こちらをグラフ表示すると以下のような状態遷移図ができあがります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/d78b9363-90e5-8f16-1faf-5c717b440350.png)
概ね冒頭のUMLと同じような図ができたと思います。
ひとまず作ったHSMを動かしてみます。

```Python:HSMの簡単な動作例1
>>> model.state			# まずは現在の初期確認
'A'
>>> model.fromAtoB()	# 状態Bへ遷移
True
>>> model.state
'B'
```

階層をもつ状態に対する遷移においても特殊なメソッドは必要なく、遷移(transitionsリスト)で定義されているトリガーイベントで遷移できます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/8e29e02b-3ccd-8c49-691a-0217a3f02979.png)
なおこの状態はまだ子状態B1でもなくB2でもなく、状態Bそのものになっています。
次に状態Bの子状態B1に遷移してみます。

```Python:HSMの簡単な動作例2
>>> model.fromBtoB1()
True
>>> model.state
'B_B1'
```

この操作により状態Bから子状態B1へ遷移しました。図示すると以下の通りです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/9ff9b0d4-a4c2-2c23-9dfc-a9fb6d44375c.png)
なお、子状態へのアクセスは定義例でもあるとおり「親状態 + 識別子 + 子状態」というように表します。
識別子はデフォルトでアンダーバー('\_')で、状態Bの子状態へは'B_B1'のようになります。
ちなみにこの識別子('\_')は変更することが可能です。

もちろん次のように子状態間（階層内）での遷移も可能です。

```Python:HSMの簡単な動作例3
>>> model.fromB1toB2()
True
>>> model.state
'B_B2'
```

子状態B2に遷移できたことが確認できたと思います。こちらを図示すると以下になります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/b22b087d-c23c-8507-fbff-a9c60f783fc4.png)
以上が簡単なHSMの動作例になります。
このように階層型のステートマシンを簡単に実現できることが理解できたかと思います。
もちろん、通常のステートマシン(transitions.Machine)と同様にコールバックを実行させることが可能ですが、詳細は後の記事で紹介します。

# 階層状態の遷移補足
ここからは階層状態を持つステートマシンに関する補足になります。
階層を持つ状態やその子状態への遷移、またはその逆に子状態からの遷移など、階層を持たないステートマシンにはない挙動があるので、それらについて補足説明します。

以下のコードは冒頭のステートマシンにちょっとした遷移と状態を加えた補足説明用のステートマシン定義例になります。
なお説明の中で階層外状態と言った場合、「状態Bの子状態(B1, B2)と同階層の外」という意味の状態で、今回は主に状態A、C、D、Eを指します。
また状態Bと階層を持たない各状態を親状態（こちらは状態Bを含む状態A、C、D、Eを指す）と呼ぶこととします。

```Python:階層状態の補足説明用ステートマシン定義
from transitions.extensions import HierarchicalMachine as Machine

states = ['A', {'name': 'B', 'children':['B1', 'B2'] }, 'C', 'D', 'E']
transitions = [
	['AtoB', 'A', 'B'],
	['BtoC', 'B', 'C'],
	['inB1', 'B', 'B_B1'],
	['B1toB2', 'B_B1', 'B_B2'],
	['B2toB1', 'B_B2', 'B_B1'],
	['B2toE', 'B_B2', 'E'],
	['CtoD', 'C', 'D'],
	['DtoB2', 'D', 'B_B2'],
]

class Model(object):
	pass

model = Model()
machine = Machine(model=model, states=states, transitions=transitions, initial='B', 
				  auto_transitions=False, ordered_transitions=False)
```

こちらも理解の為に図示しておきましょう。これにより以下のようなステートマシンが定義されました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/47608017-9919-7fd8-0de0-df3eaf5108bb.png)
現在初期状態はBであり、ここから色々と操作して遷移してみます。

## 階層状態からの遷移
親状態である状態Bおよび子状態であるB1もしくはB2からの遷移について確認してみます。

### 親状態間の遷移
まず状態B→状態Cへの遷移について確認します。

```Python:階層状態からの遷移1
>>> model.state		# まずは現在の状態を表示 
'B'
>>> model.BtoC()	# 状態Cへ遷移するトリガーイベント
True
>>> model.state		# トリガーイベント後の状態を確認
'C'
```

こちらは当然といえば当然ですが、現在の状態が(階層を持つ)状態Bから状態Cへは普通に遷移できます。
これは階層を持たない場合の状態B→状態Cという遷移となんら変わりません。
遷移結果を図に表すと以下のとおりです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/0e0db724-4612-b9dc-8d73-277a56561bb1.png)

### 子状態から階層外状態への遷移(遷移未定義)
次に子状態B2から状態Cに遷移できるか確認してみましょう
以下はあらかじめ子状態B2に状態を設定しておき、そこからの遷移になります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/301d9bd5-bb67-c53e-7816-e809c76e277b.png)
トリガーイベントとしては状態Bから状態Cへのトリガーイベントと同じモノを使ってみます。

```Python:階層状態からの遷移2
>>> model.state		# まずは現在の状態を表示 
'B_B2'
>>> model.BtoC()	# 状態Cへ遷移するトリガーイベント
True
>>> model.state		# トリガーイベント後の状態を確認
'C'
```

子状態B2から階層外である状態Cへ遷移できました。
これを図示すると以下のようになります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/d440832f-15ce-eae0-b458-dc737f76ada7.png)
このように「**親状態が別の状態への遷移を持っていれば、子状態にいても同じトリガーイベントで子状態を抜け遷移できる**」という事がわかります。

### 子状態からの階層外状態への遷移(遷移定義)
今度は親状態では定義されていない、子状態から階層外の別の状態への遷移について確認してみます。
こちらも状態Bの子状態B2からはじめてみます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/301d9bd5-bb67-c53e-7816-e809c76e277b.png)
子状態B2には階層外の状態Eへの遷移が定義されています。
こちらは以下の通り状態B2から状態E間に定義されたトリガーイベントで遷移することができます。

```Python:階層状態からの遷移3
>>> model.state		# まずは現在の状態を表示 
'B_B2'
>>> model.B2toE()	# 状態Eへ遷移するトリガーイベント
True
>>> model.state		# トリガーイベント後の状態を確認
'E'
```

こちらを図示すると以下の通りです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/5398f298-42ef-d994-5872-939a3b080a9b.png)
一方で、状態Bから状態Eへの遷移はできるでしょうか。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/47608017-9919-7fd8-0de0-df3eaf5108bb.png)
現在状態(state)が状態Bにおいて、B2toEトリガーイベントをおこしてみます。

```Python:階層状態からの遷移4
>>> model.state		# まずは現在の状態を表示 
'B'
>>> model.B2toE()	# 状態Eへ遷移するトリガーイベント
Traceback (most recent call last):
～略～
transitions.core.MachineError: "Can't trigger event B2toE from state B!"
>>> model.state		# トリガーイベント後の状態を確認
'B'
```

状態Bから状態Eへのトリガーイベントを起こしてみると、上記の通り例外が発生し遷移もできませんでした。
このようにHSMにおいて「**子状態のみで定義された階層外状態への遷移は親状態からは遷移できない**」という特徴があります。

## 階層外状態からの遷移
今度は階層外遷移(ここでは状態D）から子状態を持つ状態Bや子状態B2への遷移について確認してみましょう。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/b18611e9-96ef-8fbd-0cdc-97d8645d3a63.png)
状態Dには状態B2への遷移を行うためのトリガーイベントが定義されています。
これは以下の通り、状態DからB2へ遷移することが可能です。

```Python:階層外状態からの遷移（状態Dから子状態B2へ)
>>> model.state		# まずは現在の状態を表示 
'D'
>>> model.DtoB2()	# 子状態B2へ遷移するトリガーイベント
True
>>> model.state		# トリガーイベント後の状態を確認
'B_B2'
```

これを図示すると以下の通りです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/0bee0349-4dd7-9cbb-c264-86803e3cd259.png)
一方で状態Dから状態Bへの遷移は行う事はできるでしょうか。
こちらは当然のことながら、状態Dから親状態Bへの遷移そのものが定義されていないので、状態Dから親状態Bへは遷移できません。
状態Dから状態Bへの遷移を行うためには、新たにadd_transitionメソッドなどで遷移を追加しないと実現することができない点にご注意ください。

# 複雑なHSMの実現
ちょっと応用というかHSMのいくつかの実装例を示します。
HSMを使うとより大規模なステートマシンを実現することができます。

## 複数の階層状態を持つステートマシン
まずは複数の階層状態を持つステートマシンになります。
今回は2つの階層状態を定義して、hasSDフラグの有無で遷移する方を選択しています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/2b8caa5d-3b1d-212d-9f0d-30f6ff93df9b.png)
例えば組込みでいうとプログラム開始時にSDカードの有無で状態遷移を変えたい場合などに応用できたりします。（もちろんこれよりシンプルな定義はありえます）

コードは長くなったので、以下にしまっておきます。
<details><summary>定義例と実行例はここをクリックして下さい。 </summary><div>

```Python:複数の階層状態を持つステートマシンの定義例
from transitions.extensions import HierarchicalMachine as Machine
from transitions.extensions.nesting import NestedState

state_start = {'name': 'Default'}
child_state1 = {'name': 'DisableSD', 	'children':['init', 'processing']}
child_state2 = {'name': 'EnableSD', 	'children':['init', 'processing']}
state_end = {'name': 'End'}
states = [state_start, child_state1, child_state2, state_end]
child = lambda state, no: state['name'] + NestedState.separator +state['children'][no]

transitions = [
  {'trigger':'start',	'source':state_start['name'],		'dest':child(child_state1, 0), 	'unless':'hasSD'},
  {'trigger':'start',	'source':state_start['name'],		'dest':child(child_state2, 0), 	'conditions':'hasSD'},
  {'trigger':'end',		'source':child_state1['name'],		'dest':state_end['name']},
  {'trigger':'end',		'source':child_state2['name'],		'dest':state_end['name']},
]

child_transitions = [
	{'trigger':'ok',		'source':child(child_state1, 0),	'dest':child(child_state1, 1)},
	{'trigger':'ok',		'source':child(child_state2, 0),	'dest':child(child_state2, 1)},
]
transitions += child_transitions

class Model(object):
	def __init__(self, hasSD=False):
		self.__hasSD = hasSD
		
	def hasSD(self):
		return self.__hasSD

model = Model(hasSD=False)
machine = Machine(model=model, states=states, transitions=transitions, initial=states[0]['name'], 
				  auto_transitions=False, ordered_transitions=False)
```

動作例は以下の通り。

```Python:複数の階層状態を持つステートマシンの動作例(hasSD=False時)
>>> model.state
'Default'
>>> model.start()
True
>>> model.state
'DisableSD_init'
>>> model.ok()
True
>>> model.state
'DisableSD_processing'
>>> model.end()
True
>>> model.state
'End'
```
</div></details>


# 多段階層状態を持つステートマシン
次に2階層の階層状態を持つステートマシンになります。
こちらは子状態Runningの中に更に子状態Connectedがあります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/291448/56f0e199-3466-7f9e-1a4a-e728356e6cc8.png)
応用としてはデバイスや通信の接続状態によって状態を管理したい場合に使えるかと思います。（もちろんこれよりシンプルな定義はありえます）
下記コードの動作例でも示していますが、親状態のトリガーイベントを使って、子状態の子状態（孫状態）から階層外への遷移（たとえばEnd状態）への遷移も可能です。

コードは長くなったので、以下にしまっておきます。
<details><summary>定義例と実行例はここをクリックして下さい。 </summary><div>

```Python:多段階層状態を持つステートマシンの定義例
from transitions.extensions import HierarchicalMachine as Machine

# 子の状態
c_mode1 = {'name': 'Wait'}
c_mode2 = {'name': 'Connected', 'children':[{'name':'Prepare'}, {'name':'Processing'}]}

# 親の状態
p_init = {'name': 'Initialize'}
p_run = {'name': 'Running', 	'children':[c_mode1, c_mode2]}
p_end = {'name': 'End'}
states = [p_init, p_run, p_end]

class Model(object):
	pass

model = Model()
machine = Machine(model=model, states=states, initial=states[0]['name'], 
				  auto_transitions=False, ordered_transitions=False)

# 遷移定義(階層状態クラスのメンバ変数childrenはmachineインスタンス生成後でないと使えないため）
child = lambda state, no : machine.get_state(state).children[no].name	#子の状態名を取得する
machine.add_transitions([
  	{'trigger':'initialized',	'source':p_init['name'],					'dest':child(p_run['name'], 0)},
  	{'trigger':'terminate',		'source':p_run['name'],						'dest':p_end['name']},
	{'trigger':'connect',		'source':child(p_run['name'], 0),			'dest':child(child(p_run['name'], 1), 0)},
	{'trigger':'disconnect',	'source':child(p_run['name'], 1),			'dest':child(p_run['name'], 0)},
	{'trigger':'ok',			'source':child(child(p_run['name'], 1), 0),	'dest':child(child(p_run['name'], 1), 1)}
])
```

動作例は以下の通り。

```Python:多段階層状態を持つステートマシンの動作例
>>> model.state
'Initialize'
>>> model.initialized()
True
>>> model.state
'Running_Wait'
>>> model.connect()
True
>>> model.state
'Running_Connected_Prepare'
>>> model.ok()
True
>>> model.state
'Running_Connected_Processing'
>>> model.terminate()
True
>>> model.state
'End'
```
</div></details>


# まとめ
HSMの基本動作を確認しましたが、まとめると以下になります。

* transitions.extensions.HierarchicalMachineで階層型ステートマシンを定義できる
* 階層型ステートマシンでは通常のステートマシン同様に状態、遷移に関するメソッドやコールバックを定義/設定できる。
* 親状態が別の状態への遷移を持っていれば、子状態にいても同じトリガーイベントで子状態を抜け遷移できる
* 子状態のみで定義された階層外状態への遷移は親状態からは遷移できない。その逆もしかり。
* 階層を持つ状態は複数定義可能であり、同時に多段階層をもつ状態も実現できる。

ということで、今回はHSMの実現と基本動作を紹介しました。
HSMは複雑なステートマシンを実現できる一方、設計が複雑になりテストが大変になったりするので必要以上に使うことはありません。
シンプルな設計で機能を実現できることが一番なので、HSMを使われる際はその辺りもよくよく考えた上で使われると良いかと思います。
次回は、HSM独自の状態や細かなオプション、メソッド、メンバ変数等について紹介したいと思います。
以上、少しでもお約に立てればと思います。