It has been 500 days since the last update, the content of the article may be outdated.

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

以下内容来百度百科

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

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

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

问题描述

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

模拟和结果

bash
(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 .

开始模拟

bash
$ 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进行输出的合并

bash
$ 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)" ;
}

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

源码解读

头文件

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

cpp
#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"//包含了本个例使用的湿空气物理,计算饱和水汽压

输出诊断量

cpp
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。

cpp
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.

cpp
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种:

cpp
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 };
并且定义了四种单位之间的转换函数:

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

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


初始化Mesh类

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

cpp
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。

cpp
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
cpp
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)再循环空气柱,自下而上构建出一条湿绝热廓线。

cpp
// 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()函数的定义如下,此处用来构建一个满足固定大气模型的廓线:

cpp
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()函数进行微分方程的数值积分:

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

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

cpp
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

Moist Adiabats T-P with Multiple Condensing Species


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

cpp
  // 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微分方程的积分问题

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