はじめに
拡散方程式とは拡散が生じている物質あるいは物理量の密度のゆらぎを記述する偏微分方程式のことです。
ちょっと何言ってるかわからないかもしれませんが、物体を熱が伝わる熱伝導の時間変化もこの方程式で表すことができます。
今回は例として1次元の熱伝導をシミュレーションしてみましょう。
拡散方程式
一般的な拡散方程式は以下の形で表されます。
ここでは拡散物質の密度、
は拡散係数です。
1次元の熱伝導方程式は上の式から以下のように導出できます。
ここでは温度、
は位置、
は熱拡散率を表します。
数値解析が行えるようにこれを差分化します。時間を、空間を
の長さの微小区間に等分して、上の式をテイラー展開してごにょごにょすると以下の式が得られます。
ここでは
での温度を表します。
式変形すると以下のようになります。
この時左辺は時刻での値、右辺は時刻
での値となるため現在時刻
での値を使うことで時刻
での値が求まるということを示しています。
こうして現在時刻の結果から未来の状態を予測する方法を陽解法といい、今回は陽解法をつかって解いてみます。
上の式より、境界条件との値を決めてあげれば簡単にシミュレーションができそうです。
安定化条件
陽解法で解く場合は時間刻みを大きくしていくと計算が早く終わりますが、大きくしすぎると誤差の影響により解が発散して解けなくなります。
上の式の安定性の条件は以下であることが知られています。
空間刻みを小さく設定した場合はそれに伴って時間刻みも小さくしないといけなくなり、計算時間が非常に長くなるということです。
実装
例題設定
例として以下のような問題を考えます。
長さの棒の片側を常に1度であっためて、逆側は断熱されていて熱の移動がないような状況を考えてみます。棒の初期温度は室温で0度としましょう。
は棒の材質等で決まりますが、今回は簡単のため適当に1としておきます。
まとめると以下のようになります。
ここで境界条件についてですが、ではどの時刻においても同じ値になるように設定しており、そういう境界条件をディリクレ条件といい、
では温度の勾配がどの時刻においてもゼロになるように設定しており、こちらはノイマン条件と呼びます。シミュレーション対象に応じて使い分けます。
実装
それでは実際に実装していきましょう。
クラスをいくつか作りました。それぞれの役割は以下です。
- Config: シミュレーションの設定値を管理する
- Geom: シミュレーション対象の定義
- BoundaryCondition: 境界条件の定義(オーバーライドしてカスタマイズできるように)
- InitialCondition: 初期値の定義(オーバーライドしてカスタマイズできるように)
- ExplicitSolver: 拡散方程式のソルバー
拡張性が高くなるように心がけました。
import dataclasses import matplotlib.pyplot as plt import numpy as np @dataclasses.dataclass class Config: """解析設定""" dx: float dt: float steps: int alpha: float = 1.0 class Geom: """ジオメトリ定義""" def __init__(self, length: float, dx: float): self.length = length self.dx = dx self.x = np.arange(0, length + dx, dx) self.N = len(self.x) class BoundaryCondition: """境界条件の抽象クラス""" def apply(self, u: np.ndarray) -> None: raise NotImplementedError class DirichletNeumannBC(BoundaryCondition): """左側ディリクレ条件、右側ノイマン条件の境界条件設定クラス""" def __init__(self, left: float = 0.0): self.left = left def apply(self, u: np.ndarray) -> None: u[0] = self.left u[-1] = u[-2] class InitialCondition: """初期値設定の抽象クラス""" def apply(self, x: np.ndarray) -> np.ndarray: raise NotImplementedError class UniformIC(InitialCondition): """すべて初期値を一定値に設定する""" def __init__(self, temp=0): self.temp = temp def apply(self, x: np.ndarray) -> np.ndarray: return x * 0 + self.temp class ExplicitSolver: def __init__( self, config: Config, geom: Geom, bc: BoundaryCondition, ic: InitialCondition ): self.cfg = config self.geom = geom self.bc = bc self.u = ic.apply(geom.x) self.bc.apply(self.u) self.r = config.alpha * config.dt / config.dx**2 if self.r > 0.5: print("Warning: 安定性に問題があります(r > 0.5)") def step(self): u = self.u r = self.r u_new = u.copy() u_new[1:-1] = u[1:-1] + r * (u[2:] - 2 * u[1:-1] + u[:-2]) # 境界条件を適用 self.bc.apply(u_new) self.u = u_new def run(self): for _ in range(self.cfg.steps): self.step() return self.u def plot(x: np.ndarray, u: np.ndarray, title="Solution"): fig, ax = plt.subplots(figsize=(6, 4)) ax.plot(x, u) ax.set_xlabel("x") ax.set_ylabel("u") ax.set_ylim(-0.2, 1.2) ax.set_title(title) plt.show() cfg = Config(dx=0.01, dt=0.00005, steps=5000, alpha=1.0) geom = Geom(length=1.0, dx=cfg.dx) bc = DirichletNeumannBC(left=1.0) ic = UniformIC(temp=0) solver = ExplicitSolver(cfg, geom, bc, ic) plot(geom.x, solver.u, title="Initial State") u = solver.run() plot(geom.x, u, title="Solution Steps: {}".format(cfg.steps))
結果は以下のようになります。
まずは初期状態。

2000ステップ目の状態。

の時はずっと温度は1度で、ステップが進むにつれて
の領域に熱が伝わっている様子がわかるかと思います。
の部分には断熱条件(ノイマン条件)を設定しているので微分がゼロになっていることがわかります。
ちゃんと実装できてそうですね。。
おわりに
今回は拡散方程式の数値解析について取り扱ってみました。2次元への拡張や陰解法等はおいおいやっていこうかと思います。