research(R4): L38 wall "ripple" is a coil-ownership step, not a wave
Offline replay of the L37 and L38 tube fields on the FI Style 2 helix shows no 7.8 m ripple from the tube distance itself (< 0.3 cm). The real signal is a step: the upper coil's 1 m fallback floor strip, composed independently, puts a support root 0.44 m from the lower bore's centreline at head height; the 7.8 m "period" is a sinusoid fitted to that step. Proposes pairing the floor cap with its bore. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
945225d942
commit
cf57c87daf
5 files changed
+776
No files matched your search
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit a route-only model of fallback support intersecting a neighboring bore."""
|
||||
import json
|
||||
import math
|
||||
|
||||
from r4_tunnel_wall_ripple import (BORE, LIFT, SPACING, WINDOW, Helix,
|
||||
detrend_profile, ripple_amplitude)
|
||||
|
||||
|
||||
def load_route():
|
||||
with open('Saved/Linux/l38/owner-fi-style2-layout.json', encoding='utf-8') as source:
|
||||
passage = json.load(source)['passages'][0]
|
||||
points = [tuple(value/1024.0 for value in point['position_q'])
|
||||
for point in passage['control_points']]
|
||||
return Helix(points)
|
||||
|
||||
|
||||
def rounded_box(side, vertical):
|
||||
outside = math.hypot(max(side, 0.0), max(vertical, 0.0))
|
||||
inside = min(max(side, vertical), 0.0)
|
||||
return outside+inside
|
||||
|
||||
|
||||
def fallback_field(route, x, y, z, mode):
|
||||
"""Use tube-only native density and exact route geometry, omitting FI density."""
|
||||
bore_d, bore_i, bore_t = route.tube(x, y, z, True)
|
||||
bore_center = route.evaluate(bore_i, bore_t)
|
||||
bore_floor = bore_center[2]-BORE+LIFT
|
||||
_, _, floor_i = route.l38_field(x, y, z, 'owner')
|
||||
if floor_i < 0:
|
||||
return -bore_d, bore_i, floor_i
|
||||
floor_t = route.project(floor_i, x, y, z, True, True)
|
||||
floor_curve = route.evaluate(floor_i, floor_t, True)
|
||||
floor_q = floor_curve[2]+LIFT
|
||||
horizontal = math.hypot(x-floor_curve[0], y-floor_curve[1])
|
||||
side = horizontal-0.55
|
||||
top = z-floor_q
|
||||
fallback_depth = 1.0
|
||||
cap_floor = floor_q
|
||||
if mode == 'paired':
|
||||
cap_depth = 0.375
|
||||
if (horizontal > 0.55 or top < -cap_depth or top > 0.125):
|
||||
cap_floor = bore_floor
|
||||
d = max(bore_d, cap_floor-z)
|
||||
native = -bore_d
|
||||
composed = max(native, -d)
|
||||
support = rounded_box(side, max(top, floor_q-fallback_depth-z))
|
||||
if mode in ('shared-owner', 'paired'):
|
||||
support = max(support, -d)
|
||||
if native >= 0.0 and support < 0.001:
|
||||
composed = min(composed, support)
|
||||
return composed, bore_i, floor_i
|
||||
|
||||
|
||||
def audit_failure_window(route, mode):
|
||||
start = (route.total-WINDOW)*0.31
|
||||
radii = []
|
||||
for sample in range(round(WINDOW/SPACING)+1):
|
||||
distance = start+sample*SPACING
|
||||
pose = route.route_pose(distance)
|
||||
before = route.route_pose(max(0.0, distance-0.25))
|
||||
after = route.route_pose(min(route.total, distance+0.25))
|
||||
tx, ty = after[0]-before[0], after[1]-before[1]
|
||||
length = math.hypot(tx, ty)
|
||||
nx, ny = -ty/length, tx/length
|
||||
roots = []
|
||||
for sx, sy in ((nx, ny), (-nx, -ny)):
|
||||
prev_r = 0.0
|
||||
prev_d = fallback_field(route, pose[0], pose[1], pose[2], mode)[0]
|
||||
best_r, best_error = float('nan'), float('inf')
|
||||
radius = SPACING
|
||||
while radius <= max(8.0, pose[3]+4.0)+1.0e-6:
|
||||
value = fallback_field(route, pose[0]+sx*radius,
|
||||
pose[1]+sy*radius, pose[2], mode)[0]
|
||||
if (prev_d < 0.0) != (value < 0.0):
|
||||
denominator = prev_d-value
|
||||
crossing = (prev_r+(radius-prev_r)*prev_d/denominator
|
||||
if denominator > 1.0e-12 else 0.5*(prev_r+radius))
|
||||
error = abs(crossing-pose[3])
|
||||
if error < best_error:
|
||||
best_r, best_error = crossing, error
|
||||
prev_d, prev_r = value, radius
|
||||
radius += SPACING
|
||||
roots.append(best_r)
|
||||
if all(math.isfinite(root) for root in roots):
|
||||
radii.append((distance, 0.5*(roots[0]+roots[1])))
|
||||
if len(radii) < 192:
|
||||
return len(radii), float('nan'), float('nan'), float('nan'), float('nan')
|
||||
detrended = detrend_profile([radius for _, radius in radii], start)
|
||||
amp, period = ripple_amplitude(detrended)
|
||||
return len(radii), amp, period, min(r for _, r in radii), max(r for _, r in radii)
|
||||
|
||||
|
||||
def main():
|
||||
actual = load_route()
|
||||
print(f'[saved FI Style 2 failure window] controls={actual.count+1} '
|
||||
f'length={actual.total:.3f}m window={(actual.total-WINDOW)*0.31:.3f}m')
|
||||
variants = [
|
||||
('independent 1 m fallback', 'owner'),
|
||||
('owner fallback + shared domain only', 'shared-owner'),
|
||||
('0.375 m floor band + paired bore domain', 'paired'),
|
||||
]
|
||||
for description, mode in variants:
|
||||
samples, amplitude, period, low, high = audit_failure_window(actual, mode)
|
||||
print(f' {description}: local={amplitude*100:.3f}cm period={period:.4f}m '
|
||||
f'radius_range={low:.4f}..{high:.4f}m valid={samples}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Probe the L38 rounded fallback strip with and without a bore-domain clip."""
|
||||
import json
|
||||
import math
|
||||
|
||||
from r4_tunnel_wall_ripple import Helix, LIFT, SPACING
|
||||
|
||||
|
||||
def load_route():
|
||||
with open('Saved/Linux/l38/owner-fi-style2-layout.json', encoding='utf-8') as source:
|
||||
passage = json.load(source)['passages'][0]
|
||||
points = [tuple(value/1024.0 for value in point['position_q'])
|
||||
for point in passage['control_points']]
|
||||
return Helix(points)
|
||||
|
||||
|
||||
def fallback_density(route, x, y, z, mode):
|
||||
carve_density, bore_index, floor_index = route.l38_field(x, y, z, 'supportband')
|
||||
if floor_index < 0:
|
||||
return 1.0, bore_index, floor_index, 0.0, 0.0
|
||||
t = route.project(floor_index, x, y, z, True, True)
|
||||
floor_curve = route.evaluate(floor_index, t, True)
|
||||
floor_q = floor_curve[2]+LIFT
|
||||
horizontal = math.hypot(x-floor_curve[0], y-floor_curve[1])
|
||||
side = horizontal-0.55
|
||||
top = z-floor_q
|
||||
depth = 1.0
|
||||
bore_distance, bore_index, bore_t = route.tube(x, y, z, True)
|
||||
bore_center_z = route.evaluate(bore_index, bore_t)[2]
|
||||
bore_floor = bore_center_z-3.0+LIFT
|
||||
floor_to_bore_center = floor_q-bore_center_z
|
||||
if mode == 'neighbor-limited' and floor_to_bore_center > 0.0:
|
||||
depth = min(depth, max(0.0, floor_to_bore_center-0.01))
|
||||
bottom = floor_q-depth-z
|
||||
vertical = max(top, bottom)
|
||||
outside = math.hypot(max(side, 0.0), max(vertical, 0.0))
|
||||
inside = min(max(side, vertical), 0.0)
|
||||
support = outside+inside
|
||||
carve_distance = -carve_density
|
||||
if mode == 'shared-domain':
|
||||
support = max(support, -carve_distance)
|
||||
elif mode == 'paired':
|
||||
if horizontal <= 0.55 and -0.375 <= top <= 0.125:
|
||||
cap_floor = floor_q
|
||||
else:
|
||||
cap_floor = bore_floor
|
||||
carve_distance = max(bore_distance, cap_floor-z)
|
||||
support = max(support, -carve_distance)
|
||||
# Native air is positive. In L38 the fallback strip is min-composed into it.
|
||||
return min(1.0, support), bore_index, floor_index, support, carve_distance, depth, floor_q
|
||||
|
||||
|
||||
def nearest_wall_root(route, pose, nx, ny, mode):
|
||||
previous_r = 0.0
|
||||
previous = fallback_density(route, pose[0], pose[1], pose[2], mode)[0]
|
||||
crossings = []
|
||||
for step in range(1, 65):
|
||||
radius = step*SPACING
|
||||
x, y = pose[0]+nx*radius, pose[1]+ny*radius
|
||||
value = fallback_density(route, x, y, pose[2], mode)[0]
|
||||
if (previous < 0.0) != (value < 0.0):
|
||||
den = previous-value
|
||||
crossing = (previous_r+(radius-previous_r)*previous/den
|
||||
if den > 1.0e-12 else 0.5*(previous_r+radius))
|
||||
crossings.append(crossing)
|
||||
previous_r, previous = radius, value
|
||||
return min(crossings, key=lambda value: abs(value-3.0)) if crossings else float('nan'), crossings
|
||||
|
||||
|
||||
def main():
|
||||
route = load_route()
|
||||
for distance in (150.744, 150.869):
|
||||
pose = route.route_pose(distance)
|
||||
before = route.route_pose(max(0.0, distance-0.25))
|
||||
after = route.route_pose(min(route.total, distance+0.25))
|
||||
tx, ty = after[0]-before[0], after[1]-before[1]
|
||||
length = math.hypot(tx, ty)
|
||||
nx, ny = -ty/length, tx/length
|
||||
print(f's={distance:.3f}m segment={pose[4]} t={pose[5]:.3f} z={pose[2]:.4f}m')
|
||||
for side, sx, sy in (('left', nx, ny), ('right', -nx, -ny)):
|
||||
for mode in ('current', 'shared-domain', 'paired', 'neighbor-limited'):
|
||||
root, crossings = nearest_wall_root(route, pose, sx, sy, mode)
|
||||
at_center = fallback_density(route, pose[0], pose[1], pose[2], mode)
|
||||
label = mode
|
||||
print(f' {side:5s} {label:19s} nearest3_root={root:.4f}m '
|
||||
f'all_roots={[round(r,4) for r in crossings]} '
|
||||
f'bore={at_center[1]} floor={at_center[2]} '
|
||||
f'support={at_center[3]:.4f} cap_d={at_center[4]:.4f} '
|
||||
f'depth={at_center[5]:.4f} floor_z={at_center[6]:.4f}')
|
||||
floor_index = 134
|
||||
floor_distance = route.arc[floor_index] + 0.5*(route.arc[floor_index+1]-route.arc[floor_index])
|
||||
floor_pose = route.route_pose(floor_distance)
|
||||
intended_floor = floor_pose[2]-floor_pose[3]+LIFT
|
||||
current_floor = fallback_density(route, floor_pose[0], floor_pose[1], intended_floor, 'current')
|
||||
paired_floor = fallback_density(route, floor_pose[0], floor_pose[1], intended_floor, 'paired')
|
||||
paired_below = fallback_density(route, floor_pose[0], floor_pose[1], intended_floor-0.25, 'paired')
|
||||
limited_floor = fallback_density(route, floor_pose[0], floor_pose[1], intended_floor, 'neighbor-limited')
|
||||
print(f' floor owner segment={floor_index} intended_z={intended_floor:.4f}m '
|
||||
f'current_support={current_floor[3]:.4f}m paired_top={paired_floor[3]:.4f}m '
|
||||
f'paired_0.25m_below={paired_below[3]:.4f}m '
|
||||
f'limited_support={limited_floor[3]:.4f}m '
|
||||
f'limited_depth={limited_floor[5]:.4f}m')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,440 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Small synthetic reproduction of the L37/L38 FI helix wall audit."""
|
||||
import math
|
||||
import json
|
||||
import bisect
|
||||
import argparse
|
||||
|
||||
R = 3.0
|
||||
PITCH = 3.31
|
||||
TURNS = 12.95
|
||||
N_PER_TURN = 20
|
||||
BORE = 3.0
|
||||
LIFT = 0.15
|
||||
SPACING = 0.125
|
||||
WINDOW = 32.0
|
||||
PERIODS = [0.5 + 0.0625 * i for i in range(121)]
|
||||
|
||||
|
||||
def catmull(v0, v1, v2, v3, t):
|
||||
t2, t3 = t*t, t*t*t
|
||||
return 0.5*((2*v1)+(-v0+v2)*t+(2*v0-5*v1+4*v2-v3)*t2
|
||||
+(-v0+3*v1-3*v2+v3)*t3)
|
||||
|
||||
|
||||
def catmull_d(v0, v1, v2, v3, t):
|
||||
return 0.5*((-v0+v2)+2*(2*v0-5*v1+4*v2-v3)*t
|
||||
+3*(-v0+3*v1-3*v2+v3)*t*t)
|
||||
|
||||
|
||||
def catmull_dd(v0, v1, v2, v3, t):
|
||||
return (2*v0-5*v1+4*v2-v3)+3*(-v0+3*v1-3*v2+v3)*t
|
||||
|
||||
|
||||
class Helix:
|
||||
def __init__(self, points=None):
|
||||
self.count = math.ceil(TURNS * N_PER_TURN) if points is None else len(points)-1
|
||||
self.end_theta = 2*math.pi*TURNS
|
||||
self.dtheta = self.end_theta/self.count
|
||||
self.k = PITCH/(2*math.pi)
|
||||
self.pts = ([(R*math.cos(i*self.dtheta), R*math.sin(i*self.dtheta),
|
||||
self.k*i*self.dtheta) for i in range(self.count+1)]
|
||||
if points is None else [tuple(p) for p in points])
|
||||
self.sorted_z = sorted((p[2], i) for i, p in enumerate(self.pts))
|
||||
self.segs = []
|
||||
for i in range(self.count):
|
||||
a, b = self.pts[i], self.pts[i+1]
|
||||
dx, dy, dz = b[0]-a[0], b[1]-a[1], b[2]-a[2]
|
||||
length = math.sqrt(dx*dx+dy*dy+dz*dz)
|
||||
smooth = i > 2 and i+12 < self.count and length <= 8.0
|
||||
p0, p3 = self.pts[max(0, i-1)], self.pts[min(self.count, i+2)]
|
||||
self.segs.append((a, b, p0, p3, smooth, length))
|
||||
self.arc = [0.0]
|
||||
for seg in self.segs:
|
||||
self.arc.append(self.arc[-1]+seg[5])
|
||||
self.total = self.arc[-1]
|
||||
|
||||
def route_pose(self, distance):
|
||||
distance = max(0.0, min(self.total, distance))
|
||||
i = min(self.count-1, max(0, int(distance/(self.total/self.count))))
|
||||
while i+1 < self.count and self.arc[i+1] < distance:
|
||||
i += 1
|
||||
while i > 0 and self.arc[i] > distance:
|
||||
i -= 1
|
||||
a, b, p0, p3, smooth, _ = self.segs[i]
|
||||
t = (distance-self.arc[i])/(self.arc[i+1]-self.arc[i])
|
||||
x = a[0]+t*(b[0]-a[0]); y = a[1]+t*(b[1]-a[1])
|
||||
if smooth:
|
||||
x = catmull(p0[0], a[0], b[0], p3[0], t)
|
||||
y = catmull(p0[1], a[1], b[1], p3[1], t)
|
||||
z = a[2]+t*(b[2]-a[2])
|
||||
return (x, y, z, BORE, i, t)
|
||||
|
||||
def candidate_indices(self, x, y, target_z):
|
||||
"""Segments around the height owner and its adjacent overlapping coils."""
|
||||
j = bisect.bisect_left(self.sorted_z, (target_z, -1))
|
||||
near = self.sorted_z[max(0, min(len(self.sorted_z)-1, j))][1]
|
||||
if j > 0 and abs(self.sorted_z[j-1][0]-target_z) < abs(self.sorted_z[j][0]-target_z):
|
||||
near = self.sorted_z[j-1][1]
|
||||
found = set()
|
||||
for center in (near, near-N_PER_TURN, near+N_PER_TURN):
|
||||
for j in range(center-4, center+5):
|
||||
if 0 <= j < self.count:
|
||||
found.add(j)
|
||||
return sorted(found)
|
||||
|
||||
def evaluate(self, index, t, floor=False):
|
||||
a, b, p0, p3, smooth, _ = self.segs[index]
|
||||
z = a[2]+t*(b[2]-a[2])
|
||||
dz = b[2]-a[2]
|
||||
if smooth:
|
||||
x = catmull(p0[0], a[0], b[0], p3[0], t)
|
||||
y = catmull(p0[1], a[1], b[1], p3[1], t)
|
||||
dx = catmull_d(p0[0], a[0], b[0], p3[0], t)
|
||||
dy = catmull_d(p0[1], a[1], b[1], p3[1], t)
|
||||
ddx = catmull_dd(p0[0], a[0], b[0], p3[0], t)
|
||||
ddy = catmull_dd(p0[1], a[1], b[1], p3[1], t)
|
||||
else:
|
||||
x = a[0]+t*(b[0]-a[0]); y = a[1]+t*(b[1]-a[1])
|
||||
dx = b[0]-a[0]; dy = b[1]-a[1]
|
||||
ddx = ddy = 0.0
|
||||
if floor:
|
||||
z -= BORE
|
||||
return (x, y, z, dx, dy, dz, ddx, ddy)
|
||||
|
||||
def project(self, index, x, y, z, l38, floor=False):
|
||||
a, b, _, _, _, _ = self.segs[index]
|
||||
az = a[2] - (BORE-LIFT if floor else 0.0)
|
||||
dx, dy, dz = b[0]-a[0], b[1]-a[1], b[2]-a[2]
|
||||
chord_z = z
|
||||
den = dx*dx+dy*dy+dz*dz
|
||||
t = max(0.0, min(1.0, ((x-a[0])*dx+(y-a[1])*dy+(chord_z-az)*dz)/den))
|
||||
if not l38:
|
||||
return t
|
||||
def dist2(u):
|
||||
c = self.evaluate(index, u, floor)
|
||||
ez = (c[2]+LIFT)-z if floor else c[2]-z
|
||||
ex, ey = c[0]-x, c[1]-y
|
||||
return ex*ex+ey*ey+ez*ez
|
||||
for _ in range(4):
|
||||
c = self.evaluate(index, t, floor)
|
||||
ex, ey = c[0]-x, c[1]-y
|
||||
ez = (c[2]+LIFT)-z if floor else c[2]-z
|
||||
grad = ex*c[3]+ey*c[4]+ez*c[5]
|
||||
curv = c[3]*c[3]+c[4]*c[4]+c[5]*c[5]+ex*c[6]+ey*c[7]
|
||||
if curv <= 1.0e-9:
|
||||
break
|
||||
nt = max(0.0, min(1.0, t-grad/curv))
|
||||
if abs(nt-t) <= 1.0e-10:
|
||||
t = nt
|
||||
break
|
||||
t = nt
|
||||
if dist2(0.0) < dist2(t):
|
||||
t = 0.0
|
||||
if dist2(1.0) < dist2(t):
|
||||
t = 1.0
|
||||
return t
|
||||
|
||||
def tube(self, x, y, z, l38):
|
||||
best = (float('inf'), -1, 0.0)
|
||||
for i in self.candidate_indices(x, y, z):
|
||||
a, b, _, _, _, _ = self.segs[i]
|
||||
t = self.project(i, x, y, z, l38)
|
||||
c = self.evaluate(i, t)
|
||||
radius = BORE
|
||||
d = math.sqrt((x-c[0])**2+(y-c[1])**2+(z-c[2])**2)-radius
|
||||
if d < best[0] or (d == best[0] and i < best[1]):
|
||||
best = (d, i, t)
|
||||
return best
|
||||
|
||||
def l38_field(self, x, y, z, cap_mode='owner'):
|
||||
"""Return sign-inverted L38 carve field, optionally without its floor cap."""
|
||||
bore_d, bore_i, bore_t = self.tube(x, y, z, True)
|
||||
if cap_mode == 'none':
|
||||
return -bore_d, bore_i, bore_i
|
||||
candidates = set(self.candidate_indices(x, y, z))
|
||||
candidates.update(self.candidate_indices(x, y, z+BORE-LIFT))
|
||||
candidates.update(self.candidate_indices(x, y, z+BORE-LIFT-0.98))
|
||||
nearest_floor = (float('inf'), -1, 0.0, 0.0, 0.0, 0.0)
|
||||
nearest_player = (float('inf'), -1, 0.0, 0.0, 0.0, 0.0)
|
||||
nearest_band = None
|
||||
for i in sorted(candidates):
|
||||
t = self.project(i, x, y, z, True, True)
|
||||
c = self.evaluate(i, t, True)
|
||||
fx, fy = c[0], c[1]
|
||||
floor_q = c[2]+LIFT
|
||||
horiz2 = (x-fx)**2+(y-fy)**2
|
||||
floor_score = horiz2+(z-floor_q)**2
|
||||
player_score = horiz2+(z-(floor_q+0.98))**2
|
||||
if (floor_score, i) < (nearest_floor[0], nearest_floor[1]):
|
||||
nearest_floor = (floor_score, i, t, floor_q, horiz2, BORE)
|
||||
if (player_score, i) < (nearest_player[0], nearest_player[1]):
|
||||
nearest_player = (player_score, i, t, floor_q, horiz2, BORE)
|
||||
if abs(floor_q-z) <= 0.125 and horiz2 <= BORE*BORE:
|
||||
if nearest_band is None or (floor_score, i) < (nearest_band[0], nearest_band[1]):
|
||||
nearest_band = (floor_score, i, t, floor_q, horiz2, BORE)
|
||||
bore_c = self.evaluate(bore_i, bore_t)
|
||||
bore_floor = bore_c[2]-BORE+LIFT
|
||||
bore_horiz2 = (x-bore_c[0])**2+(y-bore_c[1])**2
|
||||
near_bore_floor = abs(bore_floor-z) <= 0.125 and bore_horiz2 <= BORE*BORE
|
||||
if cap_mode == 'matched':
|
||||
# Preserve the floor owner where the nearest bore itself is in its floor band.
|
||||
floor_owner = nearest_band if near_bore_floor and nearest_band else nearest_floor
|
||||
cap_floor = floor_owner[3] if near_bore_floor else bore_floor
|
||||
elif cap_mode == 'paired':
|
||||
# Keep the local route-floor owner only inside its narrow ownership band;
|
||||
# outside that band, pair the floor cap with the same bore as the tube.
|
||||
if near_bore_floor and nearest_band:
|
||||
floor_owner = nearest_band
|
||||
cap_floor = nearest_band[3]
|
||||
elif nearest_floor[0] <= 0.65**2:
|
||||
floor_owner = nearest_floor
|
||||
cap_floor = nearest_floor[3]
|
||||
else:
|
||||
floor_owner = nearest_floor
|
||||
cap_floor = bore_floor
|
||||
elif cap_mode == 'band':
|
||||
floor_owner = nearest_band if nearest_band else nearest_floor
|
||||
cap_floor = nearest_band[3] if nearest_band else bore_floor
|
||||
elif cap_mode == 'walkband':
|
||||
floor_owner = nearest_band if nearest_band else nearest_floor
|
||||
if nearest_band and nearest_band[4] <= 0.55**2:
|
||||
cap_floor = nearest_band[3]
|
||||
else:
|
||||
cap_floor = bore_floor
|
||||
elif cap_mode == 'supportband':
|
||||
floor_owner = nearest_floor
|
||||
vertical_offset = z-nearest_floor[3]
|
||||
if -1.0 <= vertical_offset <= 0.125 and nearest_floor[4] <= 0.55**2:
|
||||
cap_floor = nearest_floor[3]
|
||||
else:
|
||||
cap_floor = bore_floor
|
||||
else:
|
||||
if near_bore_floor and nearest_band:
|
||||
floor_owner = nearest_band
|
||||
elif nearest_floor[0] <= 0.65**2:
|
||||
floor_owner = nearest_floor
|
||||
elif nearest_player[0] <= 1.50**2:
|
||||
floor_owner = nearest_player
|
||||
elif bore_d <= 0:
|
||||
floor_owner = (bore_d, bore_i, bore_t, bore_floor, bore_horiz2, BORE)
|
||||
elif nearest_player[1] >= 0:
|
||||
floor_owner = nearest_player
|
||||
else:
|
||||
floor_owner = nearest_floor
|
||||
cap_floor = floor_owner[3]
|
||||
return -max(bore_d, cap_floor-z), bore_i, floor_owner[1]
|
||||
|
||||
|
||||
def ripple_amplitude(samples):
|
||||
best_amp, best_period = 0.0, 0.0
|
||||
for period in PERIODS:
|
||||
omega = 2*math.pi/period
|
||||
cc = ss = cs = yc = ys = 0.0
|
||||
for distance, value in samples:
|
||||
c, s = math.cos(omega*distance), math.sin(omega*distance)
|
||||
cc += c*c; ss += s*s; cs += c*s
|
||||
yc += value*c; ys += value*s
|
||||
det = cc*ss-cs*cs
|
||||
if det <= 1.0e-9:
|
||||
continue
|
||||
co = (yc*ss-ys*cs)/det
|
||||
si = (ys*cc-yc*cs)/det
|
||||
amp = math.hypot(co, si)
|
||||
if amp > best_amp:
|
||||
best_amp, best_period = amp, period
|
||||
return best_amp, best_period
|
||||
|
||||
|
||||
def detrend_profile(values, start):
|
||||
n = len(values)
|
||||
mean_i = 0.5*(n-1)
|
||||
mean = sum(values)/n
|
||||
xx = xy = 0.0
|
||||
for i, value in enumerate(values):
|
||||
x = (i-mean_i)*SPACING
|
||||
xy += x*(value-mean)
|
||||
xx += x*x
|
||||
slope = xy/xx if xx else 0.0
|
||||
return [(start+i*SPACING, value-mean-((i-mean_i)*SPACING)*slope)
|
||||
for i, value in enumerate(values)]
|
||||
|
||||
|
||||
def interpolate_audit_path(h, distance):
|
||||
return h.route_pose(distance)
|
||||
|
||||
|
||||
def wall_crossing(h, pose, nx, ny, field, variant):
|
||||
px, py, pz = pose[:3]
|
||||
prev_r, prev_d = 0.0, field(px, py, pz, variant)[0]
|
||||
best_r, best_err = float('nan'), float('inf')
|
||||
max_r = max(8.0, pose[3]+4.0)
|
||||
r = SPACING
|
||||
while r <= max_r+1.0e-6:
|
||||
d = field(px+nx*r, py+ny*r, pz, variant)[0]
|
||||
if (prev_d < 0.0) != (d < 0.0):
|
||||
den = prev_d-d
|
||||
cross = prev_r+(r-prev_r)*prev_d/den if den > 1.0e-12 else 0.5*(prev_r+r)
|
||||
err = abs(cross-pose[3])
|
||||
if err < best_err:
|
||||
best_r, best_err = cross, err
|
||||
prev_d, prev_r = d, r
|
||||
r += SPACING
|
||||
return best_r
|
||||
|
||||
|
||||
def audit(h, field, variant):
|
||||
starts = [0.03, 0.31, 0.457, 0.61, 0.91]
|
||||
local = []
|
||||
coherent = []
|
||||
owner_switches = 0
|
||||
last_bore = last_floor = None
|
||||
count = 0
|
||||
for fraction in starts:
|
||||
start = (h.total-WINDOW)*fraction
|
||||
radii = []
|
||||
for sample in range(round(WINDOW/SPACING)+1):
|
||||
distance = start+sample*SPACING
|
||||
pose = interpolate_audit_path(h, distance)
|
||||
before = interpolate_audit_path(h, max(0.0, distance-0.25))
|
||||
after = interpolate_audit_path(h, min(h.total, distance+0.25))
|
||||
tx, ty = after[0]-before[0], after[1]-before[1]
|
||||
tl = math.hypot(tx, ty)
|
||||
if tl <= 1.0e-6:
|
||||
continue
|
||||
nx, ny = -ty/tl, tx/tl
|
||||
left = wall_crossing(h, pose, nx, ny, field, variant)
|
||||
right = wall_crossing(h, pose, -nx, -ny, field, variant)
|
||||
if not math.isfinite(left) or not math.isfinite(right):
|
||||
continue
|
||||
radii.append(0.5*(left+right))
|
||||
_, bi, fi = field(pose[0], pose[1], pose[2], variant)
|
||||
if last_bore is not None and bi != last_bore:
|
||||
owner_switches += 1
|
||||
if last_floor is not None and fi != last_floor:
|
||||
owner_switches += 1
|
||||
last_bore, last_floor = bi, fi
|
||||
count += 1
|
||||
if len(radii) < 192:
|
||||
continue
|
||||
vals = detrend_profile(radii, start)
|
||||
coherent.extend(vals)
|
||||
amp, period = ripple_amplitude(vals)
|
||||
local.append((amp, period, start, min(radii), max(radii)))
|
||||
peak = max(local, default=(0, 0, 0, 0, 0))
|
||||
coh_amp, coh_period = ripple_amplitude(coherent)
|
||||
return peak, coh_amp, coh_period, owner_switches, count
|
||||
|
||||
|
||||
def floor_audit(h, field, variant):
|
||||
max_abs = 0.0
|
||||
peak_to_peak = 0.0
|
||||
for fraction in (0.03, 0.31, 0.457, 0.61, 0.91):
|
||||
start = (h.total-WINDOW)*fraction
|
||||
residuals = []
|
||||
for sample in range(round(WINDOW/SPACING)+1):
|
||||
distance = start+sample*SPACING
|
||||
pose = h.route_pose(distance)
|
||||
expected = pose[2]-pose[3]+LIFT
|
||||
previous = None
|
||||
hits = []
|
||||
for vertical in range(7):
|
||||
z = expected-1.0+vertical*0.25
|
||||
density = field(pose[0], pose[1], z, variant)[0]
|
||||
if previous is not None and previous[1] < 0.0 and density >= 0.0:
|
||||
previous_z, previous_d = previous
|
||||
den = density-previous_d
|
||||
crossing = (previous_z+(z-previous_z)*(-previous_d)/den
|
||||
if den > 1.0e-12 else z)
|
||||
hits.append((abs(crossing-expected), crossing-expected))
|
||||
previous = (z, density)
|
||||
if hits:
|
||||
residuals.append(min(hits)[1])
|
||||
if residuals:
|
||||
max_abs = max(max_abs, max(abs(v) for v in residuals))
|
||||
peak_to_peak = max(peak_to_peak, max(residuals)-min(residuals))
|
||||
return max_abs, peak_to_peak
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--actual-only', action='store_true')
|
||||
parser.add_argument('--synthetic-only', action='store_true')
|
||||
parser.add_argument('--paired-only', action='store_true')
|
||||
parser.add_argument('--band-only', action='store_true')
|
||||
parser.add_argument('--walkband-only', action='store_true')
|
||||
args = parser.parse_args()
|
||||
h = Helix()
|
||||
seg = h.segs[40][5]
|
||||
turn_len = math.sqrt((2*math.pi*R)**2+PITCH**2)
|
||||
period = 7.812
|
||||
print(f'controls={h.count+1} segments={h.count} turns={TURNS:.2f} total_arc={h.total:.3f}m')
|
||||
print(f'control_chord={seg:.5f}m; exact_helix_turn_arc={turn_len:.5f}m; '
|
||||
f'{period:.3f}m={period/seg:.3f} control chords={period/turn_len:.3f} turns '
|
||||
f'={period/PITCH:.3f} pitches')
|
||||
|
||||
def l37(x, y, z, _variant):
|
||||
d, i, _t = h.tube(x, y, z, False)
|
||||
return -d, i, i
|
||||
|
||||
def l38_tube(x, y, z, _variant):
|
||||
d, i, _t = h.tube(x, y, z, True)
|
||||
return -d, i, i
|
||||
|
||||
if not args.actual_only and not args.paired_only and not args.band_only and not args.walkband_only:
|
||||
for label, fun, variant in [
|
||||
('L37 smoothed tube', l37, 'none'),
|
||||
('L38 nearest smoothed tube only', l38_tube, 'none'),
|
||||
('L38 tube + independently selected floor owner', h.l38_field, 'owner'),
|
||||
('L38 tube + paired cap with a narrow route-floor band', h.l38_field, 'paired'),
|
||||
]:
|
||||
peak, coh_amp, coh_period, switches, samples = audit(h, fun, variant)
|
||||
print(f'{label}: wall_period={peak[1]:.4f}m local_amp={peak[0]:.5f}m '
|
||||
f'coherent_period={coh_period:.4f}m coherent_amp={coh_amp:.5f}m '
|
||||
f'radius_range={peak[3]:.5f}..{peak[4]:.5f}m '
|
||||
f'owner_switches={switches} samples={samples}')
|
||||
|
||||
if not args.synthetic_only:
|
||||
route_path = 'Saved/Linux/l38/owner-fi-style2-layout.json'
|
||||
with open(route_path, encoding='utf-8') as source:
|
||||
passage = json.load(source)['passages'][0]
|
||||
points = [tuple(v/1024.0 for v in item['position_q'])
|
||||
for item in passage['control_points']]
|
||||
actual = Helix(points)
|
||||
print(f'logged FI Style 2 controls: {len(points)} points, '
|
||||
f'{actual.total:.3f} m chord arc; audited windows at '
|
||||
f'{[(round((actual.total-WINDOW)*f, 3)) for f in (0.03, 0.31, 0.457, 0.61, 0.91)]} m')
|
||||
|
||||
if not args.paired_only and not args.band_only and not args.walkband_only:
|
||||
for label, smooth in [('L37 raw tube', False), ('L38 raw tube', True)]:
|
||||
def actual_field(x, y, z, _variant):
|
||||
distance, index, _t = actual.tube(x, y, z, smooth)
|
||||
return -distance, index, index
|
||||
peak, coh_amp, coh_period, switches, samples = audit(actual, actual_field, 'none')
|
||||
print(f'actual controls {label}: wall_period={peak[1]:.4f}m '
|
||||
f'local_amp={peak[0]*100:.3f}cm coherent_period={coh_period:.4f}m '
|
||||
f'coherent_amp={coh_amp*100:.3f}cm radius_range='
|
||||
f'{peak[3]:.4f}..{peak[4]:.4f}m samples={samples}')
|
||||
cap_variants = ([('L38 cap floor owner restricted to 0.55 m walk band', 'walkband')]
|
||||
if args.walkband_only else
|
||||
[('L38 cap uses a route floor owner only in its narrow floor band', 'band')]
|
||||
if args.band_only else
|
||||
[('L38 cap paired outside route-floor ownership band', 'paired')]
|
||||
if args.paired_only else
|
||||
[('L38 cap with independent floor owner', 'owner'),
|
||||
('L38 cap paired outside route-floor ownership band', 'paired')])
|
||||
for label, mode in cap_variants:
|
||||
peak, coh_amp, coh_period, switches, samples = audit(
|
||||
actual, actual.l38_field, mode)
|
||||
print(f'actual controls {label}: wall_period={peak[1]:.4f}m '
|
||||
f'local_amp={peak[0]*100:.3f}cm coherent_period={coh_period:.4f}m '
|
||||
f'coherent_amp={coh_amp*100:.3f}cm radius_range='
|
||||
f'{peak[3]:.4f}..{peak[4]:.4f}m samples={samples}')
|
||||
if args.walkband_only:
|
||||
floor_max, floor_range = floor_audit(actual, actual.l38_field, 'walkband')
|
||||
print(f'actual controls narrow-band floor: max_abs_error={floor_max*100:.5f}cm '
|
||||
f'window_peak_to_peak={floor_range*100:.5f}cm')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user