<think>我们正在使用YADE 2022.01a版本。要生成一个圆柱体容器并在其中填充颗粒,我们需要以下步骤:
1. 创建圆柱形容器(作为墙体)。
2. 在圆柱体内生成颗粒堆积。
注意:YADE中,圆柱形容器可以通过创建圆柱体侧壁和两个端盖(顶盖和底盖)来实现。通常,我们使用墙体(Wall)来定义容器边界。
步骤:
1. 导入必要的模块。
2. 定义圆柱体的几何参数(半径、高度)和位置。
3. 创建圆柱体的侧壁:实际上,在YADE中,我们可以使用多个墙体来近似圆柱形容器,但更简单的方式是使用圆柱体几何体(如Cylinder)并设置其为墙体。然而,YADE的墙体通常用无限平面(Wall)或由多个平面围成的区域。对于圆柱形容器,我们可以使用圆柱形的墙体(Cylinder),但注意YADE中的Cylinder类可以表示一个圆柱体,并且可以设置为固定的墙体。
具体方法:
- 使用`yade.wall.Cylinder`来创建圆柱体侧壁。注意,这个圆柱体是无限长的,所以我们需要两个平面(作为顶盖和底盖)来封闭它。
- 创建两个圆盘(作为顶盖和底盖),我们可以使用`yade.wall.Disk`或者用两个无限平面(Wall)来模拟。但是,由于圆柱体是轴对称的,通常我们使用两个与圆柱体轴线垂直的平面来作为顶和底。
另一种方法是使用三个墙体:
a) 一个圆柱体侧壁(Cylinder),其轴线沿z轴,半径为R,位置在原点(或指定位置)。
b) 一个底盖(Wall),位于z=0处,法向量为(0,0,1) [注意法向量方向指向容器内部]。
c) 一个顶盖(Wall),位于z=height处,法向量为(0,0,-1)。
注意:墙体的法向量指向容器外部,这样颗粒就会在法向量指向的一侧被阻挡。但是,在YADE中,墙体默认是单向的,即法向量方向为墙体的正方向,颗粒从正方向穿过墙体会被阻挡,而从反方向则不会。因此,对于容器,我们通常将法向量指向容器内部,这样颗粒在容器内部碰撞到墙体时会被弹回。
然而,对于圆柱体侧壁(Cylinder),其法向量是径向向外的(即从轴线指向外),所以如果我们想要颗粒在圆柱体内部,那么圆柱体应该被设置为容器内壁,即我们需要将圆柱体的法向量方向反转(或者使用负半径)?实际上,YADE的Cylinder类可以通过设置`sense`参数来控制方向,但默认是外部(即内部是空的,外部是实体)。所以,如果我们想要一个圆柱形空腔,那么圆柱体墙体的法向量是向外的,颗粒在圆柱体外部会被阻挡,而在内部则不会。因此,我们需要将圆柱体墙体的方向反转,使得内部成为实体,外部为空?这不符合我们的需求。
实际上,我们想要的是颗粒在圆柱体内部,所以我们需要一个空腔。因此,我们应该使用一个圆柱体,其内部是空的,外部是实体,这样颗粒在圆柱体内部就不会受到墙体的阻挡(因为墙体的作用在外部)。这显然不对。
正确理解:在YADE中,圆柱体墙体(Cylinder)是一个无限长的圆柱面。它有两个面:内表面和外表面。默认情况下,圆柱体墙体的作用是在圆柱面的外部(即半径大于圆柱半径的区域)是实体,而内部是空的。因此,如果我们想要一个圆柱形容器,我们需要将颗粒限制在圆柱体内部,那么我们应该将圆柱体墙体的作用域设置为内部(即颗粒在圆柱体内部时不受此墙体影响,而一旦要穿出圆柱体则被阻挡)。但实际上,默认的圆柱体墙体是阻挡外部颗粒的,所以我们需要一个反向的圆柱体墙体。
解决方案:使用`Cylinder`类创建时,我们可以通过设置`sense`参数来控制作用方向。`sense`参数可以取0(双向),1(只阻挡从内到外,即法向量方向),-1(只阻挡从外到内)。但是,我们想要的是颗粒不能从内部穿出,所以我们应该设置`sense=1`(法向量方向为从轴线指向外,所以颗粒从内部(法向量反方向)穿到外部(法向量方向)时被阻挡)。但注意,默认的圆柱体墙体法向量是向外的,所以当颗粒从内向外运动(即从圆柱体内部穿过圆柱面到外部)时,会被墙体阻挡。这正是我们想要的。
因此,步骤为:
1. 创建圆柱体侧壁:`Cylinder(center=(0,0,h/2), axis=(0,0,1), radius=R, length=h, fixed=True, sense=1)`
注意:这里我们假设圆柱体轴线沿z轴,中心在(0,0,h/2),半径为R,高度为h。但是,实际上Cylinder类只表示一个圆柱面,没有高度限制(无限长),所以我们需要用两个端盖来限制高度。
2. 创建底盖:位于z=0,法向量为(0,0,1)(指向z轴正方向,即容器内部上方)。这样,当颗粒从z=0平面以下向上运动(即从容器内部向外部)时,会被底盖阻挡?实际上,我们想要颗粒在容器内部,所以底盖应该阻止颗粒向下运动(即z负方向)。因此,底盖的法向量应该指向容器内部(即向上),这样当颗粒从上方(容器内部)撞击底盖时,会被弹回。但是,墙体的作用是在法向量指向的一侧阻挡颗粒。因此,底盖应该这样设置:位置在(0,0,0),法向量(0,0,1)(向上),这样颗粒在底盖下方(z<0)时,如果向正方向(z正方向)运动,穿过墙体时不会被阻挡(因为墙体的阻挡是对于从法向量反方向穿过到正方向的情况)。这似乎不符合我们的需求。
实际上,在YADE中,墙体(Wall)的阻挡规则是:当颗粒从墙体的背面(法向量反方向一侧)穿过到正面(法向量方向一侧)时,会被阻挡。因此,对于底盖,我们想要阻挡颗粒掉到底面以下(即z<0),所以我们应该设置底盖的位置在z=0,法向量为(0,0,1)(指向z正方向)。这样,颗粒在z=0以下(背面)时,如果向z正方向运动(穿过墙体到正面),就会被阻挡。同时,颗粒在容器内部(z>0)时,向z负方向运动(穿过墙体到背面)则不会被阻挡(因为墙体是单向的,只阻挡从背面到正面的运动)。这显然不是我们想要的。
正确的做法是:我们想要颗粒在容器内部(0<z<h)中,所以底盖应该阻挡颗粒向下(z负方向)运动,即当颗粒到达z=0时,不能再向下。因此,底盖应该设置为:位置在z=0,法向量为(0,0,1)(向上),并且设置`sense=1`(默认),这样墙体只阻挡从背面(z<0)到正面(z>0)的运动,而不阻挡从正面到背面的运动。但这样设置,颗粒从容器内部(z>0)向下穿过底盖(到z<0)时不会被阻挡。所以我们需要一个反向的阻挡。
因此,我们需要将底盖的法向量设置为(0,0,-1)(向下),并且设置位置为(0,0,0)。这样,墙体背面是z>0(法向量反方向),正面是z<0。当颗粒从容器内部(z>0,背面)向下运动穿过墙体(到z<0,正面)时,就会被阻挡。同时,颗粒从外部(z<0)向上运动(穿过墙体到z>0)则不会被阻挡(因为我们只关心容器内部,外部颗粒可以忽略)。所以,底盖应该这样创建:`Wall(position=(0,0,0), axis=(0,0,-1), fixed=True)`。
同样,顶盖应该设置在z=h处,法向量为(0,0,1)(向下?不对,应该是向上?)。我们想要阻挡颗粒向上运动超过z=h,所以顶盖应该阻挡颗粒从z=h以下向上运动(超过z=h)。因此,顶盖的位置在(0,0,h),法向量为(0,0,1)(向上),这样当颗粒从容器内部(z<h,背面)向上运动穿过顶盖(到z>h,正面)时,会被阻挡。所以顶盖:`Wall(position=(0,0,h), axis=(0,0,1), fixed=True)`。
但是,这样设置顶盖的法向量向上,那么背面就是z<h,正面就是z>h。颗粒从背面(z<h)穿过到正面(z>h)时被阻挡,符合要求。
总结墙体:
- 圆柱体侧壁:`Cylinder(center=(0,0,h/2), axis=(0,0,1), radius=R, length=h, fixed=True, sense=1)`
注意:Cylinder的创建参数中,center是中心坐标,axis是轴线方向向量,radius是半径,length在这里其实没有用(因为Cylinder是无限长的),所以我们只需要半径和轴线位置。实际上,Cylinder类只需要中心点、轴线方向和半径即可。高度由两个端盖控制。
- 底盖:`Wall(position=(0,0,0), normal=(0,0,-1))`,法向量向下,位置在(0,0,0)
- 顶盖:`Wall(position=(0,0,h), normal=(0,0,1))`,法向量向上,位置在(0,0,h)
3. 生成颗粒:在圆柱体容器内生成颗粒堆积。我们可以使用YADE的填充函数,例如`yade.pack.randomPeriPack`或`yade.pack.SpherePack`来生成球体颗粒,然后将其添加到模拟中。
具体步骤:
a) 创建一个`SpherePack`对象。
b) 在圆柱体区域内生成球体。我们可以使用`SpherePack.makeCloud`方法,指定生成区域为圆柱体(注意:圆柱体区域可以用一个立方体区域来近似,然后过滤掉圆柱体外的球体,但这样效率低)。或者,我们可以使用`makeCylinder`方法(注意:SpherePack类中并没有直接生成圆柱体内颗粒的方法,但我们可以用`makeCloud`并指定边界为圆柱体)。
我们可以这样定义圆柱体区域:中心在(0,0,h/2),半径R,高度h。所以生成颗粒的范围:x和y在[-R,R]内,z在[0,h]内。但是,我们需要确保颗粒在圆柱体内部,所以我们可以先生成一个立方体区域的颗粒,然后过滤掉圆柱体外的颗粒。
示例代码:
```python
sp = SpherePack()
# 生成一个立方体区域内的颗粒,然后过滤
sp.makeCloud(minCorner=(-R, -R, 0), maxCorner=(R, R, h), rMean=rMean, rRelFuzz=rFuzz, num=numParticles, periodic=False)
# 过滤:只保留在圆柱体内的颗粒(即颗粒中心到z轴的距离小于R,且z在[0,h]内)
# 注意:我们生成时已经限制了z在[0,h],所以只需检查x和y
sp = SpherePack([s for s in sp if math.sqrt(s[0]**2+s[1]**2) <= R]) # 注意:这样会重新生成一个SpherePack对象
```
但是,这样生成的颗粒可能仍然会有一部分在圆柱体外(因为颗粒有半径),所以我们应该考虑颗粒半径:颗粒中心到圆柱体轴线的距离加上颗粒半径不能超过圆柱体半径。同时,颗粒也不能与容器墙体重叠,但这在后续的模拟中会通过碰撞检测解决。在生成时,我们可以保守一点:颗粒中心到轴线的距离应小于等于R - s.radius(这样颗粒就不会超出圆柱体侧壁)。但是,这样生成可能会在边界处留下空隙。另一种方法是先不考虑边界,生成颗粒,然后在模拟过程中让颗粒沉降,并让墙体将颗粒推回容器内。
通常,我们先生成一个初始的颗粒云(可能有些颗粒在容器外),然后运行模拟,让颗粒在重力作用下沉降,并与墙体发生作用,最终稳定在容器内。
4. 将颗粒和墙体添加到模拟中。
5. 设置引擎(包括重力、碰撞检测、积分器等)。
因此,完整的代码框架如下:
```python
from yade import pack, plot, qt
import math
# 参数
R = 0.5 # 圆柱体半径,单位米
h = 1.0 # 圆柱体高度,单位米
rMean = 0.05 # 颗粒平均半径
rFuzz = 0.2 # 半径随机变化范围(相对值)
numParticles = 1000 # 颗粒数量(实际生成可能会少一些,因为过滤)
# 创建墙体
# 1. 圆柱体侧壁
cylinder = yade.wall.Cylinder(center=(0,0,h/2), axis=(0,0,1), radius=R, fixed=True, sense=1) # sense=1表示只阻挡从内到外
cylinder.id = O.bodies.append(cylinder)
# 2. 底盖
bottomWall = yade.wall.Wall(position=(0,0,0), normal=(0,0,-1), fixed=True) # 法向量向下,阻挡颗粒向下穿过z=0
bottomWall.id = O.bodies.append(bottomWall)
# 3. 顶盖
topWall = yade.wall.Wall(position=(0,0,h), normal=(0,0,1), fixed=True) # 法向量向上,阻挡颗粒向上穿过z=h
topWall.id = O.bodies.append(topWall)
# 生成颗粒
sp = pack.SpherePack()
sp.makeCloud(minCorner=(-R,-R,0), maxCorner=(R,R,h), rMean=rMean, rRelFuzz=rFuzz, num=numParticles, periodic=False)
# 过滤:只保留颗粒中心在圆柱体内(且考虑颗粒半径,确保颗粒不会与侧壁重叠太多,但允许在后续模拟中调整)
# 注意:这里我们只检查颗粒中心到轴线的距离小于(R-颗粒半径)的颗粒,这样颗粒就不会超出圆柱体。
sp = pack.SpherePack([s for s in sp if math.sqrt(s[0]**2+s[1]**2) <= R - s[2]]) # s[2]是半径
# 将颗粒添加到模拟中
sp.toSimulation() # 默认材质0
# 设置引擎
O.engines = [
ForceResetter(),
InsertionSortCollider([Bo1_Sphere_Aabb(), Bo1_Wall_Aabb()]),
InteractionLoop(
[Ig2_Sphere_Sphere_ScGeom(), Ig2_Wall_Sphere_ScGeom()],
[Ip2_FrictMat_FrictMat_FrictPhys()],
[Law2_ScGeom_FrictPhys_CundallStrack()]
),
NewtonIntegrator(gravity=(0,0,-9.81), damping=0.4),
# 可以添加PyRunner用于检查平衡状态
]
# 设置时间步
O.dt = 0.01 * PWaveTimeStep()
# 保存状态以便后续查看
O.saveTmp()
# 运行模拟(可以运行一段时间直到颗粒稳定)
#O.run(10000, True)
# 如果想实时查看,可以打开QT界面
qt.View()
```
注意:上述代码中,过滤颗粒的条件是`math.sqrt(s[0]**2+s[1]**2) <= R - s[2]`,这确保了颗粒中心到轴线的距离加上颗粒半径不会超过圆柱体半径(即颗粒不会超出圆柱体侧壁)。但是,这样过滤可能会使得靠近圆柱体侧壁的颗粒被过滤掉,导致靠近边界处颗粒较少。另一种方法是不过滤,让颗粒在模拟中与墙体发生碰撞自然调整位置。我们可以尝试不过滤,然后运行模拟,让颗粒在重力作用下沉降。
因此,我们可以先不进行过滤,而是生成一个立方体区域内的颗粒,然后运行模拟,让颗粒在重力作用下沉降,同时与墙体碰撞,最终稳定在容器内。
修改生成颗粒部分:
```python
sp.makeCloud(minCorner=(-R,-R,0), maxCorner=(R,R,h), rMean=rMean, rRelFuzz=rFuzz, num=numParticles, periodic=False)
sp.toSimulation()
```
这样生成的颗粒可能有一部分在圆柱体外,但后续模拟中墙体会将它们推回。
另外,注意我们使用的是`yade.wall.Cylinder`和`yade.wall.Wall`,它们都需要相应的碰撞几何体和相互作用。在引擎中,我们已经为球体和墙体添加了碰撞检测(Bo1_Sphere_Aabb和Bo1_Wall_Aabb)以及相互作用(Ig2_Wall_Sphere_ScGeom和Law2_ScGeom_FrictPhys_CundallStrack)。
但是,注意我们创建的圆柱体墙体(Cylinder)是一种特殊的墙体,在碰撞检测中需要使用`Bo1_Cylinder_Aabb`(如果存在的话)?实际上,在YADE中,Cylinder墙体也是由Wall类派生的,所以我们可以使用`Bo1_Wall_Aabb`来创建它的几何体。但是,为了确保正确,我们可以查看YADE文档。在YADE中,圆柱体墙体的碰撞检测已经内置,我们使用`Ig2_Cylinder_Sphere_ScGeom`来处理圆柱体和球体的碰撞。因此,我们需要修改相互作用几何体创建部分:
将`Ig2_Wall_Sphere_ScGeom()`替换为`Ig2_Cylinder_Sphere_ScGeom()`和`Ig2_Wall_Sphere_ScGeom()`,因为有两种墙体(圆柱体和平面墙)。我们可以这样写:
```python
InteractionLoop(
[Ig2_Sphere_Sphere_ScGeom(), Ig2_Wall_Sphere_ScGeom(), Ig2_Cylinder_Sphere_ScGeom()],
[Ip2_FrictMat_FrictMat_FrictPhys()],
[Law2_ScGeom_FrictPhys_CundallStrack()]
)
```
但是,注意:顶盖和底盖是`Wall`类型,而圆柱体是`Cylinder`类型,所以我们需要同时添加这两种几何交互。
另外,在碰撞检测中,我们需要为圆柱体添加边界体(bounding volumes)。由于圆柱体是无限长的,我们可能需要使用`Bo1_Cylinder_Aabb`来创建它的Aabb?实际上,在YADE中,圆柱体墙体的Aabb是自动创建的,并且覆盖整个空间?这显然效率低下。因此,我们可以手动设置圆柱体的Aabb范围,或者使用其他方法来优化。
但是,为了简单起见,我们可以先这样处理,如果颗粒数量不多,可以运行。
另外,我们也可以使用`yade.utils.crossSectionModule`中的`gtsSurface2Facets`来创建一个圆柱体的三角网格表面,然后作为普通几何体(Facet)加入,但这样计算量较大。因此,我们还是使用墙体。
综上,我们调整后的代码为:
```python
from yade import pack, plot, qt
import math
# 参数
R = 0.5 # 圆柱体半径,单位米
h = 1.0 # 圆柱体高度,单位米
rMean = 0.05 # 颗粒平均半径
rFuzz = 0.2 # 半径随机变化范围(相对值)
numParticles = 1000 # 颗粒数量
# 创建墙体
# 1. 圆柱体侧壁
cylinder = yade.wall.Cylinder(center=(0,0,h/2), axis=(0,0,1), radius=R, fixed=True, sense=1) # sense=1表示只阻挡从内到外
cylinder.id = O.bodies.append(cylinder)
# 2. 底盖
bottomWall = yade.wall.Wall(position=(0,0,0), normal=(0,0,-1), fixed=True) # 法向量向下,阻挡颗粒向下穿过z=0
bottomWall.id = O.bodies.append(bottomWall)
# 3. 顶盖
topWall = yade.wall.Wall(position=(0,0,h), normal=(0,0,1), fixed=True) # 法向量向上,阻挡颗粒向上穿过z=h
topWall.id = O.bodies.append(topWall)
# 生成颗粒
sp = pack.SpherePack()
sp.makeCloud(minCorner=(-R,-R,0), maxCorner=(R,R,h), rMean=rMean, rRelFuzz=rFuzz, num=numParticles, periodic=False)
sp.toSimulation()
# 设置引擎
O.engines = [
ForceResetter(),
InsertionSortCollider([Bo1_Sphere_Aabb(), Bo1_Wall_Aabb(), Bo1_Cylinder_Aabb()]), # 注意添加了Bo1_Cylinder_Aabb
InteractionLoop(
[Ig2_Sphere_Sphere_ScGeom(), Ig2_Wall_Sphere_ScGeom(), Ig2_Cylinder_Sphere_ScGeom()], # 注意添加了圆柱体与球体的相互作用
[Ip2_FrictMat_FrictMat_FrictPhys()],
[Law2_ScGeom_FrictPhys_CundallStrack()]
),
NewtonIntegrator(gravity=(0,0,-9.81), damping=0.4),
]
# 设置时间步
O.dt = 0.01 * PWaveTimeStep()
# 保存状态
O.saveTmp()
# 运行模拟(先运行一段时间,比如10000步,或者直到颗粒稳定)
# 可以添加PyRunner来检查动能,以判断是否稳定
def checkUnbalanced():
if O.iter > 1000 and unbalancedForce() < 0.05:
O.pause()
print("平衡状态达到")
# 否则继续运行
# 将PyRunner加入引擎
O.engines = O.engines + [PyRunner(command='checkUnbalanced()', iterPeriod=100)]
# 运行
O.run(10000, True)
# 打开QT界面
qt.View()
```
注意:上述代码中,我们添加了`Bo1_Cylinder_Aabb`来处理圆柱体的Aabb,以及`Ig2_Cylinder_Sphere_ScGeom`来处理圆柱体与球体的碰撞几何。
另外,我们添加了一个`PyRunner`来检查系统是否达到平衡(通过`unbalancedForce`函数)。`unbalancedForce`是YADE中用于计算系统不平衡力的函数,当值小于0.05时认为系统达到平衡。
以上就是在YADE中创建圆柱形容器并生成颗粒堆积的基本步骤。用户可以根据实际需求调整参数。
注意:如果颗粒数量较多,可能需要较长的计算时间。另外,初始生成的颗粒可能会在圆柱体外,但随着时间的推移,颗粒会沉降到容器内并达到稳定。
最后,如果用户需要保存颗粒堆积状态,可以使用`yade.utils.save`函数。
相关问题:
1. 如何调整颗粒的物理属性(如摩擦系数、弹性模量)?
2. 如何提高颗粒生成效率(避免过多的颗粒生成在容器外)?
3. 如何在圆柱体容器中实现周期性加载(如施加压力)?</think>在YADE 2022.01a中创建圆柱形容器并生成颗粒堆积的步骤如下:
### 1. 导入必要模块
```python
from yade import utils, pack, plot, qt
import math
```
### 2. 定义容器参数
```python
radius = 0.5 # 圆柱体半径(m)
height = 1.0 # 圆柱体高度(m)
thickness = 0.02 # 容器壁厚(m)
```
### 3. 创建圆柱形容器
使用三个墙体组件构建封闭容器:
```python
# 圆柱侧壁(无限长圆柱面)
cylinder = utils.wallCylinder(axis=(0,0,1), radius=radius, center=(0,0,height/2), sense=1)
# 底部盖子
bottom_wall = utils.wall(position=(0,0,0), normal=(0,0,1))
# 顶部盖子
top_wall = utils.wall(position=(0,0,height), normal=(0,0,-1))
# 添加到模拟
container_ids = [
O.bodies.append(cylinder),
O.bodies.append(bottom_wall),
O.bodies.append(top_wall)
]
```
### 4. 生成颗粒堆积
```python
# 颗粒参数
mean_radius = 0.05 # 平均半径(m)
radius_dev = 0.2 # 半径浮动范围(0.2=±20%)
# 在圆柱体内生成颗粒
particle_pack = pack.SpherePack()
particle_pack.makeCloud(
minCorner=(-radius, -radius, 0),
maxCorner=(radius, radius, height),
rMean=mean_radius, rRelFuzz=radius_dev,
num=500, # 颗粒数量
seed=1 # 随机种子
)
# 过滤圆柱体外颗粒
filtered_particles = []
for sphere in particle_pack:
x, y, z, r = sphere
if math.sqrt(x**2 + y**2) <= radius - r: # 确保颗粒在容器内
filtered_particles.append((x, y, z, r))
# 添加颗粒到模拟
particle_ids = O.bodies.append([sphere(s[0:3], s[3]) for s in filtered_particles])
```
### 5. 设置物理引擎
```python
O.engines = [
ForceResetter(),
InsertionSortCollider([Bo1_Sphere_Aabb(), Bo1_Wall_Aabb()]),
InteractionLoop(
[Ig2_Sphere_Sphere_ScGeom(), Ig2_Wall_Sphere_ScGeom()],
[Ip2_FrictMat_FrictMat_FrictPhys()],
[Law2_ScGeom_FrictPhys_CundallStrack()]
),
NewtonIntegrator(gravity=(0,0,-9.81), damping=0.4)
]
# 设置材料属性
mat = FrictMat(young=1e8, poisson=0.3, frictionAngle=0.5)
O.materials.append(mat)
```
### 6. 运行模拟
```python
# 设置时间步
O.dt = 0.01 * PWaveTimeStep()
# 运行5000步使颗粒沉降
O.run(5000, True)
# 可视化
qt.View()
```
### 关键说明:
1. **容器构建**:
- `wallCylinder`创建无限长圆柱面,`sense=1`表示法向向外
- 两个`wall`对象创建顶/底盖
- 法向量方向决定哪侧为实体(颗粒碰撞面)
2. **颗粒生成**:
- `makeCloud`在包围盒内生成颗粒
- 数学过滤确保颗粒在圆柱内:$ \sqrt{x^2 + y^2} \leq R - r $
- 考虑颗粒半径避免初始穿透
3. **物理参数**:
- `young`:杨氏模量 (Pa)
- `frictionAngle`:摩擦角 (弧度)
- `damping`:数值阻尼系数
[^1]
### 可视化效果
运行后可通过QT查看:
- 红色:容器墙体
- 彩色球体:颗粒
- 按`ESC`退出实时视图