格子玻尔兹曼机(Lattice Boltzmann Method)系列5:LBM多相流实例之Shan-Chen模型

Shan-Chen模型

在格子玻尔兹曼方法的发展过程中,出现过许多的多相流动算法,其中Shan-Chen模型是最为人熟知也是应用最多的一种,因为这是所有多相流模型中最简单的一个。

LBM用于模拟多相流动一个十分重要的特点就是简单。如果研究过FVM的多相流动的话,捕捉界面是永远无法避免的。Diffuse Interface方法,或者是对界面存在拓扑损失问题的Level-Set方法,等等,绝对是FVM多相流领域中最叫人头疼的一部分。但是LBM则完全不同,由于粒子的角度观,多相流界面的捕捉是完全不需要程序员来写的,不需要写界面捕捉函数就可以模拟多相流代码,就奠定了LBM在CFD领域的一个无可撼动的位置。

然而有得必有失,LBM不需要写界面捕捉程序带来的直接缺陷就是密度比的问题。在现实世界,液气密度比可达1000左右,但是在LBM的多相流动中,密度比最多也就只能达到十几而已。对于Shan-Chen模型,这个缺陷更加明显。然而,相较于其代码的简单程度,这样的缺陷并算不上什么大的问题。

Shan-Chen模型的主要思想是通过增加了流体粒子与固体壁面之间的作用力来进行两相流体分离。
速度计算
fai的计算
这个变量的计算除了密度以外还需要用到压力的信息。在多相流动中一般会引入一个状态方程来计算压强,这里计算压强我使用的是:Edlich-Kwong(R-K)EOS。
EOS
这里的a和b都是常数,我在程序中设定为a=2/49, b=2/21, T=0.85*0.1961,R=1.于是压强p就是密度的单变量函数。
粒子间作用力
上式中的作用力描述的是流体粒子(不论是否同相)之间的相互作用力,g是一个由自己设定的常数,w是方向向量的权重。
粒子与固壁间作用力
上式中的作用力描述的是流体粒子与固体壁面之间的作用力,s函数用于检测是否是固体粒子,如果是固体壁面,就置为1,否则就置为0.

Shan-Chen模型的算法描述就只有这么多,下面贴一下具体的代码。

//A program for lattice Boltzmann in D2Q9 mode.
//This code are basically for Shan Chen model
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define NUM_THREADS 4
//定义pi的值
#define pi 3.1415926
#define Q 9
#define D 2
/*前处理模块*/
GLdouble Lx = 50.0, Ly = 50.0; //流场的长与宽
//网格边界面:x=0,x=l,y=0,y=m.z=0,z=h,内部点为(l-1)*(m-1)*(h-1)个.
#define l 200
#define m 100
////GLdouble dx = Lx / l, dy = Ly / m, dz = Lz / h;
GLdouble dt = 1.0;
char left_boundary = 'P';//左边界为Periodic/Bounceback/Von Neumann(Flux)/Dirichlet boundaries? choose P,B,V,D for your prefer.
char right_boundary = 'P';//右边界为Periodic/Bounceback/Von Neumann(Flux)/Dirichlet boundaries? choose P,B,V,D for your prefer.
char down_boundary = 'B';//前边界为Periodic/Bounceback/Von Neumann(Flux)/Dirichlet boundaries? choose P,B,V,D for your prefer.
char up_boundary = 'B';//后边界为Periodic/Bounceback/Von Neumann(Flux)/Dirichlet boundaries? choose P,B,V,D for your prefer.
/*前处理模块结束*/
GLdouble winx1 = 0.0, winx2 = l, winy1 = 0.0, winy2 = m;//窗口边界
GLint control = 1;
GLint case_index = 0;
GLdouble w[Q] = { 4.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0, 1.0 / 36.0, 1.0 / 36.0, 1.0 / 36.0, 1.0 / 36.0 };
GLdouble e[Q][D] = { { 0.0, 0.0 },
{ 1.0, 0.0 }, { 0.0, 1.0 }, { -1.0, 0.0 }, { 0.0, -1.0 },
{ 1.0, 1.0 }, { -1.0, 1.0 }, { -1.0, -1.0 }, { 1.0, -1.0 } };
GLint opp_index[Q] = { 0, 3, 4, 1, 2, 7, 8, 5, 6 };
GLdouble c = 1.0;
GLdouble c2 = c*c;
GLdouble cs2 = c*c / 3.0;
GLdouble p0 = 0.0;
////GLdouble gravity = 0.00098;

//TT0W is the value of T/T0;   RHW and RLW are the coexisting densities in the sepcified T/T0.
GLdouble TT0W[12] = { 0.975, 0.95, 0.925, 0.9, 0.875, 0.85, 0.825, 0.8, 0.775, 0.75, 0.7, 0.65 };
GLdouble RHW[12] = { 0.16, 0.21, 0.23, 0.247, 0.265, 0.279, 0.29, 0.314, 0.30, 0.33, 0.36, 0.38 };
GLdouble RLW[12] = { 0.08, 0.067, 0.05, 0.0405, 0.038, 0.032, 0.025, 0.0245, 0.02, 0.015, 0.009, 0.006 };
//Please specify which temperature and corresponding \rho_h, \rho_l in above 'data' are chosen.
//Initial T/T0, rho_h, and rho_l for the C-S EOS are listed in above 'data' section
GLdouble TT0 = TT0W[5];
//GLdouble rho_h = RHW[5];
//GLdouble rho_l = RLW[5];
GLdouble rho_h = 6.06;
GLdouble rho_l = 0.5;
//parameters
GLdouble RR = 30.0;
GLdouble rho_w = 0.8;
GLdouble tau = 1.0;
GLint t_max = 40000;
GLint Nwri = 400;
GLdouble visc = cs2*(tau - 0.5)*dt;



class lattice_node{
public:
	GLdouble f[Q];
	GLdouble fnew[Q];
	GLdouble feq[Q];
	GLdouble u[D];
	GLdouble upu[D];
	GLdouble rho;
	GLdouble p;
	GLdouble F[D];
	GLdouble S[D];
	string status;
	lattice_node()
	{
		rho = rho_h;
		p = 0.0;
		status = "L";
	}
};
lattice_node Nodes[l + 1][m + 1];
GLdouble psx[l + 1][m + 1];
GLdouble velocity_before[l + 1][m + 1][D];

void init(void){
	glLoadIdentity();
	glClearColor(1.0, 1.0, 1.0, 0.0);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glMatrixMode(GL_PROJECTION);
	gluOrtho2D(winx1, winx2, winy1, winy2);
}

void Bounceback_Boundary()
{
	GLint i = 0, j = 0;

	if (down_boundary == 'B')
	{
#pragma omp parallel for private(i)
		for (i = 0; i <= l; i++)
		{
			Nodes[i][0].status = "Bdown";
		}
	}
	if (up_boundary == 'B')
	{
#pragma omp parallel for private(i)
		for (i = 0; i <= l; i++)
		{
			Nodes[i][m].status = "Bup";
		}
	}

	if (left_boundary == 'B')
	{
#pragma omp parallel for private(j)
		for (j = 0; j <= m; j++)
		{
			Nodes[0][j].status = "Bleft";
		}
	}
	if (right_boundary == 'B')
	{
#pragma omp parallel for private(j)
		for (j = 0; j <= m; j++)
		{
			Nodes[l][j].status = "Bright";
		}
	}
}

void calculate_feq(GLdouble rh, GLdouble u[D], GLdouble feq_temp[Q])
{
	GLdouble eu = 0.0, uv = 0.0;
	GLint k = 0, rr = 0;

	for (GLint rr = 0; rr < D; rr++)
	{
		uv += (u[rr] * u[rr]);
	}
#pragma omp parallel for private(k,rr,eu)
	for (k = 0; k < Q; k++)
	{
		eu = 0.0;
		for (rr = 0; rr < D; rr++)
		{
			eu += (e[k][rr] * u[rr]);
		}
		eu *= c;
		feq_temp[k] = w[k] * rh*(1.0 + eu / cs2 + 0.5*eu*eu / cs2 / cs2 - 0.5*uv / cs2);
	}
	return;
}

GLdouble Cal_error()
{
	GLint i = 0, j = 0, rr = 0;
	GLdouble temp_error = 0.0, final_error = 0.0;
#pragma omp parallel for private(i,j,rr,temp_error)
	for (i = 0; i <= l; i++)
	{
		for (j = 0; j <= m; j++)
		{
			if (Nodes[i][j].status == "L")
			{
				temp_error = 0.0;
				for (rr = 0; rr < D; rr++)
				{
					temp_error += (Nodes[i][j].u[rr] - velocity_before[i][j][rr])*(Nodes[i][j].u[rr] - velocity_before[i][j][rr]);
				}
				final_error += sqrt(temp_error);
			}
		}
	}
	for (rr = 0; rr < D; rr++)
	{
		std::cout << Nodes[l / 2][m / 2].u[rr] << " , ";
	}
	std::cout << endl;
	return final_error;
}

void Record_velocity(void)
{
	//Record the velocity
	GLint i = 0, j = 0, rr = 0;
#pragma omp parallel for private(i,j,rr)
	for (i = 0; i <= l; i++)
	{
		for (j = 0; j <= m; j++)
		{
			if (Nodes[i][j].status == "L")
			{
				for (rr = 0; rr < D; rr++)
				{
					velocity_before[i][j][rr] = Nodes[i][j].u[rr];
				}
			}
		}
	}
}

void initnodes()
{
	GLint i = 0, j = 0, k = 0, rr = 0;
#pragma omp parallel for private(i,j,rr,k)
	for (i = 0; i <= l; i++)
	{
		for (j = 0; j <= m; j++)
		{
			for (rr = 0; rr < D; rr++)
			{
				Nodes[i][j].u[rr] = 0.0;
				Nodes[i][j].F[rr] = 0.0;
				Nodes[i][j].S[rr] = 0.0;
				Nodes[i][j].upu[rr] = 0.0;
			}
			Nodes[i][j].p = p0;
			psx[i][j] = 0.0;

			if (Nodes[i][j].status == "L")
			{
				Nodes[i][j].rho = rho_l;
				if ((i - 0.5*l)*(i - 0.5*l) + (j-0.85*RR)*(j-0.85*RR) < RR*RR)
				{
					Nodes[i][j].rho = rho_h;
				}
			}
			else
			{
				Nodes[i][j].rho = rho_w;
			}

			calculate_feq(Nodes[i][j].rho, Nodes[i][j].u, Nodes[i][j].feq);
			for (k = 0; k < Q; k++)
			{
				Nodes[i][j].f[k] = Nodes[i][j].feq[k];
			}
		}
	}
	//the end of 2-D circulation.
	return;
}

void calcu_upr()
{
	GLint i = 0, j = 0, rr = 0;
#pragma omp parallel for private(i,j,rr)
	for (i = 0; i <= l; i++)
	{
		for (j = 0; j <= m; j++)
		{
			if (Nodes[i][j].status == "L")
			{
				for (rr = 0; rr < D; rr++)
				{
					Nodes[i][j].upu[rr] = Nodes[i][j].u[rr] + (Nodes[i][j].F[rr] + Nodes[i][j].S[rr]) / (2.0*Nodes[i][j].rho);
				}
			}
			else
			{
				for (rr = 0; rr < D; rr++)
				{
					Nodes[i][j].upu[rr] = Nodes[i][j].u[rr];
				}
			}
			//end if.
		}
	}
	//the end of 2-D circulation.
	return;
}

GLdouble Cal_p(GLdouble rho)
{
	GLdouble pa = 2.0 / 49.0, pb = 2.0 / 21.0;
	GLdouble pTc = 0.1961;
	GLdouble pT = 0.85*pTc;
	GLdouble pR = 1.0;
	return rho*pR*pT / (1 - pb*rho) - pa*rho*rho / sqrt(pT) / (1 + pb*rho);
}

void calcu_Fxy()
{
	GLint i = 0, j = 0, rr = 0, k = 0;
	GLint xp = 0, yp = 0;
	GLdouble  R = 1.0, a = 12 * cs2, b = 4.0, temp_Fxy = 0.0, temp_rho = 0.0;
	GLdouble G1 = -1.0 / 3.0, psx_w = 0.0, psx_up = 0.0;
	GLdouble Tc = 0.3773*a / (b*R);
	GLdouble TT = TT0*Tc;

	TT = cs2 / R;
#pragma omp parallel for private(i,j,temp_rho,temp_Fxy)
	for (i = 0; i <= l; i++)
	{
		for (j = 0; j <= m; j++)
		{
			if (Nodes[i][j].status == "L")
			{
				temp_rho = Nodes[i][j].rho*b / 4.0;
				//temp_Fxy = cs2*(1.0 + (4.0*temp_rho - 2.0*temp_rho*temp_rho) / pow((1.0 - temp_rho), 3.0)) - a*Nodes[i][j].rho - cs2;
				temp_Fxy = Cal_p(Nodes[i][j].rho) / Nodes[i][j].rho - cs2;

				psx[i][j] = sqrt(2.0*Nodes[i][j].rho*temp_Fxy / G1 / cs2);
				Nodes[i][j].p = Nodes[i][j].rho*cs2 + 0.5*cs2*G1*psx[i][j] * psx[i][j];
			}
			//end if.
		}
	}
	//the end of 2-D circulation.
	temp_rho = rho_w*b / 4.0;
	//temp_Fxy = cs2*(1.0 + (4.0*temp_rho - 2.0*temp_rho*temp_rho) / pow((1.0 - temp_rho), 3.0)) - a*rho_w - cs2;
	temp_Fxy = Cal_p(rho_w) / rho_w - cs2;
	psx_w = sqrt(2.0*rho_w*temp_Fxy / G1 / cs2);

	psx_up = psx_w;

#pragma omp parallel for private(i,j,rr,k,xp,yp)
	for (i = 0; i <= l; i++)
	{
		for (j = 0; j <= m; j++)
		{
			for (rr = 0; rr < D; rr++)
			{
				Nodes[i][j].F[rr] = 0.0;
				Nodes[i][j].S[rr] = 0.0;
			}
			if (Nodes[i][j].status == "L")
			{
				for (k = 1; k < Q; k++)
				{
					xp = i + (int)e[k][0];
					yp = j + (int)e[k][1];
					if (xp > l) xp = 0;
					else if (xp < 0) xp = l;
					if (yp > m) yp = 0;
					else if (yp < 0) yp = m;

					if (Nodes[xp][yp].status[0] == 'B' && yp == 0)
					{
						//interact with solid nodes.(prepare)
						for (rr = 0; rr < D; rr++)
						{
							Nodes[i][j].S[rr] += w[k] * e[k][rr] * psx_w;
						}
					}
					else if (Nodes[xp][yp].status[0] == 'B' && yp == m)
					{
						//interact with solid nodes.(prepare)
						for (rr = 0; rr < D; rr++)
						{
							Nodes[i][j].S[rr] += w[k] * e[k][rr] * psx_up;
						}
					}
					else
					{
						//interact with fluid nodes.(prepare)
						for (rr = 0; rr < D; rr++)
						{
							Nodes[i][j].F[rr] += w[k] * e[k][rr] * psx[xp][yp];
						}
					}
					//end if.
				}
				//Final wall-fluid interaction.
				for (rr = 0; rr < D; rr++)
				{
					Nodes[i][j].S[rr] = -G1*Nodes[i][j].S[rr] * psx[i][j] * c;
				}
				//Final fluid-fluid interaction.
				for (rr = 0; rr < D; rr++)
				{
					Nodes[i][j].F[rr] = -G1*Nodes[i][j].F[rr] * psx[i][j] * c;
				}
			}
			//end if.
		}
	}
	//the end of 2-D circulation.
	return;
}

void rebounce_f()
{
	GLint i = 0, j = 0;
	GLdouble swap_temp = 0.0;
#pragma omp parallel for private(i,j,swap_temp)
	for (i = 0; i <= l; i++)
	{
		for (j = 0; j <= m; j++)
		{
			if (Nodes[i][j].status[0] == 'B')
			{
				swap_temp = Nodes[i][j].f[1]; Nodes[i][j].f[1] = Nodes[i][j].f[3]; Nodes[i][j].f[3] = swap_temp;
				swap_temp = Nodes[i][j].f[2]; Nodes[i][j].f[2] = Nodes[i][j].f[4]; Nodes[i][j].f[4] = swap_temp;
				swap_temp = Nodes[i][j].f[5]; Nodes[i][j].f[5] = Nodes[i][j].f[7]; Nodes[i][j].f[7] = swap_temp;
				swap_temp = Nodes[i][j].f[6]; Nodes[i][j].f[6] = Nodes[i][j].f[8]; Nodes[i][j].f[8] = swap_temp;
			}
			//end if.
		}
	}
	//The end of 2-D circulation.
	return;
}

void Streaming()
{
	GLint i = 0, j = 0, ileft = 0, iright = 0, jup = 0, jdown = 0, k = 0;

	//Streaming process::L
#pragma omp parallel for private(i,j,ileft,iright,jdown,jup)
	for (i = 0; i <= l; i++)
	{
		ileft = (i > 0) ? (i - 1) : (l);
		iright = (i < l) ? (i + 1) : (0);
		for (j = 0; j <= m; j++)
		{
			jdown = (j > 0) ? (j - 1) : (m);
			jup = (j < m) ? (j + 1) : (0);
			//Streaming process of L&B.

			Nodes[iright][j].fnew[1] = Nodes[i][j].f[1];
			Nodes[i][jup].fnew[2] = Nodes[i][j].f[2];
			Nodes[ileft][j].fnew[3] = Nodes[i][j].f[3];
			Nodes[i][jdown].fnew[4] = Nodes[i][j].f[4];

			Nodes[iright][jup].fnew[5] = Nodes[i][j].f[5];
			Nodes[ileft][jup].fnew[6] = Nodes[i][j].f[6];
			Nodes[ileft][jdown].fnew[7] = Nodes[i][j].f[7];
			Nodes[iright][jdown].fnew[8] = Nodes[i][j].f[8];
		}
	}
	//the end of 2-D circulation.
#pragma omp parallel for private(i,j,k)
	for (i = 0; i <= l; i++)
	{
		for (j = 0; j <= m; j++)
		{
			for (k = 1; k < Q; k++)
			{
				Nodes[i][j].f[k] = Nodes[i][j].fnew[k];
			}
		}
	}
	//the end of 2-D circulation.

	return;
}

void macro_process()
{
	GLint i = 0, j = 0, k = 0, rr = 0;
	//macroscopic process
#pragma omp parallel for private(i,j,rr,k)
	for (i = 0; i <= l; i++)
	{
		for (j = 0; j <= m; j++)
		{
			if (Nodes[i][j].status == "L")
			{
				Nodes[i][j].rho = 0.0;
				for (k = 0; k < Q; k++)
				{
					Nodes[i][j].rho += Nodes[i][j].f[k];
				}

				for (rr = 0; rr < D; rr++)
				{
					Nodes[i][j].u[rr] = 0.0;
					for (k = 0; k < Q; k++)
					{
						Nodes[i][j].u[rr] += e[k][rr] * Nodes[i][j].f[k];
					}
					Nodes[i][j].u[rr] *= (c / Nodes[i][j].rho);
				}
			}
			//end if.
		}
	}
	//the end of 2-D circulation.
	return;
}

void Collision(void)
{
	GLint i = 0, j = 0, k = 0, rr = 0;
	GLdouble u_tackle[D] = { 0.0, 0.0 };

	//Collide process
#pragma omp parallel for private(i,j,rr,k,u_tackle)
	for (i = 0; i <= l; i++)
	{
		for (j = 0; j <= m; j++)
		{
			if (Nodes[i][j].status == "L")
			{
				for (rr = 0; rr < D; rr++)
				{
					u_tackle[rr] = Nodes[i][j].u[rr] + tau*(Nodes[i][j].S[rr] + Nodes[i][j].F[rr]) / Nodes[i][j].rho;
				}
				calculate_feq(Nodes[i][j].rho, u_tackle, Nodes[i][j].feq);
				for (k = 0; k < Q; k++)
				{
					Nodes[i][j].f[k] = Nodes[i][j].f[k] * (1.0 - 1.0 / tau) + Nodes[i][j].feq[k] / tau;
				}
			}
		}
	}
	//the end of 2-D circulation.
	return;
}

void drawing_liquid(void)
{
	GLint i = 0, j = 0;
	GLdouble scale_velo = 20;

	glColor3f(0.0, 0.0, 1.0);
	glPointSize(1.0);
	glBegin(GL_POINTS);
	for (i = 0; i <= l; i++)
	{
		for (j = 0; j <= m; j++)
		{
			if (Nodes[i][j].status == "L")
			{
				if (Nodes[i][j].rho > 0.25*(rho_h + 3*rho_l))
				{
					glColor3f(0.0, 0.0, 1.0);
					glVertex2f(i, j);
				}
			}
		}
	}
	glEnd();
	return;
}

void output_rho()
{
	GLint i = 0, j = m / 2, r = 0;
	printf("+++++++++++++++++++++++++++\n");
	for (j = 0; j <= m; r++)
	{
		for (i = l; i >= 0; i--)
		{
			printf("%.2f ", Nodes[i][j].rho);
		}
		printf("\n");
	}
	printf("+++++++++++++++++++++++++++\n");
}

void LBMs()
{
	GLdouble final_error = 1.0;
	FILE *fp;
	string filename1, filename2, filename3;
	GLint i, j = 0;
	
	omp_set_num_threads(NUM_THREADS);
	//标记回弹性边界条件
	Bounceback_Boundary();
	//初始化
	initnodes();

	drawing_liquid();
	glutSwapBuffers();

	for (GLint t = 0; t

这里是运行一个算例后的具体结果:
格子玻尔兹曼机(Lattice Boltzmann Method)系列5:LBM多相流实例之Shan-Chen模型_第1张图片
懒得写接触角测量函数了,所以就直接输出来信息然后用Tecplot画出界面,之后用SCA20_U进行的接触角测量。我在这里设置的是液体密度为6.06,气体密度为0.5,固体密度为0.8.

实际上固体的密度越接近0.5,那么接触角就会越大,反过来越接近6.06,那么接触角就会越小。这里就看出了Shan-Chen模型一个十分大的限制:没有办法手动设置接触角。

格子玻尔兹曼机(Lattice Boltzmann Method)系列5:LBM多相流实例之Shan-Chen模型_第2张图片
上图是一篇文献中的结果,这个程序的所有参数都是引用的这篇文献中的设置:[5].Haibo Huang, Zhitao Li, Shuaishuai Liu, Xi-yun Lu. Shan-and-Chen-type multiphase lattice Boltzmann study of viscous coupling effects for two-phase flow in porous media. Int. J.Numer. Meth. Fluids. 2008.

Shan-Chen模型的基本情况就是这么多,也许你很疑惑是在计算粒子间作用力和固体-液体作用力的时候,参数g的作用究竟是什么。实际上g显然是一个表面张力的相关量。而尽管在现实世界中一般会认为表面张力越大,接触角也会越大。以我曾经实验测量了无数种流体的接触角经验来说,统计意义上的确是有这样的相关性,但是也经常会有表面张力大接触角反而小的流体存在,而且比例还不低。所以在所有的数值模拟中,接触角和表面张力都是解耦分开的。

下一篇会写一写多相流模拟中的HCZ模型,然后这个系列基本上就到了尾声了。再后面可能会挑个时间写一写浸入边界法。

你可能感兴趣的:(格子玻尔兹曼机(Lattice Boltzmann Method)系列5:LBM多相流实例之Shan-Chen模型)