billiard3d.py (8453B)
1 import matplotlib.pyplot as plt 2 from mpl_toolkits.mplot3d import Axes3D 3 from math import pi, sin, cos, degrees, gcd #, lcm (python3.9+ only) 4 5 # Python 3.8 or lower 6 def lcm(a, b): 7 return a*b // gcd(a,b) 8 9 ############################################################################### 10 # This part is generic for any size 11 ############################################################################### 12 def is_corner(n, sizes, point): 13 for i in range(n): 14 if point[i] % sizes[i] != 0: 15 return False 16 return True 17 18 def is_bouncing(n, sizes, point): 19 for i in range(n): 20 if point[i] % sizes[i] == 0: 21 return True 22 return False 23 24 def lcm_list(L): 25 ret = 1 26 for e in L: 27 ret = lcm(ret, e) 28 return ret 29 30 # Generic n-dimensional path. 31 def get_path(n, sizes, r): 32 if n <= 1 or len(sizes) != n or len(r) != n: 33 print("Invalid sizes or starting point:", n, sizes, r) 34 return 35 36 path = [r] 37 p = r 38 d = [1] * n 39 steps = 1 40 while steps < 2*lcm_list(sizes): 41 p = [p[i] + d[i] for i in range(n)] 42 path.append(p) 43 steps = steps+1 44 d = [d[i] if p[i] % sizes[i] != 0 else -d[i] for i in range(n)] 45 return path 46 47 def is_double(path): 48 # A path is double if and only at some point the ball bounces on a 49 # corner and bounces back in the opposite direction. This happens if 50 # and only if for some i we have path[i] = path[i+2] 51 for i in range(0, len(path)-2): 52 if path[i] == path[i+2]: 53 return True 54 55 return False 56 57 def get_multiplicities(n, sizes, r): 58 d = dict() 59 path = [tuple(x) for x in get_path(n, sizes, r)] 60 61 for point in path: 62 if point in d: 63 d[point] += 1 64 else: 65 d[point] = 1 66 67 if is_double(path): 68 for p in d: 69 d[p] //= 2 70 71 return d 72 73 def print_multiplicities(n, sizes, r): 74 d = get_multiplicities(n, sizes, r) 75 76 print("") 77 print("Points with multiplicity > 1:") 78 print("") 79 80 for i in range(2, max(d.values())+1): 81 L = [] 82 for point in d: 83 if d[point] == i: 84 L.append(point) 85 86 if len(L) != 0: 87 print(len(L), "point of multiplicity", i) 88 print(L) 89 print("") 90 91 92 ############################################################################### 93 # This part is specific for 2d drawings 94 ############################################################################### 95 class Transformation: 96 def __init__(self, angle=0.0, mirror=False, shift=(0,0)): 97 self.angle = angle 98 self.mirror = mirror 99 self.shift = shift 100 101 def rotate(self, point): 102 c, s = cos(self.angle), sin(self.angle) 103 return [c*point[0] - s*point[1], s*point[0] + c*point[1]] 104 105 def reflect(self, point): 106 return [point[0], -point[1] if self.mirror else point[1]] 107 108 def translate(self, point): 109 return [point[i] + self.shift[i] for i in range(2)] 110 111 def apply_to(self, point): 112 return self.translate(self.rotate(self.reflect(point))) 113 114 def draw_line_2d(p1, p2, c="black", w=1): 115 plt.plot([p1[0], p2[0]], [p1[1], p2[1]], color=c, linewidth=w) 116 117 def draw_grid_and_rectangle(sizes, t): 118 for i in range(0, sizes[0]+1): 119 p1 = t.apply_to([i,0]) 120 p2 = t.apply_to([i,sizes[1]]) 121 color, width = ("red", 1) if i % sizes[0] == 0 else ("grey", 0.5) 122 draw_line_2d(p1, p2, c=color, w=width) 123 for i in range(0, sizes[1]+1): 124 p1 = t.apply_to([0,i]) 125 p2 = t.apply_to([sizes[0],i]) 126 color, width = ("red", 1) if i % sizes[1] == 0 else ("grey", 0.5) 127 draw_line_2d(p1, p2, c=color, w=width) 128 129 def draw_path_2d(sizes, r, transformation): 130 if len(sizes) != 2: 131 print("Cannot draw non-2d path") 132 return 133 134 plt.axis("off") 135 plt.axis("equal") 136 draw_grid_and_rectangle(sizes, transformation) 137 138 path = [transformation.apply_to(p) for p in get_path(2, sizes, r)] 139 plt.plot([p[0] for p in path], [p[1] for p in path], color="blue") 140 141 ############################################################################### 142 # This part is specific for 3d billiards 143 ############################################################################### 144 class Face: 145 def __init__(self, fixed_coordinate, value, transformation): 146 self.f = fixed_coordinate 147 self.v = value 148 self.t = transformation 149 150 def contains(self, point): 151 return point[self.f] == self.v 152 153 def proj(self, point): 154 return [point[i] for i in range(3) if i != self.f] 155 156 def draw_bouncing_points_3d2d(sizes, r, face): 157 path_3d = get_path(3, sizes, r) 158 bp = [face.t.apply_to(face.proj(p)) 159 for p in path_3d if is_bouncing(3, sizes, p) and face.contains(p)] 160 161 plt.scatter([p[0] for p in bp], [p[1] for p in bp], color="green") 162 163 def draw_3d_projections(sizes, r): 164 a, b, c = sizes 165 166 bottom = Face(2, 0, Transformation()) 167 top = Face(2, c, Transformation(shift=(a+c, -(b+c)))) 168 front = Face(1, 0, Transformation(mirror=True)) 169 back = Face(1, b, Transformation(shift=(0,b))) 170 left = Face(0, 0, Transformation(angle=pi/2)) 171 right = Face(0, a, Transformation(angle=pi/2, mirror=True, shift=(a,0))) 172 173 for face in [bottom, top, front, back, left, right]: 174 draw_path_2d(face.proj(sizes), face.proj(r), face.t) 175 draw_bouncing_points_3d2d(sizes, r, face) 176 plt.show() 177 178 def draw_line_3d(ax, p1, p2, c="black", w=1): 179 ax.plot([p1[0], p2[0]], [p1[1], p2[1]], [p1[2], p2[2]], color=c, lw=w) 180 181 def draw_box_3d(ax, s): 182 color, width = ("red", 1) 183 184 draw_line_3d(ax, ( 0, 0, 0), (s[0], 0, 0), color, width) 185 draw_line_3d(ax, ( 0, s[1], 0), (s[0], s[1], 0), color, width) 186 draw_line_3d(ax, ( 0, 0, s[2]), (s[0], 0, s[2]), color, width) 187 draw_line_3d(ax, ( 0, s[1], s[2]), (s[0], s[1], s[2]), color, width) 188 189 draw_line_3d(ax, ( 0, 0, 0), ( 0, s[1], 0), color, width) 190 draw_line_3d(ax, (s[0], 0, 0), (s[0], s[1], 0), color, width) 191 draw_line_3d(ax, ( 0, 0, s[2]), ( 0, s[1], s[2]), color, width) 192 draw_line_3d(ax, (s[0], 0, s[2]), (s[0], s[1], s[2]), color, width) 193 194 draw_line_3d(ax, ( 0, 0, 0), ( 0, 0, s[2]), color, width) 195 draw_line_3d(ax, (s[0], 0, 0), (s[0], 0, s[2]), color, width) 196 draw_line_3d(ax, ( 0, s[1], 0), ( 0, s[1], s[2]), color, width) 197 draw_line_3d(ax, (s[0], s[1], 0), (s[0], s[1], s[2]), color, width) 198 199 def draw_3d_picture(sizes, r): 200 ax = plt.figure().add_subplot(111, projection="3d") 201 202 plt.axis("off") 203 ax.set_xlim(0, max(sizes)) 204 ax.set_ylim(0, max(sizes)) 205 ax.set_zlim(0, max(sizes)) 206 207 draw_box_3d(ax, sizes) 208 209 path = get_path(3, sizes, r) 210 plt.plot([p[0] for p in path], [p[1] for p in path], [p[2] for p in path], 211 color="blue") 212 213 plt.show() 214 215 def edge_number(c1, c2): 216 a = 0 if c1 == 0 else 1 217 b = 0 if c2 == 0 else 1 218 return a + 2*b 219 220 def print_points_on_edges_coord(s, path, i): 221 L = [[], [], [], []] 222 Lname = [['*','*','*'],['*','*','*'],['*','*','*'],['*','*','*']] 223 # Kinda ugly way of saying "j and k are the other 2 coordinates" 224 j = 0 if i != 0 else 1 225 k = 2 if i != 2 else 1 226 for p in path: 227 if (p[j] == 0 or p[j] == s[j]) and (p[k] == 0 or p[k] == s[k]): 228 en = edge_number(p[j], p[k]) 229 L[en].append(p[i]) 230 Lname[en][j] = p[j] 231 Lname[en][k] = p[k] 232 233 for l in range(0,4): 234 if len(L[l]) != 0: 235 so = list(set(L[l])) 236 so.sort() 237 print(len(so), "points on edge", Lname[l], ":", so) 238 239 240 def print_points_on_edges(size, r): 241 # TODO: show a picture of this 242 243 print("") 244 245 path = get_path(3, sizes, r) 246 for i in range(0,3): 247 print_points_on_edges_coord(sizes, path, i) 248 print("") 249 250 ############################################################################### 251 # Billiard data / user input 252 ############################################################################### 253 def user_input(): 254 a = int(input("Value for a: ")) 255 b = int(input("Value for b: ")) 256 c = int(input("Value for c: ")) 257 ra = int(input("a-coordinate of r: ")) 258 rb = int(input("b-coordinate of r: ")) 259 rc = int(input("c-coordinate of r: ")) 260 pic = input("Choose p for projections or 3 for 3d (empty = all, one after the other): ") 261 return [a,b,c], [ra,rb,rc], pic 262 263 ############################################################################### 264 # Main routine below 265 ############################################################################### 266 267 if __name__ == '__main__': 268 # You can choose to write your input here or to get it interactively 269 #sizes, r, pic = [15, 9, 7], [2, 0, 0], "p" 270 #sizes, r, pic = [2, 3, 4], [0, 0, 0], "3" 271 sizes, r, pic = user_input() 272 273 print("---------------------------------") 274 print_multiplicities(len(sizes), sizes, r) 275 print("---------------------------------") 276 print_points_on_edges(sizes, r) 277 print("---------------------------------") 278 279 if pic == "p": 280 draw_3d_projections(sizes, r) 281 elif pic == "3": 282 draw_3d_picture(sizes, r) 283 else: 284 draw_3d_projections(sizes, r) 285 draw_3d_picture(sizes, r)