flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
scene_particle_gate.py
Go to the documentation of this file.
1"""dem gate: a COMPOSED analytic tree as a particle shape (SHAPE_SCENE).
2
3Two claims, both at OMP_NUM_THREADS=1 (dem's multithreaded step is nondeterministic — §6.0b):
4
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.
12
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.
16"""
17import os
18import numpy as np
19
20assert os.environ.get("OMP_NUM_THREADS") == "1", "run with OMP_NUM_THREADS=1 (dem determinism)"
21
22from peclet import geom
23from peclet import dem as pdem
24
25# --- author the particle: an ASYMMETRIC dumbbell (different lobe radii => real COM shift) -------
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)
39
40# --- shell + grid bake from the SAME reframed tree ---------------------------------------------
41NB = 96
42half = bound * 1.05
43sp = 2 * half / (NB - 1)
44gridf = np.asarray(b.bake(home, origin=[-half, -half, -half], spacing=[sp, sp, sp],
45 dims=[NB, NB, NB]))
46grid3 = gridf.reshape(NB, NB, NB, order="F")
47from skimage import measure
48verts, faces, _, _ = measure.marching_cubes(np.ascontiguousarray(grid3), level=0.0,
49 spacing=(sp, sp, sp))
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)))
55
56def make_sim(kind):
57 d = pdem.Simulation(4)
58 d.initialize(shape_type=1, radius=0.5) # placeholder shape 0
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()
63 if kind == "tree":
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)
67 else:
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)
70 # two particles on a collision course, second one rotated 90 deg about z
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
74 d.set_positions(pos)
75 q = np.zeros((2, 4), dtype=np.float32)
76 q[0] = (0, 0, 0, 1)
77 q[1] = (0, 0, np.sin(np.pi / 4), np.cos(np.pi / 4))
78 d.set_quaternions(q)
79 v = np.zeros((2, 4), dtype=np.float32)
80 v[0, 0] = +0.6; v[1, 0] = -0.6
81 d.set_velocities(v)
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)
85 return d
86
87# --- 1. static contact probe: hand-placed interpenetration with an exact closed form ----------
88# Facing +x lobes: after the reframe the lobes sit at body x = -OFF-com (radius R1) and
89# OFF-com (radius R2). Identity quaternions, centres DX apart along x => the touching pair is
90# particle0's +x lobe (R2, at +a2) against particle1's -x lobe (R1, at -a1):
91# overlap_exact = (R1 + R2) - (DX - a1 - a2)
92a1 = OFF + com[0] # |body-x| of the R1 lobe after recentring (com is negative)
93a2 = OFF - com[0]
94PEN = 0.08
95DX = a1 + a2 + (R1 + R2) - PEN
96# step(0) is dem's documented dynamics-free relaxation (overlap removal only): after it, the
97# centre separation must be the exact touching distance a1 + a2 + R1 + R2 -- the closed form the
98# tree must hit and the grid may miss by its resolution.
99def probe(kind):
100 d = make_sim(kind)
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
104 d.set_positions(pos)
105 # set_positions RESETS every particle to shape 0 (documented dem gotcha: it re-applies the
106 # default shape + inertia) -- re-assign the composed shape AFTER it, or the probe silently
107 # relaxes two placeholder spheres.
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))
111 for _ in range(40):
112 d.step(0.0)
113 q = np.array(d.get_positions())
114 return float(q[1, 0] - q[0, 0])
115touch = a1 + a2 + R1 + R2
116st, sg = probe("tree"), probe("grid")
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
120
121# --- 2. dynamics -------------------------------------------------------------------------------
122def march(d, n=400, dtstep=0.01):
123 traj = []
124 for k in range(n):
125 d.step(dtstep)
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())
129
130Tt, Vt, Qt = march(make_sim("tree"))
131Tg, Vg, Qg = march(make_sim("grid"))
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)"
135 % sp)
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)