diff options
| author | Sebastiano Tronto <sebastiano.tronto@gmail.com> | 2021-05-26 11:26:53 +0200 |
|---|---|---|
| committer | Sebastiano Tronto <sebastiano.tronto@gmail.com> | 2021-05-26 11:26:53 +0200 |
| commit | 843d9afdb48f1344bd1ab1473ee39be0cccb23ca (patch) | |
| tree | 5d5712f7a9f12a3750f23fb8c0e4382e1d228b0d | |
| parent | b776f967e46827d2936be82466ce7cdfd8cfc0d1 (diff) | |
| download | arithmeticbilliard-843d9afdb48f1344bd1ab1473ee39be0cccb23ca.tar.gz arithmeticbilliard-843d9afdb48f1344bd1ab1473ee39be0cccb23ca.zip | |
First commit
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | billiard3d.py | 197 |
2 files changed, 201 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..11ad7d8 --- /dev/null +++ b/README.md | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | See [Wikipedia: Arithmetic Billiards](https://en.wikipedia.org/wiki/Arithmetic_billiards). | ||
| 2 | |||
| 3 | This Python script uses matplotlib to draw pictures for a 3d arithmetic | ||
| 4 | billiard, either as a 2d projection or as an axonometry. | ||
diff --git a/billiard3d.py b/billiard3d.py new file mode 100644 index 0000000..d77fc1a --- /dev/null +++ b/billiard3d.py | |||
| @@ -0,0 +1,197 @@ | |||
| 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 = 0 | ||
| 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 | ############################################################################### | ||
| 48 | # This part is specific for 2d drawings | ||
| 49 | ############################################################################### | ||
| 50 | class Transformation: | ||
| 51 | def __init__(self, angle=0.0, mirror=False, shift=(0,0)): | ||
| 52 | self.angle = angle | ||
| 53 | self.mirror = mirror | ||
| 54 | self.shift = shift | ||
| 55 | |||
| 56 | def rotate(self, point): | ||
| 57 | c, s = cos(self.angle), sin(self.angle) | ||
| 58 | return [c*point[0] - s*point[1], s*point[0] + c*point[1]] | ||
| 59 | |||
| 60 | def reflect(self, point): | ||
| 61 | return [point[0], -point[1] if self.mirror else point[1]] | ||
| 62 | |||
| 63 | def translate(self, point): | ||
| 64 | return [point[i] + self.shift[i] for i in range(2)] | ||
| 65 | |||
| 66 | def apply_to(self, point): | ||
| 67 | return self.translate(self.rotate(self.reflect(point))) | ||
| 68 | |||
| 69 | def draw_line_2d(p1, p2, c="black", w=1): | ||
| 70 | plt.plot([p1[0], p2[0]], [p1[1], p2[1]], color=c, linewidth=w) | ||
| 71 | |||
| 72 | def draw_grid_and_rectangle(sizes, t): | ||
| 73 | for i in range(0, sizes[0]+1): | ||
| 74 | p1 = t.apply_to([i,0]) | ||
| 75 | p2 = t.apply_to([i,sizes[1]]) | ||
| 76 | color, width = ("red", 1) if i % sizes[0] == 0 else ("grey", 0.5) | ||
| 77 | draw_line_2d(p1, p2, c=color, w=width) | ||
| 78 | for i in range(0, sizes[1]+1): | ||
| 79 | p1 = t.apply_to([0,i]) | ||
| 80 | p2 = t.apply_to([sizes[0],i]) | ||
| 81 | color, width = ("red", 1) if i % sizes[1] == 0 else ("grey", 0.5) | ||
| 82 | draw_line_2d(p1, p2, c=color, w=width) | ||
| 83 | |||
| 84 | def draw_path_2d(sizes, r, transformation): | ||
| 85 | if len(sizes) != 2: | ||
| 86 | print("Cannot draw non-2d path") | ||
| 87 | return | ||
| 88 | |||
| 89 | plt.axis("off") | ||
| 90 | plt.axis("equal") | ||
| 91 | draw_grid_and_rectangle(sizes, transformation) | ||
| 92 | |||
| 93 | path = [transformation.apply_to(p) for p in get_path(2, sizes, r)] | ||
| 94 | plt.plot([p[0] for p in path], [p[1] for p in path], color="blue") | ||
| 95 | |||
| 96 | ############################################################################### | ||
| 97 | # This part is specific for drawing the 2d projections of 3d billiards | ||
| 98 | ############################################################################### | ||
| 99 | class Face: | ||
| 100 | def __init__(self, fixed_coordinate, value, transformation): | ||
| 101 | self.f = fixed_coordinate | ||
| 102 | self.v = value | ||
| 103 | self.t = transformation | ||
| 104 | |||
| 105 | def contains(self, point): | ||
| 106 | return point[self.f] == self.v | ||
| 107 | |||
| 108 | def proj(self, point): | ||
| 109 | return [point[i] for i in range(3) if i != self.f] | ||
| 110 | |||
| 111 | def draw_bouncing_points_3d2d(sizes, r, face): | ||
| 112 | path_3d = get_path(3, sizes, r) | ||
| 113 | bp = [face.t.apply_to(face.proj(p)) | ||
| 114 | for p in path_3d if is_bouncing(3, sizes, p) and face.contains(p)] | ||
| 115 | |||
| 116 | plt.scatter([p[0] for p in bp], [p[1] for p in bp], color="green") | ||
| 117 | |||
| 118 | def draw_3d_projections(sizes, r): | ||
| 119 | a, b, c = sizes | ||
| 120 | |||
| 121 | bottom = Face(2, 0, Transformation()) | ||
| 122 | top = Face(2, c, Transformation(shift=(a+c, -(b+c)))) | ||
| 123 | front = Face(1, 0, Transformation(mirror=True)) | ||
| 124 | back = Face(1, b, Transformation(shift=(0,b))) | ||
| 125 | left = Face(0, 0, Transformation(angle=pi/2)) | ||
| 126 | right = Face(0, a, Transformation(angle=pi/2, mirror=True, shift=(a,0))) | ||
| 127 | |||
| 128 | for face in [bottom, top, front, back, left, right]: | ||
| 129 | draw_path_2d(face.proj(sizes), face.proj(r), face.t) | ||
| 130 | draw_bouncing_points_3d2d(sizes, r, face) | ||
| 131 | plt.show() | ||
| 132 | |||
| 133 | ############################################################################### | ||
| 134 | # This part is for drawing 3d pictures | ||
| 135 | ############################################################################### | ||
| 136 | def draw_line_3d(ax, p1, p2, c="black", w=1): | ||
| 137 | ax.plot([p1[0], p2[0]], [p1[1], p2[1]], [p1[2], p2[2]], color=c, lw=w) | ||
| 138 | |||
| 139 | def draw_box_3d(ax, s): | ||
| 140 | color, width = ("red", 1) | ||
| 141 | |||
| 142 | draw_line_3d(ax, ( 0, 0, 0), (s[0], 0, 0), color, width) | ||
| 143 | draw_line_3d(ax, ( 0, s[1], 0), (s[0], s[1], 0), color, width) | ||
| 144 | draw_line_3d(ax, ( 0, 0, s[2]), (s[0], 0, s[2]), color, width) | ||
| 145 | draw_line_3d(ax, ( 0, s[1], s[2]), (s[0], s[1], s[2]), color, width) | ||
| 146 | |||
| 147 | draw_line_3d(ax, ( 0, 0, 0), ( 0, s[1], 0), color, width) | ||
| 148 | draw_line_3d(ax, (s[0], 0, 0), (s[0], s[1], 0), color, width) | ||
| 149 | draw_line_3d(ax, ( 0, 0, s[2]), ( 0, s[1], s[2]), color, width) | ||
| 150 | draw_line_3d(ax, (s[0], 0, s[2]), (s[0], s[1], s[2]), color, width) | ||
| 151 | |||
| 152 | draw_line_3d(ax, ( 0, 0, 0), ( 0, 0, s[2]), color, width) | ||
| 153 | draw_line_3d(ax, (s[0], 0, 0), (s[0], 0, s[2]), color, width) | ||
| 154 | draw_line_3d(ax, ( 0, s[1], 0), ( 0, s[1], s[2]), color, width) | ||
| 155 | draw_line_3d(ax, (s[0], s[1], 0), (s[0], s[1], s[2]), color, width) | ||
| 156 | |||
| 157 | def draw_3d_picture(sizes, r): | ||
| 158 | ax = plt.figure().add_subplot(111, projection="3d") | ||
| 159 | |||
| 160 | plt.axis("off") | ||
| 161 | ax.set_xlim(0, max(sizes)) | ||
| 162 | ax.set_ylim(0, max(sizes)) | ||
| 163 | ax.set_zlim(0, max(sizes)) | ||
| 164 | |||
| 165 | draw_box_3d(ax, sizes) | ||
| 166 | |||
| 167 | path = get_path(3, sizes, r) | ||
| 168 | plt.plot([p[0] for p in path], [p[1] for p in path], [p[2] for p in path], | ||
| 169 | color="blue") | ||
| 170 | |||
| 171 | plt.show() | ||
| 172 | |||
| 173 | ############################################################################### | ||
| 174 | # Billiard data / user input | ||
| 175 | ############################################################################### | ||
| 176 | def user_input(): | ||
| 177 | a = int(input("Value for a: ")) | ||
| 178 | b = int(input("Value for b: ")) | ||
| 179 | c = int(input("Value for c: ")) | ||
| 180 | ra = int(input("a-coordinate of r: ")) | ||
| 181 | rb = int(input("b-coordinate of r: ")) | ||
| 182 | rc = int(input("c-coordinate of r: ")) | ||
| 183 | pic = input("Choose p for projections or 3 for 3d (empty = both): ") | ||
| 184 | return [a,b,c], [ra,rb,rc], pic | ||
| 185 | |||
| 186 | # You can choose to write your input here or to get it interactively | ||
| 187 | #sizes, r, pic = [15, 9, 7], [2, 0, 0], "p" | ||
| 188 | #sizes, r, pic = [2, 3, 4], [0, 0, 0], "3" | ||
| 189 | sizes, r, pic = user_input() | ||
| 190 | |||
| 191 | if pic == "p": | ||
| 192 | draw_3d_projections(sizes, r) | ||
| 193 | elif pic == "3": | ||
| 194 | draw_3d_picture(sizes, r) | ||
| 195 | else: | ||
| 196 | draw_3d_projections(sizes, r) | ||
| 197 | draw_3d_picture(sizes, r) | ||
