38 """Shaft capsule + two pitched blade boxes, optionally AUTHORED already rotated by theta."""
39 q = (0.0, 0.0, float(np.sin(0.5 * theta)), float(np.cos(0.5 * theta)))
40 shaft = b.add_leaf(
"capsule", [SHAFT_R, 2.0], rotation=[1.0, 0.0, 0.0, 0.0])
41 bl1 = b.add_leaf(
"box", [BLADE, 0.15, 0.9], translation=[BLADE, 0.0, 0.0])
42 bl2 = b.add_leaf(
"box", [BLADE, 0.15, 0.9], translation=[-BLADE, 0.0, 0.0])
43 u = b.add_union(b.add_union(shaft, bl1), bl2)
44 return b.add_reframed(u, translation=[0.0, 0.0, 0.0], rotation=list(q))
if theta
else u
48 b = geom.SceneBuilder()
50 ni, nr, _, _ = b.encode()
51 return sim.add_analytic_wall(np.asarray(ni, dtype=np.int32),
52 np.asarray(nr, dtype=np.float32),
53 root,
False, 0.3, 0.4)
62 print(
"A. placed-vs-authored wall SDF, bitwise")
65 q = (0.0, 0.0, float(np.sin(0.5 * theta)), float(np.cos(0.5 * theta)))
66 t = (0.31, -0.17, 0.05)
68 s1 = pdem.Simulation(64)
70 home = np.asarray(s1.wall_sdf_at(w1, pts), dtype=np.float32)
71 s1.set_wall_transform(w1, t, q)
72 placed = np.asarray(s1.wall_sdf_at(w1, pts), dtype=np.float32)
74 s2 = pdem.Simulation(64)
77 s2.set_wall_transform(w2, t, (0.0, 0.0, 0.0, 1.0))
78 authored = np.asarray(s2.wall_sdf_at(w2, pts), dtype=np.float32)
80 bitwise = np.array_equal(placed.view(np.int32), authored.view(np.int32))
81 moved = float(np.abs(placed - home).max())
82 print(
" probes %d placed vs authored: bitwise-equal %s max|diff| %.3e"
83 % (len(pts), bitwise, float(np.abs(placed - authored).max())))
84 print(
" (the placement is not a no-op: max|placed - home| = %.4f)" % moved)
87 s1.set_wall_transform(w1, t, q)
88 again = np.asarray(s1.wall_sdf_at(w1, pts), dtype=np.float32)
89 absolute = np.array_equal(again.view(np.int32), placed.view(np.int32))
90 s1.set_wall_transform(w1, (0.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))
91 back = np.asarray(s1.wall_sdf_at(w1, pts), dtype=np.float32)
92 restores = np.array_equal(back.view(np.int32), home.view(np.int32))
93 print(
" absolute (100 identical calls bitwise stable): %s returns home bitwise: %s"
94 % (absolute, restores))
95 ok = bitwise
and absolute
and restores
and moved > 0.1
96 print(
" GATE A %s" % (
"PASS" if ok
else "FAIL"))
100AXIS_Q = [float(np.sin(np.pi / 4)), 0.0, 0.0, float(np.cos(np.pi / 4))]
111def drum(rotate_geometry, nsteps=400, omega=0.8, phase=0.0):
112 """Settled bed in an axisymmetric barrel; optionally rotate the barrel GEOMETRY each step."""
114 s = pdem.Simulation(4096)
115 s.set_gravity(0.0, -9.81, 0.0)
116 s.set_sphere_shape(1.0)
117 s.set_global_scale(0.4)
118 w = s.add_analytic_wall(np.asarray(ni, dtype=np.int32), np.asarray(nr, dtype=np.float32),
119 barrel,
True, 0.2, 1.0)
120 rng = np.random.default_rng(11)
123 p = rng.uniform(-1, 1, 3) * np.array([RB - 1.2, RB - 1.2, HB - 0.8])
124 if p[0] ** 2 + p[1] ** 2 < (RB - 1.2) ** 2:
126 pos = np.asarray(pos[:120], dtype=np.float32)
132 for _
in range(nsteps):
133 theta += omega * 2e-3
134 s.set_wall_velocity(w, (0.0, 0.0, 0.0), (0.0, 0.0, omega), (0.0, 0.0, 0.0))
137 s.set_wall_transform(w, (0.0, 0.0, 0.0),
138 (0.0, 0.0, float(np.sin(0.5 * a)), float(np.cos(0.5 * a))))
140 return np.asarray(s.get_positions(), dtype=np.float32)
144 print(
"B. axisymmetric control: rotating the drum GEOMETRY must not change the trajectory")
147 s0 = pdem.Simulation(64)
148 w0 = s0.add_analytic_wall(np.asarray(ni, dtype=np.int32), np.asarray(nr, dtype=np.float32),
149 barrel,
True, 0.2, 1.0)
150 pts = (np.random.default_rng(5).uniform(-5.0, 5.0, size=(4000, 3))).astype(np.float32)
151 f0 = np.asarray(s0.wall_sdf_at(w0, pts), dtype=np.float32)
153 s0.set_wall_transform(w0, (0.0, 0.0, 0.0),
154 (0.0, 0.0, float(np.sin(0.5 * th)), float(np.cos(0.5 * th))))
155 f1 = np.asarray(s0.wall_sdf_at(w0, pts), dtype=np.float32)
156 dsdf = float(np.abs(f1 - f0).max())
157 print(
" (i) self-axis rotation of the barrel: max|dSDF| = %.3e over %d probes (float32 "
158 "re-rounding of the composed transform; the analytic value is 0)" % (dsdf, len(pts)))
166 c =
drum(
True, phase=0.0)
167 e =
drum(
True, phase=0.37)
170 print(
" (ii) %-28s max|dx| %.3e (%.2e grain radii) rms %.3e"
171 % (lbl, float(d.max()), float(d.max()) / rad, float(np.sqrt((d ** 2).
mean()))))
172 return float(d.max()) / rad
173 m_sr = rep(
"static vs rotated", a, c)
174 m_rr = rep(
"rotated vs rotated (control)", c, e)
175 m_sr2 = rep(
"static vs rotated(phase)", a, e)
176 worst = max(m_sr, m_rr, m_sr2)
180 return np.array([u[:, 0].
mean(), u[:, 1].
mean(), u[:, 2].
mean()])
181 b_a, b_c, b_e = bulk(a), bulk(c), bulk(e)
182 dcom = float(np.abs(b_a - b_c).max()) / rad
183 print(
" (ii) bed centre of mass: static %s rotated %s -> |dCOM| = %.2e grain radii"
184 % (np.round(b_a, 5), np.round(b_c, 5), dcom))
185 ok = (dsdf < 1e-5
and worst < 0.5
and m_sr <= 3.0 * max(m_rr, 1e-12)
and dcom < 0.02)
186 print(
" GATE B %s (field difference at the float32 floor; the per-grain spread is bounded "
187 "by the ROTATED-vs-ROTATED control at %.2f grain radii -- two runs that both move the "
188 "geometry differ by MORE than static-vs-rotated does, so the spread is float32 rounding "
189 "amplified by a chaotic cascade, not an effect of moving the geometry; the bulk state "
190 "agrees to %.1e grain radii)" % (
"PASS" if ok
else "FAIL", m_rr, dcom))