PythonからCで書かれたFFTW3を使いたいアホの子

誰がアホやねん!
まあ、それはおいといて……
今回はCの高速FFTライブラリ、FFTW3のラッパーを自分で作ってみたいと思ったりしました。
もうすでにあるじゃん!ってツッコミは無効化です。だから、アホの子だって言ったんです。
じゃ、メモ始めます。
Windowsは今回、はごってます。我が校にいじめはありません。
※今回、コメントは英語になってます。日本語コメントはめんどいです。
※コメント英語の用法ミスは、ご自由に脳内変換してください。

動作環境:

  • Ubuntu10.04(64bit)、FreeBSD 8.1(64bit)
  • FFTW3がCから使えてれば良い

使用イメージ:

まず、どんな風に使うラッパーかと言うと、

#!/usr/bin/env python
#coding: utf-8
import fftw3py
import math

pi=math.atan(1)*4

def make_test_data(num):
    sig = []
    for i in range(num):
        sig.insert(len(sig),0.25*math.sin(pi*i/16))
    return sig

signal=make_test_data(64)

fftw3py.init(len(signal))
fftw3py.inject(signal,"with-hanning")
freq=fftw3py.exe()
fftw3py.destory()

print signal
print freq

こんなイメージ。大雑把にフローを書くと、

  1. make_test_data()で入力信号作る。(別に外部からでもいいけど割愛)
  2. fftw3py(ラッパーモジュール名).init()で初期化。
  3. fftw3py.inject()でCにおけるデータ型に変換。(with-hanningオプションつけるとハニング窓を適用)
  4. fftw3py.exe()で実行。fftw3py.destory()でメモリクリア。(実際の計算はCで行っているので、メモリクリアは必須。)

ちなみに、destoryはdestroyの最上級……ではなく、ただのスペルミスです。
細かいことは気にせずに、つまり、こういう使い方ができるラッパーを書きます。

ラッパー本体:

さて、ラッパー本体の実装には2つのファイルを作ります。今回、モジュール名をfftw3pyとしたので、fftw3py.cとfftw3py.hを作ることになります。

  • Pythonではimportすると、モジュール名のpyファイルをパスから探します。つまり、ファイル名はモジュール名と同一である必要があることに注意してください。なお、素人の言うことなので、違った場合は嘲笑でもしといてください。

ではでは、それらのファイルの中身はというと、
fftw3py.h

extern PyObject* FFTW3PY_init(PyObject* self, PyObject* args);
extern PyObject* FFTW3PY_inject(PyObject* self, PyObject* args);
extern PyObject* FFTW3PY_exe(PyObject* self, PyObject* args);
extern PyObject* FFTW3PY_destory(PyObject* self, PyObject* args);

extern void initfftw3py();

fftw3py.c

#include <math.h>
#include "Python.h"
#include "fftw3.h"
#include "fftw3py.h"

/***** global variables *****/
static double *in;
static fftw_complex *out;
static fftw_plan plan;
static int sample_points;

/***** Initialization function *****/
PyObject* FFTW3PY_init(PyObject* self, PyObject* args){
    /***** translate python variable to c variable *****/
    if(!PyArg_ParseTuple(args, "i", &sample_points))
        return NULL; // return NULL mean Error
    in = (double *)malloc(sizeof(double)*sample_points);
    out = (fftw_complex*)fftw_malloc(sizeof(fftw_complex)*sample_points);
    plan = fftw_plan_dft_r2c_1d(sample_points, in, out, FFTW_ESTIMATE);

    // If you don't have any return value, you must return null character.
    return Py_BuildValue("");
}

/***** set FFT variables *****/
PyObject* FFTW3PY_inject(PyObject* self, PyObject* args){
    int i;
    double h;
    PyObject* plist;
    const char* string = NULL;

    // Parsing args.
    if(!PyArg_ParseTuple(args,"Os",&plist,&string))
        return NULL;

    // set input signal's array from python's list object.
    for(i=0;i<sample_points;i++){
        if(!strcmp(string,"with-hanning"))
            h = (1 - cos(M_PI*i/sample_points))/2;
        else
            h=1;
        in[i] = h*PyFloat_AsDouble(PyList_GetItem(plist,i));
    }

    return Py_BuildValue("");
}

PyObject* FFTW3PY_exe(PyObject* self, PyObject* args){
    int i;
    PyObject* result = PyList_New(sample_points);

    fftw_execute(plan);
    for(i=0;i<(sample_points/2);i++){
        PyList_SetItem(result,i,PyFloat_FromDouble(out[i][0])); //real
        PyList_SetItem(result,i+sample_points/2,PyFloat_FromDouble(out[i][1])); //image
    }

    return result;
}

PyObject* FFTW3PY_destory(PyObject* self, PyObject* args){
    free(in);
    fftw_free(out);
    fftw_destroy_plan(plan);

    return Py_BuildValue("");
}

/***** Define relations for each methods *****/
static PyMethodDef methods[] = {
    {"init", FFTW3PY_init, METH_VARARGS},
    {"inject", FFTW3PY_inject, METH_VARARGS},
    {"exe",FFTW3PY_exe, METH_VARARGS},
    {"destory", FFTW3PY_destory, METH_VARARGS},
    {NULL},
};

/***** Constructor *****/
void initfftw3py(){
    Py_InitModule("fftw3py", methods);
}

です。細かい説明は抜きます。たぶん、コードから読み取っていただく方が素人の説明よりよほど分かりやすいでしょうから。
ただ、要するにPythonとCで異なるデータの扱い方に関して、双方の橋渡し的な部分が今回の肝ということです。データの扱いさえその言語のものに変換してしまえば、あとはその言語でプログラミングするのとほとんど変わりません。
続いて、コンパイルです。

コンパイル、実行:

飽きてきたので、Makefileのせます。
Makefile

.SUFFIXES: .c .so

DIST_FILES= *~ \#*\#*
TARGETS= *.so
FLAGS=`pkg-config --cflags fftw3`
LIBS=`pkg-config --libs fftw3`
PYHEAD=`../SetPyHeadEnv`

.c.so:
        @gcc -shared -fpic ${FLAGS} ${LIBS} -I${PYHEAD} $< -o `echo $@|sed s/.so//`module.so

fftw3py.so:

test:
        @python test.py

clean:
        rm -rf ${DIST_FILES}
distclean: clean
        rm -rf ${TARGETS}

ここで、突然出てきたSetPyHeadEnvとは、FreeBSDUbuntuの環境差を吸収させるためのクッションみたいなもんです。いや、自分も忘れてたんですが……
SetPyHeadEnv(shell script)

#!/bin/sh
PYVERSION=2.6

case `uname` in
    FreeBSD)
        echo "/usr/local/include/python$PYVERSION"
        #sed "s/PYHEAD=[a-z1-9/.]*/PYHEAD=\/usr\/local\/include\/python$PYVERSION/" Makefile > Makefile.x
        ;;
    Linux)
        echo "/usr/include/python$PYVERSION"
        #sed "s/PYHEAD=[a-z1-9/.]*/PYHEAD=\/usr\/include\/python$PYVERSION/" Makefile > Makefile.x
        ;;
    *)
        ;;
esac

以上。

で、makeするとfftw3pymodule.soが生成されます。これが、モジュールになります。あと、色々コンパイラに怒られますが、無視してます。ツンデレですから。

実行は、たとえばこんなイメージで使いたいんだってところで書いたPythonコードをtest.pyとかで保存したら、

% python test.py

でオーケーです。どんな結果が表示されるかって言うと、

[0.0, 0.048772580504032062, 0.095670858091272445, 0.13889255825490054, 0.20786740307563631,..., -0.095670858091272654, -0.04877258050403202]
[5.7211887261098457e-18, -1.8421947292037881e-16,..., 1.4136105444788528e-16, 0.0, 1.9294112024003265e-17]

みたいな。最初のリストが入力信号、次のリストがFFTW3が返してくれた結果となります。

こんな具合で十分かな?これを後で見て、自分が分かるだろうか……
まあ、分かれ、自分!

ネットを自動巡回するゴーストでもいるのかな?

ページビュー、面白がってつけては見たけど、何か不自然に増えている時がある。
ブログなんて初めてだから良く分からないのだけれど、こういうものなのかな?それとも、普通に閲覧している方がいらっしゃるのかな?
何にしても、「誰かに見られてる」と思うと、迂闊なことばかり書いてられないという気持ちになって、自分用のメモなのに、やたら丁寧に書いてしまう(これで丁寧という人種です、僕は)。
でも、実際のところ、自分用のメモだっていい加減に書いてばかりいられないのも事実な訳で。
どうしよう?他者の目を意識して、頭良さげに振る舞った方がいいんだろうか?
……という発想が、既に猛烈に頭が悪そうだ。
残念!

portsを弄くって、py-tkinterを入れたときの小話

FreeBSDにてwxPythonを入れる際、色々困ったことがある。
中でも、インストールすれば良かったpy-openglやpy-numpyと違い、py-tkinterは少しややこしかった。(まあ、py-numpyもaltera外さなかったらコンパイルできなくて困ったけど。make rmconfigは僕みたいなへたれユーザーにとって神は言い過ぎ)

環境:

問題:

さて。
何が問題になったかと言うと、x11-toolkits/py-tkinterは、lang/python26に依存しておりました。
もっと言うと、python2.6.5を要求してたのでございます。
しかし!
lang/python26でインストールされるのはpython2.6.6。

要するに。
インストールされているpythonは2.6.5で、py-tkinter入れるのにはpython2.6.5が必要なのに、依存しているlang/python26は2.6.6用のそれ。
という状況。

不用意に、アップデートしなければ良かったのか?

解決策というか妥協策:

タイトル通り、ports以下を少々いじくって入れました。その手順は下記。

  • これは簡易的メモです。何をしているのかわからねぇよって方は、責任を負えないのでスルーしていただけると幸いです。

手順:

  1. python2.5.5のtar玉を持ってくる。
  2. ls -la、sha256、md5などのコマンドで、持ってきたtar玉のsha256、md5、ファイルサイズを取得する。
  3. ports/distfiles以下にそれをぶちこむ。
  4. lang/python26以下のMakefileでバージョンを2.5.5に書き換え。
  5. 同じく、lang/python26以下のdistinfoを後で戻すときのために退避。これのコピーをいじる。
  6. コピーしたdistinfoを、2.5.5用に書き換える。
  7. x11-toolkits/py-tkinterでmake installする。(lang/python26以下ではしない)
  8. py-tkinterが入ったら、lang/python26のいじったファイルを元に戻す。

こんな感じです。なお、最後の元に戻すって行程を踏まないと、portsnap fetch updateができなくなりますので、必ず行ってください。

wxPythonにおけるwx.Frameの親子関係:勘当篇

タイトルが不快であったとしても、そこは深い意味を持ちません。
あるいは、僕の人間の底は浅いというだけのことです。

閑話休題

今回も小ネタです。
有り体に言うと、

TopWindowから子となるウィンドウを呼び出す

です。

遊びで、子どもの方はテキストエディタ風味にしてみました。

#!/usr/bin/env python
#coding: utf-8
import wx
import os

class NoteFrame(wx.Frame):
    def __init__(self,parent,title):
        wx.Frame.__init__(self,parent,title=title,size=(200,100))
        self.control=wx.TextCtrl(self,style=wx.TE_MULTILINE)
        self.Show(True)

class MyApp(wx.App):
    def OnInit(self):
        frame=wx.Frame(None,wx.ID_ANY,u"PyShell")
        frame.Show(True)
        noteframe=NoteFrame(frame,'Small Editor')
        self.SetTopWindow(frame)
        return True

app = MyApp(0)
app.MainLoop()

実行すると、ただのウィンドウと共に、文字入力が可能になった(だけの)ウィンドウが別に表示されます。この、入力可能な方が子ウィンドウです。

実行できたら、試しに子ウィンドウを閉じてから親ウィンドウを閉じる、次はその逆とやってみて、挙動の違いを見てみてください。

親ウィンドウが閉じられたら、子ウィンドウも閉じますよね?(なんつーネタバレ)

些細なネタですが、応用すれば特定のボタンを押したら、別ウィンドウでエディタ表示など、凝ったものを作りたいとか思ったときに便利だと思います。

追記:

「ちょっといろいろ弄ったけど、これ書き方あってるかなあ?ま、動いてるし、いっか!」Ver.
※アイコン付けてるので、コードコピーのままでは実行できないです。

#!/usr/bin/env python
#coding: utf-8
import wx
import os

class NoteFrame(wx.Frame):
    def __init__(self,parent,id,title):
        wx.Frame.__init__(self,parent,title=title,size=(200,100))
        self.control=wx.TextCtrl(self,style=wx.TE_MULTILINE)
        self.Show(True)

class ShellFrame(wx.Frame):
    def __init__(self,parent,id,title):
        wx.Frame.__init__(self,parent,id=id,title=title,size=(320,240))
        icon=wx.Icon("icon/AppIco.ico",wx.BITMAP_TYPE_ICO)
        self.SetIcon(icon)
        self.Show(True)

class MyApp(wx.App):
    def OnInit(self):
        mainframe=ShellFrame(None,wx.ID_ANY,u'PyShell')
        noteframe=NoteFrame(mainframe,wx.ID_ANY,u'Small Editor')
        self.SetTopWindow(mainframe)
        return True

app = MyApp(0)
app.MainLoop()