flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
centre_of_rotation_gate.py
Go to the documentation of this file.
1"""Centre of rotation: NaN follows the body, any finite point pins -- the world origin included.
2
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
9 through its own pin).
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).
14
15Usage: PYTHONPATH=<flow build>:<core geom build> python centre_of_rotation_gate.py
16"""
17import numpy as np
18from peclet import flow as sdflow
19from peclet import geom
20
21N, R = 64, 6.4
22MU, DT = 1.0, 2.0 # nu dt/h^2 = 2: the 100-sweep RB-GS converges to round-off (at dt=100 it leaves a 1e-4 net force)
23OMEGA = 1e-3
24KI_I, KI_R = 2, 17
25x0 = 0.5 * N + 0.3
26CEN = (x0, x0, x0)
27
28
29def solver(ni, nr, ii, ir):
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)
38 return s
39
40
41def builder_scene(**kw):
42 b = geom.SceneBuilder()
43 sph = b.add_leaf("sphere", [R])
44 b.add_instance(sph, translation=list(CEN), **kw)
45 return b.encode()
46
47
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
54 return ni, nr, ii, ir
55
56
57def net_force(s, steps=300):
58 s.set_solid_from_scene(True)
59 for _ in range(steps):
60 s.step()
61 return np.asarray(s.hydro_force_torque_reaction())[0][0].astype(float)
62
63
64ok = True
65def check(cond, msg):
66 global ok
67 ok &= bool(cond)
68 print(" %-70s %s" % (msg, "ok" if cond else "FAIL"))
69
70# (a) builder default: follows the body
71s = solver(*builder_scene(ang_vel=[0, 0, OMEGA]))
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")
74# (b) builder explicit world origin: pinned at (0,0,0)
75s = solver(*builder_scene(ang_vel=[0, 0, OMEGA], center=[0.0, 0.0, 0.0]))
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")
78# (c) raw zeros: legacy follows the body
79s = solver(*raw_scene())
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")
83# (d) explicit setter pins and survives a transform through the pin
84s = solver(*raw_scene())
85s.set_instance_motion(0, ang_vel=[0, 0, OMEGA], center=[x0, x0, x0]) # pin AT the current centre
86s.set_instance_transform(0, [x0 + 5.0, x0, x0]) # move the body off it
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")
89
90# Physics: spin about the body's centre -> zero net force; spin about a pivot -> towing drag
91RP = 8.0
92s = solver(*raw_scene()); s.set_instance_motion(0, ang_vel=[0, 0, OMEGA])
93F0 = net_force(s)
94s = solver(*raw_scene()); s.set_instance_motion(0, ang_vel=[0, 0, OMEGA], center=[x0 - RP, x0, x0])
95Fp = net_force(s)
96U = OMEGA * RP # instantaneous translation speed, along +y
97s = solver(*raw_scene()); s.set_instance_motion(0, lin_vel=[0, U, 0])
98Ft = net_force(s)
99# A sphere centred OFF the lattice (+0.3, as every page places it) carries a small
100# discretisation-asymmetry force when it spins (9e-3 mu R^2 Om at R/h = 6.4, 1.2e-3 at 9.6, and
101# 5e-13 for a lattice-centred sphere -- rotation_gate.py); it is identical for a tracked and an
102# explicit centre, so the gate asks only that it be negligible against the pivot force.
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"))