--- jupyter: description: 'Mirrors from conic sections with planex2d: an ellipse, a parabola and a hyperbola as exact rational B-splines, the whispering gallery, where every ray from one focus reaches the other, and a Cassegrain telescope, where every ray parallel to the axis meets the focus behind the primary over the same path.' --- # Mirrors from conic sections The conic sections focus. A ray from one focus of an ellipse, reflected by the ellipse, passes through the other focus, and every such path has the same length $2a$. In an elliptical room, a whisper at one focus is heard at the other. A parabola reflects every ray parallel to its axis into its focus, and a hyperbola reflects every ray aimed at one of its foci toward the other. The Cassegrain telescope combines the two, a parabolic primary mirror and a small convex hyperbolic secondary in front of its focus, which sends the light back through a hole in the primary to a focus behind it. All three curves are exact rational B-splines in planex2d. The example traces rays through a whispering gallery and a Cassegrain telescope. Every reflected ray must reach its focus, every path must be equally long, and since planex2d intersects a spline on its approximation, the misses must shrink with the spline tolerance. ![Rays in a whispering gallery and in a Cassegrain telescope](../_static/gallery/conic-mirrors.png){.result} ```python import math import numpy as np import planex as px ``` ## The conics as B-splines A rational quadratic B-spline traces a conic exactly. Between two points, with its middle pole where the tangents there meet, the middle weight sets the kind. Below 1 it is an ellipse, at 1 a parabola, and above 1 a hyperbola. A quarter of the ellipse with the semi-axes $a$ and $b$ has the poles $(a, 0)$, $(a, b)$, $(0, b)$ and the middle weight $\cos 45^\circ$. Four quarters make the whole ellipse. A parabola needs no weights. The branch of the hyperbola $(y - y_0)^2/a^2 - x^2/b^2 = 1$ between $x = \pm b \sinh u$ has its middle pole at $y_0 + a/\cosh u$ and the middle weight $\cosh u$: ```python def ellipse(a, b): """the whole ellipse around the origin, four quarters with the middle weights cos 45 degrees""" poles = [(a, 0), (a, b), (0, b), (-a, b), (-a, 0), (-a, -b), (0, -b), (a, -b), (a, 0)] w = math.cos(math.radians(45)) return px.BSpline(poles, [0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 4], 2, weights=[1, w, 1, w, 1, w, 1, w, 1]) def parabola(f, x): """y = h^2 / (4 f) for |h| <= x, its focus at (0, f)""" return px.BSpline([(-x, x * x / (4 * f)), (0, -x * x / (4 * f)), (x, x * x / (4 * f))], [0, 0, 0, 1, 1, 1], 2) def hyperbola(y0, a, b, x): """the upper branch of (v - y0)^2 / a^2 - h^2 / b^2 = 1 for |h| <= x""" u = math.asinh(x / b) ends = [(-x, y0 + a * math.cosh(u)), (x, y0 + a * math.cosh(u))] return px.BSpline([ends[0], (0, y0 + a / math.cosh(u)), ends[1]], [0, 0, 0, 1, 1, 1], 2, weights=[1, math.cosh(u), 1]) room = ellipse(5, 3) print("ellipse: one edge, area", room.Face().mass, "exact pi a b", math.pi * 5 * 3) ``` ## Rays `trace` follows a ray from a point along a direction to the nearest point where it meets a mirror, and reflects its direction about the mirror's tangent there: ```python def trace(mirror, p, d): """the point where the ray from p along d first meets the mirror, and the reflected direction""" ray = px.Line(p, (p[0] + 100 * d[0], p[1] + 100 * d[1])) q = min(((h.x, h.y) for h in mirror.Intersections(ray) if math.dist((h.x, h.y), p) > 1e-9), key=lambda h: math.dist(h, p)) t = mirror.edges[0].edge.Tangent(q) t = (t.x / math.hypot(t.x, t.y), t.y / math.hypot(t.x, t.y)) dot = d[0] * t[0] + d[1] * t[1] return q, (2 * dot * t[0] - d[0], 2 * dot * t[1] - d[1]) def miss(point, p, d): """how far point lies from the line through p along d""" return abs((point[0] - p[0]) * d[1] - (point[1] - p[1]) * d[0]) / math.hypot(*d) ``` ## The whispering gallery The room is an ellipse with the semi-axes 5 and 3, its foci at $(\pm 4, 0)$. Thirty-six rays leave the left focus every 10°. Every reflected ray has to pass through the right focus, and every path from focus to focus has to be $2a = 10$ long: ```python F1, F2 = (-4.0, 0.0), (4.0, 0.0) gallery = [] for k in range(36): angle = math.radians(5 + 10 * k) q, out = trace(room, F1, (math.cos(angle), math.sin(angle))) gallery.append((q, out)) print("largest miss of the right focus", max(miss(F2, q, out) for q, out in gallery)) print("largest |F1 P + P F2 - 2a|", max(abs(math.dist(F1, q) + math.dist(q, F2) - 10) for q, out in gallery)) ``` ## The Cassegrain telescope The primary is the parabola $y = x^2/40$, 12 wide, its focus at (0, 10). The secondary is a branch of the hyperbola with the foci (0, 10) and (0, −2), its center at (0, 4), the semi-axis $a = 4$ and $b = \sqrt{6^2 - 4^2}$. It is 3 wide, with its vertex at (0, 8), in front of the primary's focus. Light comes down parallel to the axis, from the height 20, and the secondary shades the middle, so the rays start between 2 and 5.8 from the axis. Every ray has to reach the focus (0, −2) behind the primary, and every path from the height 20 to that focus has to be equally long: ```python primary = parabola(10, 6) secondary = hyperbola(4, 4, math.sqrt(6**2 - 4**2), 1.5) focus = (0.0, -2.0) telescope = [] for x in np.linspace(2, 5.8, 12): p0 = (float(x), 20.0) q1, d1 = trace(primary, p0, (0.0, -1.0)) q2, d2 = trace(secondary, q1, d1) telescope.append((p0, q1, q2, d2)) print("largest miss of the focus", max(miss(focus, q2, d2) for p0, q1, q2, d2 in telescope)) paths = [math.dist(p0, q1) + math.dist(q1, q2) + math.dist(q2, focus) for p0, q1, q2, d2 in telescope] print("paths from", min(paths), "to", max(paths)) ``` The next figure draws the rays of both mirrors, the telescope's on both sides of its axis: ```python nbsphinx="hide-input" import sys import matplotlib.pyplot as plt sys.path.insert(0, "..") from gallery import save_figure fig, (left, right) = plt.subplots(1, 2, figsize=(12, 5.4), gridspec_kw={"width_ratios": [1.6, 1]}) for q, out in gallery: left.plot([F1[0], q[0], F2[0]], [F1[1], q[1], F2[1]], color="#d98c2b", lw=0.8) room.Plot(ax=left, face_col=None, line_col="#22303c", legend=False) for p0, q1, q2, d2 in telescope: for side in (1, -1): right.plot([side * p0[0], side * q1[0], side * q2[0], focus[0]], [p0[1], q1[1], q2[1], focus[1]], color="#d98c2b", lw=0.8) for mirror in (primary, secondary): mirror.Plot(ax=right, face_col=None, line_col="#22303c", legend=False) for ax in (left, right): ax.set_aspect("equal") ax.axis("off") save_figure(fig, "conic-mirrors", dpi=150) ``` ## The spline tolerance The misses are small but not zero. The tangent is that of the exact curve, but planex2d finds the point where a ray meets a spline on its arc approximation, which stays within `px.config.spline_tol` of the exact curve (see [Tolerances](../core/basics/tolerances.md)). As the tolerance tightens, the reflected rays come closer to the focus and the paths closer to $2a$: ```python default = px.config.spline_tol for tol in (1e-4, 1e-6, 1e-8): px.config.spline_tol = tol room = ellipse(5, 3) traced = [trace(room, F1, (math.cos(math.radians(5 + 10 * k)), math.sin(math.radians(5 + 10 * k)))) for k in range(36)] print("spline_tol", tol, "| largest miss", max(miss(F2, q, out) for q, out in traced), "| largest path error", max(abs(math.dist(F1, q) + math.dist(q, F2) - 10) for q, out in traced)) px.config.spline_tol = default ```