云计算百科
云计算领域专业知识百科平台

【零基础学智能仿真-56】CFD网格、边界条件与时间步

课程摘要

本节承接平行板层流基准,系统讲解CFD网格质量、入口—出口与壁面边界的配合,以及时间步和Courant数的关系。通过可运行Python练习,观察解析流量的积分误差,并明确它与真实CFD网格收敛的区别;结合二维网格模板、边界示意图和空间—时间研究计划,学习如何记录、检查与解释离散设置,为后续OpenFOAM工作流自动化奠定基础。


一、有了解析解,为什么仍然可能算错?

上一节,我们得到了平行板充分发展层流的解析速度:

\\[ u(y)=\\frac{G}{2\\mu}y(H-y). \\]

它应该满足:

  • 两壁速度为零;
  • 中间速度最大;
  • 速度剖面呈抛物线;
  • 最大速度是平均速度的1.5倍。

现在,假设某位学习者得到了一条接近均匀分布的速度曲线。

他不断加密网格,结果越来越稳定,于是宣布:

“模型已经实现网格无关。”

这个结论可靠吗?

不一定。如果他把壁面无滑移误设成了滑移,那么他可能稳定地求解了另一个物理问题。

因此,本节首先建立一个判断顺序:

物理问题是否正确?
↓
边界条件是否匹配?
↓
网格是否有效且有足够分辨率?
↓
时间与迭代设置是否合适?
↓
结果是否通过核查?

网格更密、残差更低、曲线更稳定,都需要放在正确物理定义下解释。

本节Python练习不求解Navier–Stokes方程,也不运行OpenFOAM。附带的OpenFOAM材料仅用于网格生成与检查练习,不是完整流动案例。

二、先固定物理问题,再改变数值设置

继续使用上一节的基准:

项目设置
板间距 \\(H\\) \\(0.01\\ \\mathrm m\\)
长度 \\(L\\) \\(1\\ \\mathrm m\\)
密度 \\(\\rho\\) \\(1000\\ \\mathrm{kg/m^3}\\)
动力黏度 \\(\\mu\\) \\(0.001\\ \\mathrm{Pa\\cdot s}\\)
运动黏度 \\(\\nu\\) \\(10^{-6}\\ \\mathrm{m^2/s}\\)
压力梯度大小 \\(G\\) \\(2\\ \\mathrm{Pa/m}\\)
壁面 静止、无滑移
假设 二维、稳态、充分发展层流、无体力

参考结果为:

\\[ q_{\\mathrm{ref}} = \\frac{GH^3}{12\\mu} = \\frac1{6000}\\ \\mathrm{m^2/s}, \\]\\[ \\bar u=0.016667\\ \\mathrm{m/s}, \\qquad u_{\\max}=0.025000\\ \\mathrm{m/s}. \\]

这里的\\(q\\)仍然是单位展向宽度流量。

研究网格影响时,不要顺便改变板间距、黏度、驱动方式或压力定义,否则比较的就不是同一个问题。

三、网格质量与网格分辨率,不是一回事

网格质量回答:

单元是否有效,几何关系是否适合所用离散方法?

网格分辨率回答:

网格是否足够细,能否描述关注的变化?

两者需要分别检查。

检查内容主要目的
尺寸、包围盒与单位 防止把mm当成m
连通性和区域 发现意外断开或额外区域
单元体积 排查无效、零或负体积
边界名称与类型 保证后续字段正确对应
非正交性、偏斜 评价面通量近似等可能受到的影响
局部尺度与过渡 判断近壁梯度等能否合理分辨

非正交性和偏斜不是同一个量。单元长宽比大,也不能脱离流动方向、梯度和离散格式,直接判定不可用。

OpenFOAM提供checkMesh检查网格有效性,实际日志应与软件版本和设置一起保存。Foundation v13网格工具说明

但要注意:

checkMesh通过,不等于边界条件正确,也不等于已经达到网格无关。

本节不提供适用于所有模型的统一网格质量阈值。

四、入口和出口条件,要成组理解

要实现平行板充分发展流动,可以采用不同路径,但不能随意混搭。

4.1 速度或流量驱动

入口给定速度剖面或流量,出口设置压力参考,其他变量采用与求解器相容的条件。

如果给定均匀入口,通常会存在发展段,不能要求整个通道立即满足充分发展抛物线剖面。

部分教程采用入口压力zeroGradient、出口速度zeroGradient等组合,但这不能直接证明整个解析场与该数值案例严格一致,仍需检查入口、出口影响和比较位置。

4.2 压力驱动

规定合适的压力驱动,让速度通过动量方程和相容边界条件求出。

不要又任意指定一个独立入口流量,要求两者同时成立。

4.3 周期驱动

流向两端采用匹配的周期设置,并通过相应源项或平均流动驱动实现目标。

仅仅写上cyclic,不会自动产生上一节的:

\\[ G=2\\ \\mathrm{Pa/m}. \\]

边界是否适定,取决于整组设置、方程和算法,不能简单概括为“任何场合都不许同时涉及速度和压力”。真实案例应参考锁定版本的完整教程。Foundation v13边界条件说明

五、上下壁面与二维empty边界,要分清位置

图中的上下边界是物理壁面,静止无滑移:

\\[ \\boldsymbol U=\\boldsymbol 0. \\]

展向的前、后两面用于二维约束,不是上下壁面。

OpenFOAM网格仍然具有三维体积。常见二维设置采用展向单层单元,并将该方向两面设置为empty,相应场文件也需要匹配。不能直接把厚度设成零,制造退化单元。Foundation v13网格定义说明

这里要区分三件事:

  • 网格中的walls是边界名称;
  • 网格的type wall描述边界类型;
  • 速度场中的noSlip表达本例静止壁面的无滑移条件。

给边界起名叫walls,不会自动替你补齐所有物理条件。

下面仅是0/U中的阅读片段,不是完整文件,也不是终端命令:

walls
{
type noSlip;
}

frontAndBack
{
type empty;
}

入口、出口、文件头部等内容尚未包含。

另外,zeroGradient表示法向梯度为零,不表示变量值为零。基本边界条件定义

六、阅读一份二维网格模板

本节材料提供了一个网格工具示例:

mesh_demo/
README.md
system/
blockMeshDict
controlDict

参考格式为Foundation v13。几何尺寸为:

\\[ 1\\ \\mathrm m \\times 0.01\\ \\mathrm m \\times 0.001\\ \\mathrm m. \\]

分块数为:

\\[ 1000\\times20\\times1. \\]

核心定义如下:

hex (0 1 2 3 4 5 6 7) (1000 20 1) simpleGrading (1 1 1)

按字典推算:

\\[ N_{\\mathrm{cells}} = 1000\\times20\\times1 = 20000, \\]\\[ \\Delta x=0.001\\ \\mathrm m, \\qquad \\Delta y=0.0005\\ \\mathrm m. \\]

这些是设计推算,不是已经执行的OpenFOAM日志。

如果已经有相应OpenFOAM的Linux或WSL环境,可以先复制模板到自己的新目录,再在该目录执行:

blockMesh
checkMesh

执行后应核查实际单元数、边界、维数、体积和质量,并保存真实日志。本材料没有执行这两条命令,因此不提供伪造的成功输出。

该目录没有完整的速度、压力、物性和数值配置,不能直接启动流动求解。模板中controlDict的deltaT=1仅供网格工具数据库初始化,不是推荐的CFD时间步。

比较流量时,还要考虑计算厚度

本模板的展向厚度为:

\\[ T=0.001\\ \\mathrm m. \\]

如果后处理输出整个计算出口的体积流量,则参考值应为:

\\[ Q_{\\mathrm{ref}} = q_{\\mathrm{ref}}T \\approx 1.666667\\times10^{-7}\\ \\mathrm{m^3/s}. \\]

要与单位宽度流量比较,应再除以\\(T\\)。

不能把\\(\\mathrm{m^3/s}\\)直接与上一节的\\(\\mathrm{m^2/s}\\)数值比较,也不能混入质量流量\\(\\mathrm{kg/s}\\)。

七、先做一个可以手算的积分误差实验

把上一节解析速度剖面分成\\(N\\)个区间:

\\[ \\Delta y=\\frac HN. \\]

用复合梯形公式计算单位宽度流量:

\\[ q_N = \\sum_{i=0}^{N-1} \\frac{u_i+u_{i+1}}2\\,\\Delta y. \\]

定义相对误差:

\\[ e_q = \\left| \\frac{q_N}{q_{\\mathrm{ref}}}-1 \\right|. \\]

计算结果为:

分段数 \\(N\\)\\(\\Delta y\\)/m积分相对误差
10 0.001 1.000000%
20 0.0005 0.250000%
40 0.00025 0.062500%
80 0.000125 0.015625%

对这条均匀采样的抛物线:

\\[ e_q=\\frac1{N^2}. \\]

所以,分段数翻倍,误差约缩小四倍。

由已知误差可以估计观察阶数:

\\[ p= \\frac{\\ln(e_N/e_{2N})}{\\ln2} \\approx2. \\]

但这只是本积分练习的观察阶数。

所有速度值都来自解析公式,程序没有在不同网格上重新求解Navier–Stokes方程。因此:

\\[ \\text{解析函数积分误差下降} \\neq \\text{真实CFD网格收敛}. \\]

真实CFD误差还可能来自空间离散、边界、迭代、时间推进和后处理,不能用这张表证明某个OpenFOAM模型已经二阶收敛。

八、时间步:一步之内,流动跨过多少单元?

一维直观估计为:

\\[ Co\\approx\\frac{|U|\\Delta t}{\\Delta x}. \\]

它称为Courant数。

先用一个独立教学速度:

\\[ U=0.1\\ \\mathrm{m/s}. \\]

这不是上一节基准的最大速度。

取:

\\[ \\Delta x=0.001\\ \\mathrm m, \\qquad \\Delta t=0.002\\ \\mathrm s. \\]

得到:

\\[ Co = \\frac{0.1\\times0.002}{0.001} = 0.2. \\]

如果课堂暂选目标:

\\[ Co\\le0.5, \\]

则估算:

\\[ \\Delta t \\le \\frac{0.5\\Delta x}{|U|} = 0.005\\ \\mathrm s. \\]

这里的0.5只是示例目标,不是所有格式和求解器的通用稳定条件。

网格加密后,原时间步未必还合适

情况\\(\\Delta x\\)/m\\(\\Delta t\\)/s\\(Co\\)估算
原设置 0.001 0.002 0.2
只把间距减半 0.0005 0.002 0.4
间距与时间步均减半 0.0005 0.001 0.2

空间和时间设置相互影响。

但减小时间步,不能修复错误壁面条件,也不能修复无效网格。

九、实际Co不能只靠一个全局速度代替

对于使用体积面通量的常见定义:

\\[ Co = \\frac{\\Delta t}{2V} \\sum_f|\\phi_f|. \\]

其中:

  • \\(V\\):单元体积;
  • \\(\\phi_f\\):面体积通量;
  • \\(f\\):单元各个面。

这个定义考虑局部网格和通量,比单个\\(U\\Delta t/\\Delta x\\)包含更多信息。OpenFOAM Courant数说明

实际计算应检查局部和最大Co,不能仅看平均速度,也不能机械地用一个最小边长代替完整的通量计算。

还要区分:

  • 数值稳定;
  • 时间误差足够小;
  • 非线性迭代充分;
  • 最终物理结果正确。

它们不是同一件事。

隐式格式也需要时间精度检查;稳态迭代次数也不能一概当成真实物理时间步。

回到上一节:

\\[ u_{\\max}=0.025\\ \\mathrm{m/s}. \\]

采用相同间距和时间步,一维估算为:

\\[ Co_{\\mathrm{estimate}} = \\frac{0.025\\times0.002}{0.001} = 0.05. \\]

在课堂目标0.5下,时间步上限估算为0.02 s。

这仍然是估算,不是某个未执行OpenFOAM案例的实测最大Co。

十、运行完整Python练习

请将两个文件放在同一目录:

lesson56_settings.py:

"""Analytical-profile quadrature and scalar Courant estimates, not a CFD solver."""
import argparse
import csv
import json
import math
from pathlib import Path

from lesson55_channel import channel_profile

def quadrature_study():
rows = []
for n in (10, 20, 40, 80):
summary, profile, _ = channel_profile(n=n)
dy = summary['parameters']['H_m']/n
q = math.fsum((a['u_x_m_per_s']+b['u_x_m_per_s'])*dy/2
for a, b in zip(profile[:-1], profile[1:]))
exact = summary['q_per_width_m2_per_s']
error = abs(q/exact-1)
order = math.log(rows[-1]['relative_error']/error)/math.log(2) if rows else None
rows.append({'N': n, 'dy_m': dy, 'q_trap_m2_per_s': q,
'q_exact_m2_per_s': exact, 'relative_error': error,
'observed_quadrature_order': order})
return rows

def courant_estimate(speed, dx, dt, target=.5):
for name, value in (('speed', speed), ('dx', dx), ('dt', dt), ('target', target)):
if (isinstance(value, bool) or not isinstance(value, (int,float))
or not math.isfinite(value) or value <= 0):
raise ValueError(name+' must be a finite positive number')
co, limit = speed*dt/dx, target*dx/speed
if not math.isfinite(co) or not math.isfinite(limit) or limit <= 0:
raise ValueError('derived Courant values invalid')
return {'speed_m_per_s': speed, 'dx_m': dx, 'dt_s': dt,
'Co_estimate': co, 'target_Co': target, 'dt_limit_s': limit}

def self_test():
rows = quadrature_study()
assert len(rows) == 4
for row in rows:
assert math.isclose(row['relative_error'], 1/row['N']**2, rel_tol=1e-9)
assert math.isclose(row['q_exact_m2_per_s'], 1/6000)
assert all(math.isclose(r['observed_quadrature_order'],2,abs_tol=1e-9) for r in rows[1:])
base = courant_estimate(.1,.001,.002)
assert math.isclose(base['Co_estimate'],.2) and math.isclose(base['dt_limit_s'],.005)
fine = courant_estimate(.1,.0005,.002)
assert math.isclose(fine['Co_estimate'],.4)
assert math.isclose(courant_estimate(.1,.0005,.001)['Co_estimate'],.2)
assert math.isclose(courant_estimate(.025,.001,.002)['Co_estimate'],.05)
for args in ((0,.001,.002),(.1,0,.002),(.1,.001,float('nan'))):
try:
courant_estimate(*args)
except ValueError:
pass
else:
raise AssertionError('invalid input accepted')
print('SELF_TEST_OK')

def write_csv(path, rows):
with path.open('x',newline='',encoding='utf-8-sig') as handle:
writer = csv.DictWriter(handle,fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)

def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('–speed',type=float,default=.1,help='positive advection speed, m/s')
parser.add_argument('–dx',type=float,default=.001,help='streamwise spacing, m')
parser.add_argument('–dt',type=float,default=.002,help='time step, s')
parser.add_argument('–co-target',type=float,default=.5,help='classroom target, not a universal rule')
parser.add_argument('–out',type=Path,help='new output directory only')
parser.add_argument('–self-test',action='store_true')
args = parser.parse_args()
if args.self_test:
self_test()
return 0
try:
rows = quadrature_study()
base = courant_estimate(args.speed,args.dx,args.dt,args.co_target)
cases = [dict(case='base',**base),
dict(case='half_dx_same_dt',**courant_estimate(args.speed,args.dx/2,args.dt,args.co_target)),
dict(case='half_dx_half_dt',**courant_estimate(args.speed,args.dx/2,args.dt/2,args.co_target))]
summary = {'mode':'ANALYTICAL_QUADRATURE_AND_SCALAR_COURANT_ESTIMATE',
'OpenFOAM_run':False, 'quadrature_source':'lesson55_channel.channel_profile',
'quadrature_is_CFD_mesh_convergence':False,
'courant_base':base, 'max_observed_quadrature_order':max(r['observed_quadrature_order'] for r in rows[1:])}
if args.out:
args.out.mkdir(parents=True,exist_ok=False)
write_csv(args.out/'quadrature.csv',rows)
write_csv(args.out/'courant_cases.csv',cases)
(args.out/'summary.json').write_text(json.dumps(summary,indent=2,allow_nan=False),encoding='utf-8')
for row in rows:
print(f'N={row["N"]}; integration_error={100*row["relative_error"]:.6f}%')
print(f'observed_quadrature_order={summary["max_observed_quadrature_order"]:.6f}')
print(f'Co={base["Co_estimate"]:.3f}; dt_limit={base["dt_limit_s"]:.6f} s')
print('MODE=QUADRATURE_AND_COURANT_ESTIMATE; OpenFOAM_NOT_RUN')
return 0
except (OSError,ValueError,ArithmeticError) as exc:
print('INPUT_OR_OUTPUT_ERROR: '+str(exc))
return 2

if __name__ == '__main__':
raise SystemExit(main())

lesson55_channel.py:

"""Analytical plane Poiseuille benchmark. No CFD equations are discretised or solved."""
import argparse
import csv
import json
import math
from pathlib import Path

def channel_profile(mu=0.001, gradient=2.0, n=20):
for name, value in (('mu', mu), ('gradient', gradient)):
if (isinstance(value, bool) or not isinstance(value, (int, float))
or not math.isfinite(value) or value <= 0):
raise ValueError(name + ' must be a finite positive number')
if type(n) is not int or not 2 <= n <= 10000:
raise ValueError('n must be an integer in [2, 10000]')
H, L, rho = 0.01, 1.0, 1000.0
nu = mu/rho
y = [H*i/n for i in range(n+1)]
u = [gradient*v*(H-v)/(2*mu) for v in y]
q = gradient*H**3/(12*mu)
mean, maximum = q/H, gradient*H**2/(8*mu)
summary = {'source': 'ANALYTICAL_PLANE_POISEUILLE', 'OpenFOAM_run': False,
'parameters': {'H_m': H, 'L_m': L, 'rho_kg_per_m3': rho,
'mu_Pa_s': mu, 'G_Pa_per_m': gradient},
'nu_m2_per_s': nu, 'U_mean_m_per_s': mean, 'U_max_m_per_s': maximum,
'q_per_width_m2_per_s': q, 'Re_Dh': mean*(2*H)/nu,
'delta_p_static_Pa': gradient*L,
'delta_p_kinematic_m2_per_s2': gradient*L/rho}
numeric = [nu, mean, maximum, q, summary['Re_Dh'], summary['delta_p_static_Pa'],
summary['delta_p_kinematic_m2_per_s2']] + u
if not all(math.isfinite(v) for v in numeric) or min(nu, mean, maximum, q) <= 0:
raise ValueError('derived values are not finite and positive')
checks = {'no_slip': u[0] == 0 and u[-1] == 0,
'symmetry': all(math.isclose(a, b, rel_tol=1e-12, abs_tol=1e-15)
for a, b in zip(u, reversed(u))),
'peak_to_mean': math.isclose(maximum/mean, 1.5, rel_tol=1e-12)}
if not all(checks.values()):
raise ValueError('analytical consistency checks failed')
summary['analytical_checks'] = checks
summary['profile_points'] = n+1
profile = [{'y_m': v, 'u_x_m_per_s': speed} for v, speed in zip(y, u)]
pressure = [{'x_m': L*i/4, 'p_static_Pa': gradient*(L-L*i/4),
'p_kinematic_m2_per_s2': gradient*(L-L*i/4)/rho} for i in range(5)]
return summary, profile, pressure

def self_test():
base, rows, pressure = channel_profile()
assert len(rows) == 21 and len(pressure) == 5
assert math.isclose(base['U_mean_m_per_s'], 1/60)
assert math.isclose(base['U_max_m_per_s'], .025)
assert math.isclose(base['q_per_width_m2_per_s'], 1/6000)
assert math.isclose(base['Re_Dh'], 1000/3)
assert math.isclose(pressure[0]['p_kinematic_m2_per_s2'], .002)
doubled, _, _ = channel_profile(gradient=4)
viscous, _, _ = channel_profile(mu=.002)
assert math.isclose(doubled['U_mean_m_per_s']/base['U_mean_m_per_s'], 2)
assert math.isclose(viscous['U_mean_m_per_s']/base['U_mean_m_per_s'], .5)
assert math.isclose(viscous['Re_Dh']/base['Re_Dh'], .25)
for kwargs in ({'mu': 0}, {'mu': -1}, {'gradient': float('nan')}, {'n': 0}):
try:
channel_profile(**kwargs)
except ValueError:
pass
else:
raise AssertionError('invalid input accepted')
print('SELF_TEST_OK')

def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('–mu', type=float, default=.001, help='dynamic viscosity, Pa s')
parser.add_argument('–gradient', type=float, default=2, help='G=-dp_static/dx, Pa/m')
parser.add_argument('–out', type=Path, help='new output directory; existing directory rejected')
parser.add_argument('–self-test', action='store_true')
args = parser.parse_args()
if args.self_test:
self_test()
return 0
try:
summary, profile, pressure = channel_profile(mu=args.mu, gradient=args.gradient)
if args.out:
args.out.mkdir(parents=True, exist_ok=False)
(args.out/'summary.json').write_text(json.dumps(summary, indent=2, allow_nan=False), encoding='utf-8')
for name, rows in (('profile.csv', profile), ('pressure.csv', pressure)):
with (args.out/name).open('x', newline='', encoding='utf-8-sig') as handle:
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
print('MODE=ANALYTICAL; OpenFOAM_NOT_RUN')
print(f'nu={summary["nu_m2_per_s"]:.9e} m2/s')
print(f'U_mean={summary["U_mean_m_per_s"]:.6f} m/s; U_max={summary["U_max_m_per_s"]:.6f} m/s')
print(f'q_per_width={summary["q_per_width_m2_per_s"]:.9f} m2/s; Re_Dh={summary["Re_Dh"]:.3f}')
print(f'delta_p_static={summary["delta_p_static_Pa"]:.6f} Pa; delta_p_kinematic={summary["delta_p_kinematic_m2_per_s2"]:.9f} m2/s2')
print('ANALYTICAL_CHECKS=PASS')
return 0
except (OSError, ValueError, ArithmeticError) as exc:
print('INPUT_OR_OUTPUT_ERROR: ' + str(exc))
return 2

if __name__ == '__main__':
raise SystemExit(main())

主程序只使用Python标准库。

10.1 执行自检

python lesson56_settings.py –self-test

预期输出:

SELF_TEST_OK

10.2 运行练习

python lesson56_settings.py

实测输出:

N=10; integration_error=1.000000%
N=20; integration_error=0.250000%
N=40; integration_error=0.062500%
N=80; integration_error=0.015625%
observed_quadrature_order=2.000000
Co=0.200; dt_limit=0.005000 s
MODE=QUADRATURE_AND_COURANT_ESTIMATE; OpenFOAM_NOT_RUN

其中:

  • 积分使用上一节的物理基准;
  • Co默认速度采用独立教学值0.1 m/s;
  • 输出明确标注未运行OpenFOAM。

10.3 保存数据

python lesson56_settings.py –out run56_01

控制台数值不变,同时生成:

run56_01/
quadrature.csv
courant_cases.csv
summary.json

课程包已经包含reference_run样本。重新运行时请使用新目录,已有输出目录不会被覆盖。

10.4 使用上一节最大速度

python lesson56_settings.py –speed 0.025

关键输出:

Co=0.050; dt_limit=0.020000 s

10.5 只把流向间距减半

python lesson56_settings.py –dx 0.0005

关键输出:

Co=0.400; dt_limit=0.002500 s

这两条命令仍会打印相同的积分误差表,因为它们只改变Co估算输入。

十一、查看实际结果图

左图展示解析速度采样的梯形积分误差。

右图展示教学速度0.1 m/s下,间距和时间步变化对Co估算的影响。

红色虚线是课堂示例目标,不是普遍安全线。

需要重新生成两张配图时,将plot_lesson56.py与时间步plot_lesson56.py放在同一目录,在安装了Matplotlib的环境运行:

python plot_lesson56.py –data run56_01 –out figures56_01

预期输出:

FIGURES_OK: lesson56_error_and_courant.png, lesson56_boundary_map.png

没有Matplotlib也不影响主程序,可以直接阅读材料中的配图。

十二、怎样设计真正的空间与时间研究?

下面是一份研究计划,不是已经计算完成的CFD结果。

网格\\(N_x\\times N_y\\times N_z\\)单元数\\(\\Delta x\\)/m\\(\\Delta y\\)/m
G1 500×10×1 5000 0.002 0.001
G2 1000×20×1 20000 0.001 0.0005
G3 2000×40×1 80000 0.0005 0.00025

每张网格都需要重新求解,并提取同一指标,例如:

  • 单位宽度流量;
  • 同一截面的速度剖面误差;
  • 壁面剪应力。

指标还要结合驱动方式选择。如果压力差本来就是指定的输入,仅看到它在各工况相同,不能证明解已经收敛。

空间研究

保持几何、物性、边界和提取方式一致,控制迭代与时间误差。

如果随网格加密同时缩小时间步,结果变化可能混合了空间和时间影响,不能全部归因于网格。

时间研究

固定空间网格,比较不同时间步在相同物理时刻的响应。

如果要与稳态解析解比较,还应确认已经达到稳态。采用小时间步,不等于已经充分发展或达到稳态。

建议至少用三组有规律的设置检查趋势。估计观察阶数或外推误差时,还要检查加密方式、渐近区和结果行为,不能机械套用本节积分实验的二阶结论。

十三、课后练习与验收

练习一:继续增加分段数

当:

\\[ N=160 \\]

时,积分误差预计是多少?

参考答案:

\\[ e_q = \\frac1{160^2} = 0.00390625\\%. \\]

练习二:保持Co不变

固定速度,将流向间距减半,时间步应该怎样变化?

参考答案:在本节一维估算中,时间步也应减半。

练习三:稳定结果是否一定正确?

壁面误设为滑移,网格加密后结果非常稳定,可以认为正确吗?

参考答案:不能。它可能收敛到了另一个物理问题。

练习四:网格检查是否替代边界检查?

checkMesh通过以后,是否还要核对U、p的边界设置?

参考答案:要。网格检查不等于完整的物理与数值验收。

本节交付

准备两份表:

  • 已运行的积分误差表。
  • 尚待真实求解的CFD空间—时间研究计划。
  • 能够区分以下三类信息,也是验收要求:

    实测Python输出
    字典设计推算
    尚未执行的OpenFOAM操作

    十四、本节小结与下一节

    可靠的CFD设置,不是简单地把网格加密、时间步缩小、残差压低,而是让每个设置服务于同一个正确物理问题。

    本节最重要的认识是:

    网格质量、边界正确性、空间误差、时间误差和迭代误差,需要分别检查,不能彼此替代。

    下一节将按照课程目录,进入“OpenFOAM工作流自动化”,把网格生成、网格检查、求解、后处理和停止条件组织起来。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【零基础学智能仿真-56】CFD网格、边界条件与时间步
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!