156 lines
5.4 KiB
Python
156 lines
5.4 KiB
Python
"""
|
|
tension_analysis.py — Motor spool rotation and tension estimation
|
|
for a Gordix-style 8-belt suspended CNC router.
|
|
|
|
Given belt lengths from kinematics.py, estimates:
|
|
- Motor spool rotation (radians) for each belt
|
|
- Tension multiplier per belt relative to the average
|
|
|
|
Assumptions:
|
|
- All belts have identical linear stiffness (EA constant).
|
|
- Tension = EA * strain, where strain ≈ (L - L_rest) / L_rest.
|
|
- All belts share the same resting length (nominal length at center position).
|
|
- Spool radius is configurable (default 0.015 m).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from collections import namedtuple
|
|
|
|
import numpy as np
|
|
|
|
from kinematics import belt_lengths, BELT_NAMES
|
|
|
|
TensionResult = namedtuple(
|
|
"TensionResult",
|
|
["belt_names", "lengths", "spool_rotations", "tension_multipliers"],
|
|
)
|
|
|
|
|
|
def resting_lengths(center_x: float = 0.0,
|
|
center_y: float = 0.0,
|
|
center_z: float = 0.0) -> dict[str, float]:
|
|
"""Compute the nominal (resting) belt lengths at a given position.
|
|
|
|
This is the length each belt would have when the sled is at the
|
|
home/center position. All tensions are referenced to these lengths.
|
|
"""
|
|
return belt_lengths(center_x, center_y, center_z)
|
|
|
|
|
|
def analyze_tension(x: float,
|
|
y: float,
|
|
z: float,
|
|
spool_radius: float = 0.015,
|
|
rest_lengths: dict[str, float] | None = None,
|
|
) -> TensionResult:
|
|
"""Analyze tension and spool rotation at a given end-effector position.
|
|
|
|
Args:
|
|
x, y, z: End-effector position in meters.
|
|
spool_radius: Motor spool radius (default 0.015 m).
|
|
rest_lengths: Resting belt lengths (from resting_lengths()). If None,
|
|
computed at (0, 0, 0).
|
|
|
|
Returns:
|
|
TensionResult with fields:
|
|
- belt_names: list of 8 belt name strings
|
|
- lengths: np.array of current belt lengths
|
|
- spool_rotations: np.array of spool rotations in radians
|
|
- tension_multipliers: np.array of relative tension (1.0 = average)
|
|
"""
|
|
if rest_lengths is None:
|
|
rest_lengths = resting_lengths(0.0, 0.0, 0.0)
|
|
|
|
current_lengths = belt_lengths(x, y, z)
|
|
|
|
names = list(BELT_NAMES)
|
|
L_curr = np.array([current_lengths[n] for n in names])
|
|
L_rest = np.array([rest_lengths[n] for n in names])
|
|
|
|
# Spool rotation: how much belt must be paid out/taken up
|
|
delta = L_curr - L_rest
|
|
spool_rot = delta / spool_radius
|
|
|
|
# Tension estimate: T = EA * (L - L_rest) / L_rest
|
|
# We only care about relative tension, so EA cancels.
|
|
strain = delta / L_rest
|
|
# Avoid division by zero — clamp minimum strain for multiplier calc
|
|
min_strain = np.min(strain)
|
|
if min_strain < 0:
|
|
# Some belts could be under zero strain (shorter than rest)
|
|
# We report as-is; negative = slack
|
|
pass
|
|
|
|
# Tension multiplier = strain / mean(|strain|)
|
|
mean_abs_strain = np.mean(np.abs(strain))
|
|
if mean_abs_strain < 1e-12:
|
|
tension_mult = np.ones_like(strain)
|
|
else:
|
|
tension_mult = strain / mean_abs_strain
|
|
|
|
return TensionResult(
|
|
belt_names=list(names),
|
|
lengths=L_curr,
|
|
spool_rotations=spool_rot,
|
|
tension_multipliers=tension_mult,
|
|
)
|
|
|
|
|
|
def print_analysis(x: float, y: float, z: float,
|
|
spool_radius: float = 0.015) -> None:
|
|
"""Pretty-print tension analysis for a single position."""
|
|
result = analyze_tension(x, y, z, spool_radius)
|
|
|
|
print(f"\n Tension Analysis @ ({x:.3f}, {y:.3f}, {z:.3f}) m")
|
|
print(f" Spool radius: {spool_radius:.3f} m")
|
|
print(f" {'Belt':<12} {'Length (m)':<12} {'ΔL (mm)':<12} "
|
|
f"{'Spool (rad)':<12} {'Tension mult':<12}")
|
|
print(" " + "-" * 60)
|
|
|
|
L_rest = resting_lengths(0.0, 0.0, 0.0)
|
|
|
|
for i, name in enumerate(result.belt_names):
|
|
delta_mm = (result.lengths[i] - L_rest[name]) * 1000.0
|
|
print(f" {name:<12} {result.lengths[i]:<12.6f} {delta_mm:<12.4f} "
|
|
f"{result.spool_rotations[i]:<12.3f} {result.tension_multipliers[i]:<12.4f}")
|
|
|
|
print(f"\n Max spool rotation: {np.max(np.abs(result.spool_rotations)):.3f} rad")
|
|
print(f" Max tension multiplier: {np.max(result.tension_multipliers):.4f}")
|
|
print(f" Min tension multiplier: {np.min(result.tension_multipliers):.4f}")
|
|
print()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("=" * 70)
|
|
print(" Tension Analysis — Gordix 8-Belt Kinematics")
|
|
print("=" * 70)
|
|
|
|
# Analyze the corner positions (worst-case normally)
|
|
test_positions = [
|
|
("Center", 0.0, 0.0, 0.0),
|
|
("Top-Left", -0.6, 1.2, 0.0),
|
|
("Top-Right", 0.6, 1.2, 0.0),
|
|
("Bottom-Left",-0.6, -1.2, 0.0),
|
|
("Bottom-Right",0.6, -1.2, 0.0),
|
|
]
|
|
|
|
for label, x, y, z in test_positions:
|
|
print(f"\n--- {label} ---")
|
|
print_analysis(x, y, z)
|
|
|
|
# Summary across the 5 positions
|
|
print("=" * 70)
|
|
print(" Summary: Spool & Tension Range")
|
|
print("=" * 70)
|
|
print(f" {'Position':<16} {'Max |spool| (rad)':<20} {'Max tension mult':<18} "
|
|
f"{'Min tension mult':<18}")
|
|
print(" " + "-" * 72)
|
|
for label, x, y, z in test_positions:
|
|
r = analyze_tension(x, y, z)
|
|
print(f" {label:<16} {np.max(np.abs(r.spool_rotations)):<20.3f} "
|
|
f"{np.max(r.tension_multipliers):<18.4f} "
|
|
f"{np.min(r.tension_multipliers):<18.4f}")
|
|
print()
|