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(); //出力ファイルをディスクに書き込 } ...
結果はこんな感じか? (
これは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 の利用を考慮しましょう.