メインコンテンツに移動

常微分方程式II

2階常微分方程式\[\frac{\text{d}^2 y}{\text{d}x^2}=-Cy\]の場合,解を一意に定めるには条件が2つ必要です.

初期値(Initial Condition)問題

\(x=0\)において, 初期条件\[ y(0)=1,\quad\frac{\text{d}y}{\text{d}x}=0\]が与えられている場合は, どうしましょうか. この場合, \[z=\frac{\text{d}y}{\text{d}x}\]とおいて, 連立常微分方程式\[\frac{\text{d}y}{\text{d}x}=z,\quad \frac{\text{d}z}{\text{d}x}=-Cy\]を, 例えば初期値\[y(0)=1,\quad z(0)=0\]で解けばよろしい. 解を\(0\leq x\leq 2\pi\)で求めることにしましょう.まずは仕様書の策定ですが,メインプログラムがこんな感じでどうかな:

#include <iostream>
#include <filesystem>
//#include "ic_solver.hpp"      //ここにソルバークラスIC_Solverを書くつもり
int main(){
    std::filesystem::path myPath=getenv("HOME");
    myPath/=""Data/test02";              //データを入れるフォルダーを HOME/Data/test02 とするつもり
    std::cout << "Using Folder[" << myPath << "]" << std::endl;
    //IC_Solver ode(myPath);                      //2階常微分方程式ソルバーインスタンス
    //ode.N=100;                                  //100分割
    //ode.solve();                    //解く!
    return 0;
}

 ここいら辺はなにかというと,前回学習した「フォルダー」を操作するためのものです. std::filesyastem::path ってのは「フォルダー名」を保存できる変数です. で,前回学習した「ホームフォルダ」を取得しています.で,ここいら辺では,【HOME/Data/test02】フォルダーにデータを入れるつもりになっています.

ここの /= ってなに? プログラムでは A=A+1 とか, A=A/2 とかが頻繁に出現します.こいつらは,A+=1  とか A/=2 とか,略記できるのです.

じゃ,これ,フォルダー名 myPath を "Data/test02" という文字で割り算するの?そこは,ほれ,ファイル名ですので

      /Users/myName を  Data/test02 で「割る」と  /Users/myName/Data/test02 になるのです.

ところが,これでビルドしてみると"no member named filesystem....." とか,エラーします.これはC++の進歩と関係してます.

プログラム言語の進歩について

どのプログラミング言語も, 時代についれて変化していきます.時に巨大な変化が起こります.

  • Fortran言語は, Fortran77が20-30年使われたのち, 2000年ごろに大幅に異なるFortran90に移行しました.今はFortran2018だそうな
  • C++言語は, C++98が15年ほど使われたのち,2014年ごろに大変更されたC++11に移行しました.2024年くらいまでは,大幅な変更はなさそう?です.
    • C++98 Fortran竜やperlザウルスが跳梁闊歩し, 人類はまだ存在していなかった.
    • C++03 暗黒時代. 全てのものを手で書く原始人の中で, Boost教団が光をもたらそうとしていた.
    • C++11 黎明期.人間にもプログラムを書くことが可能になった
    • C++14 普通の人がプログラムを書けるようになった. というかJavaScriptみたいになってきた
    • C++17 そういやこういうのもあったな
    • C++20 ついに平和が訪れた.が, ダック型やJSONが根本を覆そうとしていることに,多くの人は未だ気づいていなかった

 で,C++14までは,フォルダーを取り扱うにはBoost教団のライブラリーを使う必要があった. C++20で,ようやく普通な感じになりました.

で,黙っておくと,C++98でビルドしようとするんですよね・・・

一度再生ボタンを押してエラーが起きたのちには, そのフォルダーに .vscode/tasks.json というファイルができてるので編集し, -std=c++20  (cは小文字)を入れます:

これは要するに,cppbuildという作業が発生した時,コンパイラを

/usr/bin/g++ -fdiagnostics-color=always -g -std=c++20 ーWall -Wall ふにゃ.cpp -o ./ふにゃ

と起動せよ,と指示しているのです(再生ボタンは, cppbuild と cppdbg という作業を連発させている).これでビルドはできるが,VSCodeのエディターが赤い波線を示して困る時がある:

VSCodeは,編集中の文法チェックを Microsoft-IntelliSense ってのでやっているのだが,そいつがC++20になってないと,エラーを出してくる.せっかくなので設定してしまおう.

【拡張機能】【C++】【設定】で 検索窓に Standard とでも入力すれば

すると赤線が消えて,

動きますね!

初期値問題(つづき〜)

要求仕様に基づいて, IC_Solverクラスを実装しましょう.まずは概形を作成

#include <iostream>
#include <filesystem>
class IC_Solver { public: IC_Solver(){ } };

さて,要求仕様では

IC_Solverは,データフォルダー名を受け取って初期化する

IC_Solver.N で分割数を指定しても良い. だまっておけば100分割する(いや,そこまで仕様書には書いてねえけど)

と書いてあるので, そのように書きます:

#include <iostream>
#include <filesystem>
class IC_Solver { public: int N; IC_Solver(std::filesystem::path path) :N(100){ if (!std::filesystem::exists(path)) std::filesystem::create_directories(path); std::cout << "IC_Solver::Using DataFolder[" << path.string() << "]" << std::endl; } IC_Solver(){ throw("何やっとんじゃぼけ〜"); } };

この辺は,まあ字面からわかると思いますが,そのフォルダーがなかったら(途中のフォルダーも含め)作成する.ここの.string()ってのは, フォルダー名を "フォルダー名" と表示するのではなく, フォルダー名, と表示するためのものです.

関数のオーバーロード  &  例外

それよりも,この辺が難解ですね.仕様書に従わないバカユーザーが出現し, IC_Solver ode とデータフォルダー名を与えなかった場合,このメッセージが出て作業放棄,と指定しているのです.ちゃんとしたプログラムは,エラーした時には,だまって間違えたまま作業継続するのではなく,例外的事象が発生したよ!死ねよ!と「例外を投げる」べきなのです.

いや,わからないのは,そこじゃなくて,同じ関数が IC_Solver(引数1) とIC_Solver(引数2) のように,複数ありそうに見えるところですね.これはオーバーロードと呼ばれる機能です.関数ってのは

    集合Aの値を一つ与えると  集合Bの値が得られる

なので,集合Aの種類によって区別ができるのです!例えば sin(1.0) だと実数のサイン関数が,  sin(1.0+2.0i) だと複素数のサイン関数が得られる,というふうに使います.

なお, 巨大なプログラムに成長した時, ある時に突然「なにやっとんじゃぼけ〜」とメッセージが出て止まってしまう・・・すると,どこが間違っているのか, 探し出すのが大変なので, メッセージとしては不適切です.例えば
    throw std::excepction("IC_Solver::IC_Solver()::なにやっとんじゃぼけ〜");
と書きましょう.これで, IC_Solverクラスの, コンストラクターIC_Solver()が文句を言っていることが明確になります.あるいは,「このクソやろうおとついきやがれタコ」とか,プログラムの他の場所で決して出現しないメッセージの場合, 検索すれば発見は容易ではあります.最悪なのは throw std::exception("Error."); ですね・・・該当箇所がいっぱいありすぎて,場所の特定は至難の業です. 

さて,このボケ仕様書には, Cの値を調整できるようにしとけ,とか,とくべき範囲はどのあたりか,とか , データファイルの名前を指定,とかが欠落しているので,安上がりな実装をしてしまいましょう. その方が儲かるしな (納品後に追加機能,は別料金が取れるし♪ 損するのは発注者,わしは知らん)

変数のスコープ

今まで int number; とか変数を宣言して使ってましたが,その変数には有効範囲があります.原理は簡単:

  • その宣言を含む { から  } までで有効である

例えば, 下のプログラムだと, 青字のエリアで, 青字のNUM や Xmax が有効です.コンストラクタで NUM=1024 に設定したら,どの関数でもNUMは1024, Xmaxは3.14になってます.例外は黄色部分で,内部で重ね書きでint Xmax=5としてしまったので,ここではdouble Xmaxは隠蔽されてしまい, Xmaxは整数の5になってます. Prepare()の後でSolve()を実行した場合,Prepare()でXmax=整数5にしたのですが,有効範囲が終了しているので,Solve()ではオリジナルのdouble Xmax=3.14が復活します!

class XXXX {
   int NUM;
   double Xmax;
   ....
   XXX(){
      NUM=1024;
    Xmax=3.14;
   }
   void Prepare(){
       int Xmax=5;
       ....
   }
   void Solve(){
   ....
   }
} 

青色の NUM や Xmaxを,クラスの中では(基本的に共通の)変数なので,クラスのメンバー変数といいます.黄色のところのXmaxは, 黄色のところだけで有効なローカル変数です.

  • 多くの場合,メンバー変数は「その値をずっと保持しないといけない」という制約がありますので,容量が大きいが速度が遅い「メインメモリー」に配置されます.なので,計算は遅くなります.
  • 多くの場合,ローカル変数は,「そこでちょっとだけ使う」だけなので,CPU内部の,超高速な「キャッシュメモリー」に配置されます.
  • というわけで,ローカル変数を上手に使えば,計算が高速化します.

この「変数の有効範囲」を,変数のスコープと呼びます.

スコープを考慮して, 必要な変数を配置してみましょう.

#include <iostream>
#include <filesystem>   //フォルダー操作
#include <fstream>      //ファイル入出力
#include <cmath>        //数学関連
class IC_Solver { double C=1.0; double Xmin=0.0; double Xmax=2*M_PI;
double Ymin=1.0;
double Zmin=0.0; std::filesystem::path DataFile; //データを保存するファイル名 public: int N; IC_Solver(std::filesystem::path path) :N(100){ if (!std::filesystem::exists(path)) std::filesystem::create_directories(path);     DataFile=path/"tako_hati.dat"; //指定されたフォルダーのtako_hati.datというファイルに,結果を保存 std::cout << "IC_Solver::Using DataFile[" << DataFile.string() << "]" << std::endl; } IC_Solver(){ throw("何やっとんじゃぼけ〜"); } void Solve(){ std::ofstream ofs(DataFile); //出力ファイルを書き出し用に準備 長くなるので下に書く ofs.close();            //出力ファイルをディスクに書き込む } };

この辺りは,便利な数学関数や定数を使えるようにする. M_PI=\(\pi\), M_E=\(e=2.718....\), M_SQRT2=\(\sqrt{2}\)とか,色々あります.この辺りは,データファイルに書き出すための準備です.この辺は,計算に必要な定数を定義しています.private変数なので,外部から操作できません.発注者が困って仕様変更を申し込んできたら,別料金をむしり取ってから, public: の下に移動させたら良いです.データファイルを, Solveの時に指定したいという要望があった場合は

    std::filesystem::path DataFolder;//メンバー変数追加
  ...
    IC_Solver(std::filesystem::path path):DataFolder(path){  //pathは黄色のところを出ると消えちゃうから
       ....
    }
    ...
    void Solve(std::string onamae){// onamaeのところに"OREnoFile.dat"とかを指定して呼び出せば良い
       DataFile=DataFolder/onamae;           //指定されたフォルダーのonamae文字列というファイルに,結果を保存  
       std::cout << "IC_Solver::Using DataFile[" << DataFile.string() << "]" << std::endl;
       std::ofstream ofs(DataFile);    //出力ファイルを書き出し用に準備
       長くなるので下に書く
       ofs.close();            //出力ファイルをディスクに書き込む
    }

この場合,呼び出し側のメインプログラムは

#include <iostream>
#include <filesystem>
#include "ic_solver.hpp"
int main(){
    std::filesystem::path myPath=getenv("HOME");
    myPath/=""Data/test02";             //データを入れるフォルダーを HOME/Data/test02 とするつもり
    IC_Solver ode(myPath);                      //2階常微分方程式ソルバーインスタンス
    ode.N=100;                                  //100分割
    ode.C=1.0;                                  //C=1に設定. XminやXmaxも設定できるけどデフォルトの値でも良い
    ode.Solve("C1N100.data");            //解いて, N100C1.data というファイルに出力
    return 0;
}

 Solveのところは,まあ前のページと同じですね.

...
void Solve(std::string filename){
  DataFile=path/filename;           //指定されたフォルダーのfilename文字列というファイルに,結果を保存
  std::cout << "IC_Solver::Using DataFile[" << DataFile.string() << "]" << std::endl;
  std::ofstream ofs(DataFile);      //出力ファイルを書き出し用に準備
  double dx=xmax/N;     //区間幅はこうなる
  double y=Ymin,z=Zmin; //初期値を設定
  for(int i=0;i<N;i++)
  {
    y=y+z*dx;
    z=z-C*y*dx;
    ofs << dx*(i+1) << " " << y << std::endl;
  }
  ofs.close();            //出力ファイルをディスクに書き込
}
...

結果はこんな感じか? (

my

これはGnuPlotUtilsで  graph -Tps --label-font-name Times-Italic -X x -Y "solution y" -C --line-width 0.001 C1N100.data |ps2pdf -dEPSCrop - > C1.pdf   と入力しました.

いや,長すぎるだろう普通に?と思う人は,draw.sh の利用を考慮しましょう.

  作業中の様子

数値計算というものは, 誤差がどれくらいであるのか?を知ることが必要です. じゃ,メインプログラムをこうしちゃえ

...
    ode.N=100;
    ode.Solve("C1N100.data");
    ode.N=10;
    ode.Solve("C1N10.data");
    ode.N=5;
    ode.Solve("C1N5.data");
    ode.N=3;
    ode.Solve("C1N3.data");
    return 0;
}

GnuPlotUtilsでは  graph 〜うだだうだだ〜だだだだ〜 C1N100.data C1N100.data C1N10.data C1N5.data C1N3.data | だだだ・・・   で複数グラフ描ける. だだ・・・っだだだが,嫌いな人はdraw.sh の利用を考慮しましょう.