Skip to content

Commit d9c084b

Browse files
authored
PBD + MPM examples (#10)
* Add PBD case studies and viscoplastic MPM tutorial - Add PBD cloth simulation (12_pbd_cloth) - Add PBD mesh simulation (13_pbd_mesh) - Add PBF fluids simulation (14_pbf) - Add viscoplastic MPM material model (11_mpm_plastic) - Rename substep function to p2g2p in viscoplastic simulator * Add cloth_data.json for PBD cloth simulation * Refactor folders 12-14: restructure PBD cloth, mesh, and PBF simulators
1 parent 780a1ec commit d9c084b

33 files changed

+33901
-0
lines changed

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

11_mpm_plastic/readme.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# 2D Sand with a Sphere Collider
2+
3+
A 2D sand block falling onto a static red sphere collider simulated by Material Point Method (MPM). The sand undergoes irreversible deformation and splashing upon impact, demonstrating granular flow and frictional boundary response.
4+
5+
![results](results.gif)
6+
7+
## Dependencies
8+
```
9+
pip install numpy taichi
10+
```
11+
12+
## Run
13+
```
14+
python simulator.py
15+
```

11_mpm_plastic/results.gif

4.03 MB
Loading

11_mpm_plastic/simulator.py

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
# Material Point Method Simulation
2+
import numpy as np # numpy for linear algebra
3+
import taichi as ti # taichi for fast and parallelized computation
4+
5+
ti.init(arch=ti.cpu)
6+
7+
# sampling material particles with poisson-disk sampling
8+
################################
9+
# Poisson Disk Sampling Tool
10+
################################
11+
def poisson_disk_sampling(radius, domain_size, k=30):
12+
"""Bridson's algorithm for Poisson-disk sampling in 2D"""
13+
cell_size = radius / np.sqrt(2)
14+
grid_shape = (int(domain_size[0] / cell_size) + 1, int(domain_size[1] / cell_size) + 1)
15+
grid = -np.ones(grid_shape, dtype=int)
16+
samples = []
17+
active_list = []
18+
19+
def in_domain(p):
20+
return 0 <= p[0] < domain_size[0] and 0 <= p[1] < domain_size[1]
21+
22+
def get_cell_coords(p):
23+
return int(p[0] / cell_size), int(p[1] / cell_size)
24+
25+
def get_nearby_samples(p):
26+
i, j = get_cell_coords(p)
27+
neighbors = []
28+
for di in [-2, -1, 0, 1, 2]:
29+
for dj in [-2, -1, 0, 1, 2]:
30+
ni, nj = i + di, j + dj
31+
if 0 <= ni < grid.shape[0] and 0 <= nj < grid.shape[1]:
32+
idx = grid[ni, nj]
33+
if idx != -1:
34+
neighbors.append(samples[idx])
35+
return neighbors
36+
37+
# Start with a random point
38+
first_point = np.array([np.random.uniform(0, domain_size[0]), np.random.uniform(0, domain_size[1])])
39+
samples.append(first_point)
40+
active_list.append(0)
41+
grid[get_cell_coords(first_point)] = 0
42+
43+
while active_list:
44+
idx = np.random.choice(active_list)
45+
base_point = samples[idx]
46+
found = False
47+
for _ in range(k):
48+
angle = np.random.uniform(0, 2 * np.pi)
49+
r = np.random.uniform(radius, 2 * radius)
50+
new_point = base_point + r * np.array([np.cos(angle), np.sin(angle)])
51+
if in_domain(new_point):
52+
neighbors = get_nearby_samples(new_point)
53+
if all(np.linalg.norm(new_point - n) >= radius for n in neighbors):
54+
samples.append(new_point)
55+
active_list.append(len(samples) - 1)
56+
grid[get_cell_coords(new_point)] = len(samples) - 1
57+
found = True
58+
if not found:
59+
active_list.remove(idx)
60+
return np.array(samples)
61+
62+
# ANCHOR: data_def
63+
# simulation setup
64+
grid_size = 128 # background Eulerian grid's resolution, in 2D is [128, 128]
65+
dx = 1.0 / grid_size # the domain size is [1m, 1m] in 2D, so dx for each cell is (1/128)m
66+
dt = 2e-4 # time step size in second
67+
ppc = 8 # average particle-per-cell
68+
69+
density = 400 # kg / m^3
70+
E, nu = 3.537e5, 0.3 # sand's Young's modulus and Poisson's ratio
71+
mu, lam = E / (2 * (1 + nu)), E * nu / ((1 + nu) * (1 - 2 * nu)) # sand's Lame parameters
72+
sdf_friction = 0.5 # frictional coefficient of SDF boundary condition
73+
friction_angle_in_degrees = 25.0 # Drucker Prager friction angle
74+
# ANCHOR: D_def
75+
D = (1./4.) * dx * dx # constant D for Quadratic B-spline used for APIC
76+
# ANCHOR_END: D_def
77+
78+
# sampling material particles with poisson-disk sampling
79+
poisson_samples = poisson_disk_sampling(dx / np.sqrt(ppc), [0.2, 0.4]) # simulating a [30cm, 50cm] sand block
80+
81+
# material particles data
82+
N_particles = len(poisson_samples)
83+
x = ti.Vector.field(2, float, N_particles) # the position of particles
84+
x.from_numpy(np.array(poisson_samples) + [0.4, 0.55])
85+
v = ti.Vector.field(2, float, N_particles) # the velocity of particles
86+
vol = ti.field(float, N_particles) # the volume of particle
87+
vol.fill(0.2 * 0.4 / N_particles) # get the volume of each particle as V_rest / N_particles
88+
m = ti.field(float, N_particles) # the mass of particle
89+
m.fill(vol[0] * density)
90+
F = ti.Matrix.field(2, 2, float, N_particles) # the deformation gradient of particles
91+
F.from_numpy(np.tile(np.eye(2), (N_particles, 1, 1)))
92+
C = ti.Matrix.field(2, 2, float, N_particles) # the affine-matrix of particles
93+
94+
diff_log_J = ti.field(float, N_particles) # tracks changes in the log of the volume gained during extension
95+
96+
# grid data
97+
grid_m = ti.field(float, (grid_size, grid_size))
98+
grid_v = ti.Vector.field(2, float, (grid_size, grid_size))
99+
# ANCHOR_END: data_def
100+
101+
# ANCHOR: reset_grid
102+
def reset_grid():
103+
# after each transfer, the grid is reset
104+
grid_m.fill(0)
105+
grid_v.fill(0)
106+
# ANCHOR_END: reset_grid
107+
108+
################################
109+
# Stvk Hencky Elasticity
110+
################################
111+
# ANCHOR: stvk
112+
@ti.func
113+
def StVK_Hencky_PK1_2D(F):
114+
U, sig, V = ti.svd(F)
115+
inv_sig = sig.inverse()
116+
e = ti.Matrix([[ti.log(sig[0, 0]), 0], [0, ti.log(sig[1, 1])]])
117+
return U @ (2 * mu * inv_sig @ e + lam * e.trace() * inv_sig) @ V.transpose()
118+
# ANCHOR_END: stvk
119+
120+
################################
121+
# Drucker Prager plasticity
122+
################################
123+
# ANCHOR: drucker_prager
124+
@ti.func
125+
def Drucker_Prager_return_mapping(F, diff_log_J):
126+
dim = ti.static(F.n)
127+
sin_phi = ti.sin(friction_angle_in_degrees/ 180.0 * ti.math.pi)
128+
friction_alpha = ti.sqrt(2.0 / 3.0) * 2.0 * sin_phi / (3.0 - sin_phi)
129+
U, sig_diag, V = ti.svd(F)
130+
sig = ti.Vector([ti.max(sig_diag[i,i], 0.05) for i in ti.static(range(dim))])
131+
epsilon = ti.log(sig)
132+
epsilon += diff_log_J / dim # volume correction treatment
133+
trace_epsilon = epsilon.sum()
134+
shifted_trace = trace_epsilon
135+
if shifted_trace >= 0:
136+
for d in ti.static(range(dim)):
137+
epsilon[d] = 0.
138+
else:
139+
epsilon_hat = epsilon - (trace_epsilon / dim)
140+
epsilon_hat_norm = ti.sqrt(epsilon_hat.dot(epsilon_hat)+1e-8)
141+
delta_gamma = epsilon_hat_norm + (dim * lam + 2. * mu) / (2. * mu) * (shifted_trace) * friction_alpha
142+
epsilon -= (ti.max(delta_gamma, 0) / epsilon_hat_norm) * epsilon_hat
143+
sig_out = ti.exp(epsilon)
144+
for d in ti.static(range(dim)):
145+
sig_diag[d,d] = sig_out[d]
146+
return U @ sig_diag @ V.transpose()
147+
# ANCHOR_END: drucker_prager
148+
149+
# Particle-to-Grid (P2G) Transfers
150+
# ANCHOR: p2g
151+
@ti.kernel
152+
def particle_to_grid_transfer():
153+
for p in range(N_particles):
154+
base = (x[p] / dx - 0.5).cast(int)
155+
fx = x[p] / dx - base.cast(float)
156+
# quadratic B-spline interpolating functions
157+
w = [0.5 * (1.5 - fx) ** 2, 0.75 - (fx - 1) ** 2, 0.5 * (fx - 0.5) ** 2]
158+
# gradient of the interpolating function
159+
dw_dx = [fx - 1.5, 2 * (1.0 - fx), fx - 0.5]
160+
161+
P = StVK_Hencky_PK1_2D(F[p])
162+
for i in ti.static(range(3)):
163+
for j in ti.static(range(3)):
164+
offset = ti.Vector([i, j])
165+
dpos = (offset.cast(float) - fx) * dx
166+
weight = w[i][0] * w[j][1]
167+
grad_weight = ti.Vector([(1. / dx) * dw_dx[i][0] * w[j][1],
168+
w[i][0] * (1. / dx) * dw_dx[j][1]])
169+
170+
grid_m[base + offset] += weight * m[p] # mass transfer
171+
# ANCHOR: apic_p2g
172+
grid_v[base + offset] += weight * m[p] * (v[p] + C[p] @ dpos) # momentum Transfer, APIC formulation
173+
# ANCHOR_END: apic_p2g
174+
# internal force (stress) transfer
175+
fi = -vol[p] * P @ F[p].transpose() @ grad_weight
176+
grid_v[base + offset] += dt * fi
177+
# ANCHOR_END: p2g
178+
179+
# Grid Update
180+
@ti.kernel
181+
def update_grid():
182+
for i, j in grid_m:
183+
if grid_m[i, j] > 0:
184+
grid_v[i, j] = grid_v[i, j] / grid_m[i, j] # extract updated nodal velocity from transferred nodal momentum
185+
grid_v[i, j] += ti.Vector([0, -9.81]) * dt # gravity
186+
187+
# Dirichlet BC near the bounding box
188+
if i <= 3 or i > grid_size - 3 or j <= 2 or j > grid_size - 3:
189+
grid_v[i, j] = 0
190+
191+
x_i = (ti.Vector([i, j])) * dx # position of the grid-node
192+
193+
# ANCHOR: sphere_sdf
194+
# a sphere SDF as boundary condition
195+
sphere_center = ti.Vector([0.5, 0.5])
196+
sphere_radius = 0.05 + dx # add a dx-gap to avoid penetration
197+
if (x_i - sphere_center).norm() < sphere_radius:
198+
normal = (x_i - sphere_center).normalized()
199+
diff_vel = -grid_v[i, j]
200+
dotnv = normal.dot(diff_vel)
201+
dotnv_frac = dotnv * (1.0 - sdf_friction)
202+
grid_v[i, j] += diff_vel * sdf_friction + normal * dotnv_frac
203+
# ANCHOR_END: sphere_sdf
204+
205+
206+
# Grid-to-Particle (G2P) Transfers
207+
# ANCHOR: g2p
208+
@ti.kernel
209+
def grid_to_particle_transfer():
210+
for p in range(N_particles):
211+
base = (x[p] / dx - 0.5).cast(int)
212+
fx = x[p] / dx - base.cast(float)
213+
# quadratic B-spline interpolating functions
214+
w = [0.5 * (1.5 - fx) ** 2, 0.75 - (fx - 1) ** 2, 0.5 * (fx - 0.5) ** 2]
215+
# gradient of the interpolating function
216+
dw_dx = [fx - 1.5, 2 * (1.0 - fx), fx - 0.5]
217+
218+
new_v = ti.Vector.zero(float, 2)
219+
new_C = ti.Matrix.zero(float, 2, 2)
220+
v_grad_wT = ti.Matrix.zero(float, 2, 2)
221+
for i in ti.static(range(3)):
222+
for j in ti.static(range(3)):
223+
offset = ti.Vector([i, j])
224+
dpos = (offset.cast(float) - fx) * dx
225+
weight = w[i][0] * w[j][1]
226+
grad_weight = ti.Vector([(1. / dx) * dw_dx[i][0] * w[j][1],
227+
w[i][0] * (1. / dx) * dw_dx[j][1]])
228+
229+
new_v += weight * grid_v[base + offset]
230+
# ANCHOR: apic_g2p
231+
new_C += weight * grid_v[base + offset].outer_product(dpos) / D
232+
# ANCHOR_END: apic_g2p
233+
v_grad_wT += grid_v[base + offset].outer_product(grad_weight)
234+
235+
v[p] = new_v
236+
C[p] = new_C
237+
# note the updated F here is the trial elastic deformation gradient
238+
F[p] = (ti.Matrix.identity(float, 2) + dt * v_grad_wT) @ F[p]
239+
# ANCHOR_END: g2p
240+
241+
# Deformation Gradient and Particle State Update
242+
# ANCHOR: particle_update
243+
@ti.kernel
244+
def update_particle_state():
245+
for p in range(N_particles):
246+
# trial elastic deformation gradient
247+
F_tr = F[p]
248+
# apply return mapping to correct the trial elastic state, projecting the stress induced by F_tr
249+
# back onto the yield surface, following the direction specified by the plastic flow rule.
250+
new_F = Drucker_Prager_return_mapping(F_tr, diff_log_J[p])
251+
# track the volume change incurred by return mapping to correct volume, following https://dl.acm.org/doi/10.1145/3072959.3073651 sec 4.3.4
252+
diff_log_J[p] += -ti.log(new_F.determinant()) + ti.log(F_tr.determinant())
253+
F[p] = new_F
254+
# advection
255+
x[p] += dt * v[p]
256+
# ANCHOR_END: particle_update
257+
258+
# ANCHOR: time_step
259+
def step():
260+
# a single time step of the Material Point Method (MPM) simulation
261+
reset_grid()
262+
particle_to_grid_transfer()
263+
update_grid()
264+
grid_to_particle_transfer()
265+
update_particle_state()
266+
# ANCHOR_END: time_step
267+
268+
################################
269+
# Main
270+
################################
271+
gui = ti.GUI("2D MPM Sand", res = 512, background_color = 0xFFFFFF)
272+
frame = 0
273+
while True:
274+
for s in range(50):
275+
step()
276+
277+
gui.circles(np.array([[0.5, 0.5]]), radius = 0.05 * gui.res[0], color = 0xFF0000)
278+
gui.circles(x.to_numpy(), radius = 1.5, color = 0xD6B588)
279+
gui.show()

0 commit comments

Comments
 (0)