1"""Centre of rotation: NaN follows the body, any finite point pins -- the world origin included.
3Semantics gated (ยง7 item 3):
4 (a) builder default (NaN) -> centre = the translation, instance_center_pinned False;
5 (b) builder center=(0,0,0) -> PINNED at the world origin (the case zero-as-unset made impossible);
6 (c) raw array with zeros -> legacy 'follows the body' (every shipped page);
7 (d) set_instance_motion(center=...) -> pinned, and it STAYS pinned when the body is moved by
8 set_instance_transform (the old float comparison re-tracked it whenever the body passed
10Physics gated: a sphere spinning about a point a distance R_p from its centre translates at
11U = |w x R_p| at that instant, so its net reaction force must match the drag of the same sphere
12towed at U through the same box (Stokes, advection off); a sphere spinning about its OWN centre
13has no net force beyond the discretisation asymmetry of an off-lattice sphere (< 1% of the pivot force).
15Usage: PYTHONPATH=<flow build>:<core geom build> python centre_of_rotation_gate.py
18from peclet
import flow
as sdflow
19from peclet
import geom
30 s = sdflow.Solver(N, N, N)
31 s.set_rho(1.0); s.set_mu(MU); s.set_dt(DT); s.set_advection(
False)
32 s.set_velocity_solver_params(100); s.set_pressure_solver_params(25)
33 if hasattr(s,
"set_velocity_residual_tolerance"):
34 s.set_velocity_residual_tolerance(0.0)
35 s.set_pressure_multigrid(
True, levels=4)
36 s.set_scene(np.asarray(ni, np.int32), np.asarray(nr, float), np.asarray(ii).ravel(),
37 np.asarray(ir).ravel(), periodic=
True)
42 b = geom.SceneBuilder()
43 sph = b.add_leaf(
"sphere", [R])
44 b.add_instance(sph, translation=list(CEN), **kw)
49 b = geom.SceneBuilder()
50 sph = b.add_leaf(
"sphere", [R])
51 ni, nr, _, _ = b.encode()
52 ii = np.zeros((1, KI_I), dtype=np.int32); ir = np.zeros((1, KI_R))
53 ii[0] = (sph, -1); ir[0, 0:3] = CEN; ir[0, 6] = 1.0; ir[0, 7] = 1.0
58 s.set_solid_from_scene(
True)
59 for _
in range(steps):
61 return np.asarray(s.hydro_force_torque_reaction())[0][0].astype(float)
68 print(
" %-70s %s" % (msg,
"ok" if cond
else "FAIL"))
72c = np.asarray(s.instance_center(0))
73check(np.allclose(c, CEN)
and not s.instance_center_pinned(0),
"(a) builder NaN default -> centre = translation, not pinned")
76c = np.asarray(s.instance_center(0))
77check(np.allclose(c, 0.0)
and s.instance_center_pinned(0),
"(b) builder center=(0,0,0) -> pinned at the world ORIGIN")
80s.set_instance_motion(0, ang_vel=[0, 0, OMEGA])
81c = np.asarray(s.instance_center(0))
82check(np.allclose(c, CEN)
and not s.instance_center_pinned(0),
"(c) raw array zeros -> legacy: follows the body")
85s.set_instance_motion(0, ang_vel=[0, 0, OMEGA], center=[x0, x0, x0])
86s.set_instance_transform(0, [x0 + 5.0, x0, x0])
87c = np.asarray(s.instance_center(0))
88check(np.allclose(c, CEN)
and s.instance_center_pinned(0),
"(d) setter pin stays put when the body moves off it")
92s =
solver(*
raw_scene()); s.set_instance_motion(0, ang_vel=[0, 0, OMEGA])
94s =
solver(*
raw_scene()); s.set_instance_motion(0, ang_vel=[0, 0, OMEGA], center=[x0 - RP, x0, x0])
103rel = np.linalg.norm(F0) / np.linalg.norm(Fp)
104print(
" |F| spin about own centre / |F| about the pivot = %.2e (discretisation asymmetry of an off-lattice sphere)" % rel)
105print(
" F_y spin about pivot = %.5e F_y towed at U = %.5e ratio = %.4f" % (Fp[1], Ft[1], Fp[1] / Ft[1]))
106check(rel < 1e-2,
"spin about own centre: net force < 1% of the pivot force")
107check(abs(Fp[1] / Ft[1] - 1.0) < 0.05
and abs(Fp[0]) < 0.05 * abs(Fp[1]),
"spin about a pivot: net force = towing drag at |w x R_p| (5%)")
108print(
"CENTRE-OF-ROTATION GATE %s" % (
"PASS" if ok
else "FAIL"))