有限元原理
以一維模型方程爲例
利用變分原理
對於任意成立。
利用分部積分可以得到
現在由一組基函數組成的n維函數空間中求解該變分問題,同時在該空間中取另一組基。
則有
令基函數和滿足在空間劃分的網格單元的單元點上值爲1,其餘單元點上均爲0。則
設得
對任意成立。因此得
一維對流方程
,取,現用有限元法法求解。
初場爲
首先建立網格單元和基函數。
對於任意長度的一維單元都將其先化爲的標準單元,現使用一階基函數進行計算,單元點位於單元兩側,即。
其中,爲單元長度
標準單元和其上的基函數如下圖所示
根據標準單元得到標準單元上的係數矩陣
代入後積分得到
由於各個單元上的基函數都僅在單元內部有值,因此將每個單元上積分得到的方程組擴展到整個計算域只需要在其他位置補零即可。然後將每個單元的方程組疊加組合到一起,即得到整個計算域的離散化方程。利用R-K等方法即可由初值推進求解。
同樣按照譜方法的空間步長,將劃分爲32個等長單元。於是得到
簡單計算化簡一下可以得到
的形式,然後即可推進求解。這樣可以直接計算,但是由於我希望結果能與僞譜法一樣初始波形能一直在計算域內循環,因此需要做一些修改,加上週期邊界邊界,這個方程本身因此自然是週期的,我們讓和滿足同樣的邊界,即也滿足於是有,因此上面合併出來的一項的係數矩陣並沒有再加產生的結果。而爲了保證推進時始終有,因此需要將化簡後的A矩陣進行修改,由於波形是從左往右傳播的,因此將A的第一行改爲這只是簡單一維問題的一種取巧的方法,對於複雜的方程組問題可能信息會同時向兩側傳播,需要進行一些推導使用更爲嚴謹的形式來給出邊界條件。
具體代碼如下
#include <iostream>
#include <cmath>
#include <vector>
using namespace std;
/************************************************
多項式相關計算,用於生成基函數
************************************************/
struct poly
{
vector<double> a;
poly(int n):a(n+1,0){}
poly(){}
void set(int n)
{
a.resize(n);
for(int i=0;i<n;i++) a[i]=0;
}
const int size()
{
return a.size();
}
const int size()const
{
return a.size();
}
double& operator[](int i)
{
return a[i];
}
const double& operator[](int i)const
{
return a[i];
}
double operator()(double x)
{
double r=0;
for(int i=a.size()-1;i>0;i--)
{
r+=x*a[i];
}
return r+a[0];
}
poly integ()//不定積分
{
poly t;
t.set(a.size()+1);
for(int i=1;i<t.size();i++) t[i]=a[i-1]/i;
return t;
}
double integ(double l,double u)//定積分
{
poly t;
t.set(a.size()+1);
for(int i=1;i<t.size();i++) t[i]=a[i-1]/i;
return t(u)-t(l);
}
poly diff()//導函數
{
poly t;
t.set(a.size()-1);
for(int i=0;i<t.size();i++) t[i]=a[i+1]*(i+1);
return t;
}
poly diff() const
{
poly t;
t.set(a.size()-1);
for(int i=0;i<t.size();i++) t[i]=a[i+1]*(i+1);
return t;
}
double diff(double x)//x處導數值
{
poly t;
t.set(a.size()-1);
for(int i=0;i<t.size();i++) t[i]=a[i+1]*i;
return t(x);
}
};
poly operator*(const double k,const poly p)
{
poly t=p;
for(int i=0;i<p.size();i++) t[i]=k*p[i];
return t;
}
poly operator*(const poly p,const double k)
{
poly t=p;
for(int i=0;i<p.size();i++) t[i]=k*p[i];
return t;
}
poly operator/(const double k,const poly p)
{
poly t=p;
for(int i=0;i<p.size();i++) t[i]=k/p[i];
return t;
}
poly operator/(const poly p,const double k)
{
poly t=p;
for(int i=0;i<p.size();i++) t[i]=k/p[i];
return t;
}
poly operator*(const poly p1,const poly p2)
{
poly t;
t.set(p1.size()+p2.size()-1);
for(int i=0;i<p1.size();i++)
{
for(int j=0;j<p2.size();j++) t[i+j]+=p1[i]*p2[j];
}
int i=t.size(),j=i-1;
for(;j>=0;j--)
{
if(t[j]!=0) break;
}
t.a.resize(j+1);
return t;
}
ostream& operator<<(ostream& out,const poly p)
{
for(int i=p.size()-1;i>0;i--) out<<p[i]<<'\t';
out<<p[0];
return out;
}
vector<poly> creat_base(int n)//生成拉格朗日插值基函數
{
vector<poly> r(n+1,poly(0));
for(int i=0;i<n+1;i++)
{
r[i][0]=1;
for(int j=0;j<n+1;j++)
{
if(j==i) continue;
poly t(1);
t[0]=-j/(i-j);
t[1]=n/(i-j);
r[i]=r[i]*t;
}
}
return r;
}
/************************************************
矩陣相關計算
************************************************/
struct matrix
{
vector<vector<double>> a;
matrix(){}
matrix(int r,int c):a(r,vector<double>(c,0)){}
void set(int r,int c)
{
a.resize(r);
for(int i=0;i<r;i++)
{
a[i].resize(c);
for(int j=0;j<c;j++) a[i][j]=0;
}
}
vector<double>& operator[](int i)
{
return a[i];
}
const vector<double>& operator[](int i)const
{
return a[i];
}
const int row()
{
return a.size();
}
const int col()
{
if(a.size()==0) return 0;
return a[0].size();
}
const int row() const
{
return a.size();
}
const int col() const
{
if(a.size()==0) return 0;
return a[0].size();
}
matrix operator-()
{
matrix A(row(),col());
for(int i=0;i<row();i++)
{
for(int j=0;j<col();j++)
{
A[i][j]=-a[i][j];
}
}
return A;
}
};
matrix operator+(const matrix& A,const matrix& B)
{
if(A.row()!=B.row()||A.col()!=B.col())
{
cout<<"matrix operator+:維度不匹配"<<endl;
return matrix();
}
matrix C(A.row(),A.col());
for(int i=0;i<C.row();i++)
{
for(int j=0;j<C.col();j++)
{
C[i][j]=A[i][j]+B[i][j];
}
}
return C;
}
matrix operator-(const matrix& A,const matrix& B)
{
if(A.row()!=B.row()||A.col()!=B.col())
{
cout<<"matrix operator+:維度不匹配"<<endl;
return matrix();
}
matrix C(A.row(),A.col());
for(int i=0;i<C.row();i++)
{
for(int j=0;j<C.col();j++)
{
C[i][j]=A[i][j]-B[i][j];
}
}
return C;
}
matrix operator*(const double k,const matrix& A)
{
matrix C(A.row(),A.col());
for(int i=0;i<C.row();i++)
{
for(int j=0;j<C.col();j++)
{
C[i][j]=A[i][j]*k;
}
}
return C;
}
vector<double> operator*(const matrix& A,const vector<double>& x)
{
if(A.row()!=x.size())
{
cout<<"operator *:維度不匹配"<<endl;
return vector<double>();
}
vector<double> r(x.size(),0);
for(int i=0;i<A.row();i++)
{
for(int j=0;j<A.col();j++)
{
r[i]+=A[i][j]*x[j];
}
}
return r;
}
matrix operator*(const matrix& A,const double k)
{
matrix C(A.row(),A.col());
for(int i=0;i<C.row();i++)
{
for(int j=0;j<C.col();j++)
{
C[i][j]=A[i][j]*k;
}
}
return C;
}
matrix operator/(const matrix& A,const double k)
{
matrix C(A.row(),A.col());
for(int i=0;i<C.row();i++)
{
for(int j=0;j<C.col();j++)
{
C[i][j]=A[i][j]/k;
}
}
return C;
}
ostream& operator<<(ostream& out,const matrix& A)
{
for(int i=0;i<A.row();i++)
{
for(int j=0;j<A.col()-1;j++)
{
out<<A[i][j]<<'\t';
}
out<<A[i][A.col()-1]<<'\n';
}
return out;
}
/************************************************
矢量和矩陣相關計算
************************************************/
ostream& operator<<(ostream& out,const vector<double>& A)
{
for(int j=0;j<A.size()-1;j++)
{
out<<A[j]<<'\t';
}
out<<A[A.size()-1];
return out;
}
double norm(const vector<double>& x)//二範數、模
{
double r=0;
for(int i=0;i<x.size();i++) r+=x[i]*x[i];
return sqrt(r);
}
vector<double>& operator/(vector<double>& A,const double k)
{
for(int i=0;i<A.size();i++)
{
A[i]=A[i]/k;
}
return A;
}
double operator*(const vector<double>& a,const vector<double>& b)
{
if(a.size()!=b.size())
{
cout<<"點積維度不匹配"<<endl;
return 0;
}
double r=0;
for(int i=0;i<a.size();i++) r+=a[i]*b[i];
return r;
}
double power_method(const matrix& A)//冪法求譜半徑
{
vector<double> t(A.row(),1),t0(t);
double beta=0,beta0=1;
while(abs(beta-beta0)>1e-7)
{
beta0=beta;
t=t/norm(t);
t0=t;
t=A*t;
beta=t0*t;
}
return beta;
}
void op1(matrix& A,int i,int j)
{
for(int k=0;k<A[0].size();k++)
{
swap(A[i][k],A[j][k]);
}
}
void op2(matrix& A,int i,double k)
{
for(int j=0;j<A[0].size();j++)
{
A[i][j]*=k;
}
}
void op3(matrix& A,int i,int j,double k)
{
for(int l=0;l<A[0].size();l++)
{
A[j][l]+=k*A[i][l];
}
}
int maxr(const matrix &A,int j)
{
int Mr=j;
double M=A[j][j];
for(int i=j+1;i<A.a.size();i++)
{
if(abs(A[i][j])>M)
{
M=abs(A[i][j]);
Mr=i;
}
}
return Mr;
}
void inv_mul(matrix A,matrix& B)//inv(A)*B
{
int rank=A.a.size();
for(int i=0;i<A.a.size();i++)
{
int m=maxr(A,i);
op1(A,i,m);
op1(B,i,m);
op2(B,i,1/A[i][i]);
op2(A,i,1/A[i][i]);
for(int j=i+1;j<A.a.size();j++)
{
op3(B,i,j,-A[j][i]);
op3(A,i,j,-A[j][i]);
}
}
for(int i=A.a.size()-1;i>=0;i--)
{
for(int k=i-1;k>=0;k--)
{
op3(B,i,k,-A[k][i]);
}
}
}
matrix creat_At(int n,const vector<poly>& N)//獲得時間項在標準單元上的係數矩陣
{
if(N.size()!=n+1)
{
cout<<"creat_At:階數不匹配"<<endl;
return matrix();
}
matrix _At(n+1,n+1);
for(int i=0;i<n+1;i++)
{
for(int j=0;j<n+1;j++)
{
_At[i][j]=(N[i]*N[j]).integ(0,1);
}
}
return _At;
}
matrix creat_Ax(int n,const vector<poly>& N)//獲得一階空間導數項在標準單元上的係數矩陣
{
if(N.size()!=n+1)
{
cout<<"creat_Ax:階數不匹配"<<endl;
return matrix();
}
matrix _Ax(n+1,n+1);
for(int i=0;i<n+1;i++)
{
for(int j=0;j<n+1;j++)
{
poly t=N[j].diff();
_Ax[i][j]=-(N[i]*t).integ(0,1);
}
}
return _Ax;
}
matrix creat_Axx(int n,const vector<poly>& N)//獲得二階空間導數項在標準單元上的係數矩陣
{
if(N.size()!=n+1)
{
cout<<"creat_Ax:階數不匹配"<<endl;
return matrix();
}
matrix _Ax(n+1,n+1);
for(int i=0;i<n+1;i++)
{
for(int j=0;j<n+1;j++)
{
poly t1=N[i].diff(),t2=N[j].diff();
_Ax[i][j]=(t1*t2).integ(0,1);
}
}
return _Ax;
}
matrix get_A(int n,int o,const matrix& _A)//合併所有單元(該函數要求所有單元長度相等,如果不等需要額外修改合併方式)
{
matrix A(o*n+1,o*n+1);
for(int i=0;i<n;i++)
{
for(int j=0;j<o+1;j++)
{
for(int k=0;k<o+1;k++)
{
A[i*o+j][i*o+k]+=_A[j][k];
}
}
}
return A;
}
const int order=1;
const int NE=32,//空間單元數
NS=100;//時間步數
const double rb=-5,l=10,//計算域左邊界,計算域長度
dt=0.1,//時間步長
dx=l/NE;
void init_guass(vector<double> &u0)//設置高斯函數初場
{
for(int i=0;i<u0.size();i++)
{
u0[i]=exp(-pow((l*double(i)/(NE-1)+rb),2));
}
}
int main()
{
vector<double> u0(NE+1);
init_guass(u0);
vector<poly> N=creat_base(order);
matrix _At=creat_At(order,N),_Ax=creat_Ax(order,N),
At=get_A(NE,order,_At),Ax=get_A(NE,order,_Ax),A(NE+1,NE+1);
A=At+Ax*(dt/dx);
inv_mul(At,A);
A[0][A.col()-1]=1;
for(int i=0;i<A.col()-1;i++) A[0][i]=0;
cout<<NE+1<<'\t'<<NS<<'\t'<<rb<<'\t'<<l<<'\n';
cout<<u0<<'\n';
for(int i=0;i<NS;i++)
{
u0=A*u0;
cout<<u0<<'\n';
}
retrun 0;
}
計算結果如下
可以看到計算仍然不穩定,因此與僞譜法相同,同樣使用人工粘性,在方程右端額外添加一個二階導數項進行求解。添加一個求空間二階導數項係數矩陣的函數併合併到之前計算出的中
matrix creat_Axx(int n,const vector<poly>& N)//獲得二階空間導數項在標準單元上的係數矩陣
{
if(N.size()!=n+1)
{
cout<<"creat_Ax:階數不匹配"<<endl;
return matrix();
}
matrix _Ax(n+1,n+1);
for(int i=0;i<n+1;i++)
{
for(int j=0;j<n+1;j++)
{
poly t1=N[i].diff(),t2=N[j].diff();
_Ax[i][j]=(t1*t2).integ(0,1);
}
}
return _Ax;
}
int main()
{
vector<double> u0(NE+1);
init_guass(u0);
vector<poly> N=creat_base(order);
matrix _At=creat_At(order,N),_Ax=creat_Ax(order,N),_Axx=creat_Axx(order,N),
At=get_A(NE,order,_At),Ax=get_A(NE,order,_Ax),Axx=get_A(NE,order,_Axx),A(NE+1,NE+1);
double nu=0.5;
A=At+Ax*(dt/dx)-nu*Axx*(dt/dx*dx);
inv_mul(At,A);
A[NE][0]=1;
for(int i=1;i<A.col();i++) A[NE][i]=0;
cout<<NE+1<<'\t'<<NS<<'\t'<<rb<<'\t'<<l<<'\n';
cout<<u0<<'\n';
for(int i=0;i<NS;i++)
{
u0=A*u0;
cout<<u0<<'\n';
}
return 0;
}
試算出計算結果如下
可以看到加上合適的人工粘性後計算就收斂了。
無粘Burgers方程
現用有限元法法求解。
初場爲,同樣取週期邊界。
同樣使用一階拉格朗日插值單元
只需要在前面代碼的基礎上再添加一個計算的函數,並修改主函數即可
vector<double> sq(const vector<double>& x)//每個分量都平方
{
vector<double> r(x.size());
for(int i=0;i<x.size();i++)r[i]=x[i]*x[i];
return r;
}
int main()
{
vector<double> u0(NE+1);
init_guass(u0);
vector<poly> N=creat_base(order);
matrix _At=creat_At(order,N),_Ax=creat_Ax(order,N),_Axx=creat_Axx(order,N),
At=get_A(NE,order,_At),Ax=get_A(NE,order,_Ax),Axx=get_A(NE,order,_Axx),A(NE+1,NE+1),inv_At(NE+1,NE+1);
for(int i=0;i<NE+1;i++) inv_At[i][i]=1;
inv_mul(At,inv_At);
double nu=0.5;
cout<<NE+1<<'\t'<<NS<<'\t'<<rb<<'\t'<<l<<'\n';
cout<<u0<<'\n';
for(int i=0;i<NS;i++)
{
u0=At*u0+(0.5*dt/dx)*Ax*sq(u0)-nu*Axx*(dt/dx*dx)*u0;
u0=inv_At*u0;
u0[0]=u0[NE];
cout<<u0<<'\n';
}
return 0;
}
取計算結果如下
可以看到結果和僞譜法的到基本相同。
一些其它問題
這是我第一次用有限元求解帶一階空間偏導的偏微分方程,以前只用有限元解過拋物型的傳熱方程,沒想到兩個方程的求解異常順利。因爲對流方程由於信息的傳遞是有方向的,通常需要一些迎風的處理,而Burgers方程的非線性帶來的數值上的間斷,通常的數值方法很容易在間斷處產生異常震盪,導致計算髮散,因此也需要限制器等方式抑制間斷處的震盪。之前看到的有限元解決這兩個問題是使用的迎風有限元和間斷有限元。
迎風有限元是在基函數上添加一個不影響單元點值的迎風的函數作爲權函數()使得單元內上的權函數具有迎風性質,從而保證信息傳播的方向性,使格式穩定,沒想到直接加人工粘性就可穩定,不過這個人工粘性係數是一個可變參數需要調節,如果這個人工粘性項係數標定不準確則成爲一個二階誤差進入方程當中,不像迎風有限元一樣直接給出高階的基函數構造即可保證空間精度。不過可以利用一些穩定性分析手段將其用的函數來近似,從而避免額外的參數調節。
間斷有限元是專門處理激波處異常數值震盪的方法。這個方法和通量重構有些類似,即通量的單元和守恆量的單元錯開了半個網格,從而保證數值格式的守恆律,再通過添加限制器加強激波附近的耗散,抑制異常的數值震盪。
不過沒想到Burgers方程也直接利用普通的人工粘性就可以抑制間斷附近的震盪,甚至連人工粘性的耗散係數都不用調整,結果也精度也還可以。