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])
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)]])
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)
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))
81 q = y[0:4] / np.linalg.norm(y[0:4])
85 dq = 0.5 *
qmul(np.array([ww[0], ww[1], ww[2], 0.0]), q)
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)
93 q = y[0:4] / np.linalg.norm(y[0:4])
94 return q,
rotmat(q) @ y[4:7]
98 print(
"A. torque about a principal axis (exact closed form w(t) = tau t / I)")
100 for axis
in range(3):
101 tau = np.zeros(3); tau[axis] = 0.25
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))
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))
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"))
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])
133 rows, prev = [],
None
134 for dt
in (8e-3, 4e-3, 2e-3, 1e-3):
135 n = int(round(T / dt))
138 if np.dot(q, qr) < 0:
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))
145 print(
" dt=%.1e |dw|/|w| = %.3e |dq| = %.3e order %s"
146 % (dt, ew, eq,
" --" if np.isnan(rate)
else "%.2f" % rate))
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"))
154 print(
" crossover: the same ladder continued past the float32 floor")
155 for dt
in (2e-4, 5e-5):
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))))
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])
168 L0 =
rotmat(np.array([0.0, 0.0, 0.0, 1.0])) @ (I * w0)
170 for dt
in (4e-4, 1e-4, 5e-5):
172 q, w =
run(s, dt, int(round(T / dt)))
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"))