'Tangent lines to a rotated ellipse from a point
I need to calculate the equations of tangent lines to a rotated ellipse from a specific point outside of the ellipse. I found Ellipse.rotate() method in Sympy which is almost exactly what I need, but only rotations with increments of pi/2 are supported and I want to be able to do this with other rotation angles. Any suggestion on how to do this in Python?
Solution 1:[1]
You can define your ellipse as a sequence of transformations that are applied to the unit circle:
- Stretch by
aandb. - Rotate by
angle. - Translate by
center.
Then this sequence of transformations can be applied in reverse order and using the inverse of each transformation to the reference point, in order to solve the problem w.r.t. the unit circle. Here the angles of the two tangent points on the unit circle are given by the constraint of perpendicularity: 1 - p0*cos(t) - q0*sin(t) = 0 where p0, q0 is the reference point.
See this answer for mathematical details.
from functools import reduce
from typing import Tuple
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
import numpy as np
class Transformation:
def transform(self, point: Tuple[float, float]) -> Tuple[float, float]:
raise NotImplementedError
def inverse_transform(self, point: Tuple[float, float]) -> Tuple[float, float]:
raise NotImplementedError
class Translation(Transformation):
def __init__(self, offset: Tuple[float, float]):
self.dx, self.dy = offset
def transform(self, point):
return point[0] + self.dx, point[1] + self.dy
def inverse_transform(self, point):
return point[0] - self.dx, point[1] - self.dy
class Stretch(Transformation):
def __init__(self, scale: Tuple[float, float]):
self.scale = scale
def transform(self, point):
return point[0]*self.scale[0], point[1]*self.scale[1]
def inverse_transform(self, point):
return point[0]/self.scale[0], point[1]/self.scale[1]
class Rotation(Transformation):
def __init__(self, angle: float):
self.c, self.s = np.cos(angle), np.sin(angle)
def transform(self, point):
return self.c*point[0] - self.s*point[1], self.s*point[0] + self.c*point[1]
def inverse_transform(self, point):
return self.c*point[0] + self.s*point[1], -self.s*point[0] + self.c*point[1]
class Transformations(Transformation):
def __init__(self, *transformations: Transformation):
self.transformations = transformations
def transform(self, point):
return reduce(lambda p,t: t.transform(p), self.transformations, point)
def inverse_transform(self, point):
return reduce(lambda p,t: t.inverse_transform(p), self.transformations[::-1], point)
def find_tangent_lines(
center: Tuple[float, float],
semi_axes: Tuple[float, float],
rotation: float,
reference_point: Tuple[float, float],
):
"""Find the Ellipse's two tangents that go through a reference point.
Args:
center: The center of the ellipse.
semi_axes: The semi-major and semi-minor axes of the ellipse.
rotation: The counter-clockwise rotation of the ellipse in radians.
reference_point: The coordinates of the reference point.
Returns:
(m1, h1): Slope and intercept of the first tangent.
(m2, h2): Slope and intercept of the second tangent.
"""
transformations = Transformations(
Stretch(semi_axes),
Rotation(rotation),
Translation(center),
)
p = transformations.inverse_transform(reference_point)
p_norm = p[0]**2 + p[1]**2
if p_norm < 1:
raise ValueError('Reference point lies inside the ellipse')
theta = (
2*np.arctan((p[1] + np.sqrt(p_norm - 1))/(p[0]+1)),
2*np.arctan((p[1] - np.sqrt(p_norm - 1))/(p[0]+1)),
)
p_ellipse = [
transformations.transform((np.cos(t), np.sin(t)))
for t in theta
]
slope = [
(e[1] - reference_point[1]) / (e[0] - reference_point[0])
for e in p_ellipse
]
intercept = [
e[1] - s*e[0]
for e, s in zip(p_ellipse, slope)
]
return (
(slope[0], intercept[0]),
(slope[1], intercept[1]),
)
# Example:
CENTER = 1, 2
SEMI_AXES = 3, 1
ROTATION = np.pi/3
REFERENCE_POINT = -2, 3
(m1, h1), (m2, h2) = find_tangent_lines(
center=CENTER,
semi_axes=SEMI_AXES,
rotation=ROTATION,
reference_point=REFERENCE_POINT,
)
fig, ax = plt.subplots()
ax.add_patch(Ellipse(CENTER, 2*SEMI_AXES[0], 2*SEMI_AXES[1], ROTATION/np.pi*180, color='tab:blue', alpha=0.4))
ax.scatter([REFERENCE_POINT[0]], [REFERENCE_POINT[1]], color='tab:blue', s=50)
ax.axline((0, h1), slope=m1, color='tab:orange', lw=1)
ax.axline((0, h2), slope=m2, color='tab:orange', lw=1)
plt.show()
Solution 2:[2]
Another option is to consider all lines that go through the reference point and any point of the ellipse. The function that maps ellipse angles to the slopes of these lines will have two extrema for the two tangent lines of the ellipse. Hence, solving d/dt slopes(t) = 0 will yield the two values for t (the ellipse angles).
See this answer for mathematical details.
from typing import Tuple
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
import numpy as np
def find_tangent_lines(
center: Tuple[float, float],
semi_axes: Tuple[float, float],
rotation: float,
reference_point: Tuple[float, float],
):
"""Find the Ellipse's two tangents that go through a reference point.
Args:
center: The center of the ellipse.
semi_axes: The semi-major and semi-minor axes of the ellipse.
rotation: The counter-clockwise rotation of the ellipse in radians.
reference_point: The coordinates of the reference point.
Returns:
(m1, h1): Slope and intercept of the first tangent.
(m2, h2): Slope and intercept of the second tangent.
"""
x0, y0 = center
a, b = semi_axes
s, c = np.sin(rotation), np.cos(rotation)
p0, q0 = reference_point
A = y0 - q0
D = x0 - p0
B = a*s
E = a*c
C = b*c
F = -b*s
denominator = np.sqrt((C*D - A*F)**2 + (A*E - B*D)**2)
if not (-1 <= (B*F - C*E) / denominator <= 1):
raise ValueError('Reference point lies inside the ellipse')
beta = np.arctan2(
(C*D - A*F) / denominator,
(A*E - B*D) / denominator,
)
theta = [
-beta + np.arcsin((B*F - C*E) / denominator),
-beta - np.arcsin((B*F - C*E) / denominator) + np.pi,
]
p_ellipse = [
(
x0 + E*np.cos(t) + F*np.sin(t),
y0 + B*np.cos(t) + C*np.sin(t),
)
for t in theta
]
slope = [
(e[1] - reference_point[1]) / (e[0] - reference_point[0])
for e in p_ellipse
]
intercept = [
e[1] - s*e[0]
for e, s in zip(p_ellipse, slope)
]
return (
(slope[0], intercept[0]),
(slope[1], intercept[1]),
)
# Example:
CENTER = 1, 2
SEMI_AXES = 3, 1
ROTATION = np.pi/3
REFERENCE_POINT = -2, 3
(m1, h1), (m2, h2) = find_tangent_lines(
center=CENTER,
semi_axes=SEMI_AXES,
rotation=ROTATION,
reference_point=REFERENCE_POINT,
)
fig, ax = plt.subplots()
ax.add_patch(Ellipse(CENTER, 2*SEMI_AXES[0], 2*SEMI_AXES[1], ROTATION/np.pi*180, color='tab:blue', alpha=0.4))
ax.scatter([REFERENCE_POINT[0]], [REFERENCE_POINT[1]], color='tab:blue', s=50)
ax.axline((0, h1), slope=m1, color='tab:orange', lw=1)
ax.axline((0, h2), slope=m2, color='tab:orange', lw=1)
plt.show()
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | a_guest |
| Solution 2 | a_guest |


