通用龍格庫塔Runge-Kutta方法求解常微分方程組初值問題的C++優雅實現
1. 算法簡介
a. 事情的起因
前一段時間在C++項目過程中,需要求解一個微分方程組,看了相關的數值分析教程(《數值分析》,歐陽潔等編著,北京:高等教育出版社,2009.9),又看了別人設計好的龍格庫塔程序,覺得寫得比較繁瑣,而且不夠通用。索性自己編寫一個,借鑑了C++標準庫中泛型函數的寫法,設計了一個比較通用的龍格庫塔算法。
b. 初值問題
常微分方程組初值問題是:
dy/dx = f(x,y), a <= x <= b.
y(a) = y0.
其中f(x,y)爲x,y的已知實值函數,y0爲給定的初始值。這裏,在控制系統中,x常用時間t表示,x是標量;若y和f(x,y)是標量,則上述初值問題是常微分方程的初值問題;若y和f(x,y)是向量,則上述初值問題是常微分方程組的初值問題.可以將常微分方程的初值問題看作是常微分方程組的初值問題的特例。初值問題更詳細的介紹請參考維基百科。
c. 龍格庫塔算法(方法)
是龍格庫塔算法(方法)求解常微分方程組初值問題的優秀方法,具有很高的求解精度。比如,四級四階經典Runge-Kutta公式的局部截斷誤差是O(h5),即步長的5次方。可以根據Runge-Kutta方法的思路推導出更精確的數值算法。這裏實現的Runge-Kutta算法特指四級四階經典Runge-Kutta公式。四級四階經典Runge-Kutta公式表示如下:
y(n+1) = y(n) + h * (K1 + 2*K2 + 2*K3 + K4) / 6,
K1 = f(x(n), y(n)),
K2 = f(x(n) + h/2, y(n) + h*K1/2),
K3 = f(x(n) + h/2, y(n) + h*K2/2),
K4 = f(x(n) + h, y(n) + h*K3)。
式子中的h表示步長。
2. C++實現
a. <valarray>標準庫簡介
<valarray>是C++ STL(標準模板庫)的一個組件,可以理解爲用於存儲要進行數值運算的數據的數組(value array,數值數組)。該模板類重載了<math>或者<math.h>中對數據進行運算的函數和四則運算等操作符。對valarray<type>對象的某個操作(運算)對施加到對象內的每個元素。因此,valarray<type>對象相當於一個向量。熟悉Matlab的讀者會很好理解該對象,因爲Matlab中的函數和運算符都可以用在向量上。比如,
std::valarray<double> x, y;
y = sin(x);
這種表達在Matlab中是很常見的,對於x向量的每個元素,求其正弦值,放在y中。使用了<valarray>之後,在C++中,也可以獲得這種形式上的簡介,省去了書寫和閱讀循環體的麻煩。
注意:在VC6.0版本中(其他版本VC未測試),引入<valarray>庫會導致錯誤,因爲在<windef.h>中定義了min和max宏,與<valarray>中的min和max函數有衝突。最簡單的解決方法是:在"stdafx.h"中,增加
#define MOMINMAX ,將該語句放在<stdafx.h>文件中所有的#include<*.h>之前。這種解決方法的原因可以從<windef.h>中看出來。以下直接拷貝<windef.h>中的相關部分。
#ifndef NOMINMAX
#ifndef max
#define
max(a,b)
(((a) > (b)) ? (a) : (b))
#endif
#ifndef min
#define
min(a,b)
(((a) < (b)) ? (a) : (b))
#endif
#endif / * NOMINMAX * /
所以定義了NOMINMAX後就不會再定義min和max宏了。
b. 函數對象
函數對象是對函數的抽象,功能上類似函數,但比函數功能更強大。C++ STL的定義和實現使用了函數對象,使用C++ STL中也會大量遇到函數對象。這裏使用函數對象來給Runge-Kutta算法函數傳遞微分方程組的右端項。讀者可以參考C++ Primer瞭解更多關於函數對象的概念。
c. 單步計算
這裏給出的Runge-Kutta算法函數只進行單步計算,即給出初值和步長,計算下一步的函數值。若想計算多步,需要使用循環體,在循環體中調用單步Runge-Kutta算法函數。
d. 額外參數
有時候,微分方程組右端項會含有一些與自變量x和因變量y無關的參數,所以這裏在實現Runge-Kutta算法函數時給出了重載版本,可以傳遞額外的參數。
3.附錄
附錄分爲三部分。在第一部分給出龍格庫塔算法的完整源代碼,在第二部分給出了該算法函數的使用示例,在第三部分給出了該示例程序調試輸出的結果。給出的示例基於MFC的對話框程序。在對話框界面上添加Button,設置其ID爲IDC_BUTTON_TEST_ODE,增加消息相應函數void CODEDlg::OnButtonTestOde()。在OnButtonTestOde()中給出了四種不同情況的使用示例,分別是:單變量常微分方程(右端項使用函數的形式傳遞),單變量常微分方程(右端項使用函數對象的形式傳遞),多變量微分方程組(普通版本),多變量微分方程組(重載版本,包含額外參數)。
a. 龍格庫塔算法的完整源代碼
// RungeKutta.h: interface for the
RungeKutta method.
//
//////////////////////////////////////////////////////////////////////
#ifndef RUNGE_KUTTA_H_H
#define RUNGE_KUTTA_H_H
#include <valarray>
//單步四級四階經典龍格庫塔算法
template <typename Func>
void ODERungeKuttaOneStep(double
dxn,
//x初值
const
std::valarray<double>&
dyn, //初值y(n)
double
dh,
//步長
Func
func,
//微分方程組右端項
std::valarray<double>&
dynext
//下一步的值y(n+1)
);
//單步四級四階經典龍格庫塔算法,重載版本, 含有額外參數
template <typename Func>
void ODERungeKuttaOneStep(double
dxn,
//x初值
const
std::valarray<double>&
dyn, //初值y(n)
double
dh,
//步長
Func
func,
//微分方程組右端項
std::valarray<double>&
dynext,
//下一步的值y(n+1)
const
std::valarray<double>&
para //額外參數
);
#include "RungeKutta.inl" //template function implementation
#endif
// RungeKutta.inl: implementation of the
RungeKutta method.
//
//////////////////////////////////////////////////////////////////////
//單步四級四階經典龍格庫塔算法
// 功能:求解常微分方程組初值問題的四級四階經典龍格庫塔算法,向前計算一個步長
// 輸入:輸入參數有:dxn, x的初值; dyn, 初值向量y(n); dh, 步長;
//
func, 微分方程組右端項
// 輸入輸出參數有: dynext,
最好初始化爲與dyn長度相等的序列
// 輸出:無
// 作者:張坤
// 時間:2013.06.13
template <typename Func>
void ODERungeKuttaOneStep(double
dxn,
//x初值
const
std::valarray<double>&
dyn, //初值y(n)
double
dh,
//步長
Func
func,
//微分方程組右端項
std::valarray<double>&
dynext
//下一步的值y(n+1)
)
{
size_t n = dyn.size();
//微分方程組中方程的個數,同時是初值y(n)和下一步值y(n+1)的長度
if (n != dynext.size())
{
dynext.resize(n,
0.0); //下一步的值y(n+1)與y(n)長度相等
}
std::valarray<double>
K1(0.0, n), K2(0.0, n), K3(0.0, n), K4(0.0, n);
func(dxn, dyn,
K1);
//求解K1
func(dxn+dh/2, dyn+dh/2*K1,
K2); //求解K2
func(dxn+dh/2, dyn+dh/2*K2,
K3); //求解K3
func(dxn+dh, dyn+dh*K3,
K4);
//求解K4
dynext = dyn + (K1 + K2 + K3 +
K4)*dh/6.0; //求解下一步的值y(n+1)
}
//單步四級四階經典龍格庫塔算法,重載版本, 含有額外參數
// 功能:求解常微分方程組初值問題的四級四階經典龍格庫塔算法,向前計算一個步長
// 輸入:輸入參數有:dxn, x的初值; dyn, 初值向量y(n); dh, 步長;
//
func, 微分方程組右端項; para, 微分方程組可能需要的額外參數
// 輸入輸出參數有: dynext,
最好初始化爲與dyn長度相等的序列
// 輸出:無
// 作者:張坤
// 時間:2013.06.13
template <typename Func>
void ODERungeKuttaOneStep(double
dxn,
//x初值
const
std::valarray<double>&
dyn, //初值y(n)
double
dh,
//步長
Func
func,
//微分方程組右端項
std::valarray<double>&
dynext,
//下一步的值y(n+1)
const
std::valarray<double>&
para //額外參數
)
{
size_t n = dyn.size();
//微分方程組中方程的個數,同時是初值y(n)和下一步值y(n+1)的長度
if (n != dynext.size())
{
dynext.resize(n,
0.0); //下一步的值y(n+1)與y(n)長度相等
}
std::valarray<double>
K1(0.0, n), K2(0.0, n), K3(0.0, n), K4(0.0, n);
func(dxn, dyn, K1,
para);
//求解K1
func(dxn+dh/2, dyn+dh/2*K1, K2,
para); //求解K2
func(dxn+dh/2, dyn+dh/2*K2, K3,
para); //求解K3
func(dxn+dh, dyn+dh*K3, K4,
para);
//求解K4
dynext = dyn + (K1 + K2 + K3 + K4)*dh/6.0;
//求解下一步的值y(n+1)
}
b.龍格庫塔算法的使用示例
void odefunc1(double dx, const
std::valarray<double>&
dyn,
std::valarray<double>&
fai)
{
fai[0] = dyn[0] - 2*dx/dyn[0];
// dy/dx = y - 2*x/y; 單變量微分方程
}
struct odefunc2
{
void operator()(double dx, const
std::valarray<double>&
dyn,
std::valarray<double>&
fai)
{
fai[0] = dyn[0] -
2*dx/dyn[0]; // dy/dx = y - 2*x/y; 單變量微分方程
}
}; //struct 後面可不要丟掉分號“;”
struct odefunc3
{
void operator()(double dx, const
std::valarray<double>&
dyn,
std::valarray<double>&
fai)
{
fai[0] = dyn[1] *
dyn[2]; // dy1/dx = y2 * y3; 3變量微分方程組
fai[1] = -dyn[0] * dyn[2]; //
dy2/dx = -y1 * y3;
fai[2] = -0.51 * dyn[0] *
dyn[1]; // dy3/dx = -0.51 * y1 * y2;
}
}; //struct 後面可不要丟掉分號“;”
struct odefunc4
{
void operator()(double dx, const
std::valarray<double>&
dyn,
std::valarray<double>&
fai, const
std::valarray<double>&
para)
{
fai[0] = para[0] * (para[3] -
dyn[0]); // dy1/dx = c1 * (d1 - y1);
3變量微分方程組,帶額外參數
fai[1] = para[1] * (para[4] -
dyn[1]); // dy2/dx = c2 * (d2 - y2);
fai[2] = para[2] * (para[5] -
dyn[2]); // dy3/dx = c3 * (d3 - y3);
}
}; //struct 後面可不要丟掉分號“;”
void odeTest1() //單變量微分方程,使用函數指針的方式傳遞函數
{
double dh = 0.1; //步長
double dx0 = 0.0;
double dy0 = 1.0; //初值
std::valarray<double>
darrayn(dy0, 1); //初值,向量長度是1
std::valarray<double>
darraynext(0.0, 1); //下一步的值,最好初始化
double dx = dx0; //當前的x
TRACE("\nodeTest1\n"); //調試輸出
TRACE("xn
yn\n");
TRACE("%.1f %.7f\n", dx,
darrayn[0]); //調試輸出初值
for (int i = 0; i < 5;
++i) //向前計算5步
{
ODERungeKuttaOneStep(dx,
darrayn, dh, odefunc1, darraynext);
dx += dh; //更新x
darrayn =
darraynext;
//更新y(n),爲循環的下一步做準備
TRACE("%.1f
%.7f\n", dx, darrayn[0]); //調試輸出計算一步後的值
}
}
void odeTest2() //單變量微分方程,使用函數算子的方式傳遞函數
{
double dh = 0.1; //步長
double dx0 = 0.0;
double dy0 = 1.0; //初值
std::valarray<double>
darrayn(dy0, 1); //初值,向量長度是1
std::valarray<double>
darraynext(0.0, 1); //下一步的值,最好初始化
double dx = dx0; //當前的x
TRACE("\nodeTest2\n"); //調試輸出
TRACE("xn
yn\n");
TRACE("%.1f %.7f\n", dx,
darrayn[0]); //調試輸出初值
for (int i = 0; i < 5;
++i) //向前計算5步
{
ODERungeKuttaOneStep(dx,
darrayn, dh, odefunc2(), darraynext);
dx += dh; //更新x
darrayn =
darraynext;
//更新y(n),爲循環的下一步做準備
TRACE("%.1f
%.7f\n", dx, darrayn[0]); //調試輸出計算一步後的值
}
}
void odeTest3() //3變量微分方程組, 使用函數算子的方式傳遞函數
{
double dh = 0.01; //步長
double dx0 = 0.0;
double dy0[] = {0.0, 1.0, 1.0};
//初值
std::valarray<double>
darrayn(dy0, 3); //初值,向量長度是3
std::valarray<double>
darraynext(0.0, 3); //下一步的值,最好初始化
double dx = dx0; //當前的x
TRACE("\nodeTest3\n"); //調試輸出
TRACE("xn
y1n
y2n
y3n\n");
//調試輸出初值
TRACE("%.2f
%.7f %.7f %.7f\n", dx,
darrayn[0], darrayn[1], darrayn[2]);
for (int i = 0; i < 5;
++i) //向前計算5步
{
ODERungeKuttaOneStep(dx,
darrayn, dh, odefunc3(), darraynext);
dx += dh; //更新x
darrayn =
darraynext;
//更新y(n),爲循環的下一步做準備
//調試輸出計算一步後的值
TRACE("%.2f
%.7f %.7f %.7f\n", dx,
darrayn[0], darrayn[1], darrayn[2]);
}
}
void odeTest4() //3變量微分方程組,
帶額外參數,使用函數算子的方式傳遞函數,其意義是求3個通道的階躍響應
{
double dh = 0.1; //步長
double dx0 = 0.0;
double dy0[] = {0.0, 0.0, 0.0};
//初值
std::valarray<double>
darrayn(dy0, 3); //初值,向量長度是3
std::valarray<double>
darraynext(0.0, 3); //下一步的值,最好初始化
double dcd[] = {1.0, 1.0, 1.0, 1.0, 1.0,
1.0}; //前3個量是慣性環節的開環增益, 後三個量是三個通道的單位階躍函數
std::valarray<double>
para(dcd, 6);
double dx = dx0; //當前的x
TRACE("\nodeTest3\n"); //調試輸出
TRACE("xn
y1n
y2n
y3n\n");
//調試輸出初值
TRACE("%.1f
%.7f %.7f %.7f\n", dx,
darrayn[0], darrayn[1], darrayn[2]);
for (int i = 0; i < 5;
++i) //向前計算5步
{
ODERungeKuttaOneStep(dx,
darrayn, dh, odefunc4(), darraynext, para);
dx += dh; //更新x
darrayn =
darraynext;
//更新y(n),爲循環的下一步做準備
//調試輸出計算一步後的值
TRACE("%.1f
%.7f %.7f %.7f\n", dx,
darrayn[0], darrayn[1], darrayn[2]);
}
}
void CODEDlg::OnButtonTestOde()
{
// TODO: Add your control notification handler
code here
odeTest1();
odeTest2();
odeTest3();
odeTest4();
}
c.示例調試輸出結果
odeTest1();結果 | odeTest2();結果 |
odeTest1 xn yn 0.0 1.0000000 0.1 1.0636613 0.2 1.1194055 0.3 1.1677203 0.4 1.2087883 0.5 1.2425414 |
odeTest2 xn yn 0.0 1.0000000 0.1 1.0636613 0.2 1.1194055 0.3 1.1677203 0.4 1.2087883 0.5 1.2425414 |
odeTest3();結果 | odeTest4();結果 |
odeTest3 xn y1n y2n y3n 0.00 0.0000000 1.0000000 1.0000000 0.01 0.0066665 0.9999667 0.9999830 0.02 0.0133323 0.9998889 0.9999433 0.03 0.0199970 0.9997667 0.9998810 0.04 0.0266601 0.9996001 0.9997961 0.05 0.0333212 0.9993891 0.9996885 |
odeTest3 xn y1n y2n y3n 0.0 0.0000000 0.0000000 0.0000000 0.1 0.0634542 0.0634542 0.0634542 0.2 0.1228819 0.1228819 0.1228819 0.3 0.1785387 0.1785387 0.1785387 0.4 0.2306638 0.2306638 0.2306638 0.5 0.2794814 0.2794814 0.2794814 |
本文是作者原創,轉載必須保證文章的完整性並標明出處(blog.sina.com.cn/zhangkunhn),請尊重作者,支持原創。