Skip to content

Commit 5c05ce4

Browse files
committed
init 9_reduced_DOF using 3_contact
1 parent 6dff910 commit 5c05ce4

9 files changed

Lines changed: 331 additions & 0 deletions

File tree

9_reduced_DOF/BarrierEnergy.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# ANCHOR: val_grad_hess
2+
import math
3+
import numpy as np
4+
5+
dhat = 0.01
6+
kappa = 1e5
7+
8+
def val(x, y_ground, contact_area):
9+
sum = 0.0
10+
for i in range(0, len(x)):
11+
d = x[i][1] - y_ground
12+
if d < dhat:
13+
s = d / dhat
14+
sum += contact_area[i] * dhat * kappa / 2 * (s - 1) * math.log(s)
15+
return sum
16+
17+
def grad(x, y_ground, contact_area):
18+
g = np.array([[0.0, 0.0]] * len(x))
19+
for i in range(0, len(x)):
20+
d = x[i][1] - y_ground
21+
if d < dhat:
22+
s = d / dhat
23+
g[i][1] = contact_area[i] * dhat * (kappa / 2 * (math.log(s) / dhat + (s - 1) / d))
24+
return g
25+
26+
def hess(x, y_ground, contact_area):
27+
IJV = [[0] * len(x), [0] * len(x), np.array([0.0] * len(x))]
28+
for i in range(0, len(x)):
29+
IJV[0][i] = i * 2 + 1
30+
IJV[1][i] = i * 2 + 1
31+
d = x[i][1] - y_ground
32+
if d < dhat:
33+
IJV[2][i] = contact_area[i] * dhat * kappa / (2 * d * d * dhat) * (d + dhat)
34+
else:
35+
IJV[2][i] = 0.0
36+
return IJV
37+
# ANCHOR_END: val_grad_hess
38+
39+
# ANCHOR: init_step_size
40+
def init_step_size(x, y_ground, p):
41+
alpha = 1
42+
for i in range(0, len(x)):
43+
if p[i][1] < 0:
44+
alpha = min(alpha, 0.9 * (y_ground - x[i][1]) / p[i][1])
45+
return alpha
46+
# ANCHOR_END: init_step_size

9_reduced_DOF/GravityEnergy.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import numpy as np
2+
3+
gravity = [0.0, -9.81]
4+
5+
def val(x, m):
6+
sum = 0.0
7+
for i in range(0, len(x)):
8+
sum += -m[i] * x[i].dot(gravity)
9+
return sum
10+
11+
def grad(x, m):
12+
g = np.array([gravity] * len(x))
13+
for i in range(0, len(x)):
14+
g[i] *= -m[i]
15+
return g
16+
17+
# Hessian is 0

9_reduced_DOF/InertiaEnergy.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import numpy as np
2+
3+
def val(x, x_tilde, m):
4+
sum = 0.0
5+
for i in range(0, len(x)):
6+
diff = x[i] - x_tilde[i]
7+
sum += 0.5 * m[i] * diff.dot(diff)
8+
return sum
9+
10+
def grad(x, x_tilde, m):
11+
g = np.array([[0.0, 0.0]] * len(x))
12+
for i in range(0, len(x)):
13+
g[i] = m[i] * (x[i] - x_tilde[i])
14+
return g
15+
16+
def hess(x, x_tilde, m):
17+
IJV = [[0] * (len(x) * 2), [0] * (len(x) * 2), np.array([0.0] * (len(x) * 2))]
18+
for i in range(0, len(x)):
19+
for d in range(0, 2):
20+
IJV[0][i * 2 + d] = i * 2 + d
21+
IJV[1][i * 2 + d] = i * 2 + d
22+
IJV[2][i * 2 + d] = m[i]
23+
return IJV

9_reduced_DOF/MassSpringEnergy.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import numpy as np
2+
import utils
3+
4+
def val(x, e, l2, k):
5+
sum = 0.0
6+
for i in range(0, len(e)):
7+
diff = x[e[i][0]] - x[e[i][1]]
8+
sum += l2[i] * 0.5 * k[i] * (diff.dot(diff) / l2[i] - 1) ** 2
9+
return sum
10+
11+
def grad(x, e, l2, k):
12+
g = np.array([[0.0, 0.0]] * len(x))
13+
for i in range(0, len(e)):
14+
diff = x[e[i][0]] - x[e[i][1]]
15+
g_diff = 2 * k[i] * (diff.dot(diff) / l2[i] - 1) * diff
16+
g[e[i][0]] += g_diff
17+
g[e[i][1]] -= g_diff
18+
return g
19+
20+
def hess(x, e, l2, k):
21+
IJV = [[0] * (len(e) * 16), [0] * (len(e) * 16), np.array([0.0] * (len(e) * 16))]
22+
for i in range(0, len(e)):
23+
diff = x[e[i][0]] - x[e[i][1]]
24+
H_diff = 2 * k[i] / l2[i] * (2 * np.outer(diff, diff) + (diff.dot(diff) - l2[i]) * np.identity(2))
25+
H_local = utils.make_PSD(np.block([[H_diff, -H_diff], [-H_diff, H_diff]]))
26+
# add to global matrix
27+
for nI in range(0, 2):
28+
for nJ in range(0, 2):
29+
indStart = i * 16 + (nI * 2 + nJ) * 4
30+
for r in range(0, 2):
31+
for c in range(0, 2):
32+
IJV[0][indStart + r * 2 + c] = e[i][nI] * 2 + r
33+
IJV[1][indStart + r * 2 + c] = e[i][nJ] * 2 + c
34+
IJV[2][indStart + r * 2 + c] = H_local[nI * 2 + r, nJ * 2 + c]
35+
return IJV

9_reduced_DOF/readme.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Mass-Spring Solids Simulation
2+
3+
A square falling onto the ground under gravity is simulated with mass-spring elasticity potential and implicit Euler time integration.
4+
Each time step is solved by minimizing the Incremental Potential with the projected Newton method.
5+
6+
## Dependencies
7+
```
8+
pip install numpy scipy pygame
9+
```
10+
11+
## Run
12+
```
13+
python simulator.py
14+
```

9_reduced_DOF/simulator.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Mass-Spring Solids Simulation
2+
3+
import numpy as np # numpy for linear algebra
4+
import pygame # pygame for visualization
5+
pygame.init()
6+
7+
import square_mesh # square mesh
8+
import time_integrator
9+
10+
# simulation setup
11+
side_len = 1
12+
rho = 1000 # density of square
13+
k = 2e5 # spring stiffness
14+
n_seg = 4 # num of segments per side of the square
15+
h = 0.005 # time step size in s
16+
DBC = [] # no nodes need to be fixed
17+
y_ground = -1 # height of the planar ground
18+
19+
# initialize simulation
20+
[x, e] = square_mesh.generate(side_len, n_seg) # node positions and edge node indices
21+
v = np.array([[0.0, 0.0]] * len(x)) # velocity
22+
m = [rho * side_len * side_len / ((n_seg + 1) * (n_seg + 1))] * len(x) # calculate node mass evenly
23+
# rest length squared
24+
l2 = []
25+
for i in range(0, len(e)):
26+
diff = x[e[i][0]] - x[e[i][1]]
27+
l2.append(diff.dot(diff))
28+
k = [k] * len(e) # spring stiffness
29+
# identify whether a node is Dirichlet
30+
is_DBC = [False] * len(x)
31+
for i in DBC:
32+
is_DBC[i] = True
33+
# ANCHOR: contact_area
34+
contact_area = [side_len / n_seg] * len(x) # perimeter split to each node
35+
# ANCHOR_END: contact_area
36+
37+
# simulation with visualization
38+
resolution = np.array([900, 900])
39+
offset = resolution / 2
40+
scale = 200
41+
def screen_projection(x):
42+
return [offset[0] + scale * x[0], resolution[1] - (offset[1] + scale * x[1])]
43+
44+
time_step = 0
45+
square_mesh.write_to_file(time_step, x, n_seg)
46+
screen = pygame.display.set_mode(resolution)
47+
running = True
48+
while running:
49+
# run until the user asks to quit
50+
for event in pygame.event.get():
51+
if event.type == pygame.QUIT:
52+
running = False
53+
54+
print('### Time step', time_step, '###')
55+
56+
# fill the background and draw the square
57+
screen.fill((255, 255, 255))
58+
pygame.draw.aaline(screen, (0, 0, 255), screen_projection([-2, y_ground]), screen_projection([2, y_ground])) # ground
59+
for eI in e:
60+
pygame.draw.aaline(screen, (0, 0, 255), screen_projection(x[eI[0]]), screen_projection(x[eI[1]]))
61+
for xI in x:
62+
pygame.draw.circle(screen, (0, 0, 255), screen_projection(xI), 0.1 * side_len / n_seg * scale)
63+
64+
pygame.display.flip() # flip the display
65+
66+
# step forward simulation and wait for screen refresh
67+
[x, v] = time_integrator.step_forward(x, e, v, m, l2, k, y_ground, contact_area, is_DBC, h, 1e-2)
68+
time_step += 1
69+
pygame.time.wait(int(h * 1000))
70+
square_mesh.write_to_file(time_step, x, n_seg)
71+
72+
pygame.quit()

9_reduced_DOF/square_mesh.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import numpy as np
2+
import os
3+
4+
def generate(side_length, n_seg):
5+
# sample nodes uniformly on a square
6+
x = np.array([[0.0, 0.0]] * ((n_seg + 1) ** 2))
7+
step = side_length / n_seg
8+
for i in range(0, n_seg + 1):
9+
for j in range(0, n_seg + 1):
10+
x[i * (n_seg + 1) + j] = [-side_length / 2 + i * step, -side_length / 2 + j * step]
11+
12+
# connect the nodes with edges
13+
e = []
14+
# horizontal edges
15+
for i in range(0, n_seg):
16+
for j in range(0, n_seg + 1):
17+
e.append([i * (n_seg + 1) + j, (i + 1) * (n_seg + 1) + j])
18+
# vertical edges
19+
for i in range(0, n_seg + 1):
20+
for j in range(0, n_seg):
21+
e.append([i * (n_seg + 1) + j, i * (n_seg + 1) + j + 1])
22+
# diagonals
23+
for i in range(0, n_seg):
24+
for j in range(0, n_seg):
25+
e.append([i * (n_seg + 1) + j, (i + 1) * (n_seg + 1) + j + 1])
26+
e.append([(i + 1) * (n_seg + 1) + j, i * (n_seg + 1) + j + 1])
27+
28+
return [x, e]
29+
30+
def write_to_file(frameNum, x, n_seg):
31+
# Check if 'output' directory exists; if not, create it
32+
if not os.path.exists('output'):
33+
os.makedirs('output')
34+
35+
# create obj file
36+
filename = f"output/{frameNum}.obj"
37+
with open(filename, 'w') as f:
38+
# write vertex coordinates
39+
for row in x:
40+
f.write(f"v {float(row[0]):.6f} {float(row[1]):.6f} 0.0\n")
41+
# write vertex indices for each triangle
42+
for i in range(0, n_seg):
43+
for j in range(0, n_seg):
44+
#NOTE: each cell is exported as 2 triangles for rendering
45+
f.write(f"f {i * (n_seg+1) + j + 1} {(i+1) * (n_seg+1) + j + 1} {(i+1) * (n_seg+1) + j+1 + 1}\n")
46+
f.write(f"f {i * (n_seg+1) + j + 1} {(i+1) * (n_seg+1) + j+1 + 1} {i * (n_seg+1) + j+1 + 1}\n")

9_reduced_DOF/time_integrator.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import copy
2+
from cmath import inf
3+
4+
import numpy as np
5+
import numpy.linalg as LA
6+
import scipy.sparse as sparse
7+
from scipy.sparse.linalg import spsolve
8+
9+
import InertiaEnergy
10+
import MassSpringEnergy
11+
import GravityEnergy
12+
import BarrierEnergy
13+
14+
def step_forward(x, e, v, m, l2, k, y_ground, contact_area, is_DBC, h, tol):
15+
x_tilde = x + v * h # implicit Euler predictive position
16+
x_n = copy.deepcopy(x)
17+
18+
# Newton loop
19+
iter = 0
20+
E_last = IP_val(x, e, x_tilde, m, l2, k, y_ground, contact_area, h)
21+
p = search_dir(x, e, x_tilde, m, l2, k, y_ground, contact_area, is_DBC, h)
22+
while LA.norm(p, inf) / h > tol:
23+
print('Iteration', iter, ':')
24+
print('residual =', LA.norm(p, inf) / h)
25+
26+
# ANCHOR: filter_ls
27+
# filter line search
28+
alpha = BarrierEnergy.init_step_size(x, y_ground, p) # avoid interpenetration and tunneling
29+
while IP_val(x + alpha * p, e, x_tilde, m, l2, k, y_ground, contact_area, h) > E_last:
30+
alpha /= 2
31+
# ANCHOR_END: filter_ls
32+
print('step size =', alpha)
33+
34+
x += alpha * p
35+
E_last = IP_val(x, e, x_tilde, m, l2, k, y_ground, contact_area, h)
36+
p = search_dir(x, e, x_tilde, m, l2, k, y_ground, contact_area, is_DBC, h)
37+
iter += 1
38+
39+
v = (x - x_n) / h # implicit Euler velocity update
40+
return [x, v]
41+
42+
def IP_val(x, e, x_tilde, m, l2, k, y_ground, contact_area, h):
43+
return InertiaEnergy.val(x, x_tilde, m) + h * h * (MassSpringEnergy.val(x, e, l2, k) + GravityEnergy.val(x, m) + BarrierEnergy.val(x, y_ground, contact_area)) # implicit Euler
44+
45+
def IP_grad(x, e, x_tilde, m, l2, k, y_ground, contact_area, h):
46+
return InertiaEnergy.grad(x, x_tilde, m) + h * h * (MassSpringEnergy.grad(x, e, l2, k) + GravityEnergy.grad(x, m) + BarrierEnergy.grad(x, y_ground, contact_area)) # implicit Euler
47+
48+
def IP_hess(x, e, x_tilde, m, l2, k, y_ground, contact_area, h):
49+
IJV_In = InertiaEnergy.hess(x, x_tilde, m)
50+
IJV_MS = MassSpringEnergy.hess(x, e, l2, k)
51+
IJV_B = BarrierEnergy.hess(x, y_ground, contact_area)
52+
IJV_MS[2] *= h * h # implicit Euler
53+
IJV_B[2] *= h * h # implicit Euler
54+
IJV_In_MS = np.append(IJV_In, IJV_MS, axis=1)
55+
IJV = np.append(IJV_In_MS, IJV_B, axis=1)
56+
H = sparse.coo_matrix((IJV[2], (IJV[0], IJV[1])), shape=(len(x) * 2, len(x) * 2)).tocsr()
57+
return H
58+
59+
def search_dir(x, e, x_tilde, m, l2, k, y_ground, contact_area, is_DBC, h):
60+
projected_hess = IP_hess(x, e, x_tilde, m, l2, k, y_ground, contact_area, h)
61+
reshaped_grad = IP_grad(x, e, x_tilde, m, l2, k, y_ground, contact_area, h).reshape(len(x) * 2, 1)
62+
# eliminate DOF by modifying gradient and Hessian for DBC:
63+
for i, j in zip(*projected_hess.nonzero()):
64+
if is_DBC[int(i / 2)] | is_DBC[int(j / 2)]:
65+
projected_hess[i, j] = (i == j)
66+
for i in range(0, len(x)):
67+
if is_DBC[i]:
68+
reshaped_grad[i * 2] = reshaped_grad[i * 2 + 1] = 0.0
69+
return spsolve(projected_hess, -reshaped_grad).reshape(len(x), 2)

9_reduced_DOF/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import numpy as np
2+
import numpy.linalg as LA
3+
4+
def make_PSD(hess):
5+
[lam, V] = LA.eigh(hess) # Eigen decomposition on symmetric matrix
6+
# set all negative Eigenvalues to 0
7+
for i in range(0, len(lam)):
8+
lam[i] = max(0, lam[i])
9+
return np.matmul(np.matmul(V, np.diag(lam)), np.transpose(V))

0 commit comments

Comments
 (0)