02 二维槽道流案例注解

上一个学习案例是溃坝。在test_2d_channel_flow_fluid_shell案例中将学习到的知识点有:

  • 自己写一个ParticleGenerator

  • 流入速度设定

  • defineMaterialdefineClosure的区别

  • 周期性边界

  • 简单的流壳耦合(壁面只有单层粒子,但是壁面固定)

  • SimpleDynamics对象是如何构造和执行的(深入探究源码)

首先看一下结果。在模拟的最后一个时间步,能从轴向速度和原始ID的云图看到速度已经在整个域内形成了抛物线分布:

命令行参数

这个案例与二维溃坝的一大区别在于使用了google test,没有用回归测试。所以命令行参数是google test的参数。这里省略。

全局参数

几何定义

这里定义了水体的形状类,它继承于多边形类MultiPolygonShape,底层是用boost geometry实现的。用户需要为构造函数传入两个参数:储存多个矢量的vector(多边形的顶点集)和形状名称。channel_flow_shell函数中的createWaterBlockShape就生成了所需要的vector。

生成shell粒子一般有两种方法,第一种如同溃坝案例中一样:先定义shell的几何,然后用generateParticles<SurfaceParticles, Lattice>()生成粒子,见ball_shell_collision.cpp。第二种就是这里的方法:不定义几何(即采用默认几何),自己规定粒子位置和体积,因为是shell,我们还需要规定其表面性质,包括法方向和厚度。

具体而言,这里写了一个特化的ParticleGenerator<SurfaceParticles, Style>模板,它继承于ParticleGenerator<SurfaceParticles>WallBoundary只有声明,没有定义,但是不需要定义,因为它的作用只是将此特化的ParticleGeneratorsrc/中的ParticleGenerator区分开来。resolution_ref_是粒子间距ΔL\Delta LDL_sponge是施加流入条件的宽度(20ΔL20\Delta L);BW是壁面比水体多出的宽度(4ΔL4\Delta L);wall_thickness_是壁面(壳层)厚度。

我们将ParticleGeneratorprepareGeometricData()进行了覆盖。在函数体中,首先统计了有多少对壁面颗粒(一个上壁面+一个下壁面颗粒称为一对)。对于每一对颗粒,它的横坐标是-DL_sponge_ - BW_ + (Real(i) + 0.5) * resolution_ref_。这里之所以要给Real(i)加上0.5,是为了和流体粒子对齐,流体粒子将用lattcie生成,它们是在网格中心的。y1y2分别将上壁面向上平移ΔL/2\Delta L/2和下壁面向下平移ΔL/2\Delta L/2addPositionAndVolumetricMeasure的作用是添加位置信息和体积信息,二维模拟的shell是一维的,所以体积就是ΔL\Delta LaddSurfaceProperties的作用是添加壁面法向和厚度信息。

另外,这里将特例化的ParticleGenerator<SurfaceParticles, Style>模板定义在了SPH名字空间中。这是因为ParticleGenerator<SurfaceParticles, Style>本身就是定义在SPH名字空间的,所以它的任何显式特例化模板也必须定义在SPH名字空间中。假如把它定义在SPH名字空间之外,就会报错:

流入速度

流入速度显然是针对入口而言的,我们只需对入口处的粒子设置速度即可,这个入口正是几何中x负半轴的部分,称之为流入缓冲区(inflow buffer)。但是water_block是一个整体,如何告诉程序取其中的一部分呢?这就涉及body part的概念。body part是body的一部分。它可以让我们只模拟给定body的指定区域的粒子。这在程序中体现为AlignedBoxByCell。以下为流入速度设定的全部代码:

程序设计了一个类InflowVelocity。它的构造函数需要一个BoundaryConditionType对象boundary_condition作为参数,从boundary_condition中,先获取aligned_box_,再获取后者的halfsize。在main函数调用中可以看出,此aligned box就是AlignedBox(xAxis, Transform(Vec2d(buffer_translation)), buffer_halfsize),它就是inflow buffer。xAxis是定义在base_data_type.h中的常量,类似还有yAxiszAxisAlignedBox类与GeometricBox类很像,只不过AlignedBox多了一个对齐轴(alignment axis),所有“上/下边界、周期映射、近边界判断”等操作都沿着这个对齐轴来做。

InflowVelocity的函数调用运算符被重载了,因此它实际上会生成一个函数对象。它有三个形参:位置、速度、当前时间。u_ave是随时间变化的平均速度,形式如下:

uˉ={0.5uref[1cos(πt/tref)],t<tref;uref,ttref.\bar{u}=\begin{cases} 0.5u_\mathrm{ref}[1-\cos(\pi t/t_\mathrm{ref})], &t < t_\mathrm{ref};\\ u_\mathrm{ref}, &t\geq t_\mathrm{ref}. \end{cases}

这个函数使得平均速度从0平滑过渡到最大值。

接着检测给定点是否在aligned box(inflow buffer)内,如果是的话,则该位置非水平方向速度为原值(Vecd target_velocity = velocity;),水平速度为

ux=1.5uˉ[1y2(DH/2)2].u_x=1.5\bar{u}\left[1-\frac{y^2}{\mathrm{(DH/2)}^2}\right].

这是典型的抛物线分布,说明将入口流动设定为充分发展的流动。

定义完InflowVelocity类后,用water_block和aligned box定义了一个AlignedBoxByCell对象inflow_buffer。它用来初始化SimpleDynamics<fluid_dynamics::InflowVelocityCondition<InflowVelocity>>对象。使用AlignedBoxByCell的目的是允许程序在遍历时只遍历inflow buffer的粒子,而非体系所有粒子,以提高计算效率。使用SimpleDynamics是因为流入速度不涉及颗粒交互。只需要update即可。

SimpleDynamics是如何构造的

让我们分析一下SimpleDynamics<fluid_dynamics::InflowVelocityCondition<InflowVelocity>> parabolic_inflow(inflow_buffer);这行定义会在底层做些什么。这里模板参数是嵌套的。

1. 开始构造SimpleDynamics

首先会调用最外层SimpleDynamics的构造函数:

LocalDynamicsType是用户显式指定的InflowVelocityCondition<InflowVelocity>ExecutionPolicy保持默认值。SimpleDynamics继承于LocalDynamicsTypeInflowVelocityCondition)和BaseDynamics<void>。于是此类分别获得了InflowVelocityCondition<InflowVelocity>updatesetupDynamics成员函数和BaseDynamics<void>execsetUpdated成员函数。

inflow_buffer作为参数传入SimpleDynamics的构造函数,因此DynamicsIdentifier被推断为AlignedBoxByCellargs为空参数包。

然后进入成员初始化列表。首先会调用基类构造函数。因为多重继承于两个基类,调用基类构造函数应按照派生列表中基类的出现顺序。因此,先调用InflowVelocityCondition<InflowVelocity>的构造函数。

2. 开始构造InflowVelocityCondition<InflowVelocity>

fluid_dynamics::InflowVelocityCondition模板的构造函数如下:

类具有一个模板参数TargetVelocity类型的成员target_velocity。在这里,我们显式指定了TargetVelocityInflowVelocity。在初始化列表中,inflow_buffer被传入了基类构造函数。

3. 开始构造BaseFlowBoundaryCondition

用传入的inflow_buffer构造BaseFlowBoundaryConditionBaseFlowBoundaryCondition继承于BaseLocalDynamics<BodyPartByCell>。在构造BaseLocalDynamics<BodyPartByCell>时会传入inflow_buffer

DynamicsIdentifier被推断为BodyPartByCell类型,而也就是说identifier_是一个绑定到BodyPartByCell类型的AlignedBoxByCell对象。sph_body_被初始化为inflow_buffersph_body_。从inflow_buffer定义中可以获知它就是water_block

4. BaseFlowBoundaryCondition构造完毕

然后初始化relaxation_rate(默认),用传入的inflow_buffer的成员初始化aligned_box_transform_halfsize_,接着用自身初始化target_velocity。此时InflowVelocity的构造函数被调用。

5. 开始构造InflowVelocity

InflowVelocity的构造函数接受一个BoundaryConditionType的引用类型,显然它就是部分初始化的InflowVelocityCondition。这不会造成递归构造,因为是传引用调用,而非传值调用。

6. InflowVelocity构造完毕

target_velocity初始化完毕后,获取了系统的物理时间。这个物理时间也就是InflowVelocity中要用的current_time

7. InflowVelocityCondition<InflowVelocity>构造完毕

回到SimpleDynamics的构造函数,此时应该构造第二个基类。

8. 开始构造BaseDynamics<void>

9. BaseDynamics<void>构造完毕

此时SimpleDynamics的成员初始化列表任务完成,进入构造函数体。检查InflowVelocityCondition<InflowVelocity>是否具有initializeinteraction类,如果有,编译报错。因为InflowVelocityCondition<InflowVelocity>有的是update成员函数,所以不会报错。

10. SimpleDynamics构造完毕

parabolic_inflow是如何执行的

1. 调用SimpleDynamics<…>.exec()

在模拟时,每隔一段时间会调用一次parabolic_inflow.exec(),也即调用以下函数:

dt采用默认值0.0。函数体中,先调用this->setUpdated,这来自BaseDynamics<void>。参数this->identifier_.getSPHBody()被解析为water_block

2. 调用BaseDynamics<void>::setUpdated()

首先调用了water_blocksetNewlyUpdated(),然后将自己的is_newly_updated_设置为true

3. 退出BaseDynamics<void>::setUpdated()

然后调用this->setupDynamics(dt);。因为InflowVelocityCondition<...>BaseFlowBoundaryCondition都没有定义自己的setupDynamics,因此调用基类的成员函数。

4. 调用BaseLocalDynamics::setupDynamics(dt)

什么也不做。

5. 退出BaseLocalDynamics::setupDynamics(dt)

重点在于particle_for这一段。首先ExecutionPolicy采用的是默认的ParallelPolicythis->identifier_.LoopRange()调用BodyPartByCell对象的LoopRange()方法,它返回的是ConcurrentCellLists对象。

参数local_dynamics_function是一个lambda函数对象[&](size_t i) { this->update(i, dt); })[&]以引用捕获方式捕获了exec函数体中所有变量,包括dtthis指针。它接受粒子索引i作为参数,在函数体中调用InflowVelocityCondition<InflowVelocity>update成员函数。不用看particle_for源码我们就能猜出来,遍历inflow_buffer的每个粒子,对其调用this->update函数,采取并行模式。从这里可以看出来,使用AlignedBoxByCell对象的好处在于不用遍历water_block每个粒子。

体系定义

创建SPHBody

在二维溃坝案例中,我们使用water_block.defineMaterial<WeaklyCompressibleFluid>(rho0_f, c_f);来定义材料属性。这里却没有用defineMaterial,而是用了defineClosure。这是因为除了WeaklyCompressibleFluid还有ViscositydefineMaterialdefineClosure的核心区别是:一个只创建“单一材料模型(BaseMaterial派生类)”,另一个创建“物理闭包(Closure)”,把多个材料/本构/附加模型组合成一个整体材料对象。在 SPHinXsys 里二者最终都会把SPHBody::base_material_指向你创建的对象,但对象类型和用途不同。WeaklyCompressibleFluid需要两个参数:密度和声速,Viscosity需要一个参数:mu_f。为了让编译器知道哪些参数是传给WeaklyCompressibleFluid的,我们需要用ConstructArgsrho_fc_f打包。

Solid这种材料在创建时也是需要参数的,只不过这里使用了默认参数:密度1.0,接触刚度1.0,接触摩擦系数1.0。不过这个案例中也没有用到这些参数——你可以把密度改为任意非零值,接触刚度和接触摩擦系数改为任意值,然后会发现结果没变。当壁面运动时,这些参数才会起作用。

定义关系

shell_boundary_inner似乎是个多余的关系,后面并没有调用。shell_curvature_inner后面被调用了,但是壁面是固定的,所以曲率恒为零,所以这个关系其实也多余。我尝试把相关代码注释掉,发现结果没有变化。

壳体与流体的关系有两种:

  • 一种是ContactRelationFromShellToFluid。它“主体”是流体,更新的是每个流体粒子的contact neighbor list。在邻域构建时,会把接触体当作shell,使用shell的NormalDirection/Thickness/Average1stPrincipleCurvature等信息,对核函数权重做“沿法向dummy外推”的修正。因此,ContactRelationFromShellToFluid构造函数的第一个参数是主体water_block,第二个参数是接触体vector{&wall_boundary},第三个参数指定每一个接触体的法向是否要修正。接触体的法向应当从流体指向壁面,如果需要修正,这里输入true,告诉程序把壁面法向反向。这里,因为我们已经在ParticleGenerator<SurfaceParticles, WallBoundary>中正确定义了方向,所以这里填false

  • 另一种是ContactRelationFromFluidToShell,刚好反过来。“主体”是壳体:更新的是每个壳粒子的 contact neighbor list。用途是壳体侧要从流体取信息/受力(例如壳体受流体压力、FSI耦合时的力学更新等)。

这里显然我们应该用ContactRelationFromShellToFluid

定义数值方法

fluid integration

fluid integration与二维溃坝一致,在此不赘。

density summation

密度求和采用的是DensitySummationComplex,它实际上是BaseDensitySummationComplex<Inner<>, Contact<>>;。二维溃坝中采用的是DensitySummationComplexFreeSurface。因为这里没有自由表面,所以采用了最基础的密度求和方式。

时间步控制

时间步计算和二维溃坝基本相似,只不过对流时间步计算加入了对黏度的考虑,变成以下形式:

Δtad=CFLadmin{hminvmax,hmin4amax,hminvref,hmin2ν}\Delta t_\mathrm{ad}=\mathrm{CFL_{ad}}\min\left\{\frac{h_\mathrm{min}}{|v|_\mathrm{max}},\sqrt{\frac{h_\mathrm{min}}{4|a|_\mathrm{max}}},\frac{h_\mathrm{min}}{|v|_\mathrm{ref}},\frac{h_\mathrm{min}^2}{\nu}\right\}

Transport Velocity Correction

类似于particle shifting,传递速度修正是另一种使得粒子均匀分布的技术,可以缓解拉伸不稳定性的问题。这里采用的TransportVelocityCorrectionComplex<AllParticles>是一种较基础的类型,它是BaseTransportVelocityCorrectionComplex<SingleResolution, NoLimiter, LinearGradientCorrection, AllParticles>的别名,也即它采用的是单一分辨率、没有限制修正幅度、线性核梯度修正、对所有颗粒执行。

黏性力

ViscousForceWithWall是最常见的“流体粘性+无滑移壁面”组合,它展开为ComplexInteraction<ViscousForce<Inner<>, Contact<Wall>>, FixedViscosity, NoKernelCorrection>,也即它采用的是常数黏度、没有核梯度修正。

周期性边界

周期性边界的定义

以下两行代码定义了x方向上的周期性边界条件。PeriodicAlongAxis定义了一个周期性盒子,其构造函数第一个参数是bounding box,第二个参数是在哪个方向上采用周期性边界。PeriodicConditionUsingCellLinkedList定义了周期性边界相关的CLL。注意这里周期边界是按照单一轴来配置的,如果想在多个轴上均采用周期边界,需要写多个PeriodicAlongAxis+PeriodicConditionUsingCellLinkedList

周期性边界的初始化

先初始化CLL,再世家周期性边界条件,最后初始化系统配置(包括建立邻居表)。

water_block_complex.updateConfiguration()的作用不明确,毕竟之前刚sph_system.initializeSystemConfigurations(),这里就update了。除非shell_curvature.exec()内部会改变粒子位置/法向/需要重建接触列表(一般曲率计算不移动粒子),否则这句多半可以省。

周期性边界的执行

先bounding(把越界粒子搬回周期域内)、然后更新链表、再插入ghost(边界附近往CLL插入ghost粒子),最后更新邻居表。

粒子排序

与二维溃坝类似,这里只对water_block的粒子进行了排序。为什么我们没有对壁面进行粒子排序呢?排序是为了解决粒子在计算域里大范围移动(多是对流迁移所致)导致的邻域搜索/Cell Linked List退化,而这个问题在多数案例里主要出现在流体上,壳/壁这种结构体通常不会同样严重。且不说本例中壁面是固定不动的,就算是许多壁面运动的案例(test_2d_elastic_gate、test_2d_T_pipe_VIPO_shell),也不会对壁面进行粒子排序。如果流体是几乎不动的(test_2d_hydrostatic_fluid_shell),甚至无需对流体进行粒子排序。

文件输出与时间设定

定义文件输出

首先获取了模拟的物理时间,然后初始化迭代次数为零,设定每100步在屏幕上输出一次。总共模拟10秒。每0.05秒输出一次粒子信息。

主循环

外循环

在外循环中,输出粒子信息,更新观测粒子的邻居表,输出观测数据。这一段的用时记录在t3-t2中。

中循环

中循环也是dual-criteria timestepping的外循环。首先获取了对流时间步长。然后依次进行密度求和、更新黏性力、传输速度修正(见Schemes for fluid dynamics — SPHinXsys documentationarrow-up-right)。初始化内循环的迭代次数inner_ite_dt为0,初始化松弛时间为0。接着在屏幕上打印了中层循环的而迭代次数、物理时间、中层循环的时间步长(对流时间步长)、内层循环迭代了多少次。最后更新周期性边界、CLL和邻居表,之前已经分析过。

内循环

首先获取了内循环的时间步长(声学时间步长),然后依次进行了动量方程的求解、入口速度的施加、连续性方程的求解。这个顺序是有讲究的。动量方程会更新一步速度,然后才能施加入口速度的条件,这样确保了入口速度是我们所指定的。而连续性方程求解中需要用到速度,因此有必要把入口速度施加放在连续性方程更新之前,确保了使用正确的入口速度来求解连续性方程。然后更新各种时间和内循环迭代轮数。

打印耗时

主循环结束后,打印计算的耗时,这部分耗时除去了IO的时间。

观测数据与模拟验证

生成ObserverBody的粒子需要传入观测点。我们需要首先定义这些观测点的位置,放在标准库vector中(StdVec)。轴向观测点均布位于x轴正半轴的水体的对称线上。径向观测点均布于x=DL/2x=\mathrm{DL}/2处的垂直线上。createFluidAxialObservationPoints(resolution_ref)返回的是储存所有观测点坐标的StdVec,它用来生成观测粒子。

在每次写入观测点数据之前,需要更新其邻居表。

模拟结束时,用google test验证模拟结果。首先定义了解析解。这里使用lambda函数的原因是函数体中不能定义新的普通函数。为了获取观测点的速度,我们首先通过getBaseParticles()获取观测点粒子。通过BaseParticlesParticlePositions获取颗粒位置,它应该和createFluidAxialObservationPoints返回的没有区别,然后通过BaseParticlesgetVariableDataByName<Vecd>("Velocity")获取粒子速度。接着将每一个观测点的速度与理论值进行比较,规定容差为0.05Uf0.05U_f

这个案例使用Google test做验证,main函数中的RUN_ALL_TESTS()会运行所有TEST中的测试。TEST第一个参数是测试的函数名,第二个参数是本轮测试的名字。

Last updated

Was this helpful?