(Library) Butterworthローパスフィルター
作成者: 神代 剛
概要
GPhysオブジェクトの任意の次元にButterworthローパスフィルターを施すインスタンスメソッドです。
フィルターをかける軸方向に欠損値があると使えませんが、その方向にすべて欠損値という場合が含まれているのはOKです(具体的には、陸面がマスクされた海上観測データ時系列、とかを想定しています)。
ダウンロード
butterworth.rb (2016-10-18版)
依存ライブラリ
GPhys (電脳Rubyホームページを参照のこと)
使用方法
上記 butterworth.rb を置いたディレクトリで、
require "./butterworth"
とするとモジュールを読み込めます。
私自身は、~/lib/ruby/filter に butterworth.rb を置き (環境変数 RUBYLIB=$HOME/lib/ruby を設定しています)、
require "filter/butterworth"
として使っています。
メソッドの引数については、ソースのコメントを参照してください。 ソースの後半はサンプルプログラムになっていますので、そちらも参考になると思います。
% ruby butterworth.rb
と上記ファイルそのものを実行すると、サンプルプログラムが実行されます。
※サンプルプログラムの実行には、納多哲史さんの gphys_io_util.rb が必要です。
Tips
Butterworthフィルターは再帰型フィルター (recursive filter) なので、実用的には、時系列の最後に0を適当な長さ加えてからフィルターをかける必要があります (いわゆる zero-padding)。 当初はいくつ0を加えるのが適当なのかがよくわからなかったのですが、いろいろ調べてみて、以下のサイトを見つけました:
<URL:http://mailman.ucar.edu/pipermail/ncl-talk/2015-June/003008.html>
これはNCLのユーザーメーリングリスト ncl-talk のアーカイブで、分野も我々と同じですので、ここの記述にしたがってデフォルト値を決めています。
もちろん、陽に指定すれば任意の個数の0を時系列の末尾に加えてフィルターをかけることができます。
ソース
# -*- coding: utf-8 -*-
require "numru/gphys/gphys"
module NumRu
class GPhys
###########################################################################
#
# Butterworth low-pass filter
#
# [in]
# dim (Integer or String) : dimension
# q (Float) : order
# fc (Float) : cut-off frequency
# npad (Integer) : number of zeros padded at the end of the
# series (default: 1.5*q/fc)
# [out]
# gp_ret (GPhys) : filtered data
#
# References:
# Hamming, R. W., 1998: Digital Filters, 3rd ed. Dover Publications, Inc.,
# 284 pp.
# 伊藤久徳, 見延庄士郎, 2010: 気象学と海洋物理学で用いられるデータ解析法.
# 気象研究ノート 第221号, 日本気象学会, 253 pp.
# Roberts, J. and T. D. Roberts, 1978: Use of Butterworth low-pass filter
# for oceanographic data. J. Geophys. Res., 83(C11), 5510-5514.
# [ncl-talk] question regarding Butterworth filter
# http://mailman.ucar.edu/pipermail/ncl-talk/2015-June/003008.html
#
def butterworth(dim, q, fc, npad=nil)
npad = (1.5*q/fc).to_i unless npad
dim = dim_index(dim) if dim.is_a?(String)
dim += rank if dim < 0
if dim < 0 || dim >= rank
raise ArgumentError, "dim #{dim} does not exist"
end
if q%2 == 1
q_is_odd = true
else
q_is_odd = false
end
x = self.data.val.to_type("float")
is_nam = false
if x.class == NArrayMiss
is_nam = true
if x.count_invalid == 0
x = x.to_na
missing = false
else
missing_mask = x.get_mask
count_dim = missing_mask.to_type("int").sum(dim)
count_dim_0 = count_dim.eq(0).to_type("int").sum
count_dim_ndim = count_dim.eq(x.shape[dim]).to_type("int").sum
if count_dim_0 + count_dim_ndim == x.total/x.shape[dim]
missing = true
x = x.to_na(0.0)
else
raise "GPhys data include missing values along the filtered dimention."
end
end
end
x2_shape = x.shape
x2_shape[dim] += npad
x2 = NArray.float(*x2_shape).fill(0.0)
idx = [true]*x.rank
idx[dim] = 0..(x.shape[dim] - 1)
x2[*idx] = x
mm = q/2
y2 = nil
# forward
mm.times do |m|
y2 = bw_2nd_order(x2, dim, fc, m, q, true)
x2 = y2
end
y2 = bw_1st_order(x2, dim, fc, true) if q_is_odd
x2 = y2
# backward
mm.times do |m|
y2 = bw_2nd_order(x2, dim, fc, m, q, false)
x2 = y2
end
y2 = bw_1st_order(x2, dim, fc, false) if q_is_odd
y = y2[*idx].to_type(typecode)
if is_nam
if missing
y = NArrayMiss.to_nam(y,missing_mask)
else
y = NArrayMiss.to_nam(y)
end
end
gp_ret = self.copy
gp_ret.data.replace_val(y)
return gp_ret
end
private
def bw_1st_order(x, dim, fc, dir_forward)
x_shape = x.shape
x_rank = x.rank
x_shape_dim = x.shape[dim]
wc = Math::tan(PI*fc)
c0 = wc/(1 + wc)
c1 = wc/(1 + wc)
b1 = (1 - wc)/(1 + wc)
y = NArray.float(*x_shape).fill(0.0)
x_shape_dim.times do |i|
t = [true]*x_rank
if dir_forward
t[dim] = i
else # backward
t[dim] = (x_shape_dim - 1) - i
end
if i == 0
y[*t] = c0*x[*t]
else
t_1 = [true]*x_rank
if dir_forward
t_1[dim] = i - 1
else # backward
t_1[dim] = (x_shape_dim - 1) - (i - 1)
end
y[*t] = c0*x[*t] + c1*x[*t_1] + b1*y[*t_1]
end
end
return y
end
def bw_2nd_order(x, dim, fc, k, n, dir_forward)
x_shape = x.shape
x_rank = x.rank
x_shape_dim = x.shape[dim]
wc = Math::tan(PI*fc)
b0 = wc**2 + 2*wc*Math::sin(PI*(2*k+1)/(2*n)) + 1
b1b0 = 2 - 2*wc**2
b2b0 = -(wc**2 - 2*wc*Math::sin(PI*(2*k+1)/(2*n)) + 1)
c0b0 = wc**2
c1b0 = 2*wc**2
c2b0 = wc**2
b1 = b1b0/b0
b2 = b2b0/b0
c0 = c0b0/b0
c1 = c1b0/b0
c2 = c2b0/b0
y = NArray.float(*x_shape).fill(0.0)
x_shape_dim.times do |i|
t = [true]*x_rank
if dir_forward
t[dim] = i
else # backward
t[dim] = (x_shape_dim - 1) - i
end
if i == 0
y[*t] = c0*x[*t]
else
t_1 = [true]*x_rank
if dir_forward
t_1[dim] = i - 1
else # backward
t_1[dim] = (x_shape_dim - 1) - (i - 1)
end
if i == 1
y[*t] = c0*x[*t] + c1*x[*t_1] + b1*y[*t_1]
else
t_2 = [true]*x_rank
if dir_forward
t_2[dim] = i - 2
else # backward
t_2[dim] = (x_shape_dim - 1) - (i - 2)
end
y[*t] = c0*x[*t] + c1*x[*t_1] + c2*x[*t_2] + b1*y[*t_1] + b2*y[*t_2]
end
end
end
return y
end
end
end
if $0 == __FILE__
##################################################
# 気象研究ノート No. 221
# 「気象学と海洋物理学で用いられるデータ解析法」
# 第7章 時間フィルタリング を参考に作図
##################################################
require "numru/ggraph"
require "gphys_io_util"
include NumRu
iws = (ARGV[0] ? ARGV[0].to_i : 4)
DCL.gropn(iws)
DCL.sldiv("y", 2, 1)
DCL.sgpset("lclip", true)
### 図7.8 (p.108) ################################
# 元の図はハイパスフィルターの例なので、ローパス
# フィルターの場合は「フィルター時系列」と「元時
# 系列との差」が入れ替わった結果になることに注意。
na = NArray.float(257).fill(0.0)
(124..128).each do |t|
na[t] = 2.0*(t.to_f - 123.0)/5.0
end
(129..133).each do |t|
na[t] = 2.0*(133 - t.to_f)/5.0
end
gp = na2gphys(na,[NArray.float(na.size).indgen])
GGraph.next_fig("viewport"=>[0.15,0.85,0.15,0.85],"window"=>[0.0,256.0,-0.8,2.2])
GGraph.next_axes("xunits"=>"","xtitle"=>"","yunits"=>"","ytitle"=>"")
GGraph.line(gp,true,"title"=>"")
gp2 = gp.butterworth(0, 8, 1.0/30.0)
GGraph.line(gp2,false,"index"=>3)
gp3 = gp - gp2
GGraph.line(gp3,false,"type"=>3)
DCL.uulinz([170,200], [1.8,1.8], 1, 1)
DCL.uulinz([170,200], [1.6,1.6], 1, 3)
DCL.uulinz([170,200], [1.4,1.4], 3, 1)
DCL.sgtxzu(210.0, 1.8, "original", 0.02, 0, -1, 3)
DCL.sgtxzu(210.0, 1.6, "Butter", 0.02, 0, -1, 3)
DCL.sgtxzu(210.0, 1.4, "diff.", 0.02, 0, -1, 3)
### 次数を高くするとリンギングが強く出る #########
# これは図にはないが、本文に説明あり。
GGraph.next_fig("viewport"=>[0.15,0.85,0.15,0.85],"window"=>[0.0,256.0,-0.2,0.9])
GGraph.next_axes("xunits"=>"","xtitle"=>"","yunits"=>"","ytitle"=>"")
GGraph.line(gp,true,"title"=>"")
gp2 = gp.butterworth(0, 2, 1.0/30.0)
GGraph.line(gp2,false,"index"=>23)
gp4 = gp.butterworth(0, 4, 1.0/30.0)
GGraph.line(gp4,false,"index"=>33)
gp8 = gp.butterworth(0, 8, 1.0/30.0)
GGraph.line(gp8,false,"index"=>43)
DCL.sgtxzu(240.0, 0.77, "2nd", 0.02, 0, 1, 3)
DCL.sgtxzu(240.0, 0.70, "4th", 0.02, 0, 1, 3)
DCL.sgtxzu(240.0, 0.63, "8th", 0.02, 0, 1, 3)
DCL.uulinz([180,210], [0.77,0.77], 1, 23)
DCL.uulinz([180,210], [0.70,0.70], 1, 33)
DCL.uulinz([180,210], [0.63,0.63], 1, 43)
gp1 = gp.butterworth(0, 1, 1.0/30.0)
GGraph.line(gp1,false,"type"=>2,"index"=>1)
gp3 = gp.butterworth(0, 3, 1.0/30.0)
GGraph.line(gp3,false,"type"=>3,"index"=>1)
DCL.sgtxzu(240.0, 0.51, "1st", 0.02, 0, 1, 3)
DCL.sgtxzu(240.0, 0.44, "3rd", 0.02, 0, 1, 3)
DCL.uulinz([180,210], [0.51,0.51], 2, 1)
DCL.uulinz([180,210], [0.44,0.44], 3, 1)
### 図7.9 (p.109) ################################
# 元の図左の破線は、キャプションには陽に書かれて
# いないが、オリジナルデータにそのままフィルター
# をかけた結果ではなく、データの最後にゼロを30個
# 加えてフィルターをかけた結果だと思われる(その
# ように作図すると、元の図と一致した)。本文とは
# 齟齬があるのだが、左の破線は両端も含めて低周波
# 成分をうまく取り出せているように見える(つまり、
# 右の実線のほうに対応している)。
t = NArray.float(150).indgen
f = 2.0*sin(2.0*PI*t/12.0) + 1.5*cos(2.0*PI*t/5.5)
f2 = 2.0*sin(2.0*PI*t/12.0)
# 左図
gp = na2gphys(f,[t])
GGraph.next_fig("viewport"=>[0.15,0.85,0.15,0.85],"window"=>[0.0,149.0,-4.0,4.0])
GGraph.next_axes("xtickint"=>5,"xlabelint"=>20,"xunits"=>"day","xtitle"=>"Time","ytickint"=>0.2,"ylabelint"=>1,"yunits"=>"","ytitle"=>"Amplitude")
GGraph.line(gp, true, "title"=>"")
gp_lp = gp.butterworth(0, 8, 1.0/8.0, 30)
GGraph.line(gp_lp, false, "type"=>2)
# 右図
gp2 = na2gphys(f2,[t])
GGraph.next_fig("viewport"=>[0.15,0.85,0.15,0.85],"window"=>[0.0,149.0,-1.0,1.0])
GGraph.next_axes("xtickint"=>5,"xlabelint"=>20,"xunits"=>"day","xtitle"=>"Time","ytickint"=>0.1,"ylabelint"=>0.2,"yunits"=>"","ytitle"=>"")
GGraph.line(gp2 - gp_lp, true, "title"=>"")
gp_lp2 = gp.butterworth(0, 8, 1.0/8.0, 0)
GGraph.line(gp2 - gp_lp2, false, "type"=>2)
DCL.grcls
end
謝辞
実装にあたっては、西本絵梨子さんの (Library) GPhys(加重)移動平均メソッド および (Library) Lanczosフィルター が大変参考になりました。ありがとうございます。
参考文献
Hamming, R. W., 1998: Digital Filters, 3rd ed. Dover Publications, Inc., 284 pp.
伊藤久徳, 見延庄士郎, 2010: 気象学と海洋物理学で用いられるデータ解析法. 気象研究ノート 第221号, 日本気象学会, 253 pp.
Roberts, J. and T. D. Roberts, 1978: Use of Butterworth low-pass filter for oceanographic data. J. Geophys. Res., 83(C11), 5510-5514.
[ncl-talk] question regarding Butterworth filter <URL:http://mailman.ucar.edu/pipermail/ncl-talk/2015-June/003008.html>
キーワード:[GPhys] [拡張ライブラリ] [解析] [時系列フィルター]
参照: