flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
torque_gate.py
Go to the documentation of this file.
1"""R2 gate: dem's external-TORQUE entry point (set_external_torques).
2
3Two claims, both at OMP_NUM_THREADS=1 (dem's multithreaded step is nondeterministic -- ยง6.0b).
4One free body, no gravity, no contacts, no walls: the predictor's rotational update is the whole
5physics, so a miss is the torque term and nothing else.
6
7 A. PRINCIPAL AXIS -- EXACT. A constant world torque about a principal axis of a body whose
8 angular velocity is already along that axis keeps w x (I w) identically zero, so the discrete
9 update degenerates to w_{n+1} = w_n + (tau/I) dt and the trajectory is the closed form
10 w(t) = tau t / I with NO time-stepping error at all. Anything but round-off here is a wiring
11 bug (wrong inertia component, body/world frame confusion, torque dropped).
12
13 B. NON-PRINCIPAL AXIS -- vs a scipy reference. A constant WORLD torque about a tilted axis on a
14 body with three distinct principal moments: the body frame tumbles under it, so tau_body(t)
15 is time dependent and the trajectory is genuinely Euler's equations. Reference: solve_ivp
16 (DOP853, rtol/atol 1e-12) on the SAME system in dem's own convention --
17 q = (x,y,z,w), R(q) maps BODY -> WORLD, dq/dt = 0.5 (0, w_world) (x) q
18 dw_b/dt = invI (R^T tau_world - w_b x I w_b)
19 dem's integrator is semi-implicit Euler, so the claim is a dt-CONVERGENCE ladder (first
20 order), taken at COARSE dt where truncation dominates -- below dt ~ 1e-3 the float32 state
21 floor (gate A) is already larger than the truncation error and the ladder inverts, which is
22 itself reported.
23
24 C. Angular-momentum bookkeeping, free: dL_world/dt = tau_world exactly in the continuum, so
25 |L(t) - L(0) - tau t| is a second, convention-independent read of the same error.
26"""
27import os
28import numpy as np
29
30assert os.environ.get("OMP_NUM_THREADS") == "1", "run with OMP_NUM_THREADS=1 (dem determinism)"
31
32from peclet import dem as pdem
33from scipy.integrate import solve_ivp
34
35# Three DISTINCT principal moments (unit mass), so no accidental symmetry can hide a frame error.
36I = np.array([0.40, 0.65, 0.90])
37INV_I = 1.0 / I
38
39
40def qmul(a, b): # (x,y,z,w) convention, same algebra as dem_portable.hpp quatMult
41 ax, ay, az, aw = a
42 bx, by, bz, bw = b
43 return np.array([aw*bx + ax*bw + ay*bz - az*by,
44 aw*by - ax*bz + ay*bw + az*bx,
45 aw*bz + ax*by - ay*bx + az*bw,
46 aw*bw - ax*bx - ay*by - az*bz])
47
48
49def rotmat(q): # body -> world
50 x, y, z, w = q
51 return np.array([
52 [1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+y*w)],
53 [2*(x*y+z*w), 1-2*(x*x+z*z), 2*(y*z-x*w)],
54 [2*(x*z-y*w), 2*(y*z+x*w), 1-2*(x*x+y*y)]])
55
56
57def make_sim(w0, tau, dt):
58 """One free unit-mass body with the principal inertia I, spinning at w0 under world torque."""
59 s = pdem.Simulation(64)
60 s.set_gravity(0.0, 0.0, 0.0)
61 s.set_dt(dt)
62 s.set_positions(np.array([[0.0, 0.0, 0.0]], dtype=np.float32))
63 s.set_inv_mass(np.array([1.0], dtype=np.float32))
64 s.set_inv_inertia(np.array([INV_I], dtype=np.float32))
65 s.set_quaternions(np.array([[0.0, 0.0, 0.0, 1.0]], dtype=np.float32))
66 s.set_angular_velocities(np.array([w0], dtype=np.float32))
67 s.set_external_torques(np.array([tau], dtype=np.float32))
68 return s
69
70
71def run(s, dt, nsteps):
72 for _ in range(nsteps):
73 s.step(dt)
74 q = np.asarray(s.get_quaternions())[0].astype(np.float64)
75 w = np.asarray(s.get_angular_velocities())[0].astype(np.float64)
76 return q / np.linalg.norm(q), w
77
78
79def reference(w0_world, q0, tau, T):
80 def rhs(t, y):
81 q = y[0:4] / np.linalg.norm(y[0:4])
82 wb = y[4:7]
83 R = rotmat(q)
84 ww = R @ wb
85 dq = 0.5 * qmul(np.array([ww[0], ww[1], ww[2], 0.0]), q)
86 tb = R.T @ tau
87 dwb = INV_I * (tb - np.cross(wb, I * wb))
88 return np.concatenate([dq, dwb])
89 wb0 = rotmat(q0).T @ w0_world
90 y0 = np.concatenate([q0, wb0])
91 sol = solve_ivp(rhs, (0.0, T), y0, method="DOP853", rtol=1e-12, atol=1e-13, dense_output=True)
92 y = sol.y[:, -1]
93 q = y[0:4] / np.linalg.norm(y[0:4])
94 return q, rotmat(q) @ y[4:7]
95
96
97def gate_a():
98 print("A. torque about a principal axis (exact closed form w(t) = tau t / I)")
99 ok = True
100 for axis in range(3):
101 tau = np.zeros(3); tau[axis] = 0.25
102 dt, n = 1e-3, 4000
103 s = make_sim(np.zeros(3), tau, dt)
104 _, w = run(s, dt, n)
105 exact = np.zeros(3); exact[axis] = tau[axis] * INV_I[axis] * dt * n
106 err = np.linalg.norm(w - exact) / np.linalg.norm(exact)
107 ok = ok and err < 1e-4
108 print(" axis %d: I=%.2f w_sim=%s w_exact=%s rel err %.2e"
109 % (axis, I[axis], np.round(w, 8), np.round(exact, 8), err))
110 # The residual is float32 accumulation, not truncation: at fixed end time it must GROW with
111 # the step count. Exhibit that rather than asserting it.
112 tau = np.array([0.25, 0.0, 0.0])
113 print(" float32 floor, fixed T=4: error vs step count (truncation would be flat at zero)")
114 for dt in (4e-3, 1e-3, 2.5e-4):
115 n = int(round(4.0 / dt))
116 s = make_sim(np.zeros(3), tau, dt)
117 _, w = run(s, dt, n)
118 ex = tau[0] * INV_I[0] * 4.0
119 print(" dt=%.1e %6d steps rel err %.2e (%.1f x float32 eps per 1000 steps)"
120 % (dt, n, abs(w[0] - ex) / ex, abs(w[0] - ex) / ex / (n * 1.19e-7) * 1000))
121 print(" GATE A %s (relative error < 1e-4 = dem's float32 accumulation floor at 4000 steps;"
122 " the closed form is exact by construction)" % ("PASS" if ok else "FAIL"))
123 return ok
124
125
126def gate_b():
127 print("B. torque about a NON-principal axis vs scipy Euler-equation reference")
128 tau = np.array([0.20, -0.13, 0.31])
129 w0 = np.array([0.9, 0.4, -0.6])
130 q0 = np.array([0.0, 0.0, 0.0, 1.0])
131 T = 2.0
132 qr, wr = reference(w0, q0, tau, T)
133 rows, prev = [], None
134 for dt in (8e-3, 4e-3, 2e-3, 1e-3):
135 n = int(round(T / dt))
136 s = make_sim(w0, tau, dt)
137 q, w = run(s, dt, n)
138 if np.dot(q, qr) < 0:
139 q = -q
140 ew = np.linalg.norm(w - wr) / np.linalg.norm(wr)
141 eq = np.linalg.norm(q - qr)
142 rate = np.log2(prev / ew) if prev else float("nan")
143 rows.append((dt, ew, eq, rate))
144 prev = ew
145 print(" dt=%.1e |dw|/|w| = %.3e |dq| = %.3e order %s"
146 % (dt, ew, eq, " --" if np.isnan(rate) else "%.2f" % rate))
147 # first order in dt, and the finest step within 0.5%
148 orders = [r[3] for r in rows[1:]]
149 ok = rows[-1][1] < 1e-3 and min(orders) > 0.75
150 print(" w_ref = %s w_sim(dt=1e-3) = %s" % (np.round(wr, 6), np.round(w, 6)))
151 print(" GATE B %s (finest-dt error < 0.1%%, convergence order > 0.75 -- semi-implicit Euler)"
152 % ("PASS" if ok else "FAIL"))
153 # Below the truncation/round-off crossover the ladder inverts: report it, do not hide it.
154 print(" crossover: the same ladder continued past the float32 floor")
155 for dt in (2e-4, 5e-5):
156 s = make_sim(w0, tau, dt)
157 q, w = run(s, dt, int(round(T / dt)))
158 print(" dt=%.1e |dw|/|w| = %.3e (float32 state, %d steps)"
159 % (dt, np.linalg.norm(w - wr) / np.linalg.norm(wr), int(round(T / dt))))
160 return ok
161
162
163def gate_c():
164 print("C. angular-momentum bookkeeping: L_world(t) - L_world(0) = tau t")
165 tau = np.array([0.20, -0.13, 0.31])
166 w0 = np.array([0.9, 0.4, -0.6])
167 T = 2.0
168 L0 = rotmat(np.array([0.0, 0.0, 0.0, 1.0])) @ (I * w0)
169 ok = True
170 for dt in (4e-4, 1e-4, 5e-5):
171 s = make_sim(w0, tau, dt)
172 q, w = run(s, dt, int(round(T / dt)))
173 R = rotmat(q)
174 L = R @ (I * (R.T @ w))
175 err = np.linalg.norm(L - L0 - tau * T) / np.linalg.norm(L0 + tau * T)
176 ok = ok and err < 2e-2
177 print(" dt=%.1e |L - L0 - tau t| / |L| = %.3e" % (dt, err))
178 print(" GATE C %s" % ("PASS" if ok else "FAIL"))
179 return ok
180
181
182if __name__ == "__main__":
183 a, b, c = gate_a(), gate_b(), gate_c()
184 print("R2 GATE %s" % ("PASS" if (a and b and c) else "FAIL"))
run(s, dt, nsteps)
make_sim(w0, tau, dt)
reference(w0_world, q0, tau, T)