Skip to content

Commit 7ef5c63

Browse files
committed
init contact
1 parent dfeb526 commit 7ef5c63

8 files changed

Lines changed: 247 additions & 0 deletions

File tree

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

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

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

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

contact/simulator.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 = 1e3 # spring stiffness
14+
n_seg = 4 # num of segments per side of the square
15+
h = 0.02 # time step size in s
16+
DBC = [0, (n_seg + 1) * n_seg] # fix the left and right top nodes
17+
18+
# initialize simulation
19+
[x, e] = square_mesh.generate(side_len, n_seg) # node positions and edge node indices
20+
v = np.array([[0.0, 0.0]] * len(x)) # velocity
21+
m = [rho * side_len * side_len / ((n_seg + 1) * (n_seg + 1))] * len(x) # calculate node mass evenly
22+
# rest length squared
23+
l2 = []
24+
for i in range(0, len(e)):
25+
diff = x[e[i][0]] - x[e[i][1]]
26+
l2.append(diff.dot(diff))
27+
k = [k] * len(e) # spring stiffness
28+
# identify whether a node is Dirichlet
29+
is_DBC = [False] * len(x)
30+
for i in DBC:
31+
is_DBC[i] = True
32+
33+
# simulation with visualization
34+
time_step = 0
35+
screen = pygame.display.set_mode([900, 900])
36+
running = True
37+
while running:
38+
# run until the user asks to quit
39+
for event in pygame.event.get():
40+
if event.type == pygame.QUIT:
41+
running = False
42+
43+
print('### Time step', time_step, '###')
44+
45+
# fill the background and draw the square
46+
screen.fill((255, 255, 255))
47+
for eI in e:
48+
pygame.draw.aaline(screen, (0, 0, 255), 450 + x[eI[0]] * 200, 450 + x[eI[1]] * 200)
49+
for xI in x:
50+
pygame.draw.circle(screen, (0, 0, 255), 450 + xI * 200, 0.1 * side_len / n_seg * 200)
51+
52+
pygame.display.flip() # flip the display
53+
54+
# step forward simulation and wait for screen refresh
55+
[x, v] = time_integrator.step_forward(x, e, v, m, l2, k, is_DBC, h, 1e-2)
56+
time_step += 1
57+
pygame.time.wait(int(h * 1000))
58+
59+
pygame.quit()

contact/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]

contact/time_integrator.py

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

contact/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)