本文問題參考自文獻 \(^{[1]}\) 第一章例 6,並假設了一些條件,基於 OpenFOAM-v2206
編寫程式數值上求解該問題。筆者之前也寫過基於 OpenFOAM
求解偏分方程的貼文,OpenFOAM 程式設計 | One-Dimensional Transient Heat Conduction。
假設一群山貓(捕食者)和一群山兔(被捕食者)生活在同一片區域,那麼我們可以知道,山貓吃了山兔,繁殖力會增強,山貓的數量會增加。這樣一來,山兔的數量會隨之減少。接下來,山貓由於食物短缺而數量減少,進而導致山兔遇到山貓的機會減少(被吃掉的概率降低),結果山兔的數量又逐漸增加,這樣山貓得到食物的機會也隨之增加,其數量又再一次增加,而山兔的數量又會再一次隨之減少,如此不斷迴圈。
設任意 \(t\) 時刻山兔與山貓的數量分別是 \(\phi\) 和 \(\psi\) ,二者的變化服從下面動力學方程
其中,\(k_1\),\(k_2\),\(\mu\) 和 \(\nu\) 都是正常數。
在上述方程中有幾點需要注意:
方程組(1)因為含有乘積項,因此是非線性的。現採用線性化的特殊方法求解,即研究種群數量 \(\phi\) 和 \(\psi\) 在其穩定值附近的微小漲落。設方程組(1)的穩態解為 \(\phi=\phi_0\),\(\psi=\psi_0\),它們由下面條件決定
也就是
代數方程(2)的解為
現在,將方程組(1)的解寫為下面形式
其中,\(\xi\) 和 \(\eta\) 與 \(\phi_0\) 和 \(\psi_0\) 相比都是小量。將上述解帶入方程組(1)中可以得到關於變數 \(\xi\) 和 \(\eta\) 的方程組
其中非線性項 \(\mu\xi\eta\) 和 \(\nu\xi\eta\) 為二階小量,可以忽略;再將穩態解代入可得線性化的耦合方程組
解耦後可得到
可以知道,式(4)與 L-C 震盪電路及單擺問題同屬於相同的數學模型
其通解為
其中,\(E\) 和 \(\delta\) 為振幅和初相位,與具體問題有關。
那麼我們也可以得到本問題的最終解的形式為
其中,每個公式中振幅與初相位取決於各自的初始條件。
從上一節可知,我們需要數值求解一個耦合的常微分方程組,可以用RungeKutta法\(^{[2]}\)。簡單推導過程如下:
其中,
四階Runge-Kutta方法可以表示為:
其中,
求解程式碼採用 Python
編寫,如下所示
#!/usr/bin/python3
# -*- coding:utf-8 -*-
import numpy as np
k1 = 0.7
k2 = 0.5
mu = 0.1
nu = 0.02
def f1(phi,psi):
return k1*phi-mu*phi*psi
def f2(phi,psi):
return nu*phi*psi-k2*psi
tStart = 0
tEnd = 100.0
n = 100000
deltaT = tEnd / n
halfDeltaT = deltaT / 2.0
Solution = np.ndarray([n+1,2])
Solution[0] = [30,20]
for i in range(n):
f11 = f1(Solution[i][0], Solution[i][1])
f21 = f2(Solution[i][0], Solution[i][1])
f12 = f1(Solution[i][0] + halfDeltaT * f11, Solution[i][1] + halfDeltaT * f21)
f22 = f2(Solution[i][0] + halfDeltaT * f11, Solution[i][1] + halfDeltaT * f21)
f13 = f1(Solution[i][0] + halfDeltaT * f12, Solution[i][1] + halfDeltaT * f22)
f23 = f2(Solution[i][0] + halfDeltaT * f12, Solution[i][1] + halfDeltaT * f22)
f14 = f1(Solution[i][0] + deltaT * f11, Solution[i][1] + deltaT * f21)
f24 = f2(Solution[i][0] + deltaT * f11, Solution[i][1] + deltaT * f21)
Solution[i+1][0] = Solution[i][0] + deltaT / 6.0 * (f11 + 2*f12 + 2*f13 + f14)
Solution[i+1][1] = Solution[i][1] + deltaT / 6.0 * (f21 + 2*f22 + 2*f23 + f24)
print((i+1)*deltaT,Solution[i+1][0],Solution[i+1][1])
使用OpenFOAM
數值求解常微分方程(組)主要用到 ODESystem.H
(構造微分方程系統)和 ODESolver.H
(求解器);此外,在 OpenFOAM
中需要對常微分方程(組)進行整理\(^{[3]}\),進而方便編寫程式碼進行求解。
對於任意階常微分方程可以轉化為一系列一階常微分方程,這個過程稱為降階,一階常微分方程的個數與原方程的階數相等(對於耦合常微分方程組,其階數等於所有方程階數之和)。對於某個 \(n\) 階常微分方程,可按下面形式降階
其中,\(n\) 為階數,\(y^{(0)}=y\) 。
進一步,引入符號 \(\mathrm{D}\) 對各階導數重新定義,此過程稱為轉換
最終,使用新符號重新表達原系統,此過程稱為誘導
在 OpenFOAM
中,存在另外一個過程,該過程僅與剛性系統求解器相關,這類求解器需要雅可比矩陣和對自變數的偏導數,即
接下來,我們看一下如何實現相關求解程式碼。首先看一下如何構造方程系統。系統程式碼需要繼承 Foam::ODESystem
抽象類,並且需要全部實現三個方法nEqns()
、 derivatives()
和 jacobian()
,其中 jacobian()
方法對於非剛性求解器可以將實現置空(空函數體)。
讓我們重新回顧一下公式(1),可知 nEqns()
應該返回 2;此外, 定義 \(Y=[\phi,\psi]^{\mathrm{T}}\) ,公式(1)可整理成如下向量形式
因此,導數可按照公式(1)編寫即可,只不過需要注意是向量形式。最後,對應之前的描述的降階過程,可以知道
進而可以知道, \(D_1 = Y, D'_1=Y'\),可得到雅可比矩陣和對自變數的偏導數分別為
需要注意的是,雅可比矩陣只有一個元素 \(\frac{\partial \mathrm{D}'_1}{\partial \mathrm{D}_1}\),只不過這個元素是一個塊的形式。
具體程式碼實現如下所示
#include "ODESystem.H"
class ODEs : public Foam::ODESystem
{
public:
ODEs() {}
~ODEs() {}
// 初始化引數
ODEs(const Foam::scalar k1, const Foam::scalar mu, const Foam::scalar k2,
const Foam::scalar nu)
{
k1_ = k1;
mu_ = mu;
k2_ = k2;
nu_ = nu;
}
// 方程個數
Foam::label nEqns() const override { return 2; }
// 求導
void derivatives(const Foam::scalar x, const Foam::scalarField& y,
Foam::scalarField& dydx) const override
{ // 兩個未知量存成向量,y[0] -> \phi, y[1] -> \psi
dydx[0] = k1_ * y[0] - mu_ * y[0] * y[1];
dydx[1] = nu_ * y[0] * y[1] - k2_ * y[1];
}
// 計算符號的雅可比矩陣和關於自變數的導數
void jacobian(const Foam::scalar x, const Foam::scalarField& y, Foam::scalarField& dfdx,
Foam::scalarSquareMatrix& dfdy) const override
{
dfdx[0] = 0;
dfdx[1] = 0;
dfdy[0][0] = k1_;
dfdy[0][1] = -mu_ * y[0];
dfdy[1][0] = nu_ * y[1];
dfdy[1][1] = -k2_;
}
private:
Foam::scalar k1_;
Foam::scalar mu_;
Foam::scalar k2_;
Foam::scalar nu_;
};
對應的,我們實現下主函數
#include <iostream>
#include <memory>
#include "ODESystem.H"
#include "ODESolver.H"
class ODEs : public Foam::ODESystem
{
// 這裡的程式碼在上邊已經介紹,此處省略
};
int main(int argc, char* argv[])
{
const Foam::scalar startTime = 0.0; // 開始時間
const Foam::scalar endTime = 100.0; // 結束時間
const Foam::scalar phi0 = 30; // 山兔初始值
const Foam::scalar psi0 = 20; // 山貓初始值
const Foam::label n = 100000; //
const Foam::scalar deltaT = endTime / n; // 步長
// 係數,參考自文獻[4]
const Foam::scalar k1 = 0.7;
const Foam::scalar mu = 0.1;
const Foam::scalar k2 = 0.5;
const Foam::scalar nu = 0.02;
// 構造物件
ODEs odes(k1, mu, k2, nu);
// 構造求解器,具體使用的演演算法通過引數傳遞
Foam::dictionary dict;
dict.add("solver", argv[1]);
Foam::autoPtr<Foam::ODESolver> solver = Foam::ODESolver::New(odes, dict);
// 初始化一些變數
Foam::scalar tStart = startTime;
Foam::scalarField PhiPsi(odes.nEqns()); // 因變數
PhiPsi[0] = phi0;
PhiPsi[1] = psi0;
Foam::scalarField ddt(odes.nEqns()); // 儲存導數值
// 計算過程
for (Foam::label i = 0; i < n; ++i)
{
Foam::scalar dtEst = deltaT / 2;
Foam::scalar tEnd = tStart + deltaT;
//
odes.derivatives(tStart, PhiPsi, ddt);
solver->solve(tStart, tEnd, PhiPsi, dtEst);
//
tStart = tEnd;
//
Foam::Info << tStart << "," << PhiPsi[0] << "," << PhiPsi[1] << Foam::endl;
}
return 0;
}
此外,CMakeLists.txt
檔案可參考筆者之前的隨筆,如 OpenFOAM程式設計 | Hello OpenFOAM 和 OpenFOAM 程式設計 | One-Dimensional Transient Heat Conduction,此處不再贅述。
筆者通過命令列引數分別採用RKCK45
演演算法和 seulex
演演算法(需要用到雅可比矩陣)對該問題進行求解,從下圖可見二者求解得到的結果是一致的。
同時執行筆者之前提到的 Python
程式碼後得到的數值結果與 OpenFOAM
計算結果繪製在同一張圖中,二者高度重合。
同時,解析解法(線性化的特殊解法)得到的結論是二者均按照 \(\sqrt{k_1k_2}\) 圓頻率震盪,那麼對應的週期為 $T = 2\pi / \sqrt{k_1k_2} = 2 \pi / \sqrt{0.7*0.5} \approx 10.62 $,而數值解中得到的週期為 12.425,筆者認為在本文的條件假設下,其中的差距來自於線性解法中沒有考慮非線性,但這個解法仍然具有實際意義;讀者可以嘗試改用絕對值較小的係數來降低其非線性程度。
另外,感興趣的讀者可以嘗試使用 Matlab
或 GNU Octave
求解該問題。
[1] 顧樵. 數學物理方法[M]. 北京:科學出版社, 2012.
[2] Chenglin LI.數值計算(四十七)RungeKutta求解常微分方程組
[3] Hassan Kassem. How to solve ODE in OpenFOAM
[4] 捕食者與被捕食者模型——logistic-volterra
防止迷路,請關注筆者部落格 部落格園@Fiatanium。
喜歡的朋友還請點贊、收藏、轉發,您的支援將是筆者創作的最大動力。
本文來自部落格園,作者:Fitanium,轉載請註明原文連結:https://www.cnblogs.com/Fitanium/p/16837685.html