Skip to content

Commit 32645a1

Browse files
committed
init commit - friction
1 parent 0f1bf59 commit 32645a1

9 files changed

Lines changed: 302 additions & 0 deletions

File tree

friction/BarrierEnergy.py

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

friction/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

friction/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

friction/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_PD(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

friction/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+
```

friction/simulator.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 = 2e4 # spring stiffness
14+
n_seg = 4 # num of segments per side of the square
15+
h = 0.01 # 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+
contact_area = [side_len / n_seg] * len(x) # perimeter split to each node
34+
35+
# simulation with visualization
36+
resolution = np.array([900, 900])
37+
offset = resolution / 2
38+
scale = 200
39+
def screen_projection(x):
40+
return [offset[0] + scale * x[0], resolution[1] - (offset[1] + scale * x[1])]
41+
42+
time_step = 0
43+
screen = pygame.display.set_mode(resolution)
44+
running = True
45+
while running:
46+
# run until the user asks to quit
47+
for event in pygame.event.get():
48+
if event.type == pygame.QUIT:
49+
running = False
50+
51+
print('### Time step', time_step, '###')
52+
53+
# fill the background and draw the square
54+
screen.fill((255, 255, 255))
55+
pygame.draw.aaline(screen, (0, 0, 255), screen_projection([-2, y_ground]), screen_projection([2, y_ground])) # ground
56+
for eI in e:
57+
pygame.draw.aaline(screen, (0, 0, 255), screen_projection(x[eI[0]]), screen_projection(x[eI[1]]))
58+
for xI in x:
59+
pygame.draw.circle(screen, (0, 0, 255), screen_projection(xI), 0.1 * side_len / n_seg * scale)
60+
61+
pygame.display.flip() # flip the display
62+
63+
# step forward simulation and wait for screen refresh
64+
[x, v] = time_integrator.step_forward(x, e, v, m, l2, k, y_ground, contact_area, is_DBC, h, 1e-2)
65+
time_step += 1
66+
pygame.time.wait(int(h * 1000))
67+
68+
pygame.quit()

friction/square_mesh.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import numpy as np
2+
3+
def generate(side_length, n_seg):
4+
# sample nodes uniformly on a square
5+
x = np.array([[0.0, 0.0]] * ((n_seg + 1) ** 2))
6+
step = side_length / n_seg
7+
for i in range(0, n_seg + 1):
8+
for j in range(0, n_seg + 1):
9+
x[i * (n_seg + 1) + j] = [-side_length / 2 + i * step, -side_length / 2 + j * step]
10+
11+
# connect the nodes with edges
12+
e = []
13+
# horizontal edges
14+
for i in range(0, n_seg):
15+
for j in range(0, n_seg + 1):
16+
e.append([i * (n_seg + 1) + j, (i + 1) * (n_seg + 1) + j])
17+
# vertical edges
18+
for i in range(0, n_seg + 1):
19+
for j in range(0, n_seg):
20+
e.append([i * (n_seg + 1) + j, i * (n_seg + 1) + j + 1])
21+
# diagonals
22+
for i in range(0, n_seg):
23+
for j in range(0, n_seg):
24+
e.append([i * (n_seg + 1) + j, (i + 1) * (n_seg + 1) + j + 1])
25+
e.append([(i + 1) * (n_seg + 1) + j, i * (n_seg + 1) + j + 1])
26+
27+
return [x, e]

friction/time_integrator.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
# filter line search
27+
alpha = BarrierEnergy.init_step_size(x, y_ground, p) # avoid interpenetration and tunneling
28+
while IP_val(x + alpha * p, e, x_tilde, m, l2, k, y_ground, contact_area, h) > E_last:
29+
alpha /= 2
30+
print('step size =', alpha)
31+
32+
x += alpha * p
33+
E_last = IP_val(x, e, x_tilde, m, l2, k, y_ground, contact_area, h)
34+
p = search_dir(x, e, x_tilde, m, l2, k, y_ground, contact_area, is_DBC, h)
35+
iter += 1
36+
37+
v = (x - x_n) / h # implicit Euler velocity update
38+
return [x, v]
39+
40+
def IP_val(x, e, x_tilde, m, l2, k, y_ground, contact_area, h):
41+
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
42+
43+
def IP_grad(x, e, x_tilde, m, l2, k, y_ground, contact_area, h):
44+
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
45+
46+
def IP_hess(x, e, x_tilde, m, l2, k, y_ground, contact_area, h):
47+
IJV_In = InertiaEnergy.hess(x, x_tilde, m)
48+
IJV_MS = MassSpringEnergy.hess(x, e, l2, k)
49+
IJV_B = BarrierEnergy.hess(x, y_ground, contact_area)
50+
IJV_MS[2] *= h * h # implicit Euler
51+
IJV_B[2] *= h * h # implicit Euler
52+
IJV_In_MS = np.append(IJV_In, IJV_MS, axis=1)
53+
IJV = np.append(IJV_In_MS, IJV_B, axis=1)
54+
H = sparse.coo_matrix((IJV[2], (IJV[0], IJV[1])), shape=(len(x) * 2, len(x) * 2)).tocsr()
55+
return H
56+
57+
def search_dir(x, e, x_tilde, m, l2, k, y_ground, contact_area, is_DBC, h):
58+
projected_hess = IP_hess(x, e, x_tilde, m, l2, k, y_ground, contact_area, h)
59+
reshaped_grad = IP_grad(x, e, x_tilde, m, l2, k, y_ground, contact_area, h).reshape(len(x) * 2, 1)
60+
# eliminate DOF by modifying gradient and Hessian for DBC:
61+
for i, j in zip(*projected_hess.nonzero()):
62+
if is_DBC[int(i / 2)] | is_DBC[int(j / 2)]:
63+
projected_hess[i, j] = (i == j)
64+
for i in range(0, len(x)):
65+
if is_DBC[i]:
66+
reshaped_grad[i * 2] = reshaped_grad[i * 2 + 1] = 0.0
67+
return spsolve(projected_hess, -reshaped_grad).reshape(len(x), 2)

friction/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_PD(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)