0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで〇×ゲームのAIを一から作成する その148 Mbtree_Anim でのフレーム間の差分データの表示

Last updated at Posted at 2025-01-19

目次と前回の記事

これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

リンク 説明
marubatsu.py Marubatsu、Marubatsu_GUI クラスの定義
ai.py AI に関する関数
test.py テストに関する関数
util.py ユーティリティ関数の定義。現在は gui_play のみ定義されている
tree.py ゲーム木に関する Node、Mbtree クラスの定義
gui.py GUI に関する処理を行う基底クラスとなる GUI クラスの定義

AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

Mbtree_Anim のバグの修正と改良

前回の記事では αβ 法での枝狩りが行われたノードの数などの視覚化を行うために、Mbtree_Anim で表示されているアニメーションのフレームまでの、下記の A、P、M、R の値を表示するという改良を行いました。

記号 意味 見出し
A αβ 法で計算を行ったノードの数 計算済
P αβ 法で枝狩りを行ったノードの数 枝狩り
M ミニマックス法で計算を行ったノードの数。M = A + P で計算できる 合計
R M に対する A の比率。A / M で計算できる 割合

しかし、現状では 枝狩りが行われた際 に、いくつのノードの枝狩りが行われたかわかりづらい という問題があります。言葉の説明ではわかりづらいので具体例を挙げます。

下図は下記のプログラムを実行し、4485 フレーム目を表示した際の図で、図の「β 狩り」の文字が赤く表示されていることから、次のフレームで β 狩りが行われる ことがわかります。

from tree import Mbtree, Mbtree_Anim

mbtree = Mbtree(algo="df")
mbtree.calc_score_by_ab(mbtree.root)
Mbtree_Anim(mbtree, isscore=True)

実行結果

実際に、次の 4486 フレーム目では下図のように枝狩りが行われたノードがさらに暗く表示されます。その際に、枝狩りが行われたにも関わらず「枝狩り」の 右の数字が 9179 のまま変化しない という問題が発生していることがわかります。

ただし、その次 の評価値が確定した 4487 フレーム目では下図のように 「枝狩り」の右の数字 が 9179 から 14694 に 大きく増え、赤枠のノードの 評価値が確定 したので 「計算済」の右の数字が 1 増えています

上記から、以下の 2 種類の問題がある事がわかります。

  • 枝狩りが行われた際 に、「枝狩り」の右の数値が増えるフレーム1 つ遅れる
  • 枝狩り によって処理が省略された ノードの数 は 14694 - 9179 = 5515 という 計算を行うことで知ることができる が、そのような計算を行うのは面倒 である

前者についてはバグなので修正し、後者については枝狩りが行われた際に 処理が省略されたノードの数を表示 するという改良を行うことにします。

バグの修正

最初に、枝狩りの数値の表示が 1 フレーム遅れて増えるバグを修正します。このバグの原因について少し考えてみて下さい。

バグの原因の検証と修正

下記は、calc_score_by_ab の中で、max ノードの 子ノードの評価値が計算された次のアニメーションのフレームの処理を記録 するプログラムで、以下のような処理を行っています。この処理にバグの原因があるので、何が原因であるかについて少し考えてみて下さい。

  • 1、2 行目:このフレームで行われた処理に関するデータを nodelist_by_score 属性と ablist_by_score 属性の要素に追加する
  • 4 ~ 8 行目:β 狩りが行われた場合に残りの子ノードに対して assign_pruned_index を呼び出すことで、枝狩りが行われたノードに対する処理を行う。枝狩りが行われたノードの数 を表す num_pruned 属性の値を増やす処理 は、assign_pruned_index で行われる
1  self.nodelist_by_score.append(node)
2  self.ablist_by_score.append((alpha, beta, None, "update",
3                               self.num_calculated, self.num_pruned))
4  if score >= beta:
5      index = node.children.index(childnode)
6      for prunednode in node.children[index + 1:]:
7          assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
8      break

問題の原因は以下の通りです。

  • β 狩りが行われた場合4 ~ 8 行目 の処理によって枝狩りが行われた数を計算して num_pruned 属性の値が更新 される
  • このフレームに関するデータ は、その前の 2 行目で ablist_by_score に追加される ので、追加されたデータの中の self.num_pruned は、β 狩りが行われる前 の枝狩りが行われたノードの 数を表している

従って、このバグを修正するためには ablist_by_score にフレームのデータを追加する処理を、下記のプログラムのように β 狩りに関する処理の後で行う 必要があります。

  • 9、10 行目:4 行目の後に記述していた ablist_by_score にフレームのデータを追加する処理を、β 狩りに関する処理を行った後に移動する
  • 5 ~ 8、11、12 行目:β 狩りに関する処理を行った後で上記の 9 行目の処理が行われるようにするために、8 行目の後にあった break 文を削除し、β 狩りに関する処理を行った後の 11、12 行目にその処理を移動する
  • 13 ~ 21 行目:min ノードに対する処理でも、上記と同様の修正を行う
 1  from marubatsu import Marubatsu
 2
 3  def calc_score_by_ab(self, abroot, debug=False):    
元と同じなので省略
 4                      self.nodelist_by_score.append(node)
 5                      if score >= beta:
 6                          index = node.children.index(childnode)
 7                          for prunednode in node.children[index + 1:]:
 8                              assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
 9                      self.ablist_by_score.append((alpha, beta, None, "update",
10                                                   self.num_calculated, self.num_pruned))
11                      if score >= beta:
12                          break
元と同じなので省略
13                      self.nodelist_by_score.append(node)
14                      if score <= alpha:
15                          index = node.children.index(childnode)
16                          for prunednode in node.children[index + 1:]:
17                              assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
18                      self.ablist_by_score.append((alpha, beta, None, "update",
19                                                   self.num_calculated, self.num_pruned))
20                      if score <= alpha:
21                          break
元と同じなので省略
22      
23  Mbtree.calc_score_by_ab = calc_score_by_ab
行番号のないプログラム
from marubatsu import Marubatsu

def calc_score_by_ab(self, abroot, debug=False):           
    def assign_pruned_index(node, index):
        node.pruned_index = index
        self.num_pruned += 1
        for childnode in node.children:
            assign_pruned_index(childnode, index)
        
    def calc_ab_score(node, alpha=float("-inf"), beta=float("inf")):
        self.nodelist_by_score.append(node)
        self.ablist_by_score.append((alpha, beta, None, "start",
                                     self.num_calculated, self.num_pruned))
        if node.mb.status != Marubatsu.PLAYING:
            self.calc_score_of_node(node)
            if node.mb.turn == Marubatsu.CIRCLE:
                alpha = node.score
            else:
                beta = node.score
        else:
            if node.mb.turn == Marubatsu.CIRCLE:
                for childnode in node.children:
                    score = calc_ab_score(childnode, alpha, beta)
                    self.nodelist_by_score.append(node)
                    self.ablist_by_score.append((alpha, beta, score, "score",
                                                 self.num_calculated, self.num_pruned))
                    if score > alpha:
                        alpha = score
                    self.nodelist_by_score.append(node)
                    if score >= beta:
                        index = node.children.index(childnode)
                        for prunednode in node.children[index + 1:]:
                            assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
                    self.ablist_by_score.append((alpha, beta, None, "update",
                                                 self.num_calculated, self.num_pruned))
                    if score >= beta:
                        break
                node.score = alpha
            else:
                for childnode in node.children:
                    score = calc_ab_score(childnode, alpha, beta)
                    self.nodelist_by_score.append(node)
                    self.ablist_by_score.append((alpha, beta, score, "score",
                                                 self.num_calculated, self.num_pruned))
                    if score < beta:
                        beta = score
                    self.nodelist_by_score.append(node)
                    if score <= alpha:
                        index = node.children.index(childnode)
                        for prunednode in node.children[index + 1:]:
                            assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
                    self.ablist_by_score.append((alpha, beta, None, "update",
                                                 self.num_calculated, self.num_pruned))
                    if score <= alpha:
                        break
                node.score = beta

        self.nodelist_by_score.append(node)
        self.num_calculated += 1     
        self.ablist_by_score.append((alpha, beta, None, "end",
                                     self.num_calculated, self.num_pruned))
        node.score_index = len(self.nodelist_by_score) - 1     
        return node.score

    from ai import dprint       
    for node in self.nodelist:
        node.score_index = float("inf")
        node.pruned_index = float("inf")
    self.nodelist_by_score = []
    self.ablist_by_score = []
    self.num_calculated = 0
    self.num_pruned = 0
    calc_ab_score(abroot)
    total_nodenum = self.num_pruned + self.num_calculated
    ratio = self.num_calculated / total_nodenum * 100
    dprint(debug, "計算したノードの数",  self.num_calculated)
    dprint(debug, "枝狩りしたノードの数",  self.num_pruned)
    dprint(debug, "合計",  total_nodenum)
    dprint(debug, f"割合 {ratio:.1f}%")
    
Mbtree.calc_score_by_ab = calc_score_by_ab
修正箇所
from marubatsu import Marubatsu

def calc_score_by_ab(self, abroot, debug=False):    
元と同じなので省略
                    self.nodelist_by_score.append(node)
-                   self.ablist_by_score.append((alpha, beta, score, "score",
-                                               self.num_calculated, self.num_pruned))
                    if score >= beta:
                        index = node.children.index(childnode)
                        for prunednode in node.children[index + 1:]:
                            assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
-                           break
+                   self.ablist_by_score.append((alpha, beta, None, "update",
+                                                self.num_calculated, self.num_pruned))
+                   if score >= beta:
+                       break
元と同じなので省略
                    self.nodelist_by_score.append(node)
-                   self.ablist_by_score.append((alpha, beta, None, "update",
-                                                self.num_calculated, self.num_pruned))
                    if score <= alpha:
                        index = node.children.index(childnode)
                        for prunednode in node.children[index + 1:]:
                            assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
-                           break
+                   self.ablist_by_score.append((alpha, beta, None, "update",
+                                                self.num_calculated, self.num_pruned))
+                   if score <= alpha:
+                       break
元と同じなので省略
    
Mbtree.calc_score_by_ab = calc_score_by_ab

上記の修正後に下記のプログラムを実行して正しい処理が行われるようになったかどうかを確認します。

mbtree.calc_score_by_ab(mbtree.root)
Mbtree_Anim(mbtree, isscore=True)

下図は β 狩りの処理が行われた 4486 フレーム目の図で、先程と異なり 枝狩りの数が正しく 14694 に増えている ことが確認できます。また、このフレームでは ノードの評価値が確定しない ので 計算済の数は 1121 のまま増えていない ことも確認できます。

下図はこのノードの評価値が確定したその次 4487 フレーム目の図で、計算済の数が正しく 1121 から 1122 に 1 だけ増えている ことが確認できます。

フレーム間の差分データの表示

枝狩りの処理が行われた際に、枝狩りによって処理が省略されたノードの数を表示する方法として、本記事では 直前のフレーム に表示されていた「枝狩り」の数と 現在のフレーム で表示されている数の 差分を表示 するという方法を取ることにします。また、その際にせっかくなので「計算済」、「合計」などの差分も表示することにします。もっと良い表示方法を思いついた方は実装してみて下さい。

直前のフレームとの差分を計算するためには、直前のフレームの番号を記録しておく必要がある ので、その値を prev_frame という属性に記録する ことにします。

Mbtree_Anim クラスの __init__ メソッドの修正

prev_frame 属性を導入する場合は、その 初期化処理を行う必要 があります。Mbtree_Anim で フレームが最初に表示 された際には 直前のフレームは存在しない ので、prev_frameNone で初期化するという方法が考えられますが、None で初期化を行う と差分の計算処理を prev_frameNone の場合とそうでない場合で分けて行う必要がある点が面倒 です。

このような場合は、prev_frame 属性の初期値 として、最初に表示されるフレームである 0 を設定 するのが一般的だと思います。そのように初期化することで、最初に表示を行った際の差分がすべて 0 で計算される ことになります。

prev_frame 属性の初期化処理__init__ メソッドで行えば良い ので、下記のプログラムの 2 行目のように __init__ メソッドを修正します。

1  def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
元と同じなので省略
2      self.prev_frame = 0
3      super(Mbtree_Anim, self).__init__()
4      
5  Mbtree_Anim.__init__ = __init__
行番号のないプログラム
def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
    self.mbtree = mbtree
    self.isscore = isscore
    self.size = size
    self.width = 50
    self.height = 65
    self.nodelist = self.mbtree.nodelist_by_score if isscore else self.mbtree.nodelist 
    self.nodenum = len(self.nodelist)
    self.prev_frame = 0
    super(Mbtree_Anim, self).__init__()
    
Mbtree_Anim.__init__ = __init__
修正箇所
def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
元と同じなので省略
+   self.prev_frame = 0
    super(Mbtree_Anim, self).__init__()
    
Mbtree_Anim.__init__ = __init__

Mbtree_Anim クラスの update_ab メソッドの修正

次に、update_ab メソッドを下記のプログラムのように、直前のフレームとの差分を表示 するように修正します。なお、直前のフレームを表す prev_frame 属性には __init__ メソッドで 0 が代入 されており、その値を変更する処理をまだ記述していない ので、現時点では 常に最初の 0 フレーム目との差分が表示 されます。prev_frame 属性の更新の処理はこの後で記述します。

  • 6 行目:直前のフレームのデータから、A と P の値を取り出して変数に代入する。変数の名前は現在のフレームの A と P を代入する変数の前に prev_ をつけた名前とした。また差分を計算する際に必要のないデータは _ という変数に代入した
  • 7 行目:直前のフレームの M を計算する
  • 8 ~ 11 行目:現在のフレームと直前のフレームの A、P、M の差分を計算して変数に代入する。また、R としては M の差分に対する A の差分の比率 を計算することにした
  • 15 行目:上から表示する順で、差分を要素とする list を diff_datalist に代入する
  • 19 行目:18 行目と同様の方法で差分のデータを右揃えで描画する
 1  import matplotlib.patches as patches
 2  
 3  def update_ab(self):
元と同じなので省略
 4      num_total = num_calculated + num_pruned
 5      num_ratio = num_calculated / num_total if num_total != 0 else 0
 6      _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
 7      prev_num_total = prev_num_calculated + prev_num_pruned
 8      diff_num_calculated = num_calculated - prev_num_calculated
 9      diff_num_pruned = num_pruned - prev_num_pruned
10      diff_num_total = num_total - prev_num_total
11      diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0
12  
13      textlist = [ "計算済", "枝狩り", "合計", "割合" ]
14      datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
15      diff_datalist = [ diff_num_calculated, diff_num_pruned, diff_num_total, f"{diff_num_ratio * 100:.1f}%"]
16      for i in range(4):
17          self.abax.text(15, 1 - i * 0.7, textlist[i])
18          self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
19          self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
20  
21  Mbtree_Anim.update_ab = update_ab
行番号のないプログラム
import matplotlib.patches as patches

def update_ab(self):
    alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
    maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
    acolor = "red" if maxnode else "black"
    bcolor = "black" if maxnode else "red"
                            
    self.abax.clear()
    self.abax.set_xlim(-4, 23)
    self.abax.set_ylim(-1.5, 1.5)
    self.abax.axis("off")

    minus_inf = -3
    plus_inf = 4   
    alphacoord = max(minus_inf, alpha)
    betacoord = min(plus_inf, beta)
    
    color = "lightgray" if maxnode else "aqua"
    rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
                            height=1, fc=color)
    self.abax.add_patch(rect)
    rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
                            height=1, fc="yellow")
    self.abax.add_patch(rect)
    color = "aqua" if maxnode else "lightgray"
    rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
                            height=1, fc=color)
    self.abax.add_patch(rect)

    self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
    for num in range(minus_inf, plus_inf + 1):
        if num == minus_inf:
            numtext = "-∞"
        elif num == plus_inf:
            numtext = ""
        else:
            numtext = num
        self.abax.text(num, -1, numtext, ha="center")
        
    arrowprops = { "arrowstyle": "->"}
    self.abax.plot(alphacoord, 0, "or")
    self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=acolor)
    self.abax.plot(betacoord, 0, "ob")
    self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=bcolor)
    if score is not None:
        self.abax.plot(score, 0, "og")
        self.abax.annotate(f"score = {score}", xy=(score, 0),
                        xytext=((minus_inf + plus_inf) / 2, 1), 
                        arrowprops=arrowprops, ha="center")
            
    facecolorlist = ["aqua", "yellow", "lightgray"]
    textcolorlist = ["black", "black", "black"]
    if maxnode:
        nodetype = f"深さ {self.selectednode.mb.move_count} max node"
        textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
        if score is not None :
            if beta <= score:
                textcolorlist[0] = "red"
            elif alpha < score:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
    else:
        nodetype = f"深さ {self.selectednode.mb.move_count} min node"
        textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
        if score is not None :
            if score <= alpha:
                textcolorlist[0] = "red"
            elif score < beta:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
            
    if status == "start":
        facecolor = "white"
        nodetype += " 処理の開始"
    elif status == "score":
        facecolor = "lightyellow"
        nodetype += " 子ノードの評価値"
    elif status == "update":
        facecolor = "lightcyan"
        if maxnode:
            nodetype += " α 値の処理"
        else:
            nodetype += " β 値の処理"
    else:
        facecolor = "lavenderblush"
        nodetype += " 評価値の確定"
    self.abfig.set_facecolor(facecolor)
    self.abax.text(6, 1, nodetype)   
    for i in range(3):
        rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
        self.abax.add_patch(rect)
        self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])    
    
    num_total = num_calculated + num_pruned
    num_ratio = num_calculated / num_total if num_total != 0 else 0
    _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
    prev_num_total = prev_num_calculated + prev_num_pruned
    diff_num_calculated = num_calculated - prev_num_calculated
    diff_num_pruned = num_pruned - prev_num_pruned
    diff_num_total = num_total - prev_num_total
    diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0

    textlist = [ "計算済", "枝狩り", "合計", "割合" ]
    datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
    diff_datalist = [ diff_num_calculated, diff_num_pruned, diff_num_total, f"{diff_num_ratio * 100:.1f}%"]
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")

Mbtree_Anim.update_ab = update_ab
修正箇所
import matplotlib.patches as patches

def update_ab(self):
元と同じなので省略
    num_total = num_calculated + num_pruned
    num_ratio = num_calculated / num_total if num_total != 0 else 0
+   _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
+   prev_num_total = prev_num_calculated + prev_num_pruned
+   diff_num_calculated = num_calculated - prev_num_calculated
+   diff_num_pruned = num_pruned - prev_num_pruned
+   diff_num_total = num_total - prev_num_total
+   diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0

    textlist = [ "計算済", "枝狩り", "合計", "割合" ]
    datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
+   diff_datalist = [ diff_num_calculated, diff_num_pruned, diff_num_total, f"{diff_num_ratio * 100:.1f}%"]
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
+       self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")

Mbtree_Anim.update_ab = update_ab

上記の修正後に下記のプログラムを実行し、選択中のノード内の移動の右の > ボタンをクリックして 9351 フレーム目を表示すると、実行結果のように一番右の列に 0 フレーム目との差分の A、P、M と差分の割合のデータが表示されるようになります。なお、最初の 0 フレーム目の A、P、M はいずれも 0 なので、差分として表示されるデータ はいずれも 現在のフレームのデータと同じ になります。他のフレームの表示も確認してみて下さい。

Mbtree_Anim(mbtree, isscore=True)

実行結果

差分のデータであることの明確化

上図では差分として「2338」のような正の数値が表示されていますが、差分が正の値であった場合でも「+2338」 のように 符号を表示 することで、2338 だけ 増加した差分のデータであることが明確になります。従って、差分のデータを表示する場合常に符号を表示したほうがわかりやすい でしょう。

数値に常に符号をつけて表示するには、以前の記事で説明した f 文字列書式指定+ を記述します。具体的には、下記のプログラムのように update_ab メソッドを修正します。

  • 2 行目diff_datalist の A、P、M の要素を f 文字列の書式指定+d を記述することで、符号がついた整数の文字列に変換 する。書式指定の + が符合をつける、d が整数の文字列に変換することを表す
 1  def update_ab(self):
元と同じなので省略
 2      diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}", 
 3                        f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
 4      for i in range(4):
 5          self.abax.text(15, 1 - i * 0.7, textlist[i])
 6          self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
 7          self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
 8  
 9  Mbtree_Anim.update_ab = update_ab
行番号のないプログラム
def update_ab(self):
    alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
    maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
    acolor = "red" if maxnode else "black"
    bcolor = "black" if maxnode else "red"
                            
    self.abax.clear()
    self.abax.set_xlim(-4, 23)
    self.abax.set_ylim(-1.5, 1.5)
    self.abax.axis("off")

    minus_inf = -3
    plus_inf = 4   
    alphacoord = max(minus_inf, alpha)
    betacoord = min(plus_inf, beta)
    
    color = "lightgray" if maxnode else "aqua"
    rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
                            height=1, fc=color)
    self.abax.add_patch(rect)
    rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
                            height=1, fc="yellow")
    self.abax.add_patch(rect)
    color = "aqua" if maxnode else "lightgray"
    rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
                            height=1, fc=color)
    self.abax.add_patch(rect)

    self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
    for num in range(minus_inf, plus_inf + 1):
        if num == minus_inf:
            numtext = "-∞"
        elif num == plus_inf:
            numtext = ""
        else:
            numtext = num
        self.abax.text(num, -1, numtext, ha="center")
        
    arrowprops = { "arrowstyle": "->"}
    self.abax.plot(alphacoord, 0, "or")
    self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=acolor)
    self.abax.plot(betacoord, 0, "ob")
    self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=bcolor)
    if score is not None:
        self.abax.plot(score, 0, "og")
        self.abax.annotate(f"score = {score}", xy=(score, 0),
                        xytext=((minus_inf + plus_inf) / 2, 1), 
                        arrowprops=arrowprops, ha="center")
            
    facecolorlist = ["aqua", "yellow", "lightgray"]
    textcolorlist = ["black", "black", "black"]
    if maxnode:
        nodetype = f"深さ {self.selectednode.mb.move_count} max node"
        textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
        if score is not None :
            if beta <= score:
                textcolorlist[0] = "red"
            elif alpha < score:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
    else:
        nodetype = f"深さ {self.selectednode.mb.move_count} min node"
        textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
        if score is not None :
            if score <= alpha:
                textcolorlist[0] = "red"
            elif score < beta:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
            
    if status == "start":
        facecolor = "white"
        nodetype += " 処理の開始"
    elif status == "score":
        facecolor = "lightyellow"
        nodetype += " 子ノードの評価値"
    elif status == "update":
        facecolor = "lightcyan"
        if maxnode:
            nodetype += " α 値の処理"
        else:
            nodetype += " β 値の処理"
    else:
        facecolor = "lavenderblush"
        nodetype += " 評価値の確定"
    self.abfig.set_facecolor(facecolor)
    self.abax.text(6, 1, nodetype)   
    for i in range(3):
        rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
        self.abax.add_patch(rect)
        self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])    
    
    num_total = num_calculated + num_pruned
    num_ratio = num_calculated / num_total if num_total != 0 else 0
    _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
    prev_num_total = prev_num_calculated + prev_num_pruned
    diff_num_calculated = num_calculated - prev_num_calculated
    diff_num_pruned = num_pruned - prev_num_pruned
    diff_num_total = num_total - prev_num_total
    diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0

    textlist = [ "計算済", "枝狩り", "合計", "割合" ]
    datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
    diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}", 
                      f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")

Mbtree_Anim.update_ab = update_ab
修正箇所
def update_ab(self):
元と同じなので省略
-   diff_datalist = [ diff_num_calculated, diff_num_pruned, 
-                     diff_num_total, f"{diff_num_ratio * 100:.1f}%"]
+   diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}", 
+                     f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")

Mbtree_Anim.update_ab = update_ab

上記の修正後に下記のプログラムを実行すると、実行結果のように 差分の数値に符号が表示 されるようになったこと確認できます。

Mbtree_Anim(mbtree, isscore=True)

実行結果

直前のフレームの情報の更新方法

次に、直前のフレームを表す prev_frame 属性を 更新する処理を記述 する必要がありますが、どのように記述すれば良いかについて少し考えてみて下さい。

間違った更新方法

ぱっと思いつく方法として、update_ab メソッド内の 描画の更新を行う処理を行った直後prev_frame 属性を現在のアニメーションのフレームを表す self.play.value で更新する という方法があるでしょう。

この方法の考え方は以下の通りです。

  • フレームが変更 されると update_ab が呼び出されて描画の更新を行われる
  • update_ab 内で 描画の更新を行う処理を行った後prev_frame を現在のフレームに更新 すると、次にアニメーションのフレームが変更 されて update_ab が呼び出された際に、prev_frame には直前のフレームの値が代入 されていることになる

一見するとこの方法で問題がないように見えるかもしれませんが、この方法には問題があります。筆者も最初はこの方法を思いついて実際に実装を行ってしまいました ので、この方法の問題について説明することにします。

下記は、そのように update_ab を修正したプログラムです。

6 行目:描画を更新した後で prev_frame に現在のアニメーションのフレームを表す self.play.value を代入して更新する

1  def update_ab(self):
元と同じなので省略
2      for i in range(4):
3          self.abax.text(15, 1 - i * 0.7, textlist[i])
4          self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
5          self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
6      self.prev_frame = self.play.value
7
8  Mbtree_Anim.update_ab = update_ab
行番号のないプログラム
def update_ab(self):
    alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
    maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
    acolor = "red" if maxnode else "black"
    bcolor = "black" if maxnode else "red"
                            
    self.abax.clear()
    self.abax.set_xlim(-4, 23)
    self.abax.set_ylim(-1.5, 1.5)
    self.abax.axis("off")

    minus_inf = -3
    plus_inf = 4   
    alphacoord = max(minus_inf, alpha)
    betacoord = min(plus_inf, beta)
    
    color = "lightgray" if maxnode else "aqua"
    rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
                            height=1, fc=color)
    self.abax.add_patch(rect)
    rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
                            height=1, fc="yellow")
    self.abax.add_patch(rect)
    color = "aqua" if maxnode else "lightgray"
    rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
                            height=1, fc=color)
    self.abax.add_patch(rect)

    self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
    for num in range(minus_inf, plus_inf + 1):
        if num == minus_inf:
            numtext = "-∞"
        elif num == plus_inf:
            numtext = ""
        else:
            numtext = num
        self.abax.text(num, -1, numtext, ha="center")
        
    arrowprops = { "arrowstyle": "->"}
    self.abax.plot(alphacoord, 0, "or")
    self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=acolor)
    self.abax.plot(betacoord, 0, "ob")
    self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=bcolor)
    if score is not None:
        self.abax.plot(score, 0, "og")
        self.abax.annotate(f"score = {score}", xy=(score, 0),
                        xytext=((minus_inf + plus_inf) / 2, 1), 
                        arrowprops=arrowprops, ha="center")
            
    facecolorlist = ["aqua", "yellow", "lightgray"]
    textcolorlist = ["black", "black", "black"]
    if maxnode:
        nodetype = f"深さ {self.selectednode.mb.move_count} max node"
        textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
        if score is not None :
            if beta <= score:
                textcolorlist[0] = "red"
            elif alpha < score:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
    else:
        nodetype = f"深さ {self.selectednode.mb.move_count} min node"
        textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
        if score is not None :
            if score <= alpha:
                textcolorlist[0] = "red"
            elif score < beta:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
            
    if status == "start":
        facecolor = "white"
        nodetype += " 処理の開始"
    elif status == "score":
        facecolor = "lightyellow"
        nodetype += " 子ノードの評価値"
    elif status == "update":
        facecolor = "lightcyan"
        if maxnode:
            nodetype += " α 値の処理"
        else:
            nodetype += " β 値の処理"
    else:
        facecolor = "lavenderblush"
        nodetype += " 評価値の確定"
    self.abfig.set_facecolor(facecolor)
    self.abax.text(6, 1, nodetype)   
    for i in range(3):
        rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
        self.abax.add_patch(rect)
        self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])    
    
    num_total = num_calculated + num_pruned
    num_ratio = num_calculated / num_total if num_total != 0 else 0
    _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
    prev_num_total = prev_num_calculated + prev_num_pruned
    diff_num_calculated = num_calculated - prev_num_calculated
    diff_num_pruned = num_pruned - prev_num_pruned
    diff_num_total = num_total - prev_num_total
    diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0

    textlist = [ "計算済", "枝狩り", "合計", "割合" ]
    datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
    diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}", 
                      f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
    self.prev_frame = self.play.value

Mbtree_Anim.update_ab = update_ab
修正箇所
def update_ab(self):
元と同じなので省略
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
+   self.prev_frame = self.play.value

Mbtree_Anim.update_ab = update_ab

上記の修正後に下記のプログラムを実行し、選択中のノード内の移動の右の > ボタンをクリックして 9351 フレーム目を表示すると、実行結果のように 差分のデータにすべて +0 が表示される という問題が発生することがわかります。また、他のボタンをクリックして 別のフレームを表示 しても 同様に差分のデータにすべて +0 が表示される ことを実際に確認してみて下さい。また、この問題の原因について少し考えてみて下さい。

Mbtree_Anim(mbtree, isscore=True)

実行結果

バグの原因

この問題の原因は直前のフレームを表す prev_frame 属性の値の更新 を、描画の更新を行う update_ab メソッドの中で行っている 点にあります。そのため update_ab メソッドが、アニメーションのフレームが 変更されていない場合に呼び出される と、prev_frame が直前のフレームではなくなってしまいます

わかりづらいと思いますので、具体例として 0 フレーム目から 100 フレーム目、200 フレームに表示が変更 された場合の処理を説明します。

下記の表は、アニメーションの フレームが変更された際 に、update_ab1 回だけ 呼び出される場合の処理を表します。表の 太字の行 のように update_ab で表示を更新する際に prev_frame には直前のフレームの値が代入されている ので、差分が正しく描画されます。

現在のフレーム prev_frame の値
100 フレームが変更される前 0 0
100 フレーム目に表示が移動 100 0
update_ab での表示の更新中 100 0
update_ab の処理の終了後 100 100
200 フレーム目に表示が移動 200 100
update_ab での表示の更新中 200 100
update_ab の処理の終了後 200 200

一方、下記の表はアニメーションの フレームが変更された際 に、update_ab2 回 呼び出された場合の処理を表します。1 回目の update_ab の処理によって prev_frame の値が 現在のアニメーションのフレームに更新 されたしまうため、太字の行 のように 2 回目の update_ab で表示を更新する際 には prev_frame には現在のフレームが代入 されています。そのため、差分には必ず 0 が表示されます

現在のフレーム prev_frame の値
100 フレームが変更される前 0 0
100 フレーム目に表示が移動 100 0
1 回目の update_ab での表示の更新中 100 0
1 回目の update_ab の処理の終了後 100 100
2 回目の update_ab での表示の更新中 100 100
2 回目の update_ab の処理の終了後 100 100
200 フレーム目に表示が移動 200 100
1 回目の update_ab での表示の更新中 200 100
1 回目の update_ab の処理の終了後 200 200
2 回目の update_ab での表示の更新中 200 200
2 回目の update_ab の処理の終了後 200 200

update_ab が呼び出された回数の確認

上記では、フレームが変更された際に update_ab が 2 回以上呼び出されると差分に 0 が表示されることを示しましたが、フレームが変更された際に update_ab が何回呼び出されているの確認は行っていません。

そこで、フレームが変更された際に 本当に update_ab が 2 回以上呼び出されているかを確認 するために、下記のプログラムの 3 行目のように update_ab の中で prev_frame 属性の値を更新 した際に update_ab というメッセージを表示 するように修正します。

1  def update_ab(self):
元と同じなので省略
2      self.prev_frame = self.play.value
3      print("update_ab")
4
5  Mbtree_Anim.update_ab = update_ab
行番号のないプログラム
def update_ab(self):
    alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
    maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
    acolor = "red" if maxnode else "black"
    bcolor = "black" if maxnode else "red"
                            
    self.abax.clear()
    self.abax.set_xlim(-4, 23)
    self.abax.set_ylim(-1.5, 1.5)
    self.abax.axis("off")

    minus_inf = -3
    plus_inf = 4   
    alphacoord = max(minus_inf, alpha)
    betacoord = min(plus_inf, beta)
    
    color = "lightgray" if maxnode else "aqua"
    rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
                            height=1, fc=color)
    self.abax.add_patch(rect)
    rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
                            height=1, fc="yellow")
    self.abax.add_patch(rect)
    color = "aqua" if maxnode else "lightgray"
    rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
                            height=1, fc=color)
    self.abax.add_patch(rect)

    self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
    for num in range(minus_inf, plus_inf + 1):
        if num == minus_inf:
            numtext = "-∞"
        elif num == plus_inf:
            numtext = ""
        else:
            numtext = num
        self.abax.text(num, -1, numtext, ha="center")
        
    arrowprops = { "arrowstyle": "->"}
    self.abax.plot(alphacoord, 0, "or")
    self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=acolor)
    self.abax.plot(betacoord, 0, "ob")
    self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=bcolor)
    if score is not None:
        self.abax.plot(score, 0, "og")
        self.abax.annotate(f"score = {score}", xy=(score, 0),
                        xytext=((minus_inf + plus_inf) / 2, 1), 
                        arrowprops=arrowprops, ha="center")
            
    facecolorlist = ["aqua", "yellow", "lightgray"]
    textcolorlist = ["black", "black", "black"]
    if maxnode:
        nodetype = f"深さ {self.selectednode.mb.move_count} max node"
        textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
        if score is not None :
            if beta <= score:
                textcolorlist[0] = "red"
            elif alpha < score:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
    else:
        nodetype = f"深さ {self.selectednode.mb.move_count} min node"
        textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
        if score is not None :
            if score <= alpha:
                textcolorlist[0] = "red"
            elif score < beta:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
            
    if status == "start":
        facecolor = "white"
        nodetype += " 処理の開始"
    elif status == "score":
        facecolor = "lightyellow"
        nodetype += " 子ノードの評価値"
    elif status == "update":
        facecolor = "lightcyan"
        if maxnode:
            nodetype += " α 値の処理"
        else:
            nodetype += " β 値の処理"
    else:
        facecolor = "lavenderblush"
        nodetype += " 評価値の確定"
    self.abfig.set_facecolor(facecolor)
    self.abax.text(6, 1, nodetype)   
    for i in range(3):
        rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
        self.abax.add_patch(rect)
        self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])    
    
    num_total = num_calculated + num_pruned
    num_ratio = num_calculated / num_total if num_total != 0 else 0
    _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
    prev_num_total = prev_num_calculated + prev_num_pruned
    diff_num_calculated = num_calculated - prev_num_calculated
    diff_num_pruned = num_pruned - prev_num_pruned
    diff_num_total = num_total - prev_num_total
    diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0

    textlist = [ "計算済", "枝狩り", "合計", "割合" ]
    datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
    diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}", 
                      f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
    self.prev_frame = self.play.value
    print("update_ab")

Mbtree_Anim.update_ab = update_ab
修正箇所
def update_ab(self):
元と同じなので省略
    self.prev_frame = self.play.value
+   print("update_ab")

Mbtree_Anim.update_ab = update_ab

上記の修正後に下記のプログラムを実行すると、画像の下に実行結果の 1 行目のように update_ab が 1 回だけ表示 されるので、最初の描画を行うために update_ab が 1 回だけ呼び出された ことが確認できます。なお、実行結果の 3 行目は Mbtree_Anim の呼び出しによって 返り値として返された Mbtree_Anim クラスのインスタンス を表します。

Mbtree_Anim(mbtree, isscore=True)

実行結果

update_ab

<tree.Mbtree_Anim at 0x15b4feb9930>

その後で選択中のノード内の移動の右の > ボタンをクリックして フレームを変更 すると下記のように update_ab が 2 回表示される ので、確かに > ボタンをクリックして フレームを変更 したことで update_ab メソッドが 2 回呼び出されている ことが確認できました。

update_ab
update_ab

他のフレームを移動するボタンをクリック した際に、同様に update_ab が 2 回表示される ことを確認して下さい。

上記から、差分に常に 0 が表示されるという 問題の原因 が、フレームが変更された際 に、update_ab2 回 呼び出されたことであることが確認できました。

間違った考え方をした原因

先程、この方法の考え方を以下のように説明しました。この考え方はフレームが変更された際に update_ab が 1 回だけしか呼び出されない場合は正しい ですが、update_ab2 回以上 呼び出される場合は 正しくありません

  • アニメーションのフレームが変更されると update_ab を呼び出して描画の更新を行う
  • update_ab 内で描画の更新を行う処理を行った後で prev_frame を現在のフレームに更新すると、次にアニメーションのフレームが変更されて update_ab が呼び出された際に、prev_frame には直前のフレームの値が代入されていることになる

筆者が最初に上記の方法を考えたのは、フレームが変更された際に update_ab を 1 回だけ 呼び出されるように プログラムを記述したつもり だったからです。また、そのように 勘違いした理由 は以下の通りです。

  • Mbtree_Anim では update_abupdate_gui メソッドの中 の処理で 1 度だけ 呼び出されるので、update_ab が呼び出される回数update_gui が呼び出される回数と等しい
  • 例えば、フレームを 1 つ先に移動する > ボタンをクリック した際の イベントハンドラ は下記のプログラムのように定義されている。このプログラムから > ボタンをクリックすると、update_gui メソッドが 1 回だけ呼び出される と考えた
  • 従って > ボタンをクリックすると update_ab メソッドが 1 回だけ呼び出される と考えた
  • 他のフレームを移動するボタンに関しても同様だと考えた
        def on_next_button_clicked(b=None):
            self.play.value += 1
            self.update_gui()

上記の筆者の考え方は間違っていますが、どこに間違いがあるかがわかる人は、それほど多くないと思います。

このような勘違いは、他人が作ったモジュールを利用する際に良く起きます。今回の場合は ipywidgets モジュールを利用する際に 行われると筆者が思っていた処理 が、実際に行われている処理と異なっていた点 が問題の原因です。

update_ab が複数回呼び出されてしまう原因の検証

ipywidgets のドキュメントをしっかりと読んで、ipywidgets モジュールが行う処理を理解すれば何を勘違いしているかを理解することは可能ですが、ドキュメントはかなり長く、すべてを正しく理解するのはかなり困難 です1。そこで、フレームが変更された際に update_ab が 2 回呼び出されてしまう原因 を自力で検証することにします。

先ほど説明したように、Mbtree_Anim の処理の中で update_ab メソッドを呼び出す処理が記述 されているのは update_gui メソッドの中だけ です。従って update_gui メソッドが呼び出された場所を確認 することで update_ab メソッドが 2 回呼び出された原因を確認 することができます。

update_gui メソッドを呼び出す処理が記述されているのは create_event_handler メソッドの中だけ なので、update_gui メソッドを 呼び出す処理を行う直前 に、下記のプログラムのように printupdate_gui を呼び出した ローカル関数の名前 を表示するように create_event_handler メソッドを修正します。

  • 3、8、13、17 行目update_gui を呼び出す直前に処理を行っているローカル関数の名前を表示する処理を追加する
 1  def create_event_handler(self):
 2      def on_play_changed(changed):
 3          print("on_play_changed")
 4          self.update_gui()
 5              
 6      def on_prev_button_clicked(b=None):
 7          self.play.value -= 1
 8          print("on_prev_button_clicked")
 9          self.update_gui()
10          
11      def on_next_button_clicked(b=None):
12          self.play.value += 1
13          print("on_next_button_clicked")
14          self.update_gui()
元と同じなので省略
15      def change_frame(edge_status, diff, status_list):
元と同じなので省略
16          self.play.value = frame
17          print("change_frame")
18          self.update_gui()
元と同じなので省略
19    
20  Mbtree_Anim.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
    def on_play_changed(changed):
        print("on_play_changed")
        self.update_gui()
            
    def on_prev_button_clicked(b=None):
        self.play.value -= 1
        print("on_prev_button_clicked")
        self.update_gui()
        
    def on_next_button_clicked(b=None):
        self.play.value += 1
        print("on_next_button_clicked")
        self.update_gui()

    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)

    self.play.observe(on_play_changed, names="value")
    
    def change_frame(edge_status, diff, status_list):
        frame = self.play.value
        selectednode = self.mbtree.nodelist_by_score[frame]
        selectedstatus = self.mbtree.ablist_by_score[frame][3]
        if selectedstatus == edge_status:
            return
        while True:
            frame += diff
            node = self.mbtree.nodelist_by_score[frame]
            status = self.mbtree.ablist_by_score[frame][3]
            if node == selectednode and status in status_list:
                break
        self.play.value = frame
        print("change_frame")
        self.update_gui()
            
    def on_node_first_button_clicked(b=None):
        change_frame("start", -1, ["start"])
            
    def on_node_prev_button_clicked(b=None):
        change_frame("start", -1, ["start", "score"])

    def on_node_next_button_clicked(b=None):
        change_frame("end", 1, ["end", "score"])
        
    def on_node_last_button_clicked(b=None):
        change_frame("end", 1, ["end"])
    
    if self.abfig is not None:
        self.node_first_button.on_click(on_node_first_button_clicked)
        self.node_prev_button.on_click(on_node_prev_button_clicked)
        self.node_next_button.on_click(on_node_next_button_clicked)
        self.node_last_button.on_click(on_node_last_button_clicked)
    
Mbtree_Anim.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
    def on_play_changed(changed):
+       print("on_play_changed")
        self.update_gui()
            
    def on_prev_button_clicked(b=None):
        self.play.value -= 1
+       print("on_prev_button_clicked")
        self.update_gui()
        
    def on_next_button_clicked(b=None):
        self.play.value += 1
+       print("on_next_button_clicked")
        self.update_gui()
元と同じなので省略
    def change_frame(edge_status, diff, status_list):
元と同じなので省略
        self.play.value = frame
+       print("change_frame")
        self.update_gui()
元と同じなので省略
  
Mbtree_Anim.create_event_handler = create_event_handler

上記の修正後に下記のプログラムを実行した後で選択中のノード内の移動の右の > ボタンをクリックしてフレームを変更すると、実行結果のような表示が行われます。

Mbtree_Anim(mbtree, isscore=True)

実行結果

on_play_changed
update_ab
change_frame
update_ab

実行結果から、ローカル関数 on_play_changedchange_frame の処理によって update_ab が 2 回呼び出されている ことが確認できました。

下記は 他のフレームを移動するボタンを クリックした際に表示されるメッセージから、呼び出されるローカル関数をまとめた表 です。

1 回目に呼び出される関数 2 回目に呼び出される関数
上部の < on_play_changed on_prev_button_clicked
上部の > on_play_changed on_next_button_clicked
下部の << on_play_changed change_frame
下部の < on_play_changed change_frame
下部の > on_play_changed change_frame
下部の >> on_play_changed change_frame

1 回目の update_abすべて on_play_changed から呼び出される ことがわかります。下部の 4 つのボタン は下記のプログラムのように、いずれもそれらのボタンをクリックした際の イベントハンドラ内change_frame を呼び出す ことでフレームを移動する処理を行っているので、2 回目の update_ab は 上部のボタンも含めて いずれも ボタンをクリックした際の イベントハンドラから呼び出されている ことが確認できます。

        def on_node_first_button_clicked(b=None):
            change_frame("start", -1, ["start"])
                
        def on_node_prev_button_clicked(b=None):
            change_frame("start", -1, ["start", "score"])

        def on_node_next_button_clicked(b=None):
            change_frame("end", 1, ["end", "score"])
            
        def on_node_last_button_clicked(b=None):
            change_frame("end", 1, ["end"])

フレームを 1 つ前に移動する < ボタンをクリック した場合の処理を詳しく検証することにします。上記の表から、on_play_changedon_prev_button_clicked の順番で update_gui が呼び出された ことが確認できます。

下記はその 2 つの関数の定義で、on_play_changed はフレームを表す self.play.value の値が変更された場合 に呼び出される関数です。on_prev_button_clicked フレームを 1 つ前に移動する < ボタンをクリックした場合 に呼び出される関数です。

1  def on_play_changed(changed):
2      print("on_play_changed")
3      self.update_gui()
4        
5  def on_prev_button_clicked(b=None):
6      self.play.value -= 1
7      print("on_prev_button_clicked")
8      self.update_gui()

このことから、< ボタンをクリックすると以下の理由で update_gui が 2 回呼び出されることが確認できました。

  • on_prev_button_clicked8 行目 から update_gui が呼び出される
  • on_prev_button_clicked の 6 行目で self.play.value の値が変更された結果on_play_changed が呼び出され、その 3 行目から update_gui が呼び出される

上記から、勘違いの原因が self.play.value -= 1 という 一見すると値を代入する処理にしか見えない処理 によって on_play_changed が呼び出される ことに気が付かなかった点であることがわかります。

また、on_prev_button_clicked または on_play_changedどちらかのみから update_gui を呼び出す ようにプログラムを修正することでバグを修正することができますが、そのような修正方法は行わないほうが良い でしょう。その理由について少し考えてみて下さい。

イベントハンドラのように、何らかの処理に応じて後から別の関数を呼び出すような処理 は、このような 思わぬ処理が行われる可能性がある 点に注意が必要です。

< ボタンをクリックした際に、on_prev_button_clicked が最初に呼び出されるので、on_play_changed、on_prev_button_clicked の順で表示が行われる点がおかしいと思う人がいるかもしれないので補足します。

on_play_changed のように、ウィジェットの値が変更された際のイベントハンドラは、ウィジェットの observe メソッドによって結び付けます。observe メソッドによってウィジェットに結びつけられたイベントハンドラは、ウィジェットの値が変更された際にすぐに呼びだされるという性質があります。例えば、self.play.value -= 1 のようにウィジェットの値を代入処理によって変更した場合は、その次の行の処理が行われる前に on_play_changed が呼び出されます。

従って、< ボタンをクリックした際には下記の手順で処理が行われます。

  1. 6 行目が実行されて self.play.value の値が更新される
  2. 次の 7 行目の処理が行われる前に on_play_changed が呼び出され、その中の 2 行目で on_play_changed が表示される
  3. 7 行目が呼び出されて on_prev_button_clicked が表示される

update_ab 内で prev_frame を更新する方法の問題点

勘違いしている人が多いかもしれませんが、この バグの原因は フレームが変更された際に 複数回 update_ab が呼ばれることではありません。このバグの本当の原因は、prev_frame を更新する処理 を、フレームを変更する処理ではない場所で行っている 点にあります。

update_ab が行う処理は 描画の更新処理 であり、その処理は現状の Mbtree_Anim が行っているように、フレームが変更されない場合でも行われる可能性 があります。

従って、フレームを変更するボタンをクリックした際に update_ab が 1 回だけ呼び出されるように修正 した場合は、今後 Mbtree_Anim を修正した結果、別の場所から update_ab を呼び出す処理を記述 してしまうと、このバグが再び発生してしまう ことになります。

従って、このバグを修正する正しい方法 は、prev_frame を更新する処理 を、フレームが更新された際に行う ようにすることです。どのように修正すれば良いかについて少し考えてみて下さい。

IntSlider の操作によって行われる処理

正しい修正方法を説明する前に、フレームの移動を上部の IntSlider で行うこともできることに気が付きましたので、IntSlider でフレームを移動した場合のメッセージを確認することにします。この部分はおまけのようなものなので読み飛ばしても問題はありません。

IntSlider をドラッグしてフレームを移動した場合は、下記のように on_play_changed が 2 回呼ばれるようです。

on_play_changed
update_ab
on_play_changed
update_ab

2 回呼ばれる原因は IntSlider のドラッグ操作がリアルタイムに self.play.value の値に反映され、そのたびに on_play_changed が呼び出されることです。そのため、ドラッグ操作をゆっくり行った場合は、3 回以上 on_play_changed が呼ばれることになります。

また、様々な速度でドラッグ操作を行ってみた結果、どれだけ素早くドラッグ操作を行っても最低でも 2 回は on_play_changed が呼ばれることが判明しました。そのため、0 フレーム目からドラッグ操作を行った場合は、以下のような理由で下図のように差分の左の数値と差分の数値が一致しなくなるようです。

  • ドラッグ操作を終えるまでの間に何度か self.play.value の値が更新される
  • self.play.value の値が更新されるたびに update_ab が呼び出されて描画が更新される
  • ドラッグ操作が完了した時点では、ドラッグ操作の中で最後に描画が行われたアニメーションのフレームとの差分 のデータが表示される

なお、IntSlider では、下記の方法でも値を変更することができ、その場合は on_play_changed は 1 回しか呼び出されないようです。

  • IntSlider のボタンが表示されていない場所でクリックする
  • IntSlider の右の数値をクリックし、キーボードから値を変更する

IntSlider の値を変更した際に行われる処理に時間がかかる場合は、ドラッグ操作の途中で self.play.value の値の変更が行われるたびにその処理が行われてしまうとプログラムの動作が非常に重くなってしまうという問題が発生します。そのような問題に対処するために、IntSlider などのウィジェットは、作成する際にキーワード属性 continuous_update=False2を記述することで、ドラッグなどの操作が完了するまでの間に値を更新しないようにすることができます。

例えば、Mbtee_Anim の create_widgets の中で、アニメーションのフレームを変更する IntSlider のウィジェットを作成する処理を、下記のプログラムのように修正することで、IntSlider のドラッグ操作が完了するまで on_play_changed が呼び出されなくなります。

self.frame_slider = widgets.IntSlider(max=self.nodenum - 1, description="frame",
                                      continuous_update=False)

なお、そのようにしてしまうとドラッグをゆっくり行うことで、ドラッグ中のフレームの画像をリアルタイムに見ることができなくなってしまうという欠点があるので、本記事では上記のように修正は行いません。興味がある方は実際に上記のように修正し、IntSlider のドラッグ中に表示が変わらなくなることを確認してみて下さい。

prev_frame を更新する場所

先程説明したように、prev_frame 属性は アニメーションのフレームが変更された際に更新する必要 があります。ただし、そのことがわかっていても以下の理由から先程の間違った方法を最初に思いつく人が多いのではないかと思います。

  • プログラムの中でアニメーションのフレームを表す self.play.value を変更する場所が複数ある
  • その全ての場所で prev_frame 属性を更新する処理を記述するのは面倒
  • アニメーションのフレームが変更された場合は、必ず update_ab で表示を更新するので、そちらに prev_frame 属性を更新する処理を記述すれば良いのではなかと勘違いする

先程の表からわかるように、アニメーションのフレームを表す self.play.value の値が変更された場合 は必ず on_play_chaned が 1 回だけ呼び出される ので、その中で prev_frame 属性を更新する処理を記述 することができます。

また、以前の記事で説明したように on_play_changed の仮引数 に代入されたデータの "old" 属性に変更前の値が記録されている ので、それを利用して下記のプログラムのように修正を行うことができます。

  • 2 行目:IntSlider の変更前の値は changed.old に代入されているので、それを prev_frame 属性に代入するようにする
  • 4、7、11、14 行目の前にあったデバッグ用の print の表示はもう必要がないのですべて削除した
 1  def create_event_handler(self):
 2      def on_play_changed(changed):
 3          self.prev_frame = changed.old
 4          self.update_gui()
元と同じなので省略
 5      def on_prev_button_clicked(b=None):
 6          self.play.value -= 1
 7          self.update_gui()
 8          
 9      def on_next_button_clicked(b=None):
10          self.play.value += 1
11          self.update_gui()
元と同じなので省略
12      def change_frame(edge_status, diff, status_list):
元と同じなので省略
13          self.play.value = frame
14          self.update_gui()
元と同じなので省略 
15  
16  Mbtree_Anim.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
    def on_play_changed(changed):
        self.prev_frame = changed.old
        self.update_gui()
            
    def on_prev_button_clicked(b=None):
        self.play.value -= 1
        self.update_gui()
        
    def on_next_button_clicked(b=None):
        self.play.value += 1
        self.update_gui()

    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)

    self.play.observe(on_play_changed, names="value")
    
    def change_frame(edge_status, diff, status_list):
        frame = self.play.value
        selectednode = self.mbtree.nodelist_by_score[frame]
        selectedstatus = self.mbtree.ablist_by_score[frame][3]
        if selectedstatus == edge_status:
            return
        while True:
            frame += diff
            node = self.mbtree.nodelist_by_score[frame]
            status = self.mbtree.ablist_by_score[frame][3]
            if node == selectednode and status in status_list:
                break
        self.play.value = frame
        self.update_gui()
            
    def on_node_first_button_clicked(b=None):
        change_frame("start", -1, ["start"])
            
    def on_node_prev_button_clicked(b=None):
        change_frame("start", -1, ["start", "score"])

    def on_node_next_button_clicked(b=None):
        change_frame("end", 1, ["end", "score"])
        
    def on_node_last_button_clicked(b=None):
        change_frame("end", 1, ["end"])
    
    if self.abfig is not None:
        self.node_first_button.on_click(on_node_first_button_clicked)
        self.node_prev_button.on_click(on_node_prev_button_clicked)
        self.node_next_button.on_click(on_node_next_button_clicked)
        self.node_last_button.on_click(on_node_last_button_clicked)
    
Mbtree_Anim.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
    def on_play_changed(changed):
+       self.prev_frame = changed.old
-       print("on_play_changed")
        self.update_gui()
元と同じなので省略
    def on_prev_button_clicked(b=None):
        self.play.value -= 1
-       print("on_prev_button_clicked")        
        self.update_gui()
        
    def on_next_button_clicked(b=None):
        self.play.value += 1
-       print("on_next_button_clicked")        
        self.update_gui()
元と同じなので省略
    def change_frame(edge_status, diff, status_list):
元と同じなので省略
        self.play.value = frame
-       print("change_frame")
        self.update_gui()
元と同じなので省略 

Mbtree_Anim.create_event_handler = create_event_handler

次に下記のプログラムのように update_ab 内で prev_frame 属性の値を更新する処理を削除 する必要があります。

  • 5 行目の後にあった prev_frame 属性を更新する処理と、print によるデバッグ表示を削除する
 1  def update_ab(self):
元と同じなので省略
 2      for i in range(4):
 3          self.abax.text(15, 1 - i * 0.7, textlist[i])
 4          self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
 5          self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
 6  
 7  Mbtree_Anim.update_ab = update_ab
行番号のないプログラム
def update_ab(self):
    alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
    maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
    acolor = "red" if maxnode else "black"
    bcolor = "black" if maxnode else "red"
                            
    self.abax.clear()
    self.abax.set_xlim(-4, 23)
    self.abax.set_ylim(-1.5, 1.5)
    self.abax.axis("off")

    minus_inf = -3
    plus_inf = 4   
    alphacoord = max(minus_inf, alpha)
    betacoord = min(plus_inf, beta)
    
    color = "lightgray" if maxnode else "aqua"
    rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
                            height=1, fc=color)
    self.abax.add_patch(rect)
    rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
                            height=1, fc="yellow")
    self.abax.add_patch(rect)
    color = "aqua" if maxnode else "lightgray"
    rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
                            height=1, fc=color)
    self.abax.add_patch(rect)

    self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
    for num in range(minus_inf, plus_inf + 1):
        if num == minus_inf:
            numtext = "-∞"
        elif num == plus_inf:
            numtext = ""
        else:
            numtext = num
        self.abax.text(num, -1, numtext, ha="center")
        
    arrowprops = { "arrowstyle": "->"}
    self.abax.plot(alphacoord, 0, "or")
    self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=acolor)
    self.abax.plot(betacoord, 0, "ob")
    self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=bcolor)
    if score is not None:
        self.abax.plot(score, 0, "og")
        self.abax.annotate(f"score = {score}", xy=(score, 0),
                        xytext=((minus_inf + plus_inf) / 2, 1), 
                        arrowprops=arrowprops, ha="center")
            
    facecolorlist = ["aqua", "yellow", "lightgray"]
    textcolorlist = ["black", "black", "black"]
    if maxnode:
        nodetype = f"深さ {self.selectednode.mb.move_count} max node"
        textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
        if score is not None :
            if beta <= score:
                textcolorlist[0] = "red"
            elif alpha < score:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
    else:
        nodetype = f"深さ {self.selectednode.mb.move_count} min node"
        textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
        if score is not None :
            if score <= alpha:
                textcolorlist[0] = "red"
            elif score < beta:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
            
    if status == "start":
        facecolor = "white"
        nodetype += " 処理の開始"
    elif status == "score":
        facecolor = "lightyellow"
        nodetype += " 子ノードの評価値"
    elif status == "update":
        facecolor = "lightcyan"
        if maxnode:
            nodetype += " α 値の処理"
        else:
            nodetype += " β 値の処理"
    else:
        facecolor = "lavenderblush"
        nodetype += " 評価値の確定"
    self.abfig.set_facecolor(facecolor)
    self.abax.text(6, 1, nodetype)   
    for i in range(3):
        rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
        self.abax.add_patch(rect)
        self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])    
    
    num_total = num_calculated + num_pruned
    num_ratio = num_calculated / num_total if num_total != 0 else 0
    _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
    prev_num_total = prev_num_calculated + prev_num_pruned
    diff_num_calculated = num_calculated - prev_num_calculated
    diff_num_pruned = num_pruned - prev_num_pruned
    diff_num_total = num_total - prev_num_total
    diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0

    textlist = [ "計算済", "枝狩り", "合計", "割合" ]
    datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
    diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}", 
                      f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")

Mbtree_Anim.update_ab = update_ab
修正箇所
def update_ab(self):
元と同じなので省略
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
-   self.prev_frame = self.play.value
-   print("update_ab")        

Mbtree_Anim.update_ab = update_ab

上記の修正後に下記のプログラムを実行し、選択中のノード内の移動の右の > ボタンをクリックすると実行結果のような図が表示されます。0 フレーム目からの差分として、その左の数値と同じ値が正しく表示されることが確認できます。

Mbtree_Anim(mbtree, isscore=True)

実行結果

また、今回の記事の最初で行ったように、4485 フレーム目から 4486 フレーム目に移動すると、下図のように右にフレーム間で行われた枝狩り数の 差分である 14694 - 9179 = 5515 が右の列に正しく表示 されるようになったことが確認できます。

今回の記事のまとめ

今回の記事では、Mbtre_Anim のバグの修正と、フレームを移動した際の A、P、M の 差分のデータを表示するという改良 を行いました。

本記事で入力したプログラム

リンク 説明
marubatsu.ipynb 本記事で入力して実行した JupyterLab のファイル
tree.py 本記事で更新した tree_new.py

次回の記事

  1. 筆者も ipywidges の全ての機能を理解しているとは到底言えないと思います

  2. ウィジェットの操作の途中で継続的(continuos)に値を更新する(update)かどうかを指定するのでこのような名前になっています

0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?