2020年10月19日月曜日

wxPython 4.1のwx.ListCtrlでCheckBox

wxPythonで行の頭にCheckBoxのついたリストコントールを使いたいとき、 wx.ListCtrlにCheckListCtrlMixinを使っていた。 wxPythonの4.1で、以前のスクリプトを動かしたところこれが動かなかった。

どうやら、CheckListCtrlMixinで定義されていたCheckItem()メソッドが、 wx.ListCtrlのbuilt-ins functionになったために、 そちらが呼び出されて動作しない様子である。 新しい、wx.ListCtrlには、CheckListCtrlMixinを利用しなくても、 CheckBox付きのwx.ListCtrlが利用できるようになったらしい。

wxPythonのページに、EnableCheckBoxes(enable=True)のメソッドが説明されていた。 これを使えばよいが、これまで使いたいスクリプトをとりあえす 動かしたいということであれば、 CheckItem()をMixinのものに上書きすると動作するようになった。

def MyCheckItem(self, index, check=True):
    img_idx = self.GetItem(index).GetImage()
    if img_idx == 0 and check:
        self.SetItemImage(index, 1)
        self.OnCheckItem(index, True)
    elif img_idx == 1 and not check:
        self.SetItemImage(index, 0)
        self.OnCheckItem(index, False)

wx.ListCtrl.CheckItem = MyCheckItem
しかしながら、そのうち直さなくてはならないということで、 EnableCheckBoxes()を使ったものを書いてみた。
import wx
import wx.lib.mixins.listctrl

class CheckListCtrl(wx.ListCtrl):
    def __init__(self, panel, style):
        wx.ListCtrl.__init__(self, panel, -1, 
                style=wx.LC_REPORT,size=wx.Size(400, 300))
        self.EnableCheckBoxes(enable=True)
        self.Bind(wx.EVT_LIST_ITEM_CHECKED, self._OnCheckItem)
        self.Bind(wx.EVT_LIST_ITEM_UNCHECKED, self._OnUnCheckItem)

    def _OnCheckItem(self, evt):
        self.OnCheckItem(evt.Index, True)

    def _OnUnCheckItem(self, evt):
        self.OnCheckItem(evt.Index, False)
        
    def OnCheckItem(self, index, flag):
        print(index, flag)

class AppFrame(wx.Frame):
    def __init__(self, *args, **kwargs):

        wx.Frame.__init__(self, None ,wx.ID_ANY, 'CheckListCtrl', size=(400,300))

        self.list = CheckListCtrl(self, style=wx.LC_REPORT)

        titles = ("check", "title1", "title2")

        for index, title in enumerate(titles):
            self.list.InsertColumn(index, title, wx.LIST_FORMAT_LEFT)

        for index, title in enumerate(titles):
            self.list.Append(["name%d" % index, "value", "value" ]) 

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.sizer.Add(self.list, 1, wx.EXPAND, border=0)
        self.SetSizer(self.sizer)
        self.Show()

if __name__ == "__main__":
    app = wx.App()
    win = AppFrame()
    app.MainLoop()
wx.EVT_LIST_ITEM_CHECKEDとwx.EVT_LIST_ITEM_UNCHECKEDをハンドルして、 従来のMixInを利用した場合と同じ引数で、 OnCheckItem()メソッドを呼び出すようにしてみた。

2020年8月22日土曜日

NimでZipファイルにしたテキストを読む

nimというプログラミング言語を試してみた。 pythonのように書けるけど、静的な型付けのコンパイラなので、 実行速度のパフォーマンスがよいということである。

大きなカンマ区切りの値(CSV)やタブ区切りの値(TSV)といった テキストデータをzipで圧縮するとだいぶ小さくなるので、 そのようにして保存しておくことも多い。 そのようなテキストファイルを読み込みたいときに、 zipファイルを扱うモジュールが利用できると便利である。

そこで、datafile.txtというテキストファイルをzip圧縮した、 datafile.zipを読み込んで、各行をプリントするだけというスクリプトを書いてみた。

Python(v3.83 windows 64bit)では、以下のようなスクリプトになる。

import io
import zipfile

def readzip():
    filename = "datafile.zip"
    try:
        z = zipfile.ZipFile(filename, 'r')
    except:
        print("Opening zip failed")
        exit(-1)
    try:
        for fname in z.namelist():
            print(fname)
            fp = io.TextIOWrapper(z.open(fname, 'r'))
            line = fp.readline().strip()
            while line:
                print(line)
                line = fp.readline().strip()
            fp.close()
    finally:
        z.close()

if __name__ == '__main__':
    readzip()       
nimでは、zipパッケージにあるzipfilesモジュールを利用する。
import streams
import zip\zipfiles

proc readzip() =
    var filename = "datafile.zip"
    var z: ZipArchive
    var line = ""
    if not z.open(filename):
        echo "Opening zip failed"
        quit(1)
    try:
        for f in walkFiles(z):
            echo f
            var fs = z.getStream(f)
            while fs.readLine(line):
                echo line
            fs.close()
    finally:
        z.close()

when isMainModule:
    readzip()
コンパイルは、readzip.nimというファイルに書いたとすると、
nim c readzip.nim
でできるが、その前にzipパッケージをインストールする必要がある。 Gitがインストールされていれば、
nimble install zip
でインストールできる。 さらに、Windowsでコンパイルすると、gccでコンパイルされるときに エラーで止まってしまった。 こちらにあるように、修正するとコンパイルできた。

ついでなので、Julia(v1.5 windows 64-bit)でも書いてみた。 juliaでは、ZipFile.jlが利用できる。

using Pkg
Pkg.add("ZipFile")
としてインストールできた。
using ZipFile

function readzip()
    filename = "datafile.zip"
    local z

    try
        z = ZipFile.Reader(filename)
    catch
        println("Opening zip failed")
        exit(-1)
    end

    try
        for f in z.files
            println(f.name)

            while !eof(f)
                line = readline(f)
                println(line)
            end
        end
    finally
        close(z)
    end
end

readzip()       
Juliaでは、tryブロックもスコープを作るようで、tryブロック内で作った変数zを、 そのtryブロックから出た後でも利用するために、local z宣言をしている。

どれも似たような感じに書けたが、Juliaは、関数が充実しているからかシンプルに感じる。 whileのループは、eachline(f)を用いたforループで書くと、すっきりする。

2020年8月10日月曜日

タイムスタンプを得る(Rust)

Rustをインストールしたので、練習してみる。 プログラムの出力結果に、実行日時を付加するときにタイムスタンプがほしいことがある。 ここでのタイムスタンプは、"2020/08/10 09:00:35"といった形に現在日時を示す文字列である。 それを得る関数timestamp_now()を書いてみようと思った。

Pythonでは、

import datetime
dt = datetime.datetime.now()
timestamp_now = "%04d/%02d/%02d %02d:%02d:%02d" % (
    dt.year, dt.month, dt.day, 
    dt.hour, dt.minute, dt.second)
と簡単に得られる。

Rustでは、chronoという外部クレートを用いることで簡単に得られるようである。 ここでは、chronoを使わずに、std::timeを用いてタイムスタンプを得る。 std::time::SystemTime::now()を用いて、現在時刻のUNIX_EPOCHタイムからの秒数を得て、 Fairfieldの公式を使って日数を勘定すればタイムスタンプが得られるだろうということである。

現在時刻とUNIX_EPOCHタイムとの差を得る関数は以下の通りでよいだろう。

fn now_from_unix_epoch() -> f64 {
    let now = std::time::SystemTime::now();
    now.duration_since(std::time::SystemTime::UNIX_EPOCH).unwrap().as_secs_f64()
}
次に、その秒数が何日に相当するかを評価する関数である。 Rustは、Pythonのように、気軽に複数の値を返せるようであるので、 のこりの秒数から時、分、秒を返すことにしてある。 tzは、タイムゾーンによる時間のズレを補正するために入れてある。 日本時間はUTCより9時間進んでいるので、9を入れて計算すると日本時間となる。
fn counts_day_from_sec(seconds:f64, tz:f64) -> (u32, u32, u32, f64) {
    const DAY_SEC:f64 = 60.0*60.0*24.0;
    const HOUR_SEC:f64 = 60.0*60.0;
    const MIN_SEC: f64 = 60.0;
    let days = ((seconds + tz * HOUR_SEC)/DAY_SEC).floor();
    let remain_sec = (seconds + tz * HOUR_SEC) - (days*DAY_SEC) as f64;
    let hour = (remain_sec/HOUR_SEC).floor();
    let remain_sec = remain_sec - (hour*HOUR_SEC) as f64;
    let min = (remain_sec/MIN_SEC).floor();
    let sec = remain_sec - (min*MIN_SEC) as f64;
    (days as u32, hour as u32, min as u32, sec) 
}    
得られた日数から日付を得るために、日付から日数を得るFairfiledの公式を使った関数を作る。 1月、2月の時は、年を1年戻して、それぞれ13月、14月として計算する必要がある。
fn daycounts_fairfield(year:u32, month:u32, day:u32) -> u32 {
    let mut y = year;
    let mut m = month;
    if month < 3 {
        y = year -1;
        m = month +12;
    }
    let day_before_the_year = 365 * (y-1);
    let day_of_leep_year = y/4-y/100+y/400;
    let day_in_the_year = 31 +28 +306*(m+1)/10-122 +day;
    (day_before_the_year + day_of_leep_year + day_in_the_year) as u32
}    
次に、この関数を用いて日付を探せばよい。年、月、日を返す。
fn days_to_date(days:u32) -> (u32, u32, u32) {
    let unix_epoch = daycounts_fairfield(1970, 1, 1);
    let mut year = 1970;
    while daycounts_fairfield(year, 1, 1) - unix_epoch < days {
        year = year + 1;
    }
    year = year -1;
    let mut month = 1;
    while daycounts_fairfield(year, month, 1) - unix_epoch < days {
        month = month + 1;
    }
    month = month -1;
    let diff_days = daycounts_fairfield(year, month, 1)-unix_epoch;
    
    (year, month, days-diff_days+1)
}
以上の関数を用いて、タイムスタンプを生成する関数は、こうなる。
fn timestamp_now() -> String {
    let unixtime = now_from_unix_epoch();

    let (days, hour, minute, second) = counts_day_from_sec(unixtime, 9.0);
    let (year, month, day) = days_to_date(days);
    format!("{:04}/{:02}/{:02} {:02}:{:02}:{:02}", 
            year, month, day, hour, minute, second.floor() as u32)
}      
最後は、メイン関数。
fn main() {
    let s = timestamp_now();
    println!("{}", s);
}
以上をtimestamp.rsというファイルに記述したら、
rustc timestamp.rs
でコンパイルでき、timestamp.exeが生成される。

Rustは、f64とu32の除算は定義されていないなど、 どの型で計算するかをしっかり考えておかないといけない。 ただ、コンパイラのエラーメッセージが非常に親切で、メッセージを読むのが楽しい。 また、定数は大文字にしなさいや、変数名をスネークケースにしなさいなど記述についても指示が出た。

2020年8月1日土曜日

Windows10 Rust(mingw)をインストール

Windows10にRustをインストールしてみた。 MSYS2を用いてインストールしたgcc環境があるので、 MSVC版ではなく,こちらを利用したい。

今は、rustupを利用してインストール管理をするようである。 Windows では、rustup-init.exeをダウンロードしてインストールするようだ。 Rustの公式ページのInstallからダウンロードできる。 64ビット環境なので、64-BITのrustup-init.exeをダウンロードし、実行した。

1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
と尋ねられるので、(2)を選ぶ。 もしVisual Studioを使っているなら、defaultでよいのだろう。

Default host triple?と尋ねられるので、こちらで x86_64_pc-windows-gnu
を入力した。結局、

  
default host triple: x86_64-pc-windows-gnu
default toolchain: stable
profile: default
modify PATH variable: yes
として、再び最初のメニューが現れるので、
(1) Proceed with installation (default)
を選ぶと、無事にインストールされた。
ユーザーのホームフォルダの.cargoの中にインストールされた。

とりあえずfizzbuzzを書いて試してみる。

fn fizzbuzz(i:u32) -> String {
    if i%15==0 {
        "FizzBuzz".to_string()
    } else if i%3==0 {
        "Fizz".to_string()
    } else if i%5==0 {
        "Buzz".to_string()
    } else {
        i.to_string()
    }
}

fn main() {
    for i in 1..101 {
        println!("{}", fizzbuzz(i));
    }
}          
fizzbuzz.rsというファイルに保存したら、
rustc fizzbuzz.rs
でコンパイルリンクされfizzbuzz.exeが生成された。

2020年7月23日木曜日

Ptyhon3で以前のスクリプトを動かす(epsClip)

ドラッグアンドドロップを使った、 こちらで作成したepsファイルの バウンディングボックスを調整するスクリプトをPython3.8で動かしてみた。 新しい環境にGhostScript9.52 をインストールしたが、GhostView は使えなかったのでこちらのスクリプトを 利用する必要があるからである。 Python3.8になったためというより、wxPython や PIL が変更になったためにそのままでは動かなくなったところがある。 環境はWindows10 64bit、python3.8.3、wxPython4.1.0、PIL(pillow)7.1.2である。

まずは、
import Image
でエラーがでる。
これは、このインポートの仕方は廃止されたためで、以前に調べていた。
from PIL import Image
に修正する。

wxPythonでは、wx.EmptyBitmap、wx.EmptyImage、wx.BitmapFromImageが廃止されたようだ。 wx.EmptyBitmap()およびBitmapFromImage()は、どちらもBitmap()に書き換えればよかった。 wx.EmptyImage()は、wx.Image()で書き換える。

wx.Imageの持っていたtostring()メソッドは廃止されたので、tobytes()メソッドに書き換える。

FileDropTargetのOnDropFiles()ハンドラがbool値を返す必要があるようになったので、return True を加える。

文字列のコード変換などで、decode()を用いていたが、そのような必要はなくなった。 文字列は、utf-8となったのでバイト列にするencode()しかない。 また、バイト列には文字列にするためのdecode()メソッドがある。

もともとGUIのみのスクリプトでprintしていないので、print()関数にする修正は不要であった。 これらの修正をすると、問題なくpython3.8で動作した。

2020年7月22日水曜日

Python3.8でIDLEを使ってみた

Python3.8を使いだして、インストールされたIDLEを使ってみた。 Python2.7のときも、ときどき利用していたPythonコードをちょっと書いてみるのに便利な環境である。 JupyterやSpiderよりも手軽に軽快に利用できるので、使い始めるのはよいかと思う。

驚いたのはPython3.8でついてくるIDLEでは行番号の表示ができるようになっていたことである。 メニューからnew fileを選んでエディタを開くと、メニューのOptionsにShow Line Numbersがあり、 それを選ぶと行番号が表示される。 行番号表示をデフォルトにしたい場合は、 メニューのOption/Configure IDLEを選んで、GeneralタブにあるShow line numbers in new windows のチェックを入れればよい。

複数行をまとめてインデントをずらしたいときは、 行を選んでCtrl+[キー(インデントを減らす)とCtrl+]キー(インデントを増やす)でできる。

検索は、Ctrl+Fキーで検索用の窓が開くが、単語を選択したあとにCtrl+F3キーを押すとその単語を探してくれる。 でも検索を頻繁にするなら、慣れかもしれないがVimがやりやすい。

範囲をコメントにするにはAlt+3キー、コメントを解除するにはAlt+4キーである。

シェルのほうでも利用できるが、 TABキーを押すと登録されている単語から候補が表示され、Alt+/キーを押すとテキスト内の単語から候補が表示される。 また、Ctrl+0キーで対応するカッコを確認できる。

シェルでは、Alt+pキーとAlt+nキーで履歴を利用できる。画面でカーソルを移動させてEnterキーを押すとそこにあるテキストが入力行へと転写される。ブロックを用いたコードを実行したときなどに、ブロックごと転写されるため、再編集しやすい。ちょっと動作を確認したいときなどに便利である。タートルグラフィクスで遊ぶのにも便利だろう。

この程度操作ができればちょっとしたスクリプトを書くのに便利である。 なんといってもPythonをインストールするとすぐに利用できるのがよい。 Vimがインストールされていない環境でならIDLEをエディタとして使うのもよさそうである。

python3 wxPythonでドラッグアンドドロップ

これまでPython2.7でwxPythonを用いて、ファイルをドラッグアンドドロップで開くスクリプトを 書いて用いていた。Python3.8を使い始めて、改めてドラッグアンドドロップの部分を抜き出して、 動作を確認してみた。 環境は、Windows10 64bit, Python3.8.3, wxPython4.1.0。

tkinterにくらべて、ウィンドウ作成の部分に手間をかけているが、やっていることは以下の通り。

まず、wx.FileDropTargetを継承したクラスを作成して、 ファイルがドロップされた時に実行されるOnDropFiles()を 自分の望むように書き換える。

さらに、wx.PanelのSetDropTarget()メソッドを用いて、 作成したFileDropTargetを継承したクラスのインスタンスを設定するだけである。 実行すると、ウィンドウが開き、その中に配置されたパネルにファイルをドロップすると、 そのパスがコンソールに表示される。

import wx

class MyFileDropTarget(wx.FileDropTarget):
    def __init__(self):
        super(MyFileDropTarget, self).__init__()

    def OnDropFiles(self, x, y, filenames):
        for n in filenames:
            print(x, y, n)
        return True

class DropPanel(wx.Panel):
    def __init__(self, parent, *args, **kwargs):
        super(DropPanel, self).__init__(parent, *args, **kwargs)
        self.droptarget = MyFileDropTarget()
        self.SetDropTarget(self.droptarget)
        
class MyFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(MyFrame, self).__init__(*args, **kwargs)
        
        self.panel = DropPanel(self, size=(300, 300))       
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel,  1, wx.EXPAND)
        self.SetSizer(sizer)
        self.Fit()

class MyApp(wx.App):
    def __init__(self):
        wx.App.__init__(self, False)

    def OnInit(self):
        self.frame = MyFrame(None, title=AppName)
        #self.SetTopWindow(self.frame)
        self.frame.Show()
        return True

if __name__ == '__main__':
    AppName = "DnD Test"
    app = MyApp()
    app.MainLoop()  

2020年7月21日火曜日

Python3 tkinterでドラッグアンドドロップ

Pythonのtkinterでファイルをドロップしてそれを開くようなGUIをもったスクリプトを作るのは どうするのか調べてみた。 一つは、ctypes モジュールを用いて、WindowsのAPIを呼び出す方法がある。 もう一つは、TkDNDというTkの拡張ライブラリとそのPythonラッパーであるTkinterDnD2を用いる方法である。 ここでは、後者の方法を試してみた。 環境はWindows10、Python3.8.3、64bit環境である。

まずはこちらにいく。 TkinterDnD2のダウンロードができる。また、TkDND へのダウンロードリンクもある。 行うことは、Tkの拡張ライブラリであるTkDND2.8と、そのPythonラッパーであるTkinterDnD2をインストールすることである。

TkDNDのページへ行く。 64bit環境なら、 Windows Binaries/TkDND2.8の中にある tkdnd2.8-win32-x86_64.tar.gz をダウンロードする。

Python環境がインストールされているフォルダの中にtklというフォルダがある。 それぞれの環境ごとに場所が変わるので、 sys モジュールをインポートして、どこにpythonがインストールされているかを 調べてみるとよいだろう。 そのtklフォルダ中に、tkdnd2.8フォルダをそのまま配置する。

次に、こちらから TkinterDnD2-0.3.zipファイルをダウンロードする。 展開すると、TkinterDnD2フォルダといくつかのdemoファイルおよび 説明のhtmlファイルが得られる。 Pythonがインストールされているフォルダにある、 Lib\site-packagesの中にTkinterDnD2フォルダをそのまま配置すると完了である。

python からdemoを実行してみると動作を確認できる。 簡単なスクリプトは以下のものとなる。 エクスプローラからファイルを投げ込むと、そのファイルパスが表示される。 複数ファイルを投げ入れると、各ファイルパスがスペースで区切られたものとなる。

import sys
import os
import TkinterDnD2 as tkdnd
if sys.version > "3.0":
    import tkinter as tk
else:
    import Tkinter as tk

def drop_enter(event):
    event.widget.focus_force()
    return event.action

def drop_leave(event):
    event.widget._root().focus_force()
    return event.action

def drop_position(event):
    #print(event.x_root, event.y_root)
    return event.action

def drop(event):
    if event.data:
        print(event.data)
    return event.action

app = tkdnd.TkinterDnD.Tk()

app.drop_target_register(tkdnd.DND_FILES)
app.dnd_bind('<<DropEnter>>', drop_enter)
app.dnd_bind('<<DropLeave>>', drop_leave)
app.dnd_bind('<<DropPosition>>', drop_position)
app.dnd_bind('<<Drop>>', drop)

app.mainloop()

2020年7月18日土曜日

MacBook late 2008 にlubuntu20.04をインストールしてみた

MacBook5.1(2008 Late OSX10.5.8)が9年ぶりくらいに電源が入った。 いろいろ試して、最終的にDVDをくわえ込んだまま眠っていたのだが、 9年ぶりに電源が入りDVDを排出した。 OSは、10.5.8 Leopardだったのだが、ブラウザを使ってみるとセキュリティの関係か、 古いブラウザでは開けないページばかりである。 ブラウザを新しいバージョンにしようとしても、10.5.8をサポートしているものは見つからない。 この古いMacBookなら、10.6のSnow Leopardにアップグレードして、 OS X El Capitan までアップグレードしていく人が多いようである。 しかしながら、わざわざアップグレードしていっても、最新ではない。 古いPCなので、軽量のLinuxディストリビューションを入れて使うのがよさそうと考えた。

軽量のLinuxディストリビューションは、いくつもあって迷うところであるが、 多くのドライバが用意されていそうなUbuntuで軽量のディストリビューションということで、 Lubuntuにした。最新は、20.04であったので、そちらのISOをダウンロードし、 Etcherという起動USBを作成するソフトウェアで、Lubuntuインストール用のUSBキーを作成した。 MacOSとのデュアルブートやrEFindについてなど、検索するといろいろでてくるが、とりあえずUSBキーで起動してみた。 もうMacOSを利用するつもりもないので、シングルブートでインストールしてみる。 USBキーで起動して、普通にインストールするだけである。 パーティションは、MacOSのパーティションを削除し、デフォルト設定でインストールした。 インストーラーはわかりやすいので、迷うところはない。

インストール直後は、wifiのドライバがインストールされていないが、 このMacBookはEtherポートがあり、それが利用できたため、ネットに接続できた。 そのあとで、lubuntuのメニューから(設定>Additional Drivers)を開くと、 ドライバが探されてwifiとnVidiaのドライバをインストールすることができた。

日本語の入力は、
sudo apt install fcitx-mozc
sudo apt install font-noto font-noto-cjk-extra font-noto-color-emoji
その後、(設定>入力メソッド)でfcitxを有効にした。
日本語を入力するときは、Ctrl+Spaceを押すとMozcに切り替わった。

タッチパッドのタップでクリックしたいので、
/etc/X11/xorg.conf.d/30-touchpad.conf
を以下の内容で作成した。

Section "InputClass"
  Identifier "touchpad"
  Driver "libinput"
  MatchIsTouchpad "on"
  Option "Tapping" "on"
EndSection
指一本タップで左クリック、 二本タップで右クリック、 二本指でスワイプするとスクロールするようになった。 スワイプでのスクロールの向きは、 (設定>LXQt settings>キーボードとマウス>Mouse and Touchpad) でNatural scrollingをトグルすると変更できた。

ウィンドウタイトルなどに使われる文字のフォントサイズは、 (設定>LXQt settings>Openbox設定マネージャー) で行った。 デスクトップアイコンのラベルの大きさは、 (設定>LXQt settings>デスクトップ) で、ラベルテキストのフォントサイズで設定した。

Lubuntu 20.04では、python3.8.2がインストールされていた。 pipやtkinterは、aptを用いてインストールした。
sudo apt install python3-pip
sudo apt install python3-tk

ついでに、pythonで起動するように.bashrcに、
alias python='python3'
alias pip='pip3'
を追記した。

OSもブラウザも最新のものが利用できるようになり、 速度的にも軽快に利用できている。 サスペンドおよびサスペンドからの復帰はできた。 しかし、ハイバネーションは機能していない。 古いPCは、軽量LinuxのLiveUSBで起動してみて、 使えそうならLinuxで利用するのもよさそうである。

2020年6月29日月曜日

matplotlibでアニメーション(ライフゲーム)

ライフゲームを表示するスクリプトをmatplotlibのアニメーションで書いたものがこちら。 前のものは、FuncAnimation()へ引き渡す関数をlambda式で作成することで、 フレーム番号以外のパラメータを関数に持たせて受け渡す方法を用いましたが、 Pythonなら、クラスを用いるのもよいです。 クラスにすると、なにかパラメータを保持させていることが明確です。
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation

def Rule(alive, neighbours):
    n = sum(neighbours)
    if not alive and n==3:
        return 1 
    if alive and n==2 or n==3:
        return 1 
    if alive and n<=1:
        return 0 
    if alive and n>=4:
        return 0 
    else:
        return alive

def LifeGameUpdate(world, rule):
    w, h = world.shape
    updated_world = np.zeros((w, h))
    for i in range(h):
        for j in range(w):
            im, ip = (i-1) % h, (i+1) % h
            jm, jp = (j-1) % w, (j+1) % w
            neighbours = (
                    world[im, jm], world[im, j], world[im, jp], 
                    world[i, jm], world[i, jp],
                    world[ip, jm], world[ip, j], world[ip, jp])
            updated_world[i, j] = rule(world[i, j], neighbours)
    return updated_world

class LifeGame(object):
    def __init__(self, world, rule):
        self.world = world
        self.rule = rule

    def __call__(self, frame_count):
        plt.cla()
        plt.imshow(self.world)
        self.world = LifeGameUpdate(self.world, self.rule)

if __name__ == '__main__':
    fig = plt.figure()
    world = np.round(np.random.random((100, 100)))
    lifegame = LifeGame(world, Rule)
    anim = matplotlib.animation.FuncAnimation(fig, lifegame, interval=10)
    plt.show()
            

2020年6月28日日曜日

matplotlibでアニメーション

ある画像データをパラメータの変化に合わせてアニメーションをさせながら表示させたい。 そのような場合のメモをしておく。

pcolormesh()を用いて画像データをプロットし、 その下にplot()を用いたパラメータの変化を示す。 さらに、pcolormesh()で表示されている場所をパラメータの変化のグラフ上に線で示す。 pcolormeth()に対するカラーバーの位置も制御したい。 レイアウトの自由度を持たせるために、add_axes()メソッドを用いている。

アニメーションは、

matplotlib.animation.ArtistAnimation
matplotlib.animation.FuncAnimation
のどちらかを用いて行える。

ArtistAnimationでは、各フレームのArtistオブジェクトを保存して、 Animationを作成する。 フレーム数が多くなる場合も考えると、FuncAnimationを利用したい。 ArtistAnimation, FuncAnimationとも、blitキーワードをTrueに設定すると、 指定した場所のみ書き換えるようになるため速く描画できると思われるが、 グラフ(Axes)の外は書き換えが行われなかったりと制約もできてしまう。 pcolormesh()の書き換えでは、軸の枠線がプロットに上書きされて消えてしまうようなことも起こる。 この場合は、blit=Falseを指定することをお勧めする。

MakeData()で、(nx, ny, nz)のshapeをもつデータを作成し、 各zに対する幅nx、高さnyのデータとみて、プロットをしてみる。

FuncAnimation()には、各フレームごとに呼び出される関数を渡す必要がある。 その関数には、フレーム番号を受け取る一つの引数が必要となる。 実際のupdate()関数では、関数内部で用いるfigure、 data、az とフレーム番号を受け取るように書きたい。 そうすると、引数が多くなってしまうので、lambdaを用いて、引数をフレーム番号のみに減らし、 各フレームごとに呼び出される関数としている。

最後のほうにある、

anim.save('anim.gif', writer='pillow')

のコメントを外せば、アニメーションGIFファイルとして保存できる。 writer='pillow'を指定するためにpillow(PIL)をインストールしておく必要がある。

モジュールのバージョン
python : 3.8.3
matplotlib : 3.2.2
numpy : 1.19.0
PIL(pillow): 7.1.2


import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np

def MakeData(nx, ny, nz):
    data = np.ones((nx, ny, nz))
    ax = np.linspace(-3, 3, nx)
    ay = np.linspace(-3, 3, ny)
    az = np.linspace(0, np.pi, nz)
    for k, z in enumerate(az):
        for j, y in enumerate(ay): 
            for i, x in enumerate(ax):
                data[i, j, k] = np.sin(x+z) * np.cos(y+z)
    return data, az

def update(fig, data, az, iz):
    fig.clear()
    # create axes
    ax0 = fig.add_axes([0.1, 0.4, 0.7, 0.5])
    cbar_ax0 = fig.add_axes([0.82, 0.4, 0.05, 0.5])
    ax1 = fig.add_axes([0.1, 0.1, 0.7, 0.2])
    
    # plot data
    im0 = ax0.pcolormesh(data[:, :, iz].T)
    fig.colorbar(im0, cax=cbar_ax0)
    ax1.plot(az, '-', color='#0000FF')
    ax1.plot([iz, iz], [0, np.pi], '-', color='#FF0000')

    # put text labels
    label = ax1.text(0.1, 0.8, '%.5g (rad)' % az[iz], 
            transform=ax1.transAxes)
    ax0.set_title('Frame %d' % iz)


nx, ny, nz = 30, 60, 100
data, az = MakeData(nx, ny, nz)

fig = plt.figure(figsize=(5, 6))

anim = FuncAnimation(fig, lambda iz: update(fig, data, az, iz), nz, 
        interval=100, blit=False, repeat=False)

#anim.save('anim.gif', writer='pillow')

plt.show()