Rでフーリエ級数展開

フーリエ級数展開とは

フーリエ級数展開とは、周期 \(T\) を持つ任意の(区分的に滑らかな)周期関数 \(f(x)\) を、三角関数(正弦波・余弦波)の無限級数として表現する手法です。

周期 \(T = 2\pi\) の関数 \(f(x)\) を考えると、フーリエ級数は次のように表せられます。

\[
f(x) = \dfrac{a_0}{2} + \displaystyle\sum_{n=1}^{\infty} \left( a_n \cos(nx) + b_n \sin(nx) \right)
\]

ここで係数は以下の積分で求まります。

\[
a_0 = \dfrac{1}{\pi}\int_{-\pi}^{\pi} f(x)\, dx, \quad
a_n = \dfrac{1}{\pi}\int_{-\pi}^{\pi} f(x)\cos(nx)\, dx, \quad
b_n = \dfrac{1}{\pi}\int_{-\pi}^{\pi} f(x)\sin(nx)\, dx
\]

つまり、どんな複雑な周期波形でも、「基本周波数の正弦波・余弦波」と「その整数倍の周波数を持つ倍音(高調波)」の重ね合わせとして分解できる、という手法です。

ここで \(a_n, b_n\) はそれぞれ、その周波数成分がどれだけ含まれているかを表す「重み」です。

三角関数系 \(\{1, \cos(x), \sin(x), \cos(2x), \sin(2x), \dots\}\) は区間 \([-\pi, \pi]\)上で直交基底をなすため、通常のベクトルを正規直交基底に射影して成分を取り出すのと同じ発想で、\(f(x)\) の各周波数成分を「内積(積分)」によって取り出すことができます。

矩形波・のこぎり波・三角波のフーリエ級数展開

矩形波(不連続点あり)のこぎり波(不連続点あり)および三角波(連続だが微分不連続)をフーリエ級数で近似し、項数 \(N\) を増やすにつれて近似がどう改善していくか、またギブス現象がどう現れるかを可視化します。

なお、ギブス現象とは、矩形波やのこぎり波のように不連続点を持つ関数をフーリエ級数で近似すると、不連続点付近でオーバーシュートが生じ、項数を増やしてもその振幅がゼロに収束しない現象です。

また、オーバーシュートとはフーリエ級数の部分和(近似曲線)が不連続点(段差)のすぐ近くで、真の波形の値を一時的に超えてしまう(飛び出してしまう)量の事です。

各波形の解析的フーリエ係数

波形係数の式不連続性
矩形波 \(b_n = \dfrac{4}{n\pi}\) (\(n\)奇数のみ)関数自体が不連続 → ギブス現象あり
のこぎり波\(b_n = \dfrac{2(-1)^{n+1}}{n\pi}\)関数自体が不連続 → ギブス現象あり
三角波 \(b_n = \dfrac{8(-1)^{(n-1)/2}}{n^2\pi^2}\) (\(n\)奇数のみ)関数は連続、傾きが不連続 → ギブス現象なし(収束が速い)

対象関数の定義(すべて周期 2π、-π~πに正規化)

library(ggplot2)
library(dplyr)
library(tidyr)
library(purrr)

square_wave <- function(x) {
  x_mod <- ((x + pi) %% (2 * pi)) - pi
  ifelse(x_mod > 0, 1, -1)
}

sawtooth_wave <- function(x) {
  x_mod <- ((x + pi) %% (2 * pi)) - pi
  x_mod / pi # -1 から 1 へ直線的に増加し、周期端で不連続に戻る
}

triangle_wave <- function(x) {
  x_mod <- ((x + pi / 2) %% (2 * pi)) - pi / 2
  ifelse(x_mod <= pi / 2,
    (2 / pi) * x_mod,
    (2 / pi) * (pi - x_mod)
  )
}

target_functions <- list(
  "矩形波" = square_wave,
  "のこぎり波" = sawtooth_wave,
  "三角波" = triangle_wave
)

波形ごとのフーリエ係数(解析解)を返す関数

bn_analytic <- function(wave_name, n) {
  switch(wave_name,
    "矩形波" = ifelse(n %% 2 == 1, 4 / (n * pi), 0),
    "のこぎり波" = 2 * (-1)^(n + 1) / (n * pi),
    "三角波" = ifelse(n %% 2 == 1,
      8 * (-1)^((n - 1) / 2) / (n^2 * pi^2),
      0
    )
  )
}

フーリエ部分和(N項)を計算する汎用関数

fourier_partial_sum <- function(x, N, wave_name) {
  s <- 0
  for (n in seq_len(N)) {
    b_n <- bn_analytic(wave_name, n)
    s <- s + b_n * sin(n * x)
  }
  s
}

数値積分との一致確認(検算)

compute_bn_numeric <- function(f, n, lower = -pi, upper = pi) {
  integrand <- function(x) f(x) * sin(n * x)
  (1 / pi) * integrate(integrand, lower, upper,
    subdivisions = 500
  )$value
}

cat("=== 各波形のフーリエ係数:解析解 vs 数値積分 ===\n")
for (wave_name in names(target_functions)) {
  cat(sprintf("\n--- %s ---\n", wave_name))
  for (n in 1:4) {
    b_a <- bn_analytic(wave_name, n)
    b_n <- compute_bn_numeric(target_functions[[wave_name]], n)
    cat(sprintf("n=%d : 解析解 = %.6f, 数値積分 = %.6f\n", n, b_a, b_n))
  }
}
=== 各波形のフーリエ係数:解析解 vs 数値積分 ===

--- 矩形波 ---
n=1 : 解析解 = 1.273240, 数値積分 = 1.273240
n=2 : 解析解 = 0.000000, 数値積分 = 0.000000
n=3 : 解析解 = 0.424413, 数値積分 = 0.424413
n=4 : 解析解 = 0.000000, 数値積分 = -0.000000

--- のこぎり波 ---
n=1 : 解析解 = 0.636620, 数値積分 = 0.636620
n=2 : 解析解 = -0.318310, 数値積分 = -0.318310
n=3 : 解析解 = 0.212207, 数値積分 = 0.212207
n=4 : 解析解 = -0.159155, 数値積分 = -0.159155

--- 三角波 ---
n=1 : 解析解 = 0.810569, 数値積分 = 0.810569
n=2 : 解析解 = 0.000000, 数値積分 = 0.000000
n=3 : 解析解 = -0.090063, 数値積分 = -0.090063
n=4 : 解析解 = 0.000000, 数値積分 = -0.000000

3つの波形について、解析解(理論式)と数値積分の結果が小数点以下6桁まで一致しています。

これは、係数の導出式が正しく実装されていることの検証ができたことを意味します。

項数 N を変えて近似の様子を波形別に可視化

x_vals <- seq(-2 * pi, 2 * pi, length.out = 2000)
N_list <- c(1, 3, 5, 15, 50)

df_approx <- expand_grid(
  wave_name = names(target_functions),
  N = N_list
) %>%
  mutate(data = map2(wave_name, N, function(w, n) {
    data.frame(x = x_vals, y = fourier_partial_sum(x_vals, n, w))
  })) %>%
  unnest(data) %>%
  mutate(N_label = factor(paste0("N = ", N), levels = paste0("N = ", N_list)))

df_true <- map_dfr(names(target_functions), function(w) {
  data.frame(x = x_vals, y = target_functions[[w]](x_vals), wave_name = w)
})

p1 <- ggplot() +
  geom_line(
    data = df_true, aes(x = x, y = y),
    color = "black", linetype = "dashed", linewidth = 0.5
  ) +
  geom_line(
    data = df_approx, aes(x = x, y = y, color = N_label),
    linewidth = 0.6, alpha = 0.85
  ) +
  facet_wrap(~wave_name, ncol = 1) +
  labs(
    title = "波形別フーリエ級数近似の比較",
    subtitle = "黒破線: 真の波形 / 色線: 各次数での部分和",
    x = "x", y = "f(x)", color = "近似次数"
  ) +
  theme_minimal()

print(p1)
Figure 1

Figure 1 は3つの波形について、項数 \(N\) を増やしながらフーリエ部分和を重ね描きした結果です。

黒破線が真の波形、色付きの線が \(N=1, 3, 5, 15, 50\) での近似となり、波形ごとの収束の過程の違いが視覚的に表れています。

矩形波・のこぎり波

この2つのパネルでは、次のような特徴が見て取れます。

  • N=1(赤線)は単なる正弦波1本で、まだ矩形/のこぎり形にはほど遠い状態です。
  • N を増やす(緑・水色・紫と進む)につれて、平坦な部分では真の波形にフィットしていきますが、不連続点(ジャンプしている箇所)の直前・直後だけは、N=50(マゼンタ)になってもオーバーシュートが残っていることが確認できます。
  • このオーバーシュートの山の高さはNを増やしても縮まらず、山の幅(存在範囲)だけが不連続点に向かって狭くなっていくというギブス現象が、視覚的に再現されています。

三角波

三角波のパネルは他の2つと対照的です。

  • N=1(赤線)の時点ですでに真の三角波にかなり近い形をしており、N=3, 5 程度でほぼ真の波形と見分けがつかなくなっています。
  • 頂点(尖った角)付近でもオーバーシュートはほとんど見られず、ギブス現象特有の波打ちが視認できません。
  • これは、三角波の係数が \(b_n \propto 1/n^2\) であるため、矩形波・のこぎり波の \(b_n \propto 1/n\) よりも急速に減衰するためです。
  • 三角波は関数値自体は連続(角はあるが跳躍はない)ですので、部分和が真の関数に一様収束し、不連続点特有のオーバーシュートが生じません。

収束速度の比較(誤差のL2ノルムをNごとにプロット)

compute_l2_error <- function(wave_name, N, x_grid) {
  y_true <- target_functions[[wave_name]](x_grid)
  y_approx <- fourier_partial_sum(x_grid, N, wave_name)
  sqrt(mean((y_true - y_approx)^2))
}

x_grid <- seq(-pi + 0.001, pi - 0.001, length.out = 1000)
N_seq <- c(1, 2, 3, 5, 7, 10, 15, 20, 30, 50, 80, 120)

error_df <- expand_grid(wave_name = names(target_functions), N = N_seq) %>%
  mutate(l2_error = map2_dbl(wave_name, N, compute_l2_error, x_grid = x_grid))

p2 <- ggplot(error_df, aes(x = N, y = l2_error, color = wave_name)) +
  geom_line(linewidth = 0.8) +
  geom_point(size = 1.5) +
  scale_y_log10() +
  labs(
    title = "項数 N に対する近似誤差(L2ノルム)の収束速度比較",
    x = "項数 N", y = "L2誤差(log scale)", color = "波形"
  ) +
  theme_minimal()

print(p2)
Figure 2

三角波は連続関数であるため収束が他の2つの波形の場合よりも速い(傾きが急である)ことを確認できます。

ギブス現象の有無を波形ごとに確認

# 矩形波: 不連続点は x = 0(および x = ±π)
# のこぎり波: 不連続点は x = ±π(x = 0 は連続な滑らかな点なので注意)
# 三角波: 連続関数 → オーバーシュートは N→∞ で消える

# --- 矩形波用:x = 0 の不連続点を右側から測定 ---
overshoot_check_square <- function(N) {
  x_fine <- seq(0.0001, pi / N, length.out = 300)
  y_fine <- fourier_partial_sum(x_fine, N, "矩形波")
  max(y_fine) - 1 # 真値の上限(1)からのオーバーシュート
}

# --- のこぎり波用:x = π の不連続点を左側から測定 ---
overshoot_check_sawtooth <- function(N) {
  x_fine <- seq(pi - pi / N, pi - 0.0001, length.out = 300)
  y_fine <- fourier_partial_sum(x_fine, N, "のこぎり波")
  max(y_fine) - 1 # 真値の上限(1)からのオーバーシュート
}

cat("=== 不連続点付近のオーバーシュート量 ===\n")

cat("\n--- 矩形波(不連続点: x = 0) ---\n")
for (N in c(10, 50, 200, 1000)) {
  os <- overshoot_check_square(N)
  cat(sprintf("N = %5d : オーバーシュート = %.4f\n", N, os))
}

cat("\n--- のこぎり波(不連続点: x = π) ---\n")
for (N in c(10, 50, 200, 1000)) {
  os <- overshoot_check_sawtooth(N)
  cat(sprintf("N = %5d : オーバーシュート = %.4f\n", N, os))
}

# --- 三角波:連続関数なのでオーバーシュートは生じない ---
cat("\n--- 三角波(連続関数、Gibbs現象なし) ---\n")
for (N in c(1, 3, 5, 15)) {
  x_fine <- seq(0.0001, pi / N, length.out = 300)
  y_fine <- fourier_partial_sum(x_fine, N, "三角波")
  # 三角波は x=0付近で値0から傾き2/piで単調増加、オーバーシュートはほぼ生じない
  diff_from_true <- max(y_fine - triangle_wave(x_fine))
  cat(sprintf("N = %5d : 真値との最大差分 = %.5f\n", N, diff_from_true))
}
=== 不連続点付近のオーバーシュート量 ===

--- 矩形波(不連続点: x = 0) ---
N =    10 : オーバーシュート = 0.1823
N =    50 : オーバーシュート = 0.1791
N =   200 : オーバーシュート = 0.1790
N =  1000 : オーバーシュート = 0.1790

--- のこぎり波(不連続点: x = π) ---
N =    10 : オーバーシュート = 0.0867
N =    50 : オーバーシュート = 0.1593
N =   200 : オーバーシュート = 0.1740
N =  1000 : オーバーシュート = 0.1780

--- 三角波(連続関数、Gibbs現象なし) ---
N =     1 : 真値との最大差分 = 0.07681
N =     3 : 真値との最大差分 = 0.03531
N =     5 : 真値との最大差分 = 0.01077
N =    15 : 真値との最大差分 = 0.00037

矩形波・のこぎり波

矩形波は最初から一貫して0.179に近い値になっており、対して、のこぎり波は最初は小さめの値からスタートしていますが、Nを増やすにつれて0.087→0.159→0.174→0.178と徐々に値が大きくなり、最終的には矩形波と同じ約0.179に近づいていきます。

見た目が全く違う2つの波形であるのに、最終的にたどり着くオーバーシュートがほぼ一致しており、オーバーシュートが波形の形そのものではなく、「段差がどのくらいの大きさか」だけで決まる、という性質を示しています。

今回はどちらの波形も段差の大きさが同じですので、同じ値に収束しています。

三角波

三角波は段差(飛び跳ね)がない波形ですので、状況が異なります。

Nを増やすほど誤差は小さくなっていき(0.077→0.035→0.011→0.0004)、オーバーシュートのような一定値への張り付きは見られません。

理論値との比較

# ジャンプの大きさが 2 の不連続点における理論上のオーバーシュート:
#   (2/pi * Si(pi) - 1) * ジャンプ幅 ≈ 0.0895 * 2 = 0.1790
gibbs_theoretical <- function(jump_size) {
  si_pi <- integrate(function(t) sin(t) / t, 0.0001, pi)$value
  (2 / pi * si_pi - 1) * (jump_size / 2) # ← ジャンプ幅を2で正規化
}

cat(sprintf(
  "理論上のオーバーシュート量(ジャンプ幅=2の場合): %.4f\n",
  gibbs_theoretical(2)
))
理論上のオーバーシュート量(ジャンプ幅=2の場合): 0.1789

「段差の大きさが2のとき、オーバーシュート量は理論的に約0.179になる」という理論値と、実際にシミュレーションで観察された値との整合性が確認できました。

以上です。