Source code for gearbox.mathutils
"""Small geometry and trigonometry helpers shared across the package.
Unless a name says otherwise, **angles passed to or returned by public functions are in degrees**,
which is the convention used by gear standards and by the user-facing models.
"""
from __future__ import annotations
from collections.abc import Iterable, Sequence
from math import atan, cos, degrees, pi, radians, sin, sqrt, tan
Point = tuple[float, float]
[docs]
def involute(angle: float) -> float:
"""Return the involute function ``inv(alpha) = tan(alpha) - alpha``.
Args:
angle: Pressure angle in **degrees**.
Returns:
The involute function value (dimensionless, i.e. an angle in radians).
"""
return tan(radians(angle)) - radians(angle)
[docs]
def inverse_involute(value: float, tolerance: float = 0.0) -> float:
"""Return the angle whose involute function equals ``value``.
Solved by bisection on ``[0, pi/2]``, down to machine precision unless a looser tolerance is
requested.
Args:
value: Involute function value (radians).
tolerance: Optional absolute tolerance on the involute function to stop earlier.
Returns:
The pressure angle in **degrees**.
"""
lower = 0.0
upper = pi / 2
angle = 0.0
for _ in range(100):
angle = (lower + upper) / 2
difference = tan(angle) - angle - value
if abs(difference) <= tolerance:
break
if difference > 0:
upper = angle
else:
lower = angle
return degrees(angle)
[docs]
def sign(number: float) -> int:
"""Return -1, 0 or 1 according to the sign of ``number``."""
if number > 0.0:
return 1
if number < 0.0:
return -1
return 0
[docs]
def cartesian_to_polar(x: float, y: float) -> list[float]:
"""Convert a Cartesian vector to polar coordinates.
Args:
x: Component along the x axis.
y: Component along the y axis.
Returns:
``[radius, angle]`` with the angle in **radians**, measured from the x axis.
The angle of the null vector is arbitrarily 0.
"""
radius = sqrt(x**2 + y**2)
if x > 0.0:
phi = atan(y / x)
elif x < 0.0:
phi = atan(y / x) + pi * sign(y)
elif y > 0.0:
phi = pi / 2
elif y < 0.0:
phi = -pi / 2
else:
phi = 0.0
return [radius, phi]
[docs]
def polar_to_cartesian(radius: float, angle: float) -> list[float]:
"""Convert polar coordinates to a Cartesian vector.
Args:
radius: Radial coordinate.
angle: Angular coordinate in **radians**.
Returns:
``[x, y]``.
"""
return [radius * cos(angle), radius * sin(angle)]
[docs]
def rotate(
coords: Iterable[Sequence[float]], angle: float, center: Sequence[float] = (0, 0)
) -> list[list[float]]:
"""Rotate points counter-clockwise around ``center``.
Args:
coords: Iterable of ``(x, y)`` points.
angle: Rotation angle in **degrees**.
center: Rotation centre.
Returns:
The rotated points as ``[x, y]`` lists.
"""
cos_a = cos(radians(angle))
sin_a = sin(radians(angle))
return [
[
cos_a * (x - center[0]) - sin_a * (y - center[1]) + center[0],
sin_a * (x - center[0]) + cos_a * (y - center[1]) + center[1],
]
for x, y in coords
]