メインコンテンツに移動

偏微分方程式II

法仏型方程式いやこりゃありがてえや

熱伝導方程式

熱伝導方程式(拡散方程式)\[\frac{\partial u}{\partial t}=\kappa\Delta u\tag{1}\]を, 初期値・境界条件つきで時間発展を追うことを考えましょう.

1Dケース

\[\frac{\partial u}{\partial t}=\kappa\dfrac{\partial^2\,u}{\partial x^2}\]ですね. ここでは\(0<x<1\)として, 初期値と境界条件を\[\lim_{t\to 0} u(t,x)=g(x),\qquad \lim_{x\to 0}u(t,x)=U_0,\qquad \lim_{x\to 1}u(t,x)=U_1\]としましょう. 普通に差分してみましょう. \(u_{n,i}=u(\Delta_t n,\Delta_x i)\)として

陽解法

\[\dfrac{u_{n+1,i}-u_{n,i}}{\Delta_{t}}=\kappa\dfrac{u_{n,i+1}-2u_{n,i}+u_{n,i-1}}{\Delta_{x}^{2}},\ \left(0<x_{i}<1\right)\]すなわち\[u_{n+1,i}=Du_{n,i+1}+\left(1-2D\right)u_{n,i}+Du_{n,i-1}\]\[D=\dfrac{\kappa\Delta_{t}}{\Delta_{x}^{2}}\tag{2}\]
 この\(D\)を, 拡散に関する拡散数と呼びます. 差分式をよくみると,\[D>\dfrac{1}{2}\tag{3}\]の場合,係数が負のものが現れるので, 陽解法で係数が負は雰囲気やばそうです. 実際, Fourier解析すると, 安定解が保証できないことを確かめることができるでしょう.いや,ここはプログラム教室だから,プログラムを組んで確かめるべきなのか・・・

陰解法

陽解法では1次元しか解けないので, 2次元問題を行う前に, 陰解法も練習しましょう. \[-Du_{n+1,i-1}+(1+2D)u_{n+1,i}-Du_{n+1,i+1}=u_{n,i}\]ですね.境界条件と一緒に行列で書くと,\[\def\vp{\phantom{\sum_{1}^{2}}}
\begin{bmatrix}1 & 0 &  &  &  & \vp\\-D & 1+2D & -D &  &  & \vp\\& -D & 1+2D & -D &  & \vp\\&  & \ddots & \ddots & \ddots & \vp\\&  &  & -D & 1+2D & -D\\&  &  &  & 0 & 1\end{bmatrix}\begin{bmatrix}u_{n+1,0}\vp\\u_{n+1,1}\vp\\u_{n+1,2}\vp\\\vdots\vp\\u_{n+1,N-1}\\u_{n+1,N}\end{bmatrix}=\begin{bmatrix}U_{0}\vp\\
u_{n,1}\vp\\u_{n,2}\vp\\\vdots\vp\\u_{n,N-1}\\U_{1}\end{bmatrix}\tag{4}\]こういう三重対角行列はここで練習しましたね!

ここでは,より一般な,「Sparse Matrix」として解いてみましょう.

SPARSE Matrix

疎行列というのは, まばらにしか非ゼロ要素が存在しない行列です.

sparse

まあでも, 数値シミュレーションに出現する疎行列では,対角要素はゼロではない,という条件がつくのが多いです.疎行列の困ったちゃんなところは, 「疎行列を解こうとして未知数を消去(つまりLU分解)していったら,密行列になっちゃうかもよ」という点です.

\[\begin{bmatrix}\ 1\ & -1 & -1 & -1 & -1 & -1\\ 1 & 1\\ 1 & & \text{1}\\ 1 & & & 1\\ 1 & & & & 1\\ 1 & & & & & 1 \end{bmatrix}\to\begin{bmatrix}\ 1\ & -1 & -1 & -1 & -1 & -1\\ 0 & 2 & 1 & 1 & 1 & 1\\ 0 & 1 & 2 & 1 & 1 & 1\\ 0 & 1 & 1 & 2 & 1 & 1\\ 0 & 1 & 1 & 1 & 2 & 1\\ 0 & 1 & 1 & 1 & 1 & 2 \end{bmatrix}\]

疎行列が溢れた・・・このままじゃ俺の計算は全滅じゃあ〜〜 
落ち着いてミト!あれは・・・Eigenに向かって飛べ!     

最初の疎行列ならメモリーに格納できたが,解こうとして右辺の行列になったら密行列で,計算機がダウン!というわけです.疎行列自身の要素数(=計算機パワー)は十分小さくても,連立方程式を解こうとしてLU分解を始めると,要素数が爆発して計算機パワーを凌駕してしまうわけね.

しかし,まあ数値シミュレーションに出現する疎行列では,ブロック状に0行列が出現することが多い:

sa

この場合,理屈で言って「ここゼロだし」ということが分かることも多いわけで,そこを回避するようにプログラムを組むわけです.上の例だと, A1-B1ブロックとC4-A4ブロックの計算では,オレンジ部分はずっと0のままである.だが,それは,問題によってプログラムが特注品になる,ということでもあります.だから,頑張ってね.ただし,

なので,使ってみましょう!

基底クラス

まずは基底クラスですよね!有界領域で空間1Dなので, こんな感じかな. ほとんどpSolver1DBase.hpp と同じだが

// bSolver1DBase.hpp    Solve some equation using some method. Bounded region
#pragma once
#include <iostream>
#include <filesystem>
#include <cmath>
#include <vector>
#include <fstream>
#include "../include/pvector.hpp"
template <typename T>
class bSolver1DBase
{
protected:
   double C = 1.0, K=1.0; // Cは速度、Kは拡散係数
   double CFL, DN; // CFL条件と拡散数
   double dt, dx, time, xmax = 1.0;
   int nt = 0; // 時間ステップのカウンタ
   double LBC_value = 0.0; // 下端の境界条件の値
   double RBC_value = 0.0; // 上端の境界条件の値
   class Point
   {
   public:
       Point *pre = nullptr; // ポインタを追加して前のポイントへのリンクを作成
       Point *next = nullptr;
       double X;
       pvector<T> U;     // U_n
       Point() : U(3) {} // デフォルトコンストラクタでサイズ3のpvectorを初期化
       auto &operator[](int index) { return U[index]; }
       const auto &operator[](int index) const { return U[index]; }
   };
   std::vector<Point> Data;
   std::ofstream ofs;
public:
   bSolver1DBase(const size_t &nx, const double &dt, const std::function<double(double)> &U0) : dt(dt)
   {
       time = 0.0;         // 初期時間
       dx = xmax / nx;     // 空間刻み幅の計算
       CFL = C * dt / dx;  // CFL条件の計算
       DN = C * dt /dx/dx; // 拡散数の計算
       Data.resize(nx+1);  // 空間分割数に基づいて初期化
       for (size_t i = 0; auto &P : Data)
       {
           P.pre = &Data[i - 1];
           P.next = &Data[i + 1];
           P[nt] = P[nt - 1] = P[nt - 2] = U0(P.X = i * dx); // 初期条件の適用
           i++;
       }
       std::cout << "Booting bSolver1DBase with nx=" << Data.size() << ", dt=" << dt << std::endl;
   }
   std::string what() const
   {
       return "nSolver1DBase: nx=" + std::to_string(Data.size()) + ", dt=" + std::to_string(dt) +
              ", CFL=" + std::to_string(CFL) + " , DN=" + std::to_string(DN)  + ", time=" + std::to_string(time);
   }
   double& LBC() { return LBC_value; } // 下端の境界条件
   double& RBC() { return RBC_value; } // 上端の境界条件
   bool Write(const std::string &filename)
   {
       if (ofs) ofs.close(); // 既存のファイルストリームを閉じる
       std::filesystem::path datafile = getenv("HOME");
       datafile /= filename;
       std::filesystem::create_directories(datafile.parent_path());
       ofs.open(datafile);
       if (!ofs) throw std::ios_base::failure("Failed to open file: " + datafile.string());
       return Write();
   }
   bool Write()
   {
       try
       {
           ofs << std::endl
               << "# nt= " << nt << " time= " << time << " CFL= " << CFL << " NX= " << Data.size() << std::endl;
           for (auto &v : Data)
               ofs << v.X << " " << v[nt] << std::endl;
       }
       catch (const std::exception &e)
       {
           std::cerr << "Error writing to file: " << e.what() << std::endl;
           return false;
       }
       std::cout << "time[" << time << "]written to file successfully." << std::endl;
       return true;
   }
   virtual void Step(double U_W = 0.0) = 0;
   virtual void Initialize(void *parm = nullptr) {};
   double get_time() const { return time; }
};

まあ名前が違うところ以外は, ここいらで拡散係数などを定義.ここはフツーのstd::vectorで, でも格子点が0,1,...,NX+1 であると, 区間幅は0,1,...,NX になるから, ここの数が1個違うだけ. 人によっては,pSolver1DBase と bSolver1DBase の共通のご先祖様  Solver1DBase から派生した方がいいと思うかもだな.

あ,ここは!先ほど別のプログラムで作成した pvector.hpp ですね.こういう,よく使う汎用プログラムは,共通のフォルダーを作って,そこに入れておくのが良いです.

include/ というフォルダーが多いかな? lib/ もありだが. 

で, この辺は,有界領域の問題なので, 上下で境界条件を設定したくなるかもなあ.というものです.このあたりは, まあ今は使いませんが,のちのち, コンストラクタとStep()の間に作業が入るようになるかもしれないので,念の為に作ってあります.

 
Explicit

基底クラスがよくできてれば,派生クラスは簡単です

// heat1D_explicit.hpp
#pragma once
#include "bSolver1DBase.hpp"
class Heat1DExplicit : public bSolver1DBase<double>
{
public:
   Heat1DExplicit(const size_t &nx, const double &dt, const std::function<double(double)> &U0 = [](double x)
                  { return 0.; }) : bSolver1DBase<double>(nx, dt, U0)
   {
       std::cout << "Starting Heat1DExplicit solver with nx=" << Data.size() << ", dt=" << dt << std::endl;
   }
   void Step() override
   {
       Data[0][nt + 1] = LBC_value; // 下端の境界条件
       Data[Data.size() - 1][nt + 1] = RBC_value; // 上端の境界条件
       for (size_t i = 1; i < Data.size() - 1; ++i)
           Data[i][nt + 1] = (1.-2.*DN)* Data[i][nt] + DN * (Data[i - 1][nt] + Data[i + 1][nt]);
       time += dt;
       nt++;
   }
};
// heat1D_explicit.hpp

(2)式を書いただけ,ですね.これでmain()は

// main.cpp
#include "heat1D_explicit.hpp"
int main()
{
   size_t nx = 20;          // 空間分割数 100(text), 40, 20, 8
   double dt = 0.1 / nx /nx;    // 時間刻み幅
   size_t ntmax = 5. / dt;  // 最大時間ステップ数
   size_t ntsave = .005 / dt; // データ保存間隔
   std::string filename = "Data/test04/CDE_20.dat";
   Heat1DExplicit mySolver(nx, dt);
   mySolver.LBC() = 1.0; // 下端の境界条件
   mySolver.Initialize();
   mySolver.Write(filename);
   for (size_t nt = 0; nt < ntmax; nt++)
   {
       mySolver.Step(); // 時間ステップを実行
       if ((nt + 1) % ntsave == 0)
           mySolver.Write();
   }
   return 0;
}

ここは(3)にならないように書きました!境界条件を左が1に設定細かめにデータ保存したら, 左みたいになっちまったorz (一応収束していることは,わかる.でも,こんな図を課長に見せたら「最後どうなってんのか見えねえぞ,帰れ」となってしまう)ので,ここいらへんを改造して右図みたいにしましたとさ. 

   

改造はこんな感じにしてみた:

#include <set>
   ...
   std::set<int> tset;
   tset.insert(round(0.005/dt));tset.insert(round(0.010/dt));tset.insert(round(0.020/dt));
   tset.insert(round(0.050/dt));tset.insert(round(0.10/dt));tset.insert(round(0.20/dt));
   tset.insert(round(0.50/dt));tset.insert(round(1/dt));tset.insert(round(2/dt));
   tset.insert(round(5/dt));
   ...
   for (size_t nt = 0; nt < ntmax; nt++)
       ...
       if (tset.contains(nt+1))
           mySolver.Write();

なんで整数にしてるんだ? という点ですが, 最初double でやったら,2とかが1.999999999999999 になって, 「2.00000000000000001じゃないから保存しないもん」になったからです.このように, double 数値を A=B とか A>B とかで比較すると,たまにハマる.というのは鉄則なので, 覚えておくと良いです.

陽解法は1Dだと解けますね!でもnt=20000までかかっています.

 
Eigen/SPARSEの 使い方

Eigen/SPARSEの使い方をimplicitスキームの構築で学びましょう.   (注意: EigenのVersion 3.4.0を利用してくださいね. EigenのVersionの確認方法は, 猿に聞け)

// heat1D_implicit.hpp
#pragma once
#include "bSolver1DBase.hpp"
#include <Eigen/Sparse>
class Heat1DImplicit : public bSolver1DBase<double>
{
   Eigen::SparseLU<Eigen::SparseMatrix<double>> solver;
public:
   Heat1DImplicit(const size_t &nx, const double &dt, const std::function<double(double)> &U0 = [](double x)
                                                      { return 0.; }) : bSolver1DBase<double>(nx, dt, U0)
   {
       std::cout << "Starting Heat1DImplicit solver with nx=" << Data.size() << ", dt=" << dt << std::endl;
   }
   void Initialize(void *parm = nullptr) override
   {
Eigen::SparseMatrix<double> coef; // スパース行列を使用して係数行列を表現
coef.resize(Data.size(), Data.size());
for (int i = 0; i < Data.size(); i++) { if ((i==0)||(i==Data.size()-1)) // 境界条件の処理 coef.insert(i,i) = 1; else { coef.insert(i, i - 1) = -(DN + 0.5 * CFL); // 左隣の成分
coef.insert(i, i) = 1. + 2. * DN; // 対角成分だにゃ
coef.insert(i, i + 1) = -(DN - 0.5 * CFL); // 右隣の成分
}
} coef.makeCompressed(); // スパース行列を圧縮
solver.compute(coef);
if (solver.info()!=Eigen::Success)
throw std::runtime_error("Error::Heat1DImplicit::Initialize::Can not solve fuck.");
std::cout << "Heat1DImplicit solver initialized with nx=" << Data.size() << ", dt=" << dt << std::endl; } // 時間ステップを実行するメソッド void Step() override {
Eigen::VectorXd b(Data.size());
for (int i = 0; i < Data.size(); i++)
if (i == 0) b[i] = LBC_value; // 下端の境界条件
else if (i == Data.size() - 1) b[i] = RBC_value; // 上端の境界条件
else b[i] = Data[i][nt]; // 現在の値を設定
auto result=solver.solve(b); // 連立方程式を解く time = (++nt) * dt;
for (int i = 0; i < Data.size(); i++) Data[i][nt] = result[i]; } }; // heat1D_implicit.hpp

これでEigen/Sparseを読み込みます. まずはLU分解を入れておくところを用意. で, Initialize()で,(4)の行列を作成します.まずはこうやってSparse行列と, そのサイズを設定します.

ここでは1行目とN行目の対角成分を1に設定. この見慣れない||は, 二つの条件の論理OR(どちらかがtrueならtrue)を意味しています. なお, AND(どっちもtrueならtrue)は && です.

なんで | と & ではないのか?試しに書いてみると動くんだが?という向きのために説明しておくと, | は bit-OR  つまり二進数で 0101 と 1001 なら 1101 になる,という計算になってしまいます.&も bit-ANDで, 0101 & 1001 = 0001 になります.で,たまに論理OR, 論理ANDと結果が一致していると,動いている気がする,というわけです.この説明がわからん,という人は,黙って ||   と && と書いてくださいね!

この辺では, i行目の成分として, i-1列目, i列目, i+1列目を設定します.Eigen/Sparseは, こうやって「値を設定」したあと, このmakeCompressedで計算機メモリーを節約します.さて,設定した行列をLU分解しておけば計算が速くなります.ここで必ずやっておきましょう!で, LU分解はいつでも可能ではなく,失敗する場合がありますので,こうやって失敗を検出しておきましょう.

さて, 時間ステップ Step() では, 計算を行います.
では連立方程式\(\mathsf{A}\boldsymbol{x}=\boldsymbol{b}\) \(\mathbf{b}\)ベクトルを設定します.で,これで解けるので, 回収しておきましょう

ここらへんで解いている最中には,計算機の使用メモリーが上昇します.どれくらい上昇するかは行列に依存しますが,まあ数倍くらいで済むことが多い感じです.

 陰解法なので, 時間ステップをいくらでも大きくとれる. この例ではnt=20でt=5まで到達.

 

 

移流拡散方程式

移流項を追加した拡散方程式を, 移流拡散方程式と呼びます.

境界値問題

1Dだと\[\dfrac{\partial u}{\partial t}+c\dfrac{\partial u}{\partial x}=\kappa\dfrac{\partial^2\,u}{\partial x^2}\quad(0<x<1)\qquad u(t,0)=1\qquad u(t,1)=0\tag{5}\]

まあ通例は定常状態に大きな興味がある. というわけで時間微分は1次精度にしといてImplicitで差分化すると, \[-\left(D+\dfrac{C}{2}\right)u_{n,i-1}+(1+2D)u_{n,i}-\left(D-\dfrac{C}{2}\right)u_{n,i+1}=u_{n-1,i}\] 今やこれは簡単ですね!なお定常状態の解は\[\dfrac{c}{\kappa}\dfrac{\partial u}{\partial x}=\dfrac{\partial^2\,u}{\partial x^2}\ \to\ u(\infty,x)=\dfrac{e^{\omega x}-e^\omega}{1-e^\omega},\ \omega=\dfrac{c}{\kappa}\]ですね.

まあ定常解で\(\kappa\)を色々に変えると, こんな感じの結果が出るはず:

おえかき プログラム

こんな感じ:

#!/bin/bash
#   fig.sh: 偏微分方程式II: 拡散方程式
. draw.sh
. standards.sh
COLOR=1
X_AXIS='\fIX\sb\fR1\eb'
X_RANGE='0 1 0.25'
Y_AXIS='\fIu'
Y_RANGE='-0.1 1.4 0.5'
OPT="--pen-colors=1=DarkRed:2=MediumSeaGreen:3=Blue:4=Magenta:5=Khaki --symbol-line-width 0.0020"
#(c,κ)の解析解のデータが,ぽっちい〜
echo -e "1 1\n1 1e-1\n1 1e-2\n"|gawk '{
NX=100; dx=1/NX; omega=$1/$2;
for(i=0;i<NX+1;i++)
{
   X=i*dx;
   Y=(exp(omega*(X-1))-1)/(exp(-omega)-1);# exp(omega)なんて書いたらオーバーフローするき!
   printf("%f %f\n",X,Y);
}
printf("\n");
}'  >   $TMP/steady
#凡例
cat > $TMP/legend.dat <<@
#m=Solid:Black,S=Omit
0.05 1.3    l:implicit finite difference
0.05 1.15   l:\fIc\fR\r8=\r81, \fI\*D\sbt\eb\fR\r8=\r80.25

0.25    0.825       lb:\*k\r8=\r81
0.75 0.95 rt:\*k\r8=\r80.1
0.98 0.75 0.8 0.7 r:\*k\r8=\r810\sp-2\ep
#m=Dot:Black,S=Omit 0.01 0.5 0.10 0.5 l:\fIN\sbx\eb\fR\r8=\r820, CFL\r8=\r85
#m=Solid:Green,S=Omit 0.01 0.375 0.10 0.375 l:\fIN\sbx\eb\fR\r8=\r840, CFL\r8=\r810
#m=Solid:Magenta,S=Omit 0.01 0.25 0.10 0.25 l:\fIN\sbx\eb\fR\r8=\r880, CFL\r8=\r820 @ cat>>$TMP/steady<<@ 0.01 0.125 0.10 0.125 l:theory @ LEGEND=$TMP/legend.dat LEGEND_OPT="--line-width 0.003" cat>$TMP/nls<<@ $TMP/steady Solid:Cyan:0.007 Omit ADi_20_K1.dat.last Dot:Black:0.003 Omit ADi_20_K1e-1.dat.last Dot:Black:0.003 Omit ADi_20_K1e-2.dat.last Dot:Black:0.003 Omit ADi_40_K1e-2.dat.last Solid:Green:0.006 Omit ADi_80_K1e-2.dat.last Solid:Magenta:0.003 Omit @ NAMAE=( `cat $TMP/nls|grep -v ^#|gawk '{printf("%s ",$1);}'` ) LINE=( `cat $TMP/nls|grep -v ^#|gawk '{printf("%s ",$2);}'` ) SYMBOL=( `cat $TMP/nls|grep -v ^#|gawk '{printf("%s ",$3);}'` ) draw ADi.pdf ${NAMAE[@]}

計算プログラムよりも,こっちがむしろ大変だなwww

周期解

周期解を解いてみましょう. 具体的には, この辺と同じ初期値で解を探してみましょう. 移流部分が入っているので, 時間精度を2次にしてみた:\[\dfrac{u_{n+1,i}-u_{n,i}}{\Delta_{t}}+\dfrac{c}{2}\left(\dfrac{u_{n+1,i+1}-u_{n+1,i-1}}{2\Delta_{x}}+\dfrac{u_{n,i+1}-u_{n,i-1}}{2\Delta_{x}}\right)=\dfrac{\kappa}{2}\left(\dfrac{u_{n+1,i+1}-2u_{n+1,i}+u_{n+1,i-1}}{\Delta_{x}^{2}}+\dfrac{u_{n,i+1}-2u_{n,i}+u_{n,i-1}}{\Delta_{x}^{2}}\right)\]時間微分のところ,同じやないけ?と思う読者のために説明しておくと, これは, \((t,x)=\left(\left(n+\dfrac{1}{2}\right)\Delta_t, i\Delta_x\right)\)という,中途半端なところまわりにTaylor展開して2次精度にしているんですね!で, 解くべき行列は, 例えば拡散方程式の(4)なら,行列の一番上(\(x=0\)の境界条件)を周期条件に書き換えたら良い. 一番下は,では, \(x=0=1\)における方程式に置き換えるのかな?\[\def\vp{\vphantom{\sum_{1}^{2}}}
\begin{bmatrix}
1 & 0 &  &  &\vp&-1\\
-(D+C/2) &     2(1+D) & -(D-C/2) &                &             & \vp\\
                & -(D+C/2) &    2(1+D) & -(D-C/2) &             & \vp\\
               &                  & \ddots    & \ddots     & \ddots & \vp\\
\vp          &                 &                & -(D+C/2)  & 2(1+D)& -(D-C/2)\\
\color{magenta}{2(1+D)}&\color{magenta}{-(D-C/2)}&  &  &\color{magenta}{-(D+C/2)}& \vp
\end{bmatrix}
\begin{bmatrix}
u_{n+1,0}\vp\\
u_{n+1,1}\vp\\
u_{n+1,2}\vp\\
\vdots\vp\\
u_{n+1,N-1}\\
u_{n+1,N}
\end{bmatrix}\]
\[=
\begin{bmatrix}
0\vp\\
(D+C/2)u_{n,0}    +2(1-D)u_{n,1}       +(D+C/2)u_{n,2}\vp\\
(D+C/2)u_{n,1}     +2(1-D)u_{n,2}      +(D+C/2)u_{n,3}\vp\\
\vdots\vp\\
(D+C/2)u_{n,N-2}+2(1-D)u_{n,N-1}  +(D+C/2)u_{n,0}\vp\\
(D+C/2)u_{n,N-1}+2(1-D)u_{n,0}     +(D+C/2)u_{n,1}\vp
\end{bmatrix}
\]これは,まずい. 行列の基本変形で, 1行目を\(-(D-C/2)\)倍したのと, 残りの行の総和を計算すると, 全部0の行になります!\[\def\vp{\vphantom{\sum_{1}^{2}}}
\begin{bmatrix}
1                               & 0                               &                               &                               &\vp                           &-1\\
-(D+C/2)                  & 2(1+D)                     & -(D-C/2)                &                               &                               & \vp\\
                                 & -(D+C/2)                 & 2(1+D)                   & -(D-C/2)                &                               & \vp\\
                                 &                                 & \ddots                   & \ddots                    & \ddots                   & \vp\\
                                 &                                 &                               & -(D+C/2)                & 2(1+D)                   & -(D-C/2)\\
\color{magenta}{0} &\color{magenta}{0} &\color{magenta}{0}&\color{magenta}{0}&\color{magenta}{0}&\color{magenta}{0} \vp
\end{bmatrix}
\begin{bmatrix}u_{n+1,0}\vp\\u_{n+1,1}\vp\\u_{n+1,2}\vp\\\vdots\vp\\u_{n+1,N-1}\\u_{n+1,N}\end{bmatrix}\]
ってこた,例えば一番下に追加した「\(x=0=1\)における方程式」は, 他の行と独立ではないので,この行列は行列式が0になってます.

  • こういう状況で, LU分解とかの系統で数値解析すると,誤差が非常に大きいとか,「行列式0ぢゃん」とライブラリーが文句を言ってきて,解けない
  • こういう状況で,Jacobiとかの逐次近似系で数値解析すると,解が確定しないので,ずるずると解が平行移動してずれる現象が起こる.
    • だってほれ,正則ではない(=特異な)行列はゼロ空間があるので, とあるベクトル\(\boldsymbol{Y}\)があって, \[(\mathsf{E-A})\boldsymbol{Y}=\boldsymbol{Y}\]になるので,Jacobi法だったら\[\boldsymbol{x}^{(n)}+\varepsilon\boldsymbol{Y}=\left(\mathsf{E-A}\right)\left(\boldsymbol{x}^{(n)}+\varepsilon\boldsymbol{Y}\right)-\boldsymbol{b}\]で\(\varepsilon\)が確定しないんっすよ.

ですが, こういう時には,だいたい「保存」する量があります.(5)を\(0<x<1\)で積分すると, \[\left(\dfrac{\mathrm{d}}{\mathrm{d}t}\int_0^1 u(t,x) \mathrm{d}x\right)
+c\left[u(t,x)\right]_{x=0}^1=\left(\dfrac{\mathrm{d}}{\mathrm{d}t}\int_0^1 u(t,x) \mathrm{d}x\right)=\kappa\left[\dfrac{\partial u}{\partial x}(t,x)\right]_{x=0}^1=0\]つまり\(u\)の総量は時間変化しない \[\dfrac{\mathrm{d}}{\mathrm{d}t}\left(\int_0^1 u(t,x) \mathrm{d}x\right)=0\]です.だから,一番下の行は,\(u\)の総量は時間変化しないということで, 総和が\[S=\sum_{i=0}^{N-1} u_{0,i}\]になるという式に置き換えましょう.さらに,一番上の式で\(u_{n+1,N}=u_{n+1,0}\)がわかってるんだから, 未知数減らそうかな?\[\def\vp{\vphantom{\sum_{1}^{2}}}
\begin{bmatrix}
-(D+C/2)                 & 2(1+D)                   & -(D-C/2)               &                               & \vp                            \\
                                & -(D+C/2)               & 2(1+D)                   & -(D-C/2)               & \vp                            \\
                                &                               & \ddots                   & \ddots                   & \ddots                 \vp\\
-(D-C/2)                 &\vp                          &                               & -(D+C/2)               &\vp 2(1+D)                 \\
\color{magenta}{1}&\color{magenta}{1}&\color{magenta}{1}&\color{magenta}{1}&\vp\color{magenta}{1}
\end{bmatrix}
\begin{bmatrix}u_{n+1,0}\vp\\u_{n+1,1}\vp\\u_{n+1,2}\vp\\\vdots\vp\\u_{n+1,N-1}\end{bmatrix}=
\begin{bmatrix}
(D+C/2)u_{n,0}    +2(1-D)u_{n,1}       +(D+C/2)u_{n,2}\vp\\
(D+C/2)u_{n,1}     +2(1-D)u_{n,2}      +(D+C/2)u_{n,3}\vp\\
\vdots\vp\\
(D+C/2)u_{n,N-2}+2(1-D)u_{n,N-1}  +(D+C/2)u_{n,0}\vp\\
S\end{bmatrix}
\]

で,やってみると

  

  • \(\kappa=0\)の移流方程式だと,なんか解が「ガタガタ」する
  • \(\kappa>0\)の移流拡散方程式では, \(\kappa\)が小さいと解の概要はそんなに変わらないが,上の「ガタガタ」が消える
  • \(\kappa\)が大きくなると,どんどん,ダラーっとなる
    • サンプルのように,\(\Delta_x\to 0\)で\(\kappa\to 0\)になるのなら,オリジナルの伝達方程式を解いていると考えても良い
    • こういう, (極限では消えてなくなる)拡散的な項によって「ガタガタ」を抑える方法を, 人工粘性というます.
    • まあでも,普通は離散化したとき,勝手に入ってくるナチュラルメークな感じのを人工粘性と呼ぶ.
    • この例のように,粘性足りないからって,わざわざ入れて,肌荒れを見えなくするってのは,なんかこう,ファンデーションたっぷりな,あざとい人工粘性と呼ぶべきかも
      • ファウンデーションに時間2次精度は贅沢なのでは?と思ったあなた,やってみましょう!ここが1次でよければ逆行列いらねえしな

こうして滑らかな解は得られるのですが,そこで大きな問題が生じます.

  • 滑らかだったら,正しいのか.振動する解は,常に誤りなのか.
  • 今回は厳密解がわかっているので,厳密解に近くなるように\(\kappa\)を選びました.だが,あなた方がこれから出会う問題は,「正解がわからない」「正解を出すのが自分の仕事」であるわけです.で・・・振動する解が誤りであると考えて,振動しなくなるまで粘性を入れる・・・というのが正しいのか?もしかすると,本当は振動するのが正解で,「いや,このまま実機を作ったら振動してヤバいっすよ」と設計部門に報告するべきであったところ,ファンデーションの厚塗りで振動しないように解を作り上げて,実機を作ると・・・墜落しますよね.
  • 正解がわからないのに正解を出さねばならぬ.そのためには,さまざまな\(\Delta_x\)などのパラメータで解の傾向を調べ,「事実は,この方角にあるらしい」と推定する.それが一番重要です.
  • 解の挙動を知っているから,そうやって解くという新解法も多い(未知の問題に対する挙動は不明ってわけだ).  騙されないようにしましょう.

さて,せっかく陰解法なのに\(\text{CFL}=0.8\)は小さいな?もっと大きくしてみましょう.すると

こうなってしまいます・・・陰解法は「CFLを大きくしても発散しない」んですが,「単に発散しないだけ. それ以上でも,それ以下でもない」なのです.今回のように,波が速度1で伝わる問題でCFLを大きくすると,\(\Delta_t\)由来の誤差が大きくなって,普通にうまくいきません.あれ?でも,この辺では,うまくいってたような?それは,そこでは定常解を求めたかったからですよ!

  • 問題に応じて,適切な解析手法を選択するのは,あなたの責任である.
  • 無敵なものって...ないと思う.

桜井法の結果も示しておこう. 人工粘性があれば,不連続を含むケースも, それらしい結果が得られる. あらかじめ正しい答えを知っている場合,それに合わせて\(\kappa\)を選ぶことはできそうである:

\(\kappa\)を変えてみた  \(N\)を変えてみた

まあしかし,大体の場合,あなたの研究では上の赤字のところが成立しない(成立している場合,研究する価値がない)ので,なかなか大変であるよね.

 CFLを変えてみた

熱伝導方程式

2Dケース

二次元の熱伝導方程式\[\frac{\partial u}{\partial t}=\kappa\,\left(\dfrac{\partial^2 u}{\partial x^2}+\dfrac{\partial^2 u}{\partial y^2}\right)\]を解いてみましょう.

この場合,格子データとして\[u^{(n)}_{i,j}=u(n\Delta_t, i\Delta_x, j\Delta_x)\]が必要になります.時間ステップ\(n\)については周期性配列を使うとしても, 空間座標に\(i, j\)の2つのインデックスがあるところが1D問題とは異なります.

こういうのには「多次元配列」を使うのですが,これにはいろいろな方法があります.

多次元配列

2次元ならEigen

2次元までしか使えませんが, EigenのMatrixを利用するとA(i,j)というデータが使えますですね. お手軽で経験もあり,という訳ですが,3次元以上になると,困り始めます.

多次元配列は神への冒涜である派

そもそもコンピュータのメモリーは1次元的に「アドレス」に並んでいるものである. だから, u[i,j]のような多次元配列はマヤカシでありSyntaxSugarであり, 原理に対する許し難い冒涜と感じる原理主義者もいるでしょう.すると,自分で\(u(i_0,i_1,...i_N)\)をU[j]という1次元配列に並び替えて使えば良い!長所は計算がBoost並みに早いこと,短所は原理主義者は通常嫌われることです.とはいえ, 実戦でこれを理解していないと何かと不便なので, あとで自作を試みましょう.

Boost/Array

プログラミンゴでは「多次元配列」のように,誰もが使いたくなるような仕組みがあります.毎回自作するのは時間の無駄な気がします.で,偉い人たちがすでに作成したライブラリーと呼ばれるプログラムの塊が何種類かあります.Boostというのは,C++の有名なライブラリーで,とりあえずなんでもできることになっています.多次元配列も含まれていてBoost/Arrayを用いると,自作しなくても多次元配列が(原理的には)利用できます.長所は自分で書かなくても良いしBoostだから計算が早い,短所は!マニュアルを見なければ使い方がわからない,で,マニュアルを読んでも「?!?!」となるため,いつまでも使えるようにならないところです.

そこで,まずは,ライブラリーの利用方法を次の章で学びましょう.