Canoe Example#3:Bryan Moist Uprising Bubble

© Jiheng Hu , Nov 2023

前面两节的模拟都是干空气泡的情形,本节介绍对Bryan湿空气团的模拟,会涉及到相变和热力学的能量交换过程。 该实验由Bryan & Fritsch, 2002提出,用来作为湿空气团的模拟标准。在情景上和Robert问题一样,不过是湿空气。

以下内容来百度百科

  • 可逆干绝热过程 对于定质量的气块,它的状态是由气压(p)、温度(T)、和任意一个湿度参数(如比湿q)共同决定,而气块在垂直升降运动过程中其状态不断发生变化,因此必须获得气块状态变量随高度变化规律。 在垂直升降运动过程中,气块中所含的水汽始终未达到饱和,没有发生相变的绝热过程,称为干绝热过程。这里的干表示未饱和气块在绝热过程中没有发生水相的变化,并非指不含有水汽。由于满足垂直运动的三个基本假设,即绝热条件、准静态条件、静力平衡条件,因此他又是可逆过程,常称为可逆干绝热过程。

  • 可逆湿绝热过程 气块上升时到等熵凝结高度以上,水汽开始凝结并释放出潜热,如果饱和气块继续上升且凝结物全部保留在气块内,并与外界无热量交换;当气块下沉增温湿,这些凝结物又蒸发,使气块始终维持饱和状态,所耗的潜热与原来释放的潜热相等,沿逆过程后仍能回到原来的状态,这样的过程称为可逆湿绝热过程,又称为湿绝热过程,这里的“湿”表示在饱和的绝热气块内发生水相变化。 可逆湿绝热过程是一个等熵过程,虽然在可逆式绝热过程中发生了相变,但水汽和凝结出来的液态水总质量(mv+mt)不变,干空气质量md也不变

可逆的绝热过程是等熵过程。等熵过程的对立面是等温过程,在等温过程中,最大限度的热量被转移到了外界,使得系统温度恒定如常。由于在热力学中,温度与熵是一组共轭变量,等温过程和等熵过程也可以视为“共轭”的一对过程。

问题描述

本例将干燥模拟扩展到潮湿模拟,引入水汽和云,我们将模拟在饱和大气环境中上升的饱和气团。 大气的初始温度廓线是湿绝热线(等熵),所有层的水汽和云总水混合比保持恒定。地表温度和压力分别为289.85 K和1 bar。 模拟场的近地表由一个温度场呈高斯分布的气团(同Robert问题),充满了水蒸气。 该气泡温度比环境略高2 K以内,使得其含有略多的水汽,以在热力学和成分上都具有浮力(大气的背景成分是O2+N2)。气泡上升时,气泡的温度和压力下降,导致水蒸气凝结和潜热释放。 模拟场的垂直尺寸为10公里,水平尺寸为20公里,空间分辨率为100米。

模拟和结果

(env) jihenghu@canopy:~/bryanproj/build [14:12] 
$ cmake ~/canoe -DTASK=bryan .
$ make -j8
$ cd ..
(env) jihenghu@canopy:~/bryanproj [14:14] 
$ ln -s build/bin/bryan.release .
$ cp build/bin/bryan.inp .  
$ ln -s build/bin/combine.py .

开始模拟

$ mpirun -n 4 ./bryan.release -i bryan.inp 
time=1.0000e+03 cycle=7734
Terminating on success
cpu time used  = 2.459308e+02 (s)

模拟了1000s的变化,使用python进行输出的合并

$ python combine.py -o test
$ ncdump -h bryan-test-main.nc 
netcdf bryan-test-main {
dimensions:
	time = UNLIMITED ; // (11 currently)
	x1 = 200 ;
	x1f = 201 ;
	x2 = 400 ;
	x2f = 401 ;
	x3 = 1 ;
variables:
	float time(time) ;
		time:axis = "T" ;
		time:long_name = "time" ;
		time:units = "seconds" ;
	float x1(x1) ;
		x1:axis = "Z" ;
		x1:units = "meters" ;
		x1:long_name = "Z-coordinate at cell center" ;
	float x1f(x1f) ;
		x1f:axis = "Z" ;
		x1f:units = "meters" ;
		x1f:long_name = "Z-coordinate at cell face" ;
	float x2(x2) ;
		x2:axis = "Y" ;
		x2:units = "meters" ;
		x2:long_name = "Y-coordinate at cell center" ;
	float x2f(x2f) ;
		x2f:axis = "Y" ;
		x2f:units = "meters" ;
		x2f:long_name = "Y-coordinate at cell face" ;
	float x3(x3) ;
		x3:axis = "X" ;
		x3:units = "meters" ;
		x3:long_name = "X-coordinate at cell center" ;
	float rho(time, x1, x2, x3) ;
		rho:units = "kg/m^3" ;
		rho:long_name = "density" ;
	float press(time, x1, x2, x3) ;
		press:units = "pa" ;
		press:long_name = "pressure" ;
	float vel1(time, x1, x2, x3) ;
		vel1:units = "m/s" ;
		vel1:long_name = "velocity" ;
	float vel2(time, x1, x2, x3) ;
		vel2:units = "m/s" ;
		vel2:long_name = "velocity" ;
	float vel3(time, x1, x2, x3) ;
		vel3:units = "m/s" ;
		vel3:long_name = "velocity" ;
	float r0(time, x1, x2, x3) ;
	float vapor(time, x1, x2, x3) ;
		vapor:units = "kg/kg" ;
		vapor:long_name = "mass mixing ratio of vapor" ;
	float mse(time, x1, x2, x3) ;
		mse:units = "J/kg" ;
		mse:long_name = "moist static energy" ;
	float qtol(time, x1, x2, x3) ;
	float rh(time, x1, x2, x3) ;
	float temp(time, x1, x2, x3) ;
		temp:units = "K" ;
		temp:long_name = "temperature" ;
	float theta(time, x1, x2, x3) ;
		theta:units = "K" ;
		theta:long_name = "potential temperature" ;
	float theta_e(time, x1, x2, x3) ;
	float theta_v(time, x1, x2, x3) ;

// global attributes:
		:Conventions = "COARDS" ;
		:history = "Fri Nov 24 14:34:47 2023: ncks -A ./bryan.out3.nc ./bryan.out2.nc" ;
		:history_of_appended_files = "Fri Nov 24 14:34:47 2023: Appended file ./bryan.out3.nc had no \"history\" attribute\n",
			"" ;
		:NCO = "netCDF Operators version 4.8.1 (Homepage = http://nco.sf.net, Code = http://github.com/nco/nco)" ;
}

温度场、位温场的的演变:

源码解读

头文件

除了之前介绍的几个类库,这里新增了几个,这里使用注释的方式简介一下

#include <athena/bvals/bvals.hpp>  // defines BoundaryBase, BoundaryValues classes used for setting BCs on all data
// application
#include <application/application.hpp> // app注册
#include <application/exceptions.hpp>  // 报错信息
// 定义了vapor,cloud,chemitry,tracer,particle等介质的索引和函数,如GetVaporId();
#include <index_map.hpp>  

// climath
#include <climath/core.h> //基础的数学函数和单位转换
#include <climath/interpolation.h> // 插值工具包,包括定位,线性内插,样条等方法;
#include <climath/root.hpp> 
// special includes
#include "bryan_vapor_functions.hpp"//包含了本个例使用的湿空气物理,计算饱和水汽压

输出诊断量

int iH2O, iH2Oc;  // indices for water vapor and cloud
Real p0, grav;  //  global var declaration on surface pressure and the gravity

注册输出参数:temperature, potential temperature, virtual potential temperature, moist static energy, relatve humidity, total mixing ratio of water。

void MeshBlock::InitUserMeshBlockData(ParameterInput *pin) {
  AllocateUserOutputVariables(7);
  SetUserOutputVariableName(0, "temp");
  SetUserOutputVariableName(1, "theta");
  SetUserOutputVariableName(2, "theta_v");
  SetUserOutputVariableName(3, "mse");
  SetUserOutputVariableName(4, "theta_e");
  SetUserOutputVariableName(5, "rh");
  SetUserOutputVariableName(6, "qtol");
}

Grid iterations to assign the value before ouput, Most of the diagnosis variables are members of Class Thermoddynamics, use a pinter to refer the instance.

void MeshBlock::UserWorkBeforeOutput(ParameterInput *pin) {
  auto pthermo = Thermodynamics::GetInstance();

  for (int k = ks; k <= ke; ++k)
    for (int j = js; j <= je; ++j)
      for (int i = is; i <= ie; ++i) {
        user_out_var(0, k, j, i) = pthermo->GetTemp(this, k, j, i);
        user_out_var(1, k, j, i) = pthermo->PotentialTemp(this, p0, k, j, i);
        // theta_v
        user_out_var(2, k, j, i) =
            user_out_var(1, j, i) * pthermo->RovRd(this, k, j, i); // Theta/Rd
        // mse
        user_out_var(3, k, j, i) =
            pthermo->MoistStaticEnergy(this, grav * pcoord->x1v(i), k, j, i);
        // theta_e
        user_out_var(4, k, j, i) =
            pthermo->EquivalentPotentialTemp(this, p0, iH2O, k, j, i);
        user_out_var(5, k, j, i) =
            pthermo->RelativeHumidity(this, iH2O, k, j, i);
        // total mixing ratio
        auto &&air = AirParcelHelper::gather_from_primitive(this, k, j, i);
        user_out_var(6, k, j, i) = air.w[iH2O] + air.c[iH2Oc];
      }
}

AirParcel类定义在air_parcel.hpp, 定义了一系列的空气包的属性和计算函数,用来描述格点内气体、水凝物等介质的浓度状态。 AirParcel类包含了数种介质类型的定义及其单位的换算函数。介质使用指针表示,一共6种:

  Real *const w; //! hydro data
  Real *const c; //! cloud data
  Real *const q; //! chemistry data
  Real *const x; //! tracer data
  Real *const t; //! turbulence data
  Real const *d; //! particle data

分别表示对应物质的浓度值,可以采用air.w[iH2O]的形式引用对应的水汽含量; 浓度的单位由枚举类定义:enum class Type { MassFrac = 0, MassConc = 1, MoleFrac = 2, MoleConc = 3 }; 并且定义了四种单位之间的转换函数:

  AirParcel &ToMassFraction();
  AirParcel &ToMassConcentration();
  AirParcel &ToMoleFraction();
  AirParcel &ToMoleConcentration();

namespace:: AirParcelHelper定义了几个用于更新<void distribute_to_primitive()>、获取<AirParcel/AirColumn gather_from_conserved()>一个或一段气柱的AirParcel信息的函数。此处用于获取水含量信息。


初始化Mesh类

本个例依然不考虑额外的Forcing, 所以没有注册source function。

void Mesh::InitUserMeshData(ParameterInput *pin) {
  auto pindex = IndexMap::GetInstance();

  grav = -pin->GetReal("hydro", "grav_acc1");
  p0 = pin->GetReal("problem", "p0");

  // index
  iH2O = pindex->GetVaporId("H2O");
  iH2Oc = pindex->GetCloudId("H2O(c)");
}

初始化MeshBlock类

依然是先获取热力学的实例指针*pthermo,以便对热力学参数机型操控; 读取block中设定的初始参数: 地表温度,气团中心坐标,椭圆轴长,温度梯度参数,气团初始时刻的水汽混合比 vapor mixing ratio。

void MeshBlock::ProblemGenerator(ParameterInput *pin) {
  auto pthermo = Thermodynamics::GetInstance();

  Real Ps = p0;
  Real Ts = pin->GetReal("problem", "Ts");  // 289.85
  Real xc = pin->GetReal("problem", "xc");  // 10,000 m
  Real zc = pin->GetReal("problem", "zc");  // 2,000 m
  Real xr = pin->GetReal("problem", "xr");  // 2,000
  Real zr = pin->GetReal("problem", "zr");  // 2,000
  Real dT = pin->GetReal("problem", "dT");  // 1E5 pa
  Real qt = pin->GetReal("problem", "qt");  // 0.0196
  AirParcel air(AirParcel::Type::MassFrac); // 定义一个MassFrac为单位的空气块
  air.w[iH2O] = qt; // 设定比湿
  air.c[iH2Oc] = 0.; 

  air.ToMoleFraction();  // 转换为摩尔分数
  qt = air.w[iH2O];

遍历格点,(1)首先使用地表气压和地表温度插值出第一层(is)和地表(0)中间高度处的气温和气压,水汽,也就是 pcoord->dx1f(is) / 2.高度处的空气参数,注意is表示其实下标; (2)再循环空气柱,自下而上构建出一条湿绝热廓线。

  // construct a reversible adiabat
  for (int k = ks; k <= ke; ++k)
    for (int j = js; j <= je; ++j) {
      air.w[iH2O] = qt;
      air.w[IPR] = Ps;
      air.w[IDN] = Ts;
      air.c[iH2Oc] = 0.;

      //这里先使用地表气压和地表温度插值出第一层(is)和地表(0)中间高度处的气温和气压,水汽
      pthermo->Extrapolate(&air, pcoord->dx1f(is) / 2.,
                           Thermodynamics::Method::ReversibleAdiabat, grav);
      // 循环空气柱,自下而上构建出一条湿绝热廓线
      for (int i = is; i <= ie; ++i) {
        AirParcelHelper::distribute_to_conserved(this, k, j, i, air);
        pthermo->Extrapolate(&air, pcoord->dx1f(i),
                             Thermodynamics::Method::ReversibleAdiabat, grav);
      }
    }

Extrapolate()函数的定义如下,此处用来构建一个满足固定大气模型的廓线:

void Thermodynamics::Extrapolate	(	AirParcel * 	qfrac,
Real 	dzORdlnp,
Method 	method,
Real 	grav = 0.,
Real 	userp = 0. 
)		const
Construct an 1d atmosphere

Parameters
[in,out]	qfrac	mole fraction representation of air parcel
[in]	dzORdlnp	vertical grid spacing
[in]	method	choose from [reversible, pseudo, dry, isothermal]
[in]	grav	gravitational acceleration
[in]	userp	user parameter to adjust the temperature gradient

这个怎么实现呢,实际上Extrapolate调用了rk4IntegrateLnp()函数进行微分方程的数值积分:

#include <algorithm>
void  Thermodynamics::rk4IntegrateLnp(AirParcel *air, Real dlnp, Method method,
                                      Real adlnTdlnP)

rk4IntegrateLnp()进一步调用了calDlnTDlnP()函数来计算温压关系:

 Real Thermodynamics::calDlnTDlnP(AirParcel const& qfrac, Real latent[]) const {
   // calculate gammad
   Real gammad = GetGammad(qfrac);
  
   Real q_gas = 1.;
   for (int n = 0; n < NCLOUD; ++n) q_gas -= qfrac.c[n];
  
   Real f_sig = 1.;
   // vapor
   for (int n = 1; n <= NVAPOR; ++n)
     f_sig += qfrac.w[n] * (cp_ratio_mole_[n] - 1.);
   // cloud
   for (int n = 0; n < NCLOUD; ++n)
     f_sig += qfrac.c[n] * (cp_ratio_mole_[1 + NVAPOR + n] - 1.);
   Real cphat_ov_r = gammad / (gammad - 1.) * f_sig / q_gas;
  
   // vapor
   Real xd = q_gas;
   for (int n = 1; n <= NVAPOR; ++n) xd -= qfrac.w[n];
  
   Real c1 = 0., c2 = 0., c3 = 0.;
   for (int iv = 1; iv <= NVAPOR; ++iv) {
     c1 += qfrac.w[iv] / xd * latent[iv];
     c2 += qfrac.w[iv] / xd * latent[iv] * latent[iv];
     c3 += qfrac.w[iv] / xd;
   }
  
   return (1. + c1) / (cphat_ov_r + (c2 + c1 * c1) / (1. + c3));
 }

这个函数的来源与Cheng Li博士期间的工作:《Moist Adiabats with Multiple Condensing Species: A New Theory with Application to Giant-Planet Atmospheres》,其提出了适用于多种可凝结气体共存情形下的湿绝热温压关系(经典理论只适用于水蒸气存在的地球大气,不适用于巨行星的大气廓线构建),形式是这样的,大家感兴趣可以去研读: Moist Adiabats T-P with Multiple Condensing Species


最后这部分是再初始场中叠加Guassian气团的温湿度异常场,然后计算水的相变平衡点。

  // add temperature anomaly
  for (int k = ks; k <= ke; ++k)
    for (int j = js; j <= je; ++j)
      for (int i = is; i <= ie; ++i) {
        Real x1 = pcoord->x1v(i);
        Real x2 = pcoord->x2v(j);
        Real L = sqrt(sqr((x2 - xc) / xr) + sqr((x1 - zc) / zr));

        if (L < 1.) {
          auto &&air = AirParcelHelper::gather_from_conserved(this, k, j, i);
          air.ToMoleFraction();
          Real temp_v = air.w[IDN] * pthermo->RovRd(air);   /// 计算虚温
          temp_v *= 1. + dT * sqr(cos(M_PI * L / 2.)) / 300.;   /// 叠加保护水汽的虚温

          Real temp;
          int err = root(air.w[IDN], air.w[IDN] + dT, 1.E-8, &temp,
                         [&pthermo, &air, temp_v](Real temp) {
                           air.w[IDN] = temp;

                           auto rates =
                               pthermo->TryEquilibriumTP_VaporCloud(air, iH2O);
                           air.w[iH2O] += rates[0];
                           air.c[iH2Oc] += rates[1];

                           return temp * pthermo->RovRd(air) - temp_v;
                         });

          if (err) throw RuntimeError("pgen", "TVSolver doesn't converge");

          air.w[IDN] = temp;
          auto rates = pthermo->TryEquilibriumTP_VaporCloud(air, iH2O);
          air.w[iH2O] += rates[0];
          air.c[iH2Oc] += rates[1];

          AirParcelHelper::distribute_to_conserved(this, k, j, i, air);
        }
      }
}

TODO

  • try_equilibrium_tp_vapor_cloud.cpp 相变平衡点的计算问题
  • multiple condensing species dLnTdLnP 的计算问题
  • RK4微分方程的积分问题

  • 提取热力学假设和大气廓线构建的相关内容