vim-sandwich で関数を消す

Vim の話です。

vim-sandwich というプラグインを以前書きまして、これを更新したよ、というのがこの記事の内容です。

これは何?

ざっくりといえば、vim-surround クローンであり、おおよそ対応する機能を持っています。つまり、テキストオブジェクトを括弧やクオーテーションなどで囲んだり、これらを削除・置換したりするためのプラグインです。

  • 囲む
    foo   →   (foo)
  • 削除
    (foo)   →   foo
  • 置換
    (foo)   →   [foo]

vim-surround よりも拡張性に主眼を置いており、複雑な動作をユーザーが定義できるようになっています。

何が変わったのか?

vim-sandwich の機能の一つに関数囲みの削除があります。(参考) 関数名まで含めて括弧を消す、というわけです。

func(foo)   →   foo

しかし、言語によっては関数名の記述はちょっと違ったりしますね。例を上げれば、Vim script を書くときには頻繁に s:func() という感じの関数名を書きます。このようなかんじで関数名のパターンもカスタムしたい、という要望がきたのでできるようにしました。

Allow user to customize function name to delete · Issue #71 · machakann/vim-sandwich · GitHub

以下のような変数を定義することでグローバルなルールを定義できます。foo::bar.baz() のようなものを関数として認識したい場合は次のような設定を vimrc などに書きます。

let g:sandwich#magicchar#f#patterns = [
  \   {
  \     'header' : '\<\h\k*::\h\k*\.\h\k*',
  \     'bra'    : '(',
  \     'ket'    : ')',
  \     'footer' : '',
  \   },
  \ ]

あるいは、b:sandwich_magicchar_f_patterns を使えばバッファローカルなルールを定義できます。これは主にファイルタイプに特有のパターンを記述するのに使われるでしょう。先の Vim script の例で言えば次のようになります。

augroup sandwich-filetype-vim
  autocmd Filetype vim let b:sandwich_magicchar_f_patterns = [
    \   {
    \     'header' : '\C\<\%(\h\|[sa]:\h\|g:[A-Z]\)\k*',
    \     'bra'    : '(',
    \     'ket'    : ')',
    \     'footer' : '',
    \   },
    \ ]
augroup END

(ただし、上の設定は vim-sandwich が持つのでユーザーが自ら設定する必要はありません。)

リストの要素の辞書一つが、一種の関数のパターンに対応しており、四つのキー ('header', 'bra', 'ket', 'footer') を持っていなければいけません。'header' は関数名に一致する正規表現、'bra' は開き括弧、'ket' は閉じ括弧に一致する正規表現、'footer' は閉じ括弧に続くテキストに一致する正規表現を値に持ちます。

リストなので複数登録しておくことも可能です。これで関数囲みの削除が捗りますね!

vim-swap で関数の引数を並べ替える

Vim の話です。

vim-swap というプラグインを以前書きまして、これを更新したよ、というのがこの記事の内容です。

これは何?

もっとも代表的な用途は、関数の引数のようなコンマで区切られたテキストの順番を入れ替える、というものです。例えば、次のような三つの引数をとる関数があり Banana の上にカーソルがあるとします。

    Basket(Apple, Banana, Cherry)

この時 g< と入力すると BananaApple を入れ替えます。

    Basket(Banana, Apple, Cherry)

反対に、g> と入力すると BananaCherry を入れ替えます。

    Basket(Apple, Cherry, Banana)

もう一つ機能があって、gs と入力すると対話的に入れ替えを行う swap mode を開始します。これは何が嬉しいのかと申しますと、ちょっと複雑な入れ替えを . で繰り返せるという点です。上の g<g> も、もちろん . で繰り返し可能ですが、繰り返せるのは一つの要素の入れ替えのみです。gs は複数の要素の移動をまとめて繰り返せます。

例えば、次のバッファには Basket(Cherry, Banana, Apple) が三回現れますが、これをすべて Basket(Apple, Banana, Cherry) にするために g>g< を使うと、. を繰り返し押す必要があります。

function! Basket(Cherry, Banana, Apple) abort
  return 0
endfunction

call Basket(Cherry, Banana, Apple)

call Basket(Cherry, Banana, Apple)

しかし、gs を使って入れ替えておくと、後は . 一回で変換できます。gif を見て何となく感じてください。

swap-mode-dotrepeat

何が変わったのか?

Swap mode のコマンドを増やしました。gs と入力して swap mode に入り、以下のコマンドを使います。満足したら <Esc> キーで swap mode を抜けます。

Sort

s / S キーで要素をソートします。s は昇順、S は降順にソートします。

swap-mode-sort

Reverse

r キーで要素を反転します。

swap-mode-reverse

Grouping

g キーでカーソル下の要素と右隣りの要素をグループ化して一つの要素として扱います。G でグループ化を解除します。

swap-mode-grouping


実装してみると、逆になぜ今までなかったのかと思うような機能ばかりでした。特にグルーピングがずっと欲しかった。部分的なソートや部分的な反転を実現したかったんですけど、力尽きたので切実に欲しくなってからにします。

vim-swap は最近 Github で 100 star を超えました🎉 ありがとうございます。

Vim で Unicode 記号の入力補完をする

Julia 言語の特徴の一つとして広範な Unicode 記号を識別子として使える、というものがあります。その有用性について最初は懐疑的だったのですが、一度使ってしまうともう戻れなくなりました。過去のコードが mu だの theta だの phi だので埋め尽くされているのをみると、そっと vim を閉じたくなります。

さて、問題はこれらの記号をいかにして入力するか、ということです。Julia REPL では Tab キーを使い、\ から始まる LaTeX っぽい記法から変換することができます。例えば、

  1. \alp --Tab--> \alpha
  2. \alpha --Tab--> α

といった具合です。julia-vim が同様の機能を vim 上で実現していて非常に助かるんですけど、私思うんです、vim にはポップアップウィンドウがあるんだから \alpha を経由する必要なくない…?って。

というわけで書きました。

github.com

asyncomplete-unicodesymbol.vim

注意する点としては julia-vim と asyncomplete.vim v2 branch が必要なことです。リリースまでもうすこしらしいんですけど、よかったらデバッグに参加してみましょう。

(※ 2019/4/2 追記 asyncomplete.vim v2 無事リリースされました!今は master branch を使えば大丈夫です。)

さらに、当然といえば当然なのですが \alphaα に文字列としてはマッチしていません。このような補完 (変換?) をおこなうために、補完要素のフィルターをいじる必要があります。asyncomplete は v2 からこの部分をカスタマイズできるようになりました。わかる人は自分で何とかしてください。よくわからん、という人は asyncomplete-ezfilter.vim を使ってください。これは asyncomplete で complete-source 毎にフィルタをかけるためのプラグインです。まとめるとこんな感じです。

let g:asyncomplete_preprocessor =
  \ [function('asyncomplete#preprocessor#ezfilter#filter')]

autocmd User asyncomplete_setup call asyncomplete#register_source(
  \ asyncomplete#sources#unicodesymbol#get_source_options({
  \   'name': 'unicodesymbol',
  \   'whitelist': ['julia'],
  \   'completor': function('asyncomplete#sources#unicodesymbol#completor'),
  \ }))

let g:asyncomplete#preprocessor#ezfilter#config = {}
let g:asyncomplete#preprocessor#ezfilter#config.unicodesymbol =
  \ {ctx, items -> filter(items, 'ctx.match(v:val.menu)')}

Julia の Language server を Vim でうごかす

※ --- 2020/1/21 追記 ---

その2を書きました。

Julia の Language server を Vim でうごかす その2 - 書いたものなど

※ --- 2020/1/21 追記ここまで ---


やっと、 LanguageServer.jl を動かせるようになったのでまとめます。将来はもっと簡単になると思いますが、とりあえず。

Julia 側の準備

まずなんですが、現在 (2019/2/9) の時点では [LanguageServer.jl] は Julia v1.1.0 では正常に動作しないようです。なので、別に v1.0.3 をインストールします。この時、パスを通す必要はありません。

次に LanguageServer.jlSymbolServer.jl をインストールします。Julia v1.0.3 を起動し ]add LanguageServer SymbolServer と実行します。SymbolServerLanguageServer の依存に入っているはずなんですけど、なぜか後で手動読み込みする必要があるので、明示的にインストールします。今のところ LanguageServer は v"0.5.1" 、SymbolServer は v"0.1.2" で動作確認しています。

※ --- 2019/4/4 追記 ---

SymbolServer.jl が v"0.1.3" に更新され、julia 1.1.0 でも動作するようになりました🎉

※ --- 2019/4/4 追記ここまで ---

f:id:machakann:20190209152230p:plain

LanguageServer の起動スクリプト startlanguageserver.jl を保存しておきます。このファイルパスをのちに vimrc に書く必要があります。

  • startlanguageserver.jl
import LanguageServer
import Pkg
import SymbolServer

envpath = dirname(Pkg.Types.Context().env.project_file)

const DEPOT_DIR_NAME = ".julia"
depotpath = if Sys.iswindows()
    joinpath(ENV["USERPROFILE"], DEPOT_DIR_NAME)
else
    joinpath(ENV["HOME"], DEPOT_DIR_NAME)
end

server = LanguageServer.LanguageServerInstance(stdin, stdout, false, envpath, depotpath, Dict())
server.runlinter = true
run(server)

これで Julia 側の準備は終わりです。試しに include("path/to/startlanguageserver.jl") を実行してエラーが出ないか確かめましょう。

f:id:machakann:20190209152928p:plain

Vim 側の準備

vim-lsp と必要なプラグインをインストールします。

Plug 'prabirshrestha/async.vim'
Plug 'prabirshrestha/vim-lsp'

vim-lsp の README から引用しました。vim-plug を使った例のようですね。好きなプラグインマネージャを使うように読み替えましょう。自動補完が必要なら追加でいくつか必要です。

Plug 'prabirshrestha/asyncomplete.vim'
Plug 'prabirshrestha/async.vim'
Plug 'prabirshrestha/vim-lsp'
Plug 'prabirshrestha/asyncomplete-lsp.vim'

最後に次の設定を vimrc に追加します。実行ファイルと起動スクリプトのパスを指定する必要があります。(s:julia_exe 及び s:julia_lsp_startscript)

let s:julia_exe = 'path/to/julia-1.0.3-executable'
let s:julia_lsp_startscript = 'path/to/startlanguageserver.jl'
if executable('julia')
  autocmd User lsp_setup call lsp#register_server({
  \ 'name': 'julia',
  \ 'cmd': {server_info->[s:julia_exe, '--startup-file=no', '--history-file=no', s:julia_lsp_startscript]},
  \ 'whitelist': ['julia'],
  \ })
endif

以上で終わりです。

使用に関して

*.jl なファイルを開くと起動します。起動に少し時間がかかるようなので気長に待ちましょう。わたしのノートPCだと20秒ぐらいかかります。補完はさらにインサートモードに入って何文字かタイプした後、7~8 秒待って次にインサートモードに入るときにはじまるようです。

全部の機能が動いているかはまだ試していません。SymbolServer は結局動いているか微妙な感じもします。

f:id:machakann:20190209155720g:plain

※ 2018/2/10 追記 : リネーム機能は動いてない模様。あとでもうちょっと調べます。vscode だと動くんだけどな…

Julia で Grass-Fire algorithm を書く

なんの因果か最近は画像をぐりぐりしています。

画像の明るい点を検知してその重心位置を求める必要ができたのですが、考えてみるとなかなか難しい。いろいろ調べてみるとどうやらこういうのは BLOB (Binary Large OBject) detection というらしいです。厳密には BLOB extraction かな。

単純でいいのでとにかく速いものが欲しい、ということで探しているとここの Grass-Fire algorithm 解説を見つけました。どうも一次情報は [T. B. Moeslund, Introduction to Video and Image Processing: Building Real Systems and Applications, Springer London, London, 2012.] という本みたいですが、引用とかないし大丈夫かこれ。幸い、本を図書館で利用できたので参考にしました。

ちなみに真っ先に試したのはImages.jlblob_LoG 関数なんですが、ちょっとほしいものと違う感じでした。BLOBの中心は得られるのですが、できればサブピクセルレベルで、つまり小数点以下のレベルで座標を欲しかったんです。よく理解していないだけかもしれないです。OpenCV とかに便利で速い関数とかありそうなんですが、今のところ julia から OpenCV を使うのは簡単ではなさそう…。Grass-Fire algorithm は経路探索など様々な用途で使われるもののようです。とりあえず私の目的も果たせそうなのでいいでしょう。

いかにも再帰が向いていそうな感じなので、とりあえずその方向で書きました。閾値の thr と同じかより大きい値を持つピクセルの連続を BLOB として認識し、座標の配列として返します。

function extractblob_rec(img, thr)
    height, width = size(img)
    checked = fill!(similar(img, Bool), false)
    bloblist = Vector{NTuple{2,Int}}[]
    for x in 1:width, y in 1:height
        # Scan pixels until a new BLOB
        @inbounds if img[y, x] >= thr && !checked[y, x]
            # BLOB detection by Grass-Fire algorithm
            blob = NTuple{2,Int}[]
            grassfire!(blob, checked, img, y, x, thr)
            push!(bloblist, blob)
        end
    end
    bloblist
end


function grassfire!(blob, checked, img, y, x, thr)
    height, width = size(img)
    push!(blob, (y, x))
    checked[y, x] = true

    # check the lower pixel
    if y+1 <= height && img[y+1, x] >= thr && !checked[y+1, x]
        grassfire!(blob, checked, img, y+1, x, thr)
    end

    # check the right pixel
    if x+1 <= width && img[y, x+1] >= thr && !checked[y, x+1]
        grassfire!(blob, checked, img, y, x+1, thr)
    end

    # check the upper pixel
    if y-1 >= 1 && img[y-1, x] >= thr && !checked[y-1, x]
        grassfire!(blob, checked, img, y-1, x, thr)
    end

    # check the left pixel
    if x-1 >= 1 && img[y, x-1] >= thr && !checked[y, x-1]
        grassfire!(blob, checked, img, y, x-1, thr)
    end
    blob
end

参考と同じ簡単な 6x5 の配列を作ってテストしてみます。

julia> img
6×5 Array{Int64,2}:
 0  0  1  1  1
 0  0  0  1  0
 0  0  0  1  0
 0  1  1  0  0
 0  1  1  0  0
 0  1  1  0  0

julia> extractblob_rec(img, 1)
2-element Array{Array{Tuple{Int64,Int64},1},1}:
 [(4, 2), (5, 2), (6, 2), (6, 3), (5, 3), (4, 3)]
 [(1, 3), (1, 4), (2, 4), (3, 4), (1, 5)]

二つの BLOB を見つけたみたいです、よさそうですね。探す様子を GIF 化しました。赤が走査している場所、緑と青が BLOB です。

GrassFireAlgorithm


さて、再帰を使っていると StackOverflowError が怖いです。たしか Julia ではまだ末尾呼び出し最適化されないので、BLOB が大きくなるとスタックがあふれます。私の実際の用途を考えると BLOB のサイズはそんなに大きくならないのですが、いちおう再帰を使わない形も考えてみます。

import DataStructures: Stack

function extractblob_loop(img, thr)
    height, width = size(img)
    stack = Stack{NTuple{3,Int}}()
    checked = fill!(similar(img, Bool), false)
    bloblist = Vector{NTuple{2,Int}}[]
    for x in 1:width, y in 1:height
        # Scan pixels until a new BLOB
        @inbounds if img[y, x] < thr || checked[y, x]
            # BLOB detection by Grass-Fire algorithm
            # NOTE: re-use `stack` because making a new one is a little costly
            blob = grassfire_loop!(checked, stack, img, y, x, thr)
            push!(bloblist, blob)
        end
    end
    bloblist
end


function grassfire_loop!(checked, stack, img, y0, x0, thr)
    height, width = size(img)
    blob = NTuple{2,Int}[]
    push!(stack, (y0, x0, 4))
    push!(blob, (y0, x0))
    checked[y0, x0] = true
    while !isempty(stack)
        y, x, state = pop!(stack)
        checked[y, x] = true

        # check the lower pixel
        if state >= 4 && y+1 <= height && img[y+1, x] >= thr && !checked[y+1, x]
            push!(stack, (y, x, 3))
            push!(stack, (y+1, x, 4))
            push!(blob, (y+1, x))
            continue
        end

        # check the right pixel
        if state >= 3 && x+1 <= width && img[y, x+1] >= thr && !checked[y, x+1]
            push!(stack, (y, x, 2))
            push!(stack, (y, x+1, 4))
            push!(blob, (y, x+1))
            continue
        end

        # check the upper pixel
        if state >= 2 && y-1 >= 1 && img[y-1, x] >= thr && !checked[y-1, x]
            push!(stack, (y, x, 1))
            push!(stack, (y-1, x, 4))
            push!(blob, (y-1, x))
            continue
        end

        # check the left pixel
        if state >= 1 && x-1 >= 1 && img[y, x-1] >= thr && !checked[y, x-1]
            push!(stack, (y, x, 0))
            push!(stack, (y, x-1, 4))
            push!(blob, (y, x-1))
            continue
        end
    end
    blob
end

ランダムな配列でテストしたところ extractblob_rec と同じ結果を返すようです。

次に 800x600 ピクセルに50個程度の輝点を持つ画像を用意し、性能を確認しました。ベンチマークの結果は以下です。

julia> versioninfo()
Julia Version 1.1.0
Commit 80516ca202 (2019-01-21 21:24 UTC)
Platform Info:
  OS: Windows (x86_64-w64-mingw32)
  CPU: Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.1 (ORCJIT, broadwell)

julia> using BenchmarkTools

julia> @benchmark extractblob_rec(testimg, 240)
BenchmarkTools.Trial:
  memory estimate:  482.02 KiB
  allocs estimate:  135
  --------------
  minimum time:     408.409 μs (0.00% GC)
  median time:      453.314 μs (0.00% GC)
  mean time:        515.280 μs (4.78% GC)
  maximum time:     60.672 ms (98.67% GC)
  --------------
  samples:          9601
  evals/sample:     1

julia> @benchmark extractblob_loop(testimg, 240)
BenchmarkTools.Trial:
  memory estimate:  506.22 KiB
  allocs estimate:  140
  --------------
  minimum time:     409.692 μs (0.00% GC)
  median time:      454.168 μs (0.00% GC)
  mean time:        512.038 μs (5.07% GC)
  maximum time:     58.005 ms (99.16% GC)
  --------------
  samples:          9665
  evals/sample:     1

800x600 の画像で 1ms を切るのが目標だったので、とりあえずどちらも目標達成ですね。非力なノート PC で達成できたので十分といえるでしょう。使用メモリーのほとんどが checked 配列に費やされているので、これを再利用すると 1/10 ぐらいになります。median time を見ると再帰を使ったほうがちょっと速そうに見えますが誤差です。何回か測定すると前後します。

最終的には再帰のほうがコードは簡潔ですけど、ちょっと悩みます。どっちを使うべきか…。あと、まだどこか高速化する余地はあるか考えてみます。重心位置の計算は省略。

けものフレンズのタイトルロゴのアレ

vim-colorscheme-kemonofriends を書いたときに、けものフレンズのタイトルロゴを Vim バッファ上に出力するスクリプトを (README に載せるためだけに) 書きました。例のスクリーンショットを撮った後、当のスクリプトはどこに置いたのかわからなくなってしまって間違って消したのかと思っていたんですけど、さっきひょんなことから見つけたので gist に置くことにしました。

kemologo.vim

使い方は簡単で、vim-colorscheme-kemonofriendsをインストールして

:source kemologo.vim
:KemonofriendsLogo

これだけです。

f:id:machakann:20180923014202g:plain

BLAS ライブラリの違いによる GAMESS のパフォーマンス比較

GAMESS に BLAS ライブラリとして mkl, OpenBLAS, ATLAS をリンクした場合のパフォーマンスを比較しました。

参考 :
GAMESS をビルドする mkl 編
GAMESS をビルドする OpenBLAS 編

ATLAS もビルドしようとしてみたんですけど CPU throttling の停止ができなくて諦めました…。代わりに apt でインストールしたものを使っていますが、環境に最適化されたものでないことは留意してください。

sudo apt-get install libatlas-base-dev

CC や MP2 の計算で BLAS の差が出るときいたので、そのへんを試してみました。正直こういう計算はやったことがなく、マニュアルとかを読みながらやっていますが、妥当なインプットファイルを作れているか自信はないです。

環境

CPU: Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz
MEMORY: 16 GB
OS : Ubuntu 18.04 64bit
gcc : version 7.3.0
GAMESS : Feb 14, 2018 R1 Public Release
BLAS : mkl (intel-mkl-2018.3-051), OpenBlas v0.3.3, ATLAS (libatlas-base-dev_3.10.3-5_amd64)

Alanine の Single Point Energy - CCSD(T)/6-311G

インプットファイルはこちら

三回ずつ計算を走らせて、その中の最も良かった TOTAL WALL CLOCK TIME を比較しました。単位は秒です。結果は mkl が最も速かったものの、OpenBLAS がかなり善戦しているという結果でした。ATLAS については自分でビルドしたものではないので、その性能を出しきれてないというのはあると思います。

f:id:machakann:20180916203850j:plainf:id:machakann:20180921222253p:plain
The Computing Time of Alanine Single Point Energy

Caffeine の Single Point Energy - MP2/6-311G(d)

インプットファイルはこちら

同じく、三回ずつ計算を走らせて、その中の最も良かった TOTAL WALL CLOCK TIME を比較しました。こちらでも mkl が最も速かったのですが、差はそんなにないようです。

f:id:machakann:20180916203855j:plainf:id:machakann:20180921222256p:plain
The Computing Time of Caffeine Single Point Energy

Coumarin の Single Point Energy - B3LYP/6-311G(d)

インプットファイルはこちら

DFT 計算にはあまり BLAS を使わないのか、ほとんど差は出ませんでした。

f:id:machakann:20180916203859j:plainf:id:machakann:20180921222257p:plain
The Computing Time of Coumarin Single Point Energy

まとめ

mkl すごい。でも OpenBLAS が思った以上にいい勝負だった。

もしかすると、Single Point Energy 計算は題目として適切ではなかったかも。

おまけ : OpenBLAS を使う場合の注意

理由はよくわからないのですけど OpenBLAS がスレッドを分けると逆に遅くなるみたいです。なので、OpenBLAS を使う場合はスレッディングを無効にしたほうがよさそうです。

  • スレッディングを無効にビルドしたものをリンクした場合 (make USE_THREAD=0) は特に何もする必要はないです。

  • スレッディングが有効な OpenBLAS (make, make USE_THREAD=1) を使う場合には環境変数 OPENBLAS_NUM_THREADS でシングルスレッドで動作するようにすると大丈夫です。デフォルトで有効になっているのでこの場合が多いかもしれません。

export OPENBLAS_NUM_THREADS=1
  • OpenMP によるスレッディングが有効な OpenBLAS (make USE_OPENMP=1) を使う場合には環境変数 OMP_NUM_THREADS でシングルスレッドで動作するようにすると大丈夫です。
export OMP_NUM_THREADS=1

処理時間が以下のようにかなり変わります。

f:id:machakann:20180921222300p:plain
Alanine Single Point Energy CCSD(T)/6-311G (including OpenBLAS variations)

OpenBLAS:
OpenBLAS を make USE_THREAD=0 でビルド
GAMESS を /opt/gamess/rungms 00 4 alanine.inp と実行

OpenBLAS 1thread:
OpenBLAS を make USE_THREAD=1 でビルド
GAMESS を OPENBLAS_NUM_THREADS=1 /opt/gamess/rungms 00 4 alanine.inp と実行

OpenBLAS 4thread:
OpenBLAS を make USE_THREAD=1 でビルド
GAMESS を OPENBLAS_NUM_THREADS=4 /opt/gamess/rungms 00 4 alanine.inp と実行

OpenBLAS 1thread openmp:
OpenBLAS を make USE_OPENMP=1 でビルド
GAMESS を OMP_NUM_THREADS=1 /opt/gamess/rungms 00 4 alanine.inp と実行

OpenBLAS 4thread openmp:
OpenBLAS を make USE_OPENMP=1 でビルド
GAMESS を OMP_NUM_THREADS=4 /opt/gamess/rungms 00 4 alanine.inp と実行