import scipy.sparse as sp
import scipy.sparse.linalg as spla
# --- Voxelize: one solid cow, 2.4 m long ---
cow = mesh.copy()
cow.apply_scale(2.4 / np.ptp(cow.vertices[:, 0]))
h = 0.07 # voxel size in metres
vox = cow.voxelized(h).fill()
occ = vox.matrix # occ[i,j,k] = True if voxel is cow
origin = vox.transform[:3, 3] - h / 2 # corner of voxel (0,0,0)
els = np.argwhere(occ) # one hexahedral element per voxel
# --- Material: Hooke's law as a 6x6 matrix (Voigt notation) ---
E, nu = 1e7, 0.3 # E doesn't change the stresses
lam, mu = E * nu / ((1 + nu) * (1 - 2 * nu)), E / (2 * (1 + nu))
D = np.zeros((6, 6)); D[:3, :3] = lam
D += np.diag([2 * mu] * 3 + [mu] * 3)
# --- Element stiffness of one voxel (2x2x2 Gauss quadrature) ---
corners = np.array([[0,0,0],[1,0,0],[1,1,0],[0,1,0],[0,0,1],[1,0,1],[1,1,1],[0,1,1]])
s = 2 * corners - 1
def strain_matrix(xi):
"""B maps the 24 corner displacements to the 6 strain components at xi."""
dN = np.empty((8, 3))
for a in range(3):
b, c = [d for d in range(3) if d != a]
dN[:, a] = s[:, a] / 8 * (1 + s[:, b] * xi[b]) * (1 + s[:, c] * xi[c]) * 2 / h
B = np.zeros((6, 24))
for n, (dx, dy, dz) in enumerate(dN):
B[:, 3*n:3*n+3] = [[dx,0,0],[0,dy,0],[0,0,dz],[dy,dx,0],[0,dz,dy],[dz,0,dx]]
return B
g = 1 / np.sqrt(3)
Ke = sum(strain_matrix(xi).T @ D @ strain_matrix(xi) * (h / 2)**3
for xi in np.array(np.meshgrid([-g, g], [-g, g], [-g, g])).reshape(3, -1).T)
# --- Assemble the global stiffness matrix ---
nshape = np.array(occ.shape) + 1
node_id = lambda ijk: np.ravel_multi_index(np.asarray(ijk).T, nshape)
used, conn = np.unique(np.stack([node_id(els + c) for c in corners], axis=1),
return_inverse=True)
conn = conn.reshape(-1, 8) # element -> its 8 nodes
nodes = np.array(np.unravel_index(used, nshape)).T * h + origin
dofs = (3 * conn[:, :, None] + np.arange(3)).reshape(-1, 24)
ndof = 3 * len(nodes)
K = sp.coo_matrix(
(np.broadcast_to(Ke, (len(els), 24, 24)).ravel(),
(np.repeat(dofs, 24, axis=1).ravel(), np.tile(dofs, (1, 24)).ravel())),
shape=(ndof, ndof)).tocsr()
# --- Loads: gravity on every voxel + the press on the back ---
rho, grav, F_press = 1000.0, 9.81, 5000.0
F = np.zeros(ndof)
np.add.at(F, 3 * conn.ravel() + 2, -rho * grav * h**3 / 8)
top = els[~np.pad(occ, 1)[tuple((els + [1, 1, 2]).T)]] # nothing above these
x_top, z_top = (top * h + origin)[:, [0, 2]].T
x0 = nodes[:, 0].min()
back = top[(x_top > x0 + 0.25) & (x_top < x0 + 1.2) & (z_top > 0.3)]
back_nodes = np.searchsorted(used, node_id((back[:, None] + corners[4:]).reshape(-1, 3)))
np.add.at(F, 3 * back_nodes + 2, -F_press / len(back_nodes))
# --- Boundary condition: the lowest layer of each hoof is glued to the floor ---
z0 = nodes[:, 2].min()
hoof = np.where(nodes[:, 2] < z0 + 1.5 * h)[0]
fixed = (3 * hoof[:, None] + np.arange(3)).ravel()
free = np.setdiff1d(np.arange(ndof), fixed)
u = np.zeros(ndof)
u[free] = spla.spsolve(K[free][:, free].tocsc(), F[free])
# --- Von Mises stress in the centre of every voxel ---
sig = (D @ strain_matrix(np.zeros(3)) @ u[dofs].T).T
vm = np.sqrt(0.5 * ((sig[:, 0] - sig[:, 1])**2 + (sig[:, 1] - sig[:, 2])**2
+ (sig[:, 2] - sig[:, 0])**2) + 3 * (sig[:, 3:]**2).sum(1)) / 1e3
R = (K @ u - F)[3 * hoof + 2] # vertical force on the floor
front = nodes[hoof, 0] > nodes[hoof, 0].mean()
centres = els * h + origin + h / 2
legs = centres[:, 2] < z0 + 0.6
print(f"{len(els)} voxels, {ndof} unknowns")
print(f"cow mass: {occ.sum() * h**3 * rho:.0f} kg")
print(f"front legs carry {R[front].sum() / R.sum():.0%} of the load")
print(f"median von Mises stress: legs {np.median(vm[legs]):.1f} kPa, "
f"rest of the cow {np.median(vm[~legs]):.1f} kPa")
# --- Plot: every outer voxel face, coloured by the stress of its voxel ---
face_corners = {
(1,0,0): [(1,0,0),(1,1,0),(1,1,1),(1,0,1)], (-1,0,0): [(0,0,0),(0,0,1),(0,1,1),(0,1,0)],
(0,1,0): [(0,1,0),(0,1,1),(1,1,1),(1,1,0)], (0,-1,0): [(0,0,0),(1,0,0),(1,0,1),(0,0,1)],
(0,0,1): [(0,0,1),(1,0,1),(1,1,1),(0,1,1)], (0,0,-1): [(0,0,0),(0,1,0),(1,1,0),(1,0,0)],
}
pad = np.pad(occ, 1)
quads, stress = [], []
for d, cs in face_corners.items():
outside = ~pad[tuple((els + 1 + np.array(d)).T)]
quads.append((els[outside][:, None] + np.array(cs)) * h + origin)
stress.append(vm[outside])
quads, stress = np.concatenate(quads).reshape(-1, 3), np.concatenate(stress)
q = 4 * np.arange(len(stress))
fig = go.Figure(data=[go.Mesh3d(
x=quads[:, 0], y=quads[:, 1], z=quads[:, 2],
i=np.r_[q, q], j=np.r_[q + 1, q + 2], k=np.r_[q + 2, q + 3],
intensity=np.r_[stress, stress], intensitymode='cell',
colorscale='Inferno', cmin=0, cmax=np.percentile(vm, 99),
colorbar=dict(title='von Mises<br>stress [kPa]'),
flatshading=True, name='cow'
)])
fig.update_layout(
title='Linear elasticity FEM – stress in a cow under a press',
scene=dict(xaxis_title='X [m]', yaxis_title='Y [m]', zaxis_title='Z [m]',
aspectmode='data'),
width=800, height=600,
)
fig.show()