1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
|
# 2024 © Benjamin Dedieu <benjamin.dedieu@proton.me>
"""
This example illustrates the use of class `TimeAverager` to retrieve averaged data
of position, velocity force and contact force repartition for 2 particles called the
intruders entrained in a gravity-driven dry mono-disperse granular flow on an inclined plane.
"""
from yade import pack, qt
import numpy as np
import matplotlib as mpl
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
############
# PARAMETERS
diameterPart = 2e-3 # Diameter of the bed particles [m]
diameterPartI = 4e-3 # Diameter of the intruders [m]
nbIntruders = 2 # number of intruders
positionI = 4 * diameterPart # target intruder center position from the ground
densPart = 2500 # Density of particles [kg/m3]
phiMax = 0.61 # Max value of the dense packing solid volume fraction
phiMean = 0.6 # Solid volume fraction only used to compute the number of beads
# NbBeads = phiMean*height*width*length/Vpart
length = 15 * diameterPart # Length of the domain [m]
width = 8 * diameterPart # Width of the domain [m]
height = 6 * diameterPart # Height of the domain [m]
slopeAngle = pi / 6 # Slope angle of the inclined plane [rad]
gravityVector = Vector3(9.81 * sin(slopeAngle), 0.0, -9.81 * cos(slopeAngle))
dtSave = 0.1 # time step for saving data [s]
simuTime = 5 # simulation time after gravity deposition [s]
gridDensity = 12 # angle between two points of the grid to compute the
# contact force field on the intruder surface, in degrees
gaussianKernelWidth = 2 # gaussian parameter to diffuse the contact force
# on the grid, normalized by the distance betweeen
# 2 points on the grid
# Estimated max particle pressure from the static load
maxPressure = densPart * phiMax * height * 9.81
# Evaluate the minimal normal stiffness to be in the rigid particle limit
# (cf Roux and Combe 2002)
normalStiffness = maxPressure * diameterPart * 1e4
# Young modulus of the particles from the stiffness wanted.
young = normalStiffness / diameterPart
# Create materials
O.materials.append(ViscElMat(en=0.5, et=0., young=young, poisson=0.5, density=densPart, frictionAngle=atan(0.4), label='Mat'))
# Define grid points on the surface of the intruder to compute stress field from
# contact points. The grid is in cartesian coordinates (simpler than spherical
# coordinates for computing distance to contact positions)
# The grid is generated using the Fibonacci lattice algorythm
Ngrid = int(np.ceil(4 * np.pi / (np.deg2rad(gridDensity)**2)))
grid = np.empty((Ngrid, 3))
goldenRatio = (1 + 5**0.5) / 2
for i in range(Ngrid):
# compute theta and phi, the sperical coordinates
theta = 2 * np.pi * i / goldenRatio
phi = np.arccos(1 - 2 * (i + 0.5) / Ngrid) # the `+0.5` avoids to have singularity
# points at the poles
# convert to cartesian coordinates
grid[i, :] = diameterPartI / 2 * np.array([np.cos(theta) * np.sin(phi), np.sin(theta) * np.sin(phi), np.cos(phi)])
# compute sigma for the gaussian diffusion of contact forces:
sigma = gaussianKernelWidth * diameterPartI * gridDensity * pi / 180
####################
# FRAMEWORK CREATION
# Compute number of particles to fit in the height
numberPart = int(phiMean * height * length * width / pi / diameterPart**3 * 6)
# Define deposition height for the particle cloud
depositionHeight = 4 * height
# Define number of particles and deposition height for cloud of particles below and above the intruders
heightBelowI = positionI - 0.5 * diameterPartI
heightAboveI = height - heightBelowI
depositionHeightBelowI = heightBelowI / height * depositionHeight
depositionHeightAboveI = depositionHeight - depositionHeightBelowI
numberPartBelowI = int(depositionHeightBelowI / depositionHeight * numberPart)
numberPartAboveI = numberPart - numberPartBelowI
depositionHeightI = 1.2 * diameterPartI
depositionHeightTot = depositionHeightBelowI + depositionHeightI + depositionHeightAboveI
# Define reference positions of the inclined plane
bottomPos = 2 * diameterPart
# Create a periodic box (larger than the flume to avoid bugs):
O.periodic = True
boxHeight = bottomPos + depositionHeightTot
O.cell.setBox(length, width, boxHeight)
# Create walls
bottomWall = box(
center=(length / 2.0, width / 2.0, bottomPos),
extents=(1e6, 1e6, 0),
fixed=True,
wire=False,
color=(0, 1, 0),
material='Mat',
)
O.bodies.append(bottomWall)
# Create the particle cloud
cloudBelowI = pack.SpherePack()
cloudBelowI.makeCloud(
minCorner=(0, 0, bottomPos + 1e-4),
maxCorner=(length, width, bottomPos + depositionHeightBelowI),
rMean=diameterPart / 2,
num=numberPartBelowI,
)
cloudBelowI.toSimulation(material='Mat', color=(0, 0, 1))
cloudAboveI = pack.SpherePack()
cloudAboveI.makeCloud(
minCorner=(0, 0, bottomPos + depositionHeightBelowI + depositionHeightI),
maxCorner=(length, width, depositionHeightTot),
rMean=diameterPart / 2,
num=numberPartAboveI,
)
cloudAboveI.toSimulation(material='Mat', color=(0, 0, 1))
# Define left and right Limit of intruder zone :
leftLimitI = diameterPartI / 2
rightLimitI = diameterPartI / 2
# Create the intruders (equaly distributed along x axis)
intruders = []
for i in range(nbIntruders):
O.bodies.append(
sphere(
center=((i + 0.5) * length / nbIntruders, leftLimitI, bottomPos + depositionHeightBelowI + depositionHeightI / 2),
radius=diameterPartI / 2.,
color=(1, 0, 0),
material='Mat',
wire=False
)
)
intruders.append(O.bodies[-1])
# Evaluate the deposition time considering the free-fall time of the highest particle to the ground
depositionTime = sqrt(depositionHeightTot * 2 / abs(gravityVector[2]))
###################################
# ENGINES AND PY-RUNNER DEFINITION
O.engines = [
ForceResetter(),
InsertionSortCollider(
[Bo1_Sphere_Aabb(), Bo1_Box_Aabb()],
allowBiggerThanPeriod=True,
),
InteractionLoop(
[Ig2_Sphere_Sphere_ScGeom(), Ig2_Box_Sphere_ScGeom()],
[Ip2_ViscElMat_ViscElMat_ViscElPhys()],
[Law2_ScGeom_ViscElPhys_Basic()],
),
PyRunner(
command='initialize()',
virtPeriod=0.01,
label='init',
),
TimeAverager(
ids=[intr.id for intr in intruders],
computeContactForceField=True,
sigma=sigma,
grid=grid,
label='timeAverager',
dead=True,
),
PyRunner(
command='saveData()',
virtPeriod=dtSave,
label='dataSaver',
dead=True,
),
GlobalStiffnessTimeStepper(
defaultDt=1e-4,
viscEl=True,
timestepSafetyCoefficient=0.7,
),
NewtonIntegrator(gravity=gravityVector, damping=0.2),
]
def initialize():
"""Wait for gravity deposition then start TimeAverager and saveData and
initialize quantities"""
global step, time, Nstep
if O.time >= depositionTime:
init.dead = True
timeAverager.dead = False
dataSaver.dead = False
Nstep = int(simuTime / dtSave)
for intr in intruders:
intr.instantPos = np.empty((Nstep, 3))
intr.averagedPos = np.empty((Nstep, 3))
intr.instantVel = np.empty((Nstep, 3))
intr.averagedVel = np.empty((Nstep, 3))
intr.instantForce = np.empty((Nstep, 3))
intr.averagedForce = np.empty((Nstep, 3))
intr.averagedContactForceField = np.empty((Nstep, Ngrid, 3))
time = np.empty((Nstep))
step = 0
timeAverager.initialization()
def saveData():
global step, time, cellArea, FcFieldMean, pressureFieldMean, fig, fig0, fig1, fig2
if step <= Nstep - 1:
time[step] = O.time
for k, intr in enumerate(intruders):
# Get instantaneous quantities direclty from body objects
intr.instantPos[step] = intr.state.pos
intr.instantVel[step] = intr.state.vel
intr.instantForce[step] = O.forces.f(intr.id)
# Get averaged quantities with timeAverager.get... methods
intr.averagedPos[step] = timeAverager.getPos(intr.id)
intr.averagedVel[step] = timeAverager.getVel(intr.id)
intr.averagedForce[step] = timeAverager.getForce(intr.id)
intr.averagedContactForceField[step] = timeAverager.getContactForceField(intr.id)
# update step and re-initialize timeAverager
print(f"Data saved for step {step}/{Nstep-1}")
timeAverager.initialization()
step += 1
if step == Nstep:
# Pause the simulation
O.pause()
# Prepare plots
plt.ion()
cmap = plt.get_cmap("viridis", nbIntruders)
# Plot intruder z position over time for each intruder
fig0, ax0 = plt.subplots(constrained_layout=True)
for k, intr in enumerate(intruders):
ax0.plot(time, intr.averagedPos[:, 2] * 1000, '-', color=cmap(k), label="intruder " + str(k) + " (averaged)")
ax0.plot(time, intr.instantPos[:, 2] * 1000, '-', color=cmap(k), alpha=0.2, label="intruder " + str(k) + " (intantaneous)")
ax0.set_xlabel("$time$ [s]")
ax0.set_ylabel("$z$ [mm]")
ax0.legend()
# Plot intruder x velocity over time for each intruder
fig1, ax1 = plt.subplots(constrained_layout=True)
for k, intr in enumerate(intruders):
ax1.plot(time, intr.averagedVel[:, 0], '-', color=cmap(k), label="intruder " + str(k) + " (averaged)")
ax1.plot(time, intr.instantVel[:, 0], '-', color=cmap(k), alpha=0.2, label="intruder " + str(k) + " (intantaneous)")
ax1.set_xlabel("$time$ [s]")
ax1.set_ylabel("$v_x$ [m/s]")
ax1.legend()
# Plot intruder z total force over time for each intruder
fig2, ax2 = plt.subplots(constrained_layout=True)
for k, intr in enumerate(intruders):
ax2.plot(time, intr.averagedForce[:, 2], '-', color=cmap(k), label="intruder " + str(k) + " (averaged)")
ax2.plot(time, intr.instantForce[:, 2], '-', color=cmap(k), alpha=0.2, label="intruder " + str(k) + " (intantaneous)")
ax2.set_xlabel("$time$ [s]")
ax2.set_ylabel("$F_z$ [N]")
ax2.legend()
# Plot pressure repartition at the surface of the intruder 0
FcFieldMean = np.mean(intruders[0].averagedContactForceField, axis=0)
# compute the normal force
FcnFieldMean = np.empty((Ngrid))
for i in range(Ngrid):
FcnFieldMean[i] = np.dot(grid[i] / diameterPartI, -FcFieldMean[i])
# Adimension by the max of normal force
FcnFieldMean /= max(FcnFieldMean)
fig, ax = plt.subplots(subplot_kw={"projection": "3d"}, tight_layout=True)
x, y, z = grid[:, 0] * 1000, grid[:, 1] * 1000, grid[:, 2] * 1000
norm = mpl.colors.Normalize(vmin=min(FcnFieldMean), vmax=max(FcnFieldMean))
cmap = plt.get_cmap("viridis")
colors = np.array([cmap(norm(F)) for F in FcnFieldMean])
ax.scatter(x, y, z, color=colors)
ax.set_xlabel("x [mm]")
ax.set_ylabel("y [mm]")
ax.set_zlabel("z [mm]")
fig.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax, aspect=60, pad=0.1, label="P / Pmax [-]")
fig.suptitle("Pressure repartion on Intruder 0")
###################
# RUN WITH QT VIEW
view = qt.View()
view.viewDir = (0, 1, 0)
view.fitAABB([0, 0, 0], [length, width, boxHeight])
O.run()
|