arithmeticbilliard

Draw arithmetic billiards
git clone https://git.tronto.net/arithmeticbilliard
Download | Log | Files | Refs | README | LICENSE

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)