Sign in with STEVE

littletechbird/dirac-belt

Dirac belt (belt trick): a cube tied to anchor pads by ribbons, posed at any moment of a 720 degree untangle.

v1.0.0 Latest STEVE MIT 4
Isometric
part.py 211 lines · Python · STEVE runtime · checked
Download
1"""Dirac belt (the belt trick, or plate trick): a cube tied to fixed anchor pads by elastic ribbons, posed at any
2moment of a full 720 degree untangle, with the cube's ribbons never passing through each other or the cube.
3
4Every point p in space is turned rigidly about the centre by a rotation that depends only on its distance |p|
5from the centre: points inside the ball of radius R0 around the cube turn with the cube, points outside R1 stay
6still, and each sphere in between turns as one piece. That map is smooth, one-to-one and volume-preserving at
7every moment, so ribbons that start apart stay apart and keep their volume. The motion follows Andrew Hanson's
8360 and 720 degree quaternion paths (Visualizing Quaternions, 2006), then a six-ribbon finale.
9
10Ribbons are B-spline solids fitted through the exact mapped points (top and bottom surfaces, ruled edges and
11ends). The cube sits at the origin; ribbon +z points up. Its +x face is marked red so its turn is visible.
12"""
13import math
14
15import cadquery as cq
16from steve import Model
17
18time = 24.0 # s Moment in the motion
19ribbons = "as in the video" # Ribbons
20cube_size = 40 # mm Cube size
21ribbon_width = 18 # mm Ribbon width
22ribbon_thickness = 0.8 # mm Ribbon thickness
23ribbon_length = 108 # mm Ribbon length
24
25# ---- geometry, in the cube's half-size `a` (the video's units: a = 0.5) ----
26a = cube_size / 2
27W = ribbon_width
28TH = ribbon_thickness
29L = a + ribbon_length # anchor face distance from the centre
30R0 = 1.76 * a # rigid ball: turns with the cube (cube corner is 1.732 a)
31R1 = L - 0.3 * a # outside this nothing moves
32DELTA = 0.15 # ramp width of the log-radius profile
33PAD_HALF = 0.9 * a
34PAD_T = 0.2 * a
35
36# Proportions the fitted ribbon surfaces are checked for (the video: width 0.45, thickness 0.02, length 2.7 x cube)
37if not W <= 0.5 * cube_size:
38 raise ValueError("ribbon_width must be at most 0.5 x cube_size.")
39if not TH <= 0.02 * cube_size:
40 raise ValueError("ribbon_thickness must be at most 0.02 x cube_size.")
41if not TH <= W / 4:
42 raise ValueError("ribbon_thickness must be at most a quarter of ribbon_width.")
43if not ribbon_length >= 2.5 * cube_size:
44 raise ValueError("ribbon_length must be at least 2.5 x cube_size.")
45
46# Timeline in seconds, as in the 37.5 s video: rest, 360 degree turn (A1), Hanson 360 loop, second turn (A2),
47# Hanson 720 untangle, four side ribbons grow, six-ribbon finale loop.
48PHASES = [("rest", 0.0, 3.0), ("A1", 3.0, 9.0), ("H360", 9.0, 15.0), ("A2", 15.0, 21.0),
49 ("H720", 21.0, 28.0), ("grow", 28.0, 29.5), ("coda", 29.5, 37.5)]
50
51# (name, axis e, width direction u); the ribbon's front (gold) face looks along e x u
52RIBBONS = [("pz", (0, 0, 1), (0, -1, 0)), ("nz", (0, 0, -1), (0, 1, 0)),
53 ("px", (1, 0, 0), (0, 1, 0)), ("nx", (-1, 0, 0), (0, -1, 0)),
54 ("py", (0, 1, 0), (-1, 0, 0)), ("ny", (0, -1, 0), (1, 0, 0))]
55SHOWN = {"as in the video": None, "1": 1, "2": 2, "6": 6}[ribbons]
56
57
58def ease(x):
59 x = min(max(x, 0.0), 1.0)
60 return x * x * x * (10 - 15 * x + 6 * x * x)
61
62
63def _E(x):
64 return 2.5 * x ** 4 - 3 * x ** 5 + x ** 6 # integral of smootherstep
65
66
67def h(r):
68 """distance from the centre -> path parameter s: 1 inside R0, 0 outside R1, smooth (C3) and monotone"""
69 rho = min(max(math.log(max(r, 1e-12) / R0) / math.log(R1 / R0), 0.0), 1.0)
70 d = DELTA
71 if rho < d:
72 w = d * _E(rho / d)
73 elif rho > 1 - d:
74 w = d / 2 + (1 - 2 * d) + d * (0.5 - _E((1 - rho) / d))
75 else:
76 w = d / 2 + (rho - d)
77 return 1.0 - w / (1.0 - d)
78
79
80T = min(max(float(time), 0.0), 37.5)
81PHASE, U = "coda", 1.0
82for _name, _t0, _t1 in PHASES:
83 if T <= _t1 + 1e-12:
84 PHASE, U = _name, ease((T - _t0) / (_t1 - _t0))
85 break
86
87
88def qmul(p, q):
89 aw, ax, ay, az = p
90 bw, bx, by, bz = q
91 return (aw * bw - ax * bx - ay * by - az * bz, aw * bx + ax * bw + ay * bz - az * by,
92 aw * by - ax * bz + ay * bw + az * bx, aw * bz + ax * by - ay * bx + az * bw)
93
94
95def Q(s):
96 """unit quaternion (w, x, y, z) for path parameter s (0 = anchors, 1 = cube) at this moment"""
97 if PHASE in ("rest", "grow"):
98 return (1.0, 0.0, 0.0, 0.0)
99 if PHASE in ("A1", "A2"):
100 tau = 0.5 * U if PHASE == "A1" else 0.5 + 0.5 * U
101 ang = 2 * math.pi * s * tau
102 return (math.cos(ang), 0.0, 0.0, math.sin(ang))
103 if PHASE == "H360":
104 al = 2 * math.pi * U
105 c, sn = math.cos(math.pi * s), math.sin(math.pi * s)
106 return (c, 0.0, sn * math.sin(al), sn * math.cos(al))
107 if PHASE == "H720":
108 b = 0.5 * math.pi * U
109 c2 = math.cos(2 * math.pi * s)
110 return (math.sin(b) ** 2 + math.cos(b) ** 2 * c2, 0.0,
111 math.cos(b) * math.sin(b) * (1 - c2), math.cos(b) * math.sin(2 * math.pi * s))
112 hs = 0.5 * math.pi * s # coda: loop of the six-ribbon finale
113 al = 2 * math.pi * U
114 q1 = (math.cos(hs), math.sin(hs) * math.cos(al), math.sin(hs) * math.sin(al), 0.0)
115 q0c = (math.cos(hs), -math.sin(hs), 0.0, 0.0)
116 return qmul(q1, q0c)
117
118
119def rotate(q, p):
120 w, vx, vy, vz = q
121 px, py, pz = p
122 tx, ty, tz = 2 * (vy * pz - vz * py), 2 * (vz * px - vx * pz), 2 * (vx * py - vy * px)
123 return (px + w * tx + (vy * tz - vz * ty), py + w * ty + (vz * tx - vx * tz), pz + w * tz + (vx * ty - vy * tx))
124
125
126def phi(p):
127 return rotate(Q(h(math.sqrt(p[0] ** 2 + p[1] ** 2 + p[2] ** 2))), p)
128
129
130def grown(index):
131 """(pad scale, ribbon length fraction) of ribbon `index` at this moment"""
132 if SHOWN is not None:
133 return (1.0, 1.0) if index < SHOWN else (0.0, 0.0)
134 if index < 2:
135 return 1.0, 1.0
136 if T < 28.0:
137 return 0.0, 0.0
138 g = (T - 28.0) / 1.5
139 return ease(g / 0.5), ease((g - 0.3) / 0.7)
140
141
142def box_at(centre, e, u, size_e, size_u, size_n):
143 """box of the given sizes along e, u and n = e x u, centred on `centre`"""
144 n = (e[1] * u[2] - e[2] * u[1], e[2] * u[0] - e[0] * u[2], e[0] * u[1] - e[1] * u[0])
145 o = [centre[i] - (e[i] * size_e + u[i] * size_u + n[i] * size_n) / 2 for i in range(3)]
146 return cq.Solid.makeBox(size_e, size_u, size_n).moved(cq.Location(cq.Plane(o, e, n)))
147
148
149def ribbon_solid(e, u, frac, nl=240, nw=12):
150 n = (e[1] * u[2] - e[2] * u[1], e[2] * u[0] - e[0] * u[2], e[0] * u[1] - e[1] * u[0])
151 tip = a + frac * (L - a)
152
153 def pt(l, w, k):
154 return tuple(l * e[i] + w * u[i] + k * n[i] for i in range(3))
155
156 if frac < 1.0: # growing: only in the grow phase, where nothing has turned yet, so it is a plain slab
157 return box_at(pt((a + tip) / 2, 0, 0), e, u, tip - a, W, TH)
158 # length samples are denser in the moving shell R0..R1 (where ribbons stretch) than in the still parts
159 ls = []
160 for i in range(nl + 1):
161 x = i / nl
162 ls.append(a + (L - a) * (x - 0.10 * math.sin(2 * math.pi * x) / (2 * math.pi)))
163 ws = [-W / 2 + W * j / nw for j in range(nw + 1)]
164 layers = []
165 for k in (TH / 2, -TH / 2):
166 layers.append(cq.Face.makeSplineApprox(
167 [[cq.Vector(*phi(pt(l, w, k))) for w in ws] for l in ls], tol=5e-5 * a, minDeg=3, maxDeg=5))
168 top, bot = layers
169 faces = [top, bot]
170 for et in top.Edges(): # sides and ends: ruled between matching top and bottom edges
171 mt = et.positionAt(0.5)
172 eb = min(bot.Edges(), key=lambda x: (x.positionAt(0.5) - mt).Length)
173 if (et.startPoint() - eb.startPoint()).Length > (et.startPoint() - eb.endPoint()).Length:
174 eb = cq.Edge(eb.wrapped.Reversed())
175 faces.append(cq.Face.makeRuledSurface(et, eb))
176 return cq.Solid.makeSolid(cq.Shell.makeShell(faces)).fix()
177
178
179# cube: turns rigidly with Q(1); edges rounded; +x face marked
180qc = Q(1.0)
181mark = box_at((a - 0.02 * a, 0, 0), (1, 0, 0), (0, 1, 0), 0.04 * a, 1.6 * a, 1.6 * a) # inlaid, flush
182cube = cq.Workplane("XY").box(2 * a, 2 * a, 2 * a).edges().fillet(0.07 * a).val().cut(mark)
183ang = 2 * math.degrees(math.acos(min(1.0, abs(qc[0]))))
184if ang > 1e-9:
185 axis = (qc[1], qc[2], qc[3]) if qc[0] >= 0 else (-qc[1], -qc[2], -qc[3])
186 cube = cube.rotate(cq.Vector(0, 0, 0), cq.Vector(*axis), ang)
187 mark = mark.rotate(cq.Vector(0, 0, 0), cq.Vector(*axis), ang)
188
189model = Model()
190model.add("cube", cube, op="new", name="Cube")
191model.add("mark", mark, op="new", name="Marked face")
192pads, ribs = [], []
193for index, (key, e, u) in enumerate(RIBBONS):
194 gp, gr = grown(index)
195 if gp > 1e-3:
196 c = tuple((L + PAD_T / 2) * x for x in e)
197 model.add(f"pad_{key}", box_at(c, e, u, gp * PAD_T, gp * 2 * PAD_HALF, gp * 2 * PAD_HALF), op="new",
198 name=f"Anchor pad {key}")
199 pads.append(f"pad_{key}")
200 if gr > 1e-3:
201 model.add(f"ribbon_{key}", ribbon_solid(e, u, gr), op="new", name=f"Ribbon {key}")
202 ribs.append(f"ribbon_{key}")
203
204model.material("aluminum-6061-t6", bodies=["cube"], finish="brushed", color="#cfd1d4")
205model.material("pa6", bodies=["mark"], color="#d91f0a") # red inlay on the +x face
206if pads:
207 model.material("aluminum-6061-t6", bodies=pads, finish="anodized", color="#2a2c31")
208if ribs:
209 model.material("tpu-60a", bodies=ribs, finish="satin", color="#cc7a1a") # elastic: ribbons stretch
210result = model.result()
211

Dirac's belt trick as a solid model. A cube is tied by two (or six) elastic ribbons to anchor pads that never move. Turning the cube 360 degrees leaves the ribbons twisted in a way no sliding can undo; after 720 degrees they can be untangled without turning the cube or the pads. It is the topology behind spin-1/2 particles, which need two full turns to come back to themselves. `time` picks the moment in a 37.5 s sequence: rest, a 360 degree turn, Hanson's 360 loop, a second turn, the 720 untangle, four more ribbons grow out, and a six-ribbon finale in which the cube turns another 720 degrees. Each sphere around the cube turns as one rigid piece by an angle set only by its distance from the centre, so the ribbons never pass through each other or the cube at any moment, and each keeps its exact volume. Ribbons are B-spline solids fitted through the exact mapped points. Cube at the origin, ribbon +z up, the cube's +x face marked red. Units mm.

Dirac belt (belt trick)

A cube tied to six fixed anchor pads by elastic ribbons (two in the first half), posed at any moment of a full 720 degree untangle. Turn the cube once (360 degrees) and the ribbons end up twisted in a way that no moving of the ribbons can undo while the cube and pads stay put. Turn it twice (720 degrees) and they can be untangled completely. This is the topology behind spin-1/2 particles: the rotation group SO(3) is double-covered by the unit quaternions SU(2), and only a 720 degree turn is a loop that shrinks to nothing.

How the shape is made

Every point p is turned rigidly about the centre by the unit quaternion Q(h(|p|), t):

  • h(r) is 1 inside the ball of radius R0 = 1.76 a around the cube (a = half the cube size; the cube's corner is at 1.732 a), 0 outside R1 = anchor distance - 0.3 a, and smooth (C3) and monotone in between. Its slope is constant in log r over the middle 70 %, which keeps the ribbons' stretch as low as this construction allows.
  • Q(s, t) follows the timeline below. The two untangling loops are Andrew Hanson's 360 and 720 degree quaternion paths (A. J. Hanson, Visualizing Quaternions, Morgan Kaufmann 2006).

Because each sphere |p| = r turns as one rigid piece, the map is smooth, one-to-one and volume-preserving at every moment. Ribbons that are apart at rest therefore never touch each other, themselves, the cube or the pads, and each ribbon keeps exactly its rest volume. The cube (inside R0) turns rigidly with Q(1, t), and the pads (outside R1) never move.

Each ribbon is a closed solid: its top and bottom surfaces are B-splines fitted (to 0.001 mm at the default size) through the exact mapped points, 241 along the length by 13 across, and its sides and ends are ruled between them.

Timeline (time, seconds; the same as the video, frame = time x 60)

Time What happens
0-3 Rest
3-9 The cube turns 360 degrees about z; the ribbons twist
9-15 Hanson's 360 loop: the twist moves around but cannot be removed
15-21 The cube turns a second 360 degrees (720 in total)
21-28 Hanson's 720 untangle: back to rest, with the cube never turning back
28-29.5 Four more ribbons and pads grow out (nothing turns)
29.5-37.5 Six-ribbon finale: with all six ribbons attached the cube turns another 720 degrees, and the ribbons come back untangled

Every phase starts and ends at rest or at a whole-turn pose, so the last moment matches the first exactly.

Parameters

Parameter Meaning
time Moment in the motion, 0 to 37.5 s
ribbons as in the video (2, then 6 from 28 s), or a fixed 1, 2 or 6. The map moves all of space, so any count is valid at any moment
cube_size Edge of the cube, mm
ribbon_width mm, at most 0.5 x cube size
ribbon_thickness mm, at most 0.02 x cube size (as in the video) and a quarter of the width
ribbon_length From cube face to anchor pad, mm, at least 2.5 x cube size

The defaults reproduce the video's proportions exactly (cube 40 mm, so one video unit is 80 mm).

Layout and bodies

The cube is centred on the origin, ribbon pz points up (+z), and the cube's +x face carries a red inlay so its turn is visible. Bodies: cube, mark (the inlay, flush with the face), pad_<axis> and ribbon_<axis> for each ribbon present (pz, nz, px, nx, py, ny). Each ribbon end is glued to its cube face and to its pad face (they touch; they don't overlap).

Notes

  • The ribbons stretch by up to about 9 times along their length in the 720 untangle, like elastic bands, which is why they are TPU 60A. Real belts bend rather than stretch; the topology is the same.
  • Checked with CadQuery 2.8 / OCCT at the defaults every 0.5 s of the timeline (76 moments), plus six ribbons at 8 moments through the turns and the untangle, plus the smallest allowed proportions (cube 20, width 10, thickness 0.4, length 50 mm) with six ribbons at 5 moments: every body a valid closed solid, no overlap between any two bodies (beyond the glued contacts), no ribbon self-intersection, ribbon volume equal to the rest volume within 0.03 %, and fitted surfaces within 0.03 mm of the exact mapped points. The closest two ribbons come at the defaults is 4.2 mm (six ribbons, 22.5 s).
  • STEVE's quick mass and volume figures for the ribbons can read about 3 % high: its default volume integration is coarse on these thin B-spline solids. The exact ribbon volume is width x thickness x length at every moment.
  1. 1.0.0 Latest
More #physics
STEVEMIT

littletechbird/breaker-test-pins

Primary injection test pins (tulips) for molded case circuit breakers: hex lug end, tulip neck, all rounded-square body, seven sizes.

1 7 variants 0 1 day ago v1.1.0