1"""dem gate: a COMPOSED analytic tree as a particle shape (SHAPE_SCENE).
3Two claims, both at OMP_NUM_THREADS=1 (dem's multithreaded step is nondeterministic — §6.0b):
5 1. STATIC CONTACT PROBE. For hand-placed overlapping pairs, the analytic-tree particle and the
6 SAME shape baked to a fine grid-SDF particle must report contacts of the same count, with
7 penetration depths agreeing to the grid's own resolution error, and the tree's depths must
8 match the EXACT closed form (a dumbbell's lobe is a sphere: depth = 2r − distance).
9 2. DYNAMICS. Two settling/colliding dumbbells: 400-step single-thread trajectories, tree vs
10 baked-grid shape. Same physics, different geometry representation: positions must track to
11 the grid's resolution error, not diverge.
13The dumbbell is authored in peclet.geom, mass properties measured by implicit quadrature,
14put in its principal frame with principal_frame() (a no-op rotation here, but the COM recentre is
15real when the lobes differ), and handed to dem BOTH ways from the SAME builder.
20assert os.environ.get(
"OMP_NUM_THREADS") ==
"1",
"run with OMP_NUM_THREADS=1 (dem determinism)"
22from peclet
import geom
23from peclet
import dem
as pdem
26R1, R2, OFF = 0.30, 0.22, 0.35
27b = geom.SceneBuilder()
28s1 = b.add_leaf(
"sphere", [R1], translation=[-OFF, 0.0, 0.0])
29s2 = b.add_leaf(
"sphere", [R2], translation=[+OFF, 0.0, 0.0])
30db = b.add_union(s1, s2)
31props = b.body_properties(db, lo=[-0.8, -0.5, -0.5], hi=[0.8, 0.5, 0.5], n=40)
32home = b.principal_frame(db, lo=[-0.8, -0.5, -0.5], hi=[0.8, 0.5, 0.5], n=40)
33ph = b.body_properties(home, lo=[-0.9, -0.6, -0.6], hi=[0.9, 0.6, 0.6], n=40)
34com = np.array(props[
"com"])
35print(
"dumbbell: com=%s principal=%s (reframed com -> %s)"
36 % (np.round(com, 5), np.round(props[
"principal"], 6), np.round(ph[
"com"], 6)))
37inv_i_unit = tuple(props[
"mass"] / np.maximum(np.array(ph[
"principal"]), 1e-12))
38bound = float(OFF + max(R1, R2) + abs(com[0]) + 0.05)
43sp = 2 * half / (NB - 1)
44gridf = np.asarray(b.bake(home, origin=[-half, -half, -half], spacing=[sp, sp, sp],
46grid3 = gridf.reshape(NB, NB, NB, order=
"F")
47from skimage
import measure
48verts, faces, _, _ = measure.marching_cubes(np.ascontiguousarray(grid3), level=0.0,
50verts += np.array([-half, -half, -half])
51step = max(1, len(verts) // 400)
52shell = np.ascontiguousarray(verts[::step], dtype=np.float32)
53print(
"bake %d^3 (spacing %.4f = %.3f of the small radius), shell %d points"
54 % (NB, sp, sp / R2, len(shell)))
57 d = pdem.Simulation(4)
58 d.initialize(shape_type=1, radius=0.5)
59 d.set_domain((0.0, 0.0, 0.0), (8.0, 8.0, 8.0))
60 d.enable_periodicity(
True,
True,
True)
61 d.set_gravity(0.0, 0.0, 0.0)
62 ni, nr, ii, ir = b.encode()
64 sid = d.add_scene_shape(np.ascontiguousarray(ni, dtype=np.int32),
65 np.ascontiguousarray(nr, dtype=np.float32), home,
66 shell, inv_i_unit, bound)
68 sid = d.add_sdf_shape(np.ascontiguousarray(gridf, dtype=np.float32), NB, NB, NB,
69 (-half, -half, -half), (sp, sp, sp), shell, inv_i_unit, bound)
71 pos = np.zeros((2, 4), dtype=np.float32)
72 pos[0, 0:3] = (3.2, 4.0, 4.0); pos[0, 3] = 1.0
73 pos[1, 0:3] = (4.9, 4.1, 4.0); pos[1, 3] = 1.0
75 q = np.zeros((2, 4), dtype=np.float32)
77 q[1] = (0, 0, np.sin(np.pi / 4), np.cos(np.pi / 4))
79 v = np.zeros((2, 4), dtype=np.float32)
80 v[0, 0] = +0.6; v[1, 0] = -0.6
82 d.set_shape_ids(np.array([sid, sid], dtype=np.int32))
83 d.set_material_params(0.3, 0.2, 0.0)
84 d.set_solver_iterations(20, 20)
95DX = a1 + a2 + (R1 + R2) - PEN
101 pos = np.zeros((2, 4), dtype=np.float32)
102 pos[0, 0:3] = (3.0, 4.0, 4.0); pos[0, 3] = 1.0
103 pos[1, 0:3] = (3.0 + DX, 4.0, 4.0); pos[1, 3] = 1.0
108 d.set_shape_ids(np.array([1, 1], dtype=np.int32))
109 d.set_quaternions(np.array([[0, 0, 0, 1], [0, 0, 0, 1]], dtype=np.float32))
110 d.set_velocities(np.zeros((2, 4), dtype=np.float32))
113 q = np.array(d.get_positions())
114 return float(q[1, 0] - q[0, 0])
115touch = a1 + a2 + R1 + R2
117print(
"relaxed separation: exact %.5f tree %.5f (err %.2e) grid %.5f (err %.2e; resolution %.4f)"
118 % (touch, st, abs(st - touch), sg, abs(sg - touch), sp))
119ok1 = abs(st - touch) < 5e-3
and abs(sg - touch) < 3 * sp
126 if (k + 1) % 100 == 0:
127 traj.append(np.array(d.get_positions()))
128 return np.array(traj), np.array(d.get_velocities()), np.array(d.get_quaternions())
132dpos = np.abs(Tt - Tg).max(axis=(1, 2))
133print(
"trajectory |tree - grid| at steps 100/200/300/400:", np.round(dpos, 5))
134print(
" (the grid's own geometry error is ~%.4f; agreement is claimed at that scale, not tighter)"
136dvel = np.abs(Vt - Vg).max()
137dquat = np.abs(Qt - Qg).max()
138print(
"final state |tree - grid|: velocities %.5f quaternions %.5f" % (dvel, dquat))
139ok2 = dpos[-1] < 10 * sp
140print(
"GATE %s [tree particle collides like its own fine-grid bake]"
141 % (
"PASS" if (ok1
and ok2)
else "FAIL"))
march(d, n=400, dtstep=0.01)