Reflections in Vertical Mirrors at an Angle
In recently re-publishing some older Sunday Times teasers, I came across one from 2010 that involved working out how many images could be seen in two vertical mirrors placed at an angle to each other (see it here). This teaser generated an enormous amount of discussion when it was first published, with no agreement among puzzlers on what the answer should be (this discussion can be seen here). And when I recently mentioned it on the Sunday Times teaser discussion site (see the comment section on this page), there was still a lack of agreement on what the answer should be. In practice the teaser is not well specified so there is really no correct answer and it seems likely that it is more subtle than its author realised when he set it.
As a physicist, the problem appealed to me and I did a lot of analysis when it was first published in 2010. But when I recently started to re-publish these older teasers here, I found that I had mislaid my original material and the on-line copies on the Google discussion group had also been deleted. In consequence I have had to re-create my analysis which derives the paths of rays when an object and an observer are placed between two vertical mirrors arranged at an angle to each other. I was not ‘into Python’ in 2010 but I am now enthusiastic about the ease with which I can do things with it. So here is my code for plotting the ray paths and images formed by angled mirrors. It is not fully polished and may require some manual code changes to deal with some cases but it copes well with those cases that matter (the program is available here).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
from math import pi, degrees, radians, sin, cos, atan2 from itertools import count from sys import argv import matplotlib.path as mpath import matplotlib.patches as mpatches import matplotlib.pyplot as plt # set to true to display image positions plot_image_points = True # output bounding box use_bounding_box = True x_min, x_max, y_min, y_max = -3.0, 3.0, -1, 2 if len(argv) > 1: if len(argv) < 6: print('usage: mirrors.py r0, eta0, rf, etaf beta') exit() else: r0, eta0, rf, etaf, beta = (float(x) for x in argv[1:]) plot_image_points = (len(argv) > 6) else: # Input Data (all angles are in degrees) # the object position in polar coordinates r0, eta0 = 1.0, 9.0 # the observer position in polar coordinates rf, etaf = 10.0, -7.0 # half the angle between the two mirrors beta = 24.5 # for setting mirror length mlen = rf / cos(radians(beta)) # find a circle passing through three points def three_point_circle(p0, p1, p2): x1, y1 = p1[0] - p0[0], p1[1] - p0[1] x2, y2 = p2[0] - p0[0], p2[1] - p0[1] r1 = x1 ** 2 + y1 ** 2 r2 = x2 ** 2 + y2 ** 2 d = 2 * (x1 * y2 - x2 * y1) x = (r1 * y2 - r2 * y1) / d y = (r2 * x1 - r1 * x2) / d r = (x * x + y * y) ** 0.5 return (x + p0[0], y + p0[1]), r # return a list of ray paths from an object and an observer # when placed between two vertical mirrors at an angle # # r0, eta0 - polar coordinates of the objecct # rf, etaf - polar coordinates of the observer # beta - half the angle between the mirrors # k_max - the maximum number of times the ray hits a mirror # first_left - set for rays that hit the left mirror first (rays # start at the object and end at the onserver) def ray_points(r0, eta0, rf, etaf, beta, k_max, left_first=False): beta = radians(beta) sin_beta, cos_beta = sin(beta), cos(beta) eta0, etaf = radians(eta0), radians(etaf) # the (x, y) coordinates of the initial and final points on the ray path p0, pf = (r0 * sin(eta0), r0 * cos(eta0)), (rf * sin(etaf), rf * cos(etaf)) # reverse the x axis if the ray hits the left mirror first if left_first: eta0, etaf = -eta0, -etaf # kmax is the number of times a ray from the object to # the observer hits the mirrors - calculate the initial # ray angle (a0) t = 2 * k_max * beta - (2 * (k_max & 1) - 1) * etaf tt, bt = (rf * sin(t) - r0 * sin(eta0)), (rf * cos(t) - r0 * cos(eta0)) ak = a0 = atan2(tt, bt) # normalise to hit the right mirror if a0 < 0: a0 += pi s = r0 * sin(a0 - eta0) # now calculate the (x, y) points at which the ray hits a mirror lks = [] for k in range(1, k_max + 1): # find the next point at which the the ray hits a mirror ak = a0 - (2 * k - 1) * beta lk = s / sin(ak) if lk < 0: break x, y = lk * sin_beta, lk * cos_beta if (k & 1) == left_first: x = -x lks.append((x, y)) # return the full path with the two endpoints yield ((p0,) + tuple(lks) + (pf,)) # list rays that pass from the object to the observer after hitting # the mirrors a number of times - obtain the (x, y) vertex points on # each ray with rays that hit the right hand mirror listed first and # then those that first hit the left hand mirror. k, ofs, left, end, rays = -1, 0, False, False, [] while True: k += 1 # find a ray that passes from the object to the observer after hitting # the mirrors a total of k times for ll in ray_points(r0, eta0, rf, etaf, beta, k, left_first=left): # valid solutions have k + 2 points if len(ll) != k + 2: # repeat the process for rays that hit the left hand mirror first if left == False: k, ofs, left = 0, k - 1, True else: end = True break # print and save the vertex points on the ray s = ', '.join('(' + ', '.join(f'{i:.4f}' for i in t) + ')' for t in ll) print(f'{k+ofs}: {s}') rays.append(ll) if end: break # list the positions of the images imgs, angs = [], [] for ray in rays: if len(ray) > 2: # find the total ray path length from the object # to the point where the ray last hits a mirror ln = 0 x1, y1 = ray[0] for i in range(1, len(ray) - 1): x2, y2 = ray[i] ln += ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 x1, y1 = x2, y2 # extend the last segment on the ray path backwards # to the image point xf, yf = ray[-1] xm, ym = ray[-2] tn = atan2(yf - ym, xf - xm) ix, iy = xm - ln * cos(tn), ym - ln * sin(tn) angs.append(degrees(atan2(ix, iy))) imgs.append(((xm, ym), (xm - ln * cos(tn), ym - ln * sin(tn)))) print('image angles:', ', '.join(f'{x:.4f}' for x in sorted(angs))) # plot the rays using matplotlib fig, ax = plt.subplots() Path = mpath.Path # plot the object and observer positions ax.plot([r0 * sin(radians(eta0))], [r0 * cos(radians(eta0))], "ko") ax.plot([rf * sin(radians(etaf))], [rf * cos(radians(etaf))], "ko") # plot the mirrors mirror = [ (Path.MOVETO, (-mlen * sin(radians(beta)), mlen * cos(radians(beta)))), (Path.LINETO, (0.0, 0.0)), (Path.LINETO, (mlen * sin(radians(beta)), mlen * cos(radians(beta)))), ] codes, verts = zip(*mirror) path = mpath.Path(verts, codes) patch = mpatches.PathPatch(path, edgecolor='k', fill=False) ax.add_patch(patch) # plot the ray paths sty= ['-'] * 3 + ['--'] * 4 + ['-.'] * 60 clr= 'rbgcmy' * 10 for i, ray in enumerate(rays[1:]): cmd = Path.MOVETO rd = [] for p in ray: rd.append((cmd, p)) cmd = Path.LINETO codes, verts = zip(*rd) path = mpath.Path(verts, codes) patch = mpatches.PathPatch(path, edgecolor=clr[i], linestyle=sty[i], fill=False) ax.add_patch(patch) if plot_image_points and len(imgs) > 1: p, r = three_point_circle(rays[1][0], imgs[0][1], imgs[1][1]) codes = (Path.MOVETO, Path.LINETO) ima = [] for i, cp in enumerate(imgs): path = mpath.Path(cp, codes) patch = mpatches.PathPatch(path, edgecolor=clr[i], linestyle='dotted', fill=False) ax.add_patch(patch) ax.plot(cp[1][0], cp[1][1], 'b.') patch = mpatches.Circle(p, r, edgecolor='k', linestyle='dotted', fill=False) ax.add_patch(patch) # show the plot plt.axis('scaled') if use_bounding_box: ax.axis([x_min, x_max, y_min, y_max]) plt.axis('off') plt.show() |
To run it you will require Python 3 with the matplotlib package installed. There are five input parameters as follows: \(r_0\), the distance of the object from the mirror vertex, \(eta_0\), the angle of the object from north in degrees, \(r_f\) and \(eta_f\), the same for the observer, and \(beta\), the half angle between the mirrors in degrees.
Here is an example of the output for the official answer to the original teaser: